tft每日頭條

 > 科技

 > 計算機的十大冷知識

計算機的十大冷知識

科技 更新时间:2024-12-26 21:39:25
前言

當我們點擊Xcode的運行按鈕時,你會注意到在界面頂端的提示欄上會出現“Building”的字樣,緊接着會出現“Linking”的字樣,我們知道Building是編譯過程,那這個Linking(鍊接)是什麼過程呢?本文将對鍊接過程做一個講解,了解鍊接的過程,可以幫助你理解計算機系統的底層原理,并解答你平時關于計算機怎樣識别并執行程序的一些疑惑。另外,本文也是後續篇章的基礎,我們會由鍊接的知識延展出Mach-O文件、fishhook原理以及hook objc_msgSend的知識講解。

鍊接的基本概念

鍊接(linking)是将各種代碼和數據片段收集并組合成為一個單一文件的過程,這個文件可被加載(複制)到内存并執行。

鍊接可以執行與編譯時(complie time),也就是源代碼被翻譯成機器代碼時;也可以執行于加載時(load time),也就是在程序被加載器(load-er)加載到内存并執行時;甚至可以執行在運行時(run time),也就是由應用程序來執行。在早期的計算機系統中,鍊接是手動執行的。在現代系統中,鍊接是由叫做連接器(linker)的程序自動執行的。

鍊接的作用

鍊接器使分離編譯成為可能,我們不用将一個大型的應用程序組織為一個巨大的源文件,而是可以把它分解為更小、更好管理的模塊,可以獨立地修改和編譯這些模塊。當我們改變這些模塊中的一個時,隻需簡單地重新編譯它,并重新鍊接應用,而不必重新編譯其它文件。

下面的讨論基于這樣的環境:一個運行Linux的x86-64系統,使用标準的ELF-64目标文件格式。

編譯器驅動程序

下面的C語言示例程序,由兩個源文件組成,main.c和sum.c。main函數初始化一個整數數組,然後調用sum函數來對數組元素求和。

// 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; }

大多數的編譯系統會提供編譯器驅動程序(compile driver),包含語言預處理器、編譯器、彙編器和鍊接器。首先編譯器驅動程序會對main.c與sum.c文件的源代碼進行翻譯,翻譯過程如下:

計算機的十大冷知識(計算機大佬讓你徹底了解)1

image

其中,main.o稱為可重定位目标文件。

之後,編譯系統會運行鍊接器ld,将main.o和sum.o以及一些必要的系統目标文件組合起來,創建一個可以執行目标文件,這個過程是靜态鍊接,過程如下:

計算機的十大冷知識(計算機大佬讓你徹底了解)2

image

再之後,操作系統會調用加載器(loader),将可執行文件prog中的代碼和數據複制到内存中,然後執行。

靜态鍊接

靜态鍊接器(static linker)以一組可重定位目标文件作為輸入,生成一個完全鍊接的、可以加載和運行的可執行目标文件。輸入的可重定位目标文件由各種不同的代碼和數據節(section)組成,每一節都是一個連續的字節序列。指令在一節中,初始化了的全局變量在另一個節中,而未初始化的變量又在另外一節中。

為了構造可執行文件,鍊接器必須完成兩個重要的任務:

  • 符号解析(symbol resolution)。目标文件定義和引用符号,一個個符号對應一個函數或一個全局變量或一個靜态變量(即C語言中以static屬性聲明的變量)。符号解析的目的是将每個符号引用正好和一個符号定義關聯起來。
  • 重定位(relocation)。編譯器和彙編器生成從地址0開始的代碼和數據節。鍊接器通過把每個符号定義與一個内存位置關聯起來,從而重定位這些節,然後修改所有對這些符号的引用,使它們指向這個内存位置。

目标文件純粹是字節塊的集合,這些塊中,有些包含程序代碼,有些包含數據,而有些則是引導鍊接器和加載器的數據結構。鍊接器将這些塊連接起來,确定被連接塊的運行時位置,并且修改代碼和數據塊中的各種位置。

目标文件

目标文件有三種形式:

  • 可重定位目标文件。包含二進制代碼和數據,其形式可以在編譯時與其他可重定位目标文件合并起來,創建一個可執行目标文件。
  • 可執行目标文件。包含二進制代碼和數據,其形式可以被直接複制到内存并執行。
  • 共享目标文件。一種特殊類型的可重定位目标文件,可以在加載或者運行時被動态的加載進内存并鍊接。動态庫就是這種形式的。

目标文件的生成方式:

  • 編譯器和彙編器生成可重定位目标文件(包括共享目标文件)。
  • 鍊接器生成可執行目标文件。

目标文件的格式:

  • 在iOS和MacOS-X中,目标文件的格式是Mach-O格式。
  • X86-64 Linux和Unix系統使用可執行可連接格式ELF。
可重定位目标文件

計算機的十大冷知識(計算機大佬讓你徹底了解)3

image.png

下上展示了一個典型的ELF可重定位目标文件的格式。ELF頭包含很多信息,包括生成該文件的系統的字節大小,字節順序,ELF頭的大小,目标文件的類型,機器類型等等。節頭部表描述了不同節的位置和大小。

