本文討論了 UNIX/LINUX 平臺下三種主要的可執(zhí)行文件格式:a.out(assembler and link editor output 匯編器和鏈接編輯器的輸出)、COFF(Common Object File Format 通用對象文件格式)、ELF(Executable and Linking Format 可執(zhí)行和鏈接格式)。首先是對可執(zhí)行文件格式的一個綜述,并通過描述 ELF 文件加載過程以揭示可執(zhí)行文件內(nèi)容與加載運(yùn)行操作之間的關(guān)系。隨后依此討論了此三種文件格式,并著重討論 ELF 文件的動態(tài)連接機(jī)制,其間也穿插了對各種文件格式優(yōu)缺點(diǎn)的評價(jià)。最后對三種可執(zhí)行文件格式有一個簡單總結(jié),并提出作者對可文件格式評價(jià)的一些感想。
可執(zhí)行文件格式綜述 相對于其它文件類型,可執(zhí)行文件可能是一個操作系統(tǒng)中最重要的文件類型,因?yàn)樗鼈兪峭瓿刹僮鞯恼嬲龍?zhí)行者??蓤?zhí)行文件的大小、運(yùn)行速度、資源占用情況以及可擴(kuò)展性、可移植性等與文件格式的定義和文件加載過程緊密相關(guān)。研究可執(zhí)行文件的格式對編寫高性能程序和一些黑客技術(shù)的運(yùn)用都是非常有意義的。
不管何種可執(zhí)行文件格式,一些基本的要素是必須的,顯而易見的,文件中應(yīng)包含代碼和數(shù)據(jù)。因?yàn)槲募赡芤猛獠课募x的符號(變量和函數(shù)),因此重定位信息和符號信息也是需要的。一些輔助信息是可選的,如調(diào)試信息、硬件信息等?;旧先我庖环N可執(zhí)行文件格式都是按區(qū)間保存上述信息,稱為段(Segment)或節(jié)(Section)。不同的文件格式中段和節(jié)的含義可能有細(xì)微區(qū)別,但根據(jù)上下文關(guān)系可以很清楚的理解,這不是關(guān)鍵問題。最后,可執(zhí)行文件通常都有一個文件頭部以描述本文件的總體結(jié)構(gòu)。
相對可執(zhí)行文件有三個重要的概念:編譯(compile)、連接(link,也可稱為鏈接、聯(lián)接)、加載(load)。源程序文件被編譯成目標(biāo)文件,多個目標(biāo)文件被連接成一個最終的可執(zhí)行文件,可執(zhí)行文件被加載到內(nèi)存中運(yùn)行。因?yàn)楸疚闹攸c(diǎn)是討論可執(zhí)行文件格式,因此加載過程也相對重點(diǎn)討論。下面是LINUX平臺下ELF文件加載過程的一個簡單描述。
1:內(nèi)核首先讀ELF文件的頭部,然后根據(jù)頭部的數(shù)據(jù)指示分別讀入各種數(shù)據(jù)結(jié)構(gòu),找到標(biāo)記為可加載(loadable)的段,并調(diào)用函數(shù) mmap()把段內(nèi)容加載到內(nèi)存中。在加載之前,內(nèi)核把段的標(biāo)記直接傳遞給 mmap(),段的標(biāo)記指示該段在內(nèi)存中是否可讀、可寫,可執(zhí)行。顯然,文本段是只讀可執(zhí)行,而數(shù)據(jù)段是可讀可寫。這種方式是利用了現(xiàn)代操作系統(tǒng)和處理器對內(nèi)存的保護(hù)功能。著名的Shellcode( 參考資料 17)的編寫技巧則是突破此保護(hù)功能的一個實(shí)際例子。
2:內(nèi)核分析出ELF文件標(biāo)記為 PT_INTERP 的段中所對應(yīng)的動態(tài)連接器名稱,并加載動態(tài)連接器?,F(xiàn)代 LINUX 系統(tǒng)的動態(tài)連接器通常是 /lib/ld-linux.so.2,相關(guān)細(xì)節(jié)在后面有詳細(xì)描述。
3:內(nèi)核在新進(jìn)程的堆棧中設(shè)置一些標(biāo)記-值對,以指示動態(tài)連接器的相關(guān)操作。
4:內(nèi)核把控制傳遞給動態(tài)連接器。
5:動態(tài)連接器檢查程序?qū)ν獠课募ü蚕韼欤┑囊蕾囆?,并在需要時對其進(jìn)行加載。
6:動態(tài)連接器對程序的外部引用進(jìn)行重定位,通俗的講,就是告訴程序其引用的外部變量/函數(shù)的地址,此地址位于共享庫被加載在內(nèi)存的區(qū)間內(nèi)。動態(tài)連接還有一個延遲(Lazy)定位的特性,即只在"真正"需要引用符號時才重定位,這對提高程序運(yùn)行效率有極大幫助。
7:動態(tài)連接器執(zhí)行在ELF文件中標(biāo)記為 .init 的節(jié)的代碼,進(jìn)行程序運(yùn)行的初始化。在早期系統(tǒng)中,初始化代碼對應(yīng)函數(shù) _init(void)(函數(shù)名強(qiáng)制固定),在現(xiàn)代系統(tǒng)中,則對應(yīng)形式為
void
__attribute((constructor))
init_function(void)
{
……
}
|
其中函數(shù)名為任意。
8:動態(tài)連接器把控制傳遞給程序,從 ELF 文件頭部中定義的程序進(jìn)入點(diǎn)開始執(zhí)行。在 a.out 格式和ELF格式中,程序進(jìn)入點(diǎn)的值是顯式存在的,在 COFF 格式中則是由規(guī)范隱含定義。
從上面的描述可以看出,加載文件最重要的是完成兩件事情:加載程序段和數(shù)據(jù)段到內(nèi)存;進(jìn)行外部定義符號的重定位。重定位是程序連接中一個重要概念。我們知道,一個可執(zhí)行程序通常是由一個含有 main() 的主程序文件、若干目標(biāo)文件、若干共享庫(Shared Libraries)組成。(注:采用一些特別的技巧,也可編寫沒有 main 函數(shù)的程序,請參閱 參考資料 2)一個 C 程序可能引用共享庫定義的變量或函數(shù),換句話說就是程序運(yùn)行時必須知道這些變量/函數(shù)的地址。在靜態(tài)連接中,程序所有需要使用的外部定義都完全包含在可執(zhí)行程序中,而動態(tài)連接則只在可執(zhí)行文件中設(shè)置相關(guān)外部定義的一些引用信息,真正的重定位是在程序運(yùn)行之時。靜態(tài)連接方式有兩個大問題:如果庫中變量或函數(shù)有任何變化都必須重新編譯連接程序;如果多個程序引用同樣的變量/函數(shù),則此變量/函數(shù)會在文件/內(nèi)存中出現(xiàn)多次,浪費(fèi)硬盤/內(nèi)存空間。比較兩種連接方式生成的可執(zhí)行文件的大小,可以看出有明顯的區(qū)別。
a.out 文件格式分析 a.out 格式在不同的機(jī)器平臺和不同的 UNIX 操作系統(tǒng)上有輕微的不同,例如在 MC680x0 平臺上有 6 個 section。下面我們討論的是最"標(biāo)準(zhǔn)"的格式。
a.out 文件包含 7 個 section,格式如下:
exec header(執(zhí)行頭部,也可理解為文件頭部) |
text segment(文本段) |
data segment(數(shù)據(jù)段) |
text relocations(文本重定位段) |
data relocations(數(shù)據(jù)重定位段) |
symbol table(符號表) |
string table(字符串表) |
執(zhí)行頭部的數(shù)據(jù)結(jié)構(gòu):
struct exec {
unsigned long a_midmag; /* 魔數(shù)和其它信息 */
unsigned long a_text; /* 文本段的長度 */
unsigned long a_data; /* 數(shù)據(jù)段的長度 */
unsigned long a_bss; /* BSS段的長度 */
unsigned long a_syms; /* 符號表的長度 */
unsigned long a_entry; /* 程序進(jìn)入點(diǎn) */
unsigned long a_trsize; /* 文本重定位表的長度 */
unsigned long a_drsize; /* 數(shù)據(jù)重定位表的長度 */
};
|
文件頭部主要描述了各個 section 的長度,比較重要的字段是 a_entry(程序進(jìn)入點(diǎn)),代表了系統(tǒng)在加載程序并初試化各種環(huán)境后開始執(zhí)行程序代碼的入口。這個字段在后面討論的 ELF 文件頭部中也有出現(xiàn)。由 a.out 格式和頭部數(shù)據(jù)結(jié)構(gòu)我們可以看出,a.out 的格式非常緊湊,只包含了程序運(yùn)行所必須的信息(文本、數(shù)據(jù)、BSS),而且每個 section 的順序是固定的。這種結(jié)構(gòu)缺乏擴(kuò)展性,如不能包含"現(xiàn)代"可執(zhí)行文件中常見的調(diào)試信息,最初的 UNIX 黑客對 a.out 文件調(diào)試使用的工具是 adb,而 adb 是一種機(jī)器語言調(diào)試器!
a.out 文件中包含符號表和兩個重定位表,這三個表的內(nèi)容在連接目標(biāo)文件以生成可執(zhí)行文件時起作用。在最終可執(zhí)行的 a.out 文件中,這三個表的長度都為 0。a.out 文件在連接時就把所有外部定義包含在可執(zhí)行程序中,如果從程序設(shè)計(jì)的角度來看,這是一種硬編碼方式,或者可稱為模塊之間是強(qiáng)藕和的。在后面的討論中,我們將會具體看到ELF格式和動態(tài)連接機(jī)制是如何對此進(jìn)行改進(jìn)的。
a.out 是早期UNIX系統(tǒng)使用的可執(zhí)行文件格式,由 AT&T 設(shè)計(jì),現(xiàn)在基本上已被 ELF 文件格式代替。a.out 的設(shè)計(jì)比較簡單,但其設(shè)計(jì)思想明顯的被后續(xù)的可執(zhí)行文件格式所繼承和發(fā)揚(yáng)??梢詤㈤?參考資料 16 和閱讀 參考資料 15 源代碼加深對 a.out 格式的理解。 參考資料 12 討論了如何在"現(xiàn)代"的紅帽LINUX運(yùn)行 a.out 格式文件。
COFF 文件格式分析 COFF 格式比 a.out 格式要復(fù)雜一些,最重要的是包含一個節(jié)段表(section table),因此除了 .text,.data,和 .bss 區(qū)段以外,還可以包含其它的區(qū)段。另外也多了一個可選的頭部,不同的操作系統(tǒng)可一對此頭部做特定的定義。
COFF 文件格式如下:
File Header(文件頭部) |
Optional Header(可選文件頭部) |
Section 1 Header(節(jié)頭部) |
……… |
Section n Header(節(jié)頭部) |
Raw Data for Section 1(節(jié)數(shù)據(jù)) |
Raw Data for Section n(節(jié)數(shù)據(jù)) |
Relocation Info for Sect. 1(節(jié)重定位數(shù)據(jù)) |
Relocation Info for Sect. n(節(jié)重定位數(shù)據(jù)) |
Line Numbers for Sect. 1(節(jié)行號數(shù)據(jù)) |
Line Numbers for Sect. n(節(jié)行號數(shù)據(jù)) |
Symbol table(符號表) |
String table(字符串表) |
文件頭部的數(shù)據(jù)結(jié)構(gòu):
struct filehdr
{
unsigned short f_magic; /* 魔數(shù) */
unsigned short f_nscns; /* 節(jié)個數(shù) */
long f_timdat; /* 文件建立時間 */
long f_symptr; /* 符號表相對文件的偏移量 */
long f_nsyms; /* 符號表?xiàng)l目個數(shù) */
unsigned short f_opthdr; /* 可選頭部長度 */
unsigned short f_flags; /* 標(biāo)志 */
};
|
COFF 文件頭部中魔數(shù)與其它兩種格式的意義不太一樣,它是表示針對的機(jī)器類型,例如 0x014c 相對于 I386 平臺,而 0x268 相對于 Motorola 68000系列等。當(dāng) COFF 文件為可執(zhí)行文件時,字段 f_flags 的值為 F_EXEC(0X00002),同時也表示此文件沒有未解析的符號,換句話說,也就是重定位在連接時就已經(jīng)完成。由此也可以看出,原始的 COFF 格式不支持動態(tài)連接。為了解決這個問題以及增加一些新的特性,一些操作系統(tǒng)對 COFF 格式進(jìn)行了擴(kuò)展。Microsoft 設(shè)計(jì)了名為 PE(Portable Executable)的文件格式,主要擴(kuò)展是在 COFF 文件頭部之上增加了一些專用頭部,具體細(xì)節(jié)請參閱 參考資料 18,某些 UNIX 系統(tǒng)也對 COFF 格式進(jìn)行了擴(kuò)展,如 XCOFF(extended common object file format)格式,支持動態(tài)連接,請參閱 參考資料 5。
緊接文件頭部的是可選頭部,COFF 文件格式規(guī)范中規(guī)定可選頭部的長度可以為 0,但在 LINUX 系統(tǒng)下可選頭部是必須存在的。下面是 LINUX 下可選頭部的數(shù)據(jù)結(jié)構(gòu):
typedef struct
{
char magic[2]; /* 魔數(shù) */
char vstamp[2]; /* 版本號 */
char tsize[4]; /* 文本段長度 */
char dsize[4]; /* 已初始化數(shù)據(jù)段長度 */
char bsize[4]; /* 未初始化數(shù)據(jù)段長度 */
char entry[4]; /* 程序進(jìn)入點(diǎn) */
char text_start[4]; /* 文本段基地址 */
char data_start[4]; /* 數(shù)據(jù)段基地址 */
}
COFF_AOUTHDR;
|
字段 magic 為 0413 時表示 COFF 文件是可執(zhí)行的,注意到可選頭部中顯式定義了程序進(jìn)入點(diǎn),標(biāo)準(zhǔn)的 COFF 文件沒有明確的定義程序進(jìn)入點(diǎn)的值,通常是從 .text 節(jié)開始執(zhí)行,但這種設(shè)計(jì)并不好。
前面我們提到,COFF 格式比 a.out 格式多了一個節(jié)段表,一個節(jié)頭條目描述一個節(jié)數(shù)據(jù)的細(xì)節(jié),因此 COFF 格式能包含更多的節(jié),或者說可以根據(jù)實(shí)際需要,增加特定的節(jié),具體表現(xiàn)在 COFF 格式本身的定義以及稍早提及的 COFF 格式擴(kuò)展。我個人認(rèn)為,節(jié)段表的出現(xiàn)可能是 COFF 格式相對 a.out 格式最大的進(jìn)步。下面我們將簡單描述 COFF 文件中節(jié)的數(shù)據(jù)結(jié)構(gòu),因?yàn)楣?jié)的意義更多體現(xiàn)在程序的編譯和連接上,所以本文不對其做更多的描述。此外,ELF 格式和 COFF格式對節(jié)的定義非常相似,在隨后的 ELF 格式分析中,我們將省略相關(guān)討論。
struct COFF_scnhdr
{
char s_name[8]; /* 節(jié)名稱 */
char s_paddr[4]; /* 物理地址 */
char s_vaddr[4]; /* 虛擬地址 */
char s_size[4]; /* 節(jié)長度 */
char s_scnptr[4]; /* 節(jié)數(shù)據(jù)相對文件的偏移量 */
char s_relptr[4]; /* 節(jié)重定位信息偏移量 */
char s_lnnoptr[4]; /* 節(jié)行信息偏移量 */
char s_nreloc[2]; /* 節(jié)重定位條目數(shù) */
char s_nlnno[2]; /* 節(jié)行信息條目數(shù) */
char s_flags[4]; /* 段標(biāo)記 */
};
|
有一點(diǎn)需要注意:LINUX系統(tǒng)中頭文件coff.h中對字段s_paddr的注釋是"physical address",但似乎應(yīng)該理解為"節(jié)被加載到內(nèi)存中所占用的空間長度"。字段s_flags標(biāo)記該節(jié)的類型,如文本段、數(shù)據(jù)段、BSS段等。在COFF的節(jié)中也出現(xiàn)了行信息,行信息描述了二進(jìn)制代碼與源代碼的行號之間的對映關(guān)系,在調(diào)試時很有用。
參考資料 19是一份對COFF格式詳細(xì)描述的中文資料,更詳細(xì)的內(nèi)容請參閱 參考資料 20。
ELF文件格式分析 ELF文件有三種類型:可重定位文件:也就是通常稱的目標(biāo)文件,后綴為.o。共享文件:也就是通常稱的庫文件,后綴為.so??蓤?zhí)行文件:本文主要討論的文件格式,總的來說,可執(zhí)行文件的格式與上述兩種文件的格式之間的區(qū)別主要在于觀察的角度不同:一種稱為連接視圖(Linking View),一種稱為執(zhí)行視圖(Execution View)。
首先看看ELF文件的總體布局:
ELF header(ELF頭部) |
Program header table(程序頭表) |
Segment1(段1) |
Segment2(段2) |
……… |
Sengmentn(段n) |
Setion header table(節(jié)頭表,可選) |
段由若干個節(jié)(Section)構(gòu)成,節(jié)頭表對每一個節(jié)的信息有相關(guān)描述。對可執(zhí)行程序而言,節(jié)頭表是可選的。 參考資料 1中作者談到把節(jié)頭表的所有數(shù)據(jù)全部設(shè)置為0,程序也能正確運(yùn)行!ELF頭部是一個關(guān)于本文件的路線圖(road map),從總體上描述文件的結(jié)構(gòu)。下面是ELF頭部的數(shù)據(jù)結(jié)構(gòu):
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* 魔數(shù)和相關(guān)信息 */
Elf32_Half e_type; /* 目標(biāo)文件類型 */
Elf32_Half e_machine; /* 硬件體系 */
Elf32_Word e_version; /* 目標(biāo)文件版本 */
Elf32_Addr e_entry; /* 程序進(jìn)入點(diǎn) */
Elf32_Off e_phoff; /* 程序頭部偏移量 */
Elf32_Off e_shoff; /* 節(jié)頭部偏移量 */
Elf32_Word e_flags; /* 處理器特定標(biāo)志 */
Elf32_Half e_ehsize; /* ELF頭部長度 */
Elf32_Half e_phentsize; /* 程序頭部中一個條目的長度 */
Elf32_Half e_phnum; /* 程序頭部條目個數(shù) */
Elf32_Half e_shentsize; /* 節(jié)頭部中一個條目的長度 */
Elf32_Half e_shnum; /* 節(jié)頭部條目個數(shù) */
Elf32_Half e_shstrndx; /* 節(jié)頭部字符表索引 */
} Elf32_Ehdr;
|
下面我們對ELF頭表中一些重要的字段作出相關(guān)說明,完整的ELF定義請參閱 參考資料6和 參考資料 7。
e_ident[0]-e_ident[3]包含了ELF文件的魔數(shù),依次是0x7f、‘E‘、‘L‘、‘F‘。注意,任何一個ELF文件必須包含此魔數(shù)。 參考資料 3中討論了利用程序、工具、/Proc文件系統(tǒng)等多種查看ELF魔數(shù)的方法。e_ident[4]表示硬件系統(tǒng)的位數(shù),1代表32位,2代表64位。e_ident[5]表示數(shù)據(jù)編碼方式,1代表小印第安排序(最大有意義的字節(jié)占有最低的地址),2代表大印第安排序(最大有意義的字節(jié)占有最高的地址)。e_ident[6]指定ELF頭部的版本,當(dāng)前必須為1。e_ident[7]到e_ident[14]是填充符,通常是0。ELF格式規(guī)范中定義這幾個字節(jié)是被忽略的,但實(shí)際上是這幾個字節(jié)完全可以可被利用。如病毒Lin/Glaurung.676/666( 參考資料 1)設(shè)置e_ident[7]為0x21,表示本文件已被感染;或者存放可執(zhí)行代碼( 參考資料 2)。ELF頭部中大多數(shù)字段都是對子頭部數(shù)據(jù)的描述,其意義相對比較簡單。值得注意的是某些病毒可能修改字段e_entry(程序進(jìn)入點(diǎn))的值,以指向病毒代碼,例如上面提到的病毒Lin/Glaurung.676/666。
一個實(shí)際可執(zhí)行文件的文件頭部形式如下:(利用命令readelf)
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2‘s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x80483cc
Start of program headers: 52 (bytes into file)
Start of section headers: 14936 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 34
Section header string table index: 31
|
緊接ELF頭部的是程序頭表,它是一個結(jié)構(gòu)數(shù)組,包含了ELF頭表中字段e_phnum定義的條目,結(jié)構(gòu)描述一個段或其他系統(tǒng)準(zhǔn)備執(zhí)行該程序所需要的信息。
typedef struct {
Elf32_Word p_type; /* 段類型 */
Elf32_Off p_offset; /* 段位置相對于文件開始處的偏移量 */
Elf32_Addr p_vaddr; /* 段在內(nèi)存中的地址 */
Elf32_Addr p_paddr; /* 段的物理地址 */
Elf32_Word p_filesz; /* 段在文件中的長度 */
Elf32_Word p_memsz; /* 段在內(nèi)存中的長度 */
Elf32_Word p_flags; /* 段的標(biāo)記 */
Elf32_Word p_align; /* 段在內(nèi)存中對齊標(biāo)記 */
} Elf32_Phdr;
|
在詳細(xì)討論可執(zhí)行文件程序頭表之前,首先查看一個實(shí)際文件的輸出:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4
INTERP 0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00684 0x00684 R E 0x1000
LOAD 0x000684 0x08049684 0x08049684 0x00118 0x00130 RW 0x1000
DYNAMIC 0x000690 0x08049690 0x08049690 0x000c8 0x000c8 RW 0x4
NOTE 0x000108 0x08048108 0x08048108 0x00020 0x00020 R 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .data .dynamic .ctors .dtors .jcr .got .bss
04 .dynamic
05 .note.ABI-tag
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 080480f4 0000f4 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048108 000108 000020 00 A 0 0 4
[ 3] .hash HASH 08048128 000128 000040 04 A 4 0 4
[ 4] .dynsym DYNSYM 08048168 000168 0000b0 10 A 5 1 4
[ 5] .dynstr STRTAB 08048218 000218 00007b 00 A 0 0 1
[ 6] .gnu.version VERSYM 08048294 000294 000016 02 A 4 0 2
[ 7] .gnu.version_r VERNEED 080482ac 0002ac 000030 00 A 5 1 4
[ 8] .rel.dyn REL 080482dc 0002dc 000008 08 A 4 0 4
[ 9] .rel.plt REL 080482e4 0002e4 000040 08 A 4 b 4
[10] .init PROGBITS 08048324 000324 000017 00 AX 0 0 4
[11] .plt PROGBITS 0804833c 00033c 000090 04 AX 0 0 4
[12] .text PROGBITS 080483cc 0003cc 0001f8 00 AX 0 0 4
[13] .fini PROGBITS 080485c4 0005c4 00001b 00 AX 0 0 4
[14] .rodata PROGBITS 080485e0 0005e0 00009f 00 A 0 0 32
[15] .eh_frame PROGBITS 08048680 000680 000004 00 A 0 0 4
[16] .data PROGBITS 08049684 000684 00000c 00 WA 0 0 4
[17] .dynamic DYNAMIC 08049690 000690 0000c8 08 WA 5 0 4
[18] .ctors PROGBITS 08049758 000758 000008 00 WA 0 0 4
[19] .dtors PROGBITS 08049760 000760 000008 00 WA 0 0 4
[20] .jcr PROGBITS 08049768 000768 000004 00 WA 0 0 4
[21] .got PROGBITS 0804976c 00076c 000030 04 WA 0 0 4
[22] .bss NOBITS 0804979c 00079c 000018 00 WA 0 0 4
[23] .comment PROGBITS 00000000 00079c 000132 00 0 0 1
[24] .debug_aranges PROGBITS 00000000 0008d0 000098 00 0 0 8
[25] .debug_pubnames PROGBITS 00000000 000968 000040 00 0 0 1
[26] .debug_info PROGBITS 00000000 0009a8 001cc6 00 0 0 1
[27] .debug_abbrev PROGBITS 00000000 00266e 0002cc 00 0 0 1
[28] .debug_line PROGBITS 00000000 00293a 0003dc 00 0 0 1
[29] .debug_frame PROGBITS 00000000 002d18 000048 00 0 0 4
[30] .debug_str PROGBITS 00000000 002d60 000bcd 01 MS 0 0 1
[31] .shstrtab STRTAB 00000000 00392d 00012b 00 0 0 1
[32] .symtab SYMTAB 00000000 003fa8 000740 10 33 56 4
[33] .strtab STRTAB 00000000 0046e8 000467 00 0 0 1
|
對一個ELF可執(zhí)行程序而言,一個基本的段是標(biāo)記p_type為PT_INTERP的段,它表明了運(yùn)行此程序所需要的程序解釋器(/lib/ld-linux.so.2),實(shí)際上也就是動態(tài)連接器(dynamic linker)。最重要的段是標(biāo)記p_type為PT_LOAD的段,它表明了為運(yùn)行程序而需要加載到內(nèi)存的數(shù)據(jù)。查看上面實(shí)際輸入,可以看見有兩個可LOAD段,第一個為只讀可執(zhí)行(FLg為R E),第二個為可讀可寫(Flg為RW)。段1包含了文本節(jié).text,注意到ELF文件頭部中程序進(jìn)入點(diǎn)的值為0x80483cc,正好是指向節(jié).text在內(nèi)存中的地址。段二包含了數(shù)據(jù)節(jié).data,此數(shù)據(jù)節(jié)中數(shù)據(jù)是可讀可寫的,相對的只讀數(shù)據(jù)節(jié).rodata包含在段1中。ELF格式可以比COFF格式包含更多的調(diào)試信息,如上面所列出的形式為.debug_xxx的節(jié)。在I386平臺LINUX系統(tǒng)下,用命令file查看一個ELF可執(zhí)行程序的可能輸出是:a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped。
ELF文件中包含了動態(tài)連接器的全路徑,內(nèi)核定位"正確"的動態(tài)連接器在內(nèi)存中的地址是"正確"運(yùn)行可執(zhí)行文件的保證, 參考資料 13討論了如何通過查找動態(tài)連接器在內(nèi)存中的地址以達(dá)到顛覆(Subversiver)動態(tài)連接機(jī)制的方法。
最后我們討論ELF文件的動態(tài)連接機(jī)制。每一個外部定義的符號在全局偏移表(Global Offset Table GOT)中有相應(yīng)的條目,如果符號是函數(shù)則在過程連接表(Procedure Linkage Table PLT)中也有相應(yīng)的條目,且一個PLT條目對應(yīng)一個GOT條目。對外部定義函數(shù)解析可能是整個ELF文件規(guī)范中最復(fù)雜的,下面是函數(shù)符號解析過程的一個描述。
1:代碼中調(diào)用外部函數(shù)func,語句形式為call 0xaabbccdd,地址0xaabbccdd實(shí)際上就是符號func在PLT表中對應(yīng)的條目地址(假設(shè)地址為標(biāo)號.PLT2)。
2:PLT表的形式如下
.PLT0: pushl 4(%ebx) /* GOT表的地址保存在寄存器ebx中 */
jmp *8(%ebx)
nop; nop
nop; nop
.PLT1: jmp *name1@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
.PLT2: jmp *func@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
|
3:查看標(biāo)號.PLT2的語句,實(shí)際上是跳轉(zhuǎn)到符號func在GOT表中對應(yīng)的條目。
4:在符號沒有重定位前,GOT表中此符號對應(yīng)的地址為標(biāo)號.PLT2的下一條語句,即是pushl $offset,其中$offset是符號func的重定位偏移量。注意到這是一個二次跳轉(zhuǎn)。
5:在符號func的重定位偏移量壓棧后,控制跳到PLT表的第一條目,把GOT[1]的內(nèi)容壓棧,并跳轉(zhuǎn)到GOT[2]對應(yīng)的地址。
6:GOT[2]對應(yīng)的實(shí)際上是動態(tài)符號解析函數(shù)的代碼,在對符號func的地址解析后,會把func在內(nèi)存中的地址設(shè)置到GOT表中此符號對應(yīng)的條目中。
7:當(dāng)?shù)诙握{(diào)用此符號時,GOT表中對應(yīng)的條目已經(jīng)包含了此符號的地址,就可直接調(diào)用而不需要利用PLT表進(jìn)行跳轉(zhuǎn)。
動態(tài)連接是比較復(fù)雜的,但為了獲得靈活性的代價(jià)通常就是復(fù)雜性。其最終目的是把GOT表中條目的值修改為符號的真實(shí)地址,這也可解釋節(jié).got包含在可讀可寫段中。
動態(tài)連接是一個非常重要的進(jìn)步,這意味著庫文件可以被升級、移動到其他目錄等等而不需要重新編譯程序(當(dāng)然,這不意味庫可以任意修改,如函數(shù)入?yún)⒌膫€數(shù)、數(shù)據(jù)類型應(yīng)保持兼容性)。從很大程度上說,動態(tài)連接機(jī)制是ELF格式代替a.out格式的決定性原因。如果說面對對象的編程本質(zhì)是面對接口(interface)的編程,那么動態(tài)連接機(jī)制則是這種思想的地一個非常典型的應(yīng)用,具體的講,動態(tài)連接機(jī)制與設(shè)計(jì)模式中的橋接(BRIDGE)方法比較類似,而它的LAZY特性則與代理(PROXY)方法非常相似。動態(tài)連接操作的細(xì)節(jié)描述請參閱 參考資料 8,9,10,11。通過閱讀命令readelf、objdump 的源代碼以及 參考資料 14中所提及的相關(guān)軟件源代碼,可以對ELF文件的格式有更徹底的了解。
總結(jié) 不同時期的可執(zhí)行文件格式深刻的反映了技術(shù)進(jìn)步的過程,技術(shù)進(jìn)步通常是針對解決存在的問題和適應(yīng)新的環(huán)境。早期的UNIX系統(tǒng)使用a.out格式,隨著操作系統(tǒng)和硬件系統(tǒng)的進(jìn)步,a.out格式的局限性越來越明顯。新的可執(zhí)行文件格式COFF在UNIX System VR3中出現(xiàn),COFF格式相對a.out格式最大變化是多了一個節(jié)頭表(section head table),能夠在包含基礎(chǔ)的文本段、數(shù)據(jù)段、BSS段之外包含更多的段,但是COFF對動態(tài)連接和C++程序的支持仍然比較困難。為了解決上述問題,UNIX系統(tǒng)實(shí)驗(yàn)室(UNIX SYSTEM Laboratories USL) 開發(fā)出ELF文件格式,它被作為應(yīng)用程序二進(jìn)制接口(Application binary Interface ABI)的一部分,其目的是替代傳統(tǒng)的a.out格式。例如,ELF文件格式中引入初始化段.init和結(jié)束段.fini(分別對應(yīng)構(gòu)造函數(shù)和析構(gòu)函數(shù))則主要是為了支持C++程序。1994年6月ELF格式出現(xiàn)在LINUX系統(tǒng)上,現(xiàn)在ELF格式作為UNIX/LINUX最主要的可執(zhí)行文件格式。當(dāng)然我們完全有理由相信,在將來還會有新的可執(zhí)行文件格式出現(xiàn)。
上述三種可執(zhí)行文件格式都很好的體現(xiàn)了設(shè)計(jì)思想中分層的概念,由一個總的頭部刻畫了文件的基本要素,再由若干子頭部/條目刻畫了文件的若干細(xì)節(jié)。比較一下可執(zhí)行文件格式和以太數(shù)據(jù)包中以太頭、IP頭、TCP頭的設(shè)計(jì),我想我們能很好的感受分層這一重要的設(shè)計(jì)思想。 參考資料 21從全局的角度討論了各種文件的格式,并提出一個比較夸張的結(jié)論:Everything Is Byte!
最后的題外話:大多數(shù)資料中對a.out格式的評價(jià)較低,常見的詞語有黑暗年代(dark ages)、丑陋(ugly)等等,當(dāng)然,從現(xiàn)代的觀點(diǎn)來看,的確是比較簡單,但是如果沒有曾經(jīng)的簡單何來今天的精巧?正如我們今天可以評價(jià)石器時代的技術(shù)是ugly,那么將來的人們也可以嘲諷今天的技術(shù)是非常ugly。我想我們也許應(yīng)該用更平和的心態(tài)來對曾經(jīng)的技術(shù)有一個公正的評價(jià)。
參考資料
- 《 LINUX VIRUSES - ELF FILE FORMAT》 Marius Van Oers
- 《 A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux 》 breadbox
- 《 The Linux Virus Writing And Detection HOWTO》Alexander Bartolich
- 《 從程序員角度看ELF》Hongjiu Lu alert7(譯)
- 《 XCOFF Object File Format》
- 《 Executable and Linkable Format(ELF)》
- 《 elf文件格式--另一文本方式的elf文檔》alert7(譯)
- 《 如何修改動態(tài)庫符號表》wangdb
- 《 分析ELF的加載過程》opera
- 《 Before main() 分析》 alert7
- 《 Linkers & Loaders》John R. Levine
- 《 Running a.out executables on modern Red Hat Linux》
- 《 Cheating the ELF》
- 《 ELF Binary Analysis Tools》
- 《 dbxread.c》
- 《 Manual Reference Pages - A.OUT (5)》
- 《 Linux下緩沖區(qū)溢出攻擊的原理及對策》
- 《 Microsoft Portable Executable and Common Object File Format Specification》
- 《 COFF的文件結(jié)構(gòu)》redleaves
- 《 Common Object File Format (COFF)》
- 《 Everything Is Byte》 mala
|