在windows操作系统下,可执行文件的存储格式是PE格式;在Linux操作系统下,可执行文件的存储格式的WLF格式。它们都是COFF格式文件的变种,都是从COFF格式的文件演化而来的。
在windows平台下,目标文件(.obj),静态库文件(.lib)使用COFF格式存储;而可执行文件(.exe),动态链接库文件(.dll)使用PE格式存储。静态库文件其实就是一堆目标文件的集合。
在“WinNT.h”头文件中定义了COFF格式文件,以及PE格式文件的数据结构。这些定义是一系列的结构体,枚举,以及#define宏定义。在ImageHlp.dll中定义了编辑和读取PE文件内容的Win32API。
在64位Windows操作系统下,PE格式文件被做了少部分修改。没有新的字段定义被加入,并且去除了一些字段的定义,同时将字段的宽度从32位扩充到64位。64位windows操作系统下的PE格式文件被命名为:PE32+。
COFF文件的总体结构如下图所示:
从文件内容上来看,COFF文件由二进制数据组成。这些二进制数据从文件的零位置开始,依次存储,直到文件末尾。从数据结构的角度来看,这些二进制数据又分别属于不同的结构体或者结构体数组。这些结构体被定义在“WinNT.h”头文件中。
在COFF文件中,这些结构体或结构体数组分别表示不同的含义,记录着COFF文件中的不同内容。从文件的顶端开始,依次存储了文件头,可选头,段表,段数据,重定位表,行号表,符号表,以及字符串表的信息。这些结构体数据之间存在关联关系。比如:文件头信息中存储了符号表的开始位置,以及段表中数组元素的个数;在段表中存储了各个段的位置,重定位表的位置,行号表的位置;重定位表中的项会关联到符号表中的某个符号;而符号表中某个符号的名称可能会存储在字符串表中。
使用dumpbin工具可以将目标文件的内容导出,具体的命令格式如下:
Dumpbin /all DemoMath.obj >DemoMath.txt |
在上面的命令中,将目标文件“DemoMath.obj”的所有内容导出到文本文件“DemoMath.txt”中。命令选项“/all”表示导出所有内容,命令选项“>”表示将导出的内容存储到文件中。
文件头以一个结构体的形式存储在COFF文件的开始位置,占20个字节的大小。每一个COFF格式的二进制文件都必须包含一个文件头,它用来保存COFF文件的基本信息,如:文件标识,各个表的位置等。
使用dumpbin工具导出“DemoMath.obj”目标文件的内容后,文件头部分的信息内容如下:
Dump of file demomath.obj File Type: COFF OBJECT //表示该文件格式为COFF格式 FILE HEADER VALUES //以下依次是文件头中各项的值 14C machine (x86) //魔数 20 number of sections //段的数量 519AFB7E time date stamp Tue May 21 12:43:42 2013 //建立时间 288A file pointer to symbol table //符号表的位置 83 number of symbols //符号的数量 0 size of optional header //可选头的大小 0 characteristics //文件属性标记。零表示有重定位信息,有符号表,有行号,不可执行,具体解释可见“Characteristics字段的取值情况表”的描述。 |
在“WinNT.h”头文件中,文件头被定义为IMAGE_FILE_HEADER类型,具体的定义形式如下:
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; #define IMAGE_SIZEOF_FILE_HEADER 20 //文件头的大小 |
在文件头中,各个字段的详细解释如下表所示:
字段名称 |
类型 |
描述 |
Machine |
Word |
魔法数字,在i386平台中,该值为0x014c。这是一个平台的标识。 |
NumberOfSections |
Word |
段的数量。段表的大小由它确定。 段表的大小 = Sizeof(IMAGE_SECTION_HEADER) * NumberOfSections |
TimeDateStamp |
Dword |
该字段是一个时间戳,用来记录COFF文件创建的时间。当COFF文件作为一个可执行文件的时候,该值被用来当作加密用的比对标识。 |
PointerToSymbolTable |
Dword |
符号表在文件中的偏移量,该偏移量从文件的零位置为基准。使用该值可以确定符号表的第一个字节的位置。 |
NumberOfSymbols |
Dword |
符号表中符号的个数。 |
SizeOfOptionalHeader |
Word |
可选头的大小。通常为零。通过此值可定位段表。 |
Characteristics |
Word |
文件的属性标记,它标记了文件的类型,以及文件中所保存的数据的信息。该标记的详细说明见下表。 |
Characteristics字段的取值情况如下表所示:
名称 |
值 |
说明 |
F_RELFLG |
0x0001 |
无重定位信息标记。值为1表示无重定位信息。在目标文件中,该值为1,可执行文件中,该值为零。 |
F_EXEC |
0x0002 |
可执行标记。值为2表示该文件中所有符号都已经被解析完毕,可以被执行。在目标文件中,该值为零。 |
F_LNNO |
0x0004 |
无行号标记。值为4表示该文件中没有行号表 |
F_LSYMS |
0x0008 |
无符号标记。值为8表示该文件中没有符号表 |
F_AR32WR |
0x0100 |
该标记指出文件是 32 位的 Little-Endian COFF 文件。 |
该数据结构为可选数据,在目标文件中不存在此数据结构。只有当COFF文件作为可执行文件存在的时候,该数据结构才有意义。
段表是各个段的目录,用于检索各个段的信息。它以结构体数组的形式存储在可选头或者文件头的后面。在段表中,每一项的大小是36个字节,数组元素的个数记录在文件头的“NumberOfSections”字段中。
段的划分是基于各组数据的共同属性,而不是逻辑概念。每段是一块拥有共同属性的数据,比如代码/数据、读/写等。如果COFF文件中的数据/代码拥有相同属性,它们就能被归入同一段中。
在段表中记录了各个段在段数据区域中的位置(相对文件首位置的绝对偏移),以及各段重定位信息在重定位表中的位置。
在COFF格式的目标文件中,每一个函数形成一个.text段,因此会有多个名为.text的段。在使用工具dumpbin导出“DemoMath.obj”目标文件的内容后,除了列出.text段的同时,也将与该段相对应的重定位段一起列出。具体内容如下:
SECTION HEADER #9 .text name //段表信息的内容 0 physical address 0 virtual address 2A size of raw data 16B0 file pointer to raw data (000016B0 to 000016D9) 16DA file pointer to relocation table 0 file pointer to line numbers 1 number of relocations 0 number of line numbers 60501020 flags Code COMDAT; sym= "int __cdecl GetOperTimes(void)" (?GetOperTimes@@YAHXZ) 16 byte align Execute Read RAW DATA #9 //段的二进制数据 00000000: 55 8B EC 81 EC C0 00 00 00 53 56 57 8D BD 40 FF U.ì.ìà...SVW.?@? 00000010: FF FF B9 30 00 00 00 B8 CC CC CC CC F3 AB A1 00 ??10...?ììììó??. 00000020: 00 00 00 5F 5E 5B 8B E5 5D C3 ..._^[.?]?
RELOCATIONS #9 //段的重定位信息 Symbol Symbol Offset Type Applied To Index Name -------- ---------------- ----------------- -------- ------ 0000001F DIR32 00000000 F ?nOperTimes@@3HA (int nOperTimes) |
在“WinNT.h”头文件中,文件头被定义为IMAGE_SECTION_HEADER类型,具体的定义形式如下:
#define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; #define IMAGE_SIZEOF_SECTION_HEADER 40 |
在段表中,各个字段的详细解释如下表:
字段名称 |
类型 |
描述 |
Name |
BYTE |
节的ASCII名称。节名不保证一定是以NULL结尾的。如果你指定了长于8个字符的节名,链接器会把它截短为8个字符。在OBJ文件中存在一个机制允许更长的节名。节名通常以一个句点开始,但这并不是必须的。节名中有一个“$”时链接器会对之进行特殊处理。前面带有“$”的相同名字的节将会被合并。合并的顺序是按照“$”后面字符的字母顺序进行合并的 |
PhysicalAddress |
DWORD |
|
VirtualSize |
DWORD |
指出实际被使用的节的大小。这个域的值可以大于或小于SizeOfRawData域的值。如果VirtualSize的值大,SizeOfRawData就是可执行文件中已初始化数据的大小,剩下的字节用0填充。在OBJ文件中这个域被设为0。 |
VirtualAddress |
DWORD |
在可执行文件中,是节被加载到内存中后的RVA。在OBJ文件中应该被设为0 |
SizeOfRawData |
DWORD |
在可执行文件或OBJ文件中该节所占用的字节大小。对于可执行文件,这个值必须是PE头中给出的文件对齐值的倍数。如果是0,则说明这个节中的数据是未初始的。 |
PointerToRawData |
DWORD |
节在磁盘文件中的偏移。对于可执行文件,这个值必须是PE头部给出的文件对齐值的倍数。 |
PointerToRelocations |
DWORD |
节的重定位数据的文件偏移。只用于OBJ文件,在可执行文件中被设为0。对于OBJ文件,如果这个域的值不为0的话,它就指向一个IMAGE_RELOCATION结构数组。 |
PointerToLinenumbers |
DWORD |
节的COFF样式行号的文件偏移。如果非0,则指向一个IMAGE_LINENUMBER结构数组。只在COFF行号被生成时使用。 |
NumberOfRelocations |
WORD |
PointerToRelocations 指向的重定位的数目。在可执行文件中应该是0。 |
NumberOfLinenumbers |
WORD |
NumberOfRelocations 域指向的行号的数目。只在COFF行号被生成时使用。 |
Characteristics |
WORD |
被或到一起的一些标记,用来表示节的属性。这些标记中很多都可以通过链接器选项/SECTION来设置。 |
Characteristics字段的取值情况如下表所示:
值 |
描述 |
IMAGE_SCN_CNT_CODE |
节中包含代码。 |
IMAGE_SCN_MEM_EXECUTE |
节是可执行的。 |
IMAGE_SCN_CNT_INITIALIZED_DATA |
节中包含已初始化数据。 |
IMAGE_SCN_CNT_UNINITIALIZED_DATA |
节中包含未初始化数据。 |
IMAGE_SCN_MEM_DISCARDABLE |
节可被丢弃。用于保存链接器使用的一些信息,包括.debug$节。 |
IMAGE_SCN_MEM_NOT_PAGED |
节不可被页交换,因此它总是存在于物理内存中。经常用于内核模式的驱动程序。 |
IMAGE_SCN_MEM_SHARED |
包含节的数据的物理内存页在所有用到这个可执行体的进程之间共享。因此,每个进程看到这个节中的数据值都是完全一样的。这对一个进程的所有实例之间共享全局变量很有用。要使一个节共享,可使用/section:name,S 链接器选项。 |
IMAGE_SCN_MEM_READ |
节是可读的。几乎总是被设置。 |
IMAGE_SCN_MEM_WRITE |
节是可写的。 |
IMAGE_SCN_LNK_INFO |
节中包含链接器使用的信息。只在OBJ文件中存在。 |
IMAGE_SCN_LNK_REMOVE |
节中的数据不会成为映像的一部分。只出现在OBJ文件中。 |
IMAGE_SCN_LNK_COMDAT |
节中的内容是公共数据(comdat)。公共数据是指可被定义在多个OBJ文件中的数据。链接器将选择一个包含到可执行文件中。Comdat 对于支持C++模板函数和在函数级别上的链接是至关重要的。Comdat节只出现在OBJ文件中。 |
IMAGE_SCN_ALIGN_XBYTES |
在最终的可执行文件中这个节中数据的对齐大小。它可有许多取值(_4BYTES,_8BYTES,_16BYTES等)。如果没有被指定,缺省是16字节。这些标记只在OBJ文件中被设置。 |
在编译阶段,将某些源文件编译成目标文件的时候,在目标文件中,某些被调用函数或者数据的位置是无法确定的。这时候,编译器将这些被调用的函数或者数据的地址设定为一个默认的假值。在链接阶段,当能够确定这些被调用函数或数据的地址的时候,再用真实的地址来替换这些假值。我们将这个过程叫做重定位。
使用工具dumpbin将目标文件main.obj的内容输出为汇编格式的文件后,可以观察到这些假值的设定情况,以及需要重定位的位置。命令格式如下:
Dumpbin /disasm main.obj >mainasm.txt |
输入的汇编文件的一部分内容如下:
//objMath.SubData(nGlobalData,3);以下是执行该函数调用的汇编代码 00000080: 8B F4 mov esi,esp 00000082: 83 EC 08 sub esp,8 00000085: DD 05 00 00 00 00 fld qword ptr [__real@4008000000000000] 0000008B: DD 1C 24 fstp qword ptr [esp] 0000008E: DB 05 00 00 00 00 fild dword ptr [?nGlobalData@@3HA] 00000094: 83 EC 08 sub esp,8 00000097: DD 1C 24 fstp qword ptr [esp] 0000009A: 8D 4D EC lea ecx,[ebp-14h] 0000009D: FF 15 00 00 00 00 call dword ptr [__imp_?SubData@DemoMath@@QAEXNN@Z] 000000A3: 3B F4 cmp esi,esp 000000A5: E8 00 00 00 00 call __RTC_CheckEsp |
在上面的代码中,地址0x0000008E处引用了全局变量nGlobalData,指令格式为:DB 05 00 00 00 00。DB 05为fild汇编指令的二进制码,而后边四个字节的零(红色表示)是nGlobalData的地址,这个地址是个临时的假值。
在当前目标文件中,如果被调用的函数或数据位于另外一个目标文件中,那么在链接的时候需要对被调用的函数或数据执行重定位;如果被调用的函数或数据是全局函数或者全局变量,那么在链接的时候,需要对该全局函数或全局变量执行重定位。在示例代码中,全局变量:nGlobalData, nOperTimes,全局函数:GetOperTimes()在链接的时候需要执行重定位。
重定位表只存在于目标文件中,它存储了各个段的重定位信息。在每个段的段表中,记录了该段重定位信息在重定位表中的位置(相对于文件首位置的偏移)。
使用工具dumpbin将目标文件的内容导出后,如果某个代码段存在重定位信息(该代码段引用过了全局符号或者外部符号),那么在该代码段的后面就会列出该代码段的重定位信息。该重定位信息是重定位表中的一个片段。示例如下:
SECTION HEADER #16 //代码段的信息摘要。Subdata函数所在的代码段 .text name 0 physical address 0 virtual address 5C size of raw data 2088 file pointer to raw data (00002088 to 000020E3) 20E4 file pointer to relocation table 0 file pointer to line numbers 4 number of relocations 0 number of line numbers 60501020 flags Code COMDAT; sym= "public: void __thiscall DemoMath::SubData(double,double)" 16 byte align Execute Read
RAW DATA #16 //代码段的二进制数据内容,红色字体表示需要重定位的位置。被//VirtualAddress字段指定。 00000000: 55 8B EC 81 EC CC 00 00 00 53 56 57 51 8D BD 34 U.ì.ìì...SVWQ.?4 00000010: FF FF FF B9 33 00 00 00 B8 CC CC CC CC F3 AB 59 ???13...?ììììó?Y 00000020: 89 4D F8 A1 00 00 00 00 83 C0 01 A3 00 00 00 00 .M??.....à.£.... 00000030: DD 45 08 DC 65 10 83 EC 08 DD 1C 24 8B 45 F8 8B YE.üe..ì.Y.$.E?. 00000040: 08 E8 00 00 00 00 5F 5E 5B 81 C4 CC 00 00 00 3B .è...._^[.?ì...; 00000050: EC E8 00 00 00 00 8B E5 5D C2 10 00 ìè.....?]?.. RELOCATIONS #16 //代码段的重定位信息。 Symbol Symbol Offset Type Applied To Index Name -------- ---------------- ----------------- -------- ------ 00000024 DIR32 00000000 F ?nOperTimes@@3HA (int nOperTimes) 0000002C DIR32 00000000 F ?nOperTimes@@3HA (int nOperTimes) 00000042 REL32 00000000 59 ?OutPutInfo@DemoOutPut@@QAEXN@Z 00000052 REL32 00000000 3F __RTC_CheckEsp |
这是类DemoMath的成员函数:SubData()所在的代码段的重定位信息,在该重定位信息中,需要重定位的符号是:全局变量nOperTimes和外部函数OutPutInfo()。在上面代码中,红色字体的部分被重定位表中的字段:VirtualAddress指向,标记了需要重定位的位置。
在“WinNT.h”头文件中,文件头被定义为IMAGE_RELOCATION类型,具体的定义形式如下:
typedef struct _IMAGE_RELOCATION { union { DWORD VirtualAddress; DWORD RelocCount; // Set to the real count when IMAGE_SCN_LNK_NRELOC_OVFL is set }; DWORD SymbolTableIndex; WORD Type; } IMAGE_RELOCATION; typedef IMAGE_RELOCATION UNALIGNED *PIMAGE_RELOCATION; |
在重定位表中,各个字段的详细解释如下表:
字段名称 |
类型 |
描述 |
VirtualAddress |
DWORD |
该字段指向代码段中的一个地址。该地址所包含的数据是需要重定位的符号的地址。这部分数据将要被重定位修正。该字段指向了这个数据的第一个字节。上面的示例中,红色字体标记的部分被该字段指向。 |
RelocCount |
DWORD |
|
SymbolTableIndex |
DWORD |
需要重定位的符号在符号表中的索引,该值为符号表数组的索引。通过该值可以检索符号在符号表中的信息。 |
Type |
WORD |
一般分两种类型。DIR32表示32位绝对地址;REL32表示32位相对地址。在执行重定位的时候,对于绝对地址类型,将被替换为符号的绝对地址;而对于相对地址类型,将被替换为符号的相对地址,即:符号相对于被修正位置的地址差。 |
行号表描述了二进制代码与源代码行号之间的关系,调试阶段使用。在“WinNT.h”头文件中,文件头被定义为IMAGE_RELOCATION类型,具体的定义形式如下:
typedef struct _IMAGE_LINENUMBER { union { DWORD SymbolTableIndex; // Symbol table index of function name if Linenumber is 0. DWORD VirtualAddress; // Virtual address of line number. } Type; WORD Linenumber; // Line number. } IMAGE_LINENUMBER; typedef IMAGE_LINENUMBER UNALIGNED *PIMAGE_LINENUMBER; |
在行号表中,各个字段的详细解释如下表:
字段名称 |
类型 |
描述 |
SymbolTableIndex |
DWORD |
符号在符号表中的索引 |
VirtualAddress |
DWORD |
符号的地址值 |
Linenumber |
WORD |
行号 |
在编译阶段的词法分析过程中,编译器扫描整个C++源代码,将源代码中的函数名称,变量名称收集起来,然后写入符号表中。在符号表中主要包含如下内容:函数名称,变量名称,段的名称,以及一些常量信息,这些名称被统称为符号。
符号表中的信息被用于静态链接阶段,用来进行被引用的函数或变量的地址重定位。每一个目标文件中都会包含一个符号表。在该符号表中的符号,要么是在该目标文件中定义的函数名称或变量名称;要么是被该目标文件引用的,定义于其他目标文件中的函数名称或变量名称。在静态链接阶段,多个目标文件进行链接的时候,存在于这些目标文件中的符号表会被合并到一起,形成一个全局符号表。在C++源代码中出现的所有符号都应该能在全局符号表中被查找到。
将符号表中的符号进行分类,具体的分类情况如下:
在执行链接的时候,只关注前两种类型的符号。
如果符号的名称小于8个字节,那么将该符号的名称直接存储在符号表中;如果符号的名称大于8个字节,那么将符号的名称存储在字符串表中,原来符号表中存储符号名称的地方存储了一个地址偏移量,该地址偏移量指向了字符串表中符号名称的位置。
根据符号存储类型以及符号在段中位置的不同,符号的值有不同的解释。
使用工具dumpbin将DemoMath.obj的内容导出以后,其符号表中的一部分的内容描述如下:
000 00847809 ABS notype Static | @comp.id //绝对值常量 001 00000001 ABS notype Static | @feat.00 //绝对值常量 002 00000000 SECT1 notype Static | .drectve //段名称 //段名称符号下面紧跟段的信息。每行占用一个符号索引的位置,所以符号索引不是连续的。 Section length 201, #relocs 0, #linenums 0, checksum 0 Relocation CRC 00000000 005 00000000 SECT4 notype External | ?nOperTimes@@3HA (int nOperTimes) //变量 006 00000000 SECT1A notype () External | ?DivData@DemoMath@@QAEXNN@Z //函数 007 00000000 UNDEF notype () External | ?OutPutInfo@DemoOutPut@@QAEXPBD@Z //外部函数
|
在上面的示例中,从左到右各字段的含义依次是:符号结构体所在数组的索引,符号大小,符号在段中位置,符号类型,符号的存储类型,符号名称。在该符号表的内容中,列出了全局变量名:nOperTimes,类成员函数名:DivData,被引用的外部函数名:OutPutInfo。段的名称也被作为一个符号写入到符号表中,上面示例中的“.drectve”即为一个段的名称。
在“WinNT.h”头文件中,文件头被定义为IMAGE_SYMBOL类型,具体的定义形式如下:
typedef struct _IMAGE_SYMBOL { union { BYTE ShortName[8]; Struct { DWORD Short; // if 0, use LongName DWORD Long; // offset into string table } Name; DWORD LongName[2]; // PBYTE [2] } N; DWORD Value; SHORT SectionNumber; WORD Type; BYTE StorageClass; BYTE NumberOfAuxSymbols; } IMAGE_SYMBOL; typedef IMAGE_SYMBOL UNALIGNED *PIMAGE_SYMBOL; |
在符号表中,各个字段的详细解释如下表:
字段名称 |
类型 |
描述 |
ShortName |
BYTE |
小于8个字节的符号名称存储于此。 |
Short |
DWORD |
0表示符号名称位于字符串表中。 |
Long |
DWORD |
符号名称在字符串表中的偏移量。 |
LongName |
DWORD |
|
Value |
DWORD |
符号的值。对于变量或函数来说,符号值就是它们的地址。根据符号存储类型的不同,符号值有不同的解释。 |
SectionNumber |
SHORT |
符号所在的段落。ABS表示符号是个绝对值,是个常量;UNDEF表示符号是未定义的,即该符号的定义在其他段中;SECT1表示该符号位于编号为1的段中。 |
Type |
WORD |
符号的类型。Notype表示变量;notype()表示函数。 |
StorageClass |
BYTE |
符号的存储类型。Static表示局部变量,文件内部可见;external表示全局变量,全局范围内可见。 |
NumberOfAuxSymbols |
BYTE |
附加记录的数量。 |
符号的值的具体含义需要根据符号所在的段落(SectionNumber)以及符号的存储类型(StorageClass)来确定,这三者之间的具体关系如下表所示:
StorageClass |
SectionNumber |
Value |
Static |
SECTn(n为1,2,3…) |
如果值不为零,表示符号在段内偏移。 |
SECTn(n为1,2,3…) |
如果值为零,表示这个符号为段名。 |
|
ABS |
常量的值。 |
|
External |
UNDEF |
符号为全局变量/函数,符号定义在外部文件中,值待定。 |
SECTn(n为1,2,3…) |
符号为全局变量/函数,符号定义在当前文件中,值表示符号在段内偏移。 |
字符串表用来保存长度大于8个字节的符号名称。字符串表的前4个字节表示字符串的长度,后面的紧跟字符串的内容,它以字节为单位,以’\0’作为字符串的结束符。这里的字符串长度不仅仅是字符串自身的长度(字符串内容+’\0’),还包括前面4个字节的该数据自身的长度。
在COFF文件所包含的数据结构中,各个数据结构之间的关系如下图所示:
重定位表和符号表之间通过符号表的索引进行关联;在文件头中保存了可选头的大小和段表所包含项目的数量,通过计算可以确定段表的起始位置和结束位置。段表起始位置=文件头大小+可选头大小;其他关系通过相对文件首位置的偏移表示。
静态链接库就是一组目标文件的集合,当执行静态链接的时候,被选定的目标文件的内容就会被合并到相关的Pe文件中去。静态链接库的总体结构如下图所示:
静态链接库以签名开始,签名的数据内容为“(!<arch>\n”,长8个字节。紧跟在签名后面的是三个特别成员,分别是第一链接器节,第二链接器节,以及长名称节。在这三个特别成员之后,直到文件结束,存储的都是目标文件节的内容。
第一链接器节,第二链接器节,长名称节,以及目标文件节的数据结构都是由头数据+节数据这样的数据结构组成的。
第一链接器节。在静态链接库中必须存在该节,它包含了静态链接库中所有的符号名以及这些符号在静态链接库文件中的偏移;
第二链接器节。在静态链接库中该节可选,它包含了与第一链接器节相同的内容,但是它的内容是有序的,通过它查找符号要比在第一链接器节中查找的快;
长名称节。在静态链接库中该节可选,它是一个字符串表,用于存储名称大于16个字节的目标文件的名称。在目标文件节中,如果目标文件的名称小于16个字节,那么这个名称会被存储在头文件的名称域;如果这个名称大于16个字节,那么这个名称就会被存储到这里。而在头文件的名称域存储的则是该字符串在长名称节的偏移。
目标文件节。该节是静态链接库的主要内容,节的数量不定,它存储了若干各目标文件的内容,每一节都是头信息+目标文件的结构。目标文件的结构与2.2节描述的一致。
在WinNT.h头文件中,头信息被定义为IMAGE_ARCHIVE_MEMBER_HEADER类型,具体的定义内容如下:
typedef struct _IMAGE_ARCHIVE_MEMBER_HEADER { BYTE Name[16]; // File member name - `/' terminated. BYTE Date[12]; // File member date - decimal. BYTE UserID[6]; // File member user id - decimal. BYTE GroupID[6]; // File member group id - decimal. BYTE Mode[8]; // File member mode - octal. BYTE Size[10]; // File member size - decimal. BYTE EndHeader[2]; // String to end header. } IMAGE_ARCHIVE_MEMBER_HEADER, *PIMAGE_ARCHIVE_MEMBER_HEADER; |
从文件内容上来看,PE文件由二进制数据组成。这些二进制数据从文件的零位置开始,依次存储,直到文件末尾。从数据结构的角度来看,这些二进制数据又分别属于不同的结构体或者结构体数组。这些结构体被定义在“WinNT.h”头文件中。
在PE文件中,这些结构体或结构体数组分别表示不同的含义,记录着PE文件中的不同内容。从文件的顶端开始,依次存储了DOS头,PE头,段表,各段详细数据等信息。在进行信息字段定位的时候,PE文件采用两种方式:1利用指针。比如:在Dos头中存储一个指向PE头的指针;2利用数据结构的大小。在PE的头部信息中,一些数据结构的大小是固定的。在数据存储的时候,各个数据结构紧凑存放,中间没有空隙。在这种情况下,以一个数据结构的字段为基点,通过计算数据结构占用空间的大小,就可以定位另外一个数据结构的位置。
使用dumpbin工具可以将PE文件的内容导出,具体的命令格式如下:
Dumpbin /all DemoDlld.dll >DemoDll.txt |
在上面的命令中,将PE文件“DemoDlld.dll”的所有内容导出到文本文件“DemoDll.txt”中。命令选项“/all”表示导出所有内容,命令选项“>”表示将导出的内容存储到文件中。
所有 PE文件都必须以DOS MZ header开始,它是一个IMAGE_DOS_HEADER的结构。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ Header之后的DOS Stub。
在“WinNT.h”头文件中,DOS MZ 头被定义为IMAGE_DOS_HEADER类型,具体的定义形式如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // 魔术数字 WORD e_cblp; // 文件最后页的字节数 WORD e_cp; // 文件页数 WORD e_crlc; // 重定义元素个数 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; // 重分配表文件地址 WORD e_ovno; // 覆盖号 WORD e_res[4]; // 保留字 WORD e_oemid; // OEM 标识符 WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // 保留字 LONG e_lfanew; // PE头的地址 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; |
在DOS头中,第一个域“e_magic”被称为魔术数字,它用于表示一个MS-DOS兼容的文件类型。所有MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。
对于MS-DOS操作系统来说,许多其他的域都是有用的。但是对于 Windows NT来说,只有最后一个域e_lfnew是有用的,该域是一个指针,占用4个字节,用于指明PE头在文件中的位置。
DOS Stub实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串“This program requires Windows”,或者程序员可根据自己的意图实现完整的DOS代码。大多数情况下DOS Stub由汇编器/编译器自动生成。
PE头紧跟在DOS MS头以及实模式程序残余之后,在WinNt.h头文件中,PE头被定义为IMAGE_NT_HEADER类型,具体的定义内容如下所示:
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头标识外,又嵌套了两个结构体数据,分别是PE文件头的信息,以及PE可选头的信息。
在一个有效的PE文件中,PE头标识字段的值是0x00004550,用ASCII表示就是“PE00”。 #define IMAGE_NT_SIGNATURE定义了这个值。
见2.2.2节描述。PE中的文件头与COFF中的文件头定义一致。
在PE文件头之后,是PE可选头。该头224字节大小,它包含了许多重要的信息,例如:初始堆栈大小,程序的入口地址,首选加载基地址,操作系统版本,段对齐等。该头并非可选,而是必须要有的头。
在可选头中,包含了三类主要信息,分别是:标准域信息,WinNT附加信息,以及数据目录信息。
所谓标准域就是指和UNIX可执行文件的COFF格式所公共的部分,虽然标准域中保留了COFF文件中定义的名称,但是WindowsNT仍然将它用作了不同的目的。
在操作系统的加载器加载PE文件的时候,WinNT附件域的信息为加载器提供了支持。
在可执行文件中有许多数据结构需要被快速定位,数据目录提供了这种支持。数据目录是一个指针列表,在该列表中保存了一系列的指针值,这些指针指向了其他的数据表。如:导入表,导出表,资源表,重定位表等。数据目录以指针的形式提供了一种信息查找的方式。
使用工具dumpbin可以将PE文件的内容导出,在该内容中包含了描述可选头的摘要信息,具体的信息内容如下:
OPTIONAL HEADER VALUES //以下为标准域的信息 10B magic # (PE32) 9.00 linker version 5800 size of code 4800 size of initialized data 0 size of uninitialized data 11159 entry point (10011159) @ILT+340(__DllMainCRTStartup@12) 1000 base of code 1000 base of data //以下为WinNT附加域的信息 10000000 image base (10000000 to 1001CFFF) 1000 section alignment 200 file alignment 5.00 operating system version 0.00 image version 5.00 subsystem version 0 Win32 version 1D000 size of image 400 size of headers 0 checksum 2 subsystem (Windows GUI) 140 DLL characteristics Dynamic base NX compatible 100000 size of stack reserve 1000 size of stack commit 100000 size of heap reserve 1000 size of heap commit 0 loader flags 10 number of directories //以下为数据目录的信息 18AD0 [ 2BA] RVA [size] of Export Directory 1A000 [ 50] RVA [size] of Import Directory 1B000 [ C09] RVA [size] of Resource Directory 0 [ 0] RVA [size] of Exception Directory 0 [ 0] RVA [size] of Certificates Directory 1C000 [ 38C] RVA [size] of Base Relocation Directory 17520 [ 1C] RVA [size] of Debug Directory 0 [ 0] RVA [size] of Architecture Directory 0 [ 0] RVA [size] of Global Pointer Directory 0 [ 0] RVA [size] of Thread Storage Directory 0 [ 0] RVA [size] of Load Configuration Directory 0 [ 0] RVA [size] of Bound Import Directory 1A224 [ 1D4] RVA [size] of Import Address Table Directory 0 [ 0] RVA [size] of Delay Import Directory 0 [ 0] RVA [size] of COM Descriptor Directory 0 [ 0] RVA [size] of Reserved Directory |
在WinNT.h头文件中,可选头被定义为IMAGE_OPTIONAL_HEADER类型,具体的定义内容描述如下:
//数据目录中数据元素的个数 #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 //可选头的定义 typedef struct _IMAGE_OPTIONAL_HEADER { //标准域 WORD Magic; //魔法数字 BYTE MajorLinkerVersion;//链接器的最大版本号 BYTE MinorLinkerVersion;//链接器的最小版本号 DWORD SizeOfCode;//可执行代码长度 DWORD SizeOfInitializedData;//初始化数据的长度(data段) DWORD SizeOfUninitializedData;//未初始化数据的长度(bss段) DWORD AddressOfEntryPoint;//代码的入口地址,程序从此处开始执行 DWORD BaseOfCode;//可执行代码的起始位置 DWORD BaseOfData;//初始化数据的起始位置 //NT附件域 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;//dll状态 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; |
在可选头中,各个字段的详细解释如下表:
字段名称 |
类型 |
描述 |
Magic |
WORD |
一个签名,确定这是什么类型的头。两个最常用的值是IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b 和IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b. |
MajorLinkerVersion |
BYTE |
创建可执行文件的链接器的主版本号。对于Microsoft的链接器生成的PE文件,这个版本号的Visual Studio的版本号相一致 |
MinorLinkerVersion |
BYTE |
创建可执行文件的链接器的次版本号 |
SizeOfCode |
DWORD |
所有具有IMAGE_SCN_CNT_CODE属性的节的总的大小 |
SizeOfInitializedData |
DWORD |
所有包含已初始数据的节的总的大小。 |
SizeOfUninitializedData |
DWORD |
所有包含未初始化数据的节的总的大小。这个域总是0,因为链接器可以把未初始化数据附加到常规数据节的末尾。 |
AddressOfEntryPoint |
DWORD |
文件中将被执行的第一个代码字节的RVA。对于DLL,这个进入点将在进程初始化和关闭时,以及线程被创建和销毁时调用。在大多数可执行文件中,这个地址并不直接指向main,WinMain或DllMain函数,而是指向运行时库代码,由运行时库调用前述函数。在DLL中,这个域可以被设为0,这样的话上面所说的通知就不能被接收到。链接器选项/NOENTRY可以设置这个域为0。 |
BaseOfCode |
DWORD |
加载到内存后代码的第一个字节的RVA。 |
BaseOfData |
DWORD |
理论上,它表示加载到内存后数据的第一个字节的RVA。然而,这个域的值对于不同版本的Microsoft链接器是不一致的。在64位的可执行文件中这个域不出现。 |
ImageBase |
DWORD |
文件在内存中的首选加载地址。加载器尽可能地把PE文件加载到这个地址(就是说,如果当前这块内存没有被占用,它是对齐的并且是一个合法的地址,等等)。如果可执行文件被加载到这个地址,加载器就可以跳过进行基址重定位(在这篇文章的第二部分描述)这一步。对于EXE,缺省的ImageBase是0x400000。对于DLL,缺省是0x10000000。在链接时可以通过/BASE 选项来指定ImageBase,或者以后用REBASE工具重新设置。 |
SectionAlignment |
DWORD |
加载到内存后节的对齐大小。这个值必须大于等于FileAlignment(下一个域)。缺省的对齐值是目标CPU的页大上。对于运行在Windows 9x或Windows Me下的用户模式可执行文件,最小对齐大小是一页(4KB)。这个域可以通过链接器选项/ALIGN来设置。 |
FileAlignment |
DWORD |
在PE文件中节的对齐大小。对于x86下的可执行文件,这个值通常是0x200或0x1000。不同版本的Microsoft链接器缺省值不同。这个值必须是2的幂,并且如果SectionAlignment小于CPU的页大小,这个域必须和SectionAlignment相匹配。链接器选项/OPT:WIN98可设置x86可执行文件的文件对齐为0x1000,/OPT:NOWIN98设置文件对齐为0x200。 |
MajorOperatingSystemVersion |
WORD |
所要求的操作系统的主版本号。随着那么多版本Windows的出现,这个域的值就变得很不确切。 |
MinorOperatingSystemVersion |
WORD |
所要求的操作系统的次版本号。 |
MajorImageVersion |
WORD |
这个文件的主版本号。不被系统使用并可设为0。可以通过链接器选项/VERSION来设置。 |
MinorImageVersion |
WORD |
这个文件的次版本号。 |
MajorSubsystemVersion |
WORD |
可执行文件所要求的操作子系统的主版本号。它曾经被用来表示需要较新的Windows 95或Windows NT用户界面,而不是老版本的Windows NT界面。今天随着各种不同版本Windows的出现,这个域已不被系统使用,并且通常被设为4。可通过链接器选项/SUBSYSTEM设置这个域的值。 |
MinorSubsystemVersion |
WORD |
执行文件所要求的操作子系统的次版本号。 |
Win32VersionValue |
DWORD |
不被使用的域,通常设为0。 |
SizeOfImage |
DWORD |
映像的大小。它表示了加载文件到内存中时系统必须保留的内存的数量。这个域的值必须是SectionAlignmnet的倍数。 |
SizeOfHeaders |
DWORD |
MS-DOS头,PE头和节表的总的大小。PE文件中所有这些项目出现在任何代码或数据节之前。这个域的值被调整为文件对齐大小的整数倍。 |
CheckSum |
DWORD |
映像的校验和。IMAGEHLP.DLL中的CheckSumMappedFile函数可以计算出这个值。校验和用于内核模式的驱动和一些系统DLL。对于其它的,这个域可以为0。当使用链接器选项/RELEASE时校验和被放入文件中。 |
Subsystem |
WORD |
指示可执行文件期望的子系统(用户界面类型)的枚举值。这个域只用于EXE。一些重要的值包括: IMAGE_SUBSYSTEM_NATIVE // 映像不需要子系统 IMAGE_SUBSYSTEM_WINDOWS_GUI // 使用Windows GUI IMAGE_SUBSYSTEM_WINDOWS_CUI // 作为控制台程序运行。 // 运行时,操作系统创建一个控制台 // 窗口并提供stdin,stdout和stderr // 文件句柄。 |
DllCharacteristics |
WORD |
标记DLL的特性。对应于IMAGE_DLLCHARACTERISTICS_xxx定义。当前的值是: IMAGE_DLLCHARACTERISTICS_NO_BIND // 不要绑定这个映像 IMAGE_DLLCHARACTERISTICS_WDM_DRIVER // WDM模式的驱动程序 IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE // 当终端服务加载一个不是 // Terminal- Services-aware 的应用程 // 序时,它也加载一个包含兼容代码 // 的DLL。 |
SizeOfStackReserve |
DWORD |
在EXE文件中,为线程保留的堆栈大小。缺省是1MB,但并不是所有的内存一开始都被提交。 |
SizeOfStackCommit |
DWORD |
在EXE文件中,为堆栈初始提交的内存数量。缺省情况下,这个域是4KB。 |
SizeOfHeapReserve |
DWORD |
在EXE文件中,为默认进程堆初始保留的内存大小。缺省是1MB。然而在当前版本的Windows中,堆不经过用户干涉就能超出这里指定的大小。 |
SizeOfHeapCommit |
DWORD |
在EXE文件中,提交到堆的内存大小。缺省情况下,这里的值是4KB。 |
LoaderFlags |
DWORD |
不使用。 |
NumberOfRvaAndSizes |
DWORD |
在IMAGE_NT_HEADERS结构的末尾是一个IMAGE_DATA_DIRECTORY结构数组。此域包含了这个数组的元素个数。自从最早的Windows NT发布以来这个域的值一直是16。 |
数据目录一共有16项,每一项都存储一个指向其他数据表的指针,数据目录为数据的快速定位提供了支持。在WinNT.h头文件中,数据目录被定义为IMAGE_DATA_DIRECTORY,具体的定义内容描述如下:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //被指向元素表的起始RVA地址。 DWORD Size; //被指向元素表的长度 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 |
在数据目录的16项元素中,每一项都预定义了它的含义,IMAGE_DIRECTORY_ENTRY_XXX定义了数据目录数组的索引。该索引的具体含义描述如下表:
序号 |
索引 |
描述 |
0 |
IMAGE_DIRECTORY_ENTRY_EXPORT |
指向导出表(一个IMAGE_EXPORT_DIRECTORY结构)。 |
1 |
IMAGE_DIRECTORY_ENTRY_IMPORT |
指向导入表(一个IMAGE_IMPORT_DESCRIPTOR结构数组)。 |
2 |
IMAGE_DIRECTORY_ENTRY_RESOURCE |
指向资源(一个IMAGE_RESOURCE_DIRECTORY结构。 |
3 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION |
指向异常处理表(一个IMAGE_RUNTIME_FUNCTION_ENTRY结构数组)。CPU特定的并且基于表的异常处理。用于除x86之外的其它CPU上。 |
4 |
IMAGE_DIRECTORY_ENTRY_SECURITY |
指向一个WIN_CERTIFICATE结构的列表,它定义在WinTrust.H中。不会被映射到内存中。因此,VirtualAddress域是一个文件偏移,而不是一个RVA。 |
5 |
IMAGE_DIRECTORY_ENTRY_BASERELOC |
指向基址重定位信息。 |
6 |
IMAGE_DIRECTORY_ENTRY_DEBUG |
指向一个IMAGE_DEBUG_DIRECTORY结构数组,其中每个结构描述了映像的一些调试信息。早期的Borland链接器设置这个IMAGE_DATA_DIRECTORY结构的Size域为结构的数目,而不是字节大小。要得到IMAGE_DEBUG_DIRECTORY结构的数目,用IMAGE_DEBUG_DIRECTORY 的大小除以这个Size域。 |
7 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE |
指向特定架构数据,它是一个IMAGE_ARCHITECTURE_HEADER结构数组。不用于x86或IA-64,但看来已用于DEC/Compaq Alpha。 |
8 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR |
在某些架构体系上VirtualAddress域是一个RVA,被用来作为全局指针(gp)。不用于x86,而用于IA-64。Size域没有被使用。参见2000年11月的Under The Hood 专栏可得到关于IA-64 gp的更多信息。 |
9 |
IMAGE_DIRECTORY_ENTRY_TLS |
指向线程局部存储初始化节。 |
10 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG |
指向一个IMAGE_LOAD_CONFIG_DIRECTORY结构。IMAGE_LOAD_CONFIG_DIRECTORY中的信息是特定于Windows NT、Windows 2000和 Windows XP的(例如 GlobalFlag 值)。要把这个结构放到你的可执行文件中,你必须用名字__load_config_used 定义一个全局结构,类型是IMAGE_LOAD_CONFIG_DIRECTORY。对于非x86的其它体系,符号名是_load_config_used (只有一个下划线)。如果你确实要包含一个IMAGE_LOAD_CONFIG_DIRECTORY,那么在 C++ 中要得到正确的名字比较棘手。链接器看到的符号名必须是__load_config_used (两个下划线)。C++ 编译器会在全局符号前加一个下划线。另外,它还用类型信息修饰全局符号名。因此,要使一切正常,在 C++ 中就必须像下面这样使用: extern "C" IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {...} |
11 |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT |
指向一个 IMAGE_BOUND_IMPORT_DESCRIPTOR结构数组,对应于这个映像绑定的每个DLL。数组元素中的时间戳允许加载器快速判断绑定是否是新的。如果不是,加载器忽略绑定信息并且按正常方式解决导入API。 |
12 |
IMAGE_DIRECTORY_ENTRY_IAT |
指向第一个导入地址表(IAT)的开始位置。对应于每个被导入DLL的IAT都连续地排列在内存中。Size域指出了所有IAT的总的大小。在写入导入函数的地址时加载器使用这个地址和Size域指定的大小临时地标记IAT为可读写。 |
13 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT |
指向延迟加载信息,它是一个CImgDelayDescr结构数组,定义在Visual C++的头文件DELAYIMP.H中。延迟加载的DLL直到对它们中的API进行第一次调用发生时才会被装入。Windows中并没有关于延迟加载DLL的知识,认识到这一点很重要。延迟加载的特征完全是由链接器和运行时库实现的。 |
14 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR |
在最近更新的系统头文件中这个值已被改名为IMAGE_DIRECTORY_ENTRY_COMHEADER。它指向可执行文件中.NET信息的最高级别信息,包括元数据。这个信息是一个IMAGE_COR20_HEADER结构。 |
15 |
IMAGE_DIRECTORY_ENTRY_EXPORT |
指向导出表(一个IMAGE_EXPORT_DIRECTORY结构)。 |
当一个可执行程序或者动态链接库调用另外一个动态链接库中的变量或者函数的时候,必须将这些被调用函数或者变量导入。这些被导入的变量或函数是必须是被调用动态链接库导出表中的一个子集。也就是说,只有当这些变量或者函数被导出以后,才能被其他可执行程序或者动态链接库导入。
在PE文件中,我们将这些变量和函数统称为符号,这些被导入的符号的信息被存储在导入表中。在PE文件中,导入表位于.idata段中,该段可以单独存在。
使用工具dumpbin将可执行成DemoExe.exe中的内容导出,.idata段的部分内容如下所示:
//.idata段的信息摘要 SECTION HEADER #5 .idata name 1130 virtual size 1A000 virtual address (0041A000 to 0041B12F) 1200 size of raw data 7A00 file pointer to raw data (00007A00 to 00008BFF) 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags Initialized Data Read Write //idata段的二进制数据内容 RAW DATA #5 0041A000: 64 A0 01 00 00 00 00 00 00 00 00 00 B4 A5 01 00 d?..........′¥.. 0041A010: B0 A2 01 00 58 A1 01 00 00 00 00 00 00 00 00 00 °¢..X?.......... 0041A020: 30 AB 01 00 A4 A3 01 00 F4 A1 01 00 00 00 00 00 0?..¤£..??...... ……………. //导入表的信息 Section contains the following imports:
DemoDLLd.dll 41A2B0 Import Address Table //导入地址表地址 41A064 Import Name Table //导入名称表地址 0 time date stamp 0 Index of first forwarder reference //导出地址表数组下标 符号名称 6 ?GetOperTimes@@YAHXZ 4 ?Area@DemoMath@@QAEXN@Z 5 ?DivData@DemoMath@@QAEXNN@Z 8 ?SubData@DemoMath@@QAEXNN@Z 3 ?AddData@DemoMath@@QAEXNN@Z 0 ??0DemoMath@@QAE@XZ 1 ??1DemoMath@@QAE@XZ |
对于每一个可执行程序或者动态链接库,只要它调用了其他动态链接库中的符号,那么就会有一个或多个IMAGE_IMPORT_DESCRIPTOR类型的数组与之对应。该数组元素的个数与该可执行程序或者动态链接库所依赖的动态链接库的数量有关。
在IMAGE_IMPORT_DESCRIPTOR类型的数据结构中,存储的是与导入表相关的信息。该数据结构与其他数据结构发生关联,与该数据结构发生关联的数据实体包括:数据目录,字符串表,导入地址表,导入名称表。它们之间的关系如下图所示:
在可选头中包含了数据目录,在数据目录的第二项中,存储了指向导入表的位置的指针,以及该数据结构的大小。在数据目录的第十二项中,存储了指向第一个IAT的位置的指针,以及所有IAT数组的大小。
导入表是一个数组,该数据元素的类型是IMAGE_IMPORT_DESCRIPTOR。在该数组中,每一个数组元素都会对应一个被导入的DLL。在该数组的末尾,存储了一个所有的域为零的IMAGE_IMPORT_DESCRIPTOR类型的数组元素,表示该导入表结束。因此,在一个可执行程序或者动态链接库的导入表中,至少会包含两个数据元素。一个对应被导入符号的信息,一个所有域为零。
在每个导入表的数组的元素中,拥有两个地址字段,它们分别指向了导入地址表(IAT)和导入名称表(INT)。在导入地址表和导入名称表中,存储了被导入符号的名称或地址,它们是一个拥有多个数据元素的数组。因为导入表的数组元素可能是多个(当前可执行程序或动态链接库依赖多个其他的动态链接库),所以在一个动态链接库的导入信息中也会存在多个IAT数组以及INT数组。当PE文件被加载到内存以后,这些IAT数组会被存储在一块连续地内存区域中,它们首尾相连,中间没有空隙。在数据目录的第十二项中,存储了第一个IAT的位置,以及所有IAT数组的大小。导入表的结构布局如下图所示:
导入地址表和导入名称表的数据结构是一致的,它们均为IMAGE_THUNK_DATA类型,并以一个所有域均为零的数组元素结尾。导入地址表用于动态链接,而导入名称表用于符号地址绑定。在PE文件中,导入地址表和导入名称表中的数据内容是一样的,它们都存储着被导入符号的名称或者序号;在程序加载阶段,导入名称表的数据内容不会发生变化,而导入地址表中的被导入符号的序号或者名称将会被替换为该符号的真实虚拟内存地址。导入名称表的数据内容永远不会发生变化。
在WinNT.h头文件中,导入表被定义为IMAGE_IMPORT_DESCRIPTOR类型,具体的定义内容如下所示:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) }; 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; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; |
在该定义中,各个字段的详细解释如下表所示:
字段名称 |
类型 |
描述 |
Characteristics |
DWORD |
|
OriginalFirstThunk |
DWORD |
指向导入名称表的相对虚拟地址。 |
TimeDateStamp |
DWORD |
如果可执行文件不与被导入DLL绑定时,该值为0;如果以旧的样式绑定时,改值为时间戳;如果以新的样式绑定时,该值为-1。 |
ForwarderChain |
DWORD |
第一个被转送符号的索引,如果没有转送,则设定为-1。 |
Name |
DWORD |
该字段存储了一个相对虚拟地址。该地址指向字符串表中某个字符串,这个字符串是导入DLL的名称。 |
FirstThunk |
DWORD |
指向导入地址表的相对虚拟地址。 |
在WinNT.h头文件中,IAT以及INT被定义为IMAGE_THUNK_DATA类型,具体的定义内容如下所示:
typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE DWORD Function; // PDWORD DWORD Ordinal; DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32; |
从上面的定义可以看出,IAT数据结构是一个联合。在该联合中,每一个字段代表着在不同条件或者时间上,该数据结构可能存储不同意义的数据。在该定义中,各个字段的详细解释如下表所示:
字段名称 |
类型 |
描述 |
ForwarderString |
DWORD |
指向一个转向字符串的相对虚拟地址。转向导入的时候使用。 |
Function |
DWORD |
被导入符号的虚拟地址。动态链接完成以后写入该值。 |
Ordinal |
DWORD |
被导入符号在导出表中的序号。编译链接完毕后,该值可能会被写入到PE文件中。该值与AddressofData字段互斥。 |
AddressOfData |
DWORD |
指向一个IMAGE_IMPORT_BY_NAME类型的数据结构。编译链接完毕后,该值可能会被写入到PE文件中。该值与Ordinal字段互斥。 |
在WinNT.h头文件中,IMAGE_IMPORT_BY_NAME的定义内容如下:
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; |
在上面的定义中,各字段的详细解释如下表:
字段名称 |
类型 |
描述 |
Hint |
WORD |
导入符号最有可能的序号值。在动态链接的时候,当用符号名导入时,首先用Hint值在导出符号表中查找该符号,如果能够查找到,则命中;如果不能查找到,则使用符号名进行二分法查找。 |
Name |
BYTE |
符号名称的字符串 |
在IAT数组中,每一个数组元素都会对应一个被导入符号的地址,数组元素在不同的情况下有不同的含义。具体的含义形式有四种,它们分别是:
在不同的时间点上,或者在不同的情况下,被导出符号的地址用不同的形式表示。因此在数据结构定义的时候使用了联合。
在源程序被编译、链接完毕以后生成的PE文件中,以PE文件被加载到内存中,尚未执行动态链接的时候,在IAT中符号的地址以序号或者符号名称的形式表示;当执行完毕动态链接以后,使用符号的序号或者符号名称,通过对相关DLL导出表的查找,这些符号的序号或者符号的名称被替换成了符号的虚拟内存地址。
当IAT中的符号地址以序号或者符号名称表示的时候,可以通过DWORD值的最高位来区分是使用序号表示还是使用符号名称表示。如果最高位被置1,那么该符号地址使用序号的形式表示,DWORD值的低31位表示序号;如果最高为被置0,那么该符号地址使用符号名称的形式表示,DWORD值表示一个地址,指向了一个IMAGE_IMPORT_BY_NAME类型的数据结构。
在IMAGE_IMPORT_BY_NAME类型的数据结构中,包含了一个符号的名称字符串,以及一个WORD类型的提示值,该提示值指示了符号在导出表中最可能的序号。在符号查找的时候,如果使用Hint值查找不能命中,那么就会使用符号名称进行二分法查找。
在一个动态链接库文件中,总会有一些变量或者函数被声明为public,这些变量和函数被称为该动态链接库的接口,它们可以被其它可执行程序或者动态链接库调用。在该动态链接库中,并不是所有被声明为public类型的接口都能被其他可执行程序或动态链接库调用,只有当这些接口被导出以后才能被其他可执行程序或者动态链接库调用。
在PE文件中,我们将变量和函数统称为符号。这些被导出的符号的信息被存储在导出表。一般情况,导出表不会单独存在,在输出PE文件的时候,导出表可能会被合并到.rata段中,该段是只读数据段。
使用工具dumpbin将动态链接库DemoDlld.dll的内容导出,.rdata段的部分内容如下所示:
//只读段的摘要信息 SECTION HEADER #3 .rdata name 1D8A virtual size 17000 virtual address (10017000 to 10018D89) 1E00 size of raw data 5C00 file pointer to raw data (00005C00 to 000079FF) 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 40000040 flags Initialized Data Read Only //只读段的二进制数据内容 RAW DATA #3 10017000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 10017010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 10017020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ….. //导出表的信息 Section contains the following exports for DemoDLLd.dll
00000000 characteristics 51A86C37 time date stamp Fri May 31 17:24:07 2013 0.00 version 1 ordinal base 9 number of functions //符号地址表元素的数量 9 number of names //符号名称表元素的数量
ordinal hint RVA name //序号,数组下标,相对虚拟地址,符号名称 1 0 00011145 ??0DemoMath@@QAE@XZ 2 1 00011109 ??1DemoMath@@QAE@XZ 3 2 0001101E ??4DemoMath@@QAEAAV0@ABV0@@Z 4 3 000111C7 ?AddData@DemoMath@@QAEXNN@Z 5 4 0001116D ?Area@DemoMath@@QAEXN@Z 6 5 00011221 ?DivData@DemoMath@@QAEXNN@Z 7 6 000110B4 ?GetOperTimes@@YAHXZ 8 7 00011005 ?MulData@DemoMath@@QAEXNN@Z 9 8 0001114A ?SubData@DemoMath@@QAEXNN@Z |
对于每一个动态链接库,都会有一个IMAGE_EXEPORT_DIRECTORY类型的数据结构与之对应。该数据结构保存了与导出表有关的信息,并与其他数据结构发生关联。与该数据结构有关联的数据实体包括:数据目录,字符串表,符号地址表,符号名称表,名称序号对应关系表。这些实体之间的关系如下图所示:
可选头中包含了数据目录,数据目录的第一项指向了导出表。导出表是一个结构体类型,在该结构体的字段中保存了与导出表相关的信息,如:DLL名称字段指向了一个字符串表,在该字符串表中保存了DLL的名称;符号地址表地址字段指向了一个地址数组,该数组中保存了符号的地址;符号名称表地址字段指向了符号名称数组,该数组中保存了符号名称字符串的地址;名称序号对应关系表地址指向了名称序号对应关系的数组,该数组中存储了符号的名称与序号之间的对应关系。
符号地址表是一个DWORD类型的数组,在该数组中存储了被导出符号的相对虚拟地址,数组中每一个非零的数组元素都指向了一个符号的相对虚拟地址;符号名称表是一个DWORD类型的数组,数组中保存的是相对虚拟地址,该相对虚拟地址指向了字符串表中的相关位置,这些位置中保存了符号的名称;名称序号表是一个WORD类型的数组,该数组中保存了符号的序号。符号名称表中的元素与名称序号表中的元素通过数组下标对应。比如:在符号名称表中,数组下标为1的元素,它的序号存在在名称序号表中,数组下标也为1。
序号的计算公式为:序号 = 符号地址表数组下标 + Base字段值。在Windows16位时代,由于受到硬件大小的限制,在执行动态链接的时候,使用序号查找符号的地址,即:用序号的值减去Base的值,获得符号地址表数组的下标,进而获得符号的相对虚拟内存地址。这种方式节省了内存空间以及符号查找的时间,但是易读性差。随着时间的发展,当硬件的物理内存不在是问题的时候,开始使用符号名称查找符号的地址,具体的查找过程是:通过符号名称在名称序号对应关系表中查找到符号的序号,然后再用符号的序号查找符号的地址。虽然引入了符号名称表,但是这个表不是必须的,依然可以通过序号查找符号的地址。在一个DLL中,每一个导出符号都有一个唯一对应的序号,而导出符号名是可选的。
在动态链接的时候,可以通过两种方式进行符号地址的查找,一种是直接利用符号的序号直接查找,另外一种是利用符号的名称间接查找。在进行符号地址查找的时候,符号地址表,符号名称表,名称序号对应表之间的关系如下图所示:
在WinNT.h头文件中,导出表被定义为IMAGE_EXPORT_DIRECTORY类型,具体的定义内容如下:
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; |
在该定义中,各字段的详细解释如下表所示:
字段名称 |
类型 |
描述 |
Characteristics |
DWORD |
关于导出表的一些标记,目前没有被定义。 |
TimeDateStamp |
DWORD |
导出表被创建的时间。 |
MajorVersion |
WORD |
导出表的主版本号,目前没有使用,设为零。 |
MinorVersion |
WORD |
导出表的次版本号,目前没有使用,设为零。 |
Name |
DWORD |
一个相对虚拟地址,指向字符串表的一个位置,该位置存储了该Dll的名称。 |
Base |
DWORD |
通常设定为1,但也不是必须的。 序号 = Base + 符号地址数组下标。 |
NumberOfFunctions |
DWORD |
符号地址表中数据元素的个数。 |
NumberOfNames |
DWORD |
符号名称表以及符号名称序号对应关系表中数据元素的个数。由于符号名称表和符号名称序号对应关系表是一一对应的,所以它们的数组元素个数相同;该值可能会小于NumberOfFunctions,如果小于这个值,表示有一部分符号只通过序号对应,没有保存它们的名称。 |
AddressOfFunctions |
DWORD |
相对虚拟地址,指向符号地址表。 |
AddressOfNames |
DWORD |
相对虚拟地址,指向符号名称表。 |
AddressOfNameOrdinals |
DWORD |
相对虚拟地址,指向符号名称序号对应关系表。 |
在目标文件中,所有符号的地址都是基于文件头或者文件中某个位置的偏移地址。在将目标文件链接以后,在输出的PE文件中,这些偏移地址会被转化成相对虚拟内存地址,并且再加上一个默认内存加载位置的地址值,形成符号的基于默认内存加载位置的虚拟内存地址。基于默认内存加载位置的虚拟内存地址的计算公式为:
符号虚拟内存地址 = 默认内存加载位置 + 相对虚拟内存地址 |
可执行程序默认加载到内存的0x0400000位置,而动态链接库默认加载到内存的0x10000000位置。
当PE文件被加载到内存以后,如果该文件不能被加载到默认的内存位置,那么在指令代码中,所有使用绝对地址表示的符号的地址都需要被重定位。在Windows中,这一地址重定位的过程被叫做重定基地址。具体的操作过程是:在每一个需要进行地址重定位的符号处,将该符号当前地址的数值上再加上一个固定的数值,这个新获得的地址值就是该符号正确的虚拟内存地址。
这个固定值的计算公式是:
固定值 = DLL当前内存加载的位置 – DLL默认内存加载位置(0x10000000) |
地址重定位工作由操作系统的加载器来完成,在基地址重定位表中,记录了每一个需要进行地址重定位的符号的地址。在地址重定位的时候,加载器读取该表中的数值,然后查找到需要进行地址重定位的符号的位置,最后修正该符号的虚拟内存地址。
在PE文件被加载到内存以后,这些文件内容是以页为单位存储在内存中,每个内存页的大小是4KB。在基址重定位表中,数据表中的数据被分割成一个个数据块,每一个数据块会对应一个虚拟内存页,表示在该虚拟内存页中的符号的地址重定位信息。
使用工具dumpbin将DemoDlld.dll中的内容导出,涉及到基址重定位表的内容如下所示:
//基址重定位表的摘要信息 SECTION HEADER #7 .reloc name 524 virtual size 1C000 virtual address (1001C000 to 1001C523) 600 size of raw data 9A00 file pointer to raw data (00009A00 to 00009FFF) 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 42000040 flags Initialized Data Discardable Read Only //基址重定位表的二进制数据内容 RAW DATA #7 1001C000: 00 10 01 00 68 00 00 00 4F 35 76 35 9F 35 A4 37 1001C010: AC 37 24 38 2C 38 A4 38 AC 38 30 39 41 39 49 39 1001C020: C4 39 CC 39 D8 39 C6 3A D7 3A DD 3A EE 3A FD 3A ……………… //以下是对二进制内容的翻译。 BASE RELOCATIONS #7 //基址重定位表的数据块 相对虚拟地址为:11000,块大小为68,该数据块对应一个4KB大小的内存页 11000 RVA, 68 SizeOfBlock 低12位偏移 类型 符号的地址 符号名称 54F HIGHLOW 10019140 ?nOperTimes@@3HA (int nOperTimes) 576 HIGHLOW 100156AE 59F HIGHLOW 10019004 ___security_cookie ……. //下一个数据块,相对虚拟地址为:12000,块大小为F4,该数据块对应一个4KB大小的内存页 12000 RVA, F4 SizeOfBlock C HIGHLOW 10012010 18 HIGHLOW 1001201C 146 HIGHLOW 10015718 16F HIGHLOW 10019004 ___security_cookie ……… |
基址重定位表的结构如下图所示:
可选头中包含了数据目录,数据目录的第五项数据中包含了指向了基址重定位表的指针,以及基址重定位表的大小。基址重定位表以内存页的大小为依据进行分块,在每一个块中,都以IMAGE_BASE_RELOCATION类型的数据结构开头,后面跟随着每个符号的基地址重定位信息。这些符号的重定位信息是一系列的WORD值。这些WORD值的高4位指出了重定位的类型,而低12位是一个地址偏移。将该地址偏移数值与数据块的虚拟内存地址数值(即:IMAGE_BASE_RELOCATION. VirtualAddress)相加,可以得到该符号需要进行重定位的位置。
在基址重定位表的数据块中,所包含的重定位信息的个数的计算公式为:
重定位信息个数 = (块大小 – sizeof(IMAGE_BASE_RELOCATION))/2 因为块大小以字节为单位表示,而重定位信息以字为单位表示,转化成字需要除2 |
重定位的类型描述如下表:
类型 |
描述 |
IMAGE_REL_BASED_ABSOLUTE (0) |
这种不需操作;用于将块按32位边界对齐。位置应该为0。 |
IMAGE_REL_BASED_HIGH (1) |
重定位的高16位必须被用于被偏移量所指向的那个16位的WORD单元,此WORD是一个32位的DWORD的高位WORD。 |
IMAGE_REL_BASED_LOW (2) |
重定位的低16位必须被用于被偏移量所指向的那个16位的WORD单元,此WORD是一个32位的DWORD的低位WORD。 |
IMAGE_REL_BASED_HIGHLOW (3) |
重定位的全部32位必须应用于上面所说的全部32位。PE文件采用该类型。 |
IMAGE_REL_BASED_HIGHADJ (4) |
这种修正要求一个全32位值。高16位定位于偏移量处,低16位定位在下一个数组元素(此数组元素包括在大小的域中)的偏移量处。它们两个需要被连成一个有符号的变量。加上32位的增量。然后加上0x8000 并将有符号变量的高16位存储在偏移量处的16位域中。” |
IMAGE_REL_BASED_MIPS_JMPADDR (5) |
|
IMAGE_REL_BASED_SECTION (6) |
|
IMAGE_REL_BASED_REL32 (7) |
|
在WinNT.h头文件中,基址重定位表被定义为IMAGE_BASE_RELOCATION类型,具体的定义内容如下:
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION; |
在该定义中,各字段的详细解释如下:
字段名称 |
类型 |
描述 |
VirtualAddress |
DWORD |
数据块的虚拟内存地址 |
SizeOfBlock |
DWORD |
数据块的大小 |
在以下内容中,将以一个示例的形式来说明如何查找符号重定位的位置。使用工具dumpbin将DemoDlld.dll的基址重定位表的内容导出,其中一个数据块的内容如下:
BASE RELOCATIONS #7 //基址重定位表的数据块 相对虚拟地址为:11000,块大小为68,该数据块对应一个4KB大小的内存页 11000 RVA, 68 SizeOfBlock 低12位偏移 类型 符号的地址 符号名称 54F HIGHLOW 10019140 ?nOperTimes@@3HA (int nOperTimes) 576 HIGHLOW 100156AE 59F HIGHLOW 10019004 ___security_cookie …….
|
在上面的内容中,红色信息表示引用了符号nOperTimes处的地址需要被重定位,该引用形式必然是使用了绝对地址。
重定位地址的计算公式为:默认加载位置 + 数据块相对虚拟内存地址 + 偏移 = 0x10000000 + 0x11000 + 0x54F = 0x1001154F。处于虚拟内存地址0x1001154F处的地址值需要被重定位。
将DemoDlld.dll的内容导出为汇编格式,与地址0x1001154F相关的内容如下:
?GetOperTimes@@YAHXZ: 10011530: 55 push ebp 10011531: 8B EC mov ebp,esp 10011533: 81 EC C0 00 00 00 sub esp,0C0h 10011539: 53 push ebx 1001153A: 56 push esi 1001153B: 57 push edi 1001153C: 8D BD 40 FF FF FF lea edi,[ebp-0C0h] 10011542: B9 30 00 00 00 mov ecx,30h 10011547: B8 CC CC CC CC mov eax,0CCCCCCCCh 1001154C: F3 AB rep stos dword ptr es:[edi] 1001154E: A1 40 91 01 10 mov eax,dword ptr [?nOperTimes@@3HA] 10011553: 5F pop edi 10011554: 5E pop esi 10011555: 5B pop ebx 10011556: 8B E5 mov esp,ebp 10011558: 5D pop ebp 10011559: C3 ret |
上面红色字体标记出了关键代码行,绿色的字体是需要被重定位的地址值,该地址的当前值为0x10019140。该值的第一个字节正好对应地址0x1001154F,这就是需要被重定位的位置。该值是符号nOperTimes在PE文件中被分配的虚拟内存地址,由于在此处使用了绝对地址的形式,所以当PE文件被加载到内存以后,该符号的地址需要被重定位。
假设DemoDlld.dll被加载到了内存位置为:0x20000000,那么该地址值将被修正为:0x20000000 – 0x10000000 + 0x10019140 = 0x20019140。
在PE文件中,各个数据结构之间的关系如下图所示: