C語言目標(biāo)文件的詳細(xì)講解
前言
一個 C 語言程序經(jīng)編譯器和匯編器生成可重定位目標(biāo)文件,再經(jīng)鏈接器生成可執(zhí)行目標(biāo)文件。那么目標(biāo)文件中存放的是什么?我們的源代碼在經(jīng)編譯以后又是怎么存儲的?
文章為 《深入理解計算機(jī)系統(tǒng)》的讀書筆記,更為詳細(xì)的內(nèi)容可以閱讀原書。
目標(biāo)文件分類
目標(biāo)文件有三種形式:
- 可重定位目標(biāo)文件
包含二進(jìn)制代碼和數(shù)據(jù),其形式可以在編譯時與其他可重定位目標(biāo)文件合并,創(chuàng)建一個可執(zhí)行目標(biāo)文件 - 可執(zhí)行目標(biāo)文件
包含二進(jìn)制代碼和數(shù)據(jù),其形式可以被直接復(fù)制到內(nèi)存并執(zhí)行 - 共享目標(biāo)文件
一種特殊可重定位目標(biāo)文件,可以在加載或運(yùn)行時被動態(tài)地加載進(jìn)內(nèi)存并鏈接
可重定位目標(biāo)文件
下圖為一個典型的 ELF 可重定位目標(biāo)文件的格式。
ELF 頭以一個 16 字節(jié)的序列開始,這個序列包含了:生成該文件的系統(tǒng)的字的大小和字節(jié)順序、目標(biāo)文件的類型、機(jī)器類型、節(jié)頭部表(也稱段表)的文件偏移,以及節(jié)頭部表中條目的大小和數(shù)量等。
節(jié)頭部表是由描述文件中各個節(jié)的條目(entry)組成的數(shù)組。節(jié)頭部表描述了文件中各個節(jié)在文件中的偏移位置及節(jié)的屬性等,從節(jié)頭部表里面可以得到每個節(jié)的所有信息。
- .text:已編譯程序的機(jī)器代碼
- .rodata:只讀數(shù)據(jù)
- 比如 printf 語句中的格式串(%d\n)
- .data:已初始化的全局和靜態(tài) C 變量
- 局部 C 變量在運(yùn)行時被保存在棧中,既不在 .data 節(jié)中,也不在 .bss 節(jié)中
- .bss:未初始化的全局和靜態(tài) C 變量,以及所有被初始化為 0 的全局或靜態(tài)變量
- 目標(biāo)文件格式區(qū)分已初始化和未初始化變量是為了空間效率:
- 未初始化變量不需要占據(jù)任何實(shí)際的磁盤空間,因?yàn)闆]有初始化,值沒有意義,也就不必表示每個值
- .bss 段只是為未初始化全部變量和局部靜態(tài)變量預(yù)留位置,它并沒有內(nèi)容,也不占據(jù)實(shí)際的空間,僅僅是個占位符
- .bss 段的大小存放在節(jié)頭部表中
- 可以使用 readelf -S test 來查看 test 可執(zhí)行程序節(jié)頭部表
- 運(yùn)行時,在內(nèi)存中分配這些變量,并初始化為 0
- 目標(biāo)文件格式區(qū)分已初始化和未初始化變量是為了空間效率:
- .symtab:一個符號表,它存放在程序中定義和引用的函數(shù)和全局變量的信息
- 包含局部靜態(tài)變量
- 不包含局部非靜態(tài)變量,這些符號在運(yùn)行時在棧中被管理,鏈接器不關(guān)心這些
- .rel.text:一個 .text 節(jié)中位置的列表,當(dāng)鏈接器把這個目標(biāo)文件和其他文件組合時,需要修改這些位置
- .rel.data:被模塊引用或定義的所有全局變量的重定位信息
- .debug:一個調(diào)試符號表,其條目是程序中定義的局部變量和類型定義,程序中定義和引用的全局變量,以及原始的 C 源文件
- 只有在編譯時加入 -g 選項才會得到這張表
- .line:原始 C 源程序中的行號和 .text 節(jié)中機(jī)器指令之間的映射
- 只有在編譯時加入 -g 選項才會得到這張表
- .strtab:一個字符串表,其中包含 .symtab 和 .debug 節(jié)中的符號表,以及節(jié)頭部中的節(jié)名字
- 字符串表就是以 NULL 結(jié)尾的字符串序列
分段的優(yōu)點(diǎn)
為什么要這么麻煩,把程序的指令和數(shù)據(jù)分開存放?
- 程序被裝載進(jìn)內(nèi)存后,數(shù)據(jù)和指令分別被映射到兩個虛擬區(qū)域
- 由于數(shù)據(jù)是可讀寫的,指令是只讀的,權(quán)限可以分別設(shè)置成可讀寫和只讀
- 可以防止程序指令被改寫
- 對現(xiàn)代 CPU 而言緩存是極其重要的
- 數(shù)據(jù)區(qū)和指令區(qū)分離有利于提高程序的局部性,從而提高緩存命中率
- 啟動多個相同進(jìn)程時
- 可以共享一份指令,節(jié)省內(nèi)存
符號和符號表
每個可重定位目標(biāo)模塊 m 都有一個符號表,它包含 m 定義和引用的符號的信息。在鏈接器的上下文中,有三種不同的符號:
- 由模塊 m 定義并能被其他模塊引用的全局符號
- 全局鏈接器符號對應(yīng)于非靜態(tài)的 C 函數(shù)和全局變量
- 由其他模塊定義并被模塊 m 引用的全局符號
- 這些符號稱為外部符號,對應(yīng)于在其他模塊中定義的非靜態(tài) C 函數(shù)和全局變量
- 只被模塊 m 定義和引用的符號
- 它們對應(yīng)于帶 static 屬性的 C 函數(shù)和全局變量,這些符號在模塊 m 中任何位置都可見,但是不能被其他模塊引用
符號表是由匯編器用編譯器輸出到匯編語言 .s 文件中的符號構(gòu)造的。.symtab
節(jié)中包含 ELF 符號表,這張符號表包含一個條目的數(shù)組。下面是 ELF 符號表條目格式:
typedef struct { int name; // String table offset char type:4, // Function or data (4 bits) binding:4;// Local or global (4 bits) char reserved; // Unused short section; // Section header index long value; // Section offset or absolute address long size; // Object size in bytes } Elf64_Symbol;
name 是字符串表中的字節(jié)偏移,指向符號的字符串名字。value 是符號的位置。對于可重定位目標(biāo)文件,value 是距定義目標(biāo)的起始位置的偏移。對于可執(zhí)行目標(biāo)文件,該值是一個絕對運(yùn)行時地址。size 是目標(biāo)的大小(以字節(jié)為單位)。
每個符號都被分配到目標(biāo)文件的某個節(jié),由 section 字段表示,該字段是一個到節(jié)頭部表的索引。有三個特殊的的偽節(jié),它們在節(jié)頭部表中是沒有條目的:
- ABS:代表不該被重定位的符號
- UNDEF:代表未定義的符號,也就是在本目標(biāo)模塊中引用,但定義在其他地方的符號
- COMMON:表示還未被分配位置的未初始化的數(shù)據(jù)目標(biāo)
- 對于該類型符號,value 字段給出對齊要求,size 給出最小的大小
只有可重定位目標(biāo)文件中才有偽節(jié),可執(zhí)行目標(biāo)文件中是沒有的。
GCC 將可重定位目標(biāo)文件中的符號分配到 COMMON 和 .bss 的規(guī)則:
- COMMON:未初始化的全局變量
- .bss:未初始化的靜態(tài)變量,以及初始化為 0 的全局或靜態(tài)變量
符號解析
對于局部符號的解析是非常簡單的,因?yàn)榫幾g器只允許每個模塊中每個局部符號有一個定義。不過,對全局符號的引用解析就麻煩的多。當(dāng)編譯器遇到一個不是在當(dāng)前模塊中定義的符號(變量或函數(shù)名)時,會假設(shè)該符號是在其他某個模塊中定義的,生成一個鏈接器符號表條目,并把它交給鏈接器處理。
如果多個模塊定義同名的全局符號,會發(fā)生什么呢?下面是 Linux 編譯系統(tǒng)采用的方法。
在編譯時,編譯器向匯編器輸出每個全局符號,或者是強(qiáng)或者是弱,匯編器把這個信息編碼在可重定位目標(biāo)文件的符號表里。函數(shù)和已初始化的全局變量是強(qiáng)符號,未初始化的全局變量是弱符號。
根據(jù)強(qiáng)弱符號的定義,Linux 鏈接器使用如下規(guī)則來處理多重定義的符號名:
- 不允許有多個同名強(qiáng)符號
- 如果有一個強(qiáng)符號和多個弱符號同名,選擇強(qiáng)符號
- 如果有多個弱符號同名,從弱符號中任意選擇一個
重定位
當(dāng)匯編器生成一個目標(biāo)模塊時,它并不知道數(shù)據(jù)和代碼最終將放在內(nèi)存中的什么位置。它也不知道這個模塊引用的任何外部定義的函數(shù)或者全局變量的位置。所以,無論何時匯編器遇到對最終位置位置的目標(biāo)引用,它就會生成一個重定位條目,告訴鏈接器在將目標(biāo)文件合并成可執(zhí)行文件時如何修改這個引用。代碼的重定位條目放在 .rel.text 中,已初始化數(shù)據(jù)的重定位條目放在 .rel.data 中。
下圖為 ELF 重定位條目的格式:
符號解析完成后,代碼中的每個符號引用和正好一個符號定義關(guān)聯(lián)起來。此時,就可以開始重定位了。在重定位中,將合并輸入模塊,并為每個符號分配運(yùn)行時地址。重定位由兩步組成:
- 重定位節(jié)和符號定義
- 鏈接器將所有相同類型的節(jié)合并為同一類型的新的聚合節(jié)
- 鏈接器將運(yùn)行時內(nèi)存地址賦給新的聚合節(jié),賦給輸入模塊定義的每個節(jié),以及賦給輸入模塊定義的每個符號
- 重定位節(jié)中的符號引用
- 鏈接器修改代碼節(jié)和數(shù)據(jù)節(jié)中對每個符號的引用,使得它們指向正確的運(yùn)行時地址
可執(zhí)行目標(biāo)文件
下圖為一個典型的 ELF 可執(zhí)行文件:
可執(zhí)行文件加載到內(nèi)存:
總結(jié)
到此這篇關(guān)于C語言目標(biāo)文件的文章就介紹到這了,更多相關(guān)C語言目標(biāo)文件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++17 使用 std::string_view避免字符串拷貝優(yōu)化程序性能
這篇文章主要介紹了C++17 使用 std::string_view避免字符串拷貝優(yōu)化程序性能,幫助大家提高程序運(yùn)行速度,感興趣的朋友可以了解下2020-10-10C/C++實(shí)現(xiàn)的MD5哈希校驗(yàn)的示例代碼
MD5算法是一種廣泛使用的 Hash 算法,常用于確保信息傳輸?shù)耐暾耘c一致性,本文主要介紹了C/C++實(shí)現(xiàn)的MD5哈希校驗(yàn)的示例代碼,具有一定的參考價值,感興趣的可以了解一下2023-10-10