14.1 目标文件与 Section
汇编器通常不会直接产生完整程序,而是产生目标文件(object file):其中装有机器码字节、数据、名称,以及留给后续阶段完成的工作说明。类 Unix 系统常用 ELF,macOS 使用 Mach-O,Windows 使用 PE/COFF。三者的编码不同,却在解决同一个问题:编译时无需知道所有最终地址,同时保留足够结构,让链接器和加载器继续工作。
因此,目标文件不只是机器字节的简单堆积。文件头会说明格式、目标架构、字节序、机器字长和各张表的位置;section table 描述具名区域;symbol table 描述定义与尚未解析的引用;relocation record 标记哪些字段要等布局或其他目标文件确定后才能填写。可选元数据还支持源码调试、异常回栈、安全策略与构建工具。
Section 按语义为字节分类
编译器与汇编器会把生命周期和权限需求不同的内容放进不同 section:
.text 可执行机器指令
.rodata 不可变常量、字符串、跳转表
.data 已初始化的可写全局量
.bss 零初始化的可写全局量
.symtab 完整链接/调试符号表
.strtab 表项引用的字符串
.debug_* 源码行、类型、作用域、变量位置
.eh_frame 栈回溯规则假设源码定义了 const char banner[] = "hi"、int count = 3、int cache[1024] 和函数 tick。tick 的指令进入 .text;字符串通常进入 .rodata;count 需要在 .data 中保存四个已初始化字节;cache 则属于 .bss。目标文件会记录 .bss 在内存中需要 4096 字节,却不会在磁盘上真的写入 4096 个零。ELF 用 NOBITS section type 表达这种差异,所以文件大小与运行时内存大小是两个不同指标。
对齐、padding 与 section flags
每个 section 都有 alignment 要求。若 .text 结束于文件偏移 0x113,而 .rodata 要求 16 字节对齐,下一个合法起点就是 0x120,中间空隙是 padding。对齐使指令、向量常量和原子对象从 ISA 与 ABI 能高效或合法访问的位置开始。过大的对齐会浪费文件与地址空间,因此后端应携带真实所需 alignment,而不是给所有内容套用最大值。
Section flags 会描述内容是否需在运行时分配、可写、可执行、可合并,或由固定长度字符串组成。它们是链接器的输入,却不等于最终内存布局。链接器会合并来自多个目标文件的兼容 input sections;启用时删除不可达 section;解析布局;最后把 output sections 组合成 segments。操作系统使用页粒度地址区间与权限,所以加载器映射的是 segment,通常不会逐个映射编译器 section。
例如,.text 与只读元数据可分别进入 R-X 和 R-- load segment,而 .data、.bss 进入 RW- segment。把可写数据排除在可执行页面之外可满足 W^X 原则。RELRO 会在动态加载器完成 relocation 后,把特定表改成只读。栈策略与 thread-local storage 也可能由专门的 program header 描述。
Section 与 segment,以及可重定位文件与可执行文件
这个区别可以消除常见误解:section 为链接与工具组织信息;segment 为加载组织运行时映像。可重定位 .o 文件以 section 为主,通常还没有最终虚拟地址;可执行文件或共享对象则含 program headers,告诉加载器:哪个文件范围要映射到哪个虚拟地址、需要多大内存、何种对齐与权限。
并非每个 section 都会装入内存。.debug_*、.symtab 和许多 note section 可以只留在磁盘上供调试器使用,也能被 strip 删除而不改变正常执行;但若随意删掉 dynamic symbol table,动态链接就会失败。生产级 emitter 应使用 readelf、objdump、llvm-readobj、otool 或 dumpbin 等检查工具测试这条边界:验证 section type 与 flags,确认 .bss 不占文件 payload,对照 section/segment 两种视图,并保证除非运行时确有需要,没有 loadable page 同时具备 writable 与 executable 权限。