Appearance
.rela.* —— 重定位节
链接器要把不同
.o里的代码拼在一起,动态链接器要把.so的函数地址填到正确的位置——这些"修正地址"的动作就叫重定位(Relocation)。.rela.text、.rela.dyn、.rela.plt分别对应链接时重定位、加载时数据重定位、PLT 延迟绑定重定位。本篇把重定位表的结构、类型、和三种重定位的时机区别一次性讲透。
更新时间:2026-08-06
一、术语前置
| 术语 | 全称 | 含义 |
|---|---|---|
| RELA | Relocation with Addend | x86-64 的重定位格式(含显式加数 r_addend) |
| REL | Relocation without Addend | 32 位 x86 的重定位格式(加数藏在被修正位置) |
r_offset | Relocation Offset | 要被修正的地址(对于共享库/PIE,是相对基址的偏移) |
r_info | Relocation Info | 编码了重定位类型(低 32 位)和符号索引(高 32 位) |
r_addend | Relocation Addend | 加数——重定位计算时要额外加上的常量 |
| R_X86_64_JUMP_SLOT | Jump Slot | PLT 延迟绑定专用的重定位类型 |
| R_X86_64_GLOB_DAT | Global Data | 全局变量地址重定位 |
| R_X86_64_RELATIVE | Relative | 基址相关的重定位(不需符号表) |
| R_X86_64_64 | 64-bit Absolute | 链接时重定位:填 64 位绝对地址 |
| R_X86_64_PC32 | 32-bit PC-relative | 链接时重定位:填 32 位 PC 相对偏移 |
二、重定位在哪里发生
重定位贯穿了 ELF 的整个生命周期,但集中在三个时机:
| 时机 | 重定位节 | 谁做 | 场景 |
|---|---|---|---|
| 链接时 | .rela.text(在 .o 里) | ld(静态链接器) | 把多个 .o 拼成可执行文件/.so |
| 加载时 | .rela.dyn(在可执行文件/.so 里) | ld.so(动态链接器) | 加载 .so 时修正数据和部分函数地址 |
| 延迟绑定时 | .rela.plt(在可执行文件/.so 里) | ld.so via _dl_runtime_resolve | 第一次调用外部函数时填 GOT |

