對(duì)于任何一個(gè)學(xué)習(xí)過(guò)C語(yǔ)言的來(lái)說(shuō)脯厨,“HelloWorld”程序都不會(huì)陌生鸭津。因?yàn)樗鼞?yīng)該是你打開(kāi)新世界的看到的第一束光黄锤。至今我還記得第一次敲出這個(gè)程序的時(shí)候激動(dòng)了好久结执。但是你們知道短短的幾行代碼,是怎么讓程序運(yùn)行起來(lái)的么蔽介?
// hello.c
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Hello World!\n");
return 0;
}
程序是如何運(yùn)行起來(lái)的摘投?很多人可能會(huì)說(shuō),不就是五個(gè)步驟:預(yù)處理(Prepressing)虹蓄,編譯(Compilation)犀呼,匯編(Assembly)和鏈接(Linking),裝載(Loading)么薇组?是這樣的外臂。但是你知道每一步背后都做過(guò)一些什么嗎?如果你能回答上以下的問(wèn)題律胀,我想這個(gè)文章就沒(méi)有必要看下去了宋光。
在main()函數(shù)調(diào)用之前貌矿,程序做過(guò)一些什么?
編譯出來(lái)的可執(zhí)行文件里面有什么罪佳,在內(nèi)存中是什么樣子的逛漫,是怎么來(lái)組織的?
靜態(tài)鏈接菇民、動(dòng)態(tài)鏈接,有什么區(qū)別投储?
不同的編譯器(Micrsoft VC/VS, GCC)和不同的硬件平臺(tái)(X86第练,SPARC,MIPS玛荞,ARM)娇掏,以及不同的操作系統(tǒng)(Windows,Linux勋眯,Unix婴梧,Solaris),最終編譯出來(lái)的結(jié)果一樣么客蹋?
ELF文件塞蹭,PE文件,COFF文件讶坯,是什么番电?
如果你發(fā)現(xiàn)對(duì)其中的一些問(wèn)題,不是很了解的話辆琅,甚至沒(méi)有想過(guò)這些問(wèn)題的時(shí)候漱办,而你有向了解一下,那么就可以婉烟,跟著我的步伐一步倆步娩井,往下看啦。這個(gè)文章是為你準(zhǔn)備的似袁。需要聲明的是洞辣,本文主要針對(duì)gcc編譯器,也就是針對(duì)C和C++昙衅,不一定適用于其他語(yǔ)言的編譯屋彪。下圖為總覽。
預(yù)處理
預(yù)處理的過(guò)程绒尊,其實(shí)畜挥,主要是處理那些源代碼中以#
開(kāi)始的預(yù)編譯指令。比如#include
婴谱,#define
等蟹但,處理的規(guī)則如下:
將所有的
#define
刪除躯泰,并且展開(kāi)所有的宏定義處理所有的條件預(yù)編譯指令,比如
#if
华糖,#ifdef
麦向,#elif
,#else
客叉,#endif
等處理
#include
預(yù)編譯指令诵竭,將被包含的文件插入到該預(yù)編譯指令的位置。在這個(gè)插入的過(guò)程中兼搏,是遞歸進(jìn)行的卵慰,也就是說(shuō)被包含的文件,可能還包含其他文件佛呻。刪除所有注釋
//
和/**/
.添加行號(hào)和文件標(biāo)識(shí)裳朋,以便編譯時(shí)產(chǎn)生調(diào)試用的行號(hào)及編譯錯(cuò)誤警告行號(hào)。
保留所有的
#pragma
編譯器指令吓著,因?yàn)榫幾g器需要使用它們鲤嫡。
對(duì)于第一步預(yù)編譯的過(guò)程,可以通過(guò)以下方式完成:
gcc -E hello.c -o hello.i
或者
cpp hello.c > hello.i
編譯
編譯過(guò)程可分為6步:詞法分析绑莺、語(yǔ)法分析暖眼、語(yǔ)義分析、源代碼優(yōu)化纺裁、代碼生成罢荡、目標(biāo)代碼優(yōu)化。對(duì)應(yīng)與下圖的每一步对扶。下面我們以一個(gè)具體的表達(dá)式進(jìn)行分析:
array[index] = (index + 4)*(2 + 6);
- 詞法分析:掃描器(Scanner)將源代的字符序列分割成一系列的記號(hào)(Token)区赵。
記號(hào) | 類型 |
---|---|
array | 標(biāo)記符 |
[ | 左方括號(hào) |
index | 標(biāo)記符 |
] | 右標(biāo)記符 |
= | 賦值 |
( | 左圓括號(hào) |
index | 標(biāo)記符 |
+ | 加號(hào) |
4 | 數(shù)字 |
) | 右圓括號(hào) |
* | 乘號(hào) |
( | 左圓括號(hào) |
2 | 數(shù)字 |
+ | 加號(hào) |
6 | 數(shù)字 |
) | 右圓括號(hào) |
注:lex工具,可實(shí)現(xiàn)按照用戶描述的詞法規(guī)則將輸入的字符串分割為一個(gè)一個(gè)記號(hào)浪南。
-
語(yǔ)法分析:語(yǔ)法分析器將記號(hào)(Token)產(chǎn)生語(yǔ)法樹(shù)(Syntax Tree)笼才。
Syntax Tree
注:yacc工具(yacc: Yet Another Compiler Compiler)可實(shí)現(xiàn)語(yǔ)法分析,根據(jù)用戶給定的語(yǔ)法規(guī)則對(duì)輸入的記號(hào)序列進(jìn)行解析络凿,從而構(gòu)建一個(gè)語(yǔ)法樹(shù)骡送,所以它也被稱為“編譯器編譯器(Compiler Compiler)”。
-
語(yǔ)義分析:編譯器所分析的語(yǔ)義是靜態(tài)語(yǔ)義絮记,所謂靜態(tài)語(yǔ)義就是指在編譯期可以確定的語(yǔ)義摔踱,通常包括聲明,和類型的匹配怨愤,類型的轉(zhuǎn)換派敷。
Commented Syntax Tree
注:與之對(duì)于的為動(dòng)態(tài)語(yǔ)義分析,只有在運(yùn)行期才能確定的語(yǔ)義。
-
源代碼優(yōu)化:源代碼優(yōu)化器(Source Code Optimizer)篮愉,在源碼級(jí)別進(jìn)行優(yōu)化腐芍,例如
(2 + 6)
這個(gè)表達(dá)式,其值在編譯期就可以確定试躏。優(yōu)化后的語(yǔ)法樹(shù)猪勇。
Paste_Image.png
但是直接作用于語(yǔ)法樹(shù)比較困難,所以源代碼優(yōu)化器往往將整個(gè)語(yǔ)法數(shù)轉(zhuǎn)化為中間代碼(Intermediate Code)颠蕴。注:中間代碼是與目標(biāo)機(jī)器和運(yùn)行環(huán)境無(wú)關(guān)的泣刹。中間代碼使得編譯器被分為前端和后端。編譯器前端(1-4步)負(fù)責(zé)產(chǎn)生機(jī)器無(wú)關(guān)的中間代碼犀被;編譯器后端(5-6步)將中間代碼轉(zhuǎn)化為目標(biāo)機(jī)器代碼椅您。
目標(biāo)代碼生成:代碼生成器(Code Generator)。
目標(biāo)代碼優(yōu)化:目標(biāo)代碼優(yōu)化器(Target Code Optimizer)弱判。
最后的倆個(gè)步驟十分依賴與目標(biāo)機(jī)器襟沮,因?yàn)椴煌臋C(jī)器有不同的字長(zhǎng)锥惋,寄存器昌腰,整數(shù)數(shù)據(jù)類型和浮點(diǎn)數(shù)據(jù)類型等。
匯編
匯編器是將匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的命令膀跌,每一個(gè)匯編語(yǔ)句幾乎都對(duì)應(yīng)一條機(jī)器指令遭商。匯編相對(duì)于編譯過(guò)程比較簡(jiǎn)單,所以根據(jù)匯編指令和機(jī)器指令的對(duì)照表一一翻譯即可捅伤。匯編過(guò)程可以通過(guò)以下方式完成劫流。
as hello.s -o hello.o
或者
gcc -c hello.s -o hello.o
鏈接
靜態(tài)鏈接
把一個(gè)程序分割為多個(gè)模塊,然后通過(guò)某種方式組合形成一個(gè)單一的程序丛忆,這就是鏈接祠汇。而模塊間如何組合的問(wèn)題,歸根到底熄诡,就是模塊如何進(jìn)行通信的倆個(gè)問(wèn)題:(1) 模塊間的函數(shù)調(diào)用可很,(2) 模塊間的變量訪問(wèn)。但無(wú)論是那一個(gè)問(wèn)題凰浮,其本質(zhì)是獲取一個(gè)地址我抠,函數(shù)運(yùn)行的地址、或者變量存放的地址袜茧。
如果熟悉匯編的菜拓,應(yīng)該會(huì)知道hello.o
文件,既目標(biāo)文件笛厦,是以分段的形式組織在一起的纳鼎。其簡(jiǎn)單來(lái)說(shuō),把程序運(yùn)行的地址劃分為了一段一段的片段,有的片段是用來(lái)存放代碼喷橙,叫代碼段啥么,這樣,可以給這個(gè)段加個(gè)只讀的權(quán)限贰逾,防止程序被修改悬荣;有的片段用來(lái)存放數(shù)據(jù),叫數(shù)據(jù)段疙剑,數(shù)據(jù)經(jīng)常修改氯迂,所以可讀寫(xiě);有的片段用來(lái)存放標(biāo)識(shí)符的名字言缤,比如某個(gè)變量 嚼蚀,某個(gè)函數(shù),叫符號(hào)表管挟;等等轿曙。由于有這么多段,所以為了方便管理僻孝,所以又引入了一個(gè)段导帝,叫段表,方便查找每個(gè)段的位置穿铆。
當(dāng)文件之間相互需要鏈接的時(shí)候您单,就把相同的段合并,然后把函數(shù)荞雏,變量地址修改到正確的地址上 虐秦。這就是靜態(tài)鏈接,如下圖凤优。
但是這里有倆個(gè)問(wèn)題:
-
對(duì)于計(jì)算機(jī)的內(nèi)存和磁盤(pán)的空間浪費(fèi)比較嚴(yán)重
想想一下悦陋,現(xiàn)在一個(gè)靜態(tài)庫(kù),至少都是1MB以上筑辨。但是假如有1000個(gè)或者更多的程序在鏈接的時(shí)候俺驶,都靜態(tài)鏈接了它,那么當(dāng)這些程序運(yùn)行起來(lái)的時(shí)候挖垛,內(nèi)存中就會(huì)存在1000+相同的副本痒钝,還是一模一樣的。這樣痢毒,至少1GB空間就浪費(fèi)了送矩。
-
程序的更新,部署哪替,和發(fā)布會(huì)帶來(lái)很多麻煩
比如一個(gè)程序
Program
所使用的Lib.o
是使用的第三方廠商提供的栋荸,那么當(dāng)該廠商更新了Lib.o
(比如修復(fù)了一個(gè)bug,或者優(yōu)化了性能),那么Program
的廠商就必須要拿到最新版的Lib.o
晌块,然后與Program.o
鏈接爱沟。將新的Program
發(fā)給用戶。這樣匆背,一旦程序任何位置有一個(gè)小小的改動(dòng)呼伸,都會(huì)導(dǎo)致重新下載整個(gè)程序。
動(dòng)態(tài)鏈接
我們的想法很簡(jiǎn)單钝尸,就是當(dāng)?shù)谝粋€(gè)例子在運(yùn)行時(shí)括享,在內(nèi)存中只有一個(gè)副本;第二個(gè)例子在發(fā)生時(shí)珍促,只需要下載更新后的lib铃辖,然后鏈接,就好了猪叙。那么其實(shí)娇斩,這就是動(dòng)態(tài)鏈接的基本思想了:把鏈接這個(gè)過(guò)程推遲到運(yùn)行的時(shí)候在進(jìn)行。在運(yùn)行的時(shí)候動(dòng)態(tài)的選擇加載各種程序模塊穴翩,這個(gè)優(yōu)點(diǎn)犬第,就是后來(lái)被人們用來(lái)制作程序的插件(Plug-in)。
這里藏否,我們不得不介紹一個(gè)東西瓶殃,叫做動(dòng)態(tài)鏈接器充包。它會(huì)在程序運(yùn)行的時(shí)候副签,把程序中所有未定義的符號(hào)(比如調(diào)了動(dòng)態(tài)庫(kù)的一個(gè)函數(shù),或者訪問(wèn)了一個(gè)變量)綁定到動(dòng)態(tài)鏈接庫(kù)中基矮。簡(jiǎn)單的來(lái)說(shuō)就是把程序中函數(shù)的地址改正到動(dòng)態(tài)庫(kù)淆储,之后動(dòng)態(tài)鏈接器會(huì)把控制權(quán)交給程序,然后程序執(zhí)行家浇。
這種在裝載時(shí)修正地址本砰,經(jīng)常被稱為裝載時(shí)重定位(Load Time Relocation)。而靜態(tài)鏈接時(shí)修正钢悲,則被稱為鏈接時(shí)重定位(Link Time Relocation)点额。
可能有的人,就要問(wèn)了莺琳,多個(gè)程序應(yīng)用一個(gè)庫(kù)不會(huì)有問(wèn)題么还棱?變量沖突?是這樣的惭等。動(dòng)態(tài)鏈接文件珍手,把那些需要修改的部分分離了出來(lái),與數(shù)據(jù)放在了一起,這樣指令部分就可以保持不變琳要,而數(shù)據(jù)部分可以在每個(gè)進(jìn)程中擁有一個(gè)副本寡具,這種方案就是目前被稱為地址無(wú)關(guān)代碼(PIC,Position-independent Code)的技術(shù)稚补。
鏈接庫(kù)
通過(guò)上面童叠,我們了解到了動(dòng)態(tài)鏈接,靜態(tài)鏈接课幕。一組相應(yīng)目標(biāo)文件的集合拯钻,我們稱它為庫(kù)。因而也就有了靜態(tài)鏈接庫(kù)撰豺,動(dòng)態(tài)鏈接庫(kù)粪般。
靜態(tài)鏈接庫(kù):在
Linux
平臺(tái)上,常以.a
或者.o
為拓展名的文件污桦,我們最常用的C語(yǔ)言靜態(tài)庫(kù)亩歹,就位于/usr/lib/libc.a
;而在Windows
平臺(tái)上凡橱,常以.lib
為拓展名的文件小作,比如Visual C++附帶的多個(gè)版本C/C++運(yùn)行庫(kù),在VC安裝的目錄下的lib\
目錄稼钩。動(dòng)態(tài)鏈接庫(kù):在
Linux
平臺(tái)上顾稀,動(dòng)態(tài)鏈接文件為稱為動(dòng)態(tài)共享對(duì)象(DSO,Dynamic Shared Objects)坝撑,簡(jiǎn)稱共享對(duì)象静秆。他們一般常以.so
為拓展名的文件;而在Windows
平臺(tái)上巡李,動(dòng)態(tài)鏈接文件被稱為動(dòng)態(tài)鏈接庫(kù)(DLL抚笔,Dynamical Linking Library),通常就是我們常見(jiàn)的.dll
為拓展名的文件侨拦。
裝載
介紹裝載就不得不介紹三種文件格式了:ELF殊橙,PE,COFF∮樱現(xiàn)在PC平臺(tái)上流行的可執(zhí)行文件格式(Executable)膨蛮,無(wú)論是Windows
下的PE(Portable Executable)文件,還是Linux
下的ELF(Executable Linkable Format)文件季研,都是COFF(Common file format)文件格式的變種敞葛。可執(zhí)行文件例如训貌,Windows
下的*.exe
,Linux
下的/bin/bash
制肮。其實(shí)目標(biāo)文件冒窍,內(nèi)部結(jié)構(gòu)上來(lái)說(shuō)和可執(zhí)行文件的結(jié)構(gòu)幾乎是一樣的,所以一般跟可執(zhí)行文件格式一起用一種格式進(jìn)行存儲(chǔ)豺鼻。
下面以ELF文件為例子综液,介紹。
每一個(gè)ELF文件儒飒,都會(huì)有一個(gè)ELF文件頭谬莹,里面會(huì)記錄很多關(guān)于這個(gè)程序相關(guān)信息,通過(guò)它確定段表桩了,進(jìn)而確定各個(gè)段附帽。總的來(lái)說(shuō)井誉,裝載做了以下三件事情:
創(chuàng)建虛擬地址空間
讀取可執(zhí)行文件頭蕉扮,并且建立虛擬空間與可執(zhí)行文件的映射關(guān)系
將CPU的指令寄存器設(shè)置為運(yùn)行庫(kù)的初始函數(shù)(初始函數(shù)不止一個(gè),第一個(gè)啟動(dòng)函數(shù)為:
_start
)颗圣,初始了main()
函數(shù)的環(huán)境喳钟,然后指向可執(zhí)行文件的入口
以上就是最近幾天看完《程序員的自我修養(yǎng)》一些感悟吧。
└(o)┘;