自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

計(jì)算機(jī)大佬讓你徹底了解"深入理解計(jì)算機(jī)系統(tǒng)"

開發(fā)
本文將對(duì)鏈接過程做一個(gè)講解,了解鏈接的過程,可以幫助你理解計(jì)算機(jī)系統(tǒng)的底層原理,并解答你平時(shí)關(guān)于計(jì)算機(jī)怎樣識(shí)別并執(zhí)行程序的一些疑惑。

前言

當(dāng)我們點(diǎn)擊Xcode的運(yùn)行按鈕時(shí),你會(huì)注意到在界面頂端的提示欄上會(huì)出現(xiàn)“Building”的字樣,緊接著會(huì)出現(xiàn)“Linking”的字樣,我們知道Building是編譯過程,那這個(gè)Linking(鏈接)是什么過程呢?本文將對(duì)鏈接過程做一個(gè)講解,了解鏈接的過程,可以幫助你理解計(jì)算機(jī)系統(tǒng)的底層原理,并解答你平時(shí)關(guān)于計(jì)算機(jī)怎樣識(shí)別并執(zhí)行程序的一些疑惑。另外,本文也是后續(xù)篇章的基礎(chǔ),我們會(huì)由鏈接的知識(shí)延展出Mach-O文件、fishhook原理以及hook objc_msgSend的知識(shí)講解。

鏈接的基本概念

鏈接(linking)是將各種代碼和數(shù)據(jù)片段收集并組合成為一個(gè)單一文件的過程,這個(gè)文件可被加載(復(fù)制)到內(nèi)存并執(zhí)行。

鏈接可以執(zhí)行與編譯時(shí)(complie time),也就是源代碼被翻譯成機(jī)器代碼時(shí);也可以執(zhí)行于加載時(shí)(load time),也就是在程序被加載器(load-er)加載到內(nèi)存并執(zhí)行時(shí);甚至可以執(zhí)行在運(yùn)行時(shí)(run time),也就是由應(yīng)用程序來執(zhí)行。在早期的計(jì)算機(jī)系統(tǒng)中,鏈接是手動(dòng)執(zhí)行的。在現(xiàn)代系統(tǒng)中,鏈接是由叫做連接器(linker)的程序自動(dòng)執(zhí)行的。

鏈接的作用

鏈接器使分離編譯成為可能,我們不用將一個(gè)大型的應(yīng)用程序組織為一個(gè)巨大的源文件,而是可以把它分解為更小、更好管理的模塊,可以獨(dú)立地修改和編譯這些模塊。當(dāng)我們改變這些模塊中的一個(gè)時(shí),只需簡(jiǎn)單地重新編譯它,并重新鏈接應(yīng)用,而不必重新編譯其它文件。

下面的討論基于這樣的環(huán)境:一個(gè)運(yùn)行Linux的x86-64系統(tǒng),使用標(biāo)準(zhǔn)的ELF-64目標(biāo)文件格式。

編譯器驅(qū)動(dòng)程序

下面的C語言示例程序,由兩個(gè)源文件組成,main.c和sum.c。main函數(shù)初始化一個(gè)整數(shù)數(shù)組,然后調(diào)用sum函數(shù)來對(duì)數(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)會(huì)提供編譯器驅(qū)動(dòng)程序(compile driver),包含語言預(yù)處理器、編譯器、匯編器和鏈接器。首先編譯器驅(qū)動(dòng)程序會(huì)對(duì)main.c與sum.c文件的源代碼進(jìn)行翻譯,翻譯過程如下:

其中,main.o稱為可重定位目標(biāo)文件。

之后,編譯系統(tǒng)會(huì)運(yùn)行鏈接器ld,將main.o和sum.o以及一些必要的系統(tǒng)目標(biāo)文件組合起來,創(chuàng)建一個(gè)可以執(zhí)行目標(biāo)文件,這個(gè)過程是靜態(tài)鏈接,過程如下:

再之后,操作系統(tǒng)會(huì)調(diào)用加載器(loader),將可執(zhí)行文件prog中的代碼和數(shù)據(jù)復(fù)制到內(nèi)存中,然后執(zhí)行。

