歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> UNIX與Linux操作系統下可執行文件格式解析

UNIX與Linux操作系統下可執行文件格式解析

日期:2017/2/28 17:49:01   编辑:Linux教程

可執行文件格式綜述

  相對於其它文件類型,可執行文件可能是一個操作系統中最重要的文件類型,因為它們是完成操作的真正執行者。可執行文件的大小、運行速度、資源占用情況以及可擴展性、可移植性等與文件格式的定義和文件加載過程緊密相關。

  研究可執行文件的格式對編寫高性能程序和一些黑客技術的運用都是非常有意義的。

  不管何種可執行文件格式,一些基本的要素是必須的,顯而易見的,文件中應包含代碼和數據。因為文件可能引用外部文件定義的符號(變量和函數),因此重定位信息和符號信息也是需要的。一些輔助信息是可選的,如調試信息、硬件信息等。基本上任意一種可執行文件格式都是按區間保存上述信息,稱為段(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 程序可能引用共享庫定義的變量或函數,換句話說就是程序運行時必須知道這些變量/函數的地址。在靜態連接中,程序所有需要使用的外部定義都完全包含在可執行程序中,而動態連接則只在可執行文件中設置相關外部定義的一些引用信息,真正的重定位是在程序運行之時。靜態連接方式有兩個大問題:如果庫中變量或函數有任何變化都必須重新編譯連接程序;如果多個程序引用同樣的變量/函數,則此變量/函數會在文件/內存中出現多次,浪費硬盤/內存空間。比較兩種連接方式生成的可執行文件的大小,可以看出有明顯的區別。

Copyright © Linux教程網 All Rights Reserved