哈工大CSAPP大作业---hello的一生

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业         英才学院计算学部            

学     号         7203610626         

班     级           2036014          

学       生           罗腾          

指 导 教 师            刘宏伟           

计算机科学与技术学院

2021年5月

摘  要

    Hello程序可以说是每一个程序员打开编程世界大门的金钥匙,尽管它非常简单,但是为了让它能够运行,系统的每个主要组成部分都需要协调工作。本文从计算机系统的角度出发,分析了hello程序的生命周期:从它被程序员创建开始,到在系统上运行,输出简单的消息,然后终止。在分析过程中,会介绍一些相关的概念、组成部分,并逐步体会到计算机系统精细而又美妙的架构体系。

关键词:计算机系统;hello程序                 

目  录

第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简介

               P2P:hello程序的生命周期是从一个文件名是hello.c的源文件(源程序program)开始的,之后预处理器根据以字符#开头的命令修改原始的c程序,得到一个修改了的文本文件hello.i,之后编译器会将文本文件hello.i翻译成文本文件hello.s,再然后汇编器会将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中,该目标文件是一个二进制文件。之后该可重定位文件经过链接器的链接,生成可执行目标文件hello,此时即可把可执行文件加载到内存中,由系统执行,即成为一个进程(process)。

               O2O:当我们在键盘输入“./hello”并按下回车键后,shell将调用fork函数创建子进程,子进程通过execve系统调用启动加载器,加载器删除子进程现有的虚拟内存段之后会创建一组新的代码、数据、堆和栈。之后,虚拟地址空间的页将会被映射到可执行文件的页大小的片,新的代码和数据段将被初始为可执行文件的内容。再然后加载器跳转后会调用应用程序的main函数执行hello程序的代码,CPU会为程序分配时间片并执行逻辑控制流。当程序运行结束后它会被回收,内核会从系统中删除掉它的所有痕迹。

1.2 环境与工具

1.2.1 硬件环境

处理器:Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz

RAM:16.00GB。

1.2.2 软件环境

Windows10 64位;Vmware 15;Ubuntu 16.04 LTS 64位。

1.2.3 开发工具

Visual Studio 2022 64位;CodeBlocks 64位;Dev-c++。

1.3 中间结果

hello.c  源程序:能够被人读懂的c语言程序,以字节序列的方式储存

hello.i  预处理后文件:读取了相关头文件的内容

hello.s  编译后的汇编文件:包含了用汇编语言表示的main函数定义

hello.o  汇编后的可重定位目标执行文件:可被链接以待后续加载执行

hello   链接后的可执行文件:可以被加载到内存中由系统执行

elf.txt   hello.o的ELF

rehello.s  hello.o的反汇编文件

elf2.txt   hello的ELF

rehello2.s  hello的反汇编文件

1.4 本章小结

本章简要介绍了hello.c程序的P2P和O2O过程,并列出了实验所使用的的软硬件环境以及开发工具,同时也列出了实验中用到的中间文件及其作用。

第2章 预处理

2.1 预处理的概念与作用

预处理是指在程序源代码被翻译为目标代码的过程中,生成二进制代码 之前的过程,预处理器会根据以字符#开头的命令修改原始的c程序。

预处理阶段可以看成是编译的一部分,它会根据程序开头的相关指令读取头文件的内容并把它直接插入程序文本中,为后续的编译工作做准备。

2.2在Ubuntu下预处理的命令

               预处理命令:gcc -E hello.c -o hello.i

图1:预处理命令和hello.i文件

2.3 Hello的预处理结果解析

文件的内容大大增加,原因是预处理过程中对stdio.h,unised.h,stdlib三个头

文件进行了展开,相应头文件的源码被拷贝到了hello.i文件中。

2.4 本章小结

本章简要介绍了预处理的概念和作用,同时在ubuntu中用预处理命令对hello.c文件进行了预处理并展示了预处理后的源程序,同时也对预处理结果进行了简要解析。

第3章 编译

