ELF格式文檔詳解
一,ELF格式綜述
ELF(Executable and Linkable Format)是Linux下的一種格式標准,Linux中的ELF格式文件一共有四種:
●可重定位文件(Relocatable File):這類文件包含了代碼和數據,可被用來鏈接成可執行文件或者共享目錄文件,擴展名為.o
●可執行文件(Executable File):這類文件包含了可以直接執行的程序,一般沒有擴展名
●共享目錄文件(Shared Object File):這類文件包含了代碼和數據,擴展名為.so。
共享目錄文件一般可以在以下兩種情況下使用:
①鏈接器可以使用這類文件,與其他的共享目錄文件和可重定位文件進行鏈接,生成新的目標文件;
②動態鏈接器可以將幾個這種共享目錄文件與可執行文件結合,作為進程映像的一部分來運行。
●核心轉儲文件(Core Dump File):當進程意外終止時,系統可以將該進程的地址空間的內容以及終止時的一些其他信息轉儲在核心轉儲文件中。
一個ELF格式的文件一般包括四個部分:ELF頭表(Head Table),程序頭表(Program Head Table),節頭表(Section Head Table),節(Section)。也就是說:ELF文件中有三個“表“,其他的都是一個一個叫做”節“的東西。
實際上一個ELF文件中我們最終要用的東西都是存在各個節里邊的,三個表的存在都是為了讓我們能夠快速的找到我們需要用的節,然后從節里邊讀取我們需要的數據。那這三個表各自都有什么作用呢?
首先說ELF頭表。在ELF文件中,只有ELF頭表的位置是固定的,它一定在文件開頭位置的,其他三個部分的位置都由ELF頭表中的信息給出。我們一進到ELF文件中,立馬就能看到ELF頭表,所以ELF頭表的作用就很顯而易見了,它就是要告訴來客:我這個文件是ELF格式的,我能干啥(可執行文件,鏈接庫還是可重定位文件),我需要用多少多少位的操作系統,你得用什么樣的CPU架構來運行我,我是按什么字節序來存數據的等等。對了,你要找誰,我認識的人不多,我只認識倆哥們,他倆認識的人多,我給你喊他們去!
到這里ELF頭表就可以告訴來客:程序頭表在哪,節頭表在哪,你要找誰去問他倆吧。
記住:我們的目的是找到最終的某一個節,現在我們知道“程序頭表“和”節頭表“在哪了,而他倆知道”節“們在那,於是我們就去問這倆頭表。
”程序頭表“說:”我知道他們宿舍都在哪,但是我不知道他們每個人住幾號床。“
“節頭表”說:“你找誰問我吧,我知道他們每個人住幾號床。“
也就是說,程序頭表只可以找到同類的節的聚集地,但是節頭表可以細致的找到每一個節的位置。
那有節頭表就夠了嘛,能找到每一個節的位置不就行了,要程序頭表有啥用?
這就跟ELF文件的兩個階段有關系了,其實節頭表和程序頭表分別是在不同階段起作用的:鏈接時用到節頭表,執行時用到程序頭表。

