ELF动态链接原理分析

最近看了一下ELF的动态链接的原理和实现,分享一下...

准备文件

分析ELF动态链接的原理,当然要先准备一个.so库和一个链接这个.so库的程序了

如果我们使用libc这么庞大的库,会很麻烦,产生信息也会较多,不易分析,所以我们自己写一个

如果我们自己写的库和程序最终用到了libc库这样的大文件,也会很难分析,所以我们要从汇编函数_start开始写

//文件:main.S
//as AT&T汇编文件
.section ".text","ax"
     //.text段
.code64 //64位代码,因为我用的64位系统
.global _start
       //公开_start
.extern main
       //导入main

_start:
   call main
1: //调用main,并死循环
   jmp 1b

然后写一下main.c文件:

int myfunction(int c);
   //在我们的库文件中定义

int main(int argc,char *argv[])
{
   int c = myfunction(argc);
    //调用
   c += argc * myfunction(c);
   return c;//返回
}

最后就是lib.c文件了:

int myfunction(int c)
{
   static int d = 7; 
      //如果不用static会储存在栈里,无法体现
   return c + ++d;
}

最后编译:

gcc -c -o lib.o lib.c
gcc -fPIC -shared -fno-buitin -nostdlib -o liblib.so lib.o
gcc -c -o main.o main.S
gcc -c -o cmain.o main.c
gcc -L. -llib -o main -nostdlib -fno-builtin main.o cmain.o 
这里用了-nostdlib和-fno-builtin禁用了C库函数,防止_start的重定义和不必要的东西掺杂进去

readelf+反汇编分析

windows8@localhost ~/test $ objdump -S main

main:     文件格式 elf64-x86-64


Disassembly of section .plt:

