链接装载与库 第7章 动态链接

7.1 为什么要动态链接

静态链接的缺点:

  1. 浪费磁盘和内存空间
  2. 更新困难。如果某个模块发生更新,就需要重新打包整个程序,用户需要下载整个程序。

动态链接能够解决以上两个问题,动态链接的思想即是将程序和模块分隔开来,等到运行时才进行链接。所以在内存中,多个程序可以共享一个模块,共享内存不仅节约内存,也能够减少物理页面的换入换出,增加CPU缓存命中率。

程序可扩展性和兼容性
程序在运行时可以动态的选择加载各种程序模块,可以被用来制作程序的插件。(动态链接时,没有某个依赖的模块不是会启动失败嘛…)
动态链接还能加强程序的兼容性。在不平的平台链接到由操作系统提供的动态链接库。这些链接库相当于在程序和操作系统之间增加了一个中间层,从而消除程序对不同平台之间的依赖。

动态链接的基本实现
在静态链接中,使用ld进行链接。而在动态链接中,链接过程由动态链接器在装载的过程中完成。
据估算,动态链接相比于静态链接损失约5%的性能。(装载完成之后不就和静态链接完成一样?没明白这个5%表示啥)

7.2 简单的动态链接例子

/* Program1.c */
#include "Lib.h"

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

/* Program2.c */
#include "Lib.h"

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

/* Lib.c  */
#include <stdio.h>

void foobar(int i)
{
    printf("printing from Lib.so %d\n",i);
}


/* Lib.h */
#ifndef LIB_H
#define LIB_H
void foobar(int i);
#endif

编译得到可执行文件:

gcc -fPIC -shared -o Lib.so Lib.c
gcc -o Program1 Program1.c ./Lib.so
gcc -o Program2 Program2.c ./Lib.so
root@debian:/opt/linkLoadAndLibrary# ./Program1
printing from Lib.so 1
root@debian:/opt/linkLoadAndLibrary# ./Program2
printing from Lib.so 2

既然动态链接的过程中,不会半lib.so中的内容链接进去,那么为什么Lib.so还是参与了链接过程呢?

root@debian:/opt/linkLoadAndLibrary# gcc -o Program2 Program2.c
/tmp/cc5w71BG.o: In function `main':
Program2.c:(.text+0x21): undefined reference to `foobar'
collect2: error: ld returned 1 exit status

这是因为,链接器必须确定foobar()这个函数的性质,如果这个函数是静态目标模块中的函数,就必须对地址进行重定位,如果是动态共享对象中的函数,那么链接器就会把这个函数标记成动态链接的符号,等到装载时再进行重定位。

动态链接程序运行时空间分布
在Lib.c中加入sleep语句,来观察进程的虚拟地址空间分布。

root@debian:/opt/linkLoadAndLibrary# cat /proc/1874/maps
00420000-00421000 r-xp 00000000 08:01 3366       /opt/linkLoadAndLibrary/Program1
00421000-00422000 r--p 00000000 08:01 3366       /opt/linkLoadAndLibrary/Program1
00422000-00423000 rw-p 00001000 08:01 3366       /opt/linkLoadAndLibrary/Program1
0074e000-0076f000 rw-p 00000000 00:00 0          [heap]
b7564000-b7566000 rw-p 00000000 00:00 0
b7566000-b7717000 r-xp 00000000 08:01 262155     /lib/i386-linux-gnu/libc-2.24.so
b7717000-b7719000 r--p 001b0000 08:01 262155     /lib/i386-linux-gnu/libc-2.24.so
b7719000-b771a000 rw-p 001b2000 08:01 262155     /lib/i386-linux-gnu/libc-2.24.so
b771a000-b771d000 rw-p 00000000 00:00 0
b7721000-b7722000 r-xp 00000000 08:01 3365       /opt/linkLoadAndLibrary/Lib.so
b7722000-b7723000 r--p 00000000 08:01 3365       /opt/linkLoadAndLibrary/Lib.so
b7723000-b7724000 rw-p 00001000 08:01 3365       /opt/linkLoadAndLibrary/Lib.so
b7724000-b7727000 rw-p 00000000 00:00 0
b7727000-b7729000 r--p 00000000 00:00 0          [vvar]
b7729000-b772b000 r-xp 00000000 00:00 0          [vdso]
b772b000-b774e000 r-xp 00000000 08:01 262150     /lib/i386-linux-gnu/ld-2.24.so
b774e000-b774f000 r--p 00022000 08:01 262150     /lib/i386-linux-gnu/ld-2.24.so
b774f000-b7750000 rw-p 00023000 08:01 262150     /lib/i386-linux-gnu/ld-2.24.so
bf814000-bf835000 rw-p 00000000 00:00 0          [stack]

