因?yàn)楣ぷ餍枰罱枰罱缙脚_(tái)工程。其中涉及到了依賴庫(kù)交叉編譯等工作撵枢。
因此趁這個(gè)機(jī)會(huì)寫一個(gè)關(guān)于c/c++編譯器的工作機(jī)制的小系列吠架,
前言
廣義上的"編譯"指的是由代碼芙贫、模塊、資源等構(gòu)建成機(jī)器碼的過(guò)程傍药。狹義上的"編譯"則指的是源代碼到匯編代碼的過(guò)程磺平。而標(biāo)題中的"編譯"則是廣義上的。那為什么需要了解其中的原理呢拐辽?
了解原理可以讓我們解決編譯過(guò)程中遇到的任何問(wèn)題都可以快速定位和解決
基本過(guò)程
c/c++廣義上的編譯都需要經(jīng)過(guò)以下這4步:預(yù)處理(Prepressing)->編譯(Compilation)->匯編(Assembly)->鏈接(Linking)
示例代碼如下:
#include <stdio.h>
#define DEF_VAR 100
static int kStaticInitVar = 10;
static int kStaticUnnitVar;
void func(int var)
{
printf("%s-var:%d\n",__FUNCTION__, var);
}
int main()
{
static int localStaticInitVar = 10;
static int localStaticUnintVar;
func(kStaticInitVar + localStaticInitVar + DEF_VAR);
return 0;
}
整個(gè)編譯過(guò)程可以通過(guò)gcc -v test.c -o test.out
查看拣挪,結(jié)果如下:
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/arm-linux-gnueabihf/10/lto-wrapper
Target: arm-linux-gnueabihf
Configured with: ../src/configure -v --with-pkgversion='Raspbian 10.2.1-6+rpi1' --with-bugurl=file:///usr/share/doc/gcc-10/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-10 --program-prefix=arm-linux-gnueabihf- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libitm --disable-libquadmath --disable-libquadmath-support --enable-plugin --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-sjlj-exceptions --with-arch=armv6 --with-fpu=vfp --with-float=hard --disable-werror --enable-checking=release --build=arm-linux-gnueabihf --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 10.2.1 20210110 (Raspbian 10.2.1-6+rpi1)
COLLECT_GCC_OPTIONS='-v' '-o' 'test.out' '-mfloat-abi=hard' '-mfpu=vfp' '-mtls-dialect=gnu' '-marm' '-march=armv6+fp'
/usr/lib/gcc/arm-linux-gnueabihf/10/cc1 -quiet -v -imultilib . -imultiarch arm-linux-gnueabihf test.c -quiet -dumpbase test.c -mfloat-abi=hard -mfpu=vfp -mtls-dialect=gnu -marm -march=armv6+fp -auxbase test -version -o /tmp/ccr8xe6E.s
GNU C17 (Raspbian 10.2.1-6+rpi1) version 10.2.1 20210110 (arm-linux-gnueabihf)
compiled by GNU C version 10.2.1 20210110, GMP version 6.2.1, MPFR version 4.1.0, MPC version 1.2.0, isl version isl-0.23-GMP
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/arm-linux-gnueabihf"
ignoring nonexistent directory "/usr/lib/gcc/arm-linux-gnueabihf/10/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/arm-linux-gnueabihf/10/../../../../arm-linux-gnueabihf/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/arm-linux-gnueabihf/10/include
/usr/local/include
/usr/include/arm-linux-gnueabihf
/usr/include
End of search list.
GNU C17 (Raspbian 10.2.1-6+rpi1) version 10.2.1 20210110 (arm-linux-gnueabihf)
compiled by GNU C version 10.2.1 20210110, GMP version 6.2.1, MPFR version 4.1.0, MPC version 1.2.0, isl version isl-0.23-GMP
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: b0c2f0ffcfbe7fc710aaf45c31c63944
COLLECT_GCC_OPTIONS='-v' '-o' 'test.out' '-mfloat-abi=hard' '-mfpu=vfp' '-mtls-dialect=gnu' '-marm' '-march=armv6+fp'
as -v -march=armv6 -mfloat-abi=hard -mfpu=vfp -meabi=5 -o /tmp/ccT9vuDF.o /tmp/ccr8xe6E.s
GNU assembler version 2.35.2 (arm-linux-gnueabihf) using BFD version (GNU Binutils for Raspbian) 2.35.2
COMPILER_PATH=/usr/lib/gcc/arm-linux-gnueabihf/10/:/usr/lib/gcc/arm-linux-gnueabihf/10/:/usr/lib/gcc/arm-linux-gnueabihf/:/usr/lib/gcc/arm-linux-gnueabihf/10/:/usr/lib/gcc/arm-linux-gnueabihf/
LIBRARY_PATH=/usr/lib/gcc/arm-linux-gnueabihf/10/:/usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/:/usr/lib/gcc/arm-linux-gnueabihf/10/../../../:/lib/arm-linux-gnueabihf/:/lib/:/usr/lib/arm-linux-gnueabihf/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'test.out' '-mfloat-abi=hard' '-mfpu=vfp' '-mtls-dialect=gnu' '-marm' '-march=armv6+fp'
/usr/lib/gcc/arm-linux-gnueabihf/10/collect2 -plugin /usr/lib/gcc/arm-linux-gnueabihf/10/liblto_plugin.so -plugin-opt=/usr/lib/gcc/arm-linux-gnueabihf/10/lto-wrapper -plugin-opt=-fresolution=/tmp/ccvxcalC.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -dynamic-linker /lib/ld-linux-armhf.so.3 -X --hash-style=gnu --as-needed -m armelf_linux_eabi -o test.out /usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/crt1.o /usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/crti.o /usr/lib/gcc/arm-linux-gnueabihf/10/crtbegin.o -L/usr/lib/gcc/arm-linux-gnueabihf/10 -L/usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf -L/usr/lib/gcc/arm-linux-gnueabihf/10/../../.. -L/lib/arm-linux-gnueabihf -L/usr/lib/arm-linux-gnueabihf /tmp/ccT9vuDF.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/arm-linux-gnueabihf/10/crtend.o /usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/crtn.o
COLLECT_GCC_OPTIONS='-v' '-o' 'test.out' '-mfloat-abi=hard' '-mfpu=vfp' '-mtls-dialect=gnu' '-marm' '-march=armv6+fp'
我們可以看到,在整個(gè)編譯過(guò)程中使用到了cc1
俱诸、as
菠劝、collect2
工具來(lái)完成預(yù)處理(Prepressing)->編譯(Compilation)->匯編(Assembly)->鏈接(Linking)整個(gè)過(guò)程的。由此可知gcc是通過(guò)間接調(diào)用各種程序來(lái)完成編譯(廣義)過(guò)程睁搭,其中cc1
完成了預(yù)處理和編譯過(guò)程赶诊,as
完成匯編過(guò)程,collect2
完成鏈接過(guò)程介袜。
-
cc1
:也是通過(guò)間接調(diào)用cpp
(C Pre-Processor)c預(yù)處理器進(jìn)行預(yù)處理甫何,自身進(jìn)行編譯 -
as
:匯編器,將匯編代碼轉(zhuǎn)化為機(jī)器碼 -
collect2
:實(shí)際上的鏈接器是ld
遇伞,gcc是通過(guò)調(diào)用collect2
來(lái)間接調(diào)用ld
進(jìn)而進(jìn)行鏈接的辙喂,感興趣可以閱讀[collect2](collect2 | 懶惰的程序員 (wanglianghome.org))這邊文章
<center class="half">
<img src="https://jesonblogbucket.oss-cn-shenzhen.aliyuncs.com/編譯基本流程.png" width="800"/>
</center>
預(yù)處理
gcc -E test.c -o test.i
,參數(shù)-E
表示只進(jìn)行預(yù)處理鸠珠,不進(jìn)行后續(xù)操作巍耗,生成test.i
文件。效果等同于使用預(yù)處理器cpp
預(yù)處理后的結(jié)果如下渐排,由于文本過(guò)長(zhǎng)炬太,只粘貼關(guān)鍵部分
# 1 "test.c"
/*中間是頭文件stdio.h遞歸展開(kāi)后的結(jié)果*/
# 5 "test.c"
static int kStaticInitVar = 10;
static int kStaticUnnitVar;
void func(int var)
{
printf("%s-var:%d\n",__FUNCTION__, var);
}
int main()
{
static int localStaticInitVar = 10;
static int localStaticUnintVar;
func(kStaticInitVar + localStaticInitVar + 100); // DEF_VAR宏被替換
return 0;
}
預(yù)編譯規(guī)則如下:
- 替換所有宏定義
- 處理所有條件預(yù)編譯指令,#if驯耻、#elif亲族、#endif等等
- 遞歸展開(kāi)所有用到的#include頭文件包含指令
- 刪除所有注釋
- 添加行號(hào)以及文件標(biāo)識(shí)炒考,便于報(bào)錯(cuò)提示以及生成調(diào)試信息
編譯
gcc -S test.i -o test.s
,參數(shù)-S
表示只進(jìn)行預(yù)處理霎迫、編譯(狹義上)并生成匯編代碼斋枢,當(dāng)然可以用test.c
生成匯編代碼,使用test.i
是因?yàn)槠錇轭A(yù)編譯的產(chǎn)物知给,方便流程講解瓤帚。
編譯過(guò)程主要包括這幾個(gè)過(guò)程詞法分析、語(yǔ)法分析涩赢、語(yǔ)義分析戈次、優(yōu)化代碼。以下我只做總結(jié)概括筒扒,詳情見(jiàn):《程序員的自我修養(yǎng)-鏈接怯邪、裝載與庫(kù)》-2.2章節(jié):
詞法分析
由掃描器掃描源代碼,將關(guān)鍵字霎肯、標(biāo)識(shí)符擎颖、字面量榛斯、操作符進(jìn)行歸納和分類观游,并存儲(chǔ)到表中,供語(yǔ)法分析環(huán)節(jié)使用驮俗。
語(yǔ)法分析
把掃描器產(chǎn)生的記號(hào)生成以表達(dá)式為節(jié)點(diǎn)的語(yǔ)法樹(shù)懂缕,整個(gè)分析過(guò)程采用了上下文無(wú)關(guān)語(yǔ)法(Context-free Grammar)(感興趣可以深入了解,工作中基本用不上)王凑。通過(guò)了語(yǔ)法分析并不代表代碼過(guò)關(guān)了搪柑,此過(guò)程只是確認(rèn)最小表達(dá)式是否符合語(yǔ)法。
此圖引用至《程序員的自我修養(yǎng)-鏈接索烹、裝載與庫(kù)》工碾,侵刪~
<center class="half">
<img src="https://jesonblogbucket.oss-cn-shenzhen.aliyuncs.com/鏈接與庫(kù)-語(yǔ)法樹(shù).jpg" width="800"/>
程序員的自我修養(yǎng)-鏈接、裝載與庫(kù)-語(yǔ)法樹(shù)
</center>
語(yǔ)義分析
只進(jìn)行語(yǔ)法分析是遠(yuǎn)遠(yuǎn)不夠的百姓,好比每個(gè)詞語(yǔ)都沒(méi)問(wèn)題渊额,但是不按照語(yǔ)法進(jìn)行有意義的組合拿別人就無(wú)法理解。編譯器也一樣垒拢,例如兩個(gè)指針的乘法運(yùn)算是無(wú)意義的旬迹。
語(yǔ)義分析分為靜態(tài)語(yǔ)義和動(dòng)態(tài)語(yǔ)義。靜態(tài)語(yǔ)義是指編譯期可以確定的求类,動(dòng)態(tài)語(yǔ)義指的是在運(yùn)行時(shí)候才能確定的語(yǔ)義奔垦。
代碼優(yōu)化
編譯器通過(guò)分析源代碼,識(shí)別出其中可以進(jìn)行優(yōu)化的部分, 并進(jìn)行調(diào)整以改善程序性能尸疆,常見(jiàn)的優(yōu)化例如常量傳播椿猎、常量折疊惶岭,在c++中的有返回值優(yōu)化(RVO)等,當(dāng)然編譯器所作的優(yōu)化是有限的~
匯編
gcc test.s -o test.o
表示將匯編代碼轉(zhuǎn)化為機(jī)器碼犯眠,也等同于gcc -c test.c -o test.o
鏈接
鏈接過(guò)程本質(zhì)上就是將引用的外部符號(hào)進(jìn)行地址修正的過(guò)程~test.c
中并沒(méi)有實(shí)現(xiàn)printf
函數(shù)俗他。我們用nm命令來(lái)看看test.o
的符號(hào)列表,nm -a test.o
00000000 n .ARM.attributes
00000000 b .bss
00000000 n .comment
00000000 d .data
00000000 T func
0000000c r __FUNCTION__.2
00000000 d kStaticInitVar
00000000 b kStaticUnnitVar
00000004 d localStaticInitVar.1
00000004 b localStaticUnintVar.0
00000034 T main
00000000 n .note.GNU-stack
U printf
00000000 r .rodata
00000000 a test.c
00000000 t .text
其中printf
符號(hào)的狀態(tài)是U阔逼,表示符號(hào)在當(dāng)前文件中是未定義的兆衅。然后執(zhí)行下面命令生成test.out
:
注意:下面命令只是用于與我擁有相同環(huán)境,下面命令只是我在collect2
命令基礎(chǔ)上將參數(shù)/tmp/ccT9vuDF.o
替換為test.o
/usr/lib/gcc/arm-linux-gnueabihf/10/collect2 -plugin /usr/lib/gcc/arm-linux-gnueabihf/10/liblto_plugin.so -plugin-opt=/usr/lib/gcc/arm-linux-gnueabihf/10/lto-wrapper -plugin-opt=-fresolution=/tmp/ccvxcalC.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -dynamic-linker /lib/ld-linux-armhf.so.3 -X --hash-style=gnu --as-needed -m armelf_linux_eabi -o test.out /usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/crt1.o /usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/crti.o /usr/lib/gcc/arm-linux-gnueabihf/10/crtbegin.o -L/usr/lib/gcc/arm-linux-gnueabihf/10 -L/usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf -L/usr/lib/gcc/arm-linux-gnueabihf/10/../../.. -L/lib/arm-linux-gnueabihf -L/usr/lib/arm-linux-gnueabihf test.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/arm-linux-gnueabihf/10/crtend.o /usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/crtn.o
查看test.out
符號(hào)嗜浮,nm -a test.out
00000000 a
U abort@GLIBC_2.4
000104e8 r all_implied_fbits
0001058c r all_implied_fbits
00000000 n .ARM.attributes
0001061c r .ARM.exidx
00021030 b .bss
0002103c B __bss_end__
0002103c B _bss_end__
00021030 B __bss_start
00021030 B __bss_start__
00010354 t call_weak_fn
00000000 n .comment
00021030 b completed.0
00000000 a crtstuff.c
00000000 a crtstuff.c
00021020 d .data
00021020 D __data_start
00021020 W data_start
00010378 t deregister_tm_clones
000103dc t __do_global_dtors_aux
00020f14 d __do_global_dtors_aux_fini_array_entry
00021024 D __dso_handle
00020f18 d .dynamic
00020f18 d _DYNAMIC
00010230 r .dynstr
000101e0 r .dynsym
00021030 D _edata
00010624 r .eh_frame
00000000 a elf-init.oS
0002103c B __end__
0002103c B _end
000104dc t .fini
000104dc T _fini
00020f14 d .fini_array
00010404 t frame_dummy
00020f10 d __frame_dummy_init_array_entry
00010624 r __FRAME_END__
00010408 T func
00010584 r __FUNCTION__.2
00021000 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
000101b4 r .gnu.hash
00010274 r .gnu.version
00010280 r .gnu.version_r
00021000 d .got
000102c8 t .init
000102c8 T _init
00020f10 d .init_array
00020f14 d __init_array_end
00020f10 d __init_array_start
00010154 r .interp
000104e4 R _IO_stdin_used
00021028 d kStaticInitVar
00021034 b kStaticUnnitVar
000104d8 T __libc_csu_fini
00010478 T __libc_csu_init
U __libc_start_main@GLIBC_2.4
0002102c d localStaticInitVar.1
00021038 b localStaticUnintVar.0
0001043c T main
00010194 r .note.ABI-tag
00010170 r .note.gnu.build-id
000102d4 t .plt
U printf@GLIBC_2.4
000103a4 t register_tm_clones
000102a0 r .rel.dyn
000102a8 r .rel.plt
000104e4 r .rodata
00010318 T _start
00000000 a test.c
00010318 t .text
00021030 D __TMC_END__
00000000 a /usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/crt1.o
00000000 a /usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/crti.o
00000000 a /usr/lib/gcc/arm-linux-gnueabihf/10/../../../arm-linux-gnueabihf/crtn.o
我們對(duì)比一下可以很容易發(fā)現(xiàn)一些變化:
-
printf
符號(hào)變成了printf@GLIBC_2.4
且還是未定義:此符號(hào)gcc在鏈接時(shí)根據(jù)當(dāng)前版本修改符號(hào)羡亩,使得在運(yùn)行程序而動(dòng)態(tài)鏈接時(shí)不會(huì)鏈接到其他gcc版本的printf
-
很多符號(hào)的地址都被修正了,符號(hào)
func
符號(hào)地址從00000000被修正為00010408:?jiǎn)为?dú)模塊(*.o或者*.obj)的編譯時(shí)編譯器并不知道func
的地址 -
符號(hào)增多:目標(biāo)文件和可執(zhí)行文件elf文件格式存在差異危融,例如增加了
.dynamic
段相關(guān)信息畏铆,即動(dòng)態(tài)鏈接信息 - 其他(有時(shí)間再研究研究)
當(dāng)前演示的鏈接過(guò)程并不是靜態(tài)鏈接。如果需要進(jìn)行靜態(tài)鏈接則需要加上-static
參數(shù)吉殃。靜態(tài)鏈接比較簡(jiǎn)單辞居,說(shuō)白了就是遞歸的將所有引用到的符號(hào)歸檔到一個(gè)文件中,因此靜態(tài)鏈接后的文件都會(huì)大上許多~經(jīng)常與靜態(tài)鏈接一起提及的就是動(dòng)態(tài)鏈接蛋勺。動(dòng)態(tài)鏈接的鏈接時(shí)期是程序運(yùn)行時(shí)瓦灶,當(dāng)前鏈接環(huán)節(jié)可以理解為為動(dòng)態(tài)鏈接做準(zhǔn)備。
靜態(tài)鏈接和動(dòng)態(tài)鏈接最主要的區(qū)別:兩者的鏈接時(shí)期不一致抱完,靜態(tài)鏈接在程序編譯鏈接時(shí)期贼陶,動(dòng)態(tài)鏈接則是程序運(yùn)行時(shí)
這里可以說(shuō)內(nèi)容比較多,現(xiàn)在只是簡(jiǎn)單提一嘴巧娱,后續(xù)會(huì)有專門的文章來(lái)聊聊這兩者具體的差異以及各自的機(jī)制~
最后總結(jié)一下鏈接過(guò)程:
鏈接器就是在鏈接的時(shí)候自動(dòng)在所提供的依賴庫(kù)或者目標(biāo)文件(*.o或者*.obj)中搜索被引用的外部符號(hào)
找到之后會(huì)將絕對(duì)地址指令重新修正碉怔,使其指向正確的地址。
修正的過(guò)程被稱之為重定位禁添,被修正的地址入口稱之為重定位入口