(Note:雖然這張圖中程序頭表和節頭表的位置畫在確定的位置,但是實際上除了ELF頭表的位置固定以外 其他部分的位置都是可以變動的,具體位置要看ELF頭表中給出的信息。)
在鏈接的時候,需要用到節頭表,執行的時候,需要用到程序頭表。
Linux中保存節的時候是以“頁”為單位的(物理內存也同樣是分頁的,他們一一對應,一頁一般是4096字節,也就是4k),不夠一頁的也要占用一頁的空間,由於節的數量有很多,如果每個節都單獨存的話,就會有很多不夠一頁而占了一頁的情況發生,這樣會浪費很多空間。
當我們站在操作系統裝載可執行文件的角度看問題時,可以發現它實際上並不關心可執行文件各個節所包含的實際內容,操作系統只關心一些跟裝載相關的問題,最主要的是節的權限(可讀、可寫、可執行)。在ELF格式文件中,權限的組合種類主要分為下面的這三種:
①可讀可執行(如代碼)
②可讀可寫(如數據)
③只讀
所以有一個很好的解決辦法就是把性質相同的節(Section)“捆綁”到一起變成一個“段(Segment)”,段在保存的時候按頁為單位(一個段里有若干個”節“,具體個數不一定),段內的各個節是首尾相接依次排放好的,這樣做可以明顯減少頁面內部的碎片,起到節省內存空間的作用。
Note:英文“Section”和“Segment”都可以翻譯為“段”,但由於一般很少對“Segment”進行分析,所以有的書中就把“Section”稱作“段”,而對於“Segment”就不做翻譯,直接用英文進行描述。鑒於這篇文章對“Section“和”Segment“都有比較多的描述,所以按照網上的一些帖子的習慣,把”Section“翻譯為”節“,而把”Segment“翻譯為”段“。
舉個例子:
假如現在有兩個節,分別占4097字節和255字節,那么在保存他們兩個節的時候就需要用三頁。如果他們的權限是一樣的,那我們就可以把這兩個節捆綁到一塊作為一個段來進行映射,這個段共占4352字節,這樣就只需要用兩頁就足夠了。這樣就節省了一頁,也就是4096字節的空間。當文件中有很多節的時候,這樣做就能節省十分可觀的內存空間了。
這時就不能再用節頭表來記錄這些內容的信息了,而需要用程序頭表(Program Head Table)來記錄這些段的信息。程序頭表中列出了一系列段的信息,在創建進程映像的時候,有這些段的信息就足夠了(相當於查宿舍的時候不用找到每一個人,找到宿舍就夠了)。
所以我們拿到的不同的ELF格式文件中:
可執行文件一定有程序頭表,但是不一定有節頭表,因為可執行文件只需要進行執行操作,而執行時用到的是程序頭表。
可重定位文件一定有節頭表,但是不一定有程序頭表,因為可重定位文件只需要進行鏈接操作,而鏈接時用到的是節頭表。
二,ELF格式內部結構詳細分析
(Note:ELF格式的定義在/usr/include目錄下的“elf.h”頭文件中。)
一.ELF文件頭表
ELF頭表是一個這樣的結構體:

其中:
●E_ ident[EI_NIDENT]是一個數組,它有16個字節,這16個字節被分為好幾個部分,分別代表不同的涵義:
⑴e_ ident[0]-e_ ident[3]:這四個字節被稱為“魔數“(Magic),它們分別是“0x7F“,”E“,”L“,”F“的ASCII碼,對所有ELF格式的文件來說,這四個字節是固定的,也就是說,只有當這四個字節是這些值的時候,才代表這個文件是ELF格式的文件。相當於ELF文件的身份證。
(2)e_ ident[4]:這個字節給出了這個ELF文件是多少位的:當取“0x1”時,代表這個ELF文件是32位的;當取”0x2”時,代表這個ELF文件是64位的。
(3)e_ ident[5]:這個字節給出了這個ELF文件使用的字節順序:當取”0x1”時,代表小端序,當取”0x2”時,代表大端序。
(4)e_ ident[6]:這個字節給出了這個ELF文件頭的版本,當前只有一個版本,所以這個字節目前只能取”0x1”
(5)e_ ident[7]到e_ ident[15]:這九個字節目前還沒有實際意義,一般為0,也有的文件會用它們做一些特殊的標記。
- ●e_ type用於區分ELF文件的類型:(系統通過這個數值來判斷文件類型,而不是后綴名)

“ET_REL“代表此ELF文件為可重定位文件
”ET_EXEC“代表ELF文件為可執行文件
”ET_DYN“代表此ELF文件為動態鏈接庫
ET_CORE”代表此ELF文件是核心轉儲文件
- ●e_ machine給出了文件所需的CPU體系結構:

(上圖只截取了一部分)
ELF 文件格式被設計成可以在多個平台下使用。這並不表示同一個ELF文件可以在不同的平台下使用,而是表示不同平台下的ELF文件都遵循同一套ELF標准。e_ machine 成員就表示該ELF文件的平台屬性,比如3表示該ELF文件只能在Intel x86 機器下使用,這也是我們最常見的情況。
- ●e_ version給出了ELF的版本號,當前只有一個版本,所以只能取”0x1”
- ●e_ entry給出了程序在虛擬內存中的入口地址,是程序開始執行的位置。這一值只在程序映射到內存當中后才有效。(只有可執行文件的e_ entry才是有意義的,對於可重定位文件來說,這個值為0,因為它沒有入口地址)
- ●e_ phoff給出了程序頭表相對於ELF文件開始的處偏移量,根據這個偏移量我們可以找到程序頭表的位置
- ●e_ shoff給出了節頭表相對於ELF文件開始處的偏移量,根據這個偏移量我們可以找到節頭表的位置
- ●e_ flags給出了這個ELF文件平台相關屬性的標志位。
- ●e_ ehsize給出了這個ELF頭表的大小,以字節為單位
- ●e_ phentsize給出了程序頭表中一項的長度,以字節為單位,
- ●e_ phnum給出了程序頭表中有幾條信息(有幾個段)
- ●e_ shentsize給出了節頭表中一項的長度,以字節為單位
- ●e_ shnum給出了節頭表中有幾條信息(有幾個節)
- ●e_ shstrndx這里保存了包含各節名稱的字符串表在節頭表中的索引位置
實例:
下面是讀取了“/bin“目錄下的“ls“文件的結果:

我們可以看到Magic(魔數)的前四位是“7f“,”E“,”L“,”F“的ASCII碼,代表這個文件是ELF文件;第五位是”02“,代表這個ELF文件是64位的;第六位是”01“,代表這個ELF文件是小端序保存的(因為我生成這個文件時用的cpu是x86架構的);第七位是“01”,代表這個ELF文件的版本。(這些信息在下邊的Class,Data和Version中也有體現)
OS/ABI給出了創建這個ELF文件的系統是UNIX-System V
Type給出了這個ELF文件是一個可執行文件
Machine給出了這個ELF文件需要的系統結構是X86-64位
Entry point address給出這個ELF文件在虛擬內存中的入口地址
Start of program headers給出了這個ELF文件的程序頭表的偏移量
Strat of section headers給出了這個ELF文件的節頭表的偏移量
Size of program header給出了這個ELF文件的程序頭表的大小,以字節為單位
Number of program headers給出了這個ELF文件的程序頭表中有幾條信息(有幾個段)
Size of section headers給出了這個ELF文件的節頭表的大小,以字節為單位
Number of section headers給出了這個ELF文件的節頭表中有幾條信息(有幾個節)
二.節頭表
節頭表是一個結構體數組(一個數組,這個數組的每個元素都是一個結構體),數組中每一個元素的結構體是這樣的:

- ●sh_ name給出了節的名稱,節名是一個字符串,保存在.shstrtab節中。sh_ name的值是這個字符串在字符串表.shstrtab中的偏移量
- ●sh_ type給出了節的類型,具體類型如下:

(1) SHT_NULL代表該節為空
(2) SHT_PROGBITS類型的節中具體存的是什么東西,需要程序給出具體的解釋(保存程序,代碼,數據的節都是這種類型)。
(3) SHT_SYMTAB類型的節是一個符號表,里邊保存了一系列例如“int a;“語句中的”a”這樣的符號。
(4) SHT_STRTAB類型的節是一個字符串表,其中保存了一系列的字符串
(5) SHT_RELA類型的節中保存了重定位信息
(6) SHT_HASH類型的節中保存了一個散列表,用於實現符號的快速訪問
(7) SHT_DYNAMIC類型的節中保存了一個結構數組,該結構數組中包含了大量的動態鏈接相關信息,后面會着重分析
(8) SHT_NOTE類型的節中保存了一些提示性的信息。
(9) SHT_NOBITS類型的節在文件中沒有內容,比如.bss節。
(10) SHT_REL類型的節保存了重定位信息
(11) SHT_DYNSYM類型的節保存了動態鏈接的符號表
●sh_ flags:給出了節在虛擬進程空間的屬性,如是否可寫,是否可執行等等:

