前言
從源碼到可執行程序,經歷四個過程:預處理、編譯、匯編和鏈接,前三步由使用編譯器來完成、鏈接由鏈接來完成。
- 編譯器將編譯工作主要分為預處理,編譯和匯編三部
- 連接器的工作是把各個獨立的模塊鏈接為可執行程序
- 靜態鏈接在編譯期完成,動態鏈接在運行期完成
階段
階段名 | 生成文件類型 |
---|---|
預處理(-E) | 生成 .i文件 |
編譯(-S) | 生成 .s 文件 |
匯編(-c) | 生層 .o 文件 |
連接(-l) | 生成 可執行 文件 |
gcc 選項
選項 | 含義 |
---|---|
-v | 查看編譯器的版本,像是 gcc 執行時的詳細過程 |
-o <file> |
輸出名為 file 的文件,這個不能與源文件名稱相同 |
-E | 只預處理,不會編譯、匯編、鏈接 |
-S | 只編譯,不會匯編、鏈接 |
-c | 只編譯和匯編,不會連接 |
在編譯過程中,除非使用了-E、-S、-C選項(或者編譯出錯阻止了完整的編譯過程),否則最后的步驟都是鏈接
文件類型
編譯器支持許多和C語言程序相關的擴展名,對它們的說明如下:
擴展名 | 說明 | 后期操作 | 命令 |
---|---|---|---|
.c | C程序源代碼,在編譯之前要先進行預處理。 | 預處理、編譯、匯編 | gcc -E test.c -o test.i |
.C | **C++**程序源代碼,在編譯之前要先進行預處理。 | 預處理、編譯、匯編 | |
.cc | **C++**程序源代碼,在編譯之前要先進行預處理。 | 預處理、編譯、匯編 | |
.cxx | **C++**程序源代碼,在編譯之前要先進行預處理。 | 預處理、編譯、匯編 | |
.m | OC程序源代碼,在編譯之前要先進行預處理。 | 預處理、編譯、匯編 | |
.i | C程序預處理輸出,可以被編譯。可以理解為預處理翻譯(interpret)后的文件 | 編譯、匯編 | gcc -s test.i -o test.s |
.ii | **C++**程序預處理輸出,可以被編譯。可以理解為預處理翻譯(interpret)后的文件 | 編譯、匯編 | |
.h | C程序頭文件。(為了節省時間,許多源文件會包含相同的頭文件,GCC 允許事先編譯好頭文件,稱為“預編譯頭文件”,它合適情況下自動被用於編譯。) | 通常不會出現在命令行 | |
.s | 匯編語言。 | 匯編 | gcc -c test.s -o test.o |
1 GCC編譯C/C++的四個過程
gcc 是 GUN Compiler Collection的縮寫。
預處理(pre-processing),E:插入頭文件,替換宏,展開宏
gcc -E main.c -o main.i
編譯(Compiling)S:編譯成匯編
gcc -S main.i –o main.s
匯編(Assembling) c:編譯成目標文件
gcc –c main.s –o main.o
鏈接 (Linking):鏈接到庫中,變成可執行文件
gcc main.o –o main
./main
也可以一次性完成:
gcc main.c –o main
但一般情況下生成.o文件比較好,可以重定位文件,讓別人使用
1.1 預處理
- 處理所有的注釋,以空格代替
- 將所有的
#define
刪除, 並且展開所有的宏定義 - 處理條件編譯指令
#if
,#ifdef
,#elif
,#else
,#endif
處理條件編譯指令 - 處理處理
#include
, 展開(evolve)被包含的文件 - 保留編譯器需要使用的
#pragma
指令
C語言代碼在交給編譯器之前,會先由預處理器進行一些文本替換方面的操作,例如宏展開、文件包含、刪除部分代碼等。
在正常的情況下,GCC 不會保留預處理階段的輸出文件,也即 .i 文件。然而,可以利用 -E 選項保留預處理器的輸出文件,以用於診斷代碼。-E選項指示 GCC 在預處理完畢之后即可停止。
a、默認情況下,預處理器的輸出會被導入到標准輸出流(也就是控制台)
gcc -E main.c
b、可以利用-o選項把它導入到某個輸出文件
gcc -E main.c -o main.i
或者
gcc -E main.c > main.i
表示把預處理的結果導出到 main.i
文件中。
c、使用-C
選項阻止預處理器刪除源文件和頭文件中的注釋。
注意,這里是大寫的 -C,不是小寫的 -c。小寫的 -c 表示只編譯不鏈接。
gcc -E -C main.c -o main.i
對比之前的輸出,可以發現,保留願文件中的注釋
1.2 編譯階段
- 對預處理文件進行一系列詞法分析,語法分析和語義分析
- 詞法分析主要分析關健字,標示符,立即數等是否合法
- 語法分析主要分析表達式是否遵循語法規則
- 語義分析在語法分析的基礎上進一步分析表達式是否合法
- 分析結束后進行代碼優化生成相應的匯編代碼文件
編譯器的核心任務是把C程序翻譯成機器的匯編語言(assembly language)。
匯編語言是人類可以閱讀的編程語言,也是相當接近實際機器碼的語言。每種 CPU 架構都有不同的匯編語言。
實際上,GCC 是一個適合多種 CPU 架構的編譯器,不會把C程序語句直接翻譯成目標機器的匯編語言,而是在輸入語言和輸出匯編語言之間,利用一個中間語言,稱為 RegisterTransfer Language(簡稱 RTL,寄存器傳輸語言)。借助於這個抽象層,在任何背景下,編譯器可以選擇最經濟的方式對給定的操作編碼。
通常情況下,GCC 把匯編語言輸出存儲到臨時文件中,並且在匯編器執行完后立刻刪除它們。但是可以使用-S選項,讓編譯程序在生成匯編語言輸出之后立刻停止,而不刪除臨時文件。
-S可以理解為**Save
或者Stop
**,自己的理解僅供參考記憶。
如果沒有指定輸出文件名,那么采用**-S
**選項的 GCC 編譯過程會為每個被編譯的輸入文件生成以.s作為后綴的匯編語言文件。如下例所示:
gcc -S ts.c
編譯器預處理 ts.c
,將其翻譯成匯編語言,並將結果存儲在 ts.s
文件中,可以用文本編輯器察看.
指定輸出文件名
gcc -S main.i -o filename.s
1.3 匯編階段
- 匯編器將匯編代碼轉變為機器可以執行的指令
- 每個匯編語句幾乎都對應一條機器指令
目標文件是一種中間文件或者臨時文件,如果不設置該選項,gcc 一般不會保留目標文件,可執行文件生成完成后就自動刪除了。
注意,使用-c選項表示只編譯源文件,而不進行鏈接,因此,對於鏈接中的錯誤是無法發現的。
比如,調用一個不存在的函數,編譯器會提示的錯誤。但是該命令不會輸出錯誤信息。
gcc -c main.s -o main.o
**-c
選項:**只編譯不鏈接,僅生成目標文件。
1.4 鏈接階段
gcc main.o -o main.exe
連接器的主要作用是把各個模塊之間相互引用的部分處理好,使得各個模塊之間能夠正確的銜接
靜態鏈接
靜態鏈接使用靜態庫進行鏈接,生成的程序包含程序運行所需要的全部庫,可以直接運行,不過靜態鏈接生成的程序比較大。
動態鏈接
動態鏈接使用動態鏈接庫進行鏈接,生成的程序在執行的時候需要加載所需的動態庫才能運行。
動態鏈接生成的程序體積較小,但是必須依賴所需的動態庫,否則無法執行。
2 clang 分四步編譯main.c
這里用的clang/clang++ 分四步編譯main.c/main.cpp文件
2.1 預處理
clang++ -E main.cpp -o main.ii
2.2 編譯階段,生成匯編
clang++ -S main.ii -o main.s
2.3 匯編階段,生成目標文件
clang++ -c main.s -o mian.o
2.4 連接階段
clang++ mian.o -o main
2.5 執行
./main
2.6 源文件
#include <iostream>
int main() {
std::cout << "Hello Biter !" << std::endl;
return 0;
}
2.7 四部曲之一步到胃
clang++在編譯的過程中,保存所有編譯過程中產生的文件
2.8 產生中間文件
clang++ -save-temps main.cpp -o main
2.9 不保存中間文件
clang++ main.cpp -o main
3 案例
- 頭文件
//
// Created by langkechanxin on 2021/1/7.
//
#define MAX(a,b) (((a)>(b)) ? (a) : (b))
int g_global = 10;
- 源文件
//
// Created by langkechanxin on 2021/1/7.
//
#include "test.h"
#define LOW 0
#define HIGH 255
int max(int a, int b)
{
return MAX(a,b);
}
int main()
{
int c = max(LOW, HIGH); // Call max to get the larger number
return 0;
}
預處理
gcc -E test.c -o test.i
編譯
gcc -S test.i -o test.s
匯編
gcc -c test.s -o test.o
此時,借助 -c
選項,生成的中間產物 test.o
,此文件為二級制文件, 通過 ls -l
命令查看一下文件大小:
-rw-r--r-- 1 binny staff 912B Jan 7 15:41 test.o
借助二進制文件查看器,比如 UltraEdit 可以查看到地址范圍:00000000h - 00000390(不包含)這是下一個字節的地址編號。一共 390hB=912B.