c語(yǔ)言執(zhí)行Hello?World背后經(jīng)歷的步驟
計(jì)算機(jī)的世界,就從hello,world開(kāi)始吧!
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
“Hello World”,對(duì)于好兄弟們來(lái)說(shuō),都很熟悉吧,大學(xué)第一課、編程語(yǔ)言書(shū)本的第一個(gè)dmeo,基本都是用這個(gè)作為引子,這次我們也從hello,world開(kāi)始進(jìn)入計(jì)算機(jī)的世界遨游吧!
剛開(kāi)始學(xué)這些東西的時(shí)候,比如用VC++, 都是鼠標(biāo)點(diǎn)點(diǎn),直接出來(lái)黑窗口,可以看到我們的執(zhí)行結(jié)果,卻不知,這一系列的背后,隱匿了很多我們不知道的細(xì)節(jié),而這些東西都讓VC++這類的集成開(kāi)發(fā)環(huán)境幫我們做了(當(dāng)很多東西被封裝成簡(jiǎn)單的API給我們使用的時(shí)候,也同時(shí)證明了我們的可替代性變得越來(lái)越高,那怎么辦?好好讀這篇文章:)),有的工作了好幾年的,也不見(jiàn)得知道Hello,World的執(zhí)行過(guò)程,這次我們把它搞懂。
先來(lái)看一下整個(gè)C程序從寫(xiě)完代碼,到執(zhí)行所經(jīng)歷的步驟:

當(dāng)我們?cè)趌inux上輸入如下指令的時(shí)候,VC++或者別的C的開(kāi)發(fā)環(huán)境,就會(huì)在背后幫我們進(jìn)行上面的動(dòng)作。
$gcc hello.c $./a.out
預(yù)編譯
通過(guò)預(yù)編譯器,生成".i"文件
這個(gè)過(guò)程主要是處理源代碼文件中以“#”開(kāi)始的預(yù)編譯指令,比如:"#include",“define”
對(duì)于我們的hello程序來(lái)說(shuō),會(huì)處理#include指令,將被包含的stdio.h文件插入到第一行我們的#include指令的位置上,但是我們的stdio.h可能還包含的別的#include,所以這個(gè)過(guò)程是遞歸進(jìn)行的
如果我們有宏定義,比如#define,會(huì)展開(kāi)所有的宏定義,比如:#define PI 3.14, 在預(yù)編譯步驟中,會(huì)將#define刪除,然后將所有PI替換成3.14
如果我們?cè)诖a中存在注釋的時(shí)候,還會(huì)將注釋進(jìn)行刪除,可見(jiàn)注釋并不會(huì)對(duì)我們的代碼產(chǎn)生什么影響
如何查看預(yù)編譯后的文件呢?
$gcc -E hello.c -o hello.i
- -E:表示只進(jìn)行預(yù)編譯
- -o:指定要生成的結(jié)果文件,后面就是結(jié)果文件的名字
經(jīng)過(guò)預(yù)編譯之后的.i文件中不會(huì)包含任何宏定義,也就是#define,因?yàn)橐呀?jīng)被替換,所以當(dāng)無(wú)法判斷宏定義是否正確或者頭文件是否包含正確時(shí),可以查看預(yù)編譯后的文件來(lái)確定問(wèn)題
#define PI 3.14
int main()
{
double d = PI;
return 0;
}
--------預(yù)編譯之后---------
int main()
{
double d = 3.14;
return 0;
}
編譯
編譯的過(guò)程是把預(yù)處理文件進(jìn)行:詞法分析->語(yǔ)法分析->語(yǔ)義分析->源代碼生成->目標(biāo)代碼的生成和優(yōu)化
整個(gè)過(guò)程如下:

