大家好,我是灿视。
今天同样,是粉丝在面试腾讯优图实习生的时候,被问到的一道题。
在这里我们跟小亦一起复习(学习)下。
看文章前,可以先关注下我们。
专注于分享最优质的计算机视觉面经,持续关注AI在互联网与银行等单位中的工作机会。
对于一个程序,从编辑文本开始到可执行,到底需要经过哪些过程,编译的原理又是什么?今天我们就来聊聊C++源文件从文本到可执行文件的历程。
以Hello World为例进行说明
首先我们编写一个cpp源程序:test.cpp
#include <iostream>
using namespace std;
int main() {
cout << "hello world" << endl;
return 0;
}
使用g++命令行进行编译:
g++ -o test test.cpp
该命令行是利用gcc编译器将源程序test.cpp 变为一个test可执行文件。这就像一个被隐藏的过程,使用者可以通过简单的命令即可完成复杂的步骤。
其中会经过四个阶段:预处理阶段、编译阶段、汇编阶段和链接阶段。
①预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。产生.ii文件。
②编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件(.s文件).
③汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件 (.o或.obj文件)
④链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件(.out或.exe文件)。
在预处理阶段中,test.cpp(编辑好对的源程序文本)会由预处理器(cpp) 修改,即让test.cpp变为test.i文件。
g++命令行如下:
g++ test.cpp -E >test.i
其中-E选项是只运行C预处理器的选项;>是重定向一个输出文件 test.i。
预处理器(cpp) 的作用:提供了预处理命令
**预处理(cpp)的过程:**主要处理那些源代码文件中只能够以“#”开始的预处理指令。具体指令如下:
#define 宏定义
#undef 取消宏定义
#else #elif #endif #error
#if, #ifdef.......
主要规则如下:
a.对所有的“#define”进行宏展开;b.处理所有的条件编译指令,比如“#if”,“#ifdef”,“#elif”,“#else”,“#endif” c.处理“#include”指令,这个过程是递归的,也就是说被包含的文件可能还包含其他文件 d.删除所有的注释“//”和“/**/” e.添加行号和文件标识 f.保留所有的“#pragma”编译器指令 经过预处理后的.ii文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.ii文件中。
结束当前阶段后,如果用文本编辑器打开test.i文件,发现我们的程序前面多了很多东西。该阶段编译原理就是将头文件**#include** 库中的内容插入程序文本当中,得到了test.i文件。
当我们得到了test.i文件后
就可以进入编译阶段了,在编译阶段,接下来需要的是用**编译器(ccl)**将文本文件test.i翻译成文本文件test.s,这是一个汇编程序,编译的过程就是将预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件(.s文件)
使用-S编译选项即可以得到.s程序
g++ test.cpp -S
得到了汇编程序后,汇编器(as) 会将test.s文件进行汇编,将复杂晦涩难懂的汇编指令变为机器语言指令,每一个汇编语句几乎都对应一条机器指令,并把这些指令打包成一种 可重定位目标程序并将结果保存在test.o(.o或.obj文件)中
使用 -c 编译选项,该选项只编译生成目标文件,不链接,链接动作是在链接阶段完成的。
g++ -c test.s -o test.o
对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成,通常一个目标文件中至少有两个段:
UNIX环境下主要有三种类型的目标文件:
汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。
当一个程序调用了标准库中的函数,例如printf、cout等,这个函数已经存在于一个已经单独预编译好了的.o文件中,而这个文件必须以某种方式合并到我们的test.o当中,得到可执行的test文件。
链接的过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)。
链接就是把每个源代码独立的编译,然后按照它们的要求将它们组装起来,链接主要解决的是源代码之间的相互依赖问题,链接的过程包括地址和空间的分配,符号决议,和重定位等这些步骤。最基本的静态链接如图所示:
g++命令行如下:
g++ test.o -o test 动态链接 g++ tets.o -static -o test静态链接 2种都可生成可执行文件,前者文件只包含文件名,运行时再链接相关函数,后者编译时便链接相关函数,前者体积小,运行时没后者快,后者体积大。
这样得到一个可执行目标文件,就可以被加载到内存当中,即可被执行。
每个目标文件除了拥有自己的数据和二进制代码外,还拥有三个表,未解决符号表,地址重定向表,导出符号表。根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
1、静态链接/库
在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,因此对应的链接方式称为静态链接。
静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。
静态库的缺点在于:浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
2、动态链接/库
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
附带几道常见的面试题:
防止重复包含头文件。