您的位置:首页 > 运维架构 > Linux

动态链接库之延迟绑定探究

2014-05-07 23:55 330 查看
    动态链接库的优点比较明显,主要集中在节省内存,简化对程序的管理等,对此感兴趣的看官可以去阅读经典的教材
Linker and Loader,国内也有一本经典的教材,俞甲子 石凡 潘爱民编著的程序员的自我修养,讲的也非常好。

    延迟绑定PLT,我迷惑过很久,终于让我遇到一篇写的非常棒的博文,这就是 Position Indepentent code in share library.作者对编译链接的理解十分深刻,写了一个系列的文章,英文水平好的读者可以不听我唠叨,直接看英文原文。我这篇文章主要参考提到的博文,同时参考的程序员的自我修养,即网上的其他博文,做了下实验,对位置无关共享库的理解加深了一大步。说这些的原因是不想陷入版权纠纷,本文所有的东西,基本是学习其他博文
书籍的产物,版权不属于我,版权属于前辈。我做的只是将前辈的讲解综合以下,互相补充。

    动态链接技术,严格的说分成两类,一种是 Load-Time Relocation,这种技术容易理解,但是缺点也比较致命,不能共享,起不到节省内存的目的,目前X86_64已经不提供这种方式;另外一种属于主流的位置无关(PIC)动态库。

    位置无关动态库中,有两个需要解决的问题,一个是动态库中的变量如何访问,另一个是动态库中的函数调用如何访问。其实,我们一般调用的是动态库中的函数,别的不说,printf是libc库提供的函数,当我们调用printf的时候,我们如何找到函数的地址的呢? 这就是本文主要讲述的内容。

    前面提到了延迟绑定,何为延迟绑定呢。这个也比较好理解。libc库中有很多的函数,但是我们编写程序的时候,并不一定会调用libc库中的每个函数,更多的情况下,我们只调用了极少数函数,如果我们将每个函数的地址都解析出来,其实是一种浪费。所以采用的方法是用到函数时再进行对函数的位置进行定位。 这种技术就叫做延迟定位。

    我下面的代码用的是Position Indepentent code in share library博文中的代码,再次强调,版权和荣耀,都属于前辈。

ml_main.c

int myglob = 42;

int ml_util_func(int a)

{

    return a + 1;

}

int ml_func(int a, int b)

{

                int c = b + ml_util_func(a);

                myglob += c;

                return b + myglob;

}

gcc -shared -fpic -g libmlpic.so
ml_main.c

       解释下上面的命令,-shared参数表示生成共享库, -fpic参数表示生成位置无关动态库,前面也提到了,存在两种类型的动态库,如果不带-fpic,那么,生成的是装载时重定位共享库。当然了fpic和fPIC也有区别,这个不是我们关心的内容,不赘述。

driver.c

#define _GNU_SOURCE

#include <link.h>

#include <stdlib.h>

#include <stdio.h>

#include<time.h>

static int header_handler(struct dl_phdr_info* info, size_t
size, void* data)

{

                int j;

                printf("name=%s (%d segments) address=%p\n",

                                                info->dlpi_name, info->dlpi_phnum, (void*)info->dlpi_addr);

                for ( j = 0; j < info->dlpi_phnum; j++) {

                                printf("\t\t header %2d: address=%10p\n", j,

                                                                (void*) (info->dlpi_addr + info->dlpi_phdr[j].p_vaddr));

                                printf("\t\t\t type=%u, flags=0x%X\n",

                                                                info->dlpi_phdr[j].p_type, info->dlpi_phdr[j].p_flags);

                }

                printf("\n");

                return 0;

}

extern int ml_func(int, int);

int main(int argc, const char* argv[])

{

                dl_iterate_phdr(header_handler, NULL);

                int t = ml_func(argc, argc);

                sleep(12);

                return t;

}

gcc -o driver -g driver.c ./libmlpic.so

    我们生成了可执行程序driver,driver调用了共享库libmlpic.so中的函数ml_func。

(gdb) disas main

Dump of assembler code for function main:

0x08048697 <+0>: push %ebp

0x08048698 <+1>: mov %esp,%ebp

