日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

c語言執行Hello?World背后經歷的步驟_C 語言

作者:不吃辣。 ? 更新時間: 2022-03-19 編程語言

計算機的世界,就從hello,world開始吧!

#include <stdio.h>
int main()
{
	printf("Hello World\n");
  return 0;
}

“Hello World”,對于好兄弟們來說,都很熟悉吧,大學第一課、編程語言書本的第一個dmeo,基本都是用這個作為引子,這次我們也從hello,world開始進入計算機的世界遨游吧!

剛開始學這些東西的時候,比如用VC++, 都是鼠標點點,直接出來黑窗口,可以看到我們的執行結果,卻不知,這一系列的背后,隱匿了很多我們不知道的細節,而這些東西都讓VC++這類的集成開發環境幫我們做了(當很多東西被封裝成簡單的API給我們使用的時候,也同時證明了我們的可替代性變得越來越高,那怎么辦?好好讀這篇文章:)),有的工作了好幾年的,也不見得知道Hello,World的執行過程,這次我們把它搞懂。

先來看一下整個C程序從寫完代碼,到執行所經歷的步驟:

當我們在linux上輸入如下指令的時候,VC++或者別的C的開發環境,就會在背后幫我們進行上面的動作。

$gcc  hello.c
$./a.out

預編譯

通過預編譯器,生成".i"文件

這個過程主要是處理源代碼文件中以“#”開始的預編譯指令,比如:"#include",“define”

對于我們的hello程序來說,會處理#include指令,將被包含的stdio.h文件插入到第一行我們的#include指令的位置上,但是我們的stdio.h可能還包含的別的#include,所以這個過程是遞歸進行的

如果我們有宏定義,比如#define,會展開所有的宏定義,比如:#define PI 3.14, 在預編譯步驟中,會將#define刪除,然后將所有PI替換成3.14

如果我們在代碼中存在注釋的時候,還會將注釋進行刪除,可見注釋并不會對我們的代碼產生什么影響

如何查看預編譯后的文件呢?

$gcc -E hello.c -o hello.i
  • -E:表示只進行預編譯
  • -o:指定要生成的結果文件,后面就是結果文件的名字

經過預編譯之后的.i文件中不會包含任何宏定義,也就是#define,因為已經被替換,所以當無法判斷宏定義是否正確或者頭文件是否包含正確時,可以查看預編譯后的文件來確定問題

#define PI 3.14
int main()
{
	double d = PI;
  return 0;
}
--------預編譯之后---------
int main()
{
  double d = 3.14;
  return 0;
}

編譯

編譯的過程是把預處理文件進行:詞法分析->語法分析->語義分析->源代碼生成->目標代碼的生成和優化

整個過程如下:

其結果是產生.s的匯編文件

上面的過程相當于執行了:

$gcc -S hello.i -o hello.s

也可以用命令ccl來完成,路徑是/usr/lib/gcc/x86_64-linux-gnu/7/cc1,這個命令是將預編譯和編譯封裝了起來

$/usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c

其實gcc 的-S命令就是調用的cc1這個命令,所以gcc這個命令就是這些程序的包裝,這些成比如:cc1,ld,as這些其實都是程序,gcc會根據不同的參數去調用不同的程序,相當于在外面加了一層

來看下編譯的詳細過程:

詞法分析

這個過程會產生token,聽著token感覺好高大上,其實也就那么回事,通俗點來說,給程序中的所有的符號進行分類,而這個分類都有什么呢?比如:標識符、左括號、右括號、加號、乘號、數字、賦值、左右方括號

arr[i] = (i + 1) * (2 + 3)

對上面語句進行分類就是:arr、i是標識符,1、2、3都屬于數字,有加號、還有乘號、還有左右括號、左右方括號和賦值,就是這么簡單的分類,這里面的每個符號,都表示一個token。

語法分析

語法分析的結果是生成語法樹,一聽很懵逼是吧,聽我給你慢慢道來,先來一句總結的話,語法樹怎么生成的?可以這么理解:就是以運算符為根節點,操作數為孩子節點,將語句根據運算符的優先級從右到左,將樹從下到上構造成的。沒聽懂嗎? 上圖

  • 按照運算符的優先級,應該先計算()和[]中內容,按照我們的運算符為根節點的說法,所以以i和1為孩子節點,以+為根節點,以2和3為孩子節點,以+為根節點,[]為根節點,arr和i為孩子節點
  • 然后以*為根節點,上述生成的兩個節點看成一個整體作為*的根節點,賦值左邊的[]也是以相同的邏輯形成生成一個子樹
  • 最后以=作為根節點,將上面步驟生成的兩個根節點看成一個整體,形成一個語法樹

