计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能
学 号 2023111646
班 级 23wlR15
学 生 王浩然
指 导 教 师 吴锐
计算机科学与技术学院
2025年5月
本论文以“程序人生-Hello’s P2P”为主题,深入剖析了Hello程序从源代码到可执行文件,再到运行过程中的计算机系统层面的全过程。通过详细分析预处理、编译、汇编、链接等环节,揭示了程序构建的底层逻辑。同时,对Hello进程的创建、执行、存储管理、IO操作等运行时行为进行了全面探究,结合具体命令和工具,展示了计算机系统各层次的协同工作。研究不仅加深了对计算机系统原理的理解,还为程序开发与优化提供了实践指导,具有重要的理论与实际意义。
、
关键词:编译原理;进程管理; 存储管理;IO管理
目 录
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简介
Hello的“自白”生动描绘了一个简单程序在计算机系统中的完整生命周期。从源代码(Program)到进程(Process),再从运行(On)到结束归零(Zero),整个过程涉及编译系统、操作系统、硬件体系结构的深度协作。我们下面详细解析Hello的P2P(Program to Process)和O2O(Zero to Zero)过程,并扩展其背后的计算机系统原理。
1.1.1. P2P(From Program to Process)—— Hello的诞生
(1)编写源代码(Program)
用户用C语言编写hello.c,例如:

图1 hello.c示例(非给定)
此时,Hello只是一个静态文本文件,存储在磁盘上,尚未具备可执行能力。
(2)预处理(Preprocessing)
预处理器(cpp) 处理#include、宏定义(#define)等,生成.i文件:
![]()
图2 预处理指令示例
- 展开后的代码可能包含数千行(因为stdio.h被递归展开)。
(3)编译(Compilation)
- 编译器(gcc) 将预处理后的代码翻译成汇编语言(.s文件):
![]()
图3 编译指令示例
- 例如,printf可能被编译为call puts(编译器优化)。
(4)汇编(Assembly)
- 汇编器(as) 将.s文件转换为机器码(.o文件):
![]()
图4 汇编指令示例
- 此时,Hello已经是可重定位目标文件(Relocatable Object File),但尚未链接库函数(如printf)。
(5)链接(Linking)
- 链接器(ld) 负责:
合并多个.o文件(如hello.o + libc.o);解析符号引用(如printf的真实地址)
生成可执行文件(a.out或hello):
![]()
图4 链接指令示例
- 此时,Hello已经是一个完整的可执行程序,但仍未运行。
(6)进程创建(Process)
- 用户在Shell(如Bash)中运行:
![]()
图5 运行指令示例
- 操作系统介入:
- fork():Shell创建一个子进程(复制自身)。
- execve():加载hello可执行文件,替换子进程的地址空间。
- 进程管理:OS分配PID、内存、文件描述符等资源。
- Hello正式成为一个进程(Process),进入运行状态!
2. O2O(From Zero to Zero)—— Hello的运行与消亡
(1)进程运行(On)
- CPU执行:
取指-译码-执行:CPU从内存读取指令,解析并执行。
流水线优化:现代CPU采用超标量、乱序执行等技术加速。
- 内存管理:
虚拟内存(VA → PA):MMU(内存管理单元)通过页表转换地址。
TLB加速:缓存最近使用的页表条目,减少MMU查询时间。
多级Cache(L1/L2/L3):减少访问主存的延迟。
- IO交互:
printf最终通过系统调用(write) 向屏幕输出字符。
OS管理显卡、键盘等外设,确保数据正确传输。
(2)进程终止(Zero)
- Hello执行完毕(return 0),OS开始回收资源:
- 释放内存:清除页表、TLB条目、Cache数据。
- 关闭文件描述符:如标准输入/输出(stdin/stdout)。
- 进程表清理:从进程列表(ps)中移除该进程。
- 父进程(Shell)回收:通过wait()获取子进程退出状态。
- Hello彻底消失,回归“Zero”,仿佛从未存在过。
1.2 环境与工具
1.2.1 硬件环境
联想拯救者Y9000P处理器:Intel®Core™i9-14900HX
1.2.2 软件环境
Windows11 64位操作系统
1.2.3 开发工具
GCC、Visual Studio 、Ubuntu、Vmware

图6 软硬件环境
1.3 中间结果
| 文件名 | 介绍 |
| hello.c | hello的源程序 |
| hello.i | 源程序预处理之后的ASCII文件 |
| hello.s | 源程序经过编译后的汇编文件 |
| hello.o | 汇编文件汇编后的可重定位目标文件 |
| hello | 可重定向目标文件链接后的可执行目标文件 |
| hello.elf | 可重定位目标文件的ELF文件,便于查看ELF格式 |
| hello_re.elf | 可执行目标文件的ELF文件,便于查看ELF格式 |
| hello.asm | 可执行目标文件反汇编之后的汇编文件 |
| hello_o.asm | 可重定向目标文件反汇编之后的汇编文件 |
1.4 本章小结
本章介绍了Hello World程序的背景、开发环境(Ubuntu 22.04、GCC工具链)和中间生成文件(.i、.s、.o、可执行文件),为后续分析奠定了基础。通过P2P(Program to Process)和020(Zero to Zero)的概念,概述了程序从代码到进程的生命周期。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
1.预处理的概念:
预处理是C语言编译过程中的第一个阶段,由预处理器(cpp)执行。它处理源代码中以#开头的指令,在编译器实际编译代码之前对源代码进行文本级别的转换和处理。
2.预处理的作用:
(1)头文件包含:将#include指令替换为对应头文件的内容
(2)宏展开:处理#define定义的宏,进行文本替换
(3)条件编译:根据#ifdef、#ifndef、#if等条件编译指令选择性地包含代码
(4)删除注释:移除源代码中的所有注释
(5)添加行号和文件名标识:为编译器提供调试信息
(6)处理特殊指令:如#pragma等编译器特定指令
2.2在Ubuntu下预处理的命令

图7 Ubuntu下预处理的命令
注:-E选项让gcc在预处理结束后停止编译过程,-o将预处理结果保存到hello.i中
2.3 Hello的预处理结果解析
使用cat指令查看hello.i代码如图8

得到hello.i部分代码如图9,发现该文件以文本文件的形式保存。打开后,发现代码量相比hello.c大大增加,这是因为头文件展开,预处理将#include <stdio.h>和#include <unistd.h>指令替换为对应头文件的内容,但是可以发现原先的注释已经消失:
- stdio.h:包含了标准I/O函数的声明(如printf、fprintf等)和类型定义
- unistd.h:包含了POSIX操作系统API(如sleep、getpid等)
头文件展开导致文件体积大幅增加,因为每个头文件又可能包含其他头文件,形成复杂的依赖关系,所以出现的一些未在hello.c中引用的文件,比如features.h,应该是在stdio.h或者stdlib.h这样已引用的文件中被嵌套引用

图9 hello.i中头文件包含部分代码
预处理添加了行号和源文件信息标记,这些标记帮助编译器在后续阶段定位错误和警告的位置

预处理后的文件末尾保留了原始代码,如图11:

图11 hello.i中文件末尾部分代码
预处理结果包含大量系统相关的类型定义:如基本类型(size_t, ssize_t, off_t等),文件相关类型(FILE, fpos_t等),系统调用相关类型(pid_t, uid_t, gid_t等)

因为我们的hello.c源代码中没有显式的宏定义和条件编译代码,所以在hello.i代码中并没有很好的体现出预处理的宏展开和根据#ifdef、#ifndef、#if等条件编译指令选择性地包含代码等作用,同时因为hello.i代码过长,这里子展示了部分代码
2.4 本章小结
本章通过hello.c程序的学习,深入理解了C语言预处理阶段的工作机制和重要性。预处理作为编译过程的第一步,完成了源代码的准备工作,包括头文件包含、宏展开、条件编译等关键操作。通过实际操作生成预处理文件,可以直观地看到预处理器如何将多个文件合并为一个翻译单元,为后续的编译阶段做好准备。掌握预处理的概念和操作对于理解C语言的编译过程和调试程序具有重要意义
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
1.编译的概念
编译是将预处理后的中间代码转换为汇编代码的关键过程。这一阶段是程序构建过程中的核心环节,负责把高级语言描述的算法和逻辑转化为机器能够理解和执行的底层指令。编译器在这个阶段对代码进行多层次的解析和转换,既要保证语义的准确性,又要考虑执行效率的优化。
2.编译的作用
在编译过程中,编译器首先会对预处理后的代码进行词法分析和语法分析,构建出抽象的语法树。这一步骤确保程序的结构符合C语言的语法规范,同时会检查变量声明、类型匹配等基本语义错误。接着编译器会进行语义分析,深入检查类型兼容性、函数调用匹配等更复杂的语义规则,确保程序的逻辑正确性。
编译阶段还会进行各种优化处理,包括常量传播、死代码消除、循环优化等。这些优化措施可以显著提升程序的执行效率,减少不必要的计算和内存访问。优化后的中间表示最终会被转换为目标平台的汇编代码,这个过程中编译器需要处理各种数据类型的内存布局、函数调用的参数传递约定、控制流的转换等底层细节。
编译过程生成的汇编代码是与特定硬件架构相关的低级表示,但仍然保持一定的可读性。编译阶段不涉及内存布局,仅生成低级指令。汇编代码仍为文本文件。这个代码包含了实际的机器指令、寄存器分配方案、内存访问模式等关键信息。通过编译阶段,高级语言的抽象概念被映射到具体的机器操作,为后续的汇编和链接阶段做好准备,最终生成可执行的程序。
3.2 在Ubuntu下编译的命令
![]()
3.3 Hello的编译结果解析
使用cat hello.s指令查看完整的hello.s代码如下

