🎯 什麼情境該想到我

當你撞上 undefined reference to 'foo'multiple definition of 'x',或遇到「明明有實作、連結卻說找不到」「兩個 .c 都定義了同名全域變數卻能連過、行為卻錯亂」這類看起來像玄學的連結錯誤時。

⚙️ 怎麼用

一、先建立心智模型:連結就是「按符號拼拼圖」

模組之間只有兩種互動——函式呼叫、變數存取——兩者都可歸結為同一件事:模組間符號的引用(p75)。定義符號的模組多出一塊,引用符號的模組剛好少那一塊。

編譯 main.c 時編譯器不知道 foo() 在哪,就把呼叫指令的目標位址欄位先填 0 擱置,等連結器補。每個要被修正的地方叫一個重定位入口(Relocation Entry)

「重定位所做的就是给程序中每个这样的绝对地址引用的位置”打补丁”,使它们指向正确的地址。」(p78)

二、靜態連結是兩趟掃描

  1. 空間與位址分配:掃描所有 .o把相同性質的段合併(所有 .text 併成一個),算出輸出檔中各段的虛擬位址,同時把所有符號定義與引用收集進一張全域符號表。
    — 為什麼要合併而不是按檔案順序疊加?因為疊加會產生成百上千個零散段,每段都要按 4096 對齊,浪費巨大(p123–125)。
  2. 符號解析與重定位:讀重定位表,把每個引用點的位址修正到正確位置(p125)。

x86 32 位元只有兩種基本修正算式(p134–135):

  • R_386_32(絕對定址)→ S + A
  • R_386_PC32(相對定址)→ S + A − P

(S = 符號實際位址,A = 被修正處原本的值,P = 被修正處本身的位址。看懂這兩條,objdump -r 的輸出就沒有秘密了。)

三、undefined reference 的診斷樹

書中直接給出三大成因(p133):

成因怎麼確認
少連了某個庫nm -C / ar -t 檢查該符號到底在哪個 .a/.o
輸入目標檔路徑不對檢查 -L / -l / 相對路徑
符號的宣告與定義不一致最常見是 C ↔ C++ 的 name mangling 不一致,見下一節

機制上,readelf -s a.o 裡那些 UND 符號之所以存在,正是因為 a.o 裡有針對它們的重定位項;連結器掃完全部輸入仍找不到定義,才報錯。

四、C++ 符號修飾(name mangling)與 extern "C"

C++ 有重載、命名空間、類別,所以編譯器用「函式簽名 → 唯一符號名」的規則產生符號。GCC 規則是 _Z 開頭、巢狀接 N、每段名字前寫長度、E 結尾、參數型別接在後(p111–113):

int N::C::func(int)   →   _ZN1N1C4funcEi
  • 反解用 c++filt
  • 變數的型別不進修飾名,全域變數只按命名空間修飾。
  • MSVC 用完全不同的 ?func@C@N@@AAEHH@Z 風格,而且規則未公開——這是不同編譯器產物無法互連的主因之一。
  • 標準寫法:對外標頭檔用 #ifdef __cplusplus 包住 extern "C" {(p115–116)。

五、強符號、弱符號與 COMMON——「忘了加 extern 卻能連過」的真相

C/C++ 中函式與已初始化的全域變數是強符號,未初始化的全域變數是弱符號;也可用 __attribute__((weak)) 手動標記。連結器三條規則(p117):

  1. 強符號不可重複定義,否則 multiple definition
  2. 一強多弱 → 選強符號。
  3. 全弱 → 選佔空間最大的那個。(A 檔宣告成 int 佔 4 bytes、B 檔宣告成 double 佔 8 bytes,連結後是 8 bytes。書中加註「尽量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误」。)

未初始化全域變數不直接進 .bss,而是標成 SHN_COMMON,因為單獨編譯時還不知道別的編譯單元裡同名符號要多大,只有連結器讀完全部輸入才能定案(p136–137)。書中對這個設計直言不諱:

「这种使用 COMMON 块的方法实际上是一种类似”黑客”的取巧办法……但最本质的原因还是链接器不支持符号类型。」(p137)

逃生開關:-fno-common(或 __attribute__((nocommon)))——未初始化全域變數變成強符號,重複定義直接報錯。GCC 10 起這是預設值,所以升級編譯器後突然一堆 multiple definition,根因就在這裡。

六、弱引用:做「選配相依」

強引用找不到定義 → 連結錯誤;弱引用找不到定義 → 連結器不報錯,把該符號值設為 0(p118)。

__attribute__((weak)) int pthread_create(...);
...
if (pthread_create) { /* 多執行緒版 */ } else { /* 單執行緒版 */ }

書中實測:gcc pthread.c -o pt 印 single-thread,加 -lpthread 印 multi-thread(p118–119)。用途有三:函式庫提供可被使用者強符號覆寫的預設實作、執行期偵測 optional 相依是否存在、讓程式功能更容易裁剪與組合(少連某個 .o 仍可連結,只是少了功能)。

🧪 我實際套用的紀錄

  • 2026-08-02:(待填)

⚠️ 注意

  • undefined symbol 有三個不同時間點,訊息長得像但根因完全不同:①連結期(ld 報 undefined reference)②執行期啟動時(動態連結器解析失敗)③延遲綁定下第一次呼叫該函式時才爆。這張卡只管第 ①種,②③見 工具-動態連結與位址無關程式碼
  • .bss 只記大小、不佔檔案空間SHT_NOBITSobjdump -h 裡沒有 CONTENTS 屬性)。所以「宣告了 1MB 全域陣列,執行檔大小卻沒變」是正常的。順帶一提 static int x = 0; 會被歸到 .bss 而非 .data——初始化成 0 等同未初始化(p91–92)。
  • 連結器連靜態庫的粒度是「目標檔」不是「函式」:用到 printf() 就整個 printf.o 被拉進來。這也是為什麼 libc 刻意做成一個函式一個 .o(p147–148)。
  • 強弱符號規則是「靜默」的——一強多弱時不會有任何警告。這正是這類 bug 難查的原因。

🔗 相關工具