什么是PE文件及PE文件的結(jié)構(gòu)和簡(jiǎn)述

?

A Tour of the Win32 Portable Executable File Format

Peering Inside the PE: A Tour of the Win32 Portable Executable File Format

一個(gè)操作系統(tǒng)的可執(zhí)行文件格式在很多方面是這個(gè)系統(tǒng)的一面鏡子。雖然學(xué)習(xí)一個(gè)可執(zhí)行文件格式通常不是一個(gè)程 序員的首要任務(wù),但是你可以從這其中學(xué)到大量的知識(shí)。在這篇文章中,我會(huì)給出 MicroSoft 的所有基于 win32系統(tǒng)(如winnt,win9x)的可移植可執(zhí)行(PE)文件格式的詳細(xì)介紹。在可預(yù)知的未來(lái),包括 Windows2000 , PE 文件格式在 MicroSoft 的操作系統(tǒng)中扮演一個(gè)重要的角色。如果你在使用 Win32 或 Winnt ,那么你已經(jīng)在使用 PE 文件了。甚至你只是在 Windows3.1 下使用 Visual C++ 編程,你使用的仍然是 PE 文件(Visual C++ 的 32 位 MS-DOS 擴(kuò)展組件用這個(gè)格式)。簡(jiǎn)而言之,PE 格式已經(jīng)普遍應(yīng)用,并且在不短的將來(lái)仍是不可避免的。現(xiàn)在是時(shí)候找出這種新的可執(zhí)行文件格式為操作系統(tǒng)帶來(lái)的東西了。
我最后不會(huì)讓你盯住無(wú)窮無(wú)盡的十六進(jìn)制Dump,也不會(huì)詳細(xì)討論頁(yè)面的每一個(gè)單獨(dú)的位的重要性。代替的,我會(huì)向你介紹包含在 PE 文件中的概念,并且將他們和你每天都遇到的東西聯(lián)系起來(lái)。比如,線程局部變量的概念,如下所述:
declspec(thread) int i;
我快要發(fā)瘋了,直到我發(fā)現(xiàn)它在可執(zhí)行文件中實(shí)現(xiàn)起來(lái)是如此的簡(jiǎn)單并且優(yōu)雅。既然你們中的許多人都有使用 16 Windows 的背景,我將把 Win32 PE 文件的構(gòu)造追溯到和它等價(jià)的16 位 NE 文件。
除 了一個(gè)不同的可執(zhí)行文件格式, MicroSoft 還引入了一個(gè)用它的編譯器和匯編器生成的新的目標(biāo)模塊格式。這個(gè)新的 OBJ 文件格式有許多和PE 文件共同的東東。我做了許多無(wú)用功去查找這個(gè)新的 OBJ 文件格式的文檔。所以我以自己的理解對(duì)它進(jìn)行解析,并且,在這里,除了 PE 文件,我會(huì)描述它的一部分。
大家都知道,Windows NT 繼承了 VAX? VMS? 和 UNIX? 的傳統(tǒng)。許多 Windows NT 的創(chuàng)始人在進(jìn)入微軟前都在這些平臺(tái)上進(jìn)行設(shè)計(jì)和編碼。當(dāng)他們開(kāi)始設(shè)計(jì) Windows NT 時(shí),很自然的,為了最小化項(xiàng)目啟動(dòng)時(shí)間,他們會(huì)使用以前寫好的并且已經(jīng)測(cè)試過(guò)的工具。用這些工具生成的并且工作的可執(zhí)行和 OBJ 文件格式叫做 COFF (Common Object File Format 的首字母縮寫)。COFF 的相對(duì)年齡可以用八進(jìn)制的域來(lái)指定。COFF 本身是一個(gè)好的起點(diǎn),但是需要擴(kuò)展到一個(gè)現(xiàn)代操作系統(tǒng)如 Windows 95 和 Windows NT 的需要。這個(gè)更新的結(jié)果就是(PE格式)可移植可執(zhí)行文件格式。它被稱為"可移植的"是因?yàn)樵谒衅脚_(tái)(如x86,Alpha,MIPS等等)上實(shí)現(xiàn)的 WindowsNT 都使用相同的可執(zhí)行文件格式。當(dāng)然了,也有許多不同的東西如二進(jìn)制代碼的CPU指令。重要的是操作系統(tǒng)的裝入器和程序設(shè)計(jì)工具不需要為任何一種CPU完全 重寫就能達(dá)到目的。
MicroSoft 拋棄現(xiàn)存的32位工具和可執(zhí)行文件格式的事實(shí)證實(shí)了他們想讓 WindowsNT 升級(jí)并且運(yùn)行的更快的決心。為16位Windows編寫的虛擬設(shè)備驅(qū)動(dòng)程序用一種不同的32位文件布局--LE 文件格式--WindowsNT出現(xiàn)很早以前就存在了。比這更重要的是對(duì) OBJ 文件的替換!在 WindowsNT 的 C 編譯器以前,所有的微軟編譯器都用 Intel 的 OMF ( Object Module Format ) 規(guī)范。就像前面提到的,MicroSoft 的 Win32 編譯器生成 COFF 格式的 OBJ 文件。一些微軟的競(jìng)爭(zhēng)者,如 Borland 和 Symentec ,選擇放棄了 COFF 格式并堅(jiān)持 Intel 的 OMF 文件格式。這樣的結(jié)果是制作 OBJ 和 LIB 的公司為了使用多個(gè)不同的編譯器,不得不為每個(gè)不同的編譯器分發(fā)這些庫(kù)的不同版本(如果他們不這么做)。
PE 文件格式在 winnt.h 頭文件中文檔化了(用最不精確的語(yǔ)言)!大約在 winnt.h 的中間部分標(biāo)題為"Image Format"的一個(gè)快。在把 MS-DOS 的 MZ 文件頭和 NE 文件頭移入新的PE文件頭之前,這個(gè)塊就開(kāi)始于一個(gè)小欄。WINNT.H提供PE文件用到的生鮮數(shù)據(jù)結(jié)構(gòu)的定義,但只有很少有助于理解這些數(shù)據(jù)結(jié)構(gòu)和標(biāo)志 變量的注釋。不管誰(shuí)為PE文件格式寫出這樣的頭文件都肯定是一個(gè)信徒無(wú)疑(突然持續(xù)地冒出Michael J. O'Leary的名字來(lái))。描述名字,連同深嵌的結(jié)構(gòu)體和宏。當(dāng)你配套winnt.h進(jìn)行編碼時(shí),類似下面這樣的表達(dá)式并不鮮見(jiàn):
pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG]
.VirtualAddress;
為了有助于邏輯的理解這些winnt.h中的信息,閱讀可移植可執(zhí)行和公共對(duì)象文件格式的規(guī)格說(shuō)明,這些在MSDN既看光盤中是可用的,一直包括到2001年8月。
現(xiàn) 在讓我們轉(zhuǎn)換到COFF格式的OBJ文件的主體上來(lái),WINNT.H包括COFF OBJ和LIB的結(jié)構(gòu)化定義和類型定義。不幸的是,我還沒(méi)有找到上面提到的可執(zhí)行文件格式的類似文檔。既然PE文件和COFF OBJ文件是如此的相似,我決定是時(shí)間把這些文件帶到重點(diǎn)上來(lái),并且把它們也文檔化。僅僅讀過(guò)了關(guān)于PE文件的組成,你自己也想Dump一些PE文件來(lái)看 這些概念。如果你用微軟基于32位WINDOWS的開(kāi)發(fā)工具,DUMPBIN 程序可以將PE文件和COFF OBJ/LIB文件轉(zhuǎn)化為可讀的形式。在所有的PEDump器中,DUMPBIN是最容易理解的。它恰好有一些很好的選項(xiàng)來(lái)反匯編它正解析的文件的代碼 塊,Borland用戶可以使用tdump來(lái)瀏覽PE文件,但tdump不能解析 COFF OBJ/LIB 文件。這不是一個(gè)重要的東西因?yàn)锽orland的編譯器首先就不生成 COFF 格式的OBJ文件。
我寫了一個(gè)PE和COFF OBJ 文件的Dump程序--PEDUMP(見(jiàn)表1),我想提供一些比DUMPBIN更加可理解的輸出。雖然它沒(méi)有反匯編器以及和LIB庫(kù)文件一起工作,它在其 他方面和DUMPBIN是一樣的,并且加入了一些新的特性來(lái)使它值得被認(rèn)同。它的源代碼在任何一個(gè)MSJ電子公報(bào)版上都可以找到,所有我不打算在這里把他 全部列出。作為代替,我展示一些從PEDUMP得到的示例輸出來(lái)闡明我為它們描述的概念。
譯注:--說(shuō)實(shí)話,我從這這份代碼中幾乎唯一學(xué)到的東西就是"如何處理命令行",其它的都沒(méi)學(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[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 基本概念
讓我們復(fù)習(xí)一下幾個(gè)透過(guò)PE文件的設(shè)計(jì)了解到的基本概 念(見(jiàn)圖1)。我用術(shù)語(yǔ)"MODULE"來(lái)表示一個(gè)可執(zhí)行文件或一個(gè)DLL載入內(nèi)存的代碼(CODE)、數(shù)據(jù)(DATA)、資源(RESOURCES), 除了代碼和數(shù)據(jù)是你的程序直接使用的,一個(gè)模塊還可以由WINDOWS用來(lái)確定數(shù)據(jù)和代碼載入的位置的支撐數(shù)據(jù)結(jié)構(gòu)組成。在16位WINDOWS中,這些 支撐數(shù)據(jù)結(jié)構(gòu)在模塊數(shù)據(jù)庫(kù)(用一個(gè)HMODULE來(lái)指示的段)中。在WIN32里面,這些數(shù)據(jù)結(jié)構(gòu)在PE文件頭中,這些我將會(huì)簡(jiǎn)要地解釋一下。

圖1 PE文件略圖

關(guān)于PE文件最重要的是,磁盤上的可執(zhí)行文件和它被WINDOWS調(diào)入內(nèi)存之后是非常相像的。 WINDOWS載入器不必為從磁盤上載入一個(gè)文件而辛辛苦苦創(chuàng)建一個(gè)進(jìn)程。載入器使用內(nèi)存映射文件機(jī)制來(lái)把文件中相似的塊映射到虛擬空間中。用一個(gè)構(gòu)造式 的分析模型,一個(gè)PE文件類似一個(gè)預(yù)制的屋子。它本質(zhì)上開(kāi)始于這樣一個(gè)空間,這個(gè)空間后面有幾個(gè)把它連到其余空間的機(jī)件(就是說(shuō),把它聯(lián)系到它的DLL 上,等等)。這對(duì)PE格式的DLL是一樣容易應(yīng)用的。一旦這個(gè)模塊被載入,Windows 就可以有效的把它和其它內(nèi)存映射文件同等對(duì)待。
和16 位Windows不同的是。16位NE文件的載入器讀取文件的一部分并且創(chuàng)建完全不同的數(shù)據(jù)結(jié)構(gòu)在內(nèi)存中表示模塊。當(dāng)數(shù)據(jù)段或者代碼段需要載入時(shí),載入器 必須從全局堆中新申請(qǐng)一個(gè)段,從可執(zhí)行文件中找出生鮮數(shù)據(jù),轉(zhuǎn)到這個(gè)位置,讀入這些生鮮數(shù)據(jù),并且要進(jìn)行適當(dāng)?shù)男拚3硕猓總€(gè)16位模塊都有責(zé)任記 住當(dāng)前它使用的所有段選擇器,而不管這個(gè)段是否被丟棄了,如此等等。
對(duì)Win32來(lái)講,模塊所使用的所有代碼,數(shù)據(jù),資源,導(dǎo)入表,和其它需要的模塊數(shù)據(jù)結(jié)構(gòu)都在一個(gè)連續(xù)的內(nèi)存塊中。在這種形勢(shì)下,你只需要知道載入器把可執(zhí)行文件映射到了什么地方。通過(guò)作為映像的一部分的指針,你可以很容易的找到這個(gè)模塊所有不同的塊。
另 一個(gè)你需要知道的概念是相對(duì)虛擬地址(RVA)。PE文件中的許多域都用術(shù)語(yǔ)RVA來(lái)指定。一個(gè)RVA只是一些項(xiàng)目相對(duì)于文件映射到內(nèi)存的偏移。比如說(shuō), 載入器把一個(gè)文件映射到虛擬地址0x10000開(kāi)始的內(nèi)存塊。如果一個(gè)映像中的實(shí)際的表的首址是0x10464,那么它的RVA就是0x464。
(虛擬地址 0x10464)-(基地址 0x10000)=RVA 0x00464
為 了把一個(gè)RVA轉(zhuǎn)化成一個(gè)有用的指針,只需要把RVA值加到模塊的基地址上即可。基地址是內(nèi)存映射EXE和DLL文件的首址,在Win32中這是一個(gè)很重 要的概念。為了方便起見(jiàn),WindowsNT 和 Windows9x用模塊的基地址作為這個(gè)模塊的實(shí)例句柄(HINSTANCE)。在Win32中,把模塊的基地址叫做HINSTANCE可能導(dǎo)致混淆, 因?yàn)樾g(shù)語(yǔ)"實(shí)例句柄"來(lái)自16位Windows。一個(gè)程序在16位Windows中的每個(gè)拷貝得到它自己分開(kāi)的數(shù)據(jù)段(和一個(gè)聯(lián)系起來(lái)的全局句柄)來(lái)把它 和這個(gè)程序其它的拷貝分別開(kāi)來(lái),就形成了術(shù)語(yǔ)"實(shí)例句柄"。在Win32中,每個(gè)程序不必和其它程序區(qū)別開(kāi)來(lái),因?yàn)樗麄儾还蚕硐嗤牡刂房臻g。術(shù)語(yǔ) INSTANCE仍然保持16位windows和32位Windows之間的連續(xù)性。在Win32中重要的是你可以對(duì)任何DLL調(diào)用 GetModuleHandle()得到一個(gè)指針去訪問(wèn)它的組件(譯注)。
譯注:如果 dllname 為 NULL,則得到執(zhí)行體自己的模塊句柄。這是非常有用的,如通常編譯器產(chǎn)生的啟動(dòng)代碼將取得這個(gè)句柄并將它作為一個(gè)參數(shù)hInstance傳給WinMain !
你 最終需要理解的PE文件的概念是"塊(Section)"。PE文件中的一個(gè)塊和NE文件中的一個(gè)段或者資源等價(jià)。塊可以包含代碼或者數(shù)據(jù)。和段不同的 是,塊是內(nèi)存中連續(xù)的空間,而沒(méi)有尺寸限制。當(dāng)你的連接器和庫(kù)為你建立,并且包含對(duì)操作系統(tǒng)非常重要的信息的其它的數(shù)據(jù)塊時(shí),這些塊包含你的程序直接聲明 和使用的代碼或數(shù)據(jù)。在一些PE格式的描述中,塊也叫做對(duì)象。術(shù)語(yǔ)對(duì)象有如此多的涵義,以至于只能把代碼和數(shù)據(jù)叫做"塊"。
2 PE首部
和 其它可執(zhí)行文件格式一樣,PE文件在眾所周知的地方有一些定義文件其余部分面貌的域。首部就包含這樣象代碼和數(shù)據(jù)的位置和尺寸的地方,操作系統(tǒng)要對(duì)它進(jìn)行 干預(yù),比如初始堆棧大小,和其它重要的塊的信息,我將要簡(jiǎn)短的介紹一下。和微軟其它可執(zhí)行格式相比,主要的首部不是在文件的最開(kāi)始。典型的PE文件最開(kāi)始 的數(shù)百個(gè)字節(jié)被DOS殘留部分占用。這個(gè)殘留部分是一個(gè)可以打印如"這個(gè)程序不能在DOS下運(yùn)行!"這類信息的小程序。所以,你在一個(gè)不支持Win32的 系統(tǒng)中運(yùn)行這個(gè)程序,便可以得到這類錯(cuò)誤信息。當(dāng)載入器把一個(gè)Win32程序映射到內(nèi)存,這個(gè)映射文件的第一個(gè)字節(jié)對(duì)應(yīng)于DOS殘留部分的第一個(gè)字節(jié)。那 是無(wú)疑的。和你啟動(dòng)的任一個(gè)基于Win32 的程序一起,都有一個(gè)基于DOS的程序連帶被載入。
和微軟的其它可執(zhí)行格式一樣,你可以通過(guò)查找它的 起始偏移來(lái)得到真實(shí)首部,這個(gè)偏移放在DOS殘留首部中。WINNT.H頭文件包含了DOS殘留程序的數(shù)據(jù)結(jié)構(gòu)定義,使得很容易找到PE首部的起始位置。 e_lfanew 域是PE真實(shí)首部的偏移。為了得到PE首部在內(nèi)存中的指針,只需要把這個(gè)值加到映像的基址上即可。
file://忽/略類型轉(zhuǎn)化和指針轉(zhuǎn)化 ...
pNTHeader = dosHeader + dosHeader->e_lfanew;
一旦你有了PE主首部的指針,游戲就可以開(kāi)始了!PE主首部是一個(gè)IMAGE_NT_HEADERS的結(jié)構(gòu),在WINNT.H中定義。這個(gè)結(jié)構(gòu)由一個(gè)雙字(DWORD)和兩個(gè)子結(jié)構(gòu)組成,布局如下:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
標(biāo) 志域用ASCII表示就是"PE\0\0"。如果在DOS首部中用了e_lfanew域,你得到一個(gè)NE標(biāo)志而不是PE,那么這是16位NE文件。同樣 的,在標(biāo)志域中的LE表示這是一個(gè)Windows3.x 的虛擬設(shè)備驅(qū)動(dòng)程序(VxD)。LX表示這個(gè)文件是OS/2 2.0文件。
PE DWORD標(biāo)志后的是結(jié)構(gòu) IMAGE_FILE_HEADER 。這個(gè)域只包含這個(gè)文件最基本的信息。這個(gè)結(jié)構(gòu)表現(xiàn)為并未從它的原始COFF實(shí)現(xiàn)更改過(guò)。除了是PE首部的一部分,它還表現(xiàn)在微軟Win32編譯器生成的 COFF OBJ 文件的最開(kāi)始部分。IMAGE_FILE_HEADER的這個(gè)域顯示在下面:
表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
這個(gè)文件中的塊數(shù)目。