图14 hello.s完整代码图片
3.3.1数据
1.常量
代码中的中文字符串和格式字符串被存储在.rodata段(只读数据段), 中文字符串以UTF-8编码形式存储,编译器自动处理了多字节字符的编码。在hello.c文件中,有两个字符串常量,他们分别被保存在.LC0和.LC1中


图15 字符串常量对应在hello.c和hello.s中对应代码
(2) 数值常量:

图16 数值常量对应在hello.c中实现
在汇编语言中数值常量(int)以立即数形式保存:
cmpl $5, -20(%rbp) # argc != 5
movl $0, -4(%rbp) # i = 0
movl $1, %edi # exit(1)
cmpl $9, -4(%rbp) # i <= 9
2.变量
(1)int整型局部变量:
在hello.c中定义了局部变量i
![]()
局部变量i(int类型)通过栈空间分配:
movl $0, -4(%rbp)
- 将常量0存储到栈地址-4(%rbp)处(对应i的初始化)。
- movl表示32位操作(int类型占4字节)。
- -4(%rbp)说明i是局部变量,位于栈帧中(通过%rbp基址寻址)。

同时在循环体中存在对i的修改:
- addl $1, -4(%rbp)
- 将栈中-4(%rbp)处的值(即i)加1。
- 对应C代码中的i++。

(2)参数:
(2.1)argc(int类型):
对应C代码:
int main(int argc, char *argv[])
对应汇编实现:
movl %edi, -20(%rbp) # 保存argc到栈帧-20偏移处
cmpl $5, -20(%rbp) # 比较 argc != 5
je .L2 # 条件跳转
传递方式:通过寄存器 %edi。
存储位置:栈帧中 -20(%rbp)(4字节)。
(2.2)argv ( char** 类型的指针(指向字符串数组的指针))
在汇编中通过寄存器 + 内存偏移 的方式访问。它在代码中的关键操作如下:
- 保存 argv 到栈帧

图19 保存 argv 到栈帧指令
- %rsi: %rsi 存储第二个参数(即 argv)。
- -32(%rbp):栈帧中的一个 8 字节位置,用于保存 argv 的副本。
- 访问 argv 数组元素
movq -32(%rbp), %rax # 从栈加载 argv 的基地址到 %rax
addq $8, %rax # 计算 argv[1] 的地址(偏移 8 字节)
movq (%rax), %rsi # 加载 argv[1] 的值(即第 2 个命令行参数)
- addq $8, %rax:
- argv 是 char*[] 类型,每个元素占 8 字节(64 位地址)。
- argv[0] 在 (%rax),argv[1] 在 8(%rax),依此类推。
- movq (%rax), %rsi:
解引用指针,获取 argv[1] 的实际字符串地址。
- 访问 argv[4]
movq -32(%rbp), %rax # 加载 argv 基地址
addq $32, %rax # 计算 argv[4] 的地址(偏移 32 字节)
movq (%rax), %rdi # 加载 argv[4] 的值(第 5 个参数)
- addq $32, %rax:
argv[4] 的偏移量为 4 * 8 = 32 字节(因为每个指针占 8 字节)。
3.3.2操作
表2 hello.c和hello.s中赋值操作语句
| 操作类型 | C代码中操作语句 | 对应在hello.s中的汇编指令及解释 |
| 赋值操作 | i = 0; | movl $0, -4(%rbp) # 立即数0赋给i |
| i++; | addl $1, -4(%rbp) # i自增1 | |
| 类型转换 | sleep(atoi(argv[4])); # 字符串→int→unsigned int | call atoi@PLT # 显式调用atoi movl %eax, %edi # int→unsigned int(通过寄存器传递) |
| 算术操作 (加法) | i++; | addl $1, -4(%rbp) # i += 1 |
| 关系操作 (比较) | argc != 5, i < 10, | cmpl $5, -20(%rbp) # argc != 5 cmpl $9, -4(%rbp) # i <= 9(等价于i < 10) |
| 数组访问 | argv[1], argv[2], argv[3] | movq -32(%rbp), %rax # argv基地址 addq $8, %rax # argv[1]偏移 movq (%rax), %rsi # 解引用 |
| 指针解引用 | *argv[4](通过atoi隐式解引用) | movq 32(%rax), %rdi # 加载argv[4]的字符串地址 call atoi@PLT # 解引用并转换 |
3.3.3 控制结构
代码中的控制转移共有两处:一处是判断argc和5是否相等;一处是将i和9比较。
如果argc和4相等,则跳转到.L2,否则执行后面的语句
如果i<=9(即i<10),则跳转到.L4,否则执行后面的语句
表3 hello.c和hello.s中控制结构语句
| 控制结构 | C代码中操作语句 | 对应在hello.s中的汇编指令及解释 |
| if条件 | if(argc != 5) | cmpl $5, -20(%rbp) # 比较argc和5 je .L2 # 相等则跳转 |
| for循环 | for(i=0; i<10; i++) | movl $0, -4(%rbp) # i = 0 jmp .L3 .L4: # 循环体... addl $1, -4(%rbp) # i++ .L3: cmpl $9, -4(%rbp) # i <= 9 jle .L4 |
3.3.4函数调用
表4 hello.c和hello.s中函数调用语句
| 函数 | C代码中操作语句 | 对应在hello.s中的汇编指令及解释 |
| printf调用 | printf("Hello %s %s %s\n", argv[1], argv[2], argv[3]); | leaq .LC1(%rip), %rdi # 格式字符串地址 movq %rcx, %rdx # argv[3] movq %rax, %rsi # argv[2] movq %rsi, %rcx # argv[1] movl $0, %eax # 无浮点参数 call printf@PLT |
| sleep调用 | sleep(atoi(argv[4])); | movq -32(%rbp), %rax # argv movq 32(%rax), %rdi # argv[4] call atoi@PLT #字符串转整数 movl %eax, %edi # 参数传递 call sleep@PLT |
| exit调用 | exit(1); | movl $1, %edi call exit@PLT |
| getchar调用 | getchar(); | call getchar@PLT |
3.4 本章小结
本章通过对hello.i程序的编译过程分析,系统性地探讨了从hello.i到汇编语言的转换机制。我们重点研究了编译器如何处理不同的数据类型、控制结构和函数调用,揭示了高级语言与底层机器指令之间的映射关系。
在数据类型处理方面,我们观察到编译器对整型变量、指针和字符串常量进行了精确的底层实现。局部变量通过栈帧管理,指针操作转换为地址计算和间接寻址,而字符串常量则存储在只读数据段。这些处理方式充分体现了编译器对内存布局和访问效率的优化考量。
控制结构的转换展现了编译器将高级语言抽象转化为机器指令的精妙过程。特别是for循环的实现,通过标签跳转和条件判断指令,构建了一个高效的控制流闭环。这种转换不仅保持了原始逻辑的准确性,还通过指令级优化提升了执行效率。
通过本章的学习,我们不仅掌握了阅读和分析汇编代码的基本方法,更重要的是理解了编译器在背后所做的复杂转换工作。这种理解为我们后续的优化调试、性能分析以及底层编程打下了坚实基础。从高级语言到机器指令的转换过程,展现了计算机系统各层次之间精密的协作关系。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
1. 汇编的概念
汇编是指将汇编语言源代码(.s文件)转换为机器语言目标文件(.o文件)的翻译过程。这一过程由汇编器(如GNU的as)完成,是程序构建过程中连接高级语言与机器指令的关键环节。
2.汇编的作用
首先,汇编过程实现了从符号化指令到二进制机器码的精确转换。汇编器将人类可读的助记符(如mov、add等)转换为处理器可直接执行的二进制操作码,同时完成指令编码、地址计算等底层工作。这种转换保持了程序的逻辑结构,但将其转化为机器可直接理解的形式。
其次,汇编过程生成了可重定位的目标文件。这些.o文件包含机器指令,但尚未确定最终的内存地址,为后续的链接阶段保留了灵活性。汇编器会标记出需要重定位的符号引用,如外部函数调用和全局变量访问,这些信息存储在ELF格式的重定位节中。
此外,汇编过程还进行了基本的语法检查和指令验证。汇编器会检测指令格式是否正确、操作数是否合法等低级错误,确保生成的机器码符合目标处理器的指令集架构要求。这种检查为程序的可执行性提供了基础保障。
3.总结
从系统层面看,汇编阶段在编译工具链中承上启下。它接收编译器生成的汇编代码,输出可链接的目标文件,为最终的可执行文件生成做好准备。这个过程中,汇编器需要处理平台相关的指令编码细节,使程序能够针对特定硬件架构进行优化。总的来说,汇编过程是程序从抽象到具体、从可读到可执行的关键转化阶段,既保留了高级语言的逻辑结构,又为机器执行做好了准备,在软件开发生命周期中具有不可替代的作用。
4.2 在Ubuntu下汇编的命令
![]()
4.3 可重定位目标elf格式
1. 1.输入readelf -h hello.o查看elf头
ELF头是ELF(Executable and Linkable Format)文件的开头部分,用于描述文件的基本属性和组织结构。它包含文件的魔数(Magic Number)、类别(32位或64位)、字节序(大端或小端)、版本信息、目标架构类型、程序入口点地址、节头表和程序头表的位置及大小等关键元数据。这些信息使操作系统和链接器能够正确识别和处理文件,为后续的加载、链接和执行提供必要的基础。ELF头的存在确保了文件的可移植性和平台兼容性,是ELF格式文件的核心管理结构。

