連接器的初學(xué)者指南(翻譯)

命名:C源文件中都有什么班缎?

這部分是介紹 C源文件的組成蝴光,如果你熟悉下列代碼,可以進(jìn)入下一小結(jié)达址。

第一步需要區(qū)分 申明(declarations) 和 定義(definitions),
定義關(guān)聯(lián)一個(gè)名字并且有代碼或者數(shù)據(jù)來(lái)實(shí)現(xiàn)這個(gè)名字:

1. 定義一個(gè)變量蔑祟,讓編譯器給該變量分配空間,可能給這個(gè)空間分配一個(gè)值沉唠。
2. 定義一個(gè)函數(shù)疆虚,讓編譯器給這個(gè)函數(shù)產(chǎn)生代碼。

聲明告訴 C 編譯器在當(dāng)前程序中有這個(gè)定義,可能在別的 C源文件中径簿。(注意:定義有時(shí)會(huì)看成聲明罢屈,當(dāng)其位置在聲明的地方時(shí))。

接下來(lái)是對(duì)于變量篇亭,有兩種類型定義:

1. 全部變量缠捌,存在于整個(gè)程序的生命周期中("static extent"),在很多不同的函數(shù)中可以獲取到译蒂。
2. 局部變量鄙币,僅存在于一個(gè)特定的函數(shù)中("local extent"),僅通過(guò)這個(gè)函數(shù)才能獲取到這個(gè)變量蹂随。
(這里的獲取十嘿,是說(shuō)可以引用這個(gè)變量)

這里有兩個(gè)特例:

1. 靜態(tài)局部變量,實(shí)際上是全局變量岳锁,因?yàn)樗嬖谟谡麄€(gè)生命周期绩衷,但只能從這個(gè)特定函數(shù)獲取。
2. 同樣的靜態(tài)全局變量也可以看成全局變量激率,雖然只能在它定義的 C文件中獲取到咳燕。

這里我們把焦點(diǎn)放在了關(guān)鍵字 "靜態(tài)(static)" 上,需要指出的是將一個(gè)函數(shù)變?yōu)殪o態(tài)函數(shù)乒躺,可以減少其他地方引用該函數(shù)的行數(shù)(特別是通過(guò)同一個(gè) C文件的不同函數(shù)) -> 此處我的理解是函數(shù)的定義代碼被共享了招盲,不會(huì)在引用的地方再展開(kāi)函數(shù)。

對(duì)于全局變量和局部變量定義嘉冒,我們還可以區(qū)分變量是否初始化(也就是說(shuō)曹货,與特定名稱關(guān)聯(lián)的空間是否預(yù)先填充了特定值)。

最后讳推,我們可以通過(guò) malloc 或者 new 把信息動(dòng)態(tài)地存儲(chǔ)到內(nèi)存顶籽,沒(méi)有辦法通過(guò)名稱直接訪問(wèn)分配的內(nèi)存,所以我們必須通過(guò)指針(一個(gè)命名變量保留著一段內(nèi)存地址)银觅。這個(gè)內(nèi)存地址可以通過(guò) free 或者 delete 來(lái)銷毀礼饱,所以這被引用的空間有個(gè)動(dòng)態(tài)的范圍(dynamic extent)。

總結(jié)一下:

naming
/* This is the definition of a uninitialized global variable */
int x_global_uninit;

/* This is the definition of a initialized global variable */
int x_global_init = 1;

/* This is the definition of a uninitialized global variable, albeit
 * one that can only be accessed by name in this C file */
static int y_global_uninit;

/* This is the definition of a initialized global variable, albeit
 * one that can only be accessed by name in this C file */
static int y_global_init = 2;

/* This is a declaration of a global variable that exists somewhere
 * else in the program */
extern int z_global;

/* This is a declaration of a function that exists somewhere else in
 * the program (you can add "extern" beforehand if you like, but it's
 * not needed) */
int fn_a(int x, int y);

/* This is a definition of a function, but because it is marked as
 * static, it can only be referred to by name in this C file alone */
static int fn_b(int x)
{
  return x+1;
}

/* This is a definition of a function. */
/* The function parameter counts as a local variable */
int fn_c(int x_local)
{
  /* This is the definition of an uninitialized local variable */
  int y_local_uninit;
  /* This is the definition of an initialized local variable */
  int y_local_init = 3;

  /* Code that refers to local and global variables and other
   * functions by name */
  x_global_uninit = fn_a(x_local, x_global_init);
  y_local_uninit = fn_a(x_local, y_local_init);
  y_local_uninit += fn_b(z_global);
  return (y_global_uninit + y_local_uninit);
}

C 編譯器做了什么究驴?

C 編譯器的任務(wù)是將人們可讀的代碼翻譯為機(jī)器可理解的代碼镊绪。編譯器的輸出為目標(biāo)文件(Object file)。在 UNIX 平臺(tái)這些目標(biāo)文件通常以 .o 結(jié)尾洒忧,Windows 上以 .obj 結(jié)尾蝴韭。目標(biāo)文件的內(nèi)容是最基礎(chǔ)的兩類東西:

1. 代碼 C 文件中響應(yīng)的函數(shù)定義
2. 數(shù)據(jù) C 文件中全局變量的定義(對(duì)于初始化的全局變量,初始值已經(jīng)保存在目標(biāo)文件中了)

這兩種類型的實(shí)例跑慕,都有名字與其關(guān)聯(lián)(變量和函數(shù)的名字是在定義的時(shí)候產(chǎn)生的)万皿。

目標(biāo)文件代碼是一系列的機(jī)器指令摧找,和程序員寫入的 C 指令相關(guān)聯(lián)(if /while /goto)核行。所有這些指令都需要處理一些信息牢硅,這些信息需要保存在某個(gè)地方,這是變量的工作芝雪。該代碼還可以引用其他代碼位减余,尤其是程序中的其他C函數(shù)。

