HIT-CSAPP2025大作业:程序人生-Hello’s P2P(2024111666-牛启正)

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P(Program→Process):hello.c(源码程序)经预处理、编译、汇编、链接生成可执行文件(静态程序),通过Shell的fork创建进程、execve加载程序,最终在CPU上执行(动态进程),期间依赖OS的进程管理、存储管理、IO管理提供支持。

020(Zero→Zero):程序从无(源码编写前)到有(可执行文件生成),运行时占用CPU、内存、IO资源,终止后资源被OS回收,回归"无"的状态,全程遵循计算机系统的资源分配与回收机制。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件:Intel i7-12700H、4GB缓存、20GB内存,提供计算、存储资源

软件:Ubuntu 22.04 LTS,开发环境

编译工具:GCC 11.4.0,进行预处理、编译、汇编、链接

调试工具 GDB、EDB负责进程调试、虚拟地址空间查看

分析工具 Readelf 2.38、Objdump 2.38 ELF进行文件格式分析、反汇编

其他工具 Flameshot、Typora、文档编辑、CSDN 发布

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c:原始源码,定义程序逻辑(接收4个参数,循环打印10次信息后等待输入)。

hello.i:预处理后文件,展开头文件(stdio.h/unistd.h/stdlib.h)、删除注释、替换宏。

hello.s:汇编语言文件,将C源码转换为 x86-64 汇编指令。

hello.o:可重定位目标文件,包含机器指令但未解决外部符号引用(如printf/sleep)。

hello:可执行文件,链接printf.o等后解决符号引用,包含完整的执行逻辑和内存布局。

gdb_log.txt:GDB 调试日志,记录hello的执行流程、虚拟地址空间分布。

1.4 本章小结

明确大作业的研究对象(hello.c)、研究主线(程序生命周期)、使用的环境工具及中间结果

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理是编译过程的第一步,由预处理器(cpp)执行,对源程序文本进行替换与清理,具体包括:

展开#include头文件(将头文件内容插入源码);