DWORD TimeDateStamp
連接器產(chǎn)生這個(gè)文件的日期(對(duì)OBJ文件是編譯器),這個(gè)域保存的數(shù)是從1969年12月下午4:00開(kāi)始到現(xiàn)在經(jīng)過(guò)的秒數(shù)。

DWORD PointerToSymbolTable
COFF符號(hào)表的文件偏移量。這個(gè)域只用于有COFF調(diào)試信息的OBJ文件和PE文件,PE文件支持多種調(diào)試信息格式,所以調(diào)試器應(yīng)該指向數(shù)據(jù)目錄的IMAGE_DIRECTORY_ENTRY_DEBUG條目。

DWORD NumberOfSymbols
COFF符號(hào)表的符號(hào)數(shù)目。見(jiàn)上面。

WORD SizeOfOptionalHeader
這個(gè)結(jié)構(gòu)后面的可選首部的尺寸。在OBJ文件中,這個(gè)域是0。在可執(zhí)行文件中,這是跟在這個(gè)結(jié)構(gòu)后的IMAGE_OPTIONAL_HEADER結(jié)構(gòu)的尺寸。

WORD Characteristics
關(guān)于這個(gè)文件信息的標(biāo)志。一些重要的域如下:

0x0001 這個(gè)文件中沒(méi)有重定位信息
0x0002 可執(zhí)行文件映像(不是OBJ或LIB文件)
0x2000 文件是動(dòng)態(tài)連接庫(kù),而非程序

