鏈接器在軟件開發中扮演一個重要的角色,因爲它使得分離編譯成爲可能。我們不用將一個大型的應用程序組織成爲一個巨大的源文件,而是可以把他分解爲更小的,更好管理的模塊,可以獨立的修改和編譯這些模塊。當我們改變這些模塊的一個時,只需簡單的重新編譯它,並重新鏈接應用,而不必重新編譯其他文件。
要想了解鏈接的機制,需要知道一個程序從編輯完代碼到運行的過程(C語言程序)。
以下方的c程序爲例。經過4個階段生成可執行的二進制文件。
//hello.c
#include<stdio.h>
int main(){
printf("%s","hello.word");
return 0;
}
- 預處理階段(cpp)
預處理器(cpp)根據以字符#開頭的命令,修改原始的C程序。比如hello.c中的 #include<stdio.h> 命令告訴預處理器讀取系統頭文件stdio.h的內容(只有stdio.h頭文件裏的內容,不包括stdio.c中的定義部分),並把它直接插入到程序文本中。結果就得到了另一個C程序,通常是以.i作爲文件擴展名。
- 編譯階段(ccl)
編譯器(ccl) 將文本文件hello.i翻譯成由彙編語言組成的文本文件hello.s.
- 彙編階段(as)
彙編器(as)將hello.s翻譯成機器語言指令,將這些指令打包成一中叫做可重定位目標程序文件的格式,並將結果保存到目標文件hello.o中。
- 鏈接階段(ld)
鏈接hello.c中調用的函數,比如例子中的printf函數。 printf函數存在一個名爲printf.o的單獨的預編譯好了的目標文件中,而這個文件必須以某種方式合併到我們的heelo.o程序中。鏈接器就是負責處理這種合併。 結果得到可執行目標文件hello,它可以被加載到內存中,由系統執行。
以上就是背景知識了。下面介紹鏈接器是如何工作的。O(∩_∩)O
一.靜態鏈接
靜態鏈接器的功能就是以一組可重定位目標文件爲輸入 生成一個可以加載的可執行目標文件。
目標文件定義:
1.可重定位目標文件---包含二進制代碼和數據,可以在編譯時與其他可重定位目標文件合併起來。
2.共享目標文件---一種特殊類型的1。
3.可執行目標文件---可直接執行的。
由1+【2】--》3;
爲了瞭解鏈接器是如何把一組可重定位目標文件鏈接在一起,我們先了解下它的格式。
可重定位目標文件格式:
ELF頭 | |
.text | 已編譯程序的機器代碼 |
.rodata | 只讀數據,比如printf語句中的格式串和開關語句的跳轉表 |
.data | 已初始化的全局和靜態C變量 |
.bss | 未初始化的全局和靜態C變量 |
.symtab | 一個符號表,存放程序中定義和引用的函數和全局變量的信息。 |
.rel.text | 一個.text節中位置的列表,鏈接時就是修改這個位置 |
.rel.data | 需要重定位的全局變量信息 |
.debug | 調試符號表 |
.line | 原始C程序中行號和.text節中機器指令之間的映射。 |
.startab | 一個字符串表 |
節頭部表 |
爲了構造可執行文件,鏈接器必須完成兩個主要任務:
1.符號解析---將符號引用和符號定義聯繫起來。
2.重定位---合併各節,然後修改符號引用指向的地址。
符號解析
符號-----每個符號對應於一個函數,一個全局變量或是一個靜態變量。
在建立符號聯繫時,鏈接器要確保它們擁有唯一的名字,對於局部變量很簡答。但是對於全局變量就有點棘手。爲此linux鏈接器使用下面的規則:
(強符號:已定義的全局變量,弱符號:只聲明未定義的全局變量。)
規則1:不允許有多個同名強符號。
規則2:一強多弱 選強
規則3:多弱,任意選
----與靜態庫鏈接
重定位
將符號解析完,接下來就是重定位了
步驟1.將所有相同類型的節合併爲同一類型的新的聚合節。
步驟2.修改代碼節和數據節中對每個符號的引用,使得它們指向正確的運行時地址。----》這一步依賴於重定位條目。
重定位條目:彙編時生成的描述如何修改引用的文件,代碼重定位條目放在.rel.text中,數據重定位條目放在.rel.data中。
重定位包括 相對引用,和絕對引用。