学习一门语言,经常都是从打印“Hello,World”开始的,打过招呼后,你便可以进入程序的新世界。
就拿经典的C语言举例,基本上每个程序员在上学时就可以闭着眼睛写下“Hello,World”,这也是检测开发环境是否能正常工作常用的小程序,就像有的人看能不能上网就输个百度试试(手动斜眼,程序员应该用谷歌).
//hello.c
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
我们使用gcc
编译并运行该文件:
$ gcc hello.c -o hello
$ ./hello
输出结果:
其实,输出一行字符并没有那么简单,gcc
帮我们处理了很多步,如果你用Visual Studio
,运行按钮更是连编译指令都不用敲了,IDE是简化了很多步骤,但是深入探索背后的步骤是每个程序员必备的素养,更何况很多成熟的大型项目都是需要自己构建(Build)。
上述过程可以分为4个步骤:
- 预处理 (Prepressing)
- 编译 (Compilation)
- 汇编 (Assembly)
- 链接 (Linking)
下面我们详述这一过程:
预处理##
预处理器cpp将hello.c
及包含的头文件,这里就是stdio.h
预编译成为一个hello.i
的文件。
我们可以用以下命令只对hello.c
进行预处理:
$ gcc -E hello.c -o hello.i
或者:
$ cpp hello.c > hello.i
你没看错,预处理器就是cpp
,与C++扩展名.cpp
没有关系,具体可以man cpp
查看手册,其实gcc
只是把预编译器,编译器,汇编器,链接器这一系列工具集成在一起,通过不同的参数去调用不同的部分或者全部调用
预处理做的工作:
- 将所有的
#define
删除, 并且展开所有的宏定义,像#define MAX 1024
,那么代码文件中所有的MAX
都会被1024
代替。 - 处理所有的条件预编译指令,包括
#if
、#ifdef
、#elif
、#else
、#endif
。至于这些指令到底干嘛的,任何一本C语言教材都会有明确的解释。 - 处理
#include
指令,将所有头文件插入到预编译指令的位置,这一过程是递归进行的,也就是说,头文件里包含的头文件也会被插入头文件里。良好的代码规范都指导我们使用头文件保护,避免重复包含头文件。 - 删除所有注释
//
和/* ··· */
。注释给人看的,机器不需要看注释。 - 添加行号和文件名标识,比如打开刚刚的
hello.i
,int main()
之前插入了一句# 2 "hello.c" 2
,以便于编译器产生调试用的行号信息,这样产生编译错误或警告时,编译器就会给出文件名和行号。
-保留所有的#pragma
指令,编译器会使用它们。
经过预处理后,文件中所有的宏被展开,包含的文件也被插入,这时候就可以给编译器使用。
编译##
编译过程是整个程序构建的核心部分,包含了大量编译原理的知识,注明的参考书有龙书。
编译过程可以分为以下几个部分,每个部分深究起来都很耗费功夫,有机会可以自己实现:
- 词法分析
- 语法分析
- 语义分析
- 中间语言生成与优化
现在版本的gcc
把预处理和编译两个步骤合二为一,使用一个叫cc1
的程序完成这两个步骤,在我的计算机里位于“/usr/lib/gcc/i686-linux-gnu/4.8/cc1”。
我们可以通过以下命令生成编译后的文件:
$ gcc -S hello.c -o hello.s
也可以直接使用cc1
:
$ /usr/lib/gcc/i686-linux-gnu/4.8/cc1 hello.c
编译后生成汇编文件hello.s
汇编##
汇编器就是将汇编代码转变成机器可以执行的指令,每一条汇编语句几乎都对应一条机器指令。所以汇编器相对简单,只需要一一翻译就可以。
我们使用汇编器as
完成如上工作:
$ gcc -c hello.s -o hello.o
或者
$ as hello.s - o hello.o
也可以直接从hello.c
直接得到目标文件:
$ gcc -c hello.c -o hello.o
链接##
据说链接器的历史比编译器还长,像我们的“Hello,World”程序,生成的hello.o
中包含了printf
函数,头文件只包含了函数的申明,所以最后还需要链接到libc.a
,其实需要链接的不仅仅是printf
,我们用链接器ld
链接以下这么多模块才能生成最终的可执行文件。
$ ld -static /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i686-linux-gnu/4.8/crtbeginT.o
-L/usr/lib -L/lib hello.o --start-group -lgcc -lgcc_rh -lc --end-group
/usr/lib/gcc/i686-linux-gnu/4.8/crtend.o /usr/lib/crtn.o
一个再复杂的软件也是如此,将源代码分别独立编译,再组装起来,这个过程就叫做链接,链接的主要目的,一个是将模块间依赖的函数调用打通,还有就是模块间共通的变量打通。
链接器所做的工作主要就是“调整地址”,写汇编代码时,有这么一句jmp foo
,其实链接器就帮我们把foo
翻译成运行时的地址。
链接的主要过程:
- 地址和空间分配 (Address and Storage Allocation)
- 符号决议 (Symbol Resolution)
- 重定位 (Relocation)
举个例子,可以很清楚的解释这个过程,我们在main.c
调用了另外一个文件func.c
中的函数test()
,那么当我们在main.c
中每使用一次test()
都必须知道test()
的地址,但文件都是单独编译的,所以我们在main.c
中的做法是暂时搁置test()
的地址,当链接的时候,链接器会根据test
符号,自动填入test()
的地址,如果func.c
重新编译了,test()
地址会变化,但是编译时,没有改变的main.c
并不会编译了,只是在链接时,会链接新的test()
的地址。这个修正的过程也叫作重定位。
链接还分为静态链接和动态链接,这个以后会专门说。
如果觉得还不错,请点个赞吧~