2.1概述
在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+。
2.2COFF文件的结构
2.2.1总体结构图
COFF文件的总体结构如下图所示:
从文件内容上来看,COFF文件由二进制数据组成。这些二进制数据从文件的零位置开始,依次存储,直到文件末尾。从数据结构的角度来看,这些二进制数据又分别属于不同的结构体或者结构体数组。这些结构体被定义在“WinNT.h”头文件中。
在COFF文件中,这些结构体或结构体数组分别表示不同的含义,记录着COFF文件中的不同内容。从文件的顶端开始,依次存储了文件头,可选头,段表,段数据,重定位表,行号表,符号表,以及字符串表的信息。这些结构体数据之间存在关联关系。比如:文件头信息中存储了符号表的开始位置,以及段表中数组元素的个数;在段表中存储了各个段的位置,重定位表的位置,行号表的位置;重定位表中的项会关联到符号表中的某个符号;而符号表中某个符号的名称可能会存储在字符串表中。
使用dumpbin工具可以将目标文件的内容导出,具体的命令格式如下:
Dumpbin /all DemoMath.obj >DemoMath.txt |
在上面的命令中,将目标文件“DemoMath.obj”的所有内容导出到文本文件“DemoMath.txt”中。命令选项“/all”表示导出所有内容,命令选项“>”表示将导出的内容存储到文件中。
2.2.2文件头
文件头以一个结构体的形式存储在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 文件。 |
2.2.3可选头
该数据结构为可选数据,在目标文件中不存在此数据结构。只有当COFF文件作为可执行文件存在的时候,该数据结构才有意义。
2.2.4段表
段表是各个段的目录,用于检索各个段的信息。它以结构体数组的形式存储在可选头或者文件头的后面。在段表中,每一项的大小是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文件中被设置。 |
2.2.5重定位表
在编译阶段,将某些源文件编译成目标文件的时候,在目标文件中,某些被调用函数或者数据的位置是无法确定的。这时候,编译器将这些被调用的函数或者数据的地址设定为一个默认的假值。在链接阶段,当能够确定这些被调用函数或数据的地址的时候,再用真实的地址来替换这些假值。我们将这个过程叫做重定位。
使用工具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位相对地址。在执行重定位的时候,对于绝对地址类型,将被替换为符号的绝对地址;而对于相对地址类型,将被替换为符号的相对地址,即:符号相对于被修正位置的地址差。 |
2.2.6行号表
行号表描述了二进制代码与源代码行号之间的关系,调试阶段使用。在“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 |
行号 |
2.2.7符号表
在编译阶段的词法分析过程中,编译器扫描整个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…) |
符号为全局变量/函数,符号定义在当前文件中,值表示符号在段内偏移。 |
2.2.8字符串表
字符串表用来保存长度大于8个字节的符号名称。字符串表的前4个字节表示字符串的长度,后面的紧跟字符串的内容,它以字节为单位,以’