《计算机系统基础》大作业:程序人生

时间:2024-04-12 18:14:22

目录

摘要

本文通过hello从编写源文件hello.c到执行hello中的四个步骤(预处理、编译、汇编和链接)展开进行了讨论,并在Linux下利用相关工具,如objdump、readelf和edb,依次解读了各个步骤中所生成的中间文件(hello.i、hello.s、hello.o和hello)。以此为基础,总结并讨论了hello的进程管理、存储管理和IO管理中的相关内容。文章的各个部分又分别对应了《计算机系统基础》课程中的相关内容,既是对课程内容的一次复习,也能够对课程的内容形成一个整体性、框架性的认识,加深了对本门课程的理解。

关键词:计算机系统;hello;预处理;编译;汇编;链接;进程;虚拟内存;I/O

第1章 概述

1.1 Hello简介

多年以后,Hello Junior上校站在SIGINT信号面前,准会想起父亲Hello带他去参观虚拟地址空间的那个遥远的下午。当时,对计算机系统还不怎么了解,一个个功能单元都整齐的排列着,缓存里空荡荡的,顺着数据流望去,几个功能单元早已不堪重负,但还是有几个还光洁如新。这个符号还是新引用的,人们只是能叫出名字,大家只能用手指指点点却不知道它具体在何处。每年三月,衣衫褴楼的程序员都要守在系统旁边,大声疾呼,向大家介绍他们新发布的库。他们首先带来的是libc.so。一个身躯高大的程序员,自称丹尼斯里奇,满脸络腮胡子,手指瘦得像鸟的爪子,向观众出色地表演了他所谓的奇迹。他手里拿个大家伙,走过来走过去,大家都惊异地看见,本来大家只叫的出名字的符号拼了命想挣脱出来,一些看似丢失的东西兀然出现,整整齐齐地立在那些符号之前。“这些符号也是有生命的,”他用刺耳的声调说,“只需要使用链接器把它们唤醒!”
——以上内容仿写《百年孤独》开头,纯属娱乐。

本次大作业完整地进行了hello.c由预处理、编译、汇编和链接为hello可执行目标文件的过程。在完成大作业的过程中,对这四个步骤的概念进行了回顾并且对各个步骤中产生的文件进行了解析。之后又分别对hello的进程管理、存储管理和I/O管理过程进行了回顾与分析。完成整个大作业耗时若干天,而对hello来说却是它坎坷的一生。

1.2 环境与工具

硬件环境:i7 6700HQ;16G内存;512G硬盘
软件环境:Windows10 64位;VMware虚拟机下运行Ubuntu19.02
开发工具:Ubuntu下使用gcc、objdump、readelf和edb

1.3 中间结果

hello.i:对hello.c进行预处理时产生的中间文件。作用:(1)将源文件中以include格式包含的文件复制到编译的源文件中(2)用实际值替换用#define定义的字符串(3)根据#if后面的条件决定需要编译的代码。
hello.s:对hello.i进行编译时产生的中间文件。作用:生成汇编语言程序。
hello.o:对hello.s进行汇编时产生的中间文件。作用:通过汇编器翻译为机器指令的过程,同时这些机器指令被打包为可重定位目标程序的格式。
hello:由hello.o链接生成的可执行目标文件。作用:完成编写hello.c时所设计的功能。

1.4 本章小结

本章中,对hello的一生进行了简单地介绍,并列出了为完成本次大作业而使用的环境与工具。从hello.c到hello可执行目标文件的几个中间文件及其作业也做了简单地介绍,这些内容将在后续的章节中进行较为详细地讨论。

第2章 预处理

2.1 预处理的概念与作用

C语言中引入了预处理和条件编译等概念大大地增强了代码的移植性强以及代码的修改方便等方面的性能。在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令。预处理命令属于C语言编译器,而不是C语言的组成部分。通过预处理命令可扩展C语言程序设计的环境。

C语言预处理的工作是在编译器对源代码编译之前进行的。查询相关资料可知,预编译的主要作用有:(1)将源文件中以include格式包含的文件复制到编译的源文件中(2)用实际值替换用#define定义的字符串(3)根据#if后面的条件决定需要编译的代码

2.2在Ubuntu下预处理的命令

所使用的命令为gcc -m64 -no-pie -fno-PIC -E -o hello.i hello.c。在Ubuntu下操作时的截图如下所示,执行完指令之后文件夹中出现了hello.i文件。
《计算机系统基础》大作业:程序人生

2.3 Hello的预处理结果解析

预处理文件hello.i源于hello.c源文件,hello.c源文件中的内容如下图所示。
《计算机系统基础》大作业:程序人生
可以看到,源文件的开头处为一些注释,在main函数之前还存在一些以#include开头的指令。根据2.1部分中介绍的预处理的作用可知,预处理的过程将会对这些include包含的头文件进行处理。这些处理的过程在hello.i中均有体现,如下图所示。
《计算机系统基础》大作业:程序人生
《计算机系统基础》大作业:程序人生
还有一点值得注意的是,虽然源文件hello.c中仅寥寥几行的代码,但是预处理后的文件hello.i中却有数千行的代码,比较难以对其全部内容进行阅读。因此,以上在hello.i中查找头文件相关信息的工作,只能够通过ctrl + F进行搜索完成。

2.4 本章小结

本章中对hello.c预处理至hello.i的过程进行了讨论,并对预处理的结果hello.i文件中的相关内容进行了解析。

第3章 编译

3.1 编译的概念与作用

根据大作业的要求可知,此处的编译是指由.i文件生成.s文件,即预处理后的文件经过词法、语法和语义分析后并进行相关的优化(如-Og、-O1等),生成汇编语言程序。但是似乎编译更广义一点儿的含义是指包含了预处理、编译、汇编和链接四个步骤的全过程的行为,如下图所示。需要根据上下文进行一定的区分。

《计算机系统基础》大作业:程序人生

3.2 在Ubuntu下编译的命令

所使用的命令为gcc -m64 -no-pie -fno-PIC -S -o hello.s hello.i。在Ubuntu下的操作截图如下所示,执行完该命令后可以发现文件夹中多出了一个hello.s的文件。
《计算机系统基础》大作业:程序人生

3.3 Hello的编译结果解析

虽然说大作业中提示,应分3.3.1~3.3.x等子章节按照类型和操作对hello.s中的语句进行说明,对此我有一些不同的看法。

查看PPT中的相关页面发现共有以下的几种类型:数据、赋值、类型转换、sizeof、算术操作、逻辑/位操作、关系操作、数组/指针/结构操作、控制转移和函数操作如此多的类,而查看hello.s文件中却发现总共仅仅有53行。因此,我认为将对hello.s的解读分解成较多的板块虽然说内容比较详细,但是可能会造成对hello.s该文件中的内容缺乏一个整体性的理解。在本次大作业中,我将采用“从main函数开始,一步一步地对照hello.c源文件来解读hello.s中涉及到的语句,并在解读的过程中穿插一些课程中曾经学过的知识”这样一个思路完成本章节的撰写。

从main函数开始,hello.s中部分语句的截图如下所示。
《计算机系统基础》大作业:程序人生
可以看到,main函数中首先执行了pushq %rbp和movq %rsp, %rbp。这两条语句真的是再熟悉不过了,在学习课程第三章“程序的机器级表示”过程中每次调用一个函数就一定会出现这两条语句。%rbp为一个“被调用者保存寄存器”,在此程序中被调用者即为main函数。对于此类寄存器,被调用者可以采用两种策略进行处理:(1)不使用(2)将这类寄存器中的值压栈后正常使用,被调用者结束后将其弹栈还给调用者。此程序中后续要使用%rbp来访问函数中的局部变量,不得不使用该寄存器,因此对%rbp采用的是第二种处理方法。寄存器%rsp为栈指针,始终指向栈的顶端。movq %rsp, %rbp语句将%rsp的值赋给%rbp,而此时栈顶处存放的是旧的%rbp的值,因此可以这么理解:新的%rbp的值指向的是旧的%rbp的值在栈中存放的位置,如下图所示,该示意图也可以用来理解堆栈中局部变量的存放位置,马上就要讲到。
《计算机系统基础》大作业:程序人生
之后的几条指令为:subq 32, %rsp、movl %edi, -20(%rbp)和movq %rsi, -32(%rbp)。将%rsp的值减去一个立即数(用$符号表示)32,实际上是在栈中分配出大小为32字节的空间,用来存放被调用函数中所建立的局部变量,如上图所示。为局部变量分配完空间之后,将%edi的值存放在堆栈中%rbp-20的位置,将%rsi的值存放在堆栈中%rbp-32的位置。可以看出,堆栈中查找局部变量是通过%rbp来进行的,而并不是使用%rsp栈指针。

然而,%edi和%rsi中存放的又是什么东西呢?这就涉及到函数中参数传递的相关内容了。如下图所示,向一个函数传递参数时,根据参数的顺序,从前往后分别由%rdi、%rsi、%rdx、%rcx、%r8和%r9进行(更多的参数通过栈来传递)。
《计算机系统基础》大作业:程序人生
由此可知,%edi和%rsi中分别存放的是向main函数中传递的两个参数。传递的参数可以由hello.c源程序中看到,通过int main(int argc,char argv[])语句传递。因此,%edi中存放的是参数argc,%rsi中存放的是参数argv[],即命令行中输入参数的个数,以及所输入参数的内容(字符串)的首地址,并将这两个参数的内容存放在%rbp-20和%rbp-32中。

存放完局部变量的内容后的两条语句分别为cmpl $4, -20(%rbp)和je .L2。这两条参数就涉及到“关系操作”和“控制转移”的相关内容了。指令cmp所进行的操作实际上就是减法,但是cmp语句并不会修改减法运算中的减数与被减数,而是通过设置标志位,来判断减数与被减数之间是否相等或两者之间的大小关系。指令je为一种条件跳转指令,当cmp比较的两个对象相等时,进行跳转。更多的跳转指令如下图所示。
《计算机系统基础》大作业:程序人生
基于前面的讨论可知,-20(%rbp)中存放的是命令行中输入参数的个数。因此这条语句的意思可以理解为,将输入参数的个数与4进行比较,如果相等的话则跳转至.L2出的代码,如果不相等的话就继续运行。这与hello.c中的if语句相对应:if(argc!=4)不满足,执行之后的循环部分,满足时,执行条件判断内容的printf部分。由于printf中并没有参数,仅仅只打印一个字符串。因此,此处调用的函数为printf函数的简化版puts函数。调用puts函数(call puts)前进行了一次参数传递:movl $.LC0, %edi,也就是像puts函数中传入了需要打印的内容,也就是hello.s中.LC0段对应的内容。查看一下这段内容,如下图所示,发现这就是要答应的内容,对应hello.c源程序中printf中打印的字符串“用法: Hello 学号 姓名 秒数!\n”。
《计算机系统基础》大作业:程序人生
在打印了相关的提示信息之后,由于程序由于参数输入错误,并没有按照指定的要求运行,因此最后还会调用exit函数,返回状态设置为1,表示程序并未正常运行。这是通过movl $1, %edi和call exit这两句语句完成的,调用函数与参数传递的过程与之前调用puts函数时相似。
由之前的讨论可知,执行cmpl $4, -20(%rbp)后,若发现这两者相等,将会执行je .L2。代码跳转至.L2段,hello.s中该段内容如下图所示。
《计算机系统基础》大作业:程序人生
这部分就两行语句,movl $0, -4(%rbp)和jmp .L3。前者将一个立即数0赋值给%rbp-4的位置,后者无条件地直接跳转至.L3段。这是与hello.c源文件中的循环条件相对应的,也就是for(i=0;i<8;i++)中的i=0。变量i是在main函数中定义的一个局部变量,因此也通过栈进行管理,并使用新的%rbp寄存器进行引用。既然已经为循环进行初始化了,那么.L3段的内容应该就是for循环中其他的控制循环进行的语句。查看hello.s中.L3的语句,验证了我的想法,该段的语句如下图所示。
《计算机系统基础》大作业:程序人生
分析这段代码,可以清楚的发现这是用来控制循环体中循环的次数的。前两条语句cmpl $7, -4(%rbp)和jle .L4中,先是将%rbp-4处的值(根据之前的讨论可以知道,此处的值为循环体的初始变量i=0)与7进行比较,如果-4(%rbp)小于等于7,则会跳转至.L4段的代码。否则,程序将会继续执行下面的语句。之后的语句分别是call getchar、movl $0, %eax、leave和ret。首先调用了getchar函数,从缓冲区读走一个字符,相当于清除缓冲区。之后将%eax赋值为0,此时执行ret语句,表明main函数的返回值为0,这与程序最后的return 0对应。值得注意的是ret语句之前的这条leave语句,这条语句相当于两条语句mov %rbp, %rsp和pop %rbp的简化版。先将栈指针指向%rbp(新),也就相当于释放了main函数开始时使用subq $32, %rsp为局部变量释放的空间,此时%rsp栈指针指向栈中旧的%rbp的位置。之后执行pop语句,将旧的%rbp的值弹栈,相当于把旧的%rbp物归原主,以保证堆栈的平衡。
由以上的分析可知,.L3处的语句控制了循环的次数,且循环结束后剩余的一些语句也是在此处实现的。因此.L4段的代码一定就是循环中的循环体了。查看.L4中的语句,如下图所示。
《计算机系统基础》大作业:程序人生
从中我们看到,循环体中一共调用了三个函数printf、atoi和sleep,与源程序中相同。每次调用函数之前的语句都是用来进行参数传递的,我们一个一个来进行分析。调用printf之前,需要为%eax赋值,且第一个参数%edi中应该是所打印内容的格式串。在此,是将.LC1赋值给%edi,查看.LC1的内容,如下图所示,确实就是所答应内容的格式串Hello %s %s\n。
《计算机系统基础》大作业:程序人生
第二个和第三个参数用%rsi和%rdx存放,所对应的就是argv[1]和argv[2]。根据之前的讨论可以知道-32(%rbp)为argv数组的首地址,也就是argv[0]的位置。又由于数组在内容中是连续排列的,64位系统中每个地址元素是8字节,因此-32(%rbp)赋值给%rax后执行了addq $8, %rax,此时存放在%rax中的内容就是数组元素argv[1]所在的地址了。之后执行了movq (%rax), %rax和movq %rax, %rsi,对地址元素进行引用,并赋值给%rsi,作为传入printf函数的第二个参数。传入printf函数的第三个参数%rdx的分析结果与此类似,不再重复赘述。
需要注意的是语句中(%rax)与%rax的区别。他们分别对应了两种操作数的寻址方式:存储器寻址和寄存器寻址。(%rax)表示将%rax中的值视为一个地址,引用这个地址中的内容作为操作数,而%rax表示寄存器%rax的值就是需要的操作数。这两者比较容易混淆。