加載ELF頭和節頭部表的是節:

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

每個可重定位目标模塊(目标文件)m都有一個符号表,它包含m定義和引用的符号的信息。在鍊接器的上下文中,有三種不同的符号:

  • 由模塊m定義并能被其它模塊引用的全局符号。這些符号對應于非靜态的C函數和全局變量。
  • 由其它模塊定義并被模塊m引用的全局符号。這些符号稱為外部符号,對應于在其它模塊中定義的非靜态C函數和全局變量。
  • 隻被模塊m定義和引用的局部符号。它們對應于帶static屬性的C函數和全局變量。這些符号在模塊m中任何位置都可見,但是不能被其它模塊引用。

.symtab中的符号表不包含非靜态程序變量的任何符号,這些程序變量符号在棧中被管理,鍊接器對此類符号不感興趣。

如何解析多重定義的全局符号

鍊接器的輸入是一組可重定位目标模塊。每個模塊定義一組符号,有些是局部的(隻對定義該符号的模塊可見),有些是全局的(對其他模塊也可見)。如果多個模塊定義同名的全局符号,會發生什麼呢?下面是 Linux編譯系統采用的方法。

在編譯時,編譯器向彙編器輸出每個全局符号,或者是強( strong)或者是弱(weak),而彙編器把這個信息隐含地編碼在可重定位目标文件的符号表裡。函數和已初始化的全局變量是強符号,未初始化的全局變量是弱符号。

根據強弱符号的定義,Linux鍊接器使用下面的規則來處理多重定義的符号名

  • 規則1:不允許有多個同名的強符号。
  • 規則2:如果有一個強符号和多個弱符号同名,那麼選擇強符号。
  • 規則3:如果有多個弱符号同名,那麼從這些弱符号中任意選擇一個。
靜态庫

迄今為止,我們都是假設鍊接器讀取一組可重定位目标文件,并把它們鍊接起來,輸出一個可執行目标文件。實際上,所有的編譯系統都提供一種機制,将所有相關的目标模塊打包成一個單獨的文件,稱為靜态庫。靜态庫可以用做鍊接器的輸入,當鍊接器構造一個輸出的可執行目标文件時,它隻複制靜态庫裡被應用程序引用的目标模塊,這就減少了可執行文件在磁盤和内存中的大小。在Linux系統中,靜态庫由後綴.a标識。

重定位

一旦鍊接器完成了符号解析這一步,就把代碼中的每個符号引用和正好一個符号定義(即它的一個輸入目标模塊中的一個符号表條目)關聯起來。此時,鍊接器就知道它的輸入目标模塊中的代碼節和數據節的确切大小。現在就可以開始重定位步驟了,在這個步驟中,将合并輸入模塊,并為每個符号分配運行時地址。重定位由兩步組成:

  • 重定位節和符号定義。在這一步中,鍊接器将所有相同類型的節合并為同一類型的新的聚合節。例如,來自所有輸入模塊的.data節被全部合并成一個節,這個節成為輸出的可執行目标文件的.data節。然後,鍊接器将運行時内存地址賦給新的聚合節,賦給輸人模塊定義的每個節,以及賦給輸人模塊定義的每個符号。當這一步完成時,程序中的每條指令和全局變量都有唯一的運行時内存地址了。
  • 重定位節中的符号引用。在這一步中,鍊接器修改代碼節和數據節中對每個符号的引用,使得它們指向正确的運行時地址。要執行這一步,鍊接器依賴于可重定位目标模塊中稱為重定位條目(relocation entry)的數據結構。

當彙編器生成一個目标模塊時,它并不知道數據和代碼最終将放在内存中的什麼位置,它也并不知道這個模塊引用的任何外部定義的函數或者全局變量的位置。所以,無論何時彙編器遇到對最終位置的目标引用,它就會生成一個重定位條目,告訴鍊接器在将目标文件合并成可執行目标文件時如何修改這個引用。

可執行目标文件 與 加載可執行目标文件

見《深入理解計算機系統》

動态鍊接共享庫

靜态庫由一些缺點:靜态庫需要定期維護和更新;每個程序都會使用一些通用的标準函數,在運行時,這些函數的代碼會被複制到每個運行進程的文本段中,在一個運行上百個進行的典型系統上,這是對内存資源的浪費。

共享庫(shared library)是緻力于解決靜态庫缺陷的一個現代創新産物。共享庫是一個目标模塊,在運行或加載時,可以加載到任意内存地址,并和一個在内存中的程序鍊接起來。這個過程稱為動态鍊接,是由一個叫做動态鍊接器(dynamic linker)的程序來執行的。在Linux系統中,共享庫通常由.so後綴标識。

共享庫以兩種不同的方式來共享的。首先,在任何給定的文件系統中,對于一個庫隻有一個.so文件。所有引用該哭的可執行目标文件共享這個.so文件中的代碼和數據,而不是像靜态庫的内容那樣被複制和嵌入到引用它們的可執行文件中。其次,在内存中,一個共享庫的.text節的一個副本可以被不同的正在運行的進程共享。

計算機的十大冷知識(計算機大佬讓你徹底了解)4

image.png

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关科技资讯推荐

热门科技资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved