PE文件格式是微软Windows NT(New Technology)内核系列系统和 Win32子集中可执行的二进制文件格式。这种文件格式是微软基于COFF 文件格式的设计思想设计的。
COFF (Common Object File Format,通用目标文件格式) 是应用于数种早期UNIX系统中的目标文件和可执行文件的格式。
对应的,在linux下的可执行文件格式采用ELF(Executable and Linkable Format)。
总结:目标文件格式有DOS系统下使用的COM文件格式、和比较复杂的COFF和ELF文件格式。
windows使用COFF的一个变种PE格式,linux现在使用ELF格式。
PE文件由DOS头、DOS加载模块(DOS Stub)、PE头、区段(区段也称:节)表、区段五个部分构成。这里只记录前4个部分!
PE文件简化示意图如下,相比于linux下的ELF文件,可理解为就多出来DOS头和DOS Stub两个部分。
文件偏移地址(File Offset) | 指文件在磁盘上存放时某一地址相对于文件开头的偏移。 |
装载基址(Image Base) | PE文件装入内存时的基地址。一般情况下exe文件的装载基址都为0x00400000,而dll文件一般都是0x10000000,但都是可以更改的。 |
虚拟内存地址(virtual address) | PE文件中某个地址被装入内存后的地址 |
相对虚拟地址(relative virtual address) | 某个地址没有计算装载基址时相对于0的偏移地址 |
虚拟内存地址(VA)=装载基址(Image Base)+相对虚拟地址(RVA)
文件偏移地址=(虚拟内存地址-装载基址)-所在区段(节)的相对虚拟地址
如下为PE文件在磁盘和装入内存对应示意图,由于磁盘和内存中页的对齐大小不一致导致如下效果。
MS-DOS头存在于每个PE文件中,它的存在完全是出于兼容性的考虑。
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; //MZ标志位(也叫魔数),例如0x4D5A表示是一个MS-DOS下的可执行文件
WORD e_cblp; //文件最后一页的字节数
WORD e_cp; //文件的页数
WORD e_crlc; //DOS Stub中重定位项的个数
WORD e_cparhdr; //区段(也叫节)中头部大小
WORD e_minalloc; //最小附加内存要求
WORD e_maxalloc; //最大附加内存要求
WORD e_ss; //初始化SS寄存器
WORD e_sp; //初始化SP寄存器
WORD e_csum; //校验和
WORD e_ip; //初始化IP寄存器
WORD e_cs; //初始化CS寄存器
WORD e_lfarlc; //重定位表的偏移,其实就是指向DOS Stub的偏移
WORD e_ovno; //附加的数量
WORD e_res[4]; //保留他用
WORD e_oemid; //oem相关信息(oem:原始设备制造商)
WORD e_oeminfo; //oem相关信息
WORD e_res2[10]; //保留他用
LONG e_lfanew; //一般指向pe文件头
} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
MS-DOS头后面紧跟着DOS Stub。
下图为DOS头和DOS加载模块示意图,注意x86-windows为小端存储。
PE文件头是windows nt内核下判断可执行文件的唯一有效结构,定义如下。
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE标识
IMAGE_FILE_HEADER FileHeader; //文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展头
} IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;
值始终为0x50450000
IMAGE_FILE_HEADER是映像文件头,包含整个PE文件的概览信息。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //运行的平台
WORD NumberOfSections; //节(区段)的数量
DWORD TimeDateStamp; //文件创建时间
DWORD PointerToSymbolTable; //指向COFF符号表偏移的指针
DWORD NumberOfSymbols; //符号表中符号的数量
WORD SizeOfOptionalHeader; //扩展头的大小
WORD Characteristics; //文件属性类型(exe\dll\...)
} IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;
IMAGE_OPTIONAL_HEADER32是映像扩展头,对PE文件进行更详细的属性设定。
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion; //链接器主版本号
BYTE MinorLinkerVersion; //链接器此版本号
DWORD SizeOfCode; //所有IMAGE_SCN_CNT_CODE属性的区段总的大小
DWORD SizeOfInitializedData; //已经初始化数据块大小
DWORD SizeOfUninitializedData; //未初始化区段(.bss节)大小
DWORD AddressOfEntryPoint; //程序执行入口的RAV
DWORD BaseOfCode; //代码段起始RAV
DWORD BaseOfData; //数据段起始RAV
DWORD ImageBase; //文件装入内存时的首选装入地址
DWORD SectionAlignment; //文件装入内存时的对齐大小
DWORD FileAlignment; //文件在磁盘上对齐大小
WORD MajorOperatingSystemVersion; //要求操作系统最低主版本号
WORD MinorOperatingSystemVersion; //要求操作系统最低次版本号
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; //文件装入内存后的总大小(从ImageBase到最后)
DWORD SizeOfHeaders; //MS-DOS头、pe头、区块(节)表大小的和
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;
其中的变量DataDirectory
是PE文件中各种数据结构的索引目录,由多个IMAGE_DATA_DIRECTORY
结构体组成。
IMAGE_DATA_DIRECTORY就是上面PE头中IMAGE_OPTIONAL_HEADER32
字段结构体中变量DataDirectory
的成员类型。在此用来指示各种数据结构的索引目录。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据块起始RVA地址
DWORD Size; //数据块大小
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
数据目录表在win32 SDK(软件开发包)中定义了一些不同成员的信息,如下。
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 //导出表目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 //导入表目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 //资源目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 //异常目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 //安全目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 //基址重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 //调试目录
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 //版权信息
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 //全局指针
#define IMAGE_DIRECTORY_ENTRY_TLS 9 //线程局部存储器目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 //载入配置目录
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 //绑定导入表
#define IMAGE_DIRECTORY_ENTRY_IAT 12 //导入地址表,保存导入函数真正地址
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 //延迟导入描述,用于dll
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 //com运行时描述目录
PE文件头的数据目录表后是区段表,区段表用来描述位于其后各个区段的各种属性。
PE文件最少要有一个区段才能被加载运行。
区段表是由数个首尾相连的IMAGE_SECTION_HEADER结构体数组构成的,
可以使用IMAGE_FIRST_SECTION32(NtHeader)这个宏找到第一个区段表所在的位置。
IMAGE_SECTION_HEADER结构里包含了可以详细描述该区段(节)属性的字段信息,例如区段名、长度、属性等内容。定义如下。
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //区段(节)名
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; //区段的RAV地址
DWORD SizeOfRawData; //文件中区段对齐大小
DWORD PointerToRawData; //区段在文件中的偏移
DWORD PointerToRelocations; //重定位偏移(用于obj文件)
DWORD PointerToLinenumbers; //行号表拍内衣(用于调试)
WORD NumberOfRelocations; //重定位表中项的个数
WORD NumberOfLinenumbers; //行号表中项的个数
DWORD Characteristics; //区段的属性,描述读写情况
} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
ELF文件再往后的内容就是根据区段头表描述的地址上填充的相应的区段(节)的具体信息。