其它域定義在WINNT.H中。
PE首部的第三個(gè)組成部分是一個(gè) IMAGE_OPTIONAL_HEADER型的結(jié)構(gòu)。對(duì)PE文件,這一部分當(dāng)然不是"可選的"。COFF格式允許單獨(dú)實(shí)現(xiàn)來(lái)定義一個(gè)超出標(biāo)準(zhǔn) IMAGE_FILE_HEADER附加信息的結(jié)構(gòu)。IMAGE_OPTIONAL_HEADER里面的域是PE的實(shí)現(xiàn)者感到超出 IMAGE_FILE_HEADER基本信息以外非常關(guān)鍵的信息。
并非 IMAGE_OPTIONAL_HEADER 的所有域都是重要的(見(jiàn)圖4)。比較重要,需要知道的是ImageBase 和 SubSystem 域。你可以忽略其它域的描述。
表3 IMAGE_FILE_HEADER 的域:
WORD Magic
表現(xiàn)為一些類別的標(biāo)志字,通常是0X010B 。
BYTE MajorLinkerVersion
BYTE MinorLinkerVersion
生成這個(gè)文件的連接器的版本。這個(gè)數(shù)字以十進(jìn)制顯示比用十六進(jìn)制好。一個(gè)典型的連接器版本是2.23。

DWORD SizeOfCode
所有代碼塊的進(jìn)位尺寸。通常大多數(shù)文件只有一個(gè)代碼塊,所以這個(gè)域和 .TEXT 塊匹配。

DWORD SizeOfInitializedData
已初始化的數(shù)據(jù)組成的塊的大小(不包括代碼段)。然而,和它在文件中的表現(xiàn)形式并不一致。

DWORD SizeOfUninitializedData
載入器在虛擬內(nèi)存中申請(qǐng)空間,但在磁盤上的文件中并不占用空間的塊的尺寸。這些塊在程序啟動(dòng)時(shí)不需要指定初值,因此術(shù)語(yǔ)名就是"未初始化的數(shù)據(jù)"。未初始化的數(shù)據(jù)通常在一個(gè)名叫 .bss 的塊中。

DWORD AddressOfEntryPoint
載入器開(kāi)始執(zhí)行這個(gè)程序的地址,即這個(gè)PE文件的入口地址。這是一個(gè)RVA,通常在 .text 塊中。

DWORD BaseOfCode
代碼塊起始地址的RVA 。在內(nèi)存中,代碼塊通常在PE首部之后,數(shù)據(jù)塊之前。在微軟的連接器產(chǎn)生的EXE文件中,這個(gè)值通常是0x1000 。Borland 的連接器 TLINK32 也一樣,把映像第一個(gè)代碼塊的RVA和映像基址相加,填入這個(gè)域。
譯注:這個(gè)域好像一直沒(méi)有什么用

DWORD BaseOfData
數(shù)據(jù)塊起始地址的RVA 。在內(nèi)存中,數(shù)據(jù)塊經(jīng)常在最后,在PE首部和代碼塊之后。
譯注:這個(gè)域好像也一直沒(méi)有什么用

DWORD ImageBase
連接器創(chuàng)建一個(gè)可執(zhí)行文件時(shí),它假定這個(gè)文件被映射到內(nèi)存中的一 個(gè)指定的地方,這個(gè)地址就存在這個(gè)域中,假定一個(gè)載入地址可以使連接器優(yōu)化以便節(jié)省空間。如果載入器真的把這個(gè)文件映射到了這個(gè)地方,在運(yùn)行之前代碼不需 要任何改變。在為WindowsNT 創(chuàng)建的可執(zhí)行文件中,默認(rèn)的ImageBase 是0x10000。對(duì)DLL,默認(rèn)是0x40000。在Window95中,地址0x10000不能用來(lái)載入32位EXE文件,因?yàn)檫@個(gè)區(qū)域在一個(gè)被所有 進(jìn)程共享的線性地址空間中。因此,微軟把Win32可執(zhí)行文件的默認(rèn)基址改為0x40000,假定基址為0x10000 的老程序坐在Windows95 中需要更長(zhǎng)的載入時(shí)間,這是因?yàn)檩d入器需要重定位基址。
譯注:這個(gè)域即"Prefered Load Address",如果沒(méi)有什么意外,這就是該P(yáng)E文件載入內(nèi)存后的地址。

DWORD SectionAlignment
映射到內(nèi)存中時(shí),每個(gè)塊都必須保證開(kāi)始于這個(gè)值的整數(shù)倍。為了分頁(yè)的目的,默認(rèn)的SectionAlignment 是 0x1000。

DWORD FileAlignment
在PE文件中,組成每個(gè)塊的生鮮數(shù)據(jù)必須保證開(kāi)始于這個(gè) 值的整數(shù)倍。默認(rèn)值是0x200 字節(jié),也許是為了保證塊都開(kāi)始于一個(gè)磁盤扇區(qū)(一個(gè)扇區(qū)通常是 512 字節(jié))。這個(gè)域和NE文件中的段/資源對(duì)齊(segment/resource alignment)尺寸是等價(jià)的。和NE文件不同的是,PE文件通常沒(méi)有數(shù)百個(gè)的塊,所以,為了對(duì)齊而浪費(fèi)的通常空間很少。

WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
這個(gè)程序運(yùn)行需要的操作系統(tǒng)的最小版本號(hào)。這個(gè)域有點(diǎn)含糊,因?yàn)镾ubsystem 域(后面將會(huì)說(shuō)到)可以提供類似的功能。這個(gè)域在到目前為止的Win32中默認(rèn)是1.0。

WORD MajorImageVersion
WORD MinorImageVersion
一個(gè)可由用戶定義的域。這允許你有不同的EXE和DLL版本。你可以通過(guò)鏈接器的 /version 選項(xiàng)設(shè)置這個(gè)域的值。例如:"link /version:2.0 myobj.obj"。

WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
這個(gè)程序運(yùn)行需要的最小子系統(tǒng)版本號(hào)。這個(gè)域的一個(gè)典型值是3.10 (表示W(wǎng)indowsNT 3.1)。

DWORD Reserved1
通常是 0 。

DWORD SizeOfImage
載入器必須關(guān)心的這個(gè)映像所有部分的大小總和。是從映像的開(kāi)始到最后一個(gè)塊結(jié)尾這段區(qū)域的大小。最后一個(gè)塊結(jié)尾按SectionAlignment進(jìn)位。
譯注:這個(gè)很重要,可以大,但不可以小!

DWORD SizeOfHeaders
PE首部和塊表的大小。塊的實(shí)際數(shù)據(jù)緊跟在所有首部組件之后。

DWORD CheckSum
這個(gè)文件的CRC校驗(yàn)和。在微軟可執(zhí)行格式中,這個(gè)域被忽略并且置為0 。這個(gè)規(guī)則的一個(gè)例外情況是信任服務(wù),這類EXE文件必須有一個(gè)合法的校驗(yàn)和。

WORD Subsystem
可執(zhí)行文件的用戶界面使用的子系統(tǒng)類型。WINNT.H 定義了下面這些值:
NATIVE 1 不需要子系統(tǒng)(比如設(shè)備驅(qū)動(dòng))
WINDOWS_GUI 2 在Windows圖形用戶界面子系統(tǒng)下運(yùn)行
WINDOWS_CUI 3 在Windows字符子系統(tǒng)下運(yùn)行(控制臺(tái)程序)
OS2_CUI 5 在OS/2字符子系統(tǒng)下運(yùn)行(僅對(duì)OS/2 1.x)
POSIX_CUI 7 在 Posix 字符子系統(tǒng)下運(yùn)行

WORD DllCharacteristics
指定在何種環(huán)境下一個(gè)DLL的初始化函數(shù)(比如DllMain)將被調(diào)用的標(biāo)志變量。這個(gè)值經(jīng)常被置為0 。但是操作系統(tǒng)在下面四種情況下仍然調(diào)用DLL的初始化函數(shù)。


下面的值定義為:
1 DLL第一次載入到進(jìn)程中的地址空間中時(shí)調(diào)用
2 一個(gè)線程結(jié)束時(shí)調(diào)用
4 一個(gè)線程開(kāi)始時(shí)調(diào)用
8 退出DLL時(shí)調(diào)用

DWORD SizeOfStackReserve
為初始線程保留的虛擬內(nèi)存總數(shù)。然而并不是所有這些內(nèi)存都被提交(見(jiàn)下一個(gè)域)。這個(gè)域的默認(rèn)值是0x100000(1Mbytes)。如果你在CreateThread 中把堆棧尺寸指定為 0 ,結(jié)果將是用這個(gè)相同的值(0x10000)。

DWORD SizeOfStackCommit
開(kāi)始提交的初始線程堆棧總數(shù)。對(duì)微軟的連接器,這個(gè)域默認(rèn)是0x1000字節(jié)(一頁(yè)),TLINK32 是兩頁(yè)。

DWORD SizeOfHeapReserve
為初始進(jìn)程的堆保留的虛擬內(nèi)存總數(shù)。這個(gè)堆的句柄可以用GetPocessHeap 得到。并不是所有這些內(nèi)存都被提交(見(jiàn)下一個(gè)域)。

DWORD SizeOfHeapCommit
開(kāi)始為進(jìn)程堆提交的內(nèi)存總數(shù)。默認(rèn)是一頁(yè)。

DWORD LoaderFlags
從WINNT.H中可以看到,這些標(biāo)志是和調(diào)試支持相聯(lián)系的。我從沒(méi)有見(jiàn)到過(guò)在哪個(gè)可執(zhí)行文件中這些位都置位了,清除它讓連接器來(lái)設(shè)置它。下面的值定義為:
1. 在開(kāi)始進(jìn)程前調(diào)用一個(gè)端點(diǎn)指令
2. 進(jìn)程被載入時(shí)調(diào)用一個(gè)調(diào)試器

DWORD NumberOfRvaAndSizes
數(shù)據(jù)目錄數(shù)組中的的條目數(shù)目(見(jiàn)下面)。當(dāng)前的工具通常把這個(gè)值設(shè)為16。
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
一 個(gè)IMAGE_DATA_DIRECTORY 結(jié)構(gòu)數(shù)組。初始數(shù)組元素包含可執(zhí)行文件的重要部分的起始RVA和大小。這個(gè)數(shù)組最末的一些元素現(xiàn)在沒(méi)有使用。這個(gè)數(shù)組的第一個(gè)元素經(jīng)常時(shí)導(dǎo)出函數(shù)表的地址 和尺寸。第二個(gè)數(shù)組條目是導(dǎo)入函數(shù)表的地址和尺寸,等等。對(duì)一個(gè)完整的、已定義的數(shù)組條目,見(jiàn)IMAGE_DIRECTORY_ENTRY_XXX 在WINNT.H中的定義。這個(gè)數(shù)組允許載入器迅速查找這個(gè)映像的一個(gè)指定的塊(例如,導(dǎo)入函數(shù)表),而不需要遍歷映像的每個(gè)塊,通過(guò)比較名字來(lái)確定。大 部分?jǐn)?shù)組條目描述一整塊數(shù)據(jù)。然而,IMAGE_DIRECTORY_ENTRY_DEBUG項(xiàng)只包括 .rdata 塊的一小部分字節(jié)。


