程序编译(1)-目标文件的编译步骤

因为工作需要,最近需要搭建跨平台工程。其中涉及到了依赖库交叉编译等工作。
因此趁这个机会写一个关于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'

我们可以看到,在整个编译过程中使用到了cc1ascollect2工具来完成预处理(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

我们对比一下可以很容易发现一些变化:

  1. printf符号变成了printf@GLIBC_2.4且还是未定义:此符号gcc在链接时根据当前版本修改符号,使得在运行程序而动态链接时不会链接到其他gcc版本的printf
  2. 很多符号的地址都被修正了,符号func符号地址从00000000被修正为00010408:单独模块(*.o或者*.obj)的编译时编译器并不知道func的地址
  3. 符号增多:目标文件和可执行文件elf文件格式存在差异,例如增加了.dynamic段相关信息,即动态链接信息
  4. 其他(有时间再研究研究)

当前演示的链接过程并不是静态链接。如果需要进行静态链接则需要加上-static参数。静态链接比较简单,说白了就是递归的将所有引用到的符号归档到一个文件中,因此静态链接后的文件都会大上许多~经常与静态链接一起提及的就是动态链接。动态链接的链接时期是程序运行时,当前链接环节可以理解为为动态链接做准备。

静态链接和动态链接最主要的区别:两者的链接时期不一致,静态链接在程序编译链接时期,动态链接则是程序运行时

这里可以说内容比较多,现在只是简单提一嘴,后续会有专门的文章来聊聊这两者具体的差异以及各自的机制~

最后总结一下链接过程:

  • 链接器就是在链接的时候自动在所提供的依赖库或者目标文件(*.o或者*.obj)中搜索被引用的外部符号

  • 找到之后会将绝对地址指令重新修正,使其指向正确的地址。

  • 修正的过程被称之为重定位,被修正的地址入口称之为重定位入口

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容