在任何地方惩系,代碼如果能夠引用一個(gè)變量或者函數(shù)位岔,編譯器必須提前看到這個(gè)變量或者函數(shù)的聲明(聲明約定了定義存在于整個(gè)程序的某個(gè)地方)。

鏈接器的任務(wù)就是遵守這些約定堡牡,但編譯器是怎樣在生成目標(biāo)文件的同時(shí)處理所有的約定的呢抒抬?

基本上,編譯器會(huì)留下一個(gè)空白晤柄〔两#空白(“引用”)有一個(gè)與之關(guān)聯(lián)的名稱,但是與該名稱對(duì)應(yīng)的值未知芥颈。

我們可以描述上個(gè)示例的引用關(guān)系如下:

diag

剖析一個(gè)目標(biāo)文件

到目前為止惠勒,我們都是在上層來(lái)分析的;看看在實(shí)際中底層如何工作這也很重要爬坑。這里用到的關(guān)鍵工具是 nm ,用它可以獲取 NUIX 平臺(tái)一個(gè)目標(biāo)文件的標(biāo)記(symbols)信息纠屋。Windows平臺(tái)可以用 dumpbin 帶上 /symbols 來(lái)大致產(chǎn)生相同的效果;這里也有提供了一個(gè) Windows 版本的 nm 工具盾计。

讓我們看看通過(guò) nm 工具獲取到上面示例目標(biāo)文件的信息:

chart

不同平臺(tái)下的輸出會(huì)有一點(diǎn)點(diǎn)不同(查看 nm 的 pages 頁(yè)可以查找出版本的特性)售担,但是給出的關(guān)鍵信息是每個(gè)符號(hào)(symbol)的類別(class)和大小(size),class有不同的值:

  1. "U" 是指未定義的引用署辉,上文說(shuō)的編譯器留出的空缺是其中一種灼舍。在這個(gè)目標(biāo)文件中,有兩個(gè) "U" 類型 "fn_a" 和 "z_global"涨薪。(一些 nm 版本可能會(huì)打印一個(gè)詞語(yǔ)骑素,如 "UND" 或者 "UNDEF")
  2. "t" "T" 是指這個(gè)函數(shù)定義了,"t" 是說(shuō)函數(shù)定義在同一個(gè)文件下刚夺,"T"是說(shuō)函數(shù)定義在其他文件下(函數(shù)初始定義的地方是靜態(tài)的"static")献丑。(同樣,一些系統(tǒng)下會(huì)顯示為一個(gè)詞語(yǔ), 如 ".text")
  3. "d" "D" 是指初始化的全局變量侠姑,同樣 "d" 是定義在同一個(gè)文件下创橄,"D"是定義在其他文件下("static")。( ".data")
  4. "b" "B" "C" 是指未初始化的全局變量莽红,靜態(tài)的或者本文件中的是 "b", B 和 C 是其他的妥畏。(".bss" "COM")

我們可能獲取到的一些 Symbols 不是 C源文件最初輸入的一部分邦邦;我們將會(huì)忽略這些 Symbols,把它們視為編譯器內(nèi)部機(jī)制產(chǎn)生的試圖獲取我們程序的惡意鏈接醉蚁。

連接器做了什么燃辖?(一)

我們?cè)谥疤徇^(guò),聲明一個(gè)函數(shù)或者變量就是給 C編譯器一個(gè)約定网棍,在程序的某個(gè)地方有這個(gè)函數(shù)或者變量的定義黔龟,鏈接器的作用就是達(dá)成這個(gè)約定。通過(guò)前面的目標(biāo)文件圖滥玷,我們可以進(jìn)一步闡述填充空白的過(guò)程氏身。

為了闡述填充空白,我們?cè)僭黾右粋€(gè) C文件:

/* Initialized global variable */
int z_global = 11;
/* Second global named y_global_init, but they are both static */
static int y_global_init = 2;
/* Declaration of another global variable */
extern int x_global_init;

int fn_a(int x, int y)
{
  return(x+y);
}

int main(int argc, char *argv[])
{
  const char *message = "Hello, world";

  return fn_a(11,12);
}
diag

將這兩幅圖放一起惑畴,我們能夠?qū)⑺械墓?jié)點(diǎn)連接起來(lái)(如果存在連接不上的點(diǎn)蛋欣,鏈接器會(huì)報(bào)錯(cuò))。每個(gè)事物都有它自己的位置如贷,每個(gè)位置都有它自己的事物陷虎,而鏈接器可以填充所有的空缺,如圖所示:

diag

我們可以通過(guò) nm 指令來(lái)查看這兩個(gè)目標(biāo)文件鏈接后的信息:

chart

所有的符號(hào)(Symbol)來(lái)自這兩個(gè)目標(biāo)文件倒得,所有的未定義引用都沒(méi)有了泻红。這些也都進(jìn)行了重新排序,以便將相似類型的事物放在一起霞掺,并添加了一些附加功能谊路,以幫助操作系統(tǒng)將整個(gè)事物作為可執(zhí)行程序處理。(還有很多復(fù)雜的細(xì)節(jié)使輸出雜亂無(wú)章菩彬,但是如果您濾除任何以下劃線開(kāi)頭的內(nèi)容缠劝,它將變得更加簡(jiǎn)單。)

重復(fù)的符號(hào)

上小結(jié)中有提到如果鏈接器找不到符號(hào)的定義骗灶,就會(huì)報(bào)錯(cuò)惨恭。但是如果在連接時(shí)有兩個(gè)不同的定義對(duì)應(yīng)一個(gè)符號(hào)呢?

