【C語言】 —— 編譯和鏈接
- 一、編譯環境和運行環境
- 二、翻譯環境
- 2.1、 預處理
- 2.2、 編譯
- (1)詞法分析
- (2)語法分析
- (3)語義分析
- 2.3、 匯編
- 2.4、鏈接
- 三、運行環境
一、編譯環境和運行環境
??平時我們說寫 C語言 代碼,寫程序,不難發現:其實寫出來的都是 t e s t test test. c c c、 t e s t test test. h h h 等源文件和頭文件。我們直接打開他們,是可以直接看懂的,這也就說明了他們其實是文本文件
。但是計算機是看不懂他們的,計算機只能識別二進制指令
,無法對文件中的代碼直接執行。這時就需要將 C語言 代碼進行處理變成二進制的指令。
??
??而將代碼處理成二進制指令正是編譯器需要做的事情。
??
在 ANCI C
的任何一種實現中,存在兩個不同的環境:
- 翻譯環境:在這個環境中源代碼被轉換成可執行的機器指令(二進制指令)
- 執行環境:它用于實際執行代碼
??
二、翻譯環境
??那翻譯環境是怎么將源代碼轉換為可執行的機器指令的呢?這里我們就得展開講解一下翻譯環境所做的事情
??
??其實翻譯環境是由編譯和鏈接兩大過程組成,而編譯又可以分解成:預處理
(有些書也叫預編譯
)、編譯
、匯編
三個過程。
??
一個C語言的項目中可能由多個 . c c c 文件一起構建,那多個 . c c c 文件如何生成可執行程序呢?
- 多個 . c c c 文件
單獨
經過編譯器,編譯處理生成對應的目標文件
。 - 在 W i n d o w s Windows Windows 環境下的目標文件的后綴是
.obj
,在 L i n u s Linus Linus環境下目標文件的后綴是.o
多個目標文件
和鏈接庫
一起經過連接器處理生成最終的可執行程序
。- 鏈接庫是指
運行時庫
(它是支持程序運行的基本函數集合)、C語言庫函數
或者第三方庫
。
??這里需提一下,我們所用的 V S 2022 VS2022 VS2022 是一種集成開發環境,包括:編輯器
、編譯器
、鏈接器
、調試器
。而 c l cl cl. e x e exe exe 是其編譯器, l i n k link link. e x e exe exe 是其鏈接器
??
??我們可以在 L i n u s Linus Linus 服務器、 g c c gcc gcc 的環境下對鏈接和編譯各個階段進行觀察
??
??
2.1、 預處理
??在預處理階段,源文件和頭文件會被處理成 .i
為后綴的文件。
??在 g c c gcc gcc 中,將 .c
文件處理成 .h
文件,命令如下:
gcc -E test.c -o test.i
??
??預處理階段主要處理那些源文件中 # 開始的編譯指令。比如:# i n c l u d e include include,# d e f i n e define define,處理的規則如下:
- 將所有的 # d e f i n e define define 刪除,展開所有的宏定義
- 處理所有的條件編譯指令,如:
#if
、#ifdef
、#elif
、#else
、#endif
- 處理 # i n c l u d e include include 預編譯指令,將包含的頭文件的內容插入到該預編譯指令的位置。這個過程是遞歸進行的,也就是說被包含的頭文件也可能包含其他文件。
- 刪除所有注釋
- 添加行號和文件名標識,方便后續編譯器生成調試信息等
- 或保留所有的 # p r a g m a pragma pragma 的編譯器指令,編譯器后續會使用
??經過預處理后的 . i .i .i 文件不再包含宏定義
,因為宏已經被展開。并且包含的頭文件
都被插入
到 .i 文件
中。所以我們無法知道宏定義或者頭文件是否包含正確的時候,可以查看預處理后的 . i .i .i 文件來確認。
??
t e s t test test. c c c 文件:
??
t e s t test test. i i i 文件:
?? . i .i .i 文件只有輸入指令生成后我們才能看到,正常情況下生成中間文件編譯器用完就刪掉了。
??
??
2.2、 編譯
??編譯過程就是將預處理后的文件進行一系列的:詞法分析、語法分析、語義分析及優化,生成相應的匯編代碼文件
。
編譯過程的命令如下:
gcc -S test.i -o test.s
??編譯過程最終會生成 .s
的文件,它里面放的是匯編代碼。其實編譯階段整體上就是將 C 代碼轉換成匯編代碼
??
t e s t . s test.s test.s 文件:
??那編譯過程具體是做了什么工作呢?
??他們分別是:詞法分析、語法分析、語義分析及優化
??下面讓我們一起簡單了解
??
??假設有下面的代碼,在進行編譯時會時編譯器怎么做呢
array[index] = (index + 4) * (2 + 6);
??
(1)詞法分析
??將源代碼程序輸入掃描器
,掃描器的任務就是簡單的進行詞法分析
,把代碼中的字符分割成一系列的記號(關鍵字、標識符、字面量、特殊字符等)。
??
上述代碼進行詞法分析后得到了 16 個記號:
記號 | 類型 | 記號 | 類型 |
---|---|---|---|
array | 標識符 | 4 | 數字 |
[ | 左方括號 | ) | 右圓括號 |
index | 標識符 | * | 乘號 |
] | 右方括號 | ( | 左圓括號 |
= | 賦值 | 2 | 數字 |
( | 左圓括號 | + | 加號 |
index | 標識符 | 6 | 數字 |
+ | 加號 | ) | 右圓括號 |
??
(2)語法分析
??接下來則是語法分析
。語法分析器
會對掃描產生的記號進行語法分析,從而產生語法樹
。這些語法樹是以表達式為節點的樹
??
(3)語義分析
??由語義分析器
來完成語義分析,即對表達式的語法層面分析。編譯器所能做的分析是語義的靜態分析。靜態語義分析通常包括聲明和類型的匹配
。這個階段會報告錯誤
的語法信息
??
2.3、 匯編
??匯編是指通過匯編器將匯編代碼轉變成機器可執行的指令,每一個匯編語句幾乎都對應一條機器指令。就是按照匯編指令和機器指令的對照表一一的進行翻譯,也不做指令優化
??匯編的命令如下:
gcc -c test.s -o test.o
??經過匯編處理后文件即為目標文件( . o b j .obj .obj / . o .o .o)目標文件為二進制文件,無法通過文本編輯器打開
??
2.4、鏈接
??鏈接是一個復雜的過程,鏈接的時候需要把一堆文件鏈接在一起才生成可執行程序。
??鏈接的過程主要包括:地址和空間分配
,符號決議
和重定位
等這些步驟。
??
??鏈接主要解決的是一個項目中多個文件、多模塊之間互相調用的問題。
??比如:現在有兩個文件( t e s t . c test.c test.c 和 a d d . c add.c add.c)
??
t e s t . c test.c test.c 文件
#incldue<stdio.h>//聲明外部函數
extern int Add(int x, int y);
//聲明外部的全局變量
extern int g_val;int main()
{int a = 10;int b = 20;int c = Add(10, 20);printf("%d\n", c);return 0;
}
??
A d d . c Add.c Add.c 文件
int g_val = 2024;int Add(int x, int y)
{return x + y;
}
??
??為什么在 Add.c
中定義的文件在 test.c
文件中聲明一下就可以使用呢?
??這里進行簡單的了解
??
??經過前面的學習,我們知道每一個源文件( . c .c .c)經過編譯過程
后都會生成自己的目標文件
( . o .o .o / . o b j .obj .obj )
??
??在編譯的過程中,會對代碼中的符號進行符號的匯總,并形成相應的符號表,符號表中會存儲符號相對應的地址。在產生 t e s t . c test.c test.c 文件的符號表時,遇到只有聲明而未定義的符號 Add
和 g_val
時,會暫時將其地址擱置。
??
??到了編譯過程,編譯器會將多個目標文件鏈接在一起(目標文件的格式是一樣的,并且是分段的形式),從而生成可執行程序(可執行策劃給你續最終也是如目標文件一樣的分段形式)
??
??在合并過程中,符號表也需要合并成一份。合并后的符號表每個符號只能有一份,那 Add
和 g_val
符號自然用其有效地址的那一份,這樣符號表就完成了合并。通過 A d d Add Add 的地址,就自然而然能找到 A d d Add Add 函數了。
??
??而上述對地址的修正過程被叫做:重定位
??
??前面我們非常簡潔的講解了一個 C 的程序是如何編譯和鏈接,到最終生成可執行程序的過程,其實很多內部的細節無法展開講解。
??比如:目標文件的格式 e l f elf elf,鏈接底層實現中的空間與地址分配,符號解析和重定位等,如果你有興趣,可以看 《程序員的自我修養》 一書來詳細了解。
??
??
三、運行環境
??運行環境實際上是非常復雜的,我們這里簡單了解了解
- 程序必須載入內存中。在有操作系統的環境中:一般這個由
操作系統
完成。在獨立的環境中,程序的載入必須由手動安排
(單片機燒板子),也可以通過可執行代碼置入只讀內存
來完成。 - 程序的執行便開始。接著便調用 m a i n main main 函數
- 開始執行程序代碼。這個時候程序將使用一個運行時堆棧(函數棧幀),存儲函數的
局部變量
和返回地址
。程序同時也可以使用靜態(static)內存
,存儲于靜態內存中的變量在程序的整個執行過程一直保留他們的值。 - 終止程序。正常終止 m a i n main main 函數;也有可能是意外終止。
??
??
??
??
??
??好啦,本期關于編譯和鏈接的知識就介紹到這里啦,希望本期博客能對你有所幫助。同時,如果有錯誤的地方請多多指正,讓我們在C語言的學習路上一起進步!