C靜態庫鏈接的順序問題

原文連接:C靜態庫鏈接的順序問題html

C語言的靜態鏈接,簡單的說就是將編譯獲得的目標文件.o(.obj),打包在一塊兒,並修改目標文件中函數調用地址偏移量的過程。當在大一點的項目中,可能會遇到鏈接時,因爲靜態庫在連接器命令行中出現順序的問題,形成undefined reference錯誤。本文深刻探討一下這個問題,以及如何解決。linux

問題

以下圖。假設有這麼一個場景,在咱們的構建系統中,構建了一個兩個靜態庫文件liba.alibb.a,其中liba.a包含兩個目標文件a1.oa2.o,而libb.a包含一個目標文件b1.o。但願將main.o靜態鏈接liba.alibb.a算法

注意到黃色的箭頭表示調用關係:b1.o須要調用a1.o中的某函數,而main.o調用了a2.ob1.o中的函數。你能夠把.o文件理解爲對應的.c文件。函數

那麼以下的兩個命令哪一個會成功執行呢?注意到這兩個命令惟一的區別是對liba.alibb.a的書寫順序spa

# gcc -o a.out main.o liba.a libb.a

...undefined reference...
error: ld returned 1 exit status
# gcc -o a.out main.o libb.a liba.a

靜態鏈接的算法

要理解上面這個問題,須要理解連接器在處理靜態鏈接時候的算法。此處的闡述參考《深刻理解計算機系統》中的「連接」章節。命令行

首先,須要明確的是,連接器在考察庫文件(.a)的時候,不是把庫文件看作一個總體,而是將打包在其中的目標文件(.o)做爲考察單元。在整個鏈接過程當中,若是某個目標文件中的符號被用到了,那麼這個目標文件會單獨從庫文件中提取出來,而不會把整個庫文件鏈接進來。code

而後,連接器在工做過程當中,維護3個集合:須要參與鏈接的目標文件集合E、一個未解析符號集合U、一個在E中全部目標文件定義過的全部符號集合Dhtm

以上面第一條命令gcc -o a.out main.o liba.a libb.a爲例,咱們來一步步看看連接器的工做過程:rem

當輸入main.o後,因爲main調用了a2.o和b1.o中的函數,而此時並無在D中找到該符號,因而將引用的兩個函數保存在U中,此處假設兩個函數分別爲a2_funcb1_funcget

E               U               D         
+---------------+---------------+---------------+
|     main.o    |    a2_func    |               |
+---------------+---------------+---------------+
|               |    b1_func    |               |
+---------------+---------------+---------------+

接下來,輸入liba.a,連接器發現,a2_func存在於liba.a的a2.o中,因而將a2.o加入到E,並在D中加入a2.o中全部定義的符號,其中包括a2_func,最後移除U中的a2_func,由於這個符號已經在a2.o中找到了的。然而,U中還有b1_func,因此鏈接尚未完成。

E               U               D         
+---------------+---------------+---------------+
|     main.o    |               |    a2_func    |
+---------------+---------------+---------------+
|     a2.o      |    b1_func    | a2_func_other |
+---------------+---------------+---------------+

接着,輸入libb.a,同理,連接器發現b1_func定義在b1.o中,因此在E中加入b1.o,移除U中的b1_func,在D中加入b1.o裏面全部定義的符號

E               U               D         
+---------------+---------------+---------------+
|     main.o    |               |    a2_func    |
+---------------+---------------+---------------+
|     a2.o      |               | a2_func_other |
+---------------+---------------+---------------+
|     b1.o      |               |    b1_func    |
+---------------+---------------+---------------+

然而,因爲b1.o調用到a1.o中的函數,咱們假設是a1_func,但在D中並無找到這個函數,因此a1_func還須要加入到U

E               U               D         
+---------------+---------------+---------------+
|     main.o    |               |    a2_func    |
+---------------+---------------+---------------+
|     a2.o      |               | a2_func_other |
+---------------+---------------+---------------+
|     b1.o      |    a1_func    |    b1_func    |
+---------------+---------------+---------------+

可是,輸入結束了!連接器發現U中還有未解析的符號,因此報錯了!

能夠看到因爲連接器的算法實現,致使a1.o並無被連接器考察,因此產生了未解析符號。仔細分析,能夠知道,只要將liba.alibb.a換一下順序,就能夠連接成功!

解決辦法

通常來講有兩種辦法,一種是仔細分析依賴關係,並按照正確的順序書寫庫文件的引用。原則是被依賴的儘可能寫在右邊。可是在有些大型項目中,依賴關係可能並不容易梳理清楚。此時能夠在命令行參數中重複對庫文件的引用:

# gcc -o a.out main.o liba.la libb.la liba.a

在上面的命令中,liba.a重複書寫了兩次。

若是你使用automake,能夠用xxx_LIBADD和xxx_LDADD來控制目標文件的引用關係:

  • xxx_LIBADD:對於目標文件爲庫文件或可執行文件,需使用這個選項。表示在打包目標庫文件的時候,就將依賴的文件一併打包進來。

  • xxx_LDADD:對於可執行文件可用這個選項,來控制連接器的參數,若是你能分析清楚依賴關係,能夠在這個選項中按照正確的順序書寫,從而成功鏈接。

相關文章
相關標籤/搜索