删除注释(//和/.../);

替换#define宏定义(无类型检查的文本替换);

处理条件编译(#if/#ifdef/#else等);

保留#line等调试指令,为后续编译提供行号信息。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i(-E 表示仅执行预处理,停止后续编译过程)

2.3 Hello的预处理结果解析

展开了stdio.h等头文件的函数声明、宏定义:hello.c只有24行,hello.i数千行;

注释被完全删除(如// 秒数=手机号%5消失);

无宏定义替换(预处理后无宏替换相关变化)。

解析:预处理不检查语法错误,仅做文本操作;头文件展开后,printf/sleep/exit/atoi等函数的声明被包含在hello.i中,为后续编译阶段的语法检查提供依据。

2.4 本章小结

预处理的核心是源文件文本层面的预处理,不涉及代码的语法分析或指令转换,其目的是为编译阶段提供完整、干净的源码文件,避免头文件未展开、注释干扰等问题。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译是将预处理后的.i文件(C 源码)转换为.s文件(汇编语言程序)的过程,核心作用是"语法分析→语义分析→中间代码生成→目标代码生成",具体作用如下:

检查语法错误;

将 C 语言的变量、表达式、控制结构等转换为汇编语言的指令、寄存器操作、栈操作;

不涉及外部符号解析。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s(-S 表示仅执行预处理和编译,停止汇编过程)。

3.3 Hello的编译结果解析

编译阶段的核心是将预处理后的 C 语言抽象逻辑(hello.i),严格遵循 x86 - 64 架构的硬件规则、函数调用约定,转换为汇编指令序列(hello.s)。以下对照参考文档中 "C 语言的数据与操作" 全分类,结合 hello.c 源码与 hello.s 汇编代码,逐类进行完整解析,覆盖所有涉及的语法元素与映射逻辑。

3.3.1 数据定义:常量、变量、数组/指针

1. 常量(字符串常量)

C 源码:提示字符串:用法: Hello 学号 姓名 手机号 秒数!\n

打印格式字符串:Hello %s %s %s\n。

汇编映射:

解析:

字符串常量属于只读数据,编译器将其归类到.rodata段(Read-Only Data),避免程序运行时被意外修改,同时便于操作系统内存权限管理(设为只读)。

.LC0、.LC1 是编译器自动生成的唯一标签,用于标识两个字符串的起始地址,后续汇编指令通过标签引用字符串。

字符串以 UTF - 8 编码存储,末尾隐含 \0 终止符)。

.align 8 表示按8字节对齐,使字符串起始地址为8的整数倍,符合x86 - 64架构的内存访问对齐要求。

2. 变量:局部变量、函数参数

(1)局部变量(int i)

C 源码:

int i;

汇编映射:

解析:

x86-64架构中,函数栈帧以 %rbp(栈基址寄存器)为基准,栈空间向低地址方向扩展(%rsp 为栈指针,初始指向栈顶)。

i为int 型(4字节),分配%rbp - 4至%rbp - 1共4字节空间;subq $32, %rsp 扩容32字节,除i外,还包含函数调用的参数临时存储区、栈对齐填充空间。

局部变量存储在栈中,函数退出时栈帧释放(leave指令),变量生命周期随函数结束而终止。

(2)函数参数(int argc, char *argv [])

C 源码:

int main(int argc, char *argv[])。

汇编映射:

解析:

遵循x86-64函数调用约定:前6个整型/指针参数依次通过 %rdi、%rsi、%rdx、%rcx、%r8、%r9 寄存器传递,超过6个参数存入栈。

argc是int型(32位),使用%edi(%rdi 寄存器的低32位)传递;argv是char**类型(64位指针),使用%rsi寄存器传递。

编译器将寄存器中的参数存入栈帧,目的是保护参数值(避免后续指令复用寄存器时覆盖参数),后续通过栈帧偏移(-20(%rbp)、-32(%rbp))稳定访问参数。

3. 数组 / 指针操作(argv [] 访问)

C 源码:

argv[1]、argv[2]、argv[3]、argv[4]。

汇编映射(以printf调用时访问argv [1]、argv [2]、argv [3]为例):

解析:

argv是指针数组(char *argv[]),数组中每个元素是指向字符串的64位指针(x86-64架构指针占8字节),因此argv[i]的地址计算公式为:argv基地址+i×8。

汇编中通过"基址+偏移"寻址模式实现数组访问:-32(%rbp)是argv基地址在栈帧中的存储位置,addq $8, %rax 计算目标元素的地址,(%rax) 是间接寻址,取出该元素指向的字符串首地址。

本质是将C语言的数组下标操作 argv[i] 转换为汇编的指针算术运算。

3.3.2 赋值操作:=、赋初值、自增

1. i = 0

C 源码:

for(i = 0; i < 10; i++) 中的初始化语句 i = 0。

汇编映射:

解析:

movl是32位数据移动指令,用于处理int型数据。

源操作数$0是立即数,目标操作数-4(%rbp)是局部变量i的栈帧地址。

指令功能是将立即数0写入i的内存空间,完成C语言的显式赋初值。

2. i++

C 源码:

for(i = 0; i < 10; i++) 中的更新语句 i++

汇编映射:

解析:

addl 是32位加法指令,源操作数$1是增量,目标操作数-4(%rbp)是i的地址。

指令功能是将i的值加1,等价于C语言的i++。由于i++在循环体执行后生效,汇编中该指令位于循环体末尾。

3.3.3 类型转换:显式转换(atoi 函数调用)

C源码:

atoi(argv[4])(将argv [4]对应的字符串转换为int型,为sleep函数提供参数)。

汇编映射:

解析:

C语言中argv[4]是char* 类型(字符串),而sleep函数的参数要求是int型,因此通过atoi函数进行显式类型转换。

汇编中无直接的类型转换指令,而是通过函数调用实现:atoi函数接收字符串地址,内部通过汇编指令解析ASCII码,将其转换为二进制整数。

遵循x86-64函数返回值约定:整型返回值存储在%eax 寄存器中,因此atoi的转换结果通过%eax传递给sleep函数。

3.3.4 关系操作:!=、<(≤)

1. 不等于(argc != 5)

C 源码:

if(argc != 5)(判断命令行参数个数是否为 5,若不是则输出错误信息)。

汇编映射:

解析:

关系操作的底层实现依赖"比较指令+条件跳转指令":

比较指令cmpl(32位比较)执行减法运算(目标操作数-源操作数,此处为argc-5),不保存结果,仅修改EFLAGS寄存器的标志位(零标志位ZF、进位标志位CF等)。

条件跳转指令je(Jump if Equal)根据ZF位判断是否跳转:若ZF=1(argc == 5),跳至.L2执行正常流程;若ZF=0(argc != 5),不跳转,执行后续错误处理代码。

2. 小于(i < 10)→ 编译器优化为 ≤(i ≤ 9)

C 源码:

for(i = 0; i < 10; i++) 中的条件判断 i < 10。

汇编映射:

解析:

编译器进行了优化:i < 10(i为非负整数)等价于i≤9,将比较对象从10改为9,减少一次循环判断(无需处理i=10的情况),提升执行效率。

比较指令cmpl $9, -4(%rbp) 执行i-9,修改EFLAGS标志位;条件跳转指令jle(Jump if Less or Equal)根据标志位判断:

若i=9,i-9=0→ZF=1→跳转(执行第10次循环);

若i<9,i-9为负数→SF=1、CF=1→SF=CF→跳转(执行循环);

若i=10,i-9=1→ZF=0、SF=0、CF=0→不跳转(退出循环)。

3.3.5 控制转移:if/else、for 循环

1. if/else 分支结构

C 源码:

if(argc != 5){

printf("用法: Hello 学号 姓名 手机号 秒数!\n");

exit(1);

}

// else 分支(argc == 5):执行 for 循环

汇编映射:

解析:

汇编中无else关键字,通过"标签+跳转指令"实现分支逻辑:

if分支(argc!=5):执行错误处理代码(puts+exit),程序终止;

else分支(argc==5):通过je .L2跳转至标签.L2,执行后续for循环。

编译器优化:将printf替换为puts,因错误字符串无格式符(% s/% d),puts 效率更高(自动添加换行符,无需解析格式字符串)。

分支结构的映射核心是"条件跳转分流",标签.L2作为分支入口,实现了高层逻辑的二元分流。

2. for 循环结构

C 源码:

for(i = 0; i < 10; i++){

printf("Hello %s %s %s\n", argv[1], argv[2], argv[3]);

sleep(atoi(argv[4]));

}

汇编映射:

解析:

for循环的拆解:C语言的for(初始化; 条件; 更新)被拆解为"初始化→条件判断→循环体→更新→条件判断"的闭环,通过标签(.L2/.L3/.L4)和跳转指令实现:

初始化.L2处movl $0, -4(%rbp)(i=0);

条件判断:jmp .L3跳至.L3,执行cmpl+jle(i≤ 9?);

循环体:满足条件则跳至.L4,执行printf和sleep;

更新:循环体末尾addl $1, -4(%rbp)(i++);

重复条件判断:更新后回到.L3,直至条件不满足退出循环。

3.3.6 函数操作:参数传递、函数调用、函数返回

1. 参数传递(地址传递、值传递)

x86-64函数调用约定规定:前6个整型/指针参数依次通过 %rdi、%rsi、%rdx、%rcx、%r8、%r9 寄存器传递,超过6个参数存入栈;整型返回值存入%eax 寄存器。具体解析如下:

(1)地址传递(printf、puts、atoi 函数)

printf 函数调用:

printf("Hello %s %s %s\n", argv[1], argv[2], argv[3])

汇编映射:

解析:printf的4个参数均为地址(字符串首地址),属于地址传递。leaq .LC1(%rip), %rax 采用RIP相对寻址(RIP是当前指令下一条地址),获取格式字符串.LC1的地址,避免绝对地址硬编码,提升代码可重定位性。

puts 函数调用:

puts("用法: Hello 学号 姓名 手机号 秒数!\n")(编译器优化自 printf)

汇编映射:

解析:puts 仅需1个参数(字符串地址),通过%rdi传递,与printf的地址传递逻辑一致。

atoi 函数调用:

atoi(argv[4])

汇编映射:

解析:atoi的参数是argv [4]指向的字符串地址,通过%rdi传递,属于地址传递;返回值(转换后的整数)存入%eax。

(2)值传递(sleep、exit 函数)

sleep 函数调用:

sleep(atoi(argv[4]))

汇编映射:

解析:sleep的参数是int型整数(秒数),通过%eax接收atoi的返回值,再存入%rdi传递,属于值传递(传递的是整数本身,而非地址)。

exit 函数调用:

exit(1)

汇编映射:

解析:exit的参数是退出码(int型),通过%rdi传递,属于值传递。

2. 函数调用(call 指令 + PLT 表)

所有函数调用均通过 call 指令实现,以call printf@PLT为例:

call 指令功能:

将当前指令的下一条地址(返回地址)压入栈中,供函数返回时使用;

跳至函数的入口地址(此处为 printf@PLT)。

PLT 表作用:printf、sleep 等属于C标准库函数,采用动态链接方式,@PLT 表示通过过程链接表(Procedure Linkage Table)跳转。PLT用于延迟绑定符号地址(程序运行时才解析标准库函数的实际地址),避免程序启动时解析所有外部函数。

3. 函数返回(return 0 + leave + ret 指令)

C 源码:

return 0;(main 函数返回 0,表示程序正常终止)。

汇编映射:

解析:

movl $0, %eax:遵循x86-64函数返回值约定,int型返回值存入%eax,0表示程序正常终止(操作系统通过该值判断程序执行状态)。

leave指令:用于释放函数栈帧,等价于两条指令:

movq %rbp, %rsp:将栈指针 %rsp 恢复到栈基址 %rbp,回收栈帧空间;

popq %rbp:弹出栈中保存的调用者栈基址,恢复调用者的栈帧环境。

ret指令:函数返回核心指令,功能是弹出栈中保存的返回地址(main函数的返回地址是 __libc_start_main 函数的后续执行地址),并跳至该地址,完成main 函数退出。

3.4 本章小结

编译过程实现了从 C 源码到汇编语言的转换,核心是语法语义分析与目标代码生成,其输出的.s文件包含了程序的全部指令逻辑,为后续汇编阶段提供输入。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

汇编是将.s文件(汇编语言程序)转换为.o文件(可重定位目标文件)的过程,核心作用是 "指令编码",即:

将汇编指令转换为机器码(二进制指令)。

生成ELF格式的可重定位文件,包含代码段(.text)、数据段(.data/.bss)、重定位表(.rel.text/.rel.data)等。

记录未解析的外部符号,等待链接阶段解决。

4.2 在Ubuntu下汇编的命令

命令:gcc -c hello.s -o hello.o(-c 表示仅执行预处理、编译、汇编,停止链接过程)。

4.3 可重定位目标elf格式

ELF 头部(ELF Header)是 ELF 文件的 "总目录",记录了文件的基本属性、架构信息、节表位置等关键元数据,为操作系统和工具提供解析依据。

节(Section)是 ELF 可重定位文件的核心组成部分,按功能分类存储代码、数据、重定位信息、符号表等。

节头表(Section Headers)是描述文件各个节(Sections)的重要元数据结构。将目标文件划分为不同功能的区域,如代码、数据、符号表,包含该节的名称、类型、地址、大小、对齐要求和各种标志等信息,告诉链接器哪些节包含可执行代码,指明哪些节包含只读数据,指定需要重定位的节。

符号表提供了程序中各个标识符的名称和属性,记录了对应的内存位置或相对位置,支持动态链接。图中的num即为符号的索引号,size表示占用的大小,type则是符号类型,bind是绑定的属性(local,global,weak等),name则是符号的名称。

重定位是链接阶段的核心任务,其本质是将可重定位文件中 "未确定的符号地址"替换为 "实际内存地址"。hello.o的重定位信息主要存储在.rela.text(代码段重定位表)和.rela.eh_frame(异常处理帧重定位表)中。

4.4 Hello.o的结果解析

0000000000000000 <main>:

0: f3 0f 1e fa endbr64

4: 55 push %rbp

5: 48 89 e5 mov %rsp,%rbp

8: 48 83 ec 20 sub $0x20,%rsp

c: 89 7d ec mov %edi,-0x14(%rbp)

f: 48 89 75 e0 mov %rsi,-0x20(%rbp)

13: 83 7d ec 05 cmpl $0x5,-0x14(%rbp)

17: 74 19 je 32 <main+0x32>

19: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 20 <main+0x20>

1c: R_X86_64_PC32 .rodata-0x4

20: 48 89 c7 mov %rax,%rdi

23: e8 00 00 00 00 call 28 <main+0x28>

24: R_X86_64_PLT32 puts-0x4

28: bf 01 00 00 00 mov $0x1,%edi

2d: e8 00 00 00 00 call 32 <main+0x32>

2e: R_X86_64_PLT32 exit-0x4

32: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)

39: eb 56 jmp 91 <main+0x91>

3b: 48 8b 45 e0 mov -0x20(%rbp),%rax

3f: 48 83 c0 18 add $0x18,%rax

43: 48 8b 08 mov (%rax),%rcx

46: 48 8b 45 e0 mov -0x20(%rbp),%rax

4a: 48 83 c0 10 add $0x10,%rax

4e: 48 8b 10 mov (%rax),%rdx

51: 48 8b 45 e0 mov -0x20(%rbp),%rax

55: 48 83 c0 08 add $0x8,%rax

59: 48 8b 00 mov (%rax),%rax

5c: 48 89 c6 mov %rax,%rsi

5f: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 66 <main+0x66>

62: R_X86_64_PC32 .rodata+0x2c

66: 48 89 c7 mov %rax,%rdi

69: b8 00 00 00 00 mov $0x0,%eax

6e: e8 00 00 00 00 call 73 <main+0x73>

6f: R_X86_64_PLT32 printf-0x4

73: 48 8b 45 e0 mov -0x20(%rbp),%rax

77: 48 83 c0 20 add $0x20,%rax

7b: 48 8b 00 mov (%rax),%rax

7e: 48 89 c7 mov %rax,%rdi

81: e8 00 00 00 00 call 86 <main+0x86>

82: R_X86_64_PLT32 atoi-0x4

86: 89 c7 mov %eax,%edi

88: e8 00 00 00 00 call 8d <main+0x8d>

89: R_X86_64_PLT32 sleep-0x4

8d: 83 45 fc 01 addl $0x1,-0x4(%rbp)

91: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)

95: 7e a4 jle 3b <main+0x3b>

97: e8 00 00 00 00 call 9c <main+0x9c>

98: R_X86_64_PLT32 getchar-0x4

9c: b8 00 00 00 00 mov $0x0,%eax

a1: c9 leave

a2: c3 ret

与hello.s文件的代码相比,在反汇编文件中,执行分支转移跳转控制时,不再使用类似.L2之类的标签表明要跳转的代码地址,而是直接使用对应的地址进行跳转,如jle 3b <main+0x3b>。并且call指令也不再直接使用函数名,而是用目标代码的起始地址表示。

机器语言是计算机硬件直接执行的二进制指令,由操作码(Opcode)和操作数(Operand)构成。汇编语言是机器语言的可读性符号化表示,通过助记符(如mov、add)和标签(如main:)替代二进制的操作码和地址。每条汇编指令对应一条机器指令(伪指令除外,如.section仅指导汇编器布局)。但映射关系并非完全对称,反汇编得到的汇编代码与.s文件中的代码并非完全相同。

4.5 本章小结

理解汇编的概念和作用后,进行汇编得到.o文件,然后对可重定位目标ELF格式进行分析。接着,使用objdump进行反汇编,并将其与.s文件进行比较,以更深入地理解机器语言与汇编语言之间的关系。

