1、引言
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码
比如说我们写了一个源文件test.c,其经过一个详细的处理后会变成test.exe文件,此篇文章会详细讲解test.c文件是如何到test.exe文件的,其实主要有两个大的过程,1是编译,2是链接。而此过程所依赖的主要环境就是翻译环境,而test.exe文件想运行起来所依赖的环境是运行环境。
正文开始:
2、翻译环境
先看一幅图:假设我们在一个工程里面写了test.c,contact.c,common.c这三个源文件。每一个.c文件都会经过编译器处理,最后各自生成一个目标文件(windows底下以.obj作为后缀),这些目标文件和链接库一起经过链接器最后生成可执行程序。组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
解释第3条:(用printf举例)
Libraries就是库,printf就是包含在以后缀.LIB命名的静态库里头。要使用pritnf,就应该把这些.LIB文件的链接库链接进来
- 而编译又可分为以下三个部分(用一张图先过渡下)下面将会详细介绍一个源文件是如何到可执行程序的。
(一)、编译
- 先看一段代码:
#include<stdio.h> int g_val = 2021; int Add(int x, int y) { return x + y; } int main() { int a = 10; int b = 20; int ret = Add(a, b); printf("%d\n", ret); return 0; }
在VS环境中,我们执行该程序需要按ctrl+F5,但是此时会直接生成可执行程序,无法看到具体转换过程,所以采用linux可以很好的观察到这一现象:即将编译拆分成预处理+编译+汇编
注意:
在linux环境中,如果直接使用gcc来编译test.c文件,它默认生成一个a.out的一个文件,该文件同样也是个可执行程序,为了避免和VS环境一样直接生成可执行程序而无法观察到细节,因此我们需要在预处理后停下来,编译后停下来,汇编后停下来。操作如下:
1. 预处理 选项 gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
2. 编译 选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s中。
3. 汇编 gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中。
具体过程见下文:
(1)预处理
在linux环境中使用gcc test.c -E这个选项来编译test.c文件,就可以使其预处理后停下来
而这个命令执行后,会出现一堆代码,此时我们再使用gcc -test.c -E > test.i这个操作就可以把出现的一堆代码重定向输出到test.i文件里头,打开test.i文件
先看三幅图:
图一:
- 图二:
此时我们在test.c文件里头添加些宏定义,再和预处理阶段对比看看:
- 图三:
当我们在test.c文件中加上一些注释时,在预处理阶段又会发生什么呢?通过上述三张图:可以总结出在预处理阶段的3条结论:
完成了头文件的包含
#define定义的符号和宏的替换
注释删除
而上述三个操作都属于文本操作。
(2)编译环节
在经过上述的预处理后,就来到了编译环节:
我们使用gcc test.i -S这个指令进行编译,最终生成test.s文件,我们打开test.s文件来看看:
先简单截取一段:上述代码即汇编代码,综上可得出结论:
汇编过程即把C语言代码转化成汇编代码,主要做了以下4个内容:
语法分析
词法分析
语义分析
符号汇总
先简单解释下符号汇总:
把全局变量和函数名汇总起来,比如main和全局变量g_val,符号汇总的用处是在下一阶段汇编和链接中体现出来的,下文会详细介绍:
简单举一例:(3)汇编
在我们编译结束后生成了test.s文件,接下来进入汇编
我们使用gcc test.s -c命令进行汇编,最后生成test.o这样的一个文件,而这个test.o文件就类似于windows上的test.obj文件,即生成了目标文件。我们打开test.o文件:(简单截取一段)
上图中放的其实都是二进制信息。由此得到汇编的结论:
- 把汇编代码转换成机器指令(二进制指令)
- 形成符号表
而汇编中的生成符号表和编译中的符号汇总又有什么关联呢?接下来详细介绍:
- 重新举个例子:
如图所示,我们创建了add.c和test.c两个源文件 在linux环境中,test.c文件和add.c文件会经过预处理、编译、汇编生成对应的test.o文件和add.o文件。
而汇编中的形成符号表其实就是有一张表格,里面既有汇总的符号,也有这些符号对应的地址
- 拿上例说明:注:
test.c汇总的Add符号由于Add是声明的,找不到确切的地址,所以用红色记号笔简要表示。
理解了符号表,当生成.o文件(目标文件)时,多个.o文件经过连接器链接会生成可执行程序即test.exe文件。这个过程链接就起到了主要作用,详解见下文:
(二)、链接
链接主要实现的操作有两个:
合并段表
符号表的合并和符号表的重定位
如上图test.c文件和add.c文件会经过预处理、编译、汇编生成对应的test.o文件和add.o文件。而它俩要链接在一起,首先就要发生合并段表。
合并段表:
像add.o这样的文件是有它字节的格式的,它会把自己分成几个段。自然test.o也会这么分,两个段的格式是一样的,只不过内容是不一样的。而合并段表就是把对应的段上的数据合并在一起,如图:
注:这些段表的格式都是elf文件格式
符号表的合并和符号表的重定位:
在上述两个操作执行完毕后,就可生成可执行程序了,此时整个翻译环境就讲解完毕。
3、运行环境
程序执行的过程:
程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
程序的执行便开始。接着便调用main函数。
开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
终止程序。正常终止main函数;也有可能是意外终止。