鏈接器
目錄
一 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的指令所在地址被稱為引用符號的地址.
所屬段: 編譯器在編譯一個源文件時,會根據符號不同而把不同的符號組織在一塊數據中,這一塊數據是許多符號的數據的保存空間.這樣的空間被稱之為段.故稱一個符號所在的段為所屬段.
在一個可執行程序生成的流程中, 編譯器只能完成對單個源文件編譯的功能, 而鏈接器的工作發生在編譯器工作之后. 編譯器知道本身不能處理上述問題, 所以每碰到一處無法計算偏移的地址時,會執行以下操作:
- 獲取被引用符號的類型.根據符號的類型不同而做出以下操作
a) 當符號是全局變量:
i. 當全局變量的定義是在本文件時,編譯器會把被引用的符號的地址設為一個偏移值,這個偏移值是被引用符號在所屬段的段內偏移.但這個偏移值並不是一個有效的內存地址,因此鏈接器還需要對其進行處理,編譯器會為其生成一條重定位信息.
ii. 當全局變量的定義不在本文件, 編譯器會把被引用的符號的地址設為0
b) 當符號是一個函數
i. 當被引用符號的定義在本源文件中, 編譯器能夠計算偏移,並把這個偏移值作為被引用符號的地址. 不執行第2步.
ii. 當被引用符號的定義不在本源文件中, 編譯器會把被引用的符號的地址設為0
- 增加一條重定位信息, 重定位主要記錄的信息有:
a) 被引用符號的引用地址,
b) 被引用符號的類型(這個類型描述的是第1步中使用哪種方式去設定被引用符號的地址)
鏈接器的工作之一就是解析所有的重定位信息,修復編譯器設定不了的被引用的符號的地址.
重定位信息生成的條件和形式
重定位信息是為被引用的符號准備的, 但並非所有被引用的符號都有重定位信息.
重定位信息的生成條件是:
- 被引用符號定義在外部文件
- 被引用符號的定義在本文件中,但被引用符號所屬段和引用符號的地址的所屬段不是同一個段.
為了更形象的描述, 下面使用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處理流程
到了這里,基本上就可以明確鏈接器所需要完成的基本操作了:
- 將多個COFF文件組織成一個具有結構(代碼和數據)完整的二進制文件.
- 修復重定位數據.
- 為特定操作系統生成可識別的可執行文件.
為了完成上述操作,必須先得到所有COFF文件的符號定義(符號的定義就是符號對應的數據) ,和引用符號的位置,將各個COFF相同的段合並成同一個段.
基本操作如圖所示:
COFF文件1 |
|
COFF文件2 |
.text |
|
.text |
.data |
|
.data |
.bss |
|
.bss |
.rdata |
|
.rdata |
合並兩個COFF文件 |
.text .text |
.data .data |
.bss .bss |
.rdata .rdata |
轉換 |
可執行文件 |