重学计算机(七、动态链接和动态库)

本文详细解析了动态库的机制、制作过程,以及动态链接所需的各种段,如interp、dynamic和symbol table。重点讲解了GOT和PLT的作用,动态链接的查找过程和相关环境变量。适合深入理解动态链接的开发者阅读。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一篇写的不是很好,但是也不能沉迷上一篇,把上一篇还没详细介绍的部分,通过后面的章节来介绍,这一篇介绍动态库。

7.1 动态库是什么

7.1.1 为什么需要动态库

老生常谈,为什么需要动态库,那肯定是静态库有些缺点了,才在静态库的基础上发展出动态库。

说缺点之前,先表扬一下静态库的优点(要学会抑扬顿挫):

  1. 可以让程序可以复用,不需要程序员每个函数都要自己实现。
  2. 可以分为不同的模块,由不同部分开发。

这两个好像是一个意思,这两个优化也是动态库的优点,应该说是库的共同优点。

那静态库的缺点呢:

  1. 静态库更新的时候,需要程序重新编译链接,不能实现平滑升级,更新比较麻烦。
  2. 静态链接的特性是:把程序调用的函数都链接进可执行文件中,如果一个系统运行多个程序,多个程序中有包含重复的函数,比如c库的printf等,这些都是浪费内存的。

所以基于上面的问题,大佬们就研究出了一个新的动态库。

动态库的机制:就是在编译链接的时候,不把动态库链接进去,只把静态库链接进去,等到运行的时候,在链接动态库,通过上一篇,重学计算机(六、程序是怎么运行的)就知道有这一步骤,具体是怎么链接怎么运行的我们后面再说。

在多进程环境中,一个动态库的.text副本是可以被不同进程运行的。

这种机制就可以把静态库的第2个浪费内存的缺点优化了一下,一个内存中只有一份副本。(这里如果有人知道进程内存的分布就有疑问了,怎么实现内存中只有一份副本,这个共享内存中再详细描述)。

在理论上,更新代码的话,只需要替换这个动态库的文件就可以实现升级了。

但是动态库还是有缺点的,动态库的版本问题,加入不同版本的动态库,接口改变了,那程序运行就会出现问题。

7.1.2 制作动态库

我们先制作一个动态库,其实做动态库也是比较简单的

root@ubuntu:~/c_test/07# gcc -shared -fPIC -o libfun2.so fun2.c

就是这么简单。

fun2.c其实就是讲静态库的时候,用做静态库的文件,现在拿来做动态库了,偷懒偷懒。

-shared:就是指定编译成动态库

-fPIC:位置无关代码

之后我们用动态库链接一下:

root@ubuntu:~/c_test/07# gcc hello_world.c ./libfun2.so -o hello_world

当然也可以使用链接器的方式

root@ubuntu:~/c_test/07# gcc hello_world.c -L. -lfun2 -o hello_world

当然这种方式也是可以执行的。

7.1.3 动态库是什么

我们在介绍目标文件的时候,就介绍了动态库其实也是一种目标文件,那我们来具体看看:

