本文已参与「新人创作礼」活动,一起开启掘金创作之路
一、前言
不难理解,计算机只能理解二进制文件,那我们所写的代码是如何被编译器理解并且被执行的呢?这就涉及程序的翻译环境和执行环境,本文将以这两个内容为主题进行粗浅的讲解,如果想对这方面的知识有更为深刻的理解,大家可以参考《程序员的自我修养》一书【网盘资源,需要自取,提取码:mu32】
二、编译环境
① 一图总览编译环境
- 分析总结
- 对于每一个源文件都会生成相应的目标文件
- 所有生成的目标文件和链接库经过链接生成可执行程序
现在我们对编译环境的每个过程做具体的说明。
② 准备工作
如何看到编译过程的文件呢?在gcc编译器下可以很轻松的看到,在VS下也是可以的:按下图修改后按下ctrl + F7进行编译(之后要想正常执行需要改回来的)
但似乎只能观察到 .i 和 .obj文件。那我们还是在gcc编译器下观察吧。
我们先写下这样两段简单的代码
//Add.c文件中保存
int Add(int x, int y)
{
return x + y;
}
//test.c文件
extern Add(int x, int y);
#include <stdio.h>
//这是一条注释
int main()
{
int a = 10;
int b = 20;
int c = add(a, b);
printf("%d", c);
return 0;
}
③ 预编译过程
.i文件和.c文件有什么区别呢?不难发现,#include <stdio.h>这条语句不见了,但我们又惊奇的发现,原本短短的代码现在居然有了800多行,我们可以猜想,是在
预处理阶段将头文件包含进来了吗?答案的确是这样的。我们可以打开stdio.h这一文件来验证我们的猜想:stdio.h末尾的这几行代码是不是很眼熟呢。
我们还可以观察到我们的注释被删除了,这也是预处理的任务之一。当然,预处理还有其他的功能,就不进行一一演示了,直接在下方总结
- 预处理作用总结
- ①头文件的包含
- ②注释的删除
- ③#define的替换,宏定义的展开
- ④处理条件预编译指令,如:“#ifdef”、 “#if”、“#elif”等等
- ⑤处理行号并添加文件标识,便于编译时编译器产生调试用的行号信息以及编译时产生的编译错误和警告能够显示行号
- ⑥保留所有#pragma编译器指令,因为编译器需要使用他们
本质上预处理阶段进行的是文本操作。
④编译过程
- 编译作用总结
将C语言代码转换成了汇编代码。说的很轻巧,但却是最复杂的环节,涉及以下几个过程:
- 语法分析 2. 词法分析 3. 语义分析 4. 符号汇总
过程最为复杂,不做深入说明,大家可以参考《程序员自我修养》一书(前言里有下载链接)
⑤汇编过程
(在Linux环境下生成的是.o文件)可以看到上面的内容我们已将完全看不懂了,实际上此时汇编的过程是将编译过程生成的
汇编代码转化成二进制代码
因为每一个汇编语言几乎都对应一条机器指令,所以汇编器的汇编过程相对于编译器来说比较简单,只需要对照机器指令的对照表一一翻译就可以了。
值得一提的是汇编过程中有形成符号表这一环节,我们对这一细节进行详细说明。编译过程进行符号汇总时,编译器会记录出现过的全局符号,如Add函数中的 Add, main函数中的 main, Add, printf 。在形成符号表过程中,将符号和其相应地址一一对应的表格称为符号表,由于main函数中的Add函数只是用extern声明,但不知道Add位置的位置,所以给它一个没有意义的填充值。

那么生成的符号表有什么用呢?在链接环节就会提到。
⑥链接过程
链接过程包含以下两个过程:
- 合并段表
- 符号表的合并和重定位
合并段表 : 目标文件和可执行文件的文件格式都是elf。而elf文件会将文件内容分成一个个“段”,各个段划分的方式都是固定的,所以对于test.o文件和Add.o文件中会有相同结构的段表,这时就可以合并了。
符号表的合并和重定位 : 在汇编的过程中我们生成了多个符号表,但最后我们只能有一个符号表表,所以要对符号表进行合并。在合并的过程中发现Add函数出现了两次,这就涉及重定位了。最终我们得到以下符号表:
所以符号表究竟有什么作用呢?我们试想,如果我们在main函数中使用了Add函数,但却没有写Add函数,那么即使符号表合并后,符号表中Add函数也只是一个没有意义的地址,所以符号表的作用在于:
多个目标文件链接时,会通过符号表查询外部的符号是否真的存在。
因此我们也不难理解,即使我们没有声明外部函数,但编译仍然能通过,只不过会发出警告,原因就在于可以从符号表中找到对应函数的地址。
三、执行环境
程序执行的过程:
- 程序必须
载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。 - 程序的执行便开始。接着便
调用main函数。 - 开始执行程序代码。这个时候程序将使用一个
运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。(设计函数栈帧的知识,可以学习这篇博客 【C语言知识精讲②】函数栈帧的创建和销毁(全程图解)) 终止程序。正常终止main函数;也有可能是意外终止