其結(jié)果是產(chǎn)生.s的匯編文件
上面的過(guò)程相當(dāng)于執(zhí)行了:
$gcc -S hello.i -o hello.s
也可以用命令ccl來(lái)完成,路徑是/usr/lib/gcc/x86_64-linux-gnu/7/cc1,這個(gè)命令是將預(yù)編譯和編譯封裝了起來(lái)
$/usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c
其實(shí)gcc 的-S命令就是調(diào)用的cc1這個(gè)命令,所以gcc這個(gè)命令就是這些程序的包裝,這些成比如:cc1,ld,as這些其實(shí)都是程序,gcc會(huì)根據(jù)不同的參數(shù)去調(diào)用不同的程序,相當(dāng)于在外面加了一層
來(lái)看下編譯的詳細(xì)過(guò)程:
詞法分析
這個(gè)過(guò)程會(huì)產(chǎn)生token,聽(tīng)著token感覺(jué)好高大上,其實(shí)也就那么回事,通俗點(diǎn)來(lái)說(shuō),給程序中的所有的符號(hào)進(jìn)行分類,而這個(gè)分類都有什么呢?比如:標(biāo)識(shí)符、左括號(hào)、右括號(hào)、加號(hào)、乘號(hào)、數(shù)字、賦值、左右方括號(hào)
arr[i] = (i + 1) * (2 + 3)
對(duì)上面語(yǔ)句進(jìn)行分類就是:arr、i是標(biāo)識(shí)符,1、2、3都屬于數(shù)字,有加號(hào)、還有乘號(hào)、還有左右括號(hào)、左右方括號(hào)和賦值,就是這么簡(jiǎn)單的分類,這里面的每個(gè)符號(hào),都表示一個(gè)token。
語(yǔ)法分析
語(yǔ)法分析的結(jié)果是生成語(yǔ)法樹(shù),一聽(tīng)很懵逼是吧,聽(tīng)我給你慢慢道來(lái),先來(lái)一句總結(jié)的話,語(yǔ)法樹(shù)怎么生成的?可以這么理解:就是以運(yùn)算符為根節(jié)點(diǎn),操作數(shù)為孩子節(jié)點(diǎn),將語(yǔ)句根據(jù)運(yùn)算符的優(yōu)先級(jí)從右到左,將樹(shù)從下到上構(gòu)造成的。沒(méi)聽(tīng)懂嗎? 上圖

- 按照運(yùn)算符的優(yōu)先級(jí),應(yīng)該先計(jì)算()和[]中內(nèi)容,按照我們的運(yùn)算符為根節(jié)點(diǎn)的說(shuō)法,所以以i和1為孩子節(jié)點(diǎn),以+為根節(jié)點(diǎn),以2和3為孩子節(jié)點(diǎn),以+為根節(jié)點(diǎn),[]為根節(jié)點(diǎn),arr和i為孩子節(jié)點(diǎn)
- 然后以*為根節(jié)點(diǎn),上述生成的兩個(gè)節(jié)點(diǎn)看成一個(gè)整體作為*的根節(jié)點(diǎn),賦值左邊的[]也是以相同的邏輯形成生成一個(gè)子樹(shù)
- 最后以=作為根節(jié)點(diǎn),將上面步驟生成的兩個(gè)根節(jié)點(diǎn)看成一個(gè)整體,形成一個(gè)語(yǔ)法樹(shù)
總結(jié):
- 語(yǔ)法樹(shù)是以表達(dá)式為節(jié)點(diǎn)的樹(shù),C中一個(gè)語(yǔ)句就是一個(gè)表達(dá)式,而一個(gè)復(fù)雜的語(yǔ)句又是很多表達(dá)式的組合,比如我們的語(yǔ)句中有:賦值表達(dá)式、加法表達(dá)式、乘法表達(dá)式、數(shù)組表達(dá)式。
- 在上述的圖中,葉子節(jié)點(diǎn)都以黃色標(biāo)識(shí)出來(lái),可以看到符號(hào)和數(shù)字是最小的表達(dá)式
- 同時(shí)在語(yǔ)法分析的同時(shí),運(yùn)算符的優(yōu)先級(jí)也被確定了下來(lái),()和[]一樣高,()比*優(yōu)先級(jí)高,*比+號(hào)優(yōu)先級(jí)高
- 在語(yǔ)法分析過(guò)程中,如果出現(xiàn)了表達(dá)式不合法,比如括號(hào)不匹配等,編譯器會(huì)報(bào)錯(cuò)誤
語(yǔ)義分析
那么語(yǔ)義分析階段主要做什么事情呢?
語(yǔ)法分析,只是完成了表達(dá)式語(yǔ)法層面的分析,并不知道這個(gè)語(yǔ)句的真正意義,比如說(shuō)兩個(gè)指針做乘法運(yùn)算,語(yǔ)法分析是分析不出來(lái)的。
語(yǔ)義分析包括:靜態(tài)語(yǔ)義和動(dòng)態(tài)語(yǔ)義,靜態(tài)語(yǔ)義就是在編譯期間可以確定的語(yǔ)義,比如將浮點(diǎn)數(shù)賦值給整型的類型轉(zhuǎn)換,動(dòng)態(tài)語(yǔ)義就是運(yùn)行時(shí)才能確定的語(yǔ)義,比如0作為除數(shù)。
來(lái)個(gè)case:如果將一個(gè)浮點(diǎn)數(shù)賦值給一個(gè)指針,語(yǔ)義分析階段就會(huì)出錯(cuò)。
語(yǔ)義分析的結(jié)果就是:整個(gè) 語(yǔ)法樹(shù)的表達(dá)式,都被標(biāo)識(shí)了類型

