連結器
真正的程式很少寫在單一檔案裡。你會寫好幾個原始檔,還會用到別人寫的函式庫。每個部分都被分別編譯或組譯成一個目的檔(object file),但目的檔是不完整的:它在引用其他檔案裡的函式與資料之處留著空洞。連結器(linker)就是把所有這些目的檔合併成一支完成的程式、並把空洞填好、讓每個引用都指向正確位置的工具。把它想成一位編輯,拿著一疊分別寫成的章節,把它們組裝成一本頁碼連貫的書。
連結器主要做三件事。第一,它把各部分排好版,決定每個函式與每塊資料在合併後的程式裡坐落何處。第二,它解析符號(resolve symbols):當檔案 A 呼叫一個定義在函式庫裡的函式 printf 時,A 的目的檔留了一個標著「需要 printf」的空白;連結器找到 printf 真正的定義並記下它的位址。第三,它做重定位(relocation):回頭把每個空白用如今已知的位址補上,於是原本寫著 call ??? 的呼叫就變成 call 0x4005a0。若某個需要的符號哪裡都沒定義,你就會看到經典的「未定義引用」錯誤;若定義了兩次,就是「重複符號」錯誤。
連結器之所以重要,是因為它讓分別編譯變得實際可行:你可以改一個檔案、重新連結,而不必把全部重新編譯一遍,也可以把可重用的函式庫以目的碼形式發布。它運作在你這裡需要知道的概觀層級,位於組譯器/編譯器(產出目的檔)與載入器(把程式帶進記憶體執行)之間。它要做的一個關鍵選擇是:把函式庫的程式碼直接複製進你的程式(靜態連結),還是留一個引用、留待之後再解析(動態連結)。
main.o 呼叫一個定義在 math.o 裡的函式 sum: - 在 main.o 中,對 sum 的呼叫是個空白:『call <sum,位址未知>』。 - math.o 定義了 sum 並記下『sum 在這裡,位於我的位移 0x40』。 - 連結器把 math.o 安放好,得知 sum 的最終位址(比如 0x10240), 再把 main.o 的呼叫補成『call 0x10240』。空白現在補上了。 若沒有任何檔案定義 sum,你就會看到:undefined reference to `sum'。
先解析符號(找出每個名字定義在哪),再重定位(用真實位址把每個引用補上)。
「未定義引用」是連結器的錯誤,不是編譯器的:每個檔案各自都編譯得好好的,但它需要的某個名字在它們之中哪裡都沒定義。