Loading...
墨滴

公众号:offer多多

2021/08/29  阅读:33  主题:橙心

彻底理解链接器

在介绍本章的主题之前,我们先来看几个问题:

问题一

写C/C++的同学应该经常遇到这样的一个Error:

undefined reference to ABC 在遇到这样的问题时你知道这背后到底哪里出问题了吗? 你通常都能顺利解决类似问题吗?

彻底理解链接器:一,概念

彻底理解链接器:二,符号决议

彻底理解链接器:三,库与可执行文件

彻底理解链接器:四,重定位

彻底理解链接器:一,概念

什么是链接器(Linker) 让我们引用维基百科中对链接器的定义:

"a linker or link editor is a computer utility program that takes one or more object files generated by a compiler and combines them into a single executable file, library file, or another 'object' file."

如果你看不太懂没有关系,我来翻译一下,链接器是一个将编译器产生的目标文件打包成可执行文件或者库文件或者目标文件的程序。这个翻译比较拗口,不太好理解,

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

步骤1:预处理:

gcc -E helloworld.c -o helloworld_pre.c


步骤2: 编译:

将C语言翻译成汇编,主要工作如下:1)词法分析;2)语法分析;3)语义分析 4)优化后生成相应的汇编;


gcc -S helloworld.c -o helloworld.s


步骤3: 汇编:将上一步的汇编代码转换成机器码(mac++hine code),这一步产生的文件叫做目标文件;

gcc -c helloworld.c -o helloworld.o


步骤4:链接:将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
gcc helloworld.c -o helloworld

为了构造可执行文件,链接器完成的任务:

1、符号解析【目的:将每个符号引用和一个符号定义联系起来】

2、重定位【目的:链接器把每个符号定义与一个存储器位置联系起来,接着修改所有对这些符号的引用】

定义:

  1. 将代码和数据收集并组成为单一文件的过程,且使得该文件可以被加载到存储器并执行(目标文件 )

  2. 链接可以发生于[编译]时,也可以发生在[加载]时,也可以执行于[运行]时。

彻底理解链接器:二,符号决议

符号决议

在这个过程当中,链接器需要做的工作就是确保所有目标文件中的符号引用都有唯一的定义。

要想理解这句话我们首先来看看一个典型的c文件里都有些什么。

  1. 目标文件里有什么

代码部分:你可能会想,一个源文件中不都是代码吗,这里的代码指的是计算机可以执行的机器指令,

也就是源文件中定义的所有函数。比如上图中定义的函数fn_b以及fn_c。

数据部分:源文件中定义的全局变量。如果是已经初始化后的全局变量,该全局变量的值也存在于数据部分。

  1. 符号表(Symbol table)
// 定义未初始化的全局变量
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);
}
  • z_global以及fn_a是未定义的,因为在当前文件中,这两个变量仅仅是声明,编译器并没有找到其定义。剩余的变量编译器都可以在当前文件中找到其定义。

  • fn_b以及fn_c为当前文件定义的函数,因为在代码段。

剩余的符号都是全局变量,因此放在了数据段。

-有同学可能会问,为什么全局变量y_global_uninit ,y_global_init以及函数fn_b不可被其它目标文件引用,这是因为这些变量用static修饰过了,

在C语言中经static修饰过的函数的函数以及变量都是当前文件私有的,对外部不可见,这里一定要注意。

  • 所以static这个关键字的用法就是,如果你认为一个变量只应该被当前文件使用而不暴露给外部,那么你就可以使用static关键字修饰一下。

本质上整个符号表只是想表达两件事:

我能提供给其它文件使用的符号

我需要其它文件提供给我使用的符号

  1. 符号决议的过程(唯一的定义。有依赖关系) 在上一节符号表中,我们知道符号表给链接器提供了两种信息,一个是当前目标文件可以提供给其它目标文件使用的符号,另一个其它目标文件需要提供给当前目标文件使用的符号。有了这些信息链接器就可以进行符号决议了。如图所示,假设链接器需要链接三个目标文件:

