超详解------一个程序从编译到链接的全过程


在ANSI C标准下的一个程序中,分为两种环境,一个是翻译环境(也叫编译环境),另一个叫执行环境。翻译环境是用来将源代码转换为计算机能够识别的机器代码,执行环境是将生成的执行文件实际执行的环境。

翻译环境

在一个完整的工程中,可能会有多个源文件和多个头文件,那么编译器在编译的过程中是怎样进行处理的呢?
首先,编译器会将每个源文件进行单独的编译,生成目标文件(后缀为.obj)。编译器会检查在每个源文件中所引用的库函数,将其生成链接库。最后链接器会将目标文件和链接库进行合成,生成最终的执行文件,用于实际的执行。

如下图所示:
在这里插入图片描述
翻译环境大体上分为编译和链接两个过程。

编译

编译这部分的每个阶段都有不一样的作用,因此下面的介绍会在Linux环境下来进行更加详细的讲解(在VS环境下也可以进行操作,但是比较繁琐,没有Linux环境下方便)。

预编译阶段

1. 头文件的包含

我们在Linux下的vim编辑器写了一段实现加法功能的代码,用gcc来进行编译(gcc时Linux常用的C编译器)。我们保存之后用gcc -E test.c对这个源文件进行预处理,如果不想在屏幕上看见结果,就把预编译之后的结果放到一个文件中。用gcc -E test.c > test.i来进行实现(>是重定向符号)。
在这里插入图片描述

在这里插入图片描述
我们可以看到,在源文件test.c中原本只有十几行代码,但是在预编译之后竟然有800多行代码。细细观察我们可以发现,两者相差的只有一个头文件,那么我们就知道了,在预编译阶段,编译器会把头文件中的所有内容全部拷贝过来。

  1. 注释的删除
    我们在test.c的代码中加入了两行注释,在预编译之后会发生什么情况呢?
    在这里插入图片描述
    在预编译之后我们发现,原本在源文件中的两行注释被删除了,这也是预编译阶段的功能。因为注释是给我们写代码的时候便于理解的,但是当编译时编译器并不需要知道注释中写的什么,自然注释也没有存在的必要。
    在这里插入图片描述
  2. #define定义的符号的替换
    这次我们在test.c中的开头通过#define定义了一个常量MAX,在预处理之后又会发生什么呢?
    在这里插入图片描述
    在预编译之后打开test.i文件发现,用于定义MAX的#define语句被删除了,同时程序中的MAX也被替换了。
    在这里插入图片描述
    以上就是预编译的一些文本操作,但是它并不局限于这些功能,博主只是举例了其中的几个,其他的希望感兴趣的你们继续探索。

编译

在编译阶段,我们用gcc -S test.i 将预编译之后的test.i文件进行下一步的编译操作,生成test.s文件。
在这里插入图片描述
打开test.s文件我们发现,这些是我们不太熟悉的东西,但是我们从中可以看到几个我们见过的符号,比如main、g_val、printf等等。其实这些我们所不熟悉的东西是汇编语言,那也就是说在编译阶段,编译器将我们的源文件生成为汇编代码,也可以说是把C代码翻译为汇编代码。

在这其中编译器做了下面的四件事:

  1. 语法分析:检查原C代码中是否存在语法错误,因为要翻译为汇编代码,必须要保证不存在任何的语法错误。
  2. 词法分析:编译器会将C代码每一个语句中的每一个符号拆解出来进行分析,生成语法树之类的东西,然后做相关的处理。
  3. 语义分析:编译器分析我们所写的代码中每一句话想要实现的功能,这样才能更加准确的翻译成汇编代码。
  4. 符号汇总
    关于符号汇总,在这里要详细解释一下:
    先用gcc test.c -C生成一个test.o的文件,在这个文件下我们才能知道编译器汇总了哪些符号。
    在这里插入图片描述
    但是我们发现这里面的东西我们完全看不懂,因为在Linux下这个文件是以elf形式存放的,是二进制文件,但是我们可以用readelf -s命令来查看这个文件。
    在这里插入图片描述
    打开之后我们就可以看到汇总的符号表,对比之后我们可以发现,在符号表中展示出来的都是全局的符号,g_val是我们定义的全局变量、main和Add也相当于一个全局的函数。

以上就是编译阶段所执行的功能。

汇编

通过gcc test.c -C命令生成test.o文件,test.o就是目标文件,在上面的符号汇总我们已经可以看到,test,o中是以二进制形式存放的。也就是说,在编译阶段,会把汇编代码转换成二进制指令。

那么在这个阶段又会执行什么操作呢?

形成符号表

我在VS中写了一些简单的代码来解释:
在这里插入图片描述
在这里插入图片描述
那么在add.c和test.c中我们可以发现有不同的符号。在add.c中,Add这个符号的地址是实际存在的,我们假设其地址为0x100;在test.c中,Add这个符号它只是引用的声明,并没有实际的意义,在符号表中的地址也只是为了进行一个表示,并不实际存在。main这个符号是实际存在的,因此也有实际的地址,假设为0x400。
在这里插入图片描述
到这里我们整个编译过程就结束了,可能有人会问:那上面形成符号表有什么作用呢?我来解释一下:符号表从编译阶段开始到链接是一直存在的,并且不断继承的,每一部分都对符号表有不同的功能。

链接

我们都知道,在链接阶段会生成可执行程序,供我们运行。
在这个部分我主要想解释上面的符号表在链接阶段的作用。

在链接时,要做的一个处理是合并段表,也就是会把形成二进制文件之后生成的各种段进行一个合并,当然也包括符号表。在合并的时候,就那刚才生成的符号表来说,链接器在其中发现有两个相同的符号Add,那么在合并的时候肯定会选择有实际意义的那个Add符号。
也就是说,链接器在链接的时候,对于程序中出现的函数或者变量,会去在符号表中进行查找,看是否已经定义过,如果没有定义的话就会报错,这就是符号表在链接这部分的意义。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值