由于前段時(shí)間期末考試,所以一直沒(méi)有更新博客漫蛔,最近又來(lái)了搜狐實(shí)習(xí)嗜愈,一直在趕需求,感覺(jué)自己好久沒(méi)有更新博客了莽龟,這幾天趕完了需求蠕嫁,還是抽時(shí)間來(lái)更新一下博客吧。
正文
我們平常寫程序的時(shí)候毯盈,一般都是使用一個(gè)好用的IDE剃毒,然后寫好代碼,run一下程序就運(yùn)行起來(lái)了搂赋,但是不知道大家是不是也思考過(guò)程序到底是怎么運(yùn)行起來(lái)的呢迟赃?
這其實(shí)是一個(gè)很復(fù)雜的過(guò)程,我的了解也是非常的淺顯厂镇,所以只能簡(jiǎn)單介紹一下它的大概步驟纤壁。
一個(gè)典型的程序運(yùn)行步驟如下:
- 操作系統(tǒng)在創(chuàng)建進(jìn)程后,把控制權(quán)交到程序的入口捺信,這個(gè)入口往往是運(yùn)行庫(kù)中的某個(gè)入口函數(shù)
- 入口函數(shù)對(duì)運(yùn)行庫(kù)和程序運(yùn)行環(huán)境進(jìn)行初始化酌媒,包括堆欠痴、I/O、線程秒咨、全局變量的構(gòu)造
- 入口函數(shù)在完成初始化后喇辽,調(diào)用main函數(shù),正式開始執(zhí)行程序主體部分
- main函數(shù)執(zhí)行完畢后雨席,返回到入口函數(shù)菩咨,入口函數(shù)進(jìn)行清理工作,包括全局變量析構(gòu)陡厘、堆銷毀抽米、關(guān)閉I/O等,然后系統(tǒng)調(diào)用結(jié)束進(jìn)程
Build一個(gè)程序?qū)嶋H上包含四個(gè)步驟:
- 預(yù)處理(Prepression)
- 編譯(Compilation)
- 匯編(Assembly)
- 鏈接(Linking)
下面以最簡(jiǎn)單的一段C語(yǔ)言程序?yàn)槔谥茫褂肎CC編譯過(guò)程如下:
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
預(yù)編譯:
源代碼文件和相關(guān)的頭文件被預(yù)編譯器cpp預(yù)編譯成一個(gè).i文件云茸。
相當(dāng)于如下命令:
$gcc -E hello,c -o hello.i
或者$cpp hello.c > hello.i
預(yù)編譯過(guò)程主要處理那些源代碼文件中以”#”開始的預(yù)編譯指令。如”#include”谤饭、”#define”等标捺,處理規(guī)則如下:
- 將所有的"#define"刪除,并且展開所有的宏定義
- 處理所有條件預(yù)編譯指令揉抵,比如"#if"亡容、"#ifdef"、"#elif"冤今、"#else"萍倡、"#endif"
- 處理"#include"預(yù)編譯指令,將被包含的文件插入到該預(yù)處理指令位置辟汰。這是一個(gè)遞歸過(guò)程列敲,也就是說(shuō)被包含的文件可能還包含其他文件。
- 刪除所有的注釋
- 添加行號(hào)和文件名標(biāo)識(shí)帖汞,以便編譯時(shí)編譯器產(chǎn)生調(diào)試用的行號(hào)信息及用于編譯時(shí)產(chǎn)生編譯錯(cuò)誤或警告時(shí)能夠顯示行號(hào)
- 保留所有的#pragma編譯器指令戴而,因?yàn)榫幾g器需要使用它們
編譯:
編譯過(guò)程就是把預(yù)處理完的文件進(jìn)行一系列詞法分析、語(yǔ)法分析翩蘸、語(yǔ)義分析及優(yōu)化后生產(chǎn)相應(yīng)的匯編代碼文件所意,這個(gè)過(guò)程往往是我們所說(shuō)的整個(gè)程序構(gòu)建的核心部分,也是復(fù)雜的部分之一催首。
相當(dāng)于如下命令:
$gcc -S hello.i -o hello.s
預(yù)編譯和編譯兩個(gè)步驟也可以合并成一個(gè)步驟:
$gcc -S hello.c -o hello.s
實(shí)際上gcc這個(gè)命令只是后臺(tái)程序的包裝扶踊,它會(huì)根據(jù)不同的參數(shù)要求去調(diào)用預(yù)編譯編譯程序cc1(c++是cc1plus、Objective-C是cclobj)郎任、匯編器as秧耗、鏈接器ld。
以下面這段代碼來(lái)分析一下編譯器所做的事:
array[index] = (index + 4) * (2 + 6)
- 詞法分析:
詞法分析很簡(jiǎn)單舶治,程序被輸入到掃描器分井,掃描器運(yùn)用一種類似于有限狀態(tài)機(jī)的算法將源代碼的字符序號(hào)分割成一系列的記號(hào)车猬。
- 語(yǔ)法分析:
語(yǔ)法分析器對(duì)掃描器產(chǎn)生的記號(hào)進(jìn)行語(yǔ)法分析,從而產(chǎn)生語(yǔ)法樹尺锚。上面這段代碼會(huì)生成如下的語(yǔ)法樹:
- 語(yǔ)義分析
語(yǔ)法分析僅僅是完成了對(duì)表達(dá)式的語(yǔ)法層面的分析珠闰,但它并不了解這個(gè)語(yǔ)句是否真的有意義。比如兩個(gè)指針做乘法等瘫辩,語(yǔ)義分析器所能分析的語(yǔ)義是靜態(tài)語(yǔ)義伏嗜,靜態(tài)語(yǔ)義是指在編譯期可以確定的語(yǔ)義。靜態(tài)語(yǔ)義通常包含申明和類型的匹配伐厌,類型的轉(zhuǎn)換承绸。上面的例子經(jīng)過(guò)語(yǔ)義分析后會(huì)變成以下形式:
可以看到每個(gè)表達(dá)式都被標(biāo)識(shí)了類型。
- 中間語(yǔ)言生成
中間語(yǔ)言生成主要就是源碼級(jí)優(yōu)化器對(duì)源代碼進(jìn)行優(yōu)化弧械,如上面的例子中八酒,(2 + 6)這個(gè)表達(dá)式可以被優(yōu)化成8空民,由于直接在語(yǔ)法樹上作優(yōu)化比較困難刃唐,所以源代碼優(yōu)化器往往將整個(gè)語(yǔ)法樹轉(zhuǎn)換成中間代碼。
- 目標(biāo)代碼生成與優(yōu)化
源代碼級(jí)優(yōu)化器產(chǎn)生中間代碼后的過(guò)程都屬于編譯器后端界轩。編譯器后端包括代碼生成器和目標(biāo)代碼優(yōu)化器画饥。代碼生成器將中間代碼轉(zhuǎn)換成目標(biāo)機(jī)器代碼,目標(biāo)代碼優(yōu)化器對(duì)目標(biāo)代碼進(jìn)行優(yōu)化浊猾,比如選擇合適的尋址方式抖甘、使用位移來(lái)代替乘法運(yùn)算、刪除多余的指令等葫慎。
匯編:
匯編是將匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令衔彻,每一個(gè)匯編語(yǔ)句幾乎都對(duì)應(yīng)一條機(jī)器指令。所以匯編器的匯編過(guò)程相對(duì)于編譯器來(lái)講比較簡(jiǎn)單偷办,它沒(méi)有復(fù)雜的語(yǔ)法艰额,也沒(méi)有語(yǔ)義,也不需要做指令優(yōu)化椒涯,只是根據(jù)匯編指令和機(jī)器指令的對(duì)照表一一翻譯就可以了柄沮。
$as hello.s -o hello.o或者$gcc -c hello.s -o hello.o
也可以使用gcc命令從C源代碼文件開始,經(jīng)過(guò)預(yù)編譯废岂、編譯和匯編直接輸出目標(biāo)文件(Object File):
$gcc -c hello.c -o hello.o
鏈接:
由于我們一個(gè)程序通常包含很多個(gè)模塊祖搓,這些模塊之間相互依賴又相互獨(dú)立,所以我們一般寫程序的時(shí)候?qū)Τ绦蜻M(jìn)行了分割湖苞,鏈接就相當(dāng)于把這些分割的模塊拼接在一起拯欧,最終生成一個(gè)可執(zhí)行文件。
鏈接通常是一個(gè)讓人比較費(fèi)解的過(guò)程财骨,鏈接包括靜態(tài)鏈接和動(dòng)態(tài)鏈接哈扮。
- 靜態(tài)鏈接
最基本的靜態(tài)鏈接過(guò)程如圖所示纬纪。每個(gè)模塊的源代碼文件經(jīng)過(guò)編譯器編譯成目標(biāo)文件(一般擴(kuò)展名為.o或者.obj),目標(biāo)文件和庫(kù)一起鏈接形成最終可執(zhí)行的文件滑肉。最常見的庫(kù)就是運(yùn)行時(shí)庫(kù)(Runtime Library)包各,它是支持程序運(yùn)行的基本函數(shù)的集合。鏈接的過(guò)程包括符號(hào)解析靶庙、地址重定位等问畅。
- 動(dòng)態(tài)鏈接
靜態(tài)鏈接使得不同的程序開發(fā)者和部門能夠相對(duì)獨(dú)立的開發(fā)和測(cè)試自己的程序模塊,大大促進(jìn)了程序的開發(fā)效率六荒,但是隨著程序的增大护姆,很多缺點(diǎn)也暴露了出來(lái),比如浪費(fèi)內(nèi)存和磁盤空間掏击、模塊更新困難等卵皂,由此動(dòng)態(tài)鏈接的產(chǎn)生就是為了解決這些問(wèn)題。
為什么說(shuō)靜態(tài)鏈接會(huì)造成內(nèi)存浪費(fèi)砚亭?
舉個(gè)栗子灯变,一個(gè)程序內(nèi)部除了都保留著printf()、scanf()捅膘、strlen()等這樣的公用函數(shù)添祸,還有相當(dāng)數(shù)量的其它庫(kù)函數(shù)以及它們所需要的輔助數(shù)據(jù)結(jié)構(gòu),如果我們操作系統(tǒng)運(yùn)行了很多個(gè)程序寻仗,而這些程序基本都會(huì)使用到C語(yǔ)言的靜態(tài)庫(kù)刃泌,一般這些常用的靜態(tài)庫(kù)至少占1MB以上,如果我們運(yùn)行了100個(gè)這樣的程序署尤,那每個(gè)程序都會(huì)保存一個(gè)副本在內(nèi)存中進(jìn)行靜態(tài)鏈接耙替,那就要浪費(fèi)100MB內(nèi)存空間。
模塊更新的困難曹体?
同樣舉個(gè)栗子俗扇。如果一個(gè)程序Program1所使用的Lib.o是由一個(gè)第三方廠商提供的,當(dāng)該廠商更新了Lib.o的時(shí)候混坞,那個(gè)Program1的廠商就需要拿到最新版的Lib.o狐援,然后將其與Program1鏈接后,將新的Program1整個(gè)發(fā)布給用戶究孕。這樣做的缺點(diǎn)就是一旦程序中有任何模塊更新啥酱,整個(gè)程序就要重新鏈接、發(fā)布給用戶厨诸。這樣一旦程序任何位置的一個(gè)小改動(dòng)镶殷,都會(huì)導(dǎo)致整個(gè)程序重新下載。
動(dòng)態(tài)鏈接就不需要把程序的所有模塊的目標(biāo)文件全都鏈接在一起后再生成可執(zhí)行文件了微酬,它是要等到程序運(yùn)行時(shí)才進(jìn)行鏈接绘趋。也就是說(shuō)颤陶,動(dòng)態(tài)鏈接就是把鏈接的過(guò)程推遲到了程序運(yùn)行時(shí)再進(jìn)行。
還是舉個(gè)栗子吧陷遮。假如Program1和Program2都用到了庫(kù)Lib.o滓走,當(dāng)系統(tǒng)加載Program1程序的時(shí)候,系統(tǒng)就會(huì)加載Lib.o以及Program1依賴的所有目標(biāo)文件帽馋,Program1依賴的所有文件也都加載到了內(nèi)存中后搅方,它就會(huì)進(jìn)行鏈接(和動(dòng)態(tài)鏈接類似)。然后系統(tǒng)就把控制權(quán)交給Program1.o的程序入口绽族,程序就開始運(yùn)行姨涡,=。這個(gè)時(shí)候吧慢,如果再運(yùn)行Program2涛漂,系統(tǒng)就不需要重新加載Lib.o了,而是直接鏈接检诗。
番外
xcode的編譯器經(jīng)歷了三個(gè)階段的發(fā)展:
- GCC
GCC(GNU Compiler Collection匈仗,GNU編譯器套裝),是一套由 GNU 開發(fā)的編程語(yǔ)言編譯器岁诉,它十分的龐大锚沸,可以處理C跋选、C++涕癣、Fortran、Pascal前标、Objective-C坠韩、Java, 以及 Ada與其他語(yǔ)言。
- LLVM
LLVM是使用GCC作為前端來(lái)對(duì)用戶程序進(jìn)行語(yǔ)義分析產(chǎn)生IF(Intermidiate Format)炼列,然后LLVM使用分析結(jié)果完成代碼優(yōu)化和生成只搁。
- LLVM compliler(clang)
Clang只支持C,C++和Objective-C三種C家族語(yǔ)言俭尖,而前段也由GCC完全替換成了Clang氢惋,相對(duì)來(lái)說(shuō)效率更高,內(nèi)存占用也更小稽犁。
下面這張圖將顯示GCC焰望、LLVM-GCC、LLVM Compiler這三個(gè)編譯選項(xiàng)的不同點(diǎn):
寫在末尾:
本篇博客只是籠統(tǒng)的介紹了一下程序的運(yùn)行過(guò)程已亥,實(shí)際上程序的運(yùn)行過(guò)程非常復(fù)雜熊赖,涉及到操作系統(tǒng)、編譯原理虑椎、計(jì)算機(jī)組成原理等知識(shí)震鹉,所以想要更加深入的了解這方面知識(shí)俱笛,推薦程序員的自我修養(yǎng)這本書。