调用atoi之前将argv[3]作为参数传入,所对应的地址为-32(%rbp)再加上24。传入sleep函数的参数是atoi函数返回的结果,而返回的结果是存放在累加器%eax当中的,因此在调用完atoi函数后,直接将%eax赋值给%edi就能完成参数的传递。

这段代码的最后一条语句为addl $1, -4(%rbp)。-4(%rbp)中存放的是i,每次循环结束后需要执行for(i=0;i<8;i++)语句中的i++,以此与.L3段中的比较语句和条件跳转语句进行配合,来达到控制循环次数的作用。

3.4 本章小结

本章节与课程内容中的“程序的机器级表示”部分相对应,个人感觉这部分内容是本门课程中非常重要的一个部分。在本章节中进行了hello.i编译至hello.s的过程,并对hello.s中的内容进行了解读,阅读并详细地分析了文件中的汇编语言。分析的过程中,也对条件跳转和参数的传递展开等内容进行了讨论。

第4章 汇编

4.1 汇编的概念与作用

汇编过程是将编译后hello.s中的汇编语言通过汇编器翻译为机器指令的过程,同时这些机器指令被打包为可重定位目标程序的格式,并存放在hello.o文件中。汇编语言与机器指令之间存在这一一对应的关系,汇编器根据相关的对照表一句一句地进行翻译即可。

4.2 在Ubuntu下汇编的命令

所使用的命令为gcc -m64 -no-pie -fno-PIC -c -o hello.o hello.s。在Ubuntu下的操作截图如下所示,执行完该命令后可以发现文件夹中多出了一个hello.o的文件。
《计算机系统基础》大作业:程序人生

4.3 可重定位目标elf格式

使用readelf -a hello.o指令查看hello.o的ELF格式相关信息:
《计算机系统基础》大作业:程序人生
《计算机系统基础》大作业:程序人生
《计算机系统基础》大作业:程序人生
着重关注输出信息中的重定位节部分(Relocation section)。发现其中有两个类型为R_X86_64_32的重定位条目位于.rodata段,剩余的重定位条目均为hello.c源程序中所用到的函数:puts、exit、printf、atoi、sleep和getchar,他们的类型均为R_X86_64_PLT32,将会在链接的过程中重定位他们的符号引用地址。

