pe文件結(jié)構(gòu)圖PE文件被稱為可移植的執(zhí)行體是Portable Execute的全稱,常見的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微軟Windows操作系統(tǒng)上的程序文件(可能是間接被執(zhí)行,如DLL)
一個操作系統(tǒng)的可執(zhí)行文件格式在很多方面是這個系統(tǒng)的一面鏡子。雖然學(xué)習(xí)一個可執(zhí)行文件格式通常不是一個程序員的首要任務(wù),但是你可以從這其中學(xué)到大量的知識。在這篇文章中,我會給出 MicroSoft 的所有基于 win32系統(tǒng)(如winnt,win9x)的可移植可執(zhí)行(PE)文件格式的詳細(xì)介紹。在可預(yù)知的未來,包括 Windows2000 , PE 文件格式在 MicroSoft 的操作系統(tǒng)中扮演一個重要的角色。如果你在使用 Win32 或 Winnt ,那么你已經(jīng)在使用 PE 文件了。甚至你只是在 Windows3.1 下使用 Visual C++ 編程,你使用的仍然是 PE 文件(Visual C++ 的 32 位 MS-DOS 擴(kuò)展組件用這個格式)。簡而言之,PE 格式已經(jīng)普遍應(yīng)用,并且在不短的將來仍是不可避免的,F(xiàn)在是時候找出這種新的可執(zhí)行文件格式為操作系統(tǒng)帶來的東西了。
我最后不會讓你盯住無窮無盡的十六進(jìn)制Dump,也不會詳細(xì)討論頁面的每一個單獨的位的重要性。代替的,我會向你介紹包含在 PE 文件中的概念,并且將他們和你每天都遇到的東西聯(lián)系起來。比如,線程局部變量的概念,如下所述:
declspec(thread) int i;
我快要發(fā)瘋了,直到我發(fā)現(xiàn)它在可執(zhí)行文件中實現(xiàn)起來是如此的簡單并且優(yōu)雅。既然你們中的許多人都有使用 16 Windows 的背景,我將把 Win32 PE 文件的構(gòu)造追溯到和它等價的16 位 NE 文件。
除了一個不同的可執(zhí)行文件格式, MicroSoft 還引入了一個用它的編譯器和匯編器生成的新的目標(biāo)模塊格式。這個新的 OBJ 文件格式有許多和PE 文件共同的東東。我做了許多無用功去查找這個新的 OBJ 文件格式的文檔。所以我以自己的理解對它進(jìn)行解析,并且,在這里,除了 PE 文件,我會描述它的一部分。
大家都知道,Windows NT 繼承了 VAX? VMS? 和 UNIX? 的傳統(tǒng)。許多 Windows NT 的創(chuàng)始人在進(jìn)入微軟前都在這些平臺上進(jìn)行設(shè)計和編碼。當(dāng)他們開始設(shè)計 Windows NT 時,很自然的,為了最小化項目啟動時間,他們會使用以前寫好的并且已經(jīng)測試過的工具。用這些工具生成的并且工作的可執(zhí)行和 OBJ 文件格式叫做 COFF (Common Object File Format 的首字母縮寫)。COFF 的相對年齡可以用八進(jìn)制的域來指定。COFF 本身是一個好的起點,但是需要擴(kuò)展到一個現(xiàn)代操作系統(tǒng)如 Windows 95 和 Windows NT 的需要。這個更新的結(jié)果就是(PE格式)可移植可執(zhí)行文件格式。它被稱為"可移植的"是因為在所有平臺(如x86,Alpha,MIPS等等)上實現(xiàn)的WindowsNT 都使用相同的可執(zhí)行文件格式。當(dāng)然了,也有許多不同的東西如二進(jìn)制代碼的CPU指令。重要的是操作系統(tǒng)的裝入器和程序設(shè)計工具不需要為任何一種CPU完全重寫就能達(dá)到目的。
MicroSoft 拋棄現(xiàn)存的32位工具和可執(zhí)行文件格式的事實證實了他們想讓 WindowsNT 升級并且運行的更快的決心。為16位Windows編寫的虛擬設(shè)備驅(qū)動程序用一種不同的32位文件布局--LE 文件格式--WindowsNT出現(xiàn)很早以前就存在了。比這更重要的是對 OBJ 文件的替換!在 WindowsNT 的 C 編譯器以前,所有的微軟編譯器都用 Intel 的 OMF ( Object Module Format ) 規(guī)范。就像前面提到的,MicroSoft 的 Win32 編譯器生成 COFF 格式的 OBJ 文件。一些微軟的競爭者,如 Borland 和 Symentec ,選擇放棄了 COFF 格式并堅持 Intel 的 OMF 文件格式。這樣的結(jié)果是制作 OBJ 和 LIB 的公司為了使用多個不同的編譯器,不得不為每個不同的編譯器分發(fā)這些庫的不同版本(如果他們不這么做)。
PE 文件格式在 winnt.h 頭文件中文檔化了(用最不精確的語言)!大約在 winnt.h 的中間部分標(biāo)題為"Image Format"的一個快。在把 MS-DOS 的 MZ 文件頭和 NE 文件頭移入新的PE文件頭之前,這個塊就開始于一個小欄。WINNT.H提供PE文件用到的生鮮數(shù)據(jù)結(jié)構(gòu)的定義,但只有很少有助于理解這些數(shù)據(jù)結(jié)構(gòu)和標(biāo)志變量的注釋。不管誰為PE文件格式寫出這樣的頭文件都肯定是一個信徒無疑(突然持續(xù)地冒出Michael J. O'Leary的名字來)。描述名字,連同深嵌的結(jié)構(gòu)體和宏。當(dāng)你配套winnt.h進(jìn)行編碼時,類似下面這樣的表達(dá)式并不鮮見:
pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG]
.VirtualAddress;
為了有助于邏輯的理解這些winnt.h中的信息,閱讀可移植可執(zhí)行和公共對象文件格式的規(guī)格說明,這些在MSDN既看光盤中是可用的,一直包括到2001年8月。
現(xiàn)在讓我們轉(zhuǎn)換到COFF格式的OBJ文件的主體上來,WINNT.H包括COFF OBJ和LIB的結(jié)構(gòu)化定義和類型定義。不幸的是,我還沒有找到上面提到的可執(zhí)行文件格式的類似文檔。既然PE文件和COFF OBJ文件是如此的相似,我決定是時間把這些文件帶到重點上來,并且把它們也文檔化。僅僅讀過了關(guān)于PE文件的組成,你自己也想Dump一些PE文件來看這些概念。如果你用微軟基于32位WINDOWS的開發(fā)工具,DUMPBIN 程序可以將PE文件和COFF OBJ/LIB文件轉(zhuǎn)化為可讀的形式。在所有的PEDump器中,DUMPBIN是最容易理解的。它恰好有一些很好的選項來反匯編它正解析的文件的代碼塊,Borland用戶可以使用tdump來瀏覽PE文件,但tdump不能解析 COFF OBJ/LIB 文件。這不是一個重要的東西因為Borland的編譯器首先就不生成 COFF 格式的OBJ文件。
我寫了一個PE和COFF OBJ 文件的Dump程序--PEDUMP,我想提供一些比DUMPBIN更加可理解的輸出。雖然它沒有反匯編器以及和LIB庫文件一起工作,它在其他方面和DUMPBIN是一樣的,并且加入了一些新的特性來使它值得被認(rèn)同。它的源代碼在任何一個MSJ電子公報版上都可以找到,所有我不打算在這里把他全部列出。作為代替,我展示一些從PEDUMP得到的示例輸出來闡明我為它們描述的概念。
譯注:--說實話,我從這這份代碼中幾乎唯一學(xué)到的東西就是"如何處理命令行",其它的都沒學(xué)到。
表 1 PEDUMP.C
file://--------------------/
// PROGRAM: PEDUMP
// FILE: PEDUMP.C
// AUTHOR: Matt Pietrek - 1993
file://--------------------/
#include <windows.h>
#include <stdio.h>
#include "objdump.h"
#include "exedump.h"
#include "extrnvar.h"
// Global variables set here, and used in EXEDUMP.C and OBJDUMP.C
BOOL fShowRelocations = FALSE;
BOOL fShowRawSectionData = FALSE;
BOOL fShowSymbolTable = FALSE;
BOOL fShowLineNumbers = FALSE;
char HelpText[] =
"PEDUMP - Win32/COFF .EXE/.OBJ file dumper - 1993 Matt Pietrek\n\n"
"Syntax: PEDUMP [switches] filename\n\n"
" /A include everything in dump\n"
" /H include hex dump of sections\n"
" /L include line number information\n"
" /R show base relocations\n"
" /S show symbol table\n";
// Open up a file, memory map it, and call the appropriate dumping routine
void DumpFile(LPSTR filename)
HANDLE hFile;
HANDLE hFileMapping;
LPVOID lpFileBase;
PIMAGE_DOS_HEADER dosHeader;
hFile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if ( hFile = = INVALID_HANDLE_VALUE )
{ printf("Couldn't open file with CreateFile()\n");
return; }
hFileMapping = CreateFileMapping(hFile, NULL,
PAGE_READONLY, 0, 0, NULL);
if ( hFileMapping = = 0 )
{
CloseHandle(hFile);
printf("Couldn't open file mapping with CreateFileMapping()\n");
return;
lpFileBase = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);
if ( lpFileBase = = 0 )
CloseHandle(hFileMapping);
CloseHandle(hFile);
printf("Couldn't map view of file with MapViewOfFile()\n");
return;
printf("Dump of file %s\n\n", filename);
dosHeader = (PIMAGE_DOS_HEADER)lpFileBase;
if ( dosHeader->e_magic = = IMAGE_DOS_SIGNATURE )
{ DumpExeFile( dosHeader ); }
else if ( (dosHeader->e_magic = = 0x014C) // Does it look like a i386
&& (dosHeader->e_sp = = 0) ) // COFF OBJ file???
// The two tests above aren't what they look like. They're
// really checking for IMAGE_FILE_HEADER.Machine = = i386 (0x14C)
// and IMAGE_FILE_HEADER.SizeOfOptionalHeader = = 0;
DumpObjFile( (PIMAGE_FILE_HEADER)lpFileBase );
else
printf("unrecognized file format\n");
UnmapViewOfFile(lpFileBase);
CloseHandle(hFileMapping);
CloseHandle(hFile);
// process all the command line arguments and return a pointer to
// the filename argument.
PSTR ProcessCommandLine(int argc, char *argv[])
int i;
for ( i=1; i < argc; i++ )
strupr(argv);
// Is it a switch character?
if ( (argv[0] = = '-') || (argv[0] = = '/') )
if ( argv[1] = = 'A' )
{ fShowRelocations = TRUE;
fShowRawSectionData = TRUE;
fShowSymbolTable = TRUE;
fShowLineNumbers = TRUE; }
else if ( argv[1] = = 'H' )
fShowRawSectionData = TRUE;
else if ( argv[1] = = 'L' )
fShowLineNumbers = TRUE;
else if ( argv[1] = = 'R' )
fShowRelocations = TRUE;
else if ( argv[1] = = 'S' )
fShowSymbolTable = TRUE;
else // Not a switch character. Must be the filename
{ return argv; }
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 基本概念
讓我們復(fù)習(xí)一下幾個透過PE文件的設(shè)計了解到的基本概念。我用術(shù)語"MODULE"來表示一個可執(zhí)行文件或一個DLL載入內(nèi)存的代碼(CODE)、數(shù)據(jù)(DATA)、資源(RESOURCES),除了代碼和數(shù)據(jù)是你的程序直接使用的,一個模塊還可以由WINDOWS用來確定數(shù)據(jù)和代碼載入的位置的支撐數(shù)據(jù)結(jié)構(gòu)組成。在16位WINDOWS中,這些支撐數(shù)據(jù)結(jié)構(gòu)在模塊數(shù)據(jù)庫(用一個HMODULE來指示的段)中。在WIN32里面,這些數(shù)據(jù)結(jié)構(gòu)在PE文件頭中,這些我將會簡要地解釋一下。
PE文件略圖
關(guān)于PE文件最重要的是,磁盤上的可執(zhí)行文件和它被WINDOWS調(diào)入內(nèi)存之后是非常相像的。WINDOWS載入器不必為從磁盤上載入一個文件而辛辛苦苦創(chuàng)建一個進(jìn)程。載入器使用內(nèi)存映射文件機(jī)制來把文件中相似的塊映射到虛擬空間中。用一個構(gòu)造式的分析模型,一個PE文件類似一個預(yù)制的屋子。它本質(zhì)上開始于這樣一個空間,這個空間后面有幾個把它連到其余空間的機(jī)件(就是說,把它聯(lián)系到它的DLL上,等等)。這對PE格式的DLL是一樣容易應(yīng)用的。一旦這個模塊被載入,Windows 就可以有效的把它和其它內(nèi)存映射文件同等對待。
和16位Windows不同的是。16位NE文件的載入器讀取文件的一部分并且創(chuàng)建完全不同的數(shù)據(jù)結(jié)構(gòu)在內(nèi)存中表示模塊。當(dāng)數(shù)據(jù)段或者代碼段需要載入時,載入器必須從全局堆中新申請一個段,從可執(zhí)行文件中找出生鮮數(shù)據(jù),轉(zhuǎn)到這個位置,讀入這些生鮮數(shù)據(jù),并且要進(jìn)行適當(dāng)?shù)男拚。除此而外,每個16位模塊都有責(zé)任記住當(dāng)前它使用的所有段選擇器,而不管這個段是否被丟棄了,如此等等。
對Win32來講,模塊所使用的所有代碼,數(shù)據(jù),資源,導(dǎo)入表,和其它需要的模塊數(shù)據(jù)結(jié)構(gòu)都在一個連續(xù)的內(nèi)存塊中。在這種形勢下,你只需要知道載入器把可執(zhí)行文件映射到了什么地方。通過作為映像的一部分的指針,你可以很容易的找到這個模塊所有不同的塊。
另一個你需要知道的概念是相對虛擬地址(RVA)。PE文件中的許多域都用術(shù)語RVA來指定。一個RVA只是一些項目相對于文件映射到內(nèi)存的偏移。比如說,載入器把一個文件映射到虛擬地址0x10000開始的內(nèi)存塊。如果一個映像中的實際的表的首址是0x10464,那么它的RVA就是0x464。
。ㄌ摂M地址 0x10464)-(基地址 0x10000)=RVA 0x00464
為了把一個RVA轉(zhuǎn)化成一個有用的指針,只需要把RVA值加到模塊的基地址上即可;刂肥莾(nèi)存映射EXE和DLL文件的首址,在Win32中這是一個很重要的概念。為了方便起見,WindowsNT 和 Windows9x用模塊的基地址作為這個模塊的實例句柄(HINSTANCE)。在Win32中,把模塊的基地址叫做HINSTANCE可能導(dǎo)致混淆,因為術(shù)語"實例句柄"來自16位Windows。一個程序在16位Windows中的每個拷貝得到它自己分開的數(shù)據(jù)段(和一個聯(lián)系起來的全局句柄)來把它和這個程序其它的拷貝分別開來,就形成了術(shù)語"實例句柄"。在Win32中,每個程序不必和其它程序區(qū)別開來,因為他們不共享相同的地址空間。術(shù)語INSTANCE仍然保持16位windows和32位Windows之間的連續(xù)性。在Win32中重要的是你可以對任何DLL調(diào)用GetModuleHandle()得到一個指針去訪問它的組件(譯注)。
譯注:如果 dllname 為 NULL,則得到執(zhí)行體自己的模塊句柄。這是非常有用的,如通常編譯器產(chǎn)生的啟動代碼將取得這個句柄并將它作為一個參數(shù)hInstance傳給WinMain !
你最終需要理解的PE文件的概念是"塊(Section)"。PE文件中的一個塊和NE文件中的一個段或者資源等價。塊可以包含代碼或者數(shù)據(jù)。和段不同的是,塊是內(nèi)存中連續(xù)的空間,而沒有尺寸限制。當(dāng)你的連接器和庫為你建立,并且包含對操作系統(tǒng)非常重要的信息的其它的數(shù)據(jù)塊時,這些塊包含你的程序直接聲明和使用的代碼或數(shù)據(jù)。在一些PE格式的描述中,塊也叫做對象。術(shù)語對象有如此多的涵義,以至于只能把代碼和數(shù)據(jù)叫做"塊"。
2 PE首部
和其它可執(zhí)行文件格式一樣,PE文件在眾所周知的地方有一些定義文件其余部分面貌的域。首部就包含這樣象代碼和數(shù)據(jù)的位置和尺寸的地方,操作系統(tǒng)要對它進(jìn)行干預(yù),比如初始堆棧大小,和其它重要的塊的信息,我將要簡短的介紹一下。和微軟其它可執(zhí)行格式相比,主要的首部不是在文件的最開始。典型的PE文件最開始的數(shù)百個字節(jié)被DOS殘留部分占用。這個殘留部分是一個可以打印如"這個程序不能在DOS下運行!"這類信息的小程序。所以,你在一個不支持Win32的系統(tǒng)中運行這個程序,便可以得到這類錯誤信息。當(dāng)載入器把一個Win32程序映射到內(nèi)存,這個映射文件的第一個字節(jié)對應(yīng)于DOS殘留部分的第一個字節(jié)。那是無疑的。和你啟動的任一個基于Win32 的程序一起,都有一個基于DOS的程序連帶被載入。
和微軟的其它可執(zhí)行格式一樣,你可以通過查找它的起始偏移來得到真實首部,這個偏移放在DOS殘留首部中。WINNT.H頭文件包含了DOS殘留程序的數(shù)據(jù)結(jié)構(gòu)定義,使得很容易找到PE首部的起始位置。e_lfanew 域是PE真實首部的偏移。為了得到PE首部在內(nèi)存中的指針,只需要把這個值加到映像的基址上即可。
file://忽/略類型轉(zhuǎn)化和指針轉(zhuǎn)化 ...
pNTHeader = dosHeader + dosHeader->e_lfanew;
一旦你有了PE主首部的指針,游戲就可以開始了!PE主首部是一個IMAGE_NT_HEADERS的結(jié)構(gòu),在WINNT.H中定義。這個結(jié)構(gòu)由一個雙字(DWORD)和兩個子結(jié)構(gòu)組成,布局如下:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
標(biāo)志域用ASCII表示就是"PE"。如果在DOS首部中用了e_lfanew域,你得到一個NE標(biāo)志而不是PE,那么這是16位NE文件。同樣的,在標(biāo)志域中的LE表示這是一個Windows3.x 的虛擬設(shè)備驅(qū)動程序(VxD)。LX表示這個文件是OS/2 2.0文件。
PE DWORD標(biāo)志后的是結(jié)構(gòu) IMAGE_FILE_HEADER 。這個域只包含這個文件最基本的信息。這個結(jié)構(gòu)表現(xiàn)為并未從它的原始COFF實現(xiàn)更改過。除了是PE首部的一部分,它還表現(xiàn)在微軟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
這個文件中的塊數(shù)目。
DWORD TimeDateStamp
連接器產(chǎn)生這個文件的日期(對OBJ文件是編譯器),這個域保存的數(shù)是從1969年12月下午4:00開始到現(xiàn)在經(jīng)過的秒數(shù)。
DWORD PointerToSymbolTable
COFF符號表的文件偏移量。這個域只用于有COFF調(diào)試信息的OBJ文件和PE文件,PE文件支持多種調(diào)試信息格式,所以調(diào)試器應(yīng)該指向數(shù)據(jù)目錄的IMAGE_DIRECTORY_ENTRY_DEBUG條目。
DWORD NumberOfSymbols
COFF符號表的符號數(shù)目。見上面。
WORD SizeOfOptionalHeader
這個結(jié)構(gòu)后面的可選首部的尺寸。在OBJ文件中,這個域是0。在可執(zhí)行文件中,這是跟在這個結(jié)構(gòu)后的IMAGE_OPTIONAL_HEADER結(jié)構(gòu)的尺寸。
WORD Characteristics
關(guān)于這個文件信息的標(biāo)志。一些重要的域如下:
0x0001 這個文件中沒有重定位信息
0x0002 可執(zhí)行文件映像(不是OBJ或LIB文件)
0x2000 文件是動態(tài)連接庫,而非程序
其它域定義在WINNT.H中。
PE首部的第三個組成部分是一個IMAGE_OPTIONAL_HEADER型的結(jié)構(gòu)。對PE文件,這一部分當(dāng)然不是"可選的"。COFF格式允許單獨實現(xiàn)來定義一個超出標(biāo)準(zhǔn)IMAGE_FILE_HEADER附加信息的結(jié)構(gòu)。IMAGE_OPTIONAL_HEADER里面的域是PE的實現(xiàn)者感到超出IMAGE_FILE_HEADER基本信息以外非常關(guān)鍵的信息。
并非 IMAGE_OPTIONAL_HEADER 的所有域都是重要的(見圖4)。比較重要,需要知道的是ImageBase 和 SubSystem 域。你可以忽略其它域的描述。
表3 IMAGE_FILE_HEADER 的域:
WORD Magic
表現(xiàn)為一些類別的標(biāo)志字,通常是0X010B 。
BYTE MajorLinkerVersion
BYTE MinorLinkerVersion
生成這個文件的連接器的版本。這個數(shù)字以十進(jìn)制顯示比用十六進(jìn)制好。一個典型的連接器版本是2.23。
DWORD SizeOfCode
所有代碼塊的進(jìn)位尺寸。通常大多數(shù)文件只有一個代碼塊,所以這個域和 .TEXT 塊匹配。
DWORD SizeOfInitializedData
已初始化的數(shù)據(jù)組成的塊的大。ú话ùa段)。然而,和它在文件中的表現(xiàn)形式并不一致。
DWORD SizeOfUninitializedData
載入器在虛擬內(nèi)存中申請空間,但在磁盤上的文件中并不占用空間的塊的尺寸。這些塊在程序啟動時不需要指定初值,因此術(shù)語名就是"未初始化的數(shù)據(jù)"。未初始化的數(shù)據(jù)通常在一個名叫 .bss 的塊中。
DWORD AddressOfEntryPoint
載入器開始執(zhí)行這個程序的地址,即這個PE文件的入口地址。這是一個RVA,通常在 .text 塊中。
DWORD BaseOfCode
代碼塊起始地址的RVA 。在內(nèi)存中,代碼塊通常在PE首部之后,數(shù)據(jù)塊之前。在微軟的連接器產(chǎn)生的EXE文件中,這個值通常是0x1000 。Borland 的連接器 TLINK32 也一樣,把映像第一個代碼塊的RVA和映像基址相加,填入這個域。
譯注:這個域好像一直沒有什么用
DWORD BaseOfData
數(shù)據(jù)塊起始地址的RVA 。在內(nèi)存中,數(shù)據(jù)塊經(jīng)常在最后,在PE首部和代碼塊之后。
譯注:這個域好像也一直沒有什么用
DWORD ImageBase
連接器創(chuàng)建一個可執(zhí)行文件時,它假定這個文件被映射到內(nèi)存中的一個指定的地方,這個地址就存在這個域中,假定一個載入地址可以使連接器優(yōu)化以便節(jié)省空間。如果載入器真的把這個文件映射到了這個地方,在運行之前代碼不需要任何改變。在為WindowsNT 創(chuàng)建的可執(zhí)行文件中,默認(rèn)的ImageBase 是0x10000。對DLL,默認(rèn)是0x40000。在Window95中,地址0x10000不能用來載入32位EXE文件,因為這個區(qū)域在一個被所有進(jìn)程共享的線性地址空間中。因此,微軟把Win32可執(zhí)行文件的默認(rèn)基址改為0x40000,假定基址為0x10000 的老程序坐在Windows95 中需要更長的載入時間,這是因為載入器需要重定位基址。
譯注:這個域即"Prefered Load Address",如果沒有什么意外,這就是該PE文件載入內(nèi)存后的地址。
DWORD SectionAlignment
映射到內(nèi)存中時,每個塊都必須保證開始于這個值的整數(shù)倍。為了分頁的目的,默認(rèn)的SectionAlignment 是 0x1000。
DWORD FileAlignment
在PE文件中,組成每個塊的生鮮數(shù)據(jù)必須保證開始于這個值的整數(shù)倍。默認(rèn)值是0x200 字節(jié),也許是為了保證塊都開始于一個磁盤扇區(qū)(一個扇區(qū)通常是 512 字節(jié))。這個域和NE文件中的段/資源對齊(segment/resource alignment)尺寸是等價的。和NE文件不同的是,PE文件通常沒有數(shù)百個的塊,所以,為了對齊而浪費的通?臻g很少。
WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
這個程序運行需要的操作系統(tǒng)的最小版本號。這個域有點含糊,因為Subsystem 域(后面將會說到)可以提供類似的功能。這個域在到目前為止的Win32中默認(rèn)是1.0。
WORD MajorImageVersion
WORD MinorImageVersion
一個可由用戶定義的域。這允許你有不同的EXE和DLL版本。你可以通過鏈接器的 /version 選項設(shè)置這個域的值。例如:"link /version:2.0 myobj.obj"。
WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
這個程序運行需要的最小子系統(tǒng)版本號。這個域的一個典型值是3.10 (表示W(wǎng)indowsNT 3.1)。
DWORD Reserved1
通常是 0 。
DWORD SizeOfImage
載入器必須關(guān)心的這個映像所有部分的大小總和。是從映像的開始到最后一個塊結(jié)尾這段區(qū)域的大小。最后一個塊結(jié)尾按SectionAlignment進(jìn)位。
譯注:這個很重要,可以大,但不可以。
DWORD SizeOfHeaders
PE首部和塊表的大小。塊的實際數(shù)據(jù)緊跟在所有首部組件之后。
DWORD CheckSum
這個文件的CRC校驗和。在微軟可執(zhí)行格式中,這個域被忽略并且置為0 。這個規(guī)則的一個例外情況是信任服務(wù),這類EXE文件必須有一個合法的校驗和。
WORD Subsystem
可執(zhí)行文件的用戶界面使用的子系統(tǒng)類型。WINNT.H 定義了下面這些值:
NATIVE 1 不需要子系統(tǒng)(比如設(shè)備驅(qū)動)
WINDOWS_GUI 2 在Windows圖形用戶界面子系統(tǒng)下運行
WINDOWS_CUI 3 在Windows字符子系統(tǒng)下運行(控制臺程序)
OS2_CUI 5 在OS/2字符子系統(tǒng)下運行(僅對OS/2 1.x)
POSIX_CUI 7 在 Posix 字符子系統(tǒng)下運行
WORD DllCharacteristics
指定在何種環(huán)境下一個DLL的初始化函數(shù)(比如DllMain)將被調(diào)用的標(biāo)志變量。這個值經(jīng)常被置為0 。但是操作系統(tǒng)在下面四種情況下仍然調(diào)用DLL的初始化函數(shù)。
下面的值定義為:
1 DLL第一次載入到進(jìn)程中的地址空間中時調(diào)用
2 一個線程結(jié)束時調(diào)用
4 一個線程開始時調(diào)用
8 退出DLL時調(diào)用
DWORD SizeOfStackReserve
為初始線程保留的虛擬內(nèi)存總數(shù)。然而并不是所有這些內(nèi)存都被提交(見下一個域)。這個域的默認(rèn)值是0x100000(1Mbytes)。如果你在CreateThread 中把堆棧尺寸指定為 0 ,結(jié)果將是用這個相同的值(0x10000)。
DWORD SizeOfStackCommit
開始提交的初始線程堆棧總數(shù)。對微軟的連接器,這個域默認(rèn)是0x1000字節(jié)(一頁),TLINK32 是兩頁。
DWORD SizeOfHeapReserve
為初始進(jìn)程的堆保留的虛擬內(nèi)存總數(shù)。這個堆的句柄可以用GetPocessHeap 得到。并不是所有這些內(nèi)存都被提交(見下一個域)。
DWORD SizeOfHeapCommit
開始為進(jìn)程堆提交的內(nèi)存總數(shù)。默認(rèn)是一頁。
PE文件格式
1.PE文件的含義
PE文件的意思是Portable Executable(可移植,可執(zhí)行),它是win32可執(zhí)行文件的標(biāo)準(zhǔn)格式.它的一些特性繼承unix的COFF文件格式,同時保留了與舊版MS-DOS和WINDOWS的兼容.其可移植可執(zhí)行意味著是跨win32平臺的.
2.PE文件的層次結(jié)構(gòu)
PE文件最前面緊隨DOS MZ文件頭的是一個DOS可執(zhí)行文件(Stub).這使得PE文件成為一個合法的MS-DOS可執(zhí)行文件.DOS MZ文件頭后面是一個32位的PE文件標(biāo)志0x50450000(IMAGE_NT_SIGNATURE),即PE00.接下來的是PE的映像文件頭,包含的信息有該程序的運行平臺,有多少個節(jié),文件鏈接的時間,文件的命名格式.后面還緊跟一個可選映像頭,包含PE文件的邏輯分布信息,程序加載信息,開始地址,保留的堆棧數(shù)量,數(shù)據(jù)段大小等.可選頭還有一個重要的域,稱為:數(shù)據(jù)目錄表"的數(shù)組,表的每一項都是指向某一節(jié)的指針.可選映像頭后面緊跟的是節(jié)表和節(jié).節(jié)通過節(jié)表來實現(xiàn)索引.實際上,節(jié)的內(nèi)容才是真正執(zhí)行的數(shù)據(jù)和程序.每一個節(jié)都有相關(guān)的標(biāo)志.每一個節(jié)會被一個或多個目錄表指向,目錄表可通過可選頭的"數(shù)據(jù)目錄表"的入口找到.就像輸出函數(shù)表或基址重定位表.也存在沒有目錄表指向的節(jié).
3.PE文件層次解釋
A.DOS STUB和DOS頭
DOS插樁程序在大多數(shù)情況下由匯編器/編譯器自動產(chǎn)生.通常它調(diào)用INT 21H服務(wù)9來顯示上述字符串.可以通過IMAGE_DOS_HEADER結(jié)構(gòu)來識別一個合法的DOS頭.這個結(jié)構(gòu)的頭兩個字節(jié)肯定是"MZ".可通過該結(jié)構(gòu)的e_lfanew成員來找到PE文件的開始標(biāo)志.MS-DOS頭部占據(jù)了PE文件的頭64個字節(jié).在微軟的WINNT.H中可以找到其內(nèi)容結(jié)構(gòu)的描述.
B.PE文件頭
在DOS STUB后是PE文件頭(PE header).PE文件頭是PE相關(guān)結(jié)構(gòu)IMGAE_NT_HEADERS的簡稱,即NT映像頭,存放PE整個文件信息發(fā)布的重要字段,包含了PE裝載器用到的重要域.執(zhí)行體在操作系統(tǒng)中執(zhí)行時,PE裝載器將從DOS MZ頭中找到PE頭文件的起始偏移量e_lfanew,從而跳過DOS STUB直接定位真正的PE文件.它由3部分組成:
(1)PE文件標(biāo)志(4H字節(jié))
PE文件標(biāo)志0x50450000即PE00,標(biāo)志著NT映像頭的開始,也是PE文件中與windows有關(guān)內(nèi)容的開始.
(2)映像文件(14H字節(jié))
是NT映像文件的主要部分,包含PE文件的基本信息
(3)可選映像頭
包含PE文件的邏輯分布信息.
C.節(jié)表
節(jié)表其實是緊跟NT映像文件的一個結(jié)構(gòu)數(shù)組.其成員數(shù)目由映像文件頭結(jié)構(gòu)NumberOFSectios域的值來決定.
D.節(jié)
PE文件的真正內(nèi)容劃分為塊,稱之為節(jié).節(jié)的劃分基于各組數(shù)據(jù)的共同屬性.惟有節(jié)的屬性設(shè)置決定了節(jié)的特性和功能.典型的windows NT應(yīng)用程序可以具有9個節(jié):.texr,.bss.rdata,.data,.rsrc,edata,idata,pdata,.debug
判斷一個文件是否為PE文件
function TForm1.is_PE(FileName: string): boolean;
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結(jié)構(gòu)
FileSeek(m_file, PEDosHead._lfanew, soFromBeginning); //將指針挪至_lfanew
FileRead(m_file, PENTHead, SizeOf(PENTHead)); //讀PENTHead結(jié)構(gòu)
finally
FileClose(m_file);
end;
if (PENTHead.Signature = IMAGE_NT_SIGNATURE) then //檢驗文件頭部第一個字的值是否等于 IMAGE_DOS_SIGNATURE
Result := True;
end;
pe文件結(jié)構(gòu)圖