3.1 编译的概念与作用

编译是指编译器(ccl)将文本文件hello.i翻译为ASCII码汇编语言文件hello.s的过程。

编译的作用就在于将代码翻译成不同高级语言的不同编译器通用的输出语言:汇编代码。编译器可以检查程序的语法错误,给出相应提示;通过改变编译优化等级,用户还可以在编译的时候对代码进行优化,提升程序的性能。

编译器还可以根据用户指定的不同优化等级,尝试对代码进行安全的、等价的优化,通过优化底层代码的执行逻辑,提升代码的执行效率和性能。

3.2 在Ubuntu下编译的命令

        编译命令:gcc -S hello.i -o hello.s

图2:ubuntu下进行编译以及生成的hello.s文件

3.3 Hello的编译结果解析

3.3.1 数据

常量(字符串常量和立即数)

字符串常量被存储在了.rodata节中,如图:

图3:字符串在.rodata节中进行存储

而立即数则是直接作为汇编语言的一部分存储在.text段中,在汇编代码中,立即数前会加上一个$符号,如图:

 图4:立即数在汇编语言中的表示

局部变量

局部变量存储在内存(栈)或寄存器中。例如,源程序中循环部分的循环控制变量i,它在汇编代码中被存在了栈中-4(%rbp)处。

            

图5:源程序定义循环控制变量        图6:局部变量被放在栈中

3.3.2 赋值操作

赋值操作可通过使用最简单形式的数据传送指令—MOV类指令完成,这类指令把数据从原位置复制到目的位置,不做任何变化(movb、movw、movl、movq、movabsq分别传送字节、字、双字、四字、绝对四字)。

如图5和图6所示,对i(32位)赋初值0,就是用movl指令将立即数0传送至存储i的栈的位置。

3.3.3 算术操作

汇编中有add,sub,imul,xor等算术操作对应着c语言程序中的+、-、*、异或等操作,在hello源程序中有对循环变量进行+1的操作,对应到汇编中是通过addl指令完成的。                                                                          

图7:算数操作+1在汇编中对应的指令

3.3.4 关系操作

源程序中出现了两处关系操作,如图:

    

图8:判断是否相等                 图9:判断是否小于

它们在汇编代码中对应的指令是cmp指令类,该指令会根据两个操作数之差来设置条件码,以待后续进行相应跳转(关系操作的差异其实是根据跳转指令的不同来体现出的),如图:

                          图10:上图对应着的cmpl指令

3.3.5 数组

源程序中有一个指针数组char *argv[],数组中每一个元素都指向一个字符串的首地址。其中argv[0]存放着存放输入程序的路径和名称的地址,而argv[1]和argv[2]、argv[3]分别指向三个字符串数组的首地址,用于之后对字符串的输出。字符型指针占8个字节,对应地,argv[1]、 argv[2]和argv[3]三个元素分别被存放在了-32(%rbp)+8,-32(%rbp)+16和-32(%rbp)+24中(内存可以看成是字节数组,这里刚好相差了8个字节,是合理的)。

图11:指针数组在内存中的位置

3.3.6 控制转移

如图10所示,判断完成后,条件码已经被设置好,这时跳转指令je(jle)会根据条件码的某种组合,或者跳转,或者继续执行下一条指令。汇编器会确定所有带标号指令的地址,并将跳转目标编码为跳转指令的一部分,从而实现控制转移。

在源程序的循环语句中也用到了控制转移,如图:

图12:for循环语句对应的汇编代码

整个代码的结构十分清晰:L2中对循环变量赋初值,跳转到L3判断是否继续循环,若成功则跳转到L4,执行循环体的内容,在循环体的最后要对循环变量进行+1的操作,之后重复上述步骤直至循环条件不满足时退出循环。

3.3.7 函数操作:参数传递(地址/值)、函数调用()、局部变量、函数返回

