动态链接那些事

1、为什么要动态链接

1.1 空间浪费

  对于静态链接来说,在程序运行之前,会将程序所需的所有模块编译、链接成一个可执行文件。这种情况下,如果 Program1 和 Program2 都需要用到 Lib.o 模块,那么,内存中和磁盘中实际上就存在了两份 Lib.o 的代码。当共享的模块基数变得很大时,空间浪费无法想象。

1.2 更新困难

  动态链接对程序的更新、部署和发布也会带来很多麻烦。比如 Program1 所使用的 Lib.o 是由一个第三方厂商提供的,当该厂商更新了 Lib.o 的时候,那么 Program1 的厂商就需要拿到最新版的 Lib.o,然后将其与 Program.o 链接后,将新的 Program1 整个发布给用户。这样做的缺点很明显,即一旦程序中有任何模块更新,整个程序就要重新链接、发布给用户。

1.3 动态链接

  要解决空间浪费和更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序地目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。

2、简单的动态链接例子

2.1 简单例子

我们先实现一个最简单的动态链接库的例子,感受一下。

program1.c文件的内容:

#include "Lib.h"

int main()
{
    foobar(1);
    return 0;
}

program2.c文件的内容:

#include "Lib.h"

int main()
{
    foobar(2);
    return 0;
}

Lib.h文件的内容:

#ifndef LIB_H
#define LIB_H

void foobar(int i);

#endif

Lib.c文件的内容:

#include <stdio.h>
void foobar(int i)
{
    printf("Printing from Lib.so %d\n", i);
}

program1.c 和 program2.c 都调用了 Lib.c 里面的 foobar 函数。为了在内存中加载一次 Lib.c,使 program1 和 program2 共享。我们可以将 Lib.c 编译成共享对象(动态库)。

这里需要强调一下,这里所谓的共享,并不是共享整个 Lib.c 的内容,而是特指共享它的代码部分。 对于 Lib.c 中的数据部分,每个进程都需要一份自己的拷贝,因为它们可能需要独立地修改 Lib.c 中的数据。

先将 Lib.c 编译成共享对象:

gcc -fPIC -shared -o Lib.so Lib.c

-shared 表示的是产生共享对象。 -fPIC 的含义暂时先不用管,待会儿再说。

现在,我们来分别编译 Program1 和 Program2:

gcc -o Program1 Program1.c ./Lib.so

gcc -o Program2 Program2.c ./Lib.so

现在执行./Program1就可以执行,并看到如下输出:

Attention :注意上一步骤中,我们使用了 ./Lib.so,来指定编译链接时搜索库的路径、装载时指定的动态库搜索路径。
现代链接器在处理动态库时将 链接时路径(Link-time path)和 运行时路径(Run-time path)分开
实际上,这一步骤可以拆解成以下:

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc -fPIC -shared -o libtest.so Lib.c
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc -o Program1 Program1.c -L./ -ltest -Wl,-rpath,./
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./Program1
Printing from Lib.so 1
  • -Ldir:制定链接时搜索库的路径。比如你自己的库,可以用它制定目录,不然链接器将只在标准库的目录找。这个dir就是目录的名称

  • -Wl,option:此选项传递 option 给链接程序,指定运行时动态库路径,链接程序将动态库的路径包含在可执行文件中;如果 option 中间有逗号, 就将 option 分成多个选项, 然后传递给会链接程序

  • -ltest:指定程序所依赖的库的名字。链接程序将这个名字写到可执行文件中

为什么链接的时候需要指定动态库路径?
1、程序需要知道动态库的名称, 所以需要指定动态库的名字
2、程序编译的时候,要确定动态库中是否一定含有所需的符号,否则将会报错。起到了程序编译时候的编译错误检查作用。
参考 stackoverflow 文章

可以使用 readelf 查看 dynamic 段,其中会有可执行文件依赖的动态库(NEEDED)以及动态库的运行时路径(RUNPATH):

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -d Program1

Dynamic section at offset 0x2da8 contains 29 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libtest.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000001d (RUNPATH)            Library runpath: [./]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x1164
 0x0000000000000019 (INIT_ARRAY)         0x3d98
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x3da0
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x3b0
 0x0000000000000005 (STRTAB)             0x480
 0x0000000000000006 (SYMTAB)             0x3d8
 0x000000000000000a (STRSZ)              157 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x3fb8
 0x0000000000000002 (PLTRELSZ)           24 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x620
 0x0000000000000007 (RELA)               0x560
 0x0000000000000008 (RELASZ)             192 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000000000001e (FLAGS)              BIND_NOW
 0x000000006ffffffb (FLAGS_1)            Flags: NOW PIE
 0x000000006ffffffe (VERNEED)            0x530
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x51e
 0x000000006ffffff9 (RELACOUNT)          3
 0x0000000000000000 (NULL)               0x0

指定运行时动态库路径常见方法:
(1)gcc 参数指定 -Wl,-rpath = ${LD_PATH}
(2)配置文件 /etc/ld.so.conf 文件中添加库的搜索路径
(3)设置环境变量 export LD_LIBRARY_PATH=${LD_PATH}

执行 Program1 时,操作系统会首先在我们的虚拟进程空间中加载进一个动态链接器,动态链接器帮我们完成链接任务,然后我们的程序就开始执行了。
在这里插入图片描述

解析:
  Lib.c 被编译成 libtest.so 共享对象文件,Program1.c 被编译成 Program1.o 后,链接成可执行程序 Program1。

  上图中有一个步骤与静态链接不一样,那就是 Program1.o 被链接成可执行文件这一步,在静态链接中,这一步链接过程会把 Program1.o和 Lib.o 链接到一起,并且输出可执行文件 Program1。但在这里 Lib.o 没有被链接进来,链接的输入目标文件只有 Program1.o (当然还有C语言运行库,我们这里暂时忽略),但是从前面的命令行中我们看到,Lib.so也参与了链接过程这是怎么回事呢?

  让我们回到动态链接的机制上来,当程序模块 Program1.c 被编译成 Program1.o 时,编译器还不知道 foobar() 函数的地址。当连接器将 Program1.o 链接成可执行文件时,这时候连接器必须确定 Program1.o 所引用的 foobar() 函数的性质。如果 foobar() 是一个定义在其静态目标模块中的函数,那么链接器将会按照静态链接的规则,将 Program1.o 中的 foobar 地址引用重定位;如果 foobar() 是定义在某个动态共享对象中的函数,那么链接器就会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位, 而是在装载的时候再进行重定位

可以使用 readelf 解析出 Program1 中的符号表:其中 .dynsym 为动态符号表,包含于 .symtab 全局符号表中

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -s Program1

Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _[...]@GLIBC_2.34 (2)
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND foobar
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
     6: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND [...]@GLIBC_2.2.5 (3)

