目前网络上有关PE文件结构说明的文章太多了,本身的这篇文章只是单纯的记录本身对PE文件结构的学习、理解和总结。windows
PE(Portable Executable:可移植的执行体)是Win32环境自身所带的可执行文件格式。它的一些特性继承自Unix的Coff(Common Object File Format)文件格式。可移植的执行体意味着此文件格式是跨win32平台的,即便Windows运行在非Intel的CPU上,任何win32平台的PE装载器都能识别和使用该文件格式。固然,移植到不一样的CPU上PE执行体必然得有一些改变。除VxD和16位的Dll外,全部 win32执行文件都使用PE文件格式。所以,研究PE文件格式是咱们洞悉Windows结构的良机。数组
图表结构:网络
DOS头是用来兼容MS-DOS操做系统的
NT头包含windows PE文件的主要信息
节表:是PE文件后续节的描述
节:每一个节其实是一个容器,能够包含代码、数据等等,每一个节能够有独立的内存权限,好比代码节默认有读/执行权限,节的名字和数量能够本身定义编辑器
一、PE文件在硬盘上和在内存里是不彻底同样的,被加载到内存之后其占用的虚拟地址空间要比在硬盘上占用的空间大一些,这是由于各个节在硬盘上是连续的,而在内存中是按页对齐的。ide
二、PE结构内部,表示某个位置的地址采用了两种方式,针对在硬盘上存储文件中的地址,称为原始存储地址或物理地址表示距离文件头的偏移;另一种是针对加载到内存之后映象中的地址,称为相对虚拟地址(RVA),表示相对内存映象头的偏移。函数
三、CPU的某些指令是须要使用绝对地址的,好比取全局变量的地址,传递函数的地址编译之后的汇编指令中确定须要用到绝对地址而不是相对映象头的偏移,所以PE文件会建议操做系统将其加载到某个内存地址(这个叫基地址),这种表示方式叫作虚拟地址(VA)学习
四、PE文件没法加载到预期的地址,那么系统会帮他从新选择一个合适的基地址将他加载到此处,这时原有的VA就所有失效了,NT头保存了PE文件加载所需的信息,在不知道PE会加载到哪一个基地址以前,VA是无效的,因此在PE文件头中大部分是使用RVA来表示地址的spa
一、PE文件能够导出函数让其余的PE文件使用,也能够从其余PE文件导入函数操作系统
二、PE文件经过导出表指明本身导出那些函数,经过导入表指明须要从哪些模块导入哪些函数。线程
三、DOS头和NT头就是PE文件中两个重要的文件头
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number 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; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
重点关注字段
e_magic:一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位‘MZ’,可执行文件必须都是'MZ'开头。
e_lfanew:为32位可执行文件扩展的域,用来表示DOS头以后的NT头相对文件起始地址的偏移。
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature:相似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是'PE‘。
IMAGE_FILE_HEADER是PE文件头
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine:该文件的运行平台,是x8六、x64仍是I64
NumberOfSections:该PE文件中有多少个节,也就是节表中的项数。
TimeDateStamp:PE文件的建立时间,通常有链接器填写。
PointerToSymbolTable:COFF文件符号表在文件中的偏移。
NumberOfSymbols:符号表的数量。
SizeOfOptionalHeader:紧随其后的可选头的大小。
Characteristics:可执行文件的属性,能够是下面这些值按位相或。
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD 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; 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;
AddressOfEntryPoint:程序入口的RVA,对于exe这个地址能够理解为WinMain的RVA。对于DLL,这个地址能够理解为DllMain的RVA,若是是驱动程序,能够理解为DriverEntry的RVA。固然,实际上入口点并不是是WinMain,DllMain和DriverEntry,在这些函数以前还有一系列初始化要完成,固然,这些不是本文的重点。
BaseOfCode:代码段起始地址的RVA。
BaseOfData:数据段起始地址的RVA。
ImageBase:映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来讲,若是没法加载到这个地址,系统会自动为其选择地址。
SectionAlignment:节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,好比这个值是0x1000,那么每一个节的起始地址的低12位都为0。
FileAlignment:节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。
SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。
SizeOfHeaders:全部文件头(包括节表)的大小,这个值是以FileAlignment对齐的。
CheckSum:映象文件的校验和。
SizeOfStackReserve:运行时为每一个线程栈保留内存的大小。
SizeOfStackCommit:运行时每一个线程栈初始占用内存大小。
SizeOfHeapReserve:运行时为进程堆保留内存大小。
SizeOfHeapCommit:运行时进程堆初始占用内存大小。
NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数
DataDirectory:数据目录,这是一个数组,数组的项定义以下:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
DataDirectory数据目录
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory // IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP #define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
导出表是用来描述模块中的导出函数的结构,若是一个模块导出了函数,那么这个函数会被记录在导出表中,这样经过GetProcAddress函数就能动态获取到函数的地址。函数导出的方式有两种,一种是按名字导出,一种是按序号导出。这两种导出方式在导出表中的描述方式也不相同。
导出表定义:
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;
图表:
IMAGE_DIRECTORY_ENTRY_IMPORT就是导入表,在PE文件加载时,会根据这个表里的内容加载依赖的DLL,并填充所需函数的地址
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT叫作绑定导入表,在第一种导入表导入地址的修正是在PE加载时完成,若是一个PE文件导入的DLL或者函数多那么加载起来就会略显的慢一些,因此出现了绑定导入,在加载之前就修正了导入表,这样就会快一些。
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT叫作延迟导入表,一个PE文件也许提供了不少功能,也导入了不少其余DLL,可是并不是每次加载都会用到它提供的全部功能,也不必定会用到它须要导入的全部DLL,所以延迟导入就出现了,只有在一个PE文件真正用到须要的DLL,这个DLL才会被加载,甚至于只有真正使用某个导入函数,这个函数地址才会被修正。
IMAGE_DIRECTORY_ENTRY_IAT是导入地址表,前面的三个表实际上是导入函数的描述,真正的函数地址是被填充在导入地址表中的。
Windows使用重定位机制保证代码不管模块加载到哪一个基址都能正确被调用。
编译的时候由编译器识别出哪些项使用了模块内的直接VA,好比push一个全局变量、函数地址,这些指令的操做数在模块加载的时候就须要被重定位。
连接器生成PE文件的时候将编译器识别的重定位的项纪录在一张表里,这张表就是重定位表,保存在DataDirectory中,序号是 IMAGE_DIRECTORY_ENTRY_BASERELOC。
PE文件加载时,PE 加载器分析重定位表,将其中每一项按照如今的模块基址进行重定位。
每一个重定位项应该是一个DWORD,里面保存须要重定位的RVA,这样只须要简单操做便能找到须要重定位的项。
然而,Windows并无这样设计,缘由是这样存放太占用空间了,试想一下,加入一个文件有n个重定位项,那么就须要占用4*n个字节。
因此Windows采用了分组的方式,按照重定位项所在的页面分组,每组保存一个页面起始地址的RVA,页内的每项重定位项使用一个WORD保存重定位项在页内的偏移,这样就大大缩小了重定位表的大小。
定义:
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
VirtualAddress:页起始地址RVA。
SizeOfBlock:表示该分组保存了几项重定位项。
TypeOffset:这个域有两个含义,页内偏移用12位就能够表示,剩下的高4位用来表示重定位的类型。而事实上,Windows只用了一种类型IMAGE_REL_BASED_HIGHLOW数值是 3。
哪些项目须要被重定位呢??
1.代码中使用全局变量的指令,由于全局变量必定是模块内的地址,并且使用全局变量的语句在编译后会产生一条引用全局变量基地址的指令。
2.将模块函数指针赋值给变量或做为参数传递,由于赋值或传递参数是会产生mov和push指令,这些指令须要直接地址。
3.C++中的构造函数和析构函数赋值虚函数表指针,虚函数表中的每一项自己就是重定位项
.text默认的代码区块,它的内容全是指令代码,连接器把全部目标文件的text块链接成一个大的.text块,
.data默认的读/写数据块,全局变量,静态变量通常放在这个区段
.rdata默认只读数据区块,但程序中不多用到该块中的数据,通常两种状况用到,一是MS 的连接器产生EXE文件中用于存放调试目录,二是用于存放说明字符串,若是程序的DEF文件中指定了DESCRIPTION,字符串就会出如今rdata中
.idata包含其余外来的DLL的函数及数据信息,即输入表,将.idata区块合并成另外一个区块已成为一种惯例
.edata输出表,当建立一个输出API或数据的可执行文件时,链接器会建立一个.EXP文件,这个.EXP文件包含一个.edata区块,其会被加载到可执行文件中,常常被合并到.text或.rdata 区块中
.rsrc资源,包括模块的所有资源,如图标,菜单,位图等,这个区块是只读的,不管如何不该该把它命名为.rsrc之外的名字,也不能合并到其余的区块里
.bss未初始化的数据,不多在用,取而代之的是执行文件的.data区块的的VirtualSize被扩展大的空间里用来装未初始化的数据.
.crt用于C++ 运行时(CRT)所添加的数据
.tlsTLS的意思是线程局部存储器,用于支持经过_declspec(thread)声明的线程局部存储变量的数据,这包括数据的初始化值,也包括运行时所须要的额外变量
.reloc可执行文件的基址重定位,基址重定位通常仅Dll须要的
.sdata相对于全局指针的可被定位的 短的读写数据
.pdata异常表,包含CPU特定的IAMGE_RUNTIME_FUNTION_ENTRY结构数组,DataDirectory中的IMAGE_DIRECTORY_ENTRY_EXCEPTION指向它.
.didat延迟装入输入数据,在非Release模式下能够找到
第一:当PE文件被执行,PE装载器检查DOS MZ header里的PE header偏移量。若是找到,则跳转到PE header。
第二:PE装载器检查PE header的有效性。若是有效,就跳转到PE header的尾部。
第三:紧跟PE header的是节表。PE装载器读取其中的节索引信息,并采用文件映射方法将这些节映射到内存,同时附上节表里指定的节属性。
第四:PE文件映射入内存后,PE装载器将处理PE文件中相似import table(引入表)逻辑部分。