PE文件格式詳解(一)――基礎知識

什麼是PE文件格式:
    我們知道所有文件都是一些連續(當然實際存儲在磁盤上的時候不一定是連續的)的數據組織起來的,不同類型的文件肯定組織形式也各不相同;PE文件格式便是一種文件組織形式,它是32位Window系統中的可執行文件EXE以及動態連接庫文件DLL的組織形式。爲什麼我們雙擊一個EXE文件之後它就會被Window運行,而我們雙擊一個DOC文件就會被Word打開並顯示其中的內容;這說明文件中肯定除了存在那些文件的主體內容(比如EXE文件中的代碼,數據等,DOC文件中的文件內容等)之外還存在其他一些重要的信息。這些信息是給文件的使用者看的,比如說EXE文件的使用者就是Window,而DOC文件的使用者就是Word。Window可以根據這些信息知道把文件加載到地址空間的那個位置,知道從哪個地址開始執行;加載到內存後如何修正一些指令中的地址等等。那麼PE文件中的這些重要信息都是由誰加入的呢?是由編譯器和連接器完成的,針對不同的編譯器和連接器通常會提供不同的選項讓我們在編譯和聯結生成PE文件的時候對其中的那些Window需要的信息進行設定;當然也可以按照默認的方式編譯連接生成Window中默認的信息。例如:WindowNT默認的程序加載基址是0x40000;你可以在用VC連接生成EXE文件的時候使用選項更改這個地址值。在不同的操作系統中可執行文件的格式是不同的,比如在Linux上就有一種流行的ELF格式;當然它是由在Linux上的編譯器和連接器生成的,所以編譯器、連接器是針對不同的CPU架構和不同的操作系統而涉及出來的。在嵌入式領域中我們經常提到交叉編譯器一詞,它的作用就是在一種平臺下編譯出能在另一個平臺下運行的程序;例如,我們可以使用交叉編譯器在跑Linux的X86機器上編譯出能在Arm上運行的程序。

程序是如何運行起來的:
    一個程序從編寫出來到運行一共需要那些工具,他們都對程序作了些什麼呢?裏面都涉及哪些知識需要學習呢?先說工具:編輯器-》編譯器-》連接器-》加載器;首先我們使用編輯器編輯源文件;然後使用編譯器編譯程目標文件OBJ,這裏面涉及到編譯原理的知識;連接器把OBJ文件和其他一些庫文件和資源文件連接起來生成EXE文件,這裏面涉及到不同的連接器的知識,連接器根據OS的需要生成EXE文件保存着磁盤上;當我們運行EXE文件的時候有Window的加載器負責把EXE文件加載到線性地址空間,加載的時候便是根據上一節中說到的PE文件格式中的哪些重要信息。然後生成一個進程,如果進程中涉及到多個線程還要生成一個主線程;此後進程便開始運行;這裏面涉及的東西很多,包括:PE文件格式的內容;內存管理(CPU內存管理的硬件環境以及在此基礎上的OS內存管理方式);模塊,進程,線程的知識;只有把這些都弄清楚之後才能比較清楚的瞭解這整個過程。下面就讓我們先來學習PE文件格式吧。

PE文件的總體結構:
    下圖便是PE文件的一個總體結構:注意,圖2是在圖1的基礎上進一步細化了,不過圖2的順序是從下向上代表文件的從頭到尾的順序。

DOS MZ Header

DOS stub

PE header

Section table

Section 1

Section 2

Section ...

Section n

圖一圖2

323856721.png

我們的EXE文件在磁盤上就是按照上面的格式順序存儲的,當運行的時候它就很容易被加載器加載到線性地址空間;但是在線性空間中和在磁盤上不同,在線性空間中各個部分不一定是佔據連續的線性地址空間。下面對PE文件格式的介紹就按照上圖中對從頭到尾對每個部分進行介紹。好的,今天剛去醫院回來有些累了,就先寫到這兒吧。

嗯,不行,還有幾個重要而又基礎的概念需要在這兒先澄清一下,否則後面就會出亂子了。

幾個重要的基本概念:

1)節:PE文件的真正內容劃分成塊,稱之爲sections(節)。每節是一塊擁有共同屬性的數據,比如代碼/數據、讀/寫等。我們可以把PE文件想象成一邏輯磁盤,PE header 是磁盤的boot扇區,而sections就是各種文件,每種文件自然就有不同屬性如只讀、系統、隱藏、文檔等等。 值得我們注意的是 ---- 節的劃分是基於各組數據的共同屬性: 而不是邏輯概念。重要的不是數據/代碼是如何使用的,如果PE文件中的數據/代碼擁有相同屬性,它們就能被歸入同一節中。不必關心節中類似於"data", "code"或其他的邏輯概念: 如果數據和代碼擁有相同屬性,它們就可以被歸入同一個節中。(節名稱僅僅是個區別不同節的符號而已,類似"data", "code"的命名只爲了便於識別,惟有節的屬性設置決定了節的特性和功能)如果某塊數據想付爲只讀屬性,就可以將該塊數據放入置爲只讀的節中,當PE裝載器映射節內容時,它會檢查相關節屬性並置對應內存塊爲指定屬性。下面是常見的節名及作用:

節名

作用

.arch

最初的構建信息(Alpha Architecture Information)

.bss

未經初始化的數據

.CRT

C運行期只讀數據

.data

已經初始化的數據

.debug

調試信息

.didata

延遲輸入文件名錶

.edata

導出文件名錶

.idata

導入文件名錶

.pdata

異常信息(Exception Information)

.rdata

只讀的初始化數據

.reloc

重定位表信息

.rsrc

資源

.text

.exe或.dll文件的可執行代碼

.tls

線程的本地存儲器

.xdata

異常處理表

注意:上面已經說過了“節的劃分是基於各組數據的共同屬性: 而不是邏輯概念。重要的不是數據/代碼是如何使用的,如果PE文件中的數據/代碼擁有相同屬性,它們就能被歸入同一節中” 所以上面表中列出的節並不一定單獨成節,也就是說即使存在上面表中的某一節,在節表(section table)(後面會講到)中也不一定就有於之對應的項,因爲它可能和別的具有共同屬性的節共同組成了一節。比如 .idata 可以和 .text 合成一節而命名爲 .text,而在節表中只有和 .text 對應的項。這也就是後面的optional header中數據目錄(DataDirectory)存在的作用,因爲很多有用的節被合併了,因此加載器無法通過節表來定位它們,所以這就是數據目錄(DataDirectory)發揮作用的時候了(具體作用後面會講到)。

2)虛擬地址:虛擬地址即程序中使用的地址,也就是從程序員的角度看到的地址,有時也叫邏輯地址;通常使用段地址:偏移量的形式表示,不過在32位系統中使用的是平坦(Flat)內存模式,所以我們可以不用管段地址,只考慮32位的偏移量即可,認爲32位的偏移量就是虛擬地址,這樣一來程序員就可以認爲他是在一個段中寫程序,這個段的大小是232 = 4G的容量,當然這部分地址空間是程序和OS共享的,程序員可以利用的大約有2G(具體可以參考Win98和WinNT的內存佈局);所以我們平時在寫程序申請內存的時候實際上申請的就是這2G的線性地基空間,由於所有的4G線性地址空間都被OS作爲資源來管理(這4G的線性地址空間是通過頁表來表現出來的,OS分配線性地址空間給進程也就是分配相應的頁表給進程),所以我們無論用什麼方式使用內存最終都是轉換爲OS爲我們分配線性地址空間,至於分配的線性地址空間又如何被映射爲真正的物理內存完全是有OS負責的(更詳細資料參見“Windows 內存管理”),程序員不必操心。

3)相對虛擬地址:「相對虛擬地址(Relative VirtualAddress,RVA)」即相對於上面的基地址的偏移量。PE 文件中的許多字段內容都是以RVA 表示,一個RVA 是某一資料項的offset(偏移)值-- 從文件被映像進來的起點(即基地址)算起。舉個例子,我們說Windows加載器把一個PE 文件映像到虛擬地址空間的0x400000 處,如果此image 有一個表格開始於0x401464,那麼這個表格的RVA 就是0x1464:虛擬地址0x401464 - 基地址0x400000 = RVA 0x1464只要把RVA 加上基地址,RVA 就可以被轉換爲一個有用的指針。在PE文件中大多數地址多是RVA 而 RVA只有當PE文件被PE裝載器裝入內存後纔有意義。 如果我們直接將文件映射到內存而不是通過PE裝載器載入,那麼我們就不能直接使用那些RVA。必須先將那些RVA轉換成文件偏移量,RVAToOffset函數就起到這個作用。

4)基地址:「基地址(base address)」是一個重要概念,用來描述被映像到內存中的EXE 或DLL 的起始地址。爲了方便,Windows NT 和Windows 95 都以模塊的基地址做爲模塊的instance handle(HINSTANCE,實例句柄)。Windows95加載器把一個PE 文件映像到虛擬地址空間的0x400000 處;而WindowNT加載器把一個PE 文件映像到虛擬地址空間的0x10000 處 。

5)文件偏移量:文件中的地址與內存中表示不同,它是用偏移量(File offset)來表示的,文件中的第一個字節的偏移量是0,後面的字節依次遞增。在SoftICE和W32Dasm下顯示的地址值是內存線性地址,或稱之爲虛擬地址(Virual Address,VA)。而十六進制工具裏,如:Hiew、Hex Workshop等顯示的地址就是文件地址,稱之爲偏移量(File offset) 或物理地址(RAW offset,注意這個物理地址不是內存尋址中說到的物理地址 )。

6)模塊:「模塊(module)」一詞表示一個EXE 或DLL 被加載內存後的程序代碼、數據和資源(就是被加載到內存後的EXE或DLL整體,包括代碼、數據和資源,而不是說代碼、數據、資源分別都是模塊)。除了程序代碼和數據是你的程序直接使用的之外,模塊還內含一些支持性數據,Windows 用它來決定程序代碼和數據放在內存的什麼地方,在Win32,這些信息保留在PE頭部(即圖1中的PE header,實際上它是一個IMAGE_NT_HEADERS 結構)中。

7)邏輯地址:見“虛擬地址”

8)線性地址:線性地址是由虛擬地址(邏輯地址)轉換來的,轉換需要CPU和OS共同合作來完成;裏面涉及到全局描述符表GDT和局部描述符表LDT;不過由於32位的Window系統採用flat內存模式,所以我們可以認爲虛擬地址就是線性地址,即我們可以認爲邏輯地址中的32位偏移量就是線性地址。

9)物理地址:即最終發往地址總線上的地址,它對應着實際的物理內存,在32位的Window存儲管理中它是通過頁表由線性地址轉換出來的。

10)實際地址:即“物理地址”。

其中前面的6個概念是學習PE文件格式需要知道的,後面的幾個主要在內存管理裏面提到,在這裏爲了便於區別一起列了出來。

發佈了75 篇原創文章 · 獲贊 5 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章