Windows基础知识-PE结构之初始PE
字数 1737 2025-08-22 18:37:22
PE文件结构详解
1. PE文件概述
PE (Portable Executable) 是Windows操作系统下可执行文件的标准格式,包括:
- 可执行系列:EXE、SCR
- 库系列:DLL、OCX、CPL、DRV
- 驱动程序序列:SYS、VXD
- 对象文件系列:OBJ
PE文件由COFF(UNIX平台下的通用对象文件格式)发展而来,32位称为PE32,64位称为PE+或PE32+。
2. PE文件结构组成
PE文件头主要由以下部分组成:
- DOS Header
- DOS Stub
- NT Header
- Section Header
2.1 DOS Header (IMAGE_DOS_HEADER)
DOS头结构体大小为64字节(0x40),主要成员:
struct _IMAGE_DOS_HEADER {
USHORT e_magic; // 0x0 - DOS签名("MZ"或0x5A4D)
// ... 其他成员 ...
LONG e_lfanew; // 0x3c - 指向NT头的偏移量
};
关键成员:
e_magic:DOS签名,必须为"MZ"(0x5A4D)e_lfanew:指向NT头的偏移量
2.2 DOS Stub
位于DOS头和NT头之间的部分,由链接器写入,可以修改不影响程序运行。
2.3 NT Header (IMAGE_NT_HEADERS)
NT头结构根据操作系统不同而不同:
32位系统:
struct _IMAGE_NT_HEADERS {
ULONG Signature; // 0x0 - PE签名("PE\0\0"或0x00004550)
IMAGE_FILE_HEADER FileHeader; // 0x4
IMAGE_OPTIONAL_HEADER OptionalHeader; // 0x18
};
64位系统:
struct _IMAGE_NT_HEADERS64 {
ULONG Signature; // 0x0
IMAGE_FILE_HEADER FileHeader; // 0x4
IMAGE_OPTIONAL_HEADER64 OptionalHeader; // 0x18
};
2.3.1 File Header (IMAGE_FILE_HEADER)
文件头结构体,大小固定为20字节(0x14):
struct _IMAGE_FILE_HEADER {
USHORT Machine; // 0x0 - CPU类型标识
USHORT NumberOfSections; // 0x2 - 节区数量
// ... 其他成员 ...
USHORT SizeOfOptionalHeader; // 0x10 - 可选头大小
USHORT Characteristics; // 0x12 - 文件属性标志
};
重要成员:
Machine:CPU类型标识(如x86为0x14C)NumberOfSections:节区数量SizeOfOptionalHeader:可选头大小Characteristics:文件属性标志
2.3.2 Optional Header (IMAGE_OPTIONAL_HEADER)
可选头结构体,32位和64位有所不同:
32位(224字节/0xE0):
struct _IMAGE_OPTIONAL_HEADER {
USHORT Magic; // 0x0 - 标识(0x10B)
// ... 标准字段 ...
ULONG AddressOfEntryPoint; // 0x10 - 入口点RVA
ULONG BaseOfCode; // 0x14
ULONG BaseOfData; // 0x18
// NT附加字段
ULONG ImageBase; // 0x1c - 优先装入地址
ULONG SectionAlignment; // 0x20 - 内存中对齐值
ULONG FileAlignment; // 0x24 - 文件中对齐值
// ... 其他成员 ...
ULONG SizeOfImage; // 0x38 - 内存中映像大小
ULONG SizeOfHeaders; // 0x3c - 所有头大小
USHORT Subsystem; // 0x44 - 子系统类型
// ... 其他成员 ...
ULONG NumberOfRvaAndSizes; // 0x5c - 数据目录项数
IMAGE_DATA_DIRECTORY DataDirectory[16]; // 0x60 - 数据目录
};
64位(240字节/0xF0):
struct _IMAGE_OPTIONAL_HEADER64 {
USHORT Magic; // 0x0 - 标识(0x20B)
// ... 类似32位但无BaseOfData ...
ULONGLONG ImageBase; // 0x18
// ... 其他成员类似32位 ...
ULONG NumberOfRvaAndSizes; // 0x6c
IMAGE_DATA_DIRECTORY DataDirectory[16]; // 0x70
};
关键成员:
Magic:标识(32位为0x10B,64位为0x20B)AddressOfEntryPoint:入口点RVAImageBase:优先装入地址SectionAlignment:内存中对齐值FileAlignment:文件中对齐值SizeOfImage:内存中映像大小SizeOfHeaders:所有头大小Subsystem:子系统类型NumberOfRvaAndSizes:数据目录项数DataDirectory:数据目录数组
程序真正入口点计算公式:
入口点地址 = ImageBase + AddressOfEntryPoint
2.4 Section Header (IMAGE_SECTION_HEADER)
节区头结构体,每个40字节(0x28):
struct _IMAGE_SECTION_HEADER {
UCHAR Name[8]; // 0x0 - 节区名称
union {
ULONG PhysicalAddress;
ULONG VirtualSize; // 0x8 - 内存中节区大小
} Misc;
ULONG VirtualAddress; // 0xc - 内存中节区RVA
ULONG SizeOfRawData; // 0x10 - 文件中节区大小
ULONG PointerToRawData; // 0x14 - 文件中节区偏移
// ... 其他成员 ...
ULONG Characteristics; // 0x24 - 节区属性
};
重要成员:
Name:节区名称(如".text", ".data")VirtualSize:内存中节区大小VirtualAddress:内存中节区RVASizeOfRawData:文件中节区大小PointerToRawData:文件中节区偏移Characteristics:节区属性标志
3. PE文件解析示例代码
以下C++代码展示了如何解析PE文件头信息:
#include <iostream>
#include <fstream>
#include <vector>
#include <windows.h>
void CoutPEHeaderInformation(const std::string &filename);
LPVOID ReadPEFile(const std::string &filename);
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "请输入PE文件名" << std::endl;
return 1;
}
std::string filename = argv[1];
CoutPEHeaderInformation(filename);
return 0;
}
void CoutPEHeaderInformation(const std::string &filename) {
LPVOID pFileBuffer = nullptr;
PIMAGE_DOS_HEADER pDosHeader = nullptr;
PIMAGE_NT_HEADERS32 pNTHeader32 = nullptr;
PIMAGE_FILE_HEADER pFileHeader = nullptr;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader32 = nullptr;
PIMAGE_SECTION_HEADER pSectionHeader = nullptr;
DWORD dSectionsCounts = 0;
// 读取PE文件
pFileBuffer = ReadPEFile(filename);
if (!pFileBuffer) {
std::cerr << "读取文件失败" << std::endl;
return;
}
// 检查DOS头
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
std::cerr << "不是有效的MZ标志" << std::endl;
delete[] static_cast<char*>(pFileBuffer);
return;
}
// 检查PE签名
if (*((PWORD)((DWORD_PTR)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE) {
std::cerr << "不是有效的PE标志" << std::endl;
delete[] static_cast<char*>(pFileBuffer);
return;
}
// 获取NT头
pNTHeader32 = (PIMAGE_NT_HEADERS32)((DWORD_PTR)pFileBuffer + pDosHeader->e_lfanew);
// 获取文件头
pFileHeader = &pNTHeader32->FileHeader;
dSectionsCounts = pFileHeader->NumberOfSections;
// 获取可选头
pOptionHeader32 = &pNTHeader32->OptionalHeader;
// 获取节区头
pSectionHeader = (PIMAGE_SECTION_HEADER)(((DWORD_PTR)pOptionHeader32) + pFileHeader->SizeOfOptionalHeader);
// 打印所有节区信息
for (DWORD i = 0; i < dSectionsCounts; i++, pSectionHeader++) {
std::cout << "Section[" << i << "] 名字: "
<< reinterpret_cast<char*>(pSectionHeader->Name) << std::endl;
// 打印其他节区信息...
}
delete[] static_cast<char*>(pFileBuffer);
}
LPVOID ReadPEFile(const std::string &filename) {
std::ifstream filePE(filename, std::ios::binary | std::ios::ate);
if (!filePE.is_open()) {
std::cerr << "无法打开exe文件" << std::endl;
return nullptr;
}
std::streamsize size = filePE.tellg();
filePE.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
if (!filePE.read(buffer.data(), size)) {
std::cerr << "读取数据失败" << std::endl;
filePE.close();
return nullptr;
}
LPVOID pFileBuffer = new char[size];
memcpy(pFileBuffer, buffer.data(), size);
return pFileBuffer;
}
4. 关键概念总结
-
PE文件签名检查:
- DOS头签名:0x5A4D ("MZ")
- PE头签名:0x00004550 ("PE\0\0")
-
地址转换:
- RVA (Relative Virtual Address):相对于ImageBase的偏移
- VA (Virtual Address):实际内存地址 = ImageBase + RVA
- 文件偏移:通过节区头中的PointerToRawData和VirtualAddress转换
-
对齐方式:
- SectionAlignment:内存中对齐粒度(通常4KB)
- FileAlignment:文件中对齐粒度(通常512B或4KB)
-
入口点:
- AddressOfEntryPoint给出的是RVA
- 实际入口点 = ImageBase + AddressOfEntryPoint
-
数据目录:
- 包含16个重要数据结构的RVA和大小
- 如导入表、导出表、资源表等
通过理解PE文件结构,可以深入分析Windows可执行文件,进行逆向工程、病毒分析、安全研究等工作。