X86-64中约定,进行参数传递时,前6个参数会依次存储在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,而其余参数将存放在栈中。函数调用call指令会把下一条指令的地址压入栈,并将pc设置为目标代码段的首地址;而函数返回ret指令会从栈中弹出返回地址,并将pc设置为该地址。另外,由于寄存器时被过程共享的,因此必须保证调用时,被调用者不会覆盖调用者稍后会使用的寄存器值,因此在被调用者使用寄存器存储局部变量之前,要先对寄存器中的值进行保护,其中%rbx、%rbp和%r12至%r15是被调用者保存寄存器,而其他的寄存器除了%rsp之外都是调用者保存寄存器。

下面对hello程序中出现的函数操作进行相应解析:

Main函数是由定义在libc.so中的系统启动函数调用的,该启动函数在调用前首先初始化执行环境,在结束时会处理main函数的返回值,main函数的返回值是0,被保存在寄存器%eax中进行返回。

下图所示的汇编代码中调用了printf函数(发生在源程序中if条件判断不成功时)

 

图13:输出学号姓名等信息            图14:调用exit函数

该函数传入的参数是一个字符串常量,printf函数会将它输出到屏幕上;而图14中则是将立即数1作为参数调用函数exit,执行exit函数会返回1表示程序正常终止。而下面这幅图展示的是另一处printf函数的使用。

 

图15:主循环中的输出操作            图16:atoi函数和sleep函数

此处一共传入了三个参数,其中寄存器%rsi和%rdx中分别是argv[1]和argv[2]的值,也就是两个字符串的首地址,而寄存器%rdi中存放的是字符串常量"Hello %s %s\n",相当于是打印的格式。传入参数之后,call指令调用printf函数进行输出。

而图16中展示的汇编代码对应于源程序中的“sleep(atoi(argv[3]))”语句。首先把储存在栈中-32(%rbp)+24处的argv[3]存入寄存器%rdi中,之后调用atoi函数,该函数将字符串转换为整型数并保存在寄存器%eax中返回,而调用sleep函数之前又把这个返回的整数值存放在%edi中进行调用,sleep函数利用这个整数值使程序暂停若干时间。

3.4 本章小结

本章首先介绍了编译的概念和作用。在对hello.i进行编译后,从数据、赋值操作、算术操作、关系操作、控制转移和函数操作几方面对编译好的hello.s文件进行了分析,从机器的角度描述了程序的行为。

第4章 汇编

4.1 汇编的概念与作用

      汇编是指汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫

做可重定位目标程序的格式,并将结果保存在目标文件hello.i中的过程。汇编的

作用在于将汇编语言文件翻译成可重定位目标文件,以待链接器对它进行链接。

4.2 在Ubuntu下汇编的命令

    命令:gcc -c -o hello.o hello.s

      图17:汇编命令以及生成的hello.o文件

4.3 可重定位目标elf格式

输入readelf -a hello.o > ./elf.txt提取文件ELF,下面对ELF格式进行分析。

首先是ELF头,它以一个16字节的序列开始,该序列描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器进行语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量,如图:

                 图18:ELF头信息

    接下来是节头部表,它描述了不同节的的位置和大小等信息,其中目标文件

中每个节都有一个固定大小的条目:

    

                  图19:节头部表

     重定位条目是链接器修改代码节和数据节中对每个符号的引用,是他们指

向正确运行地址所依赖的关键数据结构。当汇编器遇到对最终位置未知的目标引

用时,他就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件

时如何修改这个引用。

     

                            图20:重定位节信息

     上图展示了ELF重定位目标的格式。偏移量是需要被修改的引用的节偏

移,符号名称标识被修改引用应指向的符号,类型告知链接器如何修改新的引用,

加数是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调

整。

     另外,每个可重定位目标模块都有一个符号表,如图:

    

                         图21:包含18个条目的符号表

        该符号表存放在程序中定义和引用的函数和全局变量的信息,但是不包含对

应于本地非静态程序变量的任何符号,这些符号在运行时是在栈中被管理的。

4.4 Hello.o的结果解析