root@ubuntu:~/c_test/07# readelf -h libfun2.so 
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 (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x5e0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6392 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         7
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 26
root@ubuntu:~/c_test/07# 

是不是很熟悉,各个段都有。也有segment段,同样也有section段。

奇怪的是竟然有入口地址,我用objdump来查看了这个,发现是这个函数deregister_tm_clones(),目前不明白,先留着。

7.2 位置无关代码

7.2.1 固定装载地址

我们根据前面的经验,程序在链接过后,都是会分配一个虚拟地址的,哪动态库是否也会分配一个地址。

如果是固定了地址的话,是很难的,动态库需要分享给其他程序使用,不可能每个程序的动态库地址都一样吧?即使一样,那如果A程序不调用这个代码呢?不就浪费了。

并且如果动态库更新了,是不是需要再次确认一下地址,查看是否冲突。

如果创建了一个新库,那是不是还要重新找地址,所以这样很麻烦。

还有一种就是在运行的时候进行重定位,可是这个重定位可能会修改动态库的地址,这样共享的其他进程就不能使用了。

所以大佬们就提出了一种位置无关代码。

7.2.2 位置无关代码

位置无关代码:让这一段代码可以加载到内存的任何位置而无需链接器修改。当然数据部分是需要修改的,每个进程可以备份一个副本。

需要编译成位置无关代码,需要在编译的时候添加上-fPIC。(上面编译的时候也是添加了)

这样就可以极大的共用了一份代码,确实是方便了,但是我们学习的人就复杂了,下面我们详细分析一下,动态库是怎么调用的。

我们把func2.c修改一下,这样才能满足下面的测试条件:

//#include <stdio.h>

int f_a = 0;
int f_b = 84;

static int bar2()
{
    f_b = 222;
    return 0;
}

int bar(int i)
{
	i = 333;
	f_a = i;
	return 0;
}

int func2(int i)
{
    static int s_a = 0;
    static int s_b = 84;

	bar(s_a);
    bar2();
    //printf("i = %d %d %d\n", i, s_a, s_b);
    return 0;
}

我们来反汇编处理一下:

0000000000000710 <bar2>:
 710:	55                   	push   %rbp
 711:	48 89 e5             	mov    %rsp,%rbp
 714:	48 8b 05 b5 08 20 00 	mov    0x2008b5(%rip),%rax        # 200fd0 <_DYNAMIC+0x1c8>
 71b:	c7 00 de 00 00 00    	movl   $0xde,(%rax)
 721:	b8 00 00 00 00       	mov    $0x0,%eax
 726:	5d                   	pop    %rbp
 727:	c3                   	retq 

0000000000000749 <func2>:
 749:	55                   	push   %rbp
 74a:	48 89 e5             	mov    %rsp,%rbp
 74d:	48 83 ec 10          	sub    $0x10,%rsp
 751:	89 7d fc             	mov    %edi,-0x4(%rbp)
 754:	8b 05 de 08 20 00    	mov    0x2008de(%rip),%eax        # 201038 <s_a.1840>
 75a:	89 c7                	mov    %eax,%edi
 75c:	e8 8f fe ff ff       	callq  5f0 <bar@plt>
 761:	b8 00 00 00 00       	mov    $0x0,%eax
 766:	e8 a5 ff ff ff       	callq  710 <bar2>
 76b:	b8 00 00 00 00       	mov    $0x0,%eax
 770:	c9                   	leaveq 
 771:	c3                   	retq   

  • 内部函数调用、跳转

    766: e8 a5 ff ff ff callq 710

    0x767 - 91 + 4 = 710 (动态库的内部是相对偏移的)

  • 内部数据访问

    内部数据访问,也是根据偏移量确定的。

    数据段跟代码段的相对地址是一样的,所以我们只要按这个相对地址访问就可以了,因为程序加载的时候也是两个段一起加载的。

    7f7aa56e8000-7f7aa56e9000 r-xp 00000000 08:01 11548961                   /root/c_test/07/libfun2.so		# 代码段
    7f7aa56e9000-7f7aa58e8000 ---p 00001000 08:01 11548961                   /root/c_test/07/libfun2.so
    7f7aa58e8000-7f7aa58e9000 r--p 00000000 08:01 11548961                   /root/c_test/07/libfun2.so
    7f7aa58e9000-7f7aa58ea000 rw-p 00001000 08:01 11548961                   /root/c_test/07/libfun2.so   # 数据段
    

    754: 8b 05 de 08 20 00 mov 0x2008de(%rip),%eax # 201038 <s_a.1840>

    这个按相对偏差来算。

  • 外部数据调用

     7e8:	48 8b 05 f9 07 20 00 	mov    0x2007f9(%rip),%rax        # 200fe8 <_DYNAMIC+0x1e8>
     7ef:	c7 00 e7 03 00 00    	movl   $0x3e7,(%rax)
    
     19 .got          00000040  0000000000200fc0  0000000000200fc0  00000fc0  2**3
                      CONTENTS, ALLOC, LOAD, DATA
    

    这个就需要借助这个.got的数据修正了,程序运行后,会修正这个.got变量,然后使这个能调用到正确的地址。

  • 外部函数调用

     811:	e8 4a fe ff ff       	callq  660 <func1@plt>
    

    这个后面详细讲。

7.3 动态链接需要的段

我们在重学计算机(三、elf文件布局和符号表)这一篇文章中也描述了完整链接后的ELF的各个段,当初画出来还觉得真多,然而看目标文件确实比较少,那是因为我当初是动态链接了,生成了一些跟动态链接的段,这次趁着这个机会把之前ELF布局关于动态链接的部分给补上。(自己留下的坑,确实要补补)

在这里插入图片描述

7.3.1 .interp

之前也讲过,不过这次要仔细的讲讲。

如果程序是静态链接的,在程序加载完之后,PC指针会指向可执行文件的入口地址,然后程序执行。

但是如果是动态链接的呢?程序还有很有跟动态库有关的符号没有找到,所以这时候,操作系统会启动一个动态链接器

那操作系统怎么知道哪个是动态链接器呢?这时候我们的ELF文件中的.interp段就是保存着这个动态链接器的地址。

我们来查看一下:

Contents of section .interp:
 400238 2f6c6962 36342f6c 642d6c69 6e75782d  /lib64/ld-linux-
 400248 7838362d 36342e73 6f2e3200           x86-64.so.2. 

这个就是动态链接器的路径。

root@ubuntu:/lib64# ls -l ld-linux-x86-64.so.2 
lrwxrwxrwx 1 root root 32 Jun  5  2020 ld-linux-x86-64.so.2 -> /lib/x86_64-linux-gnu/ld-2.23.so

其实这个是一个软链接,真正的路径在这里,这个动态链接器其实是在glibc里面的,它跟glibc是一个版本。

通过这个查看,我们也明白动态链接器其实就是一个so文件,所以操作系统在调用它的时候,也需要把他映射到内存中,然后再调用ld-linux-x86-64.so.2的入口地址。(so文件是有入口地址,在上面有讲)

动态链接器执行之后,会初始化一些环境,然后开始一些动态链接的工作,等搞完之后,就可以把PC指针交给可执行文件的入口地址了,然后运行程序。

其实我们也可以通过这个命令查看可执行文件的动态链接器:

root@ubuntu:~/c_test/07# readelf -l hello_world | grep interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

7.3.2 .dynamic

这一个段的内容比较重要,保存了动态链接的基本信息,我们可以用命令查看一下:

root@ubuntu:~/c_test/07# readelf -d hello_world

Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [./libfun2.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x400588
 0x000000000000000d (FINI)               0x400814
 0x0000000000000019 (INIT_ARRAY)         0x600e00
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x600e08
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x400298
 0x0000000000000005 (STRTAB)             0x400428
 0x0000000000000006 (SYMTAB)             0x4002d8
 0x000000000000000a (STRSZ)              196 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000
 0x0000000000000002 (PLTRELSZ)           72 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400540
 0x0000000000000007 (RELA)               0x400528
 0x0000000000000008 (RELASZ)             24 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x400508
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x4004ec
 0x0000000000000000 (NULL)               0x0

这个可能看不懂,那再配上这个图就明白了一点:

在这里插入图片描述

结合起来看,就发现这个段就是存储着各个信息的地址,到时候要使用的时候,应该是从这里取的值。

另外我们经常用ldd来查看这个可执行文件依赖哪个动态库:

root@ubuntu:~/c_test/07# ldd hello_world
	linux-vdso.so.1 =>  (0x00007ffc22495000)     # 这个暂且不讲
	./libfun2.so (0x00007fbfd63a9000)    #  这个就是我们链接的库
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbfd5fdf000)  # 这个是c语言的库
	/lib64/ld-linux-x86-64.so.2 (0x00007fbfd65ab000)	# 这个是链接器

7.3.3 动态符号表

是否回想起静态链接的时候,是不是也有一个符号表。

那时候的的符号表是:.symtab段,其中也有一个静态符号的字符串段是:.strtab。

动态库也是有的,动态符号表是:.dynsym,辅助的动态符号表的字符串段是:.dynstr。

不过在动态链接后的可执行文件中,上面的4个段都存在,这是因为动态链接后的.symtab段被当做整个程序的符号表,当然.strtab也是变成了整个程序的符号字符串段。

其中的动态符号表不变,依然还是指动态符号表,我们来查看一下:

root@ubuntu:~/c_test/07# readelf --dyn-syms hello_world

Symbol table '.dynsym' contains 14 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND func2
     6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     7: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     8: 000000000060104c     0 NOTYPE  GLOBAL DEFAULT   25 _edata
     9: 0000000000601060     0 NOTYPE  GLOBAL DEFAULT   26 _end
    10: 0000000000601040     4 OBJECT  WEAK   DEFAULT   25 f_a
    11: 000000000060104c     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    12: 0000000000400588     0 FUNC    GLOBAL DEFAULT   11 _init
    13: 0000000000400814     0 FUNC    GLOBAL DEFAULT   15 _fini

从表中,我们看到一些符号是UND的,其实也跟我们分析静态符号表一样,有一些是目前没有找到符号的,需要程序运行起来才能加载动态链接库。

有一些是有地址的,这些是别的动态库需要引用到我们的符号的地方。

所以动态符号表跟.symtab的工作方式是一样的,都是相互的查看自己需要的符号。

当然为了加快符号的查找过程,我们会辅助符号哈希表(.gnu.hash)。我们也可以用命令来查看:

root@ubuntu:~/c_test/07# readelf -sD hello_world

Symbol table of `.gnu.hash' for image:
  Num Buc:    Value          Size   Type   Bind Vis      Ndx Name
    8   0: 000000000060104c     0 NOTYPE  GLOBAL DEFAULT  25 _edata
    9   0: 0000000000601060     0 NOTYPE  GLOBAL DEFAULT  26 _end
   10   1: 0000000000601040     4 OBJECT  WEAK   DEFAULT  25 f_a
   11   1: 000000000060104c     0 NOTYPE  GLOBAL DEFAULT  26 __bss_start
   12   1: 0000000000400588     0 FUNC    GLOBAL DEFAULT  11 _init
   13   2: 0000000000400814     0 FUNC    GLOBAL DEFAULT  15 _fini

看了这个之后,发现这个只是为了方便调用者快速查找,好像也符合题意。

7.3.4 GOT和PLT

本来想在7.2.4中就介绍了这两个东西,但是发现这样介绍,确实不太好描述,并且这两个东西比较重要,程序能找到动态库就是因为这两个。

简单介绍一下:

GOT全称 Global Offset Table,即全局偏移量表。

就是.got和.got.plt。在.data段之前。每个被目标模块引用的全局符号(函数和变量)都对应于GOT中一个8字节的条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含正确的目标地址。

其中.got是函数的修正地址,.got.plt是数据的修正地址。

PLT全称Procedure Linkage Table,即过程链接表。

就是.plt和.plt.got,位于.text段之前。每个可执行程序调用的函数都有它自己的条目,每个条目16字节的代码,都是可以直接执行的。

我们来看一下:

Disassembly of section .plt:

00000000004005b0 <printf@plt-0x10>:
  4005b0:	ff 35 52 0a 20 00    	pushq  0x200a52(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4005b6:	ff 25 54 0a 20 00    	jmpq   *0x200a54(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4005bc:	0f 1f 40 00          	nopl   0x0(%rax)

00000000004005c0 <printf@plt>:
  4005c0:	ff 25 52 0a 20 00    	jmpq   *0x200a52(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  4005c6:	68 00 00 00 00       	pushq  $0x0
  4005cb:	e9 e0 ff ff ff       	jmpq   4005b0 <_init+0x28>

00000000004005d0 <__libc_start_main@plt>:
  4005d0:	ff 25 4a 0a 20 00    	jmpq   *0x200a4a(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  4005d6:	68 01 00 00 00       	pushq  $0x1
  4005db:	e9 d0 ff ff ff       	jmpq   4005b0 <_init+0x28>

00000000004005e0 <func2@plt>:
  4005e0:	ff 25 42 0a 20 00    	jmpq   *0x200a42(%rip)        # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
  4005e6:	68 02 00 00 00       	pushq  $0x2
  4005eb:	e9 c0 ff ff ff       	jmpq   4005b0 <_init+0x28>

Disassembly of section .plt.got:

0000000000400600 <.plt.got>:
  400600:	ff 25 f2 09 20 00    	jmpq   *0x2009f2(%rip)        # 600ff8 <_DYNAMIC+0x1e0>
  400606:	66 90                	xchg   %ax,%ax

Contents of section .got:
 600ff8 00000000 00000000                    ........        
Contents of section .got.plt:
 0x00601000 180e6000 00000000 00000000 00000000 ..`.............
 0x00601010 00000000 00000000 d6054000 00000000 ..........@.....
 0x00601020 e6054000 00000000 f6054000 00000000 ..@.......@.....

通过上面分析,.plt中一共有4个条目,分别是:printf@plt-0x10,printf@plt,__libc_start_main@plt,func2@plt。

我们来分析一下第一个条目的代码是做啥的:

00000000004005b0 <printf@plt-0x10>:
  4005b0:	ff 35 52 0a 20 00    	pushq  0x200a52(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4005b6:	ff 25 54 0a 20 00    	jmpq   *0x200a54(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4005bc:	0f 1f 40 00          	nopl   0x0(%rax)   #  没有操作

第一句是把0x601008里的内容入栈,然后跳转到0x601010,GOT是从0x601000开始的,所以0x601010这个地址是GOT[2]。

再来看看第二个条目,第二个条目就是我们的printf函数调用过程了:

00000000004005c0 <printf@plt>:
  4005c0:	ff 25 52 0a 20 00    	jmpq   *0x200a52(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  4005c6:	68 00 00 00 00       	pushq  $0x0
  4005cb:	e9 e0 ff ff ff       	jmpq   4005b0 <_init+0x28>

第一条指令也是先跳转到0x601018,这个地址也是GOT[4],第二条指令将0压栈,第三条指令跳转到.plt的地址继续执行。

接着来看看.plt.got

0000000000400600 <.plt.got>:
  400600:	ff 25 f2 09 20 00    	jmpq   *0x2009f2(%rip)        # 600ff8 <_DYNAMIC+0x1e0>
  400606:	66 90                	xchg   %ax,%ax

这个比较简单直接跳转到0x600ff8地址的,也就是.got地址。

看到这里是不是有疑问,好像调用关系还没缕清,往后看就明白了。

7.3.5 动态链接重定位段

动态链接也是需要重定位的,在静态链接的时候,重定位是在链接的时候,链接器来处理的,但是动态链接的重定位,是要在程序加载的时候处理的。

我们是否回想起静态链接的重定位表:.rela.text和.rela.data。一个是代码段,一个是数据段。

没错,我们动态链接也有:.rel.dyn和.rel.plt。

.rel.dyn是对数据引用的修正,所修正的位置位于.got以及数据段。

.rel.plt是对函数引用的修正,它所修正的位置位于.got.plt。

我们可以用命令来查看一下:

root@ubuntu:~/c_test/07# readelf -r hello_world

Relocation section '.rela.dyn' at offset 0x528 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000600ff8  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000601048  000a00000005 R_X86_64_COPY     0000000000601048 f_a + 0

Relocation section '.rela.plt' at offset 0x540 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601018  000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000601028  000500000007 R_X86_64_JUMP_SLO 0000000000000000 func2 + 0

这里也出现了几个新的重定位类型。

R_X86_64_RELATIVE:

R_X86_64_GLOB_DAT:

R_X86_64_JUMP_SLOT:

这些类型在后面会详细分析。

7.3.6 printf函数的调用过程

我们先从反汇编代码查看一下,执行到printf函数的时候,会发生什么?

000000000040071c <main>:
....
  40078e:	e8 2d fe ff ff       	callq  4005c0 <printf@plt>
  ...

会调用0x4005c0这个地址,那我们接着看看这个地址是干啥的:

00000000004005c0 <printf@plt>:
  4005c0:	ff 25 52 0a 20 00    	jmpq   *0x200a52(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  4005c6:	68 00 00 00 00       	pushq  $0x0
  4005cb:	e9 e0 ff ff ff       	jmpq   4005b0 <_init+0x28>

接下来要交给gdb来跟踪:

(gdb) b printf@plt         		# 打断点
Breakpoint 1 at 0x4005c0
(gdb) r
Starting program: /root/c_test/07/hello_world 

Python Exception <type 'exceptions.NameError'> Installation error: gdb.execute_unwinders function is missing: 
Breakpoint 1, 0x00000000004005c0 in printf@plt ()
(gdb) x/2xw 0x601018		#  查看这个位置的内存,发送真的跟elf中的值不一样
0x601018 <printf@got.plt>:	0x004005c6	0x00000000   # 这个地址就是执行下一步,真奇怪为啥搞这么复杂,接着会跳转到printf@plt-0x10这个函数中
# 明白了,第一遍是这样,但是从第二遍开始,这个地址就是真正的printf函数地址了。
(gdb) b *main-0x16c
Breakpoint 1 at 0x4005b0
(gdb) r
Starting program: /root/c_test/07/hello_world 
Python Exception <type 'exceptions.NameError'> Installation error: gdb.execute_unwinders function is missing: 
Python Exception <type 'exceptions.NameError'> Installation error: gdb.execute_unwinders function is missing: 

Python Exception <type 'exceptions.NameError'> Installation error: gdb.execute_unwinders function is missing: 
Breakpoint 1, 0x00000000004005b0 in ?? ()
(gdb) x/2xw 0x601010
0x601010:	0xf7deee40	0x00007fff
(gdb) disassemble 0x00007ffff7deee40
No function contains specified address.

这个博主写的真好https://luomuxiaoxiao.com/?p=578,大家可以看看这篇文章,0x00007ffff7deee40可能我没有这个符号,这个符号是_dl_runtime_resolve_avx这个函数,不过我们可以执行完printf函数来查看一下GOT[3]的值。

(gdb) x/2xw 0x601018
0x601018 <printf@got.plt>:	0xf7860810	0x00007fff
(gdb) disassemble 0x00007ffff7860810
Dump of assembler code for function printf:
   0x00007ffff7860810 <+0>:	sub    $0xd8,%rsp
   0x00007ffff7860817 <+7>:	test   %al,%al
   0x00007ffff7860819 <+9>:	mov    %rsi,0x28(%rsp)
   0x00007ffff786081e <+14>:	mov    %rdx,0x30(%rsp)
   0x00007ffff7860823 <+19>:	mov    %rcx,0x38(%rsp)
   0x00007ffff7860828 <+24>:	mov    %r8,0x40(%rsp)
   0x00007ffff786082d <+29>:	mov    %r9,0x48(%rsp)
   0x00007ffff7860832 <+34>:	je     0x7ffff786086b <printf+91>

这个就是找到符号的情况,所以第二遍就不需要再次查找了。

这一个过程就是延时绑定,将地址的绑定推迟到第一次调用的过程,我们分析完之后,是不是就明白了。

然后第一次调用的时候,地址已经在GOT[3]中了,就可以直接调用了。

7.3.7 f_a数据的引用

我们再来追踪一下数据:

(gdb) disassemble main   # 反汇编
0x0000000000400790 <+100>:	mov    0x2008b2(%rip),%eax        # 0x601048 <f_a>
  [26] .bss              NOBITS           0000000000601048  00001048
       0000000000000018  0000000000000000  WA       0     0     4

虽然这个f_a变量定义在动态库中,但是都是在可执行文件定义,这样子就不会操作变量冲突。

如果动态库中需要访问这个变量,也是通过GOT来访问,跟之前的那个机制是一样的。这样就不会操作冲突。

因为数据段都是不同进程不同副本,所以支持多进程。

7.3.8 R_X86_64_RELATIVE

这个奇怪是查看到的符号都是数字,所以不是很清楚这个东西。

root@ubuntu:~/c_test/07# readelf -r libfun2.so 

Relocation section '.rela.dyn' at offset 0x4f0 contains 12 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000200de8  000000000008 R_X86_64_RELATIVE                    770
000000200df0  000000000008 R_X86_64_RELATIVE                    730
000000201028  000000000008 R_X86_64_RELATIVE                    201028
000000201038  000000000008 R_X86_64_RELATIVE                    201048

不过概念都是知道,就是在加载完之后,还需要进行一个重定位,这一些重定位就跟本地的一些变量有关吧。这个以后碰到了,或者了解到了,再回来看看吧。

7.3.9 GOT和PLT特殊条目

GOT[0] :addr of .synamic

GOT[1]:addr of reloc entries

GOT[2]:addr of dynamic linker

GOT[3]:sys startup

7.4 动态链接

写了这么多,才刚好介绍完动态库,现在我们来简单了解一下动态链接的步骤。

7.4.1 动态链接器自举

我们之前分析了,动态链接器也是一个动态库,并且程序加载完之后,会把控制器给动态链接器的。所以作为第一个动态库需要有一点特别:

  • 不可以依赖其他任何共享对象
  • 所需要的全局和静态变量的重定位工作由它自己完成

7.4.2 装载共享对象

完成自举后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表中,也就是全局符号表。

然后链接器开始寻找可执行文件所依赖的对象,并将这些动态库的名字放入到一个装载集合中。

然后链接器开始从集合里取一个所需要的共享对象名字,然后将它相应的代码段和数据段映射到进程空间中。

如果存在符号冲突,linux下的动态链接器是这样子处理的:

当一个符号需要被加入全局符号表时,如果相同符号名已经存在,则后加入的符号被忽略。

经过我们上面的实验,在函数前面加上static的时候,函数就直接根据PC偏移来调用,就不需要通过PLT和GOT的方式来调用,这样也会加快函数调用的过程。

7.4.3 重定位和初始化

经过前面加载完成,这时候就可以搞一遍重定位了,将GOT/PLT中需要重定位都重定位一些,其实感觉都是数据部分需要重定位,函数部分都是到调用的时候再处理。

重定位完成后,如果某个共享对象有.init段,那么动态链接器会执行.init段。

7.5 显式运行时链接

动态库还有一种更灵活的方式使用,叫做显示运行时链接(这个词听着就懵逼)。

我们在前面说的都是对程序员来说是透明的,都是动态链接器默默的做了,我们只是调用函数就可以了。不过有一种方式是,动态链接器提供API,由我们程序员来处理动态库。

7.5.1 dlopen()

这个函数用来打开一个动态库,并将其加载到进程空间,完成初始化过程。

#include <dlfcn.h>
void *dlopen(const char*filename, int flag);

flag:RTLD_LAZY 表示延时绑定

​ RTLD_NOW 表示现在绑定

返回值,是一个句柄。

7.5.2 dlsym()

我们可以通过这个函数找到所需要的符号

#include <dlfcn.h>
void *dlsym(void *handle, char *symbol);

handle:就是open后的句柄

symbol:需要查找的符号名

返回值:能找到就返回该符号的值,找不到就返回NULL

7.5.3 dlerror()

判断上一次调用是否成功

#include <dlfcn.h>
const char *dlerror(void);

7.5.4 dclose()

卸载一个动态库,并且调用.finit段的代码。然后将相应的符号从符号表中去除,取消进程空间跟模块的映射关系,然后关闭模块文件。

#include <dlfcn.h>
int dlclose (void *handle);

7.5.5 例子

接下来我们就来用一下我们做的libfun2.so。

// func2.c 源码
#include <stdio.h>

//extern int g_a;

static int a;
static int *p = &a;

int f_a = 0;
int f_b = 84;

static int bar2()
{
    f_b = 222;
    return 0;
}

int bar(int i)
{
	i = 333;
	f_a = i;
	return 0;
}

int func2(int i)
{
    static int s_a = 0;
    static int s_b = 84;

    s_a = 111;
    s_b = 222;

  //  g_a = 999;

	bar(s_a);
    bar2();
    //func1();
    printf("i = %d %d %d\n", i, s_a, s_b);
    return 0;
}

// main.c的源码
#include <stdio.h>
#include <dlfcn.h>

int main()
{
    void *handle = NULL;

    // 第一步先打开
    handle = dlopen("./libfun2.so", RTLD_LAZY);  // 选择延时加载
    if(!handle) {
        // 为空就是错误
        printf("dlopen %s\n", dlerror());
        return 1;
    }

    // 第二步,找到需要的符号
    int (*func2)(int i);        // 定义个函数指针来取
    func2 = dlsym(handle, "func2");
    if(!func2) {
        printf("dlsym %s\n", dlerror());
        return 1;
    }

    // 执行函数,就是函数指针的执行
    func2(11);

    // 再来读取一个变量
    int *f_a = NULL;
    f_a = dlsym(handle, "f_a");
    if(!func2) {
        printf("dlsym f_a %s\n", dlerror());
        return 1;
    }

    printf("f_a = %d\n", *f_a);

    //第三步,关闭动态库
    dlclose(handle);

    return 0;

}

后面是执行的命令:

root@ubuntu:~/c_test/07# gcc -shared -fPIC -o libfun2.so fun2.c 
root@ubuntu:~/c_test/07# gcc main.c -o main -ldl
root@ubuntu:~/c_test/07# ./main
i = 11 111 222
f_a = 333
root@ubuntu:~/c_test/07# 

又凑了几百字,哈哈哈。

这里推荐一下这个老哥写的,写了c++部分,因为c++的符号是有装饰过的,所以要用extern c。

还有python是怎么调用动态库的。

采用dlopen、dlsym、dlclose加载动态链接库

另外Java也有定义了一个标准调用规则,叫做java本地接口(Java Native Interface,JNI)。它允许java程序调用本地的c和c++函数。原理也是用dlopen接口(或者其他类似的接口)加载动态库的,从而实现调用函数。

之前一直不知道JNI是啥,现在好像明白了点。

7.6 动态库相关

7.6.1 动态库查找过程

我们在前面分析过.dynamic段,可以看到DT_NEED的项,

 0x0000000000000001 (NEEDED)             Shared library: [./libfun2.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

如果DT_NEED里面是绝对路径,动态链接器就按照这个路径去找。

如果DT_NEED保存相对路径,那么动态链接器会/lib、/usr/lib和由/etc/ld.so.conf配置文件指定的目录中查找动态库。

linux系统为了加快查找动态库,是有一个ldconfig的程序,这个程序的作用是为了动态库目录下的各个动态库创建、删除或更新相应的符号缓存。

当动态链接器要查找动态库时,它直接从/etc/ld.so.cache里面查找。这个/etc/ld/so/cache的结构经过特殊设计,所以查找速度会加快。

回想起上一节,是不是就直接映射这个文件,原来是为了查找动态库。

7.6.2 环境变量

  1. LD_LIBRARY_PATH

    这个环境变量我们经常用,在程序调试的过程中,可以设置这个环境变量,临时改掉这个shell的动态库查找路径。

    在进程启动时,动态链接器会首先查找这个环境变量指定的目录。

    # 查看这个环境变量
    root@ubuntu:/etc/ld.so.conf.d# echo $LD_LIBRARY_PATH
    # 这个就是设置这个环境变量,退出这个shell就失效
    root@ubuntu:~/c_test/07# export  LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/c_test/07
    
  2. LD_PRELOAD

    这个环境变量上一节也说过一点。

    这个环境变量中,我们可以指定预先装载的一些动态库或目标文件。动态链接器总会优先装载这些。

    由于全局符号介入这个机制,我们在LD_PRELOAD中的动态库会优先加载,就会覆盖后面的,所以可以做到改写C标准库的函数,这个后面会讲。正式程序还是少用这个。

    其中/etc/ld.so.preload跟这个环境变量的意思是一样的。

  3. LD_DEBUG

    这个变量可以打开动态链接器的调试功能。有兴趣的可以试试。

7.6.3 总结

总结一下动态库搜索过程:

  • 由环境变量LD_PRELOAD指定的路径(或者ld.so.preload指定的路径)
  • 如果编译过程有指定路径,优先查找这个路径
  • 由环境变量LD_LIBRARY_PATH指定的路径
  • 由路径缓存文件/etc/ld.so.cache指定的路径
  • 默认共享库目录,先/usr/lib然后/lib

这个不是很确定,发现有问题的话,再回来修改。

7.7 总结

真没想到这个动态库竟然这么多东西,学习了好几天,不过也多谢《程序员自我修养——链接、装载与库》这本书,这本书讲的很详细,大家可以去看看这本书,然后自己再屡屡思路,真的受益匪浅。再次感谢大佬。

参考链接:

https://luomuxiaoxiao.com/?p=578

书籍:

《程序员的自我修养——装载、链接和库》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值