一、前言
为什么我们学习C语言的提高要首先学习C语言的编译过程呢?
我们在学习C语言的时候我们老师都是教我们如何使用交叉编译工具去编译C语言,但是我们真的知道C语言是怎么编译的吗?为什么交叉编译工具可以使我们写的C语言代码(指令)编程二进制文件呢?为什么我们又要将C语言代码(指令)编译成二进制文件呢?
针对这些疑问,我今天会让大家真正的去了解C语言的编译过程,相信你阅读完本文,你对C语言和C 语言编译器会有个全新的认识!废话少说,咱们现在就开始我们的C语言之旅!
二、什么是代码编译
不管我们编写的代码有多么简单,都必须经过编译的过程才能生成可执行文件,那到底什么是编译呢?我们又为什么要进行代码编译呢?
我们在编译C语言的时候大多数情况下都是使用的Windows环境下微软开发的 Visual C++,它被集成在Visual Studio中,所以我们通过点击一个按钮就可以生成我们想要的二进制可执行文件了,所以我们前期的学习只是达到了生成的目的,却没有真正的去了解C语言代码的编译过程。
代码的编译大概需要经过以下几个步骤:
预编译 --> 编译优化 --> 汇编 --> 链接
其中汇编过程是整个编译的重点,每一个源程序需要被机器读懂并且能跑起来,而汇编就是做这个工作的,你可以想象一下你只会说中文,而另一个人只会说英文,那您们如何交流呢?
你们的交流是不是需要一个翻译官呢?而C语言编译器就是这个翻译官,他能将我们写的代码,翻译成机器能够听懂的二进制文件,将机器语言设计成二进制文件主要是由以下几点决定的:
-
技术实现简单: 计算机是由逻辑电路组成,逻辑电路通常只有两个状态,开关的接通与断开,这两种状态正好可以用0和1表示。
-
简化运算规则: 两个二进制数和、积运算组合各有三种,运算规则简单,有利于简化计算机内部结构,提高运算速度。
-
适合逻辑运算: 逻辑代数是逻辑运算的理论依据,二进制只有两个数码,正好与逻辑代数中的真和假相吻合。
-
易于进行转换: 二进制与十进制数易于互相转换。
-
抗干扰能力强: 因为每位数据只有高低两个状态,当受到一定程度的干扰时,仍能可靠地分辨出是高还是低。
我们现在已经知道了为什么要使用二进制文件了,那么如何把我们的代码编程二进制文件呢?
想要弄清楚这个问题,我们就要详细看一下编译步骤中的汇编过程,汇编过程就是将我们的代码编译成为二进制文件的。
三、代码编译
我们知道编译过程是将我们写的代码编译成机器语言即二进制文件的,我们先来看一张图,看一下汇编各个阶段的代码格式:
3.1 预编译
在预编译的阶段,C源代码文件 hello.c
和相关头文件被预处理器 (cpp) 处理成一个ASCII
码的中间文件 hello.i
,使用如下命令进行预编译(-E
选项表示只进行预编译)
gcc -E hello.c -o hello.i
预处理只是将源文件进行修改,譬如头文件的插入,代码的选择,输出的.i
文件中的代码基本都是C语言的语法
这是一个.c
的c源代码文件,经过预编译后的.i
文件为:
.i
文件删减后结构如下图所示:
可以看到以下特征:
main
函数从第10704
行开始,前面的一大段代码是<stdio.h>
头文件的插入 ;- 不包含任何宏定义,
#define
定义的宏被展开; - 不包含任何注释;
3.2 编译
在编译的阶段,预编译后的 hello.i
文件经编译器 (gcc
) 进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件 hello.s
,使用如下命令进行预编译:
gcc -S hello.i -o hello.s
事实上,编译器所做的事就是将高级语言翻译成了机器语言。编译器进行编译的过程是十分晦涩难懂的。
hello.i
文件的内容如下所示
需要注意的是,生成的汇编代码中函数printf()
被替换成了puts()
,这是因为当printf()
只有单一参数时,与puts()
是十分类似的,于是GCC
的优化策略就将其替换以提高性能。
3.3 汇编阶段
GCC
编译的第三阶段是汇编,汇编器根据汇编指令与机器指令的对照表进行翻译,将hello.s
汇编成目标文件hello.o
。在命令中添加编译选项“-c”
,操作对象可以是hello.s
,也可以从源代码hello.c
开始,经过预处理、编译和汇编直接生成目标文件。
此时的目标文件hello.o
是一个可重定位文件(Relocatable File
),可以使用objdump
命令来查看其内容。
此时由于还未进行链接,对象文件中符号的虚拟地址无法确定,于是我们看到字符串“hello,world.”
的地址被设置为0x0000
,作为参数传递字符串地址的rdi
寄存器被设置为0x0
,而“call puts”
指令中函数puts()
的地址则被设置为下一条指令的地址0xe
。
3.5 链接阶段
GCC
编译的第四阶段是链接,可分为静态链接和动态链接两种。GCC
默认使用动态链接,添加编译选项“-static”
即可指定使用静态链接。这一阶段将目标文件及其依赖库进行链接,生成可执行文件,主要包括地址和空间分配(Address and Storage Allocation
)、符号绑定(Symbol Binding
)和重定位(Relocation
)等操作。
gcc hello.o -o hello
链接操作由链接器(ld.so
)完成,结果就得到了hello
文件,这是一个静态链接的可执行文件(Executable File
),其包含了大量的库文件,因此我们只将关键部分展示如下:
总结
总结起来编译过程就上面的四个过程:预编译、编译、汇编、链接。了解这四个过程中所做的工作,对我们理解头文件、库等的工作过程是有帮助的,而且清楚的了解编译链接过程还对我们在编程时定位错误,以及编程时尽量调动编译器的检测错误会有很大的帮助的。