对机器语言反汇编生成rehello.s文件,下面将它与hello.s对照分析。

           图22:反汇编代码

以上图所示反汇编代码为例,首先可以发现所有的立即数都是用十六进制表示的,而汇编代码中是用十进制表示的;其次,反汇编代码中每一行前面都有一个指令相应的机器编码,该机器编码是根据x86—64指令集将机器语言和汇编语言建立映射关系的,包含了指令类型,可能出现的附加的寄存器指示符字节以及一个附加的8字节常数字。

机器语言中操作数和汇编语言是不一致的,汇编语言中的操作数例如一个寄存器等,都是直接表示出来,在机器语言中则是需要进行编码处理。

另外,分支转移也有所不同:

 

图23:汇编代码中的分支转移        图24:反汇编代码中的分支转移

如图,汇编代码中使用段名L2进行跳转,而反汇编代码中使用目标代码的虚拟地址(main+0x2f)进行跳转。

同时,在函数调用方面情况也类似:汇编代码中会出现函数的名字,而反汇编代码中调用的是虚拟地址:

 

图25:汇编代码中的函数调用         图26:反汇编代码中的函数调用

可以发现,在分支转移和函数调用相应代码下面都能看到相应重定位条目,这是因为这时它们还都是对最终位置未知的目标引用,在链接器确定运行时内存地址之后,链接器会利用这些重定位条目修改相应的引用,使他们指向正确的运行时地址。

同时应注意到的是,objdump工具为了方便,将重定位条目和指令显示在了一起,实际上它们是存放在目标文件的不同节当中的。

4.5 本章小结

本章首先介绍了汇编的概念和作用。之后查看了hello.o的ELF信息,具体分析了ELF头、节头部表、重定位节和符号表这几个节。之后又用反汇编代码文件和hello.s文件,对比分析了机器语言和汇编语言的差异,包括操作数以及分支转移和函数调用等方面的不同。

5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。

链接器在软件开发中扮演关键角色,它使得分离编译成为可能,我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小且更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需要简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

图27:在Ubuntu下链接

输入如图所示的命令,即可完成链接。

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

输入readelf -a hello.o > ./elf.txt提取文件ELF,下面对ELF格式进行分析。

       总的来说,hello可执行文件的ELF格式类似于hello.o文件的格式,它是由ELF头来进行描述的:

图27:可执行文件的ELF头

和可重定位目标文件相比,它还包括了程序的入口点地址。而.text、.rodata、.data节与可重定位目标文件的节是相似的,只不过这些节已经被重定位到了它们最终的运行时内存地址。

另外,可执行文件的连续的片在被映射到连续的内存段时的映射关系是由程序头部表描述的,该表中包含目标文件中的偏移,内存地址,目标文件中的段大小等信息,如图:

    图28:程序头部表

各段的基本信息,包括段的起始地址,大小等信息被存储在节头部表中:

     

     图29:节头部表

5.4 hello的虚拟地址空间

   使用edb加载hello,从Data Dump中查看本进程的虚拟地址空间各段信息,如图所示:

 

图30:虚拟地址空间各段信息

   这一段程序的地址是从0x401000开始的,之后各节的地址都显示在左边,对应着5.3节中图29所示的节头部表中的地址信息。

5.5 链接的重定位过程分析

对机器语言反汇编生成rehello2.s文件,下面将它与rehello.s对照分析。

首先,hello与hello.o的最重要的不同之处在于链接器此时已经修改了代码节和数据节中对每个符号的引用,使他们指向了正确的地址(因此hello中也自然不存在可重定位条目了),如图:

图31:尚未修改的引用

图32:已经修改的引用

下面以此为例,分析hello中是如何进行重定位的:首先,重定位条目告诉链接器修改开始偏移量0x5e处的32位PC相对引用,这样在运行时它会指向printf例程。编译器在确定该节的运行地址为0x401125之后,可以得到引用的运行时地址=0x401125+0x5e=0x401183。并且此时编译器也已确定printf的运行地址为0x4010a0,由此可计算引用=0x4010a+(-4)-0x401183=0xffffff19,这样就完成了对引用的修改,即完成了重定位。

另外的不同来源于动态链接,即hello文件里包含了源代码中使用的库函数,如printf,getchar,atoi,exit,sleep等函数,如图:

图33:引用的库函数

另外,hello中增加了init节,该节中定义了一个小函数­_init,程序的初始化代码会调用它。

图34:init节的内容

链接的过程就是将各个目标文件重新组装在一起,包括与静态库链接和与动态库链接(加载和链接共享库)。其中与静态库链接又分为符号解析(把代码中的每个符号引用和正好一个符号定义关联起来)以及重定位,后者分为重定位节和符号定义以及重定位节中的符号引用两步,在本节开头已经进行分析,不再赘述。

5.6 hello的执行流程

加载器首先创建一段内存映像,在程序头部表的引导下,加载器将可执行文件的篇复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是_start函数的地址,该函数会调用系统启动函数_libc_start_main,后者初始化执行环境,调用用户层main函数,如图:

图35:edb中main函数

之后将依次printf、exit、printf、atoi、sleep、_getchar和exit这些函数。各个子程序名如图所示:

图36:调用与跳转的各个子程序名

5.7 Hello的动态链接分析

       动态链接器使用过程连接表PLT以及全局偏移量表.got.plt实现函数动态链接,通过edb调试,发现在dl_init前后,这些项目发生了一些内容变化:

图36:dl_init前项目内容

图37:dl_init后项目内容

由图可见,动态链接后包含了正确的绝对地址。

5.8 本章小结

       链接将各种代码和数据片段收集并合成为一个单一的可执行目标文件,主要过程包括符号解析和重定位,此外动态链接还可以在程序运行和加载时将共享库加载到任意的内存地址,和一个在内存中的程序链接起来。链接完成之后得到的可执行文件就可以被加载执行了。

6章 hello进程管理

6.1 进程的概念与作用

进程是计算机科学最深刻的概念之一。不同于“程序”或“处理器”,它是指一个正在运行的程序的实例。

系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。进程最关键的作用就在于它提供给程序的关键抽象:一个独立的逻辑控制流(就好像我们的程序独占地使用处理器);一个私有的地址空间(就好像我们的程序独占地使用内存系统)。

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

       Shell是一个交互型应用级程序,代表用户运行其他程序。

其处理流程是:从终端读取命令行并解析命令得到参数,判断是否是内置命令并进行相应的处理(即代表用户运行)。

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的、处于运行状态的子进程,函数被调用一次,却返回两次:子进程返回0,父进程返回子进程的PID。新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本;子进程获得与父进程任何打开文件描述符相同的副本;子进程有不同于父进程的PID。Shell在处理非内置命令时,会使用fork+execve运行程序。

图38:shell中输入命令行

如图,输入命令行之后,shell会为hello程序fork一个子进程。

6.4 Hello的execve过程

 exceve函数在当前进程的上下文中加载并运行一个新程序,它会覆盖当前进程的地址空间,但并没有创建一个新进程。Shell为hello程序fork一个新进程后,eval函数会调用exceve函数加载并运行hello,并带参数列表argv和环境变量列表envp。

Execve调用一次并从不返回,只有当出现错误时(例如找不到文件名等),execve才会返回到调用程序。

6.5 Hello的进程执行

进程由常驻内存的操作系统代码块(称为内核)管理,内核不是一个单独的进程,而是作为现有进程的一部分运行。

上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

一个进程执行它的控制流的一部分的每一时间段叫做时间片,进程调度时,首先会将当前进程的寄存器值保存到内存,之后通过上下文切换装载新进程之前保存的寄存器、切换地址空间并将控制转移给新进程,完成进程调度。通过上下文切换,控制流从一个进程传递到另一个进程。

Execve加载完文件名hello之后,他调用启动代码_libc_start_main,启动代码设置栈,将控制传递给hello的主函数,此时进程从内核模式转到了用户模式。在主函数的for循环中,每次输出字符串之后,都会调用sleep函数,此时进程由用户模式转到内核模式。之后的每次循环,也都是重复这样的过程。

6.6 hello的异常与信号处理

正常情况下,hello会循环的输出“hello 7203610626 罗腾”(会间隔一会,间隔时间是由用户指定的)这一信息。

程序运行过程中按键盘,如不停乱按,包括回车,进程正常执行,不过这些乱按的输入也会被输出(包括回车):

 图39:乱按的输入也会被输出

输入Ctrl-C和Ctrl-Z,会造成一种高层的软件形式的异常,即Linux信号。在本例中,输入Ctrl-C(Ctrl-Z)的结果就是内核会发送一个SIGINT(SIGSTP)信号给前台进程组中的每个进程,使他们被强制终止(被挂起)。下面结合截图进行分析(包含对其他一些命令的分析):

图40:输入Ctrl-Z

如图,输入Ctrl-Z使得进程暂时被挂起。

图41:输入ps和jobs

输入ps,可以查看当前所有进程的相关信息,可以发现此时hello程序仍然存在;输入jobs可以查看前台作业号。

图42:输入pstree命令

输入pstree命令,以树形结构显示了程序和进程间的关系。

图43:输入fg 1

输入fg 1,将hello进程重新调回前台运行。

   

    图44:输入kill -9 -3010

    输入kill -9 -3010,将信号SIGKILL(编号9)发送给hello所在进程

组中的每一个进程,使得它们都被终止,如上图所示。

图44:输入ctrl-c

输入Ctrl-C,内核会发送一个SIGINT信号给前台进程组中的每个进程,

使他们被强制终止。此时已经查找不到hello进程的信息。

6.7本章小结

       本章先回顾了进程的概念和作用,以及Shell-bash的作用与处理流程。之后介绍了hello的进程管理,包括进程的创建、加载和执行过程,以及对异常和信号的处理。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址由一个段标识符和一个指定段的地址偏移量构成,在汇编文件hello.s中用来指定数据和指令的地址。

线性地址(即虚拟地址):线性地址=段基址+逻辑地址中偏移量。Hello中的线性地址对应着hello.o反汇编文件中看到的地址(即逻辑地址)中的偏移量加上对应段的基地址。分页模式下,线性地址需经过变化得到物理地址,否则线性地址就是物理地址。

物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每字节都有一个唯一的物理地址,可以看成是对内存这个巨大数组的索引。hello运行过程中若出现加载指令访问内存,需要先通过CPU产生虚拟地址,再将该虚拟地址转换成适当的物理地址(即地址翻译)并据此访问内存相应位置。

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

逻辑地址包括段标识符和段内偏移量。前者是一个16位长的段选择符,其中前13位是索引号,即段描述符。一个段描述符描述一个段,因此可以通过段标识符的前13位,直接在段描述符表中找到一个段描述符,进而找到它所描述的一个段。线性地址包括段基址和逻辑地址中的偏移量。其中,段基址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。

下面是由逻辑地址到线性地址的转换:首先根据段选择符的T1,知道当前要转换是GDT中的段,还是LDT中的段,再根据段选择符中的前13位,在相应段描述符表中查找到对应的段描述符并得到相应段的其基地址,该基地址加上逻辑地址中的段内偏移量即为线性地址。

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

将虚拟内存作为缓存工具时,为了处理磁盘和内存之间的传输问题,VM系统采用了页式管理的方法,即将虚拟内存和物理内存分割为虚拟页和物理页这样大小固定的块(两种页面的大小是一致的),然后把页式虚拟地址与内存地址建立一一对应的页表。根据该页表,CPU中的内存管理单元MMU可以实现从虚拟地址空间中的一个元素到物理地址空间中元素之间的映射。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移VPO和一个(n-p)位的虚拟页号VPN。MMU利用VPN选择适当的页表条目PTE,将条目中的物理页号PPN和虚拟地址中的VPN串联起来即可得到相应的物理地址。如果虚拟页未缓存,会触发一次异常,调用缺页异常处理程序将物理内存中的牺牲页进行处理,并将磁盘的虚拟页加载到内存中,然后再次执行导致缺页的指令。下面两幅图分别展示了页面命中和缺页时CPU硬件执行的步骤:

图45:页面命中操作图

图46:缺页操作图

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

MMU在查阅一个PTE时,可能会从内存中多取一次数据,造成不小的时间代价。因此,许多系统都在CPU的内存管理单元中包含了一个快表,它是关于页表条目的缓存,用于加速地址翻译。通过虚拟地址中的虚拟页号提取出用于组选择和行匹配的索引和标记字段,可以在快表中取出相应的页表条目,从而在芯片上的内存管理单元中执行地址翻译步骤,大大提升地址翻译速度。

在多级页表模式下,虚拟地址被划分为多个虚拟页号和一个虚拟页面偏移量。其中每一个虚拟页号都是该级页表的一个索引,并对应下一级页表中的某一个的地址,而最后一级虚拟页号可以索引到一个具体的页表条目,该条目包含着某个物理页面的物理页号(或一个磁盘块的地址),由该物理页号加上虚拟地址中的虚拟页面偏移量即可得到一个物理地址。

图47:使用四级页表的地址翻译

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

       当MMU得到物理地址之后,它将发送地址给缓存,缓存从物理地址中抽取出缓存偏移CO,缓存组索引CI和缓存标记CT。利用组索引与缓存标记,缓存能检测是否命中一个目标块。若命中则直接根据偏移量选取相应读取数据,最终该数据将返回CPU;否则依次去下一级缓存中查找,命中后将所需要的数据向上传给CPU,同时更新各级缓存。在更新各级缓存时,如果有空闲块则将目标块放置到空闲块中,否则将对缓存中的某个块进行替换。

图48:包含三级cache的一个内存系统示例图

图49:存储器层次结构

7.6 hello进程fork时的内存映射

在shell运行hello进程时,内核会调用fork函数创建一个子进程。除了与父进程PID不同之外,该子进程和父进程的上下文相同,也可以访问任何父进程已经打开的文件。为了给新创建的进程创建虚拟内存,还创建了当前进程的mm_struct、区域结构和页表的原样副本。fork返回时,新进程的虚拟内存和调用fork时存在的虚拟内存相同,随后的写操作通过写时复制机制创建新页面。

7.7 hello进程execve时的内存映射

Hello运行时对应的子进程通过execve系统调用启动加载器,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段,同时将他们初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为hello可执行文件的内容。另外,hello程序中引用了标准C库,因此它需要与其他程序共享libc.so文件,即需要映射共享区域。最后,设置当前进程的上下文中的程序计数器,使之指向代码区域的入口。

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

通常来说,DRAM缓存不命中称为缺页。

CPU在引用虚拟内存的某一数据时,地址翻译硬件会根据从内存中取出的页表条目,从有效位推断出某一虚拟页是否缓存。若没有缓存,则触发一个缺页异常(故障)。缺页异常调用内核的缺页异常处理程序进行缺页中断处理,该程序会选择一个牺牲页(若已修改,则不能简单丢弃,需要复制回磁盘),并修改相应的页表条目。接着,内核会将所需要的虚拟页复制到内存中的物理页中,并更新相应的页表条目,随后返回重新启动导致缺页的指令,该指令会再次将之前的虚拟地址发送给地址翻译硬件。由于此时相应的虚拟页已经缓存在主存中,该指令将从内存中正常地读取字,不会再产生异常。

7.9动态存储分配管理

动态储存分配管理使用动态内存分配器来进行,它的可移植性很好,使用也很方便。

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但通用性很强。假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。内核对每个进程都会维护一个指向堆顶的变量brk。

图50:堆示意图

分配器将堆视为一组不同大小的块的集合。每个块就是一个连续的虚拟内存页。已分配的块显示地保留为供应用程序使用。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块可以由应用程序显示地释放,也可以由内存分配器自身隐式地释放。

分配器分为显示分配器和隐式分配器两类,前者要求应用显式地释放任何已分配的块,例如C语言中的 malloc (在hello程序中,printf会调用malloc函数)和 free;后者表示应用检测到已分配块不再被程序所使用,就释放这个块,比如Java,ML和Lisp等高级语言中的垃圾收集 (

记录空闲块的方法有以下几种:

  1. 隐式空闲链表:通过头部中的长度字段—隐含地链接所有块。
  2. 显式空闲链表:在空闲块中使用指针。
  3. 分离的空闲列表:按照尺寸大小size分类/组,每个类/组使用一个空闲链表。
  4. 块按大小排序:使用平衡树(如红黑树),在每个空闲块中保存指针,并用

长度(块大小)作为key值。

7.10本章小结

       本章主要聚焦于hello的存储管理。首先从hello的存储器地址空间出发,回顾了线性地址、逻辑地址、虚拟地址和物理地址的概念,接着分别回顾了逻辑地址到线性地址的变换—段式管理和线性地址到物理地址地变换—页式管理。接着更进一步,了解了TLB块表和多级页表的相关概念,同时回顾了三级Cache下的物理内存访问。紧接着,又对hello进程fork和execve时的内存映射进行了回顾。最后,了解了缺页故障与缺页中断处理,以及动态存储分配管理的基础知识。通过本章回顾,我们对系统的虚拟内存管理方式有了更进一步的认识。

结论

计算机系统构造得精细而又巧妙,通过这份报告对hello程序一生的回顾,我们对计算机系统不同部分的基础知识有了更深的认识。

Hello的一生经历了这样的过程:程序员将它带到了这个世界,最开始它以.c文本文件进行存储,接着它经过C预处理器的预处理,变成了修改了的源程序hello.i。接着,它又经过编译器,被翻译成汇编语言文件hello.s。随后,它会经过汇编器,被翻译成机器语言指令,变成可重定位目标文件hello.o。最后,它会经过链接器并被链接生成可执行目标程序hello。此时,它只需要等待shell调用fork创建进程并调用exceve函数对它进行加载运行。在这一步,物理内存会被映射和加载,hello会进入CPU流水线中执行,同时进行输入和输出。当hello程序运行结束后它会被回收,内核会从系统中删除掉它的所有痕迹。至此,hello的一生就这样结束了。

经过这次有趣而又充实的回顾,我更加惊叹于计算机系统设计过程中体现出来的那种自动化的思想:一切都是那么井井有条。人类通过他们巧妙的构思,真正地将机器的利用价值发挥到了极致。Hello从一个c语言程序,到最后输出到屏幕的那句可爱的“hello”,看似简单,实则在机器中经过了无数复杂的处理。而在处理的过程中,正是由于设计者的巧妙构思,才有效地应对了各种情况,例如一些基本的调用、跳转,又或者是各种异常等等。

而对于我来说,可能现在也多了一份对hello的特殊情感吧。希望多年以后若有机会再次回头,仍然能有新的体悟,也希望自己能一直保持初遇hello时的那一份热情和简单的快乐。

附件

hello.c  源程序:能够被人读懂的c语言程序,以字节序列的方式储存

hello.i  预处理后文件:读取了相关头文件的内容

hello.s  编译后的汇编文件:包含了用汇编语言表示的main函数定义

hello.o  汇编后的可重定位目标执行文件:可被链接以待后续加载执行

hello  链接后的可执行文件:可以被加载到内存中由系统执行

elf.txt  hello.o的ELF

rehello.s  hello.o的反汇编文件

elf2.txt  hello的ELF

rehello2.s  hello的反汇编文件

参考文献

[1]  Randal E.Bryant. 深入理解计算机系统(原书第3版)[M]. 北京:机械工业出版社,2016.

[2]  CSDN、博客园等网站

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值