簡介
對于平常的應用程序開發(fā),我們很少需要關注編譯和鏈接過程,因為通常的開發(fā)環(huán)境都是流行的集成開發(fā)環(huán)境(IDE),比如Visual Studio等。這樣的IDE一般都將編譯和鏈接的過程一步完成,通常將這種編譯和鏈接合并到一起的過程稱為構建(Build)。即使使用命令行來編譯一個源代碼文件,簡單的一句“gcc hello.c”命令就包含了非常復雜的過程。
IDE和編譯器提供的默認配置、編譯和鏈接參數對于大部分的應用程序開發(fā)而言已經足夠使用了。但是在這樣的開發(fā)過程中,我們往往會被這些復雜的集成工具所提供的強大功能所迷惑,很多系統(tǒng)軟件的運行機制與機理被掩蓋,其程序的很多莫名其妙的錯誤讓我們無所適從,面對程序運行時種種性能瓶頸我們束手無策。我們看到的是這些問題的現象,但是卻很難看清本質,所有這些問題的本質就是軟件運行背后的機理及支撐軟件運行的各種平臺和工具,如果能夠深入了解這些機制,那么解決這些問題就能夠游刃有余,收放自如了。
一、被隱藏了的過程
C語言的經典,“Hello World”程序。
使用c語言做例子吧,我使用cpp的預處理過,但是預處理的文件太長了。
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
在Linux下,當我們使用GCC來編譯Hello World程序時,只須使用最簡單的命令(源代碼文件名為hello.c):
$gcc hello.c
$./a.out
Hello World事實上,上述過程可以分解為4個步驟,分別是預處理(Prepressing)、編譯(Compilation)、匯編(Assembly)和鏈接(Linking),如圖所示。
二、預處理器(Prepressing)——cpp
- 預處理有的文章中,也被稱為預編譯。
1、預處理指令
- 使用預處理器cpp
會把.c文件預處理成一個.i文件。
$cpp hello.c > hello.i
- 實戰(zhàn)
2、預處理過程
預處理過程主要處理那些源代碼文件中的以“#”開始的預編譯指令。比如 “#include”、“#define ”等,主要處理規(guī)則如下:
- 將所有的 “#define ”刪除,并且展開所有的宏定義。
- 處理所有條件預編譯指令,比如 “#if”、“#ifdef”、“#elif”、“#else”、“#endif ”。
- 處理 “#include ”預編譯指令,將被包含的文件插入到該預編譯指令的位置。注意,這個過程是遞歸進行的,也就是說被包含的文件可能還包含其他文件。
- 刪除所有的注釋“//”和“/* */”。
- 添加行號和文件名標識,比如#2“hello.c”2,以便于編譯時編譯器產生調試用的行號信息及用于編譯時產生編譯錯誤或警告時能夠顯示行號。
- 保留所有的 #pragma 編譯器指令,因為編譯器須要使用它們。
經過預編譯后的.i文件不包含任何宏定義,因為所有的宏已經被展開,并且包含的文件也已經被插入到.i文件中。所以當我們無法判斷宏定義是否正確或頭文件包含是否正確時,可以查看預編譯后的文件來確定問題。
3、預處理生成的hello.i文件
三、編譯器(Compilation)——cc
1、編譯指令
編譯過程就是把預處理完的文件進行一系列詞法分析、語法分析、語義分析及優(yōu)化后生產相應的匯編代碼文件,這個過程往往是我們所說的整個程序構建的核心部分,也是最復雜的部分之一。我們將在下一節(jié)簡單介紹編譯的具體幾個步驟,這涉及編譯原理等一些內容,由于它不是本書介紹的核心內容,所以也僅僅是介紹而已。
目前公司產品有用到這部分的內容,因為公司產品中有自己的語言。但是產品底層的詞法分析和語法分析等部分不是我負責的。后面有機會的話繼續(xù)深入的了解下。
上面的編譯過程相當于如下命令:
$cc hello.i -S -o hello.s
可以得到匯編輸出文件hello.s
2、編譯的過程
- 詞法分析
- 語法分析
- 語義分析
- 中間語言生成
- 目標代碼生成與優(yōu)化
3、編譯生成的文件(如下圖)
后面分一篇文章,詳細的介紹下以下的指令都是什么意思。
四、匯編器(Assembly)——as
1、匯編指令
匯編器是將匯編代碼轉變成機器可以執(zhí)行的指令,每一個匯編語句幾乎都對應一條機器指令。所以匯編器的匯編過程相對于編譯器來講比較簡單,它沒有復雜的語法,也沒有語義,也不需要做指令優(yōu)化,只是根據匯編指令和機器指令的對照表一一翻譯就可以了,“匯編”這個名字也來源于此。
- 上面的匯編過程我們可以調用匯編器as來完成:
$as hello.s -o hello.o
- 實戰(zhàn)
匯編生成的目標文件hello.o如下圖所示
2、查看匯編生成的目標文件
$objdump -h hello.o
五、鏈接器(Linking)——ld
1、鏈接器
- 鏈接器是一個將編譯器產生的目標文件打包成可執(zhí)行文件或者庫文件或者目標文件的程序。
從這句話可以看出,鏈接器實際上是一個程序,它的輸入是編譯器產生的目標文件,輸出是庫、可執(zhí)行文件或者其它目標文件。
目標文件:.so 文件生成.out文件
鏈接的指令
ld /usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbeginT.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib64 -L/usr/lib --start-group -lgcc -lgcc_eh -lc --end-group hello.o -o hello.out
或者
ld /usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbeginT.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib64 -L/usr/lib -lstdc++ -lm -lgcc_s -lc -lgcc hello.o -o hello.out
2、查看鏈接生成的程序
$objdump -h hello.out
- 從下圖可知,從匯編的6個,鏈接后變成了23個。
六、執(zhí)行hello.out
$./hello.out
- 執(zhí)行報錯bash: ./test.out: /lib/ld64.so.1: bad ELF interpreter: 沒有那個文件或目錄?
bash: ./test.out: /lib/ld64.so.1: bad ELF interpreter: 沒有那個文件或目錄
解決方案見:/lib/ld64.so.1: bad ELF interpreter: 沒有那個文件或目錄,因為這個問題不太好處理,所以單獨分了一篇文章,記錄下解決問題的過程。
七、gcc能做的事:預處理、編譯、匯編、鏈接
上面預處理、編譯、匯編、鏈接,我們分別使用了cpp、cc、as、ld。
其實,預處理、編譯、匯編、鏈接這四個過程,gcc自己就能干完了。
- 1、使用gcc預處理(-E表示只進行預編譯)。
$gcc -E hello.c -o hello.i
- 2、編譯
$gcc -S hello.i -o hello.s
或者使用如下命令:
$gcc -S hello.c -o hello.s
- 3、匯編
$gcc -c hello.s -o hello.o
或者使用gcc命令從C源代碼文件開始,經過預編譯、編譯和匯編直接輸出目標文件(Object File):
$gcc -c hello.c -o hello.o
備注:
gcc -c hello.c -o hello.o小寫的-c才是僅僅匯編,
gcc -C hello.c -o hello.o如果是大寫的-C,會進行鏈接的。
- 4、鏈接
$gcc hello.o -o hello.out
或者
$gcc hello.o -o hello
- 5、執(zhí)行
- 5、查看gcc指令
[dev1@localhost test01]$ gcc --help
用法:gcc [選項] 文件...
選項:
-pass-exit-codes 在某一階段退出時返回最高的錯誤碼
--help 顯示此幫助說明
--target-help 顯示目標機器特定的命令行選項
--help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...]
顯示特定類型的命令行選項
(使用‘-v --help’顯示子進程的命令行參數)
--version 顯示編譯器版本信息
-dumpspecs 顯示所有內建 spec 字符串
-dumpversion 顯示編譯器的版本號
-dumpmachine 顯示編譯器的目標處理器
-print-search-dirs 顯示編譯器的搜索路徑
-print-libgcc-file-name 顯示編譯器伴隨庫的名稱
-print-file-name=<庫> 顯示 <庫> 的完整路徑
-print-prog-name=<程序> 顯示編譯器組件 <程序> 的完整路徑
-print-multiarch Display the target's normalized GNU triplet, used as
a component in the library path
-print-multi-directory 顯示不同版本 libgcc 的根目錄
-print-multi-lib 顯示命令行選項和多個版本庫搜索路徑間的映射
-print-multi-os-directory 顯示操作系統(tǒng)庫的相對路徑
-print-sysroot 顯示目標庫目錄
-print-sysroot-headers-suffix 顯示用于尋找頭文件的 sysroot 后綴
-Wa,<選項> 將逗號分隔的 <選項> 傳遞給匯編器
-Wp,<選項> 將逗號分隔的 <選項> 傳遞給預處理器
-Wl,<選項> 將逗號分隔的 <選項> 傳遞給鏈接器
-Xassembler <參數> 將 <參數> 傳遞給匯編器
-Xpreprocessor <參數> 將 <參數> 傳遞給預處理器
-Xlinker <參數> 將 <參數> 傳遞給鏈接器
-save-temps 不刪除中間文件
-save-temps=<arg> 不刪除中間文件
-no-canonical-prefixes 生成其他 gcc 組件的相對路徑時不生成規(guī)范化的
前綴
-pipe 使用管道代替臨時文件
-time 為每個子進程計時
-specs=<文件> 用 <文件> 的內容覆蓋內建的 specs 文件
-std=<標準> 指定輸入源文件遵循的標準
--sysroot=<目錄> 將 <目錄> 作為頭文件和庫文件的根目錄
-B <目錄> 將 <目錄> 添加到編譯器的搜索路徑中
-v 顯示編譯器調用的程序
-### 與 -v 類似,但選項被引號括住,并且不執(zhí)行命令
-E 僅作預處理,不進行編譯、匯編和鏈接
-S 編譯到匯編語言,不進行匯編和鏈接
-c 編譯、匯編到目標代碼,不進行鏈接
-o <文件> 輸出到 <文件>
-pie Create a position independent executable
-shared Create a shared library
-x <語言> 指定其后輸入文件的語言
允許的語言包括:c c++ assembler none
‘none’意味著恢復默認行為,即根據文件的擴展名猜測
源文件的語言
以 -g、-f、-m、-O、-W 或 --param 開頭的選項將由 gcc 自動傳遞給其調用的
不同子進程。若要向這些進程傳遞其他選項,必須使用 -W<字母> 選項。
報告程序缺陷的步驟請參見:
<http://bugzilla.redhat.com/bugzilla>.
八、其他知識點
1、cc、cc1、ccl、gcc
1.1、cc
- 一般的CC就是一個軟件鏈接到GCC的。只不過CC是UNIX中常用的編譯工具,而在linux中用的是GCC,有一些在UNIX中寫好的程序要放在linux中要指定命令CC編譯器,所以將CC指定為GCC。其實就是一個東西。
- 從下圖,我們可以看到,cc和gcc鏈接的庫都一致,所以說他們是一個東西的說法是正確的,但是并不是cc鏈接了gcc.
1.2、cc1
cc1是windows系統(tǒng)下提供的編譯器,安裝MinGW后,再安裝gcc可以就可以看到,作用同gcc.
linux下也有,在/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/cc1
1.3、ccl
- 很多書籍和文章都說鏈接器使用ccl,但是我們并找不到ccl的指令。(除非自己吧gcc改名為ccl例外)。
- 能找到的和編譯相關的cll,鏈接如下
a:
Compiling Directly via the CCL Compiler,從網站我們可以看到,其生成的目標文件是.ccx,而非.s文件。而從下文可以看到其名字叫做c8_compiler 。
This section describes the CCL compiler, and how to invoke it on different platforms.
The CCL compiler is a separate program, which can be invoked by the user.The name of the program is c8_compiler (or c8_compiler.exe on Microsoft Windows) //軟件的名字叫c8_compiler
b:
CCL
https://github.com/Clozure/ccl
Because CCL is written in itself, you need an already-working version of CCL to compile it.
b這個ccl,雖然和語言相關,但是并非是c語言的編譯器。
c:
https://www.researchgate.net/figure/CCL-compiler-and-interpreter_fig1_228539904
綜上所述:很多書籍中提到的ccl編譯命令是不存在的,正確的應該是cc1\cc\gcc。
1.4、gcc
GCC:全稱GNU Compiler Collection,GNU編譯器套裝,是廣泛應用的Linux系統(tǒng)的默認編譯器(特別是用于編譯Linux內核)。GCC能夠支持多種架構的處理器,跨平臺特性相對出色。不過,GNU組織要求全部的代碼由自己完成(防止版權問題),所以GCC雖然有廣泛的硬件支持,但是在各硬件平臺上卻并不是性能最優(yōu)的編譯器。
1.5、icc
ICC:全稱Intel C++ Compiler,是Intel開發(fā)的C/C++/Fortran編譯器套裝,適用于Linux、Microsoft和Mac OS X操作系統(tǒng),沒有非IA指令集版本(就是說僅供x86架構CPU使用)。ICC廣泛應用于高性能計算、分布式計算等商業(yè)計算領域,其向量化和并行化性能是業(yè)界的標桿,能夠充分發(fā)揮現代處理器的特性。
2、cpp
參考:
Oracle_手冊頁部分_用戶命令_用戶命令cpp
- 名稱
cpp - C 語言預處理程序 - 用法概要
/usr/lib/cpp [-BCHMpPRT] [
-undef] [-Dname] [
-Dname = def]
[-Idirectory] [-U
name] [-Ydirectory]
[input-file [output-file]]
- 描述
cpp 是 C 語言預處理程序。cpp 還用作其他 Sun 編譯器的第一遍操作的預處理程序。
cpp 可以接受兩個文件名作為參數。 input-file 和 output-file 分別是用于預處理程序的輸入和輸出文件。缺省情況下,它們是標準輸入和標準輸出。
3、gcc、g++
gcc和g++是什么,有什么區(qū)別?
發(fā)展至今(2020 年 6 月份),GCC 編譯器已經更新至 10.1.0 版本,其功能也由最初僅能編譯 C 語言,擴增至可以編譯多種編程語言,其中就包括 C++ 。
除此之外,當下的 GCC 編譯器還支持編譯 Go、Objective-C,Objective-C ++,Fortran,Ada,D 和 BRIG(HSAIL)等程序,甚至于 GCC 6 以及之前的版本還支持編譯 Java 程序。但本教程主要講解如何使用 GCC 編譯器編譯運行 C 和 C++ 程序,因此有關其它編程語言如何使用 GCC 編譯器編譯,將不再做具體講解。
那么,在已編輯好 C 語言或者 C++ 代碼的前提下,如何才能調用 GCC 編譯器為我們編譯程序呢?很簡單,GCC 編譯器已經為我們提供了調用它的接口,對于 C 語言或者 C++ 程序,可以通過執(zhí)行 gcc 或者 g++ 指令來調用 GCC 編譯器。
值得一提的是,實際使用中我們更習慣使用 gcc 指令編譯 C 語言程序,用 g++ 指令編譯 C++ 代碼。需要強調的一點是,這并不是 gcc 和 g++ 的區(qū)別,gcc 指令也可以用來編譯 C++ 程序,同樣 g++ 指令也可以用于編譯 C 語言程序。
那么,gcc 和 g++ 的區(qū)別是什么呢?接下來就給讀者做詳細的講解。
實際上,只要是 GCC 支持編譯的程序代碼,都可以使用 gcc 命令完成編譯??梢赃@樣理解,gcc 是 GCC 編譯器的通用編譯指令,因為根據程序文件的后綴名,gcc 指令可以自行判斷出當前程序所用編程語言的類別,比如:
- xxx.c:默認以編譯 C 語言程序的方式編譯此文件;
- xxx.cpp:默認以編譯 C++ 程序的方式編譯此文件。
- xxx.m:默認以編譯 Objective-C 程序的方式編譯此文件;
- xxx.go:默認以編譯 Go 語言程序的方式編譯此文件;
當然,gcc 指令也為用戶提供了“手動指定代表編譯方式”的接口,即使用 -x 選項。例如,gcc -xc xxx 表示以編譯 C 語言代碼的方式編譯 xxx 文件;而 gcc -xc++ xxx 則表示以編譯 C++ 代碼的方式編譯 xxx 文件。有關 -x 選項的用法,后續(xù)會給出具體樣例。
但如果使用 g++ 指令,則無論目標文件的后綴名是什么,該指令都一律按照編譯 C++ 代碼的方式編譯該文件。也就是說,對于 .c 文件來說,gcc 指令以 C 語言代碼對待,而 g++ 指令會以 C++ 代碼對待。但對于 .cpp 文件來說,gcc 和 g++ 都會以 C++ 代碼的方式編譯。
4、問chatGPT:c語言預處理、編譯、匯編、鏈接這四個過程,在linux分別由哪些實現方式
chatGPT答復如下:
在Linux系統(tǒng)上,C語言預處理、編譯、匯編和鏈接這四個過程可以使用以下工具實現:
- 預處理 - 可以使用GNU C Preprocessor (cpp)工具進行預處理。例如,在終端中輸入以下命令來生成預處理后的代碼:
cpp hello.c > hello.i
- 編譯 - 可以使用GCC(GNU Compiler Collection)或Clang等C語言編譯器進行編譯。例如,在終端中輸入以下命令將預處理后的代碼編譯成目標文件:
gcc -c hello.i -o hello.o
- 匯編 - 可以使用GNU Assembler (gas)或其他匯編器將編譯后的目標文件轉換為匯編代碼。例如,在終端中輸入以下命令將目標文件匯編成匯編代碼:
objdump -d hello.o > hello.s
- 鏈接 - 可以使用GNU Linker (ld)或其他鏈接器將多個目標文件連接成一個可執(zhí)行文件。例如,在終端中輸入以下命令將多個目標文件鏈接成可執(zhí)行文件:
gcc -o hello hello.o
請注意,不同的Linux系統(tǒng)可能會使用不同的工具和命令來進行C語言編譯和開發(fā)。以上示例僅供參考,實際操作時應根據自己的需求和情況進行調整。
5、問chatGPT:c語言預處理生成的hello.i文件,除了使用gcc可以編譯成hello.s文件,還有哪些方式可以編譯成hello.s文件
除了使用GCC編譯器,您可以使用其他C語言編譯器或預處理工具來將預處理后的"hello.i"文件編譯成匯編代碼(".s"文件)。例如:
-
Clang - Clang是另一個流行的開源C語言編譯器,與GCC類似,可以將預處理后的C源文件編譯成匯編代碼。您可以使用以下命令生成匯編代碼文件:
clang -S hello.i
-
LLVM IR - LLVM IR是一種中間表示形式,可以通過LLVM編譯器將其轉換為匯編代碼。您可以使用以下命令將預處理后的C源文件編譯為LLVM IR:
clang -emit-llvm -S hello.i
然后,您可以使用以下命令將LLVM IR編譯為匯編代碼:
llc -march=x86-64 hello.ll -o hello.s
-
CPP - 除了使用C語言編譯器,您還可以使用預處理工具CPP將預處理后的C源文件轉換為匯編代碼。您可以使用以下命令生成匯編代碼文件:
cpp hello.i > hello.cpp
g++ -S hello.cpp
請注意,不同的編譯器和工具可能會產生不同的匯編代碼,因此生成的".s"文件可能會有所不同。文章來源:http://www.zghlxwxcb.cn/news/detail-466847.html
參考
1、《程序員的自我修養(yǎng)鏈接裝載與庫》
2、深入理解計算機系統(tǒng)第七章 鏈接知識整理
3、C編譯器翻譯c程序的四個階段
4、程序員C語言快速上手——工程篇(十二)
5、GCC命令 (預處理cpp --> 編譯ccl --> 匯編as --> 鏈接ld)(轉載)
6、GCC\LD文章來源地址http://www.zghlxwxcb.cn/news/detail-466847.html
到了這里,關于【編譯、鏈接、裝載一】預處理、編譯、匯編、鏈接的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!