GCC編譯過程和原理
GCC的主要特徵
- 是一個可移植的編譯器,支援多種硬體平臺
- 跨平臺交叉編譯
- 有多種語言前端,用於解析不同的語言
- 模組化設計,可加入新語言和新CPU架構支援
- 是開源自由軟體,可免費使用
GCC的編譯流程
GCC的編譯過程可以大致分為預處理、編譯、彙編和連結四個階段。
源程式(文字)
#include <stdio.h>
#define HELLOWORD ("hello world\n")
int main(void){
printf(HELLOWORD);
return 0;
}
預處理(cpp)
生成檔案 hello.i
gcc -E hello.c -o hello.i
在預處理過程中,原始碼會被讀入,並檢查其中包含的預處理指令和宏定義,然後進行相應的替換操作。此外,預處理過程還會刪除程式中的註釋和多餘空白字元。最終生成的.i檔案包含了經過預處理後的程式碼內容。
當高階語言程式碼經過預處理生成.i檔案時,預處理過程會涉及宏替換、條件編譯等操作。以下是對這些預處理操作的解釋:
- 標頭檔案展開:
在預處理階段,編譯器會將原始檔中包含的標頭檔案內容插入到原始檔中對應的位置,以便在編譯時能夠訪問標頭檔案中定義的函式、變數、宏等內容。 - 宏替換:
在預處理階段,編譯器會將原始檔中定義的宏在使用時進行替換,即將宏名稱替換為其定義的內容。這樣可以簡化程式碼編寫,提高程式碼的可讀性和可維護性。 - 條件編譯:
透過預處理指令如#if、#else、#ifdef等,在編譯前確定某些程式碼片段是否應被包含在最終的編譯過程中。這樣可以根據條件編譯選擇性地包含程式碼,實現不同平臺、環境下的程式碼控制。 - 刪除註釋:
在預處理階段,編譯器會刪除原始檔中的註釋,包括單行註釋(//)和多行註釋(/.../),這樣可以提高編譯速度並減少編譯後程式碼的大小。 - 新增行號和檔名標識:
透過預處理指令如#line,在預處理階段新增行號和檔名標識到原始檔中,便於在編譯過程中定位錯誤資訊和除錯。 - 保留#pragma命令:
在預處理階段,編譯器會保留以#pragma開頭的預處理指令,如#pragma once、#pragma pack等,這些指令可以用來指導編譯器進行特定的處理,如控制編譯器的行為或最佳化程式碼。
hello.i檔案部分內容如下,詳細可見../code/gcc/hello.i檔案。
int main(void){
printf(("hello world\n"));
return 0;
}
在該檔案中,已經將標頭檔案包含進來,宏定義HELLOWORD替換為字串"hello world\n",並刪除了註釋和多餘空白字元。
編譯(ccl)
在這裡,編譯並不僅僅指將程式從原始檔轉換為二進位制檔案的整個過程,而是特指將經過預處理的檔案(hello.i)轉換為特定彙編程式碼檔案(hello.s)的過程。
在這個過程中,經過預處理後的.i檔案作為輸入,透過編譯器(ccl)生成相應的彙編程式碼.s檔案。編譯器(ccl)是GCC的前端,其主要功能是將經過預處理的程式碼轉換為彙編程式碼。編譯階段會對預處理後的.i檔案進行語法分析、詞法分析以及各種最佳化,最終生成對應的彙編程式碼。
彙編程式碼是以文字形式存在的程式程式碼,接著經過編譯生成.s檔案,是連線程式設計師編寫的高階語言程式碼與計算機硬體之間的橋樑。
生成檔案 hello.s:
gcc -S hello.i -o hello.s
hello.s:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15, 6
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %ecx, %ecx
movl %eax, -8(%rbp) ## 4-byte Spill
movl %ecx, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "hello world\n"
.subsections_via_symbols
現在hello.s檔案中包含了完全是彙編指令的內容,表明hello.c檔案已經被成功編譯成了組合語言。
彙編(as)
在這一步中,我們將彙編程式碼轉換成機器指令。這一步是透過彙編器(as)完成的。彙編器是GCC的後端,其主要功能是將彙編程式碼轉換成機器指令。
彙編器的工作是將人類可讀的彙編程式碼轉換為機器指令或二進位制碼,生成一個可重定位的目標程式,通常以.o作為副檔名。這個目標檔案包含了逐行轉換後的機器碼,以二進位制形式儲存。這種可重定位的目標程式為後續的連結和執行提供了基礎,使得我們的彙編程式碼能夠被計算機直接執行。
生成檔案 hello.o
gcc -c hello.s -o hello.o
連結(ld)
連結過程中,連結器的作用是將目標檔案與其他目標檔案、庫檔案以及啟動檔案等進行連結,從而生成一個可執行檔案。在連結的過程中,連結器會對符號進行解析、執行重定位、進行程式碼最佳化、確定空間佈局,進行裝載,並進行動態連結等操作。透過連結器的處理,將所有需要的依賴項打包成一個在特定平臺可執行的目標程式,使用者可以直接執行這個程式。
gcc -o hello.o -o hello
新增-v引數,可以檢視詳細的編譯過程:
gcc -v hello.c -o hello
- 靜態連結
靜態連結是指在連結程式時,需要使用的每個庫函式的一份複製被加入到可執行檔案中。透過靜態連結使用靜態庫進行連結,生成的程式包含程式執行所需要的全部庫,可以直接執行。然而,靜態連結生成的程式體積較大。 - 動態連結
動態連結是指可執行檔案只包含檔名,讓載入器在執行時能夠尋找程式所需的函式庫。透過動態連結使用動態連結庫進行連結,生成的程式在執行時需要載入所需的動態庫才能執行。相比靜態連結,動態連結生成的程式體積較小,但是必須依賴所需的動態庫,否則無法執行。
編譯方法
型別 | 定義 | 示例 |
---|---|---|
本地編譯 | 編譯原始碼的平臺和執行原始碼編譯後程式的平臺是同一個平臺。 | 在Intel x86架構/Windows平臺上編譯,生成的程式在同樣的Intel x86架構/Windows 10下執行。 |
交叉編譯 | 編譯原始碼的平臺和執行原始碼編譯後程式的平臺是兩個不同的平臺。 | 在Intel x86架構/Linux(Ubuntu)平臺上使用交叉編譯工具鏈編譯,生成的程式在ARM架構/Linux下執行。 |
GCC 與傳統編譯過程區別
傳統的三段式劃分是指將編譯過程分為前端、最佳化、後端三個階段,每個階段都有專門的工具負責。
而在GCC 中,編譯過程被分成了預處理、編譯、彙編、連結四個階段 。其中 GCC 的預處理、編譯階段屬於三段式劃分的前端部分,彙編階段屬於三段式劃分的後端部分。
GCC 的連結階段是三段式劃分後端部分的最佳化階段合併,但其與端部分的目的一致,都是為了生成可執行檔案。
GCC 編譯過程的四個階段與傳統的三段式劃分的前端、最佳化、後端三個階段有一定的重合和對應關係,但GCC更為詳細和全面地劃分了編譯過程,使得每個階段的功能更加明確和獨立。
總結
本節介紹了GCC的編譯過程,主要包括預處理、編譯、彙編和連結四個階段。並總結了 GCC 的優點和缺點:
GCC 的優點 | GCC 的缺點 |
---|---|
1)支援 JAVA/ADA/FORTRAN | 1)GCC 程式碼耦合度高,很難獨立,如整合到專用 IDE 上,模組化方式來呼叫 GCC 難 |
2)GCC 支援更多平臺 | 2)GCC 被構建成單一靜態編譯器,使得難以被作為 API 並整合到其他工具中 |
3)GCC 更流行,廣泛使用,支援完備 | 3)從 1987 年發展到 2022 年 35 年,越是後期的版本,程式碼質量越差 |
4)GCC 基於 C,不需要 C++ 編譯器即可編譯 | 4)GCC 大約有 1500 萬行程式碼,是現存大的自由程式之一 |