3 塊表
在PE首部和映像塊之間的是塊表。塊表本質(zhì)上是包含映像中每個(gè)塊信息的電話本。映像中的塊以他們的起始地址(RVA)排列,而不是按字母排列。
現(xiàn) 在,我進(jìn)一步澄清什么是一個(gè)塊。在NE文件中,你的程序代碼和數(shù)據(jù)存儲(chǔ)在相互區(qū)別開(kāi)來(lái)的段中。NE首部的一部分是一個(gè)結(jié)構(gòu)數(shù)組,每個(gè)對(duì)應(yīng)你的程序用到的一 個(gè)段。數(shù)組中的每個(gè)結(jié)構(gòu)包含一個(gè)段的信息。這些信息存儲(chǔ)了段的類型(代碼或數(shù)據(jù))、大小、和它在文件中的位置。在PE文件中,塊表和NE文件中的段表類 似。和NE文件的段表不同,PE塊表項(xiàng)不存儲(chǔ)一個(gè)代碼和數(shù)據(jù)塊的選擇子。代替的,每個(gè)塊表項(xiàng)存儲(chǔ)文件的生鮮數(shù)據(jù)映射到內(nèi)存中以后的地址。于是塊就和32位 段類似,但他們實(shí)際上不是單獨(dú)的段。它們實(shí)際上是進(jìn)程虛擬空間的一個(gè)內(nèi)存范圍。
另一個(gè)PE文件和NE文件的不同之處是它怎樣管理你的程序不用,但 操作系統(tǒng)要用的支持?jǐn)?shù)據(jù);例如可執(zhí)行文件使用的DLL列表或修正表的位置。在NE文件中,資源不被當(dāng)作段。甚至分配給他們的選擇子,資源的相關(guān)信息并未存 儲(chǔ)在NE文件首部的段表中。代替的,提交給一個(gè)分隔表的資源朝向PE首部的結(jié)尾。關(guān)于導(dǎo)入和導(dǎo)出函數(shù)的信息也沒(méi)有授權(quán)給它自己的段;它交織在NE首部中。
PE文件的故事就不一樣了。任何可能被認(rèn)為是關(guān)鍵的代碼或數(shù)據(jù)都存在一個(gè)完備的塊中。于是,導(dǎo)入函數(shù)表的信息就存在它自己的塊中,導(dǎo)出表也一樣。對(duì)重定位數(shù)據(jù)也是一樣的。程序或操作系統(tǒng)可能需要的任何代碼或數(shù)據(jù)都可以得到它們自己的塊。
在 我討論特定塊之前,我需要先描述操作系統(tǒng)管理這些塊的數(shù)據(jù)。在內(nèi)存中緊跟在PE首部的是一個(gè)IMAGE_SECTION_HEADER數(shù)組。數(shù)組的元素個(gè) 數(shù)在PE首部中給定(IMAGE_NT_HEADER.FileHeader.NumberOfSections域)。我用PEDUMP來(lái)輸出塊表和塊的 所有的域及其屬性。表5 描述了用PEDUMP輸出的一個(gè)典型EXE文件的塊表,表6 給出了 Obj 文件的塊表。
表 4 一個(gè)典型EXE文件的塊表
01 .text VirtSize: 00005AFA VirtAddr: 00001000
raw data offs: 00000400 raw data size: 00005C00
relocation offs: 00000000 relocations: 00000000
line # offs: 00009220 line #'s: 0000020C
characteristics: 60000020
CODE MEM_EXECUTE MEM_READ

02 .bss VirtSize: 00001438 VirtAddr: 00007000
raw data offs: 00000000 raw data size: 00001600
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000080
UNINITIALIZED_DATA MEM_READ MEM_WRITE

03 .rdata VirtSize: 0000015C VirtAddr: 00009000
raw data offs: 00006000 raw data size: 00000200
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 40000040
INITIALIZED_DATA MEM_READ

04 .data VirtSize: 0000239C VirtAddr: 0000A000
raw data offs: 00006200 raw data size: 00002400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE

05 .idata VirtSize: 0000033E VirtAddr: 0000D000
raw data offs: 00008600 raw data size: 00000400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE

06 .reloc VirtSize: 000006CE VirtAddr: 0000E000
raw data offs: 00008A00 raw data size: 00000800
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42000040
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
表 5 一個(gè)典型OBJ文件的塊表
01 .drectve PhysAddr: 00000000 VirtAddr: 00000000
raw data offs: 000000DC raw data size: 00000026
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 00100A00
LNK_INFO LNK_REMOVE

02 .debug$S PhysAddr: 00000026 VirtAddr: 00000000
raw data offs: 00000102 raw data size: 000016D0
relocation offs: 000017D2 relocations: 00000032
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ

03 .data PhysAddr: 000016F6 VirtAddr: 00000000
raw data offs: 000019C6 raw data size: 00000D87
relocation offs: 0000274D relocations: 00000045
line # offs: 00000000 line #'s: 00000000
characteristics: C0400040
INITIALIZED_DATA MEM_READ MEM_WRITE

04 .text PhysAddr: 0000247D VirtAddr: 00000000
raw data offs: 000029FF raw data size: 000010DA
relocation offs: 00003AD9 relocations: 000000E9
line # offs: 000043F3 line #'s: 000000D9
characteristics: 60500020
CODE MEM_EXECUTE MEM_READ

05 .debug$T PhysAddr: 00003557 VirtAddr: 00000000
raw data offs: 00004909 raw data size: 00000030
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ

每個(gè)IAMGE_SECTION_HEADER都有一個(gè)如圖7 描述的格式。注意每個(gè)塊中存儲(chǔ)的信息缺失了什么是很有趣的。首先,注意沒(méi)有指明任何預(yù)載入的屬性。NE文件格式允許你指定應(yīng)該和模塊一起載入的預(yù)載入段的 屬性。OS/2? 2.0 LX 格式有點(diǎn)類似,允許你指定預(yù)載入八頁(yè)(內(nèi)存頁(yè):譯注,下同) 。PE格式就沒(méi)有任何類似的東西。微軟必須確保Win32 需求頁(yè)面的載入性能。
表 6 IMAGE_SECTION_HEADER 的格式
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]
這 是一個(gè)為塊命名的8字節(jié)ANSI名字(不UNICODE)。大部分塊名開(kāi)始于一個(gè) ". "(比如".text"),但這并非必須的,就像你可能相信的一些PE文檔一樣。你可以在匯編語(yǔ)言中用任何一個(gè)段指示你自己的塊。或者在微軟C/C++編 譯器中用"#pragma data_seg"來(lái)指示。需要注意的是如果塊名占滿8個(gè)字節(jié),就沒(méi)有NULL結(jié)束字節(jié)了。如果你熱衷于 printf ,你可以用 %8s來(lái)避免把這個(gè)名字拷貝到一個(gè)緩沖區(qū)中,然后又在結(jié)尾加上一個(gè)NULL字節(jié)。
union {
DWORD PhysicalAddress
DWORD VirtualSize
} Misc;
在EXE 和OBJ中,這個(gè)域的意義不同。在EXE中,它保存代碼或者數(shù)據(jù)的實(shí)際尺寸。這個(gè)尺寸是未經(jīng)過(guò)校準(zhǔn)文件對(duì)齊尺寸并進(jìn)位的。后面要講到的這個(gè)結(jié)構(gòu)的 SizeOfRawData 域(這個(gè)詞有點(diǎn)不確切)保存了校準(zhǔn)文件對(duì)齊尺寸并進(jìn)位后的尺寸。Borland 的連接器調(diào)換了這兩個(gè)域的意思,于是看上去就是正確的了。對(duì)OBJ文件,這個(gè)域指示塊的物理尺寸。第一個(gè)塊開(kāi)始于地址0 。為找到OBJ 文件中的下一個(gè)塊,把SizeOfRawData加到當(dāng)前塊基址上即可。

DWORD VirtualAddress
在EXE中,這個(gè)域保存決定載入器把這個(gè)塊映射到內(nèi)存 中哪個(gè)位置的RVA 。為計(jì)算一個(gè)給定的塊在內(nèi)存中的實(shí)際起始地址,把這個(gè)映像的基址加上存儲(chǔ)在這個(gè)域的VirtualAddress即可。用微軟的工具,第一個(gè)塊的默認(rèn) RVA是0x1000 。在OBJ文件中,這個(gè)域沒(méi)有意義,被置為0 。

DWORD SizeOfRawData
在EXE中,這個(gè)域包含這個(gè)塊按文件對(duì)齊尺寸進(jìn)位后的尺 寸。比如說(shuō),假定一個(gè)文件的對(duì)齊尺寸是0x200 。如果這個(gè)塊的VirtualAddress域(前面那個(gè)域)的是0x35a ,那么這個(gè)域就是0x400 。在OBJ文件中,這個(gè)域包含由編譯器或匯編器提供的塊的精確尺寸。換句話說(shuō),對(duì)OBJ ,它等價(jià)于EXE中的VirtualSize域。

