首页
学习
活动
专区
圈层
工具
发布

PLT 与 GOT 表详解

引言

在 Linux 系统中,可执行程序对动态库接口的调用机制设计十分巧妙,其核心依赖于过程链接表(PLT)和全局偏移表(GOT)。本文将从 ELF 文件层面,深入解析这两个表的工作原理及动态库调用流程。

动态库调用流程解析

非首次调用(常规流程)

当程序并非首次调用动态库接口时,调用流程如下:

1. 代码中对`printf`的调用会被编译为`printf@plt`形式,即先跳转到 PLT 表中对应的条目

2. PLT 表是一个由汇编代码组成的数组,除索引 0 外,每个条目对应一个外部符号的调用(假设`printf`对应 PLT 表索引为 n)

3. PLT [n] 的第一条指令是跳转到 GOT 表中对应的条目(假设对应 GOT 表索引为 m)

4. 此时 GOT [m] 已存储实际函数地址,直接跳转到动态库中的`printf`函数执行

5. 函数执行完成后返回原调用处,完成一次调用

首次调用(延迟绑定机制)

Linux 采用延迟绑定(Lazy Binding)机制,首次调用时 GOT 表中尚未填充实际函数地址,流程如下:

PLT 条目的结构

以`printf`对应的 PLT [n] 为例,其汇编结构如下(伪代码):

printf@plt:

jmp *GOT[m] ; 跳转到GOT表第m项(首次调用时指向本条目的下一条指令)

push n ; 将PLT索引n压栈作为参数

jmp PLT[0] ; 跳转到PLT[0]的桩代码

PLT [0] 桩代码结构

PLT 表索引 0 的特殊桩代码结构如下(伪代码):

PLT0:

pushq [GOT+8] ; 将GOT[1]中的link_map结构地址压栈

jmp [GOT+16] ; 跳转到GOT[2]指向的_dl_runtime_resolve函数

nop ; 内存对齐填充

nop

地址解析过程

1. 首次调用时,GOT [m] 指向 PLT [n] 的第二条指令(`push n`)

2. 执行`push n`将 PLT 索引压栈,再跳转到 PLT [0]

3. PLT [0] 将 GOT [1](link_map 结构地址)压栈,然后跳转到 GOT [2] 指向的`_dl_runtime_resolve`函数

4. _dl_runtime_resolve

函数通过两个参数(link_map 指针和 PLT 索引 n)执行符号解析:

- 根据 PLT 索引 n 找到.rela.plt 重定位节中的对应条目(Elf64_Rela 结构)

- 通过条目中的 r_info 字段定位.dynsym 动态符号表中的符号信息(Elf64_sym 结构)

- 利用符号信息中的 st_name 字段从.dynstr 动态字符串表获取符号名(如 "printf")

- 遍历 link_map 记录的已加载库,查找符号对应的实际地址(如 libc.so 中的 printf 地址)

5. 将解析得到的实际地址写入 GOT [m],完成地址绑定

6. 执行实际函数并返回,后续调用将直接使用 GOT [m] 中的地址(即常规流程)

安全风险与防护

ELF 文件的 PLT/GOT 机制存在潜在安全风险:攻击者可通过修改 GOT 表中的地址,将函数调用重定向到恶意代码,执行完成后再跳转回原流程,从而篡改程序行为。

针对此类风险,常用的防护手段是 "加壳":通过加壳工具对程序进行保护,增加逆向分析和篡改的难度。例如 Virbox Protector 等工具支持多种文件类型,能有效提升程序的安全性。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Oy1AplQJHYGax9mS75Yzp-IQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券