计算机系统大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 1190200705
班 级 1903001
学 生 郭一澄
指 导 教 师 郑贵滨
计算机科学与技术学院
2021****年6月
摘 要
本文主要以hello.c程序为载体,深入分析了hello程序的执行过程中,从预处理到编译再到汇编、链接的编译器完成的过程,并对执行过程中的进程管理、存储管理以及IO管理进行了广泛的探讨。在前四个过程中,编译器能够将它变成可执行文件,本文在此基础上对此过程中的程序细节,如:汇编代码与反汇编代码的区别,链接的重定位以及动态链接等内容进行分析,并对此过程中的相关概念给出了详细的解释。当程序变成可执行文件之后,通过命令行对可执行文件输入一定的内容,我们对此时程序的进程管理以及异常信号处理与执行进行了一定的介绍,然后对整个程序运行中的存储管理及程序是如何从虚拟地址变成物理地址,其中又涉及到了哪些模块给出了详细的阐述,并对程序运行过程中的缺页故障以及缺页中断等异常进行了探讨。最后通过hello程序对Linux的IO管理进行了介绍,并对hello程序中运用到的printf与getchar两个函数的实现给出了具体分析,将hello程序的一生完整的展示出来。
关键词:编译;进程管理;存储管理;IO管理。
目 录
6.2 简述壳Shell-bash的作用与处理流程. - 26 -
7.2 Intel逻辑地址到线性地址的变换-段式管理. - 33 -
7.3 Hello的线性地址到物理地址的变换-页式管理. - 34 -
7.4 TLB与四级页表支持下的VA到PA的变换. - 35 -
7.7 hello进程execve时的内存映射. - 38 -
第1章 概述
1.1 Hello简介
对于大多数的程序员来说,无论程序是否复杂,我们通常对于程序的认识只是停留在他的代码表面。然而当我们按下鼠标开始执行程序的那一瞬间,却并没有考虑到程序是如何从一个个字符转变成各种美妙复杂的功能的,这其中又包含着多少软硬件复杂的构造与交互,每一条指令背后又隐藏着怎样复杂的原理。当我们在这学期学习了计算机系统这一门课之后,我们会认识到一个简简单单的的程序背后其实包含了各种各样的软件与硬件之间的程序与系统之间的交互内容,从编译到执行,再到后来的输入输出,内存与进程的管理与调度的实现,是多种机制共同作用得到的结果。
首先从代码到可执行文件需要经历预处理、编译、汇编和链接四个阶段,在此过程主要需要编译器的帮助。在得到了可执行文件后,需要进行程序的进程控制管理以及信号处理与内存的各种管理,同时由于程序需要输入与输出相关内容,所以IO从输入输出控制等内容也常常贯穿于其中。所以一个程序看似简单,对于计算机而言却并不是那么轻松就能完成的,需要经历一个短暂而又复杂的过程。所以在本文接下来的过程中,将主要对hello执行过程中的各个功能以及相关概念与实现进行探讨,一起感受计算机系统的精妙之处。
1.2 环境与工具
硬件环境
X64 CPU;1.58GHz;8G RAM;512GHD Disk
软件环境
Windows 10 64位;VMware Workstation Pro;U ubuntu-20.04.2.0 64位;
开发与调试工具:
GDB/OBJDUMP;EDB;VIM等
1.3 中间结果
在此过程中涉及到的文件包括如下内容:
文件名称 | 功能 |
---|---|
hello.c | hello程序源代码 |
hello.i | hello.c预编译后得到的文件 |
hello.s | hello.i编译后得到的文件 |
hello.o | hello.s汇编后得到的文件 |
hello | hello.o链接后得到的文件 |
asm.txt | hello.o程序的反汇编代码 |
asm2.txt | hello程序的反汇编代码 |
elf.txt | hello.o程序的表头信息 |
elf2.txt | hello程序的表头信息 |
1.4 本章小结
本章主要为全文的概述部分,将本次大作业主要包括的知识内容以及hello程序的处理流程、实现方式给出了初步的介绍。同时介绍了完成hello程序整个过程中所需要的环境与工具,在完成过程中的中间结果文件内容以及各个文件相应的含义与对应的步骤,为后续程序功能的完成做好的准备工作。
(第1章0.5分)
**
**
第2章 预处理
2.1 预处理的概念与作用
概念:编译过程主要处理那些源代码文件中的以“#”开始的预编译指令,修改原始的C程序,在编译之前进行的处理。 C语言的预处理主要有四个方面的内容: 1. 将所有的“#define”删除,并且展开所有的宏定义; 2. 处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。且这个过程是递归进行的,所以被包含的文件可能还包含其他文件; 3. 处理所有条件预编译指令,比如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”等;4. 删除所有的注释,添加行号和文件名标识,并保留所有的#pragma编译器指令。
作用:经过预编译后的.i文件中不包含任何宏定义,因为所有的宏已经被展开,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。并且包含的文件使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计,提高程序的效率,方便后续程序的相关执行,降低了后期编译器的工作量。
2.2在Ubuntu下预处理的命令
输入指令:gcc -E -o hello.i hello.c
以上为预处理的结果展示,可以看到原来只有28行的程序如今变成了3065行,同时从下图也可以看到文件的大小也迅速增大。
2.3 Hello的预处理结果解析
通过分析对比刚刚vim打开的hello.i文件我们可以看到,其中的各种以“#”开始的预编译指令都已被处理,如#include<stdio.h>、#include <unistd.h>等已被替换掉,预处理器读取了系统头文件stdio.h中的内容,并把它直接插入程序文本中,得到了一个新的程序,直接对stdio.h文件进行了引用。同时原来hello.c文件中的注释内容也已全部消失,通过观察还可以发现程序中多了很多typedef以及extern函数,其中很多都对应着/usr/include/x86_64-linux-gnu/的地址,用于引用库函数的内容并对函数进行了处理替换,如下图所示:
2.4 本章小结
本章主要对程序的运行预处理过程进行了介绍,并给出了预处理结果的相应展示,展示了程序如何处理“#”开始的预编译指令,为后续程序的继续执行提供了基础,并对预处理的结果进行了探讨,分析预处理的相关作用。
**
**
第3章 编译
3.1 编译的概念与作用
概念:编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。
作用:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的定义,汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。例如,C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。
3.2 在Ubuntu下编译的命令
通过输入gcc hello.i -S -o hello.s,如下图所示,得到了该程序对应的汇编指令。
3.3 Hello的编译结果解析
3.3.1 数据初始化
1.常量
在程序中对应的常量一共包括两个,对应于C程序中的:
printf(“Usage: Hello 1190200705 郭一澄!\n”);
printf(“Hello %s %s\n”,argv[1],argv[2]);
即常量为两个需要输出的字符串,在汇编代码中主要为:
(为方便查看,我将汇编代码导入到了windows下的txt文件中)
2.全局变量
分析c代码以及汇编代码可以知道此程序中的全局变量只有sleepsecs,且大小为2而非2.5,在此过程中发生了向下取整。
对应于
sleep(sleepsecs);
3.局部变量
在此过程中的局部变量也是只有一个,即使用i来完成循环,如下所示:
用来完成for循环操作。
for(i=0;i<10;i++)
4.字符数组
通过分析可以看到,以下为调用字符数组argv的相关内容,
3.3.2 数据赋值
如下所示,两个都为数据复制操作,只不过第一个对应的是全局变量为sleepsecs赋值,而第二个图片对应的则是局部变量赋值,将int i初始化为0
3.3.3 算数操作
在此过程中存在的算数操作只有一个,即程序的for循环中的i++操作,对应的汇编代码如下所示:
执行了一个addl $1, -4(%rbp)的操作。
for(i=0;i<10;i++)
3.3.4 比较关系操作
在此过程中的比较内容还是比较多的,如下面的cmp操作,用于判断是否for循环执行或者if语句的相应执行。
若-4(%rbp)<10则继续执行for循环内容
若-4(%rbp)!=3则继续执行if中的内容
3.3.5 类型转换
在查看程序的c代码是我们会发现程序的sleepsecs=2.5,而在分析汇编代码之后我们会发现其中2.5发生了向下取整操作变为了2,如下所示。
3.3.6 转移控制跳转操作
在此过程中的转移控制操作主要为for循环的使用以及if判断语句的使用,若符合对应的条件则执行跳转操作,从指定的代码位置开始运行。其对应的汇编代码如下图所示。
for(i=0;i<10;i++)
可以看到在for循环中,通过比较-4(%rbp)与9的大小来判断for循环的执行,若等于9则再此跳转回之前的程序中,执行循环内部操作,若大于9则跳出。
if(argc!=3)
通过执行cmpl $3, -20(%rbp)来比较if语句内是否为等于3,若相等则执行跳到.L2开始执行。
3.3.7 数组类型
在此过程中使用的数组主要为argv及其内容,分析c代码可知,调用数组内容的代码主要只有后面的这一句:
printf(“Hello %s %s\n”,argv[1],argv[2]);
在函数的运行中,具体的数组元素都是存放在栈中的,而且在第一个元素的基础上进行栈的+操作实现新数组元素的赋值。栈中由低到高的顺序存储数组的内容,由于argv数组的首位并不入栈,所以此时的数组元素从argv[1]开始入栈,而且分别是第二个%edi和第三个参数%rsi进行赋值,所以数组对应的赋值以及打印的汇编代码为:
3.3.8 相关函数
1.main函数
如图所示,代码开始的时候对main函数进行了调用。
2.printf函数
在本过程一共有两个需要输出的过程,即
对应的汇编代码为:
可以看到由于输出内容的不同,调用的函数也并不相同,在直接输出字符串时使用的是put,而在输出对应的内容时使用的则是printf@PLT。
同时在使用printf函数时对应的位置与寄存器存在着先后对应的函数传递关系。
根据%rdi、%rsi的顺序对函数进行输出,分别为第一个第二个擦住
3.getchar函数
由于getchar操作是在最后执行的,所以分析代码可以查找到getchar对应的位置。
\4. sleepsecs函数
sleepsecs该函数在执行过程中调用了参数2,而且在汇编代码中,函数的执行名称一般与其对应的c名称相同,所以可以直接观察到对应的操作位置。
由汇编代码可知调用sleep函数时的%edi内容为2,所以需要睡眠2秒。
5.exit函数
此函数内容较为简单,如下所示
3.4 本章小结
本章主要将程序进行编译,获得了对应的汇编指令内容,同时对程序的汇编代码得到的结果进行了分析,对程序的执行过程给出了初步的探讨,并对该过程中不同的类型与操作函数等内容给出了详细的解释,便于继续深入分析该程序。
(第3章2分)
**
**
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件 hello.o中。hello.o文件是一个二进制文件,它包含的17个字节是函数main的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了
4.2 在Ubuntu下汇编的命令
输入gcc -c hello.s -o hello.o即可完成编译过程。
从而得到了对应的文件:
4.3 可重定位目标elf格式
通过输入readelf -a hello.o > elf.txt可以得到hello.o的ELF格式分析其中内容如所示:
首先是ELF头,其对应的内容为:
ELF头以一个16字节序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。而后的部分描述了ELF头文件的相关信息,其中包括ELF头的类别、目标文件为小端序、操作系统为UNIX、机器类型为X86-64、类型为可重定位文件,以及节头部表中条目的大小和数量等各种信息。
而后是节头部表,其中内容如下所示:
主要包括的是各个节的名称大小信息,属于哪种类型及大小地址的相关内容与偏移量,此处的偏移两位在hello.o中相对于起始地址的偏移量。
重定位节.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。具体内容如下所示:
在本文件中主要对puts,exit,.rodata ,printf, sleepsecs, sleep,getchar等函数在用于链接时的重定位信息进行了描述,其中主要用于后续链接的信息为偏移量和信息。偏移量中的主要内容为对应的函数在当前重定位节中的位置信息,即.text中的偏移量,信息中主要存储的是相关内容的偏移量以及相关的存储类型与引用方式。而后的内容主要是对于其类型的描述,以及需要在链接时需要加减的常量。
.symtab是符号表,它存放在程序中定义和引用的函数和全局变量的信息,记录了程序运行中的符号,比如main,sleep,printf,getchar等符号的基本信息,其具体内容如下所示:
我们可以从上面的type栏中看到,只有main函数中的类型为FUNC函数,其余函数均为NOTYPE类型,这是因为还未将这些函数进行链接,所以其类型暂时为NOTYPE,大小为0 。同时我们还可以注意到,全局变量sleepsecs的大小为4,类型为OBJECT,与其余变量均不同。
4.4 Hello.o的结果解析
输入objdump -d -r hello.o 得到一个反汇编代码,同时重新输入将其导出为txt与hello.s进行分析对照,发现二者的不同主要在于:
1.反汇编代码是直接从main函数开始的,从上到下看可以直接了解程序的运行逻辑以及基本脉络,而在汇编代码中在开始之前进行了许多初始化的声明赋值内容,也有很多相关信息的标注,所以从内容上看二者有些差别。
\2. 反汇编代码中函数是直接从头到尾的一个整体,并没有中间分段,而是通过通过判断数值范围等操作进行条件跳转。而在汇编代码中通过。L1,.L2,等内容对函数分成了不同的段。
\3. 汇编代码与反汇编代码中的函数表示与执行不同。在汇编代码中,通常是使用call指令加上相关的函数名称开始函数的执行。而在反汇编代码中则没有使用call指令,通过在call后面加上地址偏移来执行函数后续某个位置的代码,并直接在中间加上函数的重定位条目,直接调用相关的内容。
\4. 从格式上来看,反汇编代码中给了许多相关的信息,比如指令对应的二进制编码,当前程序中各个指令的地址,而在汇编代码中则没有体现相应的内容,给的比较简单。
\5. 最后是从二者采用的进制上来看,汇编代码中使用的是16进制,前面有0x标志,而汇编代码中使用的则是十进制,折在分析过程中需要特别注意,避免弄混。
4.5 本章小结
在完成了之前的编译环节之后,本环节将汇编语言翻译成机器语言,并且对得到的结果进行了分析,对于elf.给出了各节的基本信息与重定位相关内容,并通过反汇编功能与汇编代码进行对比,探究二者之差异。·
(第4章1分)
**
**
第5章 链接
5.1 链接的概念与作用
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(load-er)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器(linker)的程序自动执行的。
链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
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的格式
输入readelf -a hello得到对应的elf信息并对其进行分析。
ELF头内容:
通过与原hello.o文件对比我们可以看到其中程序的类型变成了可执行文件,同时入口点地址发生了变化,入口点地址变为了0x4010f0,同时程序头大小与节头数量同原来相比也有所不同。
节头部表内容如下所示:
由节头表我们可以看到第一列为对应的名称,第二列为节的大小,第三列为对应的地址,第四列为节在本程序中的偏移量。可以看到从除去第一个NULL节以外一共包含26个节。且由长度以及数量上可以看出,当完成链接以后程序的中一些文件被添加了进来。
重定位节:
5.4 hello的虚拟地址空间
通过与5.3的内容进行对比我们可以看到elf头的最初几个字节内容都是相同的,后续内容由于是十六进制我们无法理解,但是内容上应该与elf中的信息是一一对应的。
通过edb依次向后查看,在此可以看到个人学号信息等相关内容。
5.5 链接的重定位过程分析
输入objdump -d -r hello >asm2.txt得到链接后的反汇编代码。
对比二者的反汇编代码可以知道:
1.通过链接后的代码中增加了共享库函数,如下图所示:
在链接之后反汇编代码中有<gmon_start>、<_dl_relocate_static_pie>、<__libc_start_main@GLIBC_2.2.5>等函数,而在hello.o中则未体现
2.在链接后,程序执行的地址发生了变化,如下图:
在链接前的hello.o中的地址只是程序的相对偏移地址,而在hello链接以后的地址为对应的虚拟地址,同时一些共享库函数的地址在其中也有所体现。
3.函数的调用方式也发生改变,在原hello.o的反汇编代码中只有一个main函数,而在hello中则多了许多函数且在不同的函数之间进行跳转,如下图为同一段功能的不同表述:
所以在链接的过程中会通过重定位对其进行函数地址的替换以及程序地址的重新修改。
而后分析hello.o的重定位过程。结合上一节中的内容以及下面的内容,可以看到中的许多原有的函数都被替换为相关的名称,同时数目上也迅速增多,增加了许多函数,对应的地址也更改为重定位以后的地址。
结合教材内容以及刚刚分析的反汇编代码我们可以知道,hello.o的重定位过程一共分为两个步骤:
1.重定位节的符号定义。在这一步中,链接器将hello.o以及所有相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,hello中的每条指令和全局变量都有唯一的运行时内存地址了。
2.重定位节中的符号引用。在这一步中,链接器修改hello的代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。此时链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
5.6 hello的执行流程
通过依次执行edb能够查看到函数的执行流程如下所示(由于栈随机化导致有的地址每次记录的不同,所以只列出了程序名):
ld-2.31.so!_dl_start
ld-2.31.so! dl_init
hello!_start
ld-2.31.so!_libc_start_main
hello!_libc_csu_init
hello!_init
libc-2.31.so!_setjmp
libc-2.31.so!_sigsetjmp
libc-2.31.so!__sigjmp_save
hello_main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!sleep@pl
hello!getchar@plt
libc-2.31.so!exit
5.7 Hello的动态链接分析
共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的。共享库也称为共享目标(shared object),在Linux系统中通常用.so后缀来表示。
通过之前的操作我们可以知道.GOT的内容(如下所示),所以通过edb进行分析如下图所示:
dl_init前
dl_init后
所以我们可以知道在hello程序的动态链接dl_init前后,0x404000附近的内容发生了变化,加载了一部分的内存地址,用于完成动态链接等相关操作。
5.8 本章小结
本章主要内容为将hello程序进行链接,对得到的文件格式以及虚拟地址空间进行了分析,本章主要使用的工具为edb,通过使用edb以及阅读文件的elf可以看到程序的执行过程以及在其中涉及到的程序地址动态链接内容与相关函数调用,同时我们还分析了动态链接前后程序的变化,使我们对这一过程有了更为直观的认识。
(第5章1分)
**
**
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括:存放在内存中的程序的代码和数据,它的栈,通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。每次用户通过向shell输入一个可执行目标文件的名字运行程序时, shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
作用:进程能够提供给应用程序的一些关键抽象,如:
1.一个独立的逻辑控制流,它提供一个假象好像程序独占地使用处理器。
2.一个私有的地址空间,它提供一个假象好像程序独占地使用内存系统。
同时通过进程能够更方便于描述和控制程序的并发执行,实现操作系统的并发性和共享性,更好的实现资源分配(CPU时间与内存等)和调度。通过进程的使用可以做到在一个时间段内有多个程序在运行,并独立分配资源,独立接受调度,独立地运行,便于实现各种控制与管理。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是指“为使用者提供操作界面”的软件,是管理进程和运行程序的程序,bash(GNU Bourne-Again Shell)是最常用的一种shell。是一种特殊的交互工具,一种命令行解释器, 其读取用户输入的字符串命令, 解释并且执行命令。它是一种特殊的应用程序, 介于系统调用/库与应用程序之间, 用于用户和Linux系统交互,其提供了运行其他程序的的接口。它可以是交互式的, 即读取用户输入的字符串;也可以是非交互式的, 即读取脚本文件并解释执行, 直至文件结束。它接收用户命令,然后调用相应的应用程序。shell命令可以分为以下三类:
内建函数(built-in function):shell自带的功能
可执行文件(executable file):保存在shell之外的脚本,提供了额外功能。
别名(alias):给某个命令的简称。
处理流程:
首先需要由用户输入相应的命令行,并对其进行读取,而后对命令行进行分析,判断输入的命令是否为内置命令,若是内置命令则对其立即进行执行,否则为其调用fork()函数,为其创建分配一个子进程并继续运行,若在此过程中由键盘输入或其他异常情况发生,则调用相应的信号处理程序。同时在输入命令时,如果末尾有&号,则表示设置此进程为后台进程;如果末尾没有&号,则表示执行完该命令后才能执行后面的命令。
6.3 Hello的fork进程创建过程
首先fork函数具有如下特点:fork函数只被调用一次,却会返回两次;一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。父进程和子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流中的指令。而父进程和子进程则是相同但是独立的地址空间。所以结合上文所述分析本程序我们可以知道:在执行hello程序时,当输入:
./hello 1190200705 郭一澄
由于在shell中并不存在这一个内置命令,所以会调用fork()函数来为其创建一个子进程,且二者是并发运行的独立进程,以任意方式交替执行它们的逻辑控制流中的指令且使用相同但独立的地址空间,所以此时二者除了PID以外,文件的内容是相同的,当然对于子进程来说是可读的,但如果子进程需要写父进程的文件时则需要写时复制。当在二者分别并行执行完各自的进程以后会由两个返回值,这就是之前提到的fork函数只被调用一次,却会返回两次,最终实现对两个进程的回收。
6.4 Hello的execve过程
相关概念:与fork函数返回两次不同的是,execve如果正常运行并不返回。execve函数加载并运行可执行目标文件filename,且带参数列表 argv和环境变量列表envp,其中argv[0]是可执行目标文件的名字。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。
在hello程序运行的过程中,需要由execve函数在当前进程的上下文中加载并运行可执行目标文件hello,并按照上述的流程执行。子进程通过execve系统调用启动加载器删除子进程现有的虚拟内存段,并创建一组新的关于hello程序的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到hello的页大小的片(chunk),新的代码和数据段被初始化为可执行文件的内容。最后跳转到hello的开始地址,main函数开始执行。
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。操作系统内核上下文切换来实现多任务。上下文切换包括:1.保存当前进程的上下文;2.恢复某个先前被抢占的进程被保存的上下文,3.将控制传递给这个新恢复的进程。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,
用户态核心态转换(用户模式和内核模式):一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式,hello程序运行的相关原理图如下所示。
所以通过以上的概念介绍,我们可以知道在hello程序中,主要的切换以及进程调度发生在sleep函数位置,所以执行sleep前hello进程会处于用户模式并一直顺序执行,但是当执行sleep函数以后会被切换到到内核模式中休眠两秒,将其加入等待序列中,当两秒钟结束后则会中断并切换到用户模式执行之前被挂起的程序,直至遇到下次切换。
6.6 hello的异常与信号处理
输入回车(输入乱码)后:
可以看到回车及乱码对于程序运行并没有影响,只是会多出空行或字符。
输入Ctrl-C后:
当向hello程序输入Ctrl-C后,会导致中断异常产生SIGINT信号,向子进程发出SIGKILL信号终止并回收,进程会终止。
输入Ctrl-Z后:
当向hello程序输入Ctrl-Z后,会导致中断异常产生SIGSTP信号,进程被挂起,与输入Ctrl-C结果不同,此时输入如下命令查看有:
输入ps后:
输入ps可以看到各个进程的pid,可看到之前被挂起的hello。
输入jobs后:
可以看到各个作业,其中包括被挂起的hello,显示已停止。
输入pstree后:
可以看到进程的各种相关信息。
输入fg后:
输入fg后程序会继续执行,到前台来完成执行。
输入kill后:
将进程hello杀死,当执行完相关的指令以后再次使用ps查看,会发现kill已经被杀死。
6.7本章小结
本章主要涉及到的内容是进程的管理,首先对进程的概念进行了探讨,同时介绍了shell的处理流程,在hello程序中涉及到的fork函数以及execve函数所具有的不同特点,二者是如何完成的。随后通过从命令行输入不同的命令,得到了不同的结果,对进程的管理控制以及上下文等内容通过实例进行了具体的阐述,对其中涉及到的终止、中断以及停止与上下文切换等内容都进行了操作演示,从而对hello程序的进程管理有了形象的认识。
(第6章1分)
**
**
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址(logical address)包含在机器语言指令中用来指定一个操作数或一条指令的地址。它促使程序员把程序分成若干段。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。对应于hello.o中的相对偏移地址。
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。
虚拟地址(Virtual Address)就是在程序运行中程序中使用的逻辑地址,程序通常运行在虚拟地址空间中。由于是段式存储模式,所以虚拟地址是二维的,用段基址和段内位移表示。
物理地址(Physical Address)是线性地址经过页式变换得到的实际内存地址,这个地址被送到地址总线上,定位实际要访问的内存单元。计算机系统的贮存被组织称一个有M个连续的字节大小的单员组成的数组。每个字节都有一个唯一的物理地址。
在hello程序中,需要将各个指令的虚拟地址变为物理地址并完成各种操作,具体的过程为:先将hello虚拟地址或逻辑地址通过运算映射等方式得到线性地址,而后线性地址再通过页式管理变换的方式转变为物理地址,从而实现hello程序的相关执行。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在x86-64系统中,进程的虚拟地址空间被按类划分为若干段,包括代码段、数据段、栈段等。每个段由一个段描述符来描述,段描述符中记录了该段的基址、长度和访问权限等属性。各段的段描述符连续存放,形成段描述符表(即GDT和LDT)。在CPU执行单元中设有几个段寄存器,其中存放的是段描述符的索引项。主要的段寄存器是cs、ds和ss,分别用于检索代码段、数据段和栈段的段描述符。
具体的Intel段式地址变换的过程是:根据指令的类型确定其对应的段(如跳转类指令用cs段,读写类指令用ds段等),再通过对应的段寄存器在段描述符表中选出段描述符;用指令给出的虚拟地址作为段内位移,对照段描述符进行界限和权限检查;检查通过后,将段内位移值与段描述符中的段基址相加,形成线性地址。其具体原理如下图所示:
所以intel处理器在进行线性地址的变换-段式管理时,先通过指令确定段找到段基址,而后通过相应的运算与偏移量结合得到对应的线性地址,进而得到虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
地址翻译是一个N元素的虚拟地址空间中的元素和一个M元素的物理地址空间中元素之间的映射。CPU中的一个控制寄存器、页表基址寄存器指向当前页表。n位的虚拟地址包含一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。MMU利用VPN来选择适当的PTE,在hello程序中,VPN 0会选择PTE 0,VPN 1选择PTE 1,以此类推。将页表条目中物理页号(PPN)和虚拟地址中的VPO串联起来,就得到了hello程序相应的物理地址。因为物理和虚拟页面都是P字节的,所以物理页面偏移PPO和VPO是相同的,下图展示了在hello程序执行中MMU如何利用页表来实现这种映射。
当页面命中时,各个步骤完全是由硬件来处理的,示意图如下所示,具体执行的步骤为:
1.处理器生成一个虚拟地址,并把它传送给MMU。
2.MMU生成PTE地址,并从高速缓存/主存请求得到它。
3.高速缓存/主存向MMU返回PTE。
4.MMU构造物理地址,并把它传送给高速缓存/主存。
5.高速缓存/主存返回所请求的数据字给处理器。
处理缺页要求硬件和操作系统内核协作完成,示意图如下所示,具体执行的步骤为:
1.处理器生成一个虚拟地址,并把它传送给MMU。
2.MMU生成PTE地址,并从高速缓存/主存请求得到它。
3.高速缓存/主存向MMU返回PTE。
4.PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5.缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
6.缺页处理程序页面调人新的页面,并更新内存中的PTE。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。关键点在于所有的地址翻译步骤都是在芯片上的MMU中执行的,因此非常快,虚拟地址中用以访问TLB的组成部分如下所示:
其处理流程为:
·第1步:CPU产生一个虚拟地址。
·第2步和第3步:MMU从TLB中取出相应的PTE。
·第4步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
·第5步:高速缓存/主存将所请求的数据字返回给CPU。
当进程运行时,它的页目录表地址被加载到CR3控制寄存器中。36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址,CPU通过CR3得到页目录表的地址,进行地址变换。变换的过程是:先用页目录号作为索引,在页目录表中定位对应的表项,从中得到页表的页帧号。同样再以页表号为索引在页表中找到对应的页表项,从中得到被映射地址的页帧号。即:VPN1提供到一个L1PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,以此类推同样的方法依次向下完成,页帧号与页内位移相拼即得到物理地址。
如下图为四级页表将虚拟地址翻译成物理地址的过程。
7.5 三级Cache支持下的物理内存访问
在Intel Core i7处理器的cache的每个CPU芯片有四个核,每个核有自己私有的L1 i-cache、L1 d-cache和L2统一的高速缓存。所有的核共享片上L3统一的高速缓存。这个层次结构的一个有趣的特性是所有的SRAM高速缓存存储器都在CPU芯片上。L1、L2和L3高速缓存是物理寻址的,块大小为64字节。L1和L2是8路组相联的,而L3是16路组相联的。其运作原理为使用更快的存储设备来保留从较慢的存储设备读取的数据的副本。当数据需要从较慢的存储设备读写时,缓存允许读写首先在较快的存储设备上完成,从而提高系统的响应能力。其示意图如下所示:
一级缓存内置在CPU中,并以与CPU相同的速度运行,使其更有效率。一级缓存越大,CPU的效率越高,但是由于CPU的内部结构,一级缓存的容量非常小。
二级缓存用于中和一级缓存和内存间的速度。CPU调用缓存从一级缓存开始,随着处理器速度的增加,一级缓存的大小难以满足需求,因此必须增加相应的二级缓存。二级缓存相比一级缓存慢,但是它比一级缓存有更多的空间。它主要用于一级缓存和内存之间的临时数据交换。
三级缓存被设计用来从二级缓存中读取丢失的数据。在具有三级缓存的CPU中,只有大约5%的数据需要从内存中调用,这进一步提高了CPU的效率。它的工作原理是使用更快的存储设备来保留从较慢的存储设备读取的数据副本。当数据需要从较慢的存储设备读取和写入时,缓存将允许读写首先在较快的设备上完成,从而提高系统的响应能力。
7.6 hello进程fork时的内存映射
当fork函数被hello进程调用时,内核会为hello进程创建各种相应的数据结构,并分配给它一个唯一的PID。为了给这个hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
同时,当fork在hello进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会为对应的hello进程创建新页面,因此为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
虚拟内存和内存映射在将程序加载到内存的过程中也扮演着关键的角色。对于hello进程execve时的相关内存映射,根据教材内容我们可以知道加载并运行hello需要以下几个步骤:
1.删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。execve函数为hello程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。hello程序与共享对象链接时,如程序中链接的libc.so,这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.最后需要为hello程序设置程序计数器(PC)。execve做的最后一件事情就是为hello程序设置当前进程上下文中的程序计数器,使之指向代码区域的入口点,下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
在请求分页系统中,每当所要访问的页面不在内存中时,便产生一个缺页中断,请求操作系统将所缺的页调入内存,在虚拟内存的习惯说法中,缺页即为DRAM缓存不命中。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页。此时应将缺页的进程阻塞,若内存中有空闲块,则分配一个块,将要调入的页装入该块,并修改页表中的相应页表项,若此时内存中没有空闲块,则要淘汰某页(若被淘汰页在内存期间被修改过,则要将其写回外存)。
缺页中断作为中断,同样要经历诸如保护CPU环境、分析中断原因、转入缺页中断处理程序、恢复CPU环境等几个步骤。但与一般的中断相比,它有以下两个明显的区别:
·在指令执行期间而非一条指令执行完后产生和处理中断信号,属于内部中断。
·一条指令在执行期间,可能产生多次缺页中断。
缺页中断处理:
当获取了发生缺页中断的虚拟地址后,操作系统检查这个地址是否有效,并检查存取与保护是否一致。如果不一致,向进程发出一个信号或kill该进程。如果地址有效且没有保护错误发生,系统则检查是否有空闲页框。如果没有空闲页框,执行页面置换算法寻找一个页面来淘汰。
如果选择的页框被修改,则安排该页写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程,让其他进程运行直至磁盘传输结束。
当页框处理完以后,操作系统查找所需页面在磁盘上的地址,通过磁盘操作将其装入。该页面正在被装入时,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一个用户进程运行。
7.9动态存储分配管理
基本原理:当运行时需要额外虚拟内存时,用动态内存分配器使用户更加方便,也有更好的可移植性。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的或空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配,保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,两种风格都要求应用显式地分配块,它们的不同之处在于由哪个实体来负责释放已分配的块,具体两种分配器的特点及相应的原理如下:
显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器(implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
7.10本章小结
本章首先我们先对Hello中涉及到的各种地址概念进行了概述,而后分别解释了变换过程中,段式管理与页式管理的作用机制在hello程序中是如何运行的,而后我们又介绍了四级列表的概念,如何实现从虚拟地址到物理地址的变换,在获得物理地址后,如何完成对物理地址的访问,介绍了hello程序运行时进程的内存映射以及fork与execve两个函数的内存映射又是什么样的,缺页故障与缺页中断时如何处理,动态内存存储又是如何分配管理的,通过以上的内容实现的Hello程序的正常运行。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个一定数目字节的序列,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式允许Linux内核引出一个简单、低级的应用接口,称为Unix(Linux)I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,每个Linux文件都有一个类型来表明它在系统中的角色:
普通文件(regular file)包含任意数据。应用程序常常要区分文本文件和二进制文件,文本文件是只含有ASCII或Unicode字符的普通文件;二进制文件是所有其他的文件,对内核而言文本文件和二进制文件没有区别。
目录(directory)包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录。
套接字(socket)是用来与另一个进程进行跨网络通信的文件。
其他文件类型包含命名通道、符号链接,以及字符和块设备。
Linux 内核将所有文件都组织成一个目录层次结构(directory hierarchy),由名为/的根目录确定,系统中的每个文件都是根目录的直接或间接的后代。
8.2 简述Unix IO接口及其函数
由教材内容可知,Unix IO接口相关功能有:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来通知它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义一些常量可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
常用的函数为:进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件,调用close函数关闭一个打开的文件,调用read和write函数来执行输入和输出,通过调用lseek函数,显示地修改当前文件的位置。
常用函数如下所示:
int open(const char *filename, int flags, mode_t mode);
int close(int fd);
ssize_t write (int fd, const void * buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
off_t lseek( int fildes, off_t offset, int whence);
8.3 printf的实现分析
通过资料我们知道,printf的执行流程如下所示:
int printf(const char *fmt, …)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
对于其中的不同变量我们给出相关的解释:
fmt:指向第一个const参数(const char * fmt)中的第一个元素的指针。fme也是一个变量,它的位置是在堆栈上分配的,它也有一个地址。
va_list定义: typedef char va_list,说明它是一个字符指针,其中(char)(&fmt) + 4) 表示的是…中的第一个参数。
vsprintf函数:返回的是一个长度,即要打印出来的字符串的长度,完成格式化,生成相关的显示信息。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write:写操作,把buf中的i个元素的值写到终端便于后续的格式输出。
随后通过调用sys_call或陷阱来实现显示,需打印的字节通过总线从寄存器复制到显卡。通过线管转换确定号输出的内容,从ASCII到子模库再到显示vram。
显示芯片根据刷新率逐行读取VRAM,并通过信号线将每个点(RGB组件)传送给LCD,最终实现打印的功能。
8.4 getchar的实现分析
getchar()是stdio.h中的库函数,在文件处理时我们通过getchar()函数从输入流stdin中获取字符。函数getchar()的原型是int getchar(void);它所做的是从stdin流中读取一个字符并转换为整数值的无符号字符。在文件处理的情况下,当遇到文件末尾时它返回EOF。如果有错误,它还返回EOF。这意味着如果stdin有数据,它无需输入就可以读取数据。第一次getchar()确实需要手动输入,但如果输入多个字符,后续的getchar()运行将直接从缓冲区读取它。
其对应的源码为:
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;
}
通过分析我们可以知道:首先键盘输入的字符会存储在缓冲区数组buf中。 输入回车后,getchar会进入缓冲区读取字符,同时通过bb来进行保存,当程序缓冲区还有内容时会继续读取当前缓冲区的内容,当缓冲区为空时,则由if(n==0)可知会调用n=read(0,buf,BUFSIZ);函数对其进行重新读取,在返回时n的长度要减小,并返回bb++的值,同时每次getchar后重复此操作。如果有循环或足够的getchar语句,那么缓冲区中的所有字符将依次读取,直到读取’\n’。通过分析库函数我们可以知道每次getchar只能读取一个字符。同时在此过程中涉及到的进程处理包括异步异常-中断的处理以及read函数的调用。通过输入键盘导致异常并读取文本至缓冲区。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
在本章中,我们主要对hello程序的IO管理进行了分析,给出了相关设备的管理方法。其中IO的设备模块化是通过文件来实现的,设备管理是通过IO接口来实现的。同时我们还对此程序中用到的接口及函数的实现进行了讨论,通过对printf与getchar这两个函数的分析与深入探讨,能够对我们平时常用的io函数的函数底层有了新的认识。
(第8章1分)
结论
通过这一段漫长的旅途,我们终于将hello程序的一生完整的显现出来。看似简单的几行代码,看似简单的几个功能,包含的却是一套复杂的机制。Hello程序的一生平凡而又不平凡,很多代表性的功能与机制都在hello一生中一一显现。接下来我们将回顾每个阶段涉及到的各种原理与方法,体会其中包含的计算机系统设计的智慧。
在本程序中,首先对hello.c程序进行了预处理,通过预处理能够除去函数中的宏定义与相关的引用,得到hello.i。
然后hello.i在编译过程中将处理过的文件进行词法、语法、语义分析并进行了相关的优化得到了汇编文本hello.s,对汇编文本的内容,如:数据、函数,变量等内容进行观察分析,以探究汇编代码的特征以及各种细节。
随后是汇编部分,在汇编中将hello.s翻译成机器语言指令也就是可重定位目标文件hello.o,我们对其中的elf头部表进行查看,查看各个内容所代表的功能与含义,并将反汇编代码与汇编代码进行对比,比较二者的不同从而对编译的中间过程有了更深入的了解。
接下来完成的是链接,此时我们得到了一个可执行文件hello,通过edb工具对可执行文件的虚拟地址、空间与重定位过程进行分析,能够看到一个完整的程序执行过程,通过对比能够将链接过程的形式展示出来。
而后是hello程序中涉及到的进程管理,我们通过命令行程序进行了执行,其中涉及到了上下文切换以及异常与信号处理,我们通过输入各种指令能够将程序的进程信息及异常处理的内容完整的展示出来,同时通过执行不同的命令,将展示了fork与execve两个函数的执行过程在hello程序中的体现。
接下来我们又从内存的角度对hello程序进行了分析,主要涉及到的是虚拟地址与物理地址之间的变换,以及四级页表与tlb的相关内容,展示了程序运行中执行两个不同函数fork与execve函数后的内存映射,当遇到缺页故障与缺页中断后会如何处理这个程序,动态存储又是如何分配管理的。
在最后的内容中,我们对hello程序的IO进行了讨论并拓展到了linux的设备管理方法与接口函数上。通过两个在本程序中用到的具体函数实例,即printf函数与getchar函数,对hello程序涉及到的IO进行了深入的探讨。
通过hello大作业与一个简单的hello程序,使我们对这一学期的学习内容有了一个综合的运用,并能够通过课本上的知识对程序的各个方面进行多角度、深层次的分析。Hello程序只是无数个复杂程序的一个简简单单的缩影,而其他程序中则无处不体现着在hello程序中用到的各种思想与原理机制。通过对hello程序的细节进行分析,我们能够将本学期学到的内容从上至下成体系的综合起来,对于今后计算机的学习以及程序的编写具有重要的帮助。
Hello简单却又不简单,简单的是程序,不简单的却是背后的每一个原理与机制的完美融合,体现的更是无数计算机工作者的智慧的结晶。在今后的学习中我们要将学到的内容充分应用,用同样的思想去分析不同的程序,从中探寻计算机运行的奥秘。
附件
文件名称 | 功能 |
---|---|
hello.c | hello程序源代码 |
hello.i | hello.c预编译后得到的文件 |
hello.s | hello.i编译后得到的文件 |
hello.o | hello.s汇编后得到的文件 |
hello | hello.o链接后得到的文件 |
asm.txt | hello.o程序的反汇编代码 |
asm2.txt | hello程序的反汇编代码 |
elf.txt | hello.o程序的表头信息 |
elf2.txt | hello程序的表头信息 |
**
**
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] (美)布赖恩特(Bryant,R.E.).深入理解计算机系统 机械工业出版社, 2016.
[2] 程序员的自我修养
[3] 2021王道操作系统考研指导
[4] 现代操作系统
[5] CSDN论坛 :https://www.csdn.net/
hello程序中的体现。
接下来我们又从内存的角度对hello程序进行了分析,主要涉及到的是虚拟地址与物理地址之间的变换,以及四级页表与tlb的相关内容,展示了程序运行中执行两个不同函数fork与execve函数后的内存映射,当遇到缺页故障与缺页中断后会如何处理这个程序,动态存储又是如何分配管理的。
在最后的内容中,我们对hello程序的IO进行了讨论并拓展到了linux的设备管理方法与接口函数上。通过两个在本程序中用到的具体函数实例,即printf函数与getchar函数,对hello程序涉及到的IO进行了深入的探讨。
通过hello大作业与一个简单的hello程序,使我们对这一学期的学习内容有了一个综合的运用,并能够通过课本上的知识对程序的各个方面进行多角度、深层次的分析。Hello程序只是无数个复杂程序的一个简简单单的缩影,而其他程序中则无处不体现着在hello程序中用到的各种思想与原理机制。通过对hello程序的细节进行分析,我们能够将本学期学到的内容从上至下成体系的综合起来,对于今后计算机的学习以及程序的编写具有重要的帮助。
Hello简单却又不简单,简单的是程序,不简单的却是背后的每一个原理与机制的完美融合,体现的更是无数计算机工作者的智慧的结晶。在今后的学习中我们要将学到的内容充分应用,用同样的思想去分析不同的程序,从中探寻计算机运行的奥秘。
附件
文件名称 | 功能 |
---|---|
hello.c | hello程序源代码 |
hello.i | hello.c预编译后得到的文件 |
hello.s | hello.i编译后得到的文件 |
hello.o | hello.s汇编后得到的文件 |
hello | hello.o链接后得到的文件 |
asm.txt | hello.o程序的反汇编代码 |
asm2.txt | hello程序的反汇编代码 |
elf.txt | hello.o程序的表头信息 |
elf2.txt | hello程序的表头信息 |
**
**
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] (美)布赖恩特(Bryant,R.E.).深入理解计算机系统 机械工业出版社, 2016.
[2] 程序员的自我修养
[3] 2021王道操作系统考研指导
[4] 现代操作系统
[5] CSDN论坛 :https://www.csdn.net/
(参考文献0分,缺失 -1分)