目錄
一、C/C++編譯過程
二、ELF檔案
三、符号解析和重定位
四、靜态連結和動态連結
一、C/C++編譯過程
C/C++程式從源代碼到可執行檔案需要經理預處理(預編譯),編譯,彙編,連結四個過程:
1、預處理:對源代碼中的僞指令(以# 開頭的指令)和特殊符号進行處理,如#include指令,預處理會将對應的頭檔案(即.h檔案,聲明全局變量和函數,相當于java中的接口類)寫入到源代碼檔案(即.c檔案,包含函數的具體實作)。預處理後生成的是.i字尾結尾的檔案,依然是文本檔案。
2、編譯:對預處理結果檔案做詞法和文法分析,在确認所有的指令都符合文法規則之後,将其翻譯成等價的中間代碼表示或彙編代碼,翻譯過程中會執行兩種通用的編譯優化,一種是代碼層面的如代碼外提,複制傳播等,一種是跟硬體強相關的優化,包括選擇更高效的機器指令,合理配置設定和指派寄存器,具體優化内容可參考《編譯原理》,可通過gcc的參數指定優化的強度,通常優化強度越高編譯越慢。因為寄存器的種類和數量是取決于CPU架構的,通常隻有對應架構下的彙編指令可以直接操作寄存器,C語言中有register關鍵字請求編譯器盡量将某個變量放入寄存器中,但是最終是否放入寄存器由編譯器根據硬體環境決定。編譯過程産生的檔案還是文本檔案,以.s結尾,該檔案是對應平台下的翻譯出的彙編代碼。
3、彙編:指把彙編語言代碼翻譯成目标機器指令的過程,該過程相對簡單,将對應的彙編指令替換成數字形式的指令碼即可。彙編完成後生成的檔案以.o結尾,通常是可重定位目标檔案。
4、連結: 編譯時是以單個.c檔案為機關編譯的,是以會産生多個.o檔案,将多個.o檔案與之依賴的共享庫如libc連結在一起形成可執行檔案,可執行檔案的字尾可以是out或者elf。Linux加載可執行檔案過程中不會校驗檔案字尾而校驗是否符合ELF檔案格式。
上述每一步操作都可以通過gcc指令單獨觸發,整體的過程如下圖:

參考: C語言編譯過程詳解
C/C++程式編譯過程詳解
二、ELF檔案
ELF全稱Executable and Linkable Format,即可執行和可連結的格式,是UNIX系統實驗室(USL)為應用程式二進制接口(Application Binary Interface,ABI)而開發和釋出的,是所有類UNIX系統的主要可執行檔案格式,windows系統對應的可執行檔案格式簡稱PE,兩者都是COFF格式的變種。
Linux上的ELF檔案主要有三種:
1、可重定向檔案,即通過彙編産生的檔案,字尾是.o,該檔案不能直接運作,
2、可執行檔案,将多個可重定向檔案和共享庫檔案通過連結産生,可以直接運作
3、共享庫,如libc的共享庫libc.so,該檔案同樣不能直接運作,同可重定向檔案相比,最大的差別在于該檔案不需要經過重定向處理。
ELF檔案的格式如下:
- ELF header: 描述整個檔案的組織,包含ELF檔案類型,硬體平台類型,程式執行入口, sections和segments的數量和起始偏移位置,大小等。
- Program Header Table: 描述檔案中的各種segments,通常一個segment包含若幹個屬性(如讀寫權限等)相同的section,将section合并成segment是為了減少記憶體空間浪費,友善記憶體管理,section的大小是任意的,但是segment的大小必須是所在作業系統的記憶體頁(如4KB)大小的整數倍。作業系統加載可執行檔案時會把LOAD類型的segment映射至虛拟位址空間。可重定向檔案中沒有此項,隻有可執行檔案中才有。
- sections 或者 segments:具體的sections,sections是将彙編代碼檔案中的各種資料做歸類儲存,友善對其做記憶體配置設定與管理, 如.text section是可執行指令的集合,.data section包含初始化的全局變量,.bss section儲存的是未初始化的全局變量和局部靜态變量,.dynsym section記錄了所有需要重定向處理的符号等。segments是從程式加載和運作的角度來描述elf檔案,sections是從連結的角度來描述elf檔案,也就是說,在連結階段,我們可以忽略program header table來處理此檔案,在運作階段可以忽略section header table來處理此程式。
- - Section Header Table: 包含了檔案各個section的屬性資訊,比如起始偏移位置,大小等。
ELF檔案格式可通過 readelf ,objdump,gdb等工具檢視具體内容,該檔案各部分詳細說明參考滕啟明寫的《ELF檔案格式分析》和《程式員的自我修養》。
參考: linux,windows 可執行檔案(ELF、PE)
ELF格式檔案詳細分析
三、符号解析和重定位
對多個可重定位目标檔案和其引用的共享庫檔案進行連結時,首先會逐一查找校驗可重定位檔案使用的所有變量或者函數,包括本子產品内定義的和引入自其他子產品的,是否存在合法的唯一的定義,如果查找校驗失敗就會報錯符号未找到(undefined reference)。所謂的符号就是源代碼中使用的函數名或者變量名,符号是為了提高代碼的可讀性,友善程式設計使用,編譯時需要将所有的符号替換成記憶體中的相對位址或者絕對位址,因為底層的機器指令隻認識記憶體位址。上述查找校驗符号并将其替換成記憶體位址的過程就稱為符号解析。
查找校驗完符号後就會将多個可重定位檔案按照輸入的檔案順序以section為次元進行合并,一個一個的拼接,因為單個可重定位目标檔案中使用的相對位址的起始位址都是0,是以合并時需要将原來的相對于0的位址都加上一個偏移位址,并改寫對應section,最後更新對應的section Table。計算偏移位址的時候除了考慮檔案拼接因素外,還需要考慮section對應segment在虛拟位址空間中的分布,考慮記憶體頁的大小,即記憶體布局優化,這個過程就稱為位址和空間配置設定,位址和空間指的是虛拟位址和空間。
單個源代碼檔案在編譯時并不知道其引用的其他子產品中的全局變量和函數的具體記憶體位址,是以編譯後的彙編代碼(.text section)中此類未知符号都有對應的特定記憶體位址表示,并在可重定位表(.text.rel section)中記錄了這類未知符号。連結時會查找這類未知符号的真實位址,并改寫彙編代碼中使用的特定位址,這個過程就是重定位,即将代碼指令中使用的假位址替換成真實記憶體位址的操作,重定位是符号解析的核心。在程式靜态編譯環節發生的重定位叫靜态重定位,在程式加載完成,動态連結過程産生的重定位稱為動态重定位。
參考: 程式的連結和裝入及Linux下動态連結的實作
ELF學習--重定位檔案
ELF學習--可執行檔案
四、靜态連結和動态連結
在靜态編譯環節将多個存在依賴關系的檔案(子產品或者庫)做合理拼接就稱為靜态連結。如果多個程序即對應的多個可執行檔案都依賴了同一個子產品,在靜态連結下,該子產品的代碼和資料則會在硬碟,記憶體中都各儲存一份,實際上代碼是可以多個程序共享的,這樣就導緻了記憶體硬碟存儲空間的浪費。如果該子產品代碼更新,就必須對可執行檔案進行二次編譯才能使用更新後的子產品代碼。
解決上述問題的方法就是動态連結,即将符合解析中核心操作重定位推遲到程式運作時進行,具體而言就是在代碼指令運作過程中隻有用到了某個來自其他子產品的全局變量或者函數才會觸發對應的重定位,該重定位由動态連結重定位表,符号表和動态連結器完成,符号表和重定位表記錄需要動态連結的符号及其所屬的子產品ID,函數名等,動态連接配接器根據重定位表的資訊找到符号對應的真實位址。動态連結下,彼此互相依賴的多個子產品可以獨立開發,獨立編譯,子產品間通過定義全局變量和函數的頭檔案調用,因為同一個頭檔案可以有不同的實作,是以可以極大提高程式的可擴充性和相容性;編譯時不需要将依賴的其他子產品檔案合并進來,是以生成的最終可執行檔案體積更小,當其他子產品出現更新時不需要對本子產品二次編譯。動态連結的問題是依賴的子產品更新後可能跟原來的接口不相容且有一定的性能損耗(與靜态連結比,在5%以下)。
某個庫檔案通過靜态連結還是動态連結的方式編譯由庫檔案本身決定,編譯形成共享庫檔案時,可以通過參數指定形成靜态連結庫檔案和動态連結庫檔案,前者.a結尾,後者.so結尾,預設是動态連結庫檔案,在連結時連結器判斷符号所屬的庫檔案是動态連結庫就會做特殊處理,将這類符号放在單獨的動态連結符号表和可重定位表中。靜态連結庫檔案中每個函數對應一個目标檔案,如printf函數對應printf.o檔案,這樣拆分是為了避免引入其他不需要的函數而導緻最終的可執行檔案體積過大。動态連結庫檔案是在程式被裝載的時候由動态連結器加載到對應程序的虛拟位址空間内(即完成庫檔案的記憶體映射),在完成動态連結後才将控制權交給可執行檔案的入口位址,由動态連接配接器保證記憶體中的庫檔案隻有一份,但資料是每個程序獨立的。動态連結器的庫檔案路徑在可執行檔案的.interp section内,Linux下通常是/lib/ld-linux.so.2。
參考: 《程式員的自我修養》
c語言程式編譯運作過程;靜态連結,動态連結
ELF--動态連結