计算机系统大作业
程序人生-Hello’s P2P
第1章 概述
1.1 Hello简介
P2P:hello.c(Progam)在经过预处理、编译、汇编、链接等处理之后,成为一个可执行文件。shell在命令行接收到执行hello的命令,通过解析命令判断命令不是内置命令后,调用fork函数创建子进程(Process)hello,并调用execute函数在前台运行hello进程。
020:在命令行输入执行hello的命令,shell调用fork函数创建hello进程(0到有),并调用execute函数在前台运行它,执行结束或收到异常信号终止时,shell将其回收(有到0)。
1.2 环境与工具
X86-64 i5CPU、Ubuntu16.04
Vmware14.1.1.28517
gcc、readelf、objdump、edb
1.3 中间结果
hello.i:预处理得到的文件,为下一步编译作准备。
hello.s:编译得到的文件,为下一步汇编作准备。
hello.o:编译得到的文件,为下一步链接作准备。
hello:链接得到的文件,为执行作准备。
1.4 本章小结
通过对hello从文件到进程,执行到终止的一系列操作,从hello短暂的一生中窥见了计算机系统的组成及其功能应用等,对计算机系统有了系统粗略的认识。
第2章 预处理
2.1 预处理的概念与作用
预处理器(如cpp)根据以字符#开头的命令修改原始的C程序。主要有三个方面的内容:宏定义、文件包含和条件编译。宏定义格式为:[#define
标识符 文本],即将文本的内容赋给宏量标识符;文件包含格式为:[#include “文件名”] 或[#include <文件名>],即将文件的内容读取并插入到C程序文本中;条件编译格式为:[#ifdef 标识符//程序段1//#else//程序段2//#endif]或[#if表达式1//程序段1//#elif表达式2//程序段2……//#else程序段n//#endif]等,即当标识符已定义或表达式满足时,才会对相应代码段进行编译。预处理实际上就是为编译作准备工作。预处理结束后,会得到另一个C程序。通常是以.i作为文件拓展名。
2.2在Ubuntu下预处理的命令
![图2-2预处理结果](https://i-blog.csdnimg.cn/blog_migrate/eda4ed900486703dfaffe9b6e97869bb.png)
2.3 Hello的预处理结果解析
执行#include命令,将stdio.h、unistd.h和stdlib.h头文件的全部内容依次插入hello.c源代码之前,形成新的程序文件。
用gedit查看可以发现main函数已经变为3110行,前面即是一系列头文件的内容。
2.4 本章小结
#命令将hello塑造得更完整了,但hello依然是一片混沌……
预处理过程将以字符#开头的命令解析并修改原始C程序,如对于hello.c相应地将C程序中列出的头文件的内容插入形成新的C程序。为下一步编译作准备。
第3章 编译
3.1 编译的概念与作用
编译是指把用高级程序设计语言书写的源程序,翻译成等价的低级语言格式目标程序的过程。实际上是编译器通过翻译高级语言,代替程序员进行了低级语言的实现。对于C语言而言,是在编译器(如cc1)的处理下,将.i文件翻译成一个ASCII汇编语言.s文件。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1数据
全局变量sleepsecs:类型设置为.globl,位于.data节中。hello.c中对其定义如下:
由于二进制的原则,在计算机中将浮点数强制类型转化为整数,是向零取整,因此得到的sleepsecs实际值如下,为2,且对该数据作了对齐限制(4字节)。
格式串:两个格式串类型设置为.string,位于.rodata节中,在hello.c中的内容如下:
在节中以两条标签.LC0、.LC1作为main函数中的调用标志,对齐限制为8字节。
局部变量i:局部变量i是一个int整型数,其长度为4字节,因此将它放在-4(%rbp)的位置,即main函数执行后的第一个参数位置。
3.3.2赋值
局部变量i的赋值:用mov指令实现,即将立即数0赋给i。
3.3.3算术操作
局部变量i的加法:i++
在汇编语言中使用add指令,对局部变量i加上1,并将结果赋给局部变量i。
3.3.4关系操作
argc != 3:在汇编语言中,用jmp指令中的je来表示判断参数argc和3是否相等。
i < 10:在汇编语言中,用jmp指令中的jle来表示判断局部变量i是否小于等于9。
3.3.5数组
argv[]数组:参数为指向二维字符数组的argv指针。在函数中使用argv[1]和argv[2]。
对应于汇编语言,将argv首地址放在了-32(%rbp)位置处,读取时通过add指令加8、16分别获得argv[1]和argv[2]。
3.3.6控制转移
if的使用:在hello.c中表述如下:
则在汇编语言中,表述为相等时通过je指令来判断argc和3是否相等,相等时跳过if的内容;不等时不执行je指令,输出字符串并调用exit()函数。
for的使用:在hello.c的表述如下:
在汇编语言中,当循环初始化,即给i赋值为0之后,先跳转.L3,这部分为循环的控制条件“i<10”,即比较i是否小于等于9,满足则进入循环体.L4;不满足则结束循环。.L4中是循环体的内容的汇编语言,末尾是循环的变量改变,即用add指令使i加1。循环体结束后依然会回到.L3的控制条件,满足再次回到循环体,反之,则结束循环。
3.3.7函数参数
printf函数:
1.对于没有参数的printf函数,如下:
汇编语言会将其优化为puts函数,并将标签为.LC0的格式串,放入参数寄存器%edi中,然后用call指令调用puts函数。
2.对于有参数的printf函数,如下:
汇编语言将argv[2]、argv[1]依次放入参数寄存器%rdx、%rsi,再将标签为.LC1的格式串放入参数寄存器%edi中,因此,参数依次为格式串,argv[1],argv[2]。并将累加寄存器%rax(用于存放返回值)置0,然后用call指令调用printf函数。
exit函数:hello.c中如下:
在汇编语言中表述为,将1作为参数存入寄存器%edi中,再使用call指令调用exit函数。
sleep函数:hello.c中如下:
在汇编语言中表述为,将运行指针%rip存放的值加上sleepsecs的值,即2,得到的结果放入参数寄存器%edi,然后使用call指令调用sleep函数。
getchar函数:hello.c中如下:
由于函数不需要参数,因此可以直接使用call指令调用。
main函数返回:hello.c中如下:
在汇编语言中将0放入%eax作为返回值,然后使用leave、ret指令退出函数。
3.4 本章小结
在编译器的帮助下,hello逐渐从混沌变得有模有样了!
编译使hello从一个高级程序语言程序变成了一个低级语言程序,同时,通过标签和端节划分使得hello的内部更加井然有序,使其朝着能被机器“读懂”又迈出了一步。
第4章 汇编
4.1 汇编的概念与作用
汇编器(如as)将汇编语言翻译为机器语言,即用二进制编码汇编语言的过程称为汇编。其中,用操作码代替汇编语言中的助记符,用地址码代替汇编语言中的地址符号或标号,并根据机器的端模式决定编码序列。汇编将低级语言真正转换为机器语言,使得机器能够通过二进制序列“读懂”并执行程序员所需要的操作。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.4 Hello.o的结果解析
objdump -d
-r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
hello.o的结果如下:
hello.s的.text节结果如下:
对比可看出,对于.text节,即程序内容部分:
-
基本的顺序操作和函数参数准备操作在机器语言和汇编语言中是相对应的,机器语言使用二进制编码不同的操作命令和不同的寄存器,且由于小端法,每条指令都是按字节取反编码的。用十六进制查看时,就可看到这些命令和寄存器的按字节取反的十六进制表示法;
-
对于程序中的不同标签部分.L2~.L4,汇编时直接将其依次翻译为机器语言,不作换位处理,因此控制转移依靠jmp指令来进行即可。在汇编中使用标签作为跳转的依据,在机器语言中,直接采用偏移量进行跳转。
-
由于机器语言是二进制编码,汇编中使用的立即数(除有标签的格式串)也采用二进制编码的形式,用十六进制查看时,立即数采用的就是十六进制的表示法。
-
对于程序中的函数调用,汇编时采用重定位节.rela.text存放各项函数,在执行call的那条指令之后,通过加上.rela.text节中声明的偏移量标记来说明调用的是哪一个函数;
-
对于程序中的非局部变量数据,汇编时同样采用重定位节.rela.text存放,包括全局变量sleepsecs、两个在.s文件中标签为.LC0和.LC1的格式串。在程序中使用时,同样是在指令之后,加上.rela.text节中声明的偏移量标记来说明使用的是哪个数据。
4.5 本章小结
汇编器为hello穿戴整齐,可爱的hello即将出炉23333
通过汇编器,hello成为了一个机器语言程序,即一个二进制编码序列块。序列块被有目的地分成若干个清晰的节,程序内部的数据、代码和其他信息有组织地存在在同一个文件中,同时设置重定位节为外部函数或常量的链接等待进一步处理。为下一步链接作充分准备。
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程。这个文件可被加载(复制)到内存并执行。现代系统中使用链接器(如ld)自动实现链接。链接器是以一组可重定位目标文件和命令行参数为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。主要的任务是符号解析和重定位。链接得到的可执行文件可以被加载到内存中运行。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
使用edb加载hello,可看到datadump显示的就是5.3的可执行文件内容的编码十六进制显示格式。
运行hello时,每条指令的地址即为相应虚拟地址。
5.5 链接的重定位过程分析
hello的结果如下:
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
-
链接过程中加入了一个新的节.plt,在其中包含了所有源代码调用的外部函数,同时外部函数名后会加上“@plt”这一后缀。除了外部函数,还存放了__libc_start_main@plt函数,__libc_start_main函数用来启动__libc_cs u_init 函数。
-
另外,还增加了一个新的节.plt.got,其中包含一个小函数.plt.got,作用是跳转到数据段中的全局偏移量表GOT中。
-
plt是对动态链接的一种优化,称为延迟绑定。在每个,plt节中的函数里,第一条指令是通过数据段的全局偏移量表GOT,间接跳转到各外部函数相应的位置。但是为了实现延迟绑定,链接器在初始化阶段没有将函数地址填入GOT,而是将“push n”的地址填入到GOT中,所以第一条指令的效果是跳转到第二条指令,相当于没有进行任何操作。第二条指令将n压栈,接着跳转到.glt.got。通过压入的n查找相应的外部函数,然后将各函数的真实地址填入.plt节的各函数中。解析完毕后,当再次调用.plt节的各函数时,即可进入各函数的真正地址。
-
链接增加了一个新的节.init,并加入了_init函数,它位于crti.o文件中,它的执行对.dynamic节中的内容(包含GOT表)进行初始化。
-
在.text节中新增了_start、deregister_tm_clones、register_tm_clones、__do_global_dtors_aux、frame_dummy、__libc_csu_init、__libc_csu_fini函数,_start函数位于crt1.o文件中,是程序启动的最基本运行库函数,可调用__libc_start_main函数。__libc_csu_main函数将调用__libc_csu_init函数和main函数。__libc _csu_init函数先后执行_init和__init_array中定义的函数,__init_array中定义的函数包括frame_dummy函数。frame_dummy函数位于crtbegin.o文件中,目的是为异常处理解除堆栈帧进行设置,它会调用register_tm_clones函数。__do_global_dtors_aux函数遍历__CTOR_LIST__列表,调用列表中的每个构造函数。
-
链接增加了一个新的节.fini,_fini函数位于crtn.o文件中,是程序结束的最基本运行库函数。
5.6 hello的执行流程
调用<ld-2.23.so!_dl_start>,地址为0x7fe6ae1b09b0
调用<ld-2.23.so!_dl_init>,地址为0x7f28d0446740
跳转<hello!_start>,地址为0x4004d0
跳转<libc-2.23.so!__libc_start_main>,地址为0x7f5836831740
调用<libc-2.23.so!__cxa_atexit>,地址为0x7f8ec5185280
调用<hello!__libc_csu_init>,地址为0x400650
调用<hello!_init>,地址为0x40042f
调用<libc-2.23.so!_setjmp>,地址为0x7fc7c2536250
调用<hello!main>,地址为0x4005c6
调用hello!puts@plt,地址为0x400460
调用hello!exit@plt,地址为0x4004a0
调用hello!printf@plt,地址为0x400470
跳转<libc-2.23.so!printf>,地址为0x7f758687d800
调用hello!sleep@plt,地址为0x4004b0
跳转<libc-2.23.so!sleep>,地址为0x7f75868f4230
调用hello!getchar@plt,地址为0x400490
跳转<libc-2.23.so!getchar>,地址为0x7f758689e160
调用<libc-2.23.so!exit>,地址为0x7f7586862030
5.7 Hello的动态链接分析
在dl_init前后,全局偏移量发生了变化。
5.8 本章小结
链接器为hello送上最后的祝福,经历千辛万苦,hello终于在大家的期盼下诞生了!
链接器ld为hello与包含着能让hello在执行过程中正常开始以及结束的各函数的各可重定位文件搭桥牵线,使hello成为一个真正的可执行文件。同时,为了优化,hello通过在运行过程中修改GOT表动态链接相应的外部函数,从而使得链接更高效了。至此,hello真正地能够在进程空间中运行。
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义是一个执行中程序的实例。系统的每一个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态构成的。进程为应用程序提供的关键抽象主要是:一、一个独立的逻辑控制流,即一个当前程序独占地使用处理器的假象;二、一个私有的地址空间,即一个当前程序独占地使用内存系统的假象。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是一种应用程序,是计算机用来解释输入命令并决定如何处理的程序,是介于用户和内核间的一个接口。
处理流程:
-
从终端读入输入的命令。
-
将输入字符串切分获得所有的参数
-
如果是内置命令则立即执行
-
否则调用相应的程序为其分配子进程并运行
-
shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
父进程通过fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。
进程图如下:
6.4 Hello的execve过程
当fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
进程上下文:进程上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存中的代码和数据、它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
逻辑控制流:一系列唯一地对应于包含在程序的可执行文件中的指令,或包含在运行时动态链接到程序的共享对象中的指令的PC值序列,叫做逻辑控制流。
多任务:一个进程和其他进程轮流运行称为多任务,也叫时间分片。
用户态和核心态的转换:运行应用程序代码的进程初始时是在用户模式中的。处理器通常以某个描述进程权限的控制寄存器中的模式位来限制一个应用可以执行的指令及其可以访问的地址空间范围。没有设置模式位时,进程运行在用户模式中,受到限制,非法操作将引起保护故障。进程从用户模式转变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用等异常。当异常发生,控制传递给异常处理程序,处理器将模式从用户模式改为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器才把模式从内核模式转为用户模式。
进程调度:操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决策叫做调度。当内核调度了一个新的进程运行时后,它就抢占了当前进程,并保存当前进程的上下文,恢复新的进程被保存的上下文,将控制恢复给这个新的进程。
发生情况:内核代表用户执行系统调用或中断,都可能发生上下文切换。
6.6 hello的异常与信号处理
a. 当hello正常执行时,如下图,可知进程正常结束,并被正常回收。
b. 当在hello执行过程中先后按下回车和英文字符时,可看出,回车被getchar函数读取,同时之后所输入的字符被默认为是命令行语句放入命令行中。
c. 当在hello执行过程中只按下英文字符时,可看出,getchar函数不会读取。
d. 当在hello执行过程中打印第二条语句之后按下ctrl+c,shell进程接收到SIGINT信号,将hello进程终止。查看ps和jobs可知进程终止之后被shell回收了。
e. 当在hello执行过程中打印第四条语句之后按下ctrl+z,父进程shell接收到SIGSTP信号,打印进程停止信息,并将hello进程挂起。查看ps可发现进程停止而不是被回收,同时查看jobs可知其后台job号为1,使用指令fg 1,shell先打印jpb号为1的进程信息,然后将后台进程hello调至前台继续运行,最后回收。
再执行两个hello进程,并将其停止,我们用ps查看它们的PID。
使用kill指令处理PID为11404的hello进程,shell进程接收到SIGKILL信号,将PID为11404的hello进程杀死,并打印出相应信息,我们再次用ps和jobs查看可发现,PID为11404的hello进程确实已经被杀死。
使用pstree指令,可以获得一棵进程调度树。
6.7本章小结
linux十分繁华,在shell的管理下,hello从懵懵懂懂的孩童成为了兢兢业业地遵守秩序生活着的众多上班族之一,最后当它离去,shell也为它打理好了身后事。linux真是一个秩序井然又具有社会保障的好地方啊!
shell接收到(在前台)执行hello的命令,通过fork为hello创建进程,并execute使hello能够在前台运行,成为万千进程中的一员。成为进程的hello,对异常能作出及时的反应,并受到父进程shell的管理,直到终止后被父进程回收,结束它的进程之旅。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在汇编程序中的地址,是相对于你当前进程数据段的地址。逻辑地址由选择符(在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏移部分)组成。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
虚拟地址:线性地址在虚拟内存中生成的地址。对于hello而言,就是它的程序代码在虚拟内存中的位置。
物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。对于hello而言,就是它的程序代码在主存所存放的位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
早期的x86的CPU内部有20根地址线,能寻址2^20个地址(这些就是全部的线性地址),也就是1MB,但是其中的寄存器只有16位,只能寻址 2^16个地址,也就是64KB,这就是带来个问题,怎么利用寄存器的16位来寻址1MB呢?于是intel想出了一个办法,把寻址分为两部分,基地址和偏移地址. CPU要访问一个20位地址,比如这个地址是代码段地址,那么首先CPU要到CS(代码段寄存器)中取出基地址索引,找到基地址位置,然后再到IP寄存器中取出指令偏移量,组合成一个20位地址(这就是线性地址)然后寻址。
7.3 Hello的线性地址到物理地址的变换-页式管理
操作系统将虚拟内存分割为称为虚拟页的固定大小的块来管理,每个虚拟页大小为P = 2p字节,类似地,物理内存被分割成物理页,大小也为P字节,也被称为页帧。操作系统对某个虚拟页的缓存判断、物理映射以及替换需要软硬件联合操作。操作系统用内存管理单元MMU实现地址翻译,将虚拟地址转换为物理地址,然后读取页表。页表是虚拟页到物理页的映射。页表是一个页表条目PTE的数组。虚拟地址中每个页在页表中的一个固定偏移量都有一个PTE。PTE中的有效位表明了该虚拟页当前是否被缓存在DRAM(虚拟内存系统的缓存)中。如果设置了有效位,地址字段就表示DRAM中相应的物理页的起始位置。
7.4 TLB与四级页表支持下的VA到PA的变换
在Inter Core i7下,假设虚拟地址空间48位,物理地址空间52位,页表大小4KB,4级页表。TLB 4路16组相联。由一个页表大小4KB,一个PTE条目8B,共512个条目,使用9位二进制索引,则共使用36位二进制索引,那么VPN共36位。因为VA 48位,所以VPO 12位。因为TLB共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。
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中添加条目。如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
7.5 三级Cache支持下的物理内存访问
为了简洁,只讨论L1 Cache的寻址细节,L2与L3Cache原理相同。
L1 Cache是8路64组相联。块大小为64B。因为共64组,所以需要6bit CI进行组寻址,由8路相联,及块大小为64B,可知需要6bit CO表示数据偏移位置,因为VA共52bit,所以CT共40bit。
由已经获得的物理地址VA,使用CI(末尾6位)进行组索引,每组8路,对8路的块分别匹配CT(前40位),如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后6位)取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据,依次为L2、L3、主存。查询到数据之后,考虑放置策略。例如:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用LRU策略进行替换。
参考图同上。
7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为hello程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域, hello程序与共享对象或目标(如libc.so)链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。
缺页中断处理:缺页异常导致控制转移到内核的缺页处理程序,处理程序执行以下步骤:
-
判断虚拟地址VA是否合法,否则引发段错误;
-
判断试图进行的内存访问是否合法,否则引发保护异常;
-
以上条件均满足则通过替换算法选择一个牺牲页,如果这个牺牲页被修改过,将它交换出去,换入新页并更新页表。
当处理程序返回时,CPU重新启动引起缺页的命令,再次将虚拟地址发送给MMU。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一段连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直至它被释放,这种释放要么是应用程序显式地执行的,要么是内存分配器自身隐式地执行的。
分配器可分为显式分配器和隐式分配器。
对于显式分配器,解决放置、分割、空闲块组织等问题可分为以下方法:
-
使用边界标记思想的隐式空闲链表;
-
显式空闲链表。
-
分离存储空闲链表
对于隐式分配器,也被称为垃圾收集器,主要思路是将内存视为一张有向可达图。典型方法为Mark&Sweep垃圾处理器。
7.10本章小结
shell的好助手MMU为hello管控着它自己的财产,hello也有自己的家咯。
hello在进程空间中也拥有自己私有的地址空间,物理地址真实地存放着它的代码、数据和其他信息,虚拟地址是它的物理地址的映射。在运行过程中,当需要访问它的这些信息时,操作系统通过页式管理查询到相应的页表位置,并进入缓存中查询数据。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
2.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
3.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:
1.int
open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2.int
close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
3.ssize_t
read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4.ssize_t
wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
观察printf函数的内容,我们可以知道,实现printf需要调用vsprintf和write函数。vsprintf的作用实际上是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出buf。
write函数的功能是将结果buf以i的长度打印在控制台上。观察write函数的内容,可知函数将寄存器压入栈中作为参数,然后通过INT_VECTOR_SYS_CALL调用系统函数syscall。
观察sys_call函数内容,call save保存中断前进程的状态,ecx中是要打印出的元素个数,ebx中的是要打印的buf字符数组中的第一个元素,它采用直接写显存的方法在显存中存储提供给它的字符串的ASCII码。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是字符串被打印在屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
为了维护秩序,hello在linux中的诸多事宜(如IO实现)也需要按照规定的方式进行,hello需要遵循的规则还有很多呀……
hello在运行过程中涉及到IO接口操作的部分,如printf、getchar函数,都需要通过系统调用以及硬件配合交互实现安全的IO输入输出。
结论
hello的历程:
hello从C文件开始,经过预处理得到hello.i文件;
经过编译,由hello.i文件得到hello.s文件;
经过汇编,由hello.s文件得到hello.o文件;
经过链接,由hello.o文件得到hello可执行文件;
在shell的命令行输入./hello命令,shell调用fork函数创建hello进程,并调用execute函数在让hello进程在前台运行,作为一个子进程,hello可以正常地接收异常信号并及时作出反应,同时也受shell的管理;
运行过程中,由hello的虚拟地址,通过MMU和页表的交互访问hello的物理地址获取相应的数据;
运行过程中,hello的IO操作需要系统调用函数实现安全的IO输入输出;
运行结束或收到信号终止之后,由shell负责回收。
附件
hello.i:预处理得到的文件,为下一步编译作准备。
hello.s:编译得到的文件,为下一步汇编作准备。
hello.o:编译得到的文件,为下一步链接作准备。
hello:链接得到的文件,为执行作准备。
参考文献
[1] Randal E.Bryant.David R.O’Hallaron.深入理解计算机系统. 北京:机械工业出版社,ISBN 978-7-111-54493-7
[2] 浅析ELF中的GOT与PLT. posted @
2017-03-28 23:53:51 作者:_well_s
https://blog.csdn.net/u011987514/article/details/67716639
[3] 内存管理第一谈:段式管理和页式管理. 2015-02-07 20:45:56
作者:浓咖啡jy
https://blog.csdn.net/jy1075518049/article/details/43610569
[4] linux编程之main()函数启动过程. posted @ 2013-01-15 15:36:57
作者:gary_ygl
https://blog.csdn.net/gary_ygl/article/details/8506007
[5] _start,
_init and frame_dummy functions.
[6] Linux
x86 Program Start Up or - How the heck do we get to main()? by Patrick Horgan.
dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html
p.s 应该会存在一些小错误哈