可執行文件格式綜述
相對于其它文件類型,可執行文件可能是一個操作系統中最重要的文件類型,因為它們是完成操作的真正執行者。可執行文件的大小、運行速度、資源占用情況以及可擴展性、可移植性等與文件格式的定義和文件加載過程緊密相關。
研究可執行文件的格式對編寫高性能程序和一些黑客技術的運用都是非常有意義的。
不管何種可執行文件格式,一些基本的要素是必須的,顯而易見的,文件中應包含代碼和數據。因為文件可能引用外部文件定義的符號(變量和函數),因此重定位信息和符號信息也是需要的。一些輔助信息是可選的,如調試信息、硬件信息等。基本上任意一種可執行文件格式都是按區間保存上述信息,稱為段(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 |
其中函數名為任意。
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(執行頭部,也可理解為文件頭部) |
執行頭部的數據結構:
struct exec { |
文件頭部主要描述了各個 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 格式文件。


