這部分是四月份的安排,拖到五一放假了,主要是對原始碼編譯過程的一次總結,總的來說,大致可分為預編譯、編譯、彙編和連結四部分。這裡簡單記錄一下:
一 概述
- 1、預處理
或者說是預編譯,指的是在編譯前需要做的一些處理,如巨集替換、include替換等等,這部分沒什麼東西
每一個.c或.cpp原始碼檔案會生成一個對應的.i檔案; - 2、編譯
編譯過程將預處理後的檔案生成為.s的彙編檔案,彙編檔案可用文字編輯器開啟檢視,裡面的彙編程式碼是直接對應CPU動作的; - 3、彙編
彙編過程將.s彙編檔案對映為可重定位目標檔案, 一般為.o或.obj副檔名。 - 4、連結
連結階段是通過連結器將不同的.o檔案進行打包,可以理解為單純的拼接操作,但操作的時候會檢查各個實現是否存在。此外,連結可執行檔案時還會匯入c或cpp的啟動相關的必要系統檔案,如cruntime等
二 其他相關知識點
-
在shell中啟動可執行檔案後,shell會呼叫作業系統的載入器將可執行檔案讀入記憶體,然後將cpu的控制權交給可執行檔案,然後開始執行。
-
可重定位目標檔案的基本組成部分如下,連結過程是基於符號完成粘合的,比如a檔案中呼叫了函式f,b檔案中定義了函式f,那麼連結過程就能正確完成;否則會出現找不到定義的連結錯誤,同樣如果出現重複定義,也會報錯。
-
至於最終生成可執行檔案還是庫檔案,取決於程式設計師。如果是生成可執行檔案,連結器還會連結呼叫main函式的相關係統檔案,這些檔案會呼叫main函式,所以如果原始碼裡面沒有實現,就會報“無法解析的外部符號main”,因為聯結器找不到main函式的實現。
-
如果是生成庫檔案,比如靜態庫,我們在linxu下可以用命令
ar rcs libstatic.a *.o *.o
,這樣可以將可重定位目標粘合在一起,在使用的時候,我們只需要include靜態庫的標頭檔案,使用其中的函式宣告,然後再靜態連結的時候連結之前生成的libstatic.lib
即可,這個時候只有那些使用到的函式定義會被複制到可執行檔案中,沒有使用的不會複製,當然其他cruntime模組肯定會被鏈進來,這是預設的。 -
如下圖是一個可執行檔案所包含的模組,主要分為程式碼段和資料段,以及其他部分,程式執行時前兩部分會載入到記憶體中,然後跳轉到系統函式
_start()
處開始執行,_start()
函式是C執行時庫中定義的,然後_start()
呼叫_libc_start_main()
,然後再呼叫使用者程式碼中的main()
函式
- 如下圖是可執行檔案載入到記憶體中虛擬地址空間佈局,我們在平時寫程式碼的時候,需要理解其中的不同區域的意義:
在linux x86-64系統中,程式碼段總是從0x400000處開始,這部分是隻讀的;然後是資料段;接下來是堆記憶體段,堆記憶體是從低地址向高地址分配的;然後是一部分為共享庫保留的記憶體區域;然後是棧空間,起始地址是248-1,這是最大合法使用者地址,棧的開闢方向是從高到低;在往上,從248開始的地址是為作業系統的程式碼和資料保留的,對使用者程式碼不可見。
-
在啟動可執行檔案後,作業系統會使用地址空間隨機化的策略,棧、共享庫和堆的執行時地址都會變化,以防止受到攻擊。
-
動態連結庫(共享庫),一種特殊的可重定位目標檔案。生成共享庫的方式和靜態庫類似,linux下編譯命令為
gcc -shared -fpic -o libshared.so A.c B.c
,這樣就可以生成位置無關的動態連結庫檔案,在使用時,通過命令gcc -o main main.c ./libshared.so
完成動態連結,這個動作只會複製符號表和重定位資訊。值得一提的是,在windows下,動態庫的符號表和可重定位資訊單獨存放在一個.lib的動態庫的匯入庫檔案中,而真正的動態庫實現在另一個同名的.dll中,所以在Windows下執行動態連結其實是靜態連結匯入庫的過程。 -
動態連結庫的使用,在可執行檔案啟動時,可執行檔案會檢查一個名為.interp的section, 裡面包含了動態連結器(ld-linux.so)的路徑名,啟動動態連結器來執行重定位程式碼和資料的工作,將動態連結的共享庫載入到某個記憶體段,然後重定位可執行檔案中由動態庫定義的符號引用。完成重定位後再將控制權交還給可執行檔案,至此完成動態庫的載入和重定位工作,以後動態庫的記憶體位置就固定了。這種動態庫方式需要在編譯時就連結動態庫,在可執行檔案開始執行前就要完成載入,這種方式稱為動態庫的靜態載入。
-
動態庫的動態載入,這種技術更加靈活,無需再編譯期將動態庫連結到應用中,在執行期間載入某個共享庫進行使用。linux下可使用
void *dlopen(const char *filename, int flag)
進行執行期載入動態庫,示例:void* handle = dlopen("./libvector.so", RTLD_LAZY)
,訊號RTLD_LAZY意思是推遲符號解析直到動態庫中的程式碼被呼叫時。使用動態庫的函式的方法:void *dlsym(void* handle, char* symbol)
,比如說我們蒂阿勇控制程式碼handle指向的共享庫中的add(int a, int b)
函式,那麼add = dlsym(handle, "add")
將返回函式add(int a, int b)
的地址供使用;最後,呼叫方法int dlclose(void* handle)
可以將動態庫關閉(解除安裝)。
以上做了一個簡要總結,這些內容在我們寫具體程式碼時可能不太重視,但是對構建知識體系,處理一些連結bug還是非常重要的。