接上一节关于C语言标准版本的教程:C语言标准版本之C89(C90)和C99 C11
不像高级编程语言,在C语言开发中,了解其编译链接过程显得相对重要,因为C语言是较为底层的语言,很多时候我们调试C程序或者解决其它问题都可能会涉及到C编译链接的相关知识,例如编译动态库或者静态库。下面我们一起来了解一下C程序的编译链接过程,结合一些实例更好了解其中的原理,这里使用的编辑器是linux的vim,编译器使用GCC。
C程序编译的起始点为源代码(hello.c),结果为可执行的字节码文件,C程序的整体编译链接过程如下图所示:
文章图片
在整个过程中,最重要的两个环节为编译阶段和链接阶段,编译由编译器完成,链接由链接器完成,编译器的最主要功能是将C源代码编译成中间代码即目标代码,链接器的功能是将目标代码和库文件代码链接成可执行代码,即可执行程序。链接器涉及的结构不会太复杂,一般会由编译器自动调用,我们使用编译器编译源代码的时候,编译器会自动调用链接器完成代码生成。C程序详细编译链接过程如下:
文章图片
下面我们就预处理、编译、汇编、链接进行详细解释,并且结合操作实例进行理解。
一、预处理(Preprocess)预处理由预处理器(Preprocessor)负责执行,这个过程并不对源代码进行解析,预处理器负责扫描源代码,处理包含头文件,宏处理命令,条件编译命令。总的来说有4个编译阶段:
1、替换三字符组合双字符组
2、行处理,将源码中的行转义字符转为一般的换行符
3、处理源码中的注释、空白等
4、处理源码中的预处理命令和宏扩展
下面看一个简单的C程序源码,这个代码定义了一个字符串,打印该字符串和该字符串的长度:
#include <
stdio.h>
#include <
string.h>
#include <
stdlib.h>
// the first C Programint main(int argc, char *argv[])
{
char *name = "hello";
// defined name of the user
printf("%s%ld\n", name, strlen(name));
// print the name and the length of the name
return 0;
}
为了验证这个预编译过程,我们目前只需要关注注释就行了,使用gcc预处理命令:gcc -E hello.c -o hello.i,-E表示执行预处理,-o表示指定输出文件,为什么是i后缀名?在这里你也可以取其它名字的后缀名,使用vim查看hello.i文件如下图所示:
文章图片
你会发现所有的注释都没有了,即验证了预处理过程,但是还不够,在这个过程中涉及到我们编写代码的内容,最明显的就是#include命令了,这是一个预处理命令,但是这是什么意思呢?首先我们要清楚C程序一般来说实现某一个功能的时候,会先在头文件(.h)中声明,后再在源文件(.c)中实现。那么include包含文件的具体含义是什么?就是将需要的头文件(如string.h)包含进hello.c文件里,相当于将整个string.h头文件的内容复制一遍放到hello.c文件里,真的吗?不会出错?在C语言任何东西都需要先声明或定义后使用,如果你先声明一个函数但是没有定义实现,调用该函数执行预处理这里也不会出错。
上面的代码中我们使用了了一个strlen()函数,这个函数在string.h头文件中,那么按照我们说的,hello.i中就会包含该函数的声明(注意声明和定义不是一个东西),使用vim打开,查找一下看:
文章图片
真的是这样!另外还可以看到strtok函数,该函数用于分割字符串,也是处理字符串常用的一个函数。预处理过程比较接近我们的代码编程,比如有时可能会处理重复包含文件、条件编译等问题,例如下面的条件编译,即使run函数没有定义,也能通过编译该源码并执行,为什么呢?因为它根本就没有参与到实际的代码编译。
文章图片
二、编译(Compile)编译使用编译器处理,主要是将预处理好的代码编译成汇编代码,这个过程比较复杂,一般来说汇编代码就是目标代码了,因为目标代码是机器代码,而汇编代码只是机器代码的一种助记符,这种助记符不像高级语言和机器语言的区别一样,高级语言是有语义的,但是汇编助记符基本没什么语义,一个汇编指令就能对应一个机器码。使用gcc –S hello.i –o hello.s编译,使用vim查看hello.s文件如下图所示:
文章图片
上面的main就是main函数,从main开始执行,另外汇编代码是分段的,上面是数据段,下面是程序代码段,最终的可执行代码也分段,它对应于内存分区,在C语言和其它编程语言里都涉及到内存分区的分析,你可能听说过栈区和堆区等,这就是可执行代码和内存的联系了。
三、汇编(Assembly)汇编过程就是使用汇编器将汇编代码翻译成目标机器代码,在这一步执行的结果就是真的机器码了,但是要注意,这个机器码还不能执行的,为什么?前面提到过,在预编译阶段仅仅是将头文件包含进来,但是并没有对应实现的文件代码,所以这个字节码文件还不是最终的产品。
执行汇编命令使用gcc –c hello.s –o hello.o,使用objdump –s –d hello.o查看该字节码文件的结构:
文章图片
这是elf文件(windows下为PE文件),.text为代码段,.rodata为数据段。
四、链接(Link)链接阶段使用链接器执行,上面说了,预编译中有些系统头文件的实现没有包含进来,那么在这一步就需要将需要的文件包含进来了,直接使用gcc hello.o –o hello即可得到最终的可执行文件。
在这一步又有一个很重要的学问了,静态库和动态库。如果使用静态库,那么在这一步链接器就会将静态库和本项目的字节码文件一起打包生成最终的可执行文件,如果使用动态库,那么在这一步也有处理,但是不会将动态库一起打包,而是在运行的时候才动态加载。这两种库开发中都会用到,一般使用其它库都需要首先引入头文件,为什么?和第一步预编译有关,首先有头文件声明才能进行参与编译过程,而库文件一般都是代表实现头文件的代码生成的字节码文件。
了解更多关于编译和链接的内容,可以查看编译原理简明教程:
1、编译原理的学习和介绍
2、编译器的结构和编译过程
3、编译原理之语法分析的运行机制
【C语言简明教程(二)(C程序编译链接过程和实例对照详解)】4、编译原理之词法分析和词法分析生成器
推荐阅读
- Python数组Array用法经典指南
- C语言简明教程(一)(C语言标准版本之C89(C90) C99 C11)
- 热门JavaScript前端开发面试题汇总和答案详解
- 热门前端开发面试题之CSS面试题和答案分析
- 热门前端开发面试题及答案汇总(一)(HTML经典面试题和答案分析)
- 如何在一个JavaScript文件中包含另一个JavaScript文件(有哪几种方式?)
- event.preventDefault() vs. return false这两种调用方式有什么区别()
- 如何在JavaScript中实现复制到剪贴板(如何原生实现,有第三方库吗?)
- 如何重新随机化或者重新排序JavaScript的数组元素()