淺談連接器

編譯過程簡介

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

什麼是連接器?

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

連接器的做用有點相似於咱們常用的壓縮軟WinRAR(Linux下是tar),壓縮軟件將一堆文件打包壓縮成一個壓縮文件,而連接器和壓縮軟件的區別在於連接器是將多個目標文件打包成一個文件而不進行壓縮。bash

寫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段中的信息便可找到使用的動態庫,從而完成動態連接

參考

微信公共號

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

相關文章
相關標籤/搜索