三、Elf64_Rela 结构体
x86-64 使用 RELA 格式(A = Addend = 显式加数),每个条目 24 字节:
bash
Elf64_Rela(24 字节)
┌────────────────┬──────────────────────┬─────────────────────┐
│ r_offset │ r_info │ r_addend │
│ 8 bytes │ 8 bytes │ 8 bytes │
└────────────────┴──────────────────────┴─────────────────────┘| 字段 | 大小 | 含义 |
|---|---|---|
r_offset | 8 字节 | 要被修正的位置:对 .o 是段内偏移;对可执行文件/.so 是虚拟地址 |
r_info | 8 字节 | 低 4 字节 = 重定位类型(如 R_X86_64_PC32);高 4 字节 = 符号表索引(指向 .symtab 或 .dynsym) |
r_addend | 8 字节 | 显式加数——计算目标地址时要额外加上的常量 |
RELAvsREL:x86-64 用 RELA(加数在结构体的r_addend字段);i386 用 REL(加数藏在r_offset指向的位置,结构体只有 8 字节)。readelf -r会自动把加数算出来,不区分格式。
四、.rela.text —— 链接时重定位(只在 .o 里)
当你编译单个 .o 时,引用了外部符号(其他 .o 里的函数、全局变量),编译器不知道它们的最终地址,于是留个"洞"并记一条重定位条目。
bash
# 看 .o 里的重定位表
$ readelf -r main.o
Relocation section '.rela.text' at offset 0x3a8 contains 5 entries:
Offset Info Type Sym.Value Sym.Name + Addend
000000000025 000a00000002 R_X86_64_PC32 000000000000 do_work - 4
000000000045 000500000002 R_X86_64_PC32 000000000000 printf - 4
000000000058 000900000002 R_X86_64_PC32 000000000000 g_tick - 4链接器 ld 合并所有 .o 后,已知所有符号的最终地址,就逐条"修补"这些位置,把重定位节从最终的可执行文件中删掉。所以 readelf -r ./a.out 看不到 .rela.text——链接时重定位只在中间产物 .o 里存在。
常用链接时重定位类型
| 类型 | 含义 | 修正内容 |
|---|---|---|
R_X86_64_PC32 | 32 位 PC 相对 | *(int32*)(P) = S + A - P(常用于 call 指令) |
R_X86_64_32 | 32 位绝对 | *(int32*)(P) = S + A(x32/内核;x86-64 常规不用) |
R_X86_64_64 | 64 位绝对 | *(int64*)(P) = S + A |
R_X86_64_32S | 32 位符号扩展 | *(int32*)(P) = S + A(符号扩展,用于 LEA) |
公式中的变量:
P= 被修正位置(r_offset),S= 符号最终地址,A= 加数(r_addend)。
五、.rela.dyn —— 加载时重定位
动态链接的可执行文件和 .so 里,有些符号的地址直到运行时才知道(如全局变量在共享库里)。这些不能由 ld 在链接时修好,必须留给 ld.so 在加载时逐条修正。
bash
$ readelf -r ./cpu_demo | grep -A20 '\.rela\.dyn'
Relocation section '.rela.dyn' at offset 0x8d8 contains 12 entries:
Offset Info Type Sym.Value Sym.Name + Addend
000000607dc8 000000000008 R_X86_64_RELATIVE 000000004081c8
000000607ff8 000100000006 R_X86_64_GLOB_DAT 000000000000 __gmon_start__ + 0
000000608190 000500000006 R_X86_64_GLOB_DAT 000000000000 stdout@GLIBC_2.2.5 + 0R_X86_64_RELATIVE:不需要符号表的重定位
这是加载时重定位里最特殊也最常见的一种。r_addend 就是修正后的值,不需要查符号表。计算就是 *P = B + A(B = 加载基址,A = r_addend)。
为什么需要它?当你写 const char* msg = "hello",msg 在 .data 里存的不是 "hello" 字符串的绝对地址(因为没链接前不知道),而是一个偏移。链接时 ld 记下这个偏移写到 r_addend,ld.so 加载时加上 ASLR 随机化后的基址就得到真实地址。这种重定位只和一个具体位置 + 基址有关,不涉及跨库符号,所以可以完全在本地完成。
R_X86_64_GLOB_DAT:全局变量重定位
r_info 指向某个符号(如 stdout@GLIBC),ld.so 找到该符号的运行时地址后,写入 r_offset 指向的位置。这是 GOT 里非 PLT 条目的填充方式。
六、.rela.plt —— 延迟绑定重定位
.rela.plt 里的每一条对应一个 PLT stub(见 plt-got.md)。条目数量 = readelf -S 中 .rela.plt 的 Size ÷ 24。
bash
$ readelf -r ./cpu_demo | grep -A10 '\.rela\.plt'
Relocation section '.rela.plt' at offset 0xa50 contains 16 entries:
Offset Info Type Sym.Value Sym.Name + Addend
000000608018 000200000007 R_X86_64_JUMP_SLOT 000000000000 printf@GLIBC_2.2.5 + 0
000000608020 000400000007 R_X86_64_JUMP_SLOT 000000000000 __stack_chk_fail + 0| 字段 | 含义 |
|---|---|
Offset | GOT 条目的虚拟地址(拉延迟绑定后填这) |
Type | 固定为 R_X86_64_JUMP_SLOT(7) |
Sym.Name | 要解析的符号名 |
延迟绑定流程(用 .rela.plt)
ld.so加载程序时,把.rela.plt中每个r_offset指向的 GOT 位置写入回跳地址(跳回 PLT stub 的第②条指令)- 首次调用
printf@plt时,GOT 未解析 → 跳回 PLT → push 重定位索引 → 调_dl_runtime_resolve _dl_runtime_resolve用索引去.rela.plt读到符号名 → 在.dynsym里搜 → 找到地址 → 写回 GOT[r_offset]- 后续调用
printf@plt时,GOT 存着真实地址 → 第一次jmp直接命中
七、重定位类型的计算语义速查
| 类型 | 出现位置 | 计算 | 用途 |
|---|---|---|---|
R_X86_64_PC32 | .rela.text(.o) | *(P) = S + A - P | call / jmp 的相对偏移 |
R_X86_64_64 | .rela.text(.o) | *(P) = S + A | 64 位绝对地址(如 vtable 指针) |
R_X86_64_RELATIVE | .rela.dyn | *(P) = B + A | PIE/.so 内部数据地址(不查符号表) |
R_X86_64_GLOB_DAT | .rela.dyn | *(P) = S | 全局变量地址 |
R_X86_64_JUMP_SLOT | .rela.plt | *(P) = S | PLT 延迟绑定的 GOT 填充 |
R_X86_64_COPY | .rela.dyn | 复制到 .bss | 跨 .so 的全局变量拷贝 |
八、实操速查
bash
# 看 .o 的重定位(链接时)
readelf -r main.o
# 看可执行文件/库的重定位(加载时 + PLT)
readelf -r ./cpu_demo
readelf -r ./cpu_demo | grep -E 'JUMP_SLOT|GLOB_DAT|RELATIVE'
# 只看 PLT 跳转槽(外部函数调用)
objdump -R ./cpu_demo
# 统计重定位类型分布
readelf -r ./cpu_demo | grep -E 'R_X86_64' | awk '{print $3}' | sort | uniq -c九、与相关文档的关系
| 文档 | 覆盖内容 | 本文的关系 |
|---|---|---|
| elf-format.md §四 | 常见 Section 简要列表 | 本文是重定位节的专题深入 |
| plt-got.md | PLT/GOT 结构与延迟绑定流程 | 本文从重定位表角度解释 PLT 怎么填 GOT |
| symbol-table.md | .dynsym / .symtab | 重定位的 r_info 高 32 位指向符号表索引 |
| compile-link-load.md | 链接与加载全流程 | 本文是整个流程中"修正地址"环节的数据结构 |
一句话总结:
Elf64_Rela是一个 24 字节的重定位条目:r_offset(修哪)、r_info(怎么修 + 符号是谁)、r_addend(加多少)。链接时的.rela.text(在.o里)被ld消费后消失;运行时的.rela.dyn由ld.so加载时逐条修正全局变量地址;.rela.plt则在首次调用时触发_dl_runtime_resolve延迟填充 GOT。