使用readelf -x.rodata hello.o指令查看一下重定位节中两个位于.rodata段的元素内容是什么,如下图所示。原来是程序中puts输出的字符串和printf的格式串。
《计算机系统基础》大作业:程序人生

4.4 Hello.o的结果解析

使用指令objdump -d -r hello.o对hello.o进行反汇编,得到的结果如下图所示。
《计算机系统基础》大作业:程序人生
将使用objdump进行反汇编得到的内容与第三章编译中得到的hello.s进行对比可以发现:(1)反汇编结果中,除了右侧的汇编语言,左侧增加了由操作码和操作数组成的机器指令。通常机器指令的第一个数码为操作码,后续的数码为操作数。(2)操作数由十进制转换为十六进制(由0x开头)(3)每条机器指令有了对应的地址,而不是像hello.s中使用.L1和.L2等诸如此类的节来表示。(4)进行跳转时,通过跳转至某一特定的地址实现,而并非像hello.s中用.L1和.L2等诸如此类的节来作为跳转的标识(5)对可重定位的条目进行标识,此时尚未进行链接,机器码中用来表示符号引用的地址处均被设置为0,将在链接过程中进行补齐。

4.5 本章小结

本章节对hello.s汇编至hello.o的过程进行了讨论。过程中使用readelf工具对可重定位目标文件hello.o的elf格式进行了查看,并解读了hello.o中的相关内容。同时,也着重关注了hello.s与hello.o的区别。

第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程(如从hello.o生成为hello可执行文件),这个文件可被加载(复制)到内存并执行。在现代系统中,链接是由叫做链接器的程序自动执行的,使得分离编译称为可能。它将一个大型的应用程序分解为更小、更好管理的模块并且可以独立地修改和编译这些模块。当更改这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

大作业模板中要求使用ld作为链接命令,然而这条命令目前从来没有接触过。因此,在此处是仍然使用gcc进行链接。使用的命令为:gcc -m64 -no-pie -fno-PIC -o hello hello.o。在Ubuntu下的操作截图如下所示,执行完该命令后可以发现文件夹中多出了一个hello的可执行文件。
《计算机系统基础》大作业:程序人生

5.3 可执行目标文件hello的格式

使用指令readelf -S hello查看hello的ELF格式的节头,如下图所示。从中可以得到各段的基本信息(包括各段的起始地址,大小等)。
《计算机系统基础》大作业:程序人生

《计算机系统基础》大作业:程序人生

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息。一种方法是可以从Data Dump区域查询到相关的内容。举例说明:根据5.3节可知,.rodata段的起始位置在0x40200处,在Data Dump区域右键Goto Expression至该地址,可以看见这里存放的是puts函数输出的字符串以及向printf函数传入的格式串,如下图所示。
《计算机系统基础》大作业:程序人生
另一种方法更为直观,就是可以使用“左上方选项卡 - Plugins - SymbolViewer”直接可以查看到加载进来的符号,如下图所示。其中所显示的地址,与5.3节中的截图完全一致。
《计算机系统基础》大作业:程序人生

5.5 链接的重定位过程分析

使用命令objdump -d -r hello对hello可执行文件进行反汇编,如下图所示。为了与4.4节对hello.o进行反汇编的结果进行比较,此处仅截取了main函数的部分。
《计算机系统基础》大作业:程序人生
可以看到,最左侧一列的地址发生了变化:hello.o中是一个由0开始的两位数,而在hello中则是使用了虚拟内存空间的地址(从0x400000开始的一个数)。且hello.o中本来设定的重定位条目,如调用的函数没有明确的调用地址(全为0),此时在链接完成之后,被填入了一个明确的虚拟内容空间中的地址。
此外还可以发现,
在此,也可以重新回顾一下重定位的过程。在课程中讲解重定位的步骤时,可以简单地归纳为两个步骤:(1)重定位节和符号定义(1)重定位节中的符号引用。在第一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。在第二步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。

5.6 hello的执行流程

从加载hello到_start,到call main,以及程序终止的所有过程,调用与跳转的各个子程序的地址由低到高依次为:
0000000000401000 <_init>:
0000000000401020 <.plt>:
0000000000401030 [email protected]:
0000000000401040 [email protected]:
0000000000401050 [email protected]:
0000000000401060 [email protected]:
0000000000401070 [email protected]:
0000000000401080 [email protected]:
0000000000401090 <_start>:
00000000004010c0 <_dl_relocate_static_pie>:
00000000004010d0 <deregister_tm_clones>:
0000000000401100 <register_tm_clones>:
0000000000401140 <__do_global_dtors_aux>:
0000000000401170 <frame_dummy>:
0000000000401172 :
0000000000401200 <__libc_csu_init>:
0000000000401260 <__libc_csu_fini>:
0000000000401264 <_fini>:

