本文以“程序人生-Hello's P2P”程序为例,分析了C语言程序从源代码到可执行进程的全生命周期。通过预处理、编译、汇编、链接四阶段,结合Ubuntu环境下的工具链,生成可执行文件;详细探讨了进程管理机制、存储管理及动态链接原理。报告结合实验截图与代码解析,阐述了操作系统对进程资源的分配与回收,以及硬件与软件协同实现程序高效执行的核心机制。最终,程序通过系统调用完成输入输出,并在执行结束后由操作系统回收资源,实现“从无到有再归于无”的闭环。
关键词:预处理;编译;进程管理;虚拟地址;ELF格式
目 录
第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 本章小结............................................................................... - 6 -
第3章 编译................................................................................... - 7 -
3.1 编译的概念与作用............................................................... - 7 -
3.2 在Ubuntu下编译的命令.................................................... - 7 -
3.3 Hello的编译结果解析........................................................ - 7 -
3.4 本章小结............................................................................... - 9 -
第4章 汇编................................................................................. - 10 -
4.1 汇编的概念与作用............................................................. - 10 -
4.2 在Ubuntu下汇编的命令.................................................. - 10 -
4.3 可重定位目标elf格式...................................................... - 10 -
4.4 Hello.o的结果解析........................................................... - 14 -
4.5 本章小结............................................................................. - 15 -
第5章 链接................................................................................. - 16 -
5.1 链接的概念与作用............................................................. - 16 -
5.2 在Ubuntu下链接的命令.................................................. - 16 -
5.3 可执行目标文件hello的格式......................................... - 16 -
5.4 hello的虚拟地址空间....................................................... - 18 -
5.5 链接的重定位过程分析..................................................... - 19 -
5.6 hello的执行流程............................................................... - 20 -
5.7 Hello的动态链接分析...................................................... - 21 -
5.8 本章小结............................................................................. - 21 -
第6章 hello进程管理.......................................................... - 23 -
6.1 进程的概念与作用............................................................. - 23 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 23 -
6.3 Hello的fork进程创建过程............................................ - 23 -
6.4 Hello的execve过程........................................................ - 23 -
6.5 Hello的进程执行.............................................................. - 24 -
6.6 hello的异常与信号处理................................................... - 25 -
6.7本章小结.............................................................................. - 26 -
第7章 hello的存储管理...................................................... - 27 -
7.1 hello的存储器地址空间................................................... - 27 -
7.2 Intel逻辑地址到线性地址的变换-段式管理.................. - 27 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 27 -
7.4 TLB与四级页表支持下的VA到PA的变换................... - 27 -
7.5 三级Cache支持下的物理内存访问................................ - 27 -
7.6 hello进程fork时的内存映射......................................... - 28 -
7.7 hello进程execve时的内存映射..................................... - 28 -
7.8 缺页故障与缺页中断处理................................................. - 28 -
7.9本章小结.............................................................................. - 28 -
参考文献....................................................................................... - 32 -
第1章 概述
1.1 Hello简介
从文本程序(hello.c)到可执行进程的生命周期中,Hello经历了预处理、编译)、汇编(、链接四阶段。在Shell中执行时,操作系统通过fork()创建子进程副本,再经execve()加载Hello的代码段和数据段到进程地址空间,借助mmap()映射动态库与内存区域。进程管理分配时间片,CPU通过取指-译码-执行流水线运行Hello的指令,存储管理通过MMU/TLB/页表完成虚拟地址到物理地址的转换,三级Cache加速数据访问,最终Hello在硬件上短暂执行并输出结果。
Hello的生存始于操作系统从“零资源”为其分配进程控制块、虚拟内存空间和文件描述符,通过存储管理实现VA→PA映射,依赖页表、Cache和Pagefile协调内存与磁盘。I/O管理驱动键盘输入启动进程,显卡渲染输出结果,信号机制处理可能的异常。执行结束后,OS回收所有资源:释放物理内存、关闭文件、销毁页表与进程上下文,使系统回归“零占用”状态。整个生命周期中,硬件与软件协同实现Hello从无到有再归于虚无的闭环。
1.2 环境与工具
环境:ubuntu22.04
工具:gdb,gcc等
1.3 中间结果
hello.i作用:预处理后的文件,包含宏展开、头文件合并、注释删除后的纯C代码。
hello.s作用:编译生成的汇编代码文件,将C代码转换为x86-64架构的汇编指令。
hello.o作用:汇编生成的可重定位目标文件(ELF格式),包含机器码和未解析符号。
hello作用:链接后的最终可执行文件,整合了目标文件与库函数。
hello.o-r.txt等文件作用:readelf处理后包含头信息、属性段等ELF格式的文件。
hello-objump.txt与hello.o-objdump.txt作用:反汇编代码,展示机器指令与汇编代码的对应关系。
gdb.txt作用:gdb的日志,展现了相关内存地址。
1.4 本章小结
Hello的生命周期展现了计算机系统中 软硬件协同 的完整闭环——从编译器工具链到操作系统资源管理,最终在硬件上实现程序的高效执行与资源回收,体现了计算机系统分层抽象与动态管理的核心思想。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是C语言编译流程的第一个阶段,由预处理器(C Preprocessor, CPP)执行,负责处理源代码中以 # 开头的指令(如宏定义、头文件包含、条件编译等),生成“纯C代码”供后续编译使用。
作用:
1.宏展开:替换代码中的宏(如 #define 定义的常量或函数宏)。
2.头文件包含:将 #include 指令指向的头文件(如 stdio.h)内容直接插入到源文件中。
3.条件编译:根据 #ifdef/#ifndef 等指令选择性保留或删除代码块。
4.删除注释:清除所有单行(//)和多行(/* ... */)注释。
5.行号标记:添加 #line 指令以保留调试信息(如源文件行号)。
2.2在Ubuntu下预处理的命令
指令:gcc -E -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello.i
以下为预处理截图:
图1 在Ubuntu下预处理hello.c的截图
2.3 Hello的预处理结果解析
预处理后的 hello.i 文件主要包含以下关键内容:
- 头文件展开
#include <stdio.h>、#include <unistd.h>、#include <stdlib.h> 被替换为对应头文件的实际内容。例如:
stdio.h 展开后包含标准输入输出函数(如 printf)的声明、FILE 结构体定义等。
unistd.h 展开后包含系统调用(如 sleep、getchar)的声明。
stdlib.h 展开后包含 exit、atoi 等函数的声明。
- 预处理标记
以 # 开头的行是预处理器的行号标记,用于保留原始代码的行号信息,方便调试和错误定位。
- 原始代码保留
main 函数代码(如 printf、for 循环、sleep 等)完整保留在文件末尾,但注释已被删除。
- 类型与宏定义
插入系统级类型定义(如 size_t、ssize_t、__gnuc_va_list 等)。
展开标准库宏(如 __attribute__ 修饰符、__restrict 等)。
- 编译器兼容性处理
包含与平台相关的定义(如 x86_64-linux-gnu 路径、__USE_GNU 宏等),确保代码在不同环境下编译一致。
2.4 本章小结
预处理是C程序编译的首要阶段,由预处理器解析以 # 开头的指令,完成宏替换、头文件展开(如插入 stdio.h 中的函数声明)、条件编译(#ifdef 等)、删除注释及添加行号标记,生成纯C代码文件(如 hello.i)。通过 gcc -E 命令触发,其输出整合了系统类型定义、平台兼容性处理(如路径宏)和用户代码(保留逻辑但去注释),为后续编译提供规范化输入,确保跨环境一致性与调试便利性。
第3章 编译
3.1 编译的概念与作用
概念:编译是将预处理后的 .i 文件(纯C代码)转换为汇编语言文件 .s 的过程,由编译器(如GCC的 cc1)完成。此阶段对代码进行词法分析、语法分析、语义检查及中间代码优化,最终生成与目标机器架构(如x86-64)对应的汇编指令。
作用:
- 语法检查:验证代码是否符合C语言规范,检测语法错误(如类型不匹配、未定义变量)。
- 优化代码:根据编译选项(如 -Og)进行局部优化(删除冗余计算、简化逻辑),提升程序效率。
- 生成汇编代码:将高级C代码翻译为低级汇编指令,与目标CPU架构(如寄存器操作、内存寻址模式)紧密相关。
- 符号解析:为全局变量、函数生成符号表,为后续链接阶段提供地址引用信息。
3.2 在Ubuntu下编译的命令
gcc -S -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.i -o hello.s
图2 在Ubuntu下编译hello.i的截图
3.3 Hello的编译结果解析
3.3.1 数据与变量
1.常量:
字符串常量存储在 .rodata 只读数据段:
.LC0: 中文错误提示字符串(UTF-8编码的八进制表示)。
.LC1: "Hello %s %s %s\n" 格式字符串。
立即数:如 $5(argc 参数检查)、$0(循环计数器初始化)。
2.变量:
局部变量:
%ebp 作为循环计数器 i,对寄存器进行分配。
%rbx 保存 argv 指针,通过偏移访问参数(如 16(%rbx) 对应 argv[2])。
-
-
- 赋值与操作符
-
1.赋值操作:
movl $0, %ebp:初始化 i = 0。
addl $1, %ebp:i++,循环计数器自增。
2.算术操作:
addq $8, %rsp:栈指针调整(rsp += 8)。
subq $8, %rsp:栈空间分配(rsp -= 8)。
3.逻辑与位操作:
cmpl $5, %edi:比较 argc 与 5。
jne .L6:若 argc != 5 则跳转,进行逻辑判断。
-
-
- 类型转换
-
显式转换:
call strtol:将 argv[4] 字符串显式转换为 long 类型,对atoi 底层实现。
-
-
- 控制转移
-
1.条件分支:
cmpl $5, %edi → jne .L6:if (argc != 5) 执行错误处理。
cmpl $9, %ebp → jle .L3:for (i=0; i<=9; i++) 控制循环。
2.循环结构:
.L2 和 .L3 标签实现 for 循环逻辑。
3.函数返回:
movl $0, %eax → ret:return 0(返回值通过 %eax 传递)。
-
-
- 函数操作
-
1.参数传递:
寄存器传参:
%edi 传递 argc,%rsi 传递 argv。
printf 参数:%esi(格式串地址)、%edx、%rcx、%r8(argv[1]、argv[2]、argv[3])。
2.函数调用:
call puts:输出错误信息。
call __printf_chk:调用带安全检查的 printf。
call sleep:延时执行。
3.局部变量存储:
%rbp 和 %rbx 保存循环计数器和 argv,通过 pushq/popq 保护寄存器。
-
-
- 数组/指针操作
-
指针偏移访问:
16(%rbx):访问 argv[2],%rbx 为 argv 基地址,每个指针占8字节。
32(%rbx):访问 argv[4],传递给 strtol。
-
-
- 其他指令
-
1.栈帧管理:
pushq %rbp、pushq %rbx:保存调用者寄存器。
subq $8, %rsp:分配栈空间。
2.调试信息:
.cfi_* 指令:生成调用帧信息,用于异常处理和调试。
3.4 本章小结
编译是C程序构建的核心阶段,将预处理后的 .i 文件通过编译器转换为目标架构的汇编文件 .s,过程涵盖词法/语法分析、语义检查及代码优化。其核心作用包括:
1.语法验证:检测类型错误与未定义行为;
2.代码生成:将C逻辑映射为机器相关汇编指令;
3.符号管理:为全局变量/函数生成符号表供链接使用。
在Ubuntu下通过 gcc -S命令触发,生成的汇编代码体现为:
数据存储:常量置于 .rodata 段,局部变量通过寄存器和栈操作管理;
控制流:if/for 转换为条件跳转与循环标签;
函数调用:参数经寄存器传递,遵循System V AMD64 ABI约定;
类型转换:显式调用 strtol 实现字符串到整型的转换;
底层指令:栈帧保护、调试信息及安全函数。
编译结果直接关联硬件行为,为链接和可执行文件生成奠定基础。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是将汇编语言文件(.s)转换为机器语言二进制目标文件(.o)的过程,由汇编器完成。此阶段将人类可读的汇编指令(如 movq、call)逐条翻译为机器码(二进制指令),并生成可重定位目标文件,供后续链接阶段使用。
作用:
- 指令翻译:将汇编指令转换为对应的机器码。
- 符号解析:记录代码中的符号及其地址偏移,生成符号表。
- 生成目标文件:输出 .o 文件,包含机器指令、数据段、重定位信息及调试信息。
- 平台适配:确保生成的机器码与目标CPU架构完全兼容。
4.2 在Ubuntu下汇编的命令
gcc -c -m64 -no-pie -fno-stack-protector -fno-PIC hello.s -o hello.o
图3 在Ubuntu下汇编hello.s的截图
4.3 可重定位目标elf格式
先用readelf的各个选项[1]对hello.o进行处理,处理指令如下图。(部分指令输出结果为空或无效,此处不列出)
图4 在readelf处理hello.o的截图
图5 hello.o-h.txt文件内容截图
该文件为64位ELF可重定位文件,架构为x86-64,使用小端序和System V ABI。无程序头,包含15个节头,节头表起始于文件偏移0x490。入口点地址为0x0,表明尚未链接。
图6 hello.o-n.txt文件内容截图
这部分包含控制流保护属性:x86 feature: IBT, SHSTK,表明编译时启用了安全增强选项。
图7 hello.o-r.txt文件内容截图
重定位信息集中在.rela.text和.rela.eh_frame节:
- .rela.text(9项):
类型R_X86_64_32:修正静态数据地址。
类型R_X86_64_PLT32:修正函数调用(如puts、exit、strtol),使用过程链接表机制。
类型R_X86_64_PC32:修正与位置无关的地址。
- .rela.eh_frame(1项):修正异常处理框架地址,指向.text起始。
图8 hello.o-s.txt文件内容截图
这部分包括:main函数:位于.text节,大小135字节。
未定义符号:puts、exit、__printf_chk等,标记为UND。
只读数据符号:.rodata.str1.8和.rodata.str1.1存放字符串常量。
图9 hello.o--S.txt文件内容截图
这部分共15个节,关键节包括:
.text(代码段):偏移0x40,大小0x87,标记为AX(可分配、可执行)。
.rela.text(代码重定位表):偏移0x310,包含9项,链接到符号表(索引12)。
.rodata.str1.8和.rodata.str1.1(只读字符串数据):分别存储格式化字符串。
.symtab(符号表):偏移0x198,包含13项,包括全局符号main和未定义符号。
图9 hello.o-t.txt文件内容截图
这部分包括以下几个部分:
.text:标记ALLOC, EXEC(可分配、可执行)。
.rodata.str1.8:标记ALLOC, MERGE, STRINGS(合并重复字符串)。
.data和.bss:标记WRITE, ALLOC(可写、可分配)。
4.4 Hello.o的结果解析
1. 函数调用与重定位
汇编代码:call puts
反汇编:1e: e8 00 00 00 00 call 23 <main+0x23>
1f: R_X86_64_PLT32 puts-0x4
机器码:e8是call的操作码,后4字节为占位符(全0),需链接时通过重定位项R_X86_64_PLT32填充puts的PLT条目地址。
不一致性:汇编中直接使用符号puts,而机器码中为占位符,体现目标文件未链接时的地址未决状态。
2. 分支跳转
汇编代码:jne .L6
反汇编:d: 75 0a jne 19 <main+0x19>
机器码:75是jne的操作码,0a为偏移量(10字节),目标地址通过相对偏移计算(当前指令地址0xd + 偏移0xa + 2 = 0x19)。
映射关系:汇编标签.L6在机器码中转换为具体偏移量,无重定位需求(因跳转目标在同一节内)。
3. 数据引用
汇编代码:movl $.LC0, %edi
反汇编:19: bf 00 00 00 00 mov $0x0,%edi
1a: R_X86_64_32 .rodata.str1.8
机器码:bf是mov $imm32, %edi的操作码,后4字节为占位符(全0),通过R_X86_64_32重定位项指向.rodata.str1.8的地址。
不一致性:汇编中直接引用.LC0标签,机器码中需重定位静态数据地址。
机器语言与汇编语言的映射关系
1. 操作码与指令
机器语言:由二进制操作码和操作数构成。例如:
endbr64 → f3 0f 1e fa
push %rbp → 55
sub $0x8,%rsp → 48 83 ec 08
汇编语言:使用助记符(如push、call)和符号(如%rbp、.LC0)表示指令和操作数。
2. 操作数编码
立即数:
汇编:mov $0x0, %ebp
机器码:bd 00 00 00 00(bd为操作码,后4字节为小端序的0)。
内存地址:
汇编:movq 16(%rbx), %rcx
机器码:48 8b 4b 10(8b为mov操作码,4b 10表示%rbx+0x10)。
3. 分支与调用
相对偏移跳转:
汇编:jle .L3
机器码:7e be(7e为jle操作码,be为有符号偏移量-66,计算目标地址)。
函数调用占位符:
汇编:call __printf_chk
机器码:e8 00 00 00 00 + 重定位项R_X86_64_PLT32,链接时替换为实际地址。
4.5 本章小结
汇编阶段通过汇编器将汇编指令逐条翻译为二进制机器码,生成包含代码段、只读数据、符号表及重定位信息的ELF格式的可重定位目标文件。该文件通过节头记录各段属性,并通过重定位项标记未解析符号地址,为链接阶段提供基础。机器语言以操作码和操作数构成,与汇编语言的符号化指令存在映射差异,体现在地址占位符、偏移量编码及安全属性的底层实现。
第5章 链接
5.1 链接的概念与作用
概念:链接是将多个目标文件和库文件合并为单个可执行文件的过程。链接器负责解析符号引用、合并代码与数据段、分配运行时地址,并生成最终的可执行格式。
作用:
- 符号解析:绑定未定义符号到库函数或其它目标文件的定义。
- 地址重定位:修正目标文件中的地址占位符,使其指向实际内存位置。
- 段合并:将各目标文件的 .text、.data 等段合并为可执行文件中的连续内存区域。
- 库链接:集成静态库或动态库,如 C 标准库 libc。
5.2 在Ubuntu下链接的命令
ld -o hello \
/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 \
-L/usr/lib/gcc/x86_64-linux-gnu/11 \
-L/usr/lib/x86_64-linux-gnu \
-lc -lgcc -lgcc_s \
/usr/lib/gcc/x86_64-linux-gnu/11/crtend.o \
/usr/lib/x86_64-linux-gnu/crtn.o \
-dynamic-linker /lib64/ld-linux-x86-64.so.2
图10 在Ubuntu下用ld链接hello.o以及相关文件的截图
5.3 可执行目标文件hello的格式
图11 用readelf处理hello的截图
(hello-a.txt文件过长,此处不列出)
1. ELF头
类型:可执行文件,入口地址为0x4010f0。
架构:x86-64,小端序,使用System V ABI。
程序头:12个条目(描述内存加载布局),节头:30个条目(描述各节信息)。
动态链接器:/lib64/ld-linux-x86-64.so.2。
2. 程序头
LOAD段:
代码段:0x401000–0x40126d,包含.init、.plt、.text、.fini,权限为R E(可读、可执行)。
只读数据段:0x402000–0x4020fc,包含.rodata、.eh_frame,权限为R。
数据段:0x403df0–0x404060,包含.data、.bss、.dynamic、.got.plt,权限为RW(可读、可写)。
动态段:位于0x403e00,记录依赖库、初始化函数地址(INIT=0x401000)等。
3. 关键节
代码节:
地址0x4010f0,包含用户代码和启动代码。
过程链接表:
地址0x401020,用于动态链接外部函数,通过.got.plt实现延迟绑定。
数据节:
.data:初始化全局变量(地址0x404048)。
.bss:未初始化数据(地址0x404060),如stdin符号。
只读数据:
地址0x402000,存储字符串常量。
4. 动态链接与重定位
重定位表:
.rela.plt:6项,修正.plt中的函数调用,类型为R_X86_64_JUMP_SLOT。
.rela.dyn:3项,修正全局数据引用。
全局偏移表(.got.plt):地址0x404000,存储动态库函数的实际地址。
5. 符号与版本控制
动态符号表:列出依赖的共享库符号,标记为UND。
版本信息:
.gnu.version:指定符号的GLIBC版本(如GLIBC_2.2.5)。
.gnu.version_r:声明依赖的库版本(如libc.so.6需兼容GLIBC_2.34)。
6. 安全与扩展属性
控制流保护:.note.gnu.property启用IBT(间接分支跟踪)和SHSTK(影子栈)。
只读重定位(GNU_RELRO):保护.got.plt等段在初始化后设为只读,防止篡改
5.4 hello的虚拟地址空间
图12 用objdump处理hello的截图
1. 代码段
GDB映射:
0x401000–0x402000 0x1000 0x1000 r-xp /home/ki/CShello/hello
权限:r-xp(可读、可执行),对应ELF程序头中的LOAD段(R E)。
地址匹配:ELF的.text节虚拟地址为0x4010f0,位于该内存段内。
内容:包含用户代码和启动代码。
2. 只读数据段
GDB映射:
0x402000–0x403000 0x1000 0x2000 r--p /home/ki/CShello/hello
权限:r--p(只读),对应ELF程序头中的LOAD段(R)。
地址匹配:ELF的.rodata节地址为0x402000,与此段完全一致。
内容:存储字符串常量。
3. 数据段
GDB映射:
0x404000–0x405000 0x1000 0x3000 rw-p /home/ki/CShello/hello
权限:rw-p(可读、可写),对应ELF程序头中的LOAD段(RW)。
地址匹配:ELF的.data节地址为0x404048,.bss为0x404060,均位于此段。
内容:包含初始化全局变量和未初始化数据。
4. 动态链接库
GDB映射:
0x7ffff7c00000–0x7ffff7e1c000 libc.so.6(权限:r--p, r-xp, rw-p)
0x7ffff7f8b000–0x7ffff7fab000 libgcc_s.so.1(权限:r--p, r-xp)
ELF动态段:
依赖库声明:NEEDED libc.so.6和NEEDED libgcc_s.so.1。
动态符号表中引用了puts、exit等库函数。
权限匹配:代码段(r-xp)与只读数据段(r--p)符合ELF的共享库加载规则。
5. 栈
GDB映射:
0x7ffffffde000–0x7ffffffff000 rw-p [stack]
权限:rw-p,用于函数调用栈和局部变量。
6. 动态链接器
GDB映射:
0x7ffff7fc3000–0x7ffff7fff000 ld-linux-x86-64.so.2
ELF程序头:
指定动态链接器路径:/lib64/ld-linux-x86-64.so.2。
动态段记录符号解析和重定位信息。
5.5 链接的重定位过程分析
1. 主要差异分析
(1) 函数调用与PLT机制
hello.o:
函数调用的地址为占位符(e8 00 00 00 00),通过重定位项R_X86_64_PLT32标记。
hello:
函数调用通过PLT实现动态绑定,地址被替换为PLT条目。
PLT条目通过.got.plt实现延迟绑定。
(2) 数据引用与地址修正
hello.o:
数据引用使用占位符地址,通过重定位项R_X86_64_32标记。
hello:
字符串常量被合并到.rodata段,地址修正为实际内存位置。
(3) 全局变量与动态符号
hello.o:
全局符号未定义,需链接时解析。
hello:
动态符号通过.got引用,地址由动态链接器填充。
2. 链接过程的核心步骤
(1)符号解析:
链接器将hello.o中的未定义符号与共享库中的定义绑定。
静态符号被合并到可执行文件的对应段。
(2)地址分配与段合并:
将各目标文件的代码段、数据段等按权限合并为连续的虚拟内存区域。
(3)重定位修正:
静态重定位:修正代码中的绝对地址引用。
类型R_X86_64_32:直接替换为最终地址。
动态重定位:通过PLT/GOT机制处理外部函数调用。
类型R_X86_64_PLT32:替换为PLT条目地址。
类型R_X86_64_JUMP_SLOT:运行时由动态链接器填充.got.plt。
3. 重定位项目的具体处理
函数调用:
在hello.o中,call指令的偏移量为占位符,重定位类型为R_X86_64_PLT32。
链接时,链接器将call目标替换为PLT条目地址,并在.rela.plt中生成R_X86_64_JUMP_SLOT条目,供动态链接器运行时解析。
数据引用:
在hello.o中,mov $0x0,%edi引用了.rodata.str1.8的地址,重定位类型为R_X86_64_32。
链接时,链接器将0x0替换为.rodata段的实际地址。
全局变量:
在hello.o中,stdin通过R_X86_64_PC32重定位,地址占位符为0x0。
链接时,链接器在.got中分配条目,并在.rela.dyn中标记R_X86_64_COPY,由动态链接器填充实际地址。
5.6 hello的执行流程
图13 反汇编处理main的截图
首先程序在main+30处跳转到puts函数,然后再main+40处跳转到exit函数,接着依次在main+72处跳转到__printf_chk函数,在main+91处跳转到strtol函数,在main+98处跳转到sleep函数,最后在main+118处跳转到gets函数。以下是反汇编各个函数的截图:
图14 反汇编处理puts的截图
图15 反汇编处理exit的截图
图16 反汇编处理__printf_chk的截图
图17 反汇编处理strtol的截图
图18 反汇编处理sleep的截图
图19 反汇编处理getc的截图
5.7 Hello的动态链接分析
图20 反汇编动态链接的截图
如图,在链接后,0x404018的地址发生了变化。
5.8 本章小结
本章阐述了链接的核心机制,涵盖从目标文件合并到可执行文件生成的全过程。链接器通过符号解析绑定未定义符、地址重定位修正代码与数据的引,并合并代码、数据等形成内存连续布局。可执行文的ELF格式包含程序头、动态及安全属。动态链接通过全局偏移在运行时解析函数地址,而虚拟地址空间映射则与ELF结构严格对应,确保程序高效安全执行。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是程序的一次执行实例,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的虚拟地址空间、文件描述符、寄存器状态等资源。
作用:
并发执行:允许多个程序同时运行。
资源隔离:进程间内存与资源相互隔离,防止相互干扰。
任务管理:操作系统通过进程控制块跟踪进程状。
权限控制:不同进程可拥有不同的用户权限,增强系统安全性。
6.2 简述壳Shell-bash的作用与处理流程
作用:
命令解析:读取用户输入的命令,解析参数和环境变量。
进程管理:通过fork创建子进程,通过execve加载并执行目标程序。
I/O重定向:支持输入输出重定、管等。
作业控制:管理前后台进。
处理流程:
1.读取命令:从终端或脚本读取输。
2.解析命令:分割命令为可执行文件名和参。
3.查找程序:在$PATH环境变量中搜索可执行文件路径。
4.创建子进程:调用fork()创建子进程。
5.加载程序:子进程中调用execve()加载并执行目标程序。
6.等待完成:父进程Shell通过waitpid()等待子进程结束,获取退出状态。
6.3 Hello的fork进程创建过程
1.Shell调用fork():
复制父进程Shell的地址空间、文件描述符、寄存器状态等,生成子进程。
关键特性:fork()返回两次:
父进程:返回子进程的PID。
子进程:返回0。
2.子进程准备执行:
子进程与父进程独立运行,共享代码段,但拥有独立的数据段和堆栈。
3.执行目标程序:子进程调用execve()加载hello,替换当前进程映像。
6.4 Hello的execve过程
1.参数传递:
execve("./hello", argv, environ)接收程序路径、参数列表和环境变量。
2.加载可执行文件:
解析hello的ELF格式,验证权限与架构兼容性。
3.内存映射:
根据程序将代码、数据等映射到进程虚拟地址空间。
4.初始化寄存器:
设置程序计数器指向入口地,栈指针指向新栈顶。
5.动态链接:
加载依赖的共享,通过动态链接解析符号。
6.进程替换完成:
原进程的代码和数据被完全替换,开始执行hello的_start函数,最终调用main。
6.5 Hello的进程执行
1. 进程上下文信息
进程上下文是进程运行时的完整状态,包括:
寄存器状态:程序计数器、栈指针、通用寄存器等。
内存管理信息:页表、地址空间映射。
进程控制块:进程ID、优先级、状态等。
2. 进程时间片与调度触发
时间片:操作系统为每个进程分配的CPU时间。
触发调度的场景:
(1)时间片耗尽:hello进程持续运行至时间片结束,CPU被强制回收。
(2)主动让出CPU:hello执行系统调用或等待I/O时主动阻塞。
(3)高优先级抢占:更高优先级进程就绪,抢占hello的CPU使用权。
3. 进程调度的核心过程
(1)中断当前进程:
时钟中断:时间片耗尽时触发,CPU切换到核心态。
系统调用或异常:如hello调用printf触发write系统调用。
(2)保存上下文:
将hello的寄存器状态、程序计数器等保存到其PCB中。
(3)选择新进程:
调度器从就绪队列中选择下一个进程。
(4)加载新上下文:
从新进程的PCB恢复寄存器、内存映射等状态。
(5)切换CPU控制权:
跳转到新进程的程序计数器地址,继续执行。
4. 用户态与核心态转换
用户态到核心态:
触发条件:系统调用、中断、异常。
操作流程:
(1)CPU通过陷阱指令进入内核。
(2)保存用户态寄存器到内核栈。
(3)执行内核代码。
核心态到用户态:
触发条件:系统调用/中断处理完成。
操作流程:
(1)从内核栈恢复用户态寄存器。
(2)通过返回到用户空间指令切换回用户态。
5. 结合hello进程的完整流程
(1)启动与调度:
Shell通过fork+execve加载hello,进程进入就绪队列。
调度器分配时间片,hello开始运行。
(2)时间片耗尽:
时钟中断触发,进入核心态,保存hello上下文,调度器选择新进程。
(3)系统调用触发:
hello调用printf,write系统调用,用户态切换到核心态,内核处理I/O。
I/O完成,返回用户态继续执行hello。
(4)进程终止:
hello执行完毕,调用exit系统调用,进入核心态释放资源,父进程回收状态。
6.6 hello的异常与信号处理
按下ctrl+c时有以下错误:
图21 hello异常截图
- 异常原因
在运行C语言编写的hello程序时按下 Ctrl+C,触发了 Python相关的错误。这表明该C程序内部可能 间接调用了Python解释器 或 依赖了嵌入Python的库。按下Ctrl+C时,系统向进程发送 SIGINT信号,但此时Python解释器正在初始化,信号中断了Python的启动流程,导致异常。
2. 信号处理流程
(1)Ctrl+C触发SIGINT信号:
默认行为是终止进程。
(2)信号传递时机:
若SIGINT发生在Python解释器初始化阶段,可能中断关键操作,引发Keyboard Interrupt异常。
(3)Python初始化流程:
Python启动时会加载site模块,若此时被SIGINT中断,会导致Fatal Python Error。
6.7本章小结
本章阐述了进程管理的核心机制,涵盖从进程概念到执行调度的全流程。进程作为程序执行的实例,通过独立地址空间和资源隔离实现并发与安全,由操作系统通过进程控制块跟踪状态。Shell作为用户与系统的桥梁,解析命令后通过fork创建子进程、execve加载目标程序,并管理I/O与作业。进程调度依赖时间片轮转与上下文切换,触发场景包括时间片耗尽、系统调用或高优先级抢占,期间通过用户态与核心态转换处理中断与资源访问。信号处理在异常时介入,若进程依赖外部环境,信号可能中断关键流程,导致致命错误。整体上,进程管理通过资源分配、状态切换与信号响应,保障多任务高效、安全执行。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序直接使用的地址(如hello中的0x401090),由 段选择子 + 偏移量 组成。
线性地址:逻辑地址通过 段式管理 转换后的地址,在32位系统中直接作为虚拟地址。
虚拟地址:在分页机制中,进程视角的连续地址空间,需通过 页式管理 转换为物理地址。
物理地址:实际内存硬件上的地址,由CPU通过MMU转换得到。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel使用 段描述符表 实现段式管理:
1.逻辑地址 = 段选择子(16位) + 偏移量(32/64位)。
2.CPU通过段选择子从GDT中获取 段基址 和 段界限。
3.线性地址 = 段基址 + 偏移量。
7.3 Hello的线性地址到物理地址的变换-页式管理
现代系统采用 页式管理:
1.虚拟地址(VA) 分割为页表索引和页内偏移。
2.MMU逐级查询页表,获取物理页框号。
3.物理地址(PA) = PFN × 页大小(4KB) + 页内偏移。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(快表):缓存最近使用的虚拟页到物理页的映射,加速地址转换。
四级页表:
1.CR3寄存器 指向PML4表基址。
2.虚拟地址的48位分割为:PML4(9) + PDPT(9) + PD(9) + PT(9) + Offset(12)。
3.若TLB命中,直接获取PFN;否则逐级查询页表,并更新TLB。
7.5 三级Cache支持下的物理内存访问
CPU访问物理内存时,通过缓存层级减少延迟:
1.L1 Cache:分指令/数据缓存,访问速度1-3周期。
2.L2 Cache:统一缓存,速度约10周期。
3.L3 Cache:共享缓存,速度约30-40周期。
若数据不在Cache(Cache Miss),需从内存加载,耗时约100+周期。
7.6 hello进程fork时的内存映射
写时复制(Copy-On-Write):
1.fork()复制父进程的页表,父子进程共享物理页。
2.当任一进程尝试写入共享页时,触发缺页异常,内核复制该页并更新页表。
3.hello的代码段(只读)保持共享,数据段(可写)按需复制。
7.7 hello进程execve时的内存映射
1.清除原内存映像:释放旧进程的代码、数据、堆栈段。
2.加载新程序:根据ELF程序头,将hello的代码段(.text)、数据段(.data)映射到虚拟地址空间。
3.初始化堆栈:设置命令行参数(argv)和环境变量(environ)。
4.动态链接:加载依赖库(如libc.so)并解析符号。
7.8 缺页故障与缺页中断处理
缺页故障:访问未映射的虚拟地址或权限不足时触发。
处理流程:
1.CPU陷入内核态,保存上下文。
2.内核检查缺页原因:
文件映射缺页:从磁盘(如可执行文件)加载数据到物理页。
匿名页缺页:分配零页(如堆栈扩展)。
写时复制:复制物理页并更新页表。
3..恢复进程执行,重新执行触发缺页的指令。
7.9本章小结
存储管理通过多级地址转换机制实现高效内存分配与隔离。逻辑地址经Intel段式管理转换为线性地址,再通过四级页表与TLB完成虚拟地址到物理地址的映射,确保进程间内存隔离与安全。物理内存访问借助三级缓存优化性能,减少延迟。进程管理中,fork采用写时复制共享内存资源,execve替换进程内存映像并加载目标程序,动态链接依赖库。缺页中断机制处理未映射或权限异常,通过文件加载、零页分配或页复制保障内存连续性。整体上,存储管理结合地址转换、缓存优化与动态内存策略,平衡效率与资源隔离,支撑系统稳定高效运行。
结论
- 预处理阶段
输入:hello.c
处理:预处理器解析以#开头的指令,完成宏展开、头文件插入、条件编译、注释删除等,生成纯C代码文件hello.i。
关键作用:规范化输入,确保跨平台兼容性与调试信息保留。
- 编译阶段
输入:hello.i
处理:编译器进行词法/语法分析、语义检查与代码优化,生成x86-64架构的汇编文件hello.s。
关键作用:将高级C代码转换为低级汇编指令,生成符号表,为后续地址绑定提供基础。
- 汇编阶段
输入:hello.s
处理:汇编器将汇编指令逐条翻译为机器码,生成可重定位目标文件hello.o,包含ELF格式的代码段、数据段及重定位信息。
关键作用:生成二进制目标文件,标记未解析符号地址,为链接做准备。
- 链接阶段
输入:hello.o及系统库
处理:链接器解析符号引用,合并代码段与数据段,分配运行时虚拟地址,生成可执行文件hello。
关键机制:静态重定位修正绝对地址,动态链接通过PLT/GOT实现延迟绑定。
- 进程创建与加载
Shell处理:输入./hello后,Shell通过fork()创建子进程,子进程调用execve()加载hello的ELF文件。
内存映射:操作系统分配虚拟地址空间,映射代码段、数据段、堆栈段,加载动态库。
资源分配:进程控制块、文件描述符、页表等初始化。
- 进程执行与调度
CPU执行:取指-译码-执行流水线运行hello的指令,通过MMU/TLB/页表完成虚拟地址到物理地址的转换。
调度机制:时间片轮转触发上下文切换,用户态与内核态转换处理系统调用。
三级缓存优化:L1/L2/L3 Cache加速物理内存访问,减少CPU访存延迟。
- 存储管理
地址转换:逻辑地址→线性地址→虚拟地址→物理地址。
写时复制:fork()时共享父进程页表,首次写入触发缺页中断复制物理页。
缺页处理:未映射页触发缺页中断,内核分配物理页或从文件加载数据。
- I/O与信号处理
输出:printf通过vsprintf格式化字符串,调用write系统调用写入标准输出,经显卡驱动渲染至屏幕。
输入:getchar通过read系统调用读取键盘缓冲区,等待回车触发中断。
信号处理:Ctrl+C发送SIGINT信号,默认终止进程;若进程依赖外部库(如Python),可能引发异常处理流程。
- 进程终止与资源回收
终止:hello执行完毕或收到终止信号后,调用exit()系统调用。
资源回收:操作系统释放物理内存、关闭文件描述符、销毁页表与PCB,回归“零占用”状态。
附件
hello.i作用:预处理后的文件,包含宏展开、头文件合并、注释删除后的纯C代码。
hello.s作用:编译生成的汇编代码文件,将C代码转换为x86-64架构的汇编指令。
hello.o作用:汇编生成的可重定位目标文件(ELF格式),包含机器码和未解析符号。
hello作用:链接后的最终可执行文件,整合了目标文件与库函数。
hello.o-r.txt等文件作用:readelf处理后包含头信息、属性段等ELF格式的文件。
hello-objump.txt与hello.o-objdump.txt作用:反汇编代码,展示机器指令与汇编代码的对应关系。
gdb.txt作用:gdb的日志,展现了相关内存地址。
参考文献
[1] https://blog.csdn.net/yfldyxl/article/details/81566279