2.输入readelf -S hello.o查看section头


图21 section头
从上图可以看出,hello.o中有14个节。下面对各个节进行具体分析:
表5 每节详细信息解释
| 节区编号 | 节区名称 | 类型 | 用途 | 标志 | 大小(字节) | 对齐 |
| [0] | NULL | NULL | 无效节区,用于标记未定义的节区引用 | - | 0 | 0 |
| [1] | .text | PROGBITS | 存储程序的机器指令(如main函数的代码) | AX | 0xaa3 | 1 |
| [2] | .rela.text | RELA | 记录.text节中需要重定位的指令(如printf调用地址) | I | 0xc0 | 8 |
| [3] | .data | PROGBITS | 存储已初始化的全局变量(代码中未使用) | WA | 0 | 1 |
| [4] | .bss | NOBITS | 存储未初始化的全局变量(代码中未使用) | WA | 0 | 1 |
| [5] | .rodata | PROGBITS | 存储只读数据(如字符串常量"Hello %s %s %s\n") | A | 0x49 | 8 |
| [6] | .comment | PROGBITS | 存储编译器版本信息(如GCC: (Ubuntu 11.4.0)) | MS | 0x2c | 1 |
| [7] | .note.GNU-stack | PROGBITS | 标记栈属性(如是否可执行) | O | 0 | 1 |
| [8] | .note.gnu.property | NOTE | 存储ABI属性和硬件特性(如控制流完整性) | A | 0x20 | 8 |
| [9] | .eh_frame | PROGBITS | 存储异常处理信息(用于栈展开和调试) | A | 0x38 | 8 |
| [10] | .rela.eh_frame | RELA | 记录.eh_frame节中需要重定位的条目 | I | 0x118 | 8 |
| [11] | .symtab | SYMTAB | 符号表,记录所有符号(如函数、变量)的定义和引用 | - | 0x108 | 8 |
| [12] | .strtab | STRTAB | 存储符号名称字符串(供.symtab节使用) | - | 0x32 | 1 |
| [13] | .shstrtab | STRTAB | 存储节区名称字符串(如".text"、".data"等) | - | 0x74 | 1 |
3.输入readelf -s hello.o查看符号表

图22 符号表
该符号表包含11个条目,记录了目标文件中定义和引用的各种符号信息。符号表是链接过程中解析符号引用的关键数据结构,为后续的重定位和链接提供必要的信息。
前4个条目描述了与文件本身相关的本地符号信息:第0条是空符号,用于占位;第1条标识了源文件名为"hello.c";第2、3条分别标记了.text和.rodata节区的起始位置;这些本地符号的作用域仅限于当前目标文件,在链接时不会被导出。
从第4条开始定义了全局符号:第4条是main函数定义,大小为163字节,位于.text节区;其余条目(5-10)都是未定义的全局符号引用,包括:标准库函数(puts、exit、printf等)、系统调用(sleep、getchar)。这些未定义符号需要在链接时从其他目标文件或库中解析。
符号表中包含多种类型:FILE类型标记源文件;SECTION类型标记节区;FUNC类型表示函数;NOTYPE表示类型未指定;这些类型信息帮助链接器正确识别和处理各个符号。符号的绑定属性分为:LOCAL表示局部符号;GLOBAL表示全局符号;全局符号可以被其他目标文件引用,是模块间交互的接口。
Ndx字段指示符号所属节区:UND(未定义)表示外部引用;ABS表示绝对符号;数字索引对应具体的节区;这个信息对重定位过程至关重要。
该符号表完整记录了:本目标文件定义的符号(如main函数);需要从外部解析的符号(如库函数);各符号的类型、大小和位置信息;这些信息使得链接器能够正确地将多个目标文件合并成可执行文件。
4.输入readelf -r hello.o查看可重定位段信息

图23 可重定位段信息
(1).rela.text节区分析(8个重定位条目)
该重定位节区完整记录了代码段中所有需要链接器修正的位置信息。前两个条目(偏移量0x10和0x62)是针对.rodata节区内字符串常量的数据引用重定位,采用R_X86_64_PC32类型,这种PC相对寻址方式使得程序可以独立于加载地址运行。其中0x10处引用的是错误提示字符串"用法: Hello...",0x62处引用的是格式字符串"Hello %s %s %s\n"。
其余六个条目均为函数调用的重定位信息,统一使用R_X86_64_PLT32类型,这种类型支持过程链接表(PLT)的延迟绑定机制。具体包括:偏移量0x24处的puts函数调用(对应错误信息输出),0x26处的exit函数调用(程序异常终止),0x67处的printf函数调用(主循环中的格式化输出),0x82处的atoi函数调用(字符串转整数),0x89处的sleep函数调用(程序暂停),以及0x98处的getchar函数调用(等待用户输入)。每个条目末尾的"-4"修正值用于调整返回地址,确保指令指针能正确指向下一条指令。
(2).rela.eh_frame节区分析(1个重定位条目)
该节区包含单个重定位条目,位于偏移量0x20处,类型为R_X86_64_PC32。这个重定位指向.text节的起始位置(+0偏移),用于确保异常处理框架能正确关联到代码段。当程序发生异常或进行调试时,异常处理机制通过这个重定位信息准确定位代码区域,实现栈展开和错误定位功能。这种重定位在支持C++异常处理和调试回溯的场景中尤为重要。
(3)重定位机制深度解析
所有重定位条目共同构成了可重定位目标文件的核心链接信息。R_X86_64_PC32类型通过计算目标地址与重定位地址的相对偏移,使程序具备位置无关特性;而R_X86_64_PLT32类型则实现了动态链接的延迟绑定,在首次调用函数时才进行地址解析,优化了程序启动性能。链接器处理时,会根据这些条目精确修改.text节中指定偏移处的机器码:对于数据引用,填入与.rodata节的正确偏移量;对于函数调用,填入PLT表的跳转地址。这种机制既保持了编译阶段的灵活性,又确保了最终可执行文件的正确性。
4.4 Hello.o的结果解析
输入objdump -d -r hello.o 对hello.o进行反汇编,并与第3章中的 hello.s进行对照分析