●sh_ addr:這個成員給出了節的虛擬地址。如果該節可以被加載,則這個數值代表該節被加載后在進程地址空間中的虛擬地址;否則為0。
●sh_ offset:這個成員給出了節在ELF文件中的偏移量。如果該節存在於ELF文件中,則這個數值代表節在ELF文件中的偏移量;否則無意義。比如.bss節的sh_ offset成員就沒有意義。
●sh_ link 和sh_ info:這兩個成員是節的鏈接信息,如果節的類型是與鏈接相關的(不論是動態鏈接還是靜態鏈接),比如重定位表,符號表等節,那么這兩個成員的意義如下圖:(對於其他節,這兩個成員沒有實際意義)

實例:/bin目錄下的ls文件的節頭信息太長了,我只截取了一部分。

[Nr]一列給出了對應節在節頭表數組中的下標;后面每一列都是按上面所講的給出對應的信息。
三.程序頭表
程序頭表也一個結構體數組,數組中的每一個元素內的結構體是這樣的:

其中:
- ●p_ type給出了這個段的類型:

⑴PT_NULL代表空段
(2)PT_LOAD代表這是一個可裝載段
(3)PT_DYNAMIC代表這個段中存的是用於動態鏈接的信息
(4)PT_INTERP代表了這個段中存的是表明鏈接器絕對路徑的字符串
(5)PT_NOTE代表了這個段中存的是專有的編譯器信息
- ●p_ flags給出了該段的訪問權限
(1)PF_R代表可讀
(2)PF_W代表可寫
(3)PF_X代表可執行
- ●p_ offset給出了該段在ELF文件中的偏移量
- ●p_ vaddr給出了該段需要映射到進程虛擬地址空間中的位置。
- ●p_ paddr在只支持物理尋址,不支持虛擬尋址的系統當中才使用。
- ●p_ filese給出了該段的大小,以字節為單位。
- ●p_ memse給出了該段在虛擬地址空間當中的長度,單位為字節。與p_ filesz不等時會通過截斷數據或者以0填充的方式處理。
- ●p_ align給出了段內存和二進制文件當中的對齊方式,即p_ offset和p_ vaddr必須是p_ align的整數倍。
實例:
下面讀取“bin”錄下的“ls”文件的程序頭表:

可以看到各個段的偏移量,虛擬地址,段大小,訪問權限等信息。在最下面也給出了各個段中包含的節都是哪些,也驗證了上面說的“段就是若干個節捆綁在一起形成的”這句話。
其中在動態鏈接的時候用到的是兩個類型為“LOAD”的段,這兩個“LOAD”段一個是可讀可執行的,另一個是可讀可寫的,只有他們兩個段是要被映射到內存空間的,而其他的段都是在裝載時起到輔助作用的段。
四.節
節是ELF文件中有實際內容的東西,前面講的三個頭表都是為了能夠快速找到需要的節的一些工具。所以說,節才是我們最終想要讀取的。
ELF文件中有很多的節,他們分別保存着不同的信息,比如有的節保存了代碼,有的節保存了數據,有的節保存了調試信息等等。這樣分開存的好處就是可以分別給他們不同的權限,像代碼就是可讀可執行的,而數據則是可讀可寫的,分開存可以防止在不經意之間將代碼改掉帶來預期之外的錯誤。
要判斷一個節里邊具體保存的是什么類型的信息,可以通過節頭表來了解到。下面介紹幾個常用的節:
●.interp:這個節中保存的是一個字符串,這個字符串描述的是動態鏈接器的絕對路徑(在Linux中一般為/lib64/ld-linux.so.2)。
●.bss:這個節中保存了程序中沒有初始化的數據,這個節在程序運行時,在內存中會被清零,該節本身不占用磁盤空間。
●.data和.data1:這兩個節中保存的是程序中初始化的數據,主要是已初始化的全局變量和靜態變量。
●.rodata:這個節中存的是只讀的數據,比如靜態變量,字符串常量以及“count“修飾的變量。
●.comment:這個節中保存了注釋信息。
●.text:這個節中保存了程序的可執行代碼。
●.debug和.stab以及.stabstr:這幾個節中保存的是調試信息,對程序的調試有很大幫助。它們占的空間可能比程序本身還要多,一般在程序交付的時候可以用strip命令去掉文件中的debug信息以精簡文件大小,這一個動作可能讓程序的體積減少一半以上,但是這樣也就令這個程序失去了調試的便利性。
●.line:這個節中保存了調試時用的行號信息。
●.dynstr:這個節中保存的是動態鏈接時的字符串表,主要是動態鏈接符號的符號名
●.symtab:這個節中是一個符號表,保存了保存變量名,函數名等
●.dynsym:這個節中保存的是動態鏈接時的符號表,主要用於保存動態鏈接時的符號表。
Note:.dynsym其實是.symtab的一個子集,.dynsym相當於是從.symtab中取出了與動態鏈接有關的符號組成的一個表。
●.hash:這個節中保存的是符號表的哈希表,用來加快符號查找速度。
●.init:這個節中保存了程序執行前的初始化代碼,這些代碼早於main函數被執行。
●.fini:這個節中保存了程序退出時執行的代碼,這些代碼晚於main函數被執行。
●.shstrtab:這個節中保存了一個字符串,里邊全是節的名稱。
●.dynamic:這個節中保存的是一個結構體數組,這個數組每一個元素都是一個結構體,其中保存的是一些與動態鏈接有關的信息,比如:依賴於哪些共享對象,動態鏈接符號表的位置,動態鏈接重定位表的位置,共享對象初始化代碼的地址等。
結構體如下:(定義在/usr/include目錄下的elf.h頭文件中)

