內(nèi)功修煉:程序是如何運(yùn)行起來(lái)的

對(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ǔ)言的編譯屋彪。下圖為總覽。

GCC編譯過(guò)程

預(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);
Compilation
  • 詞法分析:掃描器(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)鏈接,如下圖凤优。


靜態(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)┘;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末在岂,一起剝皮案震驚了整個(gè)濱河市奔则,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蔽午,老刑警劉巖易茬,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異及老,居然都是意外死亡抽莱,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)写半,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)岸蜗,“玉大人尉咕,你說(shuō)我怎么就攤上這事叠蝇。” “怎么了年缎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵悔捶,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我单芜,道長(zhǎng)蜕该,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任洲鸠,我火速辦了婚禮堂淡,結(jié)果婚禮上馋缅,老公的妹妹穿的比我還像新娘。我一直安慰自己绢淀,他們只是感情好萤悴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著皆的,像睡著了一般覆履。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上费薄,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天硝全,我揣著相機(jī)與錄音,去河邊找鬼楞抡。 笑死伟众,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的召廷。 我是一名探鬼主播赂鲤,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼柱恤!你這毒婦竟也來(lái)了数初?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤梗顺,失蹤者是張志新(化名)和其女友劉穎泡孩,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體寺谤,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡仑鸥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了变屁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眼俊。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖粟关,靈堂內(nèi)的尸體忽然破棺而出疮胖,到底是詐尸還是另有隱情,我是刑警寧澤闷板,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布澎灸,位于F島的核電站,受9級(jí)特大地震影響遮晚,放射性物質(zhì)發(fā)生泄漏性昭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一县遣、第九天 我趴在偏房一處隱蔽的房頂上張望糜颠。 院中可真熱鬧汹族,春花似錦、人聲如沸其兴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)忌警。三九已至搁拙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間法绵,已是汗流浹背箕速。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留朋譬,地道東北人盐茎。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像徙赢,于是被迫代替她去往敵國(guó)和親字柠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容