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个字节表示字符串的长度,后面的紧跟字符串的内容,它以字节为单位,以’\0’作为字符串的结束符。这里的字符串长度不仅仅是字符串自身的长度(字符串内容+’\0’),还包括前面4个字节的该数据自身的长度。
2.2.9各数据结构之间的关系
在COFF文件所包含的数据结构中,各个数据结构之间的关系如下图所示:
重定位表和符号表之间通过符号表的索引进行关联;在文件头中保存了可选头的大小和段表所包含项目的数量,通过计算可以确定段表的起始位置和结束位置。段表起始位置=文件头大小+可选头大小;其他关系通过相对文件首位置的偏移表示。
2.3Lib文件的结构
2.3.1总体结构图
静态链接库就是一组目标文件的集合,当执行静态链接的时候,被选定的目标文件的内容就会被合并到相关的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;
|
2.4PE文件的结构
2.4.1总体结构图
从文件内容上来看,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”表示导出所有内容,命令选项“>”表示将导出的内容存储到文件中。
2.4.2 DOS头
2.4.2.1DOS MZ 头
所有 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头在文件中的位置。
2.4.2.2DOS Stub
DOS Stub实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串“This program requires Windows”,或者程序员可根据自己的意图实现完整的DOS代码。大多数情况下DOS Stub由汇编器/编译器自动生成。
2.4.3 PE头
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可选头的信息。
2.4.3.1PE头标识
在一个有效的PE文件中,PE头标识字段的值是0x00004550,用ASCII表示就是“PE00”。 #define IMAGE_NT_SIGNATURE定义了这个值。
2.4.3.2文件头
见2.2.2节描述。PE中的文件头与COFF中的文件头定义一致。
2.4.3.3可选头
在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
|
|
请发表评论