PE就是Portable Executable(可移植的执行体)。它是 Win32环境自身所带的执行体文件格式,如:exe文件、DLL 、ocx文件均为PE格式。"portable executable"意味着此文件格式是跨win32平台的 : 即使Windows运行在非Intel的CPU上,任何win32平台的PE装载器都能识别和使用该文件格式。当然,移植到不同的CPU上PE执行体必然得有一些改变。所有 win32执行体 (除了VxD和16位的Dll)都使用PE文件格式,包括NT的内核模式驱动程序(kernel mode drivers)。
PE文件框架构成
DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2
Section ...
Section n
1 DOS Header
PE文件最开始是一个简单的 DOS MZ header,它是一个 IMAGE_DOS_HEADER 结构。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随 MZ Header 之后的 DOS Stub。
2 DOS Stub
DOS Stub 是一个有效的 DOS 程序。当程序在DOS下运时,输出象 "This program cannot be run in DOS mode" 这样的提示。
3 PE Header
紧接着 DOS Stub 的是 PE Header。它是一个 IMAGE_NT_HEADERS 结构。其中包含了很多PE文件被载入内存时需要用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从 DOS MZ header 中找到 PE header 的起始偏移量。
4 Section Table
PE Header 接下来的数组结构 Section Table (节表)。如果PE文件里有5个节,那么此 Section Table 结构数组内就有5个成员,每个成员包含对应节的属性、文件偏移量、虚拟偏移量等。
5 Sections
PE文件的真正内容划分成块,称之为sections(节)。Sections 是以其起始位址来排列,而不是以其字母次序来排列。通过节表提供的信息,
节名 作用
.arch 最初的构建信息(Alpha Architecture Information)
.bss 未经初始化的数据
.CRT C运行期只读数据
.data 已经初始化的数据
.debug 调试信息
.didata 延迟输入文件名表
.edata 导出文件名表
.idata 导入文件名表
.pdata 异常信息(Exception Information)
.rdata 只读的初始化数据
.reloc 重定位表信息
.rsrc 资源
.text .exe或.dll文件的可执行代码
.tls 线程的本地存储器
.xdata 异常处理表
所有常见的PE结构定义在winnt.h头文件中都有。我们一般关心以下几项:
IMAGE_DOS_HEADER
IMAGE_NT_HEADERS
IMAGE_FILE_HEADER
IMAGE_OPTIONAL_HEADER32
IMAGE_SECTION_HEADER
IMAGE_IMPORT_DESCRIPTOR
IMAGE_EXPORT_DIRECTORY
IMAGE_RESOURCE_DIRECTORY
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE Header
WORD e_magic; // "MZ"
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of Header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // 指向PEHeader的RVA
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE文件头标志 :“PE/0/0”。在开始DOS header的偏移3CH处所指向的地址开始
IMAGE_FILE_HEADER FileHeader; //PE文件物理分布的信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //PE文件逻辑分布的信息
}IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //该文件运行所需要的CPU,对于Intel平台是14Ch
WORD NumberOfSections; //文件的节数目
DWORD TimeDateStamp; //文件创建日期和时间
DWORD PointerToSymbolTable; //用于调试
DWORD NumberOfSymbols; //符号表中符号个数
WORD SizeOfOptionalHeader; //OptionalHeader 结构大小
WORD Characteristics; //文件信息标记,区分文件是exe还是dll
}IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; //标志字(总是010bh)
BYTE MajorLinkerVersion; //连接器版本号
BYTE MinorLinkerVersion; //
DWORD SizeOfCode; //代码段大小
DWORD SizeOfInitializedData; //已初始化数据块大小
DWORD SizeOfUninitializedData; //未初始化数据块大小
DWORD AddressOfEntryPoint; //PE装载器准备运行的PE文件的第一个指令的RVA,也就是OEP
DWORD BaseOfCode; //代码段起始RVA
DWORD BaseOfData; //数据段起始RVA
DWORD ImageBase; //PE文件的装载地址
DWORD SectionAlignment; //块对齐
DWORD FileAlignment; //文件块对齐
WORD MajorOperatingSystemVersion; //所需操作系统版本号
WORD MinorOperatingSystemVersion; //
WORD MajorImageVersion; //用户自定义版本号
WORD MinorImageVersion; //
WORD MajorSubsystemVersion; //win32子系统版本。若PE文件是专门为Win32设计的
WORD MinorSubsystemVersion; //该子系统版本必定是4.0否则对话框不会有3维立体感
DWORD Win32VersionValue; //保留
DWORD SizeOfImage; //内存中整个PE映像体的尺寸
DWORD SizeOfHeaders; //所有头+节表的大小
DWORD CheckSum; //此程序的一个CRC 校验和
WORD Subsystem; //NT用来识别PE文件属于哪个子系统
WORD DllCharacteristics; //一组标志位,用来指出dll的初始化函数(例如 DllMain)在什么环境下被调用
DWORD SizeOfStackReserve; // 线程初始堆栈的保留大小
DWORD SizeOfStackCommit; // 一开始就被指定给执行线程初始堆栈的内存数量
DWORD SizeOfHeapReserve; // 保留给最初的进程堆(process heap)的虚拟内存数量
DWORD SizeOfHeapCommit; // 一开始就被指定给进程堆(process heap)的内存数量
DWORD LoaderFlags; // Debug用
DWORD NumberOfRvaAndSizes; // 在DataDirectory(下一个域)数组的成员结构个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //表的RVA地址
DWORD Size; //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text”
union {
DWORD PhysicalAddress; //物理地址
DWORD VirtualSize; //真实长度
} Misc;
DWORD VirtualAddress; //RVA
DWORD SizeOfRawData; //物理长度
DWORD PointerToRawData; //节基于文件的偏移量
DWORD PointerToRelocations; //重定位的偏移
DWORD PointerToLinenumbers; //行号表的偏移
WORD NumberOfRelocations; //重定位项数目
WORD NumberOfLinenumbers; //行号表的数目
DWORD Characteristics; //节属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
RVA 及其相关概念:
RAV(Relative Virual Address)相对虚拟地址,RVA是虚拟空间中到参考点的一段距离。RVA就是类似文件偏移量的东西。当然它是相对虚拟空间里的一个地址,而不是文件头部。举例说明,如果PE文件装入虚拟地址(VA)空间的400000h处,且进程从虚址401000h开始执行,我们可以说进程执行起始地址在RVA 1000h。每个RVA都是相对于模块的起始VA的。虛址(VA)0x401000h - 基址(BA)0x400000h = RVA 0x1464h。基址(Base Address)用来描述被映射到内存中的exe或者dll的起始位置。
PE文件格式使用RVA是为了减少PE装载器的负担,因为每个模块都有可能被重载到任何虚拟地址空间,如果让PE装载器修正每个重定位项,这肯定是个梦魇。相反,如果所有重定位项都使用RVA,那么PE装载器就不必操心那些东西了: 它只要将整个模块重定位到新的起始VA。这就象相对路径和绝对路径的概念: RVA类似相对路径,VA就象绝对路径。