HIT计算机与系统大作业

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业      未来技术学院(2+X人工智能)                  

学     号        2021113688        

班   级       21WL026        

学       生       张煜    

指 导 教 师       吴锐     

计算机科学与技术学院

2023年5月

摘  要

本文从计算机系统的角度,遍历了“hello.c”程序在Linux系统下的生命周期,对“hello”程序的整个生命周期进行了系统的分析。通过gcc、objdump、edb等工具对其代码进行预处理、编译、汇编、链接与反汇编并进行分析,同时对“hello”的进程运行、内存管理等过程的进行探索,以更深入理解Linux系统下的存储层次结构、异常控制流、虚拟内存等相关内容。在整个过程中,还涉及到存储问题,虚拟内存与物理内存的分配方式;IO管理将用户端与硬件设备端结合连接起来。

首先,在键盘上手动编写“hello.c”源程序,之后运行C预处理器(cpp)将其进行预处理生成“hello.i”文件,运行C编译器(ccl)将其进行翻译生成汇编语言文件“hello.s”,然后运行汇编器(as)将其翻译成一个可重定位目标文件“hello.o”,最后运行链接器程序ld将“hello.o”和系统目标文件组合起来,创建了一个可执行目标文件“hello”。当shell接收到“./hello”的指令后开始调用fork函数创建进程,execve加载“hello”进入内存,由CPU控制程序逻辑流的运行、中断、上下文切换和异常的处理。最后,结束进程并由父进程进行回收,“hello”走向“生命”的尽头。

关键词:预处理;编译;汇编;链接;进程;存储;IO管理;计算机系统;程序生命周期;Linux                            

目  录

第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 本章小结

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

1.P2P过程:

hello的生命周期开始是从一个高级C语言程序开始的,主要经历了四个阶段:

  1. 经过预处理器cpp进行预处理,生成文本文件hello.i。
  2. 经过编译器ccl生成hello.s汇编程序。
  3. 接着经过汇编器as生成hello.o文件。
  4. 经过链接器ld将其与引用到的库函数链接,生成可执行文件hello。

再通过系统创建一个新进程并且把程序内容加载,实现有程序到进程的转化。

  1. O2O过程:

程序运行前,shell调用execve函数将hello程序加载到相应的上下文中,将程序内容载入物理内存。然后调用main函数。程序运行结束后,父进程回收进程,释放虚拟内存空间,删除相关内容。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:Windows10 64位;VMware Workstation Pro15.5.1;Ubuntu 20.04.4

1.3 中间结果

1.hello.i:hello.c预处理后的文件。

2.hello.s:hello.i编译后的文件。

3.hello.o:hello.s汇编后的文件。

4.hello:hello.o链接后的文件。

5.hello1asm.txt:hello.o反汇编后代码。

6.hello2asm.txt:hello反汇编后代码。

7.hello.o_elf:hello.o用readelf -a hello.o指令生成的文件。

8.hello_elf:hello用readelf -a hello指令生成的文件。

1.4 本章小结

叙述了hello程序的P2P和O2O过程,列举了在这过程中的软硬件条件环境,同时还罗列了在这个过程中生成的文件及其作用。


第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

预处理(preprocessing)指的是预处理器(cpp)根据以字符“#”开头的命令,修改原始的C程序并生成.i文件的过程。此外,预处理还会删除程序中的注释与多余的空白字符。C语言提供多种预处理功能,主要处理#开始的预处理指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。主要流程如下:

1、将源文件中用#include形式声明的文件复制到新的程序中。

2、用实际值替换用#define定义的字符串。

3、根据#if后面的条件决定需要编译的代码。

2.1.2预处理的作用

预处理能够使程序简洁易读、便于维护,节省工作量。预处理的过程中并未直接解析程序的代码,而是对程序文本进行了处理。简单来说,只是处于文本层面上的一种操作。

所有的预处理器(cpp)命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。

(1)添加对应的头文件在#include处。

(2)删除#define并展开对应的宏,#undef取消已经定义的宏。