Symbol table '.symtab' contains 36 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS Scrt1.o
     2: 000000000000038c    32 OBJECT  LOCAL  DEFAULT    4 __abi_tag
     3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
     4: 0000000000001090     0 FUNC    LOCAL  DEFAULT   16 deregister_tm_clones
     5: 00000000000010c0     0 FUNC    LOCAL  DEFAULT   16 register_tm_clones
     6: 0000000000001100     0 FUNC    LOCAL  DEFAULT   16 __do_global_dtors_aux
     7: 0000000000004010     1 OBJECT  LOCAL  DEFAULT   26 completed.0
     8: 0000000000003db0     0 OBJECT  LOCAL  DEFAULT   22 __do_global_dtor[...]
     9: 0000000000001140     0 FUNC    LOCAL  DEFAULT   16 frame_dummy
    10: 0000000000003da8     0 OBJECT  LOCAL  DEFAULT   21 __frame_dummy_in[...]
    11: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS Program1.c
    12: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    13: 00000000000020e0     0 OBJECT  LOCAL  DEFAULT   20 __FRAME_END__
    14: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    15: 0000000000003db8     0 OBJECT  LOCAL  DEFAULT   23 _DYNAMIC
    16: 0000000000002004     0 NOTYPE  LOCAL  DEFAULT   19 __GNU_EH_FRAME_HDR
    17: 0000000000003fb8     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_
    18: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_mai[...]
    19: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]
    20: 0000000000004000     0 NOTYPE  WEAK   DEFAULT   25 data_start
    21: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    22: 0000000000001164     0 FUNC    GLOBAL HIDDEN    17 _fini
    23: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND foobar
    24: 0000000000004000     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    25: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    26: 0000000000004008     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    27: 0000000000002000     4 OBJECT  GLOBAL DEFAULT   18 _IO_stdin_used
    28: 0000000000004018     0 NOTYPE  GLOBAL DEFAULT   26 _end
    29: 0000000000001060    38 FUNC    GLOBAL DEFAULT   16 _start
    30: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    31: 0000000000001149    25 FUNC    GLOBAL DEFAULT   16 main
    32: 0000000000004010     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
    33: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
    34: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@G[...]
    35: 0000000000001000     0 FUNC    GLOBAL HIDDEN    12 _init

  那么链接器如何知道 foobar 的引用是一个静态符号还是一个动态符号呢?这就是为什么在编译的时候要用到 Lib.so 的原因。Lib.so 中保存了完整的符号信息,把 Lib.so 作为链接的输入文件之一,链接器在解析符号时就可以知道 foobar 是一个定义在 Lib.so 的动态符号,这样链接器就可以对 foobar 的引用做特殊的处理,使它成为一个动态符号的引用。

关于模块
在静态链接时,整个程序最终只有一个可执行文件,它是一个不可以分割的整体;但是在动态链接下,一个程序被分成了若干个文件,有程序的主要部分,
即可执行文件(Program1)和程序所依赖的共享对象(Lib.so),很多时候,我们也把这部分称为模块,
即动态链接下的可执行文件和共享对象都可以看作是程序的一个模块

2.2 动态链接程序运行时地址空间分布

  对于静态链接的可执行文件来说,整个进程只有一个文件要被映射,即可执行文件。而对于动态链接,除了可执行文件,还有它所依赖的共享目标文件。
还是以上面的 Program1 为例,对 Lib.c 稍作修改:

#include <stdio.h>
void foobar(int i)
{
    printf("Printing from Lib.so %d\n", i);
    sleep(-1);
}

然后就可以查看进程的虚拟地址空间分布:

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./Program1 &
[1] 4801
Printing from Lib.so 1
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ cat /proc/4801/maps
5636a1ef2000-5636a1ef3000 r--p 00000000 08:03 2883771                    /home/liangjie/Desktop/cfp/Program1
5636a1ef3000-5636a1ef4000 r-xp 00001000 08:03 2883771                    /home/liangjie/Desktop/cfp/Program1
5636a1ef4000-5636a1ef5000 r--p 00002000 08:03 2883771                    /home/liangjie/Desktop/cfp/Program1
5636a1ef5000-5636a1ef6000 r--p 00002000 08:03 2883771                    /home/liangjie/Desktop/cfp/Program1
5636a1ef6000-5636a1ef7000 rw-p 00003000 08:03 2883771                    /home/liangjie/Desktop/cfp/Program1
5636a1f8f000-5636a1fb0000 rw-p 00000000 00:00 0                          [heap]
7fde22c00000-7fde22c28000 r--p 00000000 08:03 4988401                    /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22c28000-7fde22dbd000 r-xp 00028000 08:03 4988401                    /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22dbd000-7fde22e15000 r--p 001bd000 08:03 4988401                    /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22e15000-7fde22e19000 r--p 00214000 08:03 4988401                    /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22e19000-7fde22e1b000 rw-p 00218000 08:03 4988401                    /usr/lib/x86_64-linux-gnu/libc.so.6
7fde22e1b000-7fde22e28000 rw-p 00000000 00:00 0 
7fde22ea3000-7fde22ea6000 rw-p 00000000 00:00 0 
7fde22eb5000-7fde22eb6000 r--p 00000000 08:03 2883703                    /home/liangjie/Desktop/cfp/Lib.so
7fde22eb6000-7fde22eb7000 r-xp 00001000 08:03 2883703                    /home/liangjie/Desktop/cfp/Lib.so
7fde22eb7000-7fde22eb8000 r--p 00002000 08:03 2883703                    /home/liangjie/Desktop/cfp/Lib.so
7fde22eb8000-7fde22eb9000 r--p 00002000 08:03 2883703                    /home/liangjie/Desktop/cfp/Lib.so
7fde22eb9000-7fde22eba000 rw-p 00003000 08:03 2883703                    /home/liangjie/Desktop/cfp/Lib.so
7fde22eba000-7fde22ebc000 rw-p 00000000 00:00 0 
7fde22ebc000-7fde22ebe000 r--p 00000000 08:03 4988059                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fde22ebe000-7fde22ee8000 r-xp 00002000 08:03 4988059                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fde22ee8000-7fde22ef3000 r--p 0002c000 08:03 4988059                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fde22ef4000-7fde22ef6000 r--p 00037000 08:03 4988059                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fde22ef6000-7fde22ef8000 rw-p 00039000 08:03 4988059                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffe759f3000-7ffe75a14000 rw-p 00000000 00:00 0                          [stack]
7ffe75b22000-7ffe75b26000 r--p 00000000 00:00 0                          [vvar]
7ffe75b26000-7ffe75b28000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

  我们可以看到,整个进程虚拟地址空间中,相比与静态链接多了几个文件的映射。Lib.so 和 Program1 一样,它们都是被操作系统用同样的方法映射至进程的虚拟地址空间,只是它们占据的虚拟地址和长度不同。Program1 除了使用 Lib.so之外,它还用到了动态链接形式的C语言运行库 libc.so.6。另外还有一个值得关注的共享对象就是 ld-linux-x86-64.so.2,它实际上是Linux下的动态链接器。动态链接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行 Program1 之前(这时候已经完成了装载),首先会把控制权交给动态链接器,由它完成所有的动态链接工作,完成之后再把控制权交给 Program1,然后 Program1 程序开始执行。

3、地址无关代码

3.1 固定装载地址的困扰

关于共享目标文件在内存中的地址分配,主要有两种解决方案,分别是:

  • 静态共享库(Static Shared Library)(地址固定)
  • 动态共享库(Dynamic Shared Libary)(地址不固定)