在 C++ 中耙旦,這場(chǎng)景的處理很直接脱羡。這語(yǔ)言有嚴(yán)格的一個(gè)定義對(duì)應(yīng)一個(gè)符號(hào)的規(guī)則,在連接時(shí)只能有一個(gè)定義與符號(hào)對(duì)應(yīng)免都,不能多也不能少锉罐。(C ++標(biāo)準(zhǔn)的相關(guān)部分是3.2,其中還提到了一些例外情況绕娘,我們將在以后介紹脓规。)

在 C 語(yǔ)言中,這個(gè)規(guī)則有些模糊险领。任何函數(shù)或者初始化的全局變量必須有明確的定義侨舆,但是未初始化的全局變量可以視為臨時(shí)定義秒紧。C 語(yǔ)言允許(或者不禁止)不同的源文件有對(duì)一個(gè)符號(hào)的臨時(shí)定義。

然而鏈接器除了處理 C 和 C++ 外還要處理其他語(yǔ)言挨下,這些語(yǔ)言不一定適用一個(gè)定義對(duì)應(yīng)一個(gè)符號(hào)的規(guī)則熔恢。例如:Fortran語(yǔ)言的普通模型是將全局變量拷貝到引用到它的每個(gè)文件中,鏈接器要求選擇其中一個(gè)拷貝(如果他們大小不一复颈,選擇其中最大的)绩聘,將其他重復(fù)的折疊起來(lái)拋棄沥割。(這個(gè)模型被稱為鏈接器的共用模型耗啦,以 Fortran 的關(guān)鍵字 "COMMON" 命名)

因此,對(duì)于 UNIX 連接器來(lái)說(shuō)机杜,至少在重復(fù)符號(hào)是未初始化的全局變量時(shí)帜讲,通常它們不會(huì)抱怨符號(hào)的重復(fù)定義(這有時(shí)被說(shuō)成是連接的“寬松聲明/定義模型”)。如果你擔(dān)心這個(gè)問(wèn)題椒拗,查閱鏈接器的手冊(cè)通常有 "--work-properly"選項(xiàng)來(lái)嚴(yán)格限制重復(fù)定義的行為似将。例如:GUN 工具鏈就有 "-fno-common" 選項(xiàng)將未初始化的變量放在 BBS 段(BBS segment) 而不是 Common 塊(BBS block)。

操作系統(tǒng)做了什么蚀苛?

現(xiàn)在鏈接器已經(jīng)將所有的符號(hào)引用都連接到了對(duì)應(yīng)的定義在验,生成了一個(gè)可執(zhí)行程序,我們需要暫停一會(huì)兒來(lái)簡(jiǎn)明的說(shuō)明一下在程序運(yùn)行的時(shí)候堵未,操作系統(tǒng)所扮演的角色腋舌。

運(yùn)行程序顯然涉及執(zhí)行機(jī)器代碼,所以操作系統(tǒng)必須將磁盤上的可執(zhí)行文件翻譯為機(jī)器代碼送入CPU可讀取的地方--計(jì)算機(jī)內(nèi)存渗蟹。程序被送入內(nèi)存的部分被命名為代碼段(code segment)或者文本段(text segment)块饺。

沒(méi)有數(shù)據(jù)的代碼什么也不是,所以全部的全局變量也要被送入計(jì)算機(jī)內(nèi)存雌芽。然而初始化和未被初始化的全局變量是有區(qū)別的授艰。初始化的全局變量有初始值保存在之前的目標(biāo)文件和可執(zhí)行文件中。當(dāng)程序執(zhí)行起來(lái)世落,操作系統(tǒng)會(huì)將這些值拷貝到內(nèi)存的數(shù)據(jù)段(data segment)淮腾。對(duì)于未被初始化的全局變量,操作系統(tǒng)假設(shè)它們的初始值為 0屉佳,沒(méi)有必要拷貝任何值谷朝。這些初始化為0的內(nèi)存塊稱為bss段(BBS segment)。

這意味著可以將空間保存在磁盤上的可執(zhí)行文件中忘古。 初始化變量的初始值必須存儲(chǔ)在文件中徘禁,但是對(duì)于未初始化變量,我們只需要計(jì)算它們需要多少空間即可髓堪。

diag

讀者也許注意到送朱,前面我們所討論的目標(biāo)文件和鏈接器只涉及到了全局變量娘荡,并沒(méi)有討論過(guò)局部變量和動(dòng)態(tài)分配的內(nèi)存(指針對(duì)象)。

其實(shí)這些數(shù)據(jù)的分配并不需要任何鏈接器的參與驶沼,因?yàn)樗鼈兊纳芷诎l(fā)生在程序的運(yùn)行時(shí)(run time)炮沐,在鏈接器完成了它的工作之后。為了文章的完整性回怜,在這里簡(jiǎn)單的介紹一下:

  1. 局部變量被分配在棧內(nèi)存上大年,棧內(nèi)存隨著函數(shù)的調(diào)用和完成,增長(zhǎng)或者減小玉雾。
  2. 動(dòng)態(tài)分配的內(nèi)存受到堆內(nèi)存管理翔试,malloc 函數(shù)在這個(gè)區(qū)域內(nèi)搜索可用的空間。

我們把這部分內(nèi)存分配加入到圖中复旬,完成在程序運(yùn)行時(shí)刻的內(nèi)存分配模型垦缅。因?yàn)槎押蜅T诔绦蜻\(yùn)行時(shí)大小是會(huì)隨時(shí)改變的,通常安排堆往一個(gè)方向擴(kuò)展內(nèi)存驹碍,棧往另外一個(gè)方向壁涎。通過(guò)這種方式,程序只會(huì)在這兩端相遇時(shí)耗盡內(nèi)存(此時(shí)志秃,內(nèi)存空間已經(jīng)滿了)

