计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 1190201123
班 级 1903006
学 生 张程期
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
摘 要
文章主要介绍了hello程序在Linux系统的“一生”,即hello程序从.c文件一步步预处理、编译、汇编、链接、最后生成可执行文件的过程,实现了P2P的过程——“From Program to Process”。除此以外,还详细的探索了我们的计算机系统是如何对hello程序进行进程管理、存储管理、I/O管理,深入理解了hello程序的生命周期,在这一过程中对计算机系统有更深的理解。
关键词:hello程序;P2P;020;预处理;编译;汇编;链接;进程;存储;I/O;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
第1章 概述
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
1.1 Hello简介
P2P过程:P2P,即“From Program to Process”。指hello程序在Linux系统中由程序变成进程的过程。在Linux系统中,hello.c文件经过cpp的预处理、ccl的编译、as的汇编、ld的链接,最终成为了可执行程序。在shell中输入启动命令后,shell为其frok一个子进程,hello启动,成为了一个真正的进程。
020过程:020,即“From zero to zero”。指的是在进程中hello从零开始,一步步结束自己的工作,最后被shell回收的过程。shell为hello进程execve,映射虚拟内存,然后载入物理内存,CPU为该进程分配时间片执行逻辑控制流,接着进入main函数执行代码,待程序执行结束后,shell回收hello进程,内核删除相关数据结构。
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上;
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位;
开发与调试工具:gcc;gdb;edb;readelf;HexEdit;
1.3 中间结果
文件名称 | 文件作用 |
hello.i | hello.c经过预处理后的文件 |
hello.s | hello.i经过编译后的汇编文件 |
hello.o | hello.s经过汇编后的可重定位目标文件 |
hello | hello.o经过链接后的可执行目标文件 |
表1 输出文件名称及其作用
1.4 本章小结
本章简要介绍了hello的P2P,020过程,列出了实验的环境和工具,列出了本次实验所生成的中间结果文件,以及各文件的作用。
(第1章0.5分)
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
第2章 预处理
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
2.1 预处理的概念与作用
预处理的概念:
在程序编译之前,预处理器cpp根据以字‘#’开头的命令进行处理,修改原始的C程序,生成一个新的文本文件的过程。
预处理的作用:
①处理头文件。头文件指令如#include等,在预处理阶段将会将头文件中的定义统统加入到.i文件中。
②处理宏定义。用实际值替换#define定义的宏定义,进行宏替换。
③处理条件编译指令。例如#if,#ifdef,#ifndef,#else,#elif,#endif等指令,根据条件保留需要编译的代码,去除不需要的代码。
④处理特殊符号。预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
在终端中输入预处理命令:gcc hello.c -E -o hello.i,如图1所示
图1 输入预处理命令
生成了hello.i文件,如图2所示:
图2 生成hello.i文件示意图
2.3 Hello的预处理结果解析
打开hello.i文件,发现仍为C语言程序,内容扩充到了3060行,并且注释部分被删去,如图3所示:
图3 hello.i最后一部分示意图
在hello.i中,发现stido.h文件被展开写在了第13行开始的部分,如图4所示:
图4 stido.h展开示意图
另外,如果C文件中有#define宏定义会进行宏替换,hello.c中没有宏定义,所以在hello.i中没有体现出这一点。
2.4 本章小结
本章探索了hello程序生命周期中的预处理阶段,hello.c文件经过预处理后,生成了更易于编译器理解的hello.i文件。
(以下格式自行编排,编辑时删除)
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译器ccl将文本文件hello.i翻译成汇编语言程序hello.s的过程。
编译的作用:
通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码,便于后续生成机器代码。
3.2 在Ubuntu下编译的命令
在终端中输入命令:gcc -S hello.i -o hello.s,如图5所示:
图5 输入编译命令
生成的hello.s文件如图6所示:
图6 编译生成hello.s文件
3.3 Hello的编译结果解析
3.3.1汇编代码段介绍
汇编代码段介绍如图7所示:
图7 汇编代码介绍
3.3.2 数据
①字符串
从汇编代码可以看出程序声明了两个字符串,存储在只读数据段中,如图8所示:
图8 字符串声明
这两个字符串分别作为printf函数的参数,如图9、10所示:
图9 LC0作用 图10 LC1作用
②局部变量i
main函数中声明了一个整型局部变量i,从图11汇编代码中可以看出i存放在堆栈中,具体放在了rbp-4的位置上。
图11 局部变量i的存储
③立即数
立即数直接存放在汇编代码中,例如图11中将0赋值给i。
④main函数整型参数argc
参数argc被放在堆栈中,具体放在rbp-20的位置,如图12所示:
图12 argc存放
⑤main函数字符数组参数argv[]
数组argv[]存放在堆栈中,具体存放在rbp-32的位置,如图13所示:
图13 argv存放
3.3.3 全局函数
hello.c声明了一个全局函数int main(int argc,char *argv[])。
由图14可知该函数是一个全局函数
图14 main函数类型声明
3.3.4 赋值操作
hello.c中需要赋值的地方为i = 0。汇编代码中通过movl指令来实现,赋值操作如图15所示:
图15 i的赋值
除了movl以外,还有不通的后缀有不同的功能,如表2所示:
指令 | 功能 |
movb | 一个字节 |
movw | 两个字节 |
movl | 四个字节 |
movq | 八个字节 |
表2 mov不同后缀功能表
3.3.5 类型转换
hello.c中的类型转换为:atoi(argv[3])。调用了atoi函数,在汇编代码中体现为图16:
图16 类型转换
3.3.6 算术操作
如表3,汇编指令中基本算术操作如下:
指令 | 效果 | 描述 |
leaq S,D | D⬅&S | 加载有效地址 |
INC D | D⬅D+1 | 加一 |
DEC D | D⬅D-1 | 减一 |
NEG D | D⬅-D | 取负 |
NOT D | D⬅~D | 取补 |
ADD S,D | D⬅D+S | 相加 |
SUB S,D | D⬅D-S | 相减 |
IMUL S,D | D⬅D*S | 相乘 |
表3 汇编指令基本算术操作
在hello.c中,i++调用了addl,加了立即数1,如图17所示。
图17 i++的实现
3.3.7 关系操作
关系操作主要使用两条指令:cmp和test。
根据cmp和test结果,使用相应的跳转指令,跳转到相应的位置,常用跳转指令如表4所示。
JE | 等于则跳转 |
JNE | 不等于则跳转 |
JZ | 为 0 则跳转 |
JNZ | 不为 0 则跳转 |
JS | 为负则跳转 |
JNS | 不为负则跳转 |
JC | 进位则跳转 |
JNC | 不进位则跳转 |
JO | 溢出则跳转 |
JNO | 不溢出则跳转 |
JA | 无符号大于则跳转 |
JNA | 无符号不大于则跳转 |
JAE | 无符号大于等于则跳转 |
JNAE | 无符号不大于等于则跳转 |
JG | 有符号大于则跳转 |
JNG | 有符号不大于则跳转 |
JGE | 有符号大于等于则跳转 |
JNGE | 有符号不大于等于则跳转 |
JB | 无符号小于则跳转 |
JNB | 无符号不小于则跳转 |
JBE | 无符号小于等于则跳转 |
JNBE | 无符号不小于等于则跳转 |
JL | 有符号小于则跳转 |
JNL | 有符号不小于则跳转 |
JLE | 有符号小于等于则跳转 |
JNLE | 有符号不小于等于则跳转 |
JP | 奇偶位置位则跳转 |
JNP | 奇偶位清除则跳转 |
JPE | 奇偶位相等则跳转 |
JPO | 奇偶位不等则跳转 |
表4 常用跳转指令
hello中有两处关系操作:
①判断argc!=4
如图18所示,比较argc和4,若相等则跳转到.L2,若不等则输出一些东西后调用exit函数。
图18 argc!=4的汇编代码实现
②判断i<8
如图19所示i与8比较,若有符号小于等于,则跳转到.L4,否则将向下执行汇编代码。
图19 i<8的汇编代码实现
3.3.8 数组/指针/结构
程序中只有一个数组char *argv[],存储在堆栈中,在3.3.2的④中已经介绍过了。具体存储如图13所示。
3.3.9 控制转移
控制转移紧跟着关系操作,判断argc!=4与i<8分别如图18、19所示,常用跳转指令表如表4所示。
3.3.10 函数操作
①返回值
函数的返回值通常保存在rax中,main函数返回如图20所示。
图20 函数返回值
②参数传递
函数参数按照顺序存储在寄存器%rdi,%rsi,%rdx,%rcx,%r8,%r9中,再多出的参数存储在栈中。hello中pirntf函数和exit函数的参数传递如图21所示。
图21 参数传递
除此以外,还有sleep函数的参数为atoi(argv[3]),main函数的参数为argc和argv[],分别用%rdi和%rsi传递。
③分配和释放内存
函数所需内存的内存提前分配,并且在函数结束后释放这些内存。
3.4 本章小结
本章探索了编译器ccl将hello.i编译为hello.s发生的变化,理解了汇编语言的作用,体会到了编译器的机制。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
汇编器as将汇编语言指令翻译成机器语言指令,利用.s文件生成.o文件(可重定位目标文件)的过程。
汇编的作用:
将汇编语言指令翻译成计算机可识别的机器语言指令,生成一个可重定位目标文件。为后续计算机的识别作铺垫。
4.2 在Ubuntu下汇编的命令
在终端中输入指令:gcc hello.s -c -o hello.o,如图22所示。
图22 汇编指令输入
生成的hello.o文件如图23所示:
图23 生成的hello.o文件
4.3 可重定位目标elf格式
①ELF头
如图24所示,以一个16字节的字节序列开始,描述了生成该文件的系统字的大小和字节顺序。下面的信息为帮助链接器语法分析和解释目标文件的信息。包括:ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移、节头部表中条目的大小和数量等信息。
图24 ELF头
②节头
如图25所示,节头显示了各个节的名字、类型、位置、大小等信息。
图25 节头
③重定位节
重定位节保存着代码的重定位条目。如图26,hello.o有两个重定位节,分别为.rela.text和.rela.eh_frame。
图26 重定位节
重定位条目格式,各符号的信息如表5所示。
Offset | 需要被修改的引用节的偏移 |
Info | 包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节, |
symbol | 标识被修改引用应该指向的符号, |
type | 重定位的类型 |
Type | 告知链接器应该如何修改新的应用 |
Attend | 一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整 |
Name | 重定向到的目标的名称。 |
表5 重定位条目格式
④符号表
如图27所示,符号表存放引用的函数和全局变量的信息。Value表示对于目标节的起始位置偏移,Size表示目标的大小,Type表示目标是变量或者函数,Bind表示符号是本地的还是全局的。
图27 符号表
4.4 Hello.o的结果解析
图28 反汇编代码
图29 hello.s
机器语言的构成
机器语言由若干个二进制字节构成,其中包括操作码和地址码两种字节,操作码给出了操作的具体功能,地址码给出了进行操作的地址。
映射关系
机器语言与汇编语言是可以一一映射的,机器语言就是一种计算机易于理解的语言,而汇编语言对我们更友好,更易读懂。
两者的差别
①分支转移:汇编代码中分支转移用.L0等符号。而反汇编代码中,分支转移表示为主函数加段内偏移量。
②函数调用:汇编代码中直接使用函数名。而反汇编代码中,函数调用使用主函数加偏移量,并且在.rela.text节中为该函数添加重定位条目等待链接。
③访问全局变量:汇编代码中用.LC0等符号。而反汇编代码中为0x0,需要重定位后才能显示具体地址。
4.5 本章小结
本章探索了hello程序的汇编阶段,从hello.s汇编成hello.o,分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代 码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。
链接的作用:
链接器使得分离编译成为可能,生成的可执行文件可以直接加载到内存执行。
5.2 在Ubuntu下链接的命令
在终端输入指令如下,如图30所示:
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/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
图30 链接指令
链接生成的可执行文件hello如图31所示:
图30 可执行文件hello
在终端中运行hello如图31所示:
图31 hello运行结果
5.3 可执行目标文件hello的格式
①ELF头
由图32可以看出,文件类型变成了可执行文件,并且有27个节头
图32 ELF头
②节头
节头部表存储了各个节的名称、大小、类型、地址、偏移量等信息。
图33 节头
③重定位节
重定位节保存着代码的重定位条目。如图34,hello有两个重定位节,分别为.rela.dyn和.rela.plt。
图34 重定位节
④符号表
如图35所示,符号表存放引用的函数和全局变量的信息。Value表示对于目标节的起始位置偏移,Size表示目标的大小,Type表示目标是变量或者函数,Bind表示符号是本地的还是全局的。
图35 符号表
5.4 hello的虚拟地址空间
用edb打开hello后,可以看到hello的虚拟地址空间起始地址为0x401000,如图36,结束地址为0x401ff0,如图37。
图36 hello的起始位置
图37 hello的结束位置
接着根据节头部表的地址信息,可以在edb中查看各节的位置。
例如节头部表中.rodata的位置为0x402000,大小为0x3b,在edb中查看该位置即可找到.rodata。如图38所示
图38 查看.radata信息
按照这样的方式查看每一个节,发现都符合。
5.5 链接的重定位过程分析
5.5.1 两者的不同
在终端中输入objdump -d -r hello,得到hello反汇编main函数结果如图39所示,其他部分截图如图40所示。对比hello.o的反汇编结果如图28所示。
图39 hello的main函数反汇编结果
图40 hello部分反汇编结果
①可以看到hello.o的反汇编代码只有.text一个节,且只实现了main函数,而hello的反汇编代码中增加了.plt,.init,.fini节,且各个函数都有了其具体实现。
②hello.o的main函数是从地址0开始的,未完成重定位,调用函数是直接call+函数名实现的。而hello的main函数有了其具体的地址,函数的调用也是用call+地址+地址偏移量实现的。
5.5.2 重定位分析
①重定位段和符号定义:在这个步骤,链接器将所有相同类型的段都整合进入一个新的聚合的段中。例如,所有的输入模块中的.data段都会被整合进入输出的可执行文件的.data段中。链接器接下来为新生成的段,原模块中的段以及原模块中的符号赋予运行时地址。当这步完成后,程序中的每一个指令和全局变量都有了一个独一无二的运行时地址内存地址。
②用段重定位符号引用:在这步中,链接器会修改代码和数据段中的每个符号引用,这时符号引用会指向正确的运行时地址。为了执行这步,链接器依赖一种在可重定位目标文件中的数据结构,称为重定位实体(relocation entries),如图41所示。
图41 重定位实体
图42 重定位算法伪代码
5.6 hello的执行流程
hello的执行流程如表6所示
程序名 | 地址 |
ld-2.29so!_dl_start | 0x7f810f43b048 |
ld-2.29so!_dl_init | 0x7f810f41ec10 |
hello!_start | 0x4010f0 |
libc-2.29so!__libc_start_main | 0x7f810f224fc0 |
libc-2.29so!__cxa_atexit | 0x7f810f247f60 |
hello!__libc_csu_init | 0x4011c0 |
hello!_init | 0x401000 |
libc-2.29so!_setjmp | 0x7f810f243e00 |
libc-2.29so!__sigsetjmp | 0x7f810f243d30 |
hello!main | 0x401125 |
hello!printf@plt | 0x401040 |
hello!atoi@plt | 0x401060 |
hello!sleep@plt | 0x401080 |
hello!getchar@plt | 0x401050 |
hello!exit@plt | 0x401070 |
libc-2.27so!exit | 0x7f810f247bc0 |
表6 hello执行流程
5.7 Hello的动态链接分析
为了得到函数运行的地址,避免修改调用函数的代码段,链接器需要采用延迟绑定的方式进行动态链接。
延迟绑定需要全局变量偏移表(GOT)和过程链接表(PLT)共同支持实现。当一个目标模块调用共享库中的某个函数时,需要在数据段找到它的GOT,找到函数的目标地址,然后在代码段运行他的PLT,跳转到目标函数。在加载时,动态链接器会重定位GOT中的条目,以保证目标地址的正确。
在终端中用readelf查看.got.plt表的起始地址,如图43所示
图43 .got.plt表起始地址
在不运行前,进入edb查看该地址(0x404000)下的信息,如图44所示
图44 GOT表信息
可以看出起始数据0x403e50后的16个字节皆为0。
运行到main函数后,再查看表中的信息,如图45所示
图45 执行init后GOT表信息
可以看出0x403e50后面多了一个0x7fdad4ca1190和0x7fdad4c8abb0两个地址,分别为GOT[1]和GOT[2]。GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。
进入0x7fdad4c8abb0,在edb中查看共享库模块,如图46所示
图46 共享库模块
通过这样的方法,实现了动态链接。
5.8 本章小结
本章探索了hello生命中链接的过程,讨论了hello.o是如何通过链接生成hello可执行文件的。分析了hello的虚拟地址空间、重定位、以及动态链接分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程是执行中的程序实例。系统中的每个程序都运行在某个进程的上下文中。
进程的作用:
进程向我们提供一个假象,就好像我们地程序是系统中当前运行地唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条的执行我们程序地指令。我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:
shell是一个交互型应用级程序,代表用户运行其他程序。bash是变种、缺省的Linux shell。
处理流程:
1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2. 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3. 当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有從處理的結果中用到的注释删除,並且按照下面的顺序实行命令的检查:
A. 内建的命令
B. shell函数(由用户自己定义的)
C. 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令。
6.3 Hello的fork进程创建过程
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
对于hello来说,当我们在终端中输入./hello 1190201123 张程期 1时,shell会对我们的命令进行解析,由于我们的命令不是内置命令,所以shell此时会调用fork创建一个子进程。
6.4 Hello的execve过程
①在当前进程中载入并运行程序,传入三个参数
filename:包含准备载入当前进程空间的新程序的路径名。既可以是绝对路径,又可以是相对路径。
argv[]:指定了传给新进程的命令行参数,该数组对应于c语言main函数的argv参数数组,格式也相同,argv[0]对应命令名,通常情况下该值与filename中的basename(就是绝对路径的最后一个)相同。
envp[]:最后一个参数envp指定了新程序的环境列表。参数envp对应于新程序的environ数组。
②Loader删除子进程现有的虚拟内存段,创建一组新的段(栈与堆初始化为0),并将虚拟地址空间中的页映射到可执行文件的页大小的片chunk,新的代码与数据段被初始化为可执行文件的内容,然后跳到_start………… 除了一些头部信息实际没读文件,直到缺页中断。加载器映射用户地址区域如图47所示。
图47 加载器映射用户地址区域
③覆盖当前进程的代码、数据、栈。
④由于是将调用进程取而代之,因此对execve的调用将永远不能返回,也无需检查它的返回值,因为该值始终为-1,实际上,一旦返回就表明了错误,通常会有error值来判断。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
首先介绍上下文信息和进程时间片:
上下文信息:
内核为每个进程维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
进程时间片:
一个进程执行他的控制流的时间段。
接下来阐述进程调度的过程,用户态与核心态转换。
进程调度过程:
操作系统内核采用一种叫上下文切换的形式来管理进程调度。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换
1) 保存当前进程的上下文。
2) 恢复某个先前被抢占的进程被保存的上下文
3) 将控制传递给这个新恢复的进程。
用户态与核心态的转换:
用户态与核心态的转换如图48所示。
图48 用户态与核心态的转换
6.6 hello的异常与信号处理
1.异常
可能出现的异常如下:
①陷阱,例如执行了sleep,exit等系统调用。
②中断,可能出现外部I/O设备异常导致的中断。
③故障,例如缺页故障。
④终止,出现了不可恢复的错误。
2.信号
可能导致出现信号如下情况
①ctrl-z
信号:发送一个SIGTSTP信号到前台进程组中的每个进程,挂起前台作业。
若在hello程序运行时输入ctrl-z,如图49所示
图49 在程序执行时输入ctrl-z
此时查看ps和jobs如图50所示
图50 查看ps和jobs
此时发现hello进程已经被挂起。
若此时输入fg命令,进程收到一个SIGCONT信号,hello继续执行。
图51 输入fg命令
②ctrl-c
在进程执行时输入ctrl-c,内核会发送一个SIGINT信号到前台进程组的每个进程,终止前台作业。
图52 输入ctrl-c
此时再输入ps查看,发现没有hello进程。
图53 查看ps
③乱按
如图54,乱按只会输出到终端界面,不会影响进程。
图54 乱按
6.7本章小结
本章探索了hello进程在shell下被创建,控制的过程,以及hello进程收到各种信号、异常的处理方式。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:由段标识符+段内偏移量组成,与物理地址无关,是hello程序编译为hello.o中的相对偏移地址。
线性地址:线性地址是逻辑地址到物理地址变换的中间层。逻辑地址加上相应的基地址就成为了线性地址,如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。
虚拟地址:与线性地址相同。
物理地址:物理内存中对应的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
实模式下: 逻辑地址CS:EA =物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址, 段地址+偏移地址=线性地址。
图55 逻辑地址转化为线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
在页式管理下,虚拟内存被分割成称为虚拟页(VP)的节。类似地,物理内存被分割为物理页(PP)。每个虚拟页或物理页大小为P= 2p字节。每个虚拟(线性)地址对应某个虚拟页面的某个字节。
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。这一过程如图56所示。
图56 使用页表的地址翻译
根据虚拟地址访问某个页示意图如图57所示
图57 根据虚拟地址访问页面
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会要求从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在Ll中,那么开销就下降到1个或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在 MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器( Translation Lookaside Buffer,TLB)。
如图58,使用TLB进行地址翻译时,会将VPN解释为两部分:TLB标记,与TLB索引。
图58 虚拟地址中用以访问TLB的组成部分
图59描述了使用k级页表层次结构的地址翻译。虚拟地址被划分成为k个VPN和1个 VPO。每个VPN i都是一个到第i级页表的索引,其中 1≤i≤k。第j级页表中的每个PTE,1≤j≤k一l,都指向第j+1级的某个页表的基址。第k级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。对于只有一级的页表结构,PPO和VPO是相同的。
图59 使用k级页表的地址翻译
解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA。
7.5 三级Cache支持下的物理内存访问
得到物理地址PA后,根据cache,将PA分为CT(标记位)、CS(组号)、CO(偏移量)三部分。然后根据CS寻找到对应的组,比较每一个cacheline是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次访问L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache。
图60 采用四级页表和三级cache的core i7翻译
7.6 hello进程fork时的内存映射
当fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
hello进程被execve函数加载并运行分为以下几个步骤:
①删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
②映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为a.out文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。图61概括了私有区域的不同映射。
③映射共享区域。如果 a.out程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
④设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
图61 加载器映射用户空间地址区域
7.8 缺页故障与缺页中断处理
缺页处理流程如下:
第1步:处理器生成一个虚拟地址,并把它传送给MMU。
第2步:MMU生成PTE地址,并从高速缓存/主存请求得到它。
第3步:高速缓存/主存向MMU返回PTE。
第4步:PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
第6步:缺页处理程序页面调入新的页面,并更新内存中的PTE。
第7步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,在 MMU执行了图62中的步骤之后,主存就会将所请求字返回给处理器。
图62 缺页处理
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)(见图63)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。
图62 堆
分配器有两种风格:
显式分配器( explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和 delete操作符与C中的malloc和 free相当。
隐式分配器(implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器( garbage collec-tor),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
分配方式实现:
1. 隐式空闲链表
隐式链表堆块结构如图63所示:
图62 隐式空闲链表堆块结构
利用这样的堆块结构,构成链表如图63所示
图63 隐式空闲链表结构
我们称这种结构为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
2. 显式空闲链表
显式空闲链表堆块结构如图64所示,堆被组织成为了双向的空闲链表,对于空闲块有其前驱和后继指针。
图64 显式空闲链表堆块结构
7.10本章小结
本章探索了hello程序的存储空间,各种地址之间的转换,内存访问,内存映射,缺页故障或中断的处理,以及动态内存分配器的存储管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件。
设备管理:所有的输人和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
接口:
①打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
②Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件< unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
③改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
④读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。
类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新é。
⑤关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
函数:
1.int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
返回:若成功则为新文件描述符,若出错为-1。
2.int close(int fd);
返回:若成功则为0, 若出错则为-1。
3.ssize_t read(int fd, void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。
返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。
4.ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。
返回:若成功则为写的字节数,若出错则为-1。
8.3 printf的实现分析
首先查看printf的源码:
图65 printf源码
再看vsprintf的源码:
图66 vsprintf的源码
可以看出arg是传入的参数,也就是我们待打印的信息,vsprintf返回的是待打印字符串的长度,然后紧跟着就是printf里面的write(buf,i),于是我们取看write的实现:
图67 write的实现
不难看出,write的作用就是将字符串打印到屏幕上,wirte内部调用了sys_call函数,我们来看sys_call的实现:
图68 sys_call的实现
可以看出,sys_call的作用为打印出遇到\0之前的所有字符。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数的功能为:
当程序调用getchar时,程序就等着用户按键.用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
函数源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | int getchar(void) { static char buf[BUFSIZ]; static char *bb = buf; static int n = 0; if(n == 0) { n = read(0, buf, BUFSIZ); bb = buf; } return(--n >= 0)?(unsigned char) *bb++ : EOF; } |
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章探索了linux下IO设备及管理方法、UNIX IO接口及其函数、PRINTF和GETCHAR函数的实现分析。
(第8章1分)
结论
hello的一生经历了如下阶段:
①hello.c经过预编译,得到了与处理过的hello.i文件。
②hello.i经过编译,得到了汇编代码文件hello.s。
③hello.s经过汇编,得到了二进制可重定位目标文件hello.o。
④hello.o经过连接,生成了可执行文件hello。
⑤shell调用了fork函数,为hello程序创建了一个子进程,并调用execve函数对hello程序进行了加载,最后执行了hello程序,在进程执行阶段,hello接受了很多信号和异常。
⑥hello在执行时,系统对其进行了存储管理,通过动态内存分配器为其分配了内存。
⑦hello在执行时,调用了printf等IO函数。
⑧hello在执行结束后,被父进程shell回收,内核回收了其相关的信息。
我的感悟:
经过一学期的学习,可以说对我们的代码时怎么跑起来的有了更深刻的感悟与理解,这不仅是对好奇心的满足,也对后续代码的编写能有更深的理解,避免很多错误,少走很多弯路,可以说是受益匪浅。
计算机的设计是如此的全面和周到,很难想象我们在计算机上做的一点点小小的改动其背后都有着复杂的逻辑,这是一代代人对计算机系统的深入理解的结果,很难想象逻辑的潜力有多么巨大。
(结论0分,缺失 -1分,根据内容酌情加分)
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/86b2c2ff8df79c75021771eee85569c6.gif)
附件
文件名称 | 文件作用 |
hello.i | hello.c经过预处理后的文件 |
hello.s | hello.i经过编译后的汇编文件 |
hello.o | hello.s经过汇编后的可重定位目标文件 |
hello | hello.o经过链接后的可执行目标文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] 博客园 printf函数实现的深入剖析
[3] CSDN 浅谈getchar()函数
[4] CSDN fork函数与execve函数
[5] CSDN 动态链接的步骤与实现
[6] CSDN 汇编语言算术操作
(参考文献0分,缺失 -1分)