摘 要
本文讲述了hello的“一生”,即以hello为典例,讲述了一个程序从预处理到编译、到汇编与链接,再到进程管理,并在各部分中都进一步地结合hello讨论了程序处理过程中的有关概念与作用,结合Linux系统工具将hello.c文件一步步地变成可执行文件hello,再结合进程、存储管理的有关概念讨论了hello的执行过程。
关键词: 预处理;编译;汇编;链接;进程管理;存储管理

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述 – 4 –
1.1 Hello简介 – 4 –
1.2 环境与工具 – 4 –
1.3 中间结果 – 4 –
1.4 本章小结 – 4 –
第2章 预处理 – 5 –
2.1 预处理的概念与作用 – 5 –
2.2在Ubuntu下预处理的命令 – 5 –
2.3 Hello的预处理结果解析 – 5 –
2.4 本章小结 – 5 –
第3章 编译 – 6 –
3.1 编译的概念与作用 – 6 –
3.2 在Ubuntu下编译的命令 – 6 –
3.3 Hello的编译结果解析 – 6 –
3.4 本章小结 – 6 –
第4章 汇编 – 7 –
4.1 汇编的概念与作用 – 7 –
4.2 在Ubuntu下汇编的命令 – 7 –
4.3 可重定位目标elf格式 – 7 –
4.4 Hello.o的结果解析 – 7 –
4.5 本章小结 – 7 –
第5章 链接 – 8 –
5.1 链接的概念与作用 – 8 –
5.2 在Ubuntu下链接的命令 – 8 –
5.3 可执行目标文件hello的格式 – 8 –
5.4 hello的虚拟地址空间 – 8 –
5.5 链接的重定位过程分析 – 8 –
5.6 hello的执行流程 – 8 –
5.7 Hello的动态链接分析 – 8 –
5.8 本章小结 – 9 –
第6章 hello进程管理 – 10 –
6.1 进程的概念与作用 – 10 –
6.2 简述壳Shell-bash的作用与处理流程 – 10 –
6.3 Hello的fork进程创建过程 – 10 –
6.4 Hello的execve过程 – 10 –
6.5 Hello的进程执行 – 10 –
6.6 hello的异常与信号处理 – 10 –
6.7本章小结 – 10 –
第7章 hello的存储管理 – 11 –
7.1 hello的存储器地址空间 – 11 –
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 –
7.9动态存储分配管理 – 11 –
7.10本章小结 – 12 –
第8章 hello的IO管理 – 13 –
8.1 Linux的IO设备管理方法 – 13 –
8.2 简述Unix IO接口及其函数 – 13 –
8.3 printf的实现分析 – 13 –
8.4 getchar的实现分析 – 13 –
8.5本章小结 – 13 –
结论 – 14 –
附件 – 15 –
参考文献 – 16 –

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P过程:
通过编译器驱动程序(complier driver)将hello.c程序从ASCII码源文件翻译成可执行目标文件,具体过程如下:驱动程序首先运行C预处理器(cpp),它将C的源程序hello.c翻译成一个ASCII码的中间文件hello.i,接下来,驱动程序运行C编译器(cc1),它将hello.i翻译成一个ASCII码汇编语言文件hello.s,然后,驱动程序运行汇编器(as),将hello.s翻译成一个可重定位目标文件hello.o,最后运行链接器程序ld,将hello.o和一些必要的系统目标文件组合起来,创建一个可执行目标文件,随后shell通过fork为hello程序创建进程,完成了P2P过程.
020过程:
Shell通过execve函数在fork产生的子进程中加载hello,具体过程如下:(1)删除已存在的用户区域。删除当前虚拟地址的用户部分已存在的数据结构。(2)映射私有区域。为hello的代码段、数据、bss和栈区域创建新的区域结构。(3)
映射共享区域(4)设置程序计数器PC,使之指向代码区域的入口点。
随后,CPU为hello分切时间片执行逻辑控制流。hello通过Unix I/O管理来控制输出,在其执行完成后由shell回收,并由内核删除其在系统中的所有痕迹,完成020过程
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
软件环境:virtualbox,codeblocks,edb,gcc,gedit,objdump,readelf等
1.3 中间结果
1、hello.i(.c经过预处理后的拓展)
2、hello.s(.i文件经编译后得到的汇编文本文件)
3、hello.o(汇编后得到的可重定位目标文件)
4、helloelf.elf(hello.o的elf格式)
5、hello(.o经链接后得到的可执行目标文件)
6、hello.elf(hello的elf格式)