DWORD PointerToRawData
這是一個(gè)基于文件的偏移,通過(guò)這個(gè)偏移,可以找到 由編譯器或匯編器產(chǎn)生的生鮮數(shù)據(jù)。如果你的程序自己要把一個(gè)PE或COFF文件映射到內(nèi)存(而不是讓操作系統(tǒng)來(lái)載入),那么這個(gè)域比 VirtualAddress更重要。在這種情況下你有一個(gè)完全線性的文件映射,所以你會(huì)在這個(gè)偏移處找到塊的數(shù)據(jù),而不是在 VirtualAddress域指定的RVA 處找到。
DWORD PointerToRelocations
在OBJ中,這是指向塊 的重定位信息的基于文件的偏移值。每個(gè)OBJ塊的重定位信息緊跟在這個(gè)塊的生鮮數(shù)據(jù)之后。在EXE中,這個(gè)域(和后面的)是沒(méi)有意義的,被置為0 。連接器產(chǎn)生EXE時(shí),它解決了大部分的這種修正值,只剩下基址的重定位和導(dǎo)入函數(shù),將在載入時(shí)解決。關(guān)于基本重定位信息和導(dǎo)入函數(shù)保留在他們自己的塊 中,所以對(duì)一個(gè)EXE ,沒(méi)有必要在每個(gè)塊的生鮮數(shù)據(jù)之后都緊跟它的重定位信息。

DWORD PointerToLinenumbers
這是行號(hào)表基于文件的偏移量。行號(hào)表把源 文件的一行和(編譯器)為這一行產(chǎn)生的(機(jī)器)代碼的首址聯(lián)系起來(lái)。在如CodeView格式的現(xiàn)代調(diào)試格式中,行號(hào)信息存儲(chǔ)為調(diào)試信息的一部分。然而, 在COFF調(diào)試格式中,行號(hào)信息和符號(hào)名/型信息的存儲(chǔ)是分開(kāi)的。通常只有代碼塊(如 .text )有行號(hào)信息。在EXE文件中,行號(hào)信息在塊的生鮮數(shù)據(jù)之后,朝著文件的結(jié)尾方向收集。在OBJ文件中,一個(gè)塊的行號(hào)信息跟在生鮮塊數(shù)據(jù)和這個(gè)塊的重定位 表之后。

WORD NumberOfRelocations
塊的重定位表中的重定位項(xiàng)的數(shù)目(參考上面的PointerToRelocations域)。這個(gè)域似乎只和OBJ文件有關(guān)。

WORD NumberOfLinenumbers
塊的行號(hào)表中的行號(hào)項(xiàng)的數(shù)目(參考上面的PointerToLinenumbers域)。

DWORD Characteristics
大部分程序員的稱之為標(biāo)志,COFF/PE格式稱之為特征。這個(gè)域是指示塊屬性的標(biāo)志集(如代碼/數(shù)據(jù),可讀,可寫)。一個(gè)對(duì)所有可能的塊屬性的完整的列表,見(jiàn)WINNT.H中的IMAGE_SCN_XXX_XXX的定義。如下是比較重要的一些標(biāo)志:

0x00000020 這個(gè)塊包含代碼。通常和可執(zhí)行標(biāo)志(0x80000000)一起置位。
0x00000040 這個(gè)塊包含已初始化的數(shù)據(jù)。除了可執(zhí)行塊和 .bss 塊之外幾乎所有的塊的這個(gè)標(biāo)志都置位。
0x00000080 這個(gè)塊包含未初始化的數(shù)據(jù)(如 .bss 塊)
0x00000200 這個(gè)塊包含注釋或其它的信息。這個(gè)塊的一個(gè)典型用法是編譯器產(chǎn)生的 .drectve 塊,包含鏈接器命令。
0x00000800 這個(gè)塊的內(nèi)容不應(yīng)放進(jìn)最終的EXE文件中。這些塊是編譯器或匯編器用來(lái)給連接器傳遞信息的。0x02000000 這個(gè)塊可以被丟棄,因?yàn)橐坏┧惠d入,其進(jìn)程就不需要它了。最通常的可丟棄塊是基本重定位塊( .reloc )。
0x10000000 這個(gè)塊是可共享的。和DLL一起使用時(shí),這個(gè)塊的數(shù)據(jù)可以在使用這個(gè)DLL的進(jìn)程之間共享。默認(rèn)時(shí)數(shù)據(jù)塊是非共享的,這意味著使用這個(gè)DLL的各個(gè)進(jìn)程都 有自己對(duì)這個(gè)塊的數(shù)據(jù)的副本。在更專業(yè)的術(shù)語(yǔ)中,共享塊告訴內(nèi)存管理器把使用這個(gè)DLL的所有進(jìn)程把的這個(gè)塊的頁(yè)面映射到內(nèi)存中相同的物理頁(yè)面。為使一個(gè) 塊可共享,在連接時(shí)用SHARE屬性。如:
LINK /SECTION:MYDATA,RWS ...
告訴連接器叫做"MYDATA"的塊是可讀的,可寫的,共享的。
0x20000000 這個(gè)塊是可執(zhí)行的。這個(gè)標(biāo)志通常在"包含代碼"標(biāo)志(0x00000020)被置位時(shí)置位。
0x40000000 這個(gè)塊是可讀的。在EXE文件中,這個(gè)域幾乎總被置位。
0x80000000 這個(gè)塊是可寫的。如果在一個(gè)EXE塊中這個(gè)塊未被置位,載入器會(huì)把這塊的內(nèi)存映射頁(yè)面標(biāo)為只讀或"只執(zhí)行"。有此屬性的典型的塊是 .data 和 .bss 。有趣的是,.idata 塊也有這個(gè)屬性。
PE 格式中還缺少"頁(yè)表"的概念。在LX格式中,OS/2的IMAGE_SECTION_TABLE等價(jià)物不直接指向文件中的代碼或數(shù)據(jù)塊。代替的,它指向一 個(gè)指示塊中特定范圍的屬性和位置的頁(yè)查找表。PE格式分配所有的,并且確保所有的塊中的數(shù)據(jù)將連續(xù)的存儲(chǔ)在文件中。比較這兩種格式:LX可以允許更大的靈 活性,但PE風(fēng)格更簡(jiǎn)單,更容易協(xié)同工作。我已經(jīng)寫了這兩種文件的Dumper 。
PE格式另一個(gè)值得歡迎的改變是所有項(xiàng)目的位置都存儲(chǔ)為簡(jiǎn)單的 雙字(DWORD)偏移。在NE格式中,幾乎所有東西的位置都存儲(chǔ)為它們的扇區(qū)值。為了得到實(shí)際的偏移,你第一步需要查找NE首部的對(duì)齊單元尺寸并把它轉(zhuǎn) 化為扇區(qū)尺寸(典型的是 16 和512 字節(jié))。然后你需要把扇區(qū)尺寸乘以指定的扇區(qū)偏移才得到實(shí)際的文件偏移。如果NE文件的某些東西偶然存儲(chǔ)為一個(gè)扇區(qū)偏移,這可能是相對(duì)于NE首部的。因?yàn)? NE首部并不在文件的開(kāi)始,你需要在自己的代碼中調(diào)整這個(gè)文件的NE首部。總之,PE格式比NE,LX,或LE格式更容易協(xié)同工作(假定你能使用內(nèi)存映像 文件)。

4 通用塊
已經(jīng)看到了大體上塊是什么和它們位于何處,讓我們看一下你將會(huì)在EXE和OBJ文件中找到的通用塊。這個(gè)列表決不是完整的,但包含了你每天都碰到的塊(甚至你沒(méi)有意識(shí)到的)。
.text 塊是編譯器或匯編器結(jié)束時(shí)產(chǎn)生的通用代碼塊。因?yàn)镻E文件運(yùn)行在32位模式下,并且沒(méi)有16位段的限制,沒(méi)有理由根據(jù)分開(kāi)的源文件把代碼分為分開(kāi)的塊。代 替的,連接器把從不同的OBJ文件得來(lái)的 .text 塊連接起來(lái)放到EXE文件中的一個(gè)大 .text 塊中。如果你用 Borland C++ ,編譯器把產(chǎn)生的代碼放到名為 CODE 的塊中。Borland C++ 生成的PE文件有一個(gè)名為 CODE 的塊而不是名為 .text 。我將會(huì)簡(jiǎn)短的解釋一下。

