一个.c文件是怎么跑起来的?——程序的编译

前言

我们在日常生活中总是说一个源程序要先经过编译然后才能运行,源文件是给人看的,要把它翻译成二进制文件机器才能看懂。emmm,感觉有点抽象,今天我们就来聊一聊编译这回事儿。

一. 程序的翻译环境和执行环境

一个源程序想跑起来必须经过两个环境处理:翻译环境和执行环境

翻译环境

翻译环境中有两个非常重要的工具,一个是编译器,一个是链接器,这两个工具分别完成两个过程,即编译和链接。

像VS的编译器是cl.exe,链接器是link.exe,这两个文件都可以在安装的路径底下找到。
注意我这里的说法哟,我说的是“VS的编译器”,难道VS不是一个编译器吗?还真不是。

VS是一个集成开发环境,比如DEV C++,CodeBlocks这些我们都把他们叫集成开发环境。一个集成开发环境包括编辑,编译,链接,调试这样一些功能。不同得集成开发环境得各种组件可能不一样,比如VS得编辑器是cl.exe,而CodeBlocks用的是gcc。

执行环境

执行环境就是用来运行可执行程序得,通常就是我们的操作系统

整个过程可用这样一张图表示
在这里插入图片描述

二.详解翻译环境

翻译包含两个步骤,编译+链接,我们平常很少提到链接这个过程,其实经常说的编译就是指的翻译阶段,这里为了讲清楚细节从而将它细分出来。

一个程序中的所有.c文件会经过编译器单独编译生成对应的目标文件。这种目标文件在Windows环境下后缀名是.obj,在Linux环境是.o。下文我用到的文件后缀名都是Linux环境下的命名方式

得到目标文件后,链接器会把这些目标文件和链接库链接在一起生成可执行程序。

整个过程如下:
在这里插入图片描述
下面来详细讲解编译和链接这两个过程

2.1编译

编译又可以细分为三步:预编译(也叫预处理),编译和汇编

2.1.1预编译

这步主要干三件事:

  1. 头文件的包含
  2. define定义标识符的替换
  3. 注释删除

以这样一个源文件为例:

#include <stdio.h>
#define SZ 10
//这是一个测试样例
int main()
{
	for (int i = 0; i < SZ; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

那么它经过预处理阶段后就会变成这样

xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
int main()
{
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

其中“xxxxxxxx”是头文件里的内容,非常复杂,有几百行,同时我们还发现注释消失了,而且SZ被替换成了10,这就是预处理阶段做的3件事。

总的来说,预处理就是做一些文本替换的工作

这个过程在VS这种集成开发环境下不太好验证,有兴趣的老铁可以在B站上搜相关的教程,在VScode上搭载gcc编译器,通过命令行的方式操作验证。

2.1.2编译

这个阶段是把c语言代码转换成汇编代码,放到一个xxx.s的文件中

没错,编译器不是直接将C代码转换成二进制指令的,中间还要先转换成汇编代码然后再向二进制语言过渡。
这个过程要干这些事:

  1. 语法分析
  2. 词法分析
  3. 语义分析
  4. 符号汇总

这些过程都很复杂,有兴趣可以看看《程序员的自我修养》,里面有详细的讲解。

这里简单提一下符号汇总,因为后面还会用到它。
符号汇总顾名思义,就是把符号收集起来,不过要加个限定,是把收集全局的符号。

以同一个工程中的test.c和add.c为例(下文会一直使用这个例子)
在这里插入图片描述
前面说过,编译是对各个.c文件进行单独地处理,所以会对test.c和add.c分开汇总符号。
test.s收集到的符号就是Add和main
add.s收集到的符号只有Add

2.1.3汇编

这一步会把汇编指令转换成二进制指令

经过转换后就得到了若干个目标文件,以上面的例子为例,就是test.o和add.o。

这个过程中还发生了重要的一步,形成符号表
在上一步编译阶段汇总了符号,这里就会把符号形成一个表,每个符号既有名字也有地址
例如:
test.o的符号表为

名称地址
Add0x000
main0x104

(因为Add在test.c中只有声明,所以找不到该函数的有效地址,故用空指针代替)

add.o的符号表为

名称地址
Add0x100

到了这步可能你还是不明白收集符号有什么意义,别着急,继续往下看。

2.2链接

这一步主要干两件事
1.合并段表
2.符号表合并和重定位

合并段表
目标文件已经是二进制文件了,这种目标文件是有格式的,以Linux环境下的目标文件为例,目标文件的格式elf这种格式,目标文件被分成一段一段的,可执行程序的格式也是elf,合并段表可简单理解为将对应的段合并形成新的段
在这里插入图片描述

符号表合并和重定位
例如,test.o有Add和main两个符号,add.o有Add这一个符号,那么合并后的符号表就包含Add和main两个符号。main好说,它的地址就是0x104,那么Add呢?这个时候就要符号重定位了,Add的地址应该是那个有效地址0x100

名称地址
Add0x100
main0x104

符号表用来干什么的呢?千呼万唤开出来。现在揭晓答案。
在链接这个阶段,符号表合并合并好之后,链接器会检查各文件内的符号是否在符号表中。比如我因为手误,在main函数里面写成了add()函数,链接器在符号表中查找,发现没有这个符号,就会给你报错。
在这里插入图片描述
至此也就能解释为什么同一个工程中的不同文件里面的全局变量/函数能互相使用了,就是因为符号表这个东西记录了各符号的地址,所以能通过符号表找到对应的函数和变量。

在我以前的文章中讲到过static的用法,若是在函数或全局变量前加上static,其它文件就无法使用它,这是因为改变了它的外部链接属性,现在理解起来就很清晰了。
static的用法

其它的文件中能不能访问这个符号,就是由该符号的链接属性决定的。正是因为它具有外部链接属性,才会进入符号表,通过符号表将它和其它文件链接起来,而改变外部链接属性后,这个符号压根就不会进入符号表,外部自然就访问不到它了。

三.总结

所有内容可用这样一张图来概括
在这里插入图片描述

本次分享结束,欢迎评论区留下宝贵意见。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值