哈工大计算机系统大作业

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机类
学   号 1190200128
班   级 1903001
学 生 詹先佑    
指 导 教 师 郑贵滨

计算机科学与技术学院
2021年6月
摘 要
可以说每一个学过编程的人来说,接触的第一个程序便是Hello World,它是我们编程之路的开始。而本论文所谈及的hello.c程序是Hello World程序的一个小拓展,本文中会在Ubuntu20.04的环境下合理地利用一些工具,并结合csapp的相关知识来对hello.c程序进行相应的分析,研究其在计算机内部的运行过程,以达到对计算机内部运行机制有更好的理解的目的。
关键词:分析hello.c、Ubuntu20.04、csapp、理解计算机、工具

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在Ubuntu下预处理的命令 - 5 -
2.3 Hello的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在Ubuntu下编译的命令 - 6 -
3.3 Hello的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在Ubuntu下汇编的命令 - 7 -
4.3 可重定位目标elf格式 - 7 -
4.4 Hello.o的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在Ubuntu下链接的命令 - 8 -
5.3 可执行目标文件hello的格式 - 8 -
5.4 hello的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 hello的执行流程 - 8 -
5.7 Hello的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 hello进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
6.3 Hello的fork进程创建过程 - 10 -
6.4 Hello的execve过程 - 10 -
6.5 Hello的进程执行 - 10 -
6.6 hello的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 hello的存储管理 - 11 -
7.1 hello的存储器地址空间 - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级Cache支持下的物理内存访问 - 11 -
7.6 hello进程fork时的内存映射 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 hello的IO管理 - 13 -
8.1 Linux的IO设备管理方法 - 13 -
8.2 简述Unix IO接口及其函数 - 13 -
8.3 printf的实现分析 - 13 -
8.4 getchar的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
(1)Hello的P2P介绍:
Hello的P2P就是From Program to Process的整个过程。hello.c首先通过cpp预处理生成hello.i,预处理就是 解释源程序当中的所有的预处理指令,如#include(文件包含)、#define(宏定义)、#if(条件编译) 等以井号’#’开头的 语句就是预处理指令;然后调用ccl将hello.i编译成hello.s,这个过程就是将经过预处理之后生成的hello.i 文件进一步的翻译,它包括词法和语法的分析, 最终生成对应硬件平台的汇编语言;再用gcc编译器调用汇编器as将hello.s汇编成hello.o文件,这是个二进制文件;最后调用连接器ld将libc标准库和hello.o文件链接生成hello可执行文件。在bash下输入相关命令后即可调用fork函数为可执行文件创建子进程,这是P2P的过程。
(2)Hello的020介绍:
Hello的020就是From Zero-0 to Zero-0。在bash调用fork函数创建子进程后,还会调用execve函数来进行虚拟内存的映射,即mmp;然后开始加载物理内存,进入到main函数当中执行相关的代码,打印出信息。在进程中,TLB、4级页表、3级Cache,Pagefile等等设计会加快程序的运行。程序运行完成后,bash会回收子进程,内核清楚数据痕迹。这就是020的过程
1.2 环境与工具
硬件环境:Intel® Core™ i5-9300H CPU;2.40GHz;8G RAM;120GHD Disk 以上
软件环境:win10家庭中文版、vmware16、Ubuntu20.04LTS
开发与调试工具:Dev-C++、objdump、gdb、edb、hexedit、gcc等

1.3 中间结果
hello.c:即要分析的C语言源程序
hello.i:hello.c首先通过cpp预处理生成的hello.i,以分析预处理过程
hello.s:调用ccl编译hello.i形成的hello.s,以分析编译的过程
hello.o:调用汇编器as将hello.s汇编成的hello.o,以分析汇编的过程
hello:链接器链接hello.o和libc标准库生成hello可执行文件,已分析链接的过程

1.4 本章小结
第一章主要介绍了hello程序的P2P过程和020过程,大概了解了hello程序在计算机中运行的机制;然后列举出了本论文在分析过程中用到的软硬件环境和工具,及其需要的相关文件,为后面的章节打下基础。

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理是运行C语言程序的第一个步骤,在这个阶段中会读取C语言程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。或者说是扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。预处理过程先于编译器对源代码进行处理,读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行转换。
预处理的作用:
在C 语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。

2.2在Ubuntu下预处理的命令
预处理命令:
gcc -E hello.c -o hello.i。gcc是是GCC中的GUN C Compiler ,即C语言编译器,而后面加一个-E表示只进行预处理而不进行其他步骤。
输入命令截图:

生成的hello.i的代码截图:

图一

2.3 Hello的预处理结果解析
通过gcc的预处理阶段,原本没有多少的源程序代码扩展到了3065行,这是因为在预处理阶段中,会进行头文件的展开、宏替换、去掉注释和多余的空白字符。
当然,代码扩充这么多的最主要原因是对头文件进行了展开,利用ubuntu的文本编辑器打开hello.i后使用查找工具分别查询stdio、unistd、stdlib三个字样来寻找stdio.h头文件、unistd.h头文件、stdlib.h头文件展开的位置。查询后发现13行到729行是对stdio.h头文件的展开,730行到1968行是对unistd.h头文件的展开、1969行到3045行是对stdlib.h头文件的展开。下面是头文件展开的开始位置截图

图二(stdio.h展开的开始位置)

图三(unistd.h展开的开始位置)

图四(stdlib.h展开的开始位置)

hello.i前面的部分绝大部分是对头文件的展开,而最后的部分便是C语言源程序的复制,如截图所示:

图五(hello.i最后部分)

最后顺着usr/include的路径可以在文件夹中找到stdio.h、unistd.h、stdlib.h三个文件,可以发现在头文件中存在许多的宏定义和if/endif/else/elif指令,所以预处理阶段还要根据if/endif/else/elif指令对头文件中进行条件编译。

图六(头文件位置)

图七(stdio.h头文件部分宏定义截图)

2.4 本章小结
本章主要介绍了预处理的概念和作用,并使用gcc -E hello.c -o hello.i命令生成了hello.i文件,最后对hello.i文件进行分析,已到达更加理解预处理的过程的目的。从分析过程中,我们可以学习到在预处理阶段主要有四个要做的事:头文件展开、宏替换(又称宏代换)、条件编译、去掉注释和多余的空白字符。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的概念:
编译是从.i文件到.s文件的过程,此次大作业中是hello.i到hello.s的过程。编译过程将预处理得到的文件转换为汇编文件。
编译的作用:
经过预处理得到的输出文件中,将只有常量,如数字、字符串、变量的定义,以及C语言的关键字,如main, if, else, for, while, {, }, +, -, *, , 等等。编译所要做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将预处理程序翻译成等价的中间代码表示或汇编代码。(此次大作业中是hello.i到hello.s的过程)

3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
输入命令截图:

图八
生成的hello.s代码部分截图:

图九
3.3 Hello的编译结果解析
3.3.1生成的hello.s文件的分析:
.file :C语言源文件名
.text:程序代码段
.globl :声明一个全局变量
.data :包含静态初始化的数据,所以有初值的全局变量和static变量在data区
.align :声明对指令或者数据的存放地址进行对齐的 方式
.type 声明是函数类型还是对象类型
.size :声明大小
.string:声明一个字符串类型
.long:声明一个long类型
3.3.2数据:
1)在hello.c文件中定义了sleepsecs全局变量,局部变量i,使用了整形、字符串、数组三种类型的数据
2)首先观察int类型的sleepsecs这个全局变量,因为sleepsecs全局变量已经被初始化为2.5,所以从代码中可以看出,先在.text节中声明一个名为sleeosecs的全局变量,又因为.data节存放已经初始化的全局变量,所以sleepsecs这个全局变量存到.data节中。再设置为4=字节对齐方式(.align 4),设置为对象(.type),设置大小为4(.size),然后转换为long类型(.long)。关于sleepsecs的代码截图如下:

图十
3)对于局部变量int类型的i,它是存储在栈当中,从图十一代码截图中标黄的部分我们可以知道i变量保存着栈空间的地址为%rbp-4的位置处

图十一
4)对于整形变量argc,argc是main函数的第一个形式参数,它存储了参数的个数,观察hello.s文件中的代码可以知道,在经过开辟栈空间的操作以后,传入的第一个参数应该就是argc,所以argc存储在地址为%rbp-20的位置上(由图十二代码截图红线部分可知)

图十二
5)对于立即数,在代码中,如果数字前面有$符号,那么这个数字就是立即数,汇编代码中是允许立即数存在的(立即数形如图十三中标红部分)

图十三
6)对于argv[]数组,它是main函数中传入的第二个参数,用于存放各个输入的参数,第一个参数是文件名,后面的参数就是键入的数据。argv数组一个元素是8个字节大小,即32位,从图十四的标红部分可以得到验证

