什么是PE文件及PE文件的結構和簡述

?

A Tour of the Win32 Portable Executable File Format

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

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

DWORD LoaderFlags
從WINNT.H中可以看到,這些標志是和調試支持相聯系的。我從沒有見到過在哪個可執行文件中這些位都置位了,清除它讓連接器來設置它。下面的值定義為:
1. 在開始進程前調用一個端點指令
2. 進程被載入時調用一個調試器

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


3 塊表
在PE首部和映像塊之間的是塊表。塊表本質上是包含映像中每個塊信息的電話本。映像中的塊以他們的起始地址(RVA)排列,而不是按字母排列。
現 在,我進一步澄清什么是一個塊。在NE文件中,你的程序代碼和數據存儲在相互區別開來的段中。NE首部的一部分是一個結構數組,每個對應你的程序用到的一 個段。數組中的每個結構包含一個段的信息。這些信息存儲了段的類型(代碼或數據)、大小、和它在文件中的位置。在PE文件中,塊表和NE文件中的段表類 似。和NE文件的段表不同,PE塊表項不存儲一個代碼和數據塊的選擇子。代替的,每個塊表項存儲文件的生鮮數據映射到內存中以后的地址。于是塊就和32位 段類似,但他們實際上不是單獨的段。它們實際上是進程虛擬空間的一個內存范圍。
另一個PE文件和NE文件的不同之處是它怎樣管理你的程序不用,但 操作系統要用的支持數據;例如可執行文件使用的DLL列表或修正表的位置。在NE文件中,資源不被當作段。甚至分配給他們的選擇子,資源的相關信息并未存 儲在NE文件首部的段表中。代替的,提交給一個分隔表的資源朝向PE首部的結尾。關于導入和導出函數的信息也沒有授權給它自己的段;它交織在NE首部中。
PE文件的故事就不一樣了。任何可能被認為是關鍵的代碼或數據都存在一個完備的塊中。于是,導入函數表的信息就存在它自己的塊中,導出表也一樣。對重定位數據也是一樣的。程序或操作系統可能需要的任何代碼或數據都可以得到它們自己的塊。
在 我討論特定塊之前,我需要先描述操作系統管理這些塊的數據。在內存中緊跟在PE首部的是一個IMAGE_SECTION_HEADER數組。數組的元素個 數在PE首部中給定(IMAGE_NT_HEADER.FileHeader.NumberOfSections域)。我用PEDUMP來輸出塊表和塊的 所有的域及其屬性。表5 描述了用PEDUMP輸出的一個典型EXE文件的塊表,表6 給出了 Obj 文件的塊表。
表 4 一個典型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 一個典型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

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

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

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

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

DWORD PointerToLinenumbers
這是行號表基于文件的偏移量。行號表把源 文件的一行和(編譯器)為這一行產生的(機器)代碼的首址聯系起來。在如CodeView格式的現代調試格式中,行號信息存儲為調試信息的一部分。然而, 在COFF調試格式中,行號信息和符號名/型信息的存儲是分開的。通常只有代碼塊(如 .text )有行號信息。在EXE文件中,行號信息在塊的生鮮數據之后,朝著文件的結尾方向收集。在OBJ文件中,一個塊的行號信息跟在生鮮塊數據和這個塊的重定位 表之后。

WORD NumberOfRelocations
塊的重定位表中的重定位項的數目(參考上面的PointerToRelocations域)。這個域似乎只和OBJ文件有關。

WORD NumberOfLinenumbers
塊的行號表中的行號項的數目(參考上面的PointerToLinenumbers域)。

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

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

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

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

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

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

DWORD TimeDateStamp
指示這個文件的創建時間。

DWORD ForwarderChain
這個域聯系到前向鏈。前向鏈包括一個DLL函數向另一 個DLL轉送引用。比如,在WindowsNT中,NTDLL.DLL就出現了的一些前向的它向KERNEL32.DLL導出的函數。應用程序可能以為它 調用的是NTDLL.DLL中的函數,但它最終調用的是KERNEL32.DLL中的函數。這個域還包含一個FirstThunk數組的索引(即刻描 述)。用這個域索引得函數會前向引用到另一個DLL 。不幸的是,函數怎樣前向引用的格式沒有文檔,并且前向函數的例子也很難找。

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

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


