淺談鏈接器

目錄

  • 編譯過程簡介
  • 什麼是鏈接器?
  • 鏈接器可操作的元素:目標文件
  • 符號表(Symbol table)
    • 符號決議
  • 庫與可執行文件
    • 靜態庫
    • 動態庫
  • 參考
  • 微信公共號

編譯過程簡介

C語言的編譯過程由五個階段組成:

  • 步驟1:預處理:主要是處理以#開頭的語句,主要工作如下:1)將#include包含的頭文件直接拷貝到.c文件中;2)將#define定義的宏進行替換;3)處理條件編譯指令#ifdef;4)將代碼中的註釋刪除;5)添加行號和文件標示,這樣的在調試和編譯出錯的時候才知道是是哪個文件的哪一行 ;6)保留#pragma編譯器指令,因為編譯器需要使用它們。
gcc -E helloworld.c -o helloworld_pre.c
  • 步驟2: 編譯:將C語言翻譯成彙編,主要工作如下:1)詞法分析;2)語法分析;3)語義分析 4)優化後生成相應的彙編;
gcc -S helloworld.c -o helloworld.s
  • 步驟3: 彙編:將上一步的彙編代碼轉換成機器碼(machine code),這一步產生的文件叫做目標文件;
gcc -c helloworld.c -o helloworld.o
  • 步驟4:鏈接:將多個目標文以及所需的庫文件(.so等)鏈接成最終的可執行文件(executable file)。
gcc helloworld.c -o helloworld

什麼是鏈接器?

鏈接器是一個將編譯器產生的目標文件打包成可執行文件或者庫文件或者目標文件的程序。

鏈接器的作用有點類似於我們經常使用的壓縮軟WinRAR(Linux下是tar),壓縮軟件將一堆文件打包壓縮成一個壓縮文件,而鏈接器和壓縮軟件的區別在於鏈接器是將多個目標文件打包成一個文件而不進行壓縮。

寫C或者C++的u同學經常遇到這樣一個錯誤:

undefined reference to function ABC.

鏈接器可操作的元素:目標文件

鏈接器可操作的最小元素是一個簡單的目標文件
從廣義上來講,目標文件與可執行文件的格式幾乎是一模一樣的,在Linux下,我們把它們統稱為ELF文件。

ELF文件標準裏面把系統中採用ELF格式的文件歸為以下四類:

  • 可重定位文件(Relocatable File):Linux的.o文件,這類文件包含了代碼和數據,可以被用來鏈接成可執行文件或共享目標文件,靜態鏈接庫也歸屬於這一類;

  • 可執行文件(Executable File):比如bin/bash文件,這類文件包含了可以直接執行的程序,它的代表就是ELF文件,他們一般都沒有擴展名;

  • 共享目標文件(shared Object File): 比如Linux的.so文件,這種文件包含了代碼和數據,可以在以下兩種情況下使用,一種是鏈接器可以直接使用這種文件跟其他的可重定位文件和共享目標文件鏈接,產生新的目標文件。第二種是動態鏈接器可以將幾個這樣的共享目標文件與可執行文件結合,作為進程映射的一部分來運行。

  • 核心轉儲文件(Core Dump File): Linux下面的core dump,當進程意外終止時,系統可以將該進程的地址空間的內容及終止時的一些其他信息轉儲到核心轉儲文件中。

符號表(Symbol table)

編譯器在遇到外部定義的全局變量或者函數時只要能在當前文件找到其聲明,編譯器就認為編譯正確。而尋找使用變量定義的這項任務就被留給了鏈接器。鏈接器的其中一項任務就是要確定所使用的變量要有其唯一的定義。雖然編譯器給鏈接器留了一項任務,但為了讓鏈接器工作的輕鬆一點編譯器還是多做了一點工作的,這部分工作就是符號表(Symbol table)。

符號表中保存的信息有兩個部分:

  • 該目標文件中引用的全局變量以及函數;
  • 該目標文件中定義的全局變量以及函數。

編譯器在編譯過程中每次遇到一個全局變量或者函數名都會在符號表中添加一項,最終編譯器會統計一張符號表。

假設C語言源碼如下:

// 定義未初始化的全局變量
int g_x_uninit;

// 定義初始化的全局變量
int g_x_init = 1;

// 定義未初始化的全局私有變量,只能在當前文件中使用
static int g_y_uninit;

// 定義初始化的全局私有變量
static int g_y_init = 2;

// 聲明全局變量,該變量的定義在其它文件
extern int g_z;

// 函數聲明,該函數的定義在其它文件
int fn_a(int x, int y);

// 私有函數定義,該函數只能在當前文件中使用
static int fn_b(int x)
{
    return x + 1;
}

// 函數定義
int fn_c(int local_x)
{
    int local_y_uninit;
    int local_y_init = 3;
    // 對全局變量,局部變量以及函數的使用
    g_x_uninit = fn_a(local_x, g_x_init);
    g_y_uninit = fn_a(local_x, local_y_init);
    local_y_uninit += fn_b(g_z);
    return (g_y_uninit + local_y_uninit);
}

編譯器將為此文件統計出如下一張符號表:

名字 類型 是否可被外部引用 區域
g_z 引用,未定義
fn_a 引用,未定義
fn_b 定義 代碼段
fn_c 定義 代碼段
g_x_init 定義 數據段
g_y_uninit 定義 數據段
g_x_uninit 定義 數據段
g_y_init 定義 數據段

