Linux系統(tǒng)中編譯、鏈接的基石-ELF文件:扒開它的層層外衣,從字節(jié)碼的粒度來探索
大家好,我是 ELF 文件,大名叫 Executable and Linkable Format。經(jīng)常在 Linux 系統(tǒng)中開發(fā)的小伙伴們,對于我肯定是再熟悉不過了,特別是那些需要了解編譯、鏈接的家伙們,估計已經(jīng)把我研究的透透的。為了結(jié)識更多的小伙伴,我的開放日,我會像洋蔥一樣,一層一層地?fù)荛_我的心,讓更多的小伙伴來了解我,歡迎大家前來圍觀。以前啊,我看到有些小伙伴在研究我的時候,看一下頭部的匯總信息,然后再瞅幾眼 Section 的布局,就當(dāng)做熟悉我了。從科學(xué)的態(tài)度上來說,這是遠(yuǎn)遠(yuǎn)不夠的,未達(dá)究竟。當(dāng)你面對編譯、鏈接的詳細(xì)過程時,還是會一臉懵逼。
作為一種文件,那么肯定就需要遵守一定的格式,我也不例外。從宏觀上看,可以把我拆卸成四個部分:
就拿鏈接器和加載器來說吧,這兩個家伙的性格是不一樣的,它們看我的眼光也是不一樣的。鏈接器在看我的時候,它的眼睛里只有 3 部分內(nèi)容:
- 可執(zhí)行文件:被操作系統(tǒng)中的加載器從硬盤上讀取,載入到內(nèi)存中去執(zhí)行;
- 目標(biāo)文件:被鏈接器讀取,用來產(chǎn)生一個可執(zhí)行文件或者共享庫文件;
- 共享庫文件:在動態(tài)鏈接的時候,由 ld-linux.so 來讀取;
Sections
, 它改了個名字,叫做 Segments
(段)。換湯不換藥,本質(zhì)上都是一樣一樣的。可以理解為:一個 Segment 可能包含一個或者多個 Sections,就像下面這樣:2
點內(nèi)容就可以了:還有一點差點忘記給你提個醒了:在
- 一個 ELF 文件一共由 4 個部分組成;
- 鏈接器和加載器,它們在使用我的時候,只會使用它們感興趣的部分;
Linux
系統(tǒng)中,會有不同的數(shù)據(jù)結(jié)構(gòu)來描述上面所說的每部分內(nèi)容。我知道有些小伙伴比較性急,我先把這幾個結(jié)構(gòu)體告訴你。初次見面,先認(rèn)識一下即可,千萬不要深究哦。描述 ELF header 的結(jié)構(gòu)體:頭部內(nèi)容,就相當(dāng)于是一個總管,它決定了這個完整的 ELF 文件內(nèi)部的所有信息,比如:
你是不是有點納悶,好像沒有說 Sections(從鏈接器角度看) 或者 Segments(從加載器角度看) 在 ELF 文件的什么地方。為了方便描述,我就把
- 這是一個 ELF 文件;
- 一些基本信息:版本,文件類型,機(jī)器類型;
- Program header table(程序頭表)的開始地址,在整個文件的什么地方;
- Section header table(節(jié)頭表)的開始地址,在整個文件的什么地方;
Sections
和 Segments
全部統(tǒng)一稱為 Sections 啦!其實是這樣的,在一個 ELF 文件中,存在很多個 Sections,這些 Sections 的具體信息,是在 Program header table
或者 Section head table
中進(jìn)行描述的。Section head table
來舉例吧:假如一個 ELF 文件中一共存在 4
個 Section: .text、.rodata、.data、.bss
,那么在 Section head table
中,將會有 4
個 Entry(條目)來分別描述這 4 個 Section 的具體信息(嚴(yán)格來說,不止 4 個 Entry,因為還存在一些其他輔助的 Sections),就像下面這樣:// mymath.c
int my_add(int a, int b)
{
return a b;
}
// main.c
#include
extern int my_add(int a, int b);
int main()
{
int i = 1;
int j = 2;
int k = my_add(i, j);
printf("k = %d \n", k);
}
從剛才的描述中可以知道:動態(tài)庫文件 libmymath.so
, 目標(biāo)文件 main.o
和 可執(zhí)行文件 main
,它們都是 ELF 文件,只不過屬于不同的類型。這里就以可執(zhí)行文件 main 來拆解它!我們首先用指令 readelf -h main
來看一下 main 文件中,ELF header
的信息。readelf 這個工具,可是一個好東西啊!一定要好好的利用它。
ELF header
中描述的所有內(nèi)容了。這個內(nèi)容與結(jié)構(gòu)體 Elf32_Ehdr
中的成員變量是一一對應(yīng)的!有沒有發(fā)現(xiàn)圖中第 15 行顯示的內(nèi)容:Size of this header: 52 (bytes)
。也就是說:ELF header
部分的內(nèi)容,一共是 52
個字節(jié)。那么我就把開頭的這 52
個字節(jié)碼給你看一下。這回,我用 od -Ax -t x1 -N 52 main
這個指令來讀取 main 中的字節(jié)碼,簡單解釋一下其中的幾個選項:-Ax: 顯示地址的時候,用十六進(jìn)制來表示。如果使用 -Ad,意思就是用十進(jìn)制來顯示地址;-t -x1: 顯示字節(jié)碼內(nèi)容的時候,使用十六進(jìn)制(x),每次顯示一個字節(jié)(1);-N 52:只需要讀取 52 個字節(jié);
52
個字節(jié)的內(nèi)容,你可以對照上面的結(jié)構(gòu)體中每個字段來解釋了。首先看一下前 16 個字節(jié)。在結(jié)構(gòu)體中的第一個成員是 unsigned char e_ident[EI_NIDENT];
,EI_NIDENT
的長度是 16
,代表了 EL header
中的開始 16
個字節(jié),具體含義如下:0 - 15 個字節(jié)main
文件中顯示的是 1
,代表小端格式。啥意思呢,看下面這張圖就明白了:36
個字節(jié)(52 - 16 = 32),也以這樣的字節(jié)碼含義畫出來:16 - 31 個字節(jié):在一個
ELF
文件中,存在很多字符串,例如:變量名、Section名稱、鏈接器加入的符號等等,這些字符串的長度都是不固定的,因此用一個固定的結(jié)構(gòu)來表示這些字符串,肯定是不現(xiàn)實的。于是,聰明的人類就想到:把這些字符串集中起來,統(tǒng)一放在一起,作為一個獨立的 Section
來進(jìn)行管理。13
就可以了,表示:這個字符串從偏移 13 個字節(jié)處開始。main
文件中的字符串表,在 ELF header
的最后 2 個字節(jié)是 0x1C 0x00
,它對應(yīng)結(jié)構(gòu)體中的成員 e_shstrndx
,意思是這個 ELF 文件中,字符串表是一個普通的 Section,在這個 Section 中,存儲了 ELF
文件中使用到的所有的字符串。既然是一個 Section
,那么在 Section header table
中,就一定有一個表項 Entry 來描述它,那么是哪一個表項呢?這就是 0x1C 0x00
這個表項,也就是第 28
個表項。這里,我們還可以用指令 readelf -S main
來看一下這個 ELF
文件中所有的 Section
信息:28
個 Section,描述的正是字符串表 Section:Section
在 ELF
文件中的偏移地址是 0x0016ed
,長度是 0x00010a
個字節(jié)。下面,我們從 ELF header
的二進(jìn)制數(shù)據(jù)中,來推斷這信息。那我就來演示一下:如何通過
ELF header
中提供的信息,把字符串表這個 Section
給找出來,然后把它的字節(jié)碼打印出來給各位看官瞧瞧。要想打印字符串表 Section
的內(nèi)容,就必須知道這個 Section
在 ELF
文件中的偏移地址。要想知道偏移地址,只能從 Section head table
中第 28
個表項描述信息中獲取。要想知道第 28
個表項的地址,就必須知道 Section head table
在 ELF
文件中的開始地址,以及每一個表項的大小。正好最后這 2
個需求信息,在 ELF header
中都告訴我們了,因此我們倒著推算,就一定能成功。ELF header
中的第 32
到 35
字節(jié)內(nèi)容是:F8 17 00 00
(注意這里的字節(jié)序,低位在前),表示的就是 Section head table
在 ELF 文件中的開始地址(e_shoff
)。0x000017F8 = 6136
,也就是說 ?Section head table
的開始地址位于 ELF
文件的第 6136
個字節(jié)處。知道了開始地址,再來算一下第 28
個表項 Entry 的地址。ELF header
中的第 46、47
字節(jié)內(nèi)容是:28 00
,表示每個表項的長度是 0x0028 = 40
個字節(jié)。注意這里的計算都是從 0
開始的,因此第 28
個表項的開始地址就是:6136 28 * 40 = 7256
,也就是說用來描述字符串表這個 Section
的表項,位于 ELF
文件的 7256
字節(jié)的位置。od -Ad -t x1 -j 7256 -N 40 main
。其中的 -j 7256
選項,表示跳過前面的 7256
個字節(jié),也就是我們從 main
這個 ELF
文件的 7256
字節(jié)處開始讀取,一共讀 40
個字節(jié)。40
個字節(jié)的內(nèi)容,就對應(yīng)了 Elf32_Shdr
結(jié)構(gòu)體中的每個成員變量:4
個字段:sh_name: 暫時不告訴你,馬上就解釋到了;sh_type:表示這個 Section 的類型,3 表示這是一個 string table;sh_offset: 表示這個 Section,在 ELF 文件中的偏移量。0x000016ed = 5869,意思是字符串表這個 Section 的內(nèi)容,從 ELF 文件的 5869 個字節(jié)處開始;sh_size:表示這個 Section 的長度。0x0000010a = 266 個字節(jié),意思是字符串表這個 Section 的內(nèi)容,一共有 266 個字節(jié)。還記得剛才我們使用
readelf
工具,讀取到字符串表 Section
在 ELF 文件中的偏移地址是 0x0016ed
,長度是 0x00010a
個字節(jié)嗎?與我們這里的推斷是完全一致的!Section
在 ELF
文件中的偏移量以及長度,那么就可以把它的字節(jié)碼內(nèi)容讀取出來。執(zhí)行指令: od -Ad -t c -j 5869 -N 266 main
,所有這些參數(shù)應(yīng)該不用再解釋了吧?!Section
中存儲的全部是字符串?剛才沒有解釋 sh_name
這個字段,它表示字符串表這個 Section
本身的名字,既然是名字,那一定是個字符串。但是這個字符串不是直接存儲在這里的,而是存儲了一個索引,索引值是 0x00000011
,也就是十進(jìn)制數(shù)值 17
。現(xiàn)在我們來數(shù)一下字符串表 Section
內(nèi)容中,第 17
個字節(jié)開始的地方,存儲的是什么?不要偷懶,數(shù)一下,是不是看到了:“.shstrtab” 這個字符串(\0是字符串的分隔符)?!好了,如果看到這里,你全部都能看懂,那么關(guān)于字符串表這部分的內(nèi)容,說明你已經(jīng)完全理解了,給你一百個贊?。。?/p>從下面的這張圖(指令:
readelf -S main
):14
個表項中,加載(虛擬)地址是 0x08048470
,它位于 ELF
文件中的偏移量是 0x000470
,長度是 0x0001b2
個字節(jié)。那我們就來試著讀一下其中的內(nèi)容。首先計算這個表項 Entry
的地址:6136 14 * 40 = 6696
。然后讀取這個表項 Entry
,讀取指令是 od -Ad -t x1 -j 6696 -N 40 main
:5
個字段內(nèi)容:sh_name: 這回應(yīng)該清楚了,表示代碼段的名稱在字符串表 Section 中的偏移位置。0x9B = 155 字節(jié),也就是在字符串表 Section 的第 155 字節(jié)處,存儲的就是代碼段的名字?;剡^頭去找一下,看一下是不是字符串 “.text”;sh_type:表示這個 Section 的類型,1(SHT_PROGBITS) 表示這是代碼;sh_addr:表示這個 Section 加載的虛擬地址是 0x08048470,這個值與 ELF header 中的 e_entry 字段的值是相同的;sh_offset: 表示這個 Section,在 ELF 文件中的偏移量。0x00000470 = 1136,意思是這個 Section 的內(nèi)容,從 ELF 文件的 1136 個字節(jié)處開始;sh_size:表示這個 Section 的長度。0x000001b2 = 434 個字節(jié),意思是代碼段一共有 434 個字節(jié)。以上這些分析結(jié)構(gòu),與指令
readelf -S main
讀取出來的完全一樣!Section
中的字符串時,不要告訴我,你真的是從 0
開始數(shù)到 155
?。】梢杂嬎阋幌拢鹤址淼?span>開始地址是 5869
(十進(jìn)制),加上 155
,結(jié)果就是 6024
,所以從 6024
開始的地方,就是代碼段的名稱,也就是 “.text”。知道了以上這些信息,我們就可以讀取代碼段的字節(jié)碼了.使用指令:od -Ad -t x1 -j 1136 -N 434 main
即可。內(nèi)容全部是黑乎乎的的字節(jié)碼,我就不貼出來了。文章的開頭,我就介紹了:我是一個通用的文件結(jié)構(gòu),鏈接器和加載器在看待我的時候,眼光是不同的。為了對
Program header
有更感性的認(rèn)識,我還是先用 readelf
這個工具來從總體上看一下 main
文件中的所有段信息。執(zhí)行指令:readelf -l main
,得到下面這張圖:布局如下圖所示:
- 這是一個可執(zhí)行程序;
- 入口地址是 0x8048470;
- 一共有 9 個 Program header,是從 ELF 文件的 52 個偏移地址開始的;
Section
與 Segment
本質(zhì)上是一樣的,可以理解為:一個 Secgment 由一個或多個 Sections 組成。從上圖中可以看到,第 2
個 program header
這個段,由那么多的 Section
組成,這下更明白一些了吧?!從圖中還可以看到,一共有 2
個 LOAD
類型的段:ELF header
中得知如下信息:通過計算,得到可讀、可執(zhí)行的
- 字段
e_phoff
:Program header table 位于 ELF 文件偏移 52 個字節(jié)的地方。- 字段
e_phentsize
: 每一個表項的長度是 32 個字節(jié);- 字段
e_phnum
: 一共有 9 個表項 Entry;
LOAD
段,位于偏移量 116
字節(jié)處。執(zhí)行讀取指令:od -Ad -t x1 -j 116 -N 32 main
:p_type: 段的類型,1: 表示這個段需要加載到內(nèi)存中;p_offset: 段在 ELF 文件中的偏移地址,這里值為 0,表示這個段從 ELF 文件的頭部開始;p_vaddr:段加載到內(nèi)存中的虛擬地址 0x08048000;p_paddr:段加載的物理地址,與虛擬地址相同;p_filesz: 這個段在 ELF 文件中,占據(jù)的字節(jié)數(shù),0x0744 = 1860 個字節(jié);p_memsz:這個段加載到內(nèi)存中,需要占據(jù)的字節(jié)數(shù),0x0744= 1860 個字節(jié)。注意:有些段是不需要加載到內(nèi)存中的;經(jīng)過上述分析,我們就知道:從
ELF
文件的第 1
到 第 1860
個字節(jié),都是屬于這個 LOAD
段的內(nèi)容。在被執(zhí)行時,這個段需要被加載到內(nèi)存中虛擬地址為 0x08048000
這個地方,從這里開始,又是一個全新的故事了。到這里,我已經(jīng)像洋蔥一樣,把自己的層層外衣都扒開,讓你看到最細(xì)的顆粒度了,這下子,您是否對我有足夠的了解了呢?
2
個重點即可:鏈接器和加載器也都是按照這樣的原理來解析 ELF 文件的,明白了這些道理,后面在學(xué)習(xí)具體的鏈接、加載過程時,就不會迷路啦!- EOF -
- ELF header 描述了文件的總體信息,以及兩個 table 的相關(guān)信息(偏移地址,表項個數(shù),表項長度);
- 每一個 table 中,包括很多個表項 Entry,每一個表項都描述了一個 Section/Segment 的具體信息。