用readelf工具查看Lib.sh的装载属性。

root@debian:/opt/linkLoadAndLibrary# readelf -l Lib.so

Elf file type is DYN (Shared object file)
Entry point 0x420
There are 7 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x00000000 0x00000000 0x00658 0x00658 R E 0x1000
  LOAD           0x000f00 0x00001f00 0x00001f00 0x00118 0x0011c RW  0x1000
  DYNAMIC        0x000f0c 0x00001f0c 0x00001f0c 0x000e0 0x000e0 RW  0x4
  NOTE           0x000114 0x00000114 0x00000114 0x00024 0x00024 R   0x4
  GNU_EH_FRAME   0x0005bc 0x000005bc 0x000005bc 0x00024 0x00024 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000f00 0x00001f00 0x00001f00 0x00100 0x00100 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
   01     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
   02     .dynamic
   03     .note.gnu.build-id
   04     .eh_frame_hdr
   05
   06     .init_array .fini_array .jcr .dynamic .got

附 readelf -l 选项说明:

    -l
       --program-headers
       --segments
           Displays the information contained in the file's segment headers, if it has any.

Lib.so的地址是从0x00000000开始的,很明显这个地址无效,因为从上面的例子可以看出,Lib.so的装载地址并不是0x00000000。

7.3 地址无关代码

7.3.1 固定装载地址的困扰

如果像普通可执行文件一样,共享模块装载的地址也是固定的,那么多个共享模块不能共用同一个地址。因为如果它们使用相同的地址,如果被同一个程序使用的话,就造成了冲突。
所以共享对象在编译时不能假设自己在进程虚拟地址空间中的位置。

7.3.2 装载时重定位

和静态链接中的重定位思想一样。在装载时,当所有模块地址确定之后,对程序中所有绝对地址引用进行重定位。
由于重定位需要修改指令,那么动态模块的指令部分就无法在多个进程之间共享,对于不同进程必须有不同的副本。
(这个地方不是特别理解,意思是动态模块还会调用其他模块,而其他模块的地址又是不确定的,所以需要重定位?也就是说所有的模块,虽然物理内存中只有一份,但是在每个进程中都是对应着不同的虚拟地址的?)

7.3.3 地址无关代码

-fPIC即是用来生成地址无关代码的。
地址无关代码即是为了解决装载时重定位导致的无法在多个进程之间共享的问题:将指令中需要被修改的部分分离出来,和数据部分放在一起。
地址引用方式可以分为以下4种:

  • 模块内部的函数调用,跳转
  • 模块内部的数据访问,比如模块中定义的全局变量,静态变量。
  • 模块外部的函数调用,跳转
  • 模块外部的数据访问,比如其他模块中定义的全局变量

1 模块内部的函数调用,跳转
这是最简单的情况。对于现代系统来讲,模块内部的跳转,函数调用都可以是相对地址调用,或者是基于寄存器的相对调用(?),这些指令本身就不需要重定位

2 模块内部的数据访问
由于没有像调用函数一样的相对地址调用指令,所以要用相对地址的方式访问变量要复杂一些。
由于没有汇编的基础知识储备,这个地方理解的不是很清楚,记录如下:

  1. 先获取当前指令的pc值,即当前指令的地址。通过一个很巧妙的get_pc_thunk方法:
