高級語言寫就的程序如何跑起來,編譯器上的build命令其實干了四件事:
預處理 (文本文件->文件文件)
編譯 (文本文件->文本文件)
匯編 (文本文件->二進制文件)
鏈接
文本文件:數字、字母等都用指定編碼如ASCII碼/UNICODE碼等方式編碼并存儲的二進制文件。
二進制文件:編碼格式自由的二進制文件。
今天不講鏈接,只講前三個功能,不求精細,但求思路連貫。
每個.cpp文件是一個編譯單元,.h文件不是,編譯器對每個編譯單元分別處理。
展開宏定義:#define #undef
處理預編譯指令:#if #ifdef #ifndef #elif #else #endif
遞歸處理頭文件:#include
刪除注釋
添加行號和文件名標識別,用于debug
前端(平臺無關)
詞法分析:形象點說就是掃描器,將字符識別為有意義的記號:關鍵字、標識符、字面量(數字、字符串)、操作符等,順便將標示符放到符號表,數字、字符串放到文字表等,鏈接階段要用。
語法分析:對每個表達式構造語法樹-操作符在父節點上,左右操作數在左右孩子上。
語義分析:語法對不代表這句話有意義,不過這階段只能做靜態語義分析,給每個語法樹節點標上類型,靜態類型檢查、類型轉換等都是在這個階段完成。
看完數學之美會知道,自然語言處理已經放棄了語義分析這些傳統方法,轉而采用統計學方法,一大原因就是自然語言是上下文相關的,而人類發明的高級語言是上下文無關的。
源代碼優化:將語法樹用 三地址碼( x = yOPz)等格式表達,容易做優化,優化的代碼包括常量相加之類。
后端(平臺相關)
匯編語言生成:將中間代碼轉化為匯編指令。
匯編指令優化:比如用移位代替乘法等。
這項工作比較簡單,將匯編指令轉化為機器指令,參考匯編指令與機器指令對照表一對一翻譯。
最終獲得的文件稱為可重定位或共享目標文件。
可執行文件格式
可執行文件格式主要有兩種:
PE( Portable Executable ) Windows下。
ELF( Executable Linkable Format ) Linux下。
兩者都是COFF(Common File Format)格式的變種。
由于目標文件一般與可執行文件采用相同的格式存儲,所以廣義上將目標文件與可執行文件看做同一種類型的文件。
Linux下,統稱為ELF文件,或者統稱為目標文件
ELF文件類型說明生成實例
可重定位文件(Relocatable File)可用于鏈接為可執行文件或共享目標文件編譯器和匯編器Linux .o, Windows .obj, 靜態庫
共享目標文件(Shared Object File)特殊的可重定位文件,可鏈接為新目標文件;動態鏈接器將之與可執行文件結合編譯器和匯編器Linux .so, Windows .dll
可執行文件(Executable File)可以直接執行鏈接器Linux 無拓展名, Windows .exe
下表只包含主要段,每個段基本上都有獨特的數據結構定義。
ELF文件部分段
ELF Header文件頭,說明存儲方式、版本、運行平臺,程序頭入口地址和長度,段表的位置、數量和長度
.text代碼段,機器指令流
.data數據段,初始化的全局變量、局部靜態變量
.rodata只讀數據段,放置只讀變量和字符串常量
.BSS存放未初始化的全局變量和局部靜態變量,但由于強弱符號特性,未初始化全局變量有時并不放這里
.comment編譯器版本信息
.strtab字符串表,變量的名字等各種字符串
.shstrtab段名表,每個段的名字
Section Table段表,頭文件之后最重要的部分,存儲各段的段名-在段名表的偏移,段的長度、偏移,讀寫權限等
.symtab符號表,最重要的是全局符號:函數和全局變量信息,符號名(字符串表偏移)、類型、值(可重定位文件中指在代碼段或數據段偏移,可執行文件中指虛擬地址)
.rel.text重定位表,記錄了.text中需要修改的位置(引用了其他編譯單元的全局符號),鏈接時需要更改
.rel.data重定位表,記錄了.data中需要修改的位置(定義或者引用的全局符號位置),鏈接時需要更改
鏈接分兩種:
靜態鏈接:加載前進行,各個編譯單元生成可重定位目標文件(.a或者.obj)后,將它們組合成可執行文件。
動態鏈接:加載時進行,對于共享目標文件(.so或者.dll),靜態鏈接階段不處理這些文件,只是在生成的可執行文件中添加.interp段、.dynamic和.dynsym等段,其中記錄關于動態鏈接器地址、依賴的共享目標文件與重定位信息和符號表等信息。
注:加載的意思就是創建進程,開始運行之前。
本文只談靜態鏈接,靜態鏈接分為兩個階段:1. 虛擬空間分配 2. 符號解析及重定位
由前一篇知,每個可重定位目標文件都包含了很多段,如.text/.data等,那么如何把多個可重定位文件合并為一個可執行文件呢?
如果按序疊加,也即所有可重定位文件原封不動摞在一起,這會導致段太多,且相同名稱段分散。實際處理的辦法是相似段合并。
那么虛擬空間分配的工作就清楚了:
掃描所有可重定位文件,獲取每個段的長度、屬性和位置。
合并同名段,并給合并后的段分配一個虛擬地址(虛擬地址占據的位置和字節大小在ELF文件的數據結構中早就留好了),虛擬地址分配有規則(因為這里的虛擬地址對應的就是加載后進程的虛擬空間)。
收集各文件符號表所定義或者引用的所有全局符號到同一個符號表。
符號地址確定,由前一篇目標文件格式知道,每個可重定位文件都有一個符號表,里面存儲全局符號的名字、屬性和值(在xx段的偏移)。既然已經知道每一段的虛擬地址,那么可以計算得到每個符號的虛擬地址。
既然所有全局符號的虛擬地址都知道了,那么接下來就需要進行符號重定位操作。
重定位:
原因:CPU執行代碼段指令,指令中指明去哪個虛擬地址讀取信息,可重定位文件代碼段如果引用外部符號,那么外部符號的虛擬地址(符號定義的位置)就需要在知道每個符號的虛擬地址后進行更新(未知時填的是0)。
依據:由目標文件的格式知道,其包含重定位表,重定位表描述.text/.data段任何需要更改的指令的位置信息。
過程:
符號解析:每個重定位位置,都是對一個全局符號的引用,必須知道這個符號的虛擬地址,如果在全局符號表找不到這個符號,則報符號未定義錯誤。
指令修正:按照重定位表給出的記錄的修正方式進行修正,有多種方式。
動態鏈接的原因
由于代碼量的膨脹,需要分為不同模塊進行開發,最后將互相依賴的模塊組合,這是鏈接的目的。前文講了靜態鏈接,在程序加載之前生成一個可執行文件。但是靜態鏈接也有明顯的弊端:
浪費內存和磁盤空間:任何一個可執行文件都需要拷貝使用到的目標文件。
不利于程序的發布和更新:如果更新了程序,那么必須重新獲得可執行文件。
沒有插件功能:不能夠開放接口,將實現交給使用方自定義。
兼容性較差:程序在不同平臺運行時,不能動態選擇依賴的鏈接庫。
動態鏈接:在程序加載時才進行鏈接(當然也可以在運行時進行手動動態鏈接),提高了靈活性,但也降低了啟動速度。
加載可執行文件和動態鏈接庫到內存;
將執行流程轉到動態鏈接器(一段共享目標文件),其進行符號解析與重定位工作,不像靜態文件一樣直接轉到main函數開始執行;
動態鏈接器工作完成后,轉到main函數開始執行。
動態鏈接庫的核心是如何生成地址無關的代碼和數據,有兩個問題需要解決:
共享目標文件是共享的,不應該進行重定位(加載地址相關)。
可執行文件的代碼是不能進行重定位的。
模塊內部的函數調用和變量引用采用相對地址;模塊間的函數調用和變量引用采用間接方式訪問,也即添加一個.got段(全局偏移表),其存儲所引用符號的真正地址。那么共享文件的代碼段和不可改數據段就可以共享了,.got段和可變數據才每個進程私有。
可執行文件不能重定位,那么其依賴的動態庫符號必須認為是在可執行文件中定義。這就帶來了一個新問題——共享目標文件也定義了此符號,怎么辦?
解決辦法就是認為所有共享目標文件定義的符號都是在可執行文件中定義。如果可執行文件中確實已經定義,將.got段符號真正地址設為可執行文件中的地址;如果可執行文件中未定義,那么將.got段符號地址設為共享目標文件中的地址。