靜態(tài)鏈接

靜態(tài)鏈接器(static linker)以一組可重定位目標(biāo)文件作為輸入,生成一個(gè)完全鏈接的、可以加載和運(yùn)行的可執(zhí)行目標(biāo)文件。輸入的可重定位目標(biāo)文件由各種不同的代碼和數(shù)據(jù)節(jié)(section)組成,每一節(jié)都是一個(gè)連續(xù)的字節(jié)序列。指令在一節(jié)中,初始化了的全局變量在另一個(gè)節(jié)中,而未初始化的變量又在另外一節(jié)中。

為了構(gòu)造可執(zhí)行文件,鏈接器必須完成兩個(gè)重要的任務(wù):

  • 符號(hào)解析(symbol resolution)。目標(biāo)文件定義和引用符號(hào),一個(gè)個(gè)符號(hào)對(duì)應(yīng)一個(gè)函數(shù)或一個(gè)全局變量或一個(gè)靜態(tài)變量(即C語言中以static屬性聲明的變量)。符號(hào)解析的目的是將每個(gè)符號(hào)引用正好和一個(gè)符號(hào)定義關(guān)聯(lián)起來。
  • 重定位(relocation)。編譯器和匯編器生成從地址0開始的代碼和數(shù)據(jù)節(jié)。鏈接器通過把每個(gè)符號(hào)定義與一個(gè)內(nèi)存位置關(guān)聯(lián)起來,從而重定位這些節(jié),然后修改所有對(duì)這些符號(hào)的引用,使它們指向這個(gè)內(nèi)存位置。

目標(biāo)文件純粹是字節(jié)塊的集合,這些塊中,有些包含程序代碼,有些包含數(shù)據(jù),而有些則是引導(dǎo)鏈接器和加載器的數(shù)據(jù)結(jié)構(gòu)。鏈接器將這些塊連接起來,確定被連接塊的運(yùn)行時(shí)位置,并且修改代碼和數(shù)據(jù)塊中的各種位置。

目標(biāo)文件

目標(biāo)文件有三種形式:

  • 可重定位目標(biāo)文件。包含二進(jìn)制代碼和數(shù)據(jù),其形式可以在編譯時(shí)與其他可重定位目標(biāo)文件合并起來,創(chuàng)建一個(gè)可執(zhí)行目標(biāo)文件。
  • 可執(zhí)行目標(biāo)文件。包含二進(jìn)制代碼和數(shù)據(jù),其形式可以被直接復(fù)制到內(nèi)存并執(zhí)行。
  • 共享目標(biāo)文件。一種特殊類型的可重定位目標(biāo)文件,可以在加載或者運(yùn)行時(shí)被動(dòng)態(tài)的加載進(jìn)內(nèi)存并鏈接。動(dòng)態(tài)庫就是這種形式的。

目標(biāo)文件的生成方式:

  • 編譯器和匯編器生成可重定位目標(biāo)文件(包括共享目標(biāo)文件)。
  • 鏈接器生成可執(zhí)行目標(biāo)文件。

目標(biāo)文件的格式:

  • 在iOS和MacOS-X中,目標(biāo)文件的格式是Mach-O格式。
  • X86-64 Linux和Unix系統(tǒng)使用可執(zhí)行可連接格式ELF。

可重定位目標(biāo)文件

下上展示了一個(gè)典型的ELF可重定位目標(biāo)文件的格式。ELF頭包含很多信息,包括生成該文件的系統(tǒng)的字節(jié)大小,字節(jié)順序,ELF頭的大小,目標(biāo)文件的類型,機(jī)器類型等等。節(jié)頭部表描述了不同節(jié)的位置和大小。

加載ELF頭和節(jié)頭部表的是節(jié):

  • .text:已編譯程序的機(jī)器代碼。
  • .rodata:只讀數(shù)據(jù),比如 printf語句中的格式串和開關(guān)語句的跳轉(zhuǎn)表。
  • .data:已初始化的全局和靜態(tài)C變量。局部C變量在運(yùn)行時(shí)被保存在棧中,既不出現(xiàn)在,data節(jié)中,也不出現(xiàn)在.bss節(jié)中
  • .bss:未初始化的全局和靜態(tài)C變量,以及所有被初始化為0的全局或靜態(tài)變量。在目標(biāo)文件中這個(gè)節(jié)不占據(jù)實(shí)際的空間,它僅僅是一個(gè)占位符。目標(biāo)文件格式區(qū)分已初始化和未初始化變量是為了空間效率:在目標(biāo)文件中,未初始化變量不需要占據(jù)任何實(shí)際的磁盤空間。運(yùn)行時(shí),在內(nèi)存中分配這些變量,初始值為0。
  • .symtab:一個(gè)符號(hào)表,它存放在程序中定義和引用的函數(shù)和全局變量的信息。一些程序員錯(cuò)誤地認(rèn)為必須通過-g選項(xiàng)來編譯一個(gè)程序,才能得到符號(hào)表信息。實(shí)際上,每個(gè)可重定位目標(biāo)文件在. symtab中都有一張符號(hào)表(除非程序員特意用 STRIP命令去掉它)。然而,和編譯器中的符號(hào)表不同, symtab符號(hào)表不包含局部變量的條目。
  • .rel.text:一個(gè).text節(jié)中位置的列表,當(dāng)鏈接器把這個(gè)目標(biāo)文件和其他文件組合時(shí),需要修改這些位置。一般而言,任何調(diào)用外部函數(shù)或者引用全局變量的指令都需要修改。另一方面,調(diào)用本地函數(shù)的指令則不需要修改。注意,可執(zhí)行目標(biāo)文件中并不需要重定位信息,因此通常省略,除非用戶顯式地指示鏈接器包含這些信息。
  • .rel.data:被模塊引用或定義的所有全局變量的重定位信息。一般而言,任何已初始化的全局變量,如果它的初始值是一個(gè)全局變量地址或者外部定義函數(shù)的地址,都需要被修改。
  • .debug:一個(gè)調(diào)試符號(hào)表,其條目是程序中定義的局部變量和類型定義,程序中定義和引用的全局變量,以及原始的C源文件。只有以-g選項(xiàng)調(diào)用編譯器驅(qū)動(dòng)程序時(shí),才會(huì)得到這張表。
  • .line:原始C源程序中的行號(hào)和.text節(jié)中機(jī)器指令之間的映射。只有以-g選項(xiàng)調(diào)用編譯器驅(qū)動(dòng)程序時(shí),才會(huì)得到這張表。
  • .strtab:一個(gè)字符串表,其內(nèi)容包括. symtab和, debug節(jié)中的符號(hào)表,以及節(jié)頭部中的節(jié)名字。字符串表就是以nu11結(jié)尾的字符串的序列。

符號(hào)和符號(hào)表

每個(gè)可重定位目標(biāo)模塊(目標(biāo)文件)m都有一個(gè)符號(hào)表,它包含m定義和引用的符號(hào)的信息。在鏈接器的上下文中,有三種不同的符號(hào):

  • 由模塊m定義并能被其它模塊引用的全局符號(hào)。這些符號(hào)對(duì)應(yīng)于非靜態(tài)的C函數(shù)和全局變量。
  • 由其它模塊定義并被模塊m引用的全局符號(hào)。這些符號(hào)稱為外部符號(hào),對(duì)應(yīng)于在其它模塊中定義的非靜態(tài)C函數(shù)和全局變量。
  • 只被模塊m定義和引用的局部符號(hào)。它們對(duì)應(yīng)于帶static屬性的C函數(shù)和全局變量。這些符號(hào)在模塊m中任何位置都可見,但是不能被其它模塊引用。

.symtab中的符號(hào)表不包含非靜態(tài)程序變量的任何符號(hào),這些程序變量符號(hào)在棧中被管理,鏈接器對(duì)此類符號(hào)不感興趣。

如何解析多重定義的全局符號(hào)

