靜態鏈接

一、在Linux下一個程序的編譯過程可分爲:預編譯、編譯、彙編、鏈接。

1、預編譯

gcc -E hello.c -o hello.i

預編譯過程注意處理那些源代碼文件中的以#開始的預編譯指令。比如“#include”“#define”“#ifdef”等。刪除註釋,​添加行號。

2、編譯

編譯過程就是把預處理完的文件進行一系列的詞法分析、語法分析、語義分析及優化後生產相應的彙編代碼文件。

gcc -S hello.i -o hello.s

3、彙編

彙編過程是根據彙編指令和機器指令的對照表一一翻譯。

as hello.s -o hello.o

4、鏈接

將模塊間的符號引用進行重定位。

​二、目標文件的格式

1、可執行文件的格式分爲:Windows下PE、Linux下爲ELF,都是COFF格式的變種。​

不光是可執行文件,動態庫和靜態庫文件都是按照可執行文件格式存儲。

​2、ELF文件類型

可重定位文件(.o或.obj)、可執行文件(.exe)共享目標文件(.so或.dll)、核心轉儲文件(core dump)。在Linux下可以使用file命令查看文件格式,file xxx.o。

3、目標文件存放方式

程序指令存放在代碼段(.text),已初始化的全局變量和局部變量存放在數據段(.data),未初始化的全局變量和局部變量存放在.bss段。默認情況下未初始化的全局變量和局部變量值都爲0,所以沒有必要放在.data段分配空間存放數據0,只需要可執行文件記錄所有未初始化的變量的大小總和並在.bss段爲未初始化的全局和局部變量預留位置,.bss段沒有內容不佔據空間。​

​4、ELF文件結構描述

(1)文件頭

使用命令readelf -h a.o可以查看a.o文件的文件頭。

文件頭中定義了ELF魔數、文件機器字節長度、數據存儲方式、版本、運行平臺、ABI版本、ELF重定位類型、硬件平臺、硬件平臺版本、入口地址、程序頭入口和長度、段表的位置和長度及段的數量。

(2)​段表

ELF文件中有很多段,段表就是保存這些段的基本屬性的結構。段表是ELF文件中除了文件頭之外最重要的結構。描述了ELF各個段的信息,如段名、段的長度、編譯、權限及其他屬性。段表本身在ELF文件中的位置由ELF文件頭成員e_shoff決定。

​查看段表結構有兩個命令:

objdump -h a.o  只顯示文件中的關鍵段。

readelf -S a.o​    顯示真正的段表結構。

readelf -s a.o​    顯示文件的符號。

(3)重定位表

鏈接時需要進行重定位的代碼和數據存放在此表中。.rel.text就是針對.text段的重定位表, .rel.data就是針對.data段的重定位表。

(4)其他表及表結構

​三、符號

我們將函數和變量統稱爲符號。鏈接的過程就是符號的粘合。

1、符號的修飾和函數的簽名

在C語言使用之前由於已經有很多彙編等機器語言的庫,爲了能使用這些庫必須解決符號衝突的問題,否則庫中使用的符號名在C語言中就不能使用,所以有了符號修飾。不同的語言有不同的修飾方法,但是當工程太大時仍然存在符號衝突的問題,C++爲此引入了命名空間的概念。同時C++有類、繼承等機制,所以C++的符號修飾方法更爲複雜。

2、extern "C"

C++爲了與C兼容,在符號管理上,C++有一個用來聲明和定義C符號的關鍵字​extern "C",編譯器會把此聲明的全部代碼當成C語言處理。編譯時按C語言修飾方法進行修飾。

但是由於C語言不支持extern "C"​語法,爲了C語言和C++兼容使用C++宏__plusplus, C++編譯器在編譯C++時默認會定義此宏。

3、強弱符號

在編程中時常還是會碰到符號衝突的問題,編譯器默認函數和初始化的全局變量爲強符號,默認未初始化的全局變量爲弱符號​,可以使用GCC的__attribute__((weak))來定義(不是聲明)一個強符號爲弱符號。如果定義了兩個強符號則編譯會報錯重複定義。

