一笋妥、簡介
編譯器編譯源代碼后生成的文件叫目標(biāo)文件(Linux下.o或Windows下.obj)挖垛,從結(jié)構(gòu)上講,它是已經(jīng)編譯后的可執(zhí)行文件格式痴昧,只是還沒有經(jīng)過鏈接的過程,其中可能有些符號(hào)或有些地址還沒有被調(diào)整冠王。其實(shí)它本身就是按照可執(zhí)行文件格式存儲(chǔ)的赶撰,只是跟真正的可執(zhí)行文件在結(jié)構(gòu)上稍有不同。
二柱彻、目標(biāo)文件是什么樣的
目標(biāo)文件中的內(nèi)容至少有編譯后的機(jī)器指令代碼豪娜、數(shù)據(jù),除了這些數(shù)據(jù)以外哟楷,還包括了鏈接時(shí)所需要的一些信息瘤载,比如符號(hào)表、調(diào)試信息卖擅、字符串等鸣奔。
一般目標(biāo)文件將這些信息按不同的屬性,以“節(jié)”的形式存儲(chǔ)惩阶,有時(shí)候也叫“段”挎狸,在一般情況下,他們都表示一個(gè)一定長度的區(qū)域断楷。
程序源代碼被編譯后的機(jī)器指令經(jīng)常被放在代碼段(Code Section
)里锨匆,代碼段常見的名字有.code
或.text
。
全局變量和局部靜態(tài)變量數(shù)據(jù)經(jīng)常放在數(shù)據(jù)段(Data Section
)冬筒,數(shù)據(jù)段的名字一般都叫.data
恐锣。
File Header |
---|
.text section |
.data section |
.bss section |
對(duì)照上面的表格來看,一般C語言的編譯后執(zhí)行語句都編譯成機(jī)器代碼舞痰,保存在.text
段土榴。
已初始化的全局變量和局部靜態(tài)變量都保存在.data
段。
未初始化的全局變量和局部靜態(tài)變量都保存在.bss
段匀奏。
我們知道未初始化的全局變量和局部靜態(tài)變量默認(rèn)值都為0鞭衩,本來他們也可以被在.data
段的,但是因?yàn)樗鼈兌际?娃善,示意圖為它們?cè)?code>.data段分配空間并且存放數(shù)據(jù)0是沒有必要的论衍。程序運(yùn)行的時(shí)候它們的確是要占內(nèi)存空間的,并且可執(zhí)行文件必須記錄所有未初始化的全局變量和局部靜態(tài)變量的大小綜合聚磺,記為.bss
段坯台,所以.bss
段只是為未初始化的全局變量和局部靜態(tài)變量預(yù)留位置而已,它并沒有內(nèi)容瘫寝,所以它在文件中也不占據(jù)空間蜒蕾。
總體來說,程序源代碼被編譯以后主要分成兩種段:程序指令和程序數(shù)據(jù)焕阿,代碼段屬于程序指令咪啡,而數(shù)據(jù)段和.bss
段屬于程序數(shù)據(jù)。
為什么要將數(shù)據(jù)和指令的存放分開暮屡?
1.當(dāng)程序被裝載后撤摸,數(shù)據(jù)和指令分別被映射到兩個(gè)虛存區(qū)域,由于數(shù)據(jù)區(qū)對(duì)于進(jìn)程來說是可讀寫的褒纲,而指令區(qū)域?qū)τ谶M(jìn)程來說是只讀的准夷,所以這兩個(gè)虛存區(qū)域的權(quán)限可以被分別設(shè)置成可讀寫和只讀,這樣可以防止程序的指令被有意或無意的改寫莺掠。
2.另外一方面對(duì)于現(xiàn)代CPU來說衫嵌,他們有著極為強(qiáng)大的緩存(Cache)體系,由于緩存在現(xiàn)代的計(jì)算機(jī)中地位非常重要彻秆,所以程序必須盡量提高緩存的命中率楔绞。指令區(qū)和數(shù)據(jù)區(qū)的分離有利于提高程序的局部性。
3.第3個(gè)原因是最重要的原因唇兑,就是當(dāng)系統(tǒng)中運(yùn)行著多個(gè)該程序的副本時(shí)墓律,它們的指令都是一樣的,所以內(nèi)存中只須要保存一份改程序的指令部分幔亥,對(duì)于指令這種只讀的區(qū)域是這樣耻讽,對(duì)于其他的只讀數(shù)據(jù)也一樣,比如很多程序里面帶有的圖標(biāo)帕棉、圖片针肥、文本等資源也是屬于可以共享的,這樣就可以節(jié)省大量內(nèi)存香伴。
3慰枕、深入目標(biāo)文件的具體細(xì)節(jié)
下面列舉一個(gè)簡單的C語言demo(SimpleSection.c)作為分析對(duì)象:
int printf(const char *format);
int global_init_var = 84;
int global_uninit_var;
void func1(int i){
printf("%d\n", i);
}
int main(void){
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
使用GCC來編譯這個(gè)文件:
$ gcc -c SimpleSection.c
使用binutils
的odjdump
工具查看object
內(nèi)部的結(jié)構(gòu):
$ objdump -h SimpleSection.o
會(huì)得到如下幾個(gè)段的信息:
0 .text
1 .data
2 .bss
3 .rodata
4 .comment
5 .note.GNU-stack
逐個(gè)來看著幾個(gè)段,看看他們包含了什么內(nèi)容即纲。
(1)代碼段
包含的是func1()
和main()
的指令具帮。
(2)數(shù)據(jù)段和只讀數(shù)據(jù)段
.data
段保存的是那些已經(jīng)初始化了的全局靜態(tài)變量和局部靜態(tài)變量,分別是global_init_var
和static_var
。
.rodata
存放的是只讀數(shù)據(jù)蜂厅,不光在語義上支持C++
的const
關(guān)鍵字匪凡,而且操作系統(tǒng)在加載的時(shí)候可以將.rodata
段的屬性映射成只讀,這樣對(duì)于這個(gè)段的任何修改操作都會(huì)作為非法操作處理掘猿。保證了程序的安全性病游。
(3)BSS段
.bss
段存放的是未初始化的全局變量和局部靜態(tài)變量,上述代碼中只有static_var2
被存放在了.bss
段稠通,而global_uninit_var
卻沒有被存放在任何段衬衬,只是一個(gè)未定義的COMMON
符號(hào)。這其實(shí)跟不同的語言與不同的編譯器實(shí)現(xiàn)有關(guān)改橘,有些編譯器會(huì)將全局的未初始化變量存放在目標(biāo)文件的.bss
段滋尉,有些則不存放,只是預(yù)留一個(gè)未定義的全局變量符號(hào)飞主,等到最終鏈接成可執(zhí)行文件的時(shí)候再在.bss
段分配空間兼砖。
(4)其他段
常用的段名 | 說明 |
---|---|
.rodata1 | 存放只讀數(shù)據(jù),比如字符串常量、全局const變量 |
.comment | 存放的是編譯器版本信息 |
.debug | 調(diào)試信息 |
.dynamic | 動(dòng)態(tài)鏈接信息 |
.hash | 符號(hào)哈希表 |
.line | 調(diào)試時(shí)的行號(hào)表 |
.note | 額外的編譯器信息 |
.strtab | 字符串表,用于存儲(chǔ)ELF文件中用到的各種字符串 |
自定義段
正常情況下官辽,GCC編譯出來的目標(biāo)文件中纳账,代碼會(huì)被放到.text
段中,全局變量和靜態(tài)變量會(huì)被放到.data
和.bss
段,有時(shí)候我們希望變量或某些代碼能夠放到你所指定的段中去,GCC提供了一個(gè)擴(kuò)展機(jī)制,使得程序員可以指定變量所處的段:
__attribute__((section("Foo"))) int glbal = 42;
__attribute__((section("BAR"))) void foo()
{
...
}
鏈接的接口—符號(hào)
鏈接過程的本質(zhì)就是要把多個(gè)不同的目標(biāo)文件之間相互“粘”在一起眼姐,在鏈接中,我們將函數(shù)和變量統(tǒng)稱為符號(hào)佩番,函數(shù)名或變量名就是符號(hào)名众旗,我們可以將符號(hào)看做是鏈接中的粘合劑,整個(gè)鏈接過程正是基于符號(hào)才能正確完成趟畏,鏈接過程中很關(guān)鍵的一部分就是符號(hào)的管理贡歧,每一個(gè)目標(biāo)文件都會(huì)有一個(gè)相應(yīng)的符號(hào)表,這個(gè)表里面記錄了目標(biāo)文件中所用到的所有符號(hào)赋秀,每個(gè)定義的符號(hào)有一個(gè)對(duì)應(yīng)的值利朵,叫做符號(hào)值,對(duì)于變量和函數(shù)來說猎莲,符號(hào)值就是他們的地址绍弟。
函數(shù)簽名
函數(shù)簽名包含了一個(gè)函數(shù)的信息,包括函數(shù)名著洼,它的參數(shù)類型樟遣,它所在的類和名稱空間及其他信息而叼。函數(shù)簽名用于識(shí)別不同的函數(shù)。