鏈接器的輸入是一組可重定位目標(biāo)模塊。每個(gè)模塊定義一組符號(hào),有些是局部的(只對(duì)定義該符號(hào)的模塊可見),有些是全局的(對(duì)其他模塊也可見)。如果多個(gè)模塊定義同名的全局符號(hào),會(huì)發(fā)生什么呢?下面是 Linux編譯系統(tǒng)采用的方法。

在編譯時(shí),編譯器向匯編器輸出每個(gè)全局符號(hào),或者是強(qiáng)( strong)或者是弱(weak),而匯編器把這個(gè)信息隱含地編碼在可重定位目標(biāo)文件的符號(hào)表里。函數(shù)和已初始化的全局變量是強(qiáng)符號(hào),未初始化的全局變量是弱符號(hào)。

根據(jù)強(qiáng)弱符號(hào)的定義,Linux鏈接器使用下面的規(guī)則來處理多重定義的符號(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è)。

靜態(tài)庫

迄今為止,我們都是假設(shè)鏈接器讀取一組可重定位目標(biāo)文件,并把它們鏈接起來,輸出一個(gè)可執(zhí)行目標(biāo)文件。實(shí)際上,所有的編譯系統(tǒng)都提供一種機(jī)制,將所有相關(guān)的目標(biāo)模塊打包成一個(gè)單獨(dú)的文件,稱為靜態(tài)庫。靜態(tài)庫可以用做鏈接器的輸入,當(dāng)鏈接器構(gòu)造一個(gè)輸出的可執(zhí)行目標(biāo)文件時(shí),它只復(fù)制靜態(tài)庫里被應(yīng)用程序引用的目標(biāo)模塊,這就減少了可執(zhí)行文件在磁盤和內(nèi)存中的大小。在Linux系統(tǒng)中,靜態(tài)庫由后綴.a標(biāo)識(shí)。

重定位

一旦鏈接器完成了符號(hào)解析這一步,就把代碼中的每個(gè)符號(hào)引用和正好一個(gè)符號(hào)定義(即它的一個(gè)輸入目標(biāo)模塊中的一個(gè)符號(hào)表?xiàng)l目)關(guān)聯(lián)起來。此時(shí),鏈接器就知道它的輸入目標(biāo)模塊中的代碼節(jié)和數(shù)據(jù)節(jié)的確切大小。現(xiàn)在就可以開始重定位步驟了,在這個(gè)步驟中,將合并輸入模塊,并為每個(gè)符號(hào)分配運(yùn)行時(shí)地址。重定位由兩步組成:

  • 重定位節(jié)和符號(hào)定義。在這一步中,鏈接器將所有相同類型的節(jié)合并為同一類型的新的聚合節(jié)。例如,來自所有輸入模塊的.data節(jié)被全部合并成一個(gè)節(jié),這個(gè)節(jié)成為輸出的可執(zhí)行目標(biāo)文件的.data節(jié)。然后,鏈接器將運(yùn)行時(shí)內(nèi)存地址賦給新的聚合節(jié),賦給輸人模塊定義的每個(gè)節(jié),以及賦給輸人模塊定義的每個(gè)符號(hào)。當(dāng)這一步完成時(shí),程序中的每條指令和全局變量都有唯一的運(yùn)行時(shí)內(nèi)存地址了。
  • 重定位節(jié)中的符號(hào)引用。在這一步中,鏈接器修改代碼節(jié)和數(shù)據(jù)節(jié)中對(duì)每個(gè)符號(hào)的引用,使得它們指向正確的運(yùn)行時(shí)地址。要執(zhí)行這一步,鏈接器依賴于可重定位目標(biāo)模塊中稱為重定位條目(relocation entry)的數(shù)據(jù)結(jié)構(gòu)。