diag

連接器做了什么怔球?(二)

現(xiàn)在我們已經(jīng)了解了鏈接器的基本原理,接下來(lái)可以討論更復(fù)雜的細(xì)節(jié) -- 大致按照這些特性在歷史上被添加到鏈接器的順序浮还。

影響編譯器功能的主要觀察點(diǎn)是:如果有大量不同的程序需要處理同一類的事情(例如:輸出到屏幕竟坛、從磁盤讀入文件),那么在一個(gè)地方通用該代碼并讓許多不同的程序使用它顯然很有意義碑定。

鏈接不同程序時(shí)只使用相同的目標(biāo)文件是完全可行的流码,如果將相關(guān)目標(biāo)文件的整個(gè)集合放在一個(gè)易于訪問(wèn)的地方(庫(kù) library),則可以使工作變得更加輕松延刘。

(技術(shù)說(shuō)明:本節(jié)完全跳過(guò)了鏈接器的一個(gè)主要特性:重定位(relocation)漫试。不同的程序由不同的大小,當(dāng)共享庫(kù)(shared library)映射到程序內(nèi)存地址時(shí)碘赖,會(huì)被分配于不同的地址驾荣。也就是說(shuō)這個(gè)庫(kù)中的所有函數(shù)和變量會(huì)被分配在不同的地方。如果所有引用地址的方法都是相對(duì)的(“+1020bites”)普泡,而不是絕對(duì)的(“0x102218BF”)播掷,那么問(wèn)題就不那么嚴(yán)重了。如果不是這樣撼班,所有的絕對(duì)地址需要加上一個(gè)合適的偏移歧匈,這就是重定位(relocation)。本文沒(méi)有繼續(xù)深入這個(gè)話題砰嘁,因?yàn)?C/C++ 程序員很少會(huì)遇到這方面問(wèn)題件炉,大多數(shù)鏈接問(wèn)題不會(huì)是因?yàn)橹囟ㄎ灰鸬摹?

靜態(tài)庫(kù)

庫(kù)(library)最基本的形態(tài)是靜態(tài)庫(kù)(static library)勘究。前章節(jié)提到過(guò)可以通過(guò)復(fù)用目標(biāo)文件來(lái)共享代碼,事實(shí)證明斟冕,靜態(tài)庫(kù)確實(shí)沒(méi)有比這復(fù)雜得多口糕。

在 UNIX 系統(tǒng)上,產(chǎn)生靜態(tài)庫(kù)的指令為 ar 磕蛇,生成的靜態(tài)庫(kù)以 .a 作為擴(kuò)展名景描。這些庫(kù)文件命名通常以"lib"開(kāi)頭作為前綴,鏈接器在連接時(shí)會(huì)在名稱上去掉前綴和擴(kuò)展名秀撇,加上 "-l" 選項(xiàng)(例如:"-lfred" 會(huì)鏈接"libfred.a"的庫(kù))超棺。
(過(guò)去,一個(gè)程序需要調(diào)用 "ranlib" 程序來(lái)在庫(kù)的開(kāi)頭建立符號(hào)索引(index of symbols)“仆啵現(xiàn)在 ar 工具可以完成這些工作说搅。)

當(dāng)鏈接器遍歷要連接在一起的目標(biāo)文件集合時(shí)炸枣,它會(huì)建立一個(gè)尚未解析的符號(hào)列表(unresolved list)虏等。當(dāng)完成所有明確指定的目標(biāo)后,鏈接器現(xiàn)在可以在庫(kù)中查找在未解析列表(unresolved list)上保留的符號(hào)适肠。當(dāng)未解析符號(hào)的定義在這個(gè)庫(kù)的一個(gè)目標(biāo)文件中是霍衫,這個(gè)目標(biāo)文件被加載,就像用戶首先在命令行上賦值了這個(gè)目標(biāo)文件一樣侯养,然后鏈接繼續(xù)敦跌。

請(qǐng)注意從庫(kù)中提取的粒度:如果一些符號(hào)定義是需要的,包含這些符號(hào)定義目標(biāo)文件都會(huì)被加載逛揩。這意味著該過(guò)程可以向前邁出柠傍,也可以向后邁一步 -- 新添加的目標(biāo)可以解析一個(gè)未定義的引用,但是很可能會(huì)附帶一整套新的未定義引用供鏈接器解析辩稽。

另外一個(gè)注意點(diǎn)是加載的順序:庫(kù)里的符號(hào)被鏈接只有在常用鏈接完成后才被開(kāi)始惧笛,它們是按照順序執(zhí)行的,從左至右(在鏈接行中)逞泄。這意味著患整,如果從鏈接行(link line)后期的庫(kù)中拉入的對(duì)象需要鏈接行中較早的庫(kù)中的符號(hào),則鏈接程序?qū)⒉粫?huì)自動(dòng)找到它喷众。

下面實(shí)例可以解釋清除各谚,假設(shè)我們有如下目標(biāo)文件,和一個(gè)鏈接行拉入了 a.o到千、 b.o昌渤、 -lx 和 -ly。

chart

一旦鏈接器處理了 a.o憔四、 b.o膀息,就能處理 b2望抽、 a3 的引用,留下了未定義引用 x12 和 y22履婉。這時(shí)煤篙,鏈接器檢查第一個(gè)庫(kù) libx.a 中的定義,發(fā)現(xiàn)目標(biāo)文件 x1.o 能滿足未定義符號(hào) x12 的引用毁腿,然而又新增了 x23 和 y12 到未解析引用表辑奈。 (現(xiàn)在這個(gè)列表成了 y22、 x23 和 y12)已烤。