5.7 Hello的动态链接分析

为了分析hello的动态链接过程,需要重点关注所加载的符号中的_GLOBAL_OFFSET_TABLE这一项。查看Loaded Symbols发现,该项的地址位于0x404000处,因此edb一加载hello可知行文件使用Data Dump观察这个地址上的内容。在调用dl_init之前时,Data Dump中该地址处部分信息缺失,如下图所示。
《计算机系统基础》大作业:程序人生

在调用dl_init之后的结果如下图所示。
《计算机系统基础》大作业:程序人生

5.8 本章小结

在本章节中进行了由hello.o链接生成为hello可执行文件的过程。与上一章类似,使用了readelf工具查看了hello的elf格式。同时,也使用了edb软件对hello的执行流程进行了考察,并对hello的动态链接具体实现的过程进行了分析。

第6章 hello进程管理

6.1 进程的概念与作用

进程的定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
通过进程的概念,在运行程序时得以会提供两个假象:(1)程序好像是独占地使用处理器和内存的(2)程序中的代码和数据好像是系统内存中唯一的对象。

6.2 简述壳Shell-bash的作用与处理流程

Shell是一个交互型的应用级程序,它代表用户运行其他程序,bash是shell的一个变种。
Shell能够执行一系列的“读/求值”步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程相同但是独立的用户级虚拟地址空间副本,包括代码和数据段、堆、共享库以及用户栈。同时,子进程还获得与父进程任何打开文件描述符相同的副本。
注意,父进程和子进程之间的最大的区别在于它们有不同的PID。函数fork只被调用依次,但是会返回两次:一次是在父进程中,一次是在子进程中。通过返回值来区分程序是在父进程还是在子进程中执行。

6.4 Hello的execve过程

函数execve将会加载并运行可知行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,如找不到filename,exceve函数才会返回到调用程序。
与fork函数不同的是:execve函数调用一次并从不返回。

6.5 Hello的进程执行

在Linux平台下,每个进程都具有独立的私有地址空间/虚拟地址空间,它们都具有相似的结构,如下图所示。
《计算机系统基础》大作业:程序人生
处理器能够并发地执行多个进程,在进程进行调度时将会(1)将寄存器的当前值保存到内存(2)调度下一个进程执行(3)加载保存的寄存器组,并切换地址空间(上下文切换),完成调度。其中的上下文切换就是在内核模式下进行的,如下图所示。
《计算机系统基础》大作业:程序人生

6.6 hello的异常与信号处理

hello执行过程中会出现:(1)终止:会产生SIGINT信号,程序的运行被终止(2)中断:会产生SIGSTP信号,程序的运行被挂起。
执行hello程序,在程序的运行过程中ctrl + z将进程挂起,如下图所示。
《计算机系统基础》大作业:程序人生
运行ps,如下图所示。
《计算机系统基础》大作业:程序人生
运行jobs,如下图所示。
《计算机系统基础》大作业:程序人生
运行pstree,如下图所示(仅截取了部分)。
《计算机系统基础》大作业:程序人生
运行fg,进程收到SIGCONT信号,继续运行,如下图所示。
《计算机系统基础》大作业:程序人生
运行kill -9 2813,发送SIGKILL信号给指定的pid杀死指定的进程,如图所示。
《计算机系统基础》大作业:程序人生

6.7本章小结

本章节中主要对运行hello后的进程展开讨论,并简单地回顾了进程fork过程、exceve过程和进程的私有地址空间等内容。通过在终端中运行hello并输入相关的指令,对进程的异常与信号处理部分的相关内容也有了更深入的理解。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址指由程序产生的与段相关的偏移地址部分,如hello.o中的相对偏移地址。如果地址空间中的整数是连续的,那么我们称这个地址空间为一个线性地址空间。在一个带虚拟内存的系统中,CPU将会生成一个N个地址的虚拟地址,这个地址空间称为虚拟地址空间,也就是可执行文件hello中的地址。一个系统中的物理地址空间对应于系统中物理内存的M个字节。在hello运行的过程中,物理地址指的是那些真正位于内存中的且与其虚拟地址空间存在映射关系的那些地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

段式管理通过段寄存器(如CS等)与偏移地址(16/32/64,EA)的组合来完成逻辑地址到线性地址的变换。在实模式下,逻辑地址CS: EA所转换得到的物理地址为CS * 16 + EA。在保护模式下,以段描述符作为下标,到GDT/LDT表中查表获得短地址,将段地址加上偏移地址,得到线性地址,完成转换。转换的过程可用以下这页幻灯片更好地说明。
《计算机系统基础》大作业:程序人生
其中段寄存器有以下几种:(1)CS代码段,程序代码所在段(2)SS栈段,栈区所在段(3)DS数据段,全局静态数据区所在段(4)其他三个段寄存器ES、GS和FS可指向任意数据段。
值得注意的是,Linux系统为了保证处理器平台的可移植性而简化了分段机制,仅使用IA-32的分页机制,段描述符的基址初始化为0,也就是无分段处理。

7.3 Hello的线性地址到物理地址的变换-页式管理

虚拟内容被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,作为数组的索引。磁盘上数组的内容被缓存在主存中。这些数据在缓存时被分割成块,作为磁盘和主存(较高层)之间的传输单元。虚拟内存系统通过将虚拟内存分割为大小固定的“虚拟页”来处理这个问题。同时物理内存也被分割为大小等同与虚拟页的“物理页”,并与“虚拟页”之间建立映射关系。从而达到由线性地址(虚拟地址)到物理地址的变换,如下图所示。
《计算机系统基础》大作业:程序人生

7.4 TLB与四级页表支持下的VA到PA的变换

为了部分消除MMU机制从高速缓存中请求页表条目不命中时将会读取内存从而产生的巨大开销,MMU中建立了一个翻译后备缓冲器(之后将简称其为TLB)。TBL是一个专门为页表设计的一个组相联高速缓存,速度甚至接近于处理器,用来存放常用的页表条目。虚拟地址中将虚拟页号(v*n)拆分为两个部分:TLB标记和TLB索引,用以访问TLB,如下图所示。
《计算机系统基础》大作业:程序人生
四级页表下虚拟地址到物理地址的转换可以用教材中Core i7举例,如下图所示。36位的v*n被划分为四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含一级页表的物理地址。v*n1提供一个一级页表条目(PTE)的偏移量,这个PTE包含二级页表的基地址。v*n2提供到一个二级页表条目的偏移量,以此类推。
《计算机系统基础》大作业:程序人生

7.5 三级Cache支持下的物理内存访问

仍然以教材中的Core i7系统为例,其内存系统简图和地址翻译的概况如下图所示。
《计算机系统基础》大作业:程序人生
《计算机系统基础》大作业:程序人生
处理器封装包括四个核、一个大的所有核共享的L3高速缓存,以及一个DDR3内存控制器。每个核包含一个层次结构的TLB、一个层次结构的数据和指令高速缓存,以及一组快速的点到点链路(让一个核和外部I/O桥直接通信)。TLB是虚拟寻址的,是四路组相联的。L1、L2和L3高速缓存是物理寻址的,块大小为64字节。L1和L2是8路组相联的,而L3是16路组相联的。页大小可以在启动时被配置为4KB或4MB。Linux使用的是4KB的页。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

假设在运行当前进程中的程序执行了如下的execve调用:execve(“a.out”, NULL, NULL);。
函数execve在当前进程中加载并运行包含在可知行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行需要以下几个步骤:(1)删除已存在的用户区域(2)映射私有区域(3)映射共享区域(4)设置程序计数器。加载器映射用户地址空间区域的过程如下图所示。
《计算机系统基础》大作业:程序人生

7.8 缺页故障与缺页中断处理

在虚拟内存中,请求物理页时缓存未命中的现象被称为缺页。接下来将举例说明缺页中断处理的过程。如下图所示,CPU引用了VP3中的一个字,但是并没有缓存在DRAM中。
《计算机系统基础》大作业:程序人生
地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,触发了缺页夜场。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页(存放在PP3中的VP4)。若VP4被修改,那么内核会将其复制回磁盘。接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。目前,VP3已经缓存在主存中,一定会页命中并正常处理。缺页异常处理后的状态如下图所示。
《计算机系统基础》大作业:程序人生

7.9动态存储分配管理

这部分内容在课程讲解的过程中并没有相关要求,略过本节内容。

