使用鏈接器的好處
- 模塊化缩歪。程序可以分解為更小的模塊,并且可以把公共的函數(shù)做成一個(gè)庫(kù)谍憔。
- 提高效率匪蝙。可以分離編譯习贫,修改了一個(gè)源文件逛球,只需要重新編譯它,而不用編譯其他文件苫昌。并且可以使用動(dòng)態(tài)庫(kù)颤绕,這樣,公共的函數(shù)在內(nèi)存中只有一份祟身,但可以在多個(gè)程序中引用奥务。
為了構(gòu)造可執(zhí)行文件,鏈接器必須完成兩個(gè)主要任務(wù)袜硫,符號(hào)解析和重定位氯葬。
符號(hào)解析
在鏈接器的上下文中,有三種不同的符號(hào):
- 全局符號(hào)婉陷。非靜態(tài)的C函數(shù)和全局變量帚称。
- 外部符號(hào)。由其他模塊定義并被模塊m引用的全局符號(hào)秽澳。
- 局部符號(hào)闯睹。帶static屬性的C函數(shù)和全局變量。
對(duì)于在運(yùn)行時(shí)在棧上管理的符號(hào)担神,鏈接器不感興趣楼吃。其實(shí)從匯編代碼中我們也可以知道,棧上的對(duì)象已經(jīng)在編譯的時(shí)候確定了。
可以想象所刀,對(duì)于局部符號(hào)的解析還是比較容易的衙荐,因?yàn)榫驮谝粋€(gè)模塊內(nèi)引用,還有值得注意的一點(diǎn)是浮创,如果棧上定義了同名的局部變量忧吟,會(huì)覆蓋全局的變量。下面是一個(gè)例子斩披。
#include <stdio.h>
//extern int a; error
static int a = 10;
extern int a; //ok
int main(){
int a = 20;//if remove, a = 10
printf("a = %d\n",a); // a = 20
return 0;
}
編譯器把全局符號(hào)分為強(qiáng)符號(hào)和若符號(hào)溜族。函數(shù)和已初始化的全局變量就是強(qiáng)符號(hào),未初始化的全局變量是弱符號(hào)垦沉。鏈接器使用下面的規(guī)則來(lái)處理多重定義的符號(hào)名煌抒。
- 規(guī)則1:不允許有多個(gè)同名的強(qiáng)符號(hào)。
- 規(guī)則2:如果有一個(gè)強(qiáng)符號(hào)和多個(gè)弱符號(hào)同名厕倍,那么選擇強(qiáng)符號(hào)寡壮。
- 規(guī)則3:如果有多個(gè)弱符號(hào)同名,那么從這些弱符號(hào)中任意選擇一個(gè)讹弯。
規(guī)則2和規(guī)則3的應(yīng)用會(huì)造成一些不易察覺(jué)的運(yùn)行時(shí)錯(cuò)誤况既,如下面這個(gè)例子。
/* main.c */
#include <stdio.h>
void f();
int y = 15212;
int x = 15213;
int main(){
f();
printf("x = 0x%x y=0x%x\n",x,y);
return 0;
}
/* f.c */
double x;
void f(){
x = -0.0;
}
使用命令
gcc main.c f.c
編譯會(huì)出現(xiàn)以下警告
Warning: alignment 4 of symbol `x' in /tmp/ccqZNck7.o is smaller than 8 in /tmp/ccqZc8em.o
主要原因是组民,在f.c中棒仍,訪問(wèn)的x是main.c中的x,所以臭胜,這里對(duì)x的賦值會(huì)導(dǎo)致對(duì)y進(jìn)行修改莫其,因?yàn)閷?duì)x賦值要寫8個(gè)字節(jié),而x只有4個(gè)字節(jié)耸三,而y挨著x乱陡,所以y的值會(huì)被修改。所以吕晌,結(jié)果為
x = 0x0 y=0x3b6c
與靜態(tài)庫(kù)鏈接
相關(guān)的函數(shù)可以被編譯為獨(dú)立目標(biāo)模塊蛋褥,然后封裝成一個(gè)單獨(dú)的靜態(tài)庫(kù)文件。然后睛驳,應(yīng)用程序可以通過(guò)在命令行上指定單獨(dú)的文件名字來(lái)使用這些在庫(kù)中定義的函數(shù)。
先說(shuō)說(shuō)如何制作靜態(tài)庫(kù):
- 生成對(duì)應(yīng)的.o文件膜廊。
gcc -c add.c sub.c
- 將生成的.o文件打包乏沸。注意動(dòng)態(tài)庫(kù)的命名規(guī)則:lib+庫(kù)的名字+.a。
ar rcs libMylib.a *.o
- 編譯自己的文件(main.c)爪瓜。
gcc main.c libMylib.a -o prog 或
gcc main.c -L .(使用的庫(kù)文件目錄) -l Mylib(要使用的庫(kù)) -o prog
鏈接器鏈接靜態(tài)庫(kù)時(shí)對(duì)全局符號(hào)的解析使用了如下的算法蹬跃。
- 按照命令行的順序掃描.a文件和.o文件。
- 在掃描過(guò)程中維護(hù)一個(gè)未解析的符號(hào)表。
- 對(duì)于遇到的每一個(gè)新的.o和.a蝶缀,嘗試查找未解析符號(hào)表中的符號(hào)丹喻,如果找到,則在未解析符號(hào)表中刪除該符號(hào)翁都。
- 如果最后未解析的符號(hào)表非空碍论,則報(bào)錯(cuò)。
這種算法有一個(gè)問(wèn)題柄慰,就是鏈接的成功與否和鏈接的順序有關(guān)鳍悠。一個(gè)建議就是把庫(kù)放在最后。請(qǐng)看下面這個(gè)例子坐搔。
/* main.c */
#include <stdio.h>
void f2();
int main(){
f2();
return 0;
}
/* f1.c */
#include <stdio.h>
int f1(){
printf("f1()\n");
}
/* f2.c */
#include <stdio.h>
void f1();
void f2(){
f1();
printf("f2()\n");
}
gcc -c *.c
ar rcs libf1.a f1.o
ar rcs libf2.a f2.o
gcc main.c libf1.a libf2.a //error
gcc main.c libf2.a libf1.a //ok
根據(jù)鏈接器的算法藏研,我們可以理解為什么使用gcc main.c libf1.a libf2.a會(huì)出錯(cuò)。因?yàn)榻馕鰂1函數(shù)的時(shí)候概行,沒(méi)有文件引用蠢挡,所以不會(huì)把它加入到可執(zhí)行文件中,而當(dāng)掃描到f2函數(shù)的時(shí)候凳忙,后面沒(méi)有出現(xiàn)對(duì)應(yīng)的f1函數(shù)业踏,所以會(huì)出錯(cuò)。
重定位
目標(biāo)文件有三種形式:可重定位目標(biāo)文件消略,可執(zhí)行目標(biāo)文件堡称,共享目標(biāo)文件。其中艺演,可重定位目標(biāo)文件格式如下圖所示却紧。
各個(gè)字段的含義如下:
- .text:已編譯程序的機(jī)器代碼。
- .rodata:只讀數(shù)據(jù)胎撤。
- .data:已初始化的全局和靜態(tài)C變量晓殊。
- .bss:未初始化的全局和靜態(tài)C變量,以及所有被初始化為0的全局或靜態(tài)變量伤提。
- .symtab:一個(gè)符號(hào)表巫俺,它存放在程序中定義和引用的函數(shù)和全局變量的信息。
- .rel.text:一個(gè).text節(jié)中位置的列表肿男,當(dāng)鏈接器把這個(gè)目標(biāo)文件和其他文件組合時(shí)介汹,需要修改這些位置。
- .rel.data:被模塊引用或定義的所有全局變量的重定位信息舶沛。
- .debug:一個(gè)調(diào)試符號(hào)表嘹承。
- .line:原始C源程序中的行號(hào)和.text節(jié)中機(jī)器指令指令之間的映射。
- .strtab:一個(gè)字符串表如庭,其內(nèi)容包括.symtab和.debug節(jié)中的符號(hào)表叹卷,以及節(jié)頭部中的節(jié)名字。
重定位有兩步組成:1,重定位節(jié)和符號(hào)定義骤竹。在這一步中帝牡,鏈接器將所有相同類型的節(jié)合并為同一類型的新的聚合節(jié)。2蒙揣,重定位節(jié)中符號(hào)引用靶溜。在這一步中,鏈接器修改代碼節(jié)和數(shù)據(jù)節(jié)中對(duì)每個(gè)符號(hào)的引用,使得它們指向正確的運(yùn)行時(shí)地址。
還是上面的例子钝尸,對(duì)main.o進(jìn)行反匯編,我們可以得到以下的匯編代碼扣汪。
objdump -D main.o
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: b8 00 00 00 00 mov $0x0,%eax
9: e8 00 00 00 00 callq e <main+0xe> //call后面的地址為0,還沒(méi)有確定
e: b8 00 00 00 00 mov $0x0,%eax
13: 5d pop %rbp
14: c3 retq
而我們對(duì)a.out進(jìn)行反匯編锨匆,可以得到以下的代碼崭别。
objdump -D a.out
000000000000063a <main>:
63a: 55 push %rbp
63b: 48 89 e5 mov %rsp,%rbp
63e: b8 00 00 00 00 mov $0x0,%eax
643: e8 07 00 00 00 callq 64f <f2> //call后面地址已經(jīng)確定了
648: b8 00 00 00 00 mov $0x0,%eax
64d: 5d pop %rbp
64e: c3 retq
重定位有兩種最基本的類型,R_X86_64_PC32恐锣,重定位一個(gè)使用32位PC相對(duì)地址的引用茅主;R_X86_64_32,重定位一個(gè)使用32位絕對(duì)地址的引用土榴。雖然我們對(duì)其中的算法不太理解诀姚,但是我們知道,這種重定位肯定有辦法可以做到的玷禽。
動(dòng)態(tài)鏈接共享庫(kù)
共享庫(kù)是一個(gè)目標(biāo)模塊赫段,在運(yùn)行或加載時(shí),可以加載到任意的內(nèi)存地址矢赁,并和一個(gè)在內(nèi)存中的程序鏈接起來(lái)糯笙。這個(gè)過(guò)程稱為動(dòng)態(tài)鏈接,是由一個(gè)叫做動(dòng)態(tài)鏈接器的程序來(lái)執(zhí)行的撩银。
動(dòng)態(tài)庫(kù)的制作步驟如下:
- 生成與位置無(wú)關(guān)的代碼(生成與位置無(wú)關(guān)的.o文件)给涕。
gcc -c -fPIC add.c sub.c
- 將.o打包成動(dòng)態(tài)庫(kù)。注意動(dòng)態(tài)庫(kù)的命名規(guī)范:lib + 庫(kù)的名字 + .so额获。
gcc -shared *.o -o libMylib.so
- 編譯自己的文件(main.c)够庙。
gcc main.c libMylib.so -o prog 或
gcc main.c -L .(使用的庫(kù)文件目錄) -l Mylib(要使用的庫(kù)) -o app
- 指定動(dòng)態(tài)庫(kù)的路徑,最常見(jiàn)的方法是增加一個(gè)臨時(shí)的環(huán)境變量抄邀。
export LD_LIBRARY_PATH=.(動(dòng)態(tài)庫(kù)文件所在目錄)
以上面的例子為例首启。
gcc -shared f1.o f2.o libf.so
gcc main.c libf.so
./a.out: error while loading shared libraries: libf.so: cannot open shared object file: No such file or directory
export LD_LIBRARY_PATH=.(動(dòng)態(tài)庫(kù)文件所在目錄)
這是我以前知道的方法,書上還提供了另外一種方法撤摸,這里需要將main.c修改為一下形式。
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int main(){
void *handle;
void (*f2)();
handle = dlopen("./libf.so",RTLD_LAZY);
f2 = dlsym(handle,"f2");
f2();
dlclose(handle);
return 0;
}
然后使用下面的命令編譯。
gcc -rdynamic main.c libf.so -ldl
這樣編譯出來(lái)的程序不需要指定動(dòng)態(tài)庫(kù)的路徑准夷,只要?jiǎng)討B(tài)庫(kù)在當(dāng)前路徑钥飞,就可以正確執(zhí)行。上面的代碼中衫嵌,為了簡(jiǎn)單读宙,省略了錯(cuò)誤處理的部分。