(第4章1分)

5 链接

5.1 链接的概念与作用

链接(Linking)是将多个目标文件(.o)和库文件合并生成最终可执行文件或共享库的过程,由链接器(Linker)完成(如ld)。核心作用是解决符号引用、分配内存地址并合并代码与数据,使程序能正确运行。

链接的作用:

将不同目标文件中相互引用的符号关联起来

合并所有目标文件的代码和数据段,并修正符号的绝对地址

5.2 在Ubuntu下链接的命令

Ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o hello /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o -lc /usr/lib/x86_64-linux-gnu/crtn.o

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

ELF头:文件的Type发生了变化,从REL变成了EXEC(Executable file可执行文件),节头部数量也发生了变化,变为了27个。

程序头: 描述了可执行文件的连续片与连续的内存段之间的映射关系,主要包括所指向段的类型、其在ELF文件中的偏移地址、物理地址、映射到内存的虚拟地址、段的读写权限、对齐方式等。

节头表

符号表:经过链接之后符号表的符号数量变多了,因为链接之后引入了其他库函数的符号

可重定位段

5.4 hello的虚拟地址空间

用gdb查看hello的文件详细信息

对比分析:与readelf -l hello的段地址一致,说明可执行文件的段直接映射到进程的虚拟地址空间,OS 通过分页机制将虚拟地址转换为物理地址。

