大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号
班 级
学 生
指 导 教 师
计算机科学与技术学院
2022年5月
摘 要
本文通过hello小程序的一生,对我们所学的CSAPP课程进行全面的梳理与回顾。对hello程序的整个生命周期进行了系统的分析,从hello.c源程序开始,经过预处理、编译、汇编、链接、创建进程、加载hello…最后结束进程并由父进程进行回收这一系列分析,对于hello在计算机系统的一生有深入的理解。
关键词: 计算机系统;程序人生;P2P;020;
文章目录
- 第1章 概述
-
- 1.1 Hello简介
- 1.2 环境与工具
- 1.3 中间结果
- 1.4 本章小结
- 第2章 预处理
-
- 2.1 预处理的概念与作用
- 2.2在Ubuntu下预处理的命令
- 2.3 Hello的预处理结果解析
- 2.4 本章小结
- 第3章 编译
-
- 3.1 编译的概念与作用
- 3.2 在Ubuntu下编译的命令
- 3.3 Hello的编译结果解析
- 3.4 本章小结
- 第4章 汇编
-
- 4.1 汇编的概念与作用
- 4.2 在Ubuntu下汇编的命令
- 4.3 可重定位目标elf格式
- 4.4 Hello.o的结果解析
- 4.5 本章小结![在这里插入图片描述](https://img-blog.csdnimg.cn/0c0e582ef7c540b3bfb539b8799607a6.png)
- 第5章 链接
-
- 5.1 链接的概念与作用
- 5.2 在Ubuntu下链接的命令
- 5.3 可执行目标文件hello的格式
- 5.4 hello的虚拟地址空间
- 5.5 链接的重定位过程分析
- 5.6 hello的执行流程
- 5.7 Hello的动态链接分析
- 5.8 本章小结
- 第6章 hello进程管理
-
- 6.1 进程的概念与作用
- 6.2 简述壳Shell-bash的作用与处理流程
- 6.3 Hello的fork进程创建过程
- 6.4 Hello的execve过程
- 6.5 Hello的进程执行
- 6.6 hello的异常与信号处理
- 6.7本章小结
- 第7章 hello的存储管理
-
- 7.1 hello的存储器地址空间
- 7.2 Intel逻辑地址到线性地址的变换-段式管理
- 7.3 Hello的线性地址到物理地址的变换-页式管理
- 7.4 TLB与四级页表支持下的VA到PA的变换
- 7.5 三级Cache支持下的物理内存访问
- 7.6 hello进程fork时的内存映射
- 7.7 hello进程execve时的内存映射
- 7.8 缺页故障与缺页中断处理
- 7.9动态存储分配管理
- 7.10本章小结
- 第8章 hello的IO管理
-
- 8.1 Linux的IO设备管理方法
- 8.2 简述Unix IO接口及其函数
- 8.3 printf的实现分析
- 8.4 getchar的实现分析
- 8.5本章小结
- 结论
- 附件
- 参考文献
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
-
P2P:From Program to Process。
-
编写代码,得到源程序hello.c这就是Program。
-
预处理器cpp读取头文件内容,直接插入到程序文本,得到中间文件hello.i。
-
编译器ccl得到翻译成汇编语言文件hello.s。
-
汇编器as得到可重定位目标文件hello.o。
-
链接器ld得到可执行目标文件hello
-
最后在shell键入启动此程序,shell调用fork函数为其产生子进程,hello便成为了进程Process。完成了P2P。
-
-
020:From Zero-0 to Zero-0。 这说的可能是从无到有再到无吧。
-
OS的进程管理调用fork函数产生子进程,调用execve函数,并进行虚拟内存映射(mmp),并为运行的hello分配时间片以执行取指译码流水线等操作。
-
OS的存储管理以及MMU解决VA到PA的转换,cache、TLB、页表等加速访问过程,IO管理与信号处理综合软硬件对信号等进行处理;程序结束时,shell回收hello进程,内核将其所有痕迹从系统中清除。
-
至此,从0开始编写的hello.c经历了它短暂的一生,又回到了最初的起点。想必这就是020。
-
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
- 硬件环境
X64 CPU;3GHz;16G RAM;256GHD Disk 以上
- 软件环境
Windows10 64位;VirtualBox/Vmware 16;kali linux;
- 开发工具
Visual Studio 2022 64位;Visual Studio Code 2021 64位以上;edb;gdb;gcc;ld;objdump;readelf…
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
- hello.i
预处理得到的文件,根据#开头的命令修改了原始的c程序。
- hello.s
得到了以文本格式描述的汇编语言程序。
- hello.o
得到了由机器语言程序打包的可重定位目标程序,是二进制文件。
- hello
可执行目标文件。
1.4 本章小结
本章简述了hello的整个过程,列举操作的所有需要的环境与工具,也包括中间会产生的中间结果文件。相当于为后面的章节搭建起来一个大致的框架。只有骨架,还需要后续添加血肉丰富它。
第2章 预处理
2.1 预处理的概念与作用
-
预处理的概念:预处理阶段,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include < stdio.h>命令告诉预处理器读取系统头文件stdio.h 的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
-
预处理的作用: 扩展源代码。
-
宏定义: 预处理程序中的#define标识符文本,预处理工作也叫做宏展开(将宏名替换为文本)。
-
头文件包含: 预处理程序中的#include,将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件。
-
条件编译: 根据#if以及#endif和#ifdef以及#ifndef来判断执行编译的条件。[1]
-
2.2在Ubuntu下预处理的命令
预处理的命令:gcc –m64 –no-pie –fno-PIC -E hello.c -o hello.i
2.3 Hello的预处理结果解析
-
首先我们通过查看hello.i文件发现预处理的作用的确是扩展源程序,因为我们发现hello.c的主体部分还存在于hello.i中,如下图,当然它已经来到了第3199行,可见扩展真不少。
图2.3.1 hello.i中的hello.c主体部分 -
我们同样可以看到对于#include头文件的内容插入到源文件的过程,这个过程对头文件展开了很多,涉及到了引用路径/usr/include/stdio.h等等,部分stdio.h的展开如下图。
图2.3.2 stdio.h的部分展开 -
我们还可以发现hello.i直接忽略了注释部分,在其中是找不到的。
-
hello.i文件中每一行开始和结束的数字让人困惑,查阅资料之后有了了解:
-
开头的数字: 它表示后面紧接着文件的第几行,这一行的内容对应hello.i文件的下一行。以下面的stdlib.h为例:
图2.3.4 hello.i中stdlib.h
我们看第2076行的数字25,按照/usr/include/stdlib.h找到第25行,如下图,结果非常的amazing啊,和hello.i中的第2077行恰好对应。因此我们发现这是hello.i访问头文件的技巧。
图2.3.5 stdlib.h中的第25行
- 结尾的数字: 它是标志。
1:这表示新文件的开始。
2:这表示返回文件(包含另一个文件后)。
3:这表示以下文本来自系统头文件,因此应禁止某些警告。
4:这表示以下文本应被视为包含在隐式extern "C"块中。[2]
2.4 本章小结
本章说明了预处理的概念以及作用,命令以及hello.c文件的预处理结果hello.i文本文件解析。详细了解了预处理的内涵,知道了预处理这一阶段一共完成若干事情:头文件的展开、宏替换、去掉注释、条件编译。
第3章 编译
3.1 编译的概念与作用
-
编译的概念: 编译过程是整个程序构建的核心部分,将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。也就是将hello.i翻译成汇编语言文件hello.s的过程。
-
编译的作用: 将高级语言程序转化为机器可直接识别处理执行的的机器码的中间步骤,为后面的汇编做好准备。具体来说有以下几部分作用:[3]
-
词法分析。对输入的字符串进行分析和分割,形成所使用的源程序语言所允许的记号,同时标注不规范记号,产生错误提示信息。
-
语法分析。分析词法分析得到的记号序列,并按一定规则识别并生成中间表示形式,以及符号表。同时将不符合语法规则的记号识别出其位置并产生错误提示语句。
-
语义分析。即静态语法检查,分析语法分析过程中产生的中间表示形式和符号表,以检查源程序的语义是否与源语言的静态语义属性相符合。
-
代码优化。将中间表示形式进行分析并转换为功能等价但是运行时间更短或占用资源更少的等价中间代码。
-
3.2 在Ubuntu下编译的命令
编译的命令:gcc –m64 –no-pie –fno-PIC -S hello.i -o hello.s
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
- 数据
a. 局部变量int i
局部变量在运行时被保存在栈或者是寄存器里。在hello.s文件中,编译器将i存储在寄存器%ebp中,如下图所示。
b. main函数的参数argc
argc是main的个参数,我们知道参数的顺序一般是从%rdi开始的,所以第一个参数argc也应该存储在%rdi里,如下图所示。
c. 一些常数如1
这里以exit(1)中的1为例,这里在.s文件中被存在%esi中,如下图所示。
d. 字符串
在hello.s中字符串先提前存了起来,如"用法: Hello 学号 姓名 秒数!n"和"Hello %s %sn"。调用时通过他们的宏直接使用,如下图所示。
- 赋值
这个就以i=0为例吧,很简单,没啥可说的看图吧。
- 算数操作
紧接着上文的i,以i++为例,同样很简单直接看图。
- 数组char *argv[]
对于数组的操作,是将数组首地址压入栈,把栈顶存的数组首地址给%rbx来访问,这里每八个字节一个元素,所以通过+8,+16,+24就可以访问后面三个元素,.s中的处理如下图所示。
- 控制转移
一般都是和比较一起满足条件就根据跳转表跳转,这里以参数数量不等于4就退出为例,如下图所示。
- 关系操作
这里就以3.3.3的参数数量不等于4为例,jne就表示了"!="这一关系,如下图所示。
- 类型转换
这里涉及调用sleep函数时,将字符串的秒数转换为整型,这里调用了strtol函数完成了类型转换,如下图所示。
- 函数操作
main函数、exit函数、sleep函数其实在上面的例子中已经多多少少提到过来,都是大同小异,因此这里就以printf(“Hello %s %sn”,argv[1],argv[2])为例吧。
a. 参数传递:一共两个参数argv[1]和argv[2]分别存在%rsi、%rdx中,因为%rdi已经被argc用掉了。
图3.3.8.2 对于函数操作的处理——传参和调用
b. 函数调用:参数已经传好了那就直接call呗,见上图就不在赘图了。
c. 函数返回:这里以3.3.7中的strtol为例,返回值一般存在%eax中,这里返回的整数秒数就存在寄存器eax里,如下图。
图3.3.8.3 对于函数操作的处理——函数返回
3.4 本章小结
本章主要详细介绍了编译的概念与作用,以及编译的指令,最后对编译文件hello.s按照PPT中P4给出的参考对以下方面进行了解释:数据(常量、变量)、类型转换、算术操作、关系操作、数组、控制转移、函数操作(参数传递(地址/值)、函数调用()、函数返回)。
在编译的过程中,编译器将高级语言编译生成了汇编语言,作为到机器语言的一个过渡,我们理解汇编语言可以理解编程的底层实现,对于代码优化、代码效率等方面都有帮助。
第4章 汇编
4.1 汇编的概念与作用
-
汇编的概念: 汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的 17 个字节是函数 main的指令编码。这一过程就是汇编。
-
作用: 将汇编进一步翻译为计算机可以理解的二进制机器语言,这是机器可直接识别执行的代码文件。
4.2 在Ubuntu下汇编的命令
汇编的命令:gcc –m64 –no-pie –fno-PIC -c hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
- 用readelf生成elf格式的文件
命令:readelf -a hello.o > hello.elf,结果如下图所示。
- ELF头:
ELF头如下图,它以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如X86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每一个节都有一个固定大小的条目。[4]
图4.3.2 ELF头
- 节头:
节头部表描述不同节的位置和大小。如下图所示。
- 重定位节:
.rela.text如下图所示,保存的是.text节中需要被修正的信息、任何调用外部函数或者引用全局变量的指令都需要被修正、调用外部函数的指令需要重定位、引用全局变量的指令需要重定位、调用局部函数的指令不需要重定位,在可执行目标文件中不存在重定位信息。hello程序需要被重定位的是:.rodata.str1.8、puts、exit、.rodata.str1.1、printf、strtol、sleep、getc。
以.rodata.str1.8,对应字符串"用法: Hello 学号 姓名 秒数!n"。首先,链接器计算出引用的运行时地址;然后,更新该引用,使得它在运行时指向真正的内容。最后,在得到的可执行目标文件中,我们便可以得到正确的引用地址,即完成对第一个重定位条目的重定位计算。
- 符号表:
目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。符号表索引是对此数组的索引,如下图所示。
4.4 Hello.o的结果解析
- 生成hello.o的反汇编
首先,使用objdump -d -r hello.o > hello_asm.txt得到反汇编,如下图所示。
图4.4.1 hello.o的反汇编
-
与hello.s进行对照分析
-
分支转移: hello.s文件中分支转移是使用段名称进行跳转的,而hello.o文件中分支转移是通过地址进行跳转的。
图4.4.2 hello.s与hello.o分支转移的差异 -
函数调用: hello.s文件中,函数调用call后跟的是函数名称,而在hello.o文件中,call后跟的是下一条指令。这是因为 hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
图4.4.3 hello.s与hello.o函数调用的差异 -
数的进制: 在hello.o中,立即数全部是以16进制表示的,因为16进制与2进制之间的转换比十进制更加方便,所以都转换成了16进制。
图4.4.4 hello.s与hello.o数的进制的差异 -
机器语言的构成,与汇编语言的映射关系
机器语言是一种二进制语言,每一条指令、数据都由二进制来表示。汇编语言对于很多指令用一个字符串来表示,更容易读懂。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言和二进制机器语言建立一一映射的关系,也就是双射。
4.5 本章小结
本章对汇编结果进行了详尽的介绍,介绍了汇编的概念与作用,以及汇编的命令。本章主要部分在于对可重定位目标ELF格式进行了详细的分析,重点在重定位项目上。同时对hello.o文件进行反汇编,将反汇编代码与hello.s文件进行了对比,在分析比较的过程中加深了对二者的理解。
第5章 链接
5.1 链接的概念与作用
-
链接的概念:链接是将各种代码和数据片段收集并组合成一个文件的过程,这个文件可被加载(复制)到内存执行。链接可以执行于编译时、加载时、运行时。
-
链接的作用:把预编译好了的若干目标文件合并成为一个可执行目标文件。使得分离编译成为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。
5.2 在Ubuntu下链接的命令
链接的命令: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/5/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
- 获得hello的ELF格式文件
运行readelf -a hello > hello_elf结果如下图所示。
在这里插入图片描述
图5.3.1 hello的ELF格式文件
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
a) edb的Data Dump窗口。窗口显示虚拟地址由0x400000开始,从开始到结束这之间的每一个节对应5.3中的每一个节头表的声明。
图5.4.1 edb打开hello的Data Dump窗口
b) edb的Sympols窗口。证实从虚拟地址从0x400000开始和5.3节中的节头表是一一对应的。
图5.4.2 edb打开hello的Loaded Symbols窗口
5.5 链接的重定位过程分析
-
运用指令objdump -d -r hello > hello-asm.txt得到hello的反汇编文件(前面hello.o反汇编得到的是hello_asm.txt),过程如下图所示。
图5.5.1 hello的反汇编过程 -
分析hello与hello.o反汇编的不同
-
hello反汇编文件中的地址是虚拟地址,而hello.o反汇编节中的是相对偏移地址,如下图所示。
图5.5.3 hello与hello.o地址的差异 -
hello的反汇编中增加了许多外部链接的共享库函数。如puts@plt共享库函数,printf@plt共享库函数以及getchar@plt函数等。如下图所示。
图5.5.4 hello的反汇编多了外部共享库函数 -
hello的重定位过程:要合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,还要对引用符号进行重定位,.text节和.data节中对每个符号的引用,而这些需要用到在.rel_data和.rel_text节中保存的重定位信息。
5.6 hello的执行流程
子程序名 | 程序地址 |
---|---|
ld-2.33.so!_dl_start | 0x7ffff7fcdd70 |
ld-2.33.so!_dl_init | 0x7ffff7fdc060 |
hello!_start | 0x0000000000401090 |
hello!main | 0x0000000000401172 |
hello!puts@plt | 0x0000000000401030 |
hello!printf@plt | 0x0000000000401040 |
hello!strtol@plt | 0x0000000000401050 |
hello!sleep@plt | 0x0000000000401070 |
hello!getc@plt | 0x0000000000401080 |
5.7 Hello的动态链接分析
-
当程序调用一个由共享库定义的函数时,编译器无法预测这个函数运行时的地址,因为定义它的共享模块在运行时可以加载到任何位置。这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。他通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。
-
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
0x40400后面的16个字节的变化包含了动态链接器在解析函数地址时会使用的信息,包括动态链接器在ld-linux,so模式中的入口地址。
5.8 本章小结
本章结合实验中的hello可执行程序依此介绍了链接的概念及作用以及命令,并对hello的elf格式进行了详细的分析对比。以及hello的虚拟地址空间知识,并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程,遍历了整个hello的执行过程,整理了过程中的子函数,在最后对hello进行了动态链接分析,对链接有了更深的理解。
第6章 hello进程管理
6.1 进程的概念与作用
(参考教材P508)
-
进程的概念:
-
狭义定义:进程是正在运行的程序的实例。
-
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
-
-
进程的作用:
-
在现代计算机中,进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序 一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行 我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
-
每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时,shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
-
进程提供给应用程序两个关键抽象:一个独立的逻辑控制流,一个私有的地址空间。[4]
-
6.2 简述壳Shell-bash的作用与处理流程
-
**shell的作用:**shell是一个交互型应用级程序,是操作系统的最外层,shell可以合并编程语言以控制进程和文件,以及启动和控制其他程序。简单来说,shell就是一个用户跟操作系统之间交互的命令解释器。
-
处理流程:
-
首先是主程序不停地接受用户输入的命令行;
-
然后调用eval函数,它的首要任务是调用parseline函数解析以空格分隔的命令行参数,并构造最终会传递给execve的argv向量。第一个参数被假设为要么是一个内置的shell命令名,马上就会解释这个命令,要么是个可执行目标文件,会在一个新的子进程的上下文中加载并运行这个文件。如果最后一个参数是一个"&"字符,表示应该在后台执行该程序,否则,表示应该在前台执行这个程序。在解析了命令行之后,eval函数调用builtin_command函数,该函数检查第一个命令行参数是否是一个内置的shell命令。如果是,它就立即解释这个命令,并返回值1,否则返回0。
-
6.3 Hello的fork进程创建过程
父进程通过调用fork函数来创建一个子进程,fork函数调用一次返回两次。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境遍历列表envp。只有当出现错误时,execve才会返回到调用程序。所以,execve调用一次并从不返回。在execve加载了filename后,调用启动代码,启动代码设置栈,并将控制转移传递给新程序的主函数。
子进程通过execve函数系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后加载器调到_start处,最终调用应用程序的main函数。
6.5 Hello的进程执行
- 上下文信息:
上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
- 上下文切换:
当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
-
保存以前进程的上下文;
-
恢复新恢复进程被保存的上下文;
-
将控制传递给这 个新恢复的进程 ,来完成上下文切换。
- 进程时间片:
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
- 用户模式和内核模式:
处理器通常是用某个控制寄存器中的一个模式位提供两种模式的区分功能,该寄存器描述了进程当前享有的特权。
-
当设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置;
-
当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据。
6.6 hello的异常与信号处理
SIGINT、SIGSTP、SIGCONT、SIGKILL等
-
具体的命令及运行结果分析异常与信号的处理
-
对于fg命令。 通过jobs可以查看到被挂起的hello,输入fg命令之后内核发送SIGCONT信号,刚刚挂起的程序hello重新在前台运行,如下图所示。
图6.6.3 fg命令的结果分析 -
对于kill命令。 内核发送SIGKILL信号给我们指定的pid对应的hello程序,结果是终止了hello程序。如下图在hello被挂起后执行kill,最后hello进程被回收了。
图6.6.4 kill命令的结果分析 -
不停乱按。乱按键盘(包括回车),发现输入的字符与程序本来的输出连接在一起。到读取到输入的回车,getchar读回车,把回车前面的字符串当做shell指令去解析,最终肯定是command-not-found。如下图所示。
图6.6.5 乱输命令的结果分析 -
pstree命令。执行后可以看到所有的进程情况,其中可以看到当前打开的hello和pstree,如下图所示。
图6.6.6 pstree命令的结果分析
6.7本章小结
本章介绍了进程的概念与作用,简要说明了shell的作用和处理流程,介绍了hello程序的进程创建、加载、进程执行以及各种异常与信号处理的相关内容。异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。在这一过程中加深了对信号的理解。
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址
逻辑地址是指由程序hello产生的与段相关的偏移地址部分(hello.o)。
- 线性地址
线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
- 虚拟地址
有时也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
- 物理地址
物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。以 hello 为例,物理地址就是 hello 真正应该在内存的哪些地址上运行。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。[4]
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。其中TI指示段描述符是在GDT还是LDT中,而索引指示段描述符在段描述符表中的位置。由此,便可以通过段选择符的指示在段描述符表中找到对应的段描述符,然后便可从段描述符中获得段首地址,将其与逻辑地址中的偏移量相加,就得到了线性地址。[4]
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。根据书中图示理解,如下图。
图7.3.1 使用页表的地址翻译[4]
7.4 TLB与四级页表支持下的VA到PA的变换
Intel i7的地址翻译用到了TLB与四级页表的技术,其中虚拟地址48位,物理地址52位,页表大小为4KB,页表为4级。
由于页表大小是4KB,所以VPO为12位,VPN就是36位。每一个PTE条目是8字节,所以每一个页表就有512个条目,那么VPN中每一级页表对应的索引就有9位。对于TLB,因为有16组,所以TLBI就是4位,TLBT就是32位。
CPU产生虚拟地址VA,传给MMU,MMU使用前36位作为VPN,在TLB中搜索,如果命中,就得到其中的40位物理页号PPN,与12位VPO合并成52位的物理地址PA。
如果TLB没有命中,MMU就向内存中的页表请求PTE。CR3是一级页表的起始地址,VPN1是第一级页表中的偏移量,用来确定二级页表的起始地址,VPN2又作为第二级页表的偏移量,以此类推,最后在第四级页表中找到对应的PPN,与VPO组合成物理地址PA。
根据教材理解如下图:
图7.4.1 Core i7 四级页表翻译[4]
7.5 三级Cache支持下的物理内存访问
-
得到了物理地址VA,首先使用物理地址的CI进行组索引(每组8路),对8路的块分别匹配 CT进行标志位匹配。如果匹配成功且块的valid标志位为1,则命中hit。然后根据数据偏移量 CO取出数据并返回。
-
若没找到相匹配的或者标志位为0,则miss。那么cache向下一级cache,这里是二级cache甚至三级cache中寻找查询数据。然后逐级写入cache。
-
在更新cache的时候,需要判断是否有空闲块。若有空闲块(即有效位为0),则写入;若不存在,则进行驱逐一个块(LRU策略)。[4]
7.6 hello进程fork时的内存映射
当fork函数被新进程调用时,内核为新进程创建各种数据结构,并分配给它唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。[4]
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的文件。加载并运行hello需要以下几个步骤:
-
删除已存在的用户区域。 删除当前进程虚拟地址的用户部分中的已存在的区域结构;
-
映射私有区域。 为新的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零;
-
映射共享区域。 hello与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内;
-
设置程序计数器。 execve设置当前进程上下文的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,将从这个入口点开始执行。[4]
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。处理程序执行如下步骤:
-
检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。
-
检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。
-
两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。[4]
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。而分配器分为两种基本风格:显式分配器和隐式分配器。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。
已分配的块显式地保留为供应用程序使用,空闲块可用来分配。
空闲块保持空闲,直到它显式地被应用所分配。
一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。[4]
7.10本章小结
本章重点介绍了计算机中的存储,包括地址空间的分类,地址的变换规则,虚拟内存的原理,cache的工作,和动态内存的分配。虚拟内存存在于磁盘中,处理器产生一个虚拟地址,然后虚拟地址通过页表映射等相关规则被转化为物理地址,再通过cache和主存访问物理地址内保存的内容,返回给处理器。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
- 设备的模型化:文件
一个linux文件就是一个多个字节的序列。所有的IO设备都被模型化为文件,包括网络、磁盘、终端等。
- 设备管理:unix io接口
这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置等。[4]
8.2 简述Unix IO接口及其函数
-
Unix IO接口
-
打开文件: 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
-
linux shell 创建的每个进程开始时都有三个打开的文件: 标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
-
改变当前的文件位置: 对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
-
读写文件: 一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的"EOF 符号" 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
-
-
Unix IO接口的函数
-
打开文件
打开文件函数原型:int open(char* filename,int flags,mode_t mode)
返回值:若成功则为新文件描述符,否则返回-1;
flags:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)
mode:指定新文件的访问权限位。
- 关闭文件
关闭文件函数原型:int close(fd)
返回值:成功返回0,否则为-1。
- 读文件
读文件函数原型:ssize_t read(int fd,void *buf,size_t n)
返回值:成功则返回读的字节数,若EOF则为0,出错为-1。
描述:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。
- 写文件
写文件函数原型:ssize_t wirte(int fd,const void *buf,size_t n)
返回值:成功则返回写的字节数,出错则为-1
描述:从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
8.3 printf的实现分析
(1) printf的函数体。
图8.3.1 printf函数
函数体内部调用了函数vsprintf,那么我们再继续看一下vsprintf函数。其中va_list的定义被定义为字符指针。
(2) vsprintf函数
图8.3.2 vsprintf函数
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
(3) 系统函数write
由反汇编语句中的int INT_VECTOR_SYS_CALL,它表示要通过系统来调用sys_call这个函数。
(4) sys_call函数
图8.3.3 sys_call函数
显示格式化的字符串。将要输出的字符串从总线复制到显卡的显存中。sys_call将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),于是我们的打印字符串就显示在了屏幕上。[5]
8.4 getchar的实现分析
-
运行到getchar函数时,程序将控制权交给os。当你键入时,内容进入缓寸并在屏幕上回显。按enter,通知 os输入完成,这时再将控制权在交还给程序。
-
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
-
getchar调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要涉及文件IO的操作。了解了IO设备管理方法,以及一些IO函数,并且深入分析了printf函数和getchar函数的实现。看似简单的printf函数其实底层实现非常复杂,调用了函数vsprintf和系统调用write,而之后有调用了sys_call函数。getchar函数的实现也是关于中断的处理,同时它也进行了系统调用write函数。
结论
- 逐条总结hello所经历的过程。
即使是一个看不起眼的hello.c小程序其中的经历过程也是让人惊心动魄,闻风丧胆,猝不及防,甘拜下风>_<
-
预处理:hello.c经过预处理,拓展得到hello.i文本文件。
-
编译hello.i经过编译,得到汇编代码hello.s汇编文件,为转化为机器可直接识别处理执行的的机器码做铺垫。
-
汇编:hello.s经过汇编,得到二进制可重定位目标文件hello.o,是将高级语言转化为机器可直接识别执行的代码文件的过程。
-
链接:hello.o经过链接,生成了可执行文件hello。
-
进程:调用fork函数,生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行新程序hello。
-
存储管理:hello的变化过程中,会有各种地址,但最终我们真正期待的是PA物理地址。
-
IO管理:hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关。
-
hello最终被shell父进程回收,内核会收回为其创建的所有信息。我朝 CS(计算机系统-Editor+Cpp+Compiler+AS+LD + OS + CPU/RAM/IO等)挥一挥手,不带走一片云彩!
- 对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
-
计算机系统学习过程是很折磨人的,但是坚持啃完之后会发现是柳暗花明的爽快感觉,当然在短短的两三个月想吃透是不太现实的,未来还应该继续细细钻研,其中还有很多很多值得深入理解。
-
我深感计算机科学的伟大。对于大佬科学家们的智慧十分佩服,同时为自己的"菜"感到一丝忧虑,也多了一丝动力。历史长河中一个个菜鸟与Hello擦肩而过,我也是其中一个,曾经只有CS知道他的生、他的死,他的坎坷,“只有 CS 知道…他曾经…来…过…”。如今,我也知道Hello曾经来过。
附件
中间过程文件 | 文件作用 |
---|---|
hello.i | 预处理得到的文件ASCII码的中间文件 |
hello.s | 由hello.i得到的汇编语言文件 |
hello.o | 汇编得到可重定位目标文件 |
hello_asm.txt | hello.o反汇编得到的txt文本文件 |
hello.elf | hello.o的可查看elf文件 |
hello | 由ld得到可执行目标文件 |
hello_elf | hello的elf文件 |
hello-asm.txt | hello的反汇编得到的txt文本文件 |
参考文献
[1] 预处理的功能
[2] https://www.ssfiction.com/c-cyuyankaifa/518955.html
[3] 词法分析、语法分析、语义分析 – JackYang – 博客园 (cnblogs.com)
[4] Randy E.Bryant,深入理解计算机系统(第3版),机械工业出版社2021.5出版
[5] printf 函数实现的深入剖析 – Pianistx – 博客园