Linux加载库文件

就用这个图来解释。
这里写图片描述
其中红色的线是第一次调用库函数的时候程序执行流的路线。蓝色的是程序以后调用要走的路线。

0x01为什么要用这种方式
共享库的一个主要目的是允许多个正在运行的进程共享内存中相同的代码库。如果给这个库分配固定的位置是很简单,但是如果没有一个进程在使用这个库那这部分空间不是也要空出来,并且很难管理这些空间。为了解决这一问题设计了一种动态加载的库,这种库里面的代码跟位置没有关系。每次加载到内存中的位置也是随机的。


0x02
上图中出现了2个名词
PLT(过程链接表):PLT是一个数组,每个条目是16字节代码。PLT[0]比较特殊它跳转到动态连接器中,每个可执行程序调用的库函数都有自己的PLT,每个条目都负责调用一个具体的函数。PLT[1]调用系统启动函数(__lib_start_main)。 是代码段的一部分。
GOT(全局偏移量表):
是一个数组,每个条目是8个字节地址GOT[0]和GOT[1]包含动态链接器在解析函数地址时会用到的信息GOT[2]是动态连接器在ld-linux.so模块中的入口点。其余每个条目对应于被调用的函数。其地址需要在运行时被解析,每个条目都有一个与之对应的plt条目。


0x03过程
1:代码中调用外部函数func,语句形式为call 0xaabbccdd,地址0xaabbccdd实际上就是符号func在PLT表中对应的条目地址(假设地址为标号.PLT2)。

2:PLT表的形式如下

.PLT0: pushl          4(%ebx)           /* GOT表的地址保存在寄存器ebx中 */
jmp            *8(%ebx)
nop; nop
nop; nop
.PLT1: jmp            *name1@GOT(%ebx)
pushl          $offset
jmp            .PLT0@PC
.PLT2: jmp            *func@GOT(%ebx)
pushl          $offset
jmp            .PLT0@PC

3:查看标号.PLT2的语句,实际上是跳转到符号func在GOT表中对应的条目。

4:在符号没有重定位前,GOT表中此符号对应的地址为标号.PLT2的下一条语句,即是pushl offset,其中offset是符号func的重定位偏移量。注意到这是一个二次跳转。

5:在符号func的重定位偏移量压栈后,控制跳到PLT表的第一条目(.PLT0),把GOT[1]的内容(放置了用来标识特定库的代码)压栈,并跳转到GOT[2]对应的地址。

6:GOT[2]对应的实际上是动态符号解析函数的代码,在对符号func的地址解析后,会把func在内存中的地址设置到GOT表中此符号对应的条目中。

7:当第二次调用此符号时,GOT表中对应的条目已经包含了此符号的地址,就可直接调用而不需要利用PLT表进行跳转。

动态连接是比较复杂的,但为了获得灵活性的代价通常就是复杂性。其最终目的是把GOT表中条目的值修改为符号的真实地址,这也可解释节.got包含在可读可写段中。


0x04举例
源代码:

#include <stdio.h>

void hello_world(void)
{
    printf("Hello world!\n");

    return;
}

int main(int argc, char* argv[])
{
    hello_world();

    return 0;
}

编译并反汇编:

gcc -g test.c -o test
objdump -S test
0000000000400537 <main>:

int main(int argc, char* argv[])
{
  400537:   55                      push   %rbp
  400538:   48 89 e5                mov    %rsp,%rbp
  40053b:   48 83 ec 10             sub    $0x10,%rsp
  40053f:   89 7d fc                mov    %edi,-0x4(%rbp)
  400542:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
    hello_world();
  400546:   e8 db ff ff ff          callq  400526 <hello_world>

    return 0;
  40054b:   b8 00 00 00 00          mov    $0x0,%eax
}

需要注意的是

 400546:   e8 db ff ff ff          callq  400526 <hello_world>
void hello_world(void)
{
  400526:   55                      push   %rbp
  400527:   48 89 e5                mov    %rsp,%rbp
    printf("Hello world!\n");
  40052a:   bf e4 05 40 00          mov    $0x4005e4,%edi
  40052f:   e8 cc fe ff ff          callq  400400 <puts@plt>

    return;
  400534:   90                      nop
}
  400535:   5d                      pop    %rbp
  400536:   c3                      retq   