图十四
7)对于字符串类型的数据,在hello.c有两个字符串数据:“Usage: Hello 学号 姓名!\n”、“Hello %s %s\n”。在hello.s文件代码中,第一个printf输出的是“Usage: Hello 学号 姓名!\n”,我们可以观察到\345\255\246\345\217\267 \345\247\223\345\220\215\357\274\201,这些是“学号”和“姓名”这四个汉字的UTF-8格式,中间用\隔开。第二个printf输出的是“Hello %s %s\n”,输出的内容是argv[]数组的第二、三个参数,即键入的第一、二个参数(因为argv[]数组第一个参数为文件名)

图十五
3.3.3赋值操作
1)在hello.c文件中赋值操作有三个,即:sleepsecs=2.5、i=0、i++
2)sleepsecs是全局变量,并且直接赋了初值,因此直接在.data节中将sleepsecs 声明为值为2的long类型数据(隐式转换,编译器缺省)。
3)在hello.s文件中通过汇编语句movl $0, -4(%rbp)将立即数0赋给局部变量int i。 i=0赋值操作的代码截图:

图十六
4)在hello.s文件中通过汇编语句addl $1,-4(%rbp)实现i++。i++赋值操作的代码截图:

图十七
3.3.4类型转换
在hello.c文件中存在有int sleepsecs=2.5的类型转换操作。这是一种隐式类型转换,将浮点数2.5转化为int整数2。
3.3.5算术操作和逻辑/位操作
1)常见算术指令汇总表

图十八

2)在hello.s文件中通过汇编语句addl $1,-4(%rbp)实现i++,这条算术指令是将立即数1和地址为%rbp-4的空间存储的值进行相加,并存储到这个空间当中,而后面的l是因为变量i是int类型,四字节。
3)subq $32,%rsp的作用是将%rsp这个栈指针减去32,达到指向栈顶,开辟栈空间的目的

图十九

4)leaq .LC1(%rip),%rdi指令计算LC0的段地址,即%rip+LC1,然后存储的%rdi寄存器当中

图二十
3.3.6关系操作
1)在hello.c文件中,第一个关系操作是“argc!=3”,在hello.s文件中,它被编译成了cmpl $3,-20(%rbp)(如图二十一标红部分),并且通过条件码判断是否跳转到分支当中。

图二十一
2)第二个关系操作是“i<10”, 在hello.s文件中,它被编译成了cmpl $3,-20(%rbp)(如图二十二标红部分),通过条件码判断是否跳转。

图二十二
3.3.7控制转移
1)汇编语言中会设置条件码,通过条件码的情况来判断是否进行控制转移
2)在hello.c文件中,第一个控制转移是判断argc是不是3,如果不是3,就执行if里面的代码;如果是3就不执行
3)第二个控制转移是for循环,判断i是否小于10,如果小于,就继续执行循环里的语句,如果大于或等于10,就跳出循环。在汇编代码中,分析得到,先给i赋值0,然后无条件跳转到.L3;在L3里判断i是否小于或等于9,如果小于或等于9,就跳转到.L4中,执行里面的语句,如果大于9了,就调用getchar()函数。汇编代码截图如下:

图二十三
3.3.8函数操作
1)参数传递:首先定义了全局变量sleepsecs,它在整个程序中都可以直接使用;然后向main函数中传递了argc、argv[]两个参数,argc是参数的个数,argv[]保存了键入的参数和文件名。
2)函数调用:在hello.c文件中调用了printf函数(打印信息)、exit函数(退出程序)、sleep函数(程序“睡眠”一定时间)、getchar函数(键入字符串)
3):函数返回:main函数返回了0,一般函数返回值保存在rax寄存器当中。
3.4 本章小结
本章主要是讲述了编译的阶段编译器对各种数据类型的操,以及对相应的汇编代码的一定分析。通过理解这些过程,我们更容易看懂汇编语言,更加清楚C语言编写的逻辑。

第4章 汇编
4.1 汇编的概念与作用
概念
驱动程序运行汇编器as,将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编,同时这个机器语言文件也是可重定位目标文件。
作用
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位 目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它 包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
汇编命令:as hello.s -o hello.o
截图:

图二十四
4.3 可重定位目标elf格式
(1)读取可重定位目标文件
键入readelf -h hello.o命令来查看elf可重定位目标文件,具体信息如截图所示:

