連結練習題 (Practice - Linking)
Related Concepts
- 07-Linking/01-Static-Linking-and-Object-Files
- 07-Linking/02-Symbol-Resolution-and-Relocation
- 07-Linking/03-Executable-Loading-and-Dynamic-Linking
- 07-Linking/04-Library-Interpositioning-and-Tools
| 關鍵字 | 答案 |
|---|---|
| 連結器兩大任務 | symbol resolution(每個 reference ↔ 恰好一個 definition)+ relocation(指派執行期位址並修補參考) |
| gcc 四階段 | cpp(.c→.i)→ cc1(.i→.s)→ as(.s→.o)→ ld(→executable);gcc -v 觀察 |
| 目的檔三形式 | relocatable / executable / shared;linker 只產生 executable |
| 已初始化 global | .data(strong symbol) |
| 未初始化 global | COMMON(weak symbol) |
| 未初始化 static、初始化為 0 | .bss(不占檔案空間) |
| 多個 strong 同名 | Rule 1 → link error |
| 一 strong 多 weak | Rule 2 → 選 strong(無提示) |
| 多個 weak | Rule 3 → 任選(unknown) |
| 靜態庫掃描 | 由左至右,維護 E/U/D 三集合;函式庫放命令列尾端 |
| PC-relative relocation | *refptr = ADDR(symbol) + addend − (ADDR(s) + offset) |
| 絕對 relocation | *refptr = ADDR(symbol) + addend |
.bss 大小(program header) |
memsz − filesz |
| GOT / PLT | GOT 在 data segment(8-byte 位址);PLT 在 code segment(16-byte 程式碼);lazy binding |
| 插入三時機存取需求 | compile-time 需原始碼 → link-time 需 .o → run-time 只需可執行檔(LD_PRELOAD) |
--wrap f |
f → __wrap_f;__real_f → f |
| 反組譯 / 涵蓋 size+nm / 動態相依 | objdump / readelf / ldd |
Question 1 - 連結器兩大任務 [recall]
靜態連結器
ld接收一組 relocatable object files,輸出 fully linked executable。
請說出連結器必須完成的兩大任務,並各以一句話說明其內容。
- Symbol resolution:把每個 symbol reference 對應到「恰好一個」symbol definition。
- Relocation:compiler/assembler 產生的 sections 都從位址 0 開始;連結器合併同型別 section、為每個符號定義指派執行期位址,再依 assembler 產生的 relocation entries 機械式修補所有參考。
Question 2 - 目的檔三種形式 [recall]
目的檔有三種形式。請列出名稱、各自的用途,以及「哪一種只能由 linker 產生」。
- Relocatable object file:可在編譯期與其他
.o合併;由 compiler + assembler 產生。 - Executable object file:可直接複製進記憶體執行;只有 linker 能產生。
- Shared object file:特殊的 relocatable,可在 load time 或 run time 動態載入連結;由 linker 產生(如
gcc -shared -fpic)。
另注意格式隨系統:Linux 用 ELF、Windows 用 PE、Mac OS-X 用 Mach-O。
Question 3 - Section 歸屬 [recall]
在現代 gcc 的 ELF relocatable object file 慣例下,下列各項分別放在哪個 section(或 pseudosection)?
(a)int g = 42;(global) (b)int g;(global,未初始化) (c)static int s;(d)int z = 0;(global) (e)printf的格式字串與switch的 jump table
(a) .data (b) COMMON(未初始化 global) (c) .bss (d) .bss(初始化為 0 者也進 .bss) (e) .rodata。
記法:未初始化 global → COMMON(weak,決定權推遲給 linker);未初始化 static 與初始化為 0 者 → .bss(不占檔案空間,執行期才配置並清 0)。
Question 4 - Strong/Weak 三規則 [recall]
編譯時每個全域符號以 strong 或 weak 匯出。
請說明哪些符號是 strong、哪些是 weak,以及 Linux linker 處理重複符號名的三條規則。
Strong:函式、已初始化的全域變數;Weak:未初始化的全域變數。
Rule 1:多個同名 strong → 連結錯誤(multiple definition)。
Rule 2:一個 strong + 多個 weak → 選 strong(通常無任何提示)。
Rule 3:多個 weak → 任選一個(結果不可預測)。防禦:gcc -fno-common 直接報錯。
Question 5 - 靜態函式庫與掃描順序 [recall]
Linker 由左至右掃描命令列上的 object files 與 archives,過程維護三個集合 E、U、D。
請說明三集合的意義,以及遇到 archive 時 linker 做什麼、掃描結束時如何判定成敗。
E = 將合併成執行檔的 object files;U = 已參照、尚未定義的符號;D = 已定義的符號。
遇到 archive 時,用 U 中的未解析符號比對各 member:能解析者加入 E 並更新 U/D,反覆迭代至不動點;未被選中的 member 直接丟棄。
掃描結束時 U 非空 → 報錯終止;U 為空 → 合併重定位 E 產出執行檔。因此函式庫應放命令列尾端。
Question 6 - Executable 與載入 [recall]
相較於 relocatable object file,executable object file 多了什麼、少了什麼?
又,執行./prog時,程式的 entry point 是哪個函式、定義在哪、之後的呼叫鏈為何?
多:ELF header 中的 entry point、program (segment) header table、.init section;少:.rel sections(已完全重定位)。
Entry point 是 _start,定義於系統目的檔 crt1.o(所有 C 程式相同);_start → __libc_start_main(libc.so)→ 使用者的 main。
另注意 .symtab/.debug/.line/.strtab 不會載入記憶體。
Question 7 - GOT 與 PLT [recall]
PIC 程式碼透過 GOT 與 PLT 呼叫 shared library 中的函式。
請說明兩者各位於哪個 segment、entry 的形式,以及 lazy binding 下「第一次呼叫」與「之後呼叫」的差異。
GOT 在 data segment,是 8-byte 位址 entries 的陣列;PLT 在 code segment,是 16-byte 程式碼 entries 的陣列。
GOT entry 初值指向對應 PLT entry 的第二條指令:第一次呼叫會 push 函式 ID、跳入 dynamic linker,由它解析出 run-time 位址並覆寫 GOT entry,再轉給目標函式。
之後每次呼叫只多一條間接跳轉指令 + 一次記憶體引用,直達目標。
Question 8 - 插入三時機 [recall]
Library interpositioning 可在三個時間點進行,三者所需的「存取權」遞減。
請列出三種時機、各自的關鍵機制與所需材料。
- Compile-time:需原始碼;本地
malloc.h用#define malloc(size) mymalloc(size)巨集替換,靠gcc -I.讓 preprocessor 先搜目前目錄。 - Link-time:需 relocatable object files (.o);linker
--wrap f旗標,f→__wrap_f、__real_f→f。 - Run-time:只需可執行檔;
LD_PRELOAD使 dynamic linker 解析未定義參考時優先搜尋指定的 shared libraries。
Question 9 - 目的檔工具 [recall]
GNU binutils 提供多個操作 object file 的工具。下列需求各用哪個工具?
(a) 反組譯.text(b) 功能涵蓋size與nm(c) 列出執行檔需要的 shared libraries (d) 刪除 symbol table (e) 建立 static library
(a) objdump -d(「所有 binary 工具之母」) (b) readelf (c) ldd(非 binutils) (d) strip (e) ar(如 ar rcs libvector.a addvec.o multvec.o)。
Question 10 - 符號分類 [application]
swap.c內容如下(m.o中定義了int buf[2] = {1, 2};)。請對buf、bufp0、bufp1、swap、temp逐一判斷:是否在swap.o的.symtab有 entry?若有,是定義還是參考、屬於哪個 section?extern int buf[]; int *bufp0 = &buf[0]; int *bufp1; void swap() { int temp; /* ... */ }
buf:有 entry,external 參考(定義在 m.o 的.data),Ndx = UNDEF。bufp0:有 entry,global 定義,已初始化 →.data(且其初始值是buf的位址,需要.rel.datarelocation)。bufp1:有 entry,global 定義,未初始化 → COMMON。swap:有 entry,global 定義 →.text。temp:沒有 entry——local 非 static 變數由 stack 在執行期管理,連結器不關心。
Question 11 - PC-relative Relocation 計算 [application]
main.o的.text中有一條call sum,其 relocation entry 為:
r.offset = 0xf、r.type = R_X86_64_PC32、r.symbol = sum、r.addend = -4。
已知 linker 決定ADDR(.text) = 0x4004d0、ADDR(sum) = 0x4004e8。求重定位後填入指令的 32 位元值,並驗證執行期 call 會跳到sum。
refaddr = ADDR(s) + r.offset = 0x4004d0 + 0xf = 0x4004df。
*refptr = ADDR(sum) + addend − refaddr = 0x4004e8 + (−4) − 0x4004df = 0x5。
驗證:call 位於 0x4004de,執行時 PC = 下一條指令位址 0x4004e3;PC + 0x5 = 0x4004e8,正好是 sum 的第一條指令。(addend = −4 補償參照起點與 PC 相差的 4 bytes。)
Question 12 - 函式庫命令列順序 [application]
p.o呼叫libx.a中的函式,libx.a呼叫liby.a,而liby.a又回頭呼叫libx.a(循環相依)。
(a)gcc p.o libx.a liby.a為何可能連結失敗?
(b) 給出兩種可成功連結的作法。
(a) 掃描到 liby.a 時,它對 libx.a 的參考被加入 U;但 libx.a 已掃描完畢不會回頭,掃描結束 U 非空 → undefined reference 錯誤。
(b) 方法一:重複列出函式庫 gcc p.o libx.a liby.a libx.a;方法二:把 libx.a、liby.a 合併成單一 archive。
通則:對每個被參照的符號,命令列上至少要有一個定義跟在參照之後。
Question 13 - 型別不一致的重複定義 [analysis]
兩個模組連結成一個執行檔(x86-64,int 4 bytes、double 8 bytes):
/* foo5.c */ /* bar5.c */ int y = 15212; double x; int x = 15213; void f() { x = -0.0; } int main() { f(); ... }連結能否成功?執行
f()後x、y的值各是什麼?為什麼這類 bug 特別難除錯?
能連結成功:foo5.c 的 int x 已初始化為 strong,bar5.c 的 double x 未初始化為 weak,Rule 2 選 strong 的 4-byte int x(linker 頂多給 alignment 警告)。
但 f() 中的 x = -0.0 以 double(8 bytes)寫入,一次覆寫相鄰的 x 與 y 兩個位置:x = 0x0、y = 0x80000000(−0.0 的 IEEE 754 位元樣式)。
難除錯原因:linker 靜默或僅警告,且症狀(y 被莫名改掉)常在離錯誤點很遠的執行後期才顯現;防禦用 gcc -fno-common。
Question 14 - Run-time 插入與遞迴陷阱 [analysis]
你只有可執行檔
intr(無原始碼、無.o),想追蹤它的malloc呼叫。你寫了 wrapper:void *malloc(size_t size) { void *ptr = malloc(size); /* 直接呼叫 malloc */ printf("malloc(%d) = %p\n", (int)size, ptr); return ptr; }(a) 應選哪種插入機制、如何建置與執行? (b) 上面的 wrapper 有什麼致命問題、如何修正?
(a) 只有可執行檔 → run-time 插入:gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl 建成 PIC shared library,再以 LD_PRELOAD="./mymalloc.so" ./intr 執行;dynamic linker 解析未定義參考時會優先搜尋 mymalloc.so,把 intr 的 malloc 綁到 wrapper。
(b) wrapper 與 target 同名,malloc(size) 會解析回 wrapper 自己 → 無限遞迴。
修正:用 mallocp = dlsym(RTLD_NEXT, "malloc") 取得搜尋順序中「下一個」(即 libc 真正的)malloc,經函式指標呼叫。
| 題型 | 答題模式 |
|---|---|
| 兩大任務 | symbol resolution + relocation,一字不漏 |
| Section 歸屬 | 先問「global 還是 static?有無初始化?初始化為 0?」→ .data / COMMON / .bss / .rodata |
| 重複符號 | 先判 strong(函式、已初始化 global)/ weak(未初始化 global),再套 Rule 1/2/3;多強答 error、多弱答 unknown |
.symtab 有無 entry |
local 非 static 變數一律無;static local 有(唯一名稱如 x.1) |
| 靜態庫連結失敗 | 檢查命令列順序:定義必須跟在參照之後;循環相依可重複列出 |
| Relocation 計算 | PC32:ADDR(sym)+addend−(ADDR(s)+offset),驗證 PC(下一指令)+ 值 = 目標;絕對:ADDR(sym)+addend |
| Program header | .bss 大小 = memsz − filesz;合法性驗 vaddr mod align = off mod align |
| GOT/PLT | GOT=data segment/位址,PLT=code segment/程式碼;第一次呼叫觸發 dynamic linker 覆寫 GOT |
| 插入機制選擇 | 看手上材料:原始碼→compile-time、.o→link-time(--wrap)、只有執行檔→run-time(LD_PRELOAD) |
| 工具選擇 | 反組譯 objdump、全結構 readelf(⊇ size+nm)、動態相依 ldd、刪符號表 strip |