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