中間語(yǔ)言生成
編譯器在源代碼級(jí)別會(huì)有一個(gè)優(yōu)化的過(guò)程,比如我們上述的表達(dá)式2 + 3就可以被優(yōu)化成5:

直接在語(yǔ)法樹(shù)上做優(yōu)化比較困難,所以源碼優(yōu)化器將整個(gè)語(yǔ)法樹(shù)轉(zhuǎn)化成中間代碼,它是語(yǔ)法樹(shù)的順序表示,此時(shí)的中間代碼和目標(biāo)機(jī)器和運(yùn)行時(shí)環(huán)境還是無(wú)關(guān)的,中間代碼使編譯器可以分為前端和后端
目標(biāo)代碼生成和優(yōu)化
代碼生成器將中間代碼轉(zhuǎn)換成目標(biāo)機(jī)器碼:這個(gè)過(guò)程依賴于目標(biāo)機(jī)器,不同的機(jī)器有不同的字長(zhǎng)、寄存器等,此時(shí)生成的就是匯編代碼了
movl i, $ecx addl $4, %ecx ....
目標(biāo)代碼優(yōu)化器對(duì)目標(biāo)代碼進(jìn)行優(yōu)化,比如選擇一個(gè)合適的尋址方式等
匯編
匯編是將匯編代碼轉(zhuǎn)換成機(jī)器可以執(zhí)行的指令,每一個(gè)匯編語(yǔ)句幾乎都對(duì)應(yīng)一條機(jī)器指令,所以這個(gè)過(guò)程,根據(jù)匯編指令和機(jī)器指令的對(duì)照表,一一分析就可以了。
上面的過(guò)程相當(dāng)于執(zhí)行了
$gcc -c hello.c -o hello.o
或者
$gcc -c hello.s -o hello.o
或者
$as hello.s -o hello.o
又一次驗(yàn)證了上面的結(jié)論,gcc命令對(duì)as程序的封裝
匯編的結(jié)果生成的.o文件叫做目標(biāo)文件
鏈接
到目前位置,完成了編譯的整個(gè)過(guò)程,到現(xiàn)在位置,還沒(méi)有為程序中的變量分配地址,那么什么時(shí)候分配地址呢?假設(shè)已經(jīng)分配了地址,那么我們有可能在引用了別的文件中的變量或者函數(shù),那么此時(shí)怎么為他們分配地址呢?所以肯定不是在之前分配地址的。
這個(gè)過(guò)程在鏈接階段才能確定,定義在其他文件的全局變量和函數(shù)在最終運(yùn)行時(shí)的絕對(duì)地址都要在最終鏈接時(shí)才能確定,所以編譯器將一個(gè)源碼文件編譯成一個(gè)未鏈接的目標(biāo)文件,然后由鏈接器最終將這些目標(biāo)文件鏈接起來(lái)形成可執(zhí)行文件。
鏈接的主要內(nèi)容就是把各個(gè)模塊之間相互引用的部分處理好,使各個(gè)模塊之間能夠正確鏈接,這里所有的模塊之間的相互引用是指全局變量的相互引用和函數(shù)的相互調(diào)用,其實(shí)鏈接的工作就是把一些指令對(duì)其他符號(hào)的地址的引用加以修正
鏈接過(guò)程主要包括:
- 地址和空間分配
- 符號(hào)決議(靜態(tài)鏈接)
- 重定位
什么是靜態(tài)鏈接呢?
源代碼文件經(jīng)過(guò)編譯器后生成目標(biāo)文件,目標(biāo)文件和庫(kù)一起鏈接成可執(zhí)行文件,這里的庫(kù)是運(yùn)行時(shí)庫(kù),庫(kù)是一組目標(biāo)文件的包,就是一些常用的代碼編譯成目標(biāo)文件后打包存放
比如有兩個(gè)文件A.c 和B.c A中使用了B的函數(shù)foo()和變量var, 由于每個(gè)模塊都是單獨(dú)編譯的,所以在編譯階段并不知道函數(shù)foo和變量var的地址,所以就將他們地址暫時(shí)設(shè)置成0,等待鏈接器將目標(biāo)文件A和B鏈接起來(lái)的時(shí)候再修改正,這個(gè)修正的過(guò)程叫做重定位,整個(gè)過(guò)程就是靜態(tài)鏈接的基本過(guò)程。
以上所述是小編給大家介紹的c語(yǔ)言執(zhí)行Hello World背后經(jīng)歷的步驟,希望對(duì)大家有所幫助。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
C++常用函數(shù)總結(jié)(algorithm 頭文件)
本文給大家詳細(xì)介紹了algorithm 頭文件中最常用的函數(shù)及其使用方法,當(dāng)然這只是其中的一部分,algorithm 頭文件中還有很多其他的函數(shù),感興趣的朋友一起看看吧2023-12-12
C++進(jìn)程的創(chuàng)建和進(jìn)程ID標(biāo)識(shí)詳細(xì)介紹
傳統(tǒng)的C++(C++98)中并沒(méi)有引入線程這個(gè)概念。linux和unix操作系統(tǒng)的設(shè)計(jì)采用的是多進(jìn)程,進(jìn)程間的通信十分方便,同時(shí)進(jìn)程之間互相有著獨(dú)立的空間,不會(huì)污染其他進(jìn)程的數(shù)據(jù),天然的隔離性給程序的穩(wěn)定性帶來(lái)了很大的保障2022-08-08
C++通過(guò)boost.date_time進(jìn)行時(shí)間運(yùn)算
這篇文章介紹了C++通過(guò)boost.date_time進(jìn)行時(shí)間運(yùn)算的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
C++使用boost::lexical_cast進(jìn)行數(shù)值轉(zhuǎn)換
這篇文章介紹了C++使用boost::lexical_cast進(jìn)行數(shù)值轉(zhuǎn)換的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
C++11/14 線程中使用Lambda函數(shù)的方法
這篇文章主要介紹了C++11/14 線程中使用Lambda函數(shù)的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
15種?C++?常見(jiàn)報(bào)錯(cuò)原因分析
這篇文章主要介紹了15種?C++?常見(jiàn)報(bào)錯(cuò),本文通過(guò)實(shí)例代碼給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01
C語(yǔ)言實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)(鏈表)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)學(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
C語(yǔ)言中對(duì)文件最基本的讀取和寫(xiě)入函數(shù)
這篇文章主要介紹了C語(yǔ)言中對(duì)文件最基本的讀取和寫(xiě)入函數(shù),是C語(yǔ)言入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-08-08