1.4 本章小结
在这一章节中,我们从p2p过程和020过程简要讲述了hello的“一生”,并列出了本次作业所使用的硬件、软件环境、中间结果文件。

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理是一个扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏的过程
作用:正如定义所言,预处理便起到展开头文件、宏替换、条件编译等作用,是对源代码的扩展。
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i

图 1 预处理命令

2.3 Hello的预处理结果解析

图 2 .c文件内容

图 3 .i文件内容
在截图中,我们可以看到,从3047行开始是main函数部分,与hello.c一致,而之前的行数,正如2.1中对预处理的介绍,是对原函数#的扩展。
2.4 本章小结
本章中,我们介绍了预处理的概念与作用,并通过cpp指令将hello.c转为hello.i文件
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译就是把代码转为ASCII汇编语言文件的过程。
作用:编译最主要的作用是语法分析并将源语言转换为汇编语言,主要分为五个阶段:
1、词法分析,词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。
2、语法分析、语义检查,编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。
3、中间代码生成,中间代码是源程序的一种内部表示
4、代码优化,代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
5、目标代码生成,目标代码生成是编译的最后一个阶段,目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。

3.2 在Ubuntu下编译的命令
gcc -S hello.c -o hello.s

图 4 编译命令

图 5 .s文件
3.3 Hello的编译结果解析
3.3.1 数据
(1)整型常量与变量
int i ,在hello.c的main函数中,声明了一个int型的局部变量i,并执行for循环操作,通过对汇编语言与C语言的对比,不难发现,i被存在栈上空间-4(%rbp)中,main函数中的第一个参数int argc被存在%rdi中,而argc 则存在栈上-20(%rbp),而立即数则在汇编语言中直接以$+十进制数字出现

图 6 汇编内容

图 7 对应C中内容

图 8 汇编内容
(2)字符串常量
如图,printf中字符串的常量部分被储存在.Rodata中

图 9 字符串常量
3.3.2赋值
以“i=0”为例
在for循环条件中,对局部变量i赋初值0,采用mov指令实现,对应汇编语言如图

图 10 赋值
3.3.3算术操作
以“i++”为例

图 11 C中算术

图 12 对应汇编
3.3.4逻辑操作
(1)!=
在hello.c中,if中涉及不等判断,是通过cmp和jxx指令实现的

图 13 C中!=操作

图 14 对应汇编

(2)<
for循环中设计对i<8的判断,在hello.s中对应

图 15 <操作
3.3.5数组操作
在hello.c中涉及数组argv元素的访问,-32(%rbp)便是数组起始地址,通过改变偏移量与利用寄存器达到访问argv[1],argv[2],argv[3]的效果

图 16 C中对数组操作

图 17 对应汇编
3.3.6控制转移
在hello.c程序中涉及的控制转移有for和if,都是通过设置条件码与跳转指令jxx实现的,在本章的逻辑操作中就可以看到对应的控制转移操作(图例与3.3.4一致)
3.3.7函数操作
函数是一种过程的形式,要提供对函数的机器级支持,必须处理许多不同属性,包括以下一种或多种机制,假设过程P调用过程Q,Q执行后返回P
(1)传递控制,进入过程Q的时候,程序计数器PC必须被设置为Q的代码的起始地址,然后在返回时要把PC设置为P中调用Q的后一条指令的地址
(2)传递数据,P必须能为Q提供一个或多个参数,Q必须能向P返回一个值
(3)分配和释放内存,在起始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些存储空间。
将以上定义具体化到hello.s中,转移控制是通过call和ret指令实现的,执行call时,返回地址被压入栈中,随后执行被调用函数的指令,直至遇到ret指令,ret指令执行时,弹出返回地址,并跳转到这个地址,执行原函数。参数传递按%rdi,%rsi,%rdx,%rcx顺序传递,返回值则在寄存器%rax中
(1)传递控制
call与ret指令