g_z以及fn_a是未定義的,因為在當前文件中,這兩個變量僅僅是聲明,編譯器並沒有找到其定義。剩餘的變量編譯器都可以在當前文件中找到其定義。

本質上整個符號表主要表達兩件事:1)我能提供給其它文件使用的符號; 2)我需要其它文件提供給我使用的符號。

目標文件
數據段
代碼段
符號表

符號決議

有了符號表,鏈接器就可以進行符號決議了。如圖所示,假設鏈接器需要鏈接三個目標文件,如下:

鏈接器會依次掃描每一個給定的目標文件,同時鏈接器還維護了兩個集合,一個是已定義符號集合D,另一個是未定義符合集合U,下面是鏈接器進行符合決議的過程:

  • 對於當前目標文件,查找其符號表,並將已定義的符號並添加到已定義符號集合D中。
  • 對於當前目標文件,查找其符號表,將每一個當前目標文件引用的符號與已定義符號集合D進行對比,如果該符號不在集合D中則將其添加到未定義符合集合U中。
  • 當所有文件都掃描完成后,如果為定義符號集合U不為空,則說明當前輸入的目標文件集合中有未定義錯誤,鏈接器報錯,整個編譯過程終止。

鏈接過程中,只要每個目標文件所引用變量都能在其它目標文件中找到唯一的定義,整個鏈接過程就是正確的。

若鏈接器在查找了所有目標文件的符號表后都沒有找到函數,因此鏈接器停止工作並報出錯誤undefined reference to function A

庫與可執行文件

鏈接器根據目標文件構建出庫(動態庫、靜態庫)或可執行文件。

給定目標文件以及鏈接選項,鏈接器可以生成兩種庫,分別是靜態庫以及動態庫,如下圖所示,給定同樣的目標文件,鏈接器可以生成兩種不同類型的庫。

靜態庫

靜態庫在Windows下是以.lib為後綴的文件,Linux下是以.a為後綴的文件。

靜態庫是鏈接器通過靜態鏈接將其和其它目標文件合併生成可執行文件的,而靜態庫只不過是將多個目標文件進行了打包,在鏈接時只取靜態庫中所用到的目標文件。

目標文件分為三段:代碼段、數據段、符號表,在靜態鏈接時可執行文件的生成過程如下圖所示:

可執行文件的特點如下:

  • 可執行文件和目標文件一樣,也是由代碼段和數據段組成。
  • 每個目標文件中的數據段都合併到了可執行文件的數據段,每個目標文件當中的代碼段都合併到了可執行文件的代碼段。
  • 目標文件當中的符號表並沒有合併到可執行文件當中,因為可執行文件不需要這些字段。

可執行文件和目標文件沒有什麼本質的不同,可執行文件區別於目標文件的地方在於,可執行文件有一個入口函數,這個函數也就是我們在C語言當中定義的main函數,main函數在執行過程中會用到所有可執行文件當中的代碼和數據。main函數是被操作系統調用。

動態庫

靜態庫在編譯鏈接期間就被打包copy到了可執行文件,也就是說靜態庫其實是在編譯期間(Compile time)鏈接使用的。
動態鏈接可以在兩種情況下被鏈接使用,分別是加載時動態鏈接(load-time dynamic linking)運行時動態鏈接 (run-time dynamic linking)

  • 加載時動態鏈接:在這裏我們只需要簡單的把加載理解為程序從磁盤複製到內存的過程,加載時動態鏈接就出現在這個過程。操作系統會查找可執行文件依賴的動態庫信息(主要是動態庫的名字以及存放路徑),找到該動態庫后就將該動態庫從磁盤搬到內存,並進行符號決議,如果這個過程沒有問題,那麼一切準備工作就緒,程序就可以開始執行了,如果找不到相應的動態庫或者符號決議失敗,那麼會有相應的錯誤信息報告為用戶,程序運行失敗。

  • 運行時動態鏈接:run-time dynamic linking 運行時動態鏈接則不需要在編譯鏈接時提供動態庫信息,也就是說,在可執行文件被啟動運行之前,可執行文件對所依賴的動態庫信息一無所知,只有當程序運行到需要調用動態庫所提供的代碼時才會啟動動態鏈接過程。

可以使用特定的API來運行時加載動態庫,在Windows下通過LoadLibrary或者LoadLibraryEx,在Linux下通過使用dlopen、dlsym、dlclose這樣一組函數在運行時鏈接動態庫。當這些API被調用后,同樣是首先去找這些動態庫,將其從磁盤copy到內存,然後查找程序依賴的函數是否在動態庫中定義。這些過程完成后動態庫中的代碼就可以被正常使用了。

在動態鏈接下,可執行文件當中會新增兩段,即dynamic段以及GOT(Global offset table)段,這兩段內容就是是我們之前所說的必要信息。

dynamic 段中保存了可執行文件依賴哪些動態庫,動態鏈接符號表的位置以及重定位表的位置等信息。
當加載可執行文件時,操作系統根據dynamic段中的信息即可找到使用的動態庫,從而完成動態鏈接

參考

  • C語言編譯的4大過程詳解
  • C語言編程透視
  • 徹底理解鏈接器:二,符號決議

微信公共號

NFVschool,關注最前沿的網絡技術。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

您可能也會喜歡…