这里的printf函数的调用

40052f:   e8 cc fe ff ff          callq  400400 <puts@plt>

看一下0x400400位置处的内容:

0000000000400400 <puts@plt>:
  400400:   ff 25 12 0c 20 00       jmpq   *0x200c12(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400406:   68 00 00 00 00          pushq  $0x0
  40040b:   e9 e0 ff ff ff          jmpq   4003f0 <_init+0x28>
 0x4003f0:  push   QWORD PTR [rip+0x200c12]        # 0x601008
   0x4003f6:    jmp    QWORD PTR [rip+0x200c14]        # 0x601010
   0x4003fc:    nop    DWORD PTR [rax+0x0]
=> 0x400400 <puts@plt>: jmp    QWORD PTR [rip+0x200c12]        # 0x601018
 | 0x400406 <puts@plt+6>:   push   0x0
 | 0x40040b <puts@plt+11>:  jmp    0x4003f0
 | 0x400410 <__libc_start_main@plt>:    jmp    QWORD PTR [rip+0x200c0a]        # 0x601020
 | 0x400416 <__libc_start_main@plt+6>:  push   0x1
 |->   0x400406 <puts@plt+6>:   push   0x0
       0x40040b <puts@plt+11>:  jmp    0x4003f0
       0x400410 <__libc_start_main@plt>:    jmp    QWORD PTR [rip+0x200c0a]        # 0x601020
       0x400416 <__libc_start_main@plt+6>:  push   0x1

从上面代码可以看出来跳转到0x40006(也就是PLT[2])也就是下一句再一次JMP跳转到了0x4003f0(PLT[0])然后在0x4003f0地址处push了GOT[1]再然后跳转到GOT[2](也就是加载动态链接器,GOT[2]跳转结束就会把函数的解析地址写入到Got表的对应位置)

=> 0x4003f0:    push   QWORD PTR [rip+0x200c12]        # 0x601008
   0x4003f6:    jmp    QWORD PTR [rip+0x200c14]        # 0x601010
   0x4003fc:    nop    DWORD PTR [rax+0x0]
   0x400400 <puts@plt>: jmp    QWORD PTR [rip+0x200c12]        # 0x601018
   0x400406 <puts@plt+6>:   push   0x0

第二次调用:

=> 0x40052a <hello_world+4>:    mov    edi,0x4005e4
   0x40052f <hello_world+9>:    call   0x400400 <puts@plt>

mov语句和call语句合到了一起步入之后

=> 0x7ffff7a7c690 <_IO_puts>:   push   r12
   0x7ffff7a7c692 <_IO_puts+2>: push   rbp
   0x7ffff7a7c693 <_IO_puts+3>: mov    r12,rdi
   0x7ffff7a7c696 <_IO_puts+6>: push   rbx
   0x7ffff7a7c697 <_IO_puts+7>: call   0x7ffff7a98720 <strlen>

直接调用了puts函数

这里存放的就是那些用到的函数的地址了

gdb-peda$ x/100c 0x601000
0x601000:   0x28    0xe 0x60    0x0 0x0 0x0 0x0 0x0
0x601008:   0x68    0xe1    0xff    0xf7    0xff    0x7f    0x0 0x0
0x601010:   0x70    0xe8    0xde    0xf7    0xff    0x7f    0x0 0x0
0x601018:   0x90    0xc6    0xa7    0xf7    0xff    0x7f    0x0 0x0
0x601020:   0x40    0xd7    0xa2    0xf7    0xff    0x7f    0x0 0x0
0x601028:   0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601030:   0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601038 <completed.7594>:  0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601040:   0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601048:   0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601050:   0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601058:   0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601060:   0x0 0x0 0x0 0x0

总结:分析了一下具体过程其实也不麻烦一共也没用几次跳转不过这个好像挺有用,先储备一下
推荐一篇博客讲这个的,我的也是大部分抄的
http://www.cnblogs.com/xingyun/archive/2011/12/10/2283149.html

猜你喜欢

转载自blog.csdn.net/qq_38204481/article/details/79835909