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步骤 | 命令 | 等价命令 | 输出文件 | 预处理 | cpp | gcc -E | .i, .ii | 编译 | cc1, cc1plus | gcc -S | .s | 汇编 | as | gcc -c | .o, .obj | 链接 | ld | gcc | 可执行文件 | 编译步骤详解以一个很小的示例分步骤演示上述编译过程,程序代码结构如下,共有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是如何体现的?
- 编译优化作用在哪一步?
参考链接
|