pe文件
PE文件被稱為可移植的執行體是Portable Execute的全稱,常見的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微軟Windows操作系統上的程序文件(可能是間接被執行,如DLL)
MODULE標識模塊
讓我們復習一下幾個透過PE文件的設計了解到的基本概念。我用術語"MODULE"來表示一個可執行文件或一個DLL載入 內存的 代碼(CODE)、數據(DATA)、資源(RESOURCES),除了代碼和數據是你的程序直接使用的,一個模塊還可以由WINDOWS用來確定數據和代碼載入的位置的支撐數據結構組成。在16位WINDOWS中,這些支撐數據結構在模塊數據庫(用一個HMODULE來指示的段)中。在WIN32里面,這些數據結構在PE文件頭中,這些我將會簡要地解釋一下。PE文件最重要的兩個因素
關于PE文件最重要的是, 磁盤上的可執行文件和它被WINDOWS調入內存之后是非常相像的。WINDOWS載入器不必為從磁盤上載入一個文件而辛辛苦苦創建一個進程。載入器使用 內存映射文件機制來把文件中相似的塊映射到虛擬空間中。用一個構造式的分析模型,一個PE文件類似一個預制的屋子。它本質上開始于這樣一個空間,這個空間后面有幾個把它連到其余空間的機件(就是說,把它聯系到它的DLL上,等等)。這對PE格式的DLL是一樣容易應用的。一旦這個模塊被載入,Windows 就可以有效的把它和其它內存映射文件同等對待。 和16位Windows不同的是。16位NE文件的載入器讀取文件的一部分并且創建完全不同的數據結構在內存中表示模塊。當數據段或者代碼段需要載入時,載入器必須從全局堆中新申請一個段,從可執行文件中找出生鮮數據,轉到這個位置,讀入這些生鮮數據,并且要進行適當的修正。除此而外,每個16位模塊都有責任記住當前它使用的所有段選擇器,而不管這個段是否被丟棄了,如此等等。 對Win32來講,模塊所使用的所有代碼,數據,資源,導入表,和其它需要的模塊數據結構都在一個連續的內存塊中。在這種形勢下,你只需要知道載入器把可執行文件映射到了什么地方。通過作為映像的一部分的指針,你可以很容易的找到這個模塊所有不同的塊。相對虛擬地址
另一個你需要知道的概念是相對 虛擬地址(RVA)。PE文件中的許多域都用術語RVA來指定。一個RVA只是一些項目相對于文件映射到內存的偏移。比如說,載入器把一個文件映射到虛擬地址0x10000開始的內存塊。如果一個映像中的實際的表的首址是0x10464,那么它的RVA就是0x464。 (虛擬地址 0x10464)-(基地址 0x10000)=RVA 0x00464 為了把一個RVA轉化成一個有用的指針,只需要把RVA值加到模塊的基地址上即可。基地址是內存映射EXE和DLL文件的首址,在Win32中這是一個很重要的概念。為了方便起見,WindowsNT 和 Windows9x用模塊的基地址作為這個模塊的實例句柄(HINSTANCE)。在Win32中,把模塊的基地址叫做HINSTANCE可能導致混淆,因為術語"實例句柄"來自16位Windows。一個程序在16位Windows中的每個拷貝得到它自己分開的數據段(和一個聯系起來的全局句柄)來把它和這個程序其它的拷貝分別開來,就形成了術語"實例句柄"。在Win32中,每個程序不必和其它程序區別開來,因為他們不共享相同的地址空間。術語INSTANCE仍然保持16位windows和32位Windows之間的連續性。在Win32中重要的是你可以對任何DLL調用GetModuleHandle()得到一個指針去訪問它的組件(譯注)。譯注
如果 dllname 為 NULL,則得到執行體自己的模塊句柄。這是非常有用的,如通常編譯器產生的啟動代碼將取得這個句柄并將它作為一個參數hInstance傳給WinMain ! 你最終需要理解的PE文件的概念是"塊(Section)"。PE文件中的一個塊和NE文件中的一個段或者資源等價。塊可以包含代碼或者數據。和段不同的是,塊是內存中連續的空間,而沒有尺寸限制。當你的連接器和庫為你建立,并且包含對操作系統非常重要的信息的其它的數據塊時,這些塊包含你的程序直接聲明和使用的代碼或數據。在一些PE格式的描述中,塊也叫做對象。術語對象有如此多的涵義,以至于只能把代碼和數據叫做"塊"。
和其它可執行文件格式一樣,PE文件在眾所周知的地方有一些定義文件其余部分面貌的域。首部就包含這樣像代碼和數據的位置和尺寸的地方,操作系統要對它進行干預,比如初始堆棧大小,和其它重要的塊的信息,我將要簡短的介紹一下。和微軟其它可執行格式相比,主要的首部不是在文件的最開始。典型的PE文件最開始的數百個字節被DOS殘留部分占用。這個殘留部分是一個可以打印如"這個程序不能在DOS下運行!"這類信息的小程序。所以,你在一個不支持Win32的系統中運行這個程序,便可以得到這類錯誤信息。當載入器把一個Win32程序映射到內存,這個映射文件的第一個字節對應于DOS殘留部分的第一個字節。那是無疑的。和你啟動的任一個基于Win32 的程序一起,都有一個基于DOS的程序連帶被載入。 和微軟的其它可執行格式一樣,你可以通過查找它的起始偏移來得到真實首部,這個偏移放在DOS殘留首部中。WINNT.H頭文件包含了DOS殘留程序的數據結構定義,使得很容易找到PE首部的起始位置。e_lfanew 域是PE真實首部的偏移。為了得到PE首部在內存中的指針,只需要把這個值加到映像的基址上即可。 file://忽/略類型轉化和指針轉化 ... pNTHeader = dosHeader + dosHeader->e_lfanew; 一旦你有了PE主首部的指針,游戲就可以開始了!PE主首部是一個IMAGE_NT_HEADERS的結構,在WINNT.H中定義。這個結構由一個雙字(DWORD)和兩個子結構組成,布局如下: DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; 標志域用ASCII表示就是"PE"。如果在DOS首部中用了e_lfanew域,你得到一個NE標志而不是PE,那么這是16位NE文件。同樣的,在標志域中的LE表示這是一個Windows3.x 的虛擬設備驅動程序(VxD)。LX表示這個文件是OS/2 2.0文件。 PE DWORD標志后的是結構 IMAGE_FILE_HEADER 。這個域只包含這個文件最基本的信息。這個結構表現為并未從它的原始COFF實現更改過。除了是PE首部的一部分,它還表現在微軟Win32編譯器生成的COFF OBJ 文件的最開始部分。IMAGE_FILE_HEADER的這個域顯示在下面: 表2 IMAGE_FILE_HEADER Fields WORD Machine 表示CPU的類型,下面定義了一些CPU的ID 0x14d Intel i860 0x14c Intel I386 (same ID used for 486 and 586) 0x162 MIPS R3000 0x166 MIPS R4000 0x183 DEC Alpha AXP WORD NumberOfSections 這個文件中的塊數目。 DWORD TimeDateStamp 連接器產生這個文件的日期(對OBJ文件是編譯器),這個域保存的數是從1969年12月下午4:00開始到現在經過的秒數。 DWORD PointerToSymbolTable COFF符號表的文件偏移量。這個域只用于有COFF調試信息的OBJ文件和PE文件,PE文件支持多種調試信息格式,所以調試器應該指向數據目錄的IMAGE_DIRECTORY_ENTRY_DEBUG條目。 DWORD NumberOfSymbols COFF符號表的符號數目。見上面。 WORD SizeOfOptionalHeader 這個結構后面的可選首部的尺寸。在OBJ文件中,這個域是0。在可執行文件中,這是跟在這個結構后的IMAGE_OPTIONAL_HEADER結構的尺寸。 WORD Characteristics 關于這個文件信息的標志。一些重要的域如下: 0x0001 這個文件中沒有重定位信息 0x0002 可執行文件映像(不是OBJ或LIB文件) 0x2000 文件是動態連接庫,而非程序 其它域定義在WINNT.H中。 PE首部的第三個組成部分是一個IMAGE_OPTIONAL_HEADER型的結構。對PE文件,這一部分當然不是"可選的"。COFF格式允許單獨實現來定義一個超出標準IMAGE_FILE_HEADER附加信息的結構。IMAGE_OPTIONAL_HEADER里面的域是PE的實現者感到超出IMAGE_FILE_HEADER基本信息以外非常關鍵的信息。 并非 IMAGE_OPTIONAL_HEADER 的所有域都是重要的(見圖4)。比較重要,需要知道的是ImageBase 和 SubSystem 域。你可以忽略其它域的描述。 表3 IMAGE_FILE_HEADER 的域: WORD Magic 表現為一些類別的標志字,通常是0X010B 。 BYTE MajorLinkerVersion BYTE MinorLinkerVersion 生成這個文件的連接器的版本。這個數字以十進制顯示比用十六進制好。一個典型的連接器版本是2.23。 DWORD SizeOfCode 所有代碼塊的進位尺寸。通常大多數文件只有一個代碼塊,所以這個域和 .TEXT 塊匹配。 DWORD SizeOfInitializedData 已初始化的數據組成的塊的大小(不包括代碼段)。然而,和它在文件中的表現形式并不一致。 DWORD SizeOfUninitializedData 載入器在虛擬內存中申請空間,但在磁盤上的文件中并不占用空間的塊的尺寸。這些塊在程序啟動時不需要指定初值,因此術語名就是"未初始化的數據"。未初始化的數據通常在一個名叫 .bss 的塊中。 DWORD AddressOfEntryPoint 載入器開始執行這個程序的地址,即這個PE文件的入口地址。這是一個RVA,通常在 .text 塊中。 DWORD BaseOfCode 代碼塊起始地址的RVA 。在內存中,代碼塊通常在PE首部之后,數據塊之前。在微軟的連接器產生的EXE文件中,這個值通常是0x1000 。Borland 的連接器 TLINK32 也一樣,把映像第一個代碼塊的RVA和映像基址相加,填入這個域。 譯注:這個域好像一直沒有什么用 DWORD BaseOfData 數據塊起始地址的RVA 。在內存中,數據塊經常在最后,在PE首部和代碼塊之后。 譯注:這個域好像也一直沒有什么用 DWORD ImageBase 連接器創建一個可執行文件時,它假定這個文件被映射到內存中的一個指定的地方,這個地址就存在這個域中,假定一個載入地址可以使連接器優化以便節省空間。如果載入器真的把這個文件映射到了這個地方,在運行之前代碼不需要任何改變。在為WindowsNT 創建的可執行文件中,默認的ImageBase 是0x10000。對DLL,默認是0x40000。在Window95中,地址0x10000不能用來載入32位EXE文件,因為這個區域在一個被所有進程共享的線性地址空間中。因此,微軟把Win32可執行文件的默認基址改為0x40000,假定基址為0x10000 的老程序坐在Windows95 中需要更長的載入時間,這是因為載入器需要重定位基址。 譯注:這個域即"Prefered Load Address",如果沒有什么意外,這就是該PE文件載入內存后的地址。 DWORD SectionAlignment 映射到內存中時,每個塊都必須保證開始于這個值的整數倍。為了分頁的目的,默認的SectionAlignment 是 0x1000。 DWORD FileAlignment 在PE文件中,組成每個塊的生鮮數據必須保證開始于這個值的整數倍。默認值是0x200 字節,也許是為了保證塊都開始于一個磁盤扇區(一個扇區通常是 512 字節)。這個域和NE文件中的段/資源對齊(segment/resource alignment)尺寸是等價的。和NE文件不同的是,PE文件通常沒有數百個的塊,所以,為了對齊而浪費的通常空間很少。 WORD MajorOperatingSystemVersion WORD MinorOperatingSystemVersion 這個程序運行需要的操作系統的最小版本號。這個域有點含糊,因為Subsystem 域(后面將會說到)可以提供類似的功能。這個域在到目前為止的Win32中默認是1.0。 WORD MajorImageVersion WORD MinorImageVersion 一個可由用戶定義的域。這允許你有不同的EXE和DLL版本。你可以通過鏈接器的 /version 選項設置這個域的值。例如:"link /version:2.0 myobj.obj"。 WORD MajorSubsystemVersion WORD MinorSubsystemVersion 這個程序運行需要的最小子系統版本號。這個域的一個典型值是3.10 (表示WindowsNT 3.1)。 DWORD Reserved1 通常是 0 。 DWORD SizeOfImage 載入器必須關心的這個映像所有部分的大小總和。是從映像的開始到最后一個塊結尾這段區域的大小。最后一個塊結尾按SectionAlignment進位。 譯注:這個很重要,可以大,但不可以小! DWORD SizeOfHeaders PE首部和塊表的大小。塊的實際數據緊跟在所有首部組件之后。 DWORD CheckSum 這個文件的CRC校驗和。在微軟可執行格式中,這個域被忽略并且置為0 。這個規則的一個例外情況是信任服務,這類EXE文件必須有一個合法的校驗和。 WORD Subsystem 可執行文件的用戶界面使用的子系統類型。WINNT.H 定義了下面這些值: NATIVE 1 不需要子系統(比如設備驅動) WINDOWS_GUI 2 在Windows圖形用戶界面子系統下運行 WINDOWS_CUI 3 在Windows字符子系統下運行(控制臺程序) OS2_CUI 5 在OS/2字符子系統下運行(僅對OS/2 1.x) POSIX_CUI 7 在 Posix 字符子系統下運行 WORD DllCharacteristics 指定在何種環境下一個DLL的初始化函數(比如DllMain)將被調用的標志變量。這個值經常被置為0 。但是操作系統在下面四種情況下仍然調用DLL的初始化函數。 下面的值定義為: 1 DLL第一次載入到進程中的地址空間中時調用 2 一個線程結束時調用 4 一個線程開始時調用 8 退出DLL時調用 DWORD SizeOfStackReserve 為初始線程保留的虛擬內存總數。然而并不是所有這些內存都被提交(見下一個域)。這個域的默認值是0x100000(1Mbytes)。如果你在CreateThread 中把堆棧尺寸指定為 0 ,結果將是用這個相同的值(0x10000)。 DWORD SizeOfStackCommit 開始提交的初始線程堆棧總數。對微軟的連接器,這個域默認是0x1000字節(一頁),TLINK32 是兩頁。 DWORD SizeOfHeapReserve 為初始進程的堆保留的虛擬內存總數。這個堆的句柄可以用GetPocessHeap 得到。并不是所有這些內存都被提交(見下一個域)。 DWORD SizeOfHeapCommit 開始為進程堆提交的內存總數。默認是一頁。
文件的含義
PE文件的意思是Portable Executable(可移植,可執行),它是win32可執行文件的標準格式.它的一些特性繼承unix的COFF文件格式,同時保留了與舊版MS-DOS和WINDOWS的兼容.其可移植可執行意味著是跨win32平臺的.文件的層次結構
PE文件最前面緊隨DOS MZ文件頭的是一個DOS可執行文件(Stub).這使得PE文件成為一個合法的MS-DOS可執行文件.DOS MZ文件頭后面是一個32位的PE文件標志0x50450000(IMAGE_NT_SIGNATURE),即PE00.接下來的是PE的映像文件頭,包含的信息有該程序的運行平臺,有多少個節,文件鏈接的時間,文件的命名格式.后面還緊跟一個可選映像頭,包含PE文件的邏輯分布信息,程序加載信息,開始地址,保留的堆棧數量,數據段大小等.可選頭還有一個重要的域,稱為:數據目錄表"的數組,表的每一項都是指向某一節的指針.可選映像頭后面緊跟的是節表和節.節通過節表來實現索引.實際上,節的內容才是真正執行的數據和程序.每一個節都有相關的標志.每一個節會被一個或多個目錄表指向,目錄表可通過可選頭的"數據目錄表"的入口找到.就像輸出函數表或基址重定位表.也存在沒有目錄表指向的節.文件層次解釋
A.DOS STUB和DOS頭 DOS插樁程序在大多數情況下由匯編器/編譯器自動產生.通常它調用INT 21H服務9來顯示上述字符串.可以通過IMAGE_DOS_HEADER結構來識別一個合法的DOS頭.這個結構的頭兩個字節肯定是"MZ".可通過該結構的e_lfanew成員來找到PE文件的開始標志.MS-DOS頭部占據了PE文件的頭64個字節.在微軟的WINNT.H中可以找到其內容結構的描述.文件頭
在DOS STUB后是PE文件頭(PE header).PE文件頭是PE相關結構IMGAE_NT_HEADERS的簡稱,即NT映像頭,存放PE整個文件信息發布的重要字段,包含了PE裝載器用到的重要域.執行體在操作系統中執行時,PE裝載器將從DOS MZ頭中找到PE頭文件的起始偏移量e_lfanew,從而跳過DOS STUB直接定位真正的PE文件.它由3部分組成: (1)PE文件標志(4H字節) PE文件標志0x50450000即PE00,標志著NT映像頭的開始,也是PE文件中與windows有關內容的開始. (2)映像文件(14H字節) 是NT映像文件的主要部分,包含PE文件的基本信息 (3)可選映像頭 包含PE文件的邏輯分布信息. C.節表 節表其實是緊跟NT映像文件的一個結構數組.其成員數目由映像文件頭結構NumberOFSectios域的值來決定. D.節 PE文件的真正內容劃分為塊,稱之為節.節的劃分基于各組數據的共同屬性.惟有節的屬性設置決定了節的特性和功能.典型的windows NT 應用程序可以具有9個節:.texr,.bss.rdata,.data,.rsrc,edata,idata,pdata,.debug 判斷一個文件是否為PE文件 var //檢測指定文件是否有效PE文件 PEDosHead: TImageDosHeader; PENTHead: TImageNtHeaders; m_file: integer; begin Result := False; m_file := FileOpen(filename, fmOpenRead or fmShareDenyNone); //只讀和其它任意 if m_File > 0 then try FileSeek(m_file, 0, soFromBeginning); //將指針挪至文件頭 FileRead(m_file, PEDosHead, SizeOf(PEDosHead)); //讀PEDosHead結構 FileSeek(m_file, PEDosHead._lfanew, soFromBeginning); //將指針挪至_lfanew FileRead(m_file, PENTHead, SizeOf(PENTHead)); //讀PENTHead結構 finally FileClose(m_file); end; if (PENTHead.Signature = IMAGE_NT_SIGNATURE) then //檢驗文件頭部第一個字的值是否等于 IMAGE_DOS_SIGNATURE Result := True; end; pe文件結構圖總結
- 上一篇: 职业英语
- 下一篇: 释疑の字段符号 FIELD-SYMBOL