计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 电信
学 号 2023111059
班 级 23L0501
学 生 吉兆壮
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本文以程序(hello.c)为例,系统分析从源代码到进程执行的完整生命周期,涵盖预处理、编译、汇编、链接、进程管理、存储管理及I/O管理等计算机系统核心机制。通过工具链(如GCC、readelf、objdump)和内核机制(如进程调度、虚拟内存、动态链接)的剖析,揭示了程序如何从静态代码转化为动态进程,并最终被操作系统回收的全过程。
关键词: 预处理、链接、进程管理、虚拟内存
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
1. P2P(Program to Process)
a.编程(Program) Hello的源头是程序员用高级语言(如C)编写的文本文件(hello.c),属于静态的目标代码(Object Code)。
b.预处理(Preprocess) 通过预处理器(如cpp)展开头文件、宏替换,生成扩展的源代码(hello.i)。
c.编译(Compile) 编译器(如gcc -S)将hello.i翻译为汇编代码(hello.s),包含机器指令的符号表示。
d.汇编(Assemble) 汇编器(如as)将hello.s转换为可重定位目标文件(hello.o),即二进制机器码,但地址未最终确定。
e.链接(Link) 链接器(如ld)合并hello.o与库文件(如libc.so),解析外部引用,生成可执行目标程序(hello),存储在磁盘上。
f.加载运行(Process) 用户通过Shell输入./hello后,操作系统通过加载器将hello的代码和数据读入内存,创建进程(Process),分配PID、虚拟地址空间和运行时资源(如堆栈),CPU开始执行指令,Hello正式成为动态的进程实例。
2. O2O(Zero-0 to Zero-0)
a开始(Zero-0) Hello进程从零状态启动,初始化为用户态上下文(如寄存器清零、参数argc/argv压栈)。
b执行(Runtime) CPU调度:内核为Hello分配时间片,CPU执行其指令(如打印"Hello, World!")。 I/O操作:调用printf时,通过系统调用(如write)访问内核,将输出写入标准输出(如终端或文件)。 内存管理:若触发缺页(Page Fault),操作系统按需加载代码/数据页到物理内存。
c终止(Zero-0) 正常退出:Hello执行return 0或exit(),释放内存、文件描述符等资源,向父进程(如Shell)返回状态码。 异常终止:若发生错误(如段错误),内核强制终止进程,回收资源并记录错误信息。 进程消失:Hello的PCB(进程控制块)被销毁,回归零状态,仅保留可能的退出状态供父进程查询。
1.2 环境与工具
硬件环境:处理器13th Gen Intel(R) Core(TM) i7-13700H 2.40 GHz
机带RAM 16.0 GB (15.7 GB 可用)
系统类型 64 位操作系统, 基于 x64 的处理器
软件环境:Windows 11 家庭中文版 64位
Ubuntu 18.04 64位
开发工具:codeblocks,gcc,gdb,readelf,HexEdit,ld
1.3 中间结果
hello.c 原C语言文件
hello.i 预处理产生文件
hello.s 编译产生文件
hello.o 汇编产生文件
hello.out 链接产生可执行文件
hello 链接产生可执行文件
hello_o_elf.txt 查看hello.o的elf格式对应的文本文件
hello_o_asm.txt 查看hello.o的反汇编对应的文本文件
hello_elf.txt 查看hello的elf格式对应的文本文件
hello_asm.txt 查看hello的反汇编对应的文本文件
1.4 本章小结
本章对hello做了总体的介绍,简述了hello的p2p和020过程。
列出并介绍了本次实验的环境和工具,生成的中间结果文件的名字以及文件的作用。
第2章 预处理
2.1 预处理的概念与作用
1. 概念
预处理(Preprocessing)是C/C++程序编译的第一个阶段,由预处理器(如cpp)对源代码(.c文件)进行文本级处理,生成扩展后的中间文件(.i)。预处理不涉及语法分析或机器代码生成,仅按指令修改源代码文本。
2. 作用
预处理的主要功能包括:
宏替换:将#define定义的宏展开为实际值或代码片段。
文件包含:通过#include将头文件(如stdio.h)内容插入到源文件中。
条件编译:根据#ifdef、#if等指令选择性包含或排除代码块。
添加行标记:保留#line指令,用于调试时定位原始代码行号。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i

图:在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
由于原来.c文件中的宏在解析后展开加入,引入头文件内容。代码内容大量增加,达到3061行。

图:hello.i文件3061行
main函数代码排在文件的后面部分。在main函数之前,预处理器就分别读取stdio.h、unistd.h、stdlib.h中的内容,并且根据读入的顺序依次进行内容的展开。如果头文件中仍然有以字符“#”开头的内容,则预处理器继续对其进行处理,最终的hello.i文件中没有宏定义。

