動態庫在載入過程中必須將編譯時期的相對位址修正為進程虛擬位址空間中的絕對位址,這一過程稱為重定位。Linux 採用兩種主要策略:載入時重定位與位置無關程式碼 (PIC)。本文以 libincrease.so 為例,結合 readelf、objdump、gdb 實測,詳細追蹤 .rel.dyn 中 R_386_32 修正變數位址、R_386_PC32 計算函數相對偏移的完整過程。
/* increase.c */
#include
#include
#include
int count = 7;
void increase() {
count = count + 1;
}
void doincrease(int n) {
count += n;
increase();
}
gcc -m32 -g -c increase.c -o increase.o
gcc -m32 -shared -o libincrease.so increase.o
# -m32 強制 32-bit,便於人工驗證位址運算
readelf -l libincrease.so
# 兩個 LOAD segment:
# 0: R E (代碼段) VirtAddr 0x00000000
# 1: RW (數據段) VirtAddr 0x00001f0c ← .data/.bss 在此
readelf -r libincrease.so
Relocation section '.rel.dyn':
Offset Info Type Sym.Value Sym.Name
00002008 00000008 R_386_RELATIVE
00000440 00000701 R_386_32 0000200c count ← 變數重定位
00000448 00000701 R_386_32 0000200c count
00000482 00000701 R_386_32 0000200c count
0000048a 00000701 R_386_32 0000200c count
0000048f 00000602 R_386_PC32 0000046c increase ← 函數重定位
...
objdump -d -M intel libincrease.so
increase:
43f: a1 00 00 00 00 mov eax, ds:0x0 ← 佔位 0x00000440
447: a3 00 00 00 00 mov ds:0x0, eax ← 佔位 0x00000448
doincrease:
481: a1 00 00 00 00 mov eax, ds:0x0 ← 0x00000482
489: a3 00 00 00 00 mov ds:0x0, eax ← 0x0000048a
48e: e8 fc ff ff ff call 0x48f ← 相對偏移 0xfffffffc (0x0000048f)
編寫測試程式使用 dl_iterate_phdr 取得載入基址:
/* test.c */
#define _GNU_SOURCE
#include
#include
extern void doincrease(int);
int main() {
dl_iterate_phdr([](struct dl_phdr_info* i, size_t, void*) {
printf("%s @ %p\n", i->dlpi_name, (void*)i->dlpi_addr);
return 0;
}, NULL);
doincrease(6);
return 0;
}
gcc -m32 test.c -L. -lincrease -o test
LD_LIBRARY_PATH=. ./test
輸出範例:
libincrease.so @ 0xf7fd6000
count 重定位驗證Sym.Value = 0x200c0xf7fd6000 + 0x200c = 0xf7fd800cOffset=0x440,類型 R_386_32 → 將絕對位址直接寫入 Base+0x440。
(gdb) p &count
$1 = (int *) 0xf7fd800c
(gdb) x/4xb 0xf7fd6440
0xf7fd6440: 0x0c 0x80 0xfd 0xf7 ← 小端序 0xf7fd800c 完全吻合
increase 重定位驗證Sym.Value = 0x46c0xf7fd6000 + 0x46c = 0xf7fd646cOffset=0x48f,類型 R_386_PC32:公式:Reloc_Result = Sym_Addr + Addend - Reloc_Addr
Addend 為原指令中相對偏移 0xfffffffc(-4)。
Reloc_Addr = Base + Offset = 0xf7fd648f.
Result = 0xf7fd646c + 0xfffffffc - 0xf7fd648f = 0xffffffd9.
(gdb) p increase
$2 = {void ()} 0xf7fd646c
(gdb) disas/r doincrease
...
0xf7fd648e: e8 d9 ff ff ff call 0xf7fd646c
# 0xf7fd648f 位置內容為 0xffffffd9,call 指令以此為相對偏移
# 0xf7fd6493 (下一條指令) + 0xffffffd9 = 0xf7fd646c 精確命中
| 重定位類型 | 適用對象 | 修正方式 | 指令影響 |
|---|---|---|---|
| R_386_32 | 全域/靜態變數 | 直接寫入絕對虛擬位址 | mov eax, ds:addr 的 addr 欄位 |
| R_386_PC32 | 函數呼叫 | 計算相對偏移 (Sym - Reloc_Addr + Addend) | call rel32 的 rel32 欄位 |
| R_386_RELATIVE | .data/.bss 基址 | Base + Addend | 無直接指令,供動態連結器內部使用 |
若編譯加 -fPIC,變數存取改為 GOT 間接寻址,函數呼叫改為 PLT 延遲綁定,重定位類型變為 R_386_GLOB_DAT、R_386_JUMP_SLOT 等,載入時不修改代碼段,實現真正的代碼共享。本文示範的 -shared 無 -fPIC 屬於載入時重定位模式,代碼段需寫入修正,無法跨進程共享代碼頁。