图 18 传递控制
(2)传递数据
Hello.s中通过寄存器实现,如图,printf中两个参数在call printf前分别被存在%rsi,%rdx中,而双引号内容在%rdi中

图 19 C中数据传递

图 20 对应汇编中数据传递

exit(1)的参数传递,立即数1作为唯一参数被存在%edi中

图 21 exit参数传递

atoi的参数传递,argv[3]的值由rax传给rdi作为唯一参数

图 22 atoi参数传递

图 23 atoi参数传递

sleep的参数传递,将atoi返回值(存在rax中)赋给rdi作为参数

图 24 sleep参数传递

返回值:
main函数return 0,将立即数0存在rax中并ret

图 25 main返回值

atoi返回值存在rax并继续作为参数传给sleep


图 26 atoi返回值与参数传递

3.4 本章小结
本章讲述了编译的定义与作用,并结合C语言的汇编指令对hello.s中的一些典型汇编语句进行分析(涉及数据、逻辑操作、算术操作、函数等方面)
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as),将.s文件翻译成一个可重定位目标文件(二进制的.o文件)的过程
作用:机器执行的程序只是一个字节序列,它是对一系列指令的编码,机器对产生这些指令的源代码几乎一无所知,因此需要进行二进制转换。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o

图 27 汇编命令
输入指令后,可以在桌面上找到.o文件

4.3 可重定位目标elf格式
首先使用readelf,输入readelf -a hello.o > helloelf.elf 输出.elf文件。

图 28 readelf的使用
(1)ELF头
ELF头以一个16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序,剩下部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

图 29 ELF头
(2)节头部表
不同节的位置和大小都是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)指向符合以NULL结尾的字符串名字,name是字符串表中的字节偏移,type通常要么是数据、要么是函数,address是这些段加载到内存的地址,offset给出了section文件的偏移量。

图 30 节头部表
(3)符号表
符号表是由汇编器构造的,使用编译器输出到汇编语言.s中的符号,这张符号表包含一个条目的数组,name是字符串表中的字节偏移,指向符号的以null结尾的字符串的名字,value是距定义目标的节的起始地址的偏移,size是目标的大小,type通常要么是数据,要么是函数,binding字段表示符号是本地的还是全局的。

图 31 符号表
(4)重定位条目
当汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并为可执行文件时如何修改这个引用。Offset是需要被修改的引用的节偏移。Info中的前四个字节为symbol,symbol标识被修改引用应该指向的符号,type告知链接器如何修改新的引用,attend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。

图 32 重定位条目

如图,type中包含R_X86_64_PC32、R_X86_64_PLT32类型,表示重定位PC相对引用,链接器就会根据重定位算法来重定位程序中的引用

图 33 重定位算法
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

图 34 hello.o反汇编

图 35 hello.o反汇编

图 36 hello.o反汇编

图 37 hello.s
(1)机器语言的构成
在机器语言中,我们可以看到每一条指令由16进制表示的对应的节偏移地址以及操作数,在反汇编之后,通过与hello.s的对比,可以发现二者存在着一一对应的关系,但在表示层面有一定的区别
(2)操作数
机器语言反汇编之后,我们可以看到操作数都是由16进制表示的,而在hello.s中,操作数为10进制表示

图 38 hello.o反汇编后的操作数

图 39 对应hello.s中的操作数
(3)分支转移
在hello.s中,分支转移的跳转过程都是通过诸如.Li的助记符表示的,而在机器语言反汇编后,可以观察到跳转是通过寻址实现的。

图 40 hello.o中分支转移

图 41 对应hello.s中的分支转移

(4)函数调用
在机器语言进行反汇编后,可以看到在调用函数call之后,反汇编中出现了重定位入口,且反汇编的call之后跟随的是本函数下一条指令的地址,这样在重定位之后可以根据PC相对寻址确定puts函数的地址。

图 42 hello.s中函数调用

图 43 hello.o中函数调用

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
在本章节中,我们讨论了汇编的概念与作用,还简要列出了ELF文件的主要内容,讨论了机器语言和汇编语言间的区别与联系
(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程, 这个文件可被加载(复制)到内存并执行。链接可以执行于编译时, 也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时,甚至执行于运行时, 也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。
作用:链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
ld -o hello -dynamic -linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/crtn.o /usr/lib/x86_64-linux-gnu/libc.so

图 44 将hello.o与一些基本C库链接

5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
如图,通过节头部表,可以读出各段基本信息,size表示了各段的大小,address表示了各段的起始地址,offset为偏移量。我们通过对比还可以发现,在链接之后,节头部表中的节数变多,且在链接过后,address已经被分配

图 45 导出hello的elf文件

图 46 hello的elf

图 47 hello的elf

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
可以看到程序从0x0000000000400000开始,通过plugins中的symbolviewer可以看到节头表中各节的位置与链接后的address一致

图 48 edb加载hello

图 49 edb工具栏

图 50 address对比

同样地,对于程序头部分,也可以在data dump中找到各部分对应的虚拟地址。表中包含了段类型、偏移量、虚拟地址与物理地址等信息


图 51 程序头部分

5.5 链接的重定位过程分析

图 52 将hello反汇编

图 53 对比
首先我们使用objdump工具将hello与hello.o反汇编,hello.o在左,hello在右,通过对比我们可以发现
(1)在经过链接后生成的文件hello在反汇编后,有很多个节,例如.init、.plt,而在链接前,即hello.o的反汇编中,只有.text节,看不到引用的函数如printf、getchar的汇编信息
(2)在hello.o中,存在数据的重定位条目
由0x0(%rip)(尚未定位)变为0xec3(%rip)(通过链接完成重定位PC相对引用)

图 54 链接后

图 55 重定位条目

(3)函数的重定位
在hello.c中,存在许多函数引用,他们的重定位方式大致相同,我们以puts为例,在重定位前,call后跟的是main中call指令下一条指令的地址,并且在中间有重定位入口,而在重定位后,call后跟随的是puts函数的虚拟地址

图 56 函数的重定位

图 57 链接后的函数调用

图 58 函数地址
(4)跳转
在链接前,跳转指令后跟随的从main(0)开始的偏移地址,而链接后则是从main的虚拟地址开始算起的偏移。

图 59 链接前的跳转实现

图 60 链接后的跳转实现

5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
可通过symbols功能结合代码运行列出

图 61 symbols页面
从加载到程序终止的所有过程,调用的子程序分别为
ld-2.31.so!_dl_start
ld-2.31.so!_dl_init
hello!_start
libc-2.31.so!__libc_start_main
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
libc-2.31.so!exit
5.7 Hello的动态链接分析
几乎每个C程序都使用标准I/O函数,比如printf和scanf,在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在运行多进程的典型系统上,是对内存的极大浪费。
共享库是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程就称为动态链接,是由一个叫动态链接器的程序来执行的。共享库也称为共享目标,在Linux系统中通常用.so后缀表示。
可以加载而无需重定位的代码称为位置无关代码(PIC),用户对GCC使用-fpic选项指示GNU编译系统生成PIC代码。共享库的编译必须始终使用该选项。
(1)PIC数据引用
编译器在数据段开始的地方创建了一个表,叫做全局偏移量表(GOT),在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的绝对地址。
(2)PIC函数调用
采用延迟绑定技术

图 62 PLT和GOT
(3)hello中的动态链接项目分析

图 63 hello中的动态链接项目

图 64 init前

图 65 init后
通过edb调试,在dl_init前后,这些项目的内容变化正体现了延迟绑定的过程,通过plt与got协同工作实现延迟绑定,我们也在调用过后看到了GOT中的内容被动态链接器修改。
5.8 本章小结
本章主要讨论了链接的概念与作用,并结合hello的实例对链接作用、动态链接进行具体分析
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:进程提供给应用程序两个关键的抽象
(1)一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器
(2)一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:shell是一个交换型的应用级程序,它代表用户运行其他程序。Shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序
Shell处理流程:一个简单的shell处理例程便是:shell打印一个命令行提示符,等待用户在stdin上输入命令行,然后对这个命令行求值。
(1)调用函数,解析以空格为分隔的命令行参数,并构造最终传递给execve的argv向量,第一个参数被假设为要么是一个内置的shell命令,马上就会解释这个命令,要么是一个可执行目标文件,会在一个新的子进程的上下文中加载并运行这个文件。如果最后一个参数是&则为后台执行,否则前台执行。
(2)解析命令行后,shell接着调用函数,判断第一个命令行参数是否是内置命令
(3)若不是内置命令,则shell创建一个子进程,并在子进程中执行所请求的程序,如果用户要求后台执行,那么shell返回循环顶部,等待下一个命令行。否则,shell使用waitpid等待作业终止、终止后开启下一轮迭代。
(4)在过程中shell还应进行信号处理与子进程回收。
6.3 Hello的fork进程创建过程

图 66 在命令行执行hello
正如6.2节中对处理流程的描述,我们传入的第一个参数是一个可执行目标文件,而不是shell中的内置命令,故shell在处理时调用fork()函数创建了一个子进程,并在子进程中执行程序hello,fork函数被调用一次,返回两次,一次是在父进程中,一次是在新创建的子进程中,父进程返回子进程PID,子进程返回0,在上述的实例中,shell调用了fork,并在子进程中执行了hello程序。
6.4 Hello的execve过程
在6.3中,可以看到,在输入图示命令后,shell判断hello不是内置命令,于是调用了fork函数创建了一个子进程,execve过程便是在子进程中执行的,execve函数在当前进程(6.3中fork出的子进程)的上下文中加载并运行一个新的程序hello,它会覆盖当前进程的地址空间,但与fork不同,execve并不创建新进程,保持了与当前进程相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
6.5 Hello的进程执行
Hello执行的逻辑控制流:
系统中通常有多个程序在运行,进程向每个程序提供了一种假象,好像它在独占地使用处理器,用调试器(如edb)单步执行hello程序,我们能看到一系列的PC值,这个PC值的序列称为逻辑控制流

图 67 逻辑控制流
如图,假设处理器的一个物理控制流被分为3个逻辑控制流,竖线表示逻辑流的一部分,我们可以看到进程是在轮流使用处理器的,假设hello在进程A中,若可以精确的测量每条指令使用的时间,会发现在hello程序一些指令的执行间,CPU会有周期性停顿。
同样如上图所示,AB,AC时间上有重叠,称为并发,进程ABC轮流执行的概念称为多任务,一个进程执行其控制流的一部分的每一段时间称为时间片,如A有2个时间片、B有1个、c有两个
上下文切换:
首先了解用户模式与内核模式,处理器通过控制某个寄存器中的一个模式位实现二者切换,内核模式拥有高权限,而用户模式权限受限。
上述的多任务过程便是内核通过上下文切换实现的,上下文就是内核重新启动一个被抢占的进程所需的状态,包含两个层面(1)保存当前进程的上下文(2)恢复先前进程的上下文(3)将控制传递给被恢复进程,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,称为调度。

图 68 上下文切换
如图,假设hello在进程A中,当hello需要调用sleep函数时陷入内核,内核中的陷阱处理程序开始请求执行sleep,由于sleep执行时间很长,故内核执行从进程A->B的上下文切换,故在第一个kernelcode中,内核先执行了一段时间的进程A,随后在内核模式下执行进程B,在完成上下文切换后,在用户模式下执行B的指令,在sleep完成后,收到中断信号,于是内核进行B->A的上下文切换,继续依照上述过程将控制返回给hello中sleep之后的指令。

6.6 hello的异常与信号处理
(1)正常运行

图 69 正常运行
(2)乱按

图 70 乱按
可以看到我们乱按的内容被显示到终端上

图 71 乱按的特例
如图我们输入了./hello 回车 加./hello,随后./hello被当作将被处理的命令出现在命令行处
./hello 回车 加./hello 加回车,第一个回车前的内容被缓冲,随后输入的内容
在当前的hello结束后被当作命令进行处理,如图,再次运行了hello程序。

图 72 乱按的特例
(3)按回车(中断)

图 73 按回车
按回车之后是异步中断,缓冲区尚未处理的回车会在程序结束后被处理。
(4)ctrl-z(SIGTSTP信号)
输入ctrl-z的结果是发送一个SIGTSTP信号给前台进程组的每个进程,挂起前台作业

图 74 按ctrl-z
(5)ctrl-c(SIGINT信号)

图 75 按ctrl-c
在键盘上输入CTRL-C会导致内核发送一个SIGINT信号到前台进程组中的每个进程,默认情况下,结果是终止前台作业。可以看到,图中在输入ctrl-c后前台作业被终止。
(6)ctrl-z后运行命令
Ps(查看当前运行的进程)、jobs(查看当前作业)、fg 1(将被挂起的hello放到前台执行)

图 76 输入ps、jobs、fg的结果
pstree
如图为pstree的局部,pstree以树状图的形式展示了各进程间的关系

图 77 pstree的局部
Kill

图 78 kill
6.7本章小结
本章中讨论了进程、shell的概念与作用,并结合hello的实例讨论了异常控制流的有关内容。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是指由程序产生的与段相关的偏移地址部分。
1、在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。
2、把用户程序中使用的地址称为相对地址即逻辑地址。
3、逻辑地址由两个16位的地址分量构成,一个为段基址,另一个为偏移量。两个分量均为无符号数编码。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:现代系统提供了一种对主存的抽象概念,叫做虚拟内存。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美结合,它为每个程序提供了一个大的、一致的和私有的地址空间。通过一个很清晰的机制提供了三种重要的能力:1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存。2)它为每个进程提供了一致的地址空间,从而简化了内存的管理。3)它保护了每个进程的地址空间不被其他进程破坏。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理:
将虚拟内存分为大小相同的段,段地址存放于不同的寄存器当中,如CS用于存代码段地址,DS用于存放数据段地址,SS用于放堆栈段地址。管理分为(1)平坦分段模式(使用全局段描述符号表GDT)(2)多端模式(使用局部描述符表LDT)

