CFN Cloud
2026-01-09

ELF 文件简介:从 Section 到 Segment

用结构、示例与工具把 ELF 的类型、布局、重定位和动态链接串起来。

ELF(Executable and Linkable Format)是类 Unix 系统里最常见的目标文件与可执行文件格式。

很多人第一次“真正需要 ELF”不是在课堂上,而是在现场:

  • ldd 看着依赖是对的,但程序一跑就报 not found / wrong ELF class
  • 同一台机上有两份 libssl.so,线上偶发崩溃、符号找不到(undefined symbol
  • 你拿到一个二进制,只能问:它到底是静态的?动态的?入口在哪?加载了哪些段?

理解 ELF 的结构,你就能把“编译 -> 链接 -> 加载 -> 运行”这一条链路串起来,也能更快定位这些问题。

3 分钟先能用:排查动态链接问题的最短路径

先记住一条经验:ldd 只是“按当前环境模拟一次解析”,不等于真实运行时百分百一致

当你遇到“依赖库找不到/版本不对”时,按下面顺序走:

  1. 看 ELF 头和目标平台(32/64 位、架构):
file ./a.out
readelf -h ./a.out | head
  1. 看它“声明了要找哪些动态库”(NEEDED):
readelf -d ./a.out | grep NEEDED
  1. 看运行时搜索路径(RPATH/RUNPATH):
readelf -d ./a.out | grep -E 'RPATH|RUNPATH'
  1. 看动态链接器实际怎么找(必要时开调试):
LD_DEBUG=libs,files ./a.out 2>&1 | head -n 80
  1. 再回头查系统缓存与路径:
ldconfig -p | grep <libname>
echo $LD_LIBRARY_PATH

后面的内容会把这些命令背后的“为什么”讲清楚。

ELF 的两套视角

ELF 包含两套对同一份二进制的描述:

  • Section(链接器视角):编译和静态链接时使用的信息——代码、数据、符号、重定位表。
  • Segment(程序头/加载器视角):内核加载器实际映射到内存的区域,带权限(R/W/X)。

简化映射:

Section(链接器)              Segment(加载器)
.text                          LOAD (R-X)
.data + .bss                   LOAD (RW-)
.symtab / .strtab / .rel.*    不加载(仅用于链接器/调试器)

还有三种 ELF 类型——REL(可重定位 .o)、EXEC(可执行)、DYN(共享库)。类型决定了加载方式:DYN 需要动态链接器介入,EXEC 不需要,内核直接加载。

目标文件示例: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 跳转到真实地址。

动态链接再补一刀:为什么我明明装了库,程序还是找不到?

这里有一个很典型的坑:你安装了库文件(比如 /usr/local/lib/libfoo.so),ldd 也“看起来没问题”,但运行时还是报错。常见原因就三类:

  1. 库在磁盘上,但不在默认搜索路径里:默认只搜 /lib/usr/lib 等,/usr/local/lib 需要 ldconfig 更新缓存或配置 ld.so.conf.d
  2. 你有多份同名库RUNPATH/RPATHLD_LIBRARY_PATH 的优先级会让你“以为用的是 A,实际加载了 B”。
  3. ABI/架构不匹配:比如 64 位程序去加载 32 位库(wrong ELF class)。

一个实践建议:如果你只是临时验证加载顺序,优先用 LD_DEBUG=libs,它比猜测更快。

常用工具清单

# 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

参考链接