歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> elf,out,coff 三種Linux下可執行文件的歷史淵源

elf,out,coff 三種Linux下可執行文件的歷史淵源

日期:2017/2/28 15:59:38   编辑:Linux教程

elf,out,coff 三種Linux下可執行文件的歷史淵源。

本文討論了 UNIX/LINUX 平台下三種主要的可執行文件格式:a.out(assembler and link editor output 匯編器和鏈接編輯器的輸出)、COFF(Common Object File Format 通用對象文件格式)、ELF(Executable and Linking Format 可執行和鏈接格式)。首先是對可執行文件格式的一個綜述,並通過描述 ELF 文件加載過程以揭示可執行文件內容與加載運行操作之間的關系。隨後依此討論了此三種文件格式,並著重討論 ELF 文件的動態連接機制,其間也穿插了對各種文件格式優缺點的評價。最後對三種可執行文件格式有一個簡單總結,並提出作者對可文件格式評價的一些感想。

可執行文件格式綜述
相對於其它文件類型,可執行文件可能是一個操作系統中最重要的文件類型,因為它們是完成操作的真正執行者。可執行文件的大小、運行速度、資源占用情況以及可擴展性、可移植性等與文件格式的定義和文件加載過程緊密相關。研究可執行文件的格式對編寫高性能程序和一些黑客技術的運用都是非常有意義的。

不管何種可執行文件格式,一些基本的要素是必須的,顯而易見的,文件中應包含代碼和數據。因為文件可能引用外部文件定義的符號(變量和函數),因此重定位信息和符號信息也是需要的。一些輔助信息是可選的,如調試信息、硬件信息等。基本上任意一種可執行文件格式都是按區間保存上述信息,稱為段(Segment)或節(Section)。不同的文件格式中段和節的含義可能有細微區別,但根據上下文關系可以很清楚的理解,這不是關鍵問題。最後,可執行文件通常都有一個文件頭部以描述本文件的總體結構。

相對可執行文件有三個重要的概念:編譯(compile)、連接(link,也可稱為鏈接、聯接)、加載(load)。源程序文件被編譯成目標文件,多個目標文件被連接成一個最終的可執行文件,可執行文件被加載到內存中運行。因為本文重點是討論可執行文件格式,因此加載過程也相對重點討論。下面是LINUX平台下ELF文件加載過程的一個簡單描述。

1:內核首先讀ELF文件的頭部,然後根據頭部的數據指示分別讀入各種數據結構,找到標記為可加載(loadable)的段,並調用函數 mmap()把段內容加載到內存中。在加載之前,內核把段的標記直接傳遞給 mmap(),段的標記指示該段在內存中是否可讀、可寫,可執行。顯然,文本段是只讀可執行,而數據段是可讀可寫。這種方式是利用了現代操作系統和處理器對內存的保護功能。著名的Shellcode(參考資料 17)的編寫技巧則是突破此保護功能的一個實際例子。

2:內核分析出ELF文件標記為 PT_INTERP 的段中所對應的動態連接器名稱,並加載動態連接器。現代 LINUX 系統的動態連接器通常是 /lib/ld-linux.so.2,相關細節在後面有詳細描述。

3:內核在新進程的堆棧中設置一些標記-值對,以指示動態連接器的相關操作。

4:內核把控制傳遞給動態連接器。

5:動態連接器檢查程序對外部文件(共享庫)的依賴性,並在需要時對其進行加載。

6:動態連接器對程序的外部引用進行重定位,通俗的講,就是告訴程序其引用的外部變量/函數的地址,此地址位於共享庫被加載在內存的區間內。動態連接還有一個延遲(Lazy)定位的特性,即只在"真正"需要引用符號時才重定位,這對提高程序運行效率有極大幫助。

7:動態連接器執行在ELF文件中標記為 .init 的節的代碼,進行程序運行的初始化。在早期系統中,初始化代碼對應函數 _init(void)(函數名強制固定),在現代系統中,則對應形式為


void
__attribute((constructor))
init_function(void)
{
……
}

其中函數名為任意。

8:動態連接器把控制傳遞給程序,從 ELF 文件頭部中定義的程序進入點開始執行。在 a.out 格式和ELF格式中,程序進入點的值是顯式存在的,在 COFF 格式中則是由規范隱含定義。

從上面的描述可以看出,加載文件最重要的是完成兩件事情:加載程序段和數據段到內存;進行外部定義符號的重定位。重定位是程序連接中一個重要概念。我們知道,一個可執行程序通常是由一個含有 main() 的主程序文件、若干目標文件、若干共享庫(Shared Libraries)組成。(注:采用一些特別的技巧,也可編寫沒有 main 函數的程序,請參閱參考資料 2)一個 C 程序可能引用共享庫定義的變量或函數,換句話說就是程序運行時必須知道這些變量/函數的地址。在靜態連接中,程序所有需要使用的外部定義都完全包含在可執行程序中,而動態連接則只在可執行文件中設置相關外部定義的一些引用信息,真正的重定位是在程序運行之時。靜態連接方式有兩個大問題:如果庫中變量或函數有任何變化都必須重新編譯連接程序;如果多個程序引用同樣的變量/函數,則此變量/函數會在文件/內存中出現多次,浪費硬盤/內存空間。比較兩種連接方式生成的可執行文件的大小,可以看出有明顯的區別。

