知行LEARNING HANDBOOK
目录 · N05 编译、链接、Mach-O 装载与虚拟地址布局
学习手册/计算机基础/程序的编译、链接与装载
N056 分钟更新于 2026-08-20

编译、链接、Mach-O 装载与虚拟地址布局

串联编译、链接、Mach-O 装载和虚拟地址布局,建立程序运行的完整路径。

compilerlinkerloadermachoaslrvirtual-address

建议先了解

  • 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)允许整个程序映像从不同基址装载。

理解公式:

VAruntime=Baseload+OffsetimageVA_{runtime} = Base_{load} + Offset_{image}

因此不同运行中绝对地址可以变化,而某些内部相对偏移仍保持稳定。

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. 代码、命令或公式

核心公式:

RuntimeAddress=LoadBase+RelativeOffsetRuntimeAddress = LoadBase + RelativeOffset

用于理解 PIE/ASLR 下程序映像的相对布局。

[!WARNING] 真实 Mach-O、dyld、重定位、共享缓存和 ASLR 行为更复杂;该公式是本次对话用于建立模型的抽象,不是完整 loader 算法。

8. 容易混淆的概念

概念 A概念 B核心区别
编译期 offset运行时绝对地址前者可提前确定,后者可能要加载后才知道
link-time addressruntime addressPIE/ASLR 下可能不同
load commandmachine instruction前者是元数据,后者是 CPU 指令
segmentsectionsegment 更接近运行时映射,section 是更细的内容组织
.oexecutable.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 会看到 vmaddrfileoff、权限等数据字段,而不是 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. 自测问题

  1. 编译器通常知道局部变量的什么地址信息?
  2. 为什么 malloc() 返回地址不可能在链接阶段固定?
  3. PIE + ASLR 下哪些关系通常更稳定?
  4. Mach-O load command 为什么不是机器指令?
  5. LC_SEGMENT_64LC_LOAD_DYLIB 的职责有什么区别?
  6. .o 与最终 executable 的角色有什么区别?
参考答案
  1. 通常知道 stack frame 内的相对 offset,而不是最终绝对虚拟地址。
  2. 因为动态分配发生在程序运行时,由 allocator 和当前地址空间状态决定。
  3. 程序映像内部各部分的相对布局/偏移关系。
  4. 它是 loader/linker 读取的文件元数据,不是 CPU opcode。
  5. 前者描述 segment 映射,后者描述动态库依赖。
  6. .o 是待链接目标文件,最终 executable 是可被 loader 建立进程映像的文件。

14. 一句话总结

编译和链接主要确定程序映像及相对布局,loader 决定运行时映射;Mach-O load commands 是描述这套装载过程的数据结构,而不是机器指令。