计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 L190200902
班 级 1903004
学 生 崔薰化
指 导 教 师
计算机科学与技术学院
2021年5月
摘 要
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
<第1章>
本次实验,介绍了hello程序P2P,020的过程列出了实验中生成的中间文件,列出了实验使用的硬件条件,系统版本。还有基于Hello展开从Program到Process的过程,通过对其预处理、编译、汇编、链接的过程,领会程序由.c源文件到可执行文件的细节及工作方式。进一步体会程序从存储管理中的源文件,通过IO加载到主存,再通过CPU执行,将Hello的内容显示在屏幕上的具体过程原理。本次实验将在Win10系统的VMWAare虚拟机上完成,具体工作过程见后面章节。
<第2章>
本章主要介绍了预处理的概念与作用,执行预处理的命令,并结合hello.i文件,解析了hello的预处理结果。可见预处理过程是只后所有操作的基础。预处理过程只是做些代码文本的替换工作,是为编译做的预备工作的阶段,对源程序并没有进行语法检查等操作,且程序没有大的变化。这个阶段可以比较简单地完成。
<第3章>
首先,大体上来说C语言和汇编语言是能够对应的,而汇编语言是更底层的语言,虽然对人来说更加艰涩难懂,但对计算机来说却离被接受更近了一步,从被人理解到被计算机理解,这是C程序运行的必由之路。因此我认为应该区分清楚这些概念。还有以hello为例,讲述了编译器是怎么处理C语言的各个数据类型以及各类操作的实际应用说明,像是数据:常量、变量(全局/局部/静态)的存放,通过movx的赋值 = ,算术操作:+、 - 、++,关系操作: != 、<=,数组/指针的引用:A[i]、*p 控制转移:if、for的使用,函数操作:参数传递(地址/值)、函数调用()、函数返回 return的实现等等。而且我们对C语言中的数据与操作的认识也会得到发展。
<第4章>
本章介绍了hello从hello.s到hello.o的汇编过程, 使用objdump工具得到反汇编代码,和之前得到的hello.s进行比较,通过寻找不同之处了解从汇编语言映射到机器语言这一过程。还有再通过readelf查看可重定位目标程序文件,通过对elf文件结构的分析,获得相关数据的运行时地址,以及不同节的、条目的大小、偏移量等信息。同时,通过.s文本文件与由机器语言反汇编获得的汇编代码比较,易得.s文件中,通过注记符寻址和经反汇编后,重定位表示的地址信息差异。
<第5章>
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能.我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
<第6章>
本章主要是讲的hello的执行过程,从fork,execve的进程开始到return的终止,这就是hello光辉而短暂的一生。 以图文并茂的方式阐述了hello从被父进程的fork创建,再被execve加载,再到通过内核模式控制下的上下文切换,来实现hello进程以时间分片的形式并发执行的过程。最后通过hello执行过程中可能发生的异常,以及信号处理方式的实际操作实践,感受了异常处理过程。
<第7章>
首先讲述了虚拟地址、线性地址、物理地址的区别,通过虚拟地址到物理地址的转换,进一步加深了对虚拟地址空间的理解运作及其强大作用,在fork、execve过程中扮演着重要的角色,使进程的私有地址空间变成了现实。同时,还体会了动态内存管理时,申请、分割、合并、回收等具体过程,加深了我们对动态内存管理过程的理解与认识。
<第8章>
本章主要讲述了Linux的IO设备管理方法,Unix I/O接口及其函数,以及printf函数实现的分析和getchar函数的实现。在此过程中,我们对系统I/O函数和Linux中将设备映射为文件式来管理的方式有了进一步的认识。到此,hello的一生已经被叙述完毕,作为一个小小的程序被工大无数学子挥洒笔墨宣扬它那些光辉岁月,想必hello在灵魂回归bash后已经死而无憾了吧。
关键词: 概述; 预处理; 编译; 汇编; 链接; hello进程管理; hello的存储管理; hello的IO管理;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理.......................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 11 -
7.5 三级Cache支持下的物理内存访问............................................................. - 11 -
7.6 hello进程fork时的内存映射..................................................................... - 11 -
7.7 hello进程execve时的内存映射................................................................. - 11 -
7.8 缺页故障与缺页中断处理.............................................................................. - 11 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:P2P的过程就是从文本文件到可执行文件的过程,hello.c的源程序(即文本)program,经过预处理器(cpp)修改成为hello.i文件,接着通过编译器成为汇编程序,然后通过汇编器转化为可重定位目标文件hello.o,最后通过链接器变为我们所熟知的可执行目标程序hello,通过shell fork和execve产生process。还有访存是CPU上的内存管理单元MMU根据页表将CPU生成的虚拟地址翻译成物理地址,将相应的页面调度。动态内存申请是printf调用malloc进行动态内存分配,在堆中申请所需的内存。接收信号是中途接受ctrl+z挂起,ctrl+c终止。结束是程序返回后,内核向父进程发送SIGCHLD信号,此时终止的hello被父进程回收。
020:从原来OS存储管理, MMU根据TLB将VA翻译成PA, 向cache发出请求, 发生缺页故障后,逐层申请,发生一系列的不命中后,通过页面交换进入内存, 就这样,hello离开磁盘,通过I/O桥,一生的旅行就开始了。再经过上面P2P的一系列过程之后,hello的一生以被父进程回收为终点。生也OS,死也OS,form zero-O to zero-O。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1) 硬件环境: X64 CPU;2.80GHz;8G RAM;256G SSD;
2) 软件环境: Windows10 64位;Vmware 14.1.3;Ubuntu 16.04 LTS 64位;
3) 开发工具: Edb, gdb, ccp, as, ld, readelf, gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c: hello源代码
hello.i: 预处理之后的源程序,分析预处理器行为
hello.s: 编译之后的汇编程序,分析编译器行为
hello.o: 汇编之后的可重定位目标程序,分析汇编器行为
hello.elf: hello.o的ELF格式
helloexe.elf: hello的ELF格式
hello: 可执行目标程序
hello2.s: 反汇编后输出的程序
Helloobj.txt: Hello可执行程序的反汇编代码
Temp.c: 临时数据存放
1.4 本章小结
本次实验,介绍了hello程序P2P,020的过程列出了实验中生成的中间文件,列出了实验使用的硬件条件,系统版本。还有基于Hello展开从Program到Process的过程,通过对其预处理、编译、汇编、链接的过程,领会程序由.c源文件到可执行文件的细节及工作方式。进一步体会程序从存储管理中的源文件,通过IO加载到主存,再通过CPU执行,将Hello的内容显示在屏幕上的具体过程原理。本次实验将在Win10系统的VMWAare虚拟机上完成,具体工作过程见后面章节。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预编译又称为预处理,是做些代码文本的替换工作。就是为编译做的预备工作的阶段。是根据以字符#开头的命令,修改源程序的过程,最后最后生成.i的文本文件。
作用:C语言预处理主要包括3个方面:1) 宏定义;2) 文件包含;3) 条件编译;
预处理即将宏进行展开。
1) #define标识符 字符串
如上格式的宏定义中,预处理的过程中,将标识符用字符串替代,倘若含有参数,形如#define 宏名(参数表) 字符串,则还要做参数替换,但不进行语法检查,不计算。
2) 文件包含 #include <文件名>
根据#修改文件后,编译时就以包含处理以后的文件为编译单位,被包含的文件将作为源文件的一部分被编译。
3) 条件编译
有些语句希望在条件满足时才编译,还有些语句当标识符已经定义时,才编译。例如: #ifdef
标识符
程序段1
#else
程序段2
#endif
当标识符已经定义时,程序段1才参加编译。
结果就得到了另一个C程序,通常是以.i作为文件扩展名。
使用条件编译可以使目标程序变小,运行时间变短。
预编译使问题或算法的解决方案增多,有助于我们选择合适的解决方案。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i

<图2.2.1 >
2.3 Hello的预处理结果解析
<图2.3.1 >
使用Text Editor打开hello.i文件,可以发现整个文件已经被扩展成了3990行。从图片中可以看出,所谓预处理只是将头文件中的内容提取出来直接添加到原来C代码的开头,得到另一个C文件。另外值得注意的是,虽然原来的C文本很少,但头文件的内容却相当的多,C程序看似简单,但可以看出其内容非常复杂。
2.4 本章小结
本章主要介绍了预处理的概念与作用,执行预处理的命令,并结合hello.i文件,解析了hello的预处理结果。可见预处理过程是只后所有操作的基础。预处理过程只是做些代码文本的替换工作,是为编译做的预备工作的阶段,对源程序并没有进行语法检查等操作,且程序没有大的变化。这个阶段可以比较简单地完成。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序.该程序包含函数main的定义,这个过程称为编译。这是将高级语言转换成低级机器语言指令的过程。不同高级语言经过编译器编译后,都输出为同一汇编语言。编译的作用就是将高级语言源程序翻译成等价的目标程序,并且进行语法检查、调试措施、修改手段、覆盖处理、目标程序优化等步骤。此阶段编译器会完成对代码的语法和语义的分析,生成汇编代码,并将这个代码保存在hello.s文件中。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
<图3.2.1 >
3.3 Hello的编译结果解析
<图3.3.1 >
<图3.3.2 >
<图3.3.3 >
<图3.3.4 >
这个文本是C语言main函数的汇编语言翻译,笔者认为,如果和c程序似,就是这样的内容。
原C程序如下:
int main(int argc,char *argv[]){
int i;
if(argc!=4){
printf("用法: Hello 学号 姓名 秒数!\n");
exit(1);
}
for(i=0;i<8;i++){
printf("Hello %s %s\n",argv[1],argv[2]);
sleep(atoi(argv[3]));
}
getchar();
return 0;
}
看<图3.3.2 >的话,部分汇编语言如下:
.file "hello.c"
.text
.section .rodata
.align 8
.LC0:
.string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"
.LC1:
.string "Hello %s %s\n"
.text
.globl main
.type main, @function
看<图3.3.3 >的话,
main:
.LFB6:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
.L2:
movl $0, -4(%rbp)
jmp .L3
看<图3.3.4 >的话,
.L4:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
call sleep@PLT
addl $1, -4(%rbp)
.L3:
cmpl $7, -4(%rbp)
jle .L4
call getchar@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
首先看<图3.3.x>的话,说明一下所有以‘.’开头的行除了大写的标签外都是指导汇编和链接器工作的伪指令,解释意思时可以忽略,但在main函数之前的伪指令仍然值得一提。
.file 声明源文件 .text 代码段 .rodata 只读数据段 .global全局变量声明 .type指定对象类型 .string声明字符串 .align声明对齐方式,其中部分伪指令在之后的汇编和链接仍会提到。
另外用上述命令产生的是ATT汇编代码格式,不同于Intel汇编代码格式,很重要的一个不同就是源操作数和目的操作数是相反的。
在main函数之前的只读数据段中,出现了本函数的两个常量字符串:
用法: Hello 学号 姓名 秒数!\n
Hello %s %s\n
声明为:"\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"
.align 8应该是双字对齐,至于全局变量中只有main,是因为本函数并没有定义全局变量
main函数:
首先要明确main函数的两个参数分别存在寄存器rdi(edi),rsi(esi)中。rsp和rbp为栈指针,所以汇编语言的前几句是为函数分配了一个栈(大小为32个字节),rbp指向栈底,rsp指向栈顶。接着将main函数的两个参数分别传送到栈底向上20和32字节的位置,将立即数4和第一个参数相比较若相等,跳转到L2,否则输出第二个参数并退出程序。
L2:将立即数0传送到rbp-4的位置(即局部变量i)并跳转到L3
L3:将i与7相比较,如果小于等于跳转到L4,否则调用getchar函数并返回
L4:通过栈指针的移动调用printf函数打印字符串数组的第一和第二个元素,继续通过栈指针选取字符串数组的第三个元素(秒数),调用数字字符转换函数和sleep函数,并令i的值加上1
再说一次, 从汇编程序中我们可以看到
1) C程序的几个数据类型,传入的参数argc argv通过寄存器rdi rsi来存储,局部变量i通过栈来存储。
2) 最常用的数据传送操作movl,movq。
3) 加减操作addq,subq,比较操作cmpl,相等跳转je,小于等于跳转jle,直接跳转jmp。
4) 函数调用call,函数返回ret,函数的各异功能,包括睡眠,打印和类型转换。
5) 压栈操作,通过栈指针rsp,rbp对数组进行临时存储和调用,元素选取。
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。
3.3.1
data节中局部变量存放在栈中,通过指向栈底的寄存器偏移量间接寻址获得,存放在%rdi与%rsi当中待打印的string类型串“Hello”,如<图3.3.2>存放在.string节中,argc, *argv参数局部变量存放在栈中,用寄存器存放地址,相应地指向该地址单元如<图3.3.3>
3.3.2 赋值
用movx src,dec,由x控制字长(如<图3.3.3>)函数调用前,将用作返回值的寄存器初始化movl $0, %eax
3.3.3运算
加法addx 操作数1,操作数2
减法subx 操作数1,操作数2(如<图3.3.2>)
3.3.4if条件语句判断
Cmpx操作数1,操作数2 jxx条件跳转语句实现,往下跳(如<图3.3.4>)
3.3.5循环控制语句
Cmpx操作数1,操作数2 jxx条件跳转语句实现,往上跳(如<图3.3.4>)
3.3.6关系运算
Cmpx操作数1,操作数2
3.3.7函数返回
pushq %rbp,将上一个栈顶地址压栈,为函数返回时,出栈做准备
通过%rax 返回返回值(如<图3.3.4>)
3.3.8参数传递,通过栈底指针间接寻址,暂时存放在寄存器%rdi,%rsi中.(图8)
3.3.9函数调用
call 函数名,在调用前,准备好参数,存放在寄存器%rdi, %rsi当中,并将返回值寄存器初始化movl $0, %eax(如<图3.3.4>)
3.3.10数组
通过偏移地址+基址寻址
movq -32(%rbp), %rax
addq $16, %rax
3.4 本章小结
首先,大体上来说C语言和汇编语言是能够对应的,而汇编语言是更底层的语言,虽然对人来说更加艰涩难懂,但对计算机来说却离被接受更近了一步,从被人理解到被计算机理解,这是C程序运行的必由之路。因此我认为应该区分清楚这些概念。还有以hello为例,讲述了编译器是怎么处理C语言的各个数据类型以及各类操作的实际应用说明,像是数据:常量、变量(全局/局部/静态)的存放,通过movx的赋值 = ,算术操作:+、 - 、++,关系操作: != 、<=,数组/指针的引用:A[i]、*p 控制转移:if、for的使用,函数操作:参数传递(地址/值)、函数调用()、函数返回 return的实现等等。而且我们对C语言中的数据与操作的认识也会得到发展。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编器将hello.s翻译成机器语言指令并将这些指令打包成一种叫做可重定位目标程序的格式,其结果保存在目标文件hello.o中。hello.o是一个二进制文件,它包含main函数的二进制编码。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
输入:as hello.s -o hello.o
< 图4.2.1 >
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。ELF头以一个16字节序列开始,描述了生成该文件系统字的大小和字节顺序。其余的为帮助链接器进行语法分析和解释目标文件的信息,包括ELF文件的大小,节头部的起始位置(此处为1240),程序的入口地点,目标文件的类型,机器类型(此处为小端机器),节头部表的文件偏移,以及节头部表中条目的大小与数量。如<图4.3.1>
<图4.3.1>
重定位.rel.text节,有偏移量,重定位类型,符号值等。当链接器将当前目标文件与其他文件组合时,需要修改这些位置,此处,修改的有puts(), exit(), printf(), sleepsecs, sleep,getchar等函数。而程序调用的本地函数指令地址属于绝对地址,重定位类型为R_X86_64_32,不需修改重定位后的地址信息。
<图4.3.2>
重定位.symtab节,包含包含在程序中定义与引用的函数与全局变量信息。任何已初始化的全局变量地址或外部函数地址都需要被修改。
<图4.3.3>
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
<图4.4.1>
机器语言指的是二进制的机器指令集合,而机器指令是由操作码和操作数构成的.汇编语言的主体是汇编指令.汇编指令和机器指令的差别在于指令的表示方法上,汇编指令是机器指令便于记忆的书写格式。
在对比两个文件后,汇编器在汇编hello.s时:函数调用…s文件中, 函数调用之后直接跟着函数名称,而在反汇编程序中, call的目标地址是全0。这是因为hello.c中调用的函数都需要通过动态链接器才能确定函数的运行时地址, 在汇编成为机器语言的时候, 对于这些不确定地址的函数调用, 将其call指令后的相对地址设置为全0, 然后在。rela.text节中为其添加重定位条目,等待被修改。分支转移:在反汇编汇编语言中, 如<图4.4.1>分支转移时的跳转目标地址为相对偏移量,而原来的.s文件中是.L2, .L3等注记符。因为转换成机器语言,在反汇编之后,注记符不复存在。访问全局变量时,.s文件中,使用的是注记符,而反汇编文件中是.rodata+偏移量。函数调用时,.s文件中用的是call 函数名,而反汇编得到的汇编代码中,是当前PC+偏移量,来调用。
4.5 本章小结
本章介绍了hello从hello.s到hello.o的汇编过程, 使用objdump工具得到反汇编代码,和之前得到的hello.s进行比较,通过寻找不同之处了解从汇编语言映射到机器语言这一过程。还有再通过readelf查看可重定位目标程序文件,通过对elf文件结构的分析,获得相关数据的运行时地址,以及不同节的、条目的大小、偏移量等信息。同时,通过.s文本文件与由机器语言反汇编获得的汇编代码比较,易得.s文件中,通过注记符寻址和经反汇编后,重定位表示的地址信息差异。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
注意到hello函数调用了printf等一系列函数,这些函数都是由C编译器提供的标准C库中的函数,并存在于单独的预编译好的目标文件中,这些函数必须以某种方式合并到hello.o的程序中。链接器(ld)负责处理这种合并得到hello文件。如果做简单假设,那么可以看作链接器读取一组可重定位目标文件,并把它们链接起来,形成一个输出的可执行文件。不过事实上现代计算机都采取一种动态链接共享库的方式作为链接手段。得到的文件是可执行目标文件,也是我们平时见的最多的那种点开即可运行的文件,它可以被加载到内存中,由系统执行。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
<图5.2.1>
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
<图5.3.1>
<图5.3.2>
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
<图5.4.1>
使用edb打开hello可执行程序,通过edb的Data Dump窗口查看加载到虚拟虚拟地址空间的hello程序。提供动态链接信息,每个表项提供了各段在虚拟地址空间的大小、偏移量,和物理空间的地址、权限标记、对齐长度。程序包含段:
1) PHDR 保存程序头表
2) INTERP 程序映射到内存后,调用的解释器
3) LOAD 程序需要从二进制文件映射到虚拟地址空间的段,保存了常量数据、目标的空间代码等
4) DYNAMIC 保存动态链接器使用的相关信息
5) NOTE 存储辅助信息
6) GNU_STACK 权限标志,标志是否可执行
7) GNU_RELRO 指定重定位后的哪些区域只需要设置只读
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
<图5.5.1>
<图5.5.2>
不同:
1.除了原来.text节外,增加了.init, .plt, .plt.got, .fini段,;
2.同时,在.txt段中,增加了_start,deregister_tm_clones,register_tm_clones,__do_global_dtors_aux,frame_dummy,__libc_csu_init,__libc_csu_fini,_fini等函数;
3.hello的反汇编代码中函数调用时,call的地址为运行时的绝对地址,而hello.o的反汇编代码中,是重定位条目信息。
过程:为了构造可执行文件,链接器先后完成两个主要任务:符号解析和重定位。
每个符号对应一个函数、全局变量、静态变量,通过符号解析,将定义与引用关联起来。链接器维护3个集合,可重定位目标文件的集合E(该集合中的文件将会被合并为可执行文件),U引用了但尚未被定义的集合,D在前面输入集合中已被定义的符号集合。初始时,3个集合均为空。链接器会判断命令行上的每一个输入文件,f,若f为目标文件,则链接器将f添加到E, 并修改U和D来反应f中的符号定义与引用;若f为一个归档文件,则链接器尝试匹配U中未解析的符号和f中定义的符号。直至U和D均不再变化,则将f丢弃。最后,若U为空,则合并可重定位目标文件E为可执行文件;否则,报错。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能.我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程就是一个执行中程序的实例.每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程。
作用:进程为用户提供以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:作为C编写的程序,它是用户使用Linux的桥梁。Shell是一种应用程序,它为用户访问操作系统内核提供了一个交互界面。
处理流程:
1) 读入输入的命令;
2) 分割字符串,获取命令;
3) 若为内置命令则执行,否则调用相应的程序为其分配子进程执行;
4) Shell可以异步接收来自I/O设备的信号,并对这些中断信号进行处理。
6.3 Hello的fork进程创建过程
Shell(父进程)通过fork 函数创建一个新的运行的子进程.新的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork 时,子进程可以读写父进程中打开的任何文件。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行他们的逻辑控制流中的指令。
6.4 Hello的execve过程
execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。
execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序, 加载器删除子进程现有的虚拟内存段, 并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零, 通过将虚拟地址空间中的页映射到可执行文件的页大小的片, 新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址, _start最终调用main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制, 这时, 操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器(PC)的值序列,若不发生抢占,则顺序执行,若发生抢占,则当前进程被挂起,控制转移至下一个进程。
时间分片:各个进程是并发执行的,每个进程轮流在处理器上执行,一个进程执行它控制流的一部分成为时间分片。
上下文切换:
1)保存当前进程的上下文
2)恢复某个先前被抢占的进程保存的上下文
3)将控制传递给当前新恢复的进程。操作系统内核使用上下文切换的异常控制流来实现上下文切换。
Hello进程执行分析:Hello起初在用户模式下运行, 在hello进程调用sleep之后转入内核模式, 内核休眠, 并将hello进程从运行队列加入等待队列, 定时器开始计时2s, 当定时器到时, 发送一个中断信号, 此时进入内核状态执行中断处理, 将hello进程从等待队列中移出重新加入到运行队列, hello进程继续执行。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
正常运行:
<图6.6.1>
运行途中按下ctrl+z:
<图6.6.2>
运行途中按下ctrl+c:
<图6.6.3>
输入ps打印前台进程组:
<图6.6.4>
输入jobs:
<图6.6.5>
输入pstree:
<图6.6.6.1>
<图6.6.6.2>
<图6.6.6.3>
输入fg:
<图6.6.7>
输入kill:
<图6.6.8>
6.7本章小结
本章主要是讲的hello的执行过程,从fork,execve的进程开始到return的终止,这就是hello光辉而短暂的一生。 以图文并茂的方式阐述了hello从被父进程的fork创建,再被execve加载,再到通过内核模式控制下的上下文切换,来实现hello进程以时间分片的形式并发执行的过程。最后通过hello执行过程中可能发生的异常,以及信号处理方式的实际操作实践,感受了异常处理过程。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:程序代码经过编译后出现在 汇编程序中地址.逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。
线性地址:某地址空间中的地址是连续的非负整数时,该地址空间中的地址被称为线性地址。
虚拟地址:CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。
物理地址:CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义.物理地址中很大一部分是留给内存条中的内存的, 但也常被映射到其他存储器上(如显存、BIOS等)。在没有使用虚拟存储器的机器上, 虚拟地址被直接送到内存总线上, 使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下, 虚拟地址不是被直接送到内存地址总线上, 而是送到存储器管理单元MMU, 把虚拟地址映射为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1) 逻辑地址=段选择符+偏移量
2) 每个段选择符大小为16位,段描述符为8字节(注意单位)
3) GDT为全局描述符表,LDT为局部描述符表
4) 段描述符存放在描述符表中,也就是GDT或LDT中
5) 段首地址存放在段描述符中
每个段的首地址都存放在自己的段描述符中, 而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。而要想找到某个段的描述符必须通过段选择符才能找到。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元负责把一个线性地址转换为物理地址。从管理和效率的角度出发,线性地址被划分成固定长度单位的数组, 称为页(page) 。例如, 一个32位的机器, 线性地址可以达到4G, 用4KB为一个页来划分, 这样, 整个线性地址就被划分为一个2^20次方的的大数组, 共有2的20次方个页, 也就是1M个页, 我们称之为页表, 改页表中每一项存储的都是物理页的基地址。类似地, 物理内存也被分割为物理页(PP/页帧), 是分页单元将所有的物理内存都划分成了固定大小的单元为管理单位, 其大小一般与内存页大小一致。
如果不考虑TLB与多级页表,虚拟地址分为虚拟页号VPN和虚拟页偏移量VPO。通过页表基址寄存器PTBR+VPN在页表中获得条目PTE, 一条PTE中包含有效位、权限信息、物理页号,如果有效位是0+NULL则代表没有在虚拟内存空间中分配该内存, 如果是有效位0+非NULL, 则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中, 如果有效位是1则代表该内存已经缓存在了物理内存中, 可以得到其物理页号PPN, 与虚拟页偏移量共同构成物理地址PA。
7.4 TLB与四级页表支持下的VA到PA的变换
首先,CPU生成一个VA。再到MMU的TLB中寻找相应的VPN,若命中,则读取相应的PPN与原VA中的VPO结合即为所求的PA。倘若TLB不命中,则将虚拟地址划分为4个VPN和一个VPO,每个VPN i都是从一个i级页表的索引,其中,1≤j≤4.当1≤j≤3时,第j级页表的每个PTE均为执行j+1级页表的基址,第4级页表的每个PTE为物理地址的VPN,而原VA的VPO=PPO,将PTE与PPO结合之后,便是物理地址PA。
7.5 三级Cache支持下的物理内存访问
获得了物理地址VA之后, 使用CI进行组索引, 每组8路, 对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1, 则命中, 根据数据偏移量CO(后6位)取出数据返回。如果没有匹配成功或者匹配成功但是标志位是1, 则不命中, 向下一级缓存中查询数据(L2 Cache->L3 Cache->主存), 查询到数据之后, 一种简单的放置策略如下:如果映射到的组内有空闲块, 则直接放置, 否则组内都是有效块, 产生冲突(evict), 则采用最近最少使用策略LFU(Least frequently used)进行替换。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的页面标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在新进程中返回时, 新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
在当前进程中的程序执行了execve(”a.out”,NULL, NULL)调用时,execve函数在当前程序中加载并运行包含在可执行文件a.out中的程序,用a.out代替了当前程序。加载并运行a.out主要分为一下几个步骤:
1) 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构
2) 映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零
3) 映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内
4) 设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点
7.8 缺页故障与缺页中断处理
缺页时的操作
1) 处理器生成一个虚拟地址,并把它传送给MMU。
2) MMU生成PTE地址,并从高速缓存/主存请求得到它。
3) 步:高速缓存/主存向MMU返回PTE。
4) 步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5) 步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
6) 步:缺页处理程序页面调人新的页面,并更新内存中的PTE。
7) 步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令, CPU将地址重新发送给MMU。因为虚拟页面现在已经缓存在物理内存中, 所以会命中, 主存将所请求字返回给处理器。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
显示分配器:要求应用显示地释放已分配的块,C库中提供了malloc来申请,free来释放。将实现的数据结构指针存放在空闲块主体中,如,堆可组织成一个双向空闲链表,每个空闲块照中包含一个pred前驱和一个succ后继指针。使首次适配的分配时间从块总数 的线性时间减少到空闲块数量的线性时间。,释放块的时间也是线性的,有时也可能为常数。根据不同要求,分配器采取不同的放置策略,常见的有首次适配、下次适配、最佳适配。至于选择哪种放置策略,要根据实际对时间效率、空间效率的要求确定。
合并策略:为解决假碎片问题,分配器必须合并相邻的空闲块,常采用的有立即合并,延迟合并等。
7.10本章小结
首先讲述了虚拟地址、线性地址、物理地址的区别,通过虚拟地址到物理地址的转换,进一步加深了对虚拟地址空间的理解运作及其强大作用,在fork、execve过程中扮演着重要的角色,使进程的私有地址空间变成了现实。同时,还体会了动态内存管理时,申请、分割、合并、回收等具体过程,加深了我们对动态内存管理过程的理解与认识。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入输出都被当作相对应文件的读和写。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
1) 打开文件。一个应用程序通过要求内核打开相应的文件, 来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数, 叫做描述符, 它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2) Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3) 改变当前的文件位置。对于每个打开的文件, 内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作, 显式地设置文件的当前位置为K。
4) 读写文件。一个读操作就是从文件复制n>0 个字节到内存, 从当前文件位置k 开始, 然后将k增加到k+n。给定一个大小为m 字节的文件, 当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件, 应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0 个字节到一个文件, 从当前文件位置k开始, 然后更新k。
5) 关闭文件.当应用完成了对文件的访问之后,它就通知内核关闭这个文件.作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中.无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.
Unix I/O函数:
1) 进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符, 并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件, mode 参数指定了新文件的访问权限位。
返回:若成功则为新文件描述符,若出错为-1.
2) 进程通过调用close 函数关闭一个打开的文件.int close(int fd);
返回:若成功则为0, 若出错则为-1.
3) 应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf。返回值-1表示一个错误, 而返回值0 表示EOF。否则, 返回值表示的是实际传送的字节数量。
返回:若成功则为读的字节数, 若EOF 则为0, 若出错为-1。
ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。
返回:若成功则为写的字节数,若出错则为-1。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
研究printf的实现, 首先来看看printf函数的函数体。
先来看printf函数的内容:
va_list arg = (va_list)((char*)(&fmt) + 4);
va_list的定义: typedef char va_list
这说明它是一个字符指针。其中的: (char)(&fmt) + 4) 表示的是…中的第一个参数。
i = vsprintf(buf, fmt, arg);
vsprintf返回的是要打印出来的字符串的长度,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化, 产生格式化输出最后调用write。
在write函数中, 将栈中参数放入寄存器,ecx是字符个数, ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall,
Syscall如下
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
一个call save, 是为了保存中断前进程的状态。
ecx中是要打印出的元素个数, ebx中的是要打印的buf字符数组中的第一个元素,这个函数的功能就是不断的打印出字符, 直到遇到:’\0’ 停止。
就这样, syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中, 显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram, 并通过信号线向液晶显示器传输每一个点(RGB分量)。于是字符串就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要讲述了Linux的IO设备管理方法,Unix I/O接口及其函数,以及printf函数实现的分析和getchar函数的实现。在此过程中,我们对系统I/O函数和Linux中将设备映射为文件式来管理的方式有了进一步的认识。到此,hello的一生已经被叙述完毕,作为一个小小的程序被工大无数学子挥洒笔墨宣扬它那些光辉岁月,想必hello在灵魂回归bash后已经死而无憾了吧。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1) 预处理:将hello.c源程序的#开头的指令进行符号替换,生成hello.i文件
2) 编译:将hello.i转化为汇编代码,生成hello.s文件
3) 汇编:将hello.s转化为二进制的机器代码,生成hello.o的可重定位目标程序
4) 链接:对hello.o中引用的外部函数、全局变量等进行符号解析,并重定位为可执行文件hello。
5) 运行:在终端输入hello 117030****
6) 创建子程序:通过fork创建子程序;
7) 执行:通过execve加载器载入,建立虚拟内存映射,设置当前进程的上下文中的程序计数器,使之指向程序入口处。CPU为其分配相应的时间分片,形成同其他程序并发执行的假想;
8) 访存:CPU上的内存管理单元MMU根据页表将CPU生成的虚拟地址翻译成物理地址,将相应的页面调度;
9) 动态内存申请:printf调用malloc进行动态内存分配,在堆中申请所需的内存;
10) 接收信号:中途接受ctrl+z挂起,ctrl+c终止;
11) 结束:程序返回后,内核向父进程发送SIGCHLD信号,此时终止的hello被父进程回收
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
1) hello: 可执行目标程序
2) hello.c: 提供的程序
3) hello.elf: hello.o的ELF格式
4) hello.i: 预处理之后的源程序
5) hello.o: 汇编之后的可重定位目标程序
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] https://zhuanlan.zhihu.com/p/100109228
[2] https://max.book118.com/html/2019/1229/6115144142002134.shtm
[3] https://www.e-learn.cn/topic/3326922
[4] https://www.bilibili.com/read/cv4255183/
[5] https://blog.csdn.net/qq_40363447/article/details/85478752
[6] https://blog.csdn.net/lll_90/article/details/85427841
(参考文献0分,缺失 -1分)
本文详细剖析了Hello程序从预处理、编译、汇编、链接到执行的全过程,涵盖Ubuntu环境下的操作命令及各阶段解析。深入探讨了进程管理、存储管理和IO管理,涉及地址转换、段式页式管理、动态内存分配以及Unix I/O接口。通过对printf和getchar函数的分析,展示了Linux设备管理方法。

402

被折叠的 条评论
为什么被折叠?



