从源文件到可执行文件

有一定C/C++基础的人都知道,从写好的源文件生成可执行文件大概要经过编译、链接两个步骤。但是这其实只是比较笼统的说法,其中编译器完成了很多的工作,这里我们简单梳理一下。

扯点历史

大家都知道,最初并没有这么多高级的编程语言,大家写程序必须要使用计算机能理解的“机器语言”,或者说是直接的机器指令。比如要计算一个“1+2”,就可能会需要两个读取指令读取“1”和“2”,然后使用一个加法指令计算,最后再使用输出指令将结果输出。这整个的过程无疑是非常繁琐且机械化的,而且这种指令非常依赖硬件平台,换一台机器可能就会需要大面积修改。而且如果考虑到跳转指令的情况,如果后面需要修改前面的代码时跳转的地址也必须要跟着修改,这带来的工作量简直是灾难性的。

所以人们就发明了汇编器这种工具,随之诞生的还有汇编语言。“汇编”这个过程就是将相对好理解一些的汇编语言给翻译成对应的指令,同时对于汇编语言中的一些符号也能计算出其对应的地址,这样就能大大减少程序员们的工作量。

但是人们仍然不满足,汇编语言在形式上仍然更加接近机器指令,这也使得我们人类在编写和理解程序时感到十分困难,所以我们需要一个更加“高级”的语言,能在更高程度上抽象化这些指令。比如说能提供函数调用循环语句等比较实用的功能,当然这描述的其实很像就是后来的C语言。有了这样的语言,我们才能更加轻松地开发比较大型的程序。

C语言对于机器来说是非常“高级”的,与它们能理解的指令差别很大,所以还需要一个比汇编器复杂得多的工具去“翻译”它,也就是我们要提到的编译器。

编译

让我们来梳理一下,在写完一段C语言程序之后,我们把它交给编译器,编译器通过编译将这段程序转化成机器可以直接执行的指令。不过实际情况比这要稍微复杂一点,编译阶段的工作还有几个比较细节的步骤。

首先是C语言的头文件的概念。C语言通过一句“#include”宏将一个头文件包含进源文件中,这种包含关系在编译期必须要得到处理。同时,头文件中可能也会有许多的“#ifdef”宏来确保头文件的唯一性。为了处理这些,在编译之前还需要一个预处理的步骤,展开这些宏来得到“真正的”源文件。

此外,由于现在平台和系统的多样性,不同平台和系统的可执行文件也有所不同,所以现在的编译器大多都分为前端和后端两个部分。前端负责语法部分,检查语法错误并且将C语言代码转换为一种中间代码;后端负责平台部分,专门将中间代码翻译成对应平台的汇编代码。

简单总结一下,编译过程大概分成三个部分:

  • 预处理
  • 编译
  • 汇编

理论上经这三个步骤处理完以后就能得到可执行文件了。

链接

到目前为止似乎一切都很完美。等等,我们是不是还忘了什么。回想一下,一个C语言程序可能并不是全都写在同一个 .c 文件里的,可能会同时有多个文件,这下该怎么办呢。

一种简单粗暴的方式是直接将所有文件内容整合到一起,再看做一个源文件来编译。这样看似很简单,但是也会带来一些问题,比如编译速度。想象一下,如果我的程序里有100个源文件,然后在一次修改中我改动了其中一个,但是下次编译时全部100个文件都需要再重新编译一次!这对于那些动不动就需要编译几个小时的大型程序来说是无法想象的,我们需要一种更加合理的方式。

为了让没被修改的部分不重复编译,所以我们要针对每个源文件来进行单独的编译。如果说我们有 a.c 和 b.c 源文件,而且 a.c 中定义了一个函数“funcA”,在 b.c 中又调用了这个函数,那么又该怎么办呢?因为两个源文件是单独进行编译的,在编译 b.c 的时候编译器也并不知道 a.c 中存在这个函数,所以按照之前介绍的那个流程就行不通了。

这时我们就要隆重介绍链接器了。链接这个过程大致来说就是,各个源文件中的符号(比如说 a.c 中的“funcA”),通过链接的方式来进行匹配,如果匹配成功,就生成最后的源文件;如果匹配不上,就给出相应的链接错误(比如**符号未定义)。

至此,我们的整个步骤终于完整了。先对各个源文件单独进行预处理、编译、汇编得到一个目标文件,再对所有的目标文件进行链接得到最终的可执行文件。

举个栗子

以gcc编译器为例。假设我们有了一个简单的C语言程序 main.c,我们可以通过简单的几个命令来一步步完成上文提到的几个步骤。

> gcc -E main.c -o main.i    #预处理
> gcc -S main.i -o main.s    #编译
> gcc -c main.s -o main.o    #汇编
> gcc main.o -o main.exe     #链接

如果不熟悉gcc命令的话这里简单介绍一下。gcc其实是编译器提供的很多命令的一个封装,通过不同的参数就可以调用到编译器里的不同命令,并且得到指定步骤后的结果,参数-o用来指定输出的文件名。

其中main.i和main.s都是文本文件,可以直接查看其内容;而main.o和main.exe是二进制文件,具体内容需要借助特定工具来查看。

你可能感兴趣的:(从源文件到可执行文件)