本论文围绕hello程序,系统分析了C语言程序从源代码到可执行进程的全生命周期。详细阐述了预处理、编译、汇编、链接(P2P过程)及进程管理、存储管理(020过程)的核心机制。在Ubuntu环境下,利用GCC工具链生成预处理文件、汇编代码、可重定位目标文件和可执行文件,结合readelf、objdump等工具解析ELF格式与机器指令。重点探讨了动态链接、虚拟地址空间映射、页式存储管理及信号处理机制,揭示了进程通过fork、execve加载执行的内在原理。通过调试工具验证了异常处理、TLB与Cache对内存访问的优化作用,完整呈现了程序从静态代码到动态进程的转化过程及其计算机系统底层支持。
关键词:计算机系统;P2P过程;020过程
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
第1章 概述
1.1 Hello简介
P2P(From Program to Process)过程,指程序从GCC编译器的驱动程序读取并翻译为可执行目标文件hello.o;而后,操作系统外壳shell利用函数fork为hello创建进程process。具体包括以下五个步骤:
- 预处理(Preprocessing)
预处理器(cpp
)对hello.c
进行宏展开、头文件包含、注释删除等操作,生成hello.i
文本文件。 - 编译(Compilation)
编译器(如cc1
)将hello.i
翻译为汇编代码文件hello.s
,完成语法分析、语义优化,将高级语言转换为机器相关的低级指令。 - 汇编(Assembly)
汇编器(as
)将hello.s
转换为机器码,生成可重定位目标文件hello.o
,包含二进制指令和未解析的符号引用。 - 链接(Linking)
链接器(ld
)合并hello.o
与标准库(如libc.so
),解析外部符号地址,生成可执行文件hello
,具备完整的虚拟地址空间布局。 - 进程创建(Process Creation)Shell通过fork()创建子进程,再调用execve()加载hello,将其代码段、数据段映射到进程虚拟地址空间,完成从静态程序到动态进程的转变。
020(From Zero-0 to Zero-0) 指程序从初始未被加载(零状态)到执行结束后被系统回收资源并完全终止(回归零状态)的生命周期,即从无到有再到无的完整过程。主要包含以下三个过程:
- 初始状态(Zero-0)
程序未执行时,内存中无相关数据,进程表项和资源均未分配 - 加载与执行
- execve()调用加载器,为hello分配虚拟内存页,映射代码、数据段至物理内存,初始化堆栈段为0值。
- CPU通过流水线执行指令:取指→译码→执行,利用TLB和三级缓存(L1/L2/L3)加速内存访问。
- I/O管理(如printf)通过系统调用将输出传递至终端设备。
- 终止与回收(Zero-0)
- 程序执行结束后,父进程(Shell)通过waitpid()回收子进程,释放内存、文件描述符等资源。
- 内核清除进程控制块(PCB)、页表项等数据结构,hello的所有痕迹从系统中消失。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件化境:Intel Core i9-13980HX
软件环境:Windows11+ Ubuntu 24.04.2
开发与调试工具:gdb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 | 作用 |
hello.i | 预处理后文件 |
hello.s | 汇编文件 |
hello.o | 可重定位的可执行文件 |
hello | 链接后的可执行文件 |
hello_elf.txt | hello.o readelf结果 |
hello_elf2.txt | hello readelf结果 |
hello_obj.txt | hello.o反汇编结果 |
hello_obj2.txt | hello 反汇编结果 |
1.4 本章小结
本章对hello程序的P2P过程以及020过程进行总体概述,说明了本文的开发环境与调试工具以及生成的中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
- 程序预处理的概念
程序预处理是指在源代码被正式编译前,由预处理器对代码进行文本处理的阶段。它主要处理以 #
开头的预处理指令(如宏定义、文件包含、条件编译等),通过宏替换、文件合并、条件筛选等操作生成修改后的源代码,供后续编译使用。预处理不涉及语法分析,仅对文本进行直接操作,属于编译过程的“准备工作”。
- 预处理的作用
- 代码复用与模块化
通过#include
指令将头文件内容插入当前文件,减少重复代码。 - 提高可移植性
使用条件编译(如#ifdef
)可根据不同平台或环境选择性地编译代码,增强跨平台兼容性。 - 简化代码维护
宏定义(#define
)允许用常量或代码片段替换复杂表达式,修改时只需调整宏定义,无需逐一修改代码。 - 优化编译过程
预处理会删除注释和冗余空白,减少编译时的处理负担。
- 代码复用与模块化
2.2在Ubuntu下预处理的命令
Linux下使用GCC编译器进行预处理的指令为:
gcc -E hello.c -o hello.i
![]() |
图2.1 在Ubuntu下对hello.c进行预处理截图 |
2.3 Hello的预处理结果解析
起始的代码段为头文件展开,所有 #include 被替换为对应文件的内容。后面是宏和类型定义以及声明系统调用函数,如sleep和getchar。用户代码被保留在最后。
2.4 本章小结
本章讨论了预处理的概念与作用,并对hello.c文件进行预处理,最后简要解析预处理后的hello.i文件。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是将高级编程语言编写的源代码,通过编译器转换为计算机可执行的机器代码或低级中间代码的过程。这一过程通常包括词法分析、语法分析、语义分析、代码优化和目标代码生成等步骤。
编译主要有以下作用
- 跨平台兼容性:通过编译生成不同平台(如Windows、Linux)的机器码,使程序可移植。
- 性能优化:编译器会优化代码(如删除冗余计算),提升执行效率。
- 错误检查:编译时能检测语法、类型等错误,提前发现潜在问题。
- 代码保护:生成的二进制文件难以直接逆向,保护知识产权。
- 标准化执行:编译后的程序独立于源代码,确保运行环境一致性。
3.2 在Ubuntu下编译的命令
在Ubuntu系统下执行编译的命令为:
gcc- S hello.i -o hello.s
![]() |
图3.1 在Ubuntu下对hello.i进行编译截图 |
3.3 Hello的编译结果解析
3.3.1常量
整形常量在编译后的文件中以立即数的形式表示,如图3.2.1.
|
图3.2.1 整形常量在编译后的文件的表示形式 |
字符串常量存储于只读数据段中,如图3.2.2所示。
![]() |
图3.2.2 字符串常量在编译后的文件的表示形式 |
3.3.2 局部变量
局部变量int i通过堆栈实现,如图3.3.1所示,源代码中的局部变量int i在编译后存入堆栈,并用帧指针寄存器%rbp+ 偏移量的形式表示。
|
图3.3.1局部变量在编译后的文件的表示形式 |
3.3.4 if条件跳转
条件跳转通过cmpl操作判断条件是否成立并改变条件码后调用条件跳转指令je依据条件码进行条件跳转,如图3.4.1所示.
|
图3.4.1 if条件跳转在编译后的文件的表示形式 |
3.3.5 赋值操作
赋值操作通过mov指令实现,如图3.5.1所示。
![]() | |
图3.5.1 赋值操作在编译后的文件的表示形式 |
3.3.6 关系操作
C文件中循环控制中判断变量i与10大小的操作,汇编后通过cmpl指令实现,如图3.6.1所示
![]() | |
图3.6.1 关系操作汇编后的表示形式 |
3.3.7 for循环
C文件中的for循环操作汇编后通过指令cmpl比较循环控制变量和立即数10的大小并改变条件码,再通过条件跳转指令依据条件码进行跳转来实现循环控制,如图3.7.1所示。
![]() | ![]() |
图3.7.1 for循环以及其汇编实现 |
3.3.8函数操作
函数调用的参数传递通过调用函数前将参数存储到特定寄存器实现,例如调用printf函数前,函数的第一个参数,即printf的输出格式被储存到寄存器%rdi中,如图3.8.1所示。
|
图3.8.1 函数调用参数传递的汇编实现 |
函数调用通过call指令实现,如图3.8.2所示
|
图3.8.2 函数调用的汇编实现 |
3.3.9 算术操作
C文件中的算术操作i++在汇编文件中通过addl指令实现,如图3.9.1所示。
![]() | |
图3.9.1 算术操作i++及其汇编实现 |
3.3.10 数组操作
C文件中的数组argv[ ]在汇编中通过堆栈存储其首地址,再通过首地址加偏移量的方式对其个元素进行操作,如图3.10.1所示。
![]() |
图3.10.1 数组操作的汇编实现 |
3.4 本章小结
本章简要介绍了编译的概念以及意义,并对hello.c文件进行编译,最后对编译后的hello.s文件进行解析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编,即将汇编代码转换为机器码(.o目标文件)。通过这种底层转换,汇编在系统软件、高性能计算、安全研究等领域发挥着不可替代的作用,是连接高级语言与机器硬件的关键桥梁。
4.2 在Ubuntu下汇编的命令
Linux下执行汇编的命令为:
gcc -c hello.s -o hello.o
![]() |
图4.1 执行汇编操作的截图 |
4.3 可重定位目标elf格式
通过readelf -a hello.o > hello_elf.txt指令,显示hello.o文件的各种信息并保存至hello_elf.txt中。
4.3.1 ELF头(ELF header)
ELF头中包含了描述ELF文件整体结构和属性的信息,包括ELF标识、目标体系结构、节表偏移、程序头表偏移等信息,如图4.2所示。
![]() |
图4.2 ELF头 |
4.3.2 节头(section header)
ELF 文件中的节头是描述文件中各个节的关键元数据结构。每个节头对应一个节,记录了该节的名称、类型、地址、大小、对齐方式等属性,如图4.3所示
![]() |
图4.3 节头 |
4.3.3 重定位节(relocation section)
重定位节记录了在链接或加载时需要修改的地址信息,这些条目告诉链接器哪些位置的符号地址需要在链接时进行调整,以便正确地指向目标地址,如图4.4所示。
![]() |
图4.4 重定位节 |
4.3.4 符号表(symbol table)
符号表存储程序中所有符号(函数、变量)的名称和地址信息,供链接器解析跨模块的引用关系,如图4.5所示。
![]() |
图4.5 符号表 |
4.4 Hello.o的结果解析
通过指令objdump -d -r hello.o > hello_obj.txt对hello.o文件进行反汇编,并将结果保存到hello_obj.txt中。
4.4.1 机器语言与汇编语言的对照分析
反汇编代码中分支转移目标地址由相对于当前函数起始地址的偏移量给出,如图4.6所示。
![]() |
图4.6 反汇编代码中的分支转移 |
而.s文件中的分支转移目标地址由代码段名称给出,如图4.7所示。
![]() |
图4.7 .s文件分支转移 |
反汇编中的函数调用的目标地址是一个无意义的占位符,如图4.8所示,其真实目标地址需要通过链接时的重定位来确定。
![]() |
图4.8 反汇编代码中的函数调用 |
而.s文件中的函数调用指令call后面接的是目标函数的函数名,如图4.9所示。
![]() |
图4.9 函数调用的汇编实现 |
汇编语言中的立即数为十进制,而反汇编机器码中为十六进制,如图4.10所示。
![]() | ![]() |
图4.10 机器指令与汇编指令中的立即数对比 |
4.5 本章小结
本章说明了汇编的含义及作用,对hello.s文件进行汇编得到hello.o文件,并对汇编后的可执行文件进行分析。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是将一个或多个目标文件与所需的库组合成可执行文件的过程。在此过程中,链接器会解析目标文件中的符号引用,将不同目标文件的代码段和数据段按规则合并,并完成地址空间分配和重定位。对于静态链接,库代码会被直接复制到最终文件中;而动态链接则仅记录依赖信息,在程序运行时由动态链接器加载共享库。
5.1.2 链接的作用
链接的核心作用是生成可直接运行的程序,解决模块化开发中的代码整合问题。它通过合并代码、绑定符号地址,将分散编译的可重定位目标文件和系统库中的函数逻辑串联成完整的可执行文件。链接器还会优化内存布局,并生成程序头以指导操作系统加载文件到内存。最终,链接过程确保程序能独立运行,所有外部函数调用和全局数据引用均指向正确的内存位置,从而将开发者编写的源码转化为计算机可执行的指令集合。
5.2 在Ubuntu下链接的命令
Linux下的链接命令为
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
![]() |
图5.1 执行链接命令截图 |
5.3 可执行目标文件hello的格式
通过readelf -a hello > hello_elf2.txt指令,显示hello文件的各种信息并保存至hello_elf2.txt中。
5.3.1 ELF头(ELF header)
ELF头中包含了描述ELF文件整体结构和属性的信息,包括ELF标识、目标体系结构、节表偏移、程序头表偏移等信息,如图5.2所示。
![]() |
图5.2 ELF头 |
4.3.2 节头(section header)
ELF 文件中的节头是描述文件中各个节的关键元数据结构。每个节头对应一个节,记录了该节的名称、类型、地址、大小、对齐方式等属性,如图5.3所示。
可以看出,链接后的可执行文件相比未链接的目标文件,其节头中新增了动态链接相关节(如 .interp 指定动态加载器、.dynsym 动态符号表、.plt 和 .got 实现延迟绑定)、运行时初始化节(如 .init 和 .fini 定义全局构造/析构函数)、去除了重定位节(如 .rela.text 已被链接器处理),同时所有节的虚拟地址(Address 字段)被分配为具体值,且文件类型从 REL(可重定位)变为 EXEC(可执行),表明其已具备完整的加载和执行能力。链接器通过合并代码、解析外部符号、绑定动态库函数,将原始目标文件转化为可直接加载到内存并运行的独立程序。
| ![]() |
图5.3 节头 |
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
使用gdb加载可执行文件hello,在主函数处设置断点并运行至断点处,如图5.4所示。
![]() |
图5.4 gdb加载hello截图 |
使用指令info proc mappings 查看虚拟地址空间各段信息,如图5.5所示。
![]() |
图5.5通过gdb查看虚拟地址空间各段信息 |
下面举例对本进程的虚拟地址空间各段信息进行与5.3对照说明。
1 .text节
.text节对应的readelf节头信息为:
[15] .text PROGBITS 00000000004010f0 000010f0 Size: 0xd8, Flags: AX (可执行)
而对应在本进程中的虚拟空间段地址为0x401000-0x402000,包含 .text 节的虚拟地址 0x4010f0
2 .rodata节
.rodata节对应的readelf节头信息为:
[17] .rodata PROGBITS 0000000000402000 00002000 Size: 0x48, Flags: A (只读)
而对应在本进程中的虚拟空间段地址为0x402000-0x403000,包含 .rodata 节的虚拟地址 0x402000。
3 .data节和 .got.plt节
readelf节头信息为:
[22] .data PROGBITS 0000000000404030 00003030 # 已初始化数据
[21] .got.plt PROGBITS 0000000000403fe8 00002fe8 # 全局偏移表(动态链接)
而对应在本进程中的虚拟空间段地址为0x404000-0x405000,包含 .data 和 .got.plt的虚拟地址。
5.5 链接的重定位过程分析
通过指令objdump -d -r hello > hello_obj2.txt对hello文件进行反汇编,并将结果保存到hello_obj2.txt中。
5.5.1 hello与hello.o的差异
特征 | hello.o(未链接) | hello(已链接) |
函数调用地址 | call 指令目标地址为 0x0(占位符) | call 指令指向具体地址(如 puts@plt) |
全局数据引用 | 使用相对偏移(如 lea 0x0(%rip),%rax),需重定位 | 直接引用 .rodata 的实际地址(如 lea 0xead(%rip),%rax) |
重定位信息 | 存在 .rela.text 等重定位节(如 R_X86_64_PLT32) | 无重定位节(链接器已处理) |
动态链接支持 | 无 PLT/GOT 表 | 新增 .plt、.got.plt 节,支持动态链接(如 puts@plt) |
初始化代码 | 无 .init、.fini 节 | 包含 _init 和 _fini 函数,处理全局构造/析构 |
5.5.2 链接的过程
连接主要包括以下两步:
1.符号解析
符号解析是链接过程中链接器将不同模块(如目标文件、库文件)中的 符号引用(如函数名、全局变量)与其对应的 符号定义 进行匹配的过程。例如,在 hello.o 中调用 puts 函数时,目标文件仅记录了对 puts 的引用(符号未定义),链接器会遍历所有输入文件(如 libc.so)找到 puts 的实际定义,并确认其地址。此过程确保程序中的所有符号引用(如函数调用、全局变量访问)都能正确绑定到内存中的有效位置,若找不到定义则报错(如 undefined reference)。符号解析是链接的核心步骤,解决了模块化开发中代码分散的问题。
2.重定位
重定位是链接器根据符号解析的结果,对目标文件中的 地址引用 进行修正,使其指向可执行文件或内存中的 实际运行时地址 的过程。例如,hello.o 中的 call puts 指令原本指向临时占位地址(0x0),链接后修正为 puts@plt(位于 .plt 节),而 .plt 中的代码会通过 .got.plt 表间接跳转到动态库 libc.so 中的 puts 实现。链接器还会合并所有目标文件的代码段(.text)和数据段(.data、.rodata),重新计算相对偏移(如 lea 0xead(%rip),%rax 中的 0xead 对应 .rodata 中的字符串地址),最终生成可直接加载到内存运行的二进制文件。重定位实现了地址空间的统一分配,是程序从零散模块到完整可执行体的关键步骤。
5.5.3重定位过程
链接前call指令的目标地址为无意义的占位符,连接后为具体的虚拟内存地址,如图5.6所示。
![]() | ![]() |
图5.6 连接前后call指令目标地址 |
5.6 hello的执行流程
通过gdb加载hello,先设置捕获所有函数的断点,再通过对每个断点附加命令使其自动打印调用信息,如图5.7所示。
![]() ![]() |
图5.7 通过gdb查看程序执行中的过程 |
结果如图5.8所示。
![]() |
图5.8 程序执行中的过程以及各个子程序名或程序地址 |
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
5.7.1 动态连接过程
动态链接是程序运行时由动态链接器(如ld-linux.so)完成的,主要过程如下:
- 加载可执行文件
操作系统加载可执行文件(如hello),读取其头部信息,确定依赖的共享库(如libc.so)。
- 加载共享库
动态链接器递归加载所有依赖的共享库到内存,并为它们分配地址空间。
- 符号解析与重定位
解析可执行文件和共享库中的未定义符号(如printf),找到其在共享库中的实际地址。
修改可执行文件中的全局偏移表(GOT)和过程链接表(PLT),将符号的地址填充到GOT中。
- 延迟绑定(Lazy Binding)
首次调用函数(如puts)时,通过PLT跳转到动态链接器的解析函数,解析符号地址并更新GOT,后续调用直接跳转到目标地址。
5.7.2 hello动态链接前后动态链接项目的内容变化
通过gdb加载hello程序,并在main函数和printf函数调用前分别设置断点,如图5.9所示。
![]() |
图5.9 加载hello并设置断点 |
观察反汇编代码,printf函数对应GOT条目地址为0x404008,运行程序,在第一个断点处用x指令查看printf函数对应GOT条目内容,如图5.10所示。
![]() |
图5.10 动态链接前printf函数GOT条目 |
继续运行程序,在第二个断点处单步运行程序至调用printf函数后,再次用x指令查看printf函数对应GOT条目内容,如图5.11所示。
![]() |
图5.11 动态链接后printf函数GOT条目 |
可以看到,动态链接前后printf函数对应的GOT条目发生变化,动态链接前GOT条目指向动态链接器的解析逻辑,动态链接后更新为函数实际地址。
5.8 本章小结
本章简要介绍链接的作用与概念,对hello.o进行连接并生成最终的链接后的可执行程序hello,并对其进行分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是计算机系统中程序执行的基本单元,代表了一个正在运行的程序的实例。它不仅包含程序代码和当前执行状态(如程序计数器、寄存器值等),还涉及运行时所需的内存、文件描述符等系统资源。操作系统通过创建和管理进程实现对多任务的支持,每个进程在独立的地址空间中运行,确保彼此隔离且互不干扰。进程的动态性和独立性使得计算机能够高效分配CPU时间片、处理并发任务,并为用户提供程序并行执行的体验。
6.1.2 进程的作用
进程提供给应用程序的关键抽象:
一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
一个私有的地址空间,它提供一个假象好像我们的程序独占地使用内存系统[1]
6.2 简述壳Shell-bash的作用与处理流程
6.2.1壳的作用
Shell是用户与操作系统内核交互的核心接口,既提供命令行环境供用户直接输入指令,也支持脚本化编程以实现自动化任务。其核心作用包括解析和执行用户命令、管理进程的启动与终止、协调输入输出重定向(如管道|
和文件重定向>
)、维护环境变量及工作目录状态,并通过元字符扩展(如通配符*
)简化文件操作。作为系统资源的调度中介,Shell将用户请求转化为系统调用,连接应用程序与底层硬件,同时支持后台任务、作业控制及复杂脚本逻辑,是系统管理和开发效率的核心工具。
6.2.2 壳的处理流程
Shell的处理流程始于读取用户输入或脚本内容,首先进行词法解析和语法分析,分割命令与参数,处理引号及转义符。随后展开环境变量替换(如$PATH)和元字符替换(如通配符*扩展为文件名),解析重定向符号并调整输入输出流。若命令为内置功能(如cd),则直接由Shell进程执行;若为外部程序(如ls),则通过fork创建子进程并exec加载目标程序,父进程通过wait等待子进程结束并捕获退出状态。流程结束后,Shell重置标准流并返回提示符,循环等待下一条指令,同时维护会话环境的一致性。
6.3 Hello的fork进程创建过程
当用户在Shell中执行./hello时,fork进程创建过程通过fork()系统调用生成一个与Shell父进程完全相同的子进程,该子进程独立拥有新的进程ID,但继承父进程的内存、环境变量和文件描述符等资源。这一过程仅复制Shell的上下文,尚未运行目标程序,子进程与父进程在此刻处于并行状态,等待进一步指令。
6.4 Hello的execve过程
子进程调用execve()系统调用,将hello可执行文件的代码段、数据段等内容加载到其内存空间,彻底覆盖原有的Shell程序副本,重置堆栈并初始化寄存器,最终跳转到hello的入口函数(如main())。这一过程完全替换了子进程的执行逻辑,使其蜕变为一个专属于hello的新进程,而父进程则通过wait()等待子进程结束并回收其资源。
6.5 Hello的进程执行
进程调度通过时间片轮转或优先级策略分配CPU资源。进程运行时处于用户态,其上下文(寄存器、程序计数器等)存储于PCB中;当时间片耗尽或触发中断(如I/O请求),CPU通过硬件中断切换到核心态,内核保存当前进程上下文并运行调度算法,从就绪队列选择新进程,加载其上下文到寄存器并切换回用户态执行。核心态完成资源管控和状态切换,用户态则专注程序执行,两者通过系统调用/中断协作,实现进程高效切换与CPU利用率优化。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1 hello执行过程中可能会出现的异常
- 中断(异步异常):处理器外部I/O设备引起;
- 陷阱(同步异常):有意的,执行指令的结果;
- 故障(同步异常):不是有意的,但可能被修复;
- 终止(同步异常):非故意,不可恢复的致命错误造成。
6.6.2 hello执行过程中可能会产生的信号
- 中断:信号SIGTSTP,默认行为是停止直到下一个SIGCONT;
- 终止:信号SIGINT,默认行为是终止。
6.6.3 对异常和信号的处理方式
如图6.1所示,在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表(exception table) 的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序(exception handler)) 。当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下 3种情况中的一种:
1) 处理程序将控制返回给当前指令 Icurr 即当事件发生时正在执行的指令。
2) 处理程序将控制返回给Inext , 如果没有发生异常将会执行的下一条指令。
3) 处理程序终止被中断的程序[1]。
![]() |
图6.1 异常的处理方式 |
6.6.4 hello程序运行以及异常与信号的处理方式
在执行过程中按下CTRL-z终端驱动程序向前台进程组(子进程hello所在组)发送SIGTSTP信号,子进程收到后默认暂停执行(进入TASK_STOPPED状态)。
如图6.2所示,按下CTRL-z后进程停止运行,状态为STOPPED。
![]() |
图6.2 执行过程中按下CTRL-z结果截图 |
CTRL-z后运行ps命令,系统显示各进程信息,如图6.3所示。
![]() |
图6.3 运行ps命令 |
CTRL-z后运行jobs命令,显示当前会话内所有被暂停(如按Ctrl-Z)或后台运行(以&结尾启动)的任务列表,如图6.4所示,进程hello处于被暂停状态。
![]() |
图6.4 运行jobs命令 |
CTRL-z后运行pstree命令,以树状图形式展示进程间的父子关系,直观呈现进程的派生层级,如图6.5所示。
![]() |
图6.5 运行pstree命令 |
CTRL-z后运行fg命令,将后台任务或暂停的任务切换至前台继续执行,需指定任务编号,如图6.6所示。
![]() |
图6.6 运行fg命令 |
CTRL-z后运行kill-9命令,向进程发送SIGKILL信号杀死进程,如图6.7所示。
![]() |
图6.7 运行kill命令 |
按下CRTL-c后终端发送SIGINT信号给前台进程组,子进程hello默认终止执行,如图6.8所示。
![]() |
图6.8 执行过程中按下CTRL-c结果截图 |
6.7本章小结
本章简述了进程的概念和作用、异常与信号的处理方式。并结合具体程序hello简述了进程执行过程以及对异常和信号的处理。
(第6章2分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.1.1 逻辑地址
逻辑地址是程序编译后生成的地址,例如代码中循环变量i或函数printf的地址,它们存在于程序的视角中,是相对于代码段或数据段的偏移量。
7.1.2线性地址
当程序被加载到内存时,操作系统通过分段机制将逻辑地址转换为线性地址(在x86架构中表现为“段基址+偏移量”),但现代操作系统通常简化分段,逻辑地址与线性地址往往直接对应。
7.1.3 虚拟地址
虚拟地址是进程运行时所见的连续内存空间,例如hello进程通过argv[1]访问命令行参数时,参数在进程独立的虚拟地址空间中“看似连续”,实际可能分散在物理内存或磁盘交换区。
7.1.4 物理地址
操作系统通过页表将虚拟地址映射到物理地址(即实际内存芯片上的硬件地址),例如sleep(atoi(argv[4]))执行时,代码指令所在的虚拟地址会被内存管理单元(MMU)转换为物理地址,确保CPU正确访问内存中的数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器的段式管理通过段选择符(段寄存器)和段描述符实现转换。逻辑地址由“段选择符:偏移量”组成,CPU根据段选择符在全局描述符表(GDT)或局部描述符表(LDT)中查找对应的段描述符,提取段基址后与偏移量相加,生成线性地址。段描述符定义了内存段的基址、界限和访问权限,完成地址空间隔离与保护。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的转换通过页式管理实现。操作系统将线性地址划分为页目录索引、页表索引和页内偏移。CPU利用CR3寄存器定位页目录的基址,通过页目录索引找到对应的页表,再根据页表索引定位物理页框的基址,最终将基址与页内偏移相加得到物理地址。页表项中保存了权限标志(如存在位、读写权限),确保内存访问的安全性和隔离性。此过程通过多级页表结构实现虚拟内存到物理内存的映射。
7.4 TLB与四级页表支持下的VA到PA的变换
在 TLB(转换后备缓冲器) 和四级页表的支持下,虚拟地址(VA)到物理地址(PA)的转换过程如下:
- TLB查询:CPU首先用虚拟地址的高位(VPN)查询TLB,若命中则直接获取物理页框号(PPN),跳过页表遍历,极大加速转换。
- 四级页表遍历(若TLB未命中):
- 从CR3寄存器获取顶级页目录(PML4)基址。
- 依次用虚拟地址的4级索引(PML4→PDPT→PD→PT)逐级查页表项(PTE),最终得到PPN。
- 将PPN与虚拟地址的页内偏移合并,生成物理地址(PA)。
- 权限与异常检查:每级PTE验证权限位(如存在位、读写权限),若非法则触发缺页异常或保护错误。
- TLB更新:转换完成后,将VPN-PPN映射存入TLB,供后续快速访问。
7.5 三级Cache支持下的物理内存访问
在 三级Cache(L1/L2/L3) 的支持下,CPU访问物理内存的流程如下:
- L Cache查询:CPU核首先用物理地址查询私有L Cache(通常分指令/数据Cache)。若命中(数据存在),直接以极低延迟(约-4周期)返回数据。
- L Cache查询(L1未命中):访问同核共享的L Cache。若命中,数据返回CPU并回填L1 Cache(约10-0周期)。
- L Cache查询(L2未命中):访问多核共享的L Cache。若命中,数据返回并依次回填L2/L1 Cache(约0-50周期)。
- 主存访问(L3未命中):通过内存控制器访问DRAM主存(约200+周期),数据加载后按策略回填L3→L2→L1 Cache,并触发Cache替换策略(如LRU)淘汰旧数据。
- 一致性维护:多核场景下,通过MESI协议维护多级Cache间数据一致性,确保多核读写操作的原子性和可见性。
7.6 hello进程fork时的内存映射
调用 fork() 创建子进程时,内核复制父进程的地址空间结构(页表、虚拟内存区域等),但物理内存页通过写时复制(Copy-On-Write, COW) 共享。父子进程的代码段、数据段等只读部分直接共享物理页;当任一进程尝试修改共享页时,触发缺页异常,内核再分配新物理页并复制内容,实现内存隔离。
7.7 hello进程execve时的内存映射
调用 execve() 加载新程序时,内核清空原进程的地址空间,重新构建内存映射:
- 代码段(text):映射可执行文件的代码区(只读)。
- 数据段(data/bss):映射初始化/未初始化数据区(读写,私有)。
- 堆/栈:分配匿名页(动态增长)。
- 动态库:通过 mmap 加载共享库(如 libc.so)到共享内存区。
原进程的资源(如打开文件)可能保留,但代码、数据、堆栈等完全替换为新程序的内容
7.8 缺页故障与缺页中断处理
当程序访问的虚拟地址未映射到物理内存(页表项存在位为0,或权限不符)时,CPU触发缺页故障,产生缺页中断。缺页中断的处理流程如下:
- 中断响应
CPU暂停当前进程,切换到内核态,执行缺页中断处理程序。 - 错误诊断
- 检查虚拟地址的合法性(是否属于进程地址空间,权限是否匹配)。
- 若非法(如越界访问或权限错误),终止进程或发送信号(如SIGSEGV)。
- 页面加载
- 合法缺页:定位所需页面在磁盘的位置(交换空间或文件系统)。
- 物理页分配:从空闲链表中分配物理页帧,若内存不足则触发页面置换(如LRU算法)。
- 数据加载:从磁盘读取页面到物理页帧,若为写时复制(COW)则复制原页内容。
- 页表更新
修改页表项,将虚拟地址映射到新分配的物理页帧,并设置存在位及权限标志。 - 恢复执行
重新执行触发缺页的指令,此时虚拟地址已映射到有效物理内存。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)
7.10本章小结
本章关注hello的内存管理,介绍了hello从加载到执行过程中涉及到的地址变换以及内存管理过程。
(第7章 2分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
从计算机系统视角,hello程序的生命周期经历以下核心过程:
- 编译时处理:通过预处理器展开宏与头文件,编译器生成与机器相关的汇编指令,汇编器编码为可重定位目标文件,链接器完成符号解析与地址重定位,构建可执行ELF文件。
- 进程创建:Shell通过fork创建子进程副本,execve加载hello代码段至虚拟地址空间,初始化堆栈并建立页表映射,形成独立进程控制块(PCB)。
- 运行时管理:MMU通过四级页表与TLB实现VA到PA转换,三级Cache优化物理内存访问;动态链接库通过PLT/GOT实现延迟绑定。
- 执行控制:CPU按指令流水线执行逻辑,处理缺页中断与信号(如SIGINT),内核调度器分配时间片实现进程切换。
- 资源回收:进程终止后,内核释放物理页帧、文件描述符等资源,清除虚拟内存映射,完成020生命周期闭环。
看似简单的程序,其背后是设计精妙逻辑完善的计算机系统的支撑。通过完成本文,我加深了对于计算机系统的理解,运用课程的知识对具体程序进行了完整的分析,收获颇丰!
(结论0分,缺失-1分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名 | 作用 |
hello.i | 预处理后文件 |
hello.s | 汇编文件 |
hello.o | 可重定位的可执行文件 |
hello | 链接后的可执行文件 |
hello_elf.txt | hello.o readelf结果 |
hello_elf2.txt | hello readelf结果 |
hello_obj.txt | hello.o反汇编结果 |
hello_obj2.txt | hello 反汇编结果 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
- (美)兰德尔. E. 布莱恩特等著;龚奕利,贺莲译.深入理解计算机系统(原书第3版).北京:机械工业出版社, 2016.7
(参考文献0分,缺失 -1分)