PE文件结构
作者:姜江
E-mail:jznsmail@163.net
Blog:http://blog.csdn.net/jznsmail/
QQ:457283
PE文件布局
PE表头(PE Header)
PE表头包涵程序代码、资料区域大小、位置、适用的操作系统、堆栈初始大小等重要信息。PE表头并非在文件的最开始。
文件最开始数百个单元是DOS stub:一个极小的DOS程序,用来输出像"This Program cannot be run in DOS mode"这样的信息。当Win32加载程序把一个PE文件映射到内存时,内存映射文件(Memory mapped file)的第一个位单元对应到DOS Stub的第一个位单元。在DOS Stub表头中可以通过一个结构找到真正的PE表头。
pNTHeader = dosHeader + dosHeader->e_lfanew;
e_lfanew是一个相对的偏移量,指向真正的PE表头。
dosHeader是image的基址。
注意:应为内存向上增长,所以加偏移量而不是减。
PE表头是整个IMAGE_NT_HEADERS,这个结构有一个DWORD和两个子结构:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
如果e_lfanew指向一个NE Signature而不是PE Signature表示一个Win16 NE可执行文件,如果是LE Signature表示一个VxD文档。如果是LX Signatrue表OS/2文档。
IMAGE_FILE_HEADER结构如下:
DWORD Machine;指示使用那种CPU,可以在Winnt.h中找到(我的头文件中定义如下)
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
WORD NumberOfSections;EXE的OBJ中的Sections个数
DWORD TimeDateStamp;连接器产生此文件的时间。自从1969年12月31日4:00P.M之后的总秒数。
DWORD PointerToSymbolTable;COFF符号表的偏移位置。只对COFF除错有用。
DWORD NumberOfSymbols;COFF符号表中的符号个数。
DWORD SizeOfOptionalHeader;一个可有可无的表头大小,在EXE文件中,这也就是IMAGE_OPTIONAL_HEADER的大小。在OBJ文件中大多数时候为0。
WORD Characteristics;描述该文件的性质。比较重要的性质如下:
0x0001 文件没有重定位
0x0002 文件是可执行文件
0x2000 文件是动态连接库
下面列出系统定义的所有性质:
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
IMAGE_OPTINAL_HEADER结构
该结构是在IMAGE_FILE_HEADER之外的一些附加信息。
WORD Magic;定义image的状态。如0x0107表示一个rom image,0x010B表示一个正常的exe image。
BYTE MajorLinkerVersion
BYTE MinorLinkerVersion PE文件的连接器版本,以10进制表示。
其它的信息可以参看Winnt.h头文件
Section Table
内含image的每个sections信息。section以起始位置排列而不是字母顺序。section table的每个区域存储了一个地址,文件的原始资料被映射到内存。sections是一个个内存范围。程序和操作系统所需要的任何code和data都有一个相应的section存储。
PE表头最后是一个IMAGE_SECTION_HEADER结构数组,该数组元素个数纪录在IMAGE_NT_HEADER.FileHeader.NumberOfSection中。
IMAGE_SECTION_HEADER是EXE文件或OBJ文件SECTION的完整信息。
BYTE Name[IMAGE_SIZEOF_SHORT_NAME] 是8位的ANSI名称(没有NULL结束符),表示SECTION名称(如.text)。
union{
DWORD PhysicalAddress;
DWORD VirtualSize;
}Misc;
在EXE文件中代表code section或data section的虚拟内存大小(未进行alignment)。
对于OBJ文件代表SECTION的实际地址。第一个SECTION从0开始。下一个SECTION起始地址为上一个SECTION地址加上SizeOfRawData值(进行调整后的section虚拟内存大小)。
DWORD VirtualAddress;在EXE中代表载入程序将SECTION映射到的虚拟地址。SECTION的真正起始地址是该地址加上基址。这个地址常被编译程序设置成0x1000。
在OBJ文件中没有意义,总是0。
DWORD SizeOfRawData; 在EXE文件中代表section大小被对齐(alignment)后的值。
在OBJ中表示由编译指定的真正SECTION大小。
DWORD PointerToRawData;从文件头开始的偏移量,Section的初始信息可以从这个位置获得。
DWORD PointerToRelocations;在EXE中没有意义,总为0。
在OBJ中这是从文件头开始的偏移量来指向SECTION的重定位信息。每个OBJ SECTION的重定位信息紧跟在SECTION信息后。
DWORD PointerToLinenumbers;行号表的偏移地址。在exe文件中,行号信息放在档案的末尾。在obj文件中行号表放在每个section的原始资料以及重定位表之后。
WORD NumberofRelocations;重定位表中的重定位项目个数(PointerToRelocations指向)。只用于obj文件。
WORD NumberOfLinenumbers;行号表中行号个数(由PointerToLinenumbers指向)。
DWORD Characteristics; 一组标志,表示section中的属性(如code或data可读、可写等)。可以参看winnt.h中IMAGE_SCN_XXX_XXX的定义。
Sections
.text section
包涵所有一般性的程序代码。在.text中除了编译器产生出来的代码以及runtime library的代码外还有一些东西。在PE文件中,当你调用另一组模块中的函数(如USER32.DLL中的GetMessage),编译器产生的CALL指令并没有把控制权直接传给DLL中的函数,而是传给一个JMP DWORD PTR [XXXXXXXX]的指令,该指令也在.text中。JMP指令跳转到.idata中的一个DWORD中。这个DWORD含有真正的函数入口地址。
例如:
对DLL的调用方式为什么要通过这种方式?
通过对同一个DLL函数的所有调用都集中到一处,载入程序就不需要修改每个调用DLL的指令,只需要把DLL函数的真实地址放到.idata的那个DWORD中。这样调用带来一个缺点,那就是你不能以DLL函数的真正地址来初始化一个变量。
例如:
FARPROC pfnGetMessage = GetMessage;
这个变量实际存放的JMP DWORD PTR [XXXXXXXX]指令地址,而不是实际需要调用的函数地址。
对于用__declspec(dllimport)修饰的API函数,编译器不产生JMP DWORD PTR [XXXXXXXX]指令,而是产生CALL DWORD PTR[XXXXXXXX]来调用位于.idata中的XXXXXXXX。
.data section
存放初始化信息的地方。包括全局变量、字符串常量和静态变量,这些变量在编译时期就给定初值。连接器把obj和lib中的所有.data组合起来放到exe的.data中。而变量放在执行过程中的堆栈中。
.bss section
存放任何未初始化的静态变量和全局变量。obj和lib中所有.bss组合起来放在exe文件的.bss中。在section table中.bss的RawDataOffset总为0,表示这个section不占用任何空间。
.CRT section
微软C/C++ runtime library(CRT)所使用的另一个初始化的data section。这里存放用于在main或WinMan之前执行的静态C++类的构造函数。
.rsrc section
存放模块的资源。如.res文件内容。
.idata section
包涵有关模块从其他DLLs输入(import)函数和资料的相关信息。
.edata section
存放PE文件输出函数的相关信息。通常只在DLL中才看到.edata。
.reloc section
存放一个base relocation数组。base relocation是一组指令或者初始化变量的调整值。如果载入程序没有办法把exe或者dll载入预设地址,就必须做这样的调整。
.tls section
当使用__declspec(thread)时,定义的信息并没有放入.data或.bss,而是有一份拷贝放到.tls中。
.tls的全称是thread local storage。每个线程可以拥有自己的一组静态资料,使用这些资料的程序代码,不需要是那个线程正在执行。假设某个程序有数个线程处理相同的工作。如果声明一个stl如:
__declspec(thread) int i = 0; //this is a global variable declaration
每个线程将因此拥有变量i的一个副本。
.rdata section
至少有4个用途:
1.在使用微软连接程序产生的EXE中,.rdata含有debug directory(obj中不含)。
2.如果在程序的.DEF文件中指定DESCRIPTION,被指定的字符串就会出现在.rdata中。
3.GUID值都放在EXE或DLL的.rdata中
4.放置TLS(Thread Local Storage)的directory。被编译器的runtime library使用。
.drectve section
只出现在obj文件中,内含连接器命令参数的文字描述。
PE文件的输入
在被载入内存之前,存放在PE文件的.idata中的信息是给载入程序来决定函数地址并且修正它们,以便完成image用的。而在被载入之后,idata内涵的是指针,指向EXE/DLL的输入函数。
.idata section(import table)以一个IMAGE_IMPORT_DESCRIPTIOR数组开始,被PE文件连接的DLL都会在此有一个对应的IMAGE_IMPORT_DESCRIPTOR结构。
PE文件的输出
PE文件的输出函数相关信息存放在.edata。.edata一开始是一个IMAGE_EXPORT_DIRECTORY结构。
PE文档的基址重定位