COFF文件格式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了COFF文件格式相关的知识,希望对你有一定的参考价值。

链接器

目录

一 COFF-Common Object File Format-通用对象文件格式... 3

COFF的文件格式与结构体... 4

文件头... 5

numberOfSections(区段数):. 5

timeDateStamp(时间戳)  :. 5

pointerToSymbolTable(符号表文件偏移) :. 5

numberOfSymbols(符号总个数) :. 6

sizeOfOptionalHeader(可选头长度) :. 6

characteristics(文件标记) :. 6

区段表... 7

name(区段名) :. 7

sizeOfRawData(区段数据字节数) :. 7

pointerToRawData(段数据偏移) :. 7

pointerToLinenumbers(行号表偏移) :. 7

numberOfRelocations(重定位表个数) :. 7

characteristics(段标识) :. 7

重定位表... 9

重定位表的生成条件... 9

重定位表的结构... 9

virtualAddress :. 9

symbolTableIndex :. 10

type :. 10

符号表... 11

符号表的作用... 11

符号结构体... 11

name :. 11

zero :. 11

offset:. 11

section:. 11

type :. 12

Class :. 12

numberOfAuxSymbols:. 12

字符串表(String Table). 14

字符串表的位置... 14

字符串表的结构... 14

Size :. 14

Data[1]:. 14

二 链接器的工作流程... 15

COFF文件的来源... 15

链接器的作用... 15

造成单个中间文件内代码和数据不完整的原因和解决方案... 15

重定位信息生成的条件和形式... 16

链接器对COFF处理流程... 18

 

 

 

一 COFF-Common Object File Format-通用对象文件格式

COFF是一种二进制文件格式,这种二进制文件格式用于存储符号对应的数据和组织符号之间的引用关系.

   符号抽象来讲就是一部分二进制数据的名称或别名,具体来讲,符号就是函数名,变量名等等.

符号的数据就是二进制数据, 这些二进制数据一般是代码,或是全局变量和静态变量的初始值,或者是一些常量字符串等等.

符号的引用关系,抽象来讲就是有两个以上的符号,符号1的定义要依赖符号2,符号2的定义要依赖符号3.或者符号3的数据里面使用了符号1等.形象一点就是函数A里面调用了函数B,那么就可以说函数A的二进制代码中引用了函数B的二进制数据.

符号,符号数据,符号引用,符号的引用位置在COFF中是最基本的概念,也是最重要的概念.

更重要的概念是COFF的文件组织.

下面解释介绍的就是COFF文件格式,关于更详细的COFF文件格式请参照微软的文档:

Microsoft Portable Executable and Common Object File Format Specification

 

COFF文件的查看工具: CoffViewer

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

COFF的文件格式与结构体

 

COFF文件格式图

一个COFF文件的格式是经过一定严整的组织的,如上图所示它含有以下部分:

文件头(file header) :一个结构体.文件头描述一个COFF文件的全部信息,没有这个文件头,你将无法解析一个COFF文件.

因为COFF文件所保存的符号的数量,符号数据的大小是不固定的,因此,如果没有这个文件头的指引,你将在一片二进制数据中迷失方向,你不会知道你所要找的符号对应的数据在文件中的哪一个位置.

区段头(section header) :一个结构体.多个区段头组成了区段表,区段头描述的是一个区段的名字,区段的数据在文件中的何处,区段有多少符号,区段的重定位数据在何处,行号表在何处等等.

符号表(symbol table) : 一个结构体,多个符号信息组成了符号表.符号信息用于描述符号的名称,符号的类型等.

字符串表(String table) : 用于保存一些字符个数超出了符号表和区段头的名称数组最大个数的字符串.

 

 

文件头

以下就是文件头的结构:

typedef struct _FILEHEADER

{

        unsigned short machine;     // 平台名

        unsigned short numberOfSections;// 区段数

        unsigned long  timeDateStamp;   // 时间戳

        unsigned long  pointerToSymbolTable;// 符号表文件偏移

        unsigned long  numberOfSymbols;   // 符号总个数

        unsigned short sizeOfOptionalHeader;// 可选头长度

        unsigned short characteristics;    // 文件标记

    } FILEHEADER,*PFILEHEADER;

