注:Little-Endian,记不得它的中文名称了。它是指数据的排列方式。好比:十六进制的0x1234以Little-Endian方式在内存中的顺序为0x34 0x12。与之相反的是Big-Endian,这种方式下,在内存中的顺序是0x12 0x34。
这个表的内容并不全面,但在目标文件中,经常使用的也就只有这些。其它的标记我将在之后介绍PE格式时给出。
可选头
可选头接在文件头的后面,也就是从COFF文件的0x0014偏移处开始。长度能够为0。不一样长度的可选头,其结构也不一样。标准的可选头长度为24或28字节,一般是28啦。这里我就只介绍长度为28的可选头。(由于这种头的长度是自定义的,不一样的人定义的结果就不同,我只能选一种最经常使用的头来介绍,别的我也不知道)
这种头的结构以下:
typedef struct {
unsigned short usMagic; // 魔法数字
unsigned short usVersion; // 版本标识
unsigned long ulTextSize; // 正文(text)段大小
unsigned long ulInitDataSZ; // 已初始化数据段大小
unsigned long ulUninitDataSZ; // 未初始化数据段大小
unsigned long ulEntry; // 入口点
unsigned long ulTextBase; // 正文段基址
unsigned long ulDataBase; // 数据段基址(在PE32中才有)
} OPTHDR;
第一个成员usMagic仍是魔法数字,不过这回它的值应该为0x010b或0x0107。当值为0x010b时,说明COFF文件是一个通常的可执行文件;当值为,0x0107时,COFF则为一个ROM镜像文件。
usVersion是COFF文件的版本,ulTextSize是这个可执行COFF的正文段长度,ulInitDataSZ和ulUninitDataSZ分别为已初始化数据段和未初始化数据段的长度。
ulEntry是程序的入口点,也就是COFF载入内存时正文段的位置(EIP寄存器的值),当COFF文件是一个动态库时,入口点也就是动态库的入口函数。
ulTextBase是正文段的基址。
ulDataBase是数据段基址。
其实在这些成员中,只要注意usMagic和ulEntry就能够了。
段落头
段落头紧跟在可选头的后面(若是可选头的长度为0,那么它就是紧跟在文件头后)。它的长度为36个字节,以下:
typedef struct {
char cName[8]; // 段名
unsigned long ulVSize; // 虚拟大小
unsigned long ulVAddr; // 虚拟地址
unsigned long ulSize; // 段长度
unsigned long ulSecOffset; // 段数据偏移
unsigned long ulRelOffset; // 段重定位表偏移
unsigned long ulLNOffset; // 行号表偏移
unsigned short ulNumRel; // 重定位表长度
unsigned short ulNumLN; // 行号表长度
unsigned long ulFlags; // 段标识
} SECHDR;
这个头但是个重要的头头,咱们要用到的最终信息就由它来描述。一个COFF文件能够不要其它的节,但文件头和段落头这两节是必不可少的。
cName用来保存段名,经常使用的段名有.text,.data,.comment,.bss等。.text段是正文段,一般也就是代码段;.data是数据段,在这个数据段中所保存的数据是初始化过的数据;.bss段也能够用来保存数据,不过这里的数据是未初始化的,这个段也是一个空段;.comment段,看名字也知道,它是注释段,用来保存一些编译信息,算是对COFF文件的注释。
ulVSize是段数据载入内存时的大小。只在可执行文件中有效,在目标文件中总为0。若是它的长度大于段的实际长度,则多的部分将用0来填充。
ulVAddr是段数据载入或链接时的虚拟地址。对于可执行文件来讲,这个地址是相对于它的地址空间而言。当可执行文件被载入内存时,这个地址就是段中数据的第一个字节的位置。而对于目标文件而言,这只是重定位时,段数据当前位置的一个偏移量。为了计算方便,便定位的计算简化,它一般设为0。
ulSize这才是段中数据的实际长度,也就是段数据的长度,在读取段数据时就由它来肯定要读多少字节。
ulSecOffset是段数据在COFF文件中的偏移量。
ulRelOffset是该段的重定位信息的偏移量。它指向了重定位表的一个记录。
ulLNOffset是该段的行号表的偏移量。它指向的是行号表中的一个记录。
ulNumRel是重定位信息的记录数。从ulRelOffset指向的记录开始,到第ulNumRel个记录为止,都是该段的重定位信息。
ulNumLN和ulNumRel类似。不过它是行号信息的记录数。
ulFlags是该段的属性标识。其值以下表:
值 |
名称 |
说明 |
0x0020 |
STYP_TEXT |
正文段标识,说明该段是代码。 |
0x0040 |
STYP_DATA |
数据段标识,有些标识的段将用来保存已初始化数据。 |
0x0080 |
STYP_BSS |
< FONT>有这个标识段也是用来保存数据,不过这里的数据是未初始化数据。 |
注意,在BSS段中,ulVSize、ulVAddr、ulSize、ulSecOffset、ulRelOffset、ulLNOffset、ulNumRel、ulNumLN的值都为0。(上表只是部分值,其它值在PE格式中介绍,后同)
段数据
“人”如其名,这里是保存各个段的数据的位置。不一样类型的段,数据的内容、结构也不尽相同。但在目标文件中,这些数据都是原始数据(Raw Data)。不存在什么特别的格式。
重定位表
这个表所保存的是各个段的重定位信息。这是一张很大的表,由于全部段的重定位信息都在这个表里。各个段落头记录了本身的重定位信息的偏移和数量。要用到重定位信息时就到这个表里来读。固然,你也能够把整个重定位表当作多个重定位表,每一个段落都有一个本身的重定位表。这个表只在目标文件中有,可执行文件中是不存在这个表的。
既然有表,那么就会有记录。重定位表中的每一条记录就是一条重定位信息。这个记录的结构很简单,以下:
typedef struct {
unsigned long
ulAddr;
// 定位偏移
unsigned long
ulSymbol;
// 符号
unsigned short usType;
// 定位类型
} RELOC;
有够简单吧,一共只三个成员!ulAddr是要定位的内容在段内偏移。好比:一个正文段,起始位置为0x010,ulAddr的值为0x05,那你的定位信息就要写在0x15处。并且信息的长度要看你的代码的类型,32位的代码要写4个字节,16位的就只要字2个字节。
ulSymbol是符号索引,它指向符号表中的一个记录。注意,这里是索引,不是偏移!它只是符号表中的一个记录的记录号。这个成员指明了重定位信息所对映的符号。
usType是重定位类型的标识。32位代码中,一般只用两种定位方式。一是绝对定位,二是相对定位。其代码以下:
值 |
名称 |
说明 |
6 |
RELOC_ADDR32 |
32位绝对定位。 |
20 |
RELOC_REL32 |
32位相对定位。 |
对于不一样的处理器,这些值也不尽相同。这里给出的是i386平台上最经常使用的两个种定位方式的标识。
其定位方式以下:
绝对定位
在绝对定位方式下,你要给出符号的绝对地址(注意,有时候这里可能不是地址,而是值,对于常量来讲,你不用给出它的地值,只用给出它的值)。固然,这个地址也不是现成的,你要用符号的相对地址+它所在段的相对地址来获得它的绝对地址。
公式:符号绝对地址=段偏移+符号偏移
这些偏移量你要分别从段落头和符号表中获得。当段落要重定位时,固然还要先重定位段落,才能定位其中的符号。
相对定位
相对定位要复杂一些。它所要的地址信息是相对于当前位置的偏移,这个当前位置就是ulAddr所指向的这个偏移的绝对地址后四个字节(32位代码是四个字节,16位是两个字节)的位置。也就是用定位偏移+当前段偏移+机器字长÷8
公式:当前地址=定位偏移+当前段偏移+机器字长÷8
有了当前地址,相对地址就好计算了。只要用符号的绝对地址减去当前地址就能够了。
公式:相对地址=符号绝对地址-当前地址
计算好了地址,把它写到ulAddr所指向的位置,就一切OK!你已经完成了重定位的工做了。
行号表
行号表在调试时颇有用。它把可执行的二进制代码与源代码的行号之间创建了对映关系。这样,当程序执行不正确时(其实正确的也能够
J
),咱们就能够根据当前执行代码的位置得知出错源代码的行号,再加以修改。若是没有它的话,鬼才知道是哪一行出了毛病!
它的格式也很简单。只有两个成员,以下:
typedef struct {
unsigned long ulAddrORSymbol;
// 代码地址或符号索引
unsigned short usLineNo;
// 行号
} LINENO;
让咱们先看第二个成员,usLineNo。这是一个从1开始计数的计数器,它表明源代码的行号。第一个成员ulAddrORSymbol在行号大于0时,表明源代码的地址;而当行号为0时,它就成了行号所对映的符号在符号表中的索引。下面让咱们来看看符号表吧!
符号表
符号表是对象文件中用来保存符号信息的一张表,也是COFF文件中最为复杂的一张表。全部段落使用到的符号都在这个表里。它也是由不少条记录组成,每条记录都以以下结构保存:
typedef struct {
union {
char cName[8];
// 符号名称
struct {
unsigned long ulZero;
// 字符串表标识
unsigned long ulOffset;
// 字符串偏移
} e;
} e;
unsigned long ulValue;
// 符号值
short iSection;
// 符号所在段
unsigned short usType;
// 符号类型
unsigned char usClass;
// 符号存储类型
unsigned char usNumAux;
// 符号附加记录数
} SYMENT;
cName符号名称,和前面全部的名称同样,它也是8个字节,但不一样的是它在一个联合体中。和它占相同的存储空间的还有ulZero和ulOffset这两个成员。若是符号的名称只有8个字符,那很好,能够直接放到这个cName中;但是,若是名称的长度大于8个字节,这里就放不下了,只好放到字符串表中。这时候,ulZero的值就会为0,而在ulOffset中会给出咱们所用的符号的名称在字符串表中的偏移。
一个符号有了名称不够,它还要有值!ulValue就是这个符号所表明的值。
iSection成员指出了这个符号所在的段落。若是它的值为0,那么这个符号就是一个外部符号,要从其它的COFF文件中解析(链接多个目标文件就是要解析这种符号)。当它的值为-1时,说明这个符号的值是一个常量,不是它在段落中的偏移。而当它的值为-2时,这个符号只是一个调试符号,只有在调试时才会用到它。当它大于0时,才是符号所在段的索引值。
usType是符号的类型标识。它用来讲明这个符号的类型,是函数?整型?仍是其它什么。这个标识是两个字节。
低字节的低四位是基本标识,它指出了符号的基本类型,如整型,字符,结构,联合等。高四位指出了符号的高级类型,如指针(0001b),函数(0010b),数组(0011b),无类型(0000b)等。如今的编译器,一般不使用基本类型,只使用高级类型。因此,符号的基本类型一般被设为0。
高字节一般未用。
usClass是符号的存储类型标识。它指明了符号的存储方式。
其值与意义见下表:
值 |
名称 |
说明 |
NULL |
0 |
无存储类型。 |
AUTOMATIC |
1 |
自动类型。一般是在栈中分配的变量。 |
EXTERNAL |
2 |
外部符号。当为外部符号时,iSection的值应该为0,若是不为0,则ulValue为符号在段中的偏移。 |
STATIC |
3 |
静态类型。ulValue为符号在段中的偏移。若是偏移为0,那么这个符号表明段名。 |
REGISTER |
4 |
寄存器变量。 |
MEMBER_OF_STRUCT |
8 |
结构成员。ulValue值为该符号在结构中的顺序。 |
STRUCT_TAG |
10 |
结构标识符。 |
MEMBER_OF_UNION |
11 |
联合成员。ulValue值为该符号在联合中的顺序。 |
UNION_TAG |
12 |
联合标识符。 |
TYPE_DEFINITION |
13 |
类型定义。 |
FUNCTION |
101 |
函数名。 |
FILE |
102 |
文件名。 |
最后一个成员usNumAux是附加记录的数量。附加记录是用来描述符号的一些附加信息,为了便于保存,这些附加记录一般选择成为一条符号信息记录的整数倍(多数为1)。因此,若是这个成员的值为1,那么就表示在当前符号信息记录后附加了一条记录,用来保存附加信息。
附加信息的结构是与符号的类型以及存储类型相关的。不一样的类型的符号,其附加信息(若是有的话)的结构也不一样。若是你不在乎这些内容,也能够把它们乎略。
当段的类型为FILE时,附加信息就是一个字符串,它是目标文件对应源文件的名称。其它类型在介绍PE时再进行详细讨论。
字符串表
不用多说,瞎子也能看出这个表是用来保存字符串的。它紧接在符号表后。至于为何要保存字符串,前面已经说过了。这里就再也不多说了,只说说字符串的保存格式。
字符串表是全部节中最简单一节。以下图:
0 4
字符串表长度 |
字符串1\0 |
.... |
字符串n\0 |
字符串表的前四个字节是字符串表的长度,以字节为单位。其后就是以0结尾的字符串(C风格字符串)。要注意的是,字符串表的长度不只仅是字符串的长度(这个长度要包括每一个字符串后的‘\0’)的总合,它还包括这个长度域的四个字节。符号表中ulOffset成员所指出的偏移就是从字符串表起始处的偏移。好比:指像每个字符串的符号,ulOffset的值总为4。
下面给出的代码,是从字符串表中读取字符串的典型C代码。
int iStrlen,iCur=4; // iStrLen是字符串表的长度,iCur是当前字符串偏移
char *str; // 字符串表
read(fn, &iStrlen, 4); // 获得字符串表长度
str = (char *)malloc(iStrlen); // 为字符串表分配空间
while (iCur<iStrlen ) // 读字符串表,直到所有读入内存
iCur+=read(fn, str+iCur, iStrlen- iCur);
iCur=4; // 把当前字符串偏移指到每个字符串
while (iCur<iStrlen ) { // 显示每个字符串
printf("String offset 0x%04X : %s\n", iCur, str + iCur);
iCur+=(strlen(str+iCur)+1); // 计算偏移时不要忘了计算‘\0’字符所占的1个字节!
}
free(str); // 释放字符串表空间
直到这里,整个COFF的结构已经所有介绍完了。不少了解PE格式的朋友必定会奇怪,好像少了不少内容!?是的,标准的COFF文件只有这么多的东西。但MS为了和DOS的可执行文件兼容,以及对可执行文件功能的扩展,在COFF格式中加了不少它本身的标准。让我差点就认不出COFF了。但了解了COFF文件之后,再来学习PE文件的格式,那就很简单了。 想了解PE文件的格式?网上有不少它的资料,我将在本文的基础上再写几篇文章,分别介绍PE,OMF以及ELF的格式。 如今你们能够本身动手,写一个COFF文件解析器或是一个简单的链接程序了!