链接器会依次扫描每一个给定的目标文件,同时链接器还维护了两个集合, 一个是已定义符号集合D,

另一个是未定义符合集合U,下面是链接器进行符合决议的过程:

1,对于当前目标文件,查找其符号表,并将已定义的符号并添加到已定义符号集合D中。

2,对于当前目标文件,查找其符号表,将每一个当前目标文件引用的符号与已定义符号集合D进行对比,如果该符号不在集合D中则将其添加到未定义符合集合U中。

3,当所有文件都扫描完成后,如果为定义符号集合U不为空,则说明当前输入的目标文件集合中有未定义错误,链接器报错,整个编译过程终止。

上面的过程看似复杂, 其实用一句话概括就是只要每个目标文件所引用变量都能在其它目标文件中找到唯一的定义, 整个链接过程就是正确的。

在C语言中,编译器

  • 默认函数和初始化了的全局变量为强符号(Strong Symbol),

  • 未初始化的全局变量为弱符号(Weak Symbol)。

强符号之所以强,是因为它们拥有确切的数据,变量有值,函数有函数体 ; 弱符号之所以弱,是因为它们还未被初始化,没有确切的数据。

readelf -s a.out

#include <stdio.h>
//main.c
int g_data; //全局变量
static int gs_data; //静态全局变量
void hello() {
  printf("hello world"); //全局函数名j
}
int main() {
  static int a = 0; //静态局部变量
  char l_data =0;
  hello();
  return 0;
}

gcc -c main.c (只编译没链接)

root@money:~/test# readelf -s main.o

Symbol table '.symtab' contains 16 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 gs_data
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 a.2253
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    11: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM g_data
    12: 0000000000000000    24 FUNC    GLOBAL DEFAULT    1 hello
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    15: 0000000000000018    29 FUNC    GLOBAL DEFAULT    1 main
root@money:~/test

可以看出全局变量,静态全局变量,静态局部变量,全局函数名都会出现在符号表中,而局部变量不会被保存在符号表中。


  • LOCAL static 修饰。本地文件

static int gs_data;

LOCAL DEFAULT 4 gs_data --全局静态变量

static int a = 0; main

OBJECT LOCAL DEFAULT 4 a.2253 --局部静态变量(只能对具体xx函数内访问)

OBJECT GLOBAL DEFAULT COM g_data

彻底理解链接器:三,库与可执行文件

  1. 静态链接下可执行文件的生成
  1. 动态链接:编译期间(Compile time),load-time dynamic linking ,run-time dynamic linking

我们知道静态库在编译链接期间就被打包copy到了可执行文件, 也就是说静态库其实是在编译期间(Compile time)链接使用的,那么动态库又是在什么时候才链接使用的呢,

动态链接可以在两种情况下被链接使用,

分别是 load-time dynamic linking(加载时动态链接)

以及 run-time dynamic linking(运行时动态链接)

  • readelf的作用是用来查看当前elf文件的符号表,符号表中的信息只包括全局变量和函数名。

彻底理解链接器:四,重定位

-shared -fPIC 疑问

gcc -shared -fPIC -o libvector.so addvec.c multvec.c

/-fPIC,指示编译器生成与位置无关的代码/ /-shared, 指示链接器创建一个共享的目标文件/

gcc -o main2 main2.c ./libvector.so

  • main2在未执行前没有任何有关libvector.so的代码和数据节内容。

链接器只是拷贝了一个重定位和符号表信息,

  • 在运行时可以解析对libvector.so中代码和数据的引用。 main2在运行时可以和libvector.so链接

参考

  • 深入理解计算机系统 第七章 链接 https://segmentfault.com/a/1190000007278405?utm_source=sf-similar-article

公众号:offer多多

2021/08/29  阅读:33  主题:橙心

作者介绍

公众号:offer多多