當(dāng)匯編器生成一個(gè)目標(biāo)模塊時(shí),它并不知道數(shù)據(jù)和代碼最終將放在內(nèi)存中的什么位置,它也并不知道這個(gè)模塊引用的任何外部定義的函數(shù)或者全局變量的位置。所以,無論何時(shí)匯編器遇到對(duì)最終位置的目標(biāo)引用,它就會(huì)生成一個(gè)重定位條目,告訴鏈接器在將目標(biāo)文件合并成可執(zhí)行目標(biāo)文件時(shí)如何修改這個(gè)引用。

可執(zhí)行目標(biāo)文件 與 加載可執(zhí)行目標(biāo)文件

見《深入理解計(jì)算機(jī)系統(tǒng)》

動(dòng)態(tài)鏈接共享庫

靜態(tài)庫由一些缺點(diǎn):靜態(tài)庫需要定期維護(hù)和更新;每個(gè)程序都會(huì)使用一些通用的標(biāo)準(zhǔn)函數(shù),在運(yùn)行時(shí),這些函數(shù)的代碼會(huì)被復(fù)制到每個(gè)運(yùn)行進(jìn)程的文本段中,在一個(gè)運(yùn)行上百個(gè)進(jìn)行的典型系統(tǒng)上,這是對(duì)內(nèi)存資源的浪費(fèi)。

共享庫(shared library)是致力于解決靜態(tài)庫缺陷的一個(gè)現(xiàn)代創(chuàng)新產(chǎn)物。共享庫是一個(gè)目標(biāo)模塊,在運(yùn)行或加載時(shí),可以加載到任意內(nèi)存地址,并和一個(gè)在內(nèi)存中的程序鏈接起來。這個(gè)過程稱為動(dòng)態(tài)鏈接,是由一個(gè)叫做動(dòng)態(tài)鏈接器(dynamic linker)的程序來執(zhí)行的。在Linux系統(tǒng)中,共享庫通常由.so后綴標(biāo)識(shí)。

共享庫以兩種不同的方式來共享的。首先,在任何給定的文件系統(tǒng)中,對(duì)于一個(gè)庫只有一個(gè).so文件。所有引用該哭的可執(zhí)行目標(biāo)文件共享這個(gè).so文件中的代碼和數(shù)據(jù),而不是像靜態(tài)庫的內(nèi)容那樣被復(fù)制和嵌入到引用它們的可執(zhí)行文件中。其次,在內(nèi)存中,一個(gè)共享庫的.text節(jié)的一個(gè)副本可以被不同的正在運(yùn)行的進(jìn)程共享。


責(zé)任編輯:未麗燕 來源: 今日頭條
相關(guān)推薦

2014-04-10 09:40:51

System 360計(jì)算機(jī)計(jì)算機(jī)系統(tǒng)

2018-10-08 14:10:46

2023-10-27 07:47:37

計(jì)算機(jī)內(nèi)存模型

2020-07-02 08:17:12

存儲(chǔ)IO

2017-07-14 15:40:28

2020-08-12 14:18:33

計(jì)算機(jī)Linux互聯(lián)網(wǎng)

2021-01-27 14:18:17

量子計(jì)算傳統(tǒng)計(jì)算量子機(jī)器

2021-02-20 20:55:06

USB接口總線

2012-09-12 09:59:26

云計(jì)算超級(jí)計(jì)算機(jī)數(shù)據(jù)中心

2012-03-31 11:12:19

計(jì)算機(jī)好文章

2018-07-05 09:25:07

系統(tǒng)存儲(chǔ)層次

2015-09-30 11:22:19

計(jì)算機(jī)大數(shù)據(jù)

2023-10-11 18:30:39

Web系統(tǒng)程序

2012-06-20 10:40:36

量子計(jì)算機(jī)

2024-04-02 11:39:40

二進(jìn)制儲(chǔ)存器網(wǎng)絡(luò)

2009-07-01 09:03:39

Linux綠壩操作系統(tǒng)

2009-07-04 09:38:07

2021-12-08 00:02:46

網(wǎng)絡(luò)安全計(jì)算機(jī)

2009-01-05 13:23:25

軟件設(shè)計(jì)師筆記

2012-02-29 10:02:59

IBM量子計(jì)算機(jī)超級(jí)計(jì)算機(jī)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)