5.5 链接的重定位过程分析

反汇编查看hello可执行文件的反汇编条目

分析hello与hello.o的不同:

(1).在hello.o中,地址为相对偏移地址;在hello中,地址为可以由CPU直接访问的虚拟内存地址;

(2).hello的反汇编文件比hello.o的反汇编文件多了_init,.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt,_start,_dl_relocate_static_pie,__libc_csu_init,__libc_csu_fini,_fini等节和需要用到的库函数;

(3).hello.o将lea后的操作数置为0,并添加重定位条目。

重定位的过程:

首先,链接器会收集所有目标文件的符号表,解析跨文件的符号依赖关系,然后链接器会合并所有目标文件的代码段和数据段,按照程序的内存布局分配绝对地址,并修正指令中的临时占位地址

5.6 hello的执行流程

主要执行的函数及地址:

_start:0x4010f0

main:0x40112d

Printf:0x4010a0

Exit:0x4010d0

5.7 Hello的动态链接分析

动态链接依赖:执行ldd hello,查看依赖的动态库

用gdb调试动态链接过程

链接前,main和puts地址未定义;链接后,main为0x555555555100,链接前,仅目标文件节存在;链接后,共享库(如libc)加载到内存,地址范围为0x7ffff7dc8000起,puts为0x7ffff7e32900。

5.8 本章小结

在这章中,我们了解了链接的功能,通过对hello.o进行链接获得hello文件,并对其进行了分析。通过将hello文件与hello.o的反汇编代码进行比较,更深入地理解了重定位的过程。

(第5章1分)

6 hello进程管理

6.1 进程的概念与作用

进程是 "执行中的程序",是OS资源分配和调度的基本单位。

核心作用:

隔离资源:每个进程拥有独立的虚拟地址空间,避免不同程序的内存访问冲突。

并发执行:OS通过时间片轮转调度,使多个进程交替执行,提高CPU利用率。

简化编程:进程提供了"独立执行流"的抽象,程序员无需关心CPU调度细节。

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

Shell(bash)是用户与OS的接口,核心作用是"命令解释器"。

处理流程:

读取命令:从标准输入(键盘)读取用户输入的命令。

解析命令:识别命令名(./hello)和参数(后续4个参数)。

创建进程:调用fork创建子进程。

加载程序:子进程中调用execve加载hello可执行文件,替换子进程的代码和数据。

等待终止:父进程(bash)调用waitpid等待子进程终止,回收资源。

输出提示符:子进程终止后,bash 输出提示符($),等待下一条命令。

6.3 Hello的fork进程创建过程

fork的核心是 "复制进程上下文":

1.用户在 bash 中输入./hello ...,bash 调用fork系统调用。

2.OS 为子进程分配 PID、PCB(进程控制块),复制父进程的虚拟地址空间(代码段、数据段、栈、堆)、寄存器状态等。

3.复制完成后,父进程和子进程同时执行,但子进程的fork返回0,父进程的fork返回子进程PID。

4.子进程后续执行execve,父进程执行waitpid。

6.4 Hello的execve过程

execve的核心是 "替换进程镜像":

1.子进程调用execve("./hello", argv, envp),其中argv是命令参数数组,envp是环境变量数组。

2.OS 验证hello的可执行权限,读取其ELF头部,分析程序头表。

3.销毁子进程原有的虚拟地址空间(代码段、数据段等),为hello创建新的虚拟地址空间。

4.将hello的LOAD段加载到对应虚拟地址。

5.设置程序计数器(PC)为hello的入口地址(_start),开始执行hello的代码。

6.5 Hello的进程执行

结合进程上下文、时间片、用户态 / 核心态转换:

1.进程上下文:包含寄存器状态(如%rip程序计数器、%rsp栈指针)、内存映像(虚拟地址空间)、PCB 信息。

