本文以hello.c这个最简单的程序为切入点,以其为例介绍了linux系统下一个进程的生命周期。全文大致可分为三个阶段,P2P和020,存储管理。P2P过程中,hello.c经过预处理、编译、汇编、链接后,得到可执行文件并运行,这就是其从program到process的过程;020即为从电脑内没有此进程到没有此进程的一个阶段,涵盖了进程创建和进程终止,暂停的相关内容;存储管理即为计算机的hello相关的地址空间,存储结构相关解析。了解这些内容,会对Linux系统下一个程序完整的生命周期有一个更深刻的认识。
关键词:计算机系统,程序生命周期,linux,虚拟内存
目 录
第1章 概述
1.1 Hello简介
针对Hello的P2P以及020两个流程,做出如下介绍。
Hello的P2P指的是其从program到process的整个流程。首先,由程序员编写程序,得到hello.c,即这个流程中的program,随后,开始调用gcc编译器驱动程序,来读取hello.c文件,来把它翻译成一个可执行目标文件(即hello(linux系统下无后缀名)),而这个过程可以分为四个阶段完成:首先,由预处理器(cpp)对hello.c进行处理,得到修改了的源程序(文本),即hello.i;随后,由编译器(ccl)对hello.i进行处理,得到汇编程序(文本),即hello.s;随后,由汇编器(as)对hello.s进行处理,得到可重定位目标程序(二进制),即hello.o;随后,由连接器(ld),对hello.o和printf.o(在hello.c程序中调用的函数printf所在的目标文件)进行处理,得到可执行的目标程序(二进制),即hello。随后,在shell中执行命令./hello,shell即会fork一个新的子进程,运行hello,即得到一个进程(process)。综上,即为hello的P2P流程。
Hello的020指的是其从0到0的一个流程。从一开始,电脑的磁盘和内存中都没有Hello相关的内容,随后,运行./hello,shell首先fork出一个子进程,随后调用execve函数,运行hello.c中的代码,有hello的子进程在内存中运行,随后,在进程运行完成后,hello进程结束并被回收,此时,内存中不再有相关内容。
1.2 环境与工具
硬件: AMD Ryzen 9 5900HX with Radeon Graphics 3.30 GHz
机带RAM 16GB
1T SSD + 512G SSD
软件: Windows 11 家庭中文版
Ubuntu 22.04.1 LTS 64位
调试工具:Visual Studio Code 1.77.2
Visual studio 2019 64-bit
gcc、readelf、objdump、edb
1.3 中间结果
文件的名字 | 文件的作用 |
hello.i | 预处理所得,修改了的源程序(文本) |
hello.s | 编译所得,汇编程序(文本) |
hello.o | 汇编所得,可重定位目标程序(二进制) |
hello_o_elf.txt | hello.o的elf格式 |
hello_o_asm.txt | hello.o的反汇编代码 |
hello | 链接所得,可执行的目标程序(二进制) |
hello_elf.txt | hello的elf格式 |
hello_asm.txt | hello的反汇编代码 |
1.4 本章小结
本章围绕着本文的主题,即Hello的p2p和020所执行的流程进行了介绍,同时针对撰写本文时所使用的软硬件环境做了介绍,并整体性地整理并介绍了流程中产生的中间结果及其作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是gcc编译器驱动程序根据读取的.c文件进行处理的第一个步骤,在这个步骤中,由预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中的第一行(不包括注释),其内容为#include<stdio.h>,该命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。最后会得到另外一个C程序,其拓展名一般为.i,此处得到的即为hello.i文件。
2.1.2 预处理的作用
预处理过程的作用是对原代码文件中的文件包含 (include)、预编译语句 (如宏定义define等)进行分析和展开,生成 .i 文件。这样可以把头文件的代码、宏之类的内容转换成更纯粹的C代码,方便后续的编译和链接。
2.2在Ubuntu下预处理的命令
以提供的hello.c文件为例,预处理命令为 gcc -E hello.c -o hello.i
图 1预处理命令及其结果
2.3 Hello的预处理结果解析
用记事本打开hello.c和hello.i
图 2原代码及预处理后的修改过的原代码比对
观察可知,hello.i的代码量有3086行,而hello.c只有23行,主函数部分两文件之间内容没有区别,且都位于文件末尾。而hello.c中所写的注释在hello.i中被删去,同时,hello.c之中include的stdio.h,unistd.h,stdlib.h被替代为对应系统头文件中的代码并被直接插入到了文本文件之中,有图如下。
图 3修改后的原代码部分内容
同时可以看到,除了上述三个系统头文件,还插入了如"/usr/lib/gcc/x86_64-linux-gnu/11/include/stddef.h"的内容,此处是插入进了被插入进来的系统头文件的系统头文件,也就是被上述三个系统头文件所引用的其他文件,一个类似于套娃的过程,重复这样插入,直到文件内没有引用其他文件,以方便后续流程的进行。
2.4 本章小结
在本章中,针对gcc编译流程中的预处理一步的概念和作用作了介绍,介绍了ubuntu下执行预处理的命令,并围绕hello.c和hello.i进行比对,针对预处理一步所得到的结果作了解析。预处理过程中会根据#开头的命令,针对源程序文件进行各种对应处理,并删去.c文件中的注释。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
汇编语言是一种接近机器码的语言,每种CPU架构都有不同的汇编语言。而此处的编译指的就是编译器(ccl)把预处理后得到的修改后的源程序文件处理后进行翻译,生成对应的汇编语言文件(文本),通常以.s为后缀,此处得到的即是hello.s。
3.1.2 编译的作用
将修改后的原代码文件进行词法分析、语法分析、语义分析和优化,转换成一种更接近机器码的语言,为后续的汇编和链接做准备。
3.2 在Ubuntu下编译的命令
以提供的hello.c文件为例,编译命令为 gcc -S hello.i -o hello.s
图 4编译命令及其结果
3.3 Hello的编译结果解析
3.3.1 数据
一.数字常量
阅读原hello.c代码,可以看到有两处出现了数字常量。
图 5原代码文件中出现第一处数字常量
其一为此处,用作if判断的条件,将argc与数字常量4作比较。
图 6汇编文件中的该数字常量
可以看到,此处数字常量4以立即数的形式出现,与%rbp所存地址负向偏移20字节后所存内容作比较,与原代码相对应。
图 7原代码文件中出现第二处数字常量
其二为此处,用作for判断的条件,与整型变量i作比较,若i小于8(即小于等于7)则继续执行下去。
图 8汇编文件中的该数字常量
可以看到,此处该数字常量出现的形式与原代码不同,经过编译器优化后,以立即数7的形式出现,判断条件也改为小于等于,满足条件则执行L4,即循环内的代码。
- 字符串常量
在源程序文件hello.c内的文件中有着两个字符串常量。
图 9原代码中出现的两处字符串常量
该字符串即为printf函数需要打印出来的内容,即为“用法: Hello 学号 姓名 秒数!\n”以及“"Hello %s %s\n"”。
图 10汇编文件中出现的该字符串常量
可以看见,在rodata段内,存在这两个字符串常量(中文在此处以UTF-8编码的形式保存)。
- 局部变量
阅读源程序文件hello.c,可以发现存在三个局部变量。
图 11原代码中出现的三个局部变量
三个局部变量分别为int argc和int i以及char* argv[]。为使程序运行时节省从内存读取数据的时间,局部变量会被存在.bss或.data段内,并在寄存器或栈内参与运算。
图 12int argc在汇编文件中
可以看见,int argc会被存在%rsi内,并在执行时装入-20(%rbp),并与立即数4作比较,这与原代码相对应。
图 13int i在汇编文件中
可以看见,首先,在L2中,有立即数0装入-4(%rbp)内,并在后续运算中,此处数值不断执行与立即数1相加和与立即数7比较的操作。因此,此处所存的数值所对应的应当就是原代码中的int i,其被存入了栈内,并参与后续运算。
图 14char* argv[]在汇编文件中
可以看见,该变量同样储存在栈内,且其首地址为%rbp-$32,则有argv[0]=-32(%rbp),argv[1]=-32(%rbp)+$8,argv[2]=-32(%rbp)+$16,以此类推。并且argv[1]和argv[2],argv[3]会存储在%rax和%rdx,%rdi内参与后续运算。
3.3.2 幅值
在原代码程序内存在一处赋值。
图 15原代码程序内的赋值
即将0赋值给int i这个局部变量。
图 16该赋值操作在汇编文件内对应内容
据上文可得知,int i存于栈内,即-4(%rbp)对应的就是i,则此处操作即为如上的为i赋值为0。
3.3.3 类型转换
在原代码文件内存在函数atoi(),这是stdlib.h标准库内的函数,功能为将一个字符串转换为整型量。
图 17在源程序文件中引用的atoi函数
图 18该类型转换在汇编文件内对应内容
据上文可知,-32(%rbp)+$24即为argv[3],此处即为将这个字符数组取出,存入%rdi内,然后调用函数atoi,函数将该字符串数组转换为整型量,并将结果存在%eax内,结果后续存入%edi内,作为函数sleep的传入参数,与原代码内容对应。
3.3.4 算术操作
在原代码中存在一处算术操作。
图 19在源程序文件中存在的算术操作
图 20该算术操作在汇编文件内对应内容
即在原代码中执行了i++,即对int i,有i=i+1这个操作,据上文可知,int i在汇编文件中存在栈内,即为-4(%rbp),针对该内容,执行了+1的操作并存入-4(%rbp)中,与原代码内容相对应。
3.3.5 关系操作
在原代码文件内存在两次比较操作。
图 21在源程序文件内的第一次关系操作
图 22该关系操作在汇编文件内对应内容
可以看见,此处关系操作即将argc与4相比较,判断是否相等。据上文可知,int argc存在栈内,即为-20(%rbp),将此处内容与立即数4相比,若相等则跳转,与源程序内容相对应。
图 23在源程序文件内的第二次关系操作
图 24该关系操作在汇编文件内对应内容
可以看见,此处关系操作即将i与8相比较,判断是否小于,这个操作等价于将i与7相比,判断是否小于等于。同时,据上文可知,int i存在栈内,即为-4(%rbp),将此处内容与立即数7相比较,若小于等于则跳转,与源程序内容相对应。
3.3.6 数组/指针/结构操作
在源程序内,存在一处数组操作。
图 25在源程序文件内存在的数组操作
图 26该操作在汇编文件内对应内容
据上文可知,char* argv[]的首地址为-32(%rbp),则此处对应的数值即为argv[0],而后续的-32(%rbp)+$8即为argv[1],则可知,argv[n]即为-32(%rbp)+n*8,因此,在汇编内的数组操作即为找到数组的首地址之后通过偏移若干个sizeof(所存元素类型)所实现。
3.3.7 控制转移
在源程序内,存在两处控制转移。
图 27在源程序内的第一次控制转移
图 28该控制转移在汇编文件内对应内容
即为对argc内的内容进行判断,若其等于4则不执行if下的内容,等于4则反之。针对汇编内容,据上文可知,-20(%rbp)内容即为argc,若其与4不等,则不跳转,执行printf等操作,与源程序相对应。
图 29在源程序内第二次控制转移
图 30该控制转移在汇编文件内对应内容
即为对i与8进行比较,若其小于8(即小于等于7)则执行for内的内容,据上文可知,-4(%rbp)内的内容即为int i,其与立即数7比较,若小于等于则执行L4内的内容,与源程序相对应。
图 31在汇编文件内第三次控制转移
除了以上两次控制转移外,在汇编文件内还有第三次控制转移,这是无条件跳转,第一次控制转移后跳转至此处,对-4(%rbp),即int i赋0后,跳转至第二次控制转移处。
3.3.8 函数操作
在源程序内,存在6次函数操作。
图 32在源程序内第一次函数操作
图 33该函数操作在汇编文件内对应内容
源程序即为将一个字符串通过函数printf打印出来,在汇编文件内,将需要打印的.LC0字符串存入%rdi中,将%rdi内的内容传入函数puts,此处编译器做了优化,使用函数puts代替了函数printf。
图 34在源程序内第二次函数操作
图 35该函数操作在汇编文件内对应内容
源程序即为将1作为参数传入给函数exit,退出程序并返回1给操作系统。汇编内容中将立即数传入给%edi,并以其为传入参数,调用函数exit,与源程序相对应。
图 36在源程序内第三次函数操作
图 37该函数操作在汇编文件内对应内容
源程序即为将一个字符串通过函数printf打印出来,且这个字符串中含有字符串变量argv[1]和argv[2]。针对汇编文件,据上文可知,argv[1]和argv[2]这两个参数位于-32(%rbp)+$8和-32(%rbp)+$16,将其传入%rax和%rsi内,并将字符串存入%rdi内,以这三个寄存器内容为传入参数,调用函数pritnf,打印对应内容。
图 38在源程序内第四、五次函数操作
图 39该两次函数操作在汇编文件内对应内容
在源程序中即为将argv[3]内容传入函数atoi,即将字符串转换为整型量,并将返回值传入函数sleep中。而在汇编文件中,据上文可知,argv[3]对应内容为-32(%rbp)+$24,将其传入%rdi中,作为传入参数调用函数atoi,而atoi的返回值存入%eax中,随后又将其存入%edi中,进一步作为函数sleep的传入参数。
图 40在源程序内第六次函数操作
图 41该函数操作在汇编文件内对应内容
在源程序中是直接调用函数getchar,在汇编文件内也是如此。
3.4 本章小结
在本章中,针对gcc编译流程中的编译一步的概念和作用作了介绍,介绍了ubuntu下执行编译的命令,同时针对C语言中的数据与操作,针对源程序文件和汇编文件进行了比对与分析,其中数据类型包括数字常量、字符串常量和局部变量;操作类型包括赋值、类型转换、算术操作、关系操作、数组\指针\结构操作控制转移以及函数调用。整体来看,相对于源程序文件,汇编文件对其是继承与优化关系。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指汇编器(as)将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o文件中的过程。此处.s文件为hello.s,.o文件为hello.o。
4.1.2 汇编的作用
汇编阶段的作用是将文本形式的汇编语言转换成CPU能够直接执行的二进制的指令,为后续的链接做准备。
4.2 在Ubuntu下汇编的命令
以提供的hello.c文件为例,汇编命令为 gcc -c hello.s -o hello.o
图 42汇编命令及其结果
4.3 可重定位目标elf格式
首先,在shell中输入命令readelf -a hello.o > hello_o_elf.txt来将hello.o的elf格式输入至记事本hello_o_elf.txt来查看。
4.3.1 ELF头
针对ELF二进制文件,其开头通常为ELF头,在这一部分中,描述了生成该文件的系统的字的大小和字节顺序等信息,同时也描述了ELF头的大小、目标文件的类型、处理器体系结构、节头部表的文件偏移,以及节头部表中条目的大小和数量等信息,这些信息有助于链接器针对ELF文件内的信息进行解析,为后续的链接做准备。
图 43hello.o的ELF格式的ELF头
4.3.2 节头表
继续阅读hello.o的ELF格式,就会看到其节头表,这部分描述了每个节在文件中的类型,地址,大小,偏移以及可以对各部分进行的操作权限等信息。
图 44hello.o的ELF格式的节头表
4.3.3 重定位节
针对hello.o文件,其重定位节包含两个部分,.rela.text节以及.rela.eh_frame节。
其中,.rela.text节内包含可重定位代码信息,即为在可执行文件中需要修改的指令地址,链接器读该节的信息可知需修改的指令,其内有atoi等源程序文件中引用了的函数。
.rela.eh_frame节则包含了对en_frame节的重定位信息。
图 45hello.o的ELF格式的重定位节
4.3.4 符号表
针对ELF文件,其符号表中存放了程序中所定义和引用的的全局变量以及函数的信息,如针对hello.o,即包含后续会用到的atoi函数的信息。
图 46hello.o的ELF格式的符号表
4.4 Hello.o的结果解析
首先,在shell中输入命令objdump -d -r hello.o > hello_o_asm.txt,将hello.o的反汇编代码输入至记事本hello_o_asm.txt来查看。
将所获得的反汇编代码与hello.s进行比照分析,发现以下不同。
4.4.1 针对字符串常量的引用不同
在hello.s中,想要引用一个字符串常量,所用的是使%rip偏移字符串名称个字节这种格式。
图 47在hello.s中引用一个字符串变量
而在hello.o中,则不再偏移字符串名称个字节,而是0个字节,这是因为后期需要经过重定位对这个字符串的位置重新进行定位,此处暂时用0x0进行代替。
图 48在hello.o中引用一个字符串常量
4.4.2 数字的进制不同
在hello.s中,所用的立即数,偏移量等数字所使用的都是十进制形式。
图 49hello.s中的立即数和偏移量
而在hello.o中,同样的立即数和偏移量使用的都是十六进制形式。
图 50hello.o中的立即数和偏移量
4.4.3 跳转位置不同
在hello.s中,jmp,je等跳转指令会跳往某一个段,如下图,jmp指令会跳往.L3段。
图 51hello.s中的跳转
在hello.o中,jmp等跳转指令会跳往一个明确的地址。如下图,该jmp以相对寻址的方式,跳往了main+0x39+0x2+0x4b的地址,也就是main+0x86处。
图 52hello.o中的跳转
4.4.4 函数调用的不同
在hello.s中,调用函数时会直接使用call指令,引用该函数的函数名。
图 53在hello.s中的函数调用
在hello.o中,call指令后会引用下一条指令的地址,这是因为此处与前文跳转指令相同,同样使用相对寻址,但是因为还没有进行链接,无法确认这些函数的位置,所以只能用0来暂时代替相对地址,所以会表现成跳转到下一条指令的地址这种形式。
图 54在hello.o中的函数调用
4.5 本章小结
在本章中,针对gcc编译流程中的汇编一步的概念和作用作了介绍,介绍了ubuntu下执行汇编的命令。针对hello.o文件的elf格式的各节进行了解析,其可划分为ELF头,节头表,重定位节以及符号表四个部分。同时针对经过编译生成的汇编程序hello.s与经过汇编后再进行反汇编得到的汇编程序hello.o进行对比,发现了在汇编一步中汇编器(as)对程序进行的一些额外的处理,大致可划分为针对字符串常量的引用,数字进制,跳转位置以及函数调用五个方面的不同。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接指的是指将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在现代系统中,这一过程由链接器(ld)执行。
5.1.2 链接的作用
链接能够将可重定位的目标文件(即.o文件,如hello.o)整合起来,形成可执行文件(如hello)。这一操作使得分离编译称为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
以提供的hello.c文件为例,链接命令为
ld -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 /usr/lib/gcc/x86_64-linux-gnu/11/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
图 55链接命令及其结果
5.3 可执行目标文件hello的格式
首先,在shell中输入命令readelf -a hello > hello_elf.txt来将hello的ELF格式输入至记事本hello_elf.txt来查看。
浏览文件,hello的elf格式如下:
5.3.1 ELF头
hello的elf头与hello.o的elf头变化不大,它同样也描述了生成该文件的系统的字的大小和字节顺序以及ELF头的大小、目标文件的类型、处理器体系结构、节头部表的文件偏移,节头部表中条目的大小和数量等信息。但除此之外,hello的elf头中的类型从REL变成了EXEC,程序头大小和节头数量增加,并增加了入口地址这项内容。
图 56hello的elf格式的elf头
5.3.2 节头表
节头表这部分描述了每个节在文件中的类型,地址,大小,偏移以及可以对各部分进行的操作权限等信息。hello的节头表相较于hello.o的节头表增添了很多内容,节头数量增加了很多。
图 57hello的elf格式的节头表
5.3.3 程序头表
在elf二进制文件之中,只有hello这样的可执行的目标程序要求含有程序头表这部分。这部分描述了程序执行时各段的偏移和虚拟物理地址等信息。
图 58hello的elf格式的程序头表
5.3.4 动态节
图 59hello的elf格式的动态节
5.3.5 重定位节
与hello.o的重定位节对比,可以发现原来的.rela.text节变成了.rela.dy和.rela.plt两个节,并且hello的重定位节把程序所用到的所有函数以及这些函数进一步引用的那些函数都罗列了出来,相对的,hello.o只罗列出了程序文本中出现的函数。此外,hello的偏移量也发生了改变,这说明链接器已完成重定位,各函数有了一个准确的地址。
图 60hello的elf格式的重定位节
5.3.6 符号表
符号表中存放了程序中所定义和引用的的全局变量以及函数的信息,而hello的符号表与hello.o符号表相比,增加了程序的局部变量和弱符号等信息。
图 61hello的elf格式的符号表
5.4 hello的虚拟地址空间
在shell内输入命令edb --run hello,使用edb加载hello。
图 62edb加载hello界面中的data dump部分
图 63hello的elf格式中所记main函数地址
观察上图可知,使用edb观察发现从0x004010d6处开始便是程序的main函数,这与hello的elf格式中所记相同。
5.5 链接的重定位过程分析
在shell内输入命令objdump -d -r hello > hello_asm.txt来将hell的反汇编代码输入至记事本hello_asm.txt内来查看。
将所得反汇编代码与hello.o的反汇编代码进行比对分析,发现以下不同:
5.5.1 增加了函数的代码
在hello.o的反汇编代码中,仅含有main函数的代码,而在hello的反汇编代码中,因为经过链接后完成了重定位,hello.c中所用的代码从共享库中被加入到了其中,都被分配了相应的虚拟地址。增加了puts,printf,getchar等函数代码。
图 64hello的反汇编代码中的函数代码
5.5.2 跳转位置不同
在hello.o中,jmp等跳转指令所指向的地址都是相较于main函数首地址的偏移地址,而在hello中,因为经过了重定位,这些地址变成了一个准确的虚拟地址。
图 65hello.o的反汇编中的跳转
图 66hello的反汇编中的跳转
5.5.3 函数调用的不同
在hello.o中,因为没有经过重定位,call指令都会指向下一条指令的地址,即偏移量均为0,而在hello中,那些0被修改为了正确的偏移量,会指向对应函数的虚拟地址。
图 67hello.o的反汇编中的函数调用
图 68hello的反汇编中的函数调用
5.5.4 字符串常量的引用不同
在hello.o中,因为尚未进行重定位,偏移量都是用0进行代替的,而hello经过重定位之后对字符串的位置重新进行了定位,所以使用了准确的偏移量。
图 69hello.o的反汇编中的字符串常量的引用
图 70在hello的反汇编中的字符串常量的引用
5.5.5 语句地址的不同
在hello.o中,因为没进行重定位,每个语句的地址都是用main的首地址开始从0递增的,而在hello.o中,因为进行了重定位,每个语句的地址可以使用准确的虚拟地址。
图 71hello.o的反汇编中main函数各语句及其地址
图 72hello的反汇编中main函数各语句及其地址
5.5.6 链接的过程
链接这个过程可以进一步细分为符号解析和重定位两个过程。
符号解析指的是链接器将每个符号引用与一个确定的符号定义关联起来的过程。
重定位指的是链接器将符号从它们在.o文件中的相对位置重新定位到可执行文件中的最终绝对内存位置,并用它们的新位置,更新所有对这些符号的引用的过程。
而针对hello来说,链接这个过程就是链接器将其的每个符号引用与一个虚拟内存位置关相关联,重定位这些节,并将.o文件中的相对地址更新至这些绝对地址的一个过程。
5.6 hello的执行流程
_dl_start 地址:0x7f27f39b12b0
_dl_init 地址:0x0x7f27f3cfab20
_start 地址:0x4010f0
_libc_start_main 地址:0x7f27f3f90ab0
_cxa_atexit 地址:0x7f27f30ac430
_libc_csu_init 地址:0x4005c0
_setjmp 地址:0x7fce8c1b4c10
_sigsetjmp 地址:0x7fce8cb79b70
_sigjmp_save 地址:0x7fce8cb79bd0
main 地址:0x4011d6
(argc!=3时)
puts 地址:0x401090
exit 地址:0x4010d0
(此时程序打印“用法: Hello 学号 姓名 秒数!”后终止,并返回1给操作系统)
printf 地址:0x4010a0
sleep 地址:0x4010e0 (以上两个在循环体中执行8次)
此时窗口打印8行“Hello 2021111191 wangwei”
getchar 地址:0x4004d0
等待用户输入回车,输入回车后:
_dl_runtime_resolve_xsave 地址:0x7f5852c4e680
_dl_fixup 地址:0x7f5852c46df0
_uflow 地址:0x7f593ac420d0
exit 地址:0x7f889f889120
程序结束,并返回1给操作系统
5.7 Hello的动态链接分析
共享库是一个目标模块,他会在目标程序在运行或加载时,加载到任意的内存地址,并和目标程序链接起来,这个过程就是动态链接。程序按照模块拆分成各个相对独立部分,直到运行时才链接到一起,形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。查阅hello的elf格式的节头表可以得知,.got.plt起始位置为0x404000
图 73hello的elf格式的节头表中的.got.plt
找到该处地址,可以看到,在dl_init前后,项目的内容发生了变化。
图 74dl_init前
图 75dl_init后
5.8 本章小结
在本章中,针对gcc编译流程中的链接一步的概念和作用作了介绍,介绍了ubuntu下执行链接的命令。针对hello文件的elf格式的各节与hello.o文件的elf格式的各节进行了对比与解析,其可划分为ELF头,节头表,程序头表,动态节,重定位节以及符号表六个部分,并且查看了hello的虚拟地址空间,发现各节的名称都与相应的一段虚拟地址相对应。同时针对经过汇编后再进行反汇编得到的汇编程序hello.o与链接后再反汇编得到的汇编程序hello进行对比,发现链接器对程序进行处理,大致可划分为针对字符串常量的引用,写入引用代码,修改语句地址,跳转位置以及函数调用五个方面的不同。针对hello的执行过程进行了解析,并对hello的动态链接项目进行了分析。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是一个程序在执行时的实例,其可以抽象地描述为一个具有独立功能的程序在一个数据集合上运行的过程,它是操作系统分配资源和调度的基本单位。
6.1.2 进程的作用
进程提供独立的逻辑控制流,好像我们的程序独占地使用处理器;也提供一个私有的地址空间,好像我们的程序独占地使用内存系统,这呢个够有效地提高系统的效率和吞吐量。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 壳Shell-bash的作用
壳Shell-bash是一个交互型应用级程序,能代表用户运行其他程序。可以为用户提供一个用来操作linux机器的媒介,用户可以在此通过各种指令完成访问操作系统内核的服务。
6.2.2 壳Shell-bash的处理流程
1.读取从键盘输入的命令,并将字符串分割得到各个参数。
2.判断命令是否正确,并判断其是否为shell的内置指令,且将参数改造为系统调用execve() 内部处理所要求的形式。
3.如果是shell内置指令,则立即执行。
4.如果不是shell内置指令,则终端进程调用fork() 来创建子进程,自身则用系统调用waitpid() 来等待子进程完成。
5.当子进程运行时,它调用execve() 根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的、处于运行状态的子进程。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程虚拟地址空间相同的但独立的一份副本,包括代码、数据段、堆、共享库以及用户栈;子进程获得与父进程任何打开文件描述符相同的副本,所以其可以读写父进程中打开的任何文件。但是子进程所拥有的pid是与父进程不同的。
fork()函数调用一次但返回两次,在父进程中返回子进程pid,在子进程中返回0。
结合上文壳Shell-bash的处理流程,在用户输入完指令后,若其不是内置指令,shell就会创建一个子进程。因此,在shell内输入./hello 2021111191 wangwei 1,shell就会使用fork函数创建一个新的进程。
6.4 Hello的execve过程
据上文壳Shell-bash的处理流程,子进程运行时,就会使用execve()载入命令的名字所指定的文件,此处指定的便是可执行的目标程序文件hello。随后execve函数会在在进程的上下文中加载并运行hello,并调用_start创建新的且被初始化为0的栈等,将源程序内容清空,随后将控制给主函数main,并传入参数列表和环境变量列表,开始执行hello文件内的程序。
6.5 Hello的进程执行
当开始运行hello时,内存为hello分配时间片,然后在用户模式下执行并保存上下文。时间片就是逻辑流交错执行的过程中该程序执行它的控制流的一部分的所用时间。当时间片用尽时,操作系统会在内核模式下进行上下文切换,并将控制转移给其他的进程。
当hello执行函数sleep()时,进程会休眠若干秒,操作系统为了更充分地利用处理器的性能,会在这若干秒内进入内核模式,进行上下文切换并把控制转移给其他的进程。而在这若干秒过去之后,再次进入内核模式,执行上下文切换,恢复hello运行时的上下文,并将控制交还给hello。
hello在完成循环后,调用 getchar函数,hello 从用户模式进入内核模式,并进行上下文切换,控制转移给其他进程。最终,内核从其他进程回到 hello 进程,在return后进程结束。
6.6 hello的异常与信号处理
6.6.1 程序正常执行
正常执行时,hello每隔两秒打印一行“Hello 2021111191 wangwei”,共循环打印八次。全部打印完后,调用getchar()函数,等待用户输入回车后程序终止。随后,由Shell回收hello子进程。
图 76程序正常执行
6.6.2 不停乱按
在执行程序的时候不停乱按,按出来的字符会直接显示在shell内,但是并不会干扰程序的正常运行,在八句话全部打印完成之后,继续乱按,无其他反馈,输入回车后程序终止。随后,由Shell回收hello子进程。
图 77不停乱按
6.6.3 按回车
在执行过程的时候按回车,会在打印八句话的时候直接进行换行,而在八句话全部打印完成后,因为stdin中存在回车,所以执行getchar函数,会直接读取stdin内的回车,不需要额外再输入回车来终止程序。随后,由Shell回收hello子进程。随后,在执行的过程中输入了四个回车,而在getchar读取stdin内的一个回车后,其内还有三个回车,由shell轮流读取stdin内的这四个回车,故会换行三次,但没有输入命令,所以只是换行。
图 78按回车
6.6.4 按Ctrl-Z
在进程执行时按Ctrl-Z,会发送信号SIGSTP,父进程Shell接收到该信号,并执行信号处理程序,将hello进程暂停挂起,并打印相关信息。
图 79按Ctrl-Z
一. 运行ps命令
在Ctrl-Z之后运行ps命令,会打印出各进程的pid,观察后发现,其中包含刚才被暂停挂起的hello进程。
图 80按Ctrl-Z后运行ps命令
- 运行jobs命令
按Ctrl-z后运行jobs命令,会打印出当前的作业表,观察发现其中只有刚才被暂停挂起的hello作业。
图 81按Ctrl-z后运行jobs命令
- 运行pstree命令
按Ctrl-z后运行pstree命令,会打印出进程树。从其中找到了刚才执行的hello进程,因为它和pstree命令都是由bash创建子进程来执行的,故这三个进程同属于一个进程组。从祖先进程到hello进程的树为init(Ubuntu)→SessionLeader→Relay(9)→bash→hello
图 82按Ctrl-Z后运行pstree命令
- 运行fg命令
按Ctrl-z之后运行fg命令,会把后台进程重新调到前台来运行,据图79可知,hello进程在被暂停挂起前打印了三句“Hello 2021111191 wangwei”,而在fg命令之后,其继续执行,打印了剩下的五句,用户输入回车后程序终止。随后,由Shell回收hello子进程。
图 83按Ctrl-z之后运行fg命令
- 运行kill命令
因为执行kill命令需要得到进程的pid,故在按Ctrl-z后,需要先执行ps命令,打印出各进程的pid,此时得知被Ctrl-z暂停挂起的hello的进程的pid为44,此时执行命令kill -9 44,杀死pid为44的进程,即hello进程。随后,再次执行ps命令,发现hello进程已经被杀死。
图 84按Ctrl-z之后运行kill命令
6.6.5 按Crtl-C
在hello的执行过程中按Ctrl-C,会发送信号SIGINT,父进程Shell接收到该信号,并执行信号处理程序,将hello进程杀死并回收。随后再执行ps命令,会发现没有hello进程。
图 85按Ctrl-c
6.7本章小结
在本章中,针对进程的概念和作用,Shell-bash的概念和处理过程做了介绍。随后,沿着Shelll-bash的处理过程做了更详尽的介绍,首先,shell会调用fork函数,新建一个子进程,随后,在这个子进程内调用execve函数将hello载入并执行。并结合了进程上下文信息、进程时间片、用户态与核心态转换等,介绍了hello在shell中是怎么执行的。而在hello执行的过程中,分别针对正常执行,不停乱按,按回车,按Ctrl-Z(以及在按Ctrl-Z之后运行ps,jobs,pstree,fg,kill等命令),按Crtl-C等情况分别做了分析,针对异常与信号处理情况的内容作了说明。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
逻辑地址为由程序产生的与段有关的偏移地址。逻辑地址分为段选择符和段偏移量两部分。在CPU保护模式下,这两部分通过分段地址的变化处理后才会对应到相应的物理内存地址。hello的反汇编代码中的地址即为逻辑地址。
7.1.2 线性地址
线性地址是逻辑地址到物理地址变换的中间层,也是处理器可寻址的内存空间(称为线性地址空间)中的地址。在各段中,逻辑地址是段中的偏移地址,其偏移量加上基地址就是线性地址。hello反汇编代码中的逻辑地址与基地址相加后,即得到了对应内容的线性地址。
7.1.3 虚拟地址
虚拟地址是指由hello程序产生的段内偏移地址。使用虚拟寻址时,CPU通过一个虚拟地址来访问主存,这个虚拟地址在被送至内存前先通过mmu转换成适当的物理地址。逻辑地址与线性地址二者之间没有明确的界限,在linux中,虚拟地址数值就等于线性地址,所以hello反汇编代码中的逻辑地址与基地址相加后,得到的也是对应内容的虚拟地址。
7.1.4 物理地址
计算机的主存是由很多个连续的字节大小的单元组成的数组,而每一个单元都拥有一个仅属于它的物理地址。因此,因为计算机系统最终还是要对主存或寄存器上的内容进行操作,所以以上三种地址落实到最终都会转换成物理地址上,它是地址转换的最终地址。hello的虚拟地址经过mmu翻译后得到的就是物理地址,计算机系统会根据这些物理地址来操作数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器由逻辑地址到线性地址的变化通过段式管理的方式实现。每个程序在计算机系统中都有着一个与之相对应的段表,其内保存着该程序各段装入主存的状况信息。
据上文所说,虚拟地址由段选择符和段内偏移量两部分组成。同时,在段寄存器内存放着段选择符,可以通过段选择符来得到对应段首地址。
段选择符构成如下:其前13位是索引号,用来确定当前使用的段描述符在描述符表中的位置;后面3位表示一些硬件细节,包含TI(根据TI取值选择全局描述符表(GDT)或局部描述符表(TI=1,LDT))与RPL(用以判断重要等级,为0时为内核,等级最高;为3时为用户,等级最低)。
图 86段选择符
根据RPL,找到对应的段描述符表,在根据索引就可以找到对应的段描述符,进而就可以得到对应的段基地址,基地址与偏移量结合就可以得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的变换通过对虚拟地址内存空间进行分页的页式管理来完成。
在linux系统下,线性地址与虚拟地址相等。其由虚拟页号VPN和虚拟页偏移VPO组成,这二者的位数由计算机自身特性决定。
首先,计算机系统的物理页与虚拟页大小相等,因此偏移量就也要是相等的,故线性地址的VPO与物理地址的PPO的位数和内容都是相同的。随后,计算机系统通过页表基址寄存器所存的页表首地址来查询VPN对应的PTE项,就可以得到相对应的PPN。
图 87线性地址到物理地址变换示意图
若查询到的PTE,其有效位为1,则页名字,此时其内的PPN既是转换后应得的VPN,在其后填上VPO后即可完成地址变换,得到所求物理地址。
若查询到的PTE,其有效位为0,则发生缺页故障,操作系统执行缺页中断处理程序,选择被替换页,再把对应的新页面调入页表内。随后,重新调用引起缺页故障的指令,即这一条地址变换,此时发生页命中,随后便如上了。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB,即翻译后备缓冲器 (Translation Lookaside Buffer),也可以叫做快表,其用于加速对于页表的访问。
图 88 MMU 使用虚拟地址的 VPN 部分来访问TLB
多级页表可以将原本的一个大页表划分为许多个小页表。VPN中每一段对应着对应级小页表的虚拟页号,而页表内所存的则是所查的下一级页表的首地址(若是最后一级页表,则所存的是对应的PPN),结合基址寄存器,进行多次寻址,就可以翻译出对应的物理地址。
图 89使用K级页表的地址翻译
TLB与四级页表支持下的地址变换在访问时几乎与cache一致。先通过TLBI找到所在组,在通过TLBT判断是否是我们要访问的虚拟地址。
如果命中则同上,直接读出物理页号,并在其后加上VPO,组合成所求的物理地址。
如果不命中,则开始执行四级页表的寻址,整个虚拟地址被划分为四个VPN和一个VPO,根据基址寄存器内容和第一级VPN开始寻址,此后每级VPN内容即为上一级VPN指向的对应页表内容指向的页表首地址对应的偏移量,进行多次寻址,最后得到对应的PPN,其间若是发生缺页故障,则执行缺页中断处理程序,选择被替换页,再把对应的新页面调入页表内。随后,重新调用引起缺页故障的指令,即这一条地址变换,此时发生页命中,随后可以继续往下寻址或得到结果了。
这样操作虽然复杂,但是一来可以降低单个页表的大小,便于操作系统操作;二来在一级页表缺页的情况下,不需要去再往下寻址,可以提速。
7.5 三级Cache支持下的物理内存访问
在通过地址变换,最后得到需要的物理地址之后,就要到Cache或主存中去取出所需的数据了。物理地址会被划分为CO(缓存块内的地址偏移量),CI(Cache索引),CT(Cache标志)三部分。首先根据CI找到对应的组,再根据CT去和对应的组内的每一行的Tag进行比对,若有相同的,则找到其对应的块,再根据CO从这个块内找到对应的信息即可。
首先,会从1级Cache来查找,若没有名字,则去找2级Cache,重复上述流程。以1级Cache→2级Cache→3级Cache→主存的顺序来查找。查找成功后,将相应的块将其放入当前Cache中。若映射到的组内存在空闲块,则可以直接放置;否则产生冲突,进行替换。
图 90存储器山中的三级Cache及主存,越往下则越大越慢
7.6 hello进程fork时的内存映射
当fork()函数被父进程调用时,内核创建一个子进程,并为新的子进程创建各种数据结构,并分配给子进程一个新的pid。同时为这个新进程创建虚拟内存,mm_struct、vm_area_struct链表和页表的原样副本,并将两个进程的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)。
当fork在新进程中返回时,新进程拥有与调用fork的父进程相同的虚拟内存。当这两个进程中的任一个进行写操作时,写时复制机制就会创建新页面,在新的页面中进行写操作,并且原来的虚拟内存映射到创建的新页面上,因此每个进程都具有私有的地址空间。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行可执行目标文件hello,其步骤如下:
- 删除已有页表和结构体vm_area_struct链表。
- 创建新的页表和结构体vm_area_struct链表。
- 映射私有区域,即:为新程序hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有写时复制(COW)的。其中,代码和初始化的数据映射到.text和.data区(目标文件提供),.bss和栈映射到匿名文件。
- 映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置PC,指向代码区域的入口点,即将下一条待执行的指令更改为该代码区域内的第一条命令。Linux根据需要换入代码和数据页面
7.8 缺页故障与缺页中断处理
MMU在翻译某个虚拟地址时,触发了一个缺页故障(即访问了一个不存在的页)。这个异常导致控制转移到内核的缺页中断处理程序,其执行步骤如下:
- 判断地址是否合法,操作系统会搜索区域链表,确认地址是否在(合法的)某个区域内。若不在,则此地址非法,产生一个段错误,随后终止此进程。
- 判断访问是否合法。判断操作是否有读、写或执行区域内页面的权限。若没有,则违反许可,触发保护异常,产生一个段错误,随后终止此进程。
- 若地址和访问都合法,则属于正常缺页。缺页中断处理程序会选择一个被替换页,若其被修改过(dirty位为1),则将其写入内存,随后再用新的页将其替换,并更新页表。随后,重新执行引起缺页的指令,此时会页命中,并得到相对应的物理地址。
7.9本章小结
在本章中,以hello程序为案例,先是介绍了逻辑地址,线性地址,虚拟地址,物理地址的概念,又介绍了Intel逻辑地址到线性地址的变换,即其段式管理,以及线性地址到物理地址的变换,即其页式管理。分析了TLB与四级页表支持下的VA到PA的变换的机制及优点。介绍了3级Cache支持下的物理内存访问机制。分析了hello进程fork与execve时的内存映射机制与流程。又介绍了缺页中断处理程序的两种非法及合法情况下的处理方式。
结论
Hello的生命周期:
- 预处理:根据原程序内字符#开头的命令,直接修改原始的C程序文本。
- 编译:将程序文本进行分析和优化,转换为更接近机器码的语言。
- 汇编:将文本形式的汇编语言转换成CPU能够直接执行的二进制的指令。
- 链接:将可重定位的目标文件整合起来,形成可执行文件。
- 创建进程:shell进程调用fork函数为hello创建新进程。
- 加载运行:hello所在的进程与其他进程轮流运行时被分成了多个时间片, 调用execve函数启动hello,内核调度使hello程序抢占其他正在执行的进 程。
- 访问内存:MMU将虚拟地址翻译成物理地址。
- 动态申请内存:hello运行过程中可能会通过malloc函数动态申请堆中的 内存。
- 信号处理:hello运行过程中可能会产生各种异常和信号,系统会针对出现 的异常和收到的信号去调用对应的信号处理程序以做出反应。
- 终止并被回收:hello运行结束后被父进程回收,内核删除相关数据。
对计算机系统的设计与实现的深切感悟:
计算机系统与物理化学等自然科学是不同的,其他学科是建立在客观事物的基础上,每一代人去共同发展出来的。而计算机除了一些最基础的电学技术之外,进程调度,内存内存管理,进程的生命周期,这些机制都是由工作者们所共同努力设计出来的,它凝聚着一代代计算机人智慧的结晶,只有上一代人设计出了一个很酷的编程语言,下一代人才能通过这个编程语言去做一个很酷的软件,再下一代人才能用这个软件去做一些很酷的事,这种一代一代传承下去,共享下去的精神,本身就是一个很酷的东西。所以,在完成这个大作业之后,在完成这个学期的课程之后,我更深刻地体会到了我学的这些东西实际上是多么神奇,多么厉害的一些东西,它们是真真正正从0到1的。
附件
文件的名字 | 文件的作用 |
hello.i | 预处理所得,修改了的源程序(文本) |
hello.s | 编译所得,汇编程序(文本) |
hello.o | 汇编所得,可重定位目标程序(二进制) |
hello_o_elf.txt | hello.o的elf格式 |
hello_o_asm.txt | hello.o的反汇编代码 |
hello | 链接所得,可执行的目标程序(二进制) |
hello_elf.txt | hello的elf格式 |
hello_asm.txt | hello的反汇编代码 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.