图 79 平坦分段模式


图 80 多段模式
逻辑地址到线性地址的变换:
逻辑地址由段标识符和偏移量组成,段标识符有16位,前13位为索引,在根据段标识符中的标记确定在LDT还是GDT中查找索引,通过索引,便可确定对应的段描述符,从而确定基址,在通过偏移量便可实现从逻辑地址到线性地址的变换。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理:
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。VM系统通过将虚拟内存分割为虚拟页的大小固定的块来处理这个问题。类似地,物理内存被分割为物理页,大小与虚拟页相同,被称为页帧。

图 81 页式管理
线性地址到物理地址的变换:
(1)页表的概念
同任何缓存一样,虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM的某个地方,如果是,系统还必须确定这个虚拟页存放在哪个物理页中,如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换掉这个牺牲页。这些功能是由软硬件联合提供的,包括操作系统软件、MMU中的地址翻译硬件和一个存放在物理内存中的叫做页表的数据结构

图 82 页表
(2)过程
当页面命中时,处理器生成一个虚拟地址并传给MMU
MMU生成PTE地址,并从高速缓存/主存中请求得到它
高速缓存/主存向MMU返回PTE
MMU构造物理地址,并传送给高速缓存/主存
高速缓存/主存返回所请求的数据字给处理器

图 83 变换过程

