COFF
COFF代表通用对象文件格式(Common Object File Format)。 它是一种用于存储编译代码的文件格式,例如编译器或链接器输出的代码。
与大多数编译器文件格式一样,COFF在文件中定义了结构,用于存储有关程序各个部分的信息,例如.text和.data,以及关于程序声明或定义的符号。
COFF可用于存储单个函数或符号、程序片段、库或整个可执行文件。
Microsoft PE可执行格式(严格意义上的PE/COFF)包含COFF的一个版本。
尽管COFF仍然存在于今天,但它被认为过于复杂,并且在许多情况下,它的使用已被 ELF 或其他可执行文件格式所取代。
Microsoft PE/COFF
- 正文: PE
大多数Microsoft EXE和DLL文件使用基于COFF的文件格式。 微软的可移植可执行文件(PE/COFF)标准在很大程度上与COFF规范兼容,但有几个值得注意的例外。
首先,如果文件是图像 (可执行) 文件,则COFF文件头不会放置在文件的开头。 简言之,PE/COFF图像包含一个位于偏移量0x3C处的指针,该指针指向PE头。 COFF文件头紧跟在此PE签名之后。 更多信息可以在 PE 文章中找到。
其次,PE/COFF格式还改变了一些COFF魔法值(包括文件头),并赋予它们特殊的含义。
有关更多信息,请参阅Microsoft可移植可执行文件和通用目标文件格式规范文档。
文件格式
COFF文件包含许多不同的表,以及这些表引用的数据区域。
结构 | 意图 | 位置 | 长度 |
---|---|---|---|
File Header | 存储有关文件的基本信息以及指向其他结构的指针。 | 在文件的开头,Microsoft PE/COFF映像文件除外 (请参见上文)。 | 固定结构长度。 |
Optional Header | 存储有关文件执行的其他信息。 | 仅当文件头指示时才显示。紧跟在文件头之后。 | 在文件头中指定 |
Section Header | 存储有关文件中定义的不同节的信息。 | 紧跟在可选头部之后。如果未提供可选标头,则紧跟在文件标头之后。 | 通过节数 (在文件头中指定) 乘以固定结构长度来计算。 |
Section Relocation Table | 通过提供需要更改的地址信息,允许将文件重新定位到内存的任何区域。 | 每个节最多存在一个表。位置由相应的节头指示。 | 通过重新定位条目的数量(在节头中指定)乘以固定结构长度计算。 |
Section Line Number Table | 提供调试信息,将代码地址映射到源文件行号。 | 每个节最多有一个表。 位置由相应的区段标题指示。 | 通过行号条目的数量 (在节头中指定) 乘以固定结构长度来计算。 |
Symbol Table | 存储代码定义或声明的每个符号的信息。 | 指向文件头中的指针。 | 通过符号数 (在文件头中指定) 乘以固定结构长度来计算。 |
String Table | 存储任何长度超过8个字符的节或符号名称。 | 紧接符号表。 | 由字符串表的前32位表示,计算为long类型 |
大多数COFF文件在SECTION表的末尾和存储节本身的数据的符号表的开始之间包含额外的空间,尽管该数据相对于符号(和字符串)表的位置似乎没有由标准指定。
File Header
File Header位于COFF文件的开头(如上所述的Microsoft PE/COFF映像文件除外)。 它包含一个幻数字段和有关该文件的其他一般信息。
此标头的结构可以在filehdr.h中找到,长度为20字节。
{
unsigned short f_magic; /* Magic number */
unsigned short f_nscns; /*节数目*/
long f_timdat; /*时间和日期戳*/
long f_symptr; /* 文件指向符号表的指针 */
long f_nsyms; /*符号数*/
unsigned short f_opthdr; /* sizeof(Optional Header) */
unsigned short f_flags; /* Flags */
}
幻数因实现而异,例如,DJGPP在此字段中生成值为0x14C的COFF文件。
节的数量表示在节表中应该期望有多少节结构。
时间和日期戳指示COFF文件的创建时间。 此字段的类型为time_t,是自公元(1970-01-01 00:00:00 GMT)以来的秒数。
符号表指针指示符号表开始的文件内的偏移量。
符号数表示符号表中有多少(固定长度)符号结构。 该值与符号表指针相结合,让你计算字符串表的起始位置。
flags字段通常指示文件的状态。 通常,这用于快速查找可以从文件的其余部分计算出来的信息。 例如,该字段的一位指示是否已从文件移除重定位信息,另一位指示是否已解析所有外部符号,等等。
Optional Header
可选标头提供有关文件的运行时信息,因此仅存在于可执行的COFF文件中。
仅当File Header字段f_opthdr为非零时,此文件头才会出现在文件中,如果存在,则紧跟在File Header之后。
此标头的结构可以在aouthdr.h中找到,并且长度为28字节。
{
unsigned short magic; /* Magic Number */
unsigned short vstamp; /* Version stamp */
unsigned long tsize; /*文本大小(字节)*/
unsigned long dsize; /*初始化数据大小*/
unsigned long bsize; /* 未初始化的数据大小 */
unsigned long entry; /* Entry point */
unsigned long text_start; /*用于此文件的文本的基地址*/
unsigned long data_start; /*此文件使用的数据基地址*/
}
Section Header
节表(Section table)提供有关文件中包含的各个节的信息。
节表是节头(Section header)结构的数组。 它紧跟在可选标头之后(如果不存在可选标头,则紧跟在文件标头之后),其在部分标头结构中的长度由文件标头中的f_nscns字段给出。
与大多数其他表不同,节表是基于1的,这意味着第一节被称为节1,而不是节0。 之所以这样做,是因为在符号表中,节号0具有特殊意义。
该报头的结构可以在scnhdr.h中找到,并且是40字节长。
{
char s_name[8]; /* 节名称 */
long s_paddr; /*物理地址*/
long s_vaddr; /*虚拟地址*/
long s_size; /* 以字节为单位的节大小 */
long s_scnptr; /* 到节数据文件偏移*/
long s_relptr; /* 此节重定位表的文件偏移量 */
long s_lnnoptr; /* 此节的行号表的文件偏移量 */
unsigned short s_nreloc; /* 重新定位表项的数量 */
unsigned short s_nlnno; /* 行号表条目数 */
long s_flags; /* 此部分的标志 */
}
如果节的名称不超过八个字符,则节名称字段存储该节的名称,否则该字段包含指向字符串表的指针。 请参阅下面字符串表中的条目。
物理地址字段存储此文件的内存起始地址。 如果文件已链接,以便将此节加载到内存中的特定位置,则此字段将被设置为该值。
虚拟地址字段相当于虚拟内存空间的物理地址字段。 但是,在几乎所有实例中,这两个字段都包含相同的值。
s_size字段包含以字节为单位的节大小。
节指针字段包含此节的原始数据开始的文件偏移量。 这与节大小相结合,可用于加载节数据。 如果这是一个BSS节(在节的标志字段中指示),该节可能有一个大小,但节指针为零。(译者注:值在载入文件时动态生成,无需存储在文件中)
s_relptr表示此节的重定位信息的文件偏移量。 重新定位表中的条目数在s_nreloc字段中给出。 如果s_nreloc字段为零,则该部分不包含任何重新定位信息。
与RELOCATION指针类似,s_lnnoptr字段指示段的行号表的文件偏移量,其条目长度在s_ninno字段中给出。 与s_nreloc一样,如果s_nlnno为零,则该部分不包含行号信息。
Flags字段提供了有关处理节时应如何处理的重要信息。 最重要的标志值与字段类型相关,如下所示:
值 | 定义 | 设置1的含义 |
---|---|---|
0x0020 | STYP_TEXT | 该节包含可执行代码。 |
0x0040 | STYP_DATA | 该节包含初始化数据。 |
0x0080 | STYP_BSS | COFF文件不包含此节的数据,但需要为其分配空间。 |
Relocation Entries
使用重定位条目,可以将一段代码加载到任意内存地址中,然后修改代码,以使符号的内存地址正确。 这意味着你可以获取COFF文件,将文件的.text部分加载到一个位置,将.data部分加载到另一个位置,再将.bss部分加载到其他位置,然后更新.text部分,以便代码能够找到它的值并正确运行。
重定位表是重定位条目结构的数组,并且是从零开始的。
COFF文件可以包含每个节的重新定位表。 .text(STYP_TEXT)节通常具有位置调整信息,.Data(STYP_DATA)和其他节也可能具有位置调整信息。
节的重定位表将其位置和长度存储在节的头中。
此标头的结构可以在reloc.h中找到、 长度为10字节。
{
long r_vaddr; /* 引用地址(Reference Address)*/
long r_symndx; /* 符号索引(Symbol index) */
unsigned short r_type; /* 重定位类型(Type of relocation) */
}
地址(Address)字段给出节的原始数据中地址开始处的偏移量。 引用的(reference)几乎总是从给定地址开始的32位值,但这取决于实现和r_type字段。
符号索引给出了引用引用的符号表中的(从零开始的)索引。 一旦你将COFF文件加载到内存中并知道每个符号的位置,你就可以找到给定符号的新的重定位地址,并相应地更新引用。
r_type指示应该如何重定位地址。 不幸的是,它是针对具体实现的。 例如,DJGPP可以指示这是符号的32位绝对地址(类型6),或者它可以指定相对于引用位置的32位地址(类型20)。
Line Number Entries
调试程序使用行号条目将符号和物理指令地址与源文件行号相关联。
行号表是行号条目结构的数组,以零为基础。
COFF文件可以包含每个节的行号表,但是查找除.text(Styp_Text)节之外的任何其他节的行号信息是很少见的。
与重定位表一样,节的行号表的位置和长度存储在节的标题中。
此标题的结构可以在linenum.h中找到、 长度为6字节。
{
union
{
long l_symndx; /* 符号索引(Symbol Index)*/
long l_paddr; /* 物理地址(Physical Address) */
} l_addr;
unsigned short l_lnno; /*行号*/
}
简而言之,行号表将包含行号为零的条目,其中l_symndx字段将指示符号表中的函数名称符号。 此条目之后将是带有递增行号的其他条目,这些行号通过l_paddr字段指示该行开始的部分的字节偏移量。 有鉴于此,COFF文件处理过程中发生的异常可以追溯到函数和该函数中的行号。
Symbol Table
符号表(Symbol Table)包含有关此文件声明或定义的每个符号的信息。 对于每个函数名,对于每个全局变量,对于每个静态变量,对于每个定义为extern的变量,此表中都会有符号等。 此表中甚至还有一些符号,指示每个部分的开头,以及关于文件整体的更多信息。
符号表是符号条目结构的数组,并且是从零开始的。
符号表位置在文件头f_symptr字段中给出,其在条目中的长度在文件头f_nsyms字段中给出。
这个标题的结构可以在syms.h中找到、 长度为18字节。
{
char n_name[8]; /*符号名称(Symbol Name)*/
long n_value; /* 符号的值 */
short n_scnum; /* 节号*/
unsigned short n_type; /*符号类型(Symbol Type)*/
char n_sclass; /* 存储类(Storage Class) */
char n_numaux; /* 辅助计数(Auxiliary Count) */
}
如果该符号不超过八个字符,则Symbol name字段存储该符号的名称,否则该字段包含指向字符串表的指针。 参见下面字符串表上的条目。
符号表中的每个符号后面都可以有零个或多个辅助条目,这些条目提供有关该符号的更多信息。 这些条目中的每个条目都与符号表条目的长度相同,但根据父符号的不同具有不同的结构。
“节编号”字段n_scnum表示此符号所属的节。 如果该值大于零,则表示节表中的节(因此节表是基于一的)。 还可以采用以下特定值之一:
n_scnum Value | 含义 |
---|---|
N_DEBUG (2) | 调试符号。在下面的示例中,有关文件的信息已被放入如下符号中。 |
N_ABS (1) | 绝对符号。这意味着n_value字段的值是绝对数值。 |
N_UNDEF (0) | 未定义的外部符号(undefined external symbol) |
表中的n_value、n_scnum和n_sclass字段需要一起考虑,因为它们是相互关联的。 下表给出了这些字段的一些更常见的组合。 (下表中的大部分信息来自观察,而非参考来源。 它是不完整的,可能不准确。)
n_sclass | n_scnum | n_value | n_value的意义 | 典型用法 |
---|---|---|---|---|
C_EXT (2) | 0 | 0 | 未解析的外部符号 | |
0 | >0 | 变量的大小 | 未初始化的全局变量(未包含在BSS中) | |
.text | Any | 节内偏移 | 函数入口点 | |
.data | Any | 节内偏移 | 初始化全局变量 | |
C_STAT (3) | .text, .data, .bss | 0 | 表示节开始的节符号 | |
.data | Any | 节内偏移 | 初始化静态变量 | |
.bss | Any | 节内偏移 | 未初始化的静态变量 |
如表中所述,未初始化的全局变量似乎未包含在BSS节中,仅静态变量是统一包含的。 如果要将COFF文件加载到内存中,可能需要在BSS节大小之外分配空间来覆盖这些符号。
String Table
字符串表包含长度超过8个字符的任何部分或符号的字符串名称。
字符串表紧随符号表之后,可以通过使用以下公式来定位:
String Table Offset = File Header.f_symptr + File Header.f_nsyms * sizeof( Symbol Table Entry ) = File Header.f_symptr + File Header.f_nsyms * 18
字符串表的前四个字节是一个整数,表示字符串表的总大小(字节)。 表中的每个字符串都以NULL结尾。
节表名称字段和符号表名称字段实际上比上面详细介绍的要复杂得多,实际上它们看起来更像这样:
{
union
{
char name[8];
struct
{
unsigned long zeroes;
unsigned long offset;
}
}
}
如果名称不超过8个字符,则字段“zeroes”将不为零,“name”应解释为字符数组。 请注意,除非该字段的长度少于八个字符,否则该字段不会以NULL结尾。
如果name超过八个字符,则 “zeroes” 字段 (“name” 的前四个字节) 将为零。 在这种情况下,“offset”字段应用作字符串表中的偏移值,以定位符号或节名。
示例
这是一个符号表和源代码的示例,以帮助说明上面的一些信息。 (这是用DJGPP编译的。)
Symbols e_zeroes e_offset e_value e_scnum e_sclass e_type e_numaux [00] 2E 66 69 6C 65 00 00 00 00 00 00 00 FE FF 00 00 67 01 .file.......þÿ..g. [01] 74 65 73 74 2E 6C 69 62 72 61 72 79 2E 63 00 00 00 00 test.library.c.... [02] 00 00 00 00 04 00 00 00 08 00 00 00 02 00 00 00 03 00 .................. _mStaticIntInit [03] 00 00 00 00 14 00 00 00 0C 00 00 00 02 00 00 00 03 00 .................. _mStaticShortInit [04] 00 00 00 00 26 00 00 00 0E 00 00 00 02 00 00 00 03 00 ....&............. _mStaticCharInit [05] 00 00 00 00 37 00 00 00 00 00 00 00 03 00 00 00 03 00 ....7............. _mStaticInt [06] 00 00 00 00 43 00 00 00 10 00 00 00 03 00 00 00 03 00 ....C............. _mStaticShort [07] 00 00 00 00 51 00 00 00 20 00 00 00 03 00 00 00 03 00 ....Q... ......... _mStaticChar [08] 2E 74 65 78 74 00 00 00 00 00 00 00 01 00 00 00 03 01 .text............. [09] 33 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 3................. [0A] 2E 64 61 74 61 00 00 00 00 00 00 00 02 00 00 00 03 01 .data............. [0B] 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .................. [0C] 2E 62 73 73 00 00 00 00 00 00 00 00 03 00 00 00 03 01 .bss.............. [0D] 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0................. [0E] 2E 63 6F 6D 6D 65 6E 74 00 00 00 00 04 00 00 00 03 01 .comment.......... [0F] 11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .................. [10] 00 00 00 00 5E 00 00 00 00 00 00 00 02 00 00 00 02 00 ....^............. _mGlobalIntInit [11] 00 00 00 00 6E 00 00 00 04 00 00 00 02 00 00 00 02 00 ....n............. _mGlobalShortInit [12] 00 00 00 00 80 00 00 00 06 00 00 00 02 00 00 00 02 00 ....€............. _mGlobalCharInit [13] 00 00 00 00 91 00 00 00 00 00 00 00 01 00 00 00 02 00 ....‘............. _TestCall [14] 5F 54 65 73 74 41 64 64 0A 00 00 00 01 00 00 00 02 00 _TestAdd.......... [15] 00 00 00 00 9B 00 00 00 17 00 00 00 01 00 00 00 02 00 ....›............. _TestSetValue [16] 00 00 00 00 A9 00 00 00 10 00 00 00 00 00 00 00 02 00 ....©............. _mTestValue [17] 00 00 00 00 B5 00 00 00 29 00 00 00 01 00 00 00 02 00 ....µ...)......... _TestGetValue [18] 00 00 00 00 C3 00 00 00 10 00 00 00 00 00 00 00 02 00 ....Ã............. _mGlobalInt [19] 00 00 00 00 CF 00 00 00 10 00 00 00 00 00 00 00 02 00 ....Ï............. _mGlobalShort [1A] 00 00 00 00 DD 00 00 00 10 00 00 00 00 00 00 00 02 00 ....Ý............. _mGlobalChar
int mGlobalInt;
short mGlobalShort;
char mGlobalChar;
static int mStaticInt;
static short mStaticShort;
static char mStaticChar;
unsigned int mGlobalIntInit = 1;
unsigned short mGlobalShortInit = 1;
unsigned char mGlobalCharInit = 1;
unsigned static int mStaticIntInit = 1;
unsigned static short mStaticShortInit = 1;
unsigned static char mStaticCharInit = 1;
int mTestValue;
int TestCall()
{
return 1;
}
int TestAdd( int pA, int pB )
{
return pA + pB;
}
int TestSetValue( int pValue )
{
mTestValue = pValue;
return mTestValue;
}
int TestGetValue()
{
return mTestValue;
}
如你所见,静态全局变量在第2节(.data)中定义为已初始化变量,或在第3节(.bss)中定义为未初始化变量,其中n值是到相关节的偏移量。 初始化的全局变量也在第2节 (.data) 中定义,具有偏移量。
请注意,未初始化的全局变量的节号为N_UNDEF(0),因此未定义,但作为非零N_值字段,我们被告知需要为每个符号分配的空间量(在稍微浪费的过度分配中,每个符号16字节)。