在 Linux ELF 文件和动态链接机制中,这四个段(Section)共同协作,实现了位置无关代码(PIC)延迟绑定(Lazy Binding)

虽然它们名字很像,但功能和权限有着本质区别。我们可以将其分为两类:PLT 类(代码/执行)GOT 类(数据/读写)


1. .plt (Procedure Linkage Table - 过程链接表)

  • 属性代码段(权限:读取+执行 R-X)。
  • 作用:它包含了一系列小的可执行代码片段(Stub)。
  • 功能:当程序调用一个外部函数(如 printf)时,它实际上并不是直接跳到 printf 的地址(因为在编译阶段不知道地址),而是跳到 .plt 段中对应的条目。
  • 内容
    • 第一项是特殊项,负责调用动态链接器的符号解析函数。
    • 后续每一项对应一个外部函数,代码逻辑通常是:jmp *(.got.plt中的对应项)

2. .got (Global Offset Table - 全局偏移表)

  • 属性数据段(权限:读取+写入 RW-)。
  • 作用:用于存储全局变量的绝对地址。
  • 功能:程序在引用全局变量时,会先到 .got 中查找该变量的真实地址。
  • 为什么需要:为了实现位置无关代码(PIC),代码段不包含变量的绝对地址,只包含到 .got 的相对偏移。动态链接器在程序启动时会将变量的真实地址填入 .got

3. .got.plt (GOT 的 PLT 部分)

  • 属性数据段(权限:读取+写入 RW-,但在 Full RELRO 下为 R--)。
  • 作用:它是 .got 的一个子集,专门用于存储外部函数的绝对地址。
  • .plt 的配合
    • 延迟绑定(Lazy Binding)模式下:.got.plt 的初始内容指向 .plt 中的下一条指令(即“跳回 PLT”)。当函数第一次被调用时,动态链接器解析出真实地址并覆盖掉这个值。
    • 第二次调用时:.plt 里的 jmp 就会直接跳到 .got.plt 中存储的真实地址,不再进入链接器。
  • 特殊项:前三项通常预留给动态链接器的私有信息(如 link_map 结构和 _dl_runtime_resolve 函数地址)。

4. .plt.got (专门的 PLT 跳转表)

  • 属性代码段(权限:读取+执行 R-X)。
  • 作用:这是一个特殊的 .plt 段,通常用于非延迟绑定的情况,或者用于处理某些特定的重定位类型(如通过 R_X86_64_GLOB_DAT 重定位的函数指针)。
  • 区别
    • 标准的 .plt 条目通常包含三个动作:跳转到 GOT、压栈索引、跳到解析器。
    • .plt.got 条目通常直接跳转.got(而不是 .got.plt)中存储的地址,不包含延迟绑定的逻辑(没有压栈和解析器的跳转)。它通常出现在启用了 -z now(Full RELRO)或编译器优化后的二进制文件中。

总结与对比

段名称 类型 权限 存储内容 核心目的
.plt 代码 R-X 跳转代码片段 (Stubs) 函数调用的中转站,触发延迟绑定
.plt.got 代码 R-X 直接跳转代码 跳过延迟绑定逻辑,直接跳转到 GOT 地址
.got 数据 RW- 全局变量的绝对地址 变量引用的位置无关化
.got.plt 数据 RW- 外部函数的绝对地址 配合 .plt 实现函数的延迟绑定

协作流程演示(以延迟绑定为例):

  1. Call printf@plt:程序跳转到 .pltprintf 对应的条目。
  2. Jmp to .got.plt.plt 里的第一条指令跳转到 .got.plt 记录的地址。
    • 第一次调用.got.plt 填的是 .plt 的下一行。
  3. Resolve.plt 剩下的代码调用动态链接器,找到 printf 的真实地址。
  4. Update:动态链接器将 printf 的真实地址写回 .got.plt
  5. Subsequent Calls:下次再调 printf@plt 时,第 2 步的 Jmp 会直接跳到 printf 的真实地址。

为什么现在的安全保护(RELRO)会影响这些段?

  • Partial RELRO.got.plt 是可写的。攻击者可以利用堆栈溢出覆盖 .got.plt 的条目,将 printf 改为 system,从而实现 GOT Hijacking
  • Full RELRO:动态链接器在程序启动时就把所有函数解析完毕,并将 .got.plt 设为只读。此时,.plt.got 的作用就会变得更明显,因为不再需要延迟绑定逻辑了。