7.10本章小结

本章节中主要以总结课程中“虚拟内存”这一部分内容的重点为主。从段式管理到页式管理出发,较为详细地讨论了虚拟地址到物理地址的变换过程。同时,对进程在调用fork和exceve函数时的内存映射进行了回顾。最后,总结了发生缺页故障时的处理步骤。由于老师在上课的过程中没有对“动态内存”作相关的要求,这一部分的总结略去。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件。文件的类型有如下几种:(1)普通文件,包含任意数据的文件(2)目录,包含一组链接的文件,每个链接都将一个文件名映射到一个文件(3)套接字,用来与另一个进程进行跨网络通信的文件(4)命名通道(5)符号链接(6)字符和块设备。
设备管理:unix io接口,具有如下的几个作用:(1)打开和关闭文件(2)读取和写入文件(3)改变当前文件的位置

8.2 简述Unix IO接口及其函数

本教材这部分内容介绍了以下的几个函数:(1)打开文件open(),打开一个已经存在的文件或者创建一个新文件(2)关闭文件close(),关闭一个打开的文件(3)读取文件read(),从当前文件位置复制字节到内存(4)写入文件write(),从内存复制字节到当前文件位置(5)改变文件位置lseek()。

8.3 printf的实现分析

函数printf实现的内部过程中调用了vsprintf和write函数,接受一个格式串之后将匹配到的参数按照格式串的形式输出。在第三章解读汇编语言时就曾提到过这一点,调用printf函数之前除了要将累加器%rax设置为0之外,向printf函数传入的第一个参数%rdi就是所谓的“格式串”,如%s、%d等等。这些格式串如同全局变量一般存放于.rodata节当中。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

函数getchar在具体实现的过程内部将会调用read函数,将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。但是只有buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章节也上一章类似,仍然是对课程内容的总结。章节的前半部分简单地介绍了系统中IO设备的管理方法以及IO接口函数。后半部分查询了相关的资料,对printf和getchar函数的实现进行了简要地分析。

结论

最后,对hello一生中所经历的过程逐条进行总结:
(1)编写源程序hello.c
(2)hello.c进行预处理生成hello.i,可以在hello.i中找到外部文件帮助它的痕迹
(3)hello.i编译生成hello.s,其中的内容被编译为汇编语言
(4)hello.s汇编生成hello.o,其中汇编语言逐条转换为机器语言
(5)hello.o终于链接生成了可执行目标文件hello
(6)在linux下的终端中运行hello
(7)shell中调用fork函数创建子进程,并调用execve函数加载并运行hello
(8)hello似乎还挺强大的,似乎能够独占地使用CPU和内存系统
(9)在运行过程中收到信号,hello进程必须做出相应的反应,可能这就是被生活逼的喘不过气来的样子吧
(10)直到收到SIGINT信号,进程终止。从此以后,hello再也不动了,被shell父进程回收子进程。
学习完这门课程,不免也让我回想起大一刚学习C语言时我的样子。那时,我用机房电脑里的CodeBlocks写下了我的第一个hello.c。那时,谁又能想到一个齿轮和一个三角形按钮的背后竟然如此的复杂呢?

附件

hello.i:对hello.c进行预处理时产生的中间文件。
作用:(1)将源文件中以include格式包含的文件复制到编译的源文件中(2)用实际值替换用#define定义的字符串(3)根据#if后面的条件决定需要编译的代码。

hello.s:对hello.i进行编译时产生的中间文件。
作用:生成汇编语言程序。

hello.o:对hello.s进行汇编时产生的中间文件。
作用:通过汇编器翻译为机器指令的过程,同时这些机器指令被打包为可重定位目标程序的格式。

hello:由hello.o链接生成的可执行目标文件。
作用:完成编写hello.c时所设计的功能。

参考文献

[1]课程教材《深入理解计算机系统》第三版
[2]C语言中的预处理详解. https://blog.****.net/dlutbrucezhang/article/details/8753765
[3]C语言编译过程详解. https://www.cnblogs.com/CarpenterLee/p/5994681.html
[4]C语言getchar()的用法. https://blog.****.net/wkwk7600/article/details/83418109
[5]prinf函数实现的深入剖析. https://www.cnblogs.com/pianist/p/3315801.html
[6]C语言中getchar()和putchar()的实现细节. https://blog.****.net/happyforever91/article/details/51713741