0000000000400370 <myfunction@plt-0x10>:/此处涉及运行时修正全局偏移量表,暂时忽略

  400370:  ff 35 92 0c 20 00     pushq  0x200c92(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  400376:  ff 25 94 0c 20 00     jmpq   *0x200c94(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40037c:  0f 1f 40 00           nopl   0x0(%rax)

0000000000400380 <myfunction@plt>:  //调用Global Offset Table(全局偏移量表中的函数)
  400380:  ff 25 92 0c 20 00     jmpq   *0x200c92(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400386:  68 00 00 00 00        pushq  $0x0   //此处涉及运行时修正全局偏移量表,暂时忽略
  40038b:  e9 e0 ff ff ff        jmpq   400370 <myfunction@plt-0x10>

Disassembly of section .text:

0000000000400390 <_start>:
  400390:  e8 03 00 00 00        callq  400398 <main>
  400395:  eb fe                 jmp    400395 <_start+0x5>
  400397:  90                    nop

0000000000400398 <main>:
  400398:  55                    push   %rbp
  400399:  48 89 e5              mov    %rsp,%rbp //固定语句<具体参见函数调用
  40039c:  48 83 ec 20           sub    $0x20,%rsp //在栈中获取空间,保存变量
  4003a0:  89 7d ec              mov    %edi,-0x14(%rbp) //%edi=>参数1 argc 保存到栈中
  4003a3:  48 89 75 e0           mov    %rsi,-0x20(%rbp) //%rsi=>参数2 argv 保存到栈中
  4003a7:  8b 45 ec              mov    -0x14(%rbp),%eax //argc的值给%eax
  4003aa:  89 c7                 mov    %eax,%edi //调用myfunction的参数1
  4003ac:  e8 cf ff ff ff        callq  400380 <myfunction@plt> //调用myfunction
  4003b1:  89 45 fc              mov    %eax,-0x4(%rbp) //%eax=>返回值 保存到栈中
  4003b4:  8b 45 fc              mov    -0x4(%rbp),%eax //目测是没看编译器优化原因,重复操作怎么这么多
  4003b7:  89 c7                 mov    %eax,%edi       //调用myfunction参数1
  4003b9:  e8 c2 ff ff ff        callq  400380 <myfunction@plt> //再次调用
  4003be:  0f af 45 ec           imul   -0x14(%rbp),%eax //相乘
  4003c2:  01 45 fc              add    %eax,-0x4(%rbp) //取回变量再相加
  4003c5:  8b 45 fc              mov    -0x4(%rbp),%eax //依然是重复语句 %eax=>返回值
  4003c8:  c9                    leaveq //固定语句
  4003c9:  c3                    retq   //返回

通过注释,大家也应该明白了,当我们调用一个动态链接库里的函数,我们会先去call PLT段里的函数,然后跳转到GOT[即Global Offset Table]指向的地址

实际上,第一次GOT指向的正是jmp的下一条指令,然后运行时修正GOT为正确的地址,第二次以及以后就直接跳到正确的地址

我们先不讨论这么复杂的,首先假定第一次就指向了正确的地址,那么这个地址是从哪里来的呢?以及GOT中每一项表示的哪个函数怎么知道的呢?

这就要看REL了,可以用"readelf -r main"来查看:

windows8@localhost ~/test $ readelf -r main

重定位节 '.rela.plt' 位于偏移量 0x358 含有 1 个条目:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 myfunction + 0
windows8@localhost ~/test $ 
第一个Offset,指定了修要修改的位置,一般位于GOT中

此例子中我们用"readelf --sections main"确认一下

windows8@localhost ~/test $ readelf --sections main
共有 17 个节头,从偏移量 0x10d0 开始:

节头:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  /*省略*/
  [12] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000020  0000000000000008  WA       0     0     8
  /*省略*/
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
可以看到got的基地址是0x601000,需要修改的是0x6010018,正好偏移了0x18,和上面照应起来了

然后最后一项name就能找到myfunction,于是我们可以知道这里表示的就是myfunction

解释器要获取myfunction的地址,首先要知道这个myfunction的存储位置,比如本例"liblib.so"
于是它去查看dynamic section,我们可以看"readelf -d main"

windows8@localhost ~/test $ readelf -d main

Dynamic section at offset 0xee0 contains 13 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[liblib.so]
 0x0000000000000004 (HASH)               0x400258
 0x000000006ffffef5 (GNU_HASH)           0x400280
 0x0000000000000005 (STRTAB)             0x400328
 0x0000000000000006 (SYMTAB)             0x4002b0
 0x000000000000000a (STRSZ)              46 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000
 0x0000000000000002 (PLTRELSZ)           24 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400358
 0x0000000000000000 (NULL)               0x0
可以看到,它依赖于共享库liblib.so,于是解释器去查找这个库,找到这个库之后就要查找里面有没有这个函数

我们可以"readelf -s liblib.so"查看

windows8@localhost ~/test $ readelf -s liblib.so 

Symbol table '.dynsym' contains 6 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000000002e4     0 SECTION LOCAL  DEFAULT    5 
     2: 00000000000002e4    33 FUNC    GLOBAL DEFAULT    5 myfunction
     3: 0000000000201004     0 NOTYPE  GLOBAL DEFAULT    9 _edata
     4: 0000000000201008     0 NOTYPE  GLOBAL DEFAULT    9 _end
     5: 0000000000201004     0 NOTYPE  GLOBAL DEFAULT    9 __bss_start

Symbol table '.symtab' contains 20 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000000001c8     0 SECTION LOCAL  DEFAULT    1 
     2: 00000000000001f8     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000000230     0 SECTION LOCAL  DEFAULT    3 
     4: 00000000000002c0     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000000002e4     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000308     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000000320     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000200f40     0 SECTION LOCAL  DEFAULT    8 
     9: 0000000000201000     0 SECTION LOCAL  DEFAULT    9 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT   10 
    11: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS lib.c
    12: 0000000000201000     4 OBJECT  LOCAL  DEFAULT    9 d.1587
    13: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    14: 0000000000200f40     0 OBJECT  LOCAL  DEFAULT    8 _DYNAMIC
    15: 0000000000000000     0 OBJECT  LOCAL  DEFAULT    8 _GLOBAL_OFFSET_TABLE_
    16: 00000000000002e4    33 FUNC    GLOBAL DEFAULT    5 myfunction
    17: 0000000000201004     0 NOTYPE  GLOBAL DEFAULT    9 __bss_start
    18: 0000000000201004     0 NOTYPE  GLOBAL DEFAULT    9 _edata
    19: 0000000000201008     0 NOTYPE  GLOBAL DEFAULT    9 _end
里面写明mufunction这个函数的偏移是02e4,为什么是偏移呢? 我们要从Program Headers说起

我们先来看看main的program "readelf -l main"

windows8@localhost ~/test $ readelf -l main

Elf 文件类型为 EXEC (可执行文件)
入口点 0x400390
共有 9 个程序头,开始于偏移量64

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [正在请求程序解释器:/lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000448 0x0000000000000448  R E    200000
  LOAD           0x0000000000000ee0 0x0000000000600ee0 0x0000000000600ee0
                 0x0000000000000140 0x0000000000000140  RW     200000
  DYNAMIC        0x0000000000000ee0 0x0000000000600ee0 0x0000000000600ee0
                 0x0000000000000120 0x0000000000000120  RW     8
  GNU_EH_FRAME   0x00000000000003cc 0x00000000004003cc 0x00000000004003cc
                 0x000000000000001c 0x000000000000001c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RWE    8
  GNU_RELRO      0x0000000000000ee0 0x0000000000600ee0 0x0000000000600ee0
                 0x0000000000000120 0x0000000000000120  R      1
  PAX_FLAGS      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000         8

 Section to Segment mapping:
  段节...
   00     
   01     .interp 
   02     .interp .hash .gnu.hash .dynsym .dynstr .rela.plt .plt .text .eh_frame_hdr .eh_frame 
   03     .dynamic .got.plt 
   04     .dynamic 
   05     .eh_frame_hdr 
   06     
   07     .dynamic 
   08     
Type(类型)是Load的表示需要加载,然后指定了在文件的地址,在内存的地址和在内存和文件的大小

当我们用execve系统调用,操作系统就自动把文件的这些内容映射到这个地址上,于是程序便可以执行了

但是如果我们写一个共享库,共享库也这样指定地址,那么两个共享库被加载就有可能发生冲突

所以共享库不能假定自己被加载到什么地方,那他怎么访问一些static变量,全局变量呢

于是共享库指定了自己的代码段和数据段之间的偏移,共享库执行的时候用%rip加上当前执行指令到目标数据的偏移,就能够访问了

[x86_64可以直接访问%rip寄存器,对于x86_32,是利用call函数会压栈下一条指令来获取的]


好了,按照这个思路,我们来看一下liblib.so的program headers:

windows8@localhost ~/test $ readelf -l liblib.so 

Elf 文件类型为 DYN (共享目标文件)
入口点 0x2e4
共有 7 个程序头,开始于偏移量64

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000358 0x0000000000000358  R E    200000
  LOAD           0x0000000000000f40 0x0000000000200f40 0x0000000000200f40
                 0x00000000000000c4 0x00000000000000c4  RW     200000
  DYNAMIC        0x0000000000000f40 0x0000000000200f40 0x0000000000200f40
                 0x00000000000000c0 0x00000000000000c0  RW     8
  GNU_EH_FRAME   0x0000000000000308 0x0000000000000308 0x0000000000000308
                 0x0000000000000014 0x0000000000000014  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     8
  GNU_RELRO      0x0000000000000f40 0x0000000000200f40 0x0000000000200f40
                 0x00000000000000c0 0x00000000000000c0  R      1
  PAX_FLAGS      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000         8

 Section to Segment mapping:
  段节...
   00     .hash .gnu.hash .dynsym .dynstr .text .eh_frame_hdr .eh_frame 
   01     .dynamic .data 
   02     .dynamic 
   03     .eh_frame_hdr 
   04     
   05     .dynamic 
   06     
可以看到,第一个要求按照可执行文件分析是把从文件开始到0x358的范围内的东西加载到0x0这个地址
而动态库则是加载到偏移0x0的地址,至于偏移的基地址,可以是任何地址

第二个就是把从0xf40开始的0xc4字节数据加载到偏移0x200f40的地址

[其实两边同时减去0xf40也是同样的效果,等会我们看解释器的时候它就是这么做的(当然大小要加上减去的数据)]


好了,我们反汇编liblib.so看一下吧:

windows8@localhost ~/test $ objdump -S liblib.so 

liblib.so:     文件格式 elf64-x86-64


Disassembly of section .text:

00000000000002e4 <myfunction>:
 2e4:   55                      push   %rbp 
 2e5:   48 89 e5                mov    %rsp,%rbp //固定指令
 2e8:   89 7d fc                mov    %edi,-0x4(%rbp) //保存参数1
 2eb:   8b 05 0f 0d 20 00       mov    0x200d0f(%rip),%eax        # 201000 <d.1587> //得到static变量
 2f1:   83 c0 01                add    $0x1,%eax //加一
 2f4:   89 05 06 0d 20 00       mov    %eax,0x200d06(%rip)        # 201000 <d.1587> //保存回去
 2fa:   8b 05 00 0d 20 00       mov    0x200d00(%rip),%eax        # 201000 <d.1587> //重复指令,再次得到
 300:   03 45 fc                add    -0x4(%rbp),%eax //返回值
 303:   5d                      pop    %rbp //固定指令
 304:   c3                      retq   
这是就是用%rip来获取/改变static变量

[如果有人问为什么每次%rip前的值不一样,因为执行指令的那个地址是不断变化的,每执行一条指令变动一次,所以前面的偏移也要变]


好了,这样我们就知道这整个过程了,我们来总结一下:

1.shell或者我们的程序执行execve命令

2.操作系统按照program headers把我们程序载入内存

3.操作系统检查section headers有没有一个叫做"INTERP"的段[指示这个程序的解释器],有则将其载入内存,将控制权交给解释器

 如果没有,直接将控制权交给这个程序,也就是说不依赖动态链接库

4.返回用户态,执行解释器或这个程序.... 以下为解释器部分

5.根据这个可执行文件的dynamic段,找到依赖库,将其打开

6.根据依赖库的program headers加载依赖库

7.根据可执行文件的rel,获得它要用到的函数

8.对于每个函数,到依赖库中去查找,找不到报错,找到了就修改got指向它的地址

9.将控制权交给这个程序

[对于运行时修正GOT的技术依旧不提,因此实际要比这复杂]


好了,我们怎么确认这个过程呢,注意在execve之后[包括execve]在用户态执行的系统调用都可以用strace查看

解释器也运行在用户态,当然也可以查看它执行的系统调用啦:

windows8@localhost ~/test $ LD_LIBRARY_PATH=. strace ./main
execve("./main", ["./main"], [/* 62 vars */]) = 0 //执行程序
brk(0)                                  = 0x1b01000 //调整堆
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f838054c000 //匿名mmap,分配内存
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)   //不明
open("./tls/x86_64/liblib.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) //开始寻找liblib.so
open("./tls/liblib.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)  //不存在
open("./x86_64/liblib.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) //不存在
open("./liblib.so", O_RDONLY|O_CLOEXEC) = 3 //存在啦
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\344\2\0\0\0\0\0\0"..., 832) = 832 //读取832字节数据
fstat(3, {st_mode=S_IFREG|0755, st_size=5712, ...}) = 0 //得到文件信息
getcwd("/home/windows8/test", 128)      = 20 //获得当前路径,用处不明
mmap(NULL, 2101252, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f838012a000
      //映射liblib.so,注意第一个参数是NULL,也就是说可以得到任何内存,得到的内存是0x7f838012a000
mprotect(0x7f838012b000, 2093056, PROT_NONE) = 0 //略过...
mmap(0x7f838032a000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0x7f838032a000
   //映射liblib.so的第二部分,内存偏移0x7f838032a000 - 0x7f838012a000 = 0x20000 文件偏移0
   //事实上是把文件偏移0xf40映射到内存偏移0x20f40 对应了program headers 
close(3)                                = 0 //关闭文件,以下略....
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f838054b000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f838054a000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8380549000
arch_prctl(ARCH_SET_FS, 0x7f838054a680) = 0
mprotect(0x7f838032a000, 4096, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
^C--- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} ---
Process 29296 detached


至于readelf是怎么得到的哪些信息,可以去读elf64的文档[见参考资料1]


参考资料

1. ELF64文档 : http://downloads.openwatcom.org/ftp/devel/docs/elf-64-gen.pdf

2. Google [elf 动态链接] 结果:

http://www.google.com.hk/#newwindow=1&q=elf+%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5&safe=strict

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值