PE文件系统简介
1 什么是PE文件系统
PE文件是Windows操作系统下使用的可执行文件格式。 它是微软在UNIX平台的COFF(Common Object File Format, 通用对象文件格式)基础上制作而成的。 最初设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在Windows系列的操作系统下。
PE文件是指32位的可执行文件, 也称为PE32 。64位的可执行文件称为PE+或PE32+, 是PE (PE32)文件的一种扩展形式。PE文件种类如下:

2 PE文件的基本结构
PE文件结构如图:

2.1 DOS头 & DOS存根
微软创建PE文件格式时,人们正广泛使用DOS文件,所以微软充分考虑了PE文件对DOS文件的兼容性。所以在PE头的最前面添加了一个IMAGE_DOS_HEADE
结构体,用来扩展已有的DOSEXE头,DOS头和存根一共256字节。
typedef struct _IMAGE_DOS_HEADER
{
WORD e_magic;
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
IMAGE_DOS_HEADER
结构体的大小为64个字节。在该结构体中必须知道2个重要成员:
e_magic
:DOS签名(signature, 4D5A=>ASCIJ值”MZ”)e_lfanew
:指示NT头的偏移(根据不同文件拥有可变值)
DOS存根(stub)在DOS头下方,是个可选项,且大小不固定(一般为192字节即使没有DOS存根,文件也能正常运行)。DOS存根由代码与数据混合而成,代码非常简单,在画面中输出字符串Thisprogram cannot be run in DOS mode
后就退出。
2.2 NT头
IMAGE_NT_HEADERS
结构体由3个成员组成:
- 签名(Signature):其值为
50450000h("PE" 00)
- 文件头(FileHeader)结构体,20字节
- 可选头(OptionalHeader) 结构体,224字节
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //4个字节的PE标志
IMAGE_FILE_HEADER FileHeader; //文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
2.2.1 NT文件头
文件头是表现文件大致属性的IMAGE_FILE _HEADEfil结构体,20字节。
typedef struct _IMAGE_FILE_HEADER
{
WORD Machine; // 运行平台
WORD NumberOfSections; // 文件的区块数目
DWORD TimeDateStamp; // 文件创建日期和时间
DWORD PointerToSymbolTable; // 指向符号表(主要用于调试)
DWORD NumberOfSymbols; // 符号表中符号个数(同上)
WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32 结构大小
WORD Characteristics; // 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
IMAGE_ FILE_ HEADERS
结构体中有如下4种重要成员(若它们设置不正确, 将导致文件无
法正常运行),参考自MS官方文档
- Machine:每个CPU都拥有唯一的Machine码, 兼容32位Intel x86芯片的Machine码为
14C
。 - NumberOfSections:前面提到过 , PE文件把代码、 数据、 资源等依据属性分类到各节区中存储。
NumberOfSections
用来指出文件中存在的节区(section)数量。 该值一定要大于0, 且当定义量与实际节区不同时, 将发生运行错误。 - SizeOfOptionalHeader:
SizeOfOptionlHeader
成员用来指出IMAGE_OPTIONAL_ HEADER32
结构体的长度。IMAGE_ OPTIONAL HEADER32
结构体由C语言编写而成, 故其大小已经确定。 但是Windows的PE装载器需要查看IMAGE_FILE_HEADER
的SizeOfOptionlHeader
值,从而识别出IMAGE_OPTIONAL_ HEADER32
结构体的大小。 - Characteristics:该字段用于标识文件的属性, 文件是否是可运行的形态、 是否为DLL文件等信息, 是将以下信息按位或运算的结果。
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
PE32+格式的文件中使用的是
IMAGE_OPTIONAL_HEADER64
结构体, 而不是IMAGE_ OPTIONAL_ HEADER32
结构体。 2个结构体的尺寸是不同的, 所以需要在SizeOfOptionalHeader
成员中明确指出结构体的大小。
2.2.2 NT可选头
IMAGE_OPTIONAL_ HEADER32
是PE头结构体中最大的,有224字节。
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
typedef struct _IMAGE_OPTIONAL_HEADER
{
WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
BYTE MajorLinkerVersion; // 链接程序的主版本号
BYTE MinorLinkerVersion; // 链接程序的次版本号
DWORD SizeOfCode; // 所有含代码的节的总大小
DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
DWORD AddressOfEntryPoint; // 程序执行入口RVA
DWORD BaseOfCode; // 代码的区块的起始RVA
DWORD BaseOfData; // 数据的区块的起始RVA
DWORD ImageBase; // 程序的首选装载地址
DWORD SectionAlignment; // 内存中的区块的对齐大小
DWORD FileAlignment; // 文件中的区块的对齐大小
WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
WORD MajorImageVersion; // 可运行于操作系统的主版本号
WORD MinorImageVersion; // 可运行于操作系统的次版本号
WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
DWORD SizeOfImage; // 映像装入内存后的总尺寸
DWORD SizeOfHeaders; // 所有头+ 区块表的尺寸大小
DWORD CheckSum; // 映像的校检和
WORD Subsystem; // 可执行文件期望的子系统
WORD DllCharacteristics; // DLL文件特征属性设置(参考《PE文件权威指南》3.5.3小节)
DWORD SizeOfStackReserve; // 初始化时的栈大小
DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
DWORD LoaderFlags; // 与调试有关,默认为0
DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表 IMAGE_NUMBEROF_DIRECTORY_ENTRIES =16
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
- AddressOfEntryPoint:程序入口点地址(EP),指向程序最先执行的代码的地址
- ImageBase: 文件被加载到内存中时文件的优先装入地址。执行PE文件时, PE装载器先创建进程, 再将文件载入内存, 然后把EIP寄存器的值设置
ImageBase+ AddressOfEntryPoint
- DataDirectory:是
IMAGE_DATA_DIRECTORY
结构体数组, 数组的每项都有被定义的值。下面列出了各数组项的作用
//结构体数组
DataDirectory[0] //导出表地址和大小,最常用表
DataDirectory[1] //导入表格地址和大小,最常用表
DataDirectory[2] //资源表地址和大小
DataDirectory[3] //异常表地址和大小
DataDirectory[4] //证书表地址和大小
DataDirectory[5] //基址重定位表的地址和大小
DataDirectory[6] //调试信息的起始地址和大小
DataDirectory[7] //特定于体系结构的数据地址和大小
DataDirectory[8] //全局指针寄存器相对虚拟地址
DataDirectory[9] //线程本地存储(TLS)表的地址和大小
DataDirectory[10] //加载配置表地址和大小
DataDirectory[11] //绑定导入表的地址和大小
DataDirectory[12] //导入地址表地址和大小
DataDirectory[13] //延迟导入描述符地址和大小
DataDirectory[14] //CLR头地址和大小
DataDirectory[15] //保留的目录
2.2.3 数据目录项
NT可选头最后一个成员为数据目录项(DataDirectory),该字段定义了 PE 文件中出现的所有不同类型的数据的目录信息。应用程序中的数据被按照用途分成很多种类, 如导出表、导入表、资源、重定位表等。
在内存中, 这些数据被操作系统以页为单位组织起来, 并赋以不同的访问属性:在文件中, 这些数据也同样被组织起来, 按照不同类别分别存放在文件的指定位置。该结构就是用来描述这些不同类别的数据在文件(和内存)中的位置及大小的。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据起始RAV
DWORD Size; //数据块长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
如果想在PE 文件中寻找特定类型的数据, 就需要从该结构开始。比如,要想查看PE 中都调用了哪些动态链接库的函数, 则需要从数据目录表的第2 个元素(数组编号为I)的IMAGE_DATA_DIRECTORY结构获取导人表在文件中的起始位置和大小, 然后再根据VirtuaLAddress_ I地址指向的位置找到导入表相关的字节码。
2.3 节区头
PE文件中的code (代码)、 data (数据)、 resource (资源)等按照属性分类存储在不同节区, 之所以这样做是因为这样可以保证程序的安全性。若把code
与data
放在一个节区中很容易引发安全问题。假如向字符串data写数据时,由于某个原因导致溢出(输入超过缓冲区大小时),那么其下的 code (指令)就会被覆盖,应用程序就会崩溃。 因此, PE文件格式的设计者们决定把具有相似属性的数据统一保存在一个被称为 "节区” 的地方, 然后需要把各节区属性记录在节区头中(节区 属性中有文件/内存的起始位置、 大小、访问权限等)。每个节区40字节。

#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8个字节的节区名称
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //加载到内存中的节区总大小
} Misc;
DWORD VirtualAddress; // 节区的 RVA 地址
DWORD SizeOfRawData; // 磁盘上初始化数据的大小
DWORD PointerToRawData; // 在文件中节区的起始位置
DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)
WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers; // 行号表中行号的数目
DWORD Characteristics; // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
2.4 PE文件的内存映射
PE文件加载到内存时, 每个节区都要能准确完成 文件偏移间 与内存地址之间的映射。 这种映射一般称为RAW TO RVA
,如下图所示

内存映射中的几个概念
- ImageBase:PE文件加载到内存中的起始地址
- VA:虚拟内存地址
- RVA:相对虚拟内存地址
- FOA:文件偏移地址,在文件内的偏移地址
- FileAlignment:在
IMAGE_OPTIONAL_ HEADER32
中,PE文件中区块对齐大小,为200h
- SectionAlignment:在
IMAGE_OPTIONAL_ HEADER32
中,虚拟内存中区块对齐大小,为1000h
RVA to RAW 的一般流程
- 判断相应的RVA所在节区(section)
- 找到该节的起始RVA,SRVA =
IMAGE_SECTION_HEADER.VirtualAddress
- 求出节内偏移量Off,Off = SRVA – RVA
- 找到该节的起始FOA,SFOA =
IMAGE_SECTION_HEADER.PointerToRawData
- 计算FOA,FOA = SFOA + Off
例如,在”.text”节区有文件偏移地址 FileOff
- 算出实际
Off = FileOff - IMAGE_SECTION_HEADER.PointerToRawData
- 计算
RVA = IMAGE_SECTION_HEADER.VirtualAddress + Off
- 之后可以根据
ImageBase
算出在内存中的地址
讲解PE文件时经常出现 映像(Image)这一术语,PE文件加载到内存时, 文件不会原封不动地加载, 而要根据节区头中定义的节区起始地址、 节区大小等加载。 因此, 磁盘文件中的PE与内存中的PE具有不同形态。 将装载到内存中的形态称为映像以示区别, 使用这一术语能够很好地区分二者。
2.5 节区
程序中常见节区名和作用
.text
:包含了CPU执行指令,一般来说, 这是唯一可以执行的节, 也应该是唯一包含代码的节.rdata
:read only data
,包含导入与导出函数信息,与Dependency Walker工具获得的信息相同;还可以存储程序所使用的其他只读数据,有些文件中还会包含.idata
(import data)和.edata
(export data)节, 来存储导入导出信息(见下表)。.data
:包含了程序的全局数据,可以从程序的任何地方访问到。.rsrc
:包含可执行文件所使用的资源,而这些内容并不是可执行的,比如图标、图片、菜单项和字符串等。
Dependency Walker:包含在微软Visual Studio的一些版本与其他微软开发包中,支待列出可执行文件的动态链接函数。下载地址:http://www.dependencywalker.com/

3 64位PE文件系统
为了保持向下兼容性,PE32+在原32位PE文件( PE32)的基础上扩展而来,PE32+使用IMAGE_NT_HEADER64
结构体, 而PE32使用的是IMAGE_NT_HEADER32
。这2种结构体的区别在于第三个成员, 前者为IMAGE_OPTIONAL_HEADER64
, 后者为IMAGE_OPTIONAL_HEADER32
。
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader; //64位可选头
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
文件头
PE32+中IMAGE_FILE_HEADER
结构体的Machine字段值发生变化。
- PE32文件中该Machine的值固定为
0x014C
- PE32+文件的Machine值为
0x8664
可选头
PE32+ 中变化最大的部分就是IMAGE_OPTIONAL_HEADER
结构体。
Magic
字段值发生了改变,PE32中Magic值为0x010b, PE32+中Magic值为0x020bBaseOfData
:PE32文件中该字段用于指示数据节的起始地址(RVA), 而PE32+文件中删除了该字段。ImageBase
字段的数据类型由原来的双字(DWORD
)变为ULONGLONG
类型( 8个字节)。这是为了适应增大的进程虚拟内存。- 栈和堆相关的字段(
SizeOftackReserve
、SizeOfStackCommit
、SizeOfHeapReserve
SizeOfHeapCommt
)的数据类型变为ULONGLONG
类型(8个字节)。这样做也是为了与增大的 进程虚拟内存相适应。
AddressOfEntryPoint
、SizeOfhnage
等字段大小与原PE32位是一样的,都是DWORD
大小(4个字节, 32位)。这些字段的数据类型都是DWORD
意味着PE32+格式的文件占用的实际虚拟内存中, 各映像的大小最大为4GB (32位)。但是由于ImageBase
的大小为8个宇节(64位), 程序文件可以加栽到进程虚拟内存中的任意地址位。