C靜態(tài)庫連接的順序問題
C語言的靜態(tài)連接,簡單的說就是將編譯得到的目標(biāo)文件.o(.obj),打包在一起,并修改目標(biāo)文件中函數(shù)調(diào)用地址偏移量的過程。當(dāng)在大一點(diǎn)的項(xiàng)目中,可能會(huì)遇到連接時(shí),由于靜態(tài)庫在鏈接器命令行中出現(xiàn)順序的問題,造成undefined reference錯(cuò)誤。本文深入探討一下這個(gè)問題,以及如何解決。
問題
如下圖。假設(shè)有這么一個(gè)場(chǎng)景,在我們的構(gòu)建系統(tǒng)中,構(gòu)建了一個(gè)兩個(gè)靜態(tài)庫文件liba.a和libb.a,其中l(wèi)iba.a包含兩個(gè)目標(biāo)文件a1.o和a2.o,而libb.a包含一個(gè)目標(biāo)文件b1.o。希望將main.o靜態(tài)連接liba.a和libb.a。
注意到黃色的箭頭表示調(diào)用關(guān)系:b1.o需要調(diào)用a1.o中的某函數(shù),而main.o調(diào)用了a2.o和b1.o中的函數(shù)。你可以把.o文件理解為對(duì)應(yīng)的.c文件。
那么如下的兩個(gè)命令哪個(gè)會(huì)成功執(zhí)行呢?注意到這兩個(gè)命令唯一的區(qū)別是對(duì)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
靜態(tài)連接的算法
要理解上面這個(gè)問題,需要理解鏈接器在處理靜態(tài)連接時(shí)候的算法。此處的闡述參考《深入理解計(jì)算機(jī)系統(tǒng)》中的“鏈接”章節(jié)。
首先,需要明確的是,鏈接器在考察庫文件(.a)的時(shí)候,不是把庫文件看做一個(gè)整體,而是將打包在其中的目標(biāo)文件(.o)作為考察單元。在整個(gè)連接過程中,如果某個(gè)目標(biāo)文件中的符號(hào)被用到了,那么這個(gè)目標(biāo)文件會(huì)單獨(dú)從庫文件中提取出來,而不會(huì)把整個(gè)庫文件連接進(jìn)來。
然后,鏈接器在工作過程中,維護(hù)3個(gè)集合:需要參與連接的目標(biāo)文件集合E、一個(gè)未解析符號(hào)集合U、一個(gè)在E中所有目標(biāo)文件定義過的所有符號(hào)集合D。
以上面第一條命令gcc -o a.out main.o liba.a libb.a為例,我們來一步步看看鏈接器的工作過程:
當(dāng)輸入main.o后,由于main調(diào)用了a2.o和b1.o中的函數(shù),而此時(shí)并沒有在D中找到該符號(hào),于是將引用的兩個(gè)函數(shù)保存在U中,此處假設(shè)兩個(gè)函數(shù)分別為a2_func和b1_func:
- E U D
- +---------------+---------------+---------------+
- | main.o | a2_func | |
- +---------------+---------------+---------------+
- | | b1_func | |
- +---------------+---------------+---------------+
接下來,輸入liba.a,鏈接器發(fā)現(xiàn),a2_func存在于liba.a的a2.o中,于是將a2.o加入到E,并在D中加入a2.o中所有定義的符號(hào),其中包括a2_func,最后移除U中的a2_func,因?yàn)檫@個(gè)符號(hào)已經(jīng)在a2.o中找到了的。然而,U中還有b1_func,所以連接還沒有完成。
- E U D
- +---------------+---------------+---------------+
- | main.o | | a2_func |
- +---------------+---------------+---------------+
- | a2.o | b1_func | a2_func_other |
- +---------------+---------------+---------------+
接著,輸入libb.a,同理,鏈接器發(fā)現(xiàn)b1_func定義在b1.o中,所以在E中加入b1.o,移除U中的b1_func,在D中加入b1.o里面所有定義的符號(hào)
- E U D
- +---------------+---------------+---------------+
- | main.o | | a2_func |
- +---------------+---------------+---------------+
- | a2.o | | a2_func_other |
- +---------------+---------------+---------------+
- | b1.o | | b1_func |
- +---------------+---------------+---------------+
然而,由于b1.o調(diào)用到a1.o中的函數(shù),我們假設(shè)是a1_func,但在D中并沒有找到這個(gè)函數(shù),所以a1_func還需要加入到U中
- E U D
- +---------------+---------------+---------------+
- | main.o | | a2_func |
- +---------------+---------------+---------------+
- | a2.o | | a2_func_other |
- +---------------+---------------+---------------+
- | b1.o | a1_func | b1_func |
- +---------------+---------------+---------------+
但是,輸入結(jié)束了!鏈接器發(fā)現(xiàn)U中還有未解析的符號(hào),所以報(bào)錯(cuò)了!
可以看到由于鏈接器的算法實(shí)現(xiàn),導(dǎo)致a1.o并沒有被鏈接器考察,所以產(chǎn)生了未解析符號(hào)。仔細(xì)分析,可以知道,只要將liba.a和libb.a換一下順序,就可以鏈接成功!
解決辦法
一般來說有兩種辦法,一種是仔細(xì)分析依賴關(guān)系,并按照正確的順序書寫庫文件的引用。原則是被依賴的盡量寫在右邊。但是在有些大型項(xiàng)目中,依賴關(guān)系可能并不容易梳理清楚。此時(shí)可以在命令行參數(shù)中重復(fù)對(duì)庫文件的引用:
- # gcc -o a.out main.o liba.la libb.la liba.a
在上面的命令中,liba.a重復(fù)書寫了兩次。
如果你使用automake,可以用xxx_LIBADD和xxx_LDADD來控制目標(biāo)文件的引用關(guān)系:
- xxx_LIBADD:對(duì)于目標(biāo)文件為庫文件或可執(zhí)行文件,需使用這個(gè)選項(xiàng)。表示在打包目標(biāo)庫文件的時(shí)候,就將依賴的文件一并打包進(jìn)來。
- xxx_LDADD:對(duì)于可執(zhí)行文件可用這個(gè)選項(xiàng),來控制鏈接器的參數(shù),如果你能分析清楚依賴關(guān)系,可以在這個(gè)選項(xiàng)中按照正確的順序書寫,從而成功連接。