fw:C靜態庫連線的順序問題
C靜態庫連線的順序問題
C語言的靜態連線,簡單的說就是將編譯得到的目標檔案.o(.obj),打包在一起,並修改目標檔案中函式呼叫地址偏移量的過程。當在大一點的專案中,可能會遇到連線時,由於靜態庫在連結器命令列中出現順序的問題,造成undefined reference錯誤。本文深入探討一下這個問題,以及如何解決。
作者:P_Chou來源:segmentfault|2016-09-19 10:54
收藏 分享C語言的靜態連線,簡單的說就是將編譯得到的目標檔案.o(.obj),打包在一起,並修改目標檔案中函式呼叫地址偏移量的過程。當在大一點的專案中,可能會遇到連線時,由於靜態庫在連結器命令列中出現順序的問題,造成undefined reference錯誤。本文深入探討一下這個問題,以及如何解決。
問題
如下圖。假設有這麼一個場景,在我們的構建系統中,構建了一個兩個靜態庫檔案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的書寫順序
- # 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)作為考察單元。在整個連線過程中,如果某個目標檔案中的符號被用到了,那麼這個目標檔案會單獨從庫檔案中提取出來,而不會把整個庫檔案連線進來。
然後,連結器在工作過程中,維護3個集合:需要參與連線的目標檔案集合E、一個未解析符號集合U、一個在E中所有目標檔案定義過的所有符號集合D。
以上面第一條命令gcc -o a.out main.o liba.a libb.a為例,我們來一步步看看連結器的工作過程:
當輸入main.o後,由於main呼叫了a2.o和b1.o中的函式,而此時並沒有在D中找到該符號,於是將引用的兩個函式儲存在U中,此處假設兩個函式分別為a2_func和b1_func:
- 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_LIBADD和xxx_LDADD來控制目標檔案的引用關係:
- xxx_LIBADD:對於目標檔案為庫檔案或可執行檔案,需使用這個選項。表示在打包目標庫檔案的時候,就將依賴的檔案一併打包進來。
- xxx_LDADD:對於可執行檔案可用這個選項,來控制連結器的引數,如果你能分析清楚依賴關係,可以在這個選項中按照正確的順序書寫,從而成功連線。