Discuz! Board

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 889|回复: 0
打印 上一主题 下一主题

C/C++程序编译过程为什么要分为四个步骤?

[复制链接]

1272

主题

2067

帖子

7962

积分

认证用户组

Rank: 5Rank: 5

积分
7962
跳转到指定楼层
楼主
发表于 2023-11-18 17:29:15 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
https://zhuanlan.zhihu.com/p/549996872

本文以一个简单的C++程序示例,演示C++程序的详细编译过程。一条简单的gcc编译命令背后包含了四个步骤:
  • 预处理(Preprocessing):主要用于处理#开头的代码行,比如对宏做展开,对include的文件做展开,条件编译选项判断,清理注释等。文件以.i和.ii结尾。
  • 编译(Compilation):使用预处理的输出结果作为输入,生成文本格式的平台相关的汇编代码(assembly code)。文件以.s结尾。
  • 汇编(Assemble):将上一步的汇编代码转换成二进制的机器码,称为object code。产生的文件叫做目标文件,是二进制格式,文件以.o或.obj结尾。
  • 链接(Linking):链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)

图片来源:https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html
步骤命令等价命令输出文件
预处理cppgcc -E.i, .ii
编译cc1, cc1plusgcc -S.s
汇编asgcc -c.o, .obj
链接ldgcc可执行文件
编译步骤详解
以一个很小的示例分步骤演示上述编译过程,程序代码结构如下,共有3个文件,其中main.cpp依赖my_math.h:

程序目录结构
程序内容如下:

程序内容步骤1:预处理 Preprocessing
主要用于处理#开头的代码行,比如对宏做展开,对include的文件做展开,条件编译选项判断,清理注释等。文件以.i和.ii结尾。
命令:
cpp main.cpp -o main.icpp my_math.cpp -o my_math.i
预处理后生成的main.i文件大致如下,可以看到短短10行代码经过预处理之后变成了899行,这根代码展开有直接关系:
步骤2:编译 Compilation
使用预处理的输出结果作为输入,生成平台相关的汇编代码(assembly code)。结果是文本格式,文件以.s结尾。
命令:
g++ -S main.i -o main.sg++ -S my_math.i -o my_math.s
或者直接使用cc1(用于C代码)或cc1plus(用于C++代码),因为我们编写的是C++代码,故使用cc1plus:
/usr/local/libexec/gcc/x86_64-linux-gnu/12.1.0/cc1plus main.i -o main.s/usr/local/libexec/gcc/x86_64-linux-gnu/12.1.0/cc1plus my_math.i -o my_math.s
注意cc1plus没有在bash默认的搜索路径中。
编译后生成的main.s文件大致如下,40行汇编代码:
步骤3:汇编 Assemble
将上一步的汇编代码转换成二进制的机器码,称为object code。产生的文件叫做目标文件,是二进制格式,文件以.o或.obj结尾。
命令:
as main.s -o main.oas my_math.s -o my_math.o
编译后生成main.o,这是个二进制文件,file命令查看文件属性:
$ file main.o main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
步骤4:链接 Linking
链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。这是生成可执行文件的最后一步,是把众多积木碎片搭在一起的步骤,所用命令是ld。
命令:
ld -plugin /usr/local/libexec/gcc/x86_64-linux-gnu/12.1.0/liblto_plugin.so -plugin-opt=/usr/local/libexec/gcc/x86_64-linux-gnu/12.1.0/lto-wrapper -plugin-opt=-fresolution=/tmp/ccIJ5Caz.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --eh-frame-hdr -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/local/lib/gcc/x86_64-linux-gnu/12.1.0/crtbegin.o -L/usr/local/lib/gcc/x86_64-linux-gnu/12.1.0 -L/usr/local/lib/gcc/x86_64-linux-gnu/12.1.0/../../../../lib64 -L/lib/x86_64-linux-gnu -L/lib/../lib64 -L/usr/lib/x86_64-linux-gnu -L/usr/local/lib/gcc/x86_64-linux-gnu/12.1.0/../../.. -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/local/lib/gcc/x86_64-linux-gnu/12.1.0/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o main.o my_math.o -o main
上述命令明显比较长,是因为ld参数中跟了很多库文件及其路径,这些库文件是生成最终可执行文件所必须的。
如果上述命令没有报错,那么恭喜你大功告成,可执行文件已经生成,当前目录执行./main可看到结果:
$ ./mainsum=30
说明:如果仅仅会用 ld main.o my_math.o 则会报错,可通过给gcc加入 -v参数查看详细编译步骤:

ld命令不全时的报错All in One
明白了上述编译的4个步骤,今后再敲gcc编译命令的的时候,内心是不是更加清晰了一些呢。
$ g++ main.cpp my_math.cpp -o main # 四合一编译命令。$ ./main # 执行
总结
经过以上分析,我们发现C/C++程序编译过程并不像想象的那么简单,而是要经过预处理、编译、汇编、链接是个步骤。尽管我们平时使用gcc命令的时候没有关心中间结果,但每次程序的编译都少不了这几个步骤,如果你关心gcc的底层行为,可通过给gcc加入 -v参数查看详细编译步骤。
需要注意的是每个.c和.cc文件是分别单独的做预处理、编译、汇编这三个步骤,然后所有目标文件链接成一个可执行文件。
读过本文我们遗留以下问题,今后文章逐一解答:
  • 链接过程到底做了什么?
  • 什么是动态链接和静态链接?
  • main函数之前和之后做了什么?
  • c++的namespace是如何体现的?
  • 编译优化作用在哪一步?
参考链接
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|firemail ( 粤ICP备15085507号-1 )

GMT+8, 2024-11-25 23:35 , Processed in 0.062125 second(s), 19 queries .

Powered by Discuz! X3

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表