本篇论文的目的是详细阐释hello.c文件是如何通过预处理、汇编、链接等过程从源代码转换为二进制文件。本论文内容不仅涵盖对预处理、编译、汇编、链接的详细介绍,还阐述了计算机系统的工作原理。
关键词:源代码 二进制文件 计算机系统
目 录
第1章 概述
1.1 Hello简介
Hello的P2P:在计算机编程中,"From Program to Process"(P2P)需要用到hello.c的文本,经预处理、编译、汇编、链接就能够得到可执行文件。此时shell会新建一个进程。
Hello的020: From Zero-0 to Zero-0(020)开始内存中没有内容,shell使用execve系统调用启动hello程序时,操作系统会将可执行文件加载到内存中。并且,操作系统将可执行文件的虚拟内存地址映射到物理内存地址。等到hello进程结束,shell回收内存空间。
1.2 环境与工具
硬件:1、处理器:Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz 2.59 GHz
- 机带RAM:32.0 GB
- 系统类型:64 位操作系统, 基于 x64 的处理器
软件:Windows 11 家庭中文版 VMware,Ubuntu 20.04
开发与调试工具:Visual Studio
1.3 中间结果
hello.i 预处理结果,txt文本文件
hello.s hello.i编译后得到的汇编语言文件
hello.o hello.s汇编后得到的可重定位目标文件
hello.asm 反汇编hello.o得到的反汇编文件
hello1.asm 反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本章节主要介绍了hello的P2P,020,并介绍了所需的硬件、软件以及开发调试工具,最后列出了中间结果。
第2章 预处理
2.1 预处理的概念与作用
计算机程序的预处理是编译过程中的一个阶段,主要发生在源代码被编译成机器码之前。预处理指令通常以井号(#)开头,这些指令为编译器提供了额外的指令,用于修改源代码或控制编译过程。预处理器处理完这些指令后,会生成一个新的源文件,这个文件不再包含预处理指令,然后这个新文件会被送到编译器进行编译。这个过程允许程序员编写更灵活、可配置的代码,以及在不同编译环境下使用不同的设置。
2.2在Ubuntu下预处理的命令
预处理的命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
2.3.1读取源代码、识别预处理指令
文本替换:如图可知,hello.i文件的开头部分是对原文件中有“#”的部分进行预处理。对于所有找到的宏定义,编译器会将它们替换为定义时指定的文本。
预处理器将我们可能用到的库的函数合并入原来的文本中,和没有经过预处理的main函数组成hello.i文件。
2.3.2注释处理
如图可知,在hello.c文件中是有注释的,而在hello.i文件中注释被处理掉了。
但预处理器会从处理后的源代码中移除所有单行(//)和多行(/* ... */)注释。
2.3.3生成预处理后的源代码
由文件的最后部分可知,原文件中main函数部分并未发生改变,这也印证了我们对预处理的认知:预处理的过程不会对程序中定义的变量、写的函数等进行处理。完成读取代码、识别指令、文本替换、处理注释等步骤后,预处理器会生成的新的hello.i文件,这个文件包含了所有文本替换和条件编译的结果,但不包含原始的预处理指令。
2.4 本章小结
本章讲述了hello.c文件的预处理过程。程序的预处理是所有程序能够正常运行的前提和基础。预处理过程的主要意义在于对头文件、宏定义都进行处理,还在文件中加入了常见的一些库的内容,从而极大降低了编译器的工作量。通过宏定义,程序员可以创建可重用的代码片段,这些片段可以在多个地方使用,而不需要重复编写相同的代码。程序员还可以在编译时定义宏,从而控制程序的行为,而无需修改源代码,这使得程序可以根据不同的编译时配置进行定制。总的来说,预处理为程序员提供了在编译时控制代码行为的能力,从而提高了代码的灵活性、可维护性和可移植性。
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
编译是将高级编程语言编写的源代码转换成计算机能够理解和执行的低级机器码的过程。编译过程可以是单次完成的,也可以分为多个阶段。例如,一些编译器使用前端(front-end)处理源代码的分析和转换,后端(back-end)负责代码生成和优化。
3.1.2编译的作用
编译的作用包括语言转换、代码优化、错误检测、跨平台支持、资源管理、代码标准化、版本控制等。编译是软件开发生命周期中的关键环节,它确保了程序的正确性、效率和可移植性。通过编译,程序员可以生成可在不同环境中高效运行的程序。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1声明部分
.file表明源文件;.text表明代码节;.string声明了LC0和LC1两个字符串;.global表示全局变量;.type说明符号类型。
3.3.2数据部分
(1)字符串
程序中有两个字符串常量保存在只读数据.rodata段中,不能够被修改。
(2)参数
main函数有两个参数:argc和char*argv。它们分别由寄存器%edi和%rsi保存。
其中,movl指令用于压栈。argc和argv分别被保存在(%rbp)-20和(%rbp)-32的位置。
- 局部变量
可知第二行定义了一个局部变量i。在hello.s文件中,i被存放在(%rbp)-4的位置。i是放在栈上的。
- 数组
偏移量用%rax表示,-32(%rbp)代表argv数组首地址。所以,如图第36行、第39行、第42行、第50行分别表示argv[3]、argv[2]、argv[1]、argv[4]。
每个长度均为8字节。
3.3.3赋值操作
对于i=0的赋值操作,在hello.s中的对应部分表明,使用汇编指令movl使得i的初始值为0。
3.3.4关系操作
如图,进行了两次关系操作,分别为argc是否等于五的判断,(用cmpl指令)以及i每次循环结束并加一后是否小于十的判断。如图,如果argc=5,还有i=10,则都需要跳转。
3.3.5算术操作
如图,for循环有加一操作,这是用addl指令实现的。
3.3.6控制转移
转移指令在本程序中的作用是跳出if判断和跳出for循环。在上述关系操作中,若argv!=5,则执行je指令,若i=10,则执行jle指令。
3.3.7函数操作
这一部分包括主函数main、输出函数printf、sleep函数、atoi函数、getchar函数。
这些函数的调用都使用call指令。main函数,用call指令将首地址压入栈中,为局部变量与函数参数建立栈帧,转移到对应函数的地址。在函数printf的调用时,共调用了argv[1]、argv[2]、argv[3]三个参数。由于有两个输出字符串,printf函数一共调用了两次。第二次调用时,由寄存器%rsi完成对argv[1]的传递,用%rdx完成对argv[2]的传递,%rcx完成对argv[3]的传递。在函数sleep调用时,先由atoi函数把字符串转换为整数 ,然后将得到的休眠时间放入%edi中(第54行)。最后,用call指令调用sleep函数。exit函数将1放入寄存器%edi,(第29行)。然后,call指令调用,终止程序。
3.4 本章小结
本章节详细介绍了编译过程以及hello.i文件怎样变为hello.s文件。hello.s使用的汇编语言是一种低级编程语言,它与机器码非常接近,但使用助记符来代表机器指令的操作码,以及用符号来代表内存地址和常数。我们可以看到,汇编语言难以读懂,较为复杂。然而,汇编语言能够详细的展现文件编译的整个过程。从声明到数据、赋值、跳转、函数调用等等方面,汇编语言让我们程序员对hello.c文件被机器识别到执行的过程有了非常清晰的认识。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编过程通过汇编器,把hello.s文件翻译成机器语言指令。得到的可重定位目标程序,就是hello.o文件。
作用:这是一个从让人能看懂到让机器能看懂的过程。生成的.o 文件是一个二进制文件,它是一个"可重定位"的模块,意味着它可以与其他 .o 文件一起被链接器(Linker)链接,形成最终的可执行文件或库文件。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
hello.o的ELF格式的主要特点:
结构:ELF文件由文件头、程序头表、节头表、符号表、重定位表和实际的数据和代码组成。
(1)ELF文件头:以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
(2)节头:列出了文件中的各个节(sections),记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐的具体信息。
(3)重定位节:本程序没有程序头,只有重定位节:包含了重定位条目,用于在链接时调整代码和数据的地址。代码和数据指实际的机器指令和初始化的数据。在本文件中,有两个重定位节,分别是'.rela.text'和'.rela.eh_frame' 。.rela.text记录机器代码节中的一系刻位置,以偏移量进行记录。重定位节中有 puts,printf,sleep,getchar函数的信息,共计8个重定位入口。rela.eh frame中相对的只有一个重定位入口。
(4)符号表:'.symtab' 在本程序中总共包含十一条。符号表存放着定义和引用的函数和全局变量的信息。其中,value是符号的地址,size是目标的大小,type是数据或者函数,bind表示符号是本地的还是全局的。
4.4 Hello.o的结果解析
hello.asm和hello.s的对比:机器语言与汇编语言之间的映射关系是直接和明确的。
这种映射表现包括:每条指令都有一个对应的机器语言指令,汇编器(Assembler)负责将这些助记符转换成机器码;汇编语言中的寄存器名(如AX、BX、CX等)被映射到机器码中的寄存器编码等。
其主要区别
(1):反汇编文件中增加了每一条指令对应的机器语言。
左右两图对比之下,可以发现反汇编文件左边多出几列数字和字母。这些就代表每一条指令的机器语言。比如,e8 00 00 00 00就表示call指令。
- 操作数的不一致
比如,右图.s文件当中的第21行subq $32,%rsp在反汇编文件中被改成了sub $0x20,%rsp。由此可知,数本身并没有改变,但由十进制形式改为16进制形式。
- 分支转移和函数调用不同
比如,右图.s文件第33行jmp跳转指令给出的位置是L3,而在反汇编中变成je32<main+0x32>。即使用主函数地址加上偏移量寻找位置。在调用函数方面,外部函数的调用处均打上了重定位标记。左图在所有call指令后都有具体的地址。
4.5 本章小结
汇编的一个重要意义在于把程序变成机器能够理解的样子。在这一步当中,程序已经预留出指向这些外部函数的重定向入口。本章从汇编的概念与作用讲起,并在ELF格式下观察可重定位目标文件。另外,通过.asm和.s文件的对比,我们对机器语言和汇编语言之间的映射关系有了更加清晰的认知。总的来说,机器语言与汇编语言之间的映射关系是由汇编器根据特定的CPU架构和指令集来定义的。汇编语言为程序员提供了一种相对容易理解和编写的方式来生成机器码,同时保持了对硬件的直接控制能力。
第5章 链接
5.1 链接的概念与作用
概念:链接是编译过程的最终阶段之一,它发生在所有的源代码文件被编译成目标文件(如.o文件)之后。链接器(Linker)将这些目标文件合并成一个单一的可执行文件。
作用:合并代码、符号绑定、地址分配、库整合、生成可执行文件、优化内存使用、处理静态和动态库、错误检测。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
- ELF头:如图,对比hello和hello.o的ELF文件可知,一个是可执行文件,一个是可重定位文件。另外,在hello文件中我们得到了入口地址。原来的.o文件中没有程序头,现在有了12个56byte大小的程序头。节头数量由原来的14增加到27,并且节头表索引由13增加到26。
- 节头
如图,对比可知,节头描述了各个节的大小、偏移量和其他属性。链接时,各个文件的相同段被合并,并根据这个合并段的大小以及偏移量重新设置各符号的地址。
- 程序头
如下图,程序头描述了系统准备程序执行所需的段。
- 动态部分
- 符号表
5.4 hello的虚拟地址空间
根据hello.elf中的地址信息寻找各段在edb中的位置。根据5.3的节头信息,我们得到:.text位于4010f0处,则在edb中,如下图:
另外一个有趣的地方在于,edb中.dynamic以及之后的内容没有直接显现。需要单独查看才能获取。
5.5 链接的重定位过程分析
使用objdump -d -r hello 分析hello与hello.o的不同:对比链接前后得到的反汇编结果可知,链接后用于调用外部函数的重定位标记被替换了。链接后新的文件中增加了外部函数所在的调用地址。
5.5.1:链接时对库函数进行了符号解析与重定位。
对比以上两图,左边是原来的hello.o反汇编结果,右边是hello的反汇编。hello的反汇编结果出现了程序中所用到的库函数,例如printf,atoi,getchar以及sleep等函数。因此,hello.o反汇编文件只有58行,但hello反汇编则有154行。
5.5.2:jmp和call指令有了明确的地址。
对比上图和5.5.1中的左图,可以看出第114行的call指令有了明确地址。这是因为链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段。
总结:由此可知,链接的过程就是将编译好的把编译好的目标文件和其他的一些目标文件和库链接在一起,形成最终的可执行文件的过程。
5.5.3:分析hello重定位过程
可重定位文件=用到.o文件的集合A+符号定义的集合B
- 将A中所有目标模块中相同节合并
- 对B中定义的符号重定位,确定新节中所有定义符号在虚拟地址空间中的地址。
- 对引用符号重定位。修改每个节中对每个符号的引用。
5.6 hello的执行流程
hello的执行过程可以分为载入、执行和退出。
载入过程:hello!_start 0x4010f0
hello!_init 0x401000
执行过程:hello!main 0x401125
hello!puts@plt 0x401030
hello!printf@plt 0x401040
hello!getchar@pit 0x401050
hello!atoi@plt 0x401060
hello!exit@plt 0x401070
hello!sleep@pit 0x401080
退出过程 libc.so.exit
5.7 Hello的动态链接分析
动态链接,是提高程序空间效率的重要方法。通过动态链接, 我们可以调用外部共享库中的函数,而不需要将其编译在可执行文件中。在运行时动态链接的过程中,PLT表和GOT表起到了至关重要的作用。
根据hello.elf文件可知,got起始表位置为:0x404000。下面查看调用dl_init前后的内容。
如上左图为调用前,右图为调用后。可以看到,调用后字节内容发生了改变。
- 第一行右和第二行左的数据都发生了变化。实际上,PLT与GOT表均为动态链接过程中的重要部分。GOT: Global Offset Table, 全局偏移表,包含所有需要动态链接的外部函数的地址(在第一次执行后)。PLT: Procedure Link Table, 过程链接表,包含调用外部函数的跳转指令(跳转到GOT表中),以及初始化外部调用指令(用于链接器动态绑定)。PLT和GOT的合作过程:PLT的代码跳转到GOT,然后调用链接器修改GOT内容。接下来当再次调用GOT时,指向即为正确内存地址。这里,调用前后内容不同的原因其实是被调用程序的真实物理地址以动态链接的形式加入了程序。
5.8 本章小结
- 本章在介绍链接基本内容的基础上,进一步查看了Hello文件虚拟地址空间的使用情况。接下来分析了hello的执行流程,并进行了Hello的动态链接分析。经过本章的分析,我们充分认识到,动态就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。
第6章 hello进程管理
6.1 进程的概念与作用
概念:一个正在运行的程序或者软件就是一个进程,它是操作系统进行资 源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。
作用: 多进程可以完成多任务,每个进程就好比一家独立的机构,每个部门都各自在运营,每个进程也各自在运行,执行各自的任务。当一个可执行程序在现代系统上运行时,操作系统会提供一种假象——好像系统上只有这个程序在运行,看上去只有这个程序在使用处理器,主存和IO设备。处理器看上去就像在不间断的一条接一条的执行程序中的指令,即改程序的代码和数据是系统存储器中唯一的对象。这些假象是通过进程的概念来实现的。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash是Linux操作系统中最常用的命令行界面之一,它为用户提供了一个与操作系统内核交互的接口。
- 读取输入:Shell-bash从键盘或脚本文件中读取用户的命令输入。
- 命令解析:Shell解析输入的命令,识别命令的各个组成部分,如命令名、参数等。
- 命令查找:Shell在系统路径中查找对应的可执行文件。
- 执行命令:找到可执行文件后,Shell-bash执行该命令,并传递相应的参数。
- 处理输出:命令执行的结果会被输出,Shell-bash将输出显示给用户或根据用户指令进行重定向。
- 等待命令:命令执行完毕后,Shell-bash等待新的输入命令。
- 信号处理:在执行过程中,Shell-bash可以接收和处理来自操作系统的信号,如中断信号(Ctrl+C)。
- 子进程管理:当执行需要创建子进程的命令时,Shell-bash负责子进程的创建和管理。
- 退出:当用户输入退出命令或关闭Shell窗口时,Shell-bash清理环境并退出。
6.3 Hello的fork进程创建过程
输入指令后,进程调用fork,当控制转移到内核中的fork代码后,内核分配新的内存块和内核数据结构给子进程,将父进程部分数据结构内容拷贝至子进程,添加子进程到系统进程列表当中。然后fork返回,开始调度器调度。
6.4 Hello的execve过程
使用execve本质上是一次系统调用,首先将新的可执行文件的绝对路径从调用者拷贝到系统空间中。在得到可执行文件路径后,找到可执行文件并打开。当出现错误时,例如找不到filename,execve才会返回到调用程序。与fork一次调用返回两次不同,execve调用一次并不返回。
6.5 Hello的进程执行
"Hello"程序的进程执行涉及到操作系统中的多个概念,包括进程上文、进程时间片、进程调度、用户态与核心态的转换等。当"Hello"程序开始执行时,操作系统会为它创建一个新的进程上下文,并在CPU上加载这个上下文开始执行;在"Hello"程序执行期间,如果系统有多个进程,操作系统会通过时间片来决定何时切换到另一个进程;当"Hello"程序正在执行,操作系统调度器可能会根据当前的调度策略和系统负载情况,决定何时暂停"Hello"程序的执行,将CPU分配给其他进程;在"Hello"程序执行过程中,大部分时间它运行在用户态。当程序需要执行系统调用,如打印输出到控制台时,它会触发一个中断或异常,导致CPU切换到核心态来执行相应的系统服务。一旦系统调用完成,CPU会返回到用户态,并继续执行"Hello"程序;"Hello"程序中的I/O操作(如printf)通常需要通过系统调用来实现。这会导致从用户态到核心态的转换。
6.6 hello的异常与信号处理
正常运行:
Ctrl-C:
Ctrl-Z:
Ps:
Jobs:
Pstree:
Fg:
Kill:
6.7本章小结
本章首先对进程进行了简单的介绍,然后分析了hello的进程创建、执行。另外介绍了hello可能出现的异常,并对各种指令进行测试。总体上,shell负责处理进程的创建、回收等,各种异常信号也是shell在管理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)逻辑地址(Logical Address)也称为相对地址,是程序在编译时生成的地址,它们是相对于程序的起始地址的偏移量。在"Hello"程序中,逻辑地址是在编译阶段确定的,它们用于访问程序中的局部变量和函数内部的数据。
(2)线性地址(Linear Address):
线性地址是经过段式内存管理转换后的地址,它是一个平坦的地址空间,没有分段的概念。对于"Hello"程序,如果它在一个没有启用分页机制的系统中运行,其线性地址可能直接映射到物理地址。
(3)虚拟地址虚拟地址是程序在运行时使用的地址,它们由操作系统的内存管理单元(MMU)转换为物理地址。在"Hello"程序执行时,CPU生成虚拟地址来访问程序的代码和数据。这些地址通过MMU转换为物理地址,这一过程可能涉及到分页和页表。
(4)物理地址(Physical Address):物理地址是实际存储在内存芯片上的地址,它们是内存单元在物理内存中的真实位置。当"Hello"程序运行,并且虚拟地址被转换后,最终的数据访问是通过物理地址在RAM中进行的。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel架构中,段式管理是用于将逻辑地址转换为线性地址的一种内存管理机制。以下是段式管理中逻辑地址到线性地址变换的过程:在"Hello"程序的执行过程中,如果它运行在一个使用段式管理的系统中,那么每次访问内存时都会经历地址变换过程。例如,当程序打印"Hello, World!"时,字符串的逻辑地址会被转换成线性地址,然后通过分页机制转换为物理地址,最终从内存中取出数据并显示在屏幕上。
具体的转换步骤如下:给定一个完整的逻辑地址/段选择符:段内偏移地址。
看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。可以得到一个数组。
取出段选择符中前13位,在数组中查找到对应的段描述符,得到基地址。线性地址 = 基地址+段内偏移地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
在现代计算机系统中,线性地址到物理地址的变换通常通过页式管理( Paging )来实现。线性地址被分为以固定长度为单位的组,称为页。以下是页式管理中线性地址到物理地址变换的过程,结合"Hello"程序的执行来说明:
在段式管理之后,"Hello"程序的执行会生成线性地址。这些地址是平坦的内存地址空间,不包含段的概念。页表是操作系统用来将线性地址映射到物理地址的数据结构。每个条目(Page Table Entry,PTE)包含一个页帧号(Page Frame Number,PFN)。线性地址被分为几个部分:页号(Page Number,PN)、页内偏移(Page Offset)和页帧号(Page Frame Number,PFN)。页号和页帧号共同用于在页表中索引,页内偏移直接用于在页帧内定位数据。CPU或内存管理单元(MMU)使用线性地址中的页号来查找页目录,然后找到相应的页表,最后使用页帧号来定位页表条目。物理地址由页帧号和页内偏移组合而成。页帧号确定了物理页帧的起始位置,页内偏移指定了页帧内的具体位置。一旦得到物理地址,CPU就可以访问相应的内存单元,读取或写入数据。如果页表条目指示所请求的页不在物理内存中(缺页),会触发一个缺页中断。操作系统将从磁盘中加载缺失的页到物理内存中,并更新页表条目。当"Hello"程序执行并需要访问其代码或数据时,CPU生成的线性地址会通过上述页式管理过程转换为物理地址,然后程序被加载到CPU的指令寄存器并执行。
7.4 TLB与四级页表支持下的VA到PA的变换
在现代Intel架构的处理器中,虚拟地址(VA)到物理地址(PA)的变换通常涉及多级页表和转换后备缓冲(TLB)。以下是在TLB和四级页表支持下的VA到PA变换过程:
首先,虚拟地址由多个部分组成,包括虚拟页号(VPN)和虚拟页偏移(VPO)。其次,TLB是一个高速缓存,用于存储最近或频繁访问的虚拟地址到物理地址的映射条目。当虚拟地址被访问时,处理器首先检查TLB,看是否能找到对应的物理地址。在"Hello"程序的执行过程中,当程序访问其代码或数据时,CPU会使用将虚拟地址转换为物理地址。如果TLB命中(如果TLB中存在该虚拟地址的条目(TLB命中),则可以直接从TLB获取物理页号(PPN)和虚拟页偏移,然后快速合成物理地址),这个过程非常快;如果TLB未命中,就需要通过页表进行查找,这可能会稍微慢一些,但一旦页表条目被加载到TLB,后续访问将变得更快。在这一过程中,四级页表提供了更大的地址空间和更细粒度的内存管理,允许操作系统更灵活地分配和管理内存。
7.5 三级Cache支持下的物理内存访问
高速缓存结构如下:
高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位。当cpu执行一条读内存字w的指令时,它会首先向一级cache请求这个字,如果缓存命中,那么高速缓存会很快将该字返回给cpu,若不命中,则向下一级缓存发起请求。
组相连:
1、组选择:跟直接映射的是相同的
2、行匹配: 这个麻烦一点,还需要依次判断有没有那么一行跟地址中的标记位相同。
3、字抽取:同直接映射
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_ struct区域结构和页表的原样副本。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个。后来进行写操作时,写时复制机制就会创建新页面。
7.7 hello进程execve时的内存映射
当在shell中运行"Hello"程序时,如果涉及到execve系统调用,这通常意味着当前的shell进程正在被一个新的程序(在这个案例中是"Hello"程序)所替代。以下是execve时内存映射变化的一般过程:
1、现有内存区域的移除:
execve调用首先会移除当前进程(shell)的用户空间内存区域,包括代码段、数据段、BSS段和栈等。
2、创建新的内存区域:
为新程序("Hello")创建新的内存区域。这包括代码段、数据段、BSS段和栈。这些区域通常被标记为私有且写时复制(Copy-On-Write,COW)。
3、代码和数据段的映射:
"Hello"程序的代码和数据段被映射到进程的地址空间中。这通常涉及到将程序的ELF可执行文件中的.text和.data节复制到内存中。
4、BSS段的初始化:
BSS段(Block Started by Symbol,未初始化的数据段)被分配并初始化为零。
5、栈的初始化:
为新程序创建一个新的栈区域,通常包含程序的参数和环境变量。
6、堆的初始化:
堆区域被保留,但尚未初始化。程序可以在运行时请求堆内存。
7、共享库的映射:
如果"Hello"程序依赖于任何共享库(如C标准库libc.so),这些库会被映射到进程的地址空间中的共享区域。
8、设置程序计数器:
execve会设置程序计数器(PC)指向新程序的入口点,通常是_start符号,它是程序实际执行的起始点。
9、环境变量和参数的设置:
程序的环境变量和参数被设置在新的栈上,以供程序使用。
- 页表的更新:
操作系统更新页表以反映新的内存映射,确保虚拟地址到物理地址的正确转换。
11、开始执行:
所有内存映射设置完成后,CPU开始执行新程序的代码。
在"Hello"程序的上下文中,execve调用会导致shell进程的上下文完全被"Hello"程序的上下文所替代。这意味着shell进程的代码和数据将不再存在于内存中,取而代之的是"Hello"程序的代码和数据。这种内存映射的转换是进程执行新程序的关键步骤。
7.8 缺页故障与缺页中断处理
缺页故障(Page Fault)是当程序试图访问的页面当前不在物理内存中时发生的情况。处理缺页故障涉及到缺页中断(Page Fault Interrupt),这是操作系统用来响应缺页事件的机制。以下是处理缺页中断的一般过程:
1、缺页故障触发:当程序访问的数据或指令不在物理内存中时,内存管理单元(MMU)会触发一个缺页故障。
2、硬件响应:硬件响应缺页故障,保存当前的程序计数器和其他寄存器的状态,以便稍后能恢复执行。
3、中断处理程序:
操作系统的缺页中断处理程序被调用。这个处理程序负责查找缺失页面的副本,通常在次级存储(如硬盘)上。
4、确定缺失页面:操作系统确定哪个页面缺失,并检查页表条目来确定页面是否应该被加载到内存中。
5、页面选择和加载:操作系统选择一个页面来替换,这通常基于某种页面替换算法,如最近最少使用(LRU)算法。
6、内存分配:如果需要,操作系统会分配一个新的物理页面,或者从页面池中回收一个页面。
7、数据读取:操作系统从磁盘读取缺失页面的数据到新分配的物理页面中。
8、更新页表:更新页表条目以反映新的物理页面位置,并设置适当的访问权限。
9、恢复执行:一旦页面加载完成,操作系统会恢复触发缺页故障的指令的执行。
10、程序继续执行:程序从它被中断的地方继续执行,此时所需的页面已经加载到物理内存中。
7.9动态存储分配管理
动态存储分配管理是程序运行时对内存资源进行分配和释放的过程。这种管理通常由操作系统和运行时库共同完成,以支持程序在运行时根据需要动态地请求和回收内存。所有动态申请的内存都存在堆上面,用户通过保存在栈上面的一个指针来使用该内存空间。动态内存分配器维护着堆,堆顶指针是brk。有两种风格,一种叫显式分配器,使用两个函数,malloc和free,分别用于执行动态内存分配和释放。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,还介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理。
结论
hello历程:
1、预处理:宏替换、插入库函数,生成hello.i;
2、编译:将hello.i文件翻译成hello.s;
3、汇编:将hello.s翻译成为一个可重定位目标文件hello.o。
4、链接:将hello.o文件和可重定位目标文件和动态链接库链接起来,生成个可执行目标文件hello。
5、shell中父进程调用fork,子进程中execve加载运行hello;
6、映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
7、执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。
8、访问内存:MU将程序中使用的虚拟内存地址通过页表映射成物理地址。
9、信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送信号给进程并终止前台作业。
10、结束:完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
附件
hello.c 源文件
hello.i 预处理后的文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位文件
hello 链接后的可执行文件
helloelf.txt hello的elf文件
hellooelf.txt hello.o的elf文件
helloobjdump.txt hello的反汇编代码
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016..
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 内存管理 -- 快表 TLB (Translation Look-aside Buffers)_tlb快表-CSDN博客