静态共享库
  静态共享库的做法是将程序的各个模块统一交给操作系统进行管理,操作系统在某个特定的地址划分出一些地址块,为那些已知的模块预留足够的空间。因为这个地址对于不同的应用程序来说,都是固定的,所以称之为静态。
  但是静态共享库的目标地址会导致地址冲突、升级等问题。

  • 地址冲突问题:某个模块被多个程序使用,甚至多个模块被多个程序使用,那么管理这些模块的地址将是一件无比繁琐的事情。例如程序 1 需要在 0x100 - 0x200 装载模块 A。程序 1 暂停,切换到程序 2 ,程序 2 不需要依赖模块 A ,所以在 0x100 - 0x200 装载了模块 B。这时,模块 A 和 模块 B 绝对地址已经固定,无法再装载在其他地方。如果这时来了一个程序 3,即依赖模块 A,又依赖模块 B,就会发生地址冲突。
  • 升级问题:升级后的共享库必须保持共享库中全局函数和变量地址不变,如果应用程序在链接时已经绑定了这些地址,一旦更改,就必须重新链接应用程序,否则会引起应用程序崩溃。即使不变,只是增加了一些全局函数和变量,也会收到限制,因为静态共享库被分配到的虚拟地址空间有限,不能增长太多,否则可能会超出被分配的空间。

动态共享库

  采用动态共享库的方式,也称为装载时重定位(Load Time Relocation)。其基本思路是:在链接时,对所有绝对地址的引用都不作重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。

3.2 装载时重定位

  采用动态共享库的方式,也称为装载时重定位(Load Time Relocation)。其基本思路是:在链接时,对所有绝对地址的引用都不作重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。

  我们前面在静态链接时提到过重定位,那儿的重定位叫做链接时重定位(Link Time Relocation),而现在这种情况经常被称为装载时重定位(Load Time Relocation)。在 Windows 中,又叫基址重置(Rebasing),区别于静态链接的链接时重定位-link time relocation

  但是这种方式也存在一些问题。比如,动态链接模块被装载映射至虚拟空间后,指令部分是在多个进程间共享的,由于装载时重定位的方法需要修改指令,所以没有办法做到同一份指令被多个进程共享,因为指令被重定位后对于每个进程来说都是不同的。
Attention:关于上面一句话的理解

共享对象也就是动态链接库在被装载到物理内存后,始终是只有一份的,不管有多少个进程使用它。但是对于每一个进程,共享对象会映射一次到虚拟地址空间,也就是每个进程空间都有一份共享对象的映射,此时,对于不同的进程,映射的地址(基址)是不一样的(大部分情况下)


紧接着,进行装载时重定位。装载时重定位由动态链接器完成,动态链接器会被一起映射到进程空间中。它会修改共享对象中的指令,为什么会修改指令,原因在于绝对地址访问(如模块内的变量访问)是直接用 move 指令(x86)完成的。
例如:move addr 0x10,将值 0x10 放到地址 addr 中,addr 是共享对象中的全局变量地址。因为对于不同的进程,共享对象映射的地址(基址)是不一样的,所以对于 move 指令的地址部分 addr,修改也是不一样的。因此无法共享

  虽然,动态链接库中的代码是共享的,但是其中的可修改数据部分对于不同进程来说是由多个副本的,所以它们可以采用装载时重定位的方法来解决。

  基于上面提到的 “没有办法做到同一份指令被多个进程共享,因为指令被重定位后对于每个进程来说都是不同的” 问题,一种名为地址无关代码的技术被提出以克服这个问题。

  Linux 和 GCC 支持这种装载时重定位的方法,我们前面在产生共享对象时,使用了两个 GCC 参数“-shared”和“-fPIC”,如果只使用“-shared”,那么输出共享对象就是使用了装载时重定位的方法。

3.3 地址无关码

  基本思路是把指令中那些需要被修改的部分分离出来,跟数据部分放到一起,这样,剩下的指令就可以保持不变,而数据部分在每个进程中拥有一个副本。ELF 针对各种可能的访问类型(模块内部指令调用、模块内部数据访问、模块间指令调用、模块间数据访问),实现了对应地址引用方式,从而实现了PIC(Position-independent Code)。

  共享对象模块中的地址引用按照是否为跨模块分为两类:模块内部引用、模块外部引用。按照不同的引用方式又可分为:指令引用、数据引用。以如下代码为例,可得出如下四种类型:

/*
 *  pic.c
 */

static int a;
extern int b;
extern void ext();

void bar()
{
	a = 1;
	b = 2;
}

void foo()
{
	bar();
	ext();
}
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc -fPIC -shared -o libpic.so pic.c

类型1:模块内部的函数调用
  由于被调用的函数与调用者都处于同一模块,它们之间的相对位置是固定的。对于现代的系统来说,模块内部的调用都可以是相对地址调用,或者是基于寄存器的相对调用,所以对于这种指令是不需要重定位的。

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -d libpic.so 
	......
0000000000001070 <bar@plt>:
    1070:	f3 0f 1e fa          	endbr64 
    1074:	f2 ff 25 a5 2f 00 00 	bnd jmp *0x2fa5(%rip)        # 4020 <bar+0x2ee7>
    107b:	0f 1f 44 00 00  
	......
000000000000115b <foo>:
    115b:	f3 0f 1e fa          	endbr64 
    115f:	55                   	push   %rbp
    1160:	48 89 e5             	mov    %rsp,%rbp
    1163:	b8 00 00 00 00       	mov    $0x0,%eax
    1168:	e8 03 ff ff ff       	call   1070 <bar@plt>
    116d:	b8 00 00 00 00       	mov    $0x0,%eax
    1172:	e8 e9 fe ff ff       	call   1060 <ext@plt>
    1177:	90                   	nop
    1178:	5d                   	pop    %rbp
    1179:	c3                   	ret    
	......

  foo 中对 bar 的调用的那条指令实际上是一条相对地址调用指令。只要 bar 和 foo 的相对位置不变,这条指令是地址无关的。即无论模块被装载到哪个位置,这条指令都是有效的,这种相对地址的方式对于 jmp 指令也有效。

注:这里面的关于 “< bar@plt >”,在后面的 PLT 章节会去详细讲解,这里就把它理解成 < bar > 就行了

类型2:模块内部的数据(static)访问,如模块中定义的全局静态变量
  一个模块前面一般是若干个页的代码,后面紧跟着若干个页的数据,这些页之间的相对位置是固定的,即任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,所以只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据了。

  反汇编 libpic.so

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -d libpic.so 
0000000000001139 <bar>:
    1139:	f3 0f 1e fa          	endbr64 
    113d:	55                   	push   %rbp
    113e:	48 89 e5             	mov    %rsp,%rbp
    1141:	c7 05 e9 2e 00 00 01 	movl   $0x1,0x2ee9(%rip)        # 4034 <a>
    1148:	00 00 00 
    114b:	48 8b 05 86 2e 00 00 	mov    0x2e86(%rip),%rax        # 3fd8 <b>
    1152:	c7 00 02 00 00 00    	movl   $0x2,(%rax)
    1158:	90                   	nop
    1159:	5d                   	pop    %rbp
    115a:	c3                   	ret    

以访问 a 变量为例:

    1141:	c7 05 e9 2e 00 00 01 	movl   $0x1,0x2ee9(%rip)        # 4034 <a>

%rip 寄存器保存的是下一条指令的地址”0x114b”(因为这是个相对地址,所以用引号扩住)

a 的访问地址为(这里是基于模块 装载地址为 0 来计算的):0x2ee9(固定偏移量) + 0x114b(当前指令即 PC 值) = 0x4034

固定偏移量 0x2ee9 是模块 libpic.so 在编译时就算好的

