计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021111537
班 级 2103103
学 生 王羽婷
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年11月
本文通过一个程序hello.c,分析在Linux系统中源代码文件经过预处理、编译、汇编、链接、执行的全过程,从最底层的角度诠释程序,更好地理解计算机和Linux系。并说明了Linux操作系统如何对hello程序进行进程管理、存储管理和I/O管理。全文内容涵盖了计算机系统课程的主体框架,梳理、回顾了课程所学的主要知识点。
关键词:计算机系统;hello程序;Linux;
目 录
第1章 概述................................................... - 4 -
1.1 Hello简介............................................ - 4 -
1.2 环境与工具........................................... - 4 -
1.3 中间结果............................................... - 5 -
1.4 本章小结............................................... - 5 -
第2章 预处理............................................... - 6 -
2.1 预处理的概念与作用........................... - 6 -
2.2在Ubuntu下预处理的命令................ - 6 -
2.3 Hello的预处理结果解析.................... - 6 -
2.4 本章小结............................................... - 8 -
第3章 编译................................................... - 9 -
3.1 编译的概念与作用............................... - 9 -
3.2 在Ubuntu下编译的命令.................... - 9 -
3.3 Hello的编译结果解析........................ - 9 -
3.3.1常量的解析.................................... - 9 -
3.3.2变量的解析.................................. - 10 -
3.3.3类型转换...................................... - 10 -
3.3.4算数操作....................................... - 11 -
3.3.5控制转移....................................... - 11 -
3.3.6数组/指针/结构操作.................... - 11 -
3.3.7函数操作...................................... - 12 -
3.4 本章小结............................................. - 13 -
第4章 汇编................................................. - 15 -
4.1 汇编的概念与作用............................. - 15 -
4.2 在Ubuntu下汇编的命令.................. - 15 -
4.3 可重定位目标elf格式...................... - 15 -
4.3.1 readelf命令............................ - 15 -
4.3.2 ELF头........................................ - 16 -
4.3.3 节头目表................................... - 16 -
4.3.4 重定位节................................... - 17 -
4.3.5 符号表....................................... - 18 -
4.4 Hello.o的结果解析........................... - 18 -
4.5 本章小结............................................. - 19 -
第5章 链接................................................. - 21 -
5.1 链接的概念与作用............................. - 21 -
5.2 在Ubuntu下链接的命令.................. - 21 -
5.3 可执行目标文件hello的格式......... - 21 -
5.4 hello的虚拟地址空间....................... - 22 -
5.5 链接的重定位过程分析..................... - 25 -
5.6 hello的执行流程............................... - 27 -
5.7 Hello的动态链接分析...................... - 27 -
5.8 本章小结............................................. - 28 -
第6章 hello进程管理.......................... - 29 -
6.1 进程的概念与作用............................. - 29 -
6.2 简述壳Shell-bash的作用与处理流程.. - 29 -
6.3 Hello的fork进程创建过程............ - 30 -
6.4 Hello的execve过程........................ - 30 -
6.5 Hello的进程执行.............................. - 30 -
6.6 hello的异常与信号处理................... - 31 -
6.6.1 异常的类型和处理流程........... - 32 -
6.6.2 正常运行状态........................... - 33 -
6.6.3 按下Ctrl+Z.............................. - 33 -
6.6.4 按下Ctrl+C.............................. - 34 -
6.6.5 中途乱按................................... - 34 -
6.6.6 Kill命令.................................. - 34 -
6.7本章小结.............................................. - 35 -
结论............................................................... - 36 -
附件............................................................... - 37 -
参考文献....................................................... - 38 -
第1章 概述
1.1 Hello简介
1.P2P
P2P,指Program to Process,即从程序到进程。
其中的Program是指在编辑器中写下的hello.c文件。而process则是指程序经过预处理编译等操作,变为可执行文件的过程。
具体过程为:
预处理器cpp将hello.c文件中的#开头的命令进行预处理,插入对应的系统头文件中的内容,得到文件;
编译器ccl将hello.i翻译为汇编语言文件;
汇编器as将hello.s翻译为机器指令代码,得到hello.o文件;
链接器ld将hello.o和其它用到的预编译好的目标文件合并到一起并且完成引用的重定位工作,就得到了一个可执行文件hello。
在Ubuntu的shell中输入./hello命令后,shell会调用fork函数创建子进程,并且在子进程中加载该程序,由此,hello.c文件就成为了一个进程。
2.020
020,指from zero to zero。Hello程序执行前,不占用内存空间(第一个0)。P2P过程后,子进程首先调用execve,依次进行虚拟内存映射、物理内存载入;随后进入主函数执行程序代码,程序调用各种系统函数实现屏幕输出信息等功能;最终程序结束,shell父进程回收此子进程,其相关的所有内存中的状态信息和数据结构被清除(第二个0)。以上即为020的全部过程。
1.2 环境与工具
硬件环境:
CPU:Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz 1.80 GHz
RAM:8.00GB
软件环境:
Windows10 64位
Ubuntu20.04.1
开发工具:
codeblocks,vscode,vmware,edb,gdb
1.3 中间结果
hello.i:hello的预处理文件
hello.s:hello的编译文件
hello.o:hello的可重定位目标文件
hellooelf.txt:hello.o的elf信息
helloodump.txt:hello.o的反汇编信息
hello:hello的可执行目标文件
hello_elf.txt:hello的elf信息
hellodump.txt:hello的反汇编信息
1.4 本章小结
本章首先以Hello程序为例,简要解释了P2P、O2O的概念,以及其中涉及到的一些具体技术,然后写明了完成本次大作业的硬件和软件环境,最后列举了完成作业过程中生成的所有的中间文件。
第2章 预处理
2.1 预处理的概念与作用
预处理是指在编译之前的预处理阶段,预处理程序对程序进行的操作,通常会生成一个.i后缀的预处理文件。
预处理的主要作用是对程序代码文本进行替换操作,如将以#include格式包含的文件内容复制到编译的源文件中,用实际值替换#define定义的宏,以及根据#if的条件决定需要编译的代码。预处理过后程序代码中的预处理指令会被删除。预处理阶段也会删除注释和多余的空白字符等。
2.2在Ubuntu下预处理的命令
图2-1 预处理
2.3 Hello的预处理结果解析
经过预处理后,原28行代码被扩展为了3105行。
首先是源代码文件等相关的一些信息,如图。
图2-2 预处理文件
然后是预处理扩展的内容。
其中,stdio.h文件被插入在13-795行。
图2-3 预处理文件
unistd.h文件被插入在797-2027行。
图2-4 预处理文件
stdlib.h文件被插入在2036-3086行。
图2-5 预处理文件
预处理的具体过程如下(以stdio.h为例说明):
作为hello.c中包含的头文件,stdio.h是标准库文件,cpp到Linux系统的环境变量下寻找stdio.h,打开文件/usr/include/stdio.h,发现其中使用了“#define”、“#include”等,故cpp对它们进行递归展开替换,最终的hello.i文件中删除了原有的这部分;对于其中使用的“#ifdef”、“#ifndef”等条件编译语句,cpp会对条件值进行判断来决定是否对此部分进行包含。
如图为hello.c包含的头文件在系统中的路径位置,cpp从这里读取、复制和处理这些头文件,将它们添加至hello.i。
图2-6 头文件
原本的代码被放置在3088-3105行。除注释和“#include”语句被删除外,内容保持基本不变。
图2-7 预处理文件
2.4 本章小结
本章主要介绍了预处理的概念,功能,和Linux系统下的预处理指令。通过对于得到的hello.i文本的分析,我们知道了cpp预处理器在执行预处理指令时候进行的具体工作:插入头文件,扩展宏定义,删除注释等等。
第3章 编译
3.1 编译的概念与作用
编译是指把用高级程序设计语言书写的源程序,翻译成等价的机器语言格式目标程序的翻译程序。
编译除了基本的翻译操作,还包含语法检查和目标程序优化等功能。在翻译地过程中动态地检查语法错误,并将错误实时地反映出错误的类型和部位等有关信息。目标程序优化分为表达式优化、循环优化或程序全局优化,用以提高目标程序的质量,即占用的存储空间少,程序的运行时间短。可以说,编译的作用是通过一系列步骤让源代码更接近优秀的机器语言。
3.2 在Ubuntu下编译的命令
图3-1 编译
3.3 Hello的编译结果解析
3.3.1常量的解析
字符串常量
printf函数中用到的格式字符串、输出字符串被保存在.rodata段,hello.c文件中的字符串常量有两处:
在hello.s文件中我们可以找到常量字符串对应的存储位置:
其中我们发现汉字被以utf-8的格式进行编码,即每个汉字3字节,这一点在字符串的长度上也得到了体现(其中的中文感叹号也使用了utf-8编码,占3字节)
整形常量
整形常量在hello.c文件中是以代码中的数字的形式存在,if条件判断值、for循环终止条件值在.text段。
这些常量被嵌入了汇编代码中:
3.3.2变量的解析
全局变量
此程序中无全局变量
局部变量
在hello.c中出现的局部变量主要是int i
在汇编代码中可以发现其被分配在运行时栈上,由汇编代码可见,局部变量i在赋初值后被保存在地址为%rbp-4的栈位置上。
3.3.3类型转换
用到了类型转换atoi函数将字符串转换成整形常量
3.3.4算数操作
在for循环中,int i进行了++操作
在对应的汇编代码中,是通过add指令完成的
3.3.5控制转移
Hello.c中的控制转移有对于表达式的判断,通过cmp指令和jle指令组合完成对于for循环体是否循环的判断,从而完成循环体的跳转。
此外,还有对于表达式的判断,通过cmpl和je组合判断完成if语句的跳转。
3.3.6数组/指针/结构操作
在main函数中传递的参数出现了指针数组
每一个数组的元素都是一个char*类型的指针,可以指向一个字符串,而具体来说argv[0]指向文件名,argv[1] argv[2] argv[3]分别指向命令行输入的第一、二、三个参数,在汇编代码中这个指针数组的首地址被存放在rsi中,也就是存放main函数的第二个参数的寄存器。
对于指针数组argv,其每一个元素的大小都是8字节,而且数组中的元素应是连续存放的,因此地址偏移量也应该是8的倍数。
从此处的代码我们可以验证这一点。
3.3.7函数操作
main函数
首先我们可以查看main函数的汇编代码,发现其两个参数,argc和argv分别被存放在寄存器rdi和rsi中,这与我们的课本知识吻合。
继续查看后面的汇编代码,我们发现对于argv的操作,其指针数组的基地址通过寄存器存放,我们将基地址加上8的倍数的偏移量就可以访问指针数组的别的成员指针,而为了访问每一个指针成员指向的字符串,我们需要访问每一个指针指向的内存空间,也就是说,每个指针成员指向的字符串是存放在内存中的。
main函数不是由我们编写的函数调用的,是由系统函数调用,main函数可以调用别的函数,但是需要遵从寄存器保护的规则和参数传递的规则等等。
main函数的返回值是int类型的,存储在寄存器rax中,在需要返回时,先将rax的值设置为0,然后返回即可,对应于hello.c文件中的return 0。
结合之前的代码我们可以发现,rax虽然被用作传递返回值,但是在此之前都可以被main函数自由使用。
printf函数
Hello.c文件中,printf函数被调用了2次
第一次对应的汇编代码部分如下:
可以发现,第一次实际上是调用了puts函数,这是因为此次调用printf函数不需要传递额外的参数,只需要将内存中存储的字符串复制到屏幕上即可,因此编译器做了一点等价的替换。
第二次的对应汇编代码如下:
可以看到总共传递了三个参数,所以无法使用puts函数替换,第一个参数是主体字符串的首地址,被存放在rdi中,第二、第三个参数分别是替换的字符串的首地址,通过寄存器rsi和rdx传递,传递规则与课本中讲述的一致。
exit函数
exit函数的源代码如下:
对应的汇编代码如下:
我们可以看到函数传递的参数存放在edi中,对应了源代码中的exit(1),说明传递的整数值直接作为函数退出的状态值。
sleep函数
sleep函数的源代码如下:
对应的汇编代码如下:
可以看到sleep函数通过rdi传递了一个参数作为休眠时间的控制。
getchar函数
getchar函数的汇编代码如下:
由于此函数没有参数,因此不需要通过寄存器进行参数传递。
3.4 本章小结
本章围绕hello.i经编译器处理得到hello.s的过程,介绍了编译的概念、过程并具体分析了hello程序的编译结果。通过分析汇编代码,我们更加清楚地明白了全局变量和局部变量的区别,常量是如何进行存储的,对于非线性执行的跳转语句是如何进行的,以及在实现跳转的基础上如何进行逻辑控制,实现循环。以及在跳转的基础上,了解了函数是如何进行调用的,调用另一个函数的时候如何进行参数的传递,以及这个过程中怎么对寄存器的内容进行保护等等。
第4章 汇编
4.1 汇编的概念与作用
驱动程序运行(或直接运行)汇编器as,将汇编语言程序hello.s翻译成机器语言指令,并将这些指令打包成可重定位目标文件hello.o的过程称为汇编。
按照平台的不同,CPU的指令集不同,生成的可重定位目标文件的内容也不同。汇编器根据各个平台的指令集逐条语句翻译为二进制的形式存储。同时将一些常量和全局变量插在目标文件的不同的节里。同时,汇编器还会将ELF文件的信息、机器类型等数据写入ELF头,方便后续操作。
4.2 在Ubuntu下汇编的命令
图4-1 汇编
由于hello.o是二进制形式存储,输出不可读。
4.3 可重定位目标elf格式
4.3.1 readelf命令
使用命令:readelf -a hello.o > helloo_elf.txt
将hello.o中ELF格式相关信息重定向至文件helloo_elf.txt。
命令执行效果截图:
4-2 可重定位目标ELF
4.3.2 ELF头
此部分内容(helloo_elf.txt文件第1-20行)如下:
4-3 ELF头
分析:
- Magic用于标识ELF文件,7f 45 4c 46分别对应ASCII码的Del、字母E、字母L、字母F,操作系统在加载可执行文件时会确认是否正确,如果不正确则拒绝加载,其余标识位数、小/大端序、版本号等,后九个字节未定义。
- 根据头文件的信息,可知该文件是可重定位目标文件,有13个节,其余部分的信息此处不再一一列举说明。
4.3.3 节头目表
此部分列出了hello.o中的13个节的名称、类型、地址、偏移量、大小等信息。具体内容(helloo_elf.txt文件第22-50行)如下:
4-4 节头目表
分析:
- 由于是可重定位目标文件,所以每个节都从0开始,用于重定位
- .text段是可执行的,但是不能写
- .data段和.rodata段都不可执行且.rodata段不可写
- .bss段大小为0
4.3.4 重定位节
重定位节记录了各段引用的符号相关信息,在链接时,需要通过重定位节对这些位置的地址进行重定位。链接器会通过重定位条目的类型判断如何计算地址值并使用偏移量等信息计算出正确的地址。
具体内容(helloo_elf.txt文件第63-76行)如下:
4-5 重定位节
分析:
本程序需要重定位的符号有:.rodata,puts,exit,printf,sleepsecs,sleep,getchar及.text等。注意到重定位类型仅有R_X86_64_PC32(PC相对寻址)和R_X86_64_PLT32(使用PLT表寻址)两种,而未出现R_X86_64_32(绝对寻址)。
4.3.5 符号表
符号表(.symtab)存放在程序中定义和引用的函数和全局变量的信息。
具体内容(helloo_elf.txt文件第80-98行)如下:
4-6 符号表
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > helloodump.txt
反汇编代码:
4-7 反汇编代码
hello.s中对应的汇编代码:
4-8 .s汇编代码
比较分析:
二者总体相同,但也有一些细微的差异:
- 分支控制转移不同:对于跳转语句跳转的位置,hello.s中是.L2、.LC1等代码块的名称,而反汇编代码中跳转指令跳转的位置是相对于main函数起始位置偏移的地址(相对地址)。
- 函数调用表示不同:hello.s中,call指令使用的是函数名,而反汇编代码中call指令使用的是待链接器重定位的相对偏移地址,这些调用只有在链接之后才能确定运行时的实际地址,因此在.rela.text节中为其添加了重定位条目。
- hello.s中的全局变量、printf字符串等符号被替换成了待重定位的地址。
- 数的表示不同:hello.s中的操作数均为十进制,而hello.o反汇编代码中的操作数被转换成十六进制。
- hello.s中提供给汇编器的辅助信息在反汇编代码中不再出现,可能是在汇编器处理过程中被移除,如“.cfi_def_cfa_offset 16”等。
4.5 本章小结
本章主要了解了关于汇编的基础流程和作用,一个汇编语言文件是如何通过汇编器翻译成机器可识别的机器语言的,以及对于直接编译hello.c文件得到的汇编代码和反汇编hello.o文件得到的汇编代码的区别。经过汇编阶段,汇编语言代码转化为机器语言,生成的可重定位目标文件为随后的链接阶段做好了准备。
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时、执行时甚至运行时。在现代系统中,链接是由叫做链接器的程序自动执行的。本文中,链接是指将可重定向目标文件hello.o与其他一些文件组合成为可执行目标文件hello。
将目标文件集合整合成一个文件,并将其映射到对应的虚拟内存,得到可执行文件,并且在链接的过程中,会合并一些提前预制编译的模块,这也使得程序的运行效率提升。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解成更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用。
5.2 在Ubuntu下链接的命令
5-1 链接
5.3 可执行目标文件hello的格式
使用命令:readelf -a hello > hello_elf.txt
将hello中ELF格式相关信息重定向至文件hello_elf.txt。
5-2 可执行目标文件ELF
各段的基本信息(起始地址、大小等)记录在节头部表中,具体内容(hello_elf.txt第22-74行)如下:
5-3 节头
5.4 hello的虚拟地址空间
使用edb加载hello,界面如图:
5-4 edb
此时Data Dump窗口中显示的就是hello的虚拟空间内容,如下图,显示范围为0x401000至0x402000。
5-5 data dump
通过与Symbols窗口对照,可以发现各段均一一对应,如图。
5-6 data dump
5-7 data dump
5-8 data dump
Symbols与Data Dump对照截图 .text段部分:
5-9 .text段
Symbols与Data Dump对照截图 .rodata段部分:
5-10 .rodata段
Symbols与Data Dump对照截图 .data段部分:
5-11 .data段
其中,我们可以看到程序printf函数的字符串,进一步说明了各段的虚拟地址与节头部表的对应关系。
5.5 链接的重定位过程分析
使用命令:objdump -d -r hello > hellodump.txt
命令执行效果截图:
5-12 可执行文件反汇编
- 整体上来看,hello的反汇编代码比hello.o的反汇编代码多了一些节(如.init, .plt, .plt.sec等),如下图所示:
5-13 反汇编比较
- hello中加入了一些函数,如_init(),_start()以及一些主函数中调用的库函数,如下图所示:
5-14 反汇编比较
- hello中不再存在hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址,如图:
5-15 反汇编比较
根据以上分析我们可以看出,链接过程会扫描分析所有相关的可重定位目标文件,并完成两个主要任务:首先进行符号解析,将每个符号引用与一个符号定义关联起来;随后进行重定位,链接器使用汇编器产生的重定位条目的详细指令,把每个符号定义与一个内存位置关联起来。最终的结果是将程序运行所需的各部分组装在一起,形成一个可执行目标文件。
5.6 hello的执行流程
使用EDB跟踪程序执行过程,记录如下:
子程序名 | 地址(16进制) |
ld-2.31.so!_dl_start | |
ld-2.31.so!_dl_init | |
hello!_start | |
hello!_main | |
hello!printf@plt | |
hello!atoi@plt | |
hello!sleep@plt | |
hello!getchar@plt | |
5-16 EDB跟踪执行
5.7 Hello的动态链接分析
5-17 调用dl_init之前.got.plt段的内容
5-18 调用dl_init之后.got.plt段的内容
可以很明显地看出第一,二行的变化。
实际上,这是书上提到的动态链接器的延迟绑定的初始化部分。延迟绑定通过全局偏移量表(GOT)和过程链接表(PLT)的协同工作实现函数的动态链接,其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在此之后,程序调用共享库函数时,会首先跳转到PLT执行指令,第一次跳转时,GOT条目为PLT下一条指令,将函数ID压栈,然后跳转到PLT[0],在PLT[0]再将重定位表地址压栈,然后转进动态链接器,在动态链接器中使用两个栈条目确定函数运行时地址,重写GOT,再将控制传递给目标函数。以后如果再次调用同一函数,则通过间接跳转将控制直接转移至目标函数。
5.8 本章小结
链接是生成一个可执行文件的最后一步,它将各个模块整合在一起。这种模块化的整合模式使得模块化编程称为可能。链接器将各个目标文件的各个段分割合并,并确定了每个符号的实际意义与地址。静态链接在程序执行前就完成了所有的工作,而动态链接则是在程序运行的时候才进行链接。至此,一个C语言文本真正称为了一个可执行文件。
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。
进程给每个应用提供了两个抽象:逻辑控制流和私有地址空间。逻辑控制流通过上下文切换让分时处理各个进程,私有地址空间则是通过虚拟内存机制让每个程序仿佛在独占物理内存空间。这样的抽象保证了在不同的情况下运行同样的程序可以得到相同的结果,使得具体的应用不需要关心关心处理器和内存的相关事宜。
6.2 简述壳Shell-bash的作用与处理流程
作用:
Shell-bash是一个交互型应用级程序,代表用户运行其他程序。它是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。
处理流程:
shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。
简化处理流程:
- 从终端读入输入的命令;
- 将输入字符串切分获得所有的参数;
- 如果是内置命令则立即执行;
- 若不是则调用相应的程序执行;
- shell应该随时接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
在收到执行hello的命令后,shell调用fork函数创建了一个新的进程。调用fork函数的shell进程就是父进程,产生的新进程称为子进程。并且fork函数从两个进程中分别返回,父进程中返回的是子进程的进程id,子进程中返回的是0。接着两个进程互不干扰地继续执行。
在早期的Linux系统中,fork一个进程是将该进程的所有内存复制产生一个新的内存映像。由于这种方式比较浪费内存,在现在的操作系统中,fork刚执行后并不会直接复制父进程的所有内存,子进程的代码段、数据段、堆栈都暂时指向父进程的页表,即两个的虚拟空间不同,但是物理空间是同一个。当父进程或子进程有修改段段行为发生时,再为子进程的段分配空间,直到子进程执行execve函数之前,代码段都是和父进程共享的。执行execve函数后,子进程和父进程就拥有了完全分离的两个物理空间,子进程独立地执行新的代码段。这种技术被称为写时复制技术。
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有出现错误时(例如找不到可执行目标文件hello),execve才会返回到调用程序,这里与调用一次返回两次的fork函数不同。
execve函数在加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数原型如下:
int main(int argc, char **argv, char *envp)
execve函数的执行过程会覆盖当前进程的地址空间,但并没有创建一个新进程。新的程序仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
6.5 Hello的进程执行
上下文:
内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
进程上下文切换:
在内核调度了一个新的进程运行时,它就抢占当前进程,并使用一种上下文切换的机制来控制转移到新的进程。具体过程为:①保存当前进程的上下文;②恢复某个先前被抢占的进程被保存的上下文;③将控制传递给这个新恢复的进程。
进程时间片:
一个进程执行它的控制流的一部分的每一个时间段叫做时间片,多任务也叫时间分片。
进程调度:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这种决策称为调度,是由内核中的调度器代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
hello程序调用sleep函数休眠时,内核将通过进程调度进行上下文切换,将控制转移到其他进程。当hello程序休眠结束后,进程调度使hello程序重新抢占内核,继续执行。
用户态与核心态的转换:
为了保证系统安全,需要限制应用程序所能访问的地址空间范围。因而存在用户态与核心态的划分,核心态拥有最高的访问权限,而用户态的访问权限会受到一些限制。处理器使用一个寄存器作为模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,一定程度上保证了系统的安全性。
hello程序进程执行示意图:
6-1 hello程序进程执行示意图
6.6 hello的异常与信号处理
6.6.1 异常的类型和处理流程
课本上主要总结了4种类型的异常和相应的处理流程:
中断
中断类型的异常来源于I/O设备的信号,是一种异步的异常,具体的处理流程如下:
6-2 中断
可以看到其总是返回下一条指令。
陷阱
陷阱是一种进程故意达成的状态,目的是为了进行系统调动,进入内核模式,其异常是同步的,具体处理流程如下:
6-3 陷阱
可以看到其总是返回下一条指令。
故障
产生故障的原因是因为当前进程发生了一些错误,而这种错误有可能可以被修复(例如缺页错误),也有可能不能被修复。这种异常是同步的,具体处理流程如下:
6-4 故障
可以看到,处理程序要么处理故障成功,返回当前导致故障的指令使其重新执行;要么处理故障失败,程序报错并且终止。
终止
程序终止的原因是因为发生了一个不可以被修复的错误,这种异常是同步的,且不会返回,具体流程如下:
6-5 终止
6.6.2 正常运行状态
我们输入运行命令,并且按照要求输入参数,观察程序正常运行的结果如下:
6-6 正常运行
可以看到,程序按照设定的间隔休眠,然后输出指定信息。
6.6.3 按下Ctrl+Z
根据课本上关于信号和进程的知识,我们可以知道,按下Ctrl+Z之后,进程会收到一个SIGSTP 信号,使得当前的hello进程被挂起。用ps指令查看其进程PID,可以发现hello的PID是1976;再用jobs查看此时hello的后台 job号是1,调用指令fg 1将其调回前台。
6-7 ctrl+z
6.6.4 按下Ctrl+C
根据课本上的相关知识,我们知道此时进程收到一个SIGINT 信号,一次结束 hello。我们输入ps指令,发现查询不到hello进程的PID,输入指令jobs,发现也没有对应作业,因此hello进程被彻底终止。
6-8 ctrl+c
6.6.5 中途乱按
此时的程序只会将其记录在输入缓冲区,不会影响程序的运行,但是如果在hello运行结束后还有乱按的指令在缓冲区的,那么就会被作为新的命令行输入。
6-9 乱按
6.6.6 Kill命令
通过kill指令向所在的挂起的进程发出终止指令,在此之后,通过ps指令无法找到对应的进程,对应的jobs指令也无法找到作业,说明进程已经被终止。
6-10 Kill
6.7本章小结
本章主要阐述了hello的进程管理,包括进程创建、加载、执行以至终止的全过程,并分析了执行过程中的异常、信号及其处理。在hello程序运行的过程中,内核对其进行进程管理,决定何时进行进程调度,在接收到不同的异常、信号时,还要及时地进行对应的处理。
结论
- hello程序历程总结
hello程序虽然是一个简短的C语言程序,但它的产生、执行、终止和回收离不开计算机系统各方面的协同工作,具体过程如下:
- hello.c源代码文件通过C语言预处理器的预处理,得到了调整、展开后的ASCII文本文件hello.i;
- hello.i经过编译器的编译得到汇编代码文件hello.s;
- hello.s经过汇编器的汇编得到可重定向目标文件hello.o;
- hello.o经过链接器的链接过程成为可执行目标文件hello;
- 用户在shell-bash中键入执行hello程序的命令后,shell-bash解释用户的命令,找到hello可执行目标文件并为其执行fork创建新进程;
- fork得到的新进程通过调用execve完成在其上下文中对hello程序的加载,hello开始执行;
- hello作为一个进程运行,接受内核的进程调度(调用sleep后内核进行上下文切换,调度其他进程执行);
- hello执行的过程中,可能发生缺页异常等故障、系统调用等陷阱以及接收到各种信号,这些都需要操作系统与硬件设备的协同工作进行处理;
- hello执行的过程中会访问其虚拟空间内的指令和数据,需要借助各种硬件、软件机制来快速、高效完成;
- hello运行时要调用printf、getchar等函数,这些函数的实现与Linux系统IO设备管理、Unix IO接口等息息相关;
- hello程序运行结束后,父进程shell-bash会进行回收,内核也会清除在内存中为其创建的各种数据结构和信息。
- 个人感悟
- 计算机系统的设计和实现大量体现了抽象的思想:文件是对I/O设备的抽象,虚拟内存是对主存和磁盘设备的抽象,进程是对处理器、主存和I/O设备的抽象,进程是操作系统对一个正在运行的程序的抽象等等;
- 计算机系统课程内容虽然繁多,但逻辑结构清晰、层次分明。完成本论文让我体会到:一个小小的hello程序就能展现计算机系统的方方面面;
- CSAPP这本书及计算机系统课程引领我从程序员的角度第一次系统地、全面地认识了现代操作系统的各种机制、设计和运行原理。我会在未来继续深入学习相关知识并在今后的具体实践中不断尝试使用和创新。
附件
hello.i:hello的预处理文件
hello.s:hello的编译文件
hello.o:hello的可重定位目标文件
hellooelf.txt:hello.o的elf信息
helloodump.txt:hello.o的反汇编信息
hello:hello的可执行目标文件
hello_elf.txt:hello的elf信息
hellodump.txt:hello的反汇编信息
参考文献
[1] 兰德尔 E.布莱恩特,大卫 R. 奥哈拉伦. 深入理解计算机系统[M]. 北京:机械工业出版社,2016
[2] 丹尼尔 P.博伟,马可·西斯特. 深入理解LINUX内核[M]. 北京:中国电力出版社,2007
[3] 眭俊华,刘慧娜,王建鑫,秦庆旺.多核多线程技术综述[J].计算机应用,2013
[4]贺伟.malloc函数在Linux系统下的原理性实现[J].福建电脑,2010