a.out 文件格式分析
a.out 格式在不同的機器平台和不同的 UNIX 操作系統上有輕微的不同,例如在 MC680x0 平台上有 6 個 section。下面我們討論的是最"標准"的格式。

a.out 文件包含 7 個 section,格式如下:
exec header(執行頭部,也可理解為文件頭部)
text segment(文本段)
data segment(數據段)
text relocations(文本重定位段)
data relocations(數據重定位段)
symbol table(符號表)
string table(字符串表)

執行頭部的數據結構:


struct exec {
unsigned long a_midmag; /* 魔數和其它信息 */
unsigned long a_text; /* 文本段的長度 */
unsigned long a_data; /* 數據段的長度 */
unsigned long a_bss; /* BSS段的長度 */
unsigned long a_syms; /* 符號表的長度 */
unsigned long a_entry; /* 程序進入點 */
unsigned long a_trsize; /* 文本重定位表的長度 */
unsigned long a_drsize; /* 數據重定位表的長度 */
};

文件頭部主要描述了各個 section 的長度,比較重要的字段是 a_entry(程序進入點),代表了系統在加載程序並初試化各種環境後開始執行程序代碼的入口。這個字段在後面討論的 ELF 文件頭部中也有出現。由 a.out 格式和頭部數據結構我們可以看出,a.out 的格式非常緊湊,只包含了程序運行所必須的信息(文本、數據、BSS),而且每個 section 的順序是固定的。這種結構缺乏擴展性,如不能包含"現代"可執行文件中常見的調試信息,最初的 UNIX 黑客對 a.out 文件調試使用的工具是 adb,而 adb 是一種機器語言調試器!

a.out 文件中包含符號表和兩個重定位表,這三個表的內容在連接目標文件以生成可執行文件時起作用。在最終可執行的 a.out 文件中,這三個表的長度都為 0。a.out 文件在連接時就把所有外部定義包含在可執行程序中,如果從程序設計的角度來看,這是一種硬編碼方式,或者可稱為模塊之間是強藕和的。在後面的討論中,我們將會具體看到ELF格式和動態連接機制是如何對此進行改進的。

a.out 是早期UNIX系統使用的可執行文件格式,由 AT&T 設計,現在基本上已被 ELF 文件格式代替。a.out 的設計比較簡單,但其設計思想明顯的被後續的可執行文件格式所繼承和發揚。可以參閱參考資料 16 和閱讀參考資料 15 源代碼加深對 a.out 格式的理解。參考資料 12 討論了如何在"現代"的紅帽LINUX運行 a.out 格式文件。

COFF 文件格式分析
COFF 格式比 a.out 格式要復雜一些,最重要的是包含一個節段表(section table),因此除了 .text,.data,和 .bss 區段以外,還可以包含其它的區段。另外也多了一個可選的頭部,不同的操作系統可一對此頭部做特定的定義。

COFF 文件格式如下:
File Header(文件頭部)
Optional Header(可選文件頭部)
Section 1 Header(節頭部)
………
Section n Header(節頭部)
Raw Data for Section 1(節數據)
Raw Data for Section n(節數據)
Relocation Info for Sect. 1(節重定位數據)
Relocation Info for Sect. n(節重定位數據)
Line Numbers for Sect. 1(節行號數據)
Line Numbers for Sect. n(節行號數據)
Symbol table(符號表)
String table(字符串表)

文件頭部的數據結構:


struct filehdr
{
unsigned short f_magic; /* 魔數 */
unsigned short f_nscns; /* 節個數 */
long f_timdat; /* 文件建立時間 */
long f_symptr; /* 符號表相對文件的偏移量 */
long f_nsyms; /* 符號表條目個數 */
unsigned short f_opthdr; /* 可選頭部長度 */
unsigned short f_flags; /* 標志 */
};

COFF 文件頭部中魔數與其它兩種格式的意義不太一樣,它是表示針對的機器類型,例如 0x014c 相對於 I386 平台,而 0x268 相對於 Motorola 68000系列等。當 COFF 文件為可執行文件時,字段 f_flags 的值為 F_EXEC(0X00002),同時也表示此文件沒有未解析的符號,換句話說,也就是重定位在連接時就已經完成。由此也可以看出,原始的 COFF 格式不支持動態連接。為了解決這個問題以及增加一些新的特性,一些操作系統對 COFF 格式進行了擴展。Microsoft 設計了名為 PE(Portable Executable)的文件格式,主要擴展是在 COFF 文件頭部之上增加了一些專用頭部,具體細節請參閱參考資料 18,某些 UNIX 系統也對 COFF 格式進行了擴展,如 XCOFF(extended common object file format)格式,支持動態連接,請參閱參考資料 5。

Copyright © Linux教程網 All Rights Reserved