(3)#ifdef, 若宏已经定义,则返回真;#ifndef, 若宏没有定义,则返回真。

(4)处理所有的条件预编译指令,例如#if#endif,根据“#if”后面的条件决定需要编译的代码。

(5) #if,如果给定条件为真,则编译下面代码;#else是#if 的替代方案;#elif,如果前面的#if给定条件不为真,当前条件为真,则编译下面代码;#endif,结束一个 #if……#else 条件编译块。

(6)#error,当遇到标准错误时,输出错误信息。

(7)#pragma,使用标准化方法,发布特殊的命令到编译器中。

2.2在Ubuntu下预处理的命令

Ubuntu下,打开文件所在位置终端,输入指令:cpp hello.c > hello.i gcc –E hello.c –o hello.i ,即可得到预处理文件hello.i。如下图所示:

图1:Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

图2:hello.i预览结果

经过预处理之后,hello.c变为hello.i文件。打开该文件可以发现,文件由短短的二三十行变为3000多行,内容大大增加。且仍为可以阅读的C语言程序文本文件。对原程序中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容,如果代码中有#define命令还会对相应的符号进行替换。

观察发现,其中的注释已经消失,前一部分的代码为,被加载到程序中的头文件;程序的最后一部分与hello.c中的main函数完全相同。

2.4 本章小结

本章讲述了预处理的相关概念及作用,然后以Ubuntu环境下hello.c文件的预处理为例子,进行了预处理操作和与处理结果展示。


第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

编译是指编译器(cc1)将预处理过的文件转换为汇编文件,即将预处理所得的.i文件翻译成.s文件,它包含一个汇编语言程序。

图3:编译过程原理

3.1.2编译的作用

 编译就是把高级语言变成计算机可以识别的2进制语言计算机只认识1和0主要是进行词法分析语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息

3.2 在Ubuntu下编译的命令

输入指令cc1 hello.i -o hello.s 或 gcc -S hello.c -o hello.s即可进行编译。

图4:Ubuntu下编译

3.3 Hello的编译结果解析

3.3.1文件信息

图5:文件信息

记录文件相关信息的汇编代码,为之后链接过程使用。其中.file表明了源文件,.text代码段,.section .radata只读代码段,.align说明对齐方式为8字节对齐,.string字符串,.global全局变量,.type声明main是函数类型。

3.3.2数据部分

3.3.2.1字符串

图6:两个字符串

Hello程序中有两个字符串,且均为只读数据段,Printf有引用这两个字符串。

图7:printf引用两个字符串

3.3.2.2整型数

1.局部变量i:

图8:局部变量i