我们使用 readelf -S 查看 libpic.so 中各个 section 的地址,发现 0x4034 刚好在 .bss 段,符合未初始化的静态变量在 .bss 段事实。

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -S libpic.so 
There are 27 section headers, starting at offset 0x3568:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  		......
  [21] .data             PROGBITS         0000000000004028  00003028
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .bss              NOBITS           0000000000004030  00003030
       0000000000000008  0000000000000000  WA       0     0     4
  		......

当然了,模块 libpic.so 的装载地址肯定不是0。在实际装载时,会确定模块的装载地址,那么变量 a 的访问地址为:

装载地址 + 0x4034

类型3:模块间数据访问
  模块间的数据访问比模块内部稍微麻烦一些,因为模块间的数据访问目标地址要等到装载时才决定。此时,动态链接需要使用代码无关地址技术,其基本思想是把地址相关的部分放到数据段。ELF 的实现方法是:在数据段中建立一个指向这些变量的指针数组,也称为全局偏移表(Global Offset Table,GOT),当代码需要引用该全局变量时,可以通过 GOT 中相对应的项间接引用。过程示意图如下所示
在这里插入图片描述

  当指令中需要访问变量 b 时,程序会先找到 GOT,然后根据 GOT 中变量所对应的项找到变量的目标地址。每个变量都对应一个4字节的地址,链接器在装载模块的时候会查找每个变量所在的地址,然后填充 GOT 中的各个项,以确保每个指针所指向的地址正确。由于 GOT 本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以由独立的副本,相互不受影响。

  我们回顾刚才函数 bar()的反汇编代码。为访问变量 b ,我们程序首先计算出变量 b 在 got 中的位置,即

0x1152(%rip,也就是 PC 值) + 0x2e86 (固定偏移)= 0x3fd8

  然后使用寄存器间接寻址方式给变量 b 赋值2。

0000000000001139 <bar>:
    1139:	f3 0f 1e fa          	endbr64 
    113d:	55                   	push   %rbp
    113e:	48 89 e5             	mov    %rsp,%rbp
    1141:	c7 05 e9 2e 00 00 01 	movl   $0x1,0x2ee9(%rip)        # 4034 <a>
    1148:	00 00 00 
    114b:	48 8b 05 86 2e 00 00 	mov    0x2e86(%rip),%rax        # 3fd8 <b>
    1152:	c7 00 02 00 00 00    	movl   $0x2,(%rax)
    1158:	90                   	nop
    1159:	5d                   	pop    %rbp
    115a:	c3                   	ret 

  我们可以用 objdump 来查看 got 表位置:

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -h libpic.so 
......
 18 .got          00000028  0000000000003fd8  0000000000003fd8  00002fd8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .got.plt      00000028  0000000000004000  0000000000004000  00003000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
......

  可以看到 got 在文件中的偏移是 0x3fd8,我们再来看看 libpic.so 需要在动态链接时的重定位项:

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -R libpic.so 
......
0000000000003fd8 R_X86_64_GLOB_DAT  b
.....

这里的 R_X86_64_GLOB_DAT 含义:一旦知道变量 b 的运行时地址,就把它放入 0x3fd8 处。

  可以看到变量 b 的地址需要重定位,它的地址位于 0x3fd8,也就是 GOT 中偏移0表项(GOT[0]),相当于是 GOT 中的第一项(每4字节一项)。这也就有上面反汇编中的对 b 赋值语句:

  • 将 0x2e86 + PC 的值(就是变量 b 在 got 表中的表项地址,表项里存储的就是变量 b 的实际地址)写到寄存器 rax 中
  • 将立即数 2,赋值给 rax 寄存器中地址指向的值(也就是变量 b)
......
    114b:	48 8b 05 86 2e 00 00 	mov    0x2e86(%rip),%rax        # 3fd8 <b>
    1152:	c7 00 02 00 00 00    	movl   $0x2,(%rax)
 ......

类型4:模块间调用、跳转
  对于模块间函数调用,同样可以采用类型3的方法来解决。与上面的类型有所不同的是,GOT 中响应的项保存的是目标函数的地址,当模块需要调用目标函数时,可以通过 GOT 中的项进行间接跳转。

总结:
  指令中的有些地址要在装载时才能确定,也就是不同的进程可能有不同的地址。
  之前我们已经解释过,共享对象的数据段,是每个进程一份的。而数据段和代码段的相对位置又是确定的。
  由此,我们就可以在数据段中建立一个指针数组,称其为 GOT,global offset table。里面存放跨模块的数据的地址,当然,可以在装载时动态填入。然后,共享对象指令中对跨模块数据的访问,可以通过 GOT 中的指针间接访问。
  这样的好处是,指令中的地址就从跨模块数据的地址,变成了 GOT 中指针的地址,而这个地址是相对代码段确定的。
  以上,就是动态链接最最最核心的思想

3.4 共享模块全局变量问题

  共享对象代码段中,对模块内全局数据(非 static)的访问,也是通过 GOT 实现的。 既然是模块内,为啥不用相对地址呢?

  因为其他的模块可能会使用全局数据。比如 module.c 中这样的代码:

extern int global;

int foo()
{
 	global = 1;
}

int main()
{

foo();

return 0;
}

我们对 module.c 进行编译,将他编译成一个目标文件 module.o

liang@liang-virtual-machine:~/cfp$ gcc -c -fno-stack-protector module.c

随后使用 ld 对其进行链接,链接对象是一个动态库,且动态库中定义了全局变量 global,如下

liang@liang-virtual-machine:~/cfp$ gcc -fPIC -shared -o libtest.so libtest.c
liang@liang-virtual-machine:~/cfp$ cat ./libtest.c
int global = 4;

int add(int a, int b)
{
    global = a + b;
    return global;
}
liang@liang-virtual-machine:~/cfp$

使用 ld 对其进行链接,链接成可执行文件 module

liang@liang-virtual-machine:~/cfp$ ld -e main module.o  -o module  ./libtest.so 
liang@liang-virtual-machine:~/cfp$ 

在 module.c 这个代码中,对 global 进行了赋值,既然是赋值,肯定需要 global 的地址,但是编译时 (这里只进行了编译,没有链接),gcc 并不知道它在共享对象中定义了。因此,gcc会在 bss 段中定义 global,也就是说,在编译 module.c 时,就为 global 分配了虚拟内存地址。这样,如果加载共享模块 libtest.so 后,加载的模块中(数据段)也有该变量的副本,肯定会产生矛盾。

既然有可能出现这种情况,干脆,让共享对象访问自身的全局变量时,也通过 got 的方式,就避免了进程中存在多个 global 的可能,即在装载时,将 global 的虚拟内存地址存入共享变量中的 got 中。

  • 这样,如果运行时动态加载的时候,发现可执行文件中也有该变量,则会统一在 GOT 表中重定位填充为可执行文件 bss 段中该变量副本的地址。
  • 如果在共享库中对该变量进行了初始化,动态装载器还得负责将初始化的值拷贝到可执行文件bss中该变量的副本位置。
  • 如果可执行文件中没有该变量,则 GOT 表中重定位后,指向自己模块内的该变量。这样就意味着对模块内的变量访问,也采用了 GOT 表。也就是说,对于共享库中的全局对象,无论是否是内部的,还是无法决定是否是内部的,都得作为外部模块访问那样,使用 GOT 表进行访问。

我们可以使用 objdump 工具验证上述结论(共享对象访问自身的全局变量时,也是通过 got 的方式):
可以看到,got 表的范围为 0x200fd0 - 0x201000,

