GCC 编译 C++ 程序分步骤流程(预处理 gcc -E、编译 gcc -S、汇编 gcc -c 和链接 gcc 以及 gcc -o 选项)
C 或者 C++ 程序從源代碼生成可執行程序的過程,需經歷 4 個過程,分別是預處理、編譯、匯編和鏈接。
同樣,使用 GCC 編譯器編譯 C 或者 C++ 程序,也必須要經歷這 4 個過程。但考慮在實際使用中,用戶可能并不關心程序的執行結果,只想快速得到最終的可執行程序,因此 gcc 和 g++ 都對此需求做了支持。
#include <iostream>int main()
{std::cout << "hello,world" << std::endl;return 0;
}
編寫如下指令并執行
wohu@ubuntu:~/cpp/src$ g++ test.cpp
wohu@ubuntu:~/cpp/src$ ls
a.out test.cpp
同樣,GCC 編譯器會在當前目錄下生成一個名為 a.out 的可執行文件(如果之前有同名文件,舊文件會被覆蓋)。通過如下指令即可運行該文件:
wohu@ubuntu:~/cpp/src$ ./a.out
hello,world
wohu@ubuntu:~/cpp/src$
注意,gcc 或者 g++ 指令還支持用戶手動指定最終生成的可執行文件的文件名,
wohu@ubuntu:~/cpp/src$ g++ test.cpp -o main
wohu@ubuntu:~/cpp/src$ ls
a.out main test.cpp
wohu@ubuntu:~/cpp/src$ ./main
hello,world
wohu@ubuntu:~/cpp/src$
其中 -o 選項用于指定要生成的文件名,例如 -o main 即表示將生成的可執行文件名設為 main。
1. gcc -o選項
gcc -o 選項用來指定輸出文件,如果不使用 -o 選項,那么將采用默認的輸出文件。例如默認情況下,生成的可執行文件的名字默認為 a.out。
如下是 gcc -o 指令的使用語法格式:
gcc [-E|-S|-c] [infile] [-o outfile]
其中,用方括號 [] 括起來的部分可以忽略。
- [infile] 表示輸入文件(也即要處理的文件),它可以是源文件、匯編文件或者目標文件;
- [outfile] 表示輸出文件(也即處理的結果),可以是預處理文件、目標文件、可執行文件等;
值得一提的是,通常情況下 [infile] 處放置一個文件,但根據實際需要也可以放置多個文件,表示有多個輸入文件。
注意,雖然我們僅編寫了一條 gcc 或者 g++ 指令,但其底層依據是按照預處理、編譯、匯編、鏈接的過程將 C 、C++ 程序轉變為可執行程序的。而本應在預處理階段、編譯階段、匯編階段生成的中間文件,此執行方式默認是不會生成的,只會生成最終的 a.out 可執行文件(除非為 gcc 或者 g++ 額外添加 -save-temps 選項)。
查看該過程中產生的中間文件。如此,上面介紹的執行方式將不再使用,而要采用分步編譯的方式。
所謂“分步編譯”,即由用戶手動調用 GCC 編譯器完成對 C、C++源代碼的預處理、編譯、匯編以及鏈接過程,每個階段都會生成對源代碼加工后的文件。
GCC 常用的編譯選項
GCC 手冊
接下來將對如何實現分步編譯做詳細的講解,即如何將一個源代碼程序經歷預處理、編譯、匯編以及鏈接這 4 個過程,最終生成對應的可執行程序。
2. 預處理
無論是 C 還是 C++ 程序,其從源代碼轉變為可執行代碼的過程,具體可分為 4 個過程,分別為預處理(Preprocessing)、編譯(Compilation)、匯編(Assembly)和鏈接(Linking)。
默認情況下,gcc 指令會一氣呵成,直接將源代碼歷經這 4 個過程轉變為可執行代碼,且不會保留各個階段產生的中間文件。
而如果想查看這 4 個階段各自產生的中間文件,最簡單直接的方式就是對源代碼進行“分步編譯”,即控制 GCC 編譯器逐步對源代碼進行預處理、編譯、匯編以及鏈接操作。其中,通過為 gcc 指令添加 -E 選項,即可控制 GCC 編譯器僅對源代碼做預處理操作。
所謂預處理操作,主要是處理那些源文件和頭文件中以 # 開頭的命令(比如 #include、#define、#ifdef 等),并刪除程序中所有的注釋 // 和 /* … */
默認情況下 gcc -E 指令只會將預處理操作的結果輸出到屏幕上,并不會自動保存到某個文件。因此該指令往往會和 -o 選項連用,將結果導入到指令的文件中。比如:
wohu@ubuntu:~/cpp/src$ g++ -E test.cpp -o test.ii
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.ii
wohu@ubuntu:~/cpp/src$
Linux 系統中,通常用 .i 或者 .ii 作為 C/C++ 程序預處理后所得文件的后綴名。由此,就完成了 test.cpp 文件的預處理操作,并將其結果導入到了 test.ii 文件中。
讀者可以通過執行 cat test.ii 指令查看該文件中的內容,但通常沒有足夠 C++ 語言功底的讀者是看不懂的。為此,我們可以為 g++ 指令再添加一個 -C 選項,阻止 GCC 刪除源文件和頭文件中的注釋:
wohu@ubuntu:~/cpp/src$ g++ -E -C test.cpp -o test.ii
注意,這里是大寫的 -C,不是小寫的 -c。小寫的 -c 另作他用,
gcc -E 支持的常用選項
其中,對于指定 #include 搜索路徑的幾個選項,作用的先后順序如下:
- 對于用
#include ""引號形式引入的頭文件,首先搜索當前程序文件所在的目錄,其次再前往-iquote選項指定的目錄中查找; - 前往
-I選項指定的目錄中搜索; - 前往
-isystem選項指定的目錄中搜索; - 前往默認的系統路徑下搜索;
- 前往
-idirafter選項指定的目錄中搜索;
3. 編譯
所謂編譯,簡單理解就是將預處理得到的程序代碼,經過一系列的詞法分析、語法分析、語義分析以及優化,加工為當前機器支持的匯編代碼。
通過給 g++ 指令添加 -S(注意是大寫)選項,即可令 GCC 編譯器僅將指定文件加工至編譯階段,并生成對應的匯編代碼文件。例如:
wohu@ubuntu:~/cpp/src$ g++ -S test.ii
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.ii test.s
wohu@ubuntu:~/cpp/src$
可以看到,經過執行 g++ -S 指令,其生成了一個名為 test.s 的文件,這就是經過編譯的匯編代碼文件。也就是說默認情況下,編譯操作會自行新建一個文件名和指定文件相同、后綴名為 .s 的文件,并將編譯的結果保存在該文件中。
我們還可以為 gcc -S 指令添加 -o 選項,令 GCC 編譯器將編譯結果保存在我們指定的文件中。例如:
wohu@ubuntu:~/cpp/src$ g++ -S test.ii -o demo.s
wohu@ubuntu:~/cpp/src$ ls
demo.s test.cpp test.ii test.s
wohu@ubuntu:~/cpp/src$
需要注意的是,gcc -S 指令操作的文件并非必須是經過預處理后得到的 .i 或 .ii 文件,-S 選項的功能是令 GCC 編譯器將指定文件處理至編譯階段結束。這也就意味著,gcc -S 指令可以操作預處理后的 .i 或 .ii 文件,也可以操作源代碼文件:
- 如果操作對象為
.i或.ii文件,則GCC編譯器只需編譯此文件; - 如果操作對象為
.c或者.cpp源代碼文件,則GCC編譯器會對其進行預處理和編譯這 2 步操作。
因此,如果我們想直接得到 test.cpp 文件對應的匯編文件,就可以借助 gcc -S 指令:
wohu@ubuntu:~/cpp/src$ g++ -S test.cpp -o test.s
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.s
wohu@ubuntu:~/cpp/src$
由此,我們就可以直接獲得 test.cpp 對應的 test.s 匯編文件。
如果想提高文件內匯編代碼的可讀性,可以借助 -fverbose-asm 選項,GCC 編譯器會自行為匯編代碼添加必要的注釋,例如:
wohu@ubuntu:~/cpp/src$ g++ -S test.cpp -o test.s -fverbose-asm
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.s
wohu@ubuntu:~/cpp/src$
4. 匯編
如何對已得到的 test.s 執行匯編操作,并得到相應的目標文件,需要進行匯編操作。所謂目標文件,其本質為二進制文件,但由于尚未經過鏈接操作,所以無法直接運行。
簡單地理解,匯編其實就是將匯編代碼轉換成可以執行的機器指令。大部分匯編語句對應一條機器指令,有的匯編語句對應多條機器指令。相對于編譯操作,匯編過程會簡單很多,它并沒有復雜的語法,也沒有語義,也不需要做指令優化,只需要根據匯編語句和機器指令的對照表一一翻譯即可。
通過為 gcc 指令添加 -c 選項(注意是小寫字母 c),即可讓 GCC 編譯器將指定文件加工至匯編階段,并生成相應的目標文件。例如:
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.s
wohu@ubuntu:~/cpp/src$ g++ -c test.s
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.o test.s
wohu@ubuntu:~/cpp/src$
可以看到,該指令生成了和 test.s 同名但后綴名為 .o 的文件,這就是經過匯編操作得到的目標文件。
還可以為 gcc -c 指令在添加一個 -o 選項,用于將匯編操作的結果輸入到指定文件中,例如:
wohu@ubuntu:~/cpp/src$ g++ -c test.s -o test_obj.o
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.o test_obj.o test.s
wohu@ubuntu:~/cpp/src$
需要強調的一點是,和 g++ -S 類似,g++ -c 選項并非只能用于加工 .s 文件。事實上,-c 選項只是令 GCC 編譯器將指定文件加工至匯編階段,但不執行鏈接操作。這也就意味著:
- 如果指定文件為源程序文件(例如 test.cpp),則
gcc -c指令會對 test.cpp 文件執行預處理、編譯以及匯編這 3 步操作; - 如果指定文件為剛剛經過預處理后的文件(例如 test.i),則
gcc -c指令對 test.i 文件執行編譯和匯編這 2 步操作; - 如果指定文件為剛剛經過編譯后的文件(例如 test.s),則
gcc -c指令只對test.s文件執行匯編這 1 步操作。
注意,如果指定文件已經經過匯編,或者 GCC 編譯器無法識別,則 gcc -c 指令不做任何操作。
5. 鏈接
總的來說鏈接階段要完成的工作,就是將同一項目中各源文件生成的目標文件以及程序中用到的庫文件整合為一個可執行文件。
目標文件已經是二進制文件,與可執行文件的組織形式類似,只是有些函數和全局變量的地址還未找到,因此還無法執行。鏈接的作用就是找到這些目標地址,將所有的目標文件組織成一個可以執行的二進制文件。
完成鏈接操作,并不需要給 g++ 添加任何選項,只要將匯編階段得到的 test.o 作為參數傳遞給它,g++就會在其基礎上完成鏈接操作。例如:
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.o test_obj.o test.s
wohu@ubuntu:~/cpp/src$ g++ test.o
wohu@ubuntu:~/cpp/src$ ls
a.out test.cpp test.o test_obj.o test.s
wohu@ubuntu:~/cpp/src$ g++ test_obj.o -o test_obj
wohu@ubuntu:~/cpp/src$ ls
a.out test.cpp test.o test_obj test_obj.o test.s
wohu@ubuntu:~/cpp/src$ ./a.out
hello,world
wohu@ubuntu:~/cpp/src$ ./test_obj
hello,world
wohu@ubuntu:~/cpp/src$
gcc 會根據所給文件的后綴名 .o,自行判斷出此類文件為目標文件,僅需要進行鏈接操作。
通過分別執行這 a.out 和 test_obj 可執行文件,其執行結果完全相同,說明 test.o 和 test_obj.o 是完全相同的。
6. 一步生成所有中間結果
如果讀者不想執行這么多條指令,但想獲得預處理、編譯、匯編以及鏈接這 4 個過程產生的中間文件,可以執行如下指令:
wohu@ubuntu:~/cpp/src$ ls
test.cpp
wohu@ubuntu:~/cpp/src$ g++ test.cpp -save-temps
wohu@ubuntu:~/cpp/src$ ls
a.out test.cpp test.ii test.o test.s
wohu@ubuntu:~/cpp/src$
可以看到,通過給 g++ 添加 -save-temps 選項,可以使 GCC 編譯器保留編譯源文件過程中產生的所有中間文件。
總結
以上是生活随笔為你收集整理的GCC 编译 C++ 程序分步骤流程(预处理 gcc -E、编译 gcc -S、汇编 gcc -c 和链接 gcc 以及 gcc -o 选项)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: gcc 自动识别的文件扩展名,gcc/g
- 下一篇: 2022-2028年中国废旧塑料回收产业