局部变量一般存储在寄存器或者栈中。本程序中main函数声明了一个局部变量i,如图存储在栈上-4(%rbp的位置上。

2.参数argc参数 argc 作为用户传给main的参数。也是被放到了堆栈中。

3.3.2.3数组:char *argv[]

图9:char *argv[]

3.3.2.4立即数

图10:立即数

立即数如上图表示为例。

3.3.3操作部分

3.3.3.1算术操作

图11:算术表达式i++

算数表达式i++编译如图所示。

3.3.3.2关系操作

图12:关系表达式if(argc!=4)的编译

图13:关系表达式i<7的编译

3.3.3.3数组操作

14:printf执行时其中的argv[1],argv[2]编译

3.3.3.4控制转移

图15:条件跳转控制

若argc和4相等,就跳转至.L2处,否则继续执行接下来的语句。

图16:for循环体

for(i=0;i<8;i++){…}编译如图所示。从.L3处开始执行,先把i和7进行比较,若i小于等于7(即i小于8)就跳转至.L4处执行for循环体里面的语句,最后i++又开始新一轮的比较。

3.3.3.5函数操作

图17:printf的调用

printf("Hello %s %s\n",argv[1],argv[2])编译如图所示。

图18:sleep调用atoi

sleep(atoi(argv[3])的编译如图所示,先进行计算取得argv[3],然后调用atoi函数进行类型转换,将字符串型转换为整型,再调用sleep函数。

3.4 本章小结

本章首先介绍了编译的概念和作用,然后在Ubuntu下以hello.s为例,通过分析其汇编程序理解编译器是如何进行编译操作的。分别从c语言的数据,赋值语句,类型转换,算术操作,逻辑/位操作,关系操作,控制转移与函数操作这几个角度对编译器工作进行分析,通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成c语言,提高了反向推出原代码的能力


第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编是指把汇编语言翻译成机器语言的过程。

4.1.2汇编的作用

汇编的作用是将hello.s汇编语言文件翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制目标文件hello.o中。hello.o包含hello程序执行的机器指令。将汇编语言翻译成机器语言,使其在链接后能被机器识别并执行。

4.2 在Ubuntu下汇编的命令

在终端中输入指令:gcc -c hello.s -o hello.o

图19:Ubuntu下汇编指令

4.3 可重定位目标elf格式

输入指令:readelf -a hello.o > hello.elf

图20:Ubuntu下进行汇编

图21:ELF文件结构

4.3.1ELF头

图22:ELF文件中的ELF头

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

4.3.2节头

图23:ELF中的节头

4.3.3重定

图24:ELF中的重定向节

.rodata、puts、exit、printf、atoi、sleep、getchar符号的偏移。链接器会依据重定位节的信息对可重定位目标文件进行链接得到可执行文件。.rela.text,保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。.rela.eh_frame节是.eh_frame节重定位信息。

4.3.4符号表

图25:ELF文件中符号表

               

符号表,存放函数和全局变量(符号表)信息,不包括局部变量。一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。

4.4 Hello.o的结果解析

Linux下终端输入指令:objdump -d -r hello.o。

图26:objump反汇编指令结果

通过将反汇编与hello.s比较发现,汇编指令代码几乎相同,反汇编代码除了汇编代码之外,还显示了机器代码,在左侧用16进制表示。机器指令有操作码和操作数组成,和汇编指令一一对应。最左侧为相对地址。

其中跳转指令和函数调用等指令,在反汇编代码中表示为对应地址的偏移,而在hello.s中直接表示为函数名或定义的符号。在反汇编代码中,立即数是16进制显示的,而在hello.s中立即数是以十进制显示的。

1.分支转移

hello.asm文件的跳转指令不使用段名称,而是使用确定地址。段名称只是汇编语言中为便于编写而引入,因此在机器语言中显然不存在。

2.函数调用

hello.s中的函数调用使用函数名称,hello.asm中call的目标地址则是当前下一条指令。此外,由于在编译阶段没有保留符号的名字,函数调用都被写为了<main+offset>的形式。

3.立即数变为16进制格式

4.5 本章小结

本章首先进行了hello.s的汇编,将其转换为二进制可重定位目标程序文件hello.o接着,我们通过readelf读取了hello.o的ELF头、节头表、可重定位信息和符号表等最后,通过objdump反汇编目标文件,从中得到机器代码,并将机器代码与汇编代码进行对照,发现机器语言与汇编语言存在一一映射关系


5链接

5.1 链接的概念与作用

5.1.1链接的概念

链接(linking)是将各种代码和数据片段收集并组合为一个单一文件的过程,所得到的文件可以被加载(复制)到内存并执行。

5.1.2链接的作用

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

5.2 在Ubuntu下链接的命令

在Lniux终端中输入指令: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

图27:Linux进行链接

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

Linux下输入指令:readelf -a hello进行链接。

5.3.1ELF头

图28:ELF头

hello的ELF头与hello.o的ELF头基本相同。但是Type由REL(可重定向目标文件)变为DYN(共享对象文件);入口点(Entry point address)由未确定(0x0)变成了具体的地址(0x4010f0);程序头和节头的起始位置和大小都有改变;节头个数由14个变为了27个。

5.3.2节头

图29:节头

相比hello.o,hello的节头数目显著增加。

5.3.3程序头

图30:程序头

 程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。

5.3.4符号表

图31:符号表

 hello相较hello.o符号表多出了许多符号,而且额外有一张动态符号表(.dynsym)。此外,这些符号已经确定好了运行时位置。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

图32:用edb查看hello的虚拟地址

hello的虚拟空间地址开始于0x401000,结束于0x401ff0。而根据5.3中节头表,可以通过edb找到各个节的信息。

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

图33:通过通过objdump反汇编

hello的反汇编代码相较hello.o多了很多节与函数,这是因为动态链接器将共享库中hello.c用到的函数加入到了可执行文件中。同时,hello.o中的相对偏移地址变成了hello中的虚拟内存地址。

根据以上分析,我们可知链接的过程就是链接器将所有的.o文件组装到一起,文件中的各个函数按照一定的顺序排列起来。

5.6 hello的执行流程

执行流程:

ld-2.31.so!_dl_start

ld-2.31.so!_dl_init

hello!_start

libc-2.31.so!_ libc_start_main

libc-2.31.so!__cxa_atexit

ld-2.31.so!_dl_init

hello!_libc_csu_init

hello!__init

libc-2.31.so!_setjmp

libc-2.31.so!_sigsetjmp

hello!main

hello!.plt+0x70

hello!printf@plt

hello!exit@plt

5.7 Hello的动态链接分析

图34:got起始位置

动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。通过elf文件可知GOT起始表位置,在 edb中定位到该地址,如下图所示。

调用dl_init前:

图35:调用dl_init前

调用dl_init后:

图36:调用dl_init后

可以看到调用dl_init这两个位置的8个字节全部发生了改变。

5.8 本章小结

本章首先介绍了链接的概念和作用,详细说明了可执行目标文件的结构,及重定位过程。并且以可执行目标文件hello为例,具体分析了各个段、重定位过程、虚拟地址空间、执行流程等。


6hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

    进程是一个执行中程序的实例。系统中的每个程序都在某个进程的上下文中。

6.1.2进程的作用

进程提供给应用程序的关键抽象:

1.一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。2.一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

6.2.1Shell-bash的作用:

Shell 是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。Shell 有自己的编程语言用于对命令的编辑,它允许用户编写由 shell 命令组成的程序。 Shell 编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的 Shell 程序与其他应用程序具有同样的效果。

6.2.2 Shell-bash的处理流程:

Shell 首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。 将命令行划分成小块 tokens, 程序块 tokens 被处理,检查看他们是否是 shell 中所引用到的关键字。 tokens 被确定以后, shell 根据 aliases 文件中的列表来检查命令的第一个单词。如果这个单词出现在 aliases 表中,执行替换操作并且处理过程回到第一步重新分割程序块 tokens。 对~符号和所有前面带有$符号的变量进行替换, 并将命令行中的内嵌命令表达式替换成命令。然后将命令行中的内嵌命令表达式替换成命令→将命令行中的内嵌命令表达式替换成命令→命令的检查→初始化所有的输入输出重定向→执行命令。

6.3 Hello的fork进程创建过程

进程的创建采用fork函数:pid_t fork(void);创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程fork时,子进程可以读取父进程中打开的任何文件。

父进程与创建的子进程之间最大的区别在于它们有不同的PID。子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法分辨程序是在父进程还是在子进程中。

6.3 Hello的fork进程创建过程

用户输入运行命令后,shell判断其不是内置命令,于是将其判断为可执行程序,为其fork子进程:内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

6.4 Hello的execve过程

当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,需要以下步骤:

①删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。

②创建新的代码、数据、堆和栈段。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。

6.5 Hello的进程执行

6.5.1上下文信息

    上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

6.5.2进程时间片

    时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。

6.5.3进程调度

执行进程时,进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。

6.5.4用户态与核心态转换

    为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。

6.6 hello的异常与信号处理

hello执行过程中可能会出现的异常有:中断、陷阱、故障和终止,可能产生的信号有SIGINT、SIGQUIT、SIGKILL、SIGTERM、SIGALRM、SIGCHLD、SIGSTOP等。处理方式可能是将程序挂起等待下一个信号来临,或终止程序。

  1. 乱按:

图37:“乱按”的运行结果

可见“乱按”并不影响运行结果。

  1. 按回车:

图38:“按回车”的运行结果

可见“按回车”并不影响运行结果。

  1. Ctrl+Z

图39:Ctrl+Z的运行结果

在输出过程中按下CTRL+Z,程序中止并退出,此时调用ps指令查看后台进程,发现hello程序并未终止,通过fg命令继续执行该进程,发现hello程序继续执行:

图40:输入ps发现进程还在

图41:输入fg进程继续

  1. Ctrl+C”:

图42:输入“Ctrl+C”进程结束

在输出过程中按下CTRL+C,程序终止,此时用ps指令查看后台进程,未发现hello程序,说明程序已终止

6.7本章小结

本章介绍了进程的概念和作用、shell-bash的处理过程与作用并且着重分析了调用fork创建新进程,调用execve函数执行hello,hello的进程执行过程,以及hello在运行时遇到的异常与信号处理。
第7章 hello的存储管理

7.1 hello的存储器地址空间

1、逻辑地址:程序经过编译后出现在汇编代码中的地址。

2、线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。

3、虚拟地址:也就是线性地址。

4、物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址

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

段式管理是应用在IA32架构上的管理模式。一组寄存器(CS,DS,SS等)保存着当前进程各段(如代码段、数据段、堆栈段)在描述符表中的索引,可以用来查询每段的逻辑地址。当获取了形如[aaaa:bbbb]的逻辑地址,可以通过简单的运算来取得线性地址(段基址*0x10H+段内偏移)。

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

线性地址(VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。

正如同在cache中寻找内容也需要索引,从虚拟内存到物理内存也需要索引。因此在内存中,我们额外存储一个叫做页表的数据结构,作为对应的索引。因此,我们可以让每个进程都有一个页表,页表中的每一项都记录着该进程中对应的一页所投影到的物理地址、是否有效、还有一些其他信息等。

然而由于页的大小为212个字节,而虚拟内存有232个字节,导致页表项会有2^20项,占用空间确实太大了,而且很多页表项应该其实都是空的,毕竟进程普遍没有占用很大的地址空间。因此系统采用了多级页表的结构来进行索引。

系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在磁盘的起始地址。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

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

图43:TLB与四级页表支持下的VA到PA的变换

页表技术虽然能让我们再给出虚拟地址的时候,很大概率通过查找页表来找到内存地址,但是查页表也是访问内存的过程,也很浪费时间。利用局部性原理,像缓存一样,将最近使用过的页表项专门缓存起来。因此出现了TLB(后备转换缓冲器,也叫快表),之后找页表项的时候,先从快表找,找不到在访问内存中的页表项。

同理,四级页表能保证页表项的数量少一些。

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

与TLB相似,利用局部性原理,采用组相联的方式,存储一段时间内所加载的地址附近的内容。在得到物理地址后,先从L1 cache中找,没有再从L2 cache中找,然后L3 cache,然后主存。

7.6 hello进程fork时的内存映射

shell通过fork为hello创建新进程。当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给hello进程唯一的pid。为了给这个新进程创建虚拟内存,它创建当前进程的mm_struct、区域结构和样表的原样副本。它将两个进程中的每个页面都标记为只读,并将每个进程中的每个区域结构都标记为写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好的和调用fork时的虚拟内存相同。而当这两个进程中任何一个进行写操作时,就会触发一个保护故障。当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上这个写操作就可以正常执行了。

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

①删除已存在的用户区域

删除当前进程虚拟地址的用户部分中的已存在的区域结构。

②映射私有区域

为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。

③映射共享区域

hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

④设置程序计数器(PC)

execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

图44:缺页故障与缺页中断处理

缺页其实就是DRAM缓存未命中。当我们的指令中对取出一个虚拟地址时,若我们发现对该页的内存访问是合法的,而找对应的页表项式发现有效位为0,则说明该页并没有保存在主存中,出现了缺页故障。

此时进程暂停执行,内核会选择一个主存中的一个牺牲页面,如果该页面是其他进程或者这个进程本身页表项,则将这个页表对应的有效位改为0,同时把需要的页存入主存中的一个位置,并在该页表项储存相应的信息,将有效位置为1。然后进程重新执行这条语句,此时MMU就可以正常翻译这个虚拟地址了。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。

显式分配器:要求应用显式地释放任何已分配的块。

隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射,hello进程execve时的内存映射、缺页故障与缺页中断处理和动态存储分配管理。

结论

hello的程序生命周期以hello.c开始:

1.hello.c经过预处理,变成了保留预处理内容的文本文件hello,i;

2.hello.i经过编译,变成了汇编文件hello.s;

3.hello.s经过汇编,生成了保存二进制机器代码的可重定位目标文件hello.o;

4.hello.o通过与系统库进行链接,生成了可执行文件hello;

5.shell-bash进程调用fork函数,生成hello进程,又由execve函数加载运行当前进程上下文并运行程序hello;

6.hello在进程中通过多级页表、TLB等机制实现从虚拟内存(VA)到物理内存(PA)的转变,并运用动态内存,以达到和计算机交互的结果;

7.hello在运行时调用的函数中包含Unix I/O提供的函数,它用这些函数实现和I/O设备的交互;

8.hello运行结束,hello进程被shell父进程回收,释放内存并且删除有关的上下文。hello的一个程序生命周期至此结束。

总结感悟:对于《计算机系统》这门课程,经历一学期的学习,我的感受是复杂而深刻的。

学习的过程对我来说并不简单,对于偏向具象思维的我来说,这门课程的每个符号、每个语句、每个原理或许都可能阻滞我很久。四个实验,一个大作业,我或许花费了远比其它人多的时间来琢磨和消化。最后考试的我的感受也是不尽人意(虽然现在还没出成绩)。但是,万事万物都有两面性,这门课程也启发了我很多

首先,是抽象思维的提升。以前的我,面对计算机的抽象符号和语言,本能地就是敬而远之。然而,通过本门课程的学习以及四个实验、一个大作业的打磨,我自我觉得我的抽象思维较以往有了很大提升。

其次,是对计算机系统第一次较全面的认识。具象思维的限制注定我本能地不远深入了解计算机,这门课程带我走进了计算机系统的大门。从用户到操作界面,再到底层代码逻辑,再与计算机硬件系统相连,最后又回到用户交互层面。我第一次领略到计算机的复杂和神奇之处。复杂,是在这过程中各个原理和机制的复杂之处让我咂舌;神奇,是有这么一套系统的理论将这一切复杂的东西耦合起来形成一个整体。

最后,是痛苦过程中的快乐。前面说到,这门课程的学习对我来说并不简单,往往一个实验的细节之处会让我阻滞许久。但是,每次突破实验的难题时,从蹒跚回顾课堂知识转到实验实践,到最后解决问题提交实验报告,这一整个过程是饱含着令自己满意的成就感的。实验的过程当中,老师很热心地解决我们学生的问题;助教老师们也竭尽全力为同学们答疑解惑。在这里,我向他们回以最真挚的感谢!

以我的水平,创新理念和设计就谈不上了。只好以感谢来结尾:谢谢这门课程,也感谢因此课程而相遇的每一个人。既见君子,云胡不喜~
附件:

文件

作用和文件类型

hello.c

C语言源程序,文本文件

hello.i

经预处理的源程序,文本文件

hello.s

经编译的汇编文件,文本文件

hello.o

经汇编的可重定位目标文件,二进制文件

hello.elf

经readelf得到的elf格式信息,文本文件

hello.asm

经objdump得到的反汇编文件,文本文件

hello

经链接得到的可执行目标文件,二进制文件


参考文献

  1. https://www.cnblogs.com/pianist/p/3315801.html
  2. Randal E. Bryant, David R. O’Hallaron[M] Computer Systems A Programmer’s Perspective Third Edition
  3. 袁春风. 计算机系统基础. 北京:机械工业出版社,2018.7(2019.8重印)
  4. 一个简单程序从编译、链接、装载(执行)的过程-静态链接 - 知乎
  5. http://t.csdn.cn/R2MvH

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值