图二十五(ELF头)
ELF头中,Magic描述了生成该文件的系统的字的大小和字节顺序,后面的信息包含有ELF头的大小、目标文件的类型、机器类型、字节头部表等信息。
(2)读取节头表信息:
.text:已编译程序的机器代码。
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss中。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态C变量。
.symtab:一个符号表,他存放在程序中定义和引用的函数和全局变量的信息
节头部表:节头表包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度以及节的对齐方式和偏移量。
.rela重定位节。该节包括的内容是:偏移量,信息,类型,符号值,符名称和加数。
可以键入命令readelf -S hello.o来查看节头表。

图二十六(节头表)
从节头表信息中我们可以看到节头表中包含了各个节的大小、名称、偏移量等等信息,又因为是可重定位目标文件,所以每个节的位置都从开始。
(3)查看符号表信息
我们可以键入readelf -s hello.o命令来查看符号表,具体信息如截图所示:

图二十七(符号表)
符号表存放了程序中定义和引用的函数、全局变量的信息。从节头表中,我们可以观察到其中包括了符号名称、符号相对于目标节的起始位置偏移量、目标大小等等详细信息。

4.4 Hello.o的结果解析
键入objdump -d -r hello.o来查看hello.o反汇编代码,并与hello.s进行对比。

图二十八(hello.o部分编代码)

图二十九(hello.s部分代码)
总的来说,反汇编代码和机器语言大致相同,只有一些小的部分不同
(1) 分支跳转:
hello.s文件中分支转移是使用段名称进行跳转的,比如.L4,但是hello.o文件中分支转移是通过地址来进行转移的。因为段名称在汇编语言中也只是助记符,不是核心代码,所以汇编语言转换成机器语言后就会消失掉了。替换它的就是确定的地址。
(2) 函数调用:
在hello.s文件中,call指令后面跟的是函数的名称,而在hello,o文件中,call后面跟的是下一条指令。因为hello.c的函数都是共享库函数,这时候地址是不确定的,最终需要链接器才能确定函数的地址。所以在hello.s转换成hello.文件后因,call指令将相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待链接的进一步确定。
(3) 全局变量
hello.s文件中,全局变量sleepsecs是通过语句.LC0(%rip),rdi完成的,这是一种段地址加上%rip的格式;对于hello.o的反汇编代码来说,则是0加上%rip的格式,因为.rodata节中的数据是在运行时确定的,也需要重定位,先添上0占位,并为其在.rela.text节中添加重定位条目。
(4) 机器语言和汇编语言的关系
机器语言是计算机执行的二进制命令,都是0和1表示的。而汇编语言具有一定意义的文字命令,与机器语言一一对应。汇编语言可以通过汇编得到机器语言,机器语言可以通过反汇编得到汇编语言。汇编过程还包括变量内存管理,即经过汇编之后所有的变量和函数都变成了地址,而常量也变成了对应的值。计算机在设计中规定了一组指令,这组指令的集和就是所谓的机器指令系统,用机器指令形式编写的程序称为机器语言在机器语言的基础上,人们提出了采用字符和十进制数代替二进制代码,于是产生了将机器语言符号化的汇编语言
4.5 本章小结
本章对hello.o文件进行了相应的分析,熟悉了ELF头、节头表、符号表中的详细信息,并且还对比了汇编代码和机器代码的不同之处,分析了汇编语言到机器语言的映射关系。

第5章 链接
5.1 链接的概念与作用
(1)概念
是将各种代码和数据片段收集并组合为单一文件的过程,这个文件可以被加载(复制)到内存并执行。
(2)作用
1)链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,即程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
2)链接使得分离编译(seperate compila)成为可能。更便于我们维护管理,我们可以独立的修改和编译我们需要修改的小的模块。
5.2 在Ubuntu下链接的命令
键入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命令进行链接,生成hello可执行文件,截图如下:

图三十

5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
(1) ELF头:键入readelf -h hello命令查看hello文件的ELF头。从ELF头中我们可以看到文件类型为可执行文件,并且有27个节

图三十一

(2) 节头表:键入readelf -s hello命令查看hello文件的节头表。在节头表中,包含了hello文件中所以节的详细信息,包括名称,大小,类型,地址,旗标,偏移量,对齐等信息。

图三十二(节头表部分截图)
(3) 符号表:

图三十三
5.4 hello的虚拟地址空间
(1)在命令行终端中键入./edb开始运行edb,并在edb中打开hello文件

