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

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

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

书籍:

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

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内容简介 《链接器和加载器》讲述构建程序的关键工具——链接器和加载器,内容包括链接和加载、体系结构、目标文件、存储分配、符号管理、库、重定位、加载和覆盖、共享库、动态链接和加载、动态链接的共享库,以及着眼于成熟的现代链接器所做的一些变化;并介绍一个持续的实践项目,即使用Perl语言开发一个可用的小链接器。 《链接器和加载器》适合高校计算机相关专业的学生、实习程序员、语言设计者和开发人员阅读参考。 编辑推荐 《链接器和加载器》:不管你的编程语言是什么,不管你的平台是什么,你很可能总是会涉及链接器和加载器的功能。但是你知道如何最大限度地利用它们吗?只有现在,随着《链接器和加载器》的出版,总算有一本深入完整地彻底揭示编译时和运行时过程的权威著作了。 《链接器和加载器》首先通过实例深入浅出地阐述了在不同的编译器和操作系统中链接和加载过程的差异。在这个基础上,作者提出了清晰实用的忠告,来帮助你创建更快、更清晰的代码。你将会学习如何规避和Windows DLL相关的陷阱,充分利用UNIX ELF库模式等。如果你对程序设计抱有非常认真的态度,那么你可以通过这本书充分地理解这个领域内最难懂的主题之一。《链接器和加载器》对于编译器和操作系统课程同样也是一本理想的补充读物。 《链接器和加载器》特性 ◆覆盖了Windows,UNIX,Linux,BeOS和其它操作系统的动态链接过程。 ◆解释了Java链接模式,以及它是如何应用在网络小应用程序和可扩展Java代码中的。 ◆帮助你编写更优雅、更高效的代码,以及构建能够被更加高效地编译、加裁和运行的应用程序。 ◆包含了一个用Perl构建链接器的练习项目,项目文件可以从网络下载得到。 媒体推荐 “我很享受阅读这本对实现链接器和加载器的众多技术和挑战进行有效概述的书。虽然书中的多数例子都集中在今天被广泛使用的三种计算机体系结构上,但这本书也包含了很多描述过去的一些有趣和古怪的计算机体系结构的注解。通过这些真实的战例,我断定作者本人真正经历了这些事情并存活了下来给我们讲述这个故事。” ——Guy Steele 作者简介 作者:(美国)莱文(John R.Levine) 译者:李勇 莱文(John R.Levine),是很多书籍的作者或合作者,包括Lex & Yacc(O'Reilly),Programming for Graphics Files in C and C++(Wiley),以及7-heIntemetforDummies(IDG)。他还是Journal of C Language Translation的荣誉退休发行人、comp.compilers新闻组的长期仲裁人员,以及某个最早的商用Fortran 77编译器的创建考。他在耶鲁大学获得了计算机科学的博士学位。 目录 第1章 链接和加载 1.1 链接器和加载器做什么? 1.2 地址绑定:从历史的角度 1.3 链接与加载 1.4 编译器驱动 1.5 链接:一个真实的例子 练习 第2章 体系结构的问题 2.1 应用程序二进制接口 2.2 内存地址 2.3 地址构成 2.4 指令格式 2.5 过程调用和寻址能力 2.6 数据和指令引用 2.7 分页和虚拟内存 2.8 Intel 386分段 2.9 嵌入式体系结构 练习 第3章 目标文件 3.1 目标文件中都有什么? 3.2 空目标文件格式:MS-DOS的COM文件 3.3 代码区段:UNIX的a.out文件 3.4 重定位:MS-DOS的EXE文件 3.5 符号和重定位 3.6 可重定位的a.out格式 3.7 UNIX的ELF格式 3.8 IBM 360目标格式 3.9 微软可移植、可执行体格式 3.10 Intel/Microsoft的OMF文件格式 3.11 不同目标格式的比较 练习 项目 第4章 存储空间分配 4.1 段和地址 4.2 简单的存储布局 4.3 多种段类型 4.4 段与页面的对齐 4.5 公共块和其他特殊段 4.6 链接器控制脚本 4.7 实际中的存储分配 练习 项目 第5章 符号管理 5.1 绑定和名字解析 5.2 符号表格式 5.3 名称修改 5.4 弱外部符号和其他类型符号 5.5 维护调试信息 练习 项目 第6章 库 6.1 库的目的 6.2 库的格式 6.3 建立库文件 6.4 搜索库文件 6.5 性能问题 6.6 弱外部符号 练习 项目 第7章 重定位 7.1 硬件和软件重定位 7.2 链接时重定位和加载时重定位 7.3 符号和段重定位 7.4 基本的重定位技术 7.5 可重链接和重定位的输出格式 7.6 其他重定位格式 7.7 特殊情况的重定位 练习 项目 第8章 加载和覆盖 8.1 基本加载 8.2 带重定位的基本加载 8.3 位置无关代码 8.4 自举加载 8.5 树状结构的覆盖 练习 项目 第9章 共享库 9.1 绑定时间 9.2 实际的共享库 9.3 地址空间管理 9.4 共享库的结构 9.5 创建共享库 9.6 使用共享库链接 9.7 使用共享库运行 9.8 malloc hack和其他共享库问题 练习 项目 第10章 动态链接和加载 10.1 ELF动态链接 10.2 ELF文件内容 10.3 加载一个动态链接程序 10.4 使用PLT的惰性过程链接 10.5 动态链接的其他特性 10.6 运行时的动态链接 10.7 微软动态链接库 10.8 OSF/1伪静态共享库 10.9 让共享库快一些 10.10 几种动态链接方法的比较 练习 项目 第11章 高级技术 11.1 C++的技术 11.2 增量链接和重新链接 11.3 链接时的垃圾收集 11.4 链接时优化 11.5 链接时代码生成 11.6 Java链接模型 练习 项目 参考文献 序言 几乎从有计算机以来,链接器和加栽器就是软件开发工具包中的一部分,因为它们允许使用模块(而不是一个单独的大文件)来构建程序的关键工具。 早在1947年,程序员们就开始使用原始的加载器:将程序的例程存储在多个不同的磁带上,并将它们合并、重定位为一个程序。在20世纪60年代早期,这些加栽器就已经发展得相当完善了。由于那时内存很贵且容量有限,计算机的速度很慢(以今天的标准),为了创建复杂的内存覆盖策略(以将大容量的程序加载到小容量内存中),以及重新编辑先前链接过的文件(以节省重新创建程序的时间),这些链接器都包含了很多复杂的特性。 20世纪八十年代,链接技术几乎没有什么进展。链接器趋向于更加简单,虚拟内存技术将应用程序和覆盖机制中的大多数存储管理工作都转移给了操作系统,越来越快的计算机和越来越大的磁盘也使得重新链接一个程序或替换个别模块比仅仅链接改变过的地方更加容易了。从20世纪90年代起,链接器又开始变得复杂起来,增加了诸多现代特性,包括对动态链接共享库的支持和对C++独特要求的支持。同时,像IA64那样具有宽指令字和编译时访存调度特性的先进处理器架构,也需要将一些新的特性加入到链接器中,以确保在被链接的程序中可以满足代码的这些复杂需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值