编译和链接详解

  在我们将源代码打好以后,检查无错,点下“编译并执行”按钮。控制台输出一行又一行,程序随即正常的运行起来。这是编码的日常。

  那么你有没有想过,在这份代码变成一个可以跑起来的程序的过程中,它都经历了什么呢?有人说简单啊,不就是先编译再链接么。本文就详细的对编译链接的过程进行分析,里面或许真的有你不知道的东西呢。

 

我们先大体地概括一下:假设我们现在辛辛苦苦写好了一份代码,它的文件名叫做main.c。首先我们要对它进行编译,生成二进制可重定位的目标文件(main.obj)文件。其中编译还分为三步。然后我们对obj文件进行链接,得到的就是可执行文件.exe啦。具体讲解如下:

  • 1.编译

编译也分三个阶段:预编译,编译,汇编。

①预编译:main.c---->main.i

这个非常好理解,计算机在拿到我们的代码的时候,首先要处理我们的注释,将所有的注释替换为一个空格(并不是删掉)。其次是对我们的所有预编译指令进行处理。什么是预处理?一般来说是以#开头的一系列指令。我们最经常使用的#include就属于预处理啦。预编译之后,我们的main.c就变成了main.i。更多关于预处理的知识请看我的另一篇博客:C语言预编译详解

 

②编译:main.i---->main.s

首先,分析词法语法语义。随后进行代码优化(例如一个变量i连续给别的变量赋值两次,其中i因为没有被赋新值,所以系统把它第一次从内存中拿出来以后,第二次就会直接使用,而不是再从内存中重新拿一次。这就提高了效率,这就是代码优化的一个案例),最后是汇总所有符号。什么又是符号呢?可以理解为每个函数或者数据它各自有的身份证。每个数据都有自己的符号,一个函数则只会生成一个符号,就是它的函数名。所有符号进行汇总,会生成一个符号表。到时候调用这些数据函数都需要从符号表里找,所以它还是很重要的。

还有,内联函数也是在编译这一过程中展开的,并不是预编译。

 

③汇编:main.s---->main.ojb

就是将我们的指令统统转化为特定平台的机器码,让电脑能看得懂啦。汇编完成后就生成我们的二进制可重定位的目标文件。

 

经过我们上面三步,文件的编译就算完成了。此时会生成一个main.obj文件。那为什么到现在我们不能运行它呢?我们来对这个文件进行解剖,看看它里面到底都存了些什么东西。

首先在这个obj文件开始就是一个ELF Header,它里面记录了这个obj的各种信息。在linux中我们可以通过readelf -h命令来访问到它,它里面最重要的是记录了符号表的偏移量和section table段表的偏移量。

.text和.data段依旧是我们熟悉的存放指令和数据的地方(它们和虚拟地址空间中的.text段.data段是一样的),然后接下来我们的.bss段去哪了?这为何是一个comment块呢?其实,我们在查看共有几个段的时候是能看到.bss段的,而且.bss段和.comment段是从同一偏移量开始的。还可以这样?两个段公用一个空间?其实.bss段只是名存实亡,它只是在这挂了个名字,真正的它并不存在文件里。这也是.bss为文件节省空间的特性。因为它里面存放的都是未初始化或初始化为0的数据,所以暂时可以不分配空间。

再往下走就是我们非常重要的符号表了,里面记录了各数据和函数的符号。

下面是section table(假设在这里,它的偏移量是从Header里判断的),段表里记录了所有段的信息,包括每个段的偏移量和大小。所以这也是很重要的一环。

到这里,obj文件应当是讲解结束了。不过别着急,我们来看看我们所使用的源文件main.c。

#include <stdio.h>

int data1 = 1;
int data2 = 0;
int data3;
static int data4 = 2;
static int data5 = 0;
static int data6;

int main ()
{
    int a = 4;
    int b = 0;
    int c;
    static int data7 = 2;
    static int data8 = 0;
    static int data9;

    return 0;
}

可以看到,我定义了很多变量,其中局部变量、全局变量、静态局部变量、静态全局变量应有尽有。

其中data1到data9都是数据,abc则是指令(因为是局部变量,结束后就会销毁)。

数据中初始化且不为零的存在.data段:data1,data4,data7.

数据中初始化为0或没有初始化的存在.bss段:data2,data3,data5,data6,data8,data9

指令存储在.text段。但是一旦使用到abc这时候它们就要加载到栈上了,不过本质还是在.text段存储的。