图三十四(打开edb)

(2)在edb中查看hello文件,可以观察到hello的虚拟空间在0x401000开始,于0x401ff0结束。

图三十五(开始地址0x401000)

图三十六(结束地址0x401ff0)
(4) 通过节头表可以看出,并结合edb可以找到各个节的相关信息。例如.data节,它的虚拟地址开始于0x404040,大小为0x8。

图三十七

5.5 链接的重定位过程分析
(1)键入objdump -d -r hello命令对hello文件进行反汇编

图三十八(反汇编代码部分截图)
(2)对比hello的反汇编代码和hello.o的反汇编代码:
1)在hello的反汇编代码中比hello.o的反汇编代码多了许多的节,而hello.o的反汇编代码中只有.text节。

图三十九(hello的反汇编代码)

图四十(hello.o的反汇编代码)
2)hello.o反汇编代码中的地址是相对偏移地址,而hello反汇编代码中的地址是虚拟地址。

图四十一(hello的虚拟地址)

图四十二(hello.o的相对偏移地址)
3)hello的反汇编代码中增加了许多外部链接的共享库函数,比如puts@plt共享库函数,exit@plt共享库函数、sleep@plt共享库函数、getcahr@plt共享库函数等。

图四十三

(3)链接实质上就是去要合并相同的节,确定每一个节中所有定义的符号在虚拟地址空间中的地址,还要对引用的符号进行重定位,修改.text节和.data节中对每个符号的引用(实际上就是修改地址),而这些一切都需要用到在.rel_data和.rel_text节中保存的重定位信息。

5.6 hello的执行流程
(1)通过edb打开hello文件,打开后截图如下:

图四十四
(2)在终端键入./hello 1190200128 詹先佑,得到所有过程
程序名称 程序地址
ld -2.27.so!_dl_start 7efb ff4d8ea0
ld-2.27.so!_dl_init 7efb ff4e7630
hello!_start 400500
libc-2.27.so!__libc_start_main 7efb ff100ab0
hello!printf@plt 4004c0
hello!sleep@plt 4004f0
hello!getchar@plt 4004d0
libc-2.27.so!exit 7efbff122120

5.7 Hello的动态链接分析
(1)动态就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接的基本思想。对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GOT中地址跳转到目标函数。
(2)从hello的节头表我们可以得知.got.plt的起始地址是0x404000,执行dl_init前的.got.plt节截图如下:

图四十五
执行dl_init后的.got.plt节的截图如下:

图四十六
我们可以观察到调用dl_init后0x404008和0x404010处的数据发生改变,出现了两个小端排序的地址,即0x7f85442c2190和0x7f85442ad200
(3)然后又跳转到PLT执行.plt中逻辑,第一次访问跳转时,GOT 地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在 PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写 GOT,再将控制传递给目标函数,然后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结
本章首先介绍了链接的概念和作用,然后对可执行文件hello进行了相应的分析,最后分析了hello的虚拟空间、重定位过程、执行流程、动态链接,更加熟悉程序链接的整个过程。

第6章 hello进程管理
6.1 进程的概念与作用
(1)概念:
从用户角度:进程就是一个正在运行中的程序。
操作系统角度:操作系统运行一个程序,需要描述这个程序的运行过程,这个描述通过一个结构体task_struct{}来描述,统称为PCB,因此对操作系统来说进程就是PCB(process control block)程序控制块
进程的描述信息有:标识符PID,进程状态,优先级,程序计数器,上下文数据,内存指针,IO状态信息,记账信息。都需要操作系统进行调度。
(2)作用:它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
(1)作用:Shell是用户与操作系统之间完成交互式操作的一个接口程序,它为用户提供简化了的操作。而NU组织发现sh是比较好用的又进一步开发Borne Again Shell,简称bash,它是Linux系统中默认的shell程序。
(2)处理流程:
1)将用户输入的命令行进行解析,分析是否是内置命令;
2)若是内置命令,直接执行;若不是内置命令,调用fork()创建新进程/子进程
3)在子进程中,用步骤1分析命令行获取的参数,调用execve()执行指定程序。
4)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid等待作业终止后返回。
5)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
(1)因为hello是可执行程序,所以命令行键入的命令不会被解释为内置命令,然后shell到硬盘中查找hello可执行程序,如果找到,就载入到内存当中;如果找不到就报错
(2)shell运行fork()函数,创建子进程,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间的区别在于它们拥有不同的PID。但是因为是“副本”,这意味着子进程和父进程不共享数据、堆、栈等存储空间。
(3)流程图如下:

图四十七
6.4 Hello的execve过程
(1)execve函数的原型是int execve(const char *filename, char *const argv[],char *const envp[]),filename:包含准备载入当前进程空间的新程序的路径名。既可以是绝对路径,又可以是相对路径。argv[]:指定了传给新进程的命令行参数,该数组对应于c语言main函数的argv参数数组,格式也相同,argv[0]对应命令名,通常情况下该值与filename中的basename(就是绝对路径的最后一个)相同。envp[]:最后一个参数envp指定了新程序的环境列表。参数envp对应于新程序的environ数组。
(2)execve函数在新创建的子进程的上下文中加载并运行hello程序,由于是将调用进程取而代之,因此对execve的调用将永远不能返回,也无需检查它的返回值,因为该值始终为-1,实际上,一旦返回就表明了错误,通常会有error值来判断。
(3)运行hello过程:
1)hello子进程通过execve系统调用启动加载器。
2)加载器删除子进程所有的虚拟地址段,并创建一组新的代码、数据、堆段。新的栈和堆段被初始化为0。
3)通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段被初始化为可执行文件中的内容。
4)最后加载器跳到_start地址,它最终调用hello的main 函数。

6.5 Hello的进程执行
(1)进程时间片:
一个进程执行他的控制流的一部分的每一个时间段叫做时间片(time slice),多任务也叫时间分片(time slicing)
(2)进程上下文信息:
1)概念:进程上下文是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。
2)进程上下文切换流程1.保存当前进程的上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。
3)调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。
(3)用户态与核心态转换:
进程hello最开始是运行在用户模式中的,当它开始执行系统调用函数sleep或者是exit时就会陷切换到内核。内核中的处理程序完成对系统函数的调用后,系统执行上下文切换,会将控制返回给hello程序中系统调用函数之后的那条代码语句,开始执行后面代码,切换回用户模式。
6.6 hello的异常与信号处理
(1)hello执行过程中的异常:
1)异步异常:处理器外部I/O设备引起,由处理器的中断引脚指示,中断处理程序返回到下一条指令处。
2)陷进:有意的, 执行指令的结果(发生时间可预知),陷阱处理程序将控制返回到下一条指令
3)故障:不是有意的,但可能被修复,处理程序要么重新执行引起故障的指令(已修复),要么终止
4)终止:非故意,不可恢复的致命错误造成,中止当前程序。
(2)产生的信号:SIGINT,SIGSTP,SIGCONT,SIGWINCH
(3)运行截图:

图四十八(按下Ctrl-Z,Ctrl-C)

图四十九(Ctrl-z后运行ps jobs fg )

图五十(Ctrl-z后运行pstree)

图五十一(运行kill命令)
(4)异常与信号的处理
1)按下Ctrl-c属于异常中的中断,对于它的处理如截图所示:

图五十二
2)系统函数调用中可能会调用exit函数之类的,这属于异常中的陷阱,是人为故意设下的,它的处理如截图所示:

图五十三
3)对于ctrl+c或者ctrl+z命令。键盘键入后,内核就会发送SIGINT或者SIGSTP。SIGINT信号作用是终止程序hello,SIGSTP作用是挂起hello程序
4)而键入fg命令后,内核会发送SIGCONT信号,挂起的程序hello重新在前台运行。
5)键入kill指令的作用是杀死指定的程序,其过程中会发送SIGKILL信号给指定的程序。(PID号必须指定正确)
6.7本章小结
本章首先介绍了进程的概念和作用,然后阐述了hello程序调用fork函数创建进程的过程、execve过程、进程执行、异常与信号处理,更加熟悉了hello程序载入到内存后的运行流程。

第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)物理地址:物理地址(Physical Address) 是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址。如果没有启用分页机制,那么hello的线性地址就直接成为hello的物理地址了。
(2)逻辑地址:逻辑地址(Logical Address) 是指由hello产生的和段相关的偏移地址部分。只有在Intel实模式下,hello的逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,cpu不进行自动地址转换);逻辑地址也就是在Intel保护模式下程式执行代码段限长内的偏移地址(假定代码段、数据段如果完全相同)。应用程式员仅需和逻辑地址打交道,而分段和分页机制对你来说是完全透明的,仅由系统编程人员涉及。应用程式员虽然自己能直接操作内存,那也只能在操作系统给你分配的内存段操作。
(3)线性地址:是逻辑地址到物理地址变换之间的中间层。hello程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么hello的线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么hello的线性地址直接就是物理地址。
(4)虚拟地址:有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
(1)段式管理基本思想: 在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在前面所介绍的动态分区分配方式中,系统为整个进程分配一个连续的内存空间。而在段式存储管理系统中,则为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。
(2)段式管理的数据结构
为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到物理内存空间的映射,并跟踪物理内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。
·进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址(baseaddress),即段内地址。
·系统段表:系统所有占用段(已经分配的段)。
·空闲段表:内存中所有空闲段,可以结合到系统段表中。