鏈接器仍然在處理 libx.a 鸠窗,x23 的引用很容易實(shí)現(xiàn),只許從 libx.a 拉入目標(biāo)文件 x2.o 胯究。然而稍计,未解析引用表又新增了 y11 (現(xiàn)在列表成了 y22、y12裕循、y11)臣嚣。到目前為止,libx.a 中找不到未解析引用表中符號(hào)的定義了剥哑,所以鏈接器接下來(lái)開(kāi)始檢查 liby.a 硅则。

鏈接器按照同樣的方式處理 y1.o 和 y2.o 目標(biāo)文件。第一個(gè)加入的未定義引用 y21 很快就在 y2.o 目標(biāo)文件中找到了株婴,這時(shí)所有未定義的引用都找到了怎虫,未解析引用表清空,庫(kù)中的某些但不是全部目標(biāo)文件已包含在最終可執(zhí)行文件中困介。

注意當(dāng) b.o 目標(biāo)文件中有一個(gè)未解析引用 y32 時(shí)大审,情況會(huì)有一點(diǎn)點(diǎn)不一樣:鏈接 libx.a 的工作還是一樣,但是在處理 liby.a 時(shí)座哩,y3.o 目標(biāo)文件會(huì)被拉入進(jìn)來(lái)徒扶,同時(shí)未解析引用 x31 會(huì)被加入到未解析引用表中,鏈接會(huì)失敗八回,因?yàn)?x31 的定義在 libx.a 的 x3.o 目標(biāo)文件中酷愧,而 libx.a 已經(jīng)被鏈接器處理完了。
(順便說(shuō)一句缠诅,此示例在兩個(gè)庫(kù)libx.a和liby.a之間具有循環(huán)依賴性溶浴,這是很糟糕的事情,特別是在 Windows 系統(tǒng)下)

共享庫(kù)(動(dòng)態(tài)庫(kù))

對(duì)于諸如C標(biāo)準(zhǔn)庫(kù)(通常為libc)之類的流行庫(kù)管引,擁有靜態(tài)庫(kù)會(huì)存在一個(gè)明顯的缺點(diǎn) -- 每個(gè)可執(zhí)行程序都具有相同代碼的副本士败。 如果每個(gè)可執(zhí)行文件都有一個(gè) printf 和 fopen 之類的副本,這會(huì)占用很多不必要的磁盤空間。

不太明顯的缺點(diǎn)是谅将,程序一旦被靜態(tài)鏈接了漾狼,其中的代碼將永遠(yuǎn)固定。如果有人發(fā)現(xiàn) printf 函數(shù)存在 bug 饥臂,所有的程序必須被重新編譯鏈接一遍來(lái)修復(fù)代碼逊躁。

為了解決這些問(wèn)題和相關(guān)問(wèn)題,引入了共享庫(kù)(通常以 .so 擴(kuò)展名表示隅熙,或者在 Windows 計(jì)算機(jī)上以 .dll 表示稽煤,在 Mac OS X 上以 .dylib 表示)。對(duì)于這些類型的庫(kù)囚戚,普通的命令行鏈接器不一定會(huì)連接所有的點(diǎn)酵熙。而是,采用一種“ IOU”便箋驰坊,并將該便箋的付款推遲到程序?qū)嶋H運(yùn)行的那一刻匾二。

歸結(jié)為:如果鏈接器發(fā)現(xiàn)特定符號(hào)的定義在共享庫(kù)中,則它在最終可執(zhí)行文件中不包含該符號(hào)的定義拳芙。而是察藐,鏈接器在可執(zhí)行文件中記錄符號(hào)的名稱以及它應(yīng)來(lái)自哪個(gè)庫(kù)。

當(dāng)程序運(yùn)行時(shí)态鳖,操作系統(tǒng)安排這些剩余的鏈接位“及時(shí)”完成转培,以使程序運(yùn)行。 在運(yùn)行主函數(shù)(main)之前浆竭,較小版本的鏈接器(通常稱為ld.so)會(huì)處理鏈接器之前添加的便簽,并在那里進(jìn)行鏈接的最后階段 -- 拉入庫(kù)中的代碼并連接所有點(diǎn)惨寿。

這意味著所有可執(zhí)行文件都沒(méi)有 printf 函數(shù)的代碼副本邦泄。 如果有新的,固定的 printf 版本可用裂垦,只需更改 libc.so 就可以使用它顺囊,下次任何程序運(yùn)行時(shí),它將被提取蕉拢。

靜態(tài)庫(kù)和共享庫(kù)另外一個(gè)大的區(qū)別是在鏈接的顆粒度上特碳。如果一個(gè)特定的符號(hào)定義從共享庫(kù)被拉入(比如 libc.so 中的 printf 函數(shù)),整個(gè)共享庫(kù)會(huì)被映射到程序的地址空間晕换。這和靜態(tài)庫(kù)只拉入特定目標(biāo)文件不同午乓。

換句話說(shuō),共享庫(kù)本身是由于運(yùn)行鏈接程序而產(chǎn)生的(而不是像 ar 那樣僅形成一大堆對(duì)象)闸准,并且解析了同一庫(kù)中目標(biāo)文件之間的引用益愈。再次,nm 是用于說(shuō)明這一點(diǎn)的有用工具:對(duì)于上面的示例庫(kù),當(dāng)在庫(kù)的靜態(tài)版本上運(yùn)行時(shí)蒸其,它將為單個(gè)目標(biāo)文件生成結(jié)果集敏释,但對(duì)于庫(kù)的共享版本liby.so,它只生成一個(gè)結(jié)果集摸袁,只有 x31 作為未定義符號(hào)钥顽。此外,對(duì)于上一小節(jié)末尾的庫(kù)排序示例靠汁,也沒(méi)有問(wèn)題:將對(duì) x32 的引用添加到 b.o 中不會(huì)有鏈接錯(cuò)誤耳鸯,因?yàn)閥3.o和x3.o的所有內(nèi)容都是已經(jīng)拉進(jìn)來(lái)了。

