冰山之下:從extern C到程序鏈接


閱讀C++代碼的時候,或者看一些教程的時候,我們經常會看到這麼一個關鍵字extern "C",例如我們在Python調用C++之PYBIND11源碼分析這篇文章中對舉的例子進行宏展開後看到,被用作模塊初始化的函數initexample()前面就有extern "C"所修飾。那麼extern “C”`是幹什麼的呢?
解釋它很簡單,一句話就可以:它是鏈接指示符,用來告訴編譯器不要更改它所修飾的函數或者變量等的名字,確保鏈接器能正確將目標文件鏈接在一起。
但是要解釋爲什麼不使用就鏈接不通過,就說來話長了。

改名(name mangling)

改名,翻譯成重命名可能更符合習慣,就是對C++中定義的函數等的名字按照一定規則去更改,至於是什麼規則,有於C++標準沒有說明,所以C++編譯器們各顯神通,每個編譯器重命名的方法自己定,只要確保名字是唯一的就行。改名的原因也很簡單,因爲C++支持重載,這導致同一個作用域中很可能出現很多函數名字都一樣,這就給編譯器生成代碼造成了困擾。大家都叫張三,其中一個張三打架了,你總不能讓所有張三的家長都來學校吧。絕頂聰明的編譯器工程師總會有辦法解決,既然都叫張三,可以加前綴後綴後綴嘛。張三A、張三B、張三C……別的班可以根據需要改成張三甲、張三乙、張三丙……
改名字雖然解決重載的問題,但是卻給鏈接帶來的麻煩。如果大家都是C++編譯器還好,只要兩個編譯器的改名規則一樣,當鏈接外部庫的時候就能鏈接的上。但是如果是庫使用C++編譯的,當前編譯程序使用C編譯器呢?問題就來了,C中是沒有改名這種說法的,C不支持重載沒必要多此一舉,所以C編譯器不會對函數名字做修改,這樣就導致鏈接器做鏈接的時候,拿着一個頭文件中聲明的正常的名字去二進制庫文件裏面找一個改過名字的函數,怎麼叫它都不會答應,就會導致出現下面這種錯誤。

undefined reference to `balabala'
ld returned 1 exit status

問題一個一個來,那就一個一個解決,既然改名了找不到,那就允許指定某些函數不改名,因此引入了extern "C"這個指示符。被這個指示符限定的函數,或者在其中的代碼塊中的函數變量等,不做改名處理,它們原本叫什麼就是什麼,命名衝突的問題讓程序員自己解決。這樣鏈接器就能順利進行鏈接。
需要注意的是extern "C"應該被看作一個整體,實際上"C"用來表示函數使用什麼語言編寫,所有C++編譯器都被要求支持"C",當然如果使用的編譯器支持,你也可以使用extern "Ada", extern "FORTRAN"等,讓編譯器知道使用何種方式去調用其中定義的函數。它表明了這些函數應該按照對應函數的調用方式去調用。這樣做的好處不僅是別的語言可以調用你,你也可以調用別的語言,因爲大家已經遵循了同一個語言層面的ABI。關於ABI的內容,我在交叉編譯和ABI簡介中介紹過,感興趣的朋友可以去看一下。

下面用個小例子做簡單演示:

// defined in library.h
#ifdef __cplusplus
extern "C" {
#endif

int my_function(int);

#ifdef __cplusplus
}
#endif

// defined in library.cpp
#include <iostream>
# include "library.h"

int my_function(int arg) {

    std::cout << "arg is " << arg << std::endl;
}

// defined in main.c
#include "library.h"
int main(int argc, char* args[]) {
    my_function(9);
    return 0;
}

將上面的代碼保存爲三個對應的文件,使用下面兩條命令去編譯:

g++ -shared -fPIC library.cpp library.h -o libmylib.so 
gcc main.c -o main -lmylib -L .

如果頭文件library.h中函數的聲明沒有加extern "C"是編譯不過去的。在這個例子中有兩個小知識點,第一就是extern "C"可以單獨用在一行,以可以使用花括號將代碼包起來;第二就是#ifdef __cplusplus這個預處理語句,它使得一份代碼在C和C++中都能編譯。__cplusplus這個宏定義也是不需要我們自己定義的,編譯器會根據自身類型決定這個宏定義有沒有。如果我是炎黃子孫,那麼我肯定帶有龍的傳人這個標籤,不需要別人專門在你胸前別一個。

如果爲了解釋extern "C",到這裏差不多也就夠了。但好奇心驅使我,繼續尋找問題的答案。比如,鏈接器是怎麼工作的,它爲什麼需要確切的函數名字,以及拿到名字後如何在一個二進制文件中找到它?

在說鏈接器如何工作之前,我們還需要先簡單說說ELF格式。

Executable and Linkable Format