图 84 两种情况

7.4 TLB与四级页表支持下的VA到PA的变换
利用TLB
当命中时
第1步:CPU产生一个虚拟地址
第2步和第3步:MMU从 TLB中取出相应的PTE。
第4步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
第5步:高速缓存/主存将所请求的数据字返回给 CPU。
若不命中
当TLB不命中时,MMU必须从L1缓存中取出相应的PTE。

图 85 利用TLB
对4级页表层次结构的地址翻译,则虚拟地址被划分为4个VPN和1个VPO,每个VPNi都是到i级页表的索引,第j级页表中的每个PTE都指向第j+1级某页表的基址。第4级页表中的每个PTE包含某个物理页面的PPN或一个磁盘块的地址,MMU必须访问4个PTE才能确定PPN。

图 86 利用k级页表的地址翻译
7.5 三级Cache支持下的物理内存访问
(1)三级cache存储器层次结构

图 87 存储器层次结构
(3)仅在L1CACHE支持下的物理内存访问

图 88 在一级cache支持下的访问
虽然图中只展示了在L1CACHE支持下的物理内存访问,但实际上,三级CACHE支持下的物理内存访问与之类似,在本级CACHE未命中后会进入到下一级CACHE,并执行相应的未命中替换策略,多级CACHE能更好提高效率。
7.6 hello进程fork时的内存映射
当fork函数被shell调用时,内核为新进程(将执行hello的进程)创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。只有当这两个进程中的任一个后来进行写操作时,写时复制机制才就会创建新页面,才会在物理内存上有实际区别。因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
在命令行输入./hello 学号 姓名 秒数后,fork出的进程中执行了如下的execve调用:execve(“hello”,NULL,NULL)
execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前程序。加载并运行hello需要以下几个步骤:
1、删除已存在的用户区域。删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。
2、映射私有区域。为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
3、映射共享区域。hello程序与共享对象或目标(如C库)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
4、设置程序计数器(PC)。exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。
下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面