machine (平台名) : 用于说明是COFF文件属于何种平台.平台一般有:

 

描述

IMAGE_FILE_MACHINE_UNKNOWN

0x0

使用于任何平台

IMAGE_FILE_MACHINE_ALPHA

0x184

Alpha AXP™.

IMAGE_FILE_MACHINE_ARM

0x1C0

 

IMAGE_FILE_MACHINE_ALPHA64

0x284

 

IMAGE_FILE_MACHINE_I386

0x14C

x86平台

IMAGE_FILE_MACHINE_IA64

0x200

X64平台

IMAGE_FILE_MACHINE_MIPS16

0x268

 

IMAGE_FILE_MACHINE_MIPSFPU

0x266

 

IMAGE_FILE_MACHINE_MIPSFPU16

0x366

 

IMAGE_FILE_MACHINE_POWERPC

0x466

 

IMAGE_FILE_MACHINE_R3000

0x1F0

 

IMAGE_FILE_MACHINE_R4000

0x162

 

IMAGE_FILE_MACHINE_R10000

0x168

 

IMAGE_FILE_MACHINE_SH3

0x1A2

 

IMAGE_FILE_MACHINE_SH4

0x1A6

 

IMAGE_FILE_MACHINE_THUMB

0x1C2

 

 

numberOfSections(区段数):

记录COFF一共有多少个区段.在COFF文件中,根据符号的类型不同被划分到不同的区域中.然而符号的类型的数量并非总是一样的,有时候多一点,有时少一点.因此COFF文件中的区段格式也是不固定的.

timeDateStamp(时间戳)  :

COFF文件被创建的时间.

pointerToSymbolTable(符号表文件偏移) :

符号表在文件中的偏移.

numberOfSymbols(符号总个数) :

一个COFF文件中符号的总个数.

sizeOfOptionalHeader(可选头长度) :

COFF文件中都没有可选头,因此这个字段的值是0

characteristics(文件标记) :

此字段记录着文件的属性,文件的属性一般由以下值:

描述

IMAGE_FILE_LINE_NUMS_STRIPPED

0x0004

行号表被移除

IMAGE_FILE_LOCAL_SYMS_STRIPPED

0x0008

非引用符号表本移除

IMAGE_FILE_AGGRESSIVE_WS_TRIM

0x0010

 

IMAGE_FILE_LARGE_ADDRESS_AWARE

0x0020

用户层空间可大于2GB

IMAGE_FILE_16BIT_MACHINE

0x0040

保留

IMAGE_FILE_BYTES_REVERSED_LO

0x0080

 

IMAGE_FILE_32BIT_MACHINE

0x0100

32位系统架构的文件

IMAGE_FILE_DEBUG_STRIPPED

0x0200

调试信息被移除

IMAGE_FILE_UP_SYSTEM_ONLY

0x4000

File should be run only on a UP machine

IMAGE_FILE_BYTES_REVERSED_HI

0x8000

Big endian:MSB precedes LSB in memory

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

区段表

在一个COFF文件中,会有许许多多的符号. 这些符号都是有类型的,COFF将类型不同的符号分门别类,保存到了不同的区段中,于是就有了区段的存在.

由于每个区段的符号个数,符号数据的大小都是不一样的,因此,每个区段的大小肯定是不一样大的. 在组织不一样的数据保存到文件时,为了方便定位到每一个区段中的每一个符号的每一部分数据.COFF文件将每一个区段的一些信息保存到一个结构体中,如果有多个区段,就存在多个区段表,这些区段表就一张一张的保存在文件头之后.

每一张区段表都是相同的结构体,下面的结构就是区段表保存的信息:

typedef struct _ SECTIONHEADER

{

        char             name[8];        // 段名

        unsigned long  virtualSize; // 虚拟大小

        unsigned long  virtualAddress; // 虚拟地址

        unsigned long  sizeOfRawData; // 区段数据的字节数

        unsigned long  pointerToRawData; // 区段数据偏移

        unsigned long  pointerToRelocations;// 区段重定位表偏移

        unsigned long  pointerToLinenumbers; // 行号表偏移

        unsigned short  numberOfRelocations; // 重定位表个数

        unsigned short  numberOfLinenumbers; // 行号表个数

        unsigned long  characteristics;    // 段标识

} SECTIONHEADER,* SECTIONHEADER;

 

 

name(区段名) :

最大为8个字节的,以’\0’为结尾的ASCII字符串.用于记录区段的名字.区段的名字有些是特定意义的区段. 如果区段名的数量大于8个字节,则name的第一字节是一个斜杠字符:’/’,接着就是一个数字,这个数字就是字符串表的一个索引.它将索引到一个具体的区段名.

virtualSize(虚拟大小) virtualAddress(虚拟地址)在COFF文件中都没有作用,都是0.

sizeOfRawData(区段数据字节数) :

这个字段记录区段的原始数据的字节数.

pointerToRawData(段数据偏移) :

区段原始数据在文件中的偏移.

pointerToLinenumbers(行号表偏移) :

行号表的文件偏移

numberOfRelocations(重定位表个数) :

重定位表条目个数

numberOfLinenumbers(行号表个数) :

行号表条目个数

characteristics(段标识) :

这个字段记录此这个段的属性,属性一般是:

描述

IMAGE_SCN_CNT_CODE

0x00000020

区段包含可执行代码

IMAGE_SCN_CNT_INITIALIZED_DATA

0x00000040

区段包含初始化数据

IMAGE_SCN_CNT_UNINITIALIZED_DATA

0x00000080

区段包含未初始化数据

IMAGE_SCN_LNK_INFO

0x00000200

区段包含注释或其他信息,比如.drectve区段

IMAGE_SCN_LNK_REMOVE

0x00000800

区段不会成为可执行文件的一部分.

IMAGE_SCN_ALIGN_1BYTES

0x00100000

区段对齐粒度为1字节

IMAGE_SCN_ALIGN_4BYTES

0x00200000

区段对齐粒度为2字节

IMAGE_SCN_ALIGN_XBYTES

0x00N00000

区段对齐粒度为X字节

IMAGE_SCN_LNK_NRELOC_OVFL

0x01000000

区段含有外部重定位表

IMAGE_SCN_MEM_DISCARDABLE

0x02000000

当需要时,区段可能会被丢弃

IMAGE_SCN_MEM_NOT_CACHED

0x04000000

区段不能被加入到缓存

IMAGE_SCN_MEM_NOT_PAGED

0x08000000

区段不会被分页

IMAGE_SCN_MEM_SHARED

0x10000000

区段在内存会被共享

IMAGE_SCN_MEM_READ

0x40000000

区段含可以被读取

IMAGE_SCN_MEM_WRITE

0x80000000

区段含可以被写入

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

重定位表

重定位表的生成条件

重定位表的作用使得并不是每一个区段都有重定位表, 比如数据段和只读数据段是没有重定位表的,一般情况下只有那些含有可执行属性的区段才会有重定位表.

重定位表的作用是指出哪些符号引用了其他的符号,是用何种方式引用这些符号的. 但是并非所有的被引用的符号都有重定位信息. 只有当引用符号的位置和被引用的符号的数据的位置不是在同一个区段时才会产生重定位信息.

一般情况下, 全局变量和外部函数会产生重定位数据. 比如在下面的C语言代码中:

01|  int externFunction(); // 一个函数声明,它的实现代码在其他文件中

02|  int g_nNum;          // 在本文件定义的全局变量

03|  int localFunction2(); // 一个函数声明,它的实现代码在本文件中

04|  int localFunction1()  // 在本文件实现的一个函数

05|  {

06|     int nNum=0;

07|     return externFunction()+g_nNum+nNum;

08|  }

09| 

10|  int localFunction2()  // 函数的定义

11|  {

12|     return localFunction1();

13|  }

在上面的代码中, externFunction()函数, g_nNum全局变量, localFunction1(),localFunction2()函数都是符号,其中, g_nNum全局变量和localFunction1(),localFunction2()函数是本地符号,因为它们的定义在本文件中, externFunction()函数是外部符号,因为它的定义不在本文件中,第06行中有两处符号引用, externFunction()函数的调用是一处,使用g_nNum变量的值也是一处,第12行中也有一处调用了函数也属于符号引用.

但是能够产生重定位信息的只有第07行的两个符号引用.

第07行能够产生重定位信息是因为externFunction()是一个外部符号,g_nNum是一个全局变量.

第12行的localFunction1()函数和localFunction2()函数是同处于同一个代码段的,因此不具备产生重定位信息的条件.

 

PS:任何非静态局部变量都在COFF文件中没有符号.

 

重定位表的结构

typedef struct _RELOCATION

{

        unsigned long  virtualAddress;   

        unsigned long  symbolTableIndex;  

        unsigned short type;             

} RELOCATION,*PRELOCATION;

 

virtualAddress :

重定位数据产生的位置,也就是符号被引用的位置.该位置是一个文件偏移值, 是基于本区段的原始数据的开始位置的偏移.

symbolTableIndex :

用来指明是被引用的符号是哪一个符号.这是个从0开始的符号表索引.

type :

重定位类型. 重定位类型根据不同的平台有不同的类型.在x86平台下一般有如下类型:

Intel 386™

     描述

IMAGE_REL_I386_ABSOLUTE

0x0000

该重定位信息会被忽略

IMAGE_REL_I386_DIR32

0x0006

32位虚拟地址

IMAGE_REL_I386_DIR32NB

0x0007

32位相对虚拟地址

IMAGE_REL_I386_REL32

0x0014

32位相对偏移地址,一般用于跳转指令和函数调用指令

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

符号表

符号表的作用

符号表在COFF文件中是非常重要的. 没有符号表,链接器在链接多个COFF文件时,将无法识别函数的代码,全局变量的数据保存在文件中什么位置.

符号表的作用就是保存符号名,符号类型等信息.

符号结构体

typedef struct _SYMBOL

{

    union {

        char name[8];               // 符号名称

        struct {

            unsigned long zero;     // 字符串表标识

            unsigned long offset;  // 字符串偏移

        } e;

    } e;

    unsigned long value;            // 符号值

    short          section;          // 符号所在段

    unsigned short type;            // 符号类型

    unsigned char Class;            // 符号存储类型

    unsigned char numberOfAuxSymbols;// 符号附加记录数

} SYMBOL,*PSYMBOL;

name :

符号名,最大长度8字节,如果超出了8字节,将不会使用这个字段

zero :

当符号名超出8字节,这个字段的值会是0

offset:

当zero的值字段等于0时,这个字段保存的是一个字符串表的索引值 value : 符号值,这个值是一个整形值,它的意义是由section和Class的值决定的.

section:

符号所在的区段号码.这是一个有符号整形.当这个字段的值大于等于1且Class等于2的时候, value表示符号基于区段数据的偏移.

section的值有以下意义:

     描述

IMAGE_SYM_UNDEFINED

0

符号没有指定的区段.

IMAGE_SYM_ABSOLUTE

-1

这是一个绝对符号,无需重定位.

IMAGE_SYM_DEBUG

-2

符号不会有对应的区段.

IMAGE_SYM_TEXT

1

.text段(代码段)

IMAGE_SYM_DATA

2

.data段(数据段)

IMAGE_SYM_BSS

3

.bss段(未初始化数据段)

IMAGE_SYM_RDARA

4

.rdata段(只读数据段)

 

type :

符号的类型,用于区分符号是函数还是非函数.一般这个值只用0x00表示非函数,用0x20表示函数.

Class :

增强类型.用于记录符号的属性.比如符号是extern(全局的),还是static(本文件内全局)的等等.这个字段的值会配合type的值来决定value的值有何作用.下表是Class的值和描述:

     描述

IMAGE_SYM_CLASS_NULL

0

没有分配增强类型

IMAGE_SYM_CLASS_EXTERNAL

2

全局符号.

当section的值等于0时,value的值表示符号的字节数.

当section的值大于等于1时,value的值表示符号在区段中的偏移

IMAGE_SYM_CLASS_STATIC

3

Value字段的值表示符号在区段中的偏移.

IMAGE_SYM_CLASS_FUNCTION

101

调试用的符号.用于记录函数的行号信息.

.bf用于记录函数开始.

.ef用于记录函数的结束.

.lf用于记录函数的每一行,value的值表示第几行

IMAGE_SYM_CLASS_FILE

103

没有指定的区段,用于记录源文件名,原文件名会保存在附加记录中.

 

numberOfAuxSymbols:

符号可能会存在附加记录,如果这个字段的值是0,就说明符号没有附加记录,如果大于等于1,就有一条或以上的附加记录.附加记录的字节数和SYMBOL结构体的字节数一样大,并且它的位置紧随着当前的结构体.附加记录也是有结构体,而且有多种结构体,这些结构体的字节数都小于等于SYMBOL结构体的大小.Class字段的值决定了附加记录使用的是哪一种结构体.比如当Class的值等于IMAGE_SYM_CLASS_FILE时,附加记录是一个char name[18]的结构体.

总结:numberOfAuxSymbols记录是否含有附加记录,Class决定使用何种结构体区解析附加记录.

下面是Class的值所对应的附加记录的结构体:

结构体

     描述

typedef struct _FUNCTIONDEFINITIONS

{

int      TagIndex;//符号表索引

int        TotalSize;//函数数据字节数

unsigned int  PointerToLinenumber;//行号表偏移

unsigned int  PointerToNextFunction; //下个函数的

//符号表索引

short          Unused;//未使用

}FUNCTIONDEFINITIONS,*PFUNCTIONDEFINITIONS

当Class的值等于2且type的值等于0x20且section的值大于等于1,符号的附加记录才能够使用这种结构体.

这个结构体用于记录一个函数的信息

typedef struct _FUNCTIONLINE

{

    int     Unused1; // 未使用

    short    Linenumber;

    char         Unused2[6];//未使用

    unsigned int PointerToNextFunction;

    short    Unused3;//未使用

}FUNCTIONLINE,*PFUNCTIONLINE

当Class的值等于101时,name字段会存储三种不同的名字:.bf : 字段value的值不产生作用..lf : 字段value的值是函数在源文件中的一个行号..ef:字段value和FUNCTIONDEFINITIONS结构体中的TotalSize的值一样.

当name字段的是”.bf”和”.ef”时才使用这个结构体

typedef struct _FUNCTIONLINE

{

    char FileName[18];

}

当Class字段的值等于103时,字段name保存”.file”.

FileName就是本COFF文件对应的源文件的文件名

typedef struct _SECTIONDEFINITIONS

{

    unsigned int    Length;

    unsigned short NumberOfRelocations;

    unsigned short NumberOfLinenumbers;

    unsigned int    CheckSum;

    short       Number;

    char            Selection;

    char            Unused[3];

}SECTIONDEFINITIONS,*PSECTIONDEFINITIONS

当name字段保存的是一个区段名(例如:.text或.Drectve)且Class的值等于3时,附加记录使用这个结构体.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

字符串表(String Table)

字符串表的位置

在文件头,区段表和符号表任何一张表中都没有记录字符串表的位置.因为字符串表是最后一张表,它就在符号表之后.计算字符串表的文件偏移公式为:

PointerToStringTable = pointerToSymbolTable* numberOfSymbols*sizeof(FILEHEADER)

字符串表的结构

typedef struct _STRIGTABLE

{

    unsigned int Size;

    char         Data[1];

} STRIGTABLE,*PSTRIGTABLE

 

Size :

记录字符串表的所有字符的总字节数,包括Size本身.

Data[1]:

就是字符串表所保存的字符串,字符串表里并不是指保存一个字符串.而是保存了一个以上的以’\0’结尾的字符串.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