liang@liang-virtual-machine:~/cfp$ objdump -h libtest.so 
......
 18 .got          00000030  0000000000200fd0  0000000000200fd0  00000fd0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .got.plt      00000018  0000000000201000  0000000000201000  00001000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
......

我们再查看 动态可重定位表,我们发现需要重定位项 global,需要修复的地址刚好是 got 范围内,且刚好是 got[1] 条目1(.got section的大小为0x30——即.got中的条目个数为6(.got的每个条目占8字节))。

liang@liang-virtual-machine:~/cfp$ objdump -R libtest.so 

libtest.so:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE 
0000000000200e28 R_X86_64_RELATIVE  *ABS*+0x0000000000000660
0000000000200e30 R_X86_64_RELATIVE  *ABS*+0x0000000000000620
0000000000201018 R_X86_64_RELATIVE  *ABS*+0x0000000000201018
0000000000200fd0 R_X86_64_GLOB_DAT  _ITM_deregisterTMCloneTable
0000000000200fd8 R_X86_64_GLOB_DAT  global@@Base
0000000000200fe0 R_X86_64_GLOB_DAT  __gmon_start__
0000000000200fe8 R_X86_64_GLOB_DAT  _Jv_RegisterClasses
0000000000200ff0 R_X86_64_GLOB_DAT  _ITM_registerTMCloneTable
0000000000200ff8 R_X86_64_GLOB_DAT  __cxa_finalize@GLIBC_2.2.5

R_X86_64_GLOB_DAT的含义:一旦知道 global 的运行时地址,就把它放入 0x200fd8 处。
我们还可以反汇编 add 函数代码,看看是如何访问 global 变量的:

0000000000000690 <add>:
 690:	55                   	push   %rbp
 691:	48 89 e5             	mov    %rsp,%rbp
 694:	89 7d fc             	mov    %edi,-0x4(%rbp)
 697:	89 75 f8             	mov    %esi,-0x8(%rbp)
 69a:	8b 55 fc             	mov    -0x4(%rbp),%edx
 69d:	8b 45 f8             	mov    -0x8(%rbp),%eax
 6a0:	01 c2                	add    %eax,%edx
 6a2:	48 8b 05 2f 09 20 00 	mov    0x20092f(%rip),%rax        # 200fd8 <_DYNAMIC+0x198>
 6a9:	89 10                	mov    %edx,(%rax)
 6ab:	48 8b 05 26 09 20 00 	mov    0x200926(%rip),%rax        # 200fd8 <_DYNAMIC+0x198>
 6b2:	8b 00                	mov    (%rax),%eax
 6b4:	5d                   	pop    %rbp
 6b5:	c3                   	retq  

由上可见,对于 global 变量的访问,实际上是访问地址 0x200fd8 中的地址所指向的值

3.5 数据段地址无关性

  通过上面的方法,我们能保证共享对象中的代码部分地址无关,但是数据部分是不是也有绝对地址引用的问题呢?
  这里我们还是用上面地址无关码的例子,稍加改动:

static int a;
extern int b;
extern void ext();

static int*p=&a;

void bar()
{
	a = 1;
	b = 2;
	
	b = *p;
}

void foo()
{
	bar();
	ext();
}

  上面的地址无关码的例子里面加了这样一段代码的话

static int*p=&a;

  那么指针 p 指向就是一个绝对地址,它指向变量 a,而变量 a 的地址会随着共享对象的装载地址改变而改变。那么有什么办法解决这个问题呢?

  对于数据段来说,它在每个进程都有一份独立的副本,所以并不担心被进程改变。从这点来看,我们可以选择装载时重定位的方法来解决数据段中绝对地址引用问题。对于共享对象来说,如果数据段中有绝对地址引用,那么编译器和链接器就会产生一个重定位表(叫做rela.dyn),这个重定位表里面包含了 “R_X86_64_RELATIVE” 类型的重定位入口,用于解决上述问题。当动态链接器装载共享对象时,如果发现该共享对象有这样的重定位入口(动态链接重定位表),那么动态链接器就会对该共享对象进行重定位。
  通过 objdump 工具得到共享目标文件的动态重定位表,如下:

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -R libpic.so 

libpic.so:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE 
0000000000003e48 R_X86_64_RELATIVE  *ABS*+0x0000000000001130
0000000000003e50 R_X86_64_RELATIVE  *ABS*+0x00000000000010f0
0000000000004028 R_X86_64_RELATIVE  *ABS*+0x0000000000004028
0000000000004030 R_X86_64_RELATIVE  *ABS*+0x000000000000403c
0000000000003fd8 R_X86_64_GLOB_DAT  b
0000000000003fe0 R_X86_64_GLOB_DAT  __cxa_finalize
0000000000003fe8 R_X86_64_GLOB_DAT  _ITM_registerTMCloneTable
0000000000003ff0 R_X86_64_GLOB_DAT  _ITM_deregisterTMCloneTable
0000000000003ff8 R_X86_64_GLOB_DAT  __gmon_start__
0000000000004018 R_X86_64_JUMP_SLOT  ext
0000000000004020 R_X86_64_JUMP_SLOT  bar

查看 section 信息,我们发现一个重定位项:

0000000000004030 R_X86_64_RELATIVE  *ABS*+0x000000000000403c

根据下面的段表信息以及需要重定位的符号地址,可以判断出这一项需要重定位地址位于 .data 段。根据动态重定位表中的 VALUE 值,可以知道重定位符号需要被修复成目的值为:0x403c

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -S libsta.so 
There are 24 section headers, starting at offset 0x34c0:

Section Headers:
	......
  [19] .got              PROGBITS         0000000000003fd8  00002fd8
       0000000000000028  0000000000000008  WA       0     0     8
  [20] .got.plt          PROGBITS         0000000000004000  00003000
       0000000000000028  0000000000000008  WA       0     0     8
  [21] .data             PROGBITS         0000000000004028  00003028
       0000000000000010  0000000000000000  WA       0     0     8
  [22] .bss              NOBITS           0000000000004038  00003038
       0000000000000008  0000000000000000  WA       0     0     4
	......

反汇编 libpic.so ,我们看到,0x403c,刚好是变量 a 的地址,这刚好与代码想要表达的意思相符。

0000000000001139 <bar>:
    1139:	f3 0f 1e fa          	endbr64 
    113d:	55                   	push   %rbp
    113e:	48 89 e5             	mov    %rsp,%rbp
    1141:	c7 05 f1 2e 00 00 01 	movl   $0x1,0x2ef1(%rip)        # 403c <a>
    1148:	00 00 00 
    114b:	48 8b 05 86 2e 00 00 	mov    0x2e86(%rip),%rax        # 3fd8 <b>
    1152:	c7 00 02 00 00 00    	movl   $0x2,(%rax)
    1158:	48 8b 05 d1 2e 00 00 	mov    0x2ed1(%rip),%rax        # 4030 <p>
    115f:	8b 10                	mov    (%rax),%edx
    1161:	48 8b 05 70 2e 00 00 	mov    0x2e70(%rip),%rax        # 3fd8 <b>
    1168:	89 10                	mov    %edx,(%rax)
    116a:	90                   	nop
    116b:	5d                   	pop    %rbp
    116c:	c3                   	ret    

  实际上,我们甚至可以让代码段也使用这种装载时重定位的方法,而不使用地址无关代码。从前面的例子中我们看到,我们在编译共享对象时使用了“-PIC”参数,这个参数表示产生地址无关的代码段。如果我们不使用这个参数来产生共享对象又会怎么样呢?