这里我们重点看.bss段。照理来说这里面应该是有6个变量的,大小就是6*4=24个字节。

但是我在main.obj的readelf -S指令查看段的时候,发现.bss段竟然长度只有20字节,莫名其妙少了一个数据。这是怎么回事呢?

这里额外引入一个新概念:强符号和弱符号(针对C语言)。

在C语言中有这样的规则:初始化过的是强符号,未初始化的是弱符号。同名的符号会选强的那个,如果都是弱符号则选择占用内存大的那个。把这个规则带入到我们的main.c就有如下版本的描述:

假设我这个工程除了main.c还有一个文件叫sum.c,其中在sum.c里我定义了一个全局变量:int data3 = 10;这时sum.c里的data3就是强符号。这时候再去访问data3这个变量就是在访问sum.c里定义的这个了。这就是强符号取代弱符号。同样的,如果我在sum.c中的data3这样定义:long long data3;编译器还是会选择sum.c中的data3,因为虽然他没有初始化,但它占用的内存大。

(注意,上述规则在C++中不生效,C++并不将未初始化的全局变量堪称弱符号,就会导致符号多次定义报错。)

 

现在敏锐的你也许发现了,这消失的数据可能就是data3.未初始化的全局变量,妥妥的软柿子。但是别担心,现在我们的data3还没有被干掉,因为就算有别的文件中定义了另一个data3,我们在没有链接之前谁也看不见谁的文件里有什么东西,编译器只是将data3保存到了*COM*段中。这个段专门存储这种待决议的符号,等到链接的时候才统一处理。所以我们的data3之后的命运将会如何呢?它又是在哪一步重新归来呢?赶紧来看我们接下来的链接过程吧!

 

  • 2.链接

链接,人如其名,首先要做的就是将同一工程下的所有源文件合并到一起这样才能让这个程序完整。

①按段合并。

这里并不是单纯的段的叠加,而是将所有拥有相同权限(属性)的段合并在一个页面。例如,系统就把.data段和.bss段合并在了一起,因为它们都可读可写。将.rodata(用来存放字符串的段)和.text段合并,因为它们都只读。、

②调整偏移

将合并好的段进行偏移调整。

③合并符号表

④符号解析,并对属性为global/local进行判断

这一步非常重要,如果这个符号是local(static修饰过的)的,则不做任何处理。符号解析则是所有的obj文件中的符号表里的符号都要找到符号定义的地方。所以一般我们如果在编程时候使用了一个根本没定义过的变量,或者定义了同名变量,都是在这一步出错的。

那么我们在编译阶段发现data3失踪了,在经历符号解析之后它就会归来(当然,是不是原来那个它就不知道了)。在本工程中我只写了一个main.c源文件,系统将data3存在*COM*段中是说明系统觉得他有被强符号替代的可能性,所以在符号解析阶段系统就把它和其他文件中的同名符号(如果有的话)进行强弱比较,挑选出合适的符号。现在的data3本身就只有它一个叫data3,系统最后发现无可比较,就把它无罪释放啦:P

⑤给符号分配内存

⑥重定位

我们在说obj文件的时候说他是个可重定位的二进制目标文件。这个重定位是什么意思?

在第④步完成后,每个符号都有自己的合法虚拟地址空间了。在没有分配地址之前,从主函数入口访问所有的数据地址都是0,所有的函数地址都是一个偏移量。分配之后,所有的数据符号地址都是它的绝对地址,函数符号存的仍是偏移量,这个偏移量加上下一条语句的地址(pc寄存器中的值)应当等于函数的地址 。往往是调用call指令时。

历经千辛万苦我们终于生成了可执行文件 main.exe了。那它到底为什么能执行呢?它和obj文件内部有什么区别呢?我们再来看看它的解剖图。

它同样也有一个ELF Header,里面记录了main函数的地址。但是和obj文件不一样的是,ELFHeader下面是一个program headers。注意是headers,它是由好几个部分构成共同组成的。其中最重要的就是LOAD页。可以说这就是程序能够运行起来的保证。LOAD页是按页面对齐的,一个页大小为4k。当时在链接时,我们不是将段按属性合并了吗?就是合并在这里了。一个页存.text和.rodata段,另一个页存.data和.bss段。LOAD指明了哪些段在加载时应该放在同一个页中加载。

最后看一下程序运行的过程:

1.创建虚拟地址空间到物理内存的映射

2.创建页目录页表

3.加载代码段数据段

4.将可执行文件入口地址写入pc寄存器内

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值