PE 文件格式(二)

{
        strupr(argv[i]);
       
        // Is it a switch character?
        if ( (argv[i][0] = = '-') || (argv[i][0] = = '/') )
        {
            if ( argv[i][1] = = 'A' )
            {   fShowRelocations = TRUE;
                fShowRawSectionData = TRUE;
                fShowSymbolTable = TRUE;
                fShowLineNumbers = TRUE; }
            else if ( argv[i][1] = = 'H' )
                fShowRawSectionData = TRUE;
            else if ( argv[i][1] = = 'L' )
                fShowLineNumbers = TRUE;
            else if ( argv[i][1] = = 'R' )
                fShowRelocations = TRUE;
            else if ( argv[i][1] = = 'S' )
                fShowSymbolTable = TRUE;
        }
        else    // Not a switch character.  Must be the filename
        {   return argv[i]; }
    }
}

 

int main(int argc, char *argv[])
{
    PSTR filename;
   
    if ( argc = = 1 )
    {   printf(    HelpText );
        return 1; }
   
    filename = ProcessCommandLine(argc, argv);
    if ( filename )
        DumpFile( filename );
    return 0;
}

 

 
1 WIN32 與 PE 基本概念
讓我們複習一下幾個透過PE文件的設計瞭解到的基本概念(見圖1)。我用術語"MODULE"來表示一個可執行文件或一個DLL載入內存的代碼(CODE)、數據(DATA)、資源(RESOURCES),除了代碼和數據是你的程序直接使用的,一個模塊還可以由WINDOWS用來確定數據和代碼載入的位置的支撐數據結構組成。在16位WINDOWS中,這些支撐數據結構在模塊數據庫(用一個HMODULE來指示的段)中。在WIN32裏面,這些數據結構在PE文件頭中,這些我將會簡要地解釋一下。
 
圖1  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格式的描述中,塊也叫做對象。術語對象有如此多的涵義,以至於只能把代碼和數據叫做"塊"。
2 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/0/0"。如果在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 
<script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"> </script>  載入器在虛擬內存中申請空間,但在磁盤上的文件中並不佔用空間的塊的尺寸。這些塊在程序啓動時不需要指定初值,因此術語名就是"未初始化的數據"。未初始化的數據通常在一個名叫 .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  不需要子系統(比如設備驅動)
<script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"> </script>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章