0x0804869a <+3>: and $0xfffffff0,%esp

0x0804869d <+6>: sub $0x20,%esp

0x080486a0 <+9>: movl $0x0,0x4(%esp)

0x080486a8 <+17>: movl $0x80485c4,(%esp)

0x080486af <+24>: call 0x80484fc <dl_iterate_phdr@plt>

=> 0x080486b4 <+29>: mov 0x8(%ebp),%eax

0x080486b7 <+32>: mov %eax,0x4(%esp)

0x080486bb <+36>: mov 0x8(%ebp),%eax

0x080486be <+39>: mov %eax,(%esp)

0x080486c1 <+42>: call 0x804849c <ml_func@plt>

0x080486c6 <+47>: mov %eax,0x1c(%esp)

0x080486ca <+51>: movl $0xc,(%esp)

0x080486d1 <+58>: call 0x80484ec <sleep@plt>

0x080486d6 <+63>: mov 0x1c(%esp),%eax

0x080486da <+67>: leave

0x080486db <+68>: ret

End of assembler dump.

    

      我们要调用ml_func函数,但是@plt表明是动态库中的函数,0x804849c是地址,我们反汇编下

Breakpoint 1, 0x080486c1 in main (argc=1, argv=0xbffff7e4) at
driver.c:27

27                    int t = ml_func(argc, argc);

(gdb) disas 0x804849c

Dump of assembler code for function ml_func@plt:

   0x0804849c <+0>:    jmp *0x804a000

   0x080484a2 <+6>:    push
$0x0

   0x080484a7 <+11>: 
 jmp 0x804848c

End of assembler dump.

(gdb) x 0x804a000

0x804a000 <_GLOBAL_OFFSET_TABLE_+12>:    0x080484a2

     理想情况下,0x8048a00存放的是ml_func函数的地址,但是0x80484a2这个地址并不是ml_func函数的地址。所谓延迟绑定的意思就是,并不是一开始就将所有的函数的地址解析好,而是第一次调用库函数时,将库函数的实际地址绑定到GOT的指定位置。 

    我们看到0x8048a00地址是_GLOBAL_OFFSET_TABLE+12,这个可以算出,_GLOBAL_OFFSET_TABLE地址为 0x80489ff4.

(gdb) x/20x 0x8049FF4

0x8049ff4 <_GLOBAL_OFFSET_TABLE_>: 0x08049f18 0x0012c8f8 0x00123220 0x080484a2           - -地址并不是真正                                                                                           的ml_func的地址。

0x804a004 <_GLOBAL_OFFSET_TABLE_+16>: 0x080484b2 0x0018fb70 0x00147af0 0x00178130

0x804a014 <_GLOBAL_OFFSET_TABLE_+32>: 0x080484f2 0x00234e10 0x00000000 0x00000000

0x804a024 <completed.7021>: 0x00000000 0x00000000 0x00000000 0x00000000

0x804a034: 0x00000000 0x00000000 0x00000000 0x00000000

(gdb) x/30i 0x804848c
   0x804848c:
pushl  0x8049ff8
   0x8048492:
jmp    *0x8049ffc
   0x8048498:
add    %al,(%eax)
   0x804849a:
add    %al,(%eax)
   0x804849c <ml_func@plt>:
jmp    *0x804a000
   0x80484a2 <ml_func@plt+6>:
push   $0x0               -----跳到此处,发现不是函数地址,跳转后调                                                                用_dl_runtime_resolve
   0x80484a7 <ml_func@plt+11>:
jmp    0x804848c
   0x80484ac <__gmon_start__@plt>:
jmp    *0x804a004
   0x80484b2 <__gmon_start__@plt+6>:
push   $0x8
   0x80484b7 <__gmon_start__@plt+11>:
jmp    0x804848c
   0x80484bc <putchar@plt>:
jmp    *0x804a008
   0x80484c2 <putchar@plt+6>:
push   $0x10
   0x80484c7 <putchar@plt+11>:
jmp    0x804848c
   0x80484cc <__libc_start_main@plt>:
jmp    *0x804a00c
   0x80484d2 <__libc_start_main@plt+6>:
