位置无关代码与动态链接(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 获取偏移,见上表。