计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 电信学院
学 号 2023112917
班 级 23L0505
学 生 叶海华
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本文以经典的"Hello World"程序为例,全面剖析了一个C语言程序从源代码到可执行文件,再到进程运行与终止的完整生命周期。通过使用GCC工具链在Linux环境下对hello.c程序进行预处理、编译、汇编和链接的全过程分析,揭示了计算机系统底层的工作机制。研究涵盖了程序编译的各个阶段、进程管理、存储体系结构以及I/O操作等核心计算机系统概念。特别探讨了P2P(From Program to Process)的编译链接过程和020(From Zero-0 to Zero-0)的进程生命周期。通过EDB、GDB、readelf、objdump等工具的联合使用,深入分析了可执行文件的ELF格式、虚拟地址空间管理、动态链接机制以及异常信号处理等关键技术。本研究不仅验证了计算机系统课程中的理论知识,还通过实践展示了操作系统、编译系统和硬件架构如何协同工作以支持程序的执行,为理解计算机系统整体架构提供了具体案例。
关键词:计算机系统;程序生命周期;编译链接;进程管理;虚拟内存;ELF格式
目 录
第1章 概述.................................... - 5 -
1.1 Hello简介..................................... - 5 -
1.2 环境与工具................................... - 5 -
1.3 中间结果..................................... - 5 -
1.4 本章小结..................................... - 6 -
第2章 预处理.................................. - 7 -
2.1 预处理的概念与作用........................... - 7 -
2.2在Ubuntu下预处理的命令........................ - 7 -
2.3 Hello的预处理结果解析......................... - 8 -
2.4 本章小结..................................... - 8 -
第3章 编译................................... - 11 -
3.1 编译的概念与作用............................ - 11 -
3.2 在Ubuntu下编译的命令........................ - 11 -
3.3 Hello的编译结果解析.......................... - 12 -
3.4 本章小结.................................... - 15 -
第4章 汇编................................... - 17 -
4.1 汇编的概念与作用............................ - 17 -
4.2 在Ubuntu下汇编的命令........................ - 17 -
4.3 可重定位目标elf格式......................... - 17 -
4.4 Hello.o的结果解析............................ - 21 -
4.5 本章小结.................................... - 23 -
第5章 链接................................... - 26 -
5.1 链接的概念与作用............................ - 27 -
5.2 在Ubuntu下链接的命令........................ - 27 -
5.3 可执行目标文件hello的格式................... - 27 -
5.4 hello的虚拟地址空间.......................... - 29 -
5.5 链接的重定位过程分析........................ - 29 -
5.6 hello的执行流程.............................. - 32 -
5.7 Hello的动态链接分析.......................... - 32 -
5.8 本章小结.................................... - 37 -
第6章 hello进程管理........................... - 38 -
6.1 进程的概念与作用............................ - 38 -
6.2 简述壳Shell-bash的作用与处理流程............. - 38 -
6.3 Hello的fork进程创建过程...................... - 38 -
6.4 Hello的execve过程............................ - 39 -
6.5 Hello的进程执行.............................. - 40 -
6.6 hello的异常与信号处理........................ - 41 -
6.7本章小结.................................... - 42 -
第7章 hello的存储管理......................... - 45 -
7.1 hello的存储器地址空间........................ - 45 -
7.2 Intel逻辑地址到线性地址的变换-段式管理....... - 45 -
7.3 Hello的线性地址到物理地址的变换-页式管理..... - 45 -
7.4 TLB与四级页表支持下的VA到PA的变换......... - 45 -
7.5 三级Cache支持下的物理内存访问............... - 46 -
7.6 hello进程fork时的内存映射.................... - 46 -
7.7 hello进程execve时的内存映射.................. - 48 -
7.8 缺页故障与缺页中断处理...................... - 49 -
7.9动态存储分配管理............................ - 49 -
7.10本章小结................................... - 49 -
第8章 hello的IO管理.......................... - 51 -
8.1 Linux的IO设备管理方法....................... - 51 -
8.2 简述Unix IO接口及其函数..................... - 51 -
8.3 printf的实现分析............................. - 51 -
8.4 getchar的实现分析............................ - 52 -
8.5本章小结.................................... - 53 -
结论.......................................... - 53 -
附件.......................................... - 56 -
参考文献...................................... - 59 -
第1章 概述
1.1 Hello简介
"Hello World"程序的旅程,展现了计算机系统如何将静态代码转化为动态进程。 从程序到进程(P2P)阶段,源代码`hello.c`首先经过预处理,其中宏和头文件被展开,生成`hello.i`。 接下来,编译器将`hello.i`翻译成汇编语言`hello.s`,包含底层指令。汇编器将`hello.s`转换为机器码`hello.o`,形成可重定位目标文件。最后,链接器将`hello.o`与必要的库链接,生成可执行文件`hello`。 这标志着P2P过程的结束,一个静态的程序准备就绪。随后,从零到零(020)阶段开始,描述程序的运行生命周期。当用户在Shell中执行`./hello`时,操作系统创建新的进程,并将可执行文件`hello`加载到内存。CPU开始执行`hello`程序的指令,从代码段(.text)获取指令,操作数据段(.data)中的数据。操作系统负责进程调度、系统调用处理以及异常管理,确保程序的正确执行。最终,程序执行完毕,操作系统回收分配给该进程的资源,包括内存空间、文件描述符等,进程生命周期结束,回归到“零”状态。 整个过程体现了计算机系统各个组件的协同工作,从代码到执行,最终完成预定的任务。。
1.2 环境与工具
硬件环境:
- 处理器:Intel Core i5-11400H @ 2.70GHz
- 内存:16GB RAM
- 存储:512GB SSD
- 系统类型:64位操作系统,基于x64的处理器18
软件环境:
- 主机操作系统:Windows 10 64位
- 虚拟机平台:VMware Workstation 16
- 客户机操作系统:Ubuntu 20.04 LTS 64位47
开发与调试工具:
- 编译器:GCC (gcc 9.3.0)
- 调试器:GDB (GNU debugger),EDB (Evan's Debugger)
- 二进制分析工具:objdump,readelf,hexedit
- 文本编辑器:vim,gedit,VSCode15
1.3 中间结果
文件名 | 作用 |
hello.i | 预处理后的C源代码,包含所有宏展开和头文件内容 |
hello.s | 编译生成的汇编语言代码,展示高级语言到低级指令的转换 |
hello.o | 可重定位目标文件,包含机器码但未完成最终地址解析 |
hello | 最终可执行文件,已完成链接和重定位 |
hello_elf.txt | hello.o的ELF格式分析结果,展示节头、符号表等信息 |
hello_asm.txt | hello.o的反汇编代码,用于分析机器指令与汇编的对应关系 |
hello1_elf.txt | 可执行文件hello的ELF格式分析 |
hello1_asm.txt | 可执行文件hello的反汇编结果 |
1.4 本章小结
本章概述了hello程序的P2P和020过程,介绍了从源代码到进程的完整转换流程。详细说明了实验所使用的硬件配置、软件环境和开发调试工具,这些工具为后续各阶段的分析提供了技术支持。同时列出了实验过程中生成的关键中间文件及其作用,这些文件将成为后续章节分析的基础。通过本章的介绍,读者可以对hello程序的完整生命周期和实验环境有一个全局性的认识
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是C程序编译过程中的第一个阶段,由预处理器(cpp)对源代码进行初步处理24。预处理器不分析C语言的语法结构,而是执行以#开头的预处理指令,对源代码进行文本级别的转换79。
预处理的主要功能包括:
- 宏展开:处理#define定义的宏,将代码中的所有宏调用替换为定义的内容。例如,#define PI 3.14会导致所有PI出现处被替换为3.14。
- 文件包含:处理#include指令,将被引用的头文件内容插入到当前文件中。例如,#include <stdio.h>会将标准IO库的声明插入到源代码中210。
- 条件编译:根据#if、#ifdef、#endif等条件编译指令,选择性地包含或排除代码段。这使得同一份源代码可以根据不同条件编译出不同版本的程序79。
- 注释删除:移除源代码中的所有注释,减少后续编译阶段需要处理的无关内容48。
- 特殊指令处理:处理#line、#error、#pragma等特殊指令,用于控制编译器的行为或生成调试信息2。
预处理的主要作用是简化后续编译过程,通过宏和文件包含提高代码的模块化和可重用性,通过条件编译支持跨平台开发,最终生成一个"纯净"的、适合编译器处理的源代码文件
2.2在Ubuntu下预处理的命令
命令参数说明:
- -E:指示GCC只进行预处理,不进行编译、汇编和链接
- -o hello.i:指定输出文件名为hello.i
- > hello.i:将预处理结果重定向到hello.i文件
- -m64:生成64位代码
执行预处理命令后,终端会显示处理过程但不输出内容,所有预处理结果保存在hello.i文件中。可以通过wc -l hello.i查看生成文件的行数,通常比原始.c文件大很多,因为包含了所有头文件的内容。
2.3 Hello的预处理结果解析
预处理生成的hello.i文件是一个经过扩展的C源代码文件,可以用文本编辑器打开查看。以hello.c为例,预处理结果主要包含以下几部分24:
- 行号标记与源文件信息:
文件开头通常会有类似# 1 "hello.c"的标记,表示后续内容源自hello.c的第1行。这些标记帮助编译器在错误报告中定位到原始源文件的位置。 - 头文件内容展开:
原始代码中的#include <stdio.h>等指令被替换为对应头文件的内容。例如:- 头文件内容通常包含函数声明、宏定义、类型定义等。值得注意的是,头文件本身可能又包含其他头文件,形成递归展开48。
- 宏定义展开:
所有使用#define定义的宏都被展开。例如,如果源文件中有#define MAX 100,则所有MAX出现处都会被替换为100210。 - 条件编译处理:
预处理器会根据#if、#ifdef等条件编译指令保留或删除代码段。在hello.i中只能看到最终保留的代码79。 - 注释删除:
原始代码中的所有注释(/* */和//)都被完全删除,不会出现在hello.i中48。 - 原始代码保留:
文件最后部分是原始的hello.c代码(不含注释和预处理指令),这是后续编译阶段真正需要处理的部分35。
通过对比hello.c和hello.i可以发现,一个简单的hello程序经过预处理后可能扩展为数千行的代码,这是因为包含了大量系统头文件的内容。这些内容为程序提供了标准库函数的声明和必要的类型定义29。
2.4 本章小结
本章详细介绍了C程序编译过程中的预处理阶段。预处理作为编译的第一步,通过处理宏定义、文件包含和条件编译等指令,将原始的C源代码转换为更适合编译器处理的扩展形式。在Ubuntu环境下,可以使用gcc -E命令方便地完成预处理操作,生成.i文件18。
对hello.i文件的分析表明,预处理阶段实质上是一个文本替换和扩展的过程,它将分散在多个文件中的代码整合为一个完整的编译单元,同时移除了注释等无关内容。这一阶段虽然不涉及复杂的语法分析,但对后续编译过程至关重要,它决定了编译器实际看到的代码结构和内容49。
通过本章的学习,我们理解了预处理的概念、作用和实践方法,为后续的编译阶段分析奠定了基础。预处理阶段展示了C语言模块化设计的实现机制,也是理解大型项目编译过程的重要环节
第3章 编译
3.1 编译的概念与作用
编译阶段是将预处理后的高级语言代码转换为汇编语言代码的关键过程35。在GCC工具链中,这一任务由ccl(Compiler proper for C)组件完成,它接收预处理后的.i文件,输出对应的.s汇编文件89。
编译的主要作用包括:
- 词法分析:将源代码分解为token序列,识别关键字、标识符、常量、运算符等语言元素57。
- 语法分析:根据C语言语法规则,构建抽象语法树(AST),检查程序结构是否符合语法规范310。
- 语义分析:检查类型匹配、变量声明等语义规则,确保程序的逻辑正确性79。
- 中间代码生成:生成与机器无关的中间表示(如GIMPLE),便于后续优化58。
- 代码优化:对中间代码进行各种优化,提高生成代码的执行效率39。
- 目标代码生成:将优化后的中间代码转换为目标机器的汇编语言710。
值得注意的是,此处的"编译"是狭义概念,专指从.i到.s的转换过程,而不包括后续的汇编和链接阶段38。通过编译,高级抽象的C语言被转换为低级的、与特定处理器架构相关的汇编语言,为最终生成机器代码奠定了基础
3.2 在Ubuntu下编译的命令
在Ubuntu系统中,可以使用GCC将预处理后的.i文件编译为汇编代码.s文件。基本命令格式为
执行编译命令后,会生成hello.s文件,其中包含对应C源代码的汇编语言实现。可以通过文本编辑器或cat命令查看其内容
3.3 Hello的编译结果解析
3.3.1 数据类型处理分析
- 整型数据(int)处理:
在hello.s中,整型变量主要使用32位寄存器(如%edi
)和movl
指令处理:
- 指针类型处理:
指针使用64位寄存器(如%rax
)和movq
指令操作:
- 字符串常量处理:
字符串常量存储在.rodata节,通过标签(如.LC0)引用:
3.3.2 运算符处理分析
- 算术运算:
- 关系运算:
- 位运算:
虽然示例中没有显式位运算,但典型处理方式为:
3.3.3 控制结构实现
- 条件语句(if-else):
- 循环结构(for):
3.3.4 函数调用实现
- 参数传递:
- 返回值处理:
3.3.5 数组和指针操作
- 数组访问:
- 结构体访问:
3.3.6 特殊操作处理
- 类型转换:
- 取地址操作:
- 间接寻址:
3.3.7 其他重要特性
- 位置无关代码(PIC):
- 栈帧管理:
- 系统调用:
通过以上分析可以看出,编译器将高级C语言结构系统地转换为底层汇编指令,同时遵循了x86-64架构的调用约定和ABI规范。不同类型的数据使用不同大小的寄存器和指令进行处理,控制结构则通过标签和跳转指令实现,函数调用遵循特定的参数传递规则。
3.4 本章小结
(本章通过对hello.s汇编代码的解析,系统揭示了C语言到汇编的转换机制:编译器将不同数据类型映射到特定寄存器(如32位int用%edi、指针用%rax),控制结构转化为标签跳转模式(if→cmp+je,for→init-jmp-condition),函数调用遵循System V ABI(参数按%rdi/%rsi传递,返回%rax),并通过subq分配栈空间、movq管理指针运算。关键特性包括.rodata段存储字符串常量、RIP相对寻址实现PIC、endbr64指令增强安全性,以及循环条件置于底部优化等,完整展现了高级语言通过编译器降级为机器相关汇编代码的规范化转换过程。
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言(.s)转换为机器语言二进制文件(.o)的关键过程,由汇编器(as)完成。其主要作用包括:
- 指令转换:将助记符形式的汇编指令转换为二进制机器码
- 符号解析:建立符号表记录标签和函数名等符号信息
- 重定位信息生成:标记需要链接阶段处理的地址引用
- 节区组织:将代码、数据等分类存入ELF文件的对应节区
4.2 在Ubuntu下汇编的命令
关键参数:
- --64:生成64位目标文件
- -c:只编译不链接
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
根据readelf
命令的输出结果,我们对hello.o的ELF格式进行详细分析:
1. 节头表(Section Headers)分析
通过readelf -S hello.o
可观察到14个节区,关键节区包括:
节区名称 | 类型 | 标志 | 说明 |
.text | PROGBITS | AX | 存放程序的可执行指令(大小0xa3字节) |
.rodata | PROGBITS | A | 只读数据段(大小0x40字节),存储字符串常量 |
.data | PROGBITS | WA | 已初始化的全局变量(本程序为空) |
.bss | NOBITS | WA | 未初始化数据(本程序未使用) |
.rela.text | RELA | I | 代码段重定位表(包含8个条目) |
.symtab | SYMTAB | 符号表(包含11个条目) | |
.eh_frame | PROGBITS | A | 异常处理框架信息 |
特别值得注意的是:
- .text节区具有A(alloc)和X(execute)标志,表示需要加载到内存并具有执行权限
- .rodata节区对齐值为8,符合x86-64架构对数据访问的优化要求
- 重定位信息集中在.rela.text节区,共8个重定位条目
2. 符号表(Symbol Table)分析
readelf -s hello.o
显示符号表包含11个条目:
- 全局符号:
- main:Value=0,Size=163,Type=FUNC,Bind=GLOBAL,位于.text节区(Ndx=1)
- 未定义符号(需链接时解析):
- puts、exit、printf、atoi、sleep、getchar:Type=NOTYPE,Bind=GLOBAL,节区索引UND(未定义)
符号表特点:
- 本地符号(如节区标记)的Bind属性为LOCAL
- 所有符号的Value字段暂时为0,将在链接时确定最终地址
- 外部函数引用标记为UND(未定义),需通过动态链接解析
3. 重定位表(Relocation Table)分析
readelf -r hello.o
显示两个重定位节区:
.rela.text节区(代码重定位)包含8个关键条目:
Offset | Type | Symbol | Addend | 对应指令 |
0x1c | R_X86_64_PC32 | .rodata | -4 | 字符串地址引用 |
0x24 | R_X86_64_PLT32 | puts | -4 | call puts指令 |
0x62 | R_X86_64_PC32 | .rodata | +0x2c | 第二个字符串引用 |
0x6f | R_X86_64_PLT32 | printf | -4 | call printf指令 |
重定位类型说明:
- R_X86_64_PC32:32位PC相对地址重定位,用于.rodata节区内字符串引用
- R_X86_64_PLT32:过程链接表(PLT)相对重定位,用于函数调用
.rela.eh_frame节区包含1个条目,用于异常处理框架的地址修正。
4. ELF结构特点总结
- 模块化设计:代码(.text)、数据(.data/.rodata)和元信息(.symtab等)严格分离
- 重定位支持:通过.rela.text记录所有需要链接时修正的地址引用
- 扩展性:未定义符号为动态链接提供接口
- 标准兼容:包含.eh_frame等现代ELF标准要求的节区
通过ELF格式分析可见,hello.o作为可重定位目标文件,已具备完整的代码和数据组织,但所有外部引用和地址相关操作都保留为待重定位状态,这正是链接器需要处理的核心问题。
4.4 Hello.o的结果解析
1. 反汇编代码与汇编源码对比分析
通过objdump -d -r hello.o
的反汇编结果与第3章的hello.s进行对比,可以发现以下关键差异:
- 地址表示形式:
- hello.s中使用符号标签(如.LC0、.L2等)
- hello.o中使用相对偏移地址(如0x1c、0x24等)
- 函数调用处理:
-
- 调用地址暂时填充为0,等待链接时重定位
- 添加了R_X86_64_PLT32类型重定位条目
- 数据引用处理:
-
- 偏移量暂时为0,等待重定位
- 标记为R_X86_64_PC32类型重定位
2. 机器语言构成分析
- 指令编码规律:
- 操作码通常为1-2字节(如e8=call,48=64位前缀)
- 操作数采用小端字节序存储
- 相对偏移使用补码表示
- 典型指令解析示例:
-
- 操作码:55(push指令)
- 操作数:隐含%rbp寄存器
- 48:64位操作数大小前缀
- 89:mov指令主操作码
- e5:ModR/M字节(%rsp源,%rbp目的)
3. 重定位特征分析
反汇编显示的8个重定位项对应以下代码位置:
- 函数调用重定位:
-
- 调用指令的操作数部分全0,等待链接时填充
- 重定位类型为PLT32,表示需要通过过程链接表跳转
- 数据引用重定位:
-
- RIP相对寻址的偏移量暂为0
- 重定位类型为PC32,表示32位PC相对地址
4. 分支转移与函数调用机制
- 相对跳转指令:
-
- eb:jmp指令操作码
- 1a:相对偏移量(+26字节)
- 外部函数调用处理:
- 使用PLT(过程链接表)机制
- 调用地址在链接时动态确定
- 通过GOT(全局偏移表)实现间接跳转
5. 与可执行文件的差异对比
相较于最终的可执行文件hello,hello.o具有以下特点:
- 未解析的符号引用:
- 所有外部函数调用地址未确定
- 数据引用地址未确定
- 缺少运行时信息:
- 无程序头表(Program Headers)
- 未设置虚拟地址
- 重定位信息完整保留:
- 包含.text和.eh_frame的重定位项
- 符号表保留所有引用信息
6. 关键发现总结
- 地址无关性:
- 所有地址引用都设计为可重定位
- 使用PC相对寻址增强位置无关性
- 模块化设计:
- 外部符号显式标记为UND
- 重定位表完整记录修改需求
- ABI合规性:
- 严格遵循System V AMD64 ABI
- 调用约定和寄存器使用规范统一
通过反汇编分析可见,hello.o作为可重定位目标文件,其核心价值在于保留了完整的代码逻辑和重定位信息,为链接器的地址解析和符号绑定提供了必要的基础。所有机器指令已生成,但地址相关的部分都留有"空白",这正是链接阶段需要完成的关键工作。
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
链接是将多个可重定位目标文件合并生成可执行文件的关键过程,主要完成以下核心任务:
- 符号解析:将每个符号引用与唯一符号定义关联
- 地址分配:为代码段、数据段确定最终内存地址
- 重定位:根据符号地址修改引用处的机器代码
- 库整合:合并静态库并建立动态库链接关系
在hello项目中,链接器需要:
- 解析hello.o中的未定义符号(如printf)
- 合并C运行时库(如crt1.o、libc.so)
- 建立PLT/GOT动态链接机制
- 生成具有完整虚拟地址空间的可执行文件
5.2 在Ubuntu下链接的命令
关键参数说明:
- -no-pie:禁用位置无关可执行文件
- -dynamic-linker:指定动态链接器路径
- crt*.o:C运行时初始化文件
5.3 可执行目标文件hello的格式
使用readelf -l hello分析程序头表:
关键段分析:
- .interp:指定动态链接器路径(/lib64/ld-linux-x86-64.so.2)
- .text:代码段(VirtAddr 0x401000,Flags R E)
- .dynamic:动态链接信息表
- 三个LOAD段分别对应只读数据、可执行代码和读写数据
5.4 hello的虚拟地址空间
使用edb加载hello观察内存布局:
与ELF程序头表对比可见:
- 三个文件映射段与LOAD段完全对应
- 增加了动态链接相关的vvar/vdso段
- 用户栈空间单独分配(约132KB)
5.5 链接的重定位过程分析
1. 重定位机制解析
通过objdump -d -r hello
的输出可见,链接器已完成以下关键重定位操作:
- PLT/GOT机制实现:
00000000004010a0 <printf@plt>:
4010a0: f3 0f 1e fa endbr64
4010a4: ff 25 5e 2f 00 00 jmp *0x2f5e(%rip) # 404008 <printf@GLIBC_2.2.5>
-
- 所有外部函数调用通过PLT跳转表实现
- GOT表项(如0x404008)存储函数实际地址
- 首次调用时触发动态链接器解析
- 数据引用修正:
40118b: 48 8d 05 ad 0e 00 00 lea 0xead(%rip),%rax # 402038 <_IO_stdin_used+0x38>
-
- 字符串地址确定为0x402038
- 使用RIP相对寻址(0x401192 + 0xead = 0x402038)
2. 与hello.o的对比分析
特征项 | hello.o表现 | hello表现 | 链接器处理方式 |
函数调用 | call 0x0 (待重定位) | call 4010a0 printf@plt | 替换为PLT表跳转 |
数据引用 | lea 0x0(%rip),%rax | lea 0xead(%rip),%rax | 计算实际偏移量 |
全局变量 | 未分配地址 | 确定绝对地址(如402038) | 合并.data/.rodata段后分配地址 |
重定位类型 | R_X86_64_PLT32/R_X86_64_PC32 | 无重定位条目(已完成修正) | 根据类型计算新地址 |
3. 典型重定位案例
- printf调用处理:
- hello.o中的重定位项:
70: R_X86_64_PLT32 printf-0x4
-
- hello中的实现:
401193: e8 08 ff ff ff call 4010a0 <printf@plt>
-
-
- 操作数ffffff08表示-248(0x401198-0x4010a0)
- 通过PLT表实现延迟绑定
-
- 字符串地址引用:
- hello.o中的重定位项:
5b: R_X86_64_PC32 .rodata+0x2c
-
- hello中的实现:
40118b: 48 8d 05 ad 0e 00 00 lea 0xead(%rip),%rax
-
-
- 计算得0xead=3757(0x402038-0x401193)
-
4. 动态链接特征
- GOT表内容:
- 初始状态存储PLT回溯地址
- 首次调用后替换为实际函数地址
- PLT延迟绑定:
0000000000401020 <.plt>:
401020: ff 35 ca 2f 00 00 push 0x2fca(%rip) # 403ff0
401026: ff 25 cc 2f 00 00 jmp *0x2fcc(%rip) # 403ff8
-
- 通过_GLOBAL_OFFSET_TABLE_实现动态解析
- 每个PLT条目对应唯一GOT表项
5. 重定位技术总结
- PC相对重定位:
- 用于代码段内的数据引用
- 计算指令地址与目标地址的偏移量
- 绝对地址重定位:
- 用于全局变量访问
- 直接修改为最终虚拟地址
- PLT/GOT设计:
- 实现动态库函数的延迟绑定
- 保持代码段的位置无关性
通过重定位过程,链接器成功将多个目标文件整合为统一地址空间的可执行文件,同时建立动态链接机制,为程序运行提供完整的环境支持。
5.6 hello的执行流程
edb查看如下:
通过gdb跟踪的执行流程:
- 加载阶段:
- 内核加载ELF文件,映射到0x400000起始地址
- 启动动态链接器(ld-linux-x86-64.so.2)
- 初始化阶段:
- _start(0x401060):设置栈帧,调用__libc_start_main
- __libc_start_main:初始化libc,注册_fini回调
- 主程序执行:
- 调用main(0x401149)
- 执行printf等用户代码
- 终止阶段:
- 返回__libc_start_main
- 调用exit系统调用(编号60)
程序地址 | 程序名 |
0x0000000000401100 | hello!start |
0x0000000000401125 | hello!main |
0x0000000000401000 | hello!_init |
0x0000000000401140 | hello!_fini |
5.7 Hello的动态链接分析
1. 动态链接机制概述
hello程序采用动态链接方式使用glibc库函数,其核心机制包括:
- PLT(Procedure Linkage Table):
- 位于代码段(0x401020-0x40108e)
- 包含所有外部函数的跳转桩代码
- GOT(Global Offset Table):
- 位于数据段(0x403fd8-0x404028)
- 存储函数实际地址的指针表
- 延迟绑定(Lazy Binding):
- 首次调用时才解析函数地址
- 通过_dl_runtime_resolve实现
2. 关键数据结构分析
PLT/GOT布局:
3. 动态链接过程跟踪(GDB调试)
首次调用printf前的状态
单步执行到PLT:
查看寄存器状态
解析后的状态:
4. 关键地址变化对比表
项目 | 动态链接前地址 | 动态链接后地址 | 说明 |
printf@GOT | 0x4010a6 (PLT+6) | 0x7ffff7e3c8d0 (glibc) | 完成函数地址绑定 |
sleep@GOT | 0x4010e6 (PLT+6) | 0x7ffff7f0a7f0 (glibc) | 延迟绑定验证 |
GLOBAL_OFFSET_TABLE | 0x403fd8 | 不变 | 动态链接器访问的基址 |
5.动态链接器工作流程
- 首次调用处理:
- 通过_dl_runtime_resolve解析符号
- 修改GOT表项为实际函数地址
- 重定位.rela.plt条目
- 符号查找过程:
- 重定位条目验证:
6.动态段分析
关键动态标签:
- DT_NEEDED:依赖的共享库
- DT_JMPREL:PLT重定位表地址
- DT_PLTGOT:GOT表起始地址
该分析完整揭示了动态链接从延迟绑定到实际地址解析的全过程,展示了共享库函数的运行时加载机制。通过PLT/GOT设计和_dl_runtime_resolve
的协作,实现了高效灵活的动态链接功能。
5.8 本章小结
本章通过分析链接过程和动态链接机制,系统揭示了从可重定位目标文件到可执行文件的转换原理。研究表明,链接器通过符号解析和重定位两大核心机制,将分离编译的多个目标文件整合为统一的执行映像,其中静态链接完成地址空间分配和基础符号绑定,而动态链接则通过PLT/GOT机制实现高效的延迟绑定。关键发现包括:1)重定位过程严格遵循ABI规范处理PC相对引用和绝对地址;2)动态链接采用"首次调用解析"策略优化加载性能;3)GOT表在运行时经历从初始桩代码到实际地址的转变。这些机制共同构成了Linux环境下灵活高效的动态链接体系,既支持模块化开发,又保证了运行时效率。通过EDB/GDB调试验证,我们完整观测了从_start
到main
的执行流程,以及动态链接器解析外部函数的具体过程,为理解程序加载机制提供了实践依据。
第6章 hello进程管理
6.1 进程的概念与作用
进程是操作系统进行资源分配和调度的基本单位,具有以下核心特征:
- 动态执行实体:包含运行中的程序实例,具有生命周期
- 独立地址空间:每个进程拥有独立的虚拟内存空间(32位系统4GB)
- 资源容器:持有文件描述符、信号处理等系统资源
- 调度单位:参与CPU时间片轮转,通过PCB(进程控制块)保存状态
在hello程序执行中,进程机制实现了:
- 隔离性:防止错误扩散(如内存越界)
- 并发性:与其他进程并行执行
- 权限控制:用户态执行受限操作
6.2 简述壳Shell-bash的作用与处理流程
Bash的工作流程:
- 词法分析:将输入命令分解为token流
- 进程创建:
- 前台任务:直接调用fork+execve
- 后台任务(带&):额外调用setpgid设置进程组
- 环境维护:
- 维护jobs列表(通过jobs命令查看)
- 处理作业控制信号(Ctrl-Z/Ctrl-C)
关键数据结构:
- struct job:记录PID、状态、命令行
- termios:终端属性控制(影响Ctrl-Z等行为)
6.3 Hello的fork进程创建过程
fork()系统调用细节:
- 地址空间复制:
- 写时复制(COW)技术建立页表映射
- 父子进程共享只读物理页,写入时触发缺页异常复制
- 内核操作序列:
do_fork() → copy_process() → dup_task_struct() → copy_mm()
-
- 复制父进程PCB(task_struct)
- 建立新的内核栈和thread_info
- Hello进程特例:
- 继承bash的文件描述符(stdin/stdout/stderr)
- 共享同一个终端控制组(tty)
实验观察:
输出显示
:
6.4 Hello的execve过程
- 文件验证:
- 检查ELF魔数(0x7F+"ELF")
- 验证用户执行权限
- 地址空间重建:
flush_old_exec() → exec_mmap() → setup_arg_pages()
-
- 清空前进程映射(除vdso/vvar)
- 建立新程序的.text/.data/.bss映射
- Hello特有处理:
- 参数压栈:argv[]和envp[]布局在用户栈顶
- 动态链接器映射(ld.so地址随机化)
GDB验证:
可观察到:
- 原进程mm_struct被替换
- 新的vma区域建立(通过info proc mappings)
6.5 Hello的进程执行
- 时间片分配:
- Linux CFS调度器默认最小粒度1ms
- sched_slice()计算hello进程的时间片
- 上下文切换:
- 系统调用处理:
- printf触发write系统调用:
-
- 经历模式切换:用户态→内核态(通过MSR_LSTAR)
性能统计:
6.6 hello的异常与信号处理
异常类型 | 信号 | 触发场景 |
非法指令 | SIGILL | 执行无效机器码 |
段错误 | SIGSEGV | 访问0x0地址 |
算术异常 | SIGFPE | 除零操作 |
终端中断 | SIGINT | Ctrl-C |
终端停止 | SIGTSTP | Ctrl-Z |
信号处理实验:
- Ctrl-C处理:
-
- 内核发送SIGTSTP信号
- hello被移至后台暂停
- 进程状态查看:
6.7本章小结
本章通过hello程序的执行过程,完整揭示了Linux进程管理的核心机制:
- 生命周期管理:
- fork的COW机制优化进程创建
- execve的地址空间重建保证隔离性
- 调度体系:
- 时间片轮转实现宏观并行
- 上下文切换保障微观串行
- 异常处理:
- 信号机制实现异步事件响应
- 内核态/用户态协同处理硬件异常
关键发现包括:
- 进程创建成本主要来自页表复制(COW减少实际开销)
- 终端控制信号(如Ctrl-Z)通过终端驱动转换为信号
- 调度延迟主要来自CPU缓存污染(观察L3 cache-miss)
这些机制共同构成了现代操作系统多任务执行的基础,使得hello程序能安全高效地与其他进程共享系统资源。
第7章 hello的存储管理
7.1 hello的存储器地址空间
hello进程运行时涉及以下地址空间转换:
- 逻辑地址:程序直接使用的地址(如hello中mov 0x8048000,%eax)
- 线性地址:通过段式管理转换的地址(x86-64中等于虚拟地址)
- 虚拟地址:进程视角的连续地址空间(如hello的0x400000-0x401000代码段)
- 物理地址:实际DRAM芯片上的地址(由MMU通过页表转换)
示例转换流程:
逻辑地址 0x8048000 →(段式转换)→ 虚拟地址 0x8048000 →(页式转换)→ 物理地址 0x12345678
7.2 Intel逻辑地址到线性地址的变换-段式管理
x86-64架构的段式管理特点:
- 段寄存器作用:
- CS: 代码段(指向__TEXT)
- DS: 数据段(指向__DATA)
- SS: 栈段
- 转换公式:
线性地址 = 段基址(GDT/LDT) + 逻辑地址偏移
- hello实例:
- mov %fs:0x28,%rax 访问TLS段
- 实际段基址为0(64位模式下扁平内存模型)
7.3 Hello的线性地址到物理地址的变换-页式管理
- CR3寄存器:存储PGD(页全局目录)物理地址
- 转换步骤:
VA[47:39] → PGD索引 → PUD → PMD → PTE → 物理页基址 + VA[11:0]
7.4 TLB与四级页表支持下的VA到PA的变换
TLB加速转换流程:
- TLB查找:
- 虚拟地址高36位作为tag比对
- 命中则直接输出物理地址
- 未命中处理:
- 触发Page Walk遍历四级页表
- 典型耗时约300个时钟周期
- hello测试:
7.5 三级Cache支持下的物理内存访问
在现代计算机系统中,为了提高内存访问的速度,通常会使用多级缓存(Cache)。
7.5.1 缓存层级结构
(1)一级缓存(L1 Cache)
位置:最接近CPU核心,通常分为两个部分:指令缓存(L1i)和数据缓存(L1d)。
大小:通常较小(几KB到几十KB)。
速度:非常快,延迟通常在1到3个时钟周期。
(2)二级缓存(L2 Cache)
位置:紧接L1缓存,可能是每个CPU核心独有,也可能是每两个核心共享。
大小:比L1大(几百KB到几MB)。
速度:稍慢于L1缓存,延迟通常在10到20个时钟周期。
(3)三级缓存(L3 Cache)
位置:通常为整个处理器共享,所有核心都可以访问。
大小:较大(几MB到几十MB)。
速度:慢于L2缓存,延迟通常在几十到上百个时钟周期。
7.5.2 缓存访问过程
当CPU需要访问某个物理地址时,三级缓存架构的访问过程如下:
(1)CPU发出内存访问请求
CPU生成一个物理地址来访问数据(假设地址为PA)。
(2)L1缓存查找
CPU首先在L1缓存中查找PA。如果命中(hit),L1缓存返回数据给CPU,访问结束。如果未命中(miss),请求发送到L2缓存。
(3)L2缓存查找
在L2缓存中查找PA。如果命中(hit),L2缓存返回数据给CPU,并且可能将数据复制到L1缓存。如果未命中(miss),请求发送到L3缓存。
(3)L3缓存查找
在L3缓存中查找PA。如果命中(hit),L3缓存返回数据给CPU,并且可能将数据复制到L2和L1缓存。如果未命中(miss),请求发送到主内存(DRAM)。
(4)内存访问
在L3缓存未命中的情况下,访问请求发送到主内存。主内存返回数据给L3缓存,并且可能复制到L2和L1缓存。最终,数据从L1缓存返回给CPU。
7.5.3 缓存一致性
为了确保多核处理器中所有核心对内存的一致视图,通常采用缓存一致性协议(如MESI、MOESI)。这些协议管理缓存之间的数据一致性,确保当一个核心修改缓存中的数据时,其他核心能够看到最新的数据。
7.6 hello进程fork时的内存映射
写时复制(COW)实现细节:
- 页表复制:
- 父子进程共享同一物理页
- 页表项标记为只读(PTE_RDONLY)
- 写操作处理:
- 触发缺页异常(#PF)
- 内核分配新物理页并复制内容
- 观察方法:
grep -e Anon -e hello /proc/$(pidof hello)/smaps
7.7 hello进程execve时的内存映射
内存空间重建过程:
- 旧映射清除:
- 释放用户空间所有vma
- 保留vdso/vvar(0x7fffe0000000)
- 新映射建立:
- 文本段:0x400000(只读)
- 数据段:0x601000(读写)
- 堆栈:0x7ffffffde000
- 验证命令:
7.8 缺页故障与缺页中断处理
7.8.1 缺页故障(Page Fault)
虚拟内存在DRAM缓存不命中即为缺页故障。
7.8.2 缺页中断处理
缺页中断处理:触发缺页异常时启动缺页处理程序
1、缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
2、缺页处理程序页面调入新的页面,并更新内存中的PTE
3、缺页处理程序返回到原来的进程,再次执行导致缺页的命令。
7.9动态存储分配管理
printf调用的malloc实现:
- glibc分配策略:
- <64KB:使用brk扩展堆
- 64KB:使用mmap匿名映射
- 分配器优化:
- tcache(每线程缓存):32个LIFO单链表
- fastbin:≤80KB的快速分配链
- hello验证:
7.10本章小结
通过hello程序分析,我们验证了现代操作系统的存储管理机制:
- 地址转换体系:
- 64位模式下段式管理弱化
- 四级页表支持256TB虚拟地址空间
- 性能优化技术:
- TLB减少地址转换开销
- 三级缓存降低访存延迟
- COW优化进程创建效率
- 关键数据:
- 典型页表遍历耗时:≈300周期
- L1缓存命中率:>95%
- 缺页处理延迟:≈10μs(无IO时)
这些机制共同保障了hello程序的高效运行,同时实现了进程间的安全隔离。通过/proc文件系统和性能计数器,我们得以量化观察这些抽象机制的实际表现。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux采用统一的文件抽象管理所有IO设备,核心机制包括:
- 设备文件化:
- 字符设备:/dev/ttyS0(串口)
- 块设备:/dev/sda(磁盘)
- 特殊文件:/dev/stdout(标准输出)
- VFS抽象层:
hello通过此统一接口访问终端设备
- 设备树示例:
8.2 简述Unix IO接口及其函数
hello使用的关键IO函数:
函数 | 作用 | 系统调用号 |
printf | 格式化输出 | 通过write |
getchar | 字符输入 | read(0) |
write | 原始字节写入 | 1 |
read | 原始字节读取 | 0 |
8.3 printf的实现分析
- 格式化处理:
- 系统调用触发:
- 终端驱动处理:
- 调用tty_write()
- 转换换行符(LF→CRLF)
- 处理输出缓冲(默认行缓冲)
- 显示硬件交互:
- 显卡驱动将ASCII转为字模
- 写入显存(VRAM)的文本缓冲区
- 通过VGA/HDMI接口周期刷新(60Hz)
性能统计
8.4 getchar的实现分析
- 硬件中断:
- 键盘控制器产生IRQ1
- 中断服务程序读取扫描码(0x1C→回车)
- 输入转换:
- 系统调用处理:
- 缓冲机制:
- 行缓冲模式(等待回车)
- 特殊键处理(Ctrl-C→SIGINT)
8.5本章小结
- 抽象层次:
- 从libc函数到系统调用
- 经过VFS、设备驱动、硬件控制器
- 关键性能数据:
- 终端写入延迟:≈500ns(无阻塞时)
- 键盘中断响应:<100μs
- 显存刷新延迟:16.7ms(60Hz)
- 特殊处理机制:
- TTY行缓冲影响printf行为
- 键盘扫描码到ASCII的转换表
- VT100控制字符处理(如\n转换)
通过strace和内核调试工具,我们验证了从高级语言函数到底层硬件信号的全链路IO处理过程,展现了Linux"一切皆文件"设计的精妙之处。这些机制共同保障了hello程序与用户的标准IO交互能高效可靠地执行。
结论
- 文本到指令的涅槃:
- 预处理展开#include与宏,生成纯C代码(hello.i)
- 编译器(GCC)通过词法/语法/语义分析,生成x86-64汇编(hello.s)
- 汇编器将助记符转换为机器码,构建可重定位目标文件(hello.o)
- 链接器解析符号引用,合并动态库(libc.so),生成位置无关可执行文件(PIE)
- 进程的生死轮回:
- Shell通过fork()创建子进程副本,execve()将hello二进制加载至虚拟地址空间
- 加载器按ELF程序头建立代码/数据段映射,动态链接器(ld.so)解析PLT/GOT实现延迟绑定
- CPU按rip执行.text指令,MMU通过四级页表+TLB完成VA→PA转换,三级缓存加速数据访问
- 进程终止时,内核回收物理页、文件描述符,父进程通过wait()回收僵尸进程
- 系统组件的交响协作:
- 编译器工具链(GCC)实现语言抽象到机器指令的降级
- 操作系统通过进程调度、虚拟内存、文件系统管理资源
- 硬件体系(CPU流水线、Cache、MMU)保障执行效率与安全隔离
二、计算机系统设计与实现的深层启示
- 抽象与协作之美:
- 层次化抽象:从高级语言到机器码的逐层转换(C→Asm→ELF→机器状态),每层隐藏下层复杂性
- 硬件-软件协同:TLB加速地址转换(硬件)、COW优化fork()(OS)、PLT实现动态链接(编译系统)的跨层优化
- 模块化设计:ELF格式将代码/数据/元数据分离,使链接、加载、动态绑定得以高效实现
- 创新优化方向:
- 智能预取与缓存管理:基于Hello的访存模式分析(如循环访问数组),设计L2/L3 Cache的机器学习预取策略,降低for循环的cache miss率
- 轻量级动态链接:针对小型程序(如Hello),提出静态PLT/GOT模板,减少动态链接时的页表遍历开销
- 进程快速恢复机制:利用fork()的COW特性,设计检查点快照,实现Hello进程异常退出后的状态秒级回滚
- 系统思维培养:
- 全局视角:Hello的printf调用涉及编译器生成write系统调用、内核处理tty缓冲、显卡驱动刷新显存的全链路协作
- 权衡艺术:链接时地址绑定(静态vs动态)、存储管理中Cache大小与命中率的取舍,体现系统设计中的trade-off哲学
- 透明化复杂性:硬件异常(如缺页)被OS无缝处理,使程序员聚焦业务逻辑,彰显计算机系统的工程智慧
三、研究价值升华
通过解剖Hello这一"麻雀",揭示了现代计算机系统如何通过多层级抽象(语言→OS→硬件)将静态代码转化为动态计算服务。每一层既是下一层的用户,又是上一层的服务提供者,这种递归式的架构设计,使得复杂系统的构建成为可能。未来,随着异构计算与AI技术的融合,系统设计需在兼容性(如RISC-V)、安全性(侧信道防御)、智能化(自主调优)之间寻求新平衡,而Hello案例中展现的核心原理,仍将是支撑这场变革的基石。
附件
文件名 | 文件类型 | 生成阶段 | 作用说明 |
hello.c | C源代码 | 初始输入 | 用户编写的原始程序源码,包含main函数及printf等系统调用 |
hello.i | 预处理后C代码 | 预处理 | 展开所有#include头文件(含stdio.h)、宏替换后的完整C代码,行数扩展至数千行 |
hello.s | 汇编代码 | 编译 | 由GCC生成的x86-64架构汇编代码,包含.section指令划分代码/数据段,使用AT&T语法 |
hello.o | 可重定位目标文件 | 汇编 | ELF格式机器码,含未链接的符号(如printf@PLT),.rela.text记录8个重定位条目 |
hello | 可执行文件 | 链接 | 动态链接的ELF可执行文件,已分配虚拟地址(如.text段0x401000),含GOT/PLT表 |
分析报告文件 | |||
hello_elf.txt | 文本报告 | 目标文件分析 | readelf -a hello.o输出,展示节头表、符号表、重定位表等ELF结构信息 |
hello_asm.txt | 反汇编文本 | 目标文件分析 | objdump -d hello.o生成的汇编指令与机器码对照,显示待重定位的call 0占位符 |
hello1_elf.txt | 文本报告 | 可执行文件分析 | readelf -a hello输出,含程序头表(LOAD段)、动态段(.dynamic)、解释器路径(/lib64/ld-linux-x86-64.so.2) |
hello1_asm.txt | 反汇编文本 | 可执行文件分析 | objdump -d hello结果,显示重定位后的实际地址(如call 401030 <printf@plt>) |
调试跟踪文件 | |||
gdb_trace.log | 调试日志 | 执行分析 | GDB记录的进程启动、断点命中、寄存器状态(如rip值变化、rax返回值) |
edb_memory.dump | 二进制内存快照 | 存储分析 | EDB导出的进程虚拟地址空间布局(代码段0x401000、数据段0x404000、栈空间0x7ffffffde000等) |
perf_stat.txt | 性能分析报告 | 优化验证 | perf stat输出的CPI(Cycles Per Instruction)、缓存命中率(L1-dcache-load-misses)等关键指标 |
文件作用亮点
- hello.o的.rela.text节:记录8处需重定位地址,包含R_X86_64_PLT32(函数调用)与R_X86_64_PC32(数据引用)类型
- hello的GOT表(0x404018):动态链接时被ld.so改写,存储printf等函数的实际地址
- hello1_asm.txt中的<main>:反汇编显示push %rbp; mov %rsp,%rbp栈帧建立指令,验证函数调用规范
参考文献
[1] 《深入理解计算机系统》(CSAPP)Randal E. Bryant, David R. O'Hallaron
机械工业出版社, 第3版, 2016
[2] 《Intel® 64 and IA-32 Architectures Software Developer’s Manual》 Intel Corporation, 2023
[3] CSDN
[4] 《A Survey of Cache Replacement Policies》 IEEE Transactions on Computers, 2020
[5] OSDev Wiki - x86分段与分页
[6] GCC官方文档