2.时间片调度:OS 的调度器为hello进程分配时间片),进程在用户态执行main函数的循环(打印、sleep)。

3.状态转换:

执行printf/slee时,进程调用库函数,触发系统调用(sys_write/sys_sleep),CPU 从用户态切换到核心态,OS 执行系统调用服务程序。

系统调用完成后,CPU从核心态切换回用户态,进程继续执行。

4.并发执行:hello进程与其他进程交替占用CPU,OS通过保存/恢复进程上下文实现切换。

6.6 hello的异常与信号处理

以下格式自行编排,编辑时删除

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

异常与信号分类:

异常:

中断:来自I/O设备的信号,异步,总是返回到下一条指令

陷阱:有意的异常,同步,总是返回到下一条指令

故障:潜在可恢复的错误,同步,可能返回到当前指令

终止:不可恢复的错误,同步,不会返回

信号:

Ctrl+C 触发 SIGINT 信号处理,说明:SIGINT默认处理方式是终止进程,OS回收进程资源。

Ctrl+Z 触发 SIGTSTP 信号处理,说明:SIGTSTP默认处理方式是暂停进程,fg命令可恢复进程执行。

kill 命令发送 SIGKILL 信号处理,说明:SIGKILL不可捕获,直接终止进程。

6.7本章小结

hello的进程生命周期由 Shell 的fork创建、execve加载、OS 调度执行、信号处理终止组成,OS 通过进程管理机制实现了进程的创建、调度、资源隔离与回收,保证了程序的并发、安全执行。

(第6章 2 分)

7 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序代码中使用的地址(如argv[1]的地址,是相对于段的偏移量)。

线性地址:逻辑地址经段式管理转换后的地址。

虚拟地址:进程看到的地址(即线性地址,x86-64 中虚拟地址为 64 位,实际使用 48 位)。

物理地址:内存硬件的实际地址(虚拟地址经页式管理转换后得到)。

hello的地址映射:hello的代码段虚拟地址为0x401000,执行时 OS 将其映射到物理内存的某个地址(如0x10000000),进程无需关心物理地址,仅操作虚拟地址。

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

在Intel x86架构中,逻辑地址到线性地址的转换是通过段式管理机制实现的,这一机制是处理器内存寻址的基础层。当程序(如hello)访问内存时,指令中使用的地址(如mov eax, [0x8048000])首先是一个逻辑地址,由两部分组成:段选择子(Segment Selector)和段内偏移量(Offset)。段选择子存储在段寄存器(如CS代码段、DS数据段)中,它本质上是一个索引,指向全局描述符表(GDT)或局部描述符表(LDT)中的条目。

处理器根据段选择子从GDT/LDT中加载对应的段描述符(Segment Descriptor),描述符中定义了段的基地址(Base Address)、段界限(Limit)和访问权限(如可读/可写)。逻辑地址中的偏移量与段基地址相加,生成线性地址。例如,若代码段基地址为0x08048000,指令中的偏移量为0x200,则线性地址为0x08048200。这一转换过程由硬件自动完成,对程序透明。段界限和权限检查在此阶段进行,若偏移量超出段界限或权限不符,会触发通用保护异常(GPF)

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

Intel线性地址到物理地址的变换基于页式管理机制,依赖于多级页表结构来完成地址映射。线性地址由CPU生成,通常是虚拟地址空间的一部分,分为页目录指针、页目录、页表和偏移量等部分。变换过程从线性地址开始,操作系统通过CR3寄存器指向当前进程的页目录基址,结合页表项中的物理页框号逐步解析。

变换首先将线性地址分为两部分:页号和页内偏移。页号通过页表层次结构查找对应的物理页框号,而页内偏移保持不变。Intel x86-64架构采用四级页表(PML4、PDP、PD、PT),每级页表由8字节的页表项组成,包含物理地址、权限位和标志位。线性地址的高12位作为索引,分别指向PML4、PDP、PD和PT的特定条目,低12位作为页内偏移。

在查找过程中,CPU从PML4表开始,根据线性地址的最高位索引(9位)找到PML4条目。若条目有效(P位为1),提取其指向的PDP表基址,再用次高位索引(9位)找到PDP条目,依此类推直到PT条目。PT条目提供物理页框号,结合线性地址的页内偏移,生成最终的物理地址。如果任意级条目无效,会触发页错误异常,操作系统通过缺页中断加载页面或分配内存。

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

虚拟地址首先被分为几个部分:高9位用于PML4索引,次9位用于PDP索引,再次9位用于PD索引,接下来的9位用于PT索引,最后12位为页内偏移。四级页表结构从CR3寄存器开始,CR3存储PML4表的物理基址。CPU根据虚拟地址的PML4索引(位47-39)访问PML4表,获取PML4条目(8字节),其中包含PDP表的物理基址及权限位(如P位表示有效)。若条目有效,CPU继续使用PDP索引(位38-30)访问PDP表,获取PD表基址;再用PD索引(位29-21)访问PD表,获取PT表基址;最后用PT索引(位20-12)访问PT表,获取物理页框号。物理页框号(通常20位或更多,视页面大小)与虚拟地址的页内偏移(位11-0)拼接,形成最终的物理地址。如果任何一级页表条目无效(P位为0),会触发页错误异常,操作系统介入处理,如分配页面或加载数据。