图24 hello.o反汇编得到的代码
汇编代码与机器代码之间存在着严格的结构化对应关系,这种映射体现在多个层面。在指令层面,每条汇编语句都对应特定的机器码序列,包括操作码和操作数编码。处理器架构定义了标准化的编码规则,例如数据传输指令通常以MOV操作码开头,后跟ModR/M字节来编码寄存器或内存操作数。控制流指令则使用相对偏移量表示跳转目标,汇编器会精确计算标签位置与当前指令的字节距离并生成对应的二进制偏移。对于函数调用,call指令的操作数在目标文件中暂时保留为全零,由链接器后续填充实际地址,同时生成重定位记录确保正确解析外部符号。数据访问指令同样遵循架构特定的编码模式,立即数按小端序排列,内存操作数则通过复杂的地址计算字节精确描述寻址方式。整个转换过程保持程序语义的严格等价,同时将高级的符号化表示转化为处理器可执行的二进制形式,这种系统化的映射关系构成了编译工具链的基础机制。它们的差异如下:
1. 分支转移的差异分析
表6 分支转移在hello.s和hello.o反汇编中的实现
| hello.s汇编代码 | hello.o反汇编代码 |
| cmpl $5, -20(%rbp) je .L2 | 13: 83 7d ec 05 cmpl $0x5,-0x14(%rbp) 17: 74 19 je 32 <main+0x32> |
(1)标签表示方式:
汇编代码使用符号标签.L2,便于程序员理解和维护;反汇编代码使用绝对地址偏移32(即main+0x32),这是实际机器码中的表示方式
(2)跳转偏移计算:
机器码74 19中,74是je指令的操作码,19是跳转偏移量(25字节);这个偏移量是从下一条指令开始计算(地址19处的下一条指令是1b,1b + 19 = 34,与显示的32有小差异,可能是对齐或显示问题)
(3)编码特点:
短跳转(je)使用1字节偏移量(-128到127);反汇编明确显示了指令的物理地址布局,而汇编代码隐藏了这些细节
2. 函数调用的差异分析
表7 函数调用在hello.s和hello.o反汇编中的实现
| hello.s汇编代码 | hello.o反汇编代码 |
| call printf@PLT call sleep@PLT | 6e: e8 00 00 00 00 call 73 <main+0x73> # printf 8a: e8 00 00 00 00 call 8f <main+0x8f> # sleep |
对于调用目标表示:汇编代码使用符号printf@PLT,明确指示调用PLT表中的printf、反汇编代码显示为call 73 <main+0x73>,这是临时占位地址;对于重定位信息:机器码中e8后跟着4个00,这是留给链接器填充的空间,反汇编中可以看到重定位项:对于调用机制:两者都使用e8操作码表示近相对调用;最终执行时,call指令会将返回地址压栈,并跳转到目标函数
3. 数字使用进制的不同
表8 数字使用进制在hello.s和hello.o反汇编中的实现
| hello.s汇编代码 | hello.o反汇编代码 |
| movl $1, %edi # 十进制 addq $24, %rax # 十进制 cmpl $9, -4(%rbp) # 十进制 | 28: bf 01 00 00 00 mov $0x1,%edi # 十六进制 3f: 48 83 c0 18 add $0x18,%rax # 十六进制 8f: 83 7d fc 09 cmpl $0x9,-0x4(%rbp) # 十六进制 |
对于立即数编码:汇编代码使用十进制更符合人类阅读习惯、机器码统一使用十六进制表示,因为这是处理器实际使用的格式;对于字节序体现:mov $0x1,%edi编码为bf 01 00 00 00,显示小端序存储、汇编代码不直接体现这一点;对于操作数扩展:汇编中简单的$1在机器码中被扩展为4字节01 00 00 00、这种零扩展在汇编代码中是隐式的
4. 全局变量表示的不同
表9 全局变量引用在hello.s和hello.o反汇编中的实现
| hello.s汇编代码 | hello.o反汇编代码 |
| leaq .LC0(%rip), %rax # 字符串常量 leaq .LC1(%rip), %rax # 格式字符串 | 19: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # .LC0 66: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # .LC1 |
符号引用方式不同:汇编代码直接使用符号名.LC0、.LC1;反汇编代码显示为0x0(%rip)占位符,实际地址为0
4.5 本章小结
本章系统性地探讨了汇编语言与机器代码之间的映射关系及其在计算机体系结构中的核心作用。通过分析可以清晰地看到,汇编代码作为机器指令的符号化表示,与底层硬件执行的实际二进制代码之间存在着严格而系统的对应关系。这种映射不仅体现在指令到操作码的一一转换上,更贯穿于程序结构、操作数编码、控制流实现以及符号解析等各个层面。理解这种对应关系对于掌握计算机工作原理、进行底层程序优化以及处理与硬件直接交互的任务具有根本性意义。同时,现代编译工具链通过重定位信息、指令选择优化等机制,在保持语义一致性的前提下,实现了从高级抽象到底层执行的高效转换。这种从人类可读代码到机器可执行代码的系统化转换过程,正是计算机系统能够自动执行复杂程序的根本保障。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
1. 链接的概念
链接(Link)是指在计算机系统或网络环境中,通过某种方式建立不同数据单元、资源或节点之间的关联关系。在数据结构中,链接通常表现为指针或引用,用于将分散的存储单元串联成链表、树或图等动态结构;在网络中,链接(如超链接)以URL形式存在,用于指向网页、文件或其他网络资源;在文件系统中,链接可以是硬链接或符号链接,用于实现文件的快捷访问。无论是物理连接还是逻辑关联,链接的本质都是建立一种可追溯的路径或引用关系,使不同元素能够相互访问或跳转。
2. 链接的作用
链接的主要作用是增强数据的组织性和资源的可访问性。在数据结构中,链接使动态存储成为可能,支持高效的数据插入、删除和遍历操作;在互联网中,超链接构建了网页间的网状结构,实现信息的快速导航和共享;在文件系统中,链接允许用户通过不同路径访问同一文件,提高操作便捷性。此外,链接还能降低系统耦合度,例如动态库通过链接被程序调用,实现模块化开发。总体而言,链接通过建立关联关系,优化了数据管理、资源调度和系统互联,成为计算机和网络技术高效运行的基础机制。
5.2 在Ubuntu下链接的命令
![]()
图25 在Ubuntu下链接的命令
注: 也可使用如下命令
ld \
-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 \
/usr/lib/gcc/x86_64-linux-gnu/11/crtbegin.o \
hello.o \
-lc \
/usr/lib/gcc/x86_64-linux-gnu/11/crtend.o \
/usr/lib/x86_64-linux-gnu/crtn.o \
-z relro \
-o hello
这条 ld 命令手动完成了:
-
- 指定动态链接器。
- 链接 C 运行时初始化代码(CRT)。
- 链接用户目标文件(hello.o)和 libc。
- 启用安全选项(RELRO)。
但通常直接使用 gcc 更方便
5.3 可执行目标文件hello的格式
使用readelf -a hello>hello_re.elf将可执行目标文件的ELF格式输出到hello_re.elf 使用cat命令查看其内容
1.elf头
输入readelf -h hello查看elf头:

(1) 动态链接与 PIE
类型为 DYN:这是一个 位置无关的可执行文件(PIE),意味着它可以被加载到内存的任何地址运行(现代 Linux 的默认配置,增强安全性)。
需要动态链接器:运行时依赖动态链接器(如 /lib64/ld-linux-x86-64.so.2)加载共享库(如 libc.so.6)。
(2) 入口点地址 0x1100
程序执行的起始代码位于 .text 节 的 0x1100 处,通常是 _start 函数(由 crt1.o 提供),而非直接跳转到 main。
(3) 程序头和节头
程序头(13 个):描述如何将文件中的段(如代码段、数据段)映射到内存,权限包括 R(读)、W(写)、E(执行)。示例段:LOAD(可加载段,如代码和数据)DYNAMIC(动态链接信息)INTERP(动态链接器路径)
节头(31 个):描述各节(Section)的详细信息,例如:.text:存放可执行代码;.rodata:只读数据(如字符串常量);.data 和 .bss:全局变量
2.section头
输入readelf -S hello查看section头, 节头表包含了程序的名称、地址、偏移量等信息:


3. 程序头:

图27 hello_re.elf的程序头
INTERP指定动态链接器路径,由内核在加载程序时调用,负责加载共享库(如libc.so.6);LOAD 段第一个 LOAD(R):只读数据(如ELF头、.interp、.rodata),第二个 LOAD(R E):代码段(.text),权限为可读可执行,第三个 LOAD(RW):数据段(.data、.bss),权限为可读可写;DYNAMIC包含动态链接所需的元数据(如NEEDED库、重定位条目);GNU_RELRO将动态数据结构(如.got.plt)标记为只读,防止攻击者篡改(如GOT覆盖攻击)
5.4 hello的虚拟地址空间
下为使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明


