C語言的編譯過程由五個階段組成:編程
#
開頭的語句,主要工做以下:1)將#include
包含的頭文件直接拷貝到.c
文件中;2)將#define
定義的宏進行替換;3)處理條件編譯指令#ifdef
;4)將代碼中的註釋刪除;5)添加行號和文件標示,這樣的在調試和編譯出錯的時候才知道是是哪一個文件的哪一行 ;6)保留#pragma編譯器指令,由於編譯器須要使用它們。gcc -E helloworld.c -o helloworld_pre.c
gcc -S helloworld.c -o helloworld.s
gcc -c helloworld.c -o helloworld.o
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)。
符號表中保存的信息有兩個部分:
編譯器在編譯過程當中每次遇到一個全局變量或者函數名都會在符號表中添加一項,最終編譯器會統計一張符號表。
假設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,下面是連接器進行符合決議的過程:
連接過程當中,只要每一個目標文件所引用變量都能在其它目標文件中找到惟一的定義,整個連接過程就是正確的。
若連接器在查找了全部目標文件的符號表後都沒有找到函數,所以連接器中止工做並報出錯誤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,關注最前沿的網絡技術。