C语言基础-C语言怎么运行的

C语言基础-C语言怎么运行的

在前面的复习中,我们快速的回顾了C语言的基础知识。肯定很多人会有疑问,写的C语言代码硬件是如何运行的?那么今天,我们就来浅显的聊聊这个话题。

在前面的某一期系列中,我们就明确了一个事实:即不论是嵌入式系统,亦或是普通PC电脑,对于程序的运行 硬件处理器只能识别0/1的二进制码 。从类人语言的C代码,需要经过一系列的转换过程,才能完全翻译成机器识别的0/1二进制码流。为了便于解释说明,我们以嵌入式Linux编译过程为例进行解释。

总的来说,从C语言代码翻译为二进制的过程,主要经历 以下四个阶段

  • 阶段一:预编译
  • 阶段二:编译
  • 阶段三:汇编
  • 阶段四:链接

【预编译】

预编译,看一个预字,就表明它是正式编译前的准备阶段。在C语言的代码中,预编译阶段主要完成下面几个工作:

头文件展开 :我们常看到每个C文件的第一行都是#include形式,该形式表明对头文件的引用。源代码开始进行编译之前,这些include(被包含进来的)的文件需要在#include的位置进行展开。如下所示:

宏替换 :在C语言中,一些常用的变量,比如圆周率可以使用C语言的宏定义方式进行常量级别的定义。与头文件展开一样,宏定义也是在引用的位置进行变量展开。宏定义的方式为:

#define 标识符 字符串

例如我们对圆周率的宏定义可以为:

#define PI (3.1415926)

具体的宏定义展开方式与头文件展开基本一致,此处不再举例。

去掉注释 :注释,即程序猿书写的一些便于其他人理解其逻辑的一些人类语言,在代码编译过程中,这些注释是不参与功能,他们存在的价值,仅仅是一种代码的解释而已。

条件编译 :即我们可以让编译器根据我们设定的条件,让符合条件的代码参与编译,不符合条件的代码不参与编译。以此来实现我们对最后目标代码的控制(不参与编译的代码,实现的功能不会进入到目标代码中)。

即在上面的例子中,因为定义了COMPLIE_IIC,所以前半部分的宏定义生效。

【编译】

编译,根据字面理解,就是对指定的C语言代码进行“翻译”的过程,并根据C语言中我们前面讲到的各种语法规则,在 编译器 的参与下,生成 符合指定运行硬件 的汇编代码。这个部分,有专门的编译原理书籍进行详细介绍。我们一来崇尚拿来主义,二来我们不想去研究和设计编译器。这里我们重点注意编译器和符合指定运行硬件两个点:

我们都明确,目标代码是要运行在硬件上,不管你是ARM,MIPS还是X86。每个不同的硬件,存在不同的汇编指令,编译的过程是要从C语言生成汇编指令,所以 不同的硬件都会由其厂家或者其他编译器开发公司,提供符合对应硬件的编译器 。我们常见的有X86下的GCC,ARM下的arm_cc和MIPS下的mips_gcc。

【汇编】

汇编,一句话概括就是完成上一个阶段生成的汇编代码转换机器码。这个阶段,非底层的程序员不需要考虑, 编译器不会搞错的。也与c/c++开发者无关,但是我们可以利用反汇编来调试代码,学习汇编语言依然是必备的。

记得在菊花厂的第一年,很多基站问题的 定位都是通过走读反汇编代码缩小范围找到解决办法的 。当时在外企,一个困扰了同事很久的问题。也是最后我帮助查看反汇编文件,定位到了root cause。这也真的反映了反汇编的重要性,同时也体现了华为公司在技术上的精专对个人其实还是很有帮助的。如果后面大家有兴趣,我可以专门写一篇反汇编定位C语言bug的文章。

【链接】

C语言的源文件经过了预编译,编译和汇编,都已经转化为了目标文件。我们在代码开发过程中,引用的一些C语言前辈们开发的库文件也是以成型的目标文件的形式给出的。但是这些零散的二进制文件,彼此之间是零散分布的,需要一种方式将它们有机的结合在一起,形成一个可运行的统一的目标文件。那么这个过程,就是链接器的主要工作。

这里要特别说明一下 链接中的动态库和静态库问题 。大家可以简单的理解为:静态库经过链接过程,会被打包进最后的可执行文件a.out文件;动态库只有在a.out文件正式开始运行的时候,才会载入到内存,参与运行。所以,静态链接后,a.out文件的大小往往大于动态链接后的a.out文件大小。


总结一下,C语言是如何跑起来的呢?

特别注明 :编译器等工具包,一定要使用与可执行文件未来的硬件运行环境想匹配的编译器开发包。 切记切记 !!!

如果对编译原理的细节有兴趣,不妨读读下面的书籍,我当年因为兴趣使然啃过一段时间。不过因为开发工作繁忙,还是慢慢进入到书架的尘封中。不过如果有兴趣可以看看编译的细节,没准以后咱们也能开发个编译器,这也说不准。

文章被以下专栏收录