TLB在这一过程中起到加速作用。TLB是一个硬件缓存,存储最近使用过的虚拟地址到物理地址的映射条目,每个条目记录虚拟页号、物理页框号及相关元数据(如权限、页面大小)。当CPU需要转换虚拟地址时,首先查询TLB。若TLB命中(即找到匹配的虚拟页号),直接返回物理页框号,无需访问四级页表,显著减少内存访问开销。TLB条目通常分为4KB、2MB和1GB页面类型,支持不同粒度的映射。大页面(如2MB由PD条目直接提供,或1GB由PDP条目提供)可减少页表层次,降低TLB缓存压力。TLB未命中时,CPU执行完整的四级页表遍历(页表漫游),结果存入TLB以备后续使用。

TLB的管理由硬件和操作系统共同完成。上下文切换(如进程切换)或页表更新(如invlpg指令)可能导致TLB条目失效,触发TLB刷新。现代CPU支持PCID(Process Context Identifier),为每个进程分配唯一标识,减少上下文切换时的TLB清空开销。四级页表和TLB结合,支持大地址空间(2^48字节)和高效映射,同时允许操作系统实现内存保护和虚拟化。

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

当 hello 程序中的一条指令需要读取内存数据(例如加载全局变量)时,CPU 首先生成一个虚拟地址。通过 MMU(内存管理单元)的页表查询,该地址被转换为物理地址。此时,CPU 将物理地址提交给缓存子系统,开始层级化的数据查找。

物理地址首先到达 L1 Cache(通常分为指令缓存 L1-I 和数据缓存 L1-D)。L1 是速度最快、容量最小(约 32KB~64KB)的缓存,采用静态随机访问存储器(SRAM)实现。若数据在 L1 中找到(缓存命中),则直接返回给 CPU,延迟仅需 1~3 个时钟周期。若未命中(缓存未命中),则请求传递至 L2 Cache。

L2 Cache 容量较大(约 256KB~1MB),作为 L1 的备用池,进一步减少访问主存的频率。其延迟通常为 10~20 个周期。若 L2 仍未命中,则继续向 L3 Cache 查询。L3 是共享缓存(多核 CPU 中各核心共用),容量可达数 MB 到数十 MB。它通过更复杂的替换算法(如伪LRU)存储高频访问数据。若 L3 依然未命中,则需访问物理内存,此时延迟骤增至 100~300 个周期。

当三级缓存均未命中时,CPU 通过内存控制器(如 Intel 的 IMC)向物理内存(DRAM)发起请求。物理内存的访问以缓存行(Cache Line,通常 64 字节)为单位,读取的数据会按需回填到各级缓存。数据从内存加载后,首先存入 L3,再逐级填充 L2 和 L1。

7.6 hello进程fork时的内存映射

fork时的内存映射采用 "写时复制" 策略:

fork创建子进程时,OS不为子进程分配新的物理内存,而是让父子进程共享同一物理内存页框。

父子进程的页表项均设置"写保护"权限。

若父进程或子进程修改共享内存(如修改全局变量),CPU 触发页故障(Page Fault),OS为修改方分配新的物理内存页框,复制原数据,更新页表项,解除写保护,后续修改仅影响该进程的内存。

对hello的影响:fork后子进程立即执行execve,替换虚拟地址空间,因此 COW 策略避免了不必要的内存复制,提高了fork效率。

7.7 hello进程execve时的内存映射

execve时的内存映射核心是"构建新的虚拟地址空间":

销毁子进程原有的虚拟地址空间(父子进程共享的页框被释放或归父进程所有)。

解析hello的ELF程序头表,为LOAD段分配虚拟地址。

为每个LOAD段创建页表项,将虚拟地址映射到物理内存(若段内容在磁盘上,触发缺页中断,将内容载入内存)。

分配栈空间和堆空间,设置栈指针(%rsp)指向栈顶,程序计数器(%rip)指向_start。

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

缺页故障是"虚拟地址对应的页未在物理内存中"时触发的异常:

hello中的缺页场景:

execve加载hello时,.text段和.data段的内容存储在磁盘上,虚拟地址已分配但未映射到物理内存。

CPU 执行_start指令(虚拟地址0x401000),发现该页未在内存,触发缺页中断(int 0x0E)。

缺页中断处理流程:

OS 保存当前进程上下文,进入核心态。

检查虚拟地址是否合法(如是否在进程的虚拟地址空间内)。

若合法,分配物理页框,将磁盘上的.text段内容载入该页框。

更新页表项,建立虚拟地址与物理地址的映射。

恢复进程上下文,返回用户态,重新执行触发缺页的指令。

7.9动态存储分配管理

以下格式自行编排,编辑时删除

Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)