图:hello.i中引用stdio.h

图:hello.i中引用unistd.h

图:hello.i中引用stdlib.h
此外可以发现注释全部消除了,是删除所有注释(//、/* */)以减少编译负担。
2.4 本章小结
本章主要介绍了预处理的概念以及预处理在五个方面的作用与功能。
在Ubuntu下将hello.c文件预处理生成了hello.i文件。
分析了hello.i文件与源程序hello.c文件的相同与不同之处。
第3章 编译
3.1 编译的概念与作用
1. 概念
编译(Compilation) 是将预处理后的源代码(.i 或 .c 文件)转换为汇编代码(.s)的过程,由编译器(如 gcc -S)完成。此阶段进行词法分析、语法分析、语义分析及优化,生成与机器架构相关的低级中间表示。
2. 作用
翻译转换:将高级语言(如C)代码转为汇编指令(如x86或ARM)。
语法检查:检测语法错误(如缺少分号、类型不匹配)。
代码优化:对中间代码进行局部优化(如常量折叠、死代码删除)。
生成符号表:记录变量、函数等符号的地址和类型信息,供后续阶段使用。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

图:在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 数据与变量处理
- 常量
字符串常量:.LC0 存储中文和数字字符串(UTF-8编码),.LC1 存储格式化字符串 "Hello %s %s %s\n"。

图:hello.s中的字符串常量
数字常量:如 $5(cmpl $5, -20(%rbp))、$0(movl $0, %eax)等直接嵌入指令。


图:hello.s中数字常量
- 变量
局部变量:通过栈帧管理,如 -20(%rbp) 存储 argc,-32(%rbp) 存储 argv,-4(%rbp) 为循环计数器 i。

图:hello.s中的局部变量
全局变量:未出现。
静态变量:未出现。
3.3.2 类型与类型转换
- 隐式类型转换
call atoi@PLT 将字符串转为 int(argv[4] → sleep 参数)。movl $0, %eax 在 printf 前清零 eax,因 printf 返回值为 int。

图:隐式类型转换例子
- 显式类型转换
无显式转换。
- 指针操作
movq -32(%rbp), %rax 加载 argv 地址,通过偏移量访问元素(如 addq $8, %rax 访问 argv[1])。
函数调用时指针传递:argv 作为 char** 通过 %rsi 传递。
3.3.3 表达式与操作
- 算术操作
加法:addq $8, %rax(指针偏移)、addl $1, -4(%rbp)(i++)。
减法:subq $32, %rsp(分配栈空间)。
![]()
图:算术操作例子
- 逻辑与位操作
逻辑比较:cmpl $5, -20(%rbp)(检查 argc == 5)。
条件跳转:jle .L4(循环控制)。
![]()
图:逻辑与位运算例子
- 关系操作
cmpl $9, -4(%rbp) 判断 i <= 9,用于循环终止条件。
图:关系操作例子
3.3.4 控制流与函数调用
- 分支控制
if (argc != 5) → je .L2 和 call exit@PLT。
循环:.L3 和 .L4 标签实现 for (i=0; i<=9; i++)。
- 函数调用
参数传递:
printf:%rdi(格式串地址)、%rsi/%rdx/%rcx(argv[1]/[2]/[3])。
sleep:%edi(atoi 转换后的 int)。
返回值:getchar 结果通过 %eax 返回。
- 函数栈帧
pushq %rbp 保存基指针,subq $32, %rsp 分配栈空间,leave 恢复栈帧。
3.3.5 其他关键处理
- 字符串与I/O操作
puts(.LC0) 直接输出字符串,printf 格式化输出 argv 参数。
getchar@PLT 等待输入。
- 系统调用封装
exit@PLT、sleep@PLT 等通过 PLT(过程链接表)动态链接到 libc。
- 安全机制
endbr64 防止控制流劫持(Intel CET 特性)。
3.4 本章小结
通过分析 hello.s,我们直观理解了编译器如何将程序员逻辑转化为机器可执行逻辑,为后续汇编、链接阶段奠定基础。
第4章 汇编
4.1 汇编的概念与作用
1. 汇编的概念
汇编(Assembly) 是将 汇编代码(.s 文件) 转换为 可重定位目标文件(.o 文件) 的过程,由 汇编器(Assembler,如 as) 完成。该阶段将人类可读的汇编指令(如 mov, add)翻译为机器可执行的 二进制机器码,并生成目标文件所需的元数据(如符号表、重定位信息)。
- 汇编的作用
a.指令翻译
将汇编指令(如 movq %rsp, %rbp)转换为对应的 机器码(二进制形式,如 0x48 0x89 0xE5)。
解析伪指令(如 .section, .align),指导目标文件的节区布局。
b.生成目标文件结构
生成 ELF(Executable and Linkable Format) 格式的 .o 文件,包含:
代码段(.text):存储机器指令。
数据段(.data 和 .rodata):存储初始化变量和常量。
符号表(Symbol Table):记录函数、变量名及其地址。
重定位表(Relocation Table):标记链接时需要修正的地址(如外部函数 printf 的调用地址)。
c.处理符号引用
解析局部符号(如函数标签 .L2),分配相对地址。
标记未解析的外部符号(如 printf@PLT),留待链接阶段处理。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o

图:在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.3.1 ELF头。

图:ELF头
| 字段 | 值 | 含义 |
| Magic | 7f 45 4c 46 02 01 01 00... | ELF 文件标识 |
| Class | ELF64 | 64 位文件 |
| Data | 2 补码,小端序 | 数据存储方式 |
| OS/ABI | UNIX - System V | 遵循 System V ABI |
| Type | REL | 可重定位目标文件 |
| Machine | x86-64 | 目标 CPU 架构 |
| Entry point | 0x0 | 链接时决定入口 |
| Section headers | 14 个,从 1264 字节开始 | 描述所有节区 |
该 ELF 头描述了
文件格式(ELF64, 小端序, x86-64)。
文件类型(可重定位目标文件,需链接)。
节区布局(14 个节区,节头表在 1264 字节处)。
链接信息(入口地址待定,无程序头)。
这些信息帮助 链接器 和 调试工具 正确解析和处理目标文件。
4.3.2 节表

图:节表
1. 节表整体结构
该节表包含14个条目(索引0-13),每个条目描述一个节区的属性。主要字段说明:
名称:节区名称(如.text、.data)
类型:节区内容类型(如PROGBITS、NOBITS)
地址:加载到内存的虚拟地址(可执行文件有效)
偏移量:节区在文件中的起始位置
大小:节区实际内容大小
全体大小:对齐后占用的空间大小
旗标:访问权限标记(A=可分配,X=可执行,W=可写)
链接/信息:关联的其他节区索引
对齐:内存对齐要求(字节数)
2. 关键节区分析
(1) 代码与数据节区
索引 名称 类型 大小 旗标 作用
[1] .text PROGBITS 0x9d AX 存储可执行机器指令
[3] .data PROGBITS 0x0 WA 已初始化的全局变量(本例为空)
[4] .bss NOBITS 0x0 WA 未初始化的静态数据(不占文件空间)
[5] .rodata PROGBITS 0x3d A 只读数据(如字符串常量)
(2) 重定位信息
索引 名称 类型 链接 作用
[2] .rela.text RELA [11] .text节的重定位条目
[10] .rela.eh_frame RELA [11] 异常处理框架的重定位
(3) 符号与字符串表
索引 名称 类型 大小 作用
[11] .symtab SYMTAB 0x1b0 符号定义与引用
[12] .strtab STRTAB 0x48 普通字符串表
[13] .shstrtab STRTAB 0x74 节区名称字符串表
(4) 其他辅助节区
索引 名称 类型 作用
[6] .comment PROGBITS 编译器版本信息
[8] .note.gnu.property NOTE 二进制特性标记
[9] .eh_frame PROGBITS 异常处理信息
4.3.3 重定位节

图:重定位节
1. .rela.text节分析
(1) 数据引用(PC相对寻址)
偏移量 类型 符号 加数 对应指令
0x1c R_X86_64_PC32 .rodata -4 lea .rodata(%rip), %rdi
0x5f R_X86_64_PC32 .rodata +29 格式化字符串地址加载
计算方式:
S(符号地址) + A(加数) - P(当前位置)
示例:0x1c处的修正值 = .rodata基地址 - 4 - (当前指令地址 + 0x1c)
(2) 函数调用(PLT跳转)
偏移量 类型 函数 加数 对应指令
0x21 R_X86_64_PLT32 puts -4 call puts@PLT
0x69 R_X86_64_PLT32 printf -4 call printf@PLT
2. .rela.eh_frame节分析
用于建立异常处理信息与代码段的关联
确保栈展开时能正确定位函数边界
4.3.4 符号表

图:符号表
ELF文件格式中的符号表中存放了程序中定义和引用的的全局变量以及函数的信息,无局部变量。
4.4 Hello.o的结果解析

图:hello.o反汇编
1. 整体结构对比
| 特征 | hello.s (汇编代码) | hello.o (机器码) |
| 符号化标签(如.LC0、.L2) | 具体地址偏移(如0x1c、0x2f) | |
| 函数调用 | 显式标记@PLT(如puts@PLT) | 占位符+重定位条目(如e8 00 00 00 00) |
| 数据引用 | 符号化地址(如.LC0(%rip)) | PC相对偏移+重定位(如48 8d 3d 00 00...) |
| 控制流 | 标签跳转(如jle .L4) | 直接偏移编码(如7e a7) |
2.详细对比
A.指令形式不同:



B.函数调用:
C. 数据引用:

D.控制流
如a中的例子
除此之外,数字的进制也有所不同
hello.s中操作数都是十进制的,而在反汇编代码中以十六进制表示
4.5 本章小结
本章通过将.s汇编为.o文件,了解从汇编程序到可重定位目标程序(二进制)的过程。同时通过查看ELF表,查看了其中的各项内容。对hello.o二进制文件进行反汇编,得到了反汇编程序,并分析了该反汇编程序与汇编语言程序hello.s中语句的对应关系。从指令形式、函数调用、数据引用、控制流四个方面分析了二者的关系。
第5章 链接
5.1 链接的概念与作用
链接(Linking)是将编译生成的可重定位目标文件(如hello.o)转换为可执行文件(如hello)的关键过程。其核心任务是解决模块间的符号引用问题,并完成地址空间的统一布局。具体作用可分为以下方面:
- 符号解析与绑定
查找hello.o中未定义的符号(如printf、exit)在库文件(如libc.so)中的实际定义位置
建立符号引用与定义的映射关系,确保所有外部函数和全局变量能被正确访问
- 地址空间分配
合并所有输入文件的代码段(.text)、数据段(.data、.rodata)等节区
为合并后的节区分配运行时内存地址,构建进程虚拟地址空间布局
- 重定位修正
根据最终分配的地址,修改hello.o中临时填充的指令操作数(如call 0x00000000→call 0x401000)
处理两类重定位:
代码重定位:修正函数调用和跳转的目标地址
数据重定位:修正全局变量和静态数据的访问地址
- 构造可执行结构
生成ELF程序头(Program Header),定义内存加载段(LOAD Segments)
添加动态链接信息(如.dynamic节),标记依赖的共享库(如libc.so.6)
设置入口地址(Entry Point)为_start或main函数
- 库文件处理
静态链接:从.a归档中提取所需目标文件,直接合并到最终可执行文件
动态链接:仅记录依赖关系,运行时由动态链接器加载共享库(.so)
通过链接器的处理,零散的机器代码片段被整合为统一的执行实体,使程序具备可加载和执行的完整结构。这一过程既解决了编译时地址不确定的问题,也实现了代码的高效复用和模块化管理。
5.2 在Ubuntu下链接的命令


5.3 可执行目标文件hello的格式
readelf -a hello > hello1.elf或者readelf -a hello.o

EFL头:
这部分以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。
EFL头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括EFL头大小,目标文件类型,机器类型,节头部表的文件按偏移


其中,它的第一列按地址顺序列出了各段的名称及大小,第三列列出来各段的起始地址,最后一列列出来各段的偏移量
5.4 hello的虚拟地址空间
edb --run hello


1. 虚拟地址空间布局(EDB观察)
使用edb加载hello后,其典型虚拟地址空间分段如下:
| 内存区域 | 起始地址 | 权限 | 对应文件节区 | 内容描述 |
| 程序头段 | 0x400000 | R-X | .text、.plt | 可执行代码(含_start和main) |
| 只读数据段 | 0x401000 | R-- | .rodata、.eh_frame | 字符串常量、异常处理信息 |
| 读写数据段 | 0x404000 | RW- | .data、.bss、.got | 全局变量、GOT表 |
| 堆空间 | 0x405000 | RW- | (运行时分配) | malloc动态分配的内存 |
| 共享库映射区 | 0x7ffff7a00000 | R-X | libc.so的.text | 共享库代码(如printf实现) |
| 栈空间 | 0x7ffffffdd000 | RW- | (内核管理) | 局部变量、函数调用栈 |
2. 与ELF可执行文件格式的对照
通过readelf -l hello查看程序头(Program Header),关键LOAD段与内存映射的对应关系:
| ELF文件中的段 | 文件偏移 | 内存虚拟地址 | 内存大小 | 权限 | 对应内存区域 |
| PHDR | 0x40 | 0x400040 | 0x268 | R | 程序头本身 |
| LOAD(代码段) | 0x0 | 0x400000 | 0x1000 | R-X | 程序头段 |
| LOAD(数据段) | 0x2000 | 0x402000 | 0x2000 | RW- | 读写数据段 |
关键差异:
文件偏移 → 虚拟地址:链接器根据LOAD段定义将文件内容映射到指定虚拟地址(如.text段从文件0x0映射到0x400000)
.bss段的处理:ELF文件中.bss不占文件空间(p_filesz < p_memsz),加载时由系统补零初始化
3. 动态链接相关区域
通过edb观察动态链接器的内存操作:
- GOT/PLT机制:
.got.plt(0x404018):存储printf等函数的实际地址(首次调用后由动态链接器填充)
.plt(0x401020):包含跳转桩代码,
- 共享库加载:
ld-linux-x86-64.so.2将libc.so映射到0x7ffff7a00000附近
通过DT_NEEDED条目(readelf -d hello可查看)确定依赖库
4. 关键地址对照示例
以printf调用为例:
| 阶段 | 地址类型 | 值/位置 | 说明 |
| 编译时 | 临时占位 | call 0x0 | hello.o中的未解析调用 |
| 链接后 | PLT入口地址 | call 0x401030 | hello中指向printf@plt |
| 运行时 | 实际函数地址 | 0x7ffff7b04f00 | libc.so中的printf实现地址(首次调用后填入GOT) |
5.5 链接的重定位过程分析
hello反汇编文件中,每行指令都有唯一的虚拟地址,而hello.o的反汇编没有,只是相对于代码段(通常是 .text 段)的偏移地址。这是因为目标文件只是一个中间产物,还没有被链接到最终的内存地址空间。这是因为hello经过链接,已经完成重定位,每条指令分配了唯一的虚拟地址,每条指令的地址关系已经确定;

Hello.o并没有链接,所以需要告诉链接器(linker)在链接时需要执行的动作。
6f: R_X86_64_PLT32 printf-0x4告诉链接器(linker)在链接时需要执行的动作。6f 是一个字节偏移量,指示了在某个特定位置发生了重定位动作。
R_X86_64_PLT32 是一个重定位类型(relocation type),表示这是一个32位的重定位项。printf-0x4 表示需要修改的目标符号是 printf,并且要在链接时将其地址减去 0x4。
5.6 hello的执行流程
从加载hello到_start,到call main,以及程序终止的所有过程如下:
_dl_start 地址:0x7f894f9badf0
_dl_init 地址:0x0x7f894f9cac10
_start 地址:0x401090
_libc_start_main 地址:0x7fce59403ab0
_cxa_atexit 地址:0x7f38b81b9430
_libc_csu_init 地址:0x4005c0
_setjmp 地址:0x7f38b81b4c10
_sigsetjmp 地址:0x7efd8eb79b70
_sigjmp_save 地址:0x7efd8eb79bd0
main 地址:0x401176
(argc!=3时
puts 地址:0x401030
exit 地址:0x401070
此时输出窗口打印出“用法: Hello 学号 姓名 秒数!”,程序终止。)
print 地址:0x401040
sleep 地址:0x401080 (以上两个在循环体中执行8次)
此时窗口打印8行“”
getchar 地址:0x4004d0
等待用户输入回车,输入回车后:
_dl_runtime_resolve_xsave 地址:0x7f5852241680
_dl_fixup 地址:0x7f5852239df0
_uflow 地址:0x7f593a9a10d0
exit 地址:0x7f889f672120
程序终止。
5.7 Hello的动态链接分析
PLT/GOT协作流程:

当程序调用一个共享库的函数时,编译器不能预测这个函数在什么地址,因为定义它的共享模块在运行时可以加载到任何位置。这时,编译系统提供了延迟绑定的方法,即:将过程地址的加载推迟到第一次调用该过程时。通过观察edb对hello的执行情况,便可发现dl_init前后后.got.plt节发生的变化。
首先,通过readelf找到.got.plt节在地址为0x404000的地方开始,大小为0x48。因此,结束地址为0x40400047,这两个地址之间部分便是.got.plt的内容。





在edb中的Data Dump中找到这个地址,观察.got.plt节的,发现在dl_init前后,.got.plt的第8到15个字节发生了变化。
在这里,这些变化的字节分别对应GOT[1]和GOT[2]的位置。其中, GOT[1]包括动态链接器在解析函数地址时使用的信息,而GOT[2]则是动态链接器ld-linux.so模块中的入口点。加载时,动态链接器将重定位GOT中的这些条目,使它们包含正确的地址。内存的变化如下图所示:
![]()
![]()
5.8 本章小结
本章主要介绍了本章主要介绍了链接的的概念以及链接的作用,主要是将将预编译好了的一个目标文件(hello.o)或若干目标文件外加链接库合并成为一个可执行目标文件(hello)。
在Ubuntu下将hello.o文件经过链接器(ld)生成了可执行目标文件hello。
分析了hello的ELF格式,并用readelf等列出了其各节的基本信息,包括起始位置、大小等信息。
用edb查看了hello的虚拟地址空间,发现各节的名称都与相应的一段虚拟地址相对应,同时查看了各节的起始位置与大小。
对可执行目标文件hello进行反汇编,得到了反汇编程序,并与hello.o的反汇编程序进行比较。发现相比于hello.o的反汇编程序来说,经过链接过程后,hello的反汇编程序代码量增加,插入了C标准库中的函数代码,指令都分配了虚拟地址,字符串常量的引用、函数调用以及跳转指令的地址都替换为了虚拟地址。
分析了链接的过程,包括符号解析以及重定位。
使用edb执行hello,说明了从加载hello到_start,到call main,以及程序终止的所有过程,并列出了其调用与跳转的各个子程序名以及程序地址。
分析了hello程序的动态链接项目,通过edb调试,分析了在dl_init前后,这些.got.plt节的的内容变化,是由动态链接的延迟绑定造成的。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是操作系统进行资源分配和调度的基本单位,是程序的一次动态执行实例,包含独立的地址空间、寄存器集合和内核状态。
核心作用:
- 资源隔离:每个进程拥有独立的虚拟地址空间(代码/数据/堆栈段),防止相互干扰
- 并发执行:通过时间片轮转实现宏观上的多任务并行
- 权限控制:用户态进程通过系统调用进入内核态,实现特权级隔离
- 状态管理:维护进程生命周期(就绪、运行、阻塞等状态)
6.2 简述壳Shell-bash的作用与处理流程
作用:
命令行解释器:解析用户输入的命令(如./hello)
进程控制:创建子进程执行目标程序
环境管理:维护PATH等环境变量
处理流程:
- 词法分析:将输入命令拆分为令牌(./hello→可执行文件路径)
- 查找程序:在PATH路径中搜索hello文件
- 创建子进程:调用fork()复制Shell进程上下文
- 程序加载:子进程通过execve()加载hello的代码段和数据段
- 等待结束:Shell调用waitpid()等待子进程终止
6.3 Hello的fork进程创建过程
(关键步骤:
- 复制父进程:
复制PCB(进程控制块)、页表、打开的文件描述符等
子进程获得与父进程完全相同的地址空间副本(写时复制优化)
- 返回值区分:
父进程接收子进程PID
子进程接收0
- 资源继承:
继承父进程的工作目录、环境变量、信号处理方式
写时复制(COW):
实际物理内存复制延迟到子进程尝试修改数据时发生,减少开销
6.4 Hello的execve过程
执行流程:
- 权限检查:验证用户对hello文件的可执行权限
- 加载程序段:
读取ELF头部,映射.text、.data等段到内存
初始化.bss段为0
- 设置堆栈:
构建新栈帧,压入环境变量和命令行参数(argv、envp)
- 重置寄存器:
将PC寄存器指向_start入口(最终调用main)
- 资源清理:
释放原进程的代码段和数据段(保留文件描述符等共享资源)
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程上下文:
硬件上下文:PC、SP、通用寄存器、浮点寄存器
内存映像:代码/数据/堆栈段的页表映射
内核状态:系统调用参数、信号掩码、调度优先级
调度过程:
- 时间片耗尽:
时钟中断触发,CPU保存当前进程上下文到PCB
内核选择下一个就绪进程(如Shell)
- 模式切换:
用户态→内核态:通过中断/系统调用自动切换(CPU硬件完成)
内核态→用户态:恢复目标进程的寄存器上下文
- 抢占与非抢占:
普通进程采用时间片轮转(可抢占)
内核关键路径可能禁用抢占

6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1 正常运行
正常执行时,hello每隔两秒打印一行“Hello 2023111059 吉兆壮 13653590961 1”,进入循环,共打印八次。打印完毕后,调用getchar()函数,等待用户输入回车后程序终止。Shell回收hello子进程,继续等待用户输入指令。

图6.6-1 hello正常运行
6.6.2 不停乱按
当在程序执行过程中随机乱按时,按下的字符串会直接显示,但不会干扰程序的运行,由于在乱按过程中没有输入回车,所以在最后一行hello的字符串打印完毕后,需要敲一个回车才能退出程序。

图6.6-2 在hello执行过程中不停乱按
6.6.3 按回车
在hello执行过程中敲回车时,会首先再打印的过程中显示换行,一个回车显示一排换行。在打印完毕最后一行字符串后,由于输入的回车依然存在于stdin中,所以在调用getchar()函数时,会读取stdin中的回车,因此无需再敲回车键,便能终止程序。

图6.6-3 在hello执行过程中按回车
程序终止后,发现shell中出现五个空行,这是因为在程序的执行过程中,敲了六下回车键,因此都留在stdin中,getchar()只接收了其中的第一个回车,由于在程序终止后没有清空stdin,剩余的回车保留在其中。当shell继续运行时,遇到回车便开始处理,但单独的回车相当于一个空行,被shell忽略,读入但不执行任何操作,因此留下了五个空行。
6.6.4 按Ctrl-z
在程序执行过程中按Ctrl-z,产生中断异常,发送信号SIGSTP,这时hello的父进程shell会接收到信号SIGSTP并运行信号处理程序。
最终的结果是hello被挂起,并打印相关信息。

图6.6-4 在hello执行过程中按Ctrl-z
1. 输入ps
Ctrl-z之后,在shell命令行中输入ps,打印出各进程的pid,其中包括被挂起的hello。

图6.6-5 Ctrl-z后执行ps
2. 输入jobs
Ctrl-z之后,在shell命令行中输入jobs,打印出被挂起的hello的jid及标识。

图6.6-6 Ctrl-z后执行jobs
3. 输入pstree -p

Ctrl-z之后,在shell命令行中输入pstree -p,查看进程树之间的关系,同时输出对应的进程pid。在进程树中找到hello(2476),发现hello的父进程是zsh(2233),从祖先进程到hello的树为:systemed(1)→systemed(1477) →gnome-terminal-(2226) →zsh(2233)→hello(2476)。

图6.6-7 Ctrl-z后执行pstree
4. 输入fg
Ctrl-z之后,在shell命令行中输入fg,被挂起在后台的hello进程被重新调到前台执行,打印出剩余部分,按回车后终止程序。

图6.6-8 Ctrl-z后执行fg
5. 输入kill
Ctrl-z之后,输入ps,得到hello的pid为2584,因此,在shell中输入kill -9 2584,可以发送信号SIGKILL给进程,该进程被杀死。


图6.6-9 Ctrl-z后执行kill
6.6.5 按Ctrl-c
运行hello时按Ctrl-C,会导致断异常,从而内核产生信号SIGINT,发送给hello的父进程,父进程收到它后,向子进程发生SIGKILL来强制终止子进程hello并回收它。这时再运行ps,可以发现并没有进程hello,可以说明他已经被终止并回收了。

图6.6-10 在hello执行过程中按Ctrl-c
6.7本章小结
本章分析了hello的进程管理全流程:
进程创建:通过fork复制进程,execve加载程序。
进程执行:时间片调度与上下文切换实现并发,用户态与核心态分离保障系统安全。
信号处理:支持中断挂起(SIGTSTP)、终止(SIGINT)等操作,结合Shell命令(jobs、fg)实现进程控制。
进程管理是操作系统资源分配与多任务并发的核心机制,保障了程序的稳定运行与用户交互的灵活性。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序直接使用的地址,由段寄存器(CS/DS)和偏移量组成。
线性地址:段式管理转换后的地址(32位系统启用分段时有效,x86-64默认平坦模式,逻辑地址=线性地址)。
虚拟地址:进程视角的连续地址空间,通过页表映射到物理地址。
物理地址:实际DRAM内存中的地址,由MMU通过页表转换得到。
7.2 Intel逻辑地址到线性地址的变换-段式管理
x86-64简化段式管理:
代码段/数据段基址固定为0,逻辑地址直接作为线性地址。
保留FS/GS段寄存器用于线程局部存储(TLS)。
传统x86分段:线性地址 = 段基址(GDT/LDT) + 逻辑偏移,现代OS通常禁用此功能。
7.3 Hello的线性地址到物理地址的变换-页式管理
四级页表转换(x86-64):
-
- CR3寄存器定位PML4表(页表根)
- 虚拟地址高48位分为4级索引(9位/级)定位页表项(PTE)
- PTE存储物理页框号(PFN),与页内偏移(低12位)组合成物理地址。
页表项(PTE)标志:
Present位触发缺页中断,Dirty位标记写操作。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB加速:缓存近期VA→PA转换结果,减少页表遍历开销。
多级页表查询流程:

7.5 三级Cache支持下的物理内存访问
Cache层级:
L1 Cache(分指令/数据):~64KB,1-4周期延迟
L2 Cache(统一):~256KB,10周期延迟
L3 Cache(共享):~8MB,30-40周期延迟
缓存行(Cache Line):64字节为单位加载,hello的指令/数据若局部性强则命中率高。
7.6 hello进程fork时的内存映射
父进程(Shell)与子进程共享物理页,页表项标记为只读。
任一进程尝试写入时触发缺页中断,内核分配新物理页并复制数据。
资源继承:复制父进程的页表、文件描述符表,但独立维护。
7.7 hello进程execve时的内存映射
- 清除旧映射:释放原进程代码/数据页(保留文件描述符等)。
- 加载新映像:
映射hello的.text(R-X)、.data(RW-)到虚拟地址空间。
初始化.bss为零页,设置堆栈段(用户栈)。
- 动态链接:加载ld.so和libc.so,填充GOT/PLT。
7.8 缺页故障与缺页中断处理
触发场景:
访问未分配的页(Present=0)
COW写入触发页复制
用户访问内核页(权限校验失败)
处理流程:
-
- 保存寄存器状态,陷入内核
- 检查VMA(虚拟内存区域)合法性
- 分配物理页或从磁盘换入(文件映射/匿名页)
- 更新页表并返回用户态重试指令
7.9动态存储分配管理
printf调用malloc的场景:格式化缓冲区、I/O缓存等动态申请内存。
管理策略:
glibc的ptmalloc2:
小内存(<64KB)通过brk扩展堆空间,使用空闲链表管理。
大内存通过mmap匿名映射分配,减少碎片。
分配优化:
Fast bins:LIFO策略管理小内存块(<128KB)
Top chunk:未分割的剩余内存,用于新请求
7.10本章小结
hello的存储管理体现了现代OS的核心机制:
- 地址转换:段页式结合实现灵活高效的虚拟内存。
- 资源隔离:每进程独立的地址空间保障安全性。
- 性能优化:TLB/Cache减少内存访问延迟,COW提升fork效率。
- 动态扩展:缺页中断按需加载,malloc支持运行时内存分配。
这些机制共同支撑了hello进程从加载到执行的完整生命周期。
结论
hello程序的生命周期始于源代码的编译转换:预处理阶段展开宏和头文件生成hello.i,编译器将高级代码优化为汇编指令hello.s,汇编器翻译为机器码hello.o并保留重定位信息,链接器合并libc等库函数生成可执行文件hello,完成符号解析与地址绑定。当Shell通过fork创建子进程时,采用写时复制技术高效复制父进程上下文,execve则清除旧地址空间,按ELF程序头将hello的.text和.data段映射到虚拟内存,动态链接器ld.so延迟加载共享库并初始化PLT/GOT机制。进程执行时,CPU通过四级页表与TLB将虚拟地址转换为物理地址,配合三级Cache降低访存延迟,printf等I/O操作引发用户态到内核态的切换,由VFS层处理终端输出。时间片耗尽后,内核保存寄存器状态进行进程调度,缺页中断按需分配物理页或触发COW复制。最终进程终止时,系统回收内存和文件描述符等资源,父进程Shell通过waitpid获取退出状态。整个过程体现了计算机系统分层抽象(编译-链接-加载-执行)与硬件协同(MMU-Cache-流水线)的精妙设计,其中惰性策略(动态链接、COW、按需分页)在性能与资源利用率间取得平衡,而权限隔离(用户/内核态)和地址空间随机化等机制则保障了系统安全。
计算机系统的设计深刻体现了抽象与协同的艺术——从高级语言到机器指令的层层转换构建出简洁的编程接口,而底层硬件与操作系统的紧密协作则隐藏了复杂性。通过分析hello程序的生命周期,我惊叹于系统设计中惰性策略的普遍性:写时复制、按需分页、延迟绑定等技术将资源开销推迟到真正需要时才发生,这种"Just-in-Time"哲学在效率与资源利用率间实现了精妙平衡。硬件加速机制(如TLB、多级Cache、流水线)与软件策略(如页表管理、进程调度)的协同优化启示我们:垂直整合(如定制ISA扩展)比单一层优化更能释放性能潜力。现代系统的安全设计(用户/内核态隔离、ASLR)则展现了防御性深度的重要性——就像微内核架构通过最小化信任域来提升鲁棒性。未来的创新方向或许在于自适应系统:利用运行时 profiling 动态调整内存布局(如热页聚类)、基于负载特征切换调度策略(如实时/批处理混合模式),甚至引入轻量级硬件重构(FPGA动态逻辑单元)来适应不同工作负载。这种"感知-决策-优化"的闭环设计理念,或将推动计算机系统从静态分层架构进化为具有代谢能力的有机体,在保证抽象简洁性的同时实现前所未有的效率与灵活性。
附件
| 文件名 | 作用 |
| hello.c | 原C语言文件 |
| hello.i | 预处理产生文件 |
| hello.s | 编译产生文件 |
| hello.o | 汇编产生文件 |
| hello.out | 链接产生可执行文件 |
| hello | 链接产生可执行文件 |
| hello_o_elf.txt | 查看hello.o的elf格式对应的文本文件 |
| hello_o_asm.txt | 查看hello.o的反汇编对应的文本文件 |
| hello_elf.txt | 查看hello的elf格式对应的文本文件 |
| hello_asm.txt | 查看hello的反汇编对应的文本文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant等.深入理解计算机系统(原书第3版)[M]. 北京:机械工业出版社,2016.7:2.
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2301_81068830/article/details/147937225



