你知道.c是如何变成.exe的吗

2022-11-23 19:15:51 浏览数 (2)

文章目录[隐藏]

  • 前言
  • 程序的翻译环境和执行环境
    • 一. 程序的翻译环境
      • 1. 编译
        • 1.1 预处理
          • 1.1.1 头文件包含
          • 1.1.2 注释删除
          • 1.1.3 预处理指令的文本替换
        • 1.2 翻译
        • 1.3 汇编
      • 2. 链接
        • 2.1 合并段表
        • 2.2 符号表的合并和重定位
    • 二. 程序的执行环境

前言

今天我们要来探究的内容是一个或者多个源文件(.c)是如何变成一个可执行程序(.exe)的,博主将在Linux环境gcc编译器中进行分步演示,让你深入理解程序环境。

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

在ANSI C的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。 第2种是执行环境,它用于实际执行代码

我们来简单的看下示意图:

一. 程序的翻译环境

我们通常把一个或多个源文件(.c)形成一个(.exe)可执行程序叫做翻译环境,在这个环境中它其实就是将源代码转换为可执行的机器指令。 我们来简单看下形成过程,首先我们创建了一个源文件,并没有编译运行这个程序。

接下来我们运行一下这段程序,我们在源文件目录下发现了Debug文件,点击进入我们看到了.obj目标文件等一些其他文件:

我们返回上一目录,点击进入Debug文件在里面我们发现了.exe可执行程序。

那如果是多个源文件组合在一起,程序运行之后它又会产生几个.obj目标文件.exe可执行程序呢?请看下图例子:

相信大家都知道这两个源文件组合运行起来能得出正确答案,那么它到底生成了几个.obj目标文件.exe可执行程序呢?下面我们一起来观察一下目录。

我们发现目录下出现了两个.obj目标文件,而只生成了一个可执行程序。由此,我们是不是能初步的得出一个小结论:每个源文件经过编译过程都会形成各自的.obj目标文件,但.exe可执行程序只有一个。

下面这幅图就是整个翻译环境中的各个过程了:

翻译环境可分为两个过程:翻译 链接。这里我们的编译器执行编译操作,链接器执行链接操作。

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

1. 编译

关于上述的翻译环境我们只是讲了一个大概,并没有进行深入的分析。 编译分为:预处理、翻译、汇编三个阶段。

下面我将带大家在Linux环境gcc编译器中进行深入的分析每一步的过程,有些读者可能没有学习过Linux环境中的一些命令操作,这没有关系你只要保证自己能听懂就OK。

1.1 预处理

首先我们创建一个test.c的源文件,它的代码显示如下:

而且当前目录下只有一个test.c源文件,ls可以显示到当前目录下有什么文件

接下来输入gcc -E test.c -o test.i这个指令,它代表的意思是预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。这个文件我们可以随便取名,但是为了编码规范我们写成一般的这种形式,比如什么阶段生成什么后缀的文件名,这里就不做过多的赘述了。 执行完上述指令我们查看到当前目录下出现了test.i文件。

1.1.1 头文件包含

接下来输入vim test.i进入test.i文件查看里面的内容,我们发现里面的代码有800多行,那么这里面放的究竟是什么代码呢?

我们在命令模式下输入G跳转至文本末尾,我们看到的情况是这样的

接下来我们在命令模式下按下Shift : ,输入内容vs test.c,此时我们来对比两者一下,你发现什么问题了吗?

我们发现test.i中头文件不见了,但却出现了大量代码,你觉得是什么原因呢?我们想是不是源文件经过预处理将头文件stdio.h的内容全部包含进来了吗?下面我们来证明这个事实。

我们输入vim /usr/include/stdio.h进入到Linux环境中stdio.h头文件中,我们发现有900多行的代码包含在内

接下来我们对比test.istdio.h,发现它们两者之间有些内容确实是一样的,但可能由于其他原因我们观察到的可能不是完全一致,这里我们就不刨根挖底了,我们只需知道test.i里面的这些内容确实就是stdio.h中的就行了。

从这里我们就可以得出一个结论:预处理会将头文件中的内容拷贝进源文件,#include的本质就是把头文件中相关内容直接拷贝至源文件。

那么我就有一个疑问了,我们的stdio.h文件中都有900多行的代码,而你的test.i加上源代码都只有800多行,那么为什么会出现这种情况呢?先把这个问题放一放我们继续分析下面的过程。

1.1.2 注释删除

我将这份代码稍微改动一下,添加几行注释,在test.i里面观察与test.c的变化。

此时我们的test.c文件已经改变,所以接下来我们重新进行gcc -E test.c -o test.i生成test.i文件, 我们发现在预处理过后,test.c里面的注释都被空格替换了。

由此我们得出结论:实际编译器是不关心注释的,所以在预处理阶段是要被删除的;注释只是写给程序员或者其他人员看的,并不参与到程序运算当中。

1.1.3 预处理指令的文本替换

这里以#define为例,我们将代码再稍加修改一下,通过对比我们发现#define对文本进行了直接宏替换,并且预处理完之后就消失了。

那么回到上面那个问题,你知道为什么stdio.h文件的代码行数比test.i中代码数要多了吗

0 人点赞