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文件头主要由以下部分组成:

  1. DOS Header
  2. DOS Stub
  3. NT Header
  4. 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:入口点RVA
  • ImageBase:优先装入地址
  • 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:内存中节区RVA
  • SizeOfRawData:文件中节区大小
  • 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. 关键概念总结

  1. PE文件签名检查

    • DOS头签名:0x5A4D ("MZ")
    • PE头签名:0x00004550 ("PE\0\0")
  2. 地址转换

    • RVA (Relative Virtual Address):相对于ImageBase的偏移
    • VA (Virtual Address):实际内存地址 = ImageBase + RVA
    • 文件偏移:通过节区头中的PointerToRawData和VirtualAddress转换
  3. 对齐方式

    • SectionAlignment:内存中对齐粒度(通常4KB)
    • FileAlignment:文件中对齐粒度(通常512B或4KB)
  4. 入口点

    • AddressOfEntryPoint给出的是RVA
    • 实际入口点 = ImageBase + AddressOfEntryPoint
  5. 数据目录

    • 包含16个重要数据结构的RVA和大小
    • 如导入表、导出表、资源表等

通过理解PE文件结构,可以深入分析Windows可执行文件,进行逆向工程、病毒分析、安全研究等工作。

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),主要成员: 关键成员: e_magic :DOS签名,必须为"MZ"(0x5A4D) e_lfanew :指向NT头的偏移量 2.2 DOS Stub 位于DOS头和NT头之间的部分,由链接器写入,可以修改不影响程序运行。 2.3 NT Header (IMAGE_ NT_ HEADERS) NT头结构根据操作系统不同而不同: 32位系统: 64位系统: 2.3.1 File Header (IMAGE_ FILE_ HEADER) 文件头结构体,大小固定为20字节(0x14): 重要成员: Machine :CPU类型标识(如x86为0x14C) NumberOfSections :节区数量 SizeOfOptionalHeader :可选头大小 Characteristics :文件属性标志 2.3.2 Optional Header (IMAGE_ OPTIONAL_ HEADER) 可选头结构体,32位和64位有所不同: 32位(224字节/0xE0): 64位(240字节/0xF0): 关键成员: Magic :标识(32位为0x10B,64位为0x20B) AddressOfEntryPoint :入口点RVA ImageBase :优先装入地址 SectionAlignment :内存中对齐值 FileAlignment :文件中对齐值 SizeOfImage :内存中映像大小 SizeOfHeaders :所有头大小 Subsystem :子系统类型 NumberOfRvaAndSizes :数据目录项数 DataDirectory :数据目录数组 程序真正入口点计算公式: 2.4 Section Header (IMAGE_ SECTION_ HEADER) 节区头结构体,每个40字节(0x28): 重要成员: Name :节区名称(如".text", ".data") VirtualSize :内存中节区大小 VirtualAddress :内存中节区RVA SizeOfRawData :文件中节区大小 PointerToRawData :文件中节区偏移 Characteristics :节区属性标志 3. PE文件解析示例代码 以下C++代码展示了如何解析PE文件头信息: 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可执行文件,进行逆向工程、病毒分析、安全研究等工作。