編譯步驟
GCC 編譯器在編譯一個C語言程序時需要經(jīng)過以下 4 步:
1. 預處理
將C語言源程序預處理,生成.i文件杭棵。{預編譯處理(.c)
a.宏定義指令:將所有的#define刪除亿絮,并且展開所有的宏定義
b.條件編譯指令:處理所有的條件預編譯指令往枷,比如#if #ifdef #elif #else #endif等
c.頭文件包含指令:處理#include 預編譯指令铐刘,將被包含的文件插入到該預編譯指令的位置
d.特殊符號指令:預編譯器可研識別一些特殊的符號缓呛,例如:刪除所有注釋 “//”和”/* */”
e.添加行號和文件標識炎滞,以便編譯時產(chǎn)生調(diào)試用的行號及編譯錯誤警告行號
f.保留所有的#pragma編譯器指令稠诲,因為編譯器需要使用它們
2. 匯編文件
預處理后的.i文件編譯成為匯編語言捎泻,生成.s文件飒炎。{優(yōu)化程序(.s .asm)
- 編譯過程就是把預處理完的文件進行一系列的詞法分析,語法分析笆豁,語義分析及優(yōu)化后生成相應的匯編代碼
3. 目標文件
將匯編語言文件經(jīng)過匯編郎汪,生成目標文件.o文件。{匯編程序(.obj闯狱、 .o煞赢、.a、 .ko)
- 匯編器是將匯編代碼轉(zhuǎn)變成機器可以執(zhí)行的命令哄孤,每一個匯編語句幾乎都對應一條機器指令照筑。根據(jù)匯編指令和機器指令的對照表一一翻譯即可。用一下指令進行匯編
4. 可執(zhí)行文件
將各個模塊的.o文件鏈接起來生成一個可執(zhí)行程序文件录豺。{鏈接程序(.exe朦肘、 .elf、 .axf 等
通過調(diào)用鏈接器ld來鏈接程序運行需要的一大堆目標文件双饥,以及所依賴的其它庫文件媒抠,最后生成可執(zhí)行文件., 鏈接的主要內(nèi)容是把各個模塊之間相互引用的部分處理好咏花,使得各個模塊之間能夠正確地銜接趴生,鏈接分為靜態(tài)鏈接和動態(tài)鏈接。
靜態(tài)鏈接是指在編譯階段直接把靜態(tài)庫加入到可執(zhí)行文件中去昏翰,這樣可執(zhí)行文件會比較大苍匆。靜態(tài)庫文件:是一個二進制文件,存放的功能函數(shù)實現(xiàn),在文件編譯時要訪問文件,編譯之后靜態(tài)庫文件可以刪除
而動態(tài)鏈接則是指鏈接階段僅僅只加入一些描述信息,而程序執(zhí)行時再從系統(tǒng)中把相應動態(tài)庫加載到內(nèi)存中去棚菊,動態(tài)庫文件:是一個二進制文件,存放的功能函數(shù)實現(xiàn),在文件執(zhí)行時要訪問文件,編譯時不需要動態(tài)庫文件
gcc的詳細編譯過程
編寫代碼
為了能夠演示編譯的整個過程浸踩,首先創(chuàng)建一個工作目錄test4,然后生成一個 C語言編寫的hello.c程序统求,代碼如下
//hello.c
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
return 0;
}
編譯過程
對于這個程序检碗,一步到位的編譯指令是: gcc test.c -o test
實質(zhì)上据块,編譯過程是分為四個階段進行的,即預處理(Preprocessing)折剃、編譯(Compilation)另假、匯編(Assembly)和連接(Linking)
預處理
預處理的過程主要包括以下過程:
(1) 將所有的#define 刪除,并且展開所有的宏定義怕犁,并且處理所有的條件預編 譯指令边篮,比如#if #ifdef #elif #else #endif 等
(2) 處理#include 預編譯指令,將被包含的文件插入到該預編譯指令的位置
(3) 刪除所有注釋“//”和“/* */”
(4) 添加行號和文件標識奏甫,以便編譯時產(chǎn)生調(diào)試用的行號及編譯錯誤警告行號
(5)保留所有的#pragma 編譯器指令戈轿,后續(xù)編譯過程需要使用它們
gcc 進行預處理的命令:gcc -E hello.c -o hello.i
如圖,發(fā)現(xiàn)輸出 test.i 文件中存放著 test.c 經(jīng)預處理之后的代碼阵子,gcc 的-E 選項凶杖,可以讓編譯器在預處理后停止,并輸出預處理結(jié)果.
編譯為匯編語言
輸入gcc -S hello.i -o hello.s指令將預處理生成的 hello.i 文件編譯生成匯編程序 hello.s
GCC 的選項-S 使 GCC 在執(zhí)行完編譯后停止款筑,生成匯編程序
匯編
匯編過程調(diào)用對匯編代碼進行處理智蝠,生成處理器能識別的指令,保存在后綴為.o的目標文件中奈梳。由于每一個匯編語句幾乎都對應一條處理器指令杈湾,因此,匯編相 對于編譯過程比較簡單攘须,通過調(diào)用 Binutils 中的匯編器 as根據(jù)匯編指令和處理 器指令的對照表一一翻譯即可漆撞。
當程序由多個源代碼文件構(gòu)成時,每個文件都要先完成匯編工作于宙,生成.o 目標文件后浮驳,才能進入下一步的鏈接工作。注意:目標文件已經(jīng)是最終程序的某一部 分了捞魁,但是在鏈接之前還不能執(zhí)行至会。
輸入gcc -c hello.s -o hello.o
指令將編譯生成的 hello.s 文件匯編生成目標文件 hello.o
或者直接調(diào)用Binutils 中的 as將 hello.s 文件匯編生成目標文件,輸入命令as -c hello.s -o hello.o
注意:hello.o 目標文件為 ELF(Executable and Linkable Format)格式的可重定向文件
鏈接(連接)
gcc 連接器是 gas 提供的谱俭,負責將程序的目標文件與所需的所有附加的目標文件連接起來奉件,最終生成可執(zhí)行文件
鏈接分為靜態(tài)鏈接和動態(tài)鏈接,其要點如下:
(1) 靜態(tài)鏈接是指在編譯階段直接把靜態(tài)庫加入到可執(zhí)行文件中去昆著,這樣可執(zhí)行文件會比較大县貌。鏈接器將函數(shù)的代碼從其所在地(不同的目標文件或靜態(tài)鏈 接庫中)拷貝到最終的可執(zhí)行程序中。為創(chuàng)建可執(zhí)行文件凑懂,鏈接器必須要完成的主要任務(wù)是:符號解析(把目標文件中符號的定義和引用聯(lián)系起來)和 重定位(把符號定義和內(nèi)存地址對應起來然后修改所有對符號的引用)
(2)動態(tài)鏈接則是指鏈接階段僅僅只加入一些描述信息煤痕,而程序執(zhí)行時再從系統(tǒng) 中把相應動態(tài)庫加載到內(nèi)存中去
對于生成的 hello.o,輸入命令gcc hello.o -o hello將其與C標準輸入輸出庫進行連接,最終生成程序 hello然后執(zhí)行
補充
多個程序文件的編譯
通常整個程序是由多個源文件組成的摆碉,相應地也就形成了多個編譯單元祟敛,使用 GCC 能夠很好地管理這些編譯單元
假設(shè)有一個由 test1.c 和 test2.c 兩個源文件組成的程序,為了對它們進行編譯兆解,并最終生成可執(zhí)行程序 test,可以使用命令gcc test1.c test2.c -o test
如果同時處理的文件不止一個跑揉,GCC 仍然會按照預處理锅睛、編譯和鏈接的過程依次進行。如果深究起來历谍,上面這條命令大致相當于依次執(zhí)行如下三條命令:gcc -c test1.c -o test1.o现拒、gcc -c test2.c -o test2.o、gcc test1.o test2.o -o test
檢錯
gcc -pedantic illcode.c -o illcode
-pedantic 編譯選項并不能保證被編譯程序與 ANSI/ISO C 標準的完全兼容望侈,它僅僅只能用來幫助Linux 程序員離這個目標越來越近印蔬。換句話說,-pedantic 選項能夠幫助程序員發(fā)現(xiàn)一些不符合 ANSI/ISO C標準的代碼脱衙,但不是全部侥猬,事實上只有 ANSI/ISO C 語言標準中要求進行編譯器診斷的那些情況,才有可能被 GCC 發(fā)現(xiàn)并提出警告捐韩。
除了-pedantic 之外退唠,GCC 還有一些其它編譯選項也能夠產(chǎn)生有用的警告信息。這些選項大多以-W 開頭荤胁,其中最有價值的當數(shù)-Wall瞧预,使用它能夠使 GCC 產(chǎn)生盡可能多的警告信息。
gcc -Wall illcode.c -o illcode
GCC 給出的警告信息雖然從嚴格意義上說不能算作錯誤仅政,但卻很可能成為錯誤的棲身之所垢油。一個優(yōu) 秀的 Linux程序員應該盡量避免產(chǎn)生警告信息,使自己的代碼始終保持標準圆丹、健壯的特性滩愁。所以將警告信息當成編碼錯誤來對待,是一種值得贊揚的行為辫封!所以惊楼,在編譯程序時帶上-Werror 選項,那 么 GCC會在所有產(chǎn)生警告的地方停止編譯秸讹,迫使程序員對自己的代碼進行修改檀咙,如下:
gcc -Werror test.c -o test
庫文件連接
開發(fā)軟件時,完全不使用第三方函數(shù)庫的情況是比較少見的璃诀,通常來講都需要借助許多函數(shù)庫的支持才能夠完成相應的功能弧可。從程序員的角度看,函數(shù)庫實際上就是一些頭文件(.h)和庫文件(so劣欢、或lib棕诵、dll)的集合裁良。
雖然 Linux下的大多數(shù)函數(shù)都默認將頭文件放到/usr/include/目錄下,而庫文件則放到/usr/lib/目錄下校套;Windows所使用的庫文件主要放在 Visual Stido 的目錄下的 include 和 lib价脾,以及系統(tǒng)文件夾下。但有的時候笛匙,我們要用的庫不再這些目錄下侨把,所以 GCC 在編譯時必須用自己的辦法來查找所需要的頭文件和庫文件。
例如:我們的程序 test.c 是在 linux 上使用 c 連接 mysql妹孙,這個時候我們需要去 mysql 官網(wǎng)下載 MySQL Connectors 的 C 庫秋柄,下載下來解壓之后,有一個 include 文件夾蠢正,里面包含mysql connectors 的頭文件骇笔,還有一個 lib 文件夾,里面包含二進制 so 文件 libmysqlclient.so,其中 inclulde 文件夾的路徑是/usr/dev/mysql/include,lib 文件夾是/usr/dev/mysql/lib
編譯成可執(zhí)行文件
執(zhí)行gcc –c –I /usr/dev/mysql/include test.c –o test.o
命令編譯 test.c 為目標文件
鏈接
把所有目標文件鏈接成可執(zhí)行文件:
gcc –L /usr/dev/mysql/lib –lmysqlclient test.o –o test
Linux 下的庫文件分為兩大類分別是動態(tài)鏈接庫(通常以.so 結(jié)尾)和靜態(tài)鏈接庫(通常以.a結(jié)尾)嚣崭,二者的區(qū)別僅在于程序執(zhí)行時所需的代碼是在運行時動態(tài)加載的笨触,還是在編譯時靜態(tài)加載的
強制鏈接時使用靜態(tài)鏈接庫
默認情況下, GCC 在鏈接時優(yōu)先使用動態(tài)鏈接庫雹舀,只有當動態(tài)鏈接庫不存在時才考慮使用靜態(tài)鏈接庫旭旭,如果需要的話可以在編譯時加上-static 選項,強制使用靜態(tài)鏈接庫葱跋。
在/usr/dev/mysql/lib 目錄下有鏈接時所需要的庫文件 libmysqlclient.so 和 libmysqlclient.a持寄,為了讓GCC 在鏈接時只用到靜態(tài)鏈接庫,使用下面的命令:gcc –L /usr/dev/mysql/lib –static –lmysqlclient test.o –o test
靜態(tài)庫鏈接時搜索路徑順序:
ld 會去找 GCC 命令中的參數(shù)-L
再找 gcc 的環(huán)境變量 LIBRARY_PATH
再找內(nèi)定目錄 /lib /usr/lib /usr/local/lib 這是當初 compile gcc 時寫在程序內(nèi)的
動態(tài)鏈接時娱俺、執(zhí)行時搜索路徑順序:
編譯目標代碼時指定的動態(tài)庫搜索路徑
環(huán)境變量 LD_LIBRARY_PATH 指定的動態(tài)庫搜索路徑
配置文件/etc/ld.so.conf 中指定的動態(tài)庫搜索路徑
默認的動態(tài)庫搜索路徑/lib
默認的動態(tài)庫搜索路徑/usr/lib
有關(guān)環(huán)境變量:
LIBRARY_PATH 環(huán)境變量:指定程序靜態(tài)鏈接庫文件搜索路徑
LD_LIBRARY_PATH 環(huán)境變量:指定程序動態(tài)鏈接庫文件搜索路徑
ELF文件的分析
ELF全稱Executable and Linkable Format稍味,即可執(zhí)行和可鏈接的格式,是UNIX系統(tǒng)實驗室(USL)為應用程序二進制接口(Application Binary Interface荠卷,ABI)而開發(fā)和發(fā)布的模庐,是所有類UNIX系統(tǒng)的主要可執(zhí)行文件格式,windows系統(tǒng)對應的可執(zhí)行文件格式簡稱PE油宜,兩者都是COFF格式的變種掂碱。
Linux上的ELF文件主要有三種:
1、可重定向文件慎冤,即通過匯編產(chǎn)生的文件疼燥,后綴是.o,該文件不能直接運行蚁堤,
2醉者、可執(zhí)行文件,將多個可重定向文件和共享庫文件通過鏈接產(chǎn)生,可以直接運行
3撬即、共享庫立磁,如libc的共享庫libc.so,該文件同樣不能直接運行剥槐,同可重定向文件相比唱歧,最大的區(qū)別在于該文件不需要經(jīng)過重定向處理。
ELF文件的段
ELF 文件格式如下圖所示粒竖,位于 ELF Header 和 Section Header Table 之間的都是段
一個典型ELF文件包含的段 | 含義 |
---|---|
.text | 已編譯程序的指令代碼段 |
.rodata | :ro 代表 read only颅崩,即只讀數(shù)據(jù)(例如常數(shù) const) |
.data | 已初始化的 C 程序全局變量和靜態(tài)局部變量 |
.bss | 未初始化的 C 程序全局變量和靜態(tài)局部變量 |
.debug | 調(diào)試符號表,調(diào)試器用此段的信息幫助調(diào)試 |
- ELF header: 描述整個文件的組織温圆,包含ELF文件類型,硬件平臺類型孩革,程序執(zhí)行入口, sections和segments的數(shù)量和起始偏移位置岁歉,大小等。
- Program Header Table: 描述文件中的各種segments膝蜈,通常一個segment包含若干個屬性(如讀寫權(quán)限等)相同的section锅移,將section合并成segment是為了減少內(nèi)存空間浪費,方便內(nèi)存管理饱搏,section的大小是任意的非剃,但是segment的大小必須是所在操作系統(tǒng)的內(nèi)存頁(如4KB)大小的整數(shù)倍。操作系統(tǒng)加載可執(zhí)行文件時會把LOAD類型的segment映射至虛擬地址空間推沸”刚溃可重定向文件中沒有此項,只有可執(zhí)行文件中才有鬓催。
sections 或者 segments:具體的sections肺素,sections是將匯編代碼文件中的各種數(shù)據(jù)做歸類保存,方便對其做內(nèi)存分配與管理宇驾, 如.text section是可執(zhí)行指令的集合倍靡,.data section包含初始化的全局變量,.bss section保存的是未初始化的全局變量和局部靜態(tài)變量课舍,.dynsym section記錄了所有需要重定向處理的符號等塌西。segments是從程序加載和運行的角度來描述elf文件,sections是從鏈接的角度來描述elf文件筝尾,也就是說捡需,在鏈接階段,我們可以忽略program header table來處理此文件筹淫,在運行階段可以忽略section header table來處理此程序栖忠。 - Section Header Table: 包含了文件各個section的屬性信息,比如起始偏移位置,大小等庵寞。
使用 readelf -S 查看其各個 section 的信息
例如:輸入readelf -S hello命令
反匯編ELF
由于 ELF 文件無法被當做普通文本文件打開狸相,如果希望直接查看一個 ELF 文件包含的指令和數(shù)據(jù),需要使用反匯編的方法捐川。
使用objdump -D 對其進行反匯編如下
輸入命令objdump -D hello進行反匯編
使用 objdump -S 將其反匯編并且將其C語言源代碼混合顯示出來
輸入命令gcc -o hello -g hello.c脓鹃、objdump -S hello
符號解析和重定位
對多個可重定位目標文件和其引用的共享庫文件進行鏈接時,首先會逐一查找校驗可重定位文件使用的所有變量或者函數(shù)古沥,包括本模塊內(nèi)定義的和引入自其他模塊的瘸右,是否存在合法的唯一的定義,如果查找校驗失敗就會報錯符號未找到(undefined reference)岩齿。所謂的符號就是源代碼中使用的函數(shù)名或者變量名太颤,符號是為了提高代碼的可讀性,方便編程使用盹沈,編譯時需要將所有的符號替換成內(nèi)存中的相對地址或者絕對地址龄章,因為底層的機器指令只認識內(nèi)存地址。上述查找校驗符號并將其替換成內(nèi)存地址的過程就稱為符號解析乞封。
查找校驗完符號后就會將多個可重定位文件按照輸入的文件順序以section為維度進行合并做裙,一個一個的拼接,因為單個可重定位目標文件中使用的相對地址的起始地址都是0肃晚,所以合并時需要將原來的相對于0的地址都加上一個偏移地址锚贱,并改寫對應section,最后更新對應的section Table关串。計算偏移地址的時候除了考慮文件拼接因素外拧廊,還需要考慮section對應segment在虛擬地址空間中的分布,考慮內(nèi)存頁的大小晋修,即內(nèi)存布局優(yōu)化卦绣,這個過程就稱為地址和空間分配,地址和空間指的是虛擬地址和空間飞蚓。
單個源代碼文件在編譯時并不知道其引用的其他模塊中的全局變量和函數(shù)的具體內(nèi)存地址滤港,因此編譯后的匯編代碼(.text section)中此類未知符號都有對應的特定內(nèi)存地址表示,并在可重定位表(.text.rel section)中記錄了這類未知符號趴拧。鏈接時會查找這類未知符號的真實地址溅漾,并改寫匯編代碼中使用的特定地址,這個過程就是重定位著榴,即將代碼指令中使用的假地址替換成真實內(nèi)存地址的操作添履,重定位是符號解析的核心。在程序靜態(tài)編譯環(huán)節(jié)發(fā)生的重定位叫靜態(tài)重定位脑又,在程序加載完成暮胧,動態(tài)鏈接過程產(chǎn)生的重定位稱為動態(tài)重定位锐借。
參考: 程序的鏈接和裝入及Linux下動態(tài)鏈接的實現(xiàn)
ELF學習--重定位文件
ELF學習--可執(zhí)行文件
靜態(tài)鏈接和動態(tài)鏈接
在靜態(tài)編譯環(huán)節(jié)將多個存在依賴關(guān)系的文件(模塊或者庫)做合理拼接就稱為靜態(tài)鏈接。如果多個進程即對應的多個可執(zhí)行文件都依賴了同一個模塊往衷,在靜態(tài)鏈接下钞翔,該模塊的代碼和數(shù)據(jù)則會在硬盤,內(nèi)存中都各保存一份席舍,實際上代碼是可以多個進程共享的布轿,這樣就導致了內(nèi)存硬盤存儲空間的浪費。如果該模塊代碼更新来颤,就必須對可執(zhí)行文件進行二次編譯才能使用更新后的模塊代碼汰扭。
解決上述問題的方法就是動態(tài)鏈接,即將符合解析中核心操作重定位推遲到程序運行時進行福铅,具體而言就是在代碼指令運行過程中只有用到了某個來自其他模塊的全局變量或者函數(shù)才會觸發(fā)對應的重定位萝毛,該重定位由動態(tài)鏈接重定位表,符號表和動態(tài)鏈接器完成滑黔,符號表和重定位表記錄需要動態(tài)鏈接的符號及其所屬的模塊ID笆包,函數(shù)名等,動態(tài)連接器根據(jù)重定位表的信息找到符號對應的真實地址拷沸。動態(tài)鏈接下色查,彼此相互依賴的多個模塊可以獨立開發(fā)薯演,獨立編譯撞芍,模塊間通過定義全局變量和函數(shù)的頭文件調(diào)用,因為同一個頭文件可以有不同的實現(xiàn)跨扮,所以可以極大提高程序的可擴展性和兼容性序无;編譯時不需要將依賴的其他模塊文件合并進來,所以生成的最終可執(zhí)行文件體積更小衡创,當其他模塊出現(xiàn)更新時不需要對本模塊二次編譯帝嗡。動態(tài)鏈接的問題是依賴的模塊更新后可能跟原來的接口不兼容且有一定的性能損耗(與靜態(tài)鏈接比,在5%以下)璃氢。
某個庫文件通過靜態(tài)鏈接還是動態(tài)鏈接的方式編譯由庫文件本身決定哟玷,編譯形成共享庫文件時,可以通過參數(shù)指定形成靜態(tài)鏈接庫文件和動態(tài)鏈接庫文件一也,前者.a結(jié)尾巢寡,后者.so結(jié)尾,默認是動態(tài)鏈接庫文件椰苟,在鏈接時鏈接器判斷符號所屬的庫文件是動態(tài)鏈接庫就會做特殊處理抑月,將這類符號放在單獨的動態(tài)鏈接符號表和可重定位表中。靜態(tài)鏈接庫文件中每個函數(shù)對應一個目標文件舆蝴,如printf函數(shù)對應printf.o文件谦絮,這樣拆分是為了避免引入其他不需要的函數(shù)而導致最終的可執(zhí)行文件體積過大题诵。動態(tài)鏈接庫文件是在程序被裝載的時候由動態(tài)鏈接器加載到對應進程的虛擬地址空間內(nèi)(即完成庫文件的內(nèi)存映射),在完成動態(tài)鏈接后才將控制權(quán)交給可執(zhí)行文件的入口地址层皱,由動態(tài)連接器保證內(nèi)存中的庫文件只有一份性锭,但數(shù)據(jù)是每個進程獨立的。動態(tài)鏈接器的庫文件路徑在可執(zhí)行文件的.interp section內(nèi)奶甘,Linux下通常是/lib/ld-linux.so.2篷店。
參考: 《程序員的自我修養(yǎng)》
c語言程序編譯運行過程;靜態(tài)鏈接臭家,動態(tài)鏈接
ELF--動態(tài)鏈接