图28 使用edb查看hello程序及地址
与 ELF 结构的对照分析
(1) 地址随机化(ASLR)
- ELF 静态地址:
- .text 的虚拟地址为 0x1100,.data 为 0x4000(编译时确定)。
- 运行时实际地址:
- 在 EDB 中,代码段加载到 0x000056151234000(基址 0x000056151230000 + 0x4000),数据段在 0x000056151237000。
- 偏移计算:
- 代码段偏移:0x000056151234000 - 0x000056151230000 = 0x4000(与 ELF 的 .text 地址 0x1100 不同,因 PIE 基址随机化)。
(2) 权限一致性验证
表10 权限一致性验证
| ELF 结构 | ELF 权限 | EDB 权限 | 验证 |
| .text | R-X | r-x | 代码可执行,权限一致。 |
| .rodata | R-- | r-- | 只读数据,权限一致。 |
| .data/.bss | RW- | rw- | 数据可读写,权限一致。 |
| INTERP | R-- | r-- | 动态链接器路径只读,权限一致。 |
(3) 动态链接库的加载
- ELF 的 INTERP 和 DYNAMIC:
- 指定动态链接器(ld-linux-x86-64.so.2)和依赖库(如 libc.so.6)。
- EDB 中的表现:
- libc 被映射到随机地址(如 0x000075c2f8c00000),代码段权限为 r-x,与 ELF 的 LOAD 段一致。
5.5 链接的重定位过程分析
1.对比分析
(1) 未链接的 hello.o 特点
未解析的符号:调用外部函数(如 puts@plt、printf@plt)时,目标地址为 0x0,依赖重定位条目;节地址从 0 开始:所有节(如 .text、.data)的虚拟地址为 0x0,需链接器分配实际地址
(2) 已链接的 hello 特点
符号解析完成:外部函数调用指向 PLT 表(如 call 10a0 <puts@plt>),动态库符号通过 .got.plt 延迟绑定;节地址已分配:.text 起始于 0x1000,.plt 在 0x1020,.data 在 0x4000(由链接器计算基址和偏移);重定位条目消失:链接器已将重定位信息转换为实际地址
2. 链接过程的核心步骤
(1) 符号解析
合并同类节:链接器将所有输入文件(如 hello.o、crt1.o)的 .text、.data 等节合并,并分配运行时地址;解析未定义符号:查找 puts、printf 等符号的定义(在 libc.so 中),若符号未定义,链接报错(undefined reference)
(2) 重定位
修正代码中的引用:根据重定位类型(如 R_X86_64_PLT32、R_X86_64_PC32)计算目标地址。示例:hello.o 中的 call puts 被修正为 call 10a0 <puts@plt>;填充 .got 和 .plt:.got.plt 存储动态库函数的实际地址(运行时由动态链接器填充),.plt 包含跳转桩代码(如 jmp *0x3fa8 指向 puts 的 GOT 条目)





图29 hello.asm部分代码
5.6 hello的执行流程
打开gdb,并设置断点,

图30 设置断点
首次暂停,找到动态链接器入口,


地址0x7ffff7fe3290是动态链接器ld-linux-x86-64.so.2的入口点_start。由内核加载器直接调用,负责初始化动态链接环境。
指令mov %rsp,%rdi将当前栈指针%rsp的值保存到%rdi寄存器。后续%rdi将作为参数传递给_dl_start函数.
执行nexti单步观察后续操作,见图32,

图32 函数_dl_start
调用动态链接器的函数_dl_start, 地址为0x7ffff7fe4030。接下来查看寄存器状态。

对 _dl_start 函数进行了反汇编操作,可以查看其汇编代码结构

图34 _dl_start 函数反汇编
之后继续执行程序,程序又命中了 hello 可执行文件自身的 _start 断点

图35 命中_start断点
先前设置为等待状态的 __libc_start_main 断点被命中,此时程序进入该函数

图36 __libc_start_main
使用 finish 命令让 __GI__IO_puts 函数执行完毕并返回

图37 __GI__IO_puts 函数
返回 main 函数后,继续单步执行,程序执行到调用 exit 函数的指令处。最后继续执行程序,程序以代码01退出,整个调试过程结束

5.7 Hello的动态链接分析
动态链接是现代操作系统中的重要机制,它允许程序在运行时才加载和链接共享库(如glibc)。这种机制的主要优势包括:节省内存(多个程序可共享同一库的代码段);简化更新(更新库无需重新编译程序);减小可执行文件体积
过程链接表(PLT),PLT是一个代码段数组,每个条目通常为16字节:
- PLT[0]:特殊条目,跳转到动态链接器
- PLT[1...n]:每个条目对应一个需要动态解析的函数
PLT条目的典型结构:跳转到对应GOT条目;压栈重定位索引;跳转到PLT[0]启动解析
全局偏移表(GOT),GOT是一个数据段数组,每个条目8字节:GOT[0]和GOT[1]:包含动态链接器使用的解析信息;GOT[2]:指向动态链接器入口点(_dl_runtime_resolve);GOT[3...n]:对应函数的实际地址(初始指向PLT中的解析代码)
在hello的section头可以看到关于got和plt的信息

图39 got和plt的信息
使用edb调试器观察.got.plt段在动态链接前后的变化用edb打开hello,通过data dump窗口分别查看dl_init前后的变化。发现开始时GOT[1]和GOT[2]中内容均为0,在dl_init之后,二者的内容变成重定位表和动态链接器ld-linux.so运行地址
5.8 本章小结
hello程序的执行流程分析展示了从程序启动到退出的完整生命周期,包括内核加载、动态链接、程序初始化等关键阶段。特别值得关注的是动态链接分析部分,通过深入研究PLT和GOT机制,我们理解了现代操作系统如何实现高效的动态链接功能,包括延迟绑定等优化技术。这些分析不仅具有理论价值,也为程序调试和性能优化提供了实践指导。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机系统中程序执行的一个实例,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的地址空间、代码、数据和系统资源(如文件描述符、信号处理等),并与其他进程隔离,确保系统稳定性和安全性。进程的作用包括:
- 资源管理:操作系统通过进程隔离和管理CPU、内存、I/O设备等资源,避免冲突。
- 多任务并行:通过时间片轮转或优先级调度,实现多个进程的并发执行,提高系统利用率。
- 程序执行:为程序提供运行环境,包括加载代码、初始化数据、处理动态链接库等。
- 用户与系统交互:进程作为用户任务的载体,支持交互式操作(如Shell命令)或后台服务(如Web服务器)。
进程由进程控制块(PCB)描述,包含进程ID(PID)、状态(运行、就绪、阻塞等)、寄存器上下文、内存映射等信息。操作系统通过调度算法(如CFS、轮转)在多个进程间切换,实现多任务效果。
6.2 简述壳Shell-bash的作用与处理流程
Shell是用户与操作系统内核交互的接口,Bash(Bourne-Again Shell)是Linux默认的命令解释器,其作用包括:
- 命令解析与执行:读取用户输入的命令(如./hello),解析参数并调用内核服务执行程序。
- 脚本处理:支持脚本编程,通过条件判断、循环等结构实现自动化任务。
- 环境管理:维护环境变量(如PATH)、进程控制(如后台运行&)、输入输出重定向(如>)。
处理流程:
- 读取输入:通过终端或脚本获取命令字符串。
- 解析命令:分割命令为令牌(token),识别特殊字符(如管道|)。
- 扩展变量:替换$开头的环境变量(如$HOME)。
- 创建进程:调用fork()生成子进程,子进程通过execve()加载目标程序(如hello)。
- 等待完成:若为前台命令,Shell调用waitpid()等待子进程结束;若为后台命令,则继续接收新输入。
6.3 Hello的fork进程创建过程
当用户在Shell中输入./hello并按下回车后,Shell会通过一系列步骤创建并执行该程序。其中,fork()系统调用是进程创建的核心环节,其具体过程如下:
- Shell发起fork()调用
Shell首先解析命令,确定需要执行./hello,随后调用fork()系统调用。此时,操作系统会创建一个与当前Shell进程几乎完全相同的子进程。这个子进程被称为Shell的"副本",因为它继承了Shell的代码段、数据段、堆栈、环境变量、打开的文件描述符以及信号处理方式等几乎所有属性。 - 写时复制(Copy-On-Write, COW)机制
现代操作系统不会在fork()时立即复制父进程的所有内存,而是采用写时复制(COW)优化技术:- 子进程的页表(Page Table)初始时指向父进程的物理内存页。
- 只有当父进程或子进程尝试修改某个内存页时,操作系统才会真正复制该页,确保修改不会影响另一个进程。
这种方式显著提高了fork()的效率,因为大部分情况下子进程会立即调用execve()替换内存映像,无需复制大量数据。
- 进程控制块(PCB)的创建
内核为新进程分配一个唯一的进程ID(PID),并初始化其进程控制块(PCB),记录进程状态、寄存器上下文、内存映射、调度信息等。此时,子进程的状态被设置为就绪(Ready),等待被调度执行。 - 父子进程的区分
fork()的返回值决定了进程的后续行为:- 父进程(Shell):fork()返回子进程的PID,此时Shell通常会调用waitpid()等待子进程结束(除非命令以&后台运行)。
- 子进程:fork()返回0,子进程接下来会调用execve()加载hello程序,替换自己的内存映像。
- 子进程准备执行目标程序
在调用execve()之前,子进程会继承Shell的以下关键属性:- 文件描述符:例如标准输入(stdin)、标准输出(stdout)、标准错误(stderr)。
- 环境变量:如PATH、HOME等,这些会被传递给hello程序。
- 信号处理方式:除非显式设置,否则子进程会沿用父进程的信号处理函数。
- 进程调度与执行
子进程创建完成后,操作系统的调度器会决定何时为其分配CPU时间片。在Linux的完全公平调度器(CFS)策略下,子进程会被加入运行队列,与其他进程公平竞争CPU资源。
fork()创建了一个与Shell几乎相同的子进程,但通过写时复制优化避免了不必要的内存开销。子进程随后通过execve()加载hello程序,完成进程的最终形态。这一机制使得Linux能够高效地创建和管理进程,支持多任务并发执行。

