原文地址:https://eli.thegreenplace.net/2013/07/09/library-order-in-static-linking
首先我們來(lái)看一個(gè)范例:
volatile char src[] = {1, 2, 3, 4, 5};
volatile char dst[50] = {0};
void* memcpy(void* dst, void* src, int len);
int main(int argc, const char* argv[])
{
memcpy(dst, src, sizeof(src));
return dst[4];
}
這段代碼會(huì)如我們所愿返回5。如果假設(shè)這段代碼是某個(gè)大型項(xiàng)目的一部分,二這個(gè)大型項(xiàng)目又包含了一些其他的庫(kù)昔汉,其中一個(gè)庫(kù)包含了下面的這樣一段代碼:
void memcpy(char* aa, char* bb, char* cc) {
int i;
for (i = 0; i < 100; ++i) {
cc[i] = aa[i] + bb[i];
}
}
如果之前的那段代碼與這個(gè)庫(kù)鏈接在一起窄瘟,將會(huì)發(fā)生什么呢?仍然返回5還是返回其他值堰怨,或者是閃退兔簇。答案是:看情況,有可能返回正確的值锹淌,或者是段錯(cuò)誤匿值。這取決于項(xiàng)目中的對(duì)象與庫(kù)在鏈接器中被處理的順序。如果你完全理解了為什么這需要取決于鏈接順序赂摆,以及如何避免這種問(wèn)題(有時(shí)候是一些更嚴(yán)峻的問(wèn)題挟憔,如循環(huán)引用),那么你就可以跳過(guò)這篇文章了烟号。
基礎(chǔ)知識(shí)
首先澄清一下绊谭,本文的所有示例都是使用Linux上的gcc和binutils toolchain,對(duì)于clang也同樣適用汪拥;本文僅僅針對(duì)編譯和鏈接時(shí)刻的靜態(tài)鏈接過(guò)程進(jìn)行討論达传。
為了理解為什么鏈接順序如此重要,首先就要知道鏈接器是如何工作的迫筑。第一個(gè)概念就是一個(gè)對(duì)象文件會(huì)導(dǎo)出兩張符號(hào)表宪赶,exported符號(hào)表(供其他對(duì)象和庫(kù)使用),imported符號(hào)表脯燃,表示引用了哪些其他的對(duì)象或庫(kù)搂妻。在C語(yǔ)言中,如:
int imported(int);
static int internal(int x) {
return x * 2;
}
int exported(int x) {
return imported(x) * internal(x);
}
這里特意使用兩種符號(hào)表的名字來(lái)命名函數(shù)名曲伊,編譯后可以看到如下的符號(hào)表:
$ gcc -c x.c
$ nm x.o
000000000000000e T exported
U imported
0000000000000000 t internal
這里exported是external符號(hào)叽讳,在對(duì)象文件中定義,對(duì)外部可見(jiàn)坟募;imported是未定義的符號(hào)岛蚤,也就是說(shuō)需要鏈接器從某些地方找到他們。在接下來(lái)我們討論的鏈接器工作流程中懈糯,“為定義”符號(hào)就是用來(lái)表示需要鏈接器從某處找到他們的符號(hào)涤妒。internal表示在對(duì)象哪定義但是外部不可見(jiàn)。
一個(gè)庫(kù)文件就是一堆對(duì)象文件的集合赚哗,創(chuàng)建一個(gè)庫(kù)文件的過(guò)程她紫,就是把一堆的對(duì)象文件放到一起,別無(wú)其他屿储。
鏈接過(guò)程
鏈接命令
gcc main.o -L/some/lib/dir -lfoo -lbar -lbaz
c或者c++的鏈接通常是通過(guò)編譯器gcc來(lái)驅(qū)動(dòng)的贿讹,因?yàn)間cc知道如何向鏈接器提供正確的命令行參數(shù),包括支持的庫(kù)等够掠。
命令行中的參數(shù)順序就是鏈接器鏈接的順序民褂,鏈接器會(huì)做如下的工作:
- 鏈接器維護(hù)一個(gè)符號(hào)表,符號(hào)表中主要維護(hù)了兩個(gè)列表
--目前為止所有對(duì)象和庫(kù)所提供的exported符號(hào)
--目前為止所有對(duì)象和庫(kù)需要用到的未定義符號(hào) - 當(dāng)鏈接器鏈接一個(gè)新的對(duì)象文件的時(shí)候
-- 該文件生成的exported符號(hào)被添加到上面提到的exported符號(hào)表中,如果未定義符號(hào)表中有相同的符號(hào)赊堪,那么從為定義符號(hào)表中刪除面殖,因?yàn)楝F(xiàn)在它已經(jīng)被找到了。如果在exported符號(hào)表中已經(jīng)存在該符號(hào)哭廉,那么會(huì)得到一個(gè)“重復(fù)定義”的錯(cuò)誤脊僚,不同的對(duì)象生導(dǎo)出了相同的符號(hào),鏈接器無(wú)法工作
--該文件需要的imported符號(hào)中遵绰,那些無(wú)法從現(xiàn)有的exported符號(hào)表中找到的符號(hào)辽幌,會(huì)被添加到未定義符號(hào)表中 - 當(dāng)鏈接器鏈接一個(gè)新的庫(kù)文件的時(shí)候,情況會(huì)有所不同椿访。鏈接器會(huì)遍歷庫(kù)中的所有文件舶衬,針對(duì)每一個(gè)文件執(zhí)行下面的動(dòng)作
-- 如果該文件的exported符號(hào)中的任何一個(gè)在未定義符號(hào)表中可以被找到,那么該對(duì)象被鏈接赎离,并執(zhí)行如下的步驟
-- 如果對(duì)象被鏈接逛犹,那么將會(huì)按照單個(gè)對(duì)象文件的流程,將它的未定義和exported符號(hào)添加到符號(hào)表中
-- 最后梁剔,如果庫(kù)中的任何一個(gè)文件被鏈接了虽画,那么將會(huì)重新掃描該庫(kù),因?yàn)閹?kù)中的某個(gè)文件所需要的未定義符號(hào)可能正好是庫(kù)中其他的文件所生成的exported 符號(hào)荣病,只不過(guò)第一次掃描的時(shí)候码撰,因?yàn)槲募樞虻膯?wèn)題,該對(duì)象被跳過(guò)了(因?yàn)樵谖炊x符號(hào)表中還未出現(xiàn)它)个盆,沒(méi)有被鏈接
當(dāng)所有的鏈接完成后脖岛,鏈接器會(huì)檢查符號(hào)表,如果在未定義表中還有未被鏈接的符號(hào)颊亮,那么鏈接器會(huì)拋出一個(gè)“未定義”錯(cuò)誤柴梆。例如,當(dāng)你創(chuàng)建了一惡搞可執(zhí)行程序终惑,但是卻忘了包含main方法绍在,那么你就會(huì)得到如下報(bào)錯(cuò):
/usr/lib/x86_64-linux-gnu/crt1.o: In function '_start':
(.text+0x20): undefined reference to 'main'
collect2: ld returned 1 exit status
這里需要注意的是,當(dāng)鏈接器對(duì)某個(gè)庫(kù)工作了之后雹有,就不會(huì)再管他了偿渡。就算它本可以導(dǎo)出被其他庫(kù)需要的符號(hào)。鏈接器重新掃描庫(kù)文件的情況就只有一種霸奕,就是上面提到的溜宽,庫(kù)中有文件被鏈接到程序中的時(shí)候,庫(kù)中的所有其他文件都會(huì)被重新掃描一遍质帅。當(dāng)然适揉, 向鏈接器傳入不同的flag參數(shù)可以修改默認(rèn)的流程合武,后面會(huì)再講。
另外需要注意的一點(diǎn)事涡扼,當(dāng)庫(kù)中的某個(gè)對(duì)象文件所導(dǎo)出的exported符號(hào)已經(jīng)在符號(hào)表的exported列表中存在的時(shí)候,這個(gè)文件是會(huì)被略過(guò)不被鏈接的盟庞。這是靜態(tài)鏈接中非常重要的一點(diǎn)吃沪。C庫(kù)就非常依賴這個(gè)特性,基本上都是以函數(shù)作為切分對(duì)象文件的單元什猖。因此票彪,如果你的代碼中只使用了strlen,那么libc.a中只有strlen.o會(huì)被鏈接不狮,你的可執(zhí)行單元會(huì)非常小降铸。
示例
首先來(lái)定義兩個(gè)對(duì)象
$ cat simplefunc.c
int func(int i) {
return i + 21;
}
$ cat simplemain.c
int func(int);
int main(int argc, const char* argv[])
{
return func(argc);
}
$ gcc -c simplefunc.c
$ gcc -c simplemain.c
$ gcc simplefunc.o simplemain.o
$ ./a.out ; echo $?
22
一起工作正常,因?yàn)檫@里都是對(duì)象文件摇零,因此鏈接的順序無(wú)關(guān)緊要推掸,對(duì)象總是會(huì)被鏈接到程序中。將他們調(diào)換順序驻仅,依然可以正常工作:
$ gcc simplemain.o simplefunc.o
$ ./a.out ; echo $?
22
現(xiàn)在我們將simplefunc編譯成一個(gè)靜態(tài)庫(kù)
$ ar r libsimplefunc.a simplefunc.o
$ ranlib libsimplefunc.a
$ gcc simplemain.o -L. -lsimplefunc
$ ./a.out ; echo $?
22
一切正常谅畅,但是如果此時(shí)我們將鏈接的順序調(diào)換一下:
$ gcc -L. -lsimplefunc simplemain.o
simplemain.o: In function 'main':
simplemain.c:(.text+0x15): undefined reference to 'func'
collect2: ld returned 1 exit status
通過(guò)上面的講解,這個(gè)問(wèn)題就很容易理解了噪服。當(dāng)鏈接器遇到libsimplefunc.a的時(shí)候毡泻,還沒(méi)有處理過(guò)simplemain.o,因此func從未出現(xiàn)在未定義符號(hào)表中粘优,當(dāng)鏈接器檢查靜態(tài)庫(kù)中的simplefunc.o的時(shí)候仇味,發(fā)現(xiàn)他的exported的符號(hào)未func,但是符號(hào)表中并不需要這個(gè)符號(hào)雹顺,因此這個(gè)對(duì)象文件就不會(huì)被鏈接到程序中丹墨。后面當(dāng)鏈接器搜索simplemain.o的時(shí)候,發(fā)現(xiàn)了需要func符號(hào)嬉愧,將它添加到未定義符號(hào)表中带到,此時(shí)鏈接器鏈接完所有的文件,發(fā)現(xiàn)仍存在未定義符號(hào)英染,于是報(bào)錯(cuò)揽惹。
在正常工作的順序下,simplemain.o掀背處理四康,func被添加到未定義符號(hào)表中搪搏,然后鏈接靜態(tài)庫(kù)的時(shí)候,發(fā)現(xiàn)simplefunc.o導(dǎo)出的func符號(hào)正好是在未定義符號(hào)表中闪金。
這里我們看到疯溺,在鏈接的過(guò)程中非常重要的一條準(zhǔn)則
- 如果庫(kù)a需要庫(kù)b中的符號(hào)论颅,那么在鏈接命令的參數(shù)中,a應(yīng)該出現(xiàn)在b之前
循環(huán)依賴
雖然上面的準(zhǔn)則非常簡(jiǎn)單囱嫩,但是在現(xiàn)實(shí)中恃疯,a與b相互依賴的情況還是非常常見(jiàn)的,那么此時(shí)應(yīng)該怎么辦呢墨闲?是否可以在參數(shù)列表中讓a同時(shí)出現(xiàn)的b的前面和后面呢今妄?
來(lái)看如下的兩個(gè)文件:
$ cat func_dep.c
int bar(int);
int func(int i) {
return bar(i + 1);
}
$ cat bar_dep.c
int func(int);
int bar(int i) {
if (i > 3)
return i;
else
return func(i);
}
這兩個(gè)文件相互依賴,如果按照如下的順序鏈接鸳碧,會(huì)報(bào)錯(cuò):
$ gcc simplemain.o -L. -lbar_dep -lfunc_dep
./libfunc_dep.a(func_dep.o): In function 'func':
func_dep.c:(.text+0x14): undefined reference to 'bar'
collect2: ld returned 1 exit status
如果反過(guò)來(lái)就ok:
$ gcc simplemain.o -L. -lfunc_dep -lbar_dep
$ ./a.out ; echo $?
4
按照上面的流程盾鳞,這解釋得通。然后將這個(gè)例子再?gòu)?fù)雜一點(diǎn):
$ cat bar_dep.c
int func(int);
int frodo(int);
int bar(int i) {
if (i > 3)
return frodo(i);
else
return func(i);
}
$ cat frodo_dep.c
int frodo(int i) {
return 6 * i;
}
然后重新編譯這些文件瞻离,并創(chuàng)建libfunc_dep.a庫(kù):
$ ar r libfunc_dep.a func_dep.o frodo_dep.o
$ ranlib libfunc_dep.a
這個(gè)時(shí)候的依賴關(guān)系如下:
這種情況下腾仅,不管怎么樣的順序,都是會(huì)報(bào)錯(cuò)的
$ gcc -L. simplemain.o -lfunc_dep -lbar_dep
./libbar_dep.a(bar_dep.o): In function 'bar':
bar_dep.c:(.text+0x17): undefined reference to 'frodo'
collect2: ld returned 1 exit status
$ gcc -L. simplemain.o -lbar_dep -lfunc_dep
./libfunc_dep.a(func_dep.o): In function 'func':
func_dep.c:(.text+0x14): undefined reference to 'bar'
collect2: ld returned 1 exit status
這個(gè)時(shí)候可以在參數(shù)列表中重復(fù)提供參數(shù)套利,來(lái)保證所有的符號(hào)被找到:
$ gcc -L. simplemain.o -lfunc_dep -lbar_dep -lfunc_dep
$ ./a.out ; echo $?
24
通過(guò)flag控制鏈接過(guò)程
之前提到過(guò)可以通過(guò)flag參數(shù)來(lái)控制鏈接的過(guò)程推励。例如針對(duì)互相依賴的情況,可以通過(guò)--start-group和--end-group參數(shù)(man ld中對(duì)這兩個(gè)參數(shù)的解釋):
--start-group archives --end-group
The specified archives are searched repeatedly until no new undefined references are created. Normally, an archive is searched only once in the order that it is specified on the command line. If a symbol in that archive is needed to resolve an undefined symbol referred to by an object in an archive that appears later on the command line, the linker would not be able to resolve that reference. By grouping the archives, they all be searched repeatedly until all possible references are resolved.
Using this option has a significant performance cost. It is best to use it only when there are unavoidable circular references between two or more archives.
針對(duì)上面的例子:
$ gcc simplemain.o -L. -Wl,--start-group -lbar_dep -lfunc_dep -Wl,--end-group
$ ./a.out ; echo $?
24
注意到上面的"significant performance cost"警告肉迫,這也是為什么默認(rèn)流程不支持互相引用的原因吹艇。如果讓鏈接器反復(fù)重新掃描庫(kù)文件,直到不會(huì)導(dǎo)出新的exported符號(hào)為止昂拂,這回非常影響效率受神。因?yàn)殒溄邮蔷幾g程序中非常重要的一個(gè)環(huán)節(jié),他針對(duì)整個(gè)程序同時(shí)還非常消耗內(nèi)存格侯,因此最好是能夠在大部分情況以最高效的方式完成鼻听,針對(duì)特殊情況使用參數(shù)的形式來(lái)處理。
針對(duì)循環(huán)引用的情況联四,還可以通過(guò)--undefined標(biāo)記來(lái)告訴鏈接器撑碴,我想要將它添加到未定義列表中,這樣可以在只提供一次庫(kù)參數(shù)的情況下朝墩,完成鏈接:
$ gcc simplemain.o -L. -Wl,--undefined=bar -lbar_dep -lfunc_dep
$ ./a.out ; echo $?
24
回到開(kāi)始的例子
回到文章開(kāi)始的例子醉拓,假設(shè)我們?cè)诹硗獾膸?kù)libstray_memcpy.a中定義了memcpy方法,同時(shí)被鏈接到程序中
$ gcc -L. main_using_memcpy.o -lstray_memcpy
$ ./a.out
Segmentation fault (core dumped)
出錯(cuò)是因?yàn)?lstray_memcpy在main_using_memcpy.o之后收苏,他被連接到了程序中亿卤,但是如果我們將順序反轉(zhuǎn)一下:
$ gcc -L. -lstray_memcpy main_using_memcpy.o
$ ./a.out ; echo $?
5
程序運(yùn)行正常,因?yàn)殡m然我們沒(méi)有顯示地要求鹿霸,但是gcc還會(huì)讓鏈接器去鏈接C庫(kù)排吴。gcc完整的鏈接觸發(fā)命令是非常復(fù)雜的,可以通過(guò)傳入-###參數(shù)來(lái)查看懦鼠,但是在這種情況下基本上類似于:
$ gcc -L. -lstray_memcpy main_using_memcpy.o -lc
當(dāng)鏈接器遇到-lstray_memcpy的時(shí)候钻哩,因?yàn)榇藭r(shí)的未定義符號(hào)表中尚未出現(xiàn)memcpy屹堰,因此自定義的對(duì)象文件不會(huì)被鏈接到程序中,直到處理main_using_memcpy.o的時(shí)候街氢,才會(huì)發(fā)現(xiàn)自己需要memcpy符號(hào)扯键,然后在處理-lc的時(shí)候,標(biāo)準(zhǔn)庫(kù)中可以導(dǎo)出memcpy符號(hào)的文件會(huì)被鏈接到程序中珊肃,因此此時(shí)memcpy在未定義符號(hào)表中荣刑。
總結(jié)
鏈接器處理對(duì)象文件和庫(kù)文件的方式非常簡(jiǎn)單,只要理解了近范,那么鏈接中的很多錯(cuò)誤都很好理解。如果還是遇到了無(wú)法理解的錯(cuò)誤延蟹,那么文中提到的兩個(gè)命令可以幫助你調(diào)試問(wèn)題:nm(查看整個(gè)對(duì)象文件或庫(kù)的符號(hào)表)评矩;gcc的-### flag參數(shù),可以完整的展示出傳遞給底層的參數(shù)