另外一個(gè)實(shí)用的工具是 ldd膀曾,在 UNIX 系統(tǒng)中展示可執(zhí)行文件(或者靜態(tài)庫(kù))的依賴的共享庫(kù)集合县爬,以及可能在哪里找到這些庫(kù)的指示。為了使程序成功運(yùn)行添谊,加載程序需要能夠依次找到所有這些庫(kù)及其所有依賴項(xiàng)财喳。(通常,加載程序在LD_LIBRARY_PATH環(huán)境變量中包含的目錄列表中查找?guī)臁?

diag

(更大的粒度的原因是因?yàn)楝F(xiàn)代操作系統(tǒng)足夠聰明斩狱,不僅可以節(jié)省靜態(tài)庫(kù)中的重復(fù)磁盤空間耳高,還可以節(jié)省更多;使用相同共享庫(kù)的不同運(yùn)行進(jìn)程也可以共享代碼段<不能是 data segment 或者 bss segment -- 因?yàn)楫吘共煌倪M(jìn)程處于不同的地址>所踊。為此泌枪,必須一次性映射整個(gè)庫(kù),以便內(nèi)部引用全部排在同一位置秕岛;如果一個(gè)進(jìn)程將目標(biāo)文件 a.o 和 c.o 引入碌燕,而另一個(gè)進(jìn)程將目標(biāo)文件 b.o 和 c.o 引入,則操作系統(tǒng)將沒(méi)有任何可以利用的通用性继薛。)

C++ 語(yǔ)言特性

C++ 在 C 語(yǔ)言基礎(chǔ)上提供了一些其他特性修壕,其中一些特性和鏈接器相關(guān)。最開(kāi)始 C++ 是作為 C 語(yǔ)言編譯器的前端出現(xiàn)遏考,所以鏈接器的后端不需要改變慈鸠,但隨著時(shí)間推移,越來(lái)越多的 C++ 特性需要鏈接器來(lái)支持灌具。

多態(tài)和函數(shù)名混淆

首先介紹的 C++ 特性是函數(shù)的多態(tài)青团,同樣函數(shù)名的有不同的類型,通過(guò)不同的形參來(lái)區(qū)分:

diag_6.png

很明顯這樣會(huì)給鏈接器帶來(lái)一個(gè)問(wèn)題:怎么知道代碼所用的 max 函數(shù)是指哪一個(gè)咖楣?
解決方案是采用一種函數(shù)名混淆的方式督笆,因?yàn)樗械暮瘮?shù)簽名都是以文本格式混淆的,這是鏈接器用的符號(hào)表的實(shí)際名稱截歉。不同的函數(shù)簽名會(huì)被混淆成不同的名字胖腾,通過(guò)這種方式,鏈接器查找函數(shù)的問(wèn)題就解決了。

本文不打算詳細(xì)介紹函數(shù)名混淆的過(guò)程咸作,(不同的平臺(tái)上實(shí)現(xiàn)方式可能不一樣)锨阿,但是快速看看目標(biāo)文件相關(guān)信息能得到一些啟示(通過(guò)nm工具):

diag_7.png

這里,我們能看到三個(gè) max 函數(shù)在目標(biāo)文件中被定義為不同的函數(shù)名记罚,我們可以大膽的猜測(cè) "max" 后的兩個(gè)字母代表參數(shù)類型:i 是 int 墅诡、f 是 float 、d 是 double (當(dāng)類桐智、命名空間末早、操作函數(shù)加入時(shí),命名混淆會(huì)變得更加復(fù)雜)说庭。加上 --demangle 可以得到如下結(jié)果:

diag_8.png

當(dāng)C 語(yǔ)言和 C++ 混合使用時(shí)然磷,這種混淆方式很容易讓人犯錯(cuò)。C++ 編譯器生成的符號(hào)都是混淆的刊驴,而C編譯生成的符號(hào)和源文件中一致姿搜。為了解決這個(gè)問(wèn)題,C++ 運(yùn)行用戶使用 extern "C" 在函數(shù)定義和聲明的地方捆憎。它會(huì)告訴C++編譯器有這些特殊符號(hào)的函數(shù)名舅柜,是不用混淆處理的。

本文開(kāi)頭的實(shí)例中躲惰,可能會(huì)有很多讀者忘記鏈接C語(yǔ)言和C++時(shí)的 extern "C" 聲明致份。

diag_9.png

在函數(shù)簽名中最大的錯(cuò)誤暗示是-- findmax 函數(shù)沒(méi)有找到,換句話說(shuō)础拨,在 C++代碼中實(shí)際是搜尋一些例如 "_Z7findmaxii" 這樣的函數(shù)氮块,而不是 "findmax",所以會(huì)鏈接失敗太伊。

順便提醒一下雇锡,成員函數(shù)的定義中 extern "C" 會(huì)被忽略掉 (C++ 標(biāo)準(zhǔn) 7.5.4)。

靜態(tài)變量的初始化

下面介紹的另外一個(gè)特性是對(duì)象的構(gòu)造函數(shù)僚焦。一個(gè)構(gòu)造函數(shù)是一段創(chuàng)建對(duì)象的代碼,在概念上它和變量的初始化一樣曙痘,主要區(qū)別是構(gòu)造函數(shù)里面會(huì)實(shí)現(xiàn)一些代碼芳悲。