針對強弱符號,鏈接器會做如下處理:

(1)不允許強符號被多次定義

(2)如果一個符號在某個目標文件中是強符號在其他文件中是弱符號那麼選擇強符號。

(3)如果一個符號在所有目標文件中都是弱符號,那麼選擇佔用空間最大的一個。
在鏈接時一個符號若沒有找到則會報錯,這是強引用,與之相對應的還有一個弱引用。對於弱引用如果符合有定義則鏈接,如果符合未定義不會報錯。鏈接器處理強弱引用的過程一樣,只是對於未定義的弱引用鏈接器不認爲是一個錯誤。對於未定義的弱引用鏈接器會默認爲0,或者一個特殊值。但是運行時會發生錯誤。強弱引用:可以使用GCC中的__attribute__((weakref))​

4、調試信息

GCC編譯時加上-g選項可以產生有調試信息的可執行程序,對應的ELF文件中會有很多個debug相關的段。這些段佔用很大空間,所以發佈版本一般需要去掉調試信息,Linux中使用strip命令可以去掉調試信息,strip a.o

​四、鏈接

1、空間和地址分配

鏈接器鏈接是將各個目標文件合併成一個文件的過程,採用合併相識段的方式,鏈接器爲目標文件分配地址空間其實有兩個地址:

(1)​各個段合併之後再輸出的可執行文件中的地址

(2)可執行程序裝載後在進程的虛擬地址空間中的地址。​

​整個鏈接過程分爲兩步:

(1)空間與地址分配

獲取所有輸入目標文件中各個段的長度、屬性和位置,建立全局符號表,計算合併後各個段的位置和長度並建立映射關係。​

(2)符號解析與重定位

​讀取輸入文件中段的數據、重定位信息、並且進行符號解析和重定位、調整代碼中的地址等。

如下圖展示是鏈接和加載時地址變遷,a.o + b.o​->ab->進程


目標文件->可執行程序->進程虛擬地址空間

鏈接器根據重定位表中的信息進行重定位,可以使用命令查看目標文件中的重定位表:objdump -r a.o

​2、COMMON塊

目前鏈接器並不支持符號的類型,即變量類型對於鏈接器來說是透明的,鏈接器只知道符號名稱並不知道其類型是否一致。所以當定義多個名稱相同的強符號時編譯器會報錯儘管符號的類型值不同。​

處理規則:

(1)一個強符號,多個弱符號出現類型不一致時,以強符號類型爲最終類型

(2)多個弱符號出現類型不一致時,以​佔用空間最大的爲準。

3、C++相關問題

(1)函數級別的鏈接

鏈接器在鏈接靜態庫的時候是以目標文件爲單位的,比如引用了靜態庫printf函數,那麼鏈接器就會把庫中包含printf函數的那個目標文件鏈接進來,如果很多函數放在這個目標文件中,則很多沒用的函數都被一起鏈接進來,而當目標文件中包含成千上萬個函數,調用者只要使用到其中一個函數或者變量,那麼整個目標文件都會被鏈接進來,​這樣輸出文件會變的很大,造成空間浪費,所以libc.a中printf等函數每個函數就是一個目標文件。

VC++編譯器提供了一個編譯選項叫函數級別鏈接,這個選項作用就是讓所有的函數和變量保存到獨立的段中,當鏈接器需要用到某個函數時將它合併到輸出文件中,沒用到的函數則拋棄。GCC編譯器提供了類似的機制,他有兩個選項-ffunction-section和-fdata-section分別將函數或者變量保存到獨立的段中。​

C++全局對象構造函數在main之前被執行,全局對象的析構在main之後被執行。

4、靜態庫鏈接

靜態庫可以簡單看成一組目標文件的集合,即很多目標文件壓縮打包後形成一個文件。通常使用ar壓縮程序 將這些目標文件壓縮到一起,並且對其進行編號和索引,以便查找和檢索。​

查看鏈接器ld默認的鏈接腳本:ld -verbose

發佈了43 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章