🎯 什麼情境該想到我
當你遇到「編譯過但連結失敗、undefined symbol、找不到入口」這類錯誤,或想搞懂「原始碼怎麼變成能跑的程式」時。
⚙️ 怎麼用(理解四階段,對症下藥)
gcc hello.c 這一句其實是四個階段的縮寫,而且每一階段都能單獨跑出來看(PDF p63–66):
| 階段 | 實際程式 | 拆出來看 | 這一關出錯的意思 |
|---|---|---|---|
| 預處理 | cpp | gcc -E hello.c -o hello.i | 標頭/巨集展開錯 |
| 編譯 | cc1(C)/ cc1plus(C++) | gcc -S hello.i -o hello.s | 語法/型別問題 |
| 彙編 | as | gcc -c hello.s -o hello.o | 組語層問題 |
| 連結 | ld(GCC 實際呼叫的是 collect2) | ld ... | 符號有引用、沒定義 |
gcc 本身只是包裝,依參數去叫上面這些後端程式。
預處理只做 6 件事(p64):展開 #define、處理 #if/#ifdef/#elif/#else/#endif、遞迴插入 #include、刪註解、加行號與檔名標記、保留 #pragma。
書中直接點出可執行動作:「所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题」(p65)。巨集或標頭出問題時的第一動作是看
.i檔,不是靠猜。
編譯階段內部是六步(p66–72):掃描(詞法)→ 語法分析 → 語義分析 → 源碼優化 → 代碼生成 → 目標代碼優化。中間代碼是前端/後端的分界線——前端產機器無關的中間碼,後端轉成目標機器碼,所以跨平台編譯器能一個前端配 N 個後端(p72)。這也解釋了錯誤的分層:括號不匹配是語法錯(p68)、型別不匹配是語義錯(p70)、除以 0 是執行期錯。
連結不是一步,是三個子步驟(p76):
- 位址與空間分配(Address and Storage Allocation)
- 符號決議(Symbol Resolution)
- 重定位(Relocation)
「重定位所做的就是给程序中每个这样的绝对地址引用的位置”打补丁”,使它们指向正确的地址。」(p78)
術語辨析:「決議 Resolution」偏靜態連結、「綁定 Binding」偏動態連結(p76)——讀 ld 與 glibc 文件時(例如 -z now 的 “bind now”)常撞到這組詞。內部機制見 工具-符號決議與重定位。
裝載階段做的是建立映射,不是搬資料(p182–183):建立獨立虛擬位址空間 → 建立「虛擬空間 ↔ 可執行檔」的映射 → 把指令暫存器設成入口位址。做完這三步一個位元組都還沒進記憶體,靠 page fault 按需載入。
- 靜態連結時入口是 ELF header 的
e_entry;動態連結時入口是動態連結器(p200、p244)。 - 而入口也不是
main——中間還隔著執行庫的初始化,見 工具-程式啟動與執行庫。
🔎 undefined symbol 診斷樹
同一句錯誤訊息,有三個完全不同的發生時間點:
| 時間點 | 長什麼樣 | 常見成因 |
|---|---|---|
| 連結期 | ld: undefined reference to 'foo' | ①少連某個庫 ②輸入目標檔路徑不對 ③符號的宣告與定義不一致(C↔C++ mangling、呼叫慣例不同)(p133、p323–324) |
| 執行期・啟動時 | error while loading shared libraries / undefined symbol | .so 找不到或版本不對(p266) |
| 執行期・第一次呼叫時 | 跑了一陣子才爆 | 延遲綁定——用 LD_BIND_NOW / RTLD_NOW 可以把它提前(p225–227、p248) |
先分清楚是哪一關,再往下查。
🧰 工具鏈速查
| 想做什麼 | Linux | Windows |
|---|---|---|
| 看段結構 | readelf -S / objdump -h | dumpbin /HEADERS |
| 看執行視圖的 Segment | readelf -l | — |
| 看符號 | nm / readelf -s / objdump -t | dumpbin /SYMBOLS |
| 看重定位項 | objdump -r | — |
| 反解 C++ 修飾名 | c++filt | UnDecorateSymbolName() |
| 看動態依賴 | ldd / readelf -d | dumpbin /IMPORTS |
| 追裝載與綁定 | LD_DEBUG=libs|bindings|statistics|help | — |
| 看靜態庫內容 | ar -t / ar -x | lib /LIST |
| 瘦身 | strip(或 -Wl,-s 全部/-Wl,-S 只去除錯符號) | — |
🧪 我實際套用的紀錄
- 2026-07-15:(待填)
⚠️ 注意
- 「編譯錯」與「連結錯」是不同階段的問題,訊息也不同——先分清楚是哪一關。
-g產生的.debug_*段往往比程式碼本身大好幾倍,發布前記得strip(p119–120)。但別對要除錯的產物做——書中也警告不要為了瘦身而 strip 掉共享庫的除錯資訊或用-fomit-frame-pointer,「没有帧指针之后,无法准确定位函数的调用轨迹(Stack Trace)」(p314)。-nostartfiles/-nostdlib會讓全域物件的建構與解構不執行(p369、p388)。裸機/韌體情境要自己補。- 這四階段是心智模型,不是實作規格。連結階段內部的兩趟掃描、裝載階段的按需分頁,各自都是獨立的一整套機制。
🔗 相關工具
- 工具-符號決議與重定位 —— 把「連結」這個黑盒打開:兩趟掃描、強弱符號、name mangling、重定位算式
- 工具-虛擬記憶體與行程位址空間 —— 把「裝載」這個黑盒打開:VMA、page fault、Section vs Segment
- 工具-程式啟動與執行庫 —— 接在裝載之後:跳到入口位址之後、進
main之前發生了什麼 - 工具-靜態庫與動態庫 —— 連結階段的分岔選擇,也是 undefined symbol 這類錯誤的常見來源
- 工具-系統化除錯 —— 遇到連結期或載入期怪錯誤時的追查方法