位置无关代码与动态链接(PIC / PIE / GOT / PLT)
现代 Linux 可执行文件默认是 PIE(Position Independent Executable),共享库更是必须 PIC。其关键在于:代码中不出现绝对地址常量,所有地址通过相对寻址或查表获得。
为什么需要 PIC
- 共享库被加载到随机地址(ASLR),编译时无法预知绝对地址。
- 同一份库代码可被多个进程映射到不同地址,靠写时复制共享物理页。
RIP 相对寻址
x86-64 原生支持 RIP 相对数据寻址,是 PIC 的基础:
leaq msg(%rip), %rdi # rdi = rip + 距 msg 的相对偏移
movl var(%rip), %eax # 读取全局变量(相对rip)
相比 32 位 PIC(需用 __i686.get_pc_thunk 取 EIP 再算偏移),64 位简单很多。
GOT(全局偏移表,Global Offset Table)
访问外部/全局变量时,编译器生成通过 GOT 间接取地址的代码:
# 访问全局变量 gvar(PIC)
movq gvar@GOTPCREL(%rip), %rax # rax = GOT 中 gvar 条目的地址
movl (%rax), %eax # 经 GOT 条目间接读取 gvar 的值
@GOTPCREL:GOT 条目相对 RIP 的偏移。- 运行时动态链接器把变量的真实地址填入 GOT 条目。
- 若符号最终定义在同一模块,链接器可把这段访问松弛为直接的 RIP 相对访问(省去一次间接)。
PLT(过程链接表,Procedure Linker Table)
调用外部函数时,通过 PLT + GOT 实现"懒绑定"(lazy binding):
call printf@PLT # 调用 PLT 桩
printf@PLT 桩默认跳到 GOT 中对应条目;首次调用时该条目指向解析器,解析后改为函数真实地址,后续调用直达。也可强制立即绑定(BIND_NOW)。模块内(或声明为 .hidden 的)函数不会被外部覆盖,链接器可生成直接 call,不走 PLT。
符号引用修饰符
| 修饰符 | 含义 |
|---|---|
sym@PLT |
通过 PLT 调用函数 |
sym@GOTPCREL |
RIP 相对取得 GOT 条目地址(64 位) |
sym@GOT |
GOT 条目(绝对,32 位 PIC 用) |
sym@GOTOFF |
符号相对 GOT 基址的偏移 |
sym@TPOFF |
相对 TLS 基址(fs 段基址)的偏移 |
sym@GOTTPOFF |
经 GOT 取 TLS 偏移(initial-exec 模型) |
sym@INDNTPOFF |
间接 TLS 偏移(general-dynamic 模型) |
完整的 PIC 示例(调用 libc printf)
.section .rodata
fmt: .asciz "result = %d\n"
.section .text
.globl main
.type main, @function
main:
subq $8, %rsp
movl $42, %esi # 第 2 参数(printf 的 %d 值)
leaq fmt(%rip), %rdi # 第 1 参数:格式串地址
xorl %eax, %eax # al = 0:未用向量寄存器(变参函数要求)
call printf@PLT
xorl %eax, %eax # main 返回 0
addq $8, %rsp
ret
.size main, . - main
编译链接(链接 libc):
gcc -fPIE -pie hello.s -o hello
# 或带调试信息
gcc -g -fPIE -pie -o hello hello.s
要点:
- 现代发行版 gcc 默认就编译 PIE;
-no-pie关闭。 - 共享库用
gcc -shared -fPIC编译;-fPIC生成完全位置无关代码(GOT 访问受限,适合库),-fPIE用于可执行文件。
与静态/非 PIE 的区别
非 PIE 可执行文件可直接用绝对地址(32 位立即数或 movabs),但不能被 ASLR 随机化,安全性较低,且不能用作共享库。
movabsq $msg, %rdi # 64 位绝对地址(非 PIC,仅非 PIE 用)
线程局部存储(TLS)
%fs(x86-64 用户态)的基址指向线程控制块,TLS 变量通过 fs: 段前缀 + 偏移访问:
movq %fs:0, %rax # 取 TLS 基址
movl mytls@TPOFF(%rax), %eax # TPOFF 是变量相对 TLS 基址的偏移
局部执行(local-exec)模型如上:先从 %fs:0 取 TLS 基址,再加 @TPOFF 偏移访问;更复杂的模型(initial-exec / general-dynamic)经 GOT 获取偏移,见上表。