浅谈链接器

时间:2020-06-07 18:59:35   收藏:0   阅读:36

编译过程简介

C语言的编译过程由五个阶段组成:

gcc -E helloworld.c -o helloworld_pre.c
gcc -S helloworld.c -o helloworld.s
gcc -c helloworld.c -o helloworld.o
gcc helloworld.c -o helloworld

什么是链接器?

链接器是一个将编译器产生的目标文件打包成可执行文件或者库文件或者目标文件的程序。

链接器的作用有点类似于我们经常使用的压缩软WinRAR(Linux下是tar),压缩软件将一堆文件打包压缩成一个压缩文件,而链接器和压缩软件的区别在于链接器是将多个目标文件打包成一个文件而不进行压缩。

写C或者C++的u同学经常遇到这样一个错误:

undefined reference to function ABC.

链接器可操作的元素:目标文件

链接器可操作的最小元素是一个简单的目标文件
从广义上来讲,目标文件与可执行文件的格式几乎是一模一样的,在Linux下,我们把它们统称为ELF文件。

ELF文件标准里面把系统中采用ELF格式的文件归为以下四类:

符号表(Symbol table)

编译器在遇到外部定义的全局变量或者函数时只要能在当前文件找到其声明,编译器就认为编译正确。而寻找使用变量定义的这项任务就被留给了链接器。链接器的其中一项任务就是要确定所使用的变量要有其唯一的定义。虽然编译器给链接器留了一项任务,但为了让链接器工作的轻松一点编译器还是多做了一点工作的,这部分工作就是符号表(Symbol table)。

符号表中保存的信息有两个部分:

编译器在编译过程中每次遇到一个全局变量或者函数名都会在符号表中添加一项,最终编译器会统计一张符号表。

假设C语言源码如下:

// 定义未初始化的全局变量
int g_x_uninit;

// 定义初始化的全局变量
int g_x_init = 1;

// 定义未初始化的全局私有变量,只能在当前文件中使用
static int g_y_uninit;

// 定义初始化的全局私有变量
static int g_y_init = 2;

// 声明全局变量,该变量的定义在其它文件
extern int g_z;

// 函数声明,该函数的定义在其它文件
int fn_a(int x, int y);

// 私有函数定义,该函数只能在当前文件中使用
static int fn_b(int x)
{
    return x + 1;
}

// 函数定义
int fn_c(int local_x)
{
    int local_y_uninit;
    int local_y_init = 3;
    // 对全局变量,局部变量以及函数的使用
    g_x_uninit = fn_a(local_x, g_x_init);
    g_y_uninit = fn_a(local_x, local_y_init);
    local_y_uninit += fn_b(g_z);
    return (g_y_uninit + local_y_uninit);
}

编译器将为此文件统计出如下一张符号表:

名字 类型 是否可被外部引用 区域
g_z 引用,未定义
fn_a 引用,未定义
fn_b 定义 代码段
fn_c 定义 代码段
g_x_init 定义 数据段
g_y_uninit 定义 数据段
g_x_uninit 定义 数据段
g_y_init 定义 数据段

g_z以及fn_a是未定义的,因为在当前文件中,这两个变量仅仅是声明,编译器并没有找到其定义。剩余的变量编译器都可以在当前文件中找到其定义。

本质上整个符号表主要表达两件事:1)我能提供给其它文件使用的符号; 2)我需要其它文件提供给我使用的符号。

目标文件
数据段
代码段
符号表

符号决议

有了符号表,链接器就可以进行符号决议了。如图所示,假设链接器需要链接三个目标文件,如下:

技术分享图片

链接器会依次扫描每一个给定的目标文件,同时链接器还维护了两个集合,一个是已定义符号集合D,另一个是未定义符合集合U,下面是链接器进行符合决议的过程:

链接过程中,只要每个目标文件所引用变量都能在其它目标文件中找到唯一的定义,整个链接过程就是正确的。

若链接器在查找了所有目标文件的符号表后都没有找到函数,因此链接器停止工作并报出错误undefined reference to function A

库与可执行文件

链接器根据目标文件构建出库(动态库、静态库)或可执行文件。

给定目标文件以及链接选项,链接器可以生成两种库,分别是静态库以及动态库,如下图所示,给定同样的目标文件,链接器可以生成两种不同类型的库。

技术分享图片

静态库

静态库在Windows下是以.lib为后缀的文件,Linux下是以.a为后缀的文件。

静态库是链接器通过静态链接将其和其它目标文件合并生成可执行文件的,而静态库只不过是将多个目标文件进行了打包,在链接时只取静态库中所用到的目标文件。

目标文件分为三段:代码段、数据段、符号表,在静态链接时可执行文件的生成过程如下图所示:

技术分享图片

可执行文件的特点如下:

可执行文件和目标文件没有什么本质的不同,可执行文件区别于目标文件的地方在于,可执行文件有一个入口函数,这个函数也就是我们在C语言当中定义的main函数,main函数在执行过程中会用到所有可执行文件当中的代码和数据。main函数是被操作系统调用。

动态库

静态库在编译链接期间就被打包copy到了可执行文件,也就是说静态库其实是在编译期间(Compile time)链接使用的。
动态链接可以在两种情况下被链接使用,分别是加载时动态链接(load-time dynamic linking)运行时动态链接 (run-time dynamic linking)

可以使用特定的API来运行时加载动态库,在Windows下通过LoadLibrary或者LoadLibraryEx,在Linux下通过使用dlopen、dlsym、dlclose这样一组函数在运行时链接动态库。当这些API被调用后,同样是首先去找这些动态库,将其从磁盘copy到内存,然后查找程序依赖的函数是否在动态库中定义。这些过程完成后动态库中的代码就可以被正常使用了。

在动态链接下,可执行文件当中会新增两段,即dynamic段以及GOT(Global offset table)段,这两段内容就是是我们之前所说的必要信息。

dynamic 段中保存了可执行文件依赖哪些动态库,动态链接符号表的位置以及重定位表的位置等信息。
当加载可执行文件时,操作系统根据dynamic段中的信息即可找到使用的动态库,从而完成动态链接

参考

微信公共号

NFVschool,关注最前沿的网络技术。
技术分享图片

原文:https://www.cnblogs.com/kekukele/p/13061439.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!