PE磁盘文件与内存镜像结构图
PE文件中的DOS部分由MZ格式的文件头和可执行代码部分组成,可执行代码被称为DOS块(DOS stub),MZ格式的文件头由IMAGE_DOS_HEADER
结构定义,在C语言头文件winnt.h
中有对这个DOS结构详细定义,如下所示
typedef struct _IMAGE_DOS_HEADER { WORD e_magic; // DOS的头部 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; // 指向了PE文件的开头(重要) } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
在DOS文件头中,第一个字段e_magic
被定义为MZ
,标志着DOS文件的开头部分,最后一个字段e_lfanew
则指明了PE文件的开头位置,现在来说除了第一个字段和最后一个字段有些用处,其他字段几乎已经废弃了
DOS头的大小是固定的,其大小为0x3Ch
,以winXP
的notepad.exe
为例子解析如下
e_lfanew
字段的值的位置,就是真正的PE文件头的位置,该文件头是由IMAGE_NT_HEADERS
结构定义的,定义结构如下typedef struct _IMAGE_NT_HEADERS { DWORD Signature; // PE文件标识字符 IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
根据DOS解析的例子,在0x3Ch
开始的DWORD
中存储着e_lfanew
字段的值,其值指明了PE头相对于文件的偏移,用于定位PE文件
PE文件头的第一个DWORD
是一个标志,默认情况下它被定义为00004550h
也就是P,E两个字符另外加上两个零,而大部分的文件属性由标志后面的IMAGE_FILE_HEADER
和IMAGE_OPTIONAL_HEADER32
结构来定义
跟进IMAGE_FILE_HEADER
IMAGE_FILE_HEADER
的结构定义如下
typedef struct _IMAGE_FILE_HEADER { WORD Machine; // 运行平台 WORD NumberOfSections; // 文件的节数目 DWORD TimeDateStamp; // 文件创建日期和时间 DWORD PointerToSymbolTable; // 指向符号表(用于调试) DWORD NumberOfSymbols; // 符号表中的符号数量 WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HANDLER32结构的长度 WORD Characteristics; // 文件的属性 exe=010fh dll=210eh } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
跟进IMAGE_OPTIONAL_HEADER32
IMAGE_OPTIONAL_HEADER32
的结构定义如下
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; 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; DWORD SizeOfImage; // 内存中整个PE映像尺寸 DWORD SizeOfHeaders; // 所有头加节表的大小 DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; // 初始化时堆栈大小 DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; // 数据目录的结构数量 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
IMAGE_DATA_DIRECTORY
数据目录列表,它由16
个相同的IMAGE_DATA_DIRECTORY
结构组成,这16
个数据目录结构定义很简单仅仅指出了某种数据的位置和长度,定义如下
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; // 数据起始RVA DWORD Size; // 数据块的长度 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
在执行一个可执行文件时,首先将文件从磁盘原样Copy到内存中,这段存储着和磁盘相同数据的内存称作
FileBuffer
。只是简单的Copy到内存中并不能够执行这个文件,想要运行它,我们还需要对它进行”拉伸“,磁盘存储的方式和运行时的存储方式的对齐大小可能不一样,在磁盘存放的对齐方式可能导致基准地址+偏移地址
的方式并不能正确的找到地址(类似编译器在生成可执行文件时会对其进行压缩,压缩后还以运行时的寻址方式显然不能正确寻址),所以需要通过“拉伸”使文件恢复到可以执行的状态
在磁盘和FileBuffer
时其值完全相同
大致结构如下
在ImageBuffer的存放方式
ImageBuffer
FileBuffer
的对齐大小,再存入内存中ImageBuffer
的起始地址称作ImageBase