因为工作需要,最近需要搭建跨平台工程。其中涉及到了依赖库交叉编译等工作。
因此趁这个机会写一个关于c/c++编译器的工作机制的小系列,
前言
广义上的"编译"指的是由代码、模块、资源等构建成机器码的过程。狭义上的"编译"则指的是源代码到汇编代码的过程。而标题中的"编译"则是广义上的。那为什么需要了解其中的原理呢?
了解原理可以让我们解决编译过程中遇到的任何问题都可以快速定位和解决
基本过程
c/c++广义上的编译都需要经过以下这4步:预处理(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;
}
整个编译过程可以通过gcc -v test.c -o test.out
查看,结果如下:
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'
我们可以看到,在整个编译过程中使用到了cc1
、as
、collect2
工具来完成预处理(Prepressing)->编译(Compilation)->汇编(Assembly)->链接(Linking)整个过程的。由此可知gcc是通过间接调用各种程序来完成编译(广义)过程,其中cc1
完成了预处理和编译过程,as
完成汇编过程,collect2
完成链接过程。
-
cc1
:也是通过间接调用cpp
(C Pre-Processor)c预处理器进行预处理,自身进行编译 -
as
:汇编器,将汇编代码转化为机器码 -
collect2
:实际上的链接器是ld
,gcc是通过调用collect2
来间接调用ld
进而进行链接的,感兴趣可以阅读[collect2](collect2 | 懒惰的程序员 (wanglianghome.org))这边文章
<center class="half">
<img src="https://jesonblogbucket.oss-cn-shenzhen.aliyuncs.com/编译基本流程.png" width="800"/>
</center>
预处理
gcc -E test.c -o test.i
,参数-E
表示只进行预处理,不进行后续操作,生成test.i
文件。效果等同于使用预处理器cpp
预处理后的结果如下,由于文本过长,只粘贴关键部分
# 1 "test.c"
/*中间是头文件stdio.h递归展开后的结果*/
# 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;
}
预编译规则如下:
- 替换所有宏定义
- 处理所有条件预编译指令,#if、#elif、#endif等等
- 递归展开所有用到的#include头文件包含指令
- 删除所有注释
- 添加行号以及文件标识,便于报错提示以及生成调试信息
编译
gcc -S test.i -o test.s
,参数-S
表示只进行预处理、编译(狭义上)并生成汇编代码,当然可以用test.c
生成汇编代码,使用test.i
是因为其为预编译的产物,方便流程讲解。
编译过程主要包括这几个过程词法分析、语法分析、语义分析、优化代码。以下我只做总结概括,详情见:《程序员的自我修养-链接、装载与库》-2.2章节:
词法分析
由扫描器扫描源代码,将关键字、标识符、字面量、操作符进行归纳和分类,并存储到表中,供语法分析环节使用。
语法分析
把扫描器产生的记号生成以表达式为节点的语法树,整个分析过程采用了上下文无关语法(Context-free Grammar)(感兴趣可以深入了解,工作中基本用不上)。通过了语法分析并不代表代码过关了,此过程只是确认最小表达式是否符合语法。
此图引用至《程序员的自我修养-链接、装载与库》,侵删~
<center class="half">
<img src="https://jesonblogbucket.oss-cn-shenzhen.aliyuncs.com/链接与库-语法树.jpg" width="800"/>
程序员的自我修养-链接、装载与库-语法树
</center>
语义分析
只进行语法分析是远远不够的,好比每个词语都没问题,但是不按照语法进行有意义的组合拿别人就无法理解。编译器也一样,例如两个指针的乘法运算是无意义的。
语义分析分为静态语义和动态语义。静态语义是指编译期可以确定的,动态语义指的是在运行时候才能确定的语义。
代码优化
编译器通过分析源代码,识别出其中可以进行优化的部分, 并进行调整以改善程序性能,常见的优化例如常量传播、常量折叠,在c++中的有返回值优化(RVO)等,当然编译器所作的优化是有限的~
汇编
gcc test.s -o test.o
表示将汇编代码转化为机器码,也等同于gcc -c test.c -o test.o
链接
链接过程本质上就是将引用的外部符号进行地址修正的过程~test.c
中并没有实现printf
函数。我们用nm命令来看看test.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
符号的状态是U,表示符号在当前文件中是未定义的。然后执行下面命令生成test.out
:
注意:下面命令只是用于与我拥有相同环境,下面命令只是我在collect2
命令基础上将参数/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
符号,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
我们对比一下可以很容易发现一些变化:
-
printf
符号变成了printf@GLIBC_2.4
且还是未定义:此符号gcc在链接时根据当前版本修改符号,使得在运行程序而动态链接时不会链接到其他gcc版本的printf
-
很多符号的地址都被修正了,符号
func
符号地址从00000000被修正为00010408:单独模块(*.o或者*.obj)的编译时编译器并不知道func
的地址 -
符号增多:目标文件和可执行文件elf文件格式存在差异,例如增加了
.dynamic
段相关信息,即动态链接信息 - 其他(有时间再研究研究)
当前演示的链接过程并不是静态链接。如果需要进行静态链接则需要加上-static
参数。静态链接比较简单,说白了就是递归的将所有引用到的符号归档到一个文件中,因此静态链接后的文件都会大上许多~经常与静态链接一起提及的就是动态链接。动态链接的链接时期是程序运行时,当前链接环节可以理解为为动态链接做准备。
静态链接和动态链接最主要的区别:两者的链接时期不一致,静态链接在程序编译链接时期,动态链接则是程序运行时
这里可以说内容比较多,现在只是简单提一嘴,后续会有专门的文章来聊聊这两者具体的差异以及各自的机制~
最后总结一下链接过程:
链接器就是在链接的时候自动在所提供的依赖库或者目标文件(*.o或者*.obj)中搜索被引用的外部符号
找到之后会将绝对地址指令重新修正,使其指向正确的地址。
修正的过程被称之为重定位,被修正的地址入口称之为重定位入口