00000589 <__x86.get_pc_thunk.ax>:
589:   8b 04 24                mov    (%esp),%eax
58c:   c3                      ret

esp是栈顶寄存器,当进入到get_pc_thunk函数中,esp存储的栈顶地址(函数的返回地址)是下一条指令的地址。所以相当于把call get_pc_thunk指令的下一条指令的地址放入了ecx寄存器中。然后加上相对的偏移量,即得到最终变量的地址。

3 模块间的数据访问
每个模块,在数据段建立一个GOT表,也叫全局偏移表。实际是一个指针数组,分别存储每个跨模块的变量的地址。
由于数据段是每个进程都需要单独复制一份的,所有不存在浪费内存的问题。
GOT表由装载器在装载时进行重定位。指令用一种间接寻址的方式进行寻址,能过访问GOT表找到变量真实的地址。

4 模块外部的函数调用
和上面类型3的解决方法完全一样,不过指针数组中存储的是函数的地址而不是变量的地址。

-fpic和-fPIC
-fpic和具体的平台相关,产生的代码较小,但是会有一些限制。一般情况下,使用fPIC即可。

区分dso是否为PIC
readelf -d pic.so | grep TEXTREL
有输出则不是PIC的,PIC的DSO文件不会有使用代码段重定位表。

  -d
     --dynamic
           Displays the contents of the file's dynamic section, if it has one.

PIC与PIE
地址无关可执行文件。-fPIE和-fpie的关系,与-fPIC和-fpic的关系一样

7.3.4 共享模块的全局变量问题

假设在可执行文件中引用一个外部定义的全局变量为global_var。但是fPIC参数没办法对可执行文件生效,所以编译器会产生对global_var地址的引用。
可执行文件在运行时不会进行代码重定位(不进行重定位??那如何修改引用的共享库中函数及变量的地址?叫装载时重定位?有啥区别),所以变量的地址必须在链接时确定。链接器在创建可执行文件时,会在它的.bss段创建一个global变量的副本。
所有使用这个变量的指令都指向位于可执行文件中的那个副本。当共享模块被装载时,如果某个全局变量在可执文件中拥有副本,那么动态链接器就会把GOT中的相应地址指向副本。如果变量在共享模块中被初始化,那么还需要将初始化值复制可执行文件的副本中。

7.3.5 数据段地址无关性

对于数据段来说,它在每个进程中都有一份独立的副本,并不担心被改变。所以在装载时进行重定位即可。

7.4 延迟绑定(lazy binding)

动态链接的程序(地址无关的)对静态链接的程序要慢个5%。
原因:

  1. 对于全局和静态的数据的访问,以及模块间的调用,都要先定位GOT,再进行间接跳转。
  2. 进程在启动时需要进行重定位

优化措施:
延迟绑定
如果进程一启动就对所有的函数和变量进行重定位,这肯定会拖慢进程的启动速度,一种思路就是只在第一次用到时进行绑定。
ELF使用PLT(procedure linkage table)实现延迟绑定。在指令和GOT中间又增加了PLT这一层。

bar@plt:
jmp *(bar@GOT)
push n
push moduleID
jump _dl_runtime_resolve

为了实现延迟绑定,链接器初始化时,并没有将bar的地址填入到GOT表对应地址中,而是填入push n指令的地址。n是bar在重定位表“.rel.plt”中的下标。然后压入moduleID(作用是啥?)
然后调用_dl_runtime_resolve,对bar这个符号进行解析。解析之后在GOT表中填入正确的值。(应该同时会调用一遍此函数)
第一次调用bar时,会触发调用解析函数,后面就会正常执行。

7.5 动态链接相关结构

通过前面的介绍能够对动态链接的原理有个大概的认识。现在通过其结构来分析具体的实现过程

7.5.1 “.interp”段

决定使用系统的哪个动态链接器

root@debian2:/opt/test# objdump -s a.out | grep -A1 interp
Contents of section .interp:
 0154 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