圖 3. 兩個平行的指針數組
表 9. 一個EXE文件的導入表
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文件的導入表的每一個函數有一個 IMAGE_IMPORT_BY_NAME 結構。IMAGE_IMPORT_BY_NAME結構非常簡單,看上去是這樣:
WORD Hint;
BYTE Name[?];
第一個域是導入函數的導出序數的最佳猜測。和NE文件不同,這個值不是必須正確的。于是,載入器指示把它當作一個進行二分查找的建議開始值。下一個是導入函數的名字的ASCIIZ字符串。
為 什么有兩個平行的指針數組指向結構IMAGE_IMPORT_BY_NAME ?第一個數組(由Characteristics域指向的)單獨的留下來,并不被修改。經常被稱作提名表。第二個數組(由FirstThunk域指向的) 將被PE載入器覆蓋。載入器在這個數組中迭代每個指針,并查找每個IMAGE_IMPORT_BY_NAME結構指向的函數的地址。載入器然后用找到的函 數地址覆蓋這個指向IMAGE_IMPORT_BY_NAME結構的指針。JMP DWORD PTR [XXXXXXXX] 替換指示中的 [XXXXXXXX] 表示 FirstThunk 數組的一個條目。因為由載入器覆蓋的這個指針數組實際上保持所有導入函數的地址,叫做"導入地址表"。
對Borland用戶,上面的描述有點別 扭。由TLINK32產生的PE文件缺少其中一個數組。在這樣一個執行體中,IMAGE_IMPORT_DESCRIPTOR(提名數組)中 Characteristics域的是0 。于是,僅有的由FirstThunk域(導入地址表)指向的數組在PE文件中就是必須的了。故事到這里應該結束了,除非在我寫PEDUMP時深入一個有 趣的問題中。在優化上無止境的探索,微軟在WindowsNT中"優化"了系統DLL(KERNEL32.DLL等等)的thunk數組。在這個優化中, 這個數組中的指針不再指向IMAGE_IMPORT_BY_NAME結構,它們已經包含了導入函數的地址。換句話說,載入器不需要去查找函數的地址并用導 入函數的地址覆蓋thunk數組(譯注)。對希望這個數組包含指向IMAGE_IMPORT_BY_NAME結構的指針的PEDump程序,這導致了一個 問題。你可能正在思考,"但是,Matt ,為什么呢不順便使用提名表數組?"這可能是一個完美的解決方案,除非提名表數組在Borland文件中不存在。PEDUMP處理所有這些情況,但是代碼 理所當然的就有些雜亂。
譯注: 這就是 Bound Import,關于Bound Import,請參閱:
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
因為導入地址表在一個可寫的塊中,攔截一個EXE或DLL對另一個DLL的調用就相對容易。只需要修改適當地導入地址條目去指向希望攔截的函數。不需要修改調用者或被調者的任何代碼。
注 意微軟產生的PE文件的導入表并不是完全被連接器同步的,這一點很有趣。所有對另一個DLL中的函數的調用的指令都在一個導入庫中。當你連接一個DLL 時,庫管理器(LIB32.EXE或LIB.EXE)掃描將要被連接的OBJ文件并且創建一個導入庫。這個導入庫完全不同于16位NE文件連接器使用的導 入庫。32位庫管理器產生的導入庫有一個.text塊和幾個.idata$塊。導入庫中的.text塊包含 JMP [XXXX] 的替換指示,這個替換指示在OBJ文件的符號表中有一個名字來存儲它。這個符號名對將從DLL中導出的所有函數名都是唯一的(例如: _Dispatch_Message@4)。導入庫中的一個.idata$塊包含一個從其中引用的替換指示(譯注:即JMP [XXXX]中的XXXX)。另一個.idata$塊有一個導入函數名之前的提示序號(hint ordinal)的空間。這兩個域就組成了IMAGE_IMPORT_BY_NAME結構。當你晚連接一個使用導入庫的PE文件時,導入庫的塊被加到連接 器需要處理的在OBJ文件中的你的塊的列表中。一旦導入庫中的這個替換指示的名字和和要導入的函數名相同,連接器就假定這個替換指示就是這個導入函數,并 修正對這個導入函數,使其指向這個替換指示。導入庫中的這個替換指示在本質上就被當作這個導入函數本身了。
除了提供一個導入函數替換指示的代碼部 分,導入庫還提供PE文件的.idata塊(或稱導入表)的片斷。這些片斷來自于庫管理器放入導入庫中的不同的.idata$塊。簡而言之,連接器實際上 不知道出現在不同的OBJ文件中的導入函數和普通函數之間的不同。連接器只是按照它的邊框調整規則去建立并結合塊,于是,所有的事情就自然順理成章了。
6 術語
生鮮數據:原文"RawData",意指未加工過的數據,即原原本本從磁盤上讀入而未經過任何改動的數據。
替換指示:原文"thunk",本質上是一條指令,這條指令中有浮動的地址域。如文中的 jmp [xxxx],其中xxxx是一個浮動地址(floating address),或稱可重定位地址(relocatable address)。
OBJ文件:Object文件,即編譯器編譯產生的目標文件,這種文件只有在(和LIB)連接之后,才能形成可執行文件。
LIB文件:庫文件,這種文件中包含一些二進制的代碼(數據)及其符號,一般情況下,用到LIB中的哪個符號,連接器連接時,關于那個符號的二進制代碼(數據)才會放入最終的執行體中。
RES文件:Widows資源文件,由RC.EXE編譯。
EXE文件:不用多說Windows下的可執行文件,這類文件一般有導入表(Import Table)。有少數這類文件有導出表(Export Table)。
DLL文件:Dinamic Link Library ,即動態連接庫,用來向其它執行體導出函數(或數據等)。