3 分鐘徹底理解鏈接器

大家好,我是小風哥。

對於 C/C++ 程序員來說,寫完程序之後我們通常說的一句話就是” 把程序編譯一下 “,編譯完成後就生成了可執行程序,因此我們誤以爲從源代碼到可執行性程序只需要編譯這一步,實際上這是不正確的,這樣理解忽略了從源文件到可執行程序中的重要一步,那就是鏈接,link。

因此從源文件到可執行程序需要兩步:編譯與鏈接。

先說編譯。

編譯實際上僅僅將源代碼轉爲了二進制機器指令,保存這些二進制機器指令的文件叫做目標文件

注意,這還不是最終的可執行程序,每個源文件對應一個目標文件,你寫了 a.c 和 b.c 和 c.c 那麼經編譯器編譯後會生成 3 個目標文件。

實際上到這裏編譯器的工作就結束了。

現在我們得到了一堆目標文件,這些目標文件最終是怎麼變成一個可執行程序了呢?

接下來就該鏈接器登場了。

鏈接器把這些目標文件打包成最終的可執行程序。

當然,除了打包目標文件之外,鏈接器默認還會打包另一個非常重要的東西,那就是標準庫,以 C 語言爲例,那就是 C 標準庫:

所以忽略中間結果,這裏的目標文件就是中間結果,只從源頭看,我們能發現最終的可執行程序來自兩部分:我們寫的代碼與標準庫,這兩部分組成了最終的可執行程序。

看到這裏有的同學可能會問,那鏈接器的作用看起來很簡單,不就是個打包工具嗎,之前提打包這個詞只是爲了好理解,實際上鍊接器最重要的工作就是決定符號真的有定義,以及該用哪個定義,這裏的符號指的是變量名或者函數名。

以經典的 hello world 程序爲例。

int main() {
  printf("hello world!\n");
};

想必任何一個學過 C 語言的同學對此都不會陌生,我們將這段代碼保存爲 hello.c。

實際上編譯器在編譯 hello.c 遇到 printf 時根本就不知道 printf 這個符號定義在哪裏,這不是編譯器該關心的事情。

因此我們可以看到編譯器只能看到局部,看不到全局,這裏的局部就侷限在一個源文件內部。

那麼到底是誰來關心 printf 定義在哪裏呢?這就是鏈接器,不要忘了,鏈接器要打包所有目標文件,因爲鏈接器可以看到全局,鏈接器具有上帝視角。

由於我們只寫了一個 hello.c,因此最終只會生成一個目標文件,hello.c 中沒有定義 printf,那麼 printf 還能定義在哪裏呢?

再看看這張圖,你能猜到嗎?

沒錯,就定義在標準庫中。

當然,如果鏈接器翻遍所有目標文件以及標準庫都沒有找到某個符號的定義那麼就會報一個經典錯誤,那就是 undefined reference to `func'。

就像這樣:

void func(int a);
void main() {
  func(1);
}

接下來我們編譯一下,可以看到這裏實際上是沒有編譯錯誤的,因爲編譯器發現你在調用一個定義在外部模塊的函數叫做 abc 的函數,而你的使用方法也是沒有問題的,因此編譯通過。

但連接器在打包時翻遍所有目標文件以及標準庫也沒有找到一個叫做 abc 的函數,因此開始抱怨它找不到這個符號的定義,這就是爲什麼它會報錯的原因。

現在你應該明白連接器的作用了吧。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/H_CfdHCcIdf62gAoGpBnTw