7.10本章小结

hello的存储管理涉及地址转换(段式 + 页式)、缓存加速(TLB+Cache)、内存映射(fork 的 COW+execve 的地址空间构建)、缺页处理等机制,OS通过这些机制实现了虚拟地址到物理地址的高效映射,缓解了 CPU 与内存的速度差距,优化了内存资源的利用。

(第7章 2分)

8 hello的IO管理

8.1 Linux的IO设备管理方法

以下格式自行编排,编辑时删除

设备的模型化:文件

设备管理:unix io接口

文件的类型:

普通文件(regular file):包含任意数据的文件。

目录(directory)(文件夹):包含一组链接的文件,每个链接都将一个文件名映射到一个文件。

套接字(socket):用来与另一个进程进行跨网络通信的文件

命名通道

符号链接

字符和块设备

设备管理:unix io接口

打开和关闭文件

读取和写入文件

改变当前文件的位置

8.2 简述Unix IO接口及其函数

核心 Unix IO 函数:

Open:打开文件/设备,返回文件描述符:printf隐含打开标准输出(fd=1)

Read:从fd读取数据到缓冲区:getchar调用read(0, buf, 1)读取键盘输入

Write:将缓冲区数据写入fd:printf最终调用write(1, buf, len)输出到显示器

Close:关闭fd,释放资源:程序终止时自动关闭所有fd

Ioctl:设备控制:无直接使用,驱动程序内部使用

8.3 printf的实现分析

https://www.cnblogs.com/pianist/p/3315801.html

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

hello的 IO 操作(printf输出、getchar输入)依赖 Linux 的 "文件抽象" 和 Unix IO 接口,底层通过系统调用、设备驱动、中断处理实现硬件交互,OS 屏蔽了硬件细节,为用户程序提供了简洁、统一的 IO 操作接口。

(第8章 1分)

结论

总结 hello 的生命周期过程:

源码编写:hello.c定义程序逻辑,接收4个参数,循环打印10次信息后等待输入。

预处理:展开头文件、删除注释,生成hello.i。

编译:语法语义分析,将 C 源码转换为汇编指令,生成hello.s。

汇编:将汇编指令编码为机器码,生成可重定位目标文件hello.o。

链接:符号解析与重定位,合并库文件,生成可执行文件hello。

进程创建:Shell 调用fork创建子进程,execve加载hello,替换进程镜像。

进程执行:OS 调度进程,通过存储管理实现地址转换与缓存加速,通过 IO 管理实现输入输出。

进程终止:用户输入或信号触发程序终止,OS 回收进程资源。

(结论 0 分,缺失- 1 分)

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c:原始源码文件,定义程序逻辑

hello.i:预处理后的 C 文件,展开头文件、删除注释

hello.s:编译生成的汇编语言文件

hello.o:汇编生成的可重定位目标文件

Hello:链接生成的可执行目标文件

gdb_log.txt:GDB调试日志,记录执行流程与地址空间

edb_screenshot.png:EDB动态链接调试截图

csdn_url.png:CSDN 发布截图(含文章地址)

command_output.txt :预处理、编译、汇编、链接等命令的输出结果

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

  1. Randal E. Bryant, David R. O'Hallaron.深入理解计算机系统(第 3 版)[M]. 龚奕利,贺莲译。北京:机械工业出版社,2016:1-500.
  2. Linux man-pages project. Linux Programmer's Manual[EB/OL]. https://man7.org/linux/man-pages/, 2024.
  3. Pianist. printf的实现原理[EB/OL]. https://www.cnblogs.com/pianist/p/3315801.html, 2013-08-15.
  4. 操作系统实验课. x86-64 汇编与 ELF 格式分析[EB/OL]. https://ysyx.oscc.cc/slides/hello-x86.html, 2025.

(参考文献0分,缺失 -1分)

相关推荐
real_ben_ladeng2 小时前
程序人生—Hello’s P2P 2dc736403375808d93f9c97fc816f2f8
c语言·汇编·硬件架构
智者知已应修善业3 小时前
【排列顺序判断是否一次交换能得到升序】2025-1-28
c语言·c++·经验分享·笔记·算法
m0_531237174 小时前
C语言-分支与循环语句练习
c语言·开发语言
季明洵4 小时前
数据在内存中的存储
数据结构·算法·c
寒秋花开曾相惜4 小时前
(学习笔记)2.2 整数表示(2.2.3 补码编码)
c语言·开发语言·笔记·学习
我命由我123455 小时前
Visual Studio - Visual Studio 修改项目的字符集
c语言·开发语言·c++·ide·学习·visualstudio·visual studio
硬汉嵌入式6 小时前
斯坦福大学计算机科学早期发布的简明C语言教程《Essential C》
c语言·开发语言
聆风吟º6 小时前
【C标准库】理解C语言中的abs函数:计算整数的绝对值
c语言·abs·库函数·绝对值
jyhappy1236 小时前
深入理解 STM32 的 GPIO — 从零开始点亮第一颗 LED
c语言·stm32·单片机·嵌入式硬件·mcu