(3)段式管理的地址变换

图五十四
在段式 管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址(图五十四)。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
(1)基本原理:将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)。程序加载时,可将任意一页放人内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是页号,后一部分为页内地址w(位移量)
(2)页式管理数据结构
1)进程页表:完成逻辑页号(本进程的地址空间)到物理页面号(实际内存空间,也叫块号)的映射。
2)物理页面表:整个系统有一个物理页面表,描述物理内存空间的分配使用状况,其数据结构可采用位示图和空闲页链表。
3)请求表:整个系统有一个请求表,描述系统内各个进程页表的位置和大小,用于地址转换也可以结合到各进程的PCB(进程控制块)里。
(3)页式管理地址变换:
在页式系统中,指令所给出的地址分为两部分:逻辑页号和页内地址。
CPU中的内存管理单元(MMU)按逻辑页号通过查进程页表得到物理页框号,将物理页框号与页内地址相加形成物理地址。

图五十五
7.4 TLB与四级页表支持下的VA到PA的变换
(1)变换过程大概的流程图:

图五十六
(2)变换过程详解:36位的虚拟地址被分割成4个9位的片。CR3寄存器包含L1页表的物理地址。VPN1有一个到L1 PTE的偏移量,找到这个PTE以后又会包含到L2页表的地址;VPN2包含一个到L2PTE的偏移量,找到这个PTE以后又会包含到L3页表的地址;VPN3包含一个到L3PTE的偏移量,找到这个PTE以后又会包含到L4页表的地址;VPN4包含一个到L4PTE的偏移量,找到这个PTE以后就是相应的PPN
7.5 三级Cache支持下的物理内存访问
(1)物理内存访问基本流程图:

图五十七
(2)基本流程详解:
1)得到了物理地址VA,首先使用物理地址的CI进行组索引(每组8路),对8路的块分别匹配 CT进行标志位匹配。如果匹配成功且块的valid标志位为1,则命中hit。然后根据数据偏移量 CO取出数据并返回。
2)若没找到相匹配的或者标志位为0,则miss。那么cache向下一级cache,这里是二级cache甚至三级cache中寻找查询数据。然后逐级写入cache。
3)在更新cache的时候,需要判断是否有空闲块。若有空闲块(即有效位为0),则写入;若不存在,则执行驱逐策略,驱逐一个块。
7.6 hello进程fork时的内存映射
(1)作用:虚拟内存和内存映射解释fork函数如何为每个新进程
提供私有的虚拟地址空间。
(2)为新进程创建虚拟内存步骤:
1)创建当前进程的mm_struct、 vm_area_struct和页表的
原样副本。
2)两个进程中的每个页面都标记为只读
3)两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)
(3)在新进程中返回时,新进程拥有与调用fork的父进程相同的虚拟内存
(4)随后的写操作通过写时复制机制创建新页面
7.7 hello进程execve时的内存映射
(1)删除已存在的用户区域
(2)创建新的区域结构:
1)代码和初始化的数据映射到.text和.data区(目标文件提供)
2).bss和栈映射到匿名文件
(3)设置PC,指向代码区域的入口点,Linux根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
(1)缺页异常由硬件、OS内核协作完成,具体流程图如下:

图五十八
(2)处理步骤:
1) 处理器生成一个虚拟地址,并将其传送给MMU
2) MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE
3) 高速缓存/主存向MMU返回PTE
4) PTE的有效位为零, 因此 MMU 触发缺页异常
5) 缺页处理程序确定物理内存中的牺牲页 (若页面被修改,
则换出到磁盘——写回策略)
6) 缺页处理程序调入新的页面, 并更新内存中的PTE
7) 缺页处理程序返回到原来进程, 再次执行导致缺页的指令
7.9动态存储分配管理
(1)动态存储分配概念:在程序运行时程序员使用动态内存分配器 (比如 malloc)获得虚拟内存,动态内存分配器维护着进程的一个虚拟内存区域,称为堆。分配器将堆视为一组不同大小 块(blocks)的集合,每个块要么是已分配的,要么是空闲的。