$gcc -shared pic. c -o pic. so

  上面这个命令就会产生一个不使用地址无关代码而使用装载时重定位的共享对象。但正如我们前面分析过的一样,如果代码不是地址无关的,它就不能被多个进程之间共享,于是也就失去了节省内存的优点。但是装载时重定位的共享对象的运行速度要比使用地址无关代码的共享对象快,因为它省去了地址无关代码中每次访问全局数据和函数时需要做一次计算当前地址以及间接地址寻址的过程。

3.6 -fPIC 与 -fPIE

  PIE 指的是位置无关的可执行程序,用于生成位置无关的可执行程序,所谓位置无关的可执行程序,指的是,可执行程序的代码指令集可以被加载到任意位置,进程通过相对地址获取指令操作和数据,如果不是位置无关的可执行程序,则该可执行程序的代码指令集必须放到特定的位置才可运行进程。
  Linux 在高版本中使用了地址空间配置随机加载 (ASLR) 技术,即在装载时将程序装载在随机地址,以防止黑客利用已知地址信息执行恶意代码。
  为了支持这一功能,gcc 默认会将程序编译为位置无关代码的形式,以方便加载到任意位置。这时,程序入口为 elf 文件中代码的相对位置,而非内存中的绝对位置。
  编译出的文件Type为 DYN (Position-Independent Executable file),而非 EXEC (Executable file)

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -h simple
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1060
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14176 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

举个例子:

#include <stdio.h>

int global;

int main()
{
	printf("global address = %x\n", &global);
	return 0;
}

程序中定义了一个全局变量 global 并打印其地址。我们先用普通的方式编译程序

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc test.c -o test
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./test
global address = 4eb45014
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./test
global address = a9c6f014
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./test
global address = 7ba96014
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./test
global address = 2d0e014

再使用 -no-pie ,强制使用地址有关码

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc test.c -o test -no-pie
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./test
global address = 404034
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./test
global address = 404034
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ./test
global address = 404034

4、PLT

4.1 PLT

  在之前的《静态链接与动态链接》中,我们介绍了这两者的优缺点:动态链接的缺点主要就是动态链接的程序执行速度会比静态链接的程度略慢一些。

  原因就在于动态链接的可执行程序对于模块间的变量以及函数访问,都需要通过 GOT 表进行间接跳转。如此一来,程序的运行速度肯定会有所减慢。

  另一个很重要的原因就是动态链接的链接工作是在程序运行时来完成的,即程序开始执行前动态链接器会去寻找并且装载程序所需的动态共享对象,然后完成一系列的符号重定位操作。这部分动作肯定会减慢程序的启动速度。

  针对这种情况(链接工作是在程序运行时来完成的),一种称为“延迟绑定(Lazy Binding)”的解决办法出现了。延迟绑定的核心思想就是在程序启动时并不完成所有模块间函数调用的符号重定位操作,只有当目标程序需要调用某个模块外函数时才进行地址绑定(即符号查找、符号重定位)。

  要实现以上的目标,ELF文件采用了 PLT(PProcedure Linkage Table) 的结构,这种结构内包含了一些很精妙的指令序列,这也是接下来所要讲解的内容。

PTL 原理:当调用外部模块的函数时,通过 PTL 新增加的一层间接跳转。调用函数并不直接通过 GOT 跳转,而是通过一个叫作 PLT 项的结构来进行跳转。每个外部函数在 PLT 中都有一个相应的项

4.2 大体逻辑思考

  在讲解 PLT 具体细节之前,我们可以从自顶向下的角度来思考一下如何完成这一项工作。假设目标程序需要调用某个动态共享对象 liba.so内的函数 foo(),那么第一次调用该函数的时候,动态链接器就需要一个寻找 foo 函数地址的查找函数来完成绑定的工作。

  那么这个查找函数需要哪些信息呢?首先要知道绑定行为发生在哪个模块内(目标程序主模块内),其次我们要知道具体要绑定哪个函数(foo()函数)。在 Glibc 中,这个查找函数的名字就叫做 _dl_runtime_resolve()。把这个过程用伪代码描述出来,就如以下所示:

    void DSOFunction@plt()
    {
        if (DSOFunction@got[index]= RELOCATED) { 
            //如果该函数是第一次调用,GOT表内还没有该函数的地址
            让查找函数根据模块ID和被调用函数的ID来获取被调用函数的地址
            并且填入GOT的对应表项之中
            DSOFunction@got[index] = RELOCATED;
        }
        else{
            //GOT表内已经有了该函数地址,直接跳转到该函数地址
            jmp *DSOFunction@got[index];
        }
    }

这一段伪代码就是 PLT 结构之中的模块外函数的对应表项。将伪代码整理一下,我们就可以得到汇编语言级别的 PLT 表项的内容,如下所示:

    foo@plt
    jmp *(foo@got)
    push n
    push moduleID
    jmp _dl_runtime_resolve

第一条指令就是跳转到 foo() 函数所对应的 GOT 表项,如果该 GOT 表项已经被绑定好了,那就可以直接跳转到正确的函数地址。如果是第一次调用该函数,其 GOT 表项内的内容是第二条指令“push n”的地址,这一步就实现伪代码中的 if 判断。

第二条指令就是将 foo() 函数所对应的函数 ID 压入栈内,这个 ID 是 foo 函数在重定位表中的下标。

第三条指令就是将该模块的 ID 压入栈中,

第四条指令就是跳转到我们上文所说的查找函数_dl_runtime_resolve()。_dl_runtime_resolve()进行一系列查找之后,会将 foo() 函数的绝对地址填入 GOT 的对应表项中,然后将控制流转到 foo() 函数上。

一旦 foo() 函数地址被成功绑定,之后再次调用 foo() 在 PLT 的表项,就是直接通过 GOT 表项跳转到正确的地址上。以上就是 GOT 和 PLT 出现的大体逻辑。接下来讲解具体的工作流程。

4.3 GOT 与 PLT

  ELF 文件将 got 分为两部分,分别是 .got 和 .got.plt,前者用于储存全局变量,后者用于保存 DSO 中的函数引用地址。

  这里要说明一点:PLT 位于可执行程序的代码段,是可读不可写的;而 GOT 位于可执行程序的数据段,是可读可写的。另外 .got.plt 还有一个特别之处在于它的前三项都是有特定含义的,含义分别如下所示:

  • 第一项保存了.dynamic 段的地址,这其中描述了本模块动态链接的相关信息
  • 第二项保存本模块的 ID。指向内部类型为 link_map 的指针,动态链接器利用该地址来对符号进行解析
  • 第三项保存了_dl_runtime_resolve 的地址

其中第二项和第三项由动态链接器在装载共享模块的时候负责将它们初始化

这里额外提一点, link_map 是一个很重要的数据结构, 它的主要作用就是记录程序加载的所有共享库的链表, 当需要查找符号时就需要遍历该链表找到对应的共享库。这些数据结构存在于运行时动态链接器使用的空间中

我们还是以 libpic.so 为例,弄清 .got.plt 段的含义:

liang@liang-virtual-machine:~/cfp$ objdump -s -d libpic.so 
......
Contents of section .got.plt:
 201000 100e2000 00000000 00000000 00000000  .. .............
 201010 00000000 00000000 e6050000 00000000  ................
 201020 f6050000 00000000                    ........        
 ......

