Windows PE文件结构

Windows PE文件结构

Dos头

NT头

PE签名

PE文件头

PE可选头

数据目录表

节表

IMAGE_SECTION_HEADER → .text

IMAGE_SECTION_HEADER → .data

IMAGE_SECTION_HEADER → .edata

IMAGE_SECTION_HEADER → .reloc

…​.

.text

.data

.edata

.reloc

…​.

Dos头

Dos头中目前有意义的值只有两个

c
typedef struct _IMAGE_DOS_HEADER {
    0x00  WORD   e_magic;                    // 魔术数字
    0x3c  LONG   e_lfanew;                   // NT头地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

直接通过 e_lfanew 寻址到NT头, 其他数据无意义

NT头

c
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                            // PE签名
    IMAGE_FILE_HEADER FileHeader;               // PE文件头
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;     // PE可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

标准PE头

包含文件基础信息

c
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;                  // 节数量
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;              // PE可选头大小
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

扩展PE头

包含程序运行时详细信息, 例如入口点, 基地址

c
typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;                            // 魔术数字
    BYTE        MajorLinkerVersion;               // 链接器主版本号
    BYTE        MinorLinkerVersion;               // 链接器次版本号
    DWORD       SizeOfCode;                       // 代码节大小
    DWORD       SizeOfInitializedData;            // 已初始化的数据节大小
    DWORD       SizeOfUninitializedData;          // 未初始化的数据节大小
    DWORD       AddressOfEntryPoint;              // 入口点
    DWORD       BaseOfCode;                       // 指向代码部分开头(相对于镜像基)的指针。
    ULONGLONG   ImageBase;                        // 镜像基地址
    DWORD       SectionAlignment;                 // 内存中加载的节的对齐方式
    DWORD       FileAlignment;                    // 镜像文件中各部分的原始数据的对齐方式
    WORD        MajorOperatingSystemVersion;
    WORD        MinorOperatingSystemVersion;
    WORD        MajorImageVersion;
    WORD        MinorImageVersion;
    WORD        MajorSubsystemVersion;
    WORD        MinorSubsystemVersion;
    DWORD       Win32VersionValue;
    DWORD       SizeOfImage;                       // 镜像的大小
    DWORD       SizeOfHeaders;                     // 头部大小
    DWORD       CheckSum;                          // 校验和
    WORD        Subsystem;
    WORD        DllCharacteristics;
    ULONGLONG   SizeOfStackReserve;                // 要为堆栈保留的字节数
    ULONGLONG   SizeOfStackCommit;                 // 要为堆栈提交的字节数
    ULONGLONG   SizeOfHeapReserve;                 // 要为堆保留的字节数
    ULONGLONG   SizeOfHeapCommit;                  // 要为堆提交的字节数
    DWORD       LoaderFlags;                       // 加载器标志
    DWORD       NumberOfRvaAndSizes;               // 数据目录表的项数
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

重要字段:

字段描述备注

Magic

魔术数字

32位为 0x10B, 64位为 0x20B

SizeOfCode

代码节大小

如果有多个已初始化的数据节,则为所有此类节的总和

SizeOfInitializedData

已初始化的数据节大小

如果有多个已初始化的数据节,则为所有此类节的总和

SizeOfUninitializedData

未初始化的数据节大小

如果有多个未初始化的数据节,则为所有此类节的总和

AddressOfEntryPoint

入口点

对于可执行文件,这是起始地址。 对于设备驱动程序,这是初始化函数的地址。DLL可选, 没有为0, 要与ImageBase相加得到实际地址

BaseOfCode

代码节基址

指向代码部分开头(相对于镜像基)的指针。

ImageBase

镜像基址

DLL 的默认值为 0x10000000。 应用程序的默认值为 0x00400000

SectionAlignment

内存中加载的节的对齐方式

FileAlignment

镜像文件中各部分的原始数据的对齐方式

SizeOfImage

镜像的大小

包括所有标头。 必须是 SectionAlignment 的倍数。在内存中整个PE文件映射的大小

SizeOfHeaders

头部大小

以下项的组合大小,舍入为 FileAlignment 成员中指定的值的倍数。 1. IMAGE_DOS_HEADER 的e_lfanew成员 2. 4 字节签名 3. IMAGE_FILE_HEADER 的大小 4. 可选标头的大小 5. 所有节标题的大小

CheckSum

校验和

以下文件在加载时进行验证:所有驱动程序、在启动时加载的任何 DLL,以及加载到关键系统进程中的任何 DLL。

NumberOfRvaAndSizes

数据目录表的项数

可选标头的其余部分的目录条目数。 每个条目描述位置和大小。

DataDirectory

数据目录表

指向数据目录中第一个 IMAGE_DATA_DIRECTORY 结构的指针。

节表

节表的数量为NT头中 NumberOfSections 字段, 每个节表的结构如下

c
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];          // 节名称, 8字节
    union {
        DWORD   PhysicalAddress;
        DWORD   VirtualSize;                        // 节在内存中的大小
    } Misc;
    DWORD   VirtualAddress;                         // 节在内存中的地址
    DWORD   SizeOfRawData;                          // 节在硬盘中的大小, 一般经过对齐, 比VirtualSize大
    DWORD   PointerToRawData;                       // 节在硬盘中的偏移
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

数据目录表里的 VirtualAddress 是按照内存中的偏移来算的, 加载到内存中需要做如下转换:

c
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++, pSectionHeader++)
{
    // 源地址:文件缓冲区 + 节的原始数据偏移
    LPVOID pSrc = (BYTE*)pFileBuffer + pSectionHeader->PointerToRawData;
    // 目标地址:加载基址 + 节的虚拟地址
    LPVOID pDest = (BYTE*)pLoadedBase + pSectionHeader->VirtualAddress;
    // 复制节数据(SizeOfRawData是文件中实际的字节数)
    memcpy(pDest, pSrc, pSectionHeader->SizeOfRawData);
}

如果不映射到内存中, 后续如果要读取文件中的节数据, 则需要做如下转换:

plain
文件偏移 = 节的PointerToRawData + (目标 RVA - 节的VirtualAddress)
c
size_t RvaToFileOffset(IMAGE_NT_HEADERS* ntHeaders, DWORD rva)
{
    const IMAGE_SECTION_HEADER* sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
    for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i, ++sectionHeader)
    {
        DWORD sectionVA = sectionHeader->VirtualAddress;
        DWORD sectionSize = sectionHeader->Misc.VirtualSize;
        if (rva >= sectionVA && rva < sectionVA + sectionSize)
        {
            return rva - sectionVA + sectionHeader->PointerToRawData;
        }
    }
    return 0;
}

数据目录表

数据目录表共有16中条目, 其中常见的如下:

字段描述备注

0

IMAGE_DIRECTORY_ENTRY_EXPORT

导出表

1

IMAGE_DIRECTORY_ENTRY_IMPORT

导入表

2

IMAGE_DIRECTORY_ENTRY_RESOURCE

资源表

图标, 菜单, 图片等信息

5

IMAGE_DIRECTORY_ENTRY_BASERELOC

基址重定位表

9

IMAGE_DIRECTORY_ENTRY_TLS

TLS 表

线程本地存储表, 比入口点更早执行

12

IMAGE_DIRECTORY_ENTRY_IAT

导入地址表

导入表的地址表

13

IMAGE_DIRECTORY_ENTRY_DELAYIMPORT

延迟导入表

延迟导入表

c
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress;                          // RVA: 数据在内存中的地址
    DWORD Size;                                    // 数据大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

在解析数据目录表时, 通过 VirtualAddress 寻址到节表的对应范围

plain
DataDirectory [0] VirtualAddress:0x5D60, size:608   // 导出表
DataDirectory [1] VirtualAddress:0x5FC0, size:460   // 导入表
Section 2: Name=.rdata, VirtualAddress=0x5000, VirtualSize=0x1CE6, SizeOfRawData=0x2000, PointerToRawData=0x5000

可以看到 .rdata 的地址范围为 0x5000~0x6CE6, 即导入导出表都落到了 .rdata 节中

导出表

导出表位置位于 DataDirectory[0].VirtualAddress

导出表的结构体如下

c
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

其重要字段定义如下

字段描述备注

Base

基址

函数编号的起始id

NumberOfFunctions

函数数量

包含 命名函数 + 匿名函数

NumberOfNames

名称数量

命名函数数量

AddressOfFunctions

函数RVA地址表

AddressOfNames

函数名称RVA地址表

AddressOfNameOrdinals

函数名称序号表

AddressOfFunctions 里既有命名函数, 也有匿名函数

AddressOfNameOrdinals 里只有命名函数, 其里存的值是 AddressOfFunctions 的索引

所以遍历NumberOfNames的时候要先从AddressOfNameOrdinals里获取到命名函数的实际序号, 然后从AddressOfFunctions里获取到函数地址

c
for (size_t i = 0; i < exportTable->NumberOfNames; ++i)
{
    WORD ordinal = AddressOfNameOrdinals[i];  // 当前命名函数的序号
    DWORD funcNameRVA = AddressOfNames[i];  // 当前函数名的RVA

    DWORD funcRVA = AddressOfFunctions[ordinal];  // 当前函数地址的RVA

    DWORD funcNameFileOffset = RvaToFileOffset(ntHeaders, funcNameRVA);
    std::string funcName = dll.at<char>(funcNameFileOffset);

    std::println("序号: {}, 函数名: {}, RVA: 0x{:X}", ordinal + exportTable->Base, funcName, funcRVA);
}

导入表

导入表位置位于 DataDirectory[1].VirtualAddress 处, 内存为一个结构体数组

c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
字段描述备注

Characteristics

导入表结束标志

0表示导入表结束

OriginalFirstThunk

RVA指向INT表

Name

RVA 指向Dll名称

FirstThunk

RVA指向IAT表

IAT表与INT表

IAT表与INT表是两个不同的表, 但是它们的内容是相同的, 都是指向导入函数的地址表

OriginalFirstThunk(INT) 为只读表, 存储原始导入函数名, 可以找到函数的名称或函数的编号

FirstThunk(IAT) 在加载时会被替换为实际函数地址, 可以找到函数指令代码在内存空间中的实际地址

其对应的结构体定义如下:

c
typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString;  // PBYTE
        ULONGLONG Function;         // PDWORD
        ULONGLONG Ordinal;
        ULONGLONG AddressOfData;    // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA64;

其重要字段定义如下

字段描述备注

Function

函数地址

为0表示结束, 在IAT表中它将被装在为函数实际内存地址

Ordinal

函数序号

按序号导入时的序号值 (最高位为1表示序号, 否则为名称, 可以用IMAGE_ORDINAL宏判断)

AddressOfData

函数名称

IMAGE_IMPORT_BY_NAME RVA, 使用名称导入时使用

c
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

其重要字段定义如下

字段描述备注

Hint

序号

可以使用序号导入, 但是名称导入更稳定

Name

函数名称

函数名称


Windows PE文件结构
https://simonkimi.githubio.io/2026/02/26/Windows-PE文件结构/
作者
simonkimi
发布于
2026年2月26日
许可协议