Figure 2. Calling a function in another module
對(duì) 我來(lái)說(shuō),除了我用編譯器創(chuàng)建的或從運(yùn)行時(shí)庫(kù)中得到的代碼外,在 .text 塊中找到附加的代碼是比較有趣的。在一個(gè)PE文件中,當(dāng)你在另一模塊中調(diào)用一個(gè)函數(shù)時(shí)(比如在USER32.DLL中的GetMessage ),編譯器產(chǎn)生的CALL 指令并不把控制直接轉(zhuǎn)移到在DLL中的這個(gè)函數(shù)(見(jiàn)圖8)。代替的,CALL 指令把把控制轉(zhuǎn)移到一個(gè)也在 .text 中的
JMP DWORD PTR [XXXXXXXX]
指 令處。這個(gè) JMP 指令(譯注1)通過(guò)一個(gè)在 .idata 中的DWORD變量間接的轉(zhuǎn)移控制。 .idata 塊的DWORD包含操作系統(tǒng)函數(shù)入口的實(shí)際地址。在對(duì)這進(jìn)行一會(huì)兒回想之后,我開(kāi)始理解為什么DLL調(diào)用用這種方式來(lái)實(shí)現(xiàn)。通過(guò)一個(gè)位置傳送所有的對(duì)一個(gè) 給定的DLL函數(shù)的調(diào)用,載入器不需要改變每個(gè)調(diào)用DLL的指令。所有的PE載入器必須做的是把目標(biāo)函數(shù)的正確地址放到 .idata 的一個(gè) DWORD 中。不需要改變?nèi)魏蝐all指令。在NE文件中就不同了,每個(gè)段都包含一個(gè)需要應(yīng)用到這個(gè)段上的一個(gè)修正表。如果這個(gè)段把一個(gè)給定的DLL函數(shù)調(diào)用了20 次,載入器必須把這個(gè)函數(shù)的地址寫入到這個(gè)段的每個(gè)調(diào)用指令中。PE方法的缺點(diǎn)是你不能用一個(gè)DLL函數(shù)的真實(shí)地址來(lái)初始化一個(gè)變量。比如,你要考慮這樣 的情況:
FARPROC pfnGetMessage = GetMessage;
將把GetMessage的地址存到變量 pfnGetMessage 中。在16位Windows中,這可以工作,但在Win32中不能。在Win32中,變量pfnGetMessage最終存儲(chǔ)的是我前面提到的JMP DWORD PTR [XXXXXXXX] 替換指示(譯注2)。如果你想通過(guò)函數(shù)指針調(diào)用一個(gè)函數(shù),事情也會(huì)如你所預(yù)料的一樣。但是,如果你想讀取 GetMessage 開(kāi)始的字節(jié),你將不能如愿(除非你自己做跟在 .idata 指針后的工作)。后面我將會(huì)返回到這個(gè)話題上--在導(dǎo)入表的討論中。
譯注1:英文 thunk,正統(tǒng)的計(jì)算機(jī)專業(yè)術(shù)語(yǔ)為"形實(shí)轉(zhuǎn)換程序",類似宏(macro)替換,故我將它譯為"替換指示",指在具體指令中xxxxxxxx 被替換,后面出現(xiàn)的替換指示同。
譯 注2:現(xiàn)在的編譯器如VC6以上等等,產(chǎn)生的導(dǎo)入函數(shù)調(diào)用代碼不再是先來(lái)一個(gè)相對(duì)Call指令到 jmp [xxxx] 處,然后再到 xxxx 處(真正的導(dǎo)入函數(shù)入口),而是用了一種效率更高,也更容易讓人理解的方式:call [xxxx] 。以前用那種間接的方式多是為兼容編譯器。但是現(xiàn)在仍有一些編譯器,如MASM,直到版本7.0,還是用前面那種間接的方式,從這里也可以看出微軟對(duì) ASM的態(tài)度了。
雖然 Borland 可以讓編譯器輸出的代碼塊名為 .text ,但它是選擇 NAME 作為默認(rèn)的段名。為了確定PE文件中的塊名,Borland 的連接器(TLINK32.EXE)從OBJ文件中取出段名并把它截?cái)酁?字符(如果有必要)。
當(dāng) 塊名的不同只是一個(gè)小問(wèn)題時(shí),Borland PE 文件怎樣鏈接到其它模塊就是一個(gè)重要的不同。就像我在 .text 的描述中提到的,所有到OBJ的調(diào)用通過(guò)一個(gè)JMP DWORD PTR [XXXXXXXX]替換指示。在微軟系統(tǒng)下,這條指令通過(guò)一個(gè)導(dǎo)入庫(kù)到達(dá) .text 塊。因?yàn)閹?kù)管理器(LIB32)當(dāng)你鏈接外部DLL時(shí)才創(chuàng)建導(dǎo)入庫(kù)(和這個(gè)替換指示),連接器自己不需要"知道"怎樣生成這這個(gè)替換指示。導(dǎo)入庫(kù)實(shí)際上只 不過(guò)是鏈接到這個(gè)PE文件的一些更多的代碼和數(shù)據(jù)。
Borland 處理導(dǎo)入函數(shù)的系統(tǒng)只是一個(gè)簡(jiǎn)單的16位NE文件方式擴(kuò)展。Borland 連接器使用的導(dǎo)入庫(kù)實(shí)際上只不過(guò)是一個(gè)函數(shù)名連同它所在的DLL名的列表。于是TLINK32就有責(zé)任確定外部DLL的修正,并生為它成一個(gè)適當(dāng)?shù)腏MP DWORD PTR [XXXXXXXX] 替換指示 。TLINK32把這個(gè)替換指示存儲(chǔ)在它創(chuàng)建的名為 .icode 塊中。正像 .text 是默認(rèn)的代碼塊,.data 塊是已初始化數(shù)據(jù)的歸宿。這些數(shù)據(jù)包含編譯時(shí)初始化的全局和靜態(tài)局部變量。它還包括文字字符串。連接器把從OBJ/LIB文件得來(lái)的所有 .data 塊組合到EXE文件的一個(gè) .data 塊中。局部變量載入到一個(gè)線程的堆棧中,在 .data 或 .bss 中不占空間。
.bss 塊是存儲(chǔ)未初始化的全局和靜態(tài)局部變量的地方。連接器把 OBJ/LIB 文件中的所有 .bss 塊鏈接到EXE文件的一個(gè) .bss 塊中。在塊表中,.bss 塊的RawDataOffset 域置為0 ,表示這個(gè)塊在文件中不占用任何空間。TLINK 不產(chǎn)生這個(gè)塊。代替的,它擴(kuò)展 DATA 塊的虛擬尺寸(virtual size)。
.CRT 塊是微軟 C/C++ 運(yùn)行時(shí)庫(kù)利用的另一個(gè)已初始化數(shù)據(jù)的塊(從名字)。我不能理解為什么這些數(shù)據(jù)不放在 .data 中。(譯注)
譯注:從CRT的字面意思看,應(yīng)該是"C Run Time",即C運(yùn)行時(shí)庫(kù)。
.rsrc 塊這個(gè)模塊的所有資源。在Windows NT的早期,16位RC.EXE輸出的RES文件是微軟的PE連接器不能識(shí)別的格式。CVTRES 程序把這種格式的RES文件轉(zhuǎn)換成COFF格式的OBJ文件,把資源數(shù)據(jù)放在 OBJ 的 .rsrc 塊中。連接器就可以把這個(gè)資源OBJ當(dāng)作另一個(gè)OBJ來(lái)鏈接了,允許連接器"知道"關(guān)于資源的特殊東西。微軟最近發(fā)布的更多連接器可以直接處理RES文 件。
.idata 塊包含關(guān)于這個(gè)模塊從其它DLL導(dǎo)入的函數(shù)(和數(shù)據(jù))的信息(譯注)。這個(gè)塊和NE文件的模塊引用表是等價(jià)的。一個(gè)關(guān)鍵的不同是PE文件導(dǎo)入的每個(gè)函數(shù)都明確的列在這個(gè)塊中。為找到NE文件中的等價(jià)信息,你必須去挖掘這個(gè)段生鮮數(shù)據(jù)的結(jié)尾的重定位信息。
譯注:現(xiàn)在許多編譯器產(chǎn)生的EXE文件都沒(méi)有這個(gè)塊,然而ImportTable并不是沒(méi)有了,代替的,ImportTable僅由DataDirectory[1]指示,一般指向.text塊或.data塊中。
.edata 塊是這個(gè)PE文件導(dǎo)出到其它模塊的函數(shù)和數(shù)據(jù)的列表。它的NE文件等價(jià)物是條目表的聯(lián)合,駐留名表,和非駐留名表,和16位Windows不一樣,很少有 理由從一個(gè)EXE文件導(dǎo)出一些東西,所以你通常只在DLL中看到 .edata 塊。當(dāng)使用微軟的工具時(shí),.edata 塊中的數(shù)據(jù)通過(guò)EXP文件來(lái)到PE文件中。換種方法,連接器不為它自己生成這個(gè)信息。代替的,它依賴庫(kù)管理器(LIB32)來(lái)掃描OBJ文件,并創(chuàng)建 EXP文件,連接器要把它要鏈接的模塊的列表加入其中。是的,好!這些麻煩的EXP文件實(shí)際上只是擴(kuò)展名不同的OBJ文件而已。
.reloc 塊保持一個(gè)基本重定位表。基本重定位是一個(gè)對(duì)一條指令或已初始化的變量值的調(diào)整,如果載入器不能把這個(gè)文件載入到連接器假定的位置,這就是很重要的了。如 果載入器能把這個(gè)映像載入到連接器建議(prefer)的基地址,載入器就完全忽略這個(gè)塊的重定位信息。如果你愿意冒險(xiǎn),并且希望載入器可以始終把這個(gè)映 像載入到假定的基址,你可以通過(guò) /FIXED 選項(xiàng)告訴鏈接器去除這個(gè)信息。這樣可以在可執(zhí)行文件中節(jié)省空間,但會(huì)導(dǎo)致這個(gè)可執(zhí)行文件在其它的Win32實(shí)現(xiàn)中不能工作。比如,假定你為Windows NT建立了一個(gè)EXE文件,并且把基址設(shè)為 0x10000 。如果你讓連接器去除重定位信息,這個(gè)EXE文件在Windows95下將不能運(yùn)行,因?yàn)樵谶@里地址0x10000已被系統(tǒng)使用了。
注意編譯器生 成的JMP和CALL指令是很重要的,首選它使用相對(duì)偏移量的版本,而非32位平坦段中的真實(shí)偏移量版本。如果映像需要被載入非連接器假定的基址處,這些 指令不需要改變,因?yàn)樗褂玫氖窍鄬?duì)尋址。結(jié)果就是,并不需要你想象的那么多的重定位。重定位通常只需要使用指向一些數(shù)據(jù)的32位偏移。舉個(gè)例子,讓我們 看一下,你有如下的全局變量聲明:
int i;
int *ptr = &i;
如果連接器假定一個(gè)0x10000的映像 基址,變量i的地址將最終是一個(gè)特定值如0x12004 。在用來(lái)存放指針"ptr"的內(nèi)存中,連接器將寫進(jìn)0x12004 ,因?yàn)檫@是變量 i 的地址。如果載入器由于某種原因決定把這個(gè)文件載入基址0x70000處,變量i的地址將是0x72004 。.reloc 塊是映像中的一些內(nèi)存位置的列表,這些內(nèi)存位置在連接時(shí)連接器假定的載入地址和實(shí)際需要的載入地址是不同的,這個(gè)因素需要考慮。
當(dāng)你使用編譯器指 令 __declspec(thread) 時(shí),你定義的數(shù)據(jù)不在 .data 和 .bss 塊種。它最終在 .tls 塊中,這個(gè)塊指示"線程局部存儲(chǔ)",并且和Win32的TlsAlloc函數(shù)族相聯(lián)系。處理 .tls 塊時(shí),內(nèi)存管理器設(shè)置頁(yè)表以便進(jìn)程在任何時(shí)刻切換線程時(shí),都有一個(gè)新的物理內(nèi)存頁(yè)集映射到 .tls 塊的地址空間。這就允許線程內(nèi)的全局變量。在大部分情況下,利用這種機(jī)制,比基于線程分配內(nèi)存并把其指針存在一個(gè) "TlsAlloc 過(guò)的"(注:原文TlsAlloc'ed)槽(注:原文Slot)中要容易的多。
不幸的是,有一點(diǎn)需要注意--必須深入研究.tls 塊和 __declspec(thread) 的變量。在WindowsNT 和Windows95 中,如果DLL是被載入庫(kù)動(dòng)態(tài)載入的,這種線程局部存儲(chǔ)機(jī)制將不能在這個(gè)DLL中工作。然而在EXE中或一個(gè)隱含載入的DLL中,一切都工作正常。如果你 不隱含鏈接到這個(gè)DLL ,但需要按線程的數(shù)據(jù),你必須會(huì)到過(guò)去并使用 TlsAlloc 和 TlsGetValue 這種原始方式來(lái)設(shè)置線程動(dòng)態(tài)內(nèi)存分配。
雖然 .rdata 塊通常在 .data 和 .bss 塊之間,你的程序一般看不見(jiàn)并使用這些塊中的數(shù)據(jù)。.rdata 塊至少在兩種東西中使用。第一,在微軟連接器生成的EXE中,.rdata 塊存放調(diào)試目錄,這只在EXE文件中出現(xiàn)。(在 TLINK32 的 EXE 中,調(diào)試目錄在名為 ".DEBUG"的塊中)。調(diào)試目錄是一個(gè)IMAGE_DEBUG_DIRECTORY結(jié)構(gòu)數(shù)組。這些結(jié)構(gòu)保持存儲(chǔ)在文件中的變量的類型,尺寸,和位置的 調(diào)試信息。三種主要的調(diào)試信息類型顯示如下:CodeView?, COFF,和 FPO,表9顯示了PEDUMP輸出的一個(gè)典型的調(diào)試目錄。
表 7 一個(gè)典型的調(diào)試目錄
Type Size Address FilePtr Charactr TimeDate Version
COFF 000065C5 00000000 00009200 00000000 2CF8CF3D 0.00
??? 00000114 00000000 0000F7C8 00000000 2CF8CF3D 0.00
FPO 000004B0 00000000 0000F8DC 00000000 2CF8CF3D 0.00
CODEVIEW 0000B0B4 00000000 0000FD8C 00000000 2CF8CF3D 0.00

調(diào)試目錄不必在 .rdata 塊的開(kāi)始找到。為找到調(diào)試目錄表的開(kāi)始,使用數(shù)據(jù)目錄的第七個(gè)條目(IMAGE_DIRECTORY_ENTRY_DEBUG)的RVA。數(shù)據(jù)目錄在文件 的PE首部結(jié)尾部分。為確定微軟連接器生成的調(diào)試目錄的條目數(shù),用調(diào)試目錄的尺寸(在數(shù)據(jù)目錄條目的尺寸域)除以一個(gè) IMAGE_DEBUG_DIRECTORY結(jié)構(gòu)的尺寸即可。TLINK32產(chǎn)生一個(gè)簡(jiǎn)單的數(shù)目,通常是1 。PEDUMP示例程序描述了這一點(diǎn)。
.rdata 域的另一個(gè)有用的部分是"描述串"。如果你在程序的DEF文件中指定一個(gè)DESCRIPTION條目,這個(gè)指定的描述串就出現(xiàn)在 .rdata 塊中。在NE格式中,描述串總是非駐留名表的第一個(gè)條目。描述串是用來(lái)保持一個(gè)描述這個(gè)文件的有用的文本串的。不幸的是,我還沒(méi)找到一條便捷的途徑來(lái)得到 它。我看到有些描述串在PE文件的調(diào)試目錄之前,在另一些文件中它在調(diào)試目錄之后。我找不到得到這個(gè)描述串的一致的方法(或甚至這種方法根本就不存在)。
.debug$S 和 .debug$T 塊只出現(xiàn)在 OBJ 中。他們保存 CodeView 調(diào)試符號(hào)和類型信息。這些塊名是從以前16位編譯器($$SYMBOLS 和 $$TYPE)使用的段名繼承來(lái)的。.debug$T 塊的唯一用途是保持包含工程中所有OBJ的CodeView信息的PDB文件的路徑。連接器從PDB中讀取并且使用它來(lái)創(chuàng)建CodeView信息的組成部 分,這些CodeView信息放置在PE文件的結(jié)尾。
.drectve 塊只出現(xiàn)在OBJ文件中。它包含用文本表示的連接器命令。比如,在我用微軟編譯器編譯的任一OBJ中,下面的字符串都出現(xiàn)在 .drectve 塊中:
-defaultlib:LIBC -defaultlib:OLDNAMES
當(dāng)你在程序中用 __declspec(export) 時(shí),編譯器簡(jiǎn)單的把等價(jià)的命令行輸出到 .drectve 塊中(例如:"-exprot:MyFunction")。
在玩弄 PEDUMP 的過(guò)程中,我不時(shí)的遇到其它塊。例如,在Window95的KERNEL32.DLL中,有LOCKCODE和LOCKDATA塊。大概這是一種特殊的頁(yè)處理方法,是為了避免缺頁(yè)(譯注)。
譯注:缺頁(yè),在頁(yè)式內(nèi)存管理中,一條指令訪問(wèn)的虛擬內(nèi)存未映射到物理內(nèi)存中,此時(shí)將發(fā)生缺頁(yè)中斷,關(guān)于缺頁(yè)中斷,請(qǐng)參閱操作系統(tǒng)相關(guān)書籍。
從 這里學(xué)到兩個(gè)教訓(xùn)。第一:不要以為有約束而只使用編譯器或匯編器提供的標(biāo)準(zhǔn)塊。如果由于某種原因你需要一個(gè)分開(kāi)的塊,不要猶豫,自己去創(chuàng)建!在C/C++ 編譯器中,使用 #pragma code_seg 和 #pragma data_seg 。在匯編語(yǔ)言中,只不過(guò)是創(chuàng)建一個(gè)名字和和標(biāo)準(zhǔn)塊不同的32位的段(將成為一個(gè)塊)。如果使用TLINK32 ,你必須使用一個(gè)不同的類,或者關(guān)掉代碼段包裝(packing)。其它要記住的東西是使用非標(biāo)準(zhǔn)塊名你將會(huì)更透徹的理解特殊PE文件的意圖和實(shí)現(xiàn)。

5 PE文件的導(dǎo)入表
前面,我描述了函數(shù)調(diào)用怎樣到一個(gè)外部DLL中而不直接調(diào)用這個(gè)DLL 。代替的,在執(zhí)行體中的 .text 塊中(如果你用Borland C++ 就是 .icode 塊),CALL指令到達(dá)一條
JMP DWORD PTR [XXXXXXXX]
指令處。JMP指令尋找的地址把控制轉(zhuǎn)移到實(shí)際的目標(biāo)地址。PE文件的 .idata 會(huì)包含一些必要的信息,這些信息是載入器用來(lái)確定目標(biāo)函數(shù)的地址以及在執(zhí)行體映像中去修正他們的。
.idata 塊(或稱導(dǎo)入表,我更喜歡這樣叫)開(kāi)始于一個(gè)IMAGE_IMPORT_DESCRIPTOR數(shù)組。每個(gè)DLL都有一個(gè)PE文件隱含鏈接上的 IMAGE_IMPORT_DESCRIPTOR。沒(méi)有指定這個(gè)數(shù)組中結(jié)構(gòu)的數(shù)目的域。代替的,這個(gè)數(shù)組的最后一個(gè)元素是一個(gè)全NULL的 IMAGE_IMPORT_DESCRIPTOR 。IMAGE_IMPORT_DESCRIPTOR的格式顯示在表8 。
表 8 IMAGE_IMPORT_DESCRIPTOR Format
DWORD Characteristics
在一個(gè)時(shí)刻,這可能已是一個(gè)標(biāo)志集。然而,微軟改變了它的涵義并不再糊涂地升級(jí)WINNT.H 。這個(gè)月實(shí)際上是一個(gè)指向指針數(shù)組的偏移(RVA)。其中每個(gè)指針都指向一個(gè)IMAGE_IMPORT_BY_NAME結(jié)構(gòu)。

DWORD TimeDateStamp
指示這個(gè)文件的創(chuàng)建時(shí)間。

DWORD ForwarderChain
這個(gè)域聯(lián)系到前向鏈。前向鏈包括一個(gè)DLL函數(shù)向另一 個(gè)DLL轉(zhuǎn)送引用。比如,在WindowsNT中,NTDLL.DLL就出現(xiàn)了的一些前向的它向KERNEL32.DLL導(dǎo)出的函數(shù)。應(yīng)用程序可能以為它 調(diào)用的是NTDLL.DLL中的函數(shù),但它最終調(diào)用的是KERNEL32.DLL中的函數(shù)。這個(gè)域還包含一個(gè)FirstThunk數(shù)組的索引(即刻描 述)。用這個(gè)域索引得函數(shù)會(huì)前向引用到另一個(gè)DLL 。不幸的是,函數(shù)怎樣前向引用的格式?jīng)]有文檔,并且前向函數(shù)的例子也很難找。