root@debian2:/opt/test# readelf -l a.out | grep interpre
      [Requesting program interpreter: /lib/ld-linux.so.2]

7.5.2 ".dynamic"段

保存了动态链接器所需要的基本信息,比如

  1. 依赖于哪些共享对象
  2. 动态链接符号表的位置
  3. 动态链接重定位表的位置
  4. 共享对象初始化代码的地址

结构如下:

typedef struct
{
  Elf32_Sword   d_tag;                  /* Dynamic entry type */
  union
    {
      Elf32_Word d_val;                 /* Integer value */
      Elf32_Addr d_ptr;                 /* Address value */
    } d_un;
} Elf32_Dyn;

/* Legal values for d_tag (dynamic entry type).  */

#define DT_NULL         0               /* Marks end of dynamic section */
#define DT_NEEDED       1               /* Name of needed library */
#define DT_PLTRELSZ     2               /* Size in bytes of PLT relocs */
#define DT_PLTGOT       3               /* Processor defined value */
#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_RELASZ       8               /* Total size of Rela relocs */
#define DT_RELAENT      9               /* Size of one Rela reloc */
#define DT_STRSZ        10              /* Size of string table */
//其他不常用的被忽略了

".dynamic"段可以看成动态链接下ELF文件的“文件头”。

root@debian2:/opt/linkLoadLibrary# readelf -d pic.so

Dynamic section at offset 0xf08 contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x3b0
 0x0000000d (FINI)                       0x5a0
 0x00000019 (INIT_ARRAY)                 0x1efc
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x1f00
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x6ffffef5 (GNU_HASH)                   0x138
 0x00000005 (STRTAB)                     0x268
 0x00000006 (SYMTAB)                     0x178
 0x0000000a (STRSZ)                      177 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000003 (PLTGOT)                     0x2000
 0x00000002 (PLTRELSZ)                   16 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x3a0
 0x00000011 (REL)                        0x358
 0x00000012 (RELSZ)                      72 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x338
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x31a
 0x6ffffffa (RELCOUNT)                   3
 0x00000000 (NULL)                       0x0

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

root@debian2:/opt/linkLoadLibrary# ldd pic.so
        linux-gate.so.1 (0xb774d000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb758a000)
        /lib/ld-linux.so.2 (0xb774f000)

7.5.3 动态符号表

先定义两个概念:
导入符号:
引用的其他模块的符号
导出符号:
提供给其他模块用的符号

动态符号表: 表示动态链接这些模块之间的符号导入导出关系。叫.dynsym,对应于静态链接中的.symtab。很多时候,动态链接模块同时拥有.dynsym和.symtab两个表,.dynsym只保存与动态链接相关的符号。而.symtab则保存了所有的符号,包括.dynsym中保存的符号。
动态符号字符串表:.dynstr,和静态链接中的.strtab相同。

7.5.4 动态链接重定位表

共享对象的重定位和静态链接中目标文件的重定位本质是一样的。
在动态链接的文件中,重定位表分别叫做".rel.dyn"和“.rel.plt”。
".rel.dyn"就对数据引用的修正,所修正的位置位于“.got”以及数据段
“.rel.plt”是对函数引用的修正,修正的位置位于“.got.plt”

7.5.5 动态链接时进程堆栈初始化信息

当操作系统把控制权交给动态链接器时,动态链接器必须知道可执行文件和本进程的一些信息。比如可执行文件有几个segment,每个段的属性,程序的入口地址。这些信息由操作系统保存在进程的堆栈里面,从而传递给动态链接器。
堆栈里面还保存了一些辅助信息数组:

typedef struct
{
  uint32_t a_type;              /* Entry type */
  union
    {
      uint32_t a_val;           /* Integer value */
      /* We use to have pointer elements added here.  We cannot do that,
         though, since it does not work when using 32-bit definitions
         on 64-bit platforms and vice versa.  */
    } a_un;
} Elf32_auxv_t;
可以通过程序将堆栈中的初始化信息打印出来:
​```c
#include <stdio.h>
#include <elf.h>

int main( int argc, char *argv[] )
{
	int		* p = (int *) argv;
	int		i;
	Elf32_auxv_t	*aux;

	printf( "Argument count :%d \n", *(p - 1) );

	for ( i = 0; i < *(p - 1); i++ )
	{
		printf( "Argument %d :%s\n", i, *(p + i) );
	}

	p += i;
	p++;

	printf( "Environment:\n" );

	while ( *p )
	{
		printf( "%s\n", *p );
		p++;
	}
	p++;

	printf( "Auxiliary Vector: \n" );
	aux = (Elf32_auxv_t *) p;

	while ( aux->a_type != AT_NULL )
	{
		printf( "Type:%2d Value: %x\n", aux->a_type, aux->a_un.a_val );
		aux++;
	}

	return(0);
}

输出结果:

root@debian2:/opt/linkLoadLibrary# gcc printStack.c -o printStack
root@debian2:/opt/linkLoadLibrary# ./printStack
Argument count :1
Argument 0 :./printStack
Environment:
SSH_CONNECTION=10.0.2.2 56892 10.0.2.15 22
LANG=en_US.UTF-8
DISPLAY=localhost:11.0
XDG_SESSION_ID=42
USER=root
PWD=/opt/linkLoadLibrary
HOME=/root
SSH_CLIENT=10.0.2.2 56892 22
SSH_TTY=/dev/pts/1
MAIL=/var/mail/root
TERM=xterm
SHELL=/bin/bash
SHLVL=1
LANGUAGE=en_US:en
LOGNAME=root
XDG_RUNTIME_DIR=/run/user/0
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=./printStack
OLDPWD=/root
Auxiliary Vector:
Type:32 Value: b77bacd0
Type:33 Value: b77ba000
Type:16 Value: 78bfbbf
Type: 6 Value: 1000
Type:17 Value: 64
Type: 3 Value: 4a1034
Type: 4 Value: 20
Type: 5 Value: 9
Type: 7 Value: b77bc000
Type: 8 Value: 0
Type: 9 Value: 4a1450
Type:11 Value: 0
Type:12 Value: 0
Type:13 Value: 0
Type:14 Value: 0
Type:23 Value: 0
Type:25 Value: bfe485db
Type:31 Value: bfe49fef
Type:15 Value: bfe485eb

7.6 动态链接的步骤和实现

分为3步

7.6.1 动态链接器自举

…略,待日后补充

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
《程序员的自我修养:链接,装载》是一本由林锐、郭晓东、郑蕾等人合著的计算机技术书籍,在该书中,作者从程序员的视角出发,对链接装载等概念进行了深入的阐述和解析。 在计算机编程中,链接是指将各个源文件中的代码模块组合成一个可执行的程序的过程。链接可以分为静态链接动态链接两种方式。静态链接是在编译时将所有代码模块合并成一个独立的可执行文件,而动态链接是在运行时根据需要加载相应的代码模块。 装载是指将一个程序从磁盘上加载到内存中准备执行的过程。在装载过程中,操作系统会为程序分配内存空间,并将程序中的各个模块加载到相应的内存地址上。装载过程中还包括解析模块之间的引用关系,以及进行地址重定位等操作。 是指一组可重用的代码模块,通过链接装载的方式被程序调用。可以分为静态和动态。静态是在编译时将的代码链接到程序中,使程序与的代码合并为一个可执行文件。动态则是在运行时通过动态链接的方式加载并调用。 《程序员的自我修养:链接,装载》对于理解链接装载的原理和机制具有极大的帮助。通过学习这些概念,程序员可以更好地优化代码结构和组织,提高程序的性能和可维护性。同时,了解链接装载的工作原理也对于进行调试和故障排除具有重要意义。 总之,链接装载是计算机编程中的重要概念,对于程序员来说掌握这些知识是非常必要的。《程序员的自我修养:链接,装载》这本书提供了深入浅出的解释和实例,对于想要学习和掌握这些知识的程序员来说是一本非常有价值的参考书籍。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值