首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

ELF動態鏈接實戰解析:GOT、PLT 與延遲綁定機制

     編輯:關於C語言

參考文獻

  • 《ELF V1.2》
  • 《程序員的自我修養——鏈接、裝載與庫》第 6、7 章
  • 《Linux GOT 與 PLT》

實驗環境

Linux tanghuimin 2.6.32-358.el6.x86_64 #1 SMP Fri Feb 22 00:31:26 UTC 2013 x86_64 GNU/Linux

理論基礎

1. ELF 文件裝載流程

用戶態 bash → fork() 創建子進程 → execve() → 內核 sys_execve() → do_execve() → load_elf_binary():

  1. 校驗 ELF 格式有效性(魔數、程序頭表)
  2. 查找 .interp 段,設置動態鏈接器路徑
  3. 根據程序頭表映射段(代碼、數據、只讀數據)
  4. 初始化進程環境(命令行參數、環境變量、輔助向量 Auxiliary Vector)
  5. 修改系統調用返回地址為程序入口:
    靜態鏈接 → e_entry;
    動態鏈接 → 動態鏈接器入口
  6. 返回用戶態,EIP 跳轉至入口,程序開始執行

2. 動態鏈接三階段

  1. 自舉:動態鏈接器自身為靜態鏈接,啟動代碼完成自身重定位,無需外部依賴
  2. 裝載共享對象:合併符號表,按 .dynamic 中 DT_NEEDED 裝載依賴庫,構建全局符號表
  3. 重定位與初始化:遍歷重定位表修正 GOT/PLT,執行 .init 段,最後移交控制權給程序入口

3. GOT 與 PLT 詳解

GOT (Global Offset Table)

  • 實現數據引用的地址無關性:代碼段通過 GOT 間接訪問全局/靜態變量
  • 共享庫裝載前 GOT 為 0,裝載時由動態鏈接器填入絕對地址
  • 共享庫全局變量在可執行文件 BSS 段創建副本,實現進程隔離

PLT (Procedure Linkage Table) 與延遲綁定

  • 動態鏈接比靜態鏈接慢:間接跳轉 + 運行時重定位開銷
  • Lazy Binding:函數首次調用時才重定位,未用函數不處理
  • .plt 中每個函數對應一個存根,首次調用跳轉至 _dl_runtime_resolve()
  • .got.plt 專存函數地址,前三項特殊:
    1. .dynamic 段地址
    2. 模塊 ID
    3. _dl_runtime_resolve() 地址

實戰演示

源文件

// common.c - 共享庫
int val = 1;
int func(void) { return (val + 10); }

// test.c - 主程序
extern int val;
extern int func(void);
int main() { val = 10; func(); return 0; }

編譯

gcc -shared -fPIC -o common.so common.c
gcc -o test test.c ./common.so

反匯編 main 函數分析

00000000004005f4 <main>:
  4005f4: 55                    push   %rbp
  4005f5: 48 89 e5              mov    %rsp,%rbp
  4005f8: c7 05 ae 03 20 00 0a  movl   $0xa,0x2003ae(%rip)  # 6009b0 <val>
  4005ff: 00 00 00
  400602: e8 f9 fe ff ff        callq  400500 <func@plt>
  400607: b8 00 00 00 00        mov    $0x0,%eax
  40060c: c9                    leaveq
  40060d: c3                    retq
  • val = 10:RIP 相對寻址訪問 0x6009b0(BSS 段副本)
  • func():調用 func@plt (0x400500)

.plt 段存根分析

0000000000400500 <func@plt>:
  400500: ff 25 9a 04 20 00     jmpq   *0x20049a(%rip)  # 6009a0 <func@got.plt>
  400506: 68 01 00 00 00        pushq  $0x1             # 重定位索引
  40050b: e9 d0 ff ff ff        jmpq   4004e0           # 跳轉到 PLT[0] 通用入口

00000000004004e0 <.plt[0]>:
  4004e0: ff 35 a2 04 20 00     pushq  0x2004a2(%rip)  # module id (GOT+8)
  4004e6: ff 25 a4 04 20 00     jmpq   *0x2004a4(%rip)  # _dl_runtime_resolve (GOT+16)
  4004ec: 0f 1f 40 00           nopl   0x0(%rax)

首次調用流程:
func@plt → got.plt[func] 初始指向下一條指令 (0x400506) → 壓入重定位索引 → 跳轉 .plt[0] → 壓入 module id → 跳轉 _dl_runtime_resolve() → 完成重定位,將真實地址寫入 got.plt[func]。

二次調用:直接從 got.plt[func] 獲取真實地址跳轉。

GDB 動態觀察

# 斷點 main,運行前
val (0x6009b0)        = 0x1          # 共享庫初始值
module_id (0x600988)  = 0x41021188   # 已初始化
_dl_runtime_resolve   = 0x40e146f0   # 已初始化
func@got.plt (0x6009a0) = 0x400506   # 仍指向存根下一條指令

# 執行 val=10 後
val = 0xa

# 執行 func() 後
func@got.plt = 0xf7dfc55c  # 已重定向至真實函數地址

總結

通過實例完整展示了:ELF 裝載 → 動態鏈接器自舉 → 共享庫裝載 → GOT/PLT 機制 → 延遲綁定 → 運行時重定位的全過程。理解這一機制對調試動態鏈接問題、分析性能開銷、逆向工程均有重要價值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved