题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021110853
班 级 2103103
学 生 侯志一
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
人们一直认为程序是机械的,是冷漠的,可是你知道吗,哪怕是最简单的程序,也会有自己的生命周期。从诞生一刻,它经历自己的一生,从一个孩子到垂暮,最后完成自己的使命。深入理解计算机系统这本书中,作者用一个程序员的角度看待了一个程序的生命周期,本文我们将根据本书的知识,一步步介绍hello.c程序的运行周期,回顾一个程序经历的一生,从而加深你对程序的认识和计算机底层工作原理的了解。
关键词:计算机系统 进程 过程 抽象
目 录
第1章 概述
1.1 Hello简介
P2P过程:首先先有个hello.c的c程序文本,经过预处理->编译->汇编->链接四个步骤生成一个hello的二进制可执行文件,然后由shell新建一个进程给他执行。
020过程:shell执行程序,为其映射出虚拟内存,然后在开始运行进程的时候分配并载入物理内存,开始执行hello的程序,将其output的东西显示到屏幕,然后hello进程结束,shell回收内存空间。
1.2 环境与工具
硬件环境:X64 CPU Intel Core i7 6700HQ; 3.2GHz; 16G RAM; 1TB HD Disk
软件环境:Microsoft Windows10 Home 64位; VMware Workstation 14 Pro; Ubuntu 18.04
开发工具:gcc,readelf,edb
11.3 中间结果
文件名称 | 作用 |
Hello.c | 源代码 |
Hello.i | 预处理之后的文本文件 |
Hello.s | 编译之后的汇编文件 |
Hello.o | 汇编之后的可重定位目标文件 |
Hello | 链接之后形成的可执行目标文件 |
Hello.elf | 用readelf读取的ELF的信息 |
Hello.asm | 反汇编得到的汇编文件 |
1.4 本章小结
本章主要根据hello的自白,叙述了P2P过程,020过程,列出来了本次实验的硬件环境和软件环境,并展开了在实验中建立的中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念:预处理是计算机在处理一个程序时所进行的第一步,他直接对.c文件进行初步处理将处理后的结果保存在.i文件中,随后计算机再利用其它部分接着对.i文件进行处理。
2.1.2预处理的作用:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序的过程。预处理通常生成一个以.i作为文件扩展名的C程序。
我们一般认为,预处理有以下三种功能:宏定义、文件包含和条件编译。
宏定义:在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。
文件包含:文件包含命令的功能是把指定的文件插入到该命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件。使用文件包含指令可以节省时间并减少出错,方便后续处理。
条件编译:预处理程序提供了条件编译的功能。可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件
图2.1 C语言中的头文件
图2.2C语言中的宏定义
2.2在Ubuntu下预处理的命令
Ubuntu下预处理的命令如下:
gcc -E hello.c -o hello.i
图2.3生成hello.i
2.3 Hello的预处理结果解析
图2.4 hello.i中的末尾段代码
在ubuntu下用vim打开生成的hello.i文件,可以看到文件增至731行,但对于main函数的部分,代码并没有发生变化,因此可以推断,在预处理阶段,预处理器仅仅是对main函数之前的引用和声明做了改动,对于main函数本身以及之后的代码,预处理器对此并不感兴趣。
2.4 本章小结
本章主要对程序的预处理过程做了详细的介绍,并在Linux系统下,对hello.c文件进行预处理得到hello.i文件,并对其做了解析实验。预处理过程是计算机对程序进行操作的起始过程,在这个过程中预处理器会对hello.c文件进行初步的解释,对头文件、宏定义和注释进行操作,将程序中涉及到的库中的代码补充到程序中,将注释这个对于执行没有用的部分删除,最后将初步处理完成的文本保存在hello.i中,方便以后的内核器件直接使用。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念:编译是指编译器(ccl)通过词法分析、语法分析、语义检查和中间代码生成、代码优化以及目标代码生成这五个阶段来讲一个源程序翻译成目标程序的工作过程,总的来说,编译就是把代码转化为汇编指令的过程。
3.1.2编译的作用:编译程序的作用就是将高级程序语言源程序翻译为汇编语言程序,便于后续汇编与链接处理。此外,编译还可以进行错误分析,并给出提示信息;对程序中的代码进行优化。如果在语法语义等分析过后,不存在问题,编译器会生成一个过渡的代码,也就是汇编代码,在随后的步骤中,汇编器可以继续对生成的汇编代码进行操作。
3.2 在Ubuntu下编译的命令
Ubuntu下编译的命令如下:
gcc -S hello.i -o hello.s
图3.1 生成hello.s
3.3 Hello的编译结果解析
3.3.1汇编文件的结构分析
文件内容 | 含义 |
.flie | 源文件名称 |
.text | 代码段 |
.data | 存放已经初始化的全局变量和静态C变量 |
.rodata | 存放只读数据 |
.align | 对齐方式 |
.type | 表示类型 |
.size | 表示大小 |
表2 hello.s的结构
3.3.2数据类型
图3.2源程序代码
在源代码之中,我们构造了整型数组,局部变量和常量三种数据类型,接下来我们观察在hello.s中这些变量的表示方式。
- 局部变量:
图3.3 分配局部变量
从图中可以看出,对于局部变量,编译器选择在栈上为其分配空间。
- 整型数组:
图3.4 分配整型数组
从图中可以看出,对于整型数组,函数也是用栈为其分配空间,并在程序的运行过程中,逐步为每个数组成员赋值。
- 常量:
图3.5 常量的分配
从图中可以看出,对于常量,编译器并没有对其分配空间,而是简单的利用立即数代替它。
3.3.3各种操作
- 赋值操作:编译器直接利用mov指令对某个变量进行赋值操作。
- 关系操作:对于程序中的比较大小的操作,编译器利用comp指令对两个操作数进行大小比较,并利用结果对操作码进行设置。
图3.6 两个数的比较操作
3.数组操作
图3.7数组操作
从图中可以看出,对于数组元素,编译器先在栈上为其分配空间,需要访问数组元素时,在利用寄存器寻址的方式对栈中的数组进行访问。
3.5控制转移
图3.8控制转移
从图中可以看出,在程序中,由于用到for循环,所以需要条件转移,编译器利用comp指令和jle指令相结合,从而实现条件跳转。
4.函数调用
main函数
函数调用:由系统来调用,更准确地说,由execve函数来调用
函数返回:使用movl指令将%rax寄存器中的值置为0。
printf函数
图3.9 printf函数
参数传递:传入第二个字符串作为参数,通过引用地址和3.3.7中访问数组操作传递参数
Aoti函数
图3.10 aoti函数
Sleep函数
图311 sleep函数
对于函数调用,需要call指令和ret指令配合完成,call指令负责将返回地址压栈,并将PC的值跳转为调用函数的第一条指令的地址,ret指令则将返回地址弹出到PC寄存器中,从而完成函数调用的返回。在函数调用过程中,有六个寄存器负责传递参数,参数数量超过六个需要用栈来传递
3.4 本章小结
本章我们主要介绍了编译器是如何将文本编译成汇编代码的。可以发现,编译器并不是死板的按照我们原来文本的顺序,逐条语句进行翻译下来的。编译器在编译的过程中,不近会对我们的代码做一些隐式的优化,而且会将原来代码中用到的跳转,循环等操作操作用控制转移等方法进行解析。最后生成我们需要的hello.s文件。
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念:汇编是指汇编器(as)将.s结尾的汇编程序翻译成机械语言指令,把这些指令打包成可重定位目标程序格式,并将结果保存在.o二进制目标文件中的过程。
4.1.2汇编的作用:汇编代码也只是我们人可以看懂的代码,而机器并不能读懂,真正机器可以读懂并执行的是机器代码,也就是二进制代码。汇编的作用就是将我们之前再hello.s中保存的汇编代码翻译成可以攻机器执行的二进制代码,这样机器就可以根据这些01代码,真正的开始执行我们写的程序了。
4.2 在Ubuntu下汇编的命令
Ubuntu下汇编的命令如下:
gcc -c hello.s -o hello.o
图4.1 hello.o的生成
4.3 可重定位目标elf格式
图4.2 ELF header中的信息
- ELF Header
以16字节序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,其中7f 45 4c 46为固定的魔法字节,02表示64位,第一个01表示小端序,第二个01表示ELF头版本。ELF头剩下的部分包括帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可重定位的)、机器类型、节头部表的文件偏移。以及节头目表中条目的大小和数量。
- 重定位节.rela.text
一个.text节中位置的列表,存放着代码的重定位条目,其类型为 RELA,也就是说它是一个重定位表(也叫作重定位段),当链接器把这个目标文件和其他文件组合时,需要修改这些位置。在此重定位表中,每个要被重定位的地方叫重定位入口(Relocation Entry),我们可以看到每个重定位入口在段中的偏移位置,重定位入口的类型,重定位入口的名称以及重定位修正的辅助信息等。
3.重定位节.rela.eh_frame
.rela.eh_frame节同.rela.eh_frame一样属于重定位信息的节,包含的是eh_frame的重定位信息
4.符号表Symbol table .symtab
符号表中存放着在程序中定义和引用的函数和全局变量的信息,与编译器的符号表不同,.symtab符号表不包含局部变量的条目。
图4.3 符号表
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
反汇编得到的文件左边比hello.s多了机器语言,而机器语言实际上就是对汇编语言的编码,每条汇编代码都具有唯一的机器编码。就二者之间的差异更具体地分析如下:
图4.4 反汇编的结果
从图中可以清晰的看出机器代码和汇编语言之间的对应关系,从中可以发现以下不同:
1.操作数:机器语言和反汇编语言中的操作数都是十六进制的,而汇编语言是十进制的。
2.分支转移:在hello.s中,跳转指令的目标地址为段名称,如.L2、.L3;在反汇编代码中,跳转指令的目标地址为具体的地址;在机器代码中为目标指令地址与下一条指令地址的差值。
3.函数调用:在hello.s中,对函数的调用是直接在call指令后边跟函数名,而在反汇编代码中,对函数的调用的目标地址都是当前指令的下一条指令的地址。这是因为,hello.c中调用的函数都是共享库中的函数,需要通过动态链接器作用才能确定函数在运行时的地址,所以在汇编的过程中,将call后边的目标地址设置为下一条指令的地址,并且在调用函数后边生成了一个重定位条目,来告诉链接器对该函数的引用要使用函数名前边的重定位类型来进行重定位(其中最主要的两个类型是,R_X86_64_PC32重定位一个使用32位PC相对寻址的引用和R_X86_64_32重定位一个使用32位绝对寻址的引用)
4.5 本章小结
本章介绍了汇编的概念与作用,并且在Linux下将hello.s文件汇编为hello.o文件,使用readelf指令生成了可重定位目标文件的elf格式并对其具体结构进行了研究。此外,通过对比hello.o的反汇编代码与hello.s中的汇编代码,对hello.o机器代码做了进一步解析,了解了汇编语言与机器语言的相同与差异。二进制代码中,已经将所有的指令、函数名字等量变成了相应的存储地址,这样机器就可以直接读取这些代码并执行。所以总的来说hello.o已经非常接近一个机器可以执行的代码了。
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。这个过程是有叫做链接器的程序执行的。
5.1.2链接的作用: 因为有了链接这个概念的存在,所以我们的代码才回变得比较方便和简洁,同时可移植性强,模块化程度比较高。因为链接的过程可以使我们将程序封装成很多的模块,我们在变成的过程中只用写主程序的部分,对于其他的部分我们有些可以直接调用模块,就像C中的printf一样。作为编译的多后一步链接,就是处理当前程序调用其他模块的操作,将该调用的模块中的代码组合到相应的可执行文件中去。同时在链接的过程中,也可以使用静态库中的各种函数和生命。
5.2 在Ubuntu下链接的命令
Ubuntu下链接的命令如下:
Ld-ohello-dynamic-linker/lib64/ld-linux-x86-64.so.2/usr/lib/x86_64-linux-gnu/crt1.o/usr/lib/x86_64-linux-gnu/crti.ohello.o/usr/lib/x86_64-linux-gnu/libc.so/usr/lib/x86_64-linux-gnu/crtn.o
图5.1链接的过程
5.3 可执行目标文件hello的格式
1.ELF Header以16字节序列Magic开始,这个序列描述了生成该文件的系统 的字的大小和字节顺序,其中7f 45 4c 46为固定的魔法字节,02表示64位 ,第一个01表示小端序,第二个01表示ELF头版本。ELF头剩下的部分包 括帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目 标文件的类型(可重定位的)、机器类型、节头部表的文件偏移。以及节头目 表中条目的大小和数量。
图5.2 ELF header的内容
- 节头:图5.3中保存了可执行文件hello中的各个节的信息。可以看到hello 文件中的节的数目比hello.o中多了很多,说明在链接过后有很多文件有添加 了进来。下面列出每一节中各个信息条目的含义:名称和大小这个条目中存储 了每一个节的名称和这个节在重定位文件种所占的大小。地址这个条目中,保 存了各个节在重定位文件中的具体位置也就是地址。偏移量这一栏中保存的是 这个节在程序里面的地址的偏移量,也就是相对地址。
图5.3节头的内容
5.4 hello的虚拟地址空间
图5.4中分别是edb显示的hello文件的信息和5.3中输出的ELF文件的信息。我们可以着重看一下红框中的部分,这一部分中存储的ELF头信息,也就是ELF文件最开始存的数据。可以看到通过这两种方式得到的信息是完全相同的。再来说一下这个信息的含义,我们看到右侧的ELF头下面有很多文字注释,这其实就是ELF头中存储的信息,即ELF整个文件的基
图5.4 虚拟地址空间
5.5 链接的重定位过程分析
1.合并相同的部分:链接器首先将同一类型的所有部分合并到相同类型的新部分,例如,所有文件的 .data 部分合并到一个新的 .data 部分,合并完成后,新部分是可执行文件 hello 的 .data 部分。
- 确定地址:然后,链接器将内存地址分配给新的聚合部分以及输入模块定义的部分和符号。确定地址后,全局变量、指令等都具有唯一的运行时地址判断输入文件是否为库文件,如果不是则是目标文件f,目标文件放入集合E中。
- 链接器解析目标文件中的符号,如果它们出现,则将它们放在集合 U 中,如果它们看起来已定义但未使用,则将它们设置为集合 D 中链接器读入crt*库1中的目标文件
- 接入动态链接库libc.so
图5.4 得到的反汇编代码
5.6 hello的执行流程
程序名称 | 程序地址 |
hello!_start | 0x00000000004010f0 |
hello!__libc_csu_init | 0x0000000000401270 |
hello!_init | 0x0000000000401000 |
hello!frame_dummy | 0x00000000004011d0 |
hello!register_tm_clones | 0x0000000000401160 |
hello!main | 0x00000000004011d6 |
hello!printf@plt | 0x0000000000401040 |
hello!atoi@plt | 0x0000000000401060 |
hello!sleep@plt | 0x0000000000401080 |
hello!getchar@plt | 0x0000000000401050 |
hello!exit@plt | 0x0000000000401070 |
hello!__do_global_dtors_aux | 0x00000000004011a0 |
hello!deregister_tm_clones | 0x0000000000401130 |
hello!_fini | 0x00000000004012e8 |
表3:程序中各个函数的地址
5.7 Hello的动态链接分析
图5.5 edb中的动态链接
首先,动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才形成一个完整的程序。而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件,但在形成可执行文件的时候,还是会用到动态链接库,如果此时发现引用了一个外部函数,就会检查动态链接库,找到动态链接符号,但并不对符号进行重定位,直到程序执行过程中装载时再进行动态链接器使用过程链接表PLT+全局偏移量GOT实现函数的动态链接。每个被可执行程序调用的库函数都有它自己的PLT条目,每个条目负责调用一个具体的函数。GOT包含动态链接器解析函数地址时会使用的信息。加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。
5.8 本章小结
本章介绍了链接的概念与作用,根据链接后得到的可执行文件的ELF格式进行了结构分析,对比可重定位目标文件的ELF分析了不同的类型信息;然后通过使用edb调试hello程序,分析了hello程序的虚拟地址空间,验证了链接的过程,清楚了hello的执行流程,并对其动态链接做了分析,加深了对可执行文件执行过程与动态链接的理解。
(以下格式自行编排,编辑时删除)
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念:进程就是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。
6.1.2进程的作用:
进程提供给应用程序两个关键抽象:
1.一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器
2.一个私有地址空间,提供一个假象,好像程序独占地使用内存系统
6.2 简述壳Shell-bash的作用与处理流程
shell是一个linux中提供的应用程序,他在操作系统中为用户与内核之间提供了一个交互的界面,用户可以通过这个界面访问操作系统的内核服务。他的处理流程如下:
1.从界面中读取用户的输入。
2.将输入的内容转化成对应的参数。
3.如果是内核命令就直接执行,否则就为其分配新的子进程继续运行。
4.在运行期间,监控shell界面内是否有键盘输入的命令,如果有需要作出相应 的反应
6.3 Hello的fork进程创建过程
6.3.1 fork函数的机制:父进程通过调用fork函数创建一个新的子进程。新创建的子进程几乎但不完全与子进程相同。在创建子进程的过程中,内核会将父进程的代码、数据段、堆、共享库以及用户栈这些信息全部复制给子进程,同时子进程还可以读父进程打开的副本。唯一的不同就是他们的PID,这说明,虽然父进程与子进程所用到的信息几乎是完全相同的,但是这两个程序却是相互独立的,各自有自己独有的用户栈等信息。 fork函数虽然只会被调用一次,但是在返回的时候却有两次。在父进程中,fork函数返回子进程的PID;在子进程中,fork函数返回0。
图6.1fork函数与进程
6.3.2 Shell与fork:每次用户通过向Shell输入一个可执行目标文件的名字,运行程序时,Shell就会调用fork创建一个新的进程,然后再这个新进程的上下文中运行这个可执行目标文件。
6.4 Hello的execve过程
execve函数的原型为:
int execve(const char *filename,const charargv[],const char envp[])
execve函数的作用是在当前进程的上下文中加载并运行一个新的程序。与fork函数不同的是,fork函数创建了一个新的进程来运行另一个程序,而execve直接在当前的进程中删除当前进程中现有的虚拟内存段,并穿件一组新的代码、数据、堆和用户栈的段。将栈和堆初始化为0,代码段与数据段初始化为可执行文件中的内容,最后将PC指向_start的地址。在CPU开始引用被映射的虚拟页的时候,内核才会将需要用到的数据从磁盘中放入内存中。
6.5 Hello的进程执行
6.5.1进程相关概念介绍:
用户模式和内核模式:处理器通过使用控制寄存器的模式位实现限制应用程序可以执行的指令数和可以访问的地址空间范围的功能。此寄存器描述进程当前享有的权限,当设置了模式位时,进程在内核模式下运行,在内核模式下运行的进程可以执行指令集的任何指令,并且可以访问系统中的任何内存位置。当模式位未设置时,进程在用户模式下运行,并且不允许用户模式下的进程执行特权指令,例如停止处理器,更改模式位,并且不允许进程直接引用地址空间中内核区域中的代码片段和数据,此时用户程序必须通过系统调用接口间接访问内核代码和数据。
控制流:从上电时间到断点位置计算的PC值序列,程序计数器称为控制流。
逻辑控制流:使用调试器单步执行程序时,您会看到一系列程序计数器 (PC) 值,这些值与程序的可执行对象文件中包含的指令唯一对应,或者包含在运行时动态链接到程序的共享对象中。此 PC 值序列称为逻辑控制流,或简称为逻辑流。也就是说,逻辑控制流是进程中的一系列 PC 值。
进程上下文:上下文是内核重新启动抢占进程所需的状态,它由对象的值组成,例如通用寄存器,浮点寄存器,程序计数器,用户堆栈,状态寄存器,内核堆栈和各种内核数据结构。
6.5.2 程序进程执行:
在程序运行时,shell父进程首先生成一个子进程,进入待执行的。在Hello的进程执行过程中,首先在shell中输入命令加载可执行目标文件hello,经过一次上下文切换,切换的过程中处于内核状态,切换后内核代表进程hello在用户模式下运行,直到磁盘发出中断信号,执行从hello到进程A的上下文切换,将控制权返回给进程A,进程A继续运行,直到下一次异常发生。
图6.2进程上下文切换
6.6 hello的异常与信号处理
1.在程序执行时按下Ctrl-z:程序被挂起。
图6.3 在程序运行时按下Ctrl-z
2.利用ps查看正在运行的进程
图6.4 利用ps命令查看正在运行的进程
3.利用fg命令恢复挂起的进程
图6.5 利用fg命令
4.在进程运行时使用Ctrl-c命令
图6.6 在进程运行时使用Ctrl-c命令
5.利用kill指令杀死一个进程
图6.7 用kill指令杀死进程
6.7本章小结
本章介绍了进程的概念与作用,了解了Shell-bash的作用与执行流程,结合Shell-bash下对于hello的执行,研究了fork函数创建子进程的过程、execve函数的执行过程以及各种异常与信号处理的过程的结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1地址概念的介绍:
逻辑地址:逻辑地址指由程序产生的段内偏移地址。逻辑地址与虚拟地址二者之间没有明确的界限。在hello中,逻辑地址为hello.asm中的相对偏移地址。
线性地址:指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。逻辑地址,或者说段中的偏移地址,加上相应段基址就成了一个线性地址。在hello中,线性地址标志着hello应在内存上哪些具体数据块上运行。
虚拟地址:是由程序产生的由段选择符和段内偏移地址组成的地址。这两部分组成的地址并不能直接访问物理内存,而是要通过分段地址的变化处理后才会对应到相应的物理内存地址。
物理地址:指内存中物理单元的集合,地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址来存取主存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。
图7.1段选择符
索引号是“段描述符(segment descriptor)”的索引,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,每一个段描述符由8个字节组成。其中Base字段,它描述了一个段的开始位置的线性地址,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中,由段选择符中的T1字段表示选择使用哪个,=0,表示用GDT=1表示用LDT。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基 址与偏移量结合就得到了线性地址,虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行 分页的分页机制完成。通过段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和 VPO(虚拟页偏移量),根据计算机系统的特性可以确定 VPN 与 VPO 的具体位数,由于虚拟内存与物理内存的页大小相同,因此 VPO 与 PPO(物理页偏移量)一致。而 PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取。若 PTE 的有效位为 1,则发生页命中,可以直接获取到物理页号PPN,PPN 与 PPO 共同组成物理地址。若 PTE 的有效位为 0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到 PPN,与 PPO共同组成物理地址。
图7.2页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1相关概念介绍:
TLB的概念:具体是什么东西。我们注意到,每次在进行虚拟地址翻译的过程中都会有访问PTE的操作,如果在比较极端的情况下,就会存在访存的操作,这样的效率是很低的。TLB的运用,就可以将PTE上的数据缓存在L1中,也就是TLB这样一个专用的部件,他会将不同组中的PTE缓存在不同的位置,提高地址翻译的效率。
多级页表的概念:在前面我们了解了一级页表是如何进行工作的。可以发现一级页表有一个弊端,就是对于每一个程序,内核都会给他分配一个固定大小的页表,这样有一些比较小的程序会用不到开出的页表的一些部分,就造成了空间的浪费,多级页表就很好的解决了这个问题。以二级页表为例,首先我们先开一个比较小的一级页表,我们将完整的页表分组,分别对应到开出来的一节页表的一个PTE中,在执行程序的过程中,如果我们用到了一个特定的页表,那么我们就在一级页表后面动态的开出来,如果没用到就不开,这样就大大的节省了空间。
7.4.2转换过程:知道了上述概念之后,我们就来看一下虚拟地址是如何在四级页表中转换的。如图 7-6,CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT(前32位+TLBI(后4位)向TLB中匹配,如果命中,则得到 PPN (40bit)与VPO(12bit)组合成 PA(52bit)。如果TLB中没有命中,MMU 向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN,与VPO组合成PA,并且向TLB 中添加条目。
图7.3页表翻译过程
7.5 三级Cache支持下的物理内存访问
在获得了物理地址VA之后,我们接着图7-6的右侧部分进行说明。使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配 CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。 如果没有匹配成功或者匹配成功但是标志位是 1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略 LFU 进行替换
7.6 hello进程fork时的内存映射
当 fork 函数被当前进程 hello 调用时,内核为新进程 hello 创建各种数据结构,并分配给它一个唯一的 PID。为了给这个新的 hello 创建虚拟内存,它创建了当前 进程的 mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当 fork 在新进程中返回时,新进程现在的虚拟内存刚好和调用 fork 时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效的替代了当前程序。加载并运行hello需要以下几个步骤:
删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。图7.7 概括了私有区域的不同映射。
映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页现象的发生是由于页表只相当于磁盘的一个缓存,所以不可能保存磁盘中全部的信息,对于有些信息的查询就会出现查询失败的情况,也就是缺页。对于一个访问虚拟内存的指令来说,如果发生了缺页现象,CPU就会触发一个缺页异常。缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,例如图7-7中存放在PP3中的VP4,如果VP4已经被更改,那就先将他存回到磁盘中。
找到了要存储的页后,内核会从磁盘中将需要访问的内存,例如图7-7所示的VP3放入到之前已经操作过的PP3中,并且将PTE中的信息更新,这样就成功的将一个物理地址缓存在了页表中。当异常处理返回的时候,CPU会重新执行访问虚拟内存的操作,这个时候就可以正常的访问,不会发生缺页现象了。
7.9动态存储分配管理
动态内存管理的基本方法与策略介绍如下:
动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已 分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
7.10本章小结
本章主要介绍了 hello 的存储器地址空间、intel 的段式管理、hello 的页式管 理, VA 到 PA 的变换、物理内存访问,hello 进程 fork、execve 时的内存映射、 缺页故障与缺页中断处理、动态存储分配管理。
结论
Hello的一生
- 源程序阶段:程序员为hello编写了代码,hello程序从此诞生,带着程序 员的愿望和期许,hello来到这个世界,向全世界问好。
- 预处理阶段:预处理器将程序中的宏定义和文件包含解析并生成hello.i文 件,在这个阶段,hello成长了起来,这个时候的hello已经有了可以面对后 续经历的底气和条件。
- 编译阶段:编译器将C语言翻译为汇编语言并生成hello.s,这个阶段hello 完成了自己的变形,开始向计算机的底层进发。
- 汇编阶段:汇编器将hello.s翻译为机器指令,并生成相应的重定位信息, 生成可重定位目标文件hello.o.在汇编阶段之后,hello已经完全将自身融 入了二进制世界,向着最后的自己做出改变。
- 链接阶段:链接器进行符号解析,重定位,动态链接过程,将多个可重定 位目标文件合成为一个可执行目标文件,到达这一步,hello程序已经 可以在计算 机上成功运行了。
- Shell的工作:shell进程调用fork函数创建子进程,并未每个进程分配相 应的PID。
- 运行程序:子进程调用execve函数,execve函数调用启动器,并未程序分 配虚拟内存,进入程序入口之后程序开始载入物理内存,然后进入main 函数。
- 执行指令:CPU为每个进程分配时间片,hello在自己的时间片之中,顺 序执行自己的逻辑控制指令。
- MMU将程序中的虚拟地址通过页表管理翻译为物理地址。
- 信号处理:在运行过程中,如果hello收到了一些信号,需要调用信号处 理子程序处理信号之后,在返回到hello的进程中继续执行。
- 结束进程:shell父进程回收子进程,内核删除围着进程创建的所有数据结 构,hello完成了自己的使命,结束了自己的一生。Shell也在继续等待着 下一个程序的诞生和到来。
我对计算机的深切感悟:
计算机系统是计算机专业中可以说是最底层的东西,却也是万物的基础。 没有汇编语言的阅读基础,我们永远也不能知道C语言转换为机器语言之后是 什么样子的,我们也永远不能知道与我们朝夕相处的计算机究竟在了解着什么 样的语言。没有链接这项技术,我们的代码似乎永远都会是冗长的,难以编译 与理解的。没有进程管理,我们的电脑或许总是一团糟,RAM内存似乎可 能永远都会不够用。没有I/O管理,纵然再优秀的程序,我们都无法与其形成 真正意义上的交互。
感谢hello这短暂却又璀璨的一生,巩固了我这学期学习计算机系统的大 部分知识与脉络,更重要的是教会了我学计算机要永远从底层,从原理理解所 有发生的事情,这样才能真正学会计算机,学懂计算机。
计算机系统高效有序的运行离不开底层硬件的完美契合,计算机多级存储 结构、内核对进程的调度策略、动态链接的执行方式、cache替换策略、页表 替换策略、异常与信号等的处理……这些无一不体现计算机底层实现的完备与 优雅。同时,作为程序员,了解与学习计算机底层实现也有助于我们充分利用 计算机,编写出计算机底层友好的代码,提高计算、工作的效率。
总之,hello的故事,其实才刚刚开始……
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名称 | 作用 |
Hello.c | 源代码 |
Hello.i | 预处理之后的文本文件 |
Hello.s | 编译之后的汇编文件 |
Hello.o | 汇编之后的可重定位目标文件 |
Hello | 链接之后形成的可执行目标文件 |
Hello.elf | 用readelf读取的ELF的信息 |
Hello.asm | 反汇编得到的汇编文件 |
参考文献
- 姚惠慧."计算机系统体系结构层次设计关键性技术综述." 电脑知识与技术 18.21(2022):120-122. doi:10.14004/j.cnki.ckt.2022.1338.
- 朱冲, 陈雪飞,and 张聪品."编译原理中语法分析探讨及其应用." 福建电脑 31.06(2015):148-149. doi:10.16707/j.cnki.fjpc.2015.06.036.
- 林玉哲, 蒋金虎,and 张为华."多内核操作系统综述." 计算机系统应用 31.05(2022):21-29. doi:10.15888/j.cnki.csa.008426.
- 庞新法."C语言与汇编语言混合编程声明规则." 价值工程 33.26(2014):235-236. doi:10.14018/j.cnki.cn13-1085/n.2014.26.479.
- 伍永利."计算机组成原理与设计." 当代旅游(高尔夫旅行) .02(2018):143.
- 李剑.嵌入式多核代码分析器研究与实现.2014.电子科技大学,MA thesis.
(参考文献0分,缺失 -1分)