图40 fork创建进程示例
6.4 Hello的execve过程
子进程通过execve()系统调用加载hello程序,具体步骤如下:
- 参数传递:execve("/path/hello", argv, envp)接收程序路径、参数列表(如命令行参数argv)和环境变量(envp)。
- 清除原上下文:内核释放子进程原有的代码、数据、堆栈等资源(但保留PID和文件描述符)。
- 加载新程序:
- 解析hello的ELF格式头部,加载代码段(.text)和数据段(.data、.bss)到内存。
- 动态链接器(ld-linux.so)处理共享库依赖(如libc.so),完成符号解析与重定位。
- 初始化用户栈,压入argv和envp,设置程序计数器(PC)指向_start入口。
- 开始执行:进程从main()函数开始运行,进入用户态代码逻辑
6.5 Hello的进程执行
Hello程序的执行过程涉及操作系统的进程管理、CPU调度、上下文切换以及用户态与内核态的转换等多个关键机制。当Shell通过fork()创建子进程并调用execve()加载Hello程序后,该进程进入运行阶段,其完整的执行流程如下:
1. 进程上下文的初始化与程序加载
在execve()成功加载Hello程序后,操作系统会为该进程初始化完整的执行环境。这包括:
- 代码段(.text)的加载:Hello程序的机器指令被载入内存,程序计数器(PC)被设置为_start入口点,最终跳转到main()函数开始执行用户代码。
- 数据段的初始化:全局变量和静态变量被分配到.data(已初始化)或.bss(未初始化)段,堆(heap)和栈(stack)空间也被创建。
- 参数与环境的传递:命令行参数(argv)和环境变量(envp)被压入用户栈,使得main(int argc, char **argv, char **envp)可以访问它们。
- 动态链接(如适用):如果Hello依赖共享库(如libc.so),动态链接器(ld-linux.so)会在程序启动时解析符号,完成库的加载与地址重定位。
2. CPU时间片与进程调度
Hello进程在运行时,操作系统的调度器会按照一定的策略为其分配CPU时间片(通常为几毫秒到几十毫秒)。在Linux中,默认采用完全公平调度器(CFS),其核心机制包括:
- 时间片分配:CFS维护一个红黑树结构,根据进程的虚拟运行时间(vruntime)动态调整优先级,确保所有进程公平地分享CPU。
- 抢占式调度:当Hello进程的时间片用完,或更高优先级的进程就绪时,内核会触发调度,保存当前进程的上下文(寄存器、程序计数器等),并切换到另一个进程。
- I/O阻塞与唤醒:如果Hello执行了I/O操作(如printf调用),进程会进入阻塞状态,让出CPU;当I/O完成后,内核将其重新放入就绪队列等待调度。
3. 用户态与内核态的转换
Hello进程的执行过程会频繁地在用户态和内核态之间切换,主要涉及以下几种情况:
- 系统调用(Syscall):当Hello调用printf、exit等函数时,会触发write或exit_group等系统调用,通过软中断(如int 0x80或syscall指令)进入内核态。内核完成请求后(如打印字符串到终端),再返回用户态继续执行。
- 中断处理:时钟中断(时间片到期)、硬件中断(如磁盘I/O完成)等会强制CPU进入内核态,保存当前进程状态并执行中断服务例程(ISR)。
- 信号处理:如果用户按下Ctrl+C(发送SIGINT),内核会中断Hello进程,调用其注册的信号处理函数(默认终止进程)。
4. 进程终止与资源回收
当main()函数返回或调用exit()时,Hello进程进入终止阶段:
- 资源释放:内核关闭进程打开的文件描述符,释放内存映射,并清空页表。
- 父进程通知:Shell(父进程)通过wait()或waitpid()系统调用获取Hello的退出状态(如exit(0)表示成功)。
- 进程描述符清理:内核从进程表中删除Hello的PCB,最终销毁该进程
6.6 hello的异常与信号处理
6.6.1异常
在操作系统中,异常(Exception)是指程序执行过程中发生的非预期事件,导致CPU暂停当前指令流并跳转至内核预设的处理程序。根据触发方式和恢复可能性,异常可分为以下四类:
1. 中断(Interrupt)
- 触发方式:异步,由外部硬件设备(如定时器、键盘、网卡)产生,与当前执行的指令无关。
- 处理机制:
- CPU在执行完当前指令后检测中断请求,保存现场(程序计数器、寄存器状态)并跳转至中断服务例程(ISR)。
- ISR处理完成后,通过iret指令恢复原任务上下文(可能切换到其他进程)。
- 典型应用:
- 时钟中断:实现时间片轮转调度,强制切换进程。
- I/O中断:处理键盘输入(如Ctrl-C)、磁盘读写完成通知。

图41 中断处理机制
2. 陷阱(Trap)
- 触发方式:同步,由程序主动执行特定指令(如int 0x80、syscall)触发。
- 处理机制:
- CPU跳转至内核预设的陷阱处理程序,切换到内核态执行系统调用(如open、write)。
- 完成后返回用户态,继续执行后续指令。
- 典型应用:
- 系统调用:用户程序通过陷阱指令请求内核服务(如printf触发write系统调用)。
- 调试断点:调试器利用int 3陷阱实现单步执行。

图42 陷阱处理机制
3. 故障(Fault)
- 触发方式:同步,由指令执行错误引发(如缺页、除零),可能可恢复。
- 处理机制:
- CPU保存错误现场,跳转至故障处理程序。若处理程序修复问题(如加载缺失的页),则重新执行原指令;否则终止进程。
- 典型应用:
- 缺页故障(Page Fault):访问未映射的虚拟地址时,内核加载缺失页并重试指令。
- 算术异常:除零操作触发SIGFPE信号,默认终止进程。

4. 终止(Abort)
- 触发方式:同步,由不可恢复的硬件或软件错误引发(如内存校验错误、非法指令)。
- 处理机制:
- 内核直接终止进程或触发系统重启,不尝试恢复现场。
- 典型场景:
- 硬件故障:内存条损坏导致数据校验错误。
- 内核恐慌(Kernel Panic):关键数据结构被破坏时强制停机。

图44 终止处理机制
6.6.2 信号产生与处理
信号(Signal)是内核向进程通知异步事件的机制,通常由异常、用户输入或进程间通信触发:
1. 用户输入触发的信号
- Ctrl-C(SIGINT):
- 终止前台进程组,默认行为是终止进程。
- 可被捕获(如signal(SIGINT, handler)),用于优雅退出。
- Ctrl-Z(SIGTSTP):
- 暂停前台进程组,进程状态转为TASK_STOPPED。
- 通过fg命令或SIGCONT信号恢复执行。
2. 异常触发的信号
- SIGSEGV:段错误(访问非法内存)。
- SIGFPE:算术异常(如除零)。
- SIGILL:非法指令(如执行损坏的代码)。
3. 进程控制信号
- SIGKILL(kill -9):强制终止进程,不可捕获或忽略。
- SIGTERM(kill -15):请求进程正常退出,可被捕获。
4. 信号处理方式
- 默认行为:终止、暂停或忽略(如SIGCHLD)。
- 自定义处理:通过signal()或sigaction()注册处理函数。
- 屏蔽信号:使用sigprocmask()临时阻塞信号(如SIGINT)。
6.6.3实例运行
下面将结合程序运行过程中的不同操作,信号的产生和处理方式来说明hello执行过程中异常和信号的处理。
- 不停乱按
可以发现,乱按键盘时程序并没有受到信号,仍按照原流程工作,输入的信息在程序执行结束后出现在了shell的输入行内。

图45 不停乱按
- Ctrl-Z

在终端按下 Ctrl-Z 时,系统会向前台进程组中的所有进程发送 SIGTSTP 信号,这是一个重要的进程控制机制.
信号本质:
SIGTSTP (Signal Terminal Stop) 是标准暂停信号,编号为 20;SIGSTOP (19) 不同,SIGTSTP 可以被捕获或忽略;默认行为是暂停进程执行
进程状态变化:
Ctrl-Z后运行ps,打印出了各进程的pid,可以看到之前挂起的进程helloCtrl-Z后运行jobs,被挂起的标识Stopped。

图47 ps和jobs指令
Ctrl-Z后运行pstree,可看到它打印出的信息

图48 ptree打印的信息
fg,发送SIGCONT信号,使hello继续在前台运行,kill,在输入Ctrl-Z后,用ps查看hello的PID,然后用kill -9命令强制终止进程,发送SIGKILL杀死hello

图49 kill杀死进程
- Ctrl-C
会触发中断异常,操作系统向前台进程hello发送 SIGINT 信号,终止hello进程

图50 ctrlC
6.7本章小结
本章深入探讨了进程管理的核心概念及其在hello程序中的具体实现。从进程的基本概念入手,我们首先理解了进程作为程序执行实例的本质特征及其在操作系统资源管理中的关键作用。shell作为用户与操作系统内核之间的桥梁,其处理流程展现了命令解释、进程创建和环境管理的完整生命周期。特别值得关注的是hello程序通过fork系统调用创建新进程的具体过程,这一机制不仅实现了进程的复制,还通过写时复制等优化技术提高了系统效率。随后的execve过程分析揭示了程序如何替换当前进程映像并开始执行新的程序文件,这一过程涉及复杂的文件加载和内存管理操作。
(第6章2分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
hello程序运行过程中展现的存储器地址空间是现代计算机体系结构的典型范例。程序中的每个内存访问都涉及复杂的地址转换过程,从程序员可见的逻辑地址开始,经过段式管理转换为线性地址(也称为虚拟地址),最终通过页式管理映射到实际的物理地址。在hello的执行过程中,当调用printf函数时,函数指针的值(如0x400530)就是一个逻辑地址,它由代码段寄存器CS和指令指针EIP共同构成。这个逻辑地址经过段式管理单元转换后,形成32位或64位的线性地址,例如0x400530。在启用分页机制的情况下,这个线性地址还需要通过页表转换为物理地址,比如0x12a34000,才能真正访问到内存中的指令数据。值得注意的是,现代操作系统通常使用平坦内存模型,使得逻辑地址到线性地址的转换变得透明,但底层依然保持着完整的段式管理机制,这是x86架构的重要特征。hello程序中的全局变量、局部变量以及动态分配的内存,都遵循这样的地址转换流程,确保了内存访问的安全性和隔离性。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器的段式管理机制是地址转换的第一阶段,它为hello程序提供了基本的内存保护机制。当hello程序中的指令访问内存时,CPU会根据段寄存器中的段选择符在全局描述符表(GDT)或局部描述符表(LDT)中查找对应的段描述符。优点:可以分别编写和编译,可以针对不同类型的段采取不同的保护,可以按段为单位进行共享,包括通过动态链接进行代码共享,每次交换的是一组相对完整的逻辑信息

图51 段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
在计算机系统中,程序使用的是线性地址(也称为虚拟地址),而实际访问内存时需要使用物理地址。页式管理是操作系统实现虚拟内存的核心机制之一,它通过分页机制将线性地址转换为物理地址。
线性地址与物理地址的关系是内存管理的核心。线性地址由CPU生成,是程序直接访问的地址空间,其范围可能大于物理内存容量。物理地址则是实际在内存硬件上使用的地址,由内存管理单元(MMU)转换得到。分页机制的基本原理是将线性地址空间划分为固定大小的页,典型大小为4KB(x86架构)。相应地,物理内存也被划分为相同大小的页框。页表则存储着线性页号到物理页框号的映射关系,这是地址转换的关键数据结构。
线性地址通常由两部分组成:页号和页内偏移。页号用于索引页表,找到对应的物理页框;页内偏移表示数据在页内的位置,直接映射到物理页框的偏移量。例如,在32位系统中,若页大小为4KB,则低12位表示页内偏移,高20位表示页号。页表的核心作用是建立虚拟页到物理页框的映射关系。每个页表项不仅包含物理页框号,还包含重要的控制位:存在位指示该页是否在物理内存中,读写权限位控制访问权限,用户/内核权限位决定访问级别,还有其他状态位如Dirty位等。
地址转换的具体步骤是:首先CPU生成线性地址,MMU将其拆分为页号和页内偏移;然后用页号查询页表,若页表项的存在位为1,则取出物理页框号;若为0则触发缺页异常;最后将物理页框号与页内偏移拼接,得到最终的物理地址用于内存访问。现代计算机系统普遍采用多级页表结构来解决大地址空间下的页表存储问题。在32位系统中常见的二级页表结构中,线性地址被划分为页目录索引、页表索引和页内偏移三部分。转换时先从CR3寄存器获取页目录基地址,然后逐级查询,最终定位到物理页框。64位系统则采用更复杂的四级页表结构(PML4→PDPT→PD→PT),以支持更大的地址空间。这种分级结构有效减少了页表的内存占用,因为只需为实际使用的地址空间分配页表项。
当MMU发现页表项的存在位为0时,会触发缺页异常。操作系统处理缺页异常的流程包括:首先检查访问是否合法,排除越界或权限错误;然后从磁盘的Swap空间加载缺失的页到物理内存;最后更新页表并重新执行导致异常的指令。这个过程实现了虚拟内存的核心功能,使程序可以使用比物理内存更大的地址空间。

图51 页式管理
虚拟地址(VA)被划分为虚拟页号(VPN)和虚拟页内偏移(VPO)两部分。在TLB查找过程中,VPN进一步分为TLB标签(TLBT)和TLB索引(TLBI):TLBI用于定位TLB中的组,TLBT则用于比对确认是否命中。若TLB命中,则直接返回物理页号(PPN);若未命中,需进行多级页表查询。页表查询时,VPN被拆分为四个索引字段,依次作为:一级页表的索引,获取二级页表基址;二级页表的索引,获取三级页表基址;三级页表的索引,获取四级页表基址;四级页表的索引,最终获得PPN。最终将查询得到的PPN与原始VPO组合,形成物理地址(PA)。

图52 四级页表支持下的VA到PA的变换
在 Core i7 体系结构中,物理地址(PA)由三部分组成:CT(40位):Cache Tag,用于匹配 Cache 行的标签部分;CI(6位):Cache Index,用于索引 Cache 组;CO(6位):Cache Offset,用于定位数据块内的具体字节位置。
当 MMU 发送物理地址访问 Cache 时,Cache 的查找过程如下:
- 组选择:使用 CI 位确定目标 Cache 组。
- 标签匹配:在该组的所有行中,比较 CT 是否与 Cache 行的 Tag 匹配。
- 有效性检查:若匹配成功且该行的有效位(Valid Bit)为 1,则 Cache 命中。
- 数据提取:根据 CO 位从匹配的数据块中取出所需的字节。
Cache Miss 处理与块替换策略
如果当前 Cache 层级(如 L1 Cache)未命中,则需从下一级存储(如 L2 Cache)获取数据:
- 块提取:从下级存储(如 L2 Cache 或主存)加载目标数据块。
- 放置策略:
- 若 Cache 有空闲块(Invalid 或未使用),则直接将数据块存入空闲位置。
- 若无空闲块,则根据替换策略(如 LRU、FIFO 或随机替换)选择一个牺牲块(Victim Block)进行替换。
数据更新:新数据块被加载到 Cache 中,并更新 Tag 和有效位。
Core i7 采用三级 Cache 结构(L1→L2→L3),访问顺序遵循由快到慢的层次化查找:
- L1 Cache:最先查找,速度最快,若命中则直接返回数据。
- L2 Cache:若 L1 未命中,则在 L2 中查找,其访问机制与 L1 相同(组索引 + 标签匹配)。
- L3 Cache:若 L2 仍未命中,则查询 L3 Cache,其容量更大但延迟更高。
- 主存访问:若所有 Cache 均未命中,则从主存加载数据,并按照 Cache 层级逐级填充。
最终,数据会从最低层 Cache(如 L1)返回给 CPU,完成整个访存过程。

图53 三级Cache支持下的物理内存访问
当调用 `fork` 函数创建 `hello` 进程时,内核会为这个新进程分配一个唯一的进程标识符(PID),并复制父进程的进程控制块(PCB)、文件描述符表等关键数据结构。这样,`hello` 进程就拥有了与父进程相同的运行环境基础。内核并不会立即为子进程分配独立的物理内存,而是采用写时复制(Copy-On-Write, COW)机制来优化内存使用。
在虚拟内存管理方面,内核会为 `hello` 进程复制父进程的 `mm_struct`、`vm_area_struct` 和页表,使得子进程初始时拥有和父进程完全相同的虚拟地址空间布局。此时,父子进程的页表项都被设置为只读,并且对应的虚拟内存区域(VMA)被标记为私有写时复制。这意味着,两个进程共享相同的物理内存页,但只要它们都不尝试写入这些内存,就不会触发实际的物理内存复制。
如果后续父进程或子进程尝试修改某个内存页,CPU 会检测到该页是只读的,从而触发页错误异常(Page Fault)。内核的页错误处理程序会识别这是一个 COW 场景,并分配一个新的物理页,复制原始数据到新页,然后更新该进程的页表项,使其指向新页并恢复可写权限。这样,修改数据的进程拥有自己的独立副本,而另一个进程仍然引用原来的物理页,从而确保内存隔离性。这种机制既节省了内存,又保证了进程间的数据独立性。

图54 共享对象和私有的写时复制对象
当调用 `execve` 函数加载并运行 `hello` 程序时,内核会对当前进程的虚拟内存空间进行彻底的重构。首先,它会清除进程原有的用户地址空间布局,释放所有已存在的用户态内存区域,包括代码段、数据段、堆、栈以及内存映射文件等。这个过程相当于将当前进程的执行环境完全重置,为新的程序运行做好准备。
接下来,内核会基于 `hello` 可执行文件的结构重新构建虚拟内存布局。新的代码段(`.text`)和已初始化数据段(`.data`)会被映射到对应的文件内容上,采用私有写时复制(COW)的方式,确保进程修改数据时拥有独立的副本。而未初始化的数据段(`.bss`)以及栈和堆区域则被映射到匿名文件(全零填充),并设置为按需分配物理内存。这些区域的初始大小可能为零,但会在程序运行时动态扩展。
如果 `hello` 程序依赖动态链接库(如 `libc.so`),内核会将这些共享库映射到进程的共享内存区域。这样,多个进程可以共享同一份库代码,节省物理内存的使用。动态链接器会在程序启动时完成符号解析和重定位工作,确保所有外部函数调用都能正确跳转到共享库中的实现。
最后,内核会设置程序计数器(PC),使其指向 `hello` 程序的入口点(通常是 `_start` 或 `main` 函数)。当进程恢复执行时,CPU 将从该入口点开始逐条执行指令。由于采用了按需分页机制,Linux 并不会一次性加载所有代码和数据,而是根据程序的执行流程逐步换入所需的页面,从而优化内存使用效率。

图55 内存映射
7.8 缺页故障与缺页中断处理
当CPU访问虚拟页VP3时,首先会检查页表项PTE3的有效位。如果发现该位为0,说明VP3当前并未被缓存在物理内存中,这种情况通常被称为缺页(Page Fault)。此时,CPU会触发缺页异常,将控制权转交给操作系统的缺页异常处理程序。
缺页异常处理程序会选择一个合适的物理页作为牺牲页,假设这里选择了存放VP4的物理页PTE4。处理程序首先会将VP4的内容写回磁盘(如果它是脏页),然后从磁盘或交换空间加载VP3的数据到该物理页中。完成数据加载后,处理程序会更新页表,将PTE3重新映射到正确的物理页,并标记为有效。
最后,操作系统会重新执行那条触发缺页的指令。由于此时VP3已经被加载到物理内存中,并且页表项已更新,CPU再次访问VP3时就能成功命中。整个过程对应用程序是透明的,程序只会感知到短暂的延迟,而不会察觉到内存管理的细节。

7.9动态存储分配管理
在程序运行时,动态内存分配器负责管理进程的堆内存空间。堆是一个从进程未初始化数据区(.bss)末尾开始向上增长的虚拟内存区域,内核通过维护一个名为brk的指针来标记堆当前的顶部边界。这个区域在初始状态下由操作系统以二进制零填充,随着内存分配请求的推进逐步向高地址扩展。分配器将堆内存组织成不同大小的内存块,每个块代表一段连续的虚拟地址空间,其状态要么标记为已分配供程序专用,要么保持空闲待分配状态。
现代动态内存分配器主要分为显式和隐式两种类型。显式分配器要求程序员主动调用free函数来释放不再使用的内存块,而隐式分配器(垃圾回收机制)则通过运行时系统自动检测并回收程序不再引用的内存块。C语言标准库提供的malloc/free接口就是典型的显式分配器实现。当hello程序中的printf函数调用malloc时,分配器会在堆中寻找合适大小的空闲块,返回指向该内存块的void指针;若分配失败则返回NULL指针。程序使用完该内存后,必须显式调用free函数将其归还给分配器。
在实现技术上,分配器可采用不同的数据结构来管理空闲内存块。最简单的隐式空闲链表方案在每个内存块头部存储大小信息,通过遍历整个堆来查找可用块,其搜索效率与堆中块的总数成正比。这种方案支持三种基本放置策略:首次适配从堆起始端开始搜索第一个足够大的块;下次适配从上一次分配位置继续搜索;最佳适配则遍历整个堆选择最匹配请求大小的块。更高效的显式空闲链表方案将空闲块单独组织成链表结构,典型实现如C标准库的malloc,它通过维护显式空闲链表显著提高了分配效率,但需要额外的空间来存储链表指针。

图57 带标签的隐式空闲链表的数据组织方式

图57 显式空闲链表的数据组织方式
7.10本章小结
本章深入剖析了hello程序在计算机系统中的存储器地址空间管理机制,全面展现了从逻辑地址到物理地址的完整转换过程。在逻辑地址到线性地址的转换环节,我们详细分析了Intel处理器的段式管理机制,包括段选择符、段描述符以及段寄存器的具体作用,揭示了保护模式下地址转换的第一阶段工作原理。页式管理部分则系统性地阐述了线性地址到物理地址的转换过程,重点讲解了多级页表结构、页目录项和页表项的组成格式及其控制权限位的作用机制。特别值得关注的是TLB快表与四级页表协同工作下的地址转换优化过程,这一机制通过缓存最近使用的地址映射关系,显著提高了地址转换效率。在物理内存访问层面,我们探究了三级Cache层次结构如何加速内存访问,包括Cache行、组相联映射以及LRU替换策略等关键技术细节
(第7章 2分)
结论
从计算机系统的视角来看,hello程序的生命周期完整展现了现代计算体系的精妙协作。在编译期,预处理器的词法分析和宏展开构建了程序的逻辑框架,编译器通过语法树优化和指令选择生成目标架构相关的汇编代码,汇编器将其转换为包含重定位信息的机器指令,链接器最终完成地址空间布局和动态库绑定。加载阶段由操作系统主导,通过fork创建进程控制块,execve加载程序段到虚拟内存空间,动态链接器解析共享库依赖,建立起完整的执行环境。运行时CPU的流水线机制并行处理取指、译码和执行,MMU配合TLB完成虚拟地址到物理地址的转换,多级缓存体系优化内存访问性能,系统调用接口处理I/O操作。程序终止时,操作系统回收页表、文件描述符等资源,进程状态码被父进程收集。
这个过程中最深刻的系统设计智慧体现在抽象与协作的平衡:编译器前端与后端通过中间表示解耦,操作系统通过虚拟化提供统一的资源视图,硬件通过特权级划分保障系统安全。基于此,我认为未来的系统设计可以探索更智能的编译-运行时协同优化,采用可验证的形式化方法确保各层次接口的正确性,并引入量子计算概念重构传统内存层级结构,例如设计基于程序语义的智能预取机制,或者开发能够自适应调整优化策略的异构编译框架,这些创新方向都可能带来系统效率的质的飞跃。
(结论0分,缺失-1分)
附件
| 文件名 | 介绍 |
| hello.c | hello的源程序 |
| hello.i | 源程序预处理之后的ASCII文件 |
| hello.s | 源程序经过编译后的汇编文件 |
| hello.o | 汇编文件汇编后的可重定位目标文件 |
| hello | 可重定向目标文件链接后的可执行目标文件 |
| hello.elf | 可重定位目标文件的ELF文件,便于查看ELF格式 |
| hello_re.elf | 可执行目标文件的ELF文件,便于查看ELF格式 |
| hello.asm | 可执行目标文件反汇编之后的汇编文件 |
| hello_o.asm | 可重定向目标文件反汇编之后的汇编文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[2] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[3]王强.计算机C语言编译系统前后端的设计与实现[J].科学技术创新,2024,(24):128-131.
(参考文献0分,缺失 -1分)
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2301_81602707/article/details/148203807