其中d_ tag是一個標志,它取不同的值的時候,下面的共用體所存內容有不同含義。它的取值有以下幾種可能:

從上面這個表可以看出來“.dynamic”節中保存的全都是有關動態鏈接的各種細節信息,因此,這個節在動態鏈接的過程中起到了很重要的作用,這個節相當於有關動態鏈接的一個“頭表”。
●.rel.text和.rel.data:這是一些重定位表。
鏈接器在處理目標文件的時候,需要對目標文件中的某些部位進行重定位,也就是代碼和數據節中對絕對地址的引用的位置。這些重定位的信息都是保存在一系列重定位表中,對於每一個需要重定位的代碼節或者數據節,都有一個對應的重定位表。比如這個“.rel.text”就是一個代碼相關的重定位表。比如在其他庫中定義的函數或者變量,在本文件中引用到的了,編譯器在編譯的時候不知道他們的真實地址,所以暫時用一個假的地址(記作A)代替,同時生成一個重定位條目(包含P和S,后面會講到)放到“.rel.text”節中,等到以后鏈接的時候由鏈接器根據重定位表中的信息計算出他們的真實地址。(每一個需要被重定位的地方叫做一個“重定位入口”。)
“,rel,”開頭的節中有一個這樣的結構體:

其中:
1, r_ offset:代表重定位入口的偏移量,對於可重定位文件來說,這個值是該重定位入口所要修正的位置的第一個字節相對於要被重定位的節開始處的偏移。而對於可執行文件和共享對象來說,這個值是該重定位入口要修正的位置的第一個字節的虛擬地址。
2, r_info:重定位入口的類型和符號,在32位系統中,低8位表示重定位入口的類型,而高24位表示重定位入口的符號在符號表中的下標。
重定位類型一般有兩種:絕對尋址和相對尋址。這兩種類型分別有不同的重定位修正方式,如下圖。

Note:S:目標文件符號表中對應的地址,而不是.o文件;
對一個可重定位文件使用 “objdump -r 文件名“ 命令可以查看文件中的重定位表以及重定位入口(下面只截取了一部分)

Note:還有一種重定位表的名稱是“.rela.xxx”這樣的,這種重定位表稱為“需要添加常數的重定位表”,它的結構體會多一個成員:“r_addend”(加數),計算重定位時,根據重定位類型,對該值做不同處理。