liang@liang-virtual-machine:~/cfp$ readelf -S libpic.so 
......
 [19] .dynamic          DYNAMIC          0000000000200e10  00000e10
       00000000000001c0  0000000000000010  WA       4     0     8
......
  [10] .plt              PROGBITS         00000000000005d0  000005d0
       0000000000000030  0000000000000010  AX       0     0     16
  [11] .plt.got          PROGBITS         0000000000000600  00000600
       0000000000000010  0000000000000000  AX       0     0     8
......

liang@liang-virtual-machine:~/cfp$ objdump -S libpic.so 

libpic.so:     file format elf64-x86-64

Disassembly of section .plt:

00000000000005d0 <bar@plt-0x10>:
 5d0:	ff 35 32 0a 20 00    	pushq  0x200a32(%rip)        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
 5d6:	ff 25 34 0a 20 00    	jmpq   *0x200a34(%rip)        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
 5dc:	0f 1f 40 00          	nopl   0x0(%rax)

00000000000005e0 <bar@plt>:
 5e0:	ff 25 32 0a 20 00    	jmpq   *0x200a32(%rip)        # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>
 5e6:	68 00 00 00 00       	pushq  $0x0
 5eb:	e9 e0 ff ff ff       	jmpq   5d0 <_init+0x20>

00000000000005f0 <ext@plt>:
 5f0:	ff 25 2a 0a 20 00    	jmpq   *0x200a2a(%rip)        # 201020 <_GLOBAL_OFFSET_TABLE_+0x20>
 5f6:	68 01 00 00 00       	pushq  $0x1
 5fb:	e9 d0 ff ff ff       	jmpq   5d0 <_init+0x20>
......

64 位系统中地址长度是 64 比特,也就是 8 字节。按 8 字节一项并调整字节序后可得 .got.plt 的内容是

第几项地址内容备注
00x2010000x0000000000200e10.dynamic 段地址
10x2010080x0000000000000000本镜像的link_map数据结构地址,未运行无法确定,故以全 0 填充
20x2010100x0000000000000000_dl_runtime_resolve 函数地址,未运行无法确定,故以全 0 填充
30x2010180x000000000000005e6bar 对应的 .got.plt 表项,内容是 bar 的 PLT 表项地址加 6
40x2010200x000000000000005f6ext 对应的 .got.plt 表项,内容是 ext 的 PLT 表项地址加 6
00000000000005e0 <bar@plt>:
 5e0:	ff 25 32 0a 20 00    	jmpq   *0x200a32(%rip)        # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>
 5e6:	68 00 00 00 00       	pushq  $0x0
 5eb:	e9 e0 ff ff ff       	jmpq   5d0 <_init+0x20>

看到它跳转到了 0x200a32(%rip) 指向的地址,0x200a32(%rip) 的内容在反汇编结果的注释中给出了,是 0x201018 。0x201018 正是 bar 函数的 .got.plt 表项的地址,其内容是 0x00000000000005e6,这个地址实际上是 bar 的 PLT 表项地址加 6。可见 5e0 处的 jmpq 指令实际上跳到了 0x5e6 处,相当于没有跳转。0x5e6 处的 pushq 指令将 0x00 压栈,可以理解为接下来要调用的函数的参数。接着 0x5eb 处的 jmpq 指令跳转到了 0x5d0 ,即 PLT 表的第 0 项,如下:

00000000000005d0 <bar@plt-0x10>:
 5d0:	ff 35 32 0a 20 00    	pushq  0x200a32(%rip)        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
 5d6:	ff 25 34 0a 20 00    	jmpq   *0x200a34(%rip)        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
 5dc:	0f 1f 40 00          	nopl   0x0(%rax)

先是把 0x201008 即 .got.plt 表的第 1 项压栈,接着跳转到 0x201010 即 .got.plt 表的第 2 项亦即 _dl_runtime_resolve 函数,解析 bar 函数真正的地址。之后会执行 bar,并将 bar 函数真正的地址写到 bar 对应的 .got.plt 表项中。这样下次调用 bar 数时 0x5e0 处的 jmpq 指令会直接跳转到 bar 函数真正的地址,不用再调用 _dl_runtime_resolve。

4.4 .plt、.plt.got、.got 和 .got.plt 之间的区别

通过上一小节的分析:

section所在 segmentsection 属性用途
.plt代码段RE(可读,可执行).plt section 实际就是通常所说的过程链接表(Procedure Linkage Table, PLT)
.plt.got代码段RE.plt.got section 用于存放 __cxa_finalize 函数对应的 PLT 条目
.got数据段RW(可读,可写).got section 中可以用于存放全局变量的地址;.got section 中也可以用于存放不需要延迟绑定的函数的地址
.got.plt数据段RW.got.plt section 用于存放需要延迟绑定的函数的地址

5、动态链接相关结构

5.1 “.interp”段

动态链接器的位置由 ELF 可执行文件决定。在动态链接的 ELF 可执行文件中,有一个专门的段叫做”.interp”段。

“.interp”段的内容很简单,里面保存的就是一个字符串,这个字符串就是可执行文件所需要的动态链接器路径
动态链接器在 Linux 下是 Glibc 的一部分,也就是属于系统库级别。

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -s Program1

Program1:     file format elf64-x86-64

Contents of section .interp:
 0318 2f6c6962 36342f6c 642d6c69 6e75782d  /lib64/ld-linux-
 0328 7838362d 36342e73 6f2e3200           x86-64.so.2.    
Contents of section .note.gnu.property:
 0338 04000000 20000000 05000000 474e5500  .... .......GNU.
 0348 020000c0 04000000 03000000 00000000  ................
 0358 028000c0 04000000 01000000 00000000  ................
Contents of section .note.gnu.build-id:
 0368 04000000 14000000 03000000 474e5500  ............GNU.
 0378 e24d7e65 dfac3356 97a6b11b d2524780  .M~e..3V.....RG.
 0388 e12f526d                             ./Rm            
Contents of section .note.ABI-tag:
 038c 04000000 10000000 01000000 474e5500  ............GNU.
 039c 00000000 03000000 02000000 00000000  ................
Contents of section .gnu.hash:
 03b0 02000000 06000000 01000000 06000000  ................
 03c0 00008100 00000000 06000000 00000000  ................
 03d0 d165ce6d                             .e.m            
Contents of section .dynsym:
 03d8 00000000 00000000 00000000 00000000  ................
 03e8 00000000 00000000 5c000000 12000000  ........\.......
 03f8 00000000 00000000 00000000 00000000  ................
 0408 01000000 20000000 00000000 00000000  .... ...........
 0418 00000000 00000000 46000000 12000000  ........F.......
 0428 00000000 00000000 00000000 00000000  ................
 0438 1d000000 20000000 00000000 00000000  .... ...........
 0448 00000000 00000000 2c000000 20000000  ........,... ...
 0458 00000000 00000000 00000000 00000000  ................
 0468 4d000000 22000000 00000000 00000000  M..."...........
 0478 00000000 00000000                    ........        
......
......

5.2 “.dynamic”段

  类似于“.interp”这样的段,ELF中还有几个段也是专门用于动态链接的,比如 “.dynamic” 段和 ".dynsym"段等。要了解动态链接器如何完成链接过程,跟前面一样,从了解ELF文件中跟动态链接相关的结构入手将会是一个很好的途径。ELF文件中跟动态链接相关的段有好几个,相互之间的关系也比较复杂,我们先从 “.dynamic” 段入手

  动态链接ELF中最重要的结构应该是“ .dynamic”段,这个段里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。“ .dynamic”段的结构很经典,就是我们已经碰到过的ELF中眼熟的结构数组,结构定义在“elf.h”中

