鏈接

使用鏈接器的好處

  • 模塊化缩歪。程序可以分解為更小的模塊,并且可以把公共的函數(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ù):

  1. 生成對(duì)應(yīng)的.o文件膜廊。
gcc -c add.c sub.c
  1. 將生成的.o文件打包乏沸。注意動(dòng)態(tài)庫(kù)的命名規(guī)則:lib+庫(kù)的名字+.a。
ar rcs libMylib.a *.o
  1. 編譯自己的文件(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)文件格式如下圖所示却紧。

典型的ELF可重定位目標(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ù)的制作步驟如下:

  1. 生成與位置無(wú)關(guān)的代碼(生成與位置無(wú)關(guān)的.o文件)给涕。
gcc -c  -fPIC add.c sub.c
  1. 將.o打包成動(dòng)態(tài)庫(kù)。注意動(dòng)態(tài)庫(kù)的命名規(guī)范:lib + 庫(kù)的名字 + .so额获。
gcc -shared *.o -o libMylib.so
  1. 編譯自己的文件(main.c)够庙。
gcc main.c libMylib.so -o prog    或
gcc main.c -L .(使用的庫(kù)文件目錄) -l Mylib(要使用的庫(kù)) -o app
  1. 指定動(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ò)誤處理的部分。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末楔绞,一起剝皮案震驚了整個(gè)濱河市结闸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌酒朵,老刑警劉巖桦锄,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蔫耽,居然都是意外死亡结耀,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門匙铡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)图甜,“玉大人,你說(shuō)我怎么就攤上這事鳖眼『谝悖” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵钦讳,是天一觀的道長(zhǎng)矿瘦。 經(jīng)常有香客問(wèn)我,道長(zhǎng)蜂厅,這世上最難降的妖魔是什么匪凡? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮掘猿,結(jié)果婚禮上病游,老公的妹妹穿的比我還像新娘。我一直安慰自己稠通,他們只是感情好衬衬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著改橘,像睡著了一般滋尉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上飞主,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天狮惜,我揣著相機(jī)與錄音高诺,去河邊找鬼。 笑死碾篡,一個(gè)胖子當(dāng)著我的面吹牛虱而,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播开泽,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼牡拇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了穆律?” 一聲冷哼從身側(cè)響起惠呼,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎峦耘,沒(méi)想到半個(gè)月后剔蹋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贡歧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年滩租,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片利朵。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡律想,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绍弟,到底是詐尸還是另有隱情技即,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布樟遣,位于F島的核電站而叼,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏豹悬。R本人自食惡果不足惜葵陵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瞻佛。 院中可真熱鬧脱篙,春花似錦、人聲如沸伤柄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)适刀。三九已至秤朗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間笔喉,已是汗流浹背取视。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工硝皂, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贫途。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓吧彪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親丢早。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容