網(wǎng)站首頁 編程語言 正文
計算機(jī)的世界,就從hello,world開始吧!
#include <stdio.h> int main() { printf("Hello World\n"); return 0; }
“Hello World”,對于好兄弟們來說,都很熟悉吧,大學(xué)第一課、編程語言書本的第一個dmeo,基本都是用這個作為引子,這次我們也從hello,world開始進(jìn)入計算機(jī)的世界遨游吧!
剛開始學(xué)這些東西的時候,比如用VC++, 都是鼠標(biāo)點(diǎn)點(diǎn),直接出來黑窗口,可以看到我們的執(zhí)行結(jié)果,卻不知,這一系列的背后,隱匿了很多我們不知道的細(xì)節(jié),而這些東西都讓VC++這類的集成開發(fā)環(huán)境幫我們做了(當(dāng)很多東西被封裝成簡單的API給我們使用的時候,也同時證明了我們的可替代性變得越來越高,那怎么辦?好好讀這篇文章:)),有的工作了好幾年的,也不見得知道Hello,World的執(zhí)行過程,這次我們把它搞懂。
先來看一下整個C程序從寫完代碼,到執(zhí)行所經(jīng)歷的步驟:
當(dāng)我們在linux上輸入如下指令的時候,VC++或者別的C的開發(fā)環(huán)境,就會在背后幫我們進(jìn)行上面的動作。
$gcc hello.c $./a.out
預(yù)編譯
通過預(yù)編譯器,生成".i"文件
這個過程主要是處理源代碼文件中以“#”開始的預(yù)編譯指令,比如:"#include",“define”
對于我們的hello程序來說,會處理#include指令,將被包含的stdio.h文件插入到第一行我們的#include指令的位置上,但是我們的stdio.h可能還包含的別的#include,所以這個過程是遞歸進(jìn)行的
如果我們有宏定義,比如#define,會展開所有的宏定義,比如:#define PI 3.14, 在預(yù)編譯步驟中,會將#define刪除,然后將所有PI替換成3.14
如果我們在代碼中存在注釋的時候,還會將注釋進(jìn)行刪除,可見注釋并不會對我們的代碼產(chǎn)生什么影響
如何查看預(yù)編譯后的文件呢?
$gcc -E hello.c -o hello.i
- -E:表示只進(jìn)行預(yù)編譯
- -o:指定要生成的結(jié)果文件,后面就是結(jié)果文件的名字
經(jīng)過預(yù)編譯之后的.i文件中不會包含任何宏定義,也就是#define,因?yàn)橐呀?jīng)被替換,所以當(dāng)無法判斷宏定義是否正確或者頭文件是否包含正確時,可以查看預(yù)編譯后的文件來確定問題
#define PI 3.14 int main() { double d = PI; return 0; } --------預(yù)編譯之后--------- int main() { double d = 3.14; return 0; }
編譯
編譯的過程是把預(yù)處理文件進(jìn)行:詞法分析->語法分析->語義分析->源代碼生成->目標(biāo)代碼的生成和優(yōu)化
整個過程如下:
其結(jié)果是產(chǎn)生.s的匯編文件
上面的過程相當(dāng)于執(zhí)行了:
$gcc -S hello.i -o hello.s
也可以用命令ccl來完成,路徑是/usr/lib/gcc/x86_64-linux-gnu/7/cc1,這個命令是將預(yù)編譯和編譯封裝了起來
$/usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c
其實(shí)gcc 的-S命令就是調(diào)用的cc1這個命令,所以gcc這個命令就是這些程序的包裝,這些成比如:cc1,ld,as這些其實(shí)都是程序,gcc會根據(jù)不同的參數(shù)去調(diào)用不同的程序,相當(dāng)于在外面加了一層
來看下編譯的詳細(xì)過程:
詞法分析
這個過程會產(chǎn)生token,聽著token感覺好高大上,其實(shí)也就那么回事,通俗點(diǎn)來說,給程序中的所有的符號進(jìn)行分類,而這個分類都有什么呢?比如:標(biāo)識符、左括號、右括號、加號、乘號、數(shù)字、賦值、左右方括號
arr[i] = (i + 1) * (2 + 3)
對上面語句進(jìn)行分類就是:arr、i是標(biāo)識符,1、2、3都屬于數(shù)字,有加號、還有乘號、還有左右括號、左右方括號和賦值,就是這么簡單的分類,這里面的每個符號,都表示一個token。
語法分析
語法分析的結(jié)果是生成語法樹,一聽很懵逼是吧,聽我給你慢慢道來,先來一句總結(jié)的話,語法樹怎么生成的?可以這么理解:就是以運(yùn)算符為根節(jié)點(diǎn),操作數(shù)為孩子節(jié)點(diǎn),將語句根據(jù)運(yùn)算符的優(yōu)先級從右到左,將樹從下到上構(gòu)造成的。沒聽懂嗎? 上圖
- 按照運(yùn)算符的優(yōu)先級,應(yīng)該先計算()和[]中內(nèi)容,按照我們的運(yùn)算符為根節(jié)點(diǎn)的說法,所以以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),上述生成的兩個節(jié)點(diǎn)看成一個整體作為*的根節(jié)點(diǎn),賦值左邊的[]也是以相同的邏輯形成生成一個子樹
- 最后以=作為根節(jié)點(diǎn),將上面步驟生成的兩個根節(jié)點(diǎn)看成一個整體,形成一個語法樹
總結(jié):
- 語法樹是以表達(dá)式為節(jié)點(diǎn)的樹,C中一個語句就是一個表達(dá)式,而一個復(fù)雜的語句又是很多表達(dá)式的組合,比如我們的語句中有:賦值表達(dá)式、加法表達(dá)式、乘法表達(dá)式、數(shù)組表達(dá)式。
- 在上述的圖中,葉子節(jié)點(diǎn)都以黃色標(biāo)識出來,可以看到符號和數(shù)字是最小的表達(dá)式
- 同時在語法分析的同時,運(yùn)算符的優(yōu)先級也被確定了下來,()和[]一樣高,()比*優(yōu)先級高,*比+號優(yōu)先級高
- 在語法分析過程中,如果出現(xiàn)了表達(dá)式不合法,比如括號不匹配等,編譯器會報錯誤
語義分析
那么語義分析階段主要做什么事情呢?
語法分析,只是完成了表達(dá)式語法層面的分析,并不知道這個語句的真正意義,比如說兩個指針做乘法運(yùn)算,語法分析是分析不出來的。
語義分析包括:靜態(tài)語義和動態(tài)語義,靜態(tài)語義就是在編譯期間可以確定的語義,比如將浮點(diǎn)數(shù)賦值給整型的類型轉(zhuǎn)換,動態(tài)語義就是運(yùn)行時才能確定的語義,比如0作為除數(shù)。
來個case:如果將一個浮點(diǎn)數(shù)賦值給一個指針,語義分析階段就會出錯。
語義分析的結(jié)果就是:整個 語法樹的表達(dá)式,都被標(biāo)識了類型
中間語言生成
編譯器在源代碼級別會有一個優(yōu)化的過程,比如我們上述的表達(dá)式2 + 3就可以被優(yōu)化成5:
直接在語法樹上做優(yōu)化比較困難,所以源碼優(yōu)化器將整個語法樹轉(zhuǎn)化成中間代碼,它是語法樹的順序表示,此時的中間代碼和目標(biāo)機(jī)器和運(yùn)行時環(huán)境還是無關(guān)的,中間代碼使編譯器可以分為前端和后端
目標(biāo)代碼生成和優(yōu)化
代碼生成器將中間代碼轉(zhuǎn)換成目標(biāo)機(jī)器碼:這個過程依賴于目標(biāo)機(jī)器
,不同的機(jī)器有不同的字長、寄存器等,此時生成的就是匯編代碼了
movl i, $ecx addl $4, %ecx ....
目標(biāo)代碼優(yōu)化器對目標(biāo)代碼進(jìn)行優(yōu)化,比如選擇一個合適的尋址方式等
匯編
匯編是將匯編代碼轉(zhuǎn)換成機(jī)器可以執(zhí)行的指令,每一個匯編語句幾乎都對應(yīng)一條機(jī)器指令,所以這個過程,根據(jù)匯編指令和機(jī)器指令的對照表,一一分析就可以了。
上面的過程相當(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命令對as程序的封裝
匯編的結(jié)果生成的.o文件叫做目標(biāo)文件
鏈接
到目前位置,完成了編譯的整個過程,到現(xiàn)在位置,還沒有為程序中的變量分配地址,那么什么時候分配地址呢?假設(shè)已經(jīng)分配了地址,那么我們有可能在引用了別的文件中的變量或者函數(shù),那么此時怎么為他們分配地址呢?所以肯定不是在之前分配地址的。
這個過程在鏈接階段才能確定,定義在其他文件的全局變量和函數(shù)在最終運(yùn)行時的絕對地址都要在最終鏈接時才能確定,所以編譯器將一個源碼文件編譯成一個未鏈接的目標(biāo)文件,然后由鏈接器最終將這些目標(biāo)文件鏈接起來形成可執(zhí)行文件。
鏈接的主要內(nèi)容就是把各個模塊之間相互引用的部分處理好,使各個模塊之間能夠正確鏈接,這里所有的模塊之間的相互引用是指全局變量的相互引用和函數(shù)的相互調(diào)用,其實(shí)鏈接的工作就是把一些指令對其他符號的地址的引用加以修正
鏈接過程主要包括:
- 地址和空間分配
- 符號決議(靜態(tài)鏈接)
- 重定位
什么是靜態(tài)鏈接呢?
源代碼文件經(jīng)過編譯器后生成目標(biāo)文件,目標(biāo)文件和庫一起鏈接成可執(zhí)行文件,這里的庫是運(yùn)行時庫,庫是一組目標(biāo)文件的包,就是一些常用的代碼編譯成目標(biāo)文件后打包存放
比如有兩個文件A.c 和B.c A中使用了B的函數(shù)foo()和變量var, 由于每個模塊都是單獨(dú)編譯的,所以在編譯階段并不知道函數(shù)foo和變量var的地址,所以就將他們地址暫時設(shè)置成0,等待鏈接器將目標(biāo)文件A和B鏈接起來的時候再修改正,這個修正的過程叫做重定位,整個過程就是靜態(tài)鏈接的基本過程。
原文鏈接:https://blog.csdn.net/qq_42447402/article/details/122161407
相關(guān)推薦
- 2022-10-30 ???????Android?H5通用容器架構(gòu)設(shè)計詳解_Android
- 2022-09-05 springboot是怎么實(shí)現(xiàn)自動配置的?
- 2023-07-22 垃圾回收的核心知識點(diǎn)解析
- 2022-12-01 Apache?Doris?Colocate?Join?原理實(shí)踐教程_Linux
- 2022-11-17 Python?隊列Queue和PriorityQueue解析_python
- 2022-06-29 python人工智能tensorflow常見損失函數(shù)LOSS匯總_python
- 2021-12-13 VS在調(diào)試時,查看是DEBUG/RELEASE
- 2022-04-11 error: failed to push some refs to解決方法
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支