程序运行之动态链接二

对于之前的动态链接生成的可执行文件,来看下进程的地址空间分布。首先在pro1.c中加入sleep(-1)进行延时,然后将可执行完文件转入后台处理,通过pid来查询进程的地址空间分布。

cat /proc/3488/maps

558a542d3000-558a542d4000 r-xp 00000000 08:03 39585204                   /home /c_prj/programmer/pro1

558a544d3000-558a544d4000 r--p 00000000 08:03 39585204                   /home/c_prj/programmer/pro1

558a544d4000-558a544d5000 rw-p 00001000 08:03 39585204                   /home/c_prj/programmer/pro1

558a55835000-558a55856000 rw-p 00000000 00:00 0                          [heap]

7fab9392e000-7fab93b15000 r-xp 00000000 08:01 13898368                   /lib/x86_64-linux-gnu/libc-2.27.so

7fab93b15000-7fab93d15000 ---p 001e7000 08:01 13898368                   /lib/x86_64-linux-gnu/libc-2.27.so

7fab93d15000-7fab93d19000 r--p 001e7000 08:01 13898368                   /lib/x86_64-linux-gnu/libc-2.27.so

7fab93d19000-7fab93d1b000 rw-p 001eb000 08:01 13898368                   /lib/x86_64-linux-gnu/libc-2.27.so

7fab93d1b000-7fab93d1f000 rw-p 00000000 00:00 0

7fab93d1f000-7fab93d20000 r-xp 00000000 08:03 39585203                   /home/c_prj/programmer/Lib.so

7fab93d20000-7fab93f1f000 ---p 00001000 08:03 39585203                   /home/c_prj/programmer/Lib.so

7fab93f1f000-7fab93f20000 r--p 00000000 08:03 39585203                   /home/c_prj/programmer/Lib.so

7fab93f20000-7fab93f21000 rw-p 00001000 08:03 39585203                   /home/c_prj/programmer/Lib.so

7fab93f21000-7fab93f48000 r-xp 00000000 08:01 13898340                   /lib/x86_64-linux-gnu/ld-2.27.so

7fab9412f000-7fab94132000 rw-p 00000000 00:00 0

7fab94146000-7fab94148000 rw-p 00000000 00:00 0

7fab94148000-7fab94149000 r--p 00027000 08:01 13898340                   /lib/x86_64-linux-gnu/ld-2.27.so

7fab94149000-7fab9414a000 rw-p 00028000 08:01 13898340                   /lib/x86_64-linux-gnu/ld-2.27.so

7fab9414a000-7fab9414b000 rw-p 00000000 00:00 0

7fffd8c91000-7fffd8cb2000 rw-p 00000000 00:00 0                          [stack]

7fffd8d29000-7fffd8d2c000 r--p 00000000 00:00 0                          [vvar]

7fffd8d2c000-7fffd8d2e000 r-xp 00000000 00:00 0                          [vdso]

ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

上面红色标红的部分就是Lib.so被链接进去后的地址,同时还可以看到C程序运行库libc-2.27.so也被链接进来。另外还有一个是ld-2.27.so,这个实际上是Linux下的动态连接器。在程序开始运行前,首先会将控制权交给动态链接器,由它完成所有的链接工作后再把控制权交给proc1。然后开始执行

在同步看下Lib.so文件中的地址分布,发现动态链接模块的装载地址是从地址0x000000000000000开始的。这其实是个无效的地址,并且从上面的进程分布空间来看,Lib.so的最终装载地址并不是0x000000000000000。而是7fab93d1f000。因此我们可以判断,共享对象的装载地址在编译的时候是不确定的,而是在装载的时候,装载器根据当前地址空间的情况,动态分配一块足够大小的虚拟地址空间给响应的共享对象。

readelf -l Lib.so

Elf 文件类型为 DYN (共享目标文件)

Entry point 0x530

There are 7 program headers, starting at offset 64

程序头:

  Type           Offset             VirtAddr           PhysAddr

                 FileSiz            MemSiz              Flags  Align

  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000

                 0x00000000000006f4 0x00000000000006f4  R E    0x200000

  LOAD           0x0000000000000e10 0x0000000000200e10 0x0000000000200e10

                 0x0000000000000218 0x0000000000000220  RW     0x200000

  DYNAMIC        0x0000000000000e20 0x0000000000200e20 0x0000000000200e20

                 0x00000000000001c0 0x00000000000001c0  RW     0x8

  NOTE           0x00000000000001c8 0x00000000000001c8 0x00000000000001c8

                 0x0000000000000024 0x0000000000000024  R      0x4

  GNU_EH_FRAME   0x0000000000000654 0x0000000000000654 0x0000000000000654

                 0x0000000000000024 0x0000000000000024  R      0x4

  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000

                 0x0000000000000000 0x0000000000000000  RW     0x10

  GNU_RELRO      0x0000000000000e10 0x0000000000200e10 0x0000000000200e10

                 0x00000000000001f0 0x00000000000001f0  R      0x1

为什么.so文件的装载地址和进程的会不一样呢,这是为了解决动态链接导致的共享对象地址冲突问题。在动态链接的情况下,单个程序来说,我们可以给不同的模块分配不同的地址,但是多个模块被多个程序使用,管理这些模块的地址就很复杂了,这会导致分配地址冲突。

为了解决这个问题,有两种方法被引入了

1 装载时重定位

假设foobar相对于代码的其实地址是0x100, 当模块被装载到0x10000000时,假设代码段位于模块的最开始,也就是代码的装载地址也是0x10000000。那么就可以确定foobar的地址为0x10000100。这时候系统遍历模块中的重定位表,吧所有对foobar的地址引用都重定位至0x10000100。静态链接的时候这种称为链接重定位,而现在这种情况称为装载重定位。前面在产生共享对象的时候,使用了两个GCC参数”-shared”和”fPIC”,如果只使用shared,那么输出对象就是装载时重定位的方法。

2 地址无关代码

动态链接模块被装载映射至虚拟空间后,指令部分是在多个进程之间共享的。由于装载重定位的方法需要修改指令,所以没有一个方法做到同一份指令被多个进程共享。导致动态链接无法节省内存。因此我们采用地址无关的方式。ELF的做法是在数据段里面建立一个指向这些变量或者函数的指针数组。也被称为全局偏移表:GOT。当代码需要引用该全局变量或者函数的时候,可以通过GOT中相对应的项间接引用。链接器在装载模块的时候会查找每个变量或者函数的地址。然后填充GOT中的每个项,以确保每个指针指向的地址正确。并且每个进程都有独立的副本,相互不受影响。

对于前面的Lib.so文件,通过objdump –h Lib.so。可以看到GOT在文件中的偏移是0x0fe0。

再通过objdump –R Lib.so看下Lib.so需要在动态链接时重定位项:可以看到printf的地址位于0x1018。也就是GOT种的偏移28。相当于是GOT中的第7项(指针占据4个字节)

延迟绑定(PLT)

由于动态链接在处理模块间调用的时候要去查询GOT,然后再进行间接跳转。这样运行速度就比静态链接慢了很多。不过在一个程序运行过程中,可能很多函数在程序执行完时都不会被用到。比如异常或者是用户用得很少的模块,如果一开始就把所有的都链接好对性能确实是一种损失。所以ELF采用了一种叫延迟绑定的做法:函数第一次被调用的时候才进行绑定(符号查找,重定位等)。如果没有用到则不进行绑定,所以程序开始执行时,模块间的函数调用都没有进行绑定。而是需要用到的时候才由动态链接器负责绑定。这就可以大大加快程序的运行速度。

具体做法如下:

1 调用的时候不是直接根据GOT实现跳转,PLT为了实现延迟绑定,在这个过程中增加了一层间接跳转。每个外部函数在PLT中都有一个相应的项,比如bar函数在PLT中的项的地址称之为bar@plt。具体实现如下:

bar@plt:

jmp *(bar@GOT)

push n

push moduleID

jump _dl_runtimt_resolve

jmp *(bar@GOT)表示GOT中保存bar()这个函数相应的项。如果链接器在初始化阶段已经初始化该项。并且将bar()地址填入该项。那么这个跳转结果就是我们所期待的跳转到bar()。实现函数正确调用。但是为了实现延迟绑定,链接器在初始阶段并没有将bar地址放入GOT。所以从push n开始执行。数字n代表符号引用在重定位表 .rel.plt中的下标,接着又是一条push执行将moduleID压入到堆栈。然后跳转到_dl_runtimt_resolve。_dl_runtimt_resolve函数来完成符号解析和重定位工作。_dl_runtimt_resolve在进行查找和定位后将bar的真正地址填入到GOT中。一旦bar()函数解析完毕,当我们再次调用bar@plt的时候,第一条jmp指令就能够跳转到真正的bar函数,bar函数返回的时候会根据堆栈中保存的EIP直接返回到调用者,而不会执行bar@plt中第二条指令开始的那段代码。,那段代码只会在符号未被解析时执行一次。

原文地址:https://www.cnblogs.com/zhanghongfeng/p/11018195.html