typedef struct {
    Elf32_Sword d_tag;
    union {
        Elf32_Word d_val;
        Elf32_Addr d_ptr;
    } d_un;
} Elf32_Dyn;


//常见类型值

#define DT_NULL         0               /* Marks end of dynamic section */
#define DT_NEEDED       1               /* Name of needed library */
#define DT_HASH         4               /* Address of symbol hash table */
#define DT_STRTAB       5               /* Address of string table */
#define DT_SYMTAB       6               /* Address of symbol table */
#define DT_RELA         7               /* Address of Rela relocs */
#define DT_RELAENT      9               /* Size of one Rela reloc */
#define DT_STRSZ        10              /* Size of string table */
#define DT_INIT         12              /* Address of init function */
#define DT_FINI         13              /* Address of termination function */
#define DT_SONAME       14              /* Name of shared object */
#define DT_RPATH        15              /* Library search path (deprecated) */
#define DT_REL          17              /* Address of Rel relocs */
#define DT_RELENT       19              /* Size of one Rel reloc */

Elf32_Dyn 结构由一个类型值加上一个附加的数值或指针,对于不同类型,后面附加的数值或者指针有着不同含义。我们这里列举几个比较常见的类型值(这些值都是定义在“elf.h”里面的宏),如表7-2所示:

d_tag 类型d_un 的含义
DT_SYMTAB动态连接符号表的地址,d_ptr 表示 “.dynsym” 的地址
DT_STRTAB动态链接字符串表地址,d_ptr 表示 “.dynstr” 的地址
DT_STRSZ动态链接字符串表大小,d_val 表示大小
DT_HASH动态链接哈希表地址,d_ptr 表示“.hash”地址
DT_SONAME本共享对象的“SO-NAME”,我们在后面会介绍“SO-NAME”
DT_INIT初始化代码地址
DT_FINI结束代码地址
DT_NEEDED依赖的共享对象文件,d_ptr 表示所依赖的共享对象文件名
DT_REL动态链接重定位表入口
DT_RELENT动态重读位表入口数量

.dynamic 段可以看成是动态链接下 ELF 文件的“文件头”,只是我们前面看到的 ELF 文件头中保存的是静态链接时相关的内容,比如静态链接时用到的符号表、重定位表等,这里换成了动态链接下所使用的相应信息了。使用 readelf 查看“.dynamic”段的内容

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -d Lib.so 

Dynamic section at offset 0x2e20 contains 24 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x1174
 0x0000000000000019 (INIT_ARRAY)         0x3e10
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x3e18
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x2f0
 0x0000000000000005 (STRTAB)             0x3d8
 0x0000000000000006 (SYMTAB)             0x318
 0x000000000000000a (STRSZ)              127 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000003 (PLTGOT)             0x4000
 0x0000000000000002 (PLTRELSZ)           48 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x530
 0x0000000000000007 (RELA)               0x488
 0x0000000000000008 (RELASZ)             168 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x468
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x458
 0x000000006ffffff9 (RELACOUNT)          3
 0x0000000000000000 (NULL)               0x0

Linux 还提供了 ldd 命令查看一个程序主模块或一个共享库依赖于哪些共享库:

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ ldd Program1
	linux-vdso.so.1 (0x00007ffdf2b5e000)
	./Lib.so (0x00007f13d8c69000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f13d8a00000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f13d8c75000)

5.3 动态符号表

  动态符号表,段名通常叫做 .dynsym,用于表示模块之间的符号导入导出关系。.dynsym 只保存了与动态链接相关的符号,.symtab 中往往保存了所有符号,包括 .dynsym 中的符号。一般动态链接的模块同时拥有 .dynsym 和 .symtab 两个表。

  与 .symtab 类似,动态符号表也需要一些辅助的表,比如动态符号字符串表 .dynstr。 由于动态链接在程序运行时查找符号,为了加快符号的查找过程,往往还有辅助的符号哈希表. hash。

我们可以使用 readelf 查看ELF文件的动态符号表及它的哈希表:

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -sD Lib.so 

Symbol table for image contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND [...]@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
     5: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND [...]@GLIBC_2.2.5 (2)
     6: 0000000000001119    43 FUNC    GLOBAL DEFAULT   14 foobar

动态链接符号表的结构与静态链接的符号表几乎一样。

5.3 动态链接重定位表

  在静态链接中,目标文件里面包含有专门用于表示重定位信息的重定位表,比如 “.rel.text” 表示的是代码段的重定位表,“.rel.data” 是数据段的重定位表。在动态链接中,也有重定位表:

  • “.rela.dyn” 是对数据引用的修正,他所修正的位置位于 “.got” 以及数据段
  • 而 “.rela.plt” 是对函数引用的修正,他所修正的位置位于 “.got.plt”

  共享对象需要重定位的主要原因是导入符号的存在。

  动态链接下,无论是可执行文件或共享对象,一旦它依赖于其他共享对象,也就是说有导入的符号,那么它的代码或数据中就会有对于导入符号的引用。在编译时这些导入符号地址未知。在静态连接中,这些未知的地址引用在最终链接时被修正。但是在动态链接中,导入符号的地址在运行时才确定,所以需要在运行时将这些导入符号引用修正,即需要重定位。

可以使用 readelf 或者 objdump 查看重定位表中的信息

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ readelf -r Program1

Relocation section '.rela.dyn' at offset 0x558 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003da8  000000000008 R_X86_64_RELATIVE                    1140
000000003db0  000000000008 R_X86_64_RELATIVE                    1100
000000004008  000000000008 R_X86_64_RELATIVE                    4008
000000003fd8  000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0
000000003fe0  000200000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTM[...] + 0
000000003fe8  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000003ff0  000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCl[...] + 0
000000003ff8  000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0

Relocation section '.rela.plt' at offset 0x618 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003fd0  000300000007 R_X86_64_JUMP_SLO 0000000000000000 foobar + 0
liangjie@liangjie-virtual-machine:~
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -R Program1

Program1:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE 
0000000000003da8 R_X86_64_RELATIVE  *ABS*+0x0000000000001140
0000000000003db0 R_X86_64_RELATIVE  *ABS*+0x0000000000001100
0000000000004008 R_X86_64_RELATIVE  *ABS*+0x0000000000004008
0000000000003fd8 R_X86_64_GLOB_DAT  __libc_start_main@GLIBC_2.34
0000000000003fe0 R_X86_64_GLOB_DAT  _ITM_deregisterTMCloneTable@Base
0000000000003fe8 R_X86_64_GLOB_DAT  __gmon_start__@Base
0000000000003ff0 R_X86_64_GLOB_DAT  _ITM_registerTMCloneTable@Base
0000000000003ff8 R_X86_64_GLOB_DAT  __cxa_finalize@GLIBC_2.2.5
0000000000003fd0 R_X86_64_JUMP_SLOT  foobar@Base
  • 我们看到有几种重定位入口类型:R_X86_64_RELATIVE、R_X86_64_GLOB_DAT、R_X86_64_JUMP_SLOT。不同的重定位类型表示重定位时有不同的地址计算方法
  • 其中 R_X86_64_GLOB_DAT、R_X86_64_JUMP_SLOT 这两种类型表示,被修正的位置只需要直接填入符号的地址即可

6、小结

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值