連結器(the linker)
想像幾位作者各自分頭寫一本書的某一章。第 3 章寫著「參見第 7 章的食譜」,但寫的時候誰也不知道第 7 章會落在哪一頁。書能付印之前,必須有一位編輯把所有章節收齊、決定它們的順序與頁碼,並把每一個「參見第 __ 頁」的參考填上真正的號碼。連結器就是程式的那位編輯:它把分別編譯的片段組合成一個可執行的整體,並填入連接它們的位址。
具體來說,編譯器把每個原始檔轉成一個目的檔,內含機器碼,加上一張符號表,列出它定義的符號(像是函式 sort)以及它需要但沒有的符號(像是對 printf 的呼叫)。連結器拿這些目的檔加上程式庫程式碼,做三件事:把它們一起佈局成一個程式影像、解析符號(把每個「我需要 printf」對應到某個目的檔或程式庫提供的「printf 在這裡」),以及執行重定位(在一切都有了位置後調整位址)。結果是一個可供載入的可執行檔。
為什麼重要:連結讓你能用一個個小的、分別編譯的檔案建出大程式,並重用預先建好的程式庫,而不必把一切重寫一遍。如果連結器找不到你用到的某個符號的定義,你會得到熟悉的「未定義參考(undefined reference)」錯誤;如果兩個檔案都定義了同一個符號,你會得到「重複定義(multiple definition)」錯誤。連結器在建置時執行,在程式執行之前——這與載入器不同,後者較晚、在啟動時才行動。
main.o 呼叫了一個函式 add(),但只宣告它;math.o 定義了 add()。連結器把 main.o 裡的呼叫對應到 math.o 裡的定義,放置兩者,修正呼叫的目標位址,並寫出一個可執行檔。
組合目的檔、解析符號、重定位——成為一個可執行檔。
連結器在建置時執行並產生可執行檔;載入器較晚執行並把它放進記憶體。「未定義參考」是連結錯誤,不是編譯錯誤——程式碼編譯得好好的,只是在連結時找不到某個符號。