鏈接基本概念
鏈接就是將各種代碼和數(shù)據(jù)片段收集并整合成一個單一的文件的過程。這個過程可以在編譯時完成妆兑,也可以在文件加載到內(nèi)存時由加載器完成力试,甚至也可以在程序運行時完成。
以下面的兩段代碼為例:
/* /link/main.c */
int sum(int *a, int n);
int array[2] = {1, 2};
int main()
{
int val = sum(array, 2);
return val;
}
/* /link/sum.c */
int sum(int *a, int n)
{
int i, s = 0;
for (i = 0; i < n; i++) {
s += a[i];
}
return s;
}
這是一個簡單求數(shù)組元素之和的程序贝室,但代碼被分在兩個源文件中契讲,當(dāng)執(zhí)行gcc -Og -o prog main.c sum.c
時,gcc提供的編譯器驅(qū)動程序會依次調(diào)用預(yù)處理器滑频、編譯器捡偏、匯編器、鏈接器误趴,并最終形成一個可執(zhí)行文件prog霹琼。當(dāng)我們在Shell里輸入./prog
時,Shell會調(diào)用操作系統(tǒng)中一個叫加載器的函數(shù)凉当,將prog
加載到內(nèi)存中枣申。
可以看出,源文件要變成一個可執(zhí)行文件看杭,要經(jīng)歷4個過程忠藤,其中,每個源文件都會獨自經(jīng)過前3個過程楼雹,而最后一步鏈接器則是將每個源文件生成的.o
文件以及一些必要的系統(tǒng)目標(biāo)文件組合起來模孩,形成一個可執(zhí)行文件prog
。
靜態(tài)鏈接
關(guān)于靜態(tài)鏈接贮缅,這里就截取csapp里的幾段話榨咐,其總結(jié)的很好:
目標(biāo)文件
目標(biāo)文件分為三種:
編譯器和匯編器生成可重定位目標(biāo)文件和共享目標(biāo)文件,鏈接器生成可執(zhí)行目標(biāo)文件谴供。
不同操作系統(tǒng)上块茁,目標(biāo)文件有不同的格式,Windows使用PE(Portable Executable)格式,MacOS-X使用Mach-O格式数焊,Linux和Unix則使用ELF(Executable and Linkable Format)格式永淌。
可重定位目標(biāo)文件
ELF頭以一個16字節(jié)的序列開始,這個序列描述了生成該文件的系統(tǒng)的字大小和字節(jié)順序佩耳。ELF頭剩余部分包含了幫助鏈接器語法分析和解析目標(biāo)文件的信息遂蛀,包括ELF頭的大小、目標(biāo)文件的類型(可重定位干厚、可執(zhí)行或者共享的)李滴、機器類型(如x86-64)、節(jié)頭部表的文件偏移萍诱,以及節(jié)頭部表中條目的大小和數(shù)量悬嗓。不同節(jié)的位置和大小是由節(jié)頭部表描述的,其中每個節(jié)在節(jié)頭部表中都有一個固定大小的條目裕坊。
- .text
已編譯程序的機器代碼 - .rodata
只讀數(shù)據(jù) - .data
已初始化的全局和靜態(tài)C變量包竹。 - .bss
未初始化的全局和靜態(tài)C變量,以及初始化為0的全局籍凝、靜態(tài)C變量周瞎。 - .symtab
程序里定義和引用的函數(shù)和全局變量的信息。 - .rel.text
代碼的重定位條目 - .rel.data
初始化了的數(shù)據(jù)重定位條目 - .strtab
主要存儲.systab和.debug里符號的名稱饵蒂。
符號和符號表
每一個可重定位目標(biāo)模塊m都有一個符號表(.symtab)声诸,它包含m定義和引用的符號信息。有3種不同的符號:
- 由模塊m定義并能被其它模塊引用的全局符號(全局鏈接器符號)退盯,對應(yīng)于非靜態(tài)的C函數(shù)和全局變量彼乌。
- 只能被模塊m定義和引用的局部符號,對應(yīng)于帶static屬性的C函數(shù)和全局變量渊迁。
- 由其它模塊定義并被模塊m引用的全局符號慰照,這些符號稱為外部符號,對應(yīng)于在其它模塊中定義的非靜態(tài)C函數(shù)和全局變量琉朽。
需要著重注意的一點是毒租,.symtab中不包含非靜態(tài)的過程變量,這些變量是由運行時的棧來管理的箱叁,鏈接器對它們不感興趣墅垮。而對于帶有static修飾的過程變量,可以看下面的截圖:
符號表是由匯編器構(gòu)造而成的耕漱,使用由編譯器輸出到.s文件里的符號算色。符號表的內(nèi)容主要是一個由條目組成的數(shù)組,每個條目的結(jié)構(gòu)如下:
- name字段是.strtab中的字節(jié)偏移
- value字段是符號的地址螟够;對于可重定位模塊剃允,value表示的是在對應(yīng)節(jié)里的字節(jié)偏移(可用于符號解析);對于可執(zhí)行目標(biāo)文件,value表示的是絕對的運行地址(可用于重定位)斥废。
- type字段一般用于區(qū)別數(shù)據(jù)或函數(shù)。
- size字段表示的是目標(biāo)的大小给郊。
- binding字段表示符號是本地的還是全局的牡肉。
- section字段是在節(jié)頭部表中的索引,每個符號都被分配到目標(biāo)文件中的某個節(jié)淆九。
可重定位目標(biāo)文件有3個偽節(jié)(僅可重定位目標(biāo)文件有)统锤,它們在節(jié)頭部表中沒有對應(yīng)的條目:ABS用來記錄那些不應(yīng)該被重定位的符號;UNDEF用來記錄那些本模塊引用炭庙,但在別的模塊定義的符號饲窿;COMMON用來記錄那些還未重定位的未初始化的數(shù)據(jù)符號,同時焕蹄,對于COMMON符號逾雄,value表示的是它的對齊要求,size表示它的最小大小腻脏。
符號解析
鏈接器通過將每個引用與其輸入可重定位目標(biāo)文件的符號表中的符號定義精確地關(guān)聯(lián)來解析符號引用鸦泳。對于定義和引用都在同一個模塊的本地符號,解析十分簡單永品,因為每一個模塊的本地符號做鹰,編譯器只允許有一個定義。
但對于全局符號的解析就比較麻煩鼎姐,因為多個可重定位目標(biāo)文件可能會定義相同名字的全局符號钾麸。在編譯時,編譯器向匯編器輸出的每個全局符號炕桨,或者是強或者是弱饭尝,而匯編器把這個信息隱含地編碼在可重定位目標(biāo)文件的符號表里。函數(shù)和已初始化的全局變量是強符號谋作,未初始化的全局變量是弱符號困介。
而Linux鏈接器則使用下面的規(guī)則來處理多重定義的符號:
這種特性有時候會造成一些奇怪的錯誤,比如下列代碼:
/* foo5.c */
#include <stdio.h>
void f(void);
int x = 15213;
int y = 15212;
int main()
{
f();
printf("x = 0x%x y = 0x%x \n",
x, y);
return 0;
}
/* bar5.c */
double x;
void f()
{
x = -0.0;
}
根據(jù)規(guī)則2宛逗,鏈接器會選擇foo5.c里的x寞钥,但在bar5.c里的函數(shù)f將x作為double型進行了賦值。因此函數(shù)f會將foo5.c里定義的x和y都覆蓋掉(4+4=8)吭净。在多人協(xié)作的情況下睡汹,這種錯誤不容易發(fā)現(xiàn)。
附:
與靜態(tài)庫鏈接
靜態(tài)庫:相關(guān)的函數(shù)編譯成單獨的可重定位目標(biāo)文件寂殉,然后存入到一個靜態(tài)庫文件中囚巴,這個靜態(tài)庫文件其實就是一個archive包,里面有若干個可重定位目標(biāo)文件。在鏈接時彤叉,鏈接器只會取出靜態(tài)庫里被引用的目標(biāo)模塊庶柿。
創(chuàng)建靜態(tài)庫:
linux>gcc -c addvec.c multvec.c
linux>ar rcs libvector.a addvec.o multvec.o
linux>gcc -c main2.c
linux>gcc -static -o prog2c main2.o ./libvector.a
鏈接器解析過程
在符號解析階段,鏈接器是從左往右逐個掃描可重定位目標(biāo)文件和靜態(tài)庫文件秽浇,這些文件的順序是由出現(xiàn)在編譯器驅(qū)動程序的命令行參數(shù)的順序決定的浮庐。
在這次掃描中,鏈接器會維護一個可重定位目標(biāo)文件的集合E(這個集合中的文件會被合并起來形成可執(zhí)行文件)柬焕,一個未解析的符號(即引用了但是尚未找到定義的符號)集合U审残,以及一個在前面輸入文件中已定義的符號集合D。初始時斑举,E搅轿、U和D均為空。
- 對于命令行上的每個輸入文件f富玷,鏈接器會判斷f是一個目標(biāo)文件還是一個存檔文件璧坟。如果f是一個目標(biāo)文件,那么鏈接器把f添加到E凌彬,修改U和D來反映f中的符號定義和引用沸柔,并繼續(xù)下一個輸入文件。
- 如果f是一個存檔文件铲敛,那么鏈接器就嘗試匹配U中未解析的符號和由存檔文件成員定義的符號褐澎。如果某個存檔文件成員m,定義了一個符號來解析U中的一個引用伐蒋,那么就將m加到E中工三,并且鏈接器修改U和D來反映m中的符號定義和引用。對存檔文件中的所有成員目標(biāo)文件都依次進行這個過程先鱼,直到U和D都不再發(fā)生變化俭正。此時,任何不包含在E中的成員目標(biāo)文件都簡單的丟棄掉焙畔,而鏈接器將繼續(xù)處理下一個輸入文件掸读。
- 如果當(dāng)鏈接器完成對命令行上輸入文件的掃描后,U是非空的宏多,那么鏈接器就會輸出一個錯誤并終止儿惫。否則,它會合并和重定位E中的目標(biāo)文件伸但,構(gòu)成可執(zhí)行文件肾请。
需要注意的是,命令行上庫和目標(biāo)文件的順序非常重要更胖,如果定義一個符號的庫出現(xiàn)在引用這個符號的目標(biāo)文件的前面铛铁,那么引用就不能被解析隔显,鏈接會失敗。關(guān)于庫的一般準(zhǔn)則是將它們放在命令行的結(jié)尾饵逐。
重定位
一旦鏈接器完成了符號解析括眠,就把代碼中的每個符號引用和符號定義(即輸入目標(biāo)模塊中符號表里的條目)關(guān)聯(lián)起來。現(xiàn)在就可以開始重定位步驟了倍权,由兩步組成:
- 鏈接器將所有相同類型的節(jié)合并為同一類型的聚合節(jié)哺窄。然后鏈接器將運行時內(nèi)存地址賦給新的聚合節(jié),以及每個符號账锹。當(dāng)這一步完成時,程序中的每條指令和全局變量都有唯一的運行時內(nèi)存地址了坷襟。
- 鏈接器修改代碼節(jié)和數(shù)據(jù)節(jié)中對每個符號的引用奸柬,使得它們指向正確的運行時地址。在這一步婴程,鏈接器依賴于由匯編器生成的重定位條目廓奕,再節(jié)合已確定運行位置的節(jié)和符號,就可以修改代碼里對符號的引用档叔。
在還沒有進行重定位前挑格,每個可重定位目標(biāo)文件里對符號的引用都用0作為占位符表示:
重定位后:
可以看出,重定位后沾歪,代碼里符號引用處的字節(jié)被改變了漂彤。在加載的時候,加載器會把這些節(jié)中的字節(jié)直接復(fù)制到內(nèi)存灾搏,不再進行任何修改地執(zhí)行這些指令挫望。
可執(zhí)行目標(biāo)文件
可執(zhí)行文件連續(xù)的片于內(nèi)存連續(xù)段之間的映射關(guān)系由段頭部表描述:
從上圖可以看出,內(nèi)存將被可執(zhí)行目標(biāo)文件的內(nèi)容初始化為兩個內(nèi)存段确镊。第1行和第2行告訴我們第一個內(nèi)存段(代碼段)具有讀/執(zhí)行權(quán)限士骤,開始于內(nèi)存地址0x400000處,總內(nèi)存大小為0x69c字節(jié)蕾域,并且被初始化為可執(zhí)行目標(biāo)文件的頭0x69c字節(jié)拷肌,其中包括ELF頭到旦、段頭部表、.init巨缘、.text和.rodata節(jié)添忘。
加載可執(zhí)行目標(biāo)文件
要運行可執(zhí)行目標(biāo)文件prog仲器,直接在Linux shell的命令行中跳轉(zhuǎn)到對應(yīng)目錄然后輸入./prog
即可。shell會
調(diào)用一個稱為加載器的操作系統(tǒng)代碼來運行程序仰冠,另外一提的是乏冀,任何Linux程序都可以通過調(diào)用execve函數(shù)來調(diào)用加載器。加載器將可執(zhí)行目標(biāo)文件里的代碼和數(shù)據(jù)從磁盤加載到內(nèi)存洋只,然后跳轉(zhuǎn)到程序的第一條指令或入口點來運行該程序辆沦。這個將程序復(fù)制到內(nèi)存并運行的過程稱為加載。
每個Linux程序都有一個運行時內(nèi)存映像识虚,如下圖所示:
加載器在運行時肢扯,它創(chuàng)建類似上圖的內(nèi)存映像。在段頭部表的引導(dǎo)下担锤,加載器將可執(zhí)行文件的片復(fù)制到代碼段和數(shù)據(jù)段蔚晨,接下來,加載器跳轉(zhuǎn)到程序的入口點妻献,也就是_start函數(shù)的地址蛛株。這個函數(shù)是在系統(tǒng)目標(biāo)文件ctrl.o中定義的,對所有的C程序都是一樣的育拨。_start函數(shù)調(diào)用系統(tǒng)啟動函數(shù)__libc_start_main谨履,該函數(shù)定義在libc.so中。它初始化執(zhí)行環(huán)境熬丧,調(diào)用用戶層的main函數(shù)笋粟,處理main函數(shù)的返回值,并且在需要的時候把控制返回給內(nèi)核析蝴。
動態(tài)鏈接共享庫
靜態(tài)鏈接庫的兩個缺點:
- 當(dāng)靜態(tài)庫更新后害捕,我們總需要重新進行鏈接形成新的可執(zhí)行目標(biāo)文件。
-
大多數(shù)程序都會使用到相同的庫函數(shù)闷畸,在運行時尝盼,這些庫函數(shù)的代碼都會復(fù)制到每個進行的代碼段中去,這對內(nèi)存是一種浪費佑菩。
共享庫:
共享庫.PNG
共享庫的兩種共享:共享庫的共享方式.PNG
動態(tài)鏈接過程:動態(tài)鏈接過程.PNG
創(chuàng)建動態(tài)鏈接庫命令.PNG
當(dāng)加載器加載和運行部分鏈接的可執(zhí)行文件prog21時盾沫,它注意到prog21包含一個.interp節(jié)裁赠,這個節(jié)包含動態(tài)鏈接器的路徑名,動態(tài)鏈接器本身就是一個共享目標(biāo)(如在Linux系統(tǒng)上的ld-linux.so)赴精。加載器不會像它通常所做的那樣將控制傳遞給應(yīng)用佩捞,而是加載和運行這個動態(tài)鏈接器。然后蕾哟,動態(tài)鏈接器通過執(zhí)行下面的重定位完成鏈接任務(wù):
- 重定位libc.so的文本和數(shù)據(jù)到某對應(yīng)內(nèi)存段一忱。
- 重定位libvector.so的文本和數(shù)據(jù)到另外的對應(yīng)內(nèi)存段。
- 重定位prog21中所有由libc.so和libvector.so定義的符號和引用谭确。
最后帘营,動態(tài)鏈接器將控制傳遞給應(yīng)用程序。從這個時刻開始逐哈,共享庫的位置就固定了仪吧,并且在程序執(zhí)行的過程中都不會改變。
從應(yīng)用程序中加載和鏈接共享庫
前面討論的是應(yīng)用程序在加載后執(zhí)行前時鞠眉,動態(tài)鏈接器加載和鏈接動態(tài)庫的情景。然后择诈,應(yīng)用程序還可以在它運行時要求動態(tài)鏈接器加載和鏈接某個動態(tài)庫械蹋。
Linux系統(tǒng)提供了一個簡單的接口,允許應(yīng)用程序在運行時加載和鏈接共享庫:
#include <dlfcn.h>
void *dlopen(const char *filename, int flag);//返回:若成功則為指向句柄的指針羞芍,若出錯則為NULL哗戈。
void *dlsym(void *handle, char *symbol);//返回:若成功則為指向符號的指針,若出錯則為NULL荷科。
int dlclose(void *handle);//返回:若成功則為0唯咬,若出錯則為-1。
const char *dlerror(void);//返回:如果前面對dlopen畏浆、dlsym或dlclose的調(diào)用失敗胆胰,則為錯誤消息,如果前面的調(diào)用成功刻获,則為NULL蜀涨。
dlopen函數(shù)加載和鏈接共享庫filename。filename里面的外部符號則通過在它前面以RTLD_GLOBAL標(biāo)志打開的共享庫來解析蝎毡。如果可執(zhí)行文件是帶-rdynamic標(biāo)志編譯的厚柳,那么它的全局符號也是可以被后面的共享模塊用于符號解析的。flag參數(shù)必須要么包括RTLD_NOW,該標(biāo)志告訴鏈接器立即解析共享庫里對外部符號的引用沐兵,要么包括RTLD_LAZY標(biāo)志别垮,該標(biāo)志指示鏈接器推遲符號解析直到執(zhí)行來自庫中的代碼。
dlsym函數(shù)的輸入是一個指向前面已經(jīng)打開了的共享庫句柄和一個symbol名字扎谎,如果該符號存在碳想,就返回符號的地址烧董,否則返回NULL。
如果沒有其它模塊還在使用這個共享庫移袍,dlclose函數(shù)就卸載該共享庫解藻。
dlerror函數(shù)返回一個字符串,它描述的是調(diào)用dlopen葡盗、dlsym螟左、或者dlclose函數(shù)時發(fā)生的最近錯誤,如果沒有錯誤觅够,則返回NULL胶背。
linux>gcc -rdynamic -o prog2r dll.c -ldl
------------------------------------code/link/dll.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
void *handle;
void (*addvec)(int *, int *, int *, int);
char *error;
handle = dlopen("./libvector.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
addvec = dlsym(handle, "addvec");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
if (dlclose(handle) < 0) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
}
位置無關(guān)代碼
PIC數(shù)據(jù)引用:
PIC函數(shù)調(diào)用: