ELF 動態鏈接透過 PLT (Procedure Linkage Table)、GOT (Global Offset Table) 與重定位表協作,實現「首次調用解析、後續直接跳轉」的延遲綁定。本文深度剖析五大段表交互流程,以 printf 為例演示完整調用鏈路。
| 段表 | 全稱 | 核心功能 |
|---|---|---|
.plt | Procedure Linkage Table | 跳板代碼:每個外部函數一個條目,首次調用觸發解析,後續直接跳轉 GOT |
.got | Global Offset Table | 全域變數地址表:存放本模組全域/靜態變數在進程虛擬地址空間的實際地址 |
.got.plt | GOT for PLT | 函數地址表:每個外部函數一個條目,初始指向 PLT 解析桩碼,解析後更新為真實函數地址 |
.rel.plt / .rela.plt | Relocation Table for PLT | PLT 相關重定位條目:記錄符號名、類型、GOT.plt 條目偏移,供動態鏈接器解析 |
.rel.dyn / .rela.dyn | Relocation Table for Data | 數據相關重定位:全域變數、函數指針等非 PLT 重定位 |
call printf@plt → 跳轉至 .plt 中 printf 條目printf@plt:
jmp *printf@GOTPCREL(%rip) # 間接跳轉 .got.plt[printf]
pushq $reloc_index # 壓入重定位索引
jmp .plt[0] # 跳轉通用解析入口
.got.plt[printf] 指向 PLT 條目第 2、3 行(即 pushq/jmp),形成「自迴路」_dl_runtime_resolve):
.rel.plt 條目.dynsym 獲取符號名 "printf"printf 定義(如 libc.so.6).got.plt[printf] = 真實地址jmp *printf@GOTPCREL(%rip) 現指向真實 printf,直接執行call printf@plt jmp *printf@GOTPCREL(%rip) # 直接跳轉真實 printf,零開銷
.plt[printf] ──jmp──→ .got.plt[printf] ──(初始)──→ .plt[printf]+6 (pushq/jmp)
│
└─(解析後)──→ libc!printf (真實地址)
.rel.plt[printf] : { r_offset=.got.plt[printf], r_info=SYM(printf)|R_JUMP_SLOT }
.dynsym[printf] : { st_name="printf", st_value=0, st_shndx=SHN_UNDEF }
.got:數據引用重定位。全域變數、靜態變數、函數指針初始化。對應 .rel.dyn 重定位類型 R_GLOB_DAT / R_COPY。.got.plt:函數調用重定位。外部函數地址緩存。對應 .rel.plt 重定位類型 R_JUMP_SLOT。LD_BIND_NOW 未設。首次調用時解析,啟動快、未用函數不解析。LD_BIND_NOW=1 或連結時 -z now。程式載入階段解析所有 PLT 條目,啟動慢、運行穩定、利於安全強化(RELRO)。.got 只讀,.got.plt 可寫(支援延遲綁定).got + .got.plt 全部只讀(需立即綁定),防止 GOT 劫持攻擊-Wl,-z,relro,-z,now# 查看 PLT/GOT readelf -r a.out # 重定位表 readelf -s a.out | grep printf # 動態符號表 objdump -d -j .plt a.out # 反組譯 PLT gdb -ex "info file" -ex "maintenance info sections" ./a.out
ELF 動態鏈接機制以「分層間接」實現模組解耦與延遲解析,.plt/.got.plt/.rel.plt 三位一體完成函數綁定,.got/.rel.dyn 處理數據重定位,配合 RELRO 強化運行時安全。透徹理解此機制,是逆向分析、效能調優、安全加固的核心前提。