再來(lái)回顧一下前面的例子全局變量有一個(gè)特殊的初始值。在C語(yǔ)言里边坤,初始化這樣的全局變量很簡(jiǎn)單:這個(gè)特殊值是從目標(biāo)文件中的 data 區(qū)拷貝過(guò)來(lái)的名扛,當(dāng)程序運(yùn)行起來(lái)在內(nèi)存中占有內(nèi)存空間。

而對(duì)于 C++ 茧痒,構(gòu)造過(guò)程要比這種從內(nèi)存中拷貝值要復(fù)雜很多肮韧,所有在這個(gè)類里面繼承下來(lái)的構(gòu)造函數(shù)中的代碼都會(huì)被執(zhí)行。

下面詳細(xì)說(shuō)明一下,編譯器在每個(gè)C++文件的目標(biāo)文件中都包含了一些額外信息,特別是構(gòu)造函數(shù)列表對(duì)于每個(gè)文件。在鏈接時(shí)亭引,鏈接器將所有的這類列表合并成一個(gè)大的列表劲厌,同時(shí)包含了一個(gè)遍歷列表的代碼,鏈接時(shí)調(diào)用所有這些全局對(duì)象構(gòu)造函數(shù)米辐。

注意這里全局變量構(gòu)造函數(shù)的調(diào)用順序是沒(méi)有被定義的 -- 完全由鏈接器決定選擇哪一個(gè)。(侯捷的高效C++書中有詳細(xì)介紹)

我們可以通過(guò) nm 來(lái)遍歷這個(gè)列表〗炝迹考慮下面的 C++ 代碼:


diag_10.png

使用 nm --demangle 輸出如下:


diag_11.png

這里出現(xiàn)了一些不同地方,但是我們感興趣的是兩個(gè)類名為W(表示“弱”符號(hào))和節(jié)名為“.gnu.linkonce.t.stuff”的條目圣猎。這些是全局對(duì)象的構(gòu)造函數(shù)士葫,我們看到相應(yīng)的 " " 域可以識(shí)別出,這個(gè)函數(shù)是做什么用的送悔。

動(dòng)態(tài)加載庫(kù)

這章節(jié)慢显,我們會(huì)簡(jiǎn)略討論一下動(dòng)態(tài)加載動(dòng)態(tài)庫(kù)相關(guān)知識(shí)。上一章節(jié)描述了在程序運(yùn)行時(shí)動(dòng)態(tài)庫(kù)是怎樣被鏈接的放祟。在現(xiàn)代系統(tǒng)中鳍怨,鏈接可能會(huì)推遲到更晚的時(shí)間。

加載過(guò)程通過(guò)兩個(gè)系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn)跪妥,dlopen 和 dlsym(Windows下有相關(guān)的 LoadLibrary 和 GetProcAddress)鞋喇。其中第一個(gè)使用共享庫(kù)的名稱,并將其加載到正在運(yùn)行的進(jìn)程的地址空間中眉撵。當(dāng)然侦香,這些庫(kù)本身可能具有未定義的函數(shù),因此對(duì)dlopen的調(diào)用也可能觸發(fā)其他動(dòng)態(tài)庫(kù)的加載纽疟。

dlopen還允許選擇是否在加載庫(kù)時(shí)立即解析所有這些引用(RTLD_NOW)罐韩,或者當(dāng)每個(gè)未定義的引用被命中時(shí)一一的對(duì)應(yīng)(RTLD_LAZY)。第一種方式意味著dlopen調(diào)用花費(fèi)的時(shí)間更長(zhǎng)污朽,但是第二種方式則存在輕微的風(fēng)險(xiǎn)散吵,即程序稍后會(huì)發(fā)現(xiàn)存在無(wú)法解析的未定義引用,這時(shí)程序?qū)⒈唤K止蟆肆。

當(dāng)然矾睦,動(dòng)態(tài)加載庫(kù)中的符號(hào)無(wú)法命名。但是炎功,與編程問(wèn)題一樣枚冗,通過(guò)添加額外的間接級(jí)別(在這種情況下,通過(guò)使用指向符號(hào)空間的指針而不是通過(guò)名稱來(lái)引用)可以輕松解決此問(wèn)題蛇损。調(diào)用dlsym使用一個(gè)字符串參數(shù)赁温,該參數(shù)給出要找到的符號(hào)的名稱坛怪,并返回一個(gè)指向其位置的指針(如果找不到則返回NULL)。

和C++特性相關(guān)的互動(dòng)

這種動(dòng)態(tài)加載功能非常廣泛股囊,但是它如何與影響鏈接程序整體行為的 C++ 特性交互袜匿?

第一個(gè)觀察者是函數(shù)名混淆有些棘手。調(diào)用dlsym時(shí)毁涉,它將使用包含要找到的符號(hào)名稱的字符串沉帮。該名稱必須是鏈接程序可見(jiàn)的名稱。換句話說(shuō)贫堰,是函數(shù)名混淆后的名字穆壕。

因?yàn)椴煌脚_(tái)下,不同的編譯器使用的函數(shù)名混淆方式不同其屏,這意味著以一種可移植的方式動(dòng)態(tài)定位c++符號(hào)幾乎是不可能的喇勋。即使您樂(lè)于堅(jiān)持使用一個(gè)特定的編譯器并深入研究它的內(nèi)部機(jī)制,除了普通的c類函數(shù)之外偎行,還有更多的問(wèn)題等待著您去解決川背,您必須要擔(dān)心vtable之類的問(wèn)題。

總而言之蛤袒,通常最好只堅(jiān)持一個(gè)熄云,extern "C" 的入口只有 dlsym ,此入口點(diǎn)可以是工廠方法,該方法返回指向 C++ 類的完整實(shí)例的指針妙真,從而允許訪問(wèn)所有 C++ 結(jié)構(gòu)缴允。

編譯器可以為 dlopen 庫(kù)中的全局對(duì)象整理出構(gòu)造函數(shù),因?yàn)榭梢栽趲?kù)中定義幾個(gè)特殊符號(hào)珍德,并且在動(dòng)態(tài)加載庫(kù)時(shí)鏈接器(無(wú)論是加載時(shí)還是運(yùn)行時(shí))都將調(diào)用它們 或卸載-因此可以在其中放置必要的構(gòu)造函數(shù)和析構(gòu)函數(shù)調(diào)用练般。在Unix中,這些函數(shù)稱為_(kāi)init和_fini锈候,或者對(duì)于使用GNU工具鏈的最新系統(tǒng)薄料,這些函數(shù)都是標(biāo)有__attribute __((constructor))或__attribute __((destructor))的任何函數(shù)。 在Windows中泵琳,相關(guān)的函數(shù)是帶有原因參數(shù)或DLL_PROCESS_ATTACH或DLL_PROCESS_DETACH的DllMain摄职。

最后,動(dòng)態(tài)加載可以使用"重復(fù)副本"方法進(jìn)行模板實(shí)例化获列,但要使用"在鏈接時(shí)編譯模板"方法要復(fù)雜得多琳钉,在這種情況下,"鏈接時(shí)間"是在程序運(yùn)行之后(可能在 與保存源代碼的機(jī)器不同)蛛倦。 查看編譯器和鏈接器文檔以了解解決此問(wèn)題的方法。

更多信息

該頁(yè)面的內(nèi)容故意跳過(guò)了許多有關(guān)鏈接器如何工作的詳細(xì)信息啦桌,因?yàn)槲野l(fā)現(xiàn)此處的描述級(jí)別涵蓋了程序員在其程序的鏈接步驟中遇到的日常問(wèn)題的95%溯壶。

翻譯中:Beginner's Guide to Linkers

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末及皂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子且改,更是在濱河造成了極大的恐慌验烧,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件又跛,死亡現(xiàn)場(chǎng)離奇詭異碍拆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)慨蓝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門感混,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人礼烈,你說(shuō)我怎么就攤上這事弧满。” “怎么了此熬?”我有些...
    開(kāi)封第一講書人閱讀 168,561評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵庭呜,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我犀忱,道長(zhǎng)募谎,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 59,782評(píng)論 1 298
  • 正文 為了忘掉前任阴汇,我火速辦了婚禮数冬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鲫寄。我一直安慰自己吉执,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布地来。 她就那樣靜靜地躺著戳玫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪未斑。 梳的紋絲不亂的頭發(fā)上咕宿,一...
    開(kāi)封第一講書人閱讀 52,394評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音蜡秽,去河邊找鬼府阀。 笑死,一個(gè)胖子當(dāng)著我的面吹牛芽突,可吹牛的內(nèi)容都是我干的试浙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼寞蚌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼田巴!你這毒婦竟也來(lái)了钠糊?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,852評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤壹哺,失蹤者是張志新(化名)和其女友劉穎抄伍,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體管宵,經(jīng)...
    沈念sama閱讀 46,409評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡截珍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了箩朴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岗喉。...
    茶點(diǎn)故事閱讀 40,615評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖隧饼,靈堂內(nèi)的尸體忽然破棺而出沈堡,到底是詐尸還是另有隱情,我是刑警寧澤燕雁,帶...
    沈念sama閱讀 36,303評(píng)論 5 350
  • 正文 年R本政府宣布诞丽,位于F島的核電站,受9級(jí)特大地震影響拐格,放射性物質(zhì)發(fā)生泄漏僧免。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評(píng)論 3 334
  • 文/蒙蒙 一捏浊、第九天 我趴在偏房一處隱蔽的房頂上張望懂衩。 院中可真熱鬧,春花似錦金踪、人聲如沸浊洞。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,470評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)法希。三九已至,卻和暖如春靶瘸,著一層夾襖步出監(jiān)牢的瞬間苫亦,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,571評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工怨咪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屋剑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,041評(píng)論 3 377
  • 正文 我出身青樓诗眨,卻偏偏與公主長(zhǎng)得像唉匾,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子匠楚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評(píng)論 2 359

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

  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,407評(píng)論 0 5
  • 深入理解計(jì)算機(jī)系統(tǒng)-第七章(鏈接)筆記 背景 鏈接是將各種代碼和數(shù)據(jù)部分收集起來(lái)并組合成為一個(gè)單一文件的過(guò)程 這個(gè)...
    Cool_Pomelo閱讀 710評(píng)論 0 0
  • 動(dòng)態(tài)鏈接肄鸽,在可執(zhí)行文件裝載時(shí)或運(yùn)行時(shí)卫病,由操作系統(tǒng)的裝載程序加載庫(kù)。大多數(shù)操作系統(tǒng)將解析外部引用(比如庫(kù))作為加載過(guò)...
    小5筒閱讀 5,515評(píng)論 0 3
  • 剛剛過(guò)去的這個(gè)周末益咬,在很多人很平靜的度過(guò)時(shí)逮诲,兩位英雄犧牲在了救人的歸程。臺(tái)風(fēng)來(lái)臨前的幽告,被困山上的24名人員全部被救...
    立鷹閱讀 576評(píng)論 0 0
  • 夏天吃著最愛(ài)的大西瓜梅鹦,心情就像乘坐著熱氣球在天空旅行!爽冗锁! 夏天棲息在大樹(shù)陰涼處齐唆,再有一根冰淇淋就更配了!
    靜享逆時(shí)光閱讀 454評(píng)論 1 0