图 89 execve时的内存映射
7.8 缺页故障与缺页中断处理
在虚拟内存中,DRAM缓存不命中称为缺页,CPU引用了虚拟内存页中的一个字,但这页并未被缓存在DRAM中(从有效位推断),随后触发一个缺页异常(缺页故障)缺页异常调用内核中的缺页异常处理程序,替换一个DRAM页


图 90 缺页故障的产生

图 91 故障的处理流程

图 92 处理结果
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。不同系统有微小的差别但是一般来说堆存在于未初始化的数据后面并想高地址生长。对于每个进程,内核负责维护一个变量叫brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块都是一个连续的虚拟内存片有两种状态(已分配或空闲)顾名思义,已分配的块已经被进程所使用,而空闲块可以用来分配,这时他就变成了一个已分配块,而已分配的块只有在被释放才能重新在使用。
malloc函数返回一个指针,指向大小为至少size字节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。实际中,对齐依赖于编译代码在32位模式(gcc -m32)还是64位模式(默认的)中运行。在32位模式中, malloc返回的块的地址总是8的倍数。在64位模式中,该地址总是16的倍数。
如果malloc遇到问题(例如,程序要求的内存块比可用的虚拟内存还要大),那么它就返回NULL,并设置errno. malloc不初始化它返回的内存。动态内存分配器,例如malloc,可以通过使用mmp和munmap函数,显式地分配、释放堆内存。

图 93 堆块格式
7.10本章小结
本章简述了各种地址概念与转换关系、并结合hello讨论了存储管理过程
(第7章 2分)

第8章 hello的IO管理(X)
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
过程:
(1)在hello.c被编写完之后,经预处理被拓展为hello.i文件
(2)hello.i经过编译后,变为由汇编代码表示的文本文件hello.s
(3)hello.s在经过汇编后,变为由机器语言表示可重定位的hello.o文件
(4)hello.o与其他与C程序有关的文件被链接器链接,形成可执行目标文件hello
(5)在命令行输入命令后,shell为hello程序fork进程,并在该进程中调用execve加载并执行hello
(6)hello的指令被执行过程中,涉及异常处理、调度、信号处理、存储管理
(7)父进程回收hello所在的子进程。
感悟:在学习计算机系统之前,我对一个程序的理解只限于在IDE上编写程序,随后点击编译和运行就看到程序执行的结果,但在学习了这门课过后,我知道了在一个程序从“出生”到“死亡”的一个基本历程,对计算机系统有了更加深入的理解,这对我进一步学习有关计算机的有关内容打下了基础。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
6、hello.i(.c经过预处理后的拓展)
7、hello.s(.i文件经编译后得到的汇编文本文件)
8、hello.o(汇编后得到的可重定位目标文件)
9、helloelf.elf(hello.o的elf格式)
10、hello(.o经链接后得到的可执行目标文件)
11、hello.elf(hello的elf格式)
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1]百度百科
[2]深入理解计算机系统(原书第三版)
[3]CSDN ELF文件详解-初步认识
https://blog.csdn.net/daide2012/article/details/73065204?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165303884116782184679246%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165303884116782184679246&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-73065204-null-null.142^v10^pc_search_result_control_group,157^v4^new_style&utm_term=elf&spm=1018.2226.3001.4187
(参考文献0分,缺失 -1分)