对于平常的应用程序开发,我们很少去关注编译和链接过程,通常开发环境都是集成开发环境IDE做了。IDE一般都将编译和链接过程一步完成,通常将这种编译和链接并到一起的过程成为构建(build) 即使使用命令行编译,基本上也是一条命令,优化了其他的过程。那么,被隐藏的过程到底有哪些呢?
被隐藏的过程
一个 gcc 的编译过程大致如上图。可以分为以下几步:
- 预编译
- 编译
- 汇编
- 链接
那么,每个阶段具体又做了哪些工作呢?
预编译
预编译过程主要处理哪些源代码文件中以“#”开始的预编译指令。比如
“#include”、"#define"等,主要处理规则如下:
- 将所有的“#define”删除,并且展开所有的宏定义。
- 处理所有条件预编译指令,比如“#if”、“ifdef”、“#elif”、
- “#else”、"#endif"等。
- 处理 "#include" 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。头文件包含不能滥用,特别是交叉包含
- 删除所有的注释 "//" 和 "/**/"
- 添加行号和文件名标识,比如#2 "hello.c" 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能显示行号。__LINE__ 这个宏 ,在这个阶段就已经生效
- 保留所有的 #pragma 编译器指令,因为编译器要使用它们。pragma 这个宏可以在编译阶段调试代码,确定程序分支
经过预编译后,会得到一个 .i 文件,这个文件不包含任何宏定义,所有的宏都已经被展开,并且包含的文件也被插入到 .i 文件中。
预编译作用:确定宏或者头文件包含是否正确。当我们无法确认时,可以通过预编译后的文件来确定问题。 预编译用法:使用gcc 的 -E 选项 用法
gcc -E hello.c -o hello.i /* pc */ mips-linux-gnu-gcc -E hello.c -o hello.i /* 嵌入式平台 */
代码示例
预编译
"#pragma" 的用法暂时不介绍,留待后续
编译
编译过程就是把预处理完的文件进行一些列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分。
现代版的 gcc 把预编译和编译两个步骤合并成一个步骤,使用一个叫做cc1的程序来完成这两个步骤。对于C++来说是 cc1plus, Objective-C 是cc1obj,Java 是jc1。所以实际上 gcc 这个命令只是这些后台程序的包装,它会根据不同的参数要求去调用预编译程序cc1、汇编器as、链接器ld
用法:使用 gcc -S 选项
gcc -S hello.i -o hello.s mips-linux-gnu-gcc -S hello.i -o hello.s/* 嵌入式平台 */
我们以最简单的hello world程序演示
截取部分代码:注意gcc -O3 会优化掉一些细节
汇编
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器指令。汇编过程我们可以调用汇编器as 来完成:
as hello.s -o hello.o mips-linux-gnu-as hello.s -o hello.o
或者通过gcc -c 选项
gcc -c hello.s -o hello.o mips-linux-gnu-gcc -c hello.s -o hello.o/* 嵌入式平台 */
经过汇编会得到目标文件hello.o(Object File)
链接
链接过程,是一个比较复杂的过程。链接过程主要包括了以下步骤:
- 地址和空间分配(Address and Storage Allocation)
- 符合决议(Symbol Resolution)
- 重定位(Relocation)
注释:符合决议有时候又叫做符号绑定(Symbol Binding)、名称绑定(Name inding。”决议“更倾向于动态链接,”绑定“更倾向于动态链接。我们通过gcc -v 可以打印编译的详细信息。
最基本的静态链接过程如下:
链接过程分析:
这里的collect2 其实就是ld的封装。
抽象出来主要的命令
通过以上两种方式都可以手动将我们汇编的目标文件hello.o 链接成一个可执行程序a.out
说明:整个链接过程比较复杂,通过gcc -v 获取详细链接过程,从而分析。
如果把所有的路径都省略掉,那么上面的命令就是:
ld -static crt1.o crti.o crtbegin.o hello.o --start-group -lgcc -lgcc_eh -lc --end-groud crtend.o crtn.o
crt1.0 crti.o、crtbegin.o cretend.o crtn.o 这几个库表示 glibc 辅助运行库(C RunTime Library)
这5个目标文件的作用分别是启动、初始化、构造、析构和结束,它们通常会被自动链接到应用程序中。这里暂时不做过多介绍。
总结
本文以gcc 为列,介绍了一个应用程序从源代码到可执行程序的过程。主要分为
- 预编译; gcc -E hello.c -o hello.i
- 编译:gcc -S hello.i -o hello.s
- 汇编:gcc -c hello.s -o hello.o/as hello.s -o hello.o
- 链接:ld *.o hello.o
整个编译过程:又包含扫描、语法分析、语义分析、源代码优化和目标代码优化。这里不做过多介绍。链接又分为静态链接和动态链接,限于篇幅,留待后续更新。
预编译和汇编在gcc里,使用cc1合成一步,ld 通常被封装成collect2程序,事实上最终调的还是ld。gcc 通过不同的参数选项,事实上,最终还是离不开编译器(cc1)、汇编器(as)、链接器(ld)
限于篇幅,文章其他知识点待后续有空再更新。比如:#pragma 的用法;编译过程详解;目标文件和可执行文件有什么区别,为什么要有链接过程?crt1.0 crti.o 这些库到底是干什么用的等等。感兴趣的朋友可以自行了解学习。
延申阅读《程序员的自我修养,链接、装载与库》、《编译原理》