二 链接器的工作流程

COFF文件的来源

在链接器之前,编译器会将各个源文件编译成COFF文件 .一个源文件就对应一个COFF文件,所以如果一个项目中有多个源文件,那么这个项目经过编译后,就会产生等数量的COFF文件.

这些COFF文件一般被称之为中间文件.这些中间文件一般都是以.o或.obj为扩展名.

中间文件是无法直接被操作系统执行的.

链接器的作用

中间文件无法被操作系统直接执行,有两点原因:

1 中间文件的代码和数据并不是完整的

2 操作系统识别不了中间文件文件格式

链接器起到的作用就是使得一个文件成为一个操作系统可以识别的格式,并且让这个文件具备完整的代码和数据.

下面的篇幅,就是围绕这两个关键点展开的.

造成单个中间文件内代码和数据不完整的原因和解决方案

造成这样的原因是由编译器引起的.

由于编译器只负责逐个逐个地将单个源文件编译成COFF文件. 而源文件中常常会存在一些定义在其他源文件的函数和全局变量. 编译器在编译源文件的时候是不知道这些外部函数和外部全局变量的数据被存放在哪里的. 无法知道目的地址, 编译器在生产代码时也就无法计算偏移.

首先为避免一些名词造成误解,在这里先就被’引用符号’ , ’引用符号的地址’ , ’所属段’的概念进行定义:

被引用符号 : 指在二进制数据中使用了符号A,则称符号A为被引用符号.

引用符号的地址 : 引用符号和地址是不可分开陈述的. 在代码数据中使用了符号A, 使用符号A的指令所在地址被称为引用符号的地址.

所属段: 编译器在编译一个源文件时,会根据符号不同而把不同的符号组织在一块数据中,这一块数据是许多符号的数据的保存空间.这样的空间被称之为段.故称一个符号所在的段为所属段.

在一个可执行程序生成的流程中, 编译器只能完成对单个源文件编译的功能, 而链接器的工作发生在编译器工作之后. 编译器知道本身不能处理上述问题, 所以每碰到一处无法计算偏移的地址时,会执行以下操作:

  1. 获取被引用符号的类型.根据符号的类型不同而做出以下操作

a)         当符号是全局变量:

                                       i.              当全局变量的定义是在本文件时,编译器会把被引用的符号的地址设为一个偏移值,这个偏移值是被引用符号在所属段的段内偏移.但这个偏移值并不是一个有效的内存地址,因此链接器还需要对其进行处理,编译器会为其生成一条重定位信息.

                                     ii.              当全局变量的定义不在本文件, 编译器会把被引用的符号的地址设为0

b)         当符号是一个函数

                                       i.              当被引用符号的定义在本源文件中, 编译器能够计算偏移,并把这个偏移值作为被引用符号的地址. 不执行第2步.

                                     ii.              当被引用符号的定义不在本源文件中, 编译器会把被引用的符号的地址设为0

  1. 增加一条重定位信息, 重定位主要记录的信息有:

a)         被引用符号的引用地址,

b)         被引用符号的类型(这个类型描述的是第1步中使用哪种方式去设定被引用符号的地址)

 

链接器的工作之一就是解析所有的重定位信息,修复编译器设定不了的被引用的符号的地址.

 

重定位信息生成的条件和形式

重定位信息是为被引用的符号准备的, 但并非所有被引用的符号都有重定位信息.

重定位信息的生成条件是:

  1. 被引用符号定义在外部文件
  2. 被引用符号的定义在本文件中,但被引用符号所属段和引用符号的地址的所属段不是同一个段.

为了更形象的描述, 下面使用C语言代码进行说明:

int externalFunction(); // 函数声明,这个函数的定义不在本源文件

int function(int nNum)  // 函数定义

{

   return externalFunction()+1;

}

 

上面的代码在一个源文件中.经过编译器编译之后就变成了下面的机器码:

5589E583EC08E80000000083C001C9C3

为了能更具体看出不同之处,将机器码翻译成汇编:

55            push ebp    ; 这里是函数function的数据