總結:

  • 語法樹是以表達式為節點的樹,C中一個語句就是一個表達式,而一個復雜的語句又是很多表達式的組合,比如我們的語句中有:賦值表達式、加法表達式、乘法表達式、數組表達式。
  • 在上述的圖中,葉子節點都以黃色標識出來,可以看到符號和數字是最小的表達式
  • 同時在語法分析的同時,運算符的優先級也被確定了下來,()和[]一樣高,()比*優先級高,*比+號優先級高
  • 在語法分析過程中,如果出現了表達式不合法,比如括號不匹配等,編譯器會報錯誤

語義分析

那么語義分析階段主要做什么事情呢?

語法分析,只是完成了表達式語法層面的分析,并不知道這個語句的真正意義,比如說兩個指針做乘法運算,語法分析是分析不出來的。

語義分析包括:靜態語義和動態語義,靜態語義就是在編譯期間可以確定的語義,比如將浮點數賦值給整型的類型轉換,動態語義就是運行時才能確定的語義,比如0作為除數。

來個case:如果將一個浮點數賦值給一個指針,語義分析階段就會出錯。

語義分析的結果就是:整個 語法樹的表達式,都被標識了類型

中間語言生成

編譯器在源代碼級別會有一個優化的過程,比如我們上述的表達式2 + 3就可以被優化成5:

直接在語法樹上做優化比較困難,所以源碼優化器將整個語法樹轉化成中間代碼,它是語法樹的順序表示,此時的中間代碼和目標機器和運行時環境還是無關的,中間代碼使編譯器可以分為前端和后端

目標代碼生成和優化

代碼生成器將中間代碼轉換成目標機器碼:這個過程依賴于目標機器,不同的機器有不同的字長、寄存器等,此時生成的就是匯編代碼了

movl i, $ecx
addl $4, %ecx
....

目標代碼優化器對目標代碼進行優化,比如選擇一個合適的尋址方式等

匯編

匯編是將匯編代碼轉換成機器可以執行的指令,每一個匯編語句幾乎都對應一條機器指令,所以這個過程,根據匯編指令和機器指令的對照表,一一分析就可以了。

上面的過程相當于執行了

$gcc -c hello.c -o hello.o

或者

$gcc -c hello.s -o hello.o

或者

$as hello.s -o hello.o

又一次驗證了上面的結論,gcc命令對as程序的封裝

匯編的結果生成的.o文件叫做目標文件

鏈接

到目前位置,完成了編譯的整個過程,到現在位置,還沒有為程序中的變量分配地址,那么什么時候分配地址呢?假設已經分配了地址,那么我們有可能在引用了別的文件中的變量或者函數,那么此時怎么為他們分配地址呢?所以肯定不是在之前分配地址的。

這個過程在鏈接階段才能確定,定義在其他文件的全局變量和函數在最終運行時的絕對地址都要在最終鏈接時才能確定,所以編譯器將一個源碼文件編譯成一個未鏈接的目標文件,然后由鏈接器最終將這些目標文件鏈接起來形成可執行文件。

鏈接的主要內容就是把各個模塊之間相互引用的部分處理好,使各個模塊之間能夠正確鏈接,這里所有的模塊之間的相互引用是指全局變量的相互引用和函數的相互調用,其實鏈接的工作就是把一些指令對其他符號的地址的引用加以修正

鏈接過程主要包括:

  • 地址和空間分配
  • 符號決議(靜態鏈接)
  • 重定位

什么是靜態鏈接呢?

源代碼文件經過編譯器后生成目標文件,目標文件和庫一起鏈接成可執行文件,這里的庫是運行時庫,庫是一組目標文件的包,就是一些常用的代碼編譯成目標文件后打包存放

比如有兩個文件A.c 和B.c A中使用了B的函數foo()和變量var, 由于每個模塊都是單獨編譯的,所以在編譯階段并不知道函數foo和變量var的地址,所以就將他們地址暫時設置成0,等待鏈接器將目標文件A和B鏈接起來的時候再修改正,這個修正的過程叫做重定位,整個過程就是靜態鏈接的基本過程。

原文鏈接:https://blog.csdn.net/qq_42447402/article/details/122161407

欄目分類
最近更新