引言
距離上一篇博客更新已經過去了大概一兩周的時間,而對于?Linux 系統的基本指令以及 Shell 編程的學習其實基本講解完畢,Linux基礎一塊的知識就將告一段落了,如果有細節性的知識,我也會及時分享給各位,作為一名正在攀登 Linux C/C++ 開發這座高峰的學習者,最近系統性地學習了 GCC 這個至關重要的工具。它絕不僅僅是一個簡單的“編譯器命令”,而是一個驅動我們代碼變成可執行程序的強大引擎。理解其內部流程:預處理、編譯、匯編、鏈接,對于寫出高效、可調試的代碼至關重要。而本文即介紹 GCC 這個工具是如何將我們的源碼轉換為一個可執行性程序的,詳細拆解 GCC 編譯的每一步。希望能幫助到同樣在學習路上的你,也方便自己日后回顧。
1. 初識 GCC:開源的編譯基石
1.1 什么是 GCC
GCC的全稱是:GNU Compiler Collection (GNU 編譯器套件),它的主要作用是將高級語言(C, C++, Objective-C, Fortran, Ada, Go 等)編寫的源碼翻譯為計算機底層能夠理解與執行的機器碼。或者是轉換為更為底層的語言,如匯編語言。
GCC 支持多種操作系統(Linux、Windows、macOS等),同時它并不只是簡單的編譯器, 更多是一個驅動程序。它本身并不完成所有編譯工作,而是根據你給的源代碼類型(.c
,?.cpp
?等)和參數,智能地調用后臺真正的預處理器、編譯器、匯編器和鏈接器等工具來完成整個構建流程。
1.2 GCC 編譯流程
當我們編寫好了一個源代碼文件之后,之前我都不知道程序是如何運行起來的,經過了這一段時間的學習,才對C/C++程序的運行有了一定的理解,當我們編寫好了源文件,gcc 將程序編譯分為預處理→編譯→匯編→鏈接四個步驟。
1.3?POSIX標準
是由 IEEE (電氣和電子工程師協會)指定的一組標準,全稱為:可移植操作系統接口(Portable Operating System Interface),定義了不同的操作系統(尤其是類Unix系統)應該為應用程序提供的相同的接口 (API)?和服務。
該標準的核心目的就是促進應用軟件與多種類型的操作系統之間的兼容性以及可移植性,也就是說,只要遵循 POSIX 標準編寫的程序,理論上可以在任何兼容 POSIX 的操作系統上編譯和運行。
1.3.1 POSIX 標準具體內容
系統調用和庫內容:定義了操作系統應提供的核心服務,如文件的系統操作、進程管理和線程控制。
Shell 和系統工具:規定了標準命令行接口和一系列基本工具,如 awk 、 echo 等。
程序線程接口 :包含語言、函數等接口規范,使程序能夠在任何遵循 POSIX 標準的操作系統中運行。
1.4 安裝 GCC
講了這么多,我們又應該如何安裝 GCC 呢?下面我們以 Ubantu22.04?版本的 Linux 操作系統為例,
安裝 GCC 的指令如下:
sudo apt install gcc
根據提示輸入y即可。
安裝好 GCC 后,我們可以通過如下命令檢查安裝的gcc版本
gcc --version
2. 揭秘 GCC 編譯流程:從?.c/.cpp
?到可執行文件的四步舞曲
2.1 實例引入
首先我們在我們 /home/~ 目錄下創建一個實例目錄
mkdir study_helloworld
cd study_helloworld
?然后我們在目錄下編寫三個文件,實現最基礎的打印 helloworld 的功能。
2.1.1編寫源文件
1. main.c
#include "hello.h"int main()
{say_hello();return 0;
}
2. hello.h
#ifndef __HELLO_H__
#define __HELLO_H__void say_hello();#endif
3. hello.c
#include "hello.h"
#include <stdio.h>void say_hello()
{printf("Hello world!\n");
}
我們可以采用如下命令編譯可執行文件并執行:
gcc main.c hello.c -o main
./main
可以看到成功輸出了 hello world。
其實我們在輸入?gcc main.c hello.c -o main
?這樣一條簡單的命令時,背后隱藏著四個精妙的步驟。
2.2 步驟的詳細介紹
2.2.1?預處理
預處理的主要任務是對源代碼進行文本層次的加工處理,將它們轉換成編譯器可以識別的形式。
主要進行的操作如下:
頭文件包含 (
#include
):?將被?#include
?指定的頭文件(.h
)內容完整地復制并插入到?#include
?指令所在的位置。形成“單一”的、龐大的源文件。宏展開 (
#define
):?查找源代碼中所有通過?#define
?定義的宏,并將其原地替換為定義的值或代碼片段。條件編譯 (
#ifdef
,?#if
,?#endif
,?#else
,?#elif
):?根據指定的條件(通常是宏定義是否存在或值)決定保留或刪除某部分代碼塊。常用于平臺適配、功能開關。刪除注釋:?移除所有單行 (
//
) 和多行 (/* ... */
) 注釋,減少后續處理負擔。
被進行了預處理的源文件仍然是純文本文件,其拓展名一般為.i
?(C) 或?.ii
?(C++),同時我們也可以單獨進行預處理操作:
# 輸出預處理后的C代碼到hello.i
gcc -E hello.c -o hello.i# 輸出預處理后的C代碼到main.i
gcc -E main.c -o main.i
-E:Expand(展開)的縮寫,該參數指定gcc執行預處理操作
.i:intermediate(中間的)的縮寫,預處理后的源文件通常以.i作為后綴。
執行了上述指令之后我們便可以查看生成的 main.i 的預處理文件
你會看到所有頭文件都被塞進來、宏都被替換掉、注釋消失了、條件編譯后的代碼保留下來了。預處理器處理后的文件通常會比原始源文件大,因為它會展開宏和包含其他文件的內容。
2.2.2?編譯?
該步驟的主要任務是將預處理后的源代碼(.i
?/?.ii
)翻譯成特定處理器架構的匯編語言代碼。
主要進行的操作是
語法分析:?檢查代碼是否符合 C/C++ 語言的語法規則。遇到語法錯誤會在此階段報錯(
syntax error
)。語義分析:?進行更深入的檢查,確保代碼在邏輯上是有意義的。遇到類型不匹配、未聲明標識符等問題會在此階段報錯。
詞法分析:?將源代碼拆分成有意義的單詞,如關鍵字、標識符、運算符、常量等。
生成中間表示:?編譯器內部會將代碼轉換成一種或多種中間表示形式,便于進行優化和分析。
生成匯編代碼:?將優化后的中間表示轉換為目標處理器架構的匯編指令。這些指令是機器指令的人類可讀(勉強可讀)的助記符形式。
經過編譯處理之后的文件成為匯編文件后綴名為.s
?,這也是一個純文本文件,你可以用文本編輯器打開查看,里面是像?movl
,?call
,?addq
?這樣的匯編指令。
執行下面的命令對剛剛生成的預處理文件進行單獨編譯操作:
# 將預處理后的C代碼編譯成匯編代碼hello.s
gcc -S hello.i -o hello.s# 將預處理后的C代碼編譯成匯編代碼main.s
gcc -S main.i -o main.s
-S:Source(源代碼)的縮寫,該參數指定gcc將預處理后的源碼編譯為匯編語言。
.s:Assembly Source(匯編源碼)的縮寫,通常編譯后的匯編文件以.s作為后綴。
可以看到里面的內容都是匯編語言的代碼。
2.2.3 匯編
該步驟的主要任務是將匯編語言文件翻譯成機器指令,并打包成特定格式的目標文件 (Object File)。
在這一過程中所進行的主要操作是:
逐行解析:?讀取匯編文件中的每一條指令和數據定義。
生成機器碼:?將每條匯編指令一對一地翻譯成對應處理器架構的二進制機器指令。這是 CPU 真正能直接執行的代碼。
處理數據:?為程序中定義的全局變量、靜態變量分配初始存儲空間或預留空間。
生成符號表:?創建目標文件內部的符號表 (Symbol Table)。這個表記錄了該文件中定義的符號(如函數名、全局變量名)及其位置(地址),以及該文件中引用但未在此文件中定義的符號。
生成重定位信息:?記錄文件中那些在鏈接階段才能確定最終地址的位置。這些位置在目標文件中是臨時的或為0的,需要鏈接器后續修正。
經過匯編操作的文件是目標文件,通常擴展名為?.o
?(Linux/Unix) 或?.obj
?(Windows)。注意這里不同的操作系統的后綴名不同,這是一個二進制文件,包含機器指令,但還不是最終可運行的程序。
執行下面的命令對剛剛生成的匯編文件進行單獨匯編操作:
gcc -c main.s -o main.o
gcc -c hello.s -o hello.o
-c:可以被理解為Compile or Assemble(編譯或匯編),該參數可以指定gcc將匯編代碼翻譯為機器碼,但不做鏈接。此外,該參數也可以用于將.c文件直接處理為機器碼,同樣不做鏈接。
-o:Object的縮寫,通常匯編得到的機器碼文件以.o為后綴。
到這里,生成的已經是二進制文件了,就不可以使用文本編輯器直接查看該文件了。可以通過下面指令查看 main.o 的內容。
objdump -s main.o
2.2.4 鏈接
這個階段由鏈接器完成,該步驟的主要任務是將一個或者多個目標文件,以及所需的庫文件組合到一起,通過解析符號之間引用關系、分配最終的內存地址,生成一個完整的、可直接加載到內存中執行的可執行性文件或者庫文件。可以說該步驟的內容最為復雜。
下面介紹三種不同的鏈接方式:
1.?靜態鏈接 (-static
?或默認鏈接?.a
?文件):
?將靜態庫中實際被用到的目標文件代碼完整地拷貝到最終的可執行文件中。優點:程序獨立性強,運行時不需要庫文件存在。缺點:可執行文件體積大,庫更新需重新鏈接整個程序。
gcc -static main.o hello.o -o main
-static:該參數指示編譯器進行靜態鏈接,而不是默認的動態鏈接。使用這個參數,GCC會嘗試將所有用到的庫函數直接鏈接到最終生成的可執行文件中,包括C標準庫(libc)、數學庫(libm)和其他任何通過代碼引用的外部庫。
2. 動態鏈接
將動態庫 (共享庫,.so
?文件)?中符號的引用信息記錄到可執行文件中。運行時由操作系統的動態鏈接器?負責在程序加載或運行時,將所需的動態庫加載到內存,并完成最終的重定位(地址綁定)。優點:可執行文件小,節省內存(多個程序可共享同一份庫代碼),庫更新方便。缺點:程序運行時依賴庫文件存在且版本兼容。
方式一:
gcc main.o hello.o -o main
沒有添加-static關鍵字,gcc默認執行動態鏈接,即glibc庫文件沒有包含到可執行文件中。
3. 混合鏈接
有時候需要用到某些靜態庫靜態鏈接,有時候有需要動態鏈接,混合鏈接則結合二者的優點。
執行下面的指令可以將hello.o編譯為靜態鏈接庫libhello.a
ar crv libhello.a hello.o
# ar:歸檔命令,用于處理靜態庫文件。
# crv: ar命令的選項
# c:創建歸檔文件
# r:替換歸檔文件中現有的文件或者向歸檔文件中添加新文件。
# v:詳細模式(verbose mode)
結語:
理解 GCC 編譯的四步流程(預處理->編譯->匯編->鏈接)是 Linux C/C++ 開發者的一項基本功。之后我會相繼介紹 Makefile 文件的編寫,C/C++的動態鏈接庫與靜態鏈接庫等區別。最初我只知道點擊編輯器上邊的 run 按鈕就能運行程序,現在明白了這背后精妙的四步轉換。多動手實踐,,是鞏固這些知識的最佳途徑希望這篇博客能對正在學習 C/C++ 編程的同學有所幫助,如有錯誤或不足之處,歡迎在評論區留言指正。