ELF(Executable and Linkable Format) 是一種爲動態鏈接庫、可執行文件、目標文件和core dumps文件設計的一種通用格式標準。ELF的設計目標是實現一種靈活的、可擴展和跨平臺格式。在Linux平臺下可以通過readelf這個工具去讀取它的內容。
由於ELF既可以是一個庫文件,也可以是一個可執行程序,因此它可以有以下兩種視角:
在這裏插入圖片描述

其中,每個ELF文件都有一個ELF頭(ELF header),它位於文件的開始,用於描述整個文件的內容概貌,它包含ELF魔數、文件類型、目標機器的架構、程序入口、程序信息表、段信息表的地址等信息。

程序信息表(program header table)用來告訴系統如何創建這個程序的一個進程,它在可執行文件中必須存在,在可重定位文件(relocatable files )中可以沒有。段信息表(section header table)中有錄着文件中每個段的名字、大小、位置等元信息,每個段在這個表裏面都會有一條記錄與之對應。可鏈接文件必須有段信息表,其他類型文件可以沒有。另外,在ELF中除了ELF頭位置固定以外,其他段的信息位置和順序都可以是不固定的。

ELF中有很多的段,分門別類的記錄了關於這個文件的信息,比如:記錄機器指令的段(.text);記錄程序初始化數據的段(.data);記錄版本信息的字段(.comment);記錄字符串的段(.strtab)等等。其中有幾個字段和程序鏈接關係密切,鏈接器就是根據它們的信息去鏈接程序的。它們就是符號表段(.symtab)、依賴段(.dynamic)、重定位段(.rel, .rela)。由於本文主要主要講解extern "C"這個命令禁止函數重命名來防止鏈接程序的時候出錯,因此我們只着重將符號表依賴。

可執行文件、庫文件或者目標文件都會包含一個符號表,符號表的每一條記錄表示一個符號(symbol,爲了不混淆,下面把符號表中的一個符號稱爲一條記錄),每一條記錄和程序中的出現的函數、變量等一一對應。簡單的看(實際上不是,只是爲了方便說明),每條記錄包含三個字段:名字、類型和值。名字字段保存了指向了前面提到的字符串段(.strtab)中記錄記錄的名字的地址;類型保存這條記錄是一個函數或者是變量;值一般保存這條記錄實際的地址偏移。例如我們之前的例子中,函數my_function在符號表裏面就有一條記錄和它對應,其中記錄的名字字段指向了字符串段中my_funcion這個字符串,類型字段存儲了它是一個函數類型,值字段存儲它世紀的內容的地址。

符號(symbol)一般可以分爲兩種:一種是它的值記錄地址的表示的內容能在本文件中找到,這種符號稱爲已定義符號;另一種是符號的值表示的值不在本文件中,這種符號稱爲未定義符號。

依賴段中記錄本文件對其他文件的依賴,它裏面保存了依賴的名字等內容。

鋪墊就這麼多,接下來可以簡單說說鏈接器怎麼工作了。

動態鏈接

程序的運行,通常主要步驟有以下幾步:

  1. 裝載可執行文件;
  2. 裝載可執行文件的依賴,依賴的內容在依賴表(.dynamic)中;
  3. 鏈接和重定位;
  4. 將控制權交給程序。

第三步主要是動態鏈接器來完成,它的工作就是爲已定義的符號一個確切的地址,然後通過解析未定義的符號的地址。怎麼解析的呢?就是根據符號的名字來解析,它會拿着符號的名字去所有依賴中一個一個找,如果依賴中有依賴繼續遞歸式的找,直到在某個依賴中找到一個和這個符號名字一樣的已定義符號或者以找不到而告終。找到就鏈接成功找不到就報錯終止程序執行。真相大白,如果可執行文件依賴的某個動態庫改了名字,就會導致最終找不到這個在可執行文件中未定義的符號的定義,這就是爲什麼要阻止編譯器改名。編譯的時候的鏈接器就相當於爲動態鏈接器做了一次預演。

豁然開朗。

總結

extern "C"的目的是爲告訴C++編譯器不要對它修飾的函數等進行改名,主要依據就是有可能別的編譯器是不進行改名操作或者改名規則不同,這會使得鏈接器按着未定義的符號的名字卻找不到找另一個同名字的已定義的符號,導致鏈接失敗。

公衆號二維碼

本文首發於個人微信公衆號TensorBoy。微信掃描上方二維碼或者微信搜索TensorBoy並關注,及時獲取更多最新文章!

References

[1] Name mangling (C++ only)
[2] Questions about extern “C” linkage directive - C / C++
[3] How does C++ linking work in practice?
[4] Linkers part 2
[5] Static, Shared Dynamic and Loadable Linux Libraries
[6] http://www.skyfree.org/linux/references/ELF_Format.pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章