因为工作需要,最近需要搭建跨平台工程。其中涉及到了依赖库交叉编译等工作。 因此趁这个机会写一个关于c/c++编译器的工作机制的小系列,
前言 广义上的”编译”指的是由代码、模块、资源等构建成机器码的过程。狭义上的”编译”则指的是源代码到汇编代码的过程。而标题中的”编译”则是广义上的。那为什么需要了解其中的原理呢?
了解原理可以让我们解决编译过程中遇到的任何问题都可以快速定位和解决
基本过程
c/c++广义上的编译都需要经过以下这4步:预处理(Prepressing)->编译(Compilation)->汇编(Assembly)->链接(Linking)
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #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查看,结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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完成链接过程。
预处理 gcc -E test.c -o test.i,参数-E表示只进行预处理,不进行后续操作,生成test.i文件。效果等同于使用预处理器cpp
预处理后的结果如下,由于文本过长,只粘贴关键部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 1 "test.c" # 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 ); return 0 ; }
预编译规则如下:
替换所有宏定义
处理所有条件预编译指令,#if、#elif、#endif等等
递归展开所有用到的#include头文件包含指令
删除所有注释
添加行号以及文件标识,便于报错提示以及生成调试信息
编译 gcc -S test.i -o test.s,参数-S表示只进行预处理、编译(狭义上)并生成汇编代码,当然可以用test.c生成汇编代码,使用test.i是因为其为预编译的产物,方便流程讲解。
编译过程主要包括这几个过程词法分析、语法分析、语义分析、优化代码 。以下我只做总结概括,详情见:《程序员的自我修养-链接、装载与库》-2.2章节:
词法分析 由扫描器扫描源代码,将关键字、标识符、字面量、操作符 进行归纳和分类,并存储到表中,供语法分析 环节使用。
语法分析 把扫描器产生的记号生成以表达式 为节点的语法树 ,整个分析过程采用了上下文无关语法(Context-free Grammar) (感兴趣可以深入了解,工作中基本用不上)。通过了语法分析 并不代表代码过关了,此过程只是确认最小表达式是否符合语法。
此图引用至《程序员的自我修养-链接、装载与库》,侵删~
程序员的自我修养-链接、装载与库-语法树
语义分析 只进行语法分析是远远不够的,好比每个词语都没问题,但是不按照语法进行有意义的组合拿别人就无法理解。编译器也一样,例如两个指针的乘法运算是无意义的。
语义分析分为静态语义 和动态语义 。静态语义是指编译期可以确定的,动态语义指的是在运行时候才能确定的语义。
代码优化 编译器通过分析源代码,识别出其中可以进行优化的部分, 并进行调整以改善程序性能 ,常见的优化例如常量传播、常量折叠 ,在c++中的有返回值优化(RVO) 等,当然编译器所作的优化是有限的~
汇编 gcc test.s -o test.o表示将汇编代码转化为机器码,也等同于gcc -c test.c -o test.o
链接 链接过程本质上就是将引用的外部符号进行地址修正的过程~`test.c中并没有实现printf函数。我们用[nm](https://www.man7.org/linux/man-pages/man1/nm.1.html)命令来看看test.o的符号列表,nm -a test.o`
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
1 /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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 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)中搜索被引用的外部符号
找到之后会将绝对地址指令重新修正,使其指向正确的地址。
修正的过程被称之为重定位 ,被修正的地址入口称之为重定位入口