// _pefirst.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "afxwin.h"
#include <iostream>
using std::cout;
using std::endl;
LPCTSTR lpszFileToOpen = _T("C:\\windows\\notepad.exe"); // 本例打开的文件
int main(int argc, char* argv[])
{
IMAGE_NT_HEADERS stPEHeader; //NT文件
IMAGE_SECTION_HEADER *stSectionHeader=NULL; //节表
short nSection=0; //节表数
short i; // for for
CFile fOpen;
short iPEAddr;
if (fOpen.Open(lpszFileToOpen, CFile::typeBinary|CFile::shareDenyNone) == FALSE)
{
cout << _T("File open failed.") << endl;
return 0;
}
fOpen.Seek(0x3c, CFile::begin); //DOS header的偏移 0x3c 处指向开始地址
fOpen.Read(&iPEAddr, sizeof (short)); //PE文件头地址
try{fOpen.Seek(iPEAddr, CFile::begin);}
catch (...){
cout << _T("文件格式不对!") << endl;
fOpen.Close();
return 0;}
fOpen.Read(&stPEHeader, sizeof (IMAGE_NT_HEADERS)); //读PE头
if (stPEHeader.Signature != 17744) //"PE\0\0"
{
cout << _T("该文件不是PE格式!") << endl;
fOpen.Close();
return 0;
}
nSection = stPEHeader.FileHeader.NumberOfSections; //总节数
stSectionHeader = new IMAGE_SECTION_HEADER[nSection]; //分配nSection个节表
for (i=0; i<nSection; i++)
{
fOpen.Read(&stSectionHeader[i], sizeof (IMAGE_SECTION_HEADER));
cout << stSectionHeader[i].Name << endl;
}
printf("Hello World!\n");
delete [] stSectionHeader;
fOpen.Close();
return 0;
}
PE 表头(PE Header)
我们的 PE 之旅第一站是 PE 表头。像其它的微软可执行档格式一样,PE 文件有一系列
的字段,固定在一个已知(并且很容易找到)的位置上,定义文件的其它部份。PE 表头
内含的重要信息包括程序代码和资料区域的大小位置、适用的操作系统、堆栈(stack)的
最初大小等等。
和其它的微软可执行档格式一样,PE 表头并非在文件的最起头处。文件最前面的数百个
字节是所谓的 DOS stub :一个极小的 DOS 程序,用来输出像 "This Program cannot be
run in DOS mode" 这样的讯息。如果你在一个不支持Win32 的操作系统上跑一个 Win32
程序,就会获得这个错误讯息。当 Win32 加载器把一个 PE 档映像到内存,内存
映像文件(memory mapped file )的第一个字节对应到 DOS Stub 的第一个字节。好得
很,于是,你每执行一个 Win32 程序,就获得了一个免费的DOS 程序(在Win16 中
这个 DOS stub 并不加载内存)。
和其它的微软可执行档格式一样,你可以在 DOS stub 的表头中找到「真正的表头」。
WINNT.H 为 DOS stub 表头定义了一个结构,循此我们将非常容易找到 PE 表头。
e_lfanew 字段是一个相对偏移值(或说是 RVA ),指向真正的 PE 表头。为了获得指
标,你必须为 RVA 加上 image 的基地址:
// Ignoring typecasts and pointer conversion issues for clarity...
pNTHeader = dosHeader + dosHeader->e_lfanew;
一旦你拥有指向 PE 表头的指针,真正的趣味就开始了。PE 表头整个是个
IMAGE_NT_HEADERS 结构,定义于 WINNT.H 。这个结构正是 Windows 95 的module
database 。每一个被加载的 EXE 或 DLL 都以一个 IMAGE_NT_HEADERS 结构表现出
来。此一结构有一个 DWORD 和两个子结构:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
Signature 字段内容应该是 ASCII 的 PE\0\0 。如果 DOS stub 表头中的 e_lfanew 字段
指向一个 NE Signature 而不是一个 PE Signature ,表示你面对的是一个 Win16 NE 可执
行档。如果是 LE Signature 则表示你面对的是一个 VxD 档。如果是 LX Signature 则表
示 OS/2 文件。
再来是一个IMAGE_FILE_HEADER 结构。此结构内含最基础的文件信息。和 COFF 比
较起来,它似乎没有做什么更改。除了是 PE 表头的一部份,它也出现在微软 32 位
编译器所产生的 COFF OBJs 档的最前端。IMAGE_FILE_HEADER 的字段如下。
WORD Machine
文件使用于哪一种 CPU 。下面是 CPU 识别码的定义:
Intel I386 0x14C
Intel i860 0x14D
MIPS R300 0x162
MIPS R400 0x166
DEC Alpha AXP 0x184
Power PC 0x1F0 (little endian )
Motorola 68000 0x268
PA RISC 0x290 (Precision Architecture )
译注:但是在我的 Visual C++ 4.2 WINNT.H 文件中,只有如下定义:
#define IMAGE_FILE_MACHINE_I386 0x14c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x162 //MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_ALPHA 0x184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x1F0 // IBM PowerPC Little-Endian
WORD NumberOfSections
EXE 的 OBJ 中的 sections 个数
DWORD TimeDateStamp
联结器产生此一文件的时刻。其格式是自从1969 年12 月31 日4:00 P.M. 之后的总秒数。
DWORD PointerToSymbolTable
COFF 符号表格的偏移位置。此字段只对 COFF 除错信息有用。PE 档支持数种除错格
式,所以除错器应该参考 data directory (稍后描述)字段中的
IMAGE_DIRECTORY_ENTRY_DEBUG 。
DWORD NumberOfSymbols
COFF 符号表格中的符号个数。请看前一字段。
WORD SizeOfOptionalHeader
一个可有可无的表头(出现在本结构之后)的大小。在 EXE 档中,这也就是
IMAGE_OPTIONAL_HEADER 的大小。在 OBJ 档中,微软说它总是 0 。然而,观察
KERNEL32.LIB ,有一个 OBJ 在其中,而它的这个值不是 0 。所以呢,微软的话总是要
打点折扣。
WORD Characteristics
描述此一文件的性质。一些比较重要的性质如下:
0x0001 文件中没有重定位(relocation )
0x0002 文件是个可执行档(也就是说不是 OBJ 或 LIB )
0x2000 文件是动态联结函数库,不是程序。
PE 表头的第三个成份是 IMAGE_OPTIONAL_HEADER 结构。对于 PE 档而言,这一
部份其实并不是可有可无。要知道,COFF 格式允许不同的生产者在标准的
IMAGE_FILE_HEADER 之后定义一个结构。IMAGE_OPTIONAL_HEADER 正是 PE 设
计者认为在基本的 IMAGE_FILE_HEADER 信息之外还需要的一些重要信息。
你并不需要知道 IMAGE_OPTIONAL_HEADER 的所有字段。最重要的两个字段是
ImageBase 和 Subsystem 。如果你喜欢,你可以跳着读或是整个略过下面这些字段说明。
WORD Magic
这值用来定义 image 的状态:
0x0107 一个 ROM image
0x010B 一个正常的(一般的)EXE image 。大部份 PE 档都含此值。
BYTE MajorLinkerVersion
BYTE MinorLinkerVersion
产生此 PE 文件之联结器版本。以十进制而非十六进制表示。例如 2.23 版。
DWORD SizeOfCode
所有 code sections 的总和大小。大部份文件只有一个 code section ,所以此字段通常也
就是 .text section 的大小。
DWORD SizeOfInitializedData
所有内含「初始值资料」的sections (但不包括code sections )的总和大小。然而它似乎
并不包括 initialized data sections 在内。
DWORD SizeOfUninitializedData
这是「需要加载器将之付诸虚拟地址空间,但却不占用磁盘文件」的所有 sections 大小
总和。这些 sections 在程序激活时并不需要什么特别内容,所以才导至 Uninitialized data
这一称呼。未初始化的资料通常集中在 .bss section 中。
DWORD AddressOfEntryPoint
这是 image 开始执行的地址。这是一个RVA (Relative Virtual Address ),通常会落在 .text
section 。此字段对于 DLLs 或 EXEs 都适用。
DWORD BaseOfCode
这是一个 RVA (Relative Virtual Address ),表示文件中的 code section 从何开始。code
section 通常在data section 之前,在 PE 表头之后。微软联结器所产生的 EXEs 中,此
值通常为 0x1000 。Borland 的 TLINK32 则通常指定此值为 0x10000 ,因为预设情况下
它是以 64KB 为排列边界,不像微软的联结器采用 4K 为边界。
DWORD BaseOfData
这是一个 RVA (Relative Virtual Address ),表示文件中的 data section 从何开始。data
section 通常在code section 和 PE 表头之后。
DWORD ImageBase
一旦联结器产生了一个可执行档,它就假设这个文件将被映像到特定位置的内存中。
这个字段放的就是内存起始地址。假设出这样一个地址,联结器才能完成最佳化。如
果文件真的被加载到这个地址,程序代码就完全不需要修补。稍后我将对此有更多的讨论。
在 NT 3.1 EXE 中,预设的 image base 是 0x10000 ,DLL 则是 0x400000 。在 Windows
95 ,0x10000 不再适用,因为那将落在一个被所有行程共享的地址空间中。因而到了 NT
3.5 ,微软把预设的 Win32 EXE ImageBase 改为0x400000 。原来那些NT EXE 程序在
Windows 95 之中将需要比较长的加载时间,因为加载器必须重新定位。稍后我会详细讨
论所谓的「重新定位(relocation )」。
DWORD SectionAlignment
一旦映像到内存中,每一个 section 保证从一个「此值之倍数」的虚拟地址开始。
DWORD FileAlignment
在 PE 档中,组成每一个 section 的原始资料(raw data )保证是从一个「此值之倍数」
的虚拟地址开始。默认值是 0x200 ,如此一来 section 总是能够从磁盘的 sector 开始(因
为 sector 长度总是0x200 个字节)。这个字段相当于 NE 档中的 segment/resource
alignment 大小。与 NE 档不同的是,PE 档不会有上百个 sections ,所以因其特定排列
而浪费的空间不大。
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
使用此一可执行文件的子系统的最小版本。典型的数值是 4.0 ,代表 Windows 4.0 ,也就是
Windows 95 。
DWORD Reserved1
似乎总是 0 。
DWORD SizeOfImage
加载器必须在意的整个 image 大小。它的范围从 image base 开始,直到最后一个section
为止。最后一个 section 的尾端必需是 SectionAlignment 的倍数。
DWORD SizeOfHeaders
PE 表头以及 section table 的大小。各个 sections 的资料就从其后展开。
DWORD CheckSum
此文件的一个 CRC checksum 。就像微软的其它可执行档一样,此字段通常被忽略并被
设为 0 。然而,所有的 driver DLLs 、所有在开机时间加载的DLLs 、以及 server DLLs 都
必须有一个合法的 checksum 。其算法可以在 IMAGEHLP.DLL 中获得。
IMAGEHLP.DLL 的原始码可以在 Win32 SDK 中找到。
WORD Subsystem
此一可执行文件用来作为使用者接口的子系统。WINNT.H 定义了下列常数:
NATIVE=1 不需要子系统(例如驱动程序)
WINDOWS_GUI=2 在 Windows GUI 子系统中执行
WINDOWS_CUI=3 在 Windows 字符模式子系统中执行(也就是 console 应用程序)
OS2_CUI=5 在 OS/2 字符模式子系统中执行(也就是 OS/2 1.x 应用程序)
POSIZ_CUI=7 在 Posix 字符模式子系统中执行
WORD DllCharacteristics
一组旗标值,用来指示 DLL 的初始化函数(例如 DllMain )在什么环境下被呼叫。这
个值总是 0 ,但操作系统却还是会在四个 events 发生时呼叫 DLL 的初始化函数。此值
的四个数值分别意义如下:
1 :当 DLL 被加载一个行程的地址空间时呼叫之。
2 :当一个线程结束时呼叫之。
4 :当一个线程开始时呼叫之。
8 :当 DLL 退出时呼叫之。
DWORD SizeOfStackReserve
线程初始堆栈的保留大小。然而并不是所有这些内存都被系统委派(committed )。
此值预设为 0x100000 (1MB )。如果你在程序中呼叫 CreateThread 并指定其堆栈大小
为 0 ,获得的线程就有一个与此值相同大小的的堆栈。
DWORD SizeOfStackCommit
一开始即被委派(committed )给线程初始堆栈的内存数量。微软的联结器预设此一
字段为0x1000 (一个page ),Borland 的TLINK32 则把它设为 0x2000 (两个 pages )。
DWORD SizeOfHeapReserve
保留给最初的 process heap 的虚拟内存数量。这个 heap 的 handle 可以利用
GetProcessHeap 获得。注意,并不是所有这些内存都已被委派(committed )。
DWORD SizeOfHeapCommit
一开始即被委派(committed )给process heap 的内存数量。此值预设为0x1000 个位
组。
DWORD LoaderFlags (在 Windows NT 3.5 中被注明为老旧字段)
这个字段似乎和除错有关。我从来没有看过哪一个可执行文件的这些位是设立的,也不
曾清楚知道联结器如何设定它们。下面是其可能的数值与意义:
1 :在开始这个行程之前先引发一个断点(breakpoint )?
2 :在行程被加载之后引发一个除错器执行起来?
DWORD NumberOfRvaAndSizes
在 DataDirectory (下一字段)数组中的项目个数。目前的工具总是把此值设为 16 。
IMAGE_DATA_DIRECTORY DataDirectory [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
数组一开始的元素内含可执行文件重要部位的RVA (Relative Virtual Address )及大小。阵
列最后的一些元素目前还用不着。数组的第一个元素代表 exported function table (如果
有的话)的地址和大小,第二个元素代表 imported function table 的地址和大小,依此类
推。完整的列表请看 WINNT.H 中的定义。
译注:以下就是完整的列表。
// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // Description String
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // Machine Value (MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
这个数组企图让加载器能够迅速在 image 中找到特定的 section ,不需要一一经过每一
个 sections 、比对名称、再继续寻找...。
大部份的数组元素描述一整个 section 资料,但 IMAGE_DIRECTORY_ENTRY_DEBUG
却只包含 .rdata section 中的一小部份。更多信息将在「The .rdata section 」一节中披露。