《電腦組成原理/CSAPP》網課總結(二)——編譯原理基礎
這部分是四月份的安排,拖到五一放假了,主要是對源碼編譯過程的一次總結,總的來說,大致可分為預編譯、編譯、彙編和鏈接四部分。這裡簡單記錄一下:

一 概述
- 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還是非常重要的。