掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
鏈接(linking)是將各種代碼和數(shù)據(jù)片段收集并組合成為一個單一文件的過程,這個文件可被加載(復(fù)制)到內(nèi)存并執(zhí)行。

鏈接可以執(zhí)行與編譯時(complie time),也就是源代碼被翻譯成機(jī)器代碼時;也可以執(zhí)行于加載時(load time),也就是在程序被加載器(load-er)加載到內(nèi)存并執(zhí)行時;甚至可以執(zhí)行在運(yùn)行時(run time),也就是由應(yīng)用程序來執(zhí)行。在早期的計算機(jī)系統(tǒng)中,鏈接是手動執(zhí)行的。在現(xiàn)代系統(tǒng)中,鏈接是由叫做連接器(linker)的程序自動執(zhí)行的。
鏈接器使分離編譯成為可能,我們不用將一個大型的應(yīng)用程序組織為一個巨大的源文件,而是可以把它分解為更小、更好管理的模塊,可以獨立地修改和編譯這些模塊。當(dāng)我們改變這些模塊中的一個時,只需簡單地重新編譯它,并重新鏈接應(yīng)用,而不必重新編譯其它文件。
下面的討論基于這樣的環(huán)境:一個運(yùn)行Linux的x86-64系統(tǒng),使用標(biāo)準(zhǔn)的ELF-64目標(biāo)文件格式。
下面的C語言示例程序,由兩個源文件組成,main.c和sum.c。main函數(shù)初始化一個整數(shù)數(shù)組,然后調(diào)用sum函數(shù)來對數(shù)組元素求和。
// sum.c
int sum(int *a, int n) {
int s = 0;
for (int i = 0; i < n; i++) {
s += a[i];
}
return s;
}
// main.c
int array[2] = {1, 2};
int main() {
int val = sum(array, 2);
return val;
}
大多數(shù)的編譯系統(tǒng)會提供編譯器驅(qū)動程序(compile driver),包含語言預(yù)處理器、編譯器、匯編器和鏈接器。首先編譯器驅(qū)動程序會對main.c與sum.c文件的源代碼進(jìn)行翻譯,翻譯過程如下:
其中,main.o稱為可重定位目標(biāo)文件。
之后,編譯系統(tǒng)會運(yùn)行鏈接器ld,將main.o和sum.o以及一些必要的系統(tǒng)目標(biāo)文件組合起來,創(chuàng)建一個可以執(zhí)行目標(biāo)文件,這個過程是靜態(tài)鏈接,過程如下:
再之后,操作系統(tǒng)會調(diào)用加載器(loader),將可執(zhí)行文件prog中的代碼和數(shù)據(jù)復(fù)制到內(nèi)存中,然后執(zhí)行。
靜態(tài)鏈接器(static linker)以一組可重定位目標(biāo)文件作為輸入,生成一個完全鏈接的、可以加載和運(yùn)行的可執(zhí)行目標(biāo)文件。輸入的可重定位目標(biāo)文件由各種不同的代碼和數(shù)據(jù)節(jié)(section)組成,每一節(jié)都是一個連續(xù)的字節(jié)序列。指令在一節(jié)中,初始化了的全局變量在另一個節(jié)中,而未初始化的變量又在另外一節(jié)中。
為了構(gòu)造可執(zhí)行文件,鏈接器必須完成兩個重要的任務(wù):
目標(biāo)文件純粹是字節(jié)塊的集合,這些塊中,有些包含程序代碼,有些包含數(shù)據(jù),而有些則是引導(dǎo)鏈接器和加載器的數(shù)據(jù)結(jié)構(gòu)。鏈接器將這些塊連接起來,確定被連接塊的運(yùn)行時位置,并且修改代碼和數(shù)據(jù)塊中的各種位置。
目標(biāo)文件有三種形式:
目標(biāo)文件的生成方式:
目標(biāo)文件的格式:
下上展示了一個典型的ELF可重定位目標(biāo)文件的格式。ELF頭包含很多信息,包括生成該文件的系統(tǒng)的字節(jié)大小,字節(jié)順序,ELF頭的大小,目標(biāo)文件的類型,機(jī)器類型等等。節(jié)頭部表描述了不同節(jié)的位置和大小。
加載ELF頭和節(jié)頭部表的是節(jié):
每個可重定位目標(biāo)模塊(目標(biāo)文件)m都有一個符號表,它包含m定義和引用的符號的信息。在鏈接器的上下文中,有三種不同的符號:
.symtab中的符號表不包含非靜態(tài)程序變量的任何符號,這些程序變量符號在棧中被管理,鏈接器對此類符號不感興趣。
鏈接器的輸入是一組可重定位目標(biāo)模塊。每個模塊定義一組符號,有些是局部的(只對定義該符號的模塊可見),有些是全局的(對其他模塊也可見)。如果多個模塊定義同名的全局符號,會發(fā)生什么呢?下面是 Linux編譯系統(tǒng)采用的方法。
在編譯時,編譯器向匯編器輸出每個全局符號,或者是強(qiáng)( strong)或者是弱(weak),而匯編器把這個信息隱含地編碼在可重定位目標(biāo)文件的符號表里。函數(shù)和已初始化的全局變量是強(qiáng)符號,未初始化的全局變量是弱符號。
根據(jù)強(qiáng)弱符號的定義,Linux鏈接器使用下面的規(guī)則來處理多重定義的符號名
迄今為止,我們都是假設(shè)鏈接器讀取一組可重定位目標(biāo)文件,并把它們鏈接起來,輸出一個可執(zhí)行目標(biāo)文件。實際上,所有的編譯系統(tǒng)都提供一種機(jī)制,將所有相關(guān)的目標(biāo)模塊打包成一個單獨的文件,稱為靜態(tài)庫。靜態(tài)庫可以用做鏈接器的輸入,當(dāng)鏈接器構(gòu)造一個輸出的可執(zhí)行目標(biāo)文件時,它只復(fù)制靜態(tài)庫里被應(yīng)用程序引用的目標(biāo)模塊,這就減少了可執(zhí)行文件在磁盤和內(nèi)存中的大小。在Linux系統(tǒng)中,靜態(tài)庫由后綴.a標(biāo)識。
一旦鏈接器完成了符號解析這一步,就把代碼中的每個符號引用和正好一個符號定義(即它的一個輸入目標(biāo)模塊中的一個符號表條目)關(guān)聯(lián)起來。此時,鏈接器就知道它的輸入目標(biāo)模塊中的代碼節(jié)和數(shù)據(jù)節(jié)的確切大小?,F(xiàn)在就可以開始重定位步驟了,在這個步驟中,將合并輸入模塊,并為每個符號分配運(yùn)行時地址。重定位由兩步組成:
當(dāng)匯編器生成一個目標(biāo)模塊時,它并不知道數(shù)據(jù)和代碼最終將放在內(nèi)存中的什么位置,它也并不知道這個模塊引用的任何外部定義的函數(shù)或者全局變量的位置。所以,無論何時匯編器遇到對最終位置的目標(biāo)引用,它就會生成一個重定位條目,告訴鏈接器在將目標(biāo)文件合并成可執(zhí)行目標(biāo)文件時如何修改這個引用。
見《深入理解計算機(jī)系統(tǒng)》
靜態(tài)庫由一些缺點:靜態(tài)庫需要定期維護(hù)和更新;每個程序都會使用一些通用的標(biāo)準(zhǔn)函數(shù),在運(yùn)行時,這些函數(shù)的代碼會被復(fù)制到每個運(yùn)行進(jìn)程的文本段中,在一個運(yùn)行上百個進(jìn)行的典型系統(tǒng)上,這是對內(nèi)存資源的浪費。
共享庫(shared library)是致力于解決靜態(tài)庫缺陷的一個現(xiàn)代創(chuàng)新產(chǎn)物。共享庫是一個目標(biāo)模塊,在運(yùn)行或加載時,可以加載到任意內(nèi)存地址,并和一個在內(nèi)存中的程序鏈接起來。這個過程稱為動態(tài)鏈接,是由一個叫做動態(tài)鏈接器(dynamic linker)的程序來執(zhí)行的。在Linux系統(tǒng)中,共享庫通常由.so后綴標(biāo)識。
共享庫以兩種不同的方式來共享的。首先,在任何給定的文件系統(tǒng)中,對于一個庫只有一個.so文件。所有引用該哭的可執(zhí)行目標(biāo)文件共享這個.so文件中的代碼和數(shù)據(jù),而不是像靜態(tài)庫的內(nèi)容那樣被復(fù)制和嵌入到引用它們的可執(zhí)行文件中。其次,在內(nèi)存中,一個共享庫的.text節(jié)的一個副本可以被不同的正在運(yùn)行的進(jìn)程共享。

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流