2025年5月
摘 要
本文以Hello程序为研究载体,系统探究其从源代码到可执行文件再到进程生命周期的完整流程,深度解析计算机系统多层面协同机制。首先阐述预处理、编译、汇编及动态链接的核心步骤:通过gcc工具链生成hello.i(预处理展开头文件与宏)、hello.s(汇编代码)、hello.o(可重定位目标文件),最终经ld链接器解析符号引用并生成可执行文件hello,结合readelf、objdump等工具剖析ELF格式中段(.text/.rodata)、符号表、重定位条目的结构特征。其次聚焦进程动态行为,分析 bash Shell 通过 fork 创建子进程、execve 加载程序的底层逻辑,以及 SIGINT(Ctrl-C终止)、SIGTSTP(Ctrl-Z挂起)等信号处理机制;在存储管理层面,基于x86-64架构解析段式到页式的地址转换,揭示 TLB 缓存对地址转换的加速作用、三级Cache的内存访问优化,以及fork写时复制和execve地址空间重建的高效实现。最后结合Unix IO模型,剖析printf通过write系统调用实现格式化输出、getchar基于键盘中断与缓冲区的输入处理流程,展现设备文件抽象与系统调用的底层交互。研究通过对Hello程序全流程的技术拆解,清晰呈现编译工具链、操作系统内存与进程管理、硬件存储架构的协同原理,为理解程序执行机制与系统优化提供了具象化范例,体现计算机系统层次化设计与模块化抽象的工程思想。
关键词:预处理;编译;链接;进程管理;存储管理;动态链接;信号处理;P2P;020
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 环境与工具........................................................................... - 4 -
1.3 中间结果............................................................................... - 4 -
1.4 本章小结............................................................................... - 4 -
第2章 预处理............................................................................... - 5 -
2.1 预处理的概念与作用........................................................... - 5 -
2.2在Ubuntu下预处理的命令................................................ - 5 -
2.3 Hello的预处理结果解析.................................................... - 5 -
2.4 本章小结............................................................................... - 5 -
第3章 编译................................................................................... - 6 -
3.1 编译的概念与作用............................................................... - 6 -
3.2 在Ubuntu下编译的命令.................................................... - 6 -
3.3 Hello的编译结果解析........................................................ - 6 -
3.4 本章小结............................................................................... - 6 -
第4章 汇编................................................................................... - 7 -
4.1 汇编的概念与作用............................................................... - 7 -
4.2 在Ubuntu下汇编的命令.................................................... - 7 -
4.3 可重定位目标elf格式........................................................ - 7 -
4.4 Hello.o的结果解析............................................................. - 7 -
4.5 本章小结............................................................................... - 7 -
第5章 链接................................................................................... - 8 -
5.1 链接的概念与作用............................................................... - 8 -
5.2 在Ubuntu下链接的命令.................................................... - 8 -
5.3 可执行目标文件hello的格式........................................... - 8 -
5.4 hello的虚拟地址空间......................................................... - 8 -
5.5 链接的重定位过程分析....................................................... - 8 -
5.6 hello的执行流程................................................................. - 8 -
5.7 Hello的动态链接分析........................................................ - 8 -
5.8 本章小结............................................................................... - 9 -
第6章 hello进程管理.......................................................... - 10 -
6.1 进程的概念与作用............................................................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 10 -
6.3 Hello的fork进程创建过程............................................ - 10 -
6.4 Hello的execve过程........................................................ - 10 -
6.5 Hello的进程执行.............................................................. - 10 -
6.6 hello的异常与信号处理................................................... - 10 -
6.7本章小结.............................................................................. - 10 -
第7章 hello的存储管理...................................................... - 11 -
7.1 hello的存储器地址空间................................................... - 11 -
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 -
7.9动态存储分配管理.............................................................. - 11 -
7.10本章小结............................................................................ - 12 -
第8章 hello的IO管理....................................................... - 13 -
8.1 Linux的IO设备管理方法................................................. - 13 -
8.2 简述Unix IO接口及其函数.............................................. - 13 -
8.3 printf的实现分析.............................................................. - 13 -
8.4 getchar的实现分析.......................................................... - 13 -
8.5本章小结.............................................................................. - 13 -
结论............................................................................................... - 14 -
附件............................................................................................... - 15 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
Hello程序的生命周期遵循P2P(Program to Process)和020(Zero to Zero)的完整流程:
1、从代码到可执行文件(Program to Program)
预处理:hello.c通过gcc -E展开头文件与宏定义,生成hello.i。
编译:gcc -S将hello.i转换为汇编代码hello.s,实现高级语言到低级指令的转换。
汇编:gcc -c将hello.s编译为目标文件hello.o,包含机器码和符号表。
链接:gcc将hello.o与标准库动态链接,生成可执行文件hello,完成地址重定位。
2、从进程创建到终止(Process to Zero)
进程创建:执行./hello时,操作系统通过fork()创建子进程,execve()加载hello到内存,初始化堆栈、全局变量等。
程序运行:参数校验(argc!=5时退出),循环打印学号、姓名、手机号,间隔argv[4]秒(由atoi()转换)。getchar()阻塞等待输入,观察进程挂起状态(如Ctrl-Z发送SIGTSTP)。
进程终止:正常终止:循环结束或输入回车后,main()返回0,操作系统回收内存和资源。异常终止:Ctrl-C发送SIGINT强制终止进程。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:
处理器:13th Gen Intel(R) Core(TM) i9-13900HX 2.20 GHz
机带RAM:16.0GB
软件环境:Windows11 64位,VMware,Ubuntu 20.04.4 LTS
开发与调试工具: vim、objump、edb、gdb、gcc、readelf
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
表 1 中间文件的名字及作用
文件名 | 作用 |
hello.i | 预处理后的源代码文件,展开所有宏和头文件 |
hello.s | 编译生成的汇编代码文件,包含机器指令的文本表示 |
hello.o | 汇编生成的可重定位目标文件,包含机器码和符号表 |
hello.elf | 链接后的可执行文件 |
hello | 最终的可执行文件,由链接器生成,可在操作系统上直接运行 |
1.4 本章小结
本章作为全篇总纲,系统性地描述了Hello程序的生命周期及其开发环境,核心要点如下:
1、P2P与020流程:从代码(hello.c)到可执行文件(hello),再到进程的创建、执行与终止,完整展现了程序从静态到动态、从零到零的完整轨迹。
2、开发环境与工具链:明确了软硬件环境及关键工具(GCC、GDB、readelf),为后续章节的深入分析奠定技术基础。
3、中间文件的价值:通过 hello.i、hello.s、hello.o 等中间产物,揭示了编译流程的阶段性特征,为理解代码转换机制提供关键线索。
本章内容为后续章节的展开提供了全局视角与技术框架,从宏观层面回答了“Hello如何诞生与消亡”,具体技术细节将在后续章节逐一剖析。
第2章 预处理
2.1 预处理的概念与作用
预处理是C语言编译过程的第一阶段,由预处理器(cpp)负责处理源代码中的预处理指令,主要包括以下功能:
1、宏替换:将#define定义的宏展开为实际内容。例如#define MAX 100会被替换为字面量100。
2、头文件包含:将#include指令指定的头文件内容插入到当前文件中。例如#include <stdio.h>会将标准输入输出库的函数声明插入代码。
3、条件编译:根据#ifdef、#if等指令选择性地包含或排除代码块。例如通过#ifdef DEBUG控制调试代码是否参与编译。
4、删除注释:移除所有//和/* */注释内容。
5、行号标记:为后续编译阶段的错误提示添加行号标识。
作用:预处理后的代码是一个纯C语言文本文件(.i),移除了所有预处理指令,为后续编译阶段提供“干净”的输入。
2.2在Ubuntu下预处理的命令
在终端中输入以下命令gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i后对应文件夹内即出现预处理后的hello.i文件
图 1 预处理命令
2.3 Hello的预处理结果解析
生成的hello.i文件包含以下关键内容:
1、头文件展开
#include <stdio.h>被替换为stdio.h的全部内容,包含printf、getchar等函数的声明。
#include <unistd.h>展开后包含sleep函数的系统调用声明(如unsigned int sleep(unsigned int seconds);)。
#include <stdlib.h>包含exit和atoi的函数声明。
2、宏与条件编译处理
若代码中有#define定义的宏,预处理器会进行替换(本示例中未定义自定义宏)。
系统头文件内部可能包含条件编译指令(如#ifdef __x86_64__),预处理器根据当前环境选择有效代码块。
3、注释删除与代码简化
原始代码中的注释被完全移除。预处理后的代码仅保留有效语句
图 2 hello.i文件示例
2.4 本章小结
本章通过gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i命令对hello.c进行预处理,生成了扩展后的hello.i文件,并解析了其内容。预处理阶段完成了头文件插入、宏替换、条件编译选择等任务,为后续编译阶段提供了去除了冗余信息的“纯净”C代码。此阶段是程序从源代码到可执行文件的基石,确保编译器的输入完整且符合规范。
第3章 编译
3.1 编译的概念与作用
概念:
编译是将预处理后的C代码(.i文件)转换为汇编语言代码(.s文件)的过程,由编译器(如gcc)完成。
作用:
1、语法与语义分析:检查代码逻辑是否符合C语言规范(如类型匹配、作用域合法性)。
2、中间代码生成:将高级语言转换为中间表示(如抽象语法树)。
3、优化与汇编生成:对代码进行优化(如循环展开、常量折叠),并生成与目标架构匹配的汇编指令。
关键目标:生成低级的、可被汇编器直接处理的指令集,为后续生成机器码奠定基础。
3.2 在Ubuntu下编译的命令
在终端中输入以下命令gcc -S -m64 -no-pie -fno-PIC hello.i -o hello.s后对应文件夹内即出现编译后的hello.s文件
图 3 编译命令
3.3 Hello的编译结果解析
3.3.1 数据类型与变量
1、局部变量存储
变量 i(循环计数器)和 seconds(睡眠秒数)存储在栈帧中,通过 %rbp 偏移访问:
movl $0, -4(%rbp) # i = 0(初始化为0)
movl %eax, -8(%rbp) #计算结果存储
2、指针与数组访问
argv 指针数组通过寄存器 %rsi 传递,偏移访问参数:
movq -32(%rbp), %rax # argv 指针基地址 → %rax
addq $24, %rax # argv[3] 地址(偏移 24 字节)
movq (%rax), %rcx # argv[3] 值 → %rcx
3.3.2 算术与赋值操作
1、算术运算优化
i++被编译为直接修改栈内存:
addl $1, -4(%rbp) # i += 1
2、取模运算转换
atoi(argv[4]) % 5 未显式使用 idiv 指令,被优化为位操作
3.3.3 控制转移结构
1、条件分支 if (argc != 5)
参数检查通过 cmpl 和条件跳转实现:
cmpl $5, -20(%rbp) # 比较 argc 与 5
je .L2 # 相等则跳过错误处理
movl $.LC0, %edi # 错误信息地址加载
call puts # 输出错误提示
2、for 循环结构
循环条件 i < 10 被编译为计数器比较与跳转:
cmpl $9, -4(%rbp) # 比较 i 与 9(因 i 从 0 开始)
jle .L4 # i ≤ 9 时继续循环
3.3.4 函数调用与参数传递
1、printf 调用
遵循 System V AMD64 ABI,参数通过寄存器传递:
movq %rax, %rsi # argv[1] → %rsi
movq %rdx, %rcx # argv[2] → %rcx
movq %r8, %r8 # argv[3] → %r8
movl $.LC1, %edi # 格式字符串地址 → %edi
movl $0, %eax # 无浮点参数,清零 %eax
call printf@PLT # 调用 printf
2、sleep 调用
参数通过 %edi 传递整型值:
movq (%rax), %rdi # argv[4] → %rdi
call atoi@PLT # 字符串转整型
movl %eax, %edi # 结果 seconds → %edi
call sleep@PLT # 调用 sleep
3.3.5 内存与栈管理
1、栈帧分配
函数入口分配 32 字节栈空间:
subq $32, %rsp # 分配栈空间
2、参数保存
argc 和 argv 通过栈传递:
movl %edi, -20(%rbp) # argc 存储于 -20(%rbp)
movq %rsi, -32(%rbp) # argv 存储于 -32(%rbp)
3.3.6 系统调用与终止
1、异常终止
exit(1) 直接调用库函数:
movl $1, %edi # 参数 1 → %edi
call exit@PLT # 终止进程
2、正常返回
程序末尾通过 movl $0, %eax 和 ret 返回:
movl $0, %eax # 返回值 0 → %eax
leave # 恢复栈帧
ret
图 4 编译结果示例
3.4 本章小结
本章分析了编译阶段的核心作用:将预处理后的 C 代码转换为与ISA对应的汇编指令。编译器通过语法分析、中间代码生成及优化,实现了对数据类型、控制流、函数调用等结构的底层映射。生成的hello.s文件展示了for循环的跳转逻辑、printf的参数传递规则,以及算术运算的优化策略。这一过程为后续汇编阶段生成机器码奠定了基础,是程序从高级语言向硬件执行过渡的关键环节。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编是将 .s 汇编代码转换为 可重定位目标文件(.o) 的过程,由汇编器 as 执行,生成的 .o 文件是机器语言二进制文件,但尚未解决外部符号引用,需链接后执行。
作用:
1、指令编码:将助记符(如 movl、call)转换为机器码(二进制操作码)。
2、符号解析:标记未定义符号(如 printf@PLT)并生成重定位条目。
3、节(Section)划分:代码段(.text)、数据段(.data)等按功能分类存储。
4、生成重定位信息:为链接器提供地址修正依据。
4.2 在Ubuntu下汇编的命令
在终端中输入以下命令gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o后对应文件夹内即出现汇编后的hello.o文件
图 5 汇编命令
4.3 可重定位目标elf格式
1. ELF 头
Magic标识:7f 45 4c 46 表示合法的ELF文件。
文件类型:REL (可重定位文件),表明这是一个尚未链接的目标文件。
架构:Advanced Micro Devices X86-64,支持64位x86指令集。
节头表信息:共14个节头,起始位置为文件偏移1224字节。
入口点地址:0x0(可执行文件才有有效入口)。
图 6 ELF 头
2. 节头表
关键节及其作用:
表 2 关键节及其作用
节名 | 类型 | 作用 | 标志(Flags) |
.text | PROGBITS | 存储main函数的机器指令 | AX(可分配、可执行) |
.rodata | PROGBITS | 存储只读数据(如字符串常量) | A(可分配) |
.rela.text | RELA | 记录.text节的重定位条目 | INFO(链接用) |
.data | PROGBITS | 存储已初始化的全局变量(本例为空) | WA(可写、可分配) |
.bss | NOBITS | 存储未初始化的全局变量(本例为空) | WA(可写、可分配) |
.symtab | SYMTAB | 符号表,记录全局符号和外部引用 | - |
.strtab | STRTAB | 存储符号名称字符串 | - |
关键特性:
.text节大小为0x99字节,包含main函数的机器码。
.rodata节存储两个字符串常量(.LC0和.LC1),总大小0x40字节。
.rela.text和.rela.eh_frame是重定位表,记录需链接时修正的地址。
图 7 节头表
3. 重定位项目分析
使用readelf -r hello.o查看重定位条目.rela.text 节包含8个重定位条目,示例如下:
表 3 重定位条目示例
偏移量 | 类型 | 符号名称 | 加数 | 作用 |
0x1a | R_X86_64_32 | .rodata | 0 | 修正.LC0字符串的绝对地址引用 |
0x1f | R_X86_64_PLT32 | puts | -4 | 调用puts函数,PLT跳转地址修正 |
0x29 | R_X86_64_PLT32 | exit | -4 | 调用exit函数的PLT跳转地址修正 |
0x65 | R_X86_64_PLT32 | printf | -4 | 调用printf函数的PLT跳转地址修正 |
重定位类型解析:
R_X86_64_32:32位绝对地址引用,用于访问.rodata中的字符串常量。
R_X86_64_PLT32:32位PLT(过程链接表)相对偏移,用于动态链接库函数调用(如printf)。
.rela.eh_frame 节(异常处理帧重定位)包含1个条目,修正.eh_frame节对.text节的引用:
表 4 重定位条目
偏移量 | 类型 | 符号名称 | 加数 |
0x20 | R_X86_64_PC32 | .text | 0 |
图 8 重定位节
4. 符号表(.symtab)解析
符号表包含 17 项,关键符号如下:
表 5 关键符号
符号名称 | 类型 | 绑定 | 节索引 | 说明 |
main | FUNC | GLOBAL | 1 | 已定义的 main 函数 |
puts | NOTYPE | GLOBAL | UND | 未解析的 puts 函数引用 |
printf | NOTYPE | GLOBAL | UND | 未解析的 printf 函数引用 |
关键分类:
已定义符号:如 main,地址在 .text 节中明确指定。
未定义符号:如 puts、printf,需在链接阶段从库文件(如 libc.so)解析。
图 9 符号表
5. 其他关键节
.eh_frame:存储异常处理信息,用于栈展开(Stack Unwinding)。
.note.gnu.property:包含处理器特性标记(如 IBT 和 SHSTK),用于控制流完整性保护。
4.4 Hello.o的结果解析
图 10 反汇编结果
通过 objdump -d -r hello.o 反汇编 hello.o,结合第3章的 hello.s 进行对比分析,可深入理解汇编代码到机器指令的映射关系及重定位机制。
4.4.1机器指令与汇编代码映射
1、以下是关键指令的机器码与汇编代码对比分析:
函数入口与栈分配
hello.s 代码
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
hello.o 反汇编
0: endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 20 sub $0x20,%rsp
操作码:55 对应 push %rbp,48 89 e5 对应 mov %rsp,%rbp。
栈分配:sub $0x20,%rsp 对应 subq $32, %rsp(0x20=32),为局部变量分配空间。
2、参数检查与错误处理
hello.s 代码
cmpl $5, -20(%rbp)
je .L2
movl $.LC0, %edi
call puts
hello.o 反汇编
13: 83 7d ec 05 cmpl $0x5,-0x14(%rbp)
17: 74 14 je 2d <main+0x2d>
19: bf 00 00 00 00 mov $0x0,%edi ; R_X86_64_32 .rodata
1e: e8 00 00 00 00 callq 23 <main+0x23> ; R_X86_64_PLT32 puts
条件跳转:cmpl $0x5 对应 argc != 5 检查,je 2d 对应跳转至 .L2。
重定位条目:.rodata 字符串地址通过 R_X86_64_32 绝对地址修正。puts 调用通过 R_X86_64_PLT32 修正为 PLT 表偏移。
4.4.2控制转移与循环结构
1、for 循环实现
hello.s 代码
jmp .L3
.L4:
addl $1, -4(%rbp)
.L3:
cmpl $9, -4(%rbp)
jle .L4
hello.o 反汇编
34: eb 51 jmp 87 <main+0x87>
83: 83 45 fc 01 addl $0x1,-0x4(%rbp)
87: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
8b: 7e a9 jle 36 <main+0x36>
循环计数器:addl $0x1 对应 i++,cmpl $0x9 对应 i < 10。
跳转偏移:jle 36 的机器码 7e a9 中,a9 是补码偏移量(实际偏移为 -0x57,指向 0x36)。
4.4.3函数调用与参数传递
1、printf 调用
hello.s 代码
movq (%rsi), %rdx # argv[1]
movq 8(%rsi), %rcx # argv[2]
movq 16(%rsi), %r8 # argv[3]
call printf
hello.o 反汇编
5a: bf 00 00 00 00 mov $0x0,%edi ; R_X86_64_32 .rodata+0x30
5f: b8 00 00 00 00 mov $0x0,%eax
64: e8 00 00 00 00 callq 69 <main+0x69> ; R_X86_64_PLT32 printf
参数传递:%rdi 存储格式字符串地址(.rodata+0x30 对应 .LC1)。%rsi、%rdx、%rcx 分别传递 argv[1]、argv[2]、argv[3]。
重定位:printf 调用地址需通过 PLT 表修正。
2、sleep 调用
hello.s 代码
movl %eax, %edi # seconds → %edi
call sleep
hello.o 反汇编
7c: 89 c7 mov %eax,%edi
7e: e8 00 00 00 00 callq 83 <main+0x83> ; R_X86_64_PLT32 sleep
参数存储:atoi 返回值通过 %eax 传递至 %edi。
调用机制:sleep 地址由链接器解析并填充。
4.4.4操作数与重定位差异
1、绝对地址引用
mov $0x0,%edi ; R_X86_64_32 .rodata
汇编代码:直接使用符号 .LC0。
机器码:占位符 00 00 00 00 需链接时替换为 .rodata 的实际地址。
2、相对地址跳转
jle 36 <main+0x36> ; 机器码 7e a9
偏移计算:0x8b(当前地址) + 0xa9(偏移) → 0x36(目标地址)。
补码表示:0xa9 对应十进制 -87,实际跳转偏移为 0x8b - 0x55 = 0x36。
5. 机器语言与汇编语言映射总结
表 6 机器语言与汇编语言映射总结
特征 | 汇编语言表现 | 机器语言表现 |
操作数 | 符号化地址(如 .LC0) | 占位符(00 00 00 00) |
函数调用 | call printf@PLT | e8 00 00 00 00 + 重定位条目 |
条件跳转 | 标签(.L4) | 相对偏移(7e a9) |
立即数 | $0x5 | 直接编码(83 7d ec 05) |
6. 关键重定位类型解析
表 7 关键重定位类型解析
重定位条目 | 类型 | 作用 |
R_X86_64_32 | 绝对地址引用 | 修正数据段(如 .rodata)地址 |
R_X86_64_PLT32 | PLT 表相对偏移 | 动态链接库函数(如 printf) |
4.5 本章小结
本章分析了汇编阶段将hello.s转换为可重定位目标文件hello.o的过程,通过ELF格式解析发现其包含代码(.text)、只读数据(.rodata)及重定位表(.rela.text)。反汇编显示,外部函数调用(如printf)和全局数据引用通过占位符标记,需链接时修正地址。机器码与汇编指令映射清晰,分支跳转转为偏移量,函数调用依赖重定位条目,为链接阶段奠定基础。
第5章 链接
5.1 链接的概念与作用
概念:
链接是将多个可重定位目标文件(.o)和库文件合并为可执行目标文件(ELF) 的过程,
作用:
符号解析:绑定模块间的函数和全局变量引用(如 printf 到 libc.so)。
重定位:合并代码段、数据段,修正符号的虚拟地址(如 call printf 的跳转地址)。
内存布局:定义程序入口(_start)和段对齐规则(.text、.data 的加载地址)。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
在终端中输入以下命令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后对应文件夹内即出现链接后的hello文件
图 11 链接命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.3.1 elf头
在终端输入readelf -h hello来解析elf文件头,结果如下。
图 12 ELF头
5.3.2 section头
在终端输入readelf -S hello查看节头,结果如下。
图 13 节头表
5.3.3 符号表
在终端输入命令readelf -s hello并回车,结果如下。
图 14 符号表
5.3.4 可重定位段信息
在终端输入readelf -r hello并回车可查看可重定位段信息,结果如下:
图 15 重定位节
5.4 hello的虚拟地址空间
使用gdb/edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
使用edb打开hello文件查看到虚拟地址的情况如图
图 16 虚拟地址情况
通过下图中地址列的地址与上图的地址相对应可知虚拟地址空间内各段的信息
图 17 节头表
5.5 链接的重定位过程分析
通过对比 hello.o 与 hello 的反汇编结果,结合重定位条目,可深入理解链接器如何修正符号地址并生成可执行文件。
1. 符号解析与地址修正
hello.o 中的未解析符号:
callq 0x0 <main+0x23> ; R_X86_64_PLT32 puts-0x4
反汇编特征:callq 的操作数为 0x0(占位符),需通过 .rela.text 中的重定位条目修正。
重定位条目:
Offset: 0x1f Type: R_X86_64_PLT32 Symbol: puts
hello中的修正结果:
callq 0x401090 <puts@plt>
修正逻辑:
链接器根据 R_X86_64_PLT32 类型,计算 puts 的 PLT 表入口地址(0x401090)。
将 callq 指令的操作数替换为 0x401090 - 当前指令地址(0x401143) - 4 的相对偏移。
2. 关键重定位类型解析
R_X86_64_32(绝对地址引用)
示例:.rodata 中的字符串地址修正。
hello.o
mov $0x0,%edi ; R_X86_64_32 .rodata
hello
mov $0x402008,%edi ; 修正为 .rodata 中字符串的实际地址
机制:链接器将 .rodata 段的起始地址(0x402000)与偏移量(0x8)相加,得到绝对地址 0x402008。
R_X86_64_PLT32(动态链接函数调用)
示例:printf 的 PLT 调用修正。
hello.o
callq 0x0 <main+0x69> ; R_X86_64_PLT32 printf
hello
callq 0x4010a0 <printf@plt>
机制:
链接器在 .plt.sec 节中为 printf 分配 PLT 入口(0x4010a0)。
修正 callq 指令的偏移量,使其跳转到 PLT 表项,通过延迟绑定解析实际函数地址。
3. 数据段与代码段合并
hello 的虚拟地址布局:
.text 起始于 0x401000,.rodata 起始于 0x402000。
重定位影响:所有对 .rodata 的引用(如字符串地址)均被修正为绝对地址。
示例:
hello.o 中引用 argv[3]
mov -0x20(%rbp),%rax
add $0x18,%rax ; argv[3] 的偏移
hello 中无需重定位(局部变量地址在链接时已确定)
4. 动态链接与静态链接差异
静态链接:所有库函数代码被直接合并到可执行文件中,无 PLT/GOT 表。
动态链接(本例):
函数调用通过 PLT 表跳转(如 printf@plt),首次调用时触发动态链接器解析真实地址并更新 GOT 表。
重定位条目中 R_X86_64_PLT32 类型支持延迟绑定,降低启动开销。
图 18 hello反汇编结果示例
5.6 hello的执行流程
通过 gdb 跟踪执行流程:
加载阶段:内核加载 hello 到内存,动态链接器解析 .interp 并加载依赖库。
入口函数:_start(在 crt1.o 中定义)调用 __libc_start_main。
主函数执行:__libc_start_main 初始化环境后调用 main。
终止阶段:main 返回后调用 exit 系统调用(SYS_exit)。
5.7 Hello的动态链接分析
1. 动态链接的核心机制
PLT(过程链接表):存储跳转到GOT的指令,首次调用函数时触发动态链接器解析实际地址。
GOT(全局偏移表):存储函数实际地址,初始指向PLT解析代码,解析后更新为真实函数地址。
2. 动态链接过程验证(以printf为例)
由图可知执行printf前与执行后0x404020的值发生变化
图 19 gdb调试过程
5.8 本章小结
本章分析了链接阶段的核心机制:
- 符号解析与重定位:链接器修正外部函数与数据的地址,生成可执行文件。
- 虚拟地址空间布局:代码、数据段按ELF规范映射到进程地址空间。
- 动态链接:通过PLT/GOT实现共享库的延迟绑定,提升灵活性与内存效率。
链接是程序从模块化编译到可执行的关键桥梁,确保代码与资源的全局一致性。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是程序在操作系统中的一次动态执行实例
作用:
资源分配:操作系统通过进程分配 CPU、内存、I/O 等资源。
任务隔离:每个进程拥有独立的虚拟地址空间,避免程序间相互干扰。
多任务调度:通过进程切换实现并发执行,提升系统效率。
6.2 简述壳Shell-bash的作用与处理流程
Bash 的作用:
命令解析:解析用户输入的命令(如 ./hello),拆分为可执行文件与参数。
进程管理:通过 fork 和 execve 创建子进程执行程序,支持前台/后台作业控制。
环境管理:维护环境变量(如 PATH)和信号处理(如 Ctrl-C 转发 SIGINT)。
处理流程:
输入命令 → 解析参数 → 查找可执行文件 → fork子进程 → execve加载程序 → 等待或后台运行
6.3 Hello的fork进程创建过程
当用户在终端输入 ./hello 时,Bash 的执行流程如下:
1、fork 系统调用:Bash 调用 fork() 创建子进程,子进程复制父进程的地址空间。
2、进程标识:子进程获得独立 PID,父进程通过 waitpid 监控子进程状态。
3、执行权限检查:子进程检查 hello 文件权限,若合法则进入 execve 阶段。
6.4 Hello的execve过程
execve()系统调用将hello加载到子进程内存空间:
1、参数传递:接收命令行参数(argv)和环境变量(envp)。
2、加载程序:读取hello的ELF头,映射代码段(.text)、数据段(.data)到内存。
3、重置状态:初始化寄存器(如%rip指向_start入口),清空信号处理器。
4、开始执行:跳转到main函数,启动程序逻辑。
6.5 Hello的进程执行
Hello进程的执行由操作系统的进程调度机制动态管理,涉及上下文切换、时间片分配及用户态/核心态转换,具体过程如下:
1. 进程上下文与调度
进程上下文(Context):
保存Hello进程的运行状态,包括寄存器值(如%rip、%rsp)、内存页表、打开的文件描述符等。当时间片耗尽或发生阻塞时,内核通过上下文切换保存当前上下文,并加载其他进程的上下文。
时间片调度:
操作系统为Hello分配一个时间片(如10ms),若在此时间内未完成执行(如循环未结束),调度器将Hello置入就绪队列,切换至其他进程;若时间片内完成CPU任务(如计算),Hello继续占用CPU。
2. 用户态与核心态转换
Hello进程的执行在两种模式下交替进行:
用户态(User Mode):
执行用户级代码(如printf格式化输出、i++运算),权限受限,无法直接访问硬件。
核心态(Kernel Mode):
在以下场景触发转换:
系统调用:如printf调用write、sleep调用nanosleep,通过syscall指令进入内核。
中断/异常:时钟中断(时间片耗尽)、Ctrl-C(SIGINT信号)触发陷阱。
示例流程:Hello调用sleep(atoi(argv[4])),触发nanosleep系统调用。CPU从用户态切换至核心态,内核处理睡眠请求,将Hello加入等待队列,释放CPU。
定时器到期后,内核发送SIGALRM唤醒Hello,重新调度执行。
3. 进程调度策略
调度时机:主动让出CPU:Hello调用阻塞操作(如sleep、getchar)。
被动抢占:时间片耗尽、更高优先级进程就绪。
调度算法:采用完全公平调度(CFS),按Hello的虚拟运行时间分配CPU,确保多进程公平性。
4. 执行流程示例
假设Hello的参数为秒数=3,其执行流程如下:
时间片内执行:打印10次"Hello ...",每次循环占用CPU时间片。若单次循环未超时,调度器维持Hello运行。调用sleep进入阻塞:sleep(3)触发系统调用,Hello让出CPU,内核启动定时器。调度器选择其他进程执行。唤醒与恢复:3秒后内核发送SIGALRM,Hello从阻塞态转为就绪态。调度器分配时间片,Hello继续执行后续循环。
5. 关键数据结构
任务队列:
就绪队列:存放等待CPU的进程(如Hello恢复执行时)。
等待队列:存放阻塞进程(如Hello在sleep期间)。
进程控制块(PCB):存储Hello的PID、状态、优先级、上下文等信息,供调度器决策。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1异常类型与信号
表 8 异常类型
异常类型 | 触发场景 | 产生信号 | 默认处理 |
中断(Interrupt) | 用户按下 Ctrl-C(终止) | SIGINT | 终止进程 |
中断(Interrupt) | 用户按下 Ctrl-Z(暂停) | SIGTSTP | 暂停进程,转为后台作业 |
故障(Fault) | 非法内存访问(如空指针) | SIGSEGV | 终止进程并生成 core dump |
故障(Fault) | 除零操作 | SIGFPE | 终止进程 |
陷阱(Trap) | 调试断点或系统调用 | 无直接信号 | 由调试器处理 |
中止(Abort) | 不可恢复错误(如非法指令) | SIGILL | 终止进程 |
6.6.2 运行结果
1、正常运行
正常运行程序输出10次后按下回车键结束
图 20 正常运行结果
2、不停乱按
不停乱按时屏幕上会显示出按下的内容但不会影响程序的正常运行
图 21 不停乱按的结果
3、Ctrl+c
在键盘中输入Ctrl+C后向前台发送SIGINT信号,程序运行终止。
图 22 按下Ctrl+c的结果
4、Ctrl+z
按下Ctrl+z,进程收到SIGSTP信号,屏幕上显示提示信息, hello进程被挂起。
图 23 按下Ctrl+z的结果
此时输入ps命令可以看到当前的进程
图 24 输入ps命令的结果
输入jobs命令可以看到被停止的hello进程
图 25 输入jobs命令的结果
输入pstree可以看到进程的树状图
图 26 进程树状图示例
输入fg命令可以使hello进程继续执行
图 27 输入fg命令的结果
输入kill指令及对应的PID后可以杀死对应的进程
图 28 输入kill命令的结果
6.7本章小结
本章分析了hello的进程管理全流程:
进程创建:通过fork复制进程,execve加载程序。
进程执行:时间片调度与上下文切换实现并发,用户态与核心态分离保障系统安全。
信号处理:支持中断挂起(SIGTSTP)、终止(SIGINT)等操作,结合Shell命令(jobs、fg)实现进程控制。
进程管理是操作系统资源分配与多任务并发的核心机制,保障了程序的稳定运行与用户交互的灵活性。
第7章 hello的存储管理
7.1 hello的存储器地址空间
地址类型及转换流程:
- 逻辑地址:程序中的段选择符(Segment Selector)与偏移量(Offset),如 movl $0x5, 0x8048000 中的 0x8048000。
- 线性地址:逻辑地址经段式管理转换后的连续地址空间。现代操作系统通常使用平坦模式(Flat Mode),段基址为0,逻辑地址直接等于线性地址。
- 虚拟地址(VA):进程视角的地址空间,由页式管理映射到物理地址。
- 物理地址(PA):实际内存芯片中的地址,通过 MMU 转换得到。
示例:
hello 中的 argv[1] 变量在虚拟地址 0x7ffd4a3c,经页表映射后对应物理地址 0x12f8c000。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel x86-64 架构中段式管理流程:
- 段选择符:16 位字段,指向全局描述符表(GDT)或局部描述符表(LDT)中的段描述符。
- 段描述符:包含段基址、段限长和访问权限。
- 线性地址计算:
线性地址 = 段基址(段描述符中) + 偏移量(逻辑地址)
现代简化:
64 位模式下段基址默认为 0,逻辑地址直接作为线性地址使用。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理流程:
- 虚拟地址划分:64 位 VA 分为四级页表索引(9 bits × 4)和页内偏移(12 bits)。
- 页表查询:
CR3 寄存器:存储顶级页目录(PML4)的物理地址。
依次查询 PML4 → PDPT → PD → PT,最终获得物理页框号(PFN)。
- 物理地址生成:
PA = PFN × 4KB(页大小) + 页内偏移
图 29 二级页表的结构
7.4 TLB与四级页表支持下的VA到PA的变换
加速机制:
- TLB(Translation Lookaside Buffer):缓存最近使用的 VA→PA 映射,命中时直接返回 PA。
- 四级页表查询流程:
TLB Miss:依次访问 PML4(CR3指向)→ PDPT → PD → PT → 物理页。
TLB Hit:直接使用缓存结果,减少内存访问次数。
性能影响:
TLB 命中率直接影响程序性能,hello 的循环结构因局部性高,TLB 命中率较高。
图 30 加入TLB之后的完整地址映射
7.5 三级Cache支持下的物理内存访问
Cache层次结构:
- L1 Cache:分指令与数据 Cache,访问延迟 1-3 周期。
- L2 Cache:统一缓存,延迟约 10 周期。
- L3 Cache:共享缓存,延迟约 30-40 周期。
访问流程:
CPU 访问 PA 时,依次查询 L1 → L2 → L3 → 主内存。 示例:
hello 的 printf 函数因频繁访问格式字符串,数据可能缓存在 L1 Cache 中。
图 31 三级cache结构图
7.6 hello进程fork时的内存映射
写时复制(Copy-On-Write):
- fork创建子进程:复制父进程页表,共享物理页(标记为只读)。
- 写操作触发:子进程尝试修改共享页时,触发缺页异常,内核复制新物理页并更新页表。
优势:减少内存复制开销,hello 的父子进程初始共享代码段和数据段。
7.7 hello进程execve时的内存映射
地址空间替换流程:
- 释放旧空间:销毁当前进程的代码、数据、堆栈段。
- 加载新映像:
映射 .text(代码段)、.rodata(只读数据)到虚拟地址空间。
初始化 .bss(未初始化数据)为零页。
- 设置栈与堆:分配栈空间(命令行参数、环境变量)和初始堆空间。
7.8 缺页故障与缺页中断处理
处理流程:
- 触发条件:访问未映射的 VA 或权限不足(如写只读页)。
- 内核响应:
分配物理页框,若需加载数据则从磁盘(如文件或交换空间)读取。
更新页表项,标记为有效并设置权限。
- 恢复执行:重新执行触发缺页的指令。
示例:hello 首次访问全局变量时触发缺页,内核从二进制文件加载数据到内存。
图 32 缺页中断处理
7.9动态存储分配管理
动态内存管理策略:
- 隐式空闲链表:通过头部大小字段隐式链接空闲块,合并相邻空闲块减少碎片。
- 显式空闲链表:维护双向链表直接管理空闲块,分配速度快但需额外空间。
- 分离空闲链表:按大小分类空闲块(如小块、中块、大块),提升分配效率。
printf与malloc:printf 内部可能调用 malloc 分配缓冲区,使用分离空闲链表策略优化小内存分配。
7.10本章小结
本章剖析了 hello 的存储管理全流程:
- 地址转换:从逻辑地址到物理地址的段式与页式映射,TLB 加速关键路径。
- 内存层次:三级 Cache 与主存协同,提升数据访问效率。
- 进程内存:fork 的写时复制与 execve 的地址空间重建,实现高效进程管理。
- 异常处理:缺页中断动态扩展内存,保障程序灵活运行。
存储管理通过硬件(MMU、TLB、Cache)与操作系统(页表、缺页处理、动态分配)的深度协同,使 hello 程序在有限的物理资源下,高效、安全地完成从代码到执行的蜕变。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
Linux采用统一设备模型,将硬件设备抽象为文件,通过文件操作接口管理设备,核心机制如下:
1、设备文件:
所有设备(如键盘、显示器、磁盘)在/dev目录下以文件形式存在(如/dev/tty表示终端)。
设备文件分为字符设备(按字节流访问,如键盘)和块设备(按块访问,如硬盘)。
2、文件操作接口:
使用与普通文件相同的系统调用(如open、read、write、close)操作设备。
通过文件描述符标识设备,实现统一的IO管理。
8.2 简述Unix IO接口及其函数
Unix定义了以下核心IO函数:
1、文件操作函数:
int open(const char *path, int flags, mode_t mode):打开设备或文件,返回文件描述符。
ssize_t read(int fd, void *buf, size_t count):从文件描述符读取数据到缓冲区。
ssize_t write(int fd, const void *buf, size_t count):将缓冲区数据写入文件描述符。
int close(int fd):关闭文件描述符。
2、标准文件描述符:
0(标准输入,stdin)、1(标准输出,stdout)、2(标准错误,stderr)。
8.3 printf的实现分析
printf通过以下步骤实现格式化输出:
1、格式化字符串生成:
调用vsprintf将格式化参数(如%s、%d)转换为字符串,并存入用户缓冲区。
2、系统调用写入:
调用write(1, buf, len),通过syscall指令触发系统调用,陷入内核态。
3、字符显示驱动:
字模库映射:将ASCII字符转换为点阵数据(如16×16像素的字模)。
写入显存(VRAM):将点阵数据按像素RGB值写入显存对应位置。
屏幕刷新:显示芯片以固定频率(如60Hz)逐行扫描VRAM,输出到显示器。
系统调用流程:
mov $1, %rax // syscall number for write
mov $1, %rdi // fd = stdout
mov buf, %rsi // buffer address
mov len, %rdx // buffer length
syscall // 触发系统调用
8.4 getchar的实现分析
getchar通过以下流程读取键盘输入:
1、键盘中断处理:
用户按下按键时,键盘控制器发送中断信号(IRQ1)。
中断处理程序读取键盘扫描码,转换为ASCII码(如回车键→\n),存入内核输入缓冲区。
2、系统调用读取:
getchar调用read(0, &c, 1),从标准输入读取一个字符。
若缓冲区为空,进程阻塞等待;若检测到回车符(\n),返回缓冲区内容。
3、异步IO机制:
输入操作通过异步中断触发,实现非阻塞响应。
示例流程:
char c;
while (read(0, &c, 1) > 0) { // 阻塞直到输入回车
// 处理字符c
}
8.5本章小结
本章分析了Linux的IO管理机制及其在hello中的应用:
- 设备抽象:通过文件模型统一管理硬件设备,简化用户程序开发。
- 核心IO接口:printf依赖格式化字符串生成与显存写入,getchar通过中断与缓冲区实现异步输入。
- 系统调用:用户态通过syscall进入内核态,完成底层硬件操作。
Linux的IO设计平衡了灵活性与效率,是操作系统资源管理的核心组成部分。
结论
1. Hello程序所经历的过程
从代码到进程(P2P):
编写:通过C语言编写hello.c,定义程序逻辑与数据操作。
预处理:展开头文件与宏,生成hello.i。
编译:将高级代码转换为汇编指令(hello.s)。
汇编:生成可重定位目标文件hello.o,包含机器码与符号表。
链接:合并库文件与目标文件,解析符号,生成可执行文件hello。
进程创建:Shell通过fork创建子进程,execve加载hello到内存。
从进程到终止(020):
进程执行:CPU时间片调度、上下文切换、用户态与核心态转换。
输入输出:通过printf与getchar调用系统函数,与终端交互。
信号处理:响应SIGINT(Ctrl-C)、SIGTSTP(Ctrl-Z)等信号,控制进程状态。
资源回收:进程终止后,操作系统回收内存、文件描述符等资源。
2、感悟
通过感受hello程序的一生,我深刻体会到hello程序的生命周期是计算机系统各层级协同工作的缩影,编译器将逻辑映射为指令,链接器整合资源,操作系统管理进程与内存,硬件高效执行。每一层抽象既隐藏复杂性,又暴露可控接口,体现了“简单性”与“灵活性”的哲学平衡。未来计算机系统的优化方向,需在保持兼容性的同时,探索智能化、自适应化的设计,以应对异构计算与实时性需求的挑战。
附件
表 9 中间产物文件名及作用
文件名 | 作用 |
hello.i | 预处理后的源代码文件,展开所有宏和头文件 |
hello.s | 编译生成的汇编代码文件,包含机器指令的文本表示 |
hello.o | 汇编生成的可重定位目标文件,包含机器码和符号表 |
hello.elf | 链接后的可执行文件 |
hello | 最终的可执行文件,由链接器生成,可在操作系统上直接运行 |
参考文献
[1] Linux kill 命令 [EB/OL].Linux kill 命令 | 菜鸟教程.[2025-05-07].
[2] Linux缓存之TLB [EB/OL].https://blog.csdn.net/tiantianhaoxinqing__/article/details/125772525. 2022-07-16[2025-05-07].
[3] 操作系统的内存管理——页式、段式管理、段页式管理 [EB/OL].操作系统的内存管理——页式、段式管理、段页式管理-CSDN博客. 2022-05-08[2025-05-07].
[4] printf 函数实现的深入剖析 [EB/OL].[转]printf 函数实现的深入剖析 - Pianistx - 博客园. [2025-05-07].
[5] 计组复习(四):cache,虚拟内存,页表与TLB [EB/OL].计组复习(四):cache,虚拟内存,页表与TLB_tlb与页表-CSDN博客. 2024-12-10[2025-05-07].
[6] malloc原理学习:隐式空闲链表 [EB/OL].https://blog.csdn.net/qqliyunpeng/article/details/91407705. 2019-06-10[2025-05-07].
[7] 内存管理:隐式空闲链表 [EB/OL].https://zhuanlan.zhihu.com/p/376217387. [2025-05-07].
[8] Linux的IO模型进化详解 [EB/OL].https://developer.aliyun.com/article/726412. 2019-11-08[2025-05-07].
[9] 操作系统---(35)缺页中断与缺页中断处理过程 [EB/OL].https://blog.csdn.net/qq_43101637/article/details/106646554. 2020-06-09[2025-05-07].
[10] 一文搞懂Linux内核缺页中断处理 [EB/OL].https://zhuanlan.zhihu.com/p/540850512. [2025-05-07].