编译、链接、Mach-O 装载与虚拟地址布局
串联编译、链接、Mach-O 装载和虚拟地址布局,建立程序运行的完整路径。
建议先了解
- C 基础
- 虚拟地址基本概念
1. 这篇笔记解决什么问题
本篇整理“程序地址究竟什么时候确定”和“Mach-O load command 是什么”这两条紧密相关的主线。
学完后应能解释:编译阶段是否已经知道最终虚拟地址;链接器确定的是绝对地址还是相对布局;ASLR/PIE 如何改变运行地址;栈、堆、全局变量分别何时得到实际地址;Mach-O 的 load command 为什么不是机器指令。
2. 前置知识
- 编译与链接的基本流程;
- 虚拟地址与物理地址基本区分;
- C 的局部变量、全局变量和
malloc()。
待补充:Mach-O segment 与 section 的精确定义。
3. 核心概念
3.1 编译阶段
编译器主要决定:
- 类型大小;
- C struct 成员 offset;
- 局部变量在 stack frame 中的相对位置;
- 机器代码;
- 目标文件中的符号和重定位信息。
它通常不需要知道程序运行时所有对象的最终绝对虚拟地址。
3.2 链接阶段
链接器把多个目标文件和库组合为最终 executable image,并确定:
.text / __TEXT
.rodata
.data / __DATA
.bss
...
的整体布局与符号关系。
对于传统 non-PIE 且没有地址随机化的模型,代码和静态数据的链接时虚拟地址可以基本固定;现代 PIE + ASLR 下,更重要的是相对布局。
3.3 加载阶段
程序启动后,内核与 loader/dyld:
- 创建进程虚拟地址空间;
- 按 executable metadata 建立映射;
- 加载/映射动态库;
- 设置页面权限;
- 设置程序入口;
- 配合 ASLR 选择运行时基址。
3.4 ASLR 与 PIE
位置无关可执行文件(Position Independent Executable,PIE)允许整个程序映像从不同基址装载。
理解公式:
因此不同运行中绝对地址可以变化,而某些内部相对偏移仍保持稳定。
3.5 Mach-O Load Command
load command 是结构化元数据,不是 CPU 机器指令。
Mach-O 可概念化为:
Mach-O file
├── Mach Header
├── Load Commands
└── 代码/数据/链接信息
例如:
LC_SEGMENT_64
LC_LOAD_DYLIB
LC_MAIN
LC_SYMTAB
LC_CODE_SIGNATURE
它们分别描述 segment 映射、动态库依赖、入口、符号表位置、签名位置等。
真正给 CPU 执行的机器指令位于代码区域,例如 __TEXT,__text。
4. 直观理解
可执行文件可以理解成:
“程序内容” + “如何装载这些内容的说明书”
load command 就是说明书中的结构化记录。loader 读取这些记录后建立虚拟内存映射,而不是让 CPU 把它们当 opcode 顺序执行。
5. 工作原理
5.1 三种变量的地址确定时间
int global = 10;
int main(void) {
int x = 20;
int *p = malloc(sizeof(int));
}
global
属于静态程序映像。
编译/链接 → 确定它在映像中的位置
加载 → 映射到实际进程虚拟地址
无 ASLR + non-PIE 时,绝对地址可能在链接后基本固定;PIE + ASLR 时最终地址由运行时基址决定。
x
属于 stack frame。
编译器通常只知道:
x = frame base + 某个 offset
实际 stack/frame base 在运行时才形成。
malloc()
返回地址由运行时 allocator 决定,链接器不可能预先知道某次动态分配的具体地址。
5.2 Mach-O 中 LC_SEGMENT_64
它可描述:
segment name
vmaddr
vmsize
fileoff
filesize
protection
loader 根据这些信息把文件内容映射到虚拟地址空间并设置权限。
5.3 .o 与最终 executable
source.c
↓ compiler
source.o
↓ linker
Mach-O executable
↓ kernel / dyld
process
.o 也是 Mach-O 格式的一种可能形式,但主要供 linker 继续处理;最终 executable 才作为进程映像启动。
6. 示例
示例:ASLR 下的相对布局
链接时概念布局:
.text +0x0000
.data +0x5000
一次运行:
base = 0x555510000000
.text = base + 0x0000
.data = base + 0x5000
另一次运行 base 可能变化,但 .data - .text 的相对关系可以保持。
7. 代码、命令或公式
核心公式:
用于理解 PIE/ASLR 下程序映像的相对布局。
[!WARNING] 真实 Mach-O、dyld、重定位、共享缓存和 ASLR 行为更复杂;该公式是本次对话用于建立模型的抽象,不是完整 loader 算法。
8. 容易混淆的概念
| 概念 A | 概念 B | 核心区别 |
|---|---|---|
| 编译期 offset | 运行时绝对地址 | 前者可提前确定,后者可能要加载后才知道 |
| link-time address | runtime address | PIE/ASLR 下可能不同 |
| load command | machine instruction | 前者是元数据,后者是 CPU 指令 |
| segment | section | segment 更接近运行时映射,section 是更细的内容组织 |
.o | executable | .o 主要供链接器处理,executable 可被 loader 启动 |
| stack 地址 | heap 地址 | 栈由调用栈形成,heap 动态分配;两者都不是链接阶段固定对象地址 |
9. 常见误区
误区:编译时已经给所有变量分配了最终虚拟地址
错误原因:混淆“相对布局”和“运行时 placement”。
正确理解:编译器常决定 offset;loader、stack runtime、allocator 决定许多最终地址。
如何验证:多次运行启用 ASLR 的程序打印地址。
误区:Mach-O 的 18 个 load commands 就是 18 条 CPU 指令
错误原因:看到 command 一词就按 instruction 理解。
正确理解:ncmds=18 表示文件头后有 18 项装载/链接相关结构化记录。
如何验证:查看 LC_SEGMENT_64 会看到 vmaddr、fileoff、权限等数据字段,而不是 opcode。
误区:所有 load command 都负责“搬一段到内存”
错误原因:忽略动态库、符号表、入口、代码签名等元数据命令。
正确理解:只有部分命令直接描述 segment 映射。
10. 与其他知识的关系
待建链接:Mach-O 文件格式
待建链接:链接与重定位
11. 可以亲手完成的验证
实验目标:观察全局、栈和 heap 地址。
#include <stdio.h>
#include <stdlib.h>
int global = 10;
int main(void) {
int local = 20;
int *heap = malloc(sizeof(int));
printf("global = %p\n", (void *)&global);
printf("local = %p\n", (void *)&local);
printf("heap = %p\n", (void *)heap);
free(heap);
return 0;
}
多次运行比较。
预期:现代启用 ASLR 的环境中,一些地址会变化。
实验不能证明:一次平台实验不能推出所有 Mach-O/ELF loader 的通用具体地址规则。
12. 尚未解决的问题
- Mach-O segment 与 section 的精确映射关系未展开。
LC_MAIN如何最终转化为初始 PC 未展开。- relocation、GOT/PLT、lazy binding 未整理。
- dyld shared cache 与普通动态库映射差异未讨论。
13. 自测问题
- 编译器通常知道局部变量的什么地址信息?
- 为什么
malloc()返回地址不可能在链接阶段固定? - PIE + ASLR 下哪些关系通常更稳定?
- Mach-O load command 为什么不是机器指令?
LC_SEGMENT_64和LC_LOAD_DYLIB的职责有什么区别?.o与最终 executable 的角色有什么区别?
参考答案
- 通常知道 stack frame 内的相对 offset,而不是最终绝对虚拟地址。
- 因为动态分配发生在程序运行时,由 allocator 和当前地址空间状态决定。
- 程序映像内部各部分的相对布局/偏移关系。
- 它是 loader/linker 读取的文件元数据,不是 CPU opcode。
- 前者描述 segment 映射,后者描述动态库依赖。
.o是待链接目标文件,最终 executable 是可被 loader 建立进程映像的文件。
14. 一句话总结
编译和链接主要确定程序映像及相对布局,loader 决定运行时映射;Mach-O load commands 是描述这套装载过程的数据结构,而不是机器指令。