(2)管理方式:使用显式空闲链表、隐式空闲链表、分离的空闲链表、按照尺寸排序的块来管理动态内存。对于隐式空闲链表,它使用首次适配、下一次适配、最佳适配三种策略来寻找空闲块,然后是用分割法分配空闲块,再使用立即合并策略和延迟合并策略来合并空闲块,最后使用最常见的方法来释放内存块(即调用free函数)。
7.10本章小结
通过本章,我们可以回顾存储管理相关的知识,其中包括页表、TLB、虚拟内存和物理内存的关系、内存映射等等,对程序如何存储的过程更加熟悉。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(1)一个 Linux 文件 就是一个 m 字节的序列
(2)所有的I/O设备(网络、磁盘、终端)都被模型化为文件:/dev/sda2( 用户磁盘分区)、/dev/tty2(终端)
(3)甚至内核也被映射为文件:/boot/vmlinuz-3.13.0-55-generic(内核映像)、/proc (内核数据结构)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(1)接口
1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
2)linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
4)读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
(2)函数:
1)打开、关闭文件:open()、 close()
2)读、写文件:read() 、 write()
3)改变当前的文件位置 (seek),指示文件要读写位置的偏移量:lseek()
8.3 printf的实现分析
(1)从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.vsprintf函数截图如下:

图五十九
(2)字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
(3)显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(1)异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
(2)getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
(3)工作原理:getchar()是stdio.h中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。

8.5本章小结
本章主要了解了UNIX IO方面相关的内容,并且分析了printf函数和getchar()函数的实现。

结论
Hello所经历的历程:
(1) 创建hello的C语言源程序
(2) 使用gcc -E hello.c -o hello.i命令将hello.c预处理,得到hello.i
(3) 使用gcc -S hello.i -o hello.s命令将hello.i进行编译处理,得到hello.s
(4) 使用as hello.s -o hello.o命令将hello.s进行汇编处理,得到hello.o
(5) 使用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命令进行链接,生成hello可执行目标文件
(6) Shell调用fork函数创建子进程,并且调用execve函数将hello可执行目标文件加载到该子进程当中进行运行
(7) Hello运行的过程当中会有函数调用的过程,包括printf、exit、getchar等等
(8) 最终子进程会被父进程回收信息,hello停止运行

感悟:
计算机系统的内容十分多,而且复杂,是大学学习中目前遇到的最难学习的课程。但是,虽然难学,这些内容中蕴含了许多计算机的奥妙,在学习过程中也充满着乐趣。由于实在是水平有限,对于一下很深入的知识仍然不是很理解,需要在后面的学习过程中花费更多的功夫。

附件
文件名称 作用
hello.i hello.c源程序预处理后得到的文件
hello.s hello.i编译后得到的编译文件
hello.o hello.s汇编后得到的可重定位目标文件
hello 链接操作以后得到的可执行目标文件
hello.c hello.c源程序

参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
[7] C语言 getchar()原理及易错点解析 66Kevin
https://blog.csdn.net/weixin_44551646/article/details/98076863?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162495659216780261957664%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162495659216780261957664&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-2-98076863.first_rank_v2_pc_rank_v29_2&utm_term=getchar%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%88%86%E6%9E%90&spm=1018.2226.3001.4187
[8]printf函数实现的深入剖析 Pianistx https://www.cnblogs.com/pianist/p/3315801.html

[9] 操作系统内存管理——分区、页式、段式管理、段页式 南方铁匠 https://blog.csdn.net/hit_shaoqi/article/details/78516508?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162495817116780255238270%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162495817116780255238270&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-4-78516508.first_rank_v2_pc_rank_v29_2&utm_term=%E6%AE%B5%E5%BC%8F%E7%AE%A1%E7%90%86&spm=1018.2226.3001.4187
[10] Linux内核进程上下文切换深入理解 芒果520 https://blog.csdn.net/lx123010/article/details/108851345?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162495824216780271576123%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162495824216780271576123&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-108851345.first_rank_v2_pc_rank_v29_2&utm_term=%E8%BF%9B%E7%A8%8B%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2&spm=1018.2226.3001.4187

你可能感兴趣的:(csapp)