感謝 up 主 ZOMI 醬:https://space.bilibili.com/517221395
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 萬行代碼,是現存大的自由程序之一 |