原文連接:C靜態庫鏈接的順序問題html
C語言的靜態鏈接,簡單的說就是將編譯獲得的目標文件.o
(.obj
),打包在一塊兒,並修改目標文件中函數調用地址偏移量的過程。當在大一點的項目中,可能會遇到鏈接時,因爲靜態庫在連接器命令行中出現順序的問題,形成undefined reference
錯誤。本文深刻探討一下這個問題,以及如何解決。linux
以下圖。假設有這麼一個場景,在咱們的構建系統中,構建了一個兩個靜態庫文件liba.a
和libb.a
,其中liba.a
包含兩個目標文件a1.o
和a2.o
,而libb.a
包含一個目標文件b1.o
。但願將main.o靜態鏈接liba.a
和libb.a
。算法
注意到黃色的箭頭表示調用關係:b1.o
須要調用a1.o
中的某函數,而main.o
調用了a2.o
和b1.o
中的函數。你能夠把.o
文件理解爲對應的.c
文件。函數
那麼以下的兩個命令哪一個會成功執行呢?注意到這兩個命令惟一的區別是對liba.a
和libb.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
中全部目標文件定義過的全部符號集合D
。htm
以上面第一條命令gcc -o a.out main.o liba.a libb.a
爲例,咱們來一步步看看連接器的工做過程:rem
當輸入main.o
後,因爲main調用了a2.o和b1.o中的函數,而此時並無在D
中找到該符號,因而將引用的兩個函數保存在U
中,此處假設兩個函數分別爲a2_func
和b1_func
:get
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.a
和libb.a
換一下順序,就能夠連接成功!
通常來講有兩種辦法,一種是仔細分析依賴關係,並按照正確的順序書寫庫文件的引用。原則是被依賴的儘可能寫在右邊。可是在有些大型項目中,依賴關係可能並不容易梳理清楚。此時能夠在命令行參數中重複對庫文件的引用:
# gcc -o a.out main.o liba.la libb.la liba.a
在上面的命令中,liba.a
重複書寫了兩次。
若是你使用automake
,能夠用xxx_LIBAD
D和xxx_LDADD
來控制目標文件的引用關係:
xxx_LIBADD:對於目標文件爲庫文件或可執行文件,需使用這個選項。表示在打包目標庫文件的時候,就將依賴的文件一併打包進來。
xxx_LDADD:對於可執行文件可用這個選項,來控制連接器的參數,若是你能分析清楚依賴關係,能夠在這個選項中按照正確的順序書寫,從而成功鏈接。