ELF 文件简介:从 Section 到 Segment
用结构、示例与工具把 ELF 的类型、布局、重定位和动态链接串起来。
ELF(Executable and Linkable Format)是类 Unix 系统中最常见的目标文件与可执行文件格式。理解它的结构,你就能把“编译—链接—加载—运行”的关键路径串起来。
ELF 的三种类型
- 可重定位文件(Relocatable):编译器/汇编器输出的目标文件(
.o),等待链接器合并与修正地址。 - 可执行文件(Executable):可直接被加载并执行的程序。
- 共享库(Shared Object):运行时动态链接的库(
.so)。
从源代码到运行:一条清晰的链路
源代码/汇编
↓ 编译/汇编
目标文件(.o,含 Section)
↓ 链接
可执行文件(ELF,含 Segment)
↓ 加载器
映射到内存并执行
两种视角:Section vs Segment
ELF 提供两套视角:
- 链接器视角:ELF 是一组 Section,保存代码、数据、符号、重定位信息等。
- 加载器视角:ELF 是一组 Segment,描述需要映射到内存的区域以及权限(R/W/X)。
一个简化的对应关系示意:
Section 视角(链接器) Segment 视角(加载器)
[ELF Header] [ELF Header]
[Section Header Table] [Program Header Table]
.text LOAD (R-X) <- .text
.data LOAD (RW-) <- .data + .bss
.bss
.symtab/.strtab
.rel.*
目标文件示例:ELF Header + Section 表
下面是一个 目标文件(.o) 的 readelf -h 输出节选:
ELF Header:
Class: ELF64
Data: 2's complement, little endian
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x0
Start of section headers: 0x2c0 (bytes into file)
Number of section headers: 12
readelf -S 的 Section 表节选(仅示意关键列):
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 1] .text PROGBITS 0000 0040 0038 00 AX 0 0 16
[ 2] .rel.text REL 0000 0210 0018 08 6 1 8
[ 3] .data PROGBITS 0000 0080 0020 00 WA 0 0 8
[ 4] .bss NOBITS 0000 00a0 0010 00 WA 0 0 8
[ 5] .symtab SYMTAB 0000 0240 00f0 18 6 8 8
[ 6] .strtab STRTAB 0000 0330 0048 00 0 0 1
字段要点:
- Addr:加载地址(目标文件里常为 0,待链接修正)。
- Off/Size:在文件中的偏移与大小。
- Flg:权限标记(A=可分配,X=可执行,W=可写)。
目标文件布局示意
0x0000 ELF Header
0x0040 .text
0x0080 .data
0x00a0 .bss(文件中不占空间)
0x0210 .rel.text
0x0240 .symtab
0x0330 .strtab
0x02c0 Section Header Table
可执行文件示例:Program Header 与 Segment
链接完成后,可执行文件会出现 Program Header(Segment 表):
Program Headers:
Type Offset VirtAddr FileSiz MemSiz Flg Align
LOAD 0x0000 0x400000 0x0800 0x0800 R E 0x1000
LOAD 0x1000 0x601000 0x0200 0x0300 RW 0x1000
解释:
- 第一段 LOAD:包含
.text,权限 R-X。 - 第二段 LOAD:包含
.data + .bss,权限 RW-。 MemSiz > FileSiz通常说明.bss仅占内存而不占文件空间。
可用 readelf -l 查看 Section to Segment mapping,理解“Section 合并成 Segment”的过程。
重定位:把“占位地址”改成“真实地址”
目标文件中常见“占位地址”,链接器根据 .rel.* 修正它们。
一个简化示意(伪汇编):
mov data_items(%rip), %rax ; 访问全局数组
在目标文件里,编码可能是占位地址:
8b 04 bd 00 00 00 00
链接后被改成真实地址:
8b 04 bd a0 90 04 08
对应的重定位条目(节选):
Relocation section '.rel.text' contains 1 entry:
Offset Info Type Sym.Name
0x0008 ... R_X86_64_32 data_items
核心点:链接器根据重定位表,在特定偏移修正指令或数据。
共享库与 PIC / GOT / PLT
共享库需要在任意地址加载,通常使用 PIC(位置无关代码):
- GOT(Global Offset Table):保存变量/函数的真实地址。
- PLT(Procedure Linkage Table):函数调用跳板,用于延迟绑定。
一个常见的 PLT 入口(简化示意):
push@plt:
jmp *GOT[push]
pushq $reloc_index
jmp plt0
第一次调用会进入动态链接器;后续调用直接通过 GOT 跳转到真实地址。
动态链接流程(高层)
- 动态链接器加载依赖库。
- 首次调用外部符号,PLT 触发解析。
- 解析结果写入 GOT。
- 后续调用直接跳转,减少开销。
常用工具清单
# ELF 头、节、段
readelf -h a.out
readelf -S a.out
readelf -l a.out
# 反汇编、符号
objdump -d a.out
objdump -t a.out
nm -n a.out
# 体积、依赖、字符串
size a.out
ldd a.out
strings a.out
小结
理解 ELF 的关键在于:链接器关心 Section,加载器关心 Segment。
目标文件强调“可链接”,可执行文件强调“可加载”,共享库强调“可重定位与动态链接”。
FAQ
Q1:为什么可执行文件还保留 Section Header Table?
A:加载器不需要,但调试与分析工具需要它来理解符号与结构。
Q2:.bss 为什么不占文件空间?
A:它只记录大小,加载时分配内存并清零即可。
Q3:目标文件里很多地址为什么是 0?
A:它们是占位地址,等待链接器根据重定位表修正。
Q4:共享库为什么要用 PIC?
A:共享库要在不同进程、不同地址加载,PIC 避免绝对地址写死。
Q5:为什么 Segment 权限以页为单位?
A:MMU 以页为最小保护单位,代码与数据通常分离到不同页。