push   $0x18
   0x80484d7 <__libc_start_main@plt+11>:
jmp    0x804848c
   0x80484dc <printf@plt>:
jmp    *0x804a010
   0x80484e2 <printf@plt+6>:
push   $0x20
   0x80484e7 <printf@plt+11>:
jmp    0x804848c
   0x80484ec <sleep@plt>:
jmp    *0x804a014
   0x80484f2 <sleep@plt+6>:
push   $0x28
   0x80484f7 <sleep@plt+11>:
jmp    0x804848c
   0x80484fc <dl_iterate_phdr@plt>:
jmp    *0x804a018
   0x8048502 <dl_iterate_phdr@plt+6>:
push   $0x30
   0x8048507 <dl_iterate_phdr@plt+11>:
jmp    0x804848c
   0x804850c:
add    %al,(%eax)
   0x804850e:
add    %al,(%eax)

     我们看到0x80484a2处,不是函数地址,而是push 0x0,然后跳转到了0x804848c,然后,将另一个参数动态库的模块ID压栈后,调用传说中的_dl_runtime_resolve,来 获取函数的真正地址。0x123220就是_dl_runtime_resolve的地址,可以看下程序员的自我修养,.got.plt的前三项的特殊含义的说明。

(gdb) b * 0x00123220

Breakpoint 2 at 0x123220: file ../sysdeps/i386/dl-trampoline.S, line 29.

(gdb) c

Continuing.

Breakpoint 2, _dl_runtime_resolve () at ../sysdeps/i386/dl-trampoline.S:29

29 ../sysdeps/i386/dl-trampoline.S: 没有那个文件或目录.

in ../sysdeps/i386/dl-trampoline.S

(gdb) disas 0x00123220

Dump of assembler code for function _dl_runtime_resolve:

=> 0x00123220 <+0>: push %eax

0x00123221 <+1>: push %ecx

0x00123222 <+2>: push %edx

0x00123223 <+3>: mov 0x10(%esp),%edx

0x00123227 <+7>: mov 0xc(%esp),%eax

0x0012322b <+11>: call 0x11d550 <_dl_fixup>

0x00123230 <+16>: pop %edx

0x00123231 <+17>: mov (%esp),%ecx

0x00123234 <+20>: mov %eax,(%esp)

0x00123237 <+23>: mov 0x4(%esp),%eax

0x0012323b <+27>: ret $0xc

End of assembler dump.

我们看到的,_dl_runtime_resolve调用的_dl_fixup,来修改GOT表中的表项,还记的

(gdb) x 0x804a000

0x804a000 <_GLOBAL_OFFSET_TABLE_+12>: 0x080484a2

    执行完,之后,我们看下

(gdb) x/20x 0x8049FF4

0x8049ff4 <_GLOBAL_OFFSET_TABLE_>: 0x08049f18 0x0012c8f8 0x00123220
0x0012e4a7 ----这回这个地址就是ml_func的                                                                                   地址

0x804a004 <_GLOBAL_OFFSET_TABLE_+16>: 0x080484b2 0x0018fb70 0x00147af0 0x00178130

0x804a014 <_GLOBAL_OFFSET_TABLE_+32>: 0x080484f2 0x00234e10 0x00000000 0x00000000

0x804a024 <completed.7021>: 0x00000000 0x00000000 0x00000000 0x00000000

0x804a034: 0x00000000 0x00000000 0x00000000 0x00000000

    我们看到GOT发生了变化,第四项内容从原来的0x80484a2变成了,0x0012e4a7,这个地址,就是传说中的ml_func的地址,我们验证下:

(gdb) p &ml_func

$1 = (int (*)(int, int)) 0x12e4a7 <ml_func>

    Bingo,这就是一个完整的PLT的过程,对_dl_runtime_resolve函数感兴趣的兄弟可以继续深究。

转载自:http://blog.chinaunix.net/uid-24774106-id-3053007.html

参考文献:

1 程序员的自我修养

2 Position
Independent Code (PIC) in shared libraries

ELF动态解析符号过程(修订版)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux GOT PLT 延迟绑定