89E5          mov ebp,esp

83EC 08       sub esp,0x8

E8 00000000 call 00000000;此处就是externalFunction函数的调用,见注解

83C0 01       add eax,0x1

C9            leave

C3            retn

注解 : 由于externalFunction这个符号的定义不在本文件中,满足条件1,所以这个符号的地址被设定为0,0在此处既不是偏移也不是有效地址.

如果externalFunction()函数不是外部文件的函数,那么结果是这样的:

int externalFunction(){return 0;}

int function(int nNum)

{

    return externalFunction()+1;

}

X86平台上的机器码:

5589E5B8000000005DC35589E5E8EEFFFFFF83C0015DC390

翻译成汇编语言:

55            push ebp    ; 这里是符号externalFunction的数据

89E5          mov ebp,esp

B8 00000000  mov eax,0x0

5D            pop ebp

C3            retn

55            push ebp    ; 这里是符号function的数据

89E5          mov ebp,esp

E8 EEFFFFFF  call 0051772F  ; 这里是externalFunction函数调用,见注解1

83C0 01      add eax,0x1

5D            pop ebp

C3            retn

注解1: E8就是call指令,E8后是一个偏移值,是call指令到目标地址的偏移值. 这个偏移值的计算公式是:

    偏移值 = 目标地址调用地址 + 5

由于externalFunction符号和function符号同处一个源文件也同处于一个段,因此编译器可以计算符号的偏移地址,所以在这里不会产生重定位信息.

上面的代码中,只举例描述函数的地址是怎么丢失的, 并没有举例全局变量的地址是怎么丢失的. 不过它的原理是一样的: 符号引用地址(调用函数的地方)和被引用的符号(被调用的函数)的地址不在一个区段时,编译器就无法计算偏移.

例如在代码段中引用了数据段的地址,即使代码段和数据段同处于一个源文件,编译器也无法计算具体的偏移.

int g_nNum=10;

int function()

{

    return g_nNum;

}

X86平台下的机器码:

5589E5A1000000005DC3

翻译后的汇编代码:

55            push ebp     ; function 函数的数据

89E5          mov ebp,esp

A1 00000000  mov eax,[0]  ; g_nNum符号的引用地址

5D            pop ebp

C3            retn

在上面的代码中, g_nNum全局变量和function函数都是同处于一个源文件, 但编译器在编译时, 会根据符号的类型的不同分别把不同的符号组织在不同的段里. 全局变量放在.data段 , 代码放在.text段. 所以当function函数在引用g_nNum的符号时,g_nNum这个符号的地址是0 ,但是这里的0并不是编译器无法计算g_nNum符号的偏移而填入0, 这个0是一个偏移值, 是g_nNum这个符号在数据段的段内偏移. 只有当g_nNum的定义不在本文件中时,这里的0才是一个无效地址0.

 

链接器对COFF处理流程

到了这里,基本上就可以明确链接器所需要完成的基本操作了:

  1. 将多个COFF文件组织成一个具有结构(代码和数据)完整的二进制文件.
  2. 修复重定位数据.
  3. 为特定操作系统生成可识别的可执行文件.

为了完成上述操作,必须先得到所有COFF文件的符号定义(符号的定义就是符号对应的数据) ,和引用符号的位置,将各个COFF相同的段合并成同一个段.

基本操作如图所示:

 

COFF文件1

 

COFF文件2

.text

 

.text

.data

 

.data

.bss

 

.bss

.rdata

 

.rdata

合并两个COFF文件

                               

 

 

.text

.text

.data

.data

.bss

.bss

.rdata

.rdata

转换

 

可执行文件

 

以上是关于COFF文件格式的主要内容,如果未能解决你的问题,请参考以下文章

PE文件和COFF文件格式分析——签名COFF文件头和可选文件头2

程序员自我修养阅读笔记——Windows PE/COFF

程序员自我修养阅读笔记——Windows PE/COFF

windows obj文件格式解析

之间的用法差异。 a.out、.ELF、.EXE 和 .COFF

PE 学习之路 —— DOS 头NT 头