連結器(符號解析與重定位)
/ LINK-er /
在每個原始檔都被編譯並組譯成各自的目的檔之後,你手上仍是一堆各別、滿是空缺的零件,而非一個程式。連結器是那個最後的工具,它收集所有那些目的檔,加上它們所依賴的任何函式庫,把它們縫成單一個可執行檔。想像把鬆散、各自排版的章節裝訂成一本書:依序收齊、指派真實頁碼,並回頭把每個「見第 ___ 頁」的交叉參照填好,因為現在頁碼定下來了。
它做兩件大事。第一是符號解析:它讀取每個目的檔的符號表,對每個未定義(被需要)的符號,在其他目的檔與函式庫中搜尋同名且相符的已定義(被提供)符號,把需要者與提供者配對。第二是重定位:當它把所有區段排好佈局、為每個函式與變數指派了最終位址後,它走過重定位待辦清單,修補每一個空白,把每個「add 的位址,待填入」替換成真實的數字。結果是一個每個交叉參照都指向具體位址的檔案。
這就是為什麼「編譯」與「連結」是真正不同、會以不同方式失敗的步驟。編譯器處理單一檔案,抱怨語法與型別;連結器跨檔案運作,抱怨無法配對的名稱。一個程式可能乾淨地編譯卻無法連結,因為某函式被宣告卻從未被定義(「undefined reference」),或因為同一個全域變數被定義了兩次(「multiple definition」)。在 Unix 上經典的連結器叫 ld,不過你通常透過 gcc 或 clang 間接呼叫它,它們會帶著正確的函式庫替你呼叫它。
$ gcc -c main.c utils.c # 各自編譯 -> main.o、utils.o(尚未連結) $ gcc main.o utils.o -o app # 連結:解析 'add'(main 要它,utils 提供它), # 重定位,寫出可執行檔 'app'
各自編譯產生目的檔;一個連結步驟跨檔解析符號,組成可執行檔。
連結是跨檔進行的,所以抓出一個到處被宣告卻在任何地方都沒被定義的函式的是連結器,而非編譯器;那就是「undefined reference」錯誤。