DWORD Name
這是導(dǎo)入DLL的名字,指向以NULL結(jié)尾的ASCII字符串。通用例子是KERNEL32.DLL和USER32.DLL 。

PIMAGE_THUNK_DATA FirstThunk
這個(gè)域是指向 IMAGE_THUNK_DATA聯(lián)合的偏移(RVA)。幾乎在任何情況下,這個(gè)域都解釋為一個(gè)指向的IMAGE_IMPORT_BY_NAME結(jié)構(gòu)的指 針。如果這個(gè)域不是這些指針中的一個(gè),那它就被當(dāng)作一個(gè)將從這個(gè)被導(dǎo)入的DLL的導(dǎo)出序數(shù)值。如果你實(shí)際上可以從序數(shù)導(dǎo)入一個(gè)函數(shù)而不是從名字導(dǎo)入,從文 檔看,這是不清楚的。
IMAGE_IMPORT_DESCRIPTOR 的一個(gè)重要部分是導(dǎo)入的DLL的名自和兩個(gè)IMAGE_IMPORT_BY_NAME指針數(shù)組。在EXE文件中,這兩個(gè)數(shù)組(由 Characteristics域和FirstThunk域指向)是相互平行的,都是以NULL指針作為數(shù)組的最后一個(gè)元素。兩個(gè)數(shù)組中的指針都指向 IMAGE_IMPORT_BY_NAME 結(jié)構(gòu)。表3以圖形顯示了這種布局。表12顯示了PEDUMP對(duì)一個(gè)導(dǎo)入表的輸出。


圖 3. 兩個(gè)平行的指針數(shù)組
表 9. 一個(gè)EXE文件的導(dǎo)入表
GDI32.dll
Hint/Name Table: 00013064
TimeDateStamp: 2C51B75B
ForwarderChain: FFFFFFFF
First thunk RVA: 00013214
Ordn Name
48 CreatePen
57 CreateSolidBrush
62 DeleteObject
160 GetDeviceCaps
// Rest of table omitted...

KERNEL32.dll
Hint/Name Table: 0001309C
TimeDateStamp: 2C4865A0
ForwarderChain: 00000014
First thunk RVA: 0001324C
Ordn Name
83 ExitProcess
137 GetCommandLineA
179 GetEnvironmentStrings
202 GetModuleHandleA
// Rest of table omitted...

SHELL32.dll
Hint/Name Table: 00013138
TimeDateStamp: 2C41A383
ForwarderChain: FFFFFFFF
First thunk RVA: 000132E8
Ordn Name
46 ShellAboutA

USER32.dll
Hint/Name Table: 00013140
TimeDateStamp: 2C474EDF
ForwarderChain: FFFFFFFF
First thunk RVA: 000132F0
Ordn Name
10 BeginPaint
35 CharUpperA
39 CheckDlgButton
40 CheckMenuItem

// Rest of table omitted...
PE文件的導(dǎo)入表的每一個(gè)函數(shù)有一個(gè) IMAGE_IMPORT_BY_NAME 結(jié)構(gòu)。IMAGE_IMPORT_BY_NAME結(jié)構(gòu)非常簡(jiǎn)單,看上去是這樣:
WORD Hint;
BYTE Name[?];
第一個(gè)域是導(dǎo)入函數(shù)的導(dǎo)出序數(shù)的最佳猜測(cè)。和NE文件不同,這個(gè)值不是必須正確的。于是,載入器指示把它當(dāng)作一個(gè)進(jìn)行二分查找的建議開(kāi)始值。下一個(gè)是導(dǎo)入函數(shù)的名字的ASCIIZ字符串。
為 什么有兩個(gè)平行的指針數(shù)組指向結(jié)構(gòu)IMAGE_IMPORT_BY_NAME ?第一個(gè)數(shù)組(由Characteristics域指向的)單獨(dú)的留下來(lái),并不被修改。經(jīng)常被稱作提名表。第二個(gè)數(shù)組(由FirstThunk域指向的) 將被PE載入器覆蓋。載入器在這個(gè)數(shù)組中迭代每個(gè)指針,并查找每個(gè)IMAGE_IMPORT_BY_NAME結(jié)構(gòu)指向的函數(shù)的地址。載入器然后用找到的函 數(shù)地址覆蓋這個(gè)指向IMAGE_IMPORT_BY_NAME結(jié)構(gòu)的指針。JMP DWORD PTR [XXXXXXXX] 替換指示中的 [XXXXXXXX] 表示 FirstThunk 數(shù)組的一個(gè)條目。因?yàn)橛奢d入器覆蓋的這個(gè)指針數(shù)組實(shí)際上保持所有導(dǎo)入函數(shù)的地址,叫做"導(dǎo)入地址表"。
對(duì)Borland用戶,上面的描述有點(diǎn)別 扭。由TLINK32產(chǎn)生的PE文件缺少其中一個(gè)數(shù)組。在這樣一個(gè)執(zhí)行體中,IMAGE_IMPORT_DESCRIPTOR(提名數(shù)組)中 Characteristics域的是0 。于是,僅有的由FirstThunk域(導(dǎo)入地址表)指向的數(shù)組在PE文件中就是必須的了。故事到這里應(yīng)該結(jié)束了,除非在我寫PEDUMP時(shí)深入一個(gè)有 趣的問(wèn)題中。在優(yōu)化上無(wú)止境的探索,微軟在WindowsNT中"優(yōu)化"了系統(tǒng)DLL(KERNEL32.DLL等等)的thunk數(shù)組。在這個(gè)優(yōu)化中, 這個(gè)數(shù)組中的指針不再指向IMAGE_IMPORT_BY_NAME結(jié)構(gòu),它們已經(jīng)包含了導(dǎo)入函數(shù)的地址。換句話說(shuō),載入器不需要去查找函數(shù)的地址并用導(dǎo) 入函數(shù)的地址覆蓋thunk數(shù)組(譯注)。對(duì)希望這個(gè)數(shù)組包含指向IMAGE_IMPORT_BY_NAME結(jié)構(gòu)的指針的PEDump程序,這導(dǎo)致了一個(gè) 問(wèn)題。你可能正在思考,"但是,Matt ,為什么呢不順便使用提名表數(shù)組?"這可能是一個(gè)完美的解決方案,除非提名表數(shù)組在Borland文件中不存在。PEDUMP處理所有這些情況,但是代碼 理所當(dāng)然的就有些雜亂。
譯注: 這就是 Bound Import,關(guān)于Bound Import,請(qǐng)參閱:
Matt Pietrek "Inside Windows An In-Depth Look into the Win32 Portable Executable File Format, Part 2 " From MSDN Magazine March 2002 on Internet
URL :http://msdn.microsoft.com/msdnmag/issues/02/03/PE2/PE2.asp
因?yàn)閷?dǎo)入地址表在一個(gè)可寫的塊中,攔截一個(gè)EXE或DLL對(duì)另一個(gè)DLL的調(diào)用就相對(duì)容易。只需要修改適當(dāng)?shù)貙?dǎo)入地址條目去指向希望攔截的函數(shù)。不需要修改調(diào)用者或被調(diào)者的任何代碼。
注 意微軟產(chǎn)生的PE文件的導(dǎo)入表并不是完全被連接器同步的,這一點(diǎn)很有趣。所有對(duì)另一個(gè)DLL中的函數(shù)的調(diào)用的指令都在一個(gè)導(dǎo)入庫(kù)中。當(dāng)你連接一個(gè)DLL 時(shí),庫(kù)管理器(LIB32.EXE或LIB.EXE)掃描將要被連接的OBJ文件并且創(chuàng)建一個(gè)導(dǎo)入庫(kù)。這個(gè)導(dǎo)入庫(kù)完全不同于16位NE文件連接器使用的導(dǎo) 入庫(kù)。32位庫(kù)管理器產(chǎn)生的導(dǎo)入庫(kù)有一個(gè).text塊和幾個(gè).idata$塊。導(dǎo)入庫(kù)中的.text塊包含 JMP [XXXX] 的替換指示,這個(gè)替換指示在OBJ文件的符號(hào)表中有一個(gè)名字來(lái)存儲(chǔ)它。這個(gè)符號(hào)名對(duì)將從DLL中導(dǎo)出的所有函數(shù)名都是唯一的(例如: _Dispatch_Message@4)。導(dǎo)入庫(kù)中的一個(gè).idata$塊包含一個(gè)從其中引用的替換指示(譯注:即JMP [XXXX]中的XXXX)。另一個(gè).idata$塊有一個(gè)導(dǎo)入函數(shù)名之前的提示序號(hào)(hint ordinal)的空間。這兩個(gè)域就組成了IMAGE_IMPORT_BY_NAME結(jié)構(gòu)。當(dāng)你晚連接一個(gè)使用導(dǎo)入庫(kù)的PE文件時(shí),導(dǎo)入庫(kù)的塊被加到連接 器需要處理的在OBJ文件中的你的塊的列表中。一旦導(dǎo)入庫(kù)中的這個(gè)替換指示的名字和和要導(dǎo)入的函數(shù)名相同,連接器就假定這個(gè)替換指示就是這個(gè)導(dǎo)入函數(shù),并 修正對(duì)這個(gè)導(dǎo)入函數(shù),使其指向這個(gè)替換指示。導(dǎo)入庫(kù)中的這個(gè)替換指示在本質(zhì)上就被當(dāng)作這個(gè)導(dǎo)入函數(shù)本身了。
除了提供一個(gè)導(dǎo)入函數(shù)替換指示的代碼部 分,導(dǎo)入庫(kù)還提供PE文件的.idata塊(或稱導(dǎo)入表)的片斷。這些片斷來(lái)自于庫(kù)管理器放入導(dǎo)入庫(kù)中的不同的.idata$塊。簡(jiǎn)而言之,連接器實(shí)際上 不知道出現(xiàn)在不同的OBJ文件中的導(dǎo)入函數(shù)和普通函數(shù)之間的不同。連接器只是按照它的邊框調(diào)整規(guī)則去建立并結(jié)合塊,于是,所有的事情就自然順理成章了。
6 術(shù)語(yǔ)
生鮮數(shù)據(jù):原文"RawData",意指未加工過(guò)的數(shù)據(jù),即原原本本從磁盤上讀入而未經(jīng)過(guò)任何改動(dòng)的數(shù)據(jù)。
替換指示:原文"thunk",本質(zhì)上是一條指令,這條指令中有浮動(dòng)的地址域。如文中的 jmp [xxxx],其中xxxx是一個(gè)浮動(dòng)地址(floating address),或稱可重定位地址(relocatable address)。
OBJ文件:Object文件,即編譯器編譯產(chǎn)生的目標(biāo)文件,這種文件只有在(和LIB)連接之后,才能形成可執(zhí)行文件。
LIB文件:庫(kù)文件,這種文件中包含一些二進(jìn)制的代碼(數(shù)據(jù))及其符號(hào),一般情況下,用到LIB中的哪個(gè)符號(hào),連接器連接時(shí),關(guān)于那個(gè)符號(hào)的二進(jìn)制代碼(數(shù)據(jù))才會(huì)放入最終的執(zhí)行體中。
RES文件:Widows資源文件,由RC.EXE編譯。
EXE文件:不用多說(shuō)Windows下的可執(zhí)行文件,這類文件一般有導(dǎo)入表(Import Table)。有少數(shù)這類文件有導(dǎo)出表(Export Table)。
DLL文件:Dinamic Link Library ,即動(dòng)態(tài)連接庫(kù),用來(lái)向其它執(zhí)行體導(dǎo)出函數(shù)(或數(shù)據(jù)等)。