ELF格式分析动态链接原理

1. 背景

语言:C

平台:arm32

编译工具:arm-gcc

2. 问题

  • 程序A引用动态B.so中的全局变量和函数,全局变量和函数地址在编译时不确定,只有B.so装载时确定,那么如何实现才能满足装载B.so的不动态修改A的代码段中的地址?
  • 地址无关代码是什么意思? 为什么动态库要生成位置无关代码?
  • 反汇编代码中经常看到类似bl  xxx@plt是什么意思?

3. 地址无关代码

  •  概念:程序主要由指令和数据组成,链接器会为每条指令和数据分配独立的地址,当指令要访问数据时,访问的数据的绝对地址。动态库在编译阶段,数据和函数的最终地址是无法确定,需要等到动态装载时方可确定,希望程序模块中的指令在装载时不需要因为装载地址的改变而改变,所以实现的基本思想就是:把指令中需要被修改的部分分离处理,跟数据部分放在一起。这样指令部分就可以保持不变。这样的方案称为地址无关代码技术(PIC, Position-independent code)。
  • 地址引用方式分类:

        共享模块中的地址引用按照是否跨模块分成两类:模块内引用和跨模块引用;按照不同的引用方式又可以分为:指令引用和数据访问,故可以分为四类
        Type1: 模块内数据访问。比如模块内定义的全局变量,静态变量。

        Type2: 模块内函数调用。

        Type3: 模块外部的数据访问:比如其他模块中定义的全局变量。

        Type4: 模块外部的函数调用。

示例代码:

module.so:

module.h
    void module_dump();

module.c
#include"module.h"                                                                                                                    

int global_module = 10;

void module_dump() {
    global_module += 1;
}
~  

elf.c:

int local_global_init = 10;
extern global_module;


void bar() {
    int a = 0;
    int b = 1;
}

void show() {
    //Type 1
    local_global_init +=1;
    //Type3
    global_module += 2;
    //Type2
    bar();
    //Type4
    module_dump();
}   

 编译:
 

arm-linux-androideabi-gcc -fPIC -shared module.c -o module.so
arm-linux-androideabi-gcc -o elf -fPIE -o elf elf.c ./module.so

show函数的反汇编代码:

00000534 <show>:
 534:   e92d4800    push    {fp, lr}
 538:   e28db004    add fp, sp, #4
 53c:   e59f3044    ldr r3, [pc, #68]   ; 588 <show+0x54>
 540:   e08f3003    add r3, pc, r3
 544:   e59f2040    ldr r2, [pc, #64]   ; 58c <show+0x58>
 548:   e08f2002    add r2, pc, r2
 54c:   e5922000    ldr r2, [r2]
 550:   e2821001    add r1, r2, #1
 554:   e59f2034    ldr r2, [pc, #52]   ; 590 <show+0x5c>
 558:   e08f2002    add r2, pc, r2
 55c:   e5821000    str r1, [r2]
 560:   e59f202c    ldr r2, [pc, #44]   ; 594 <show+0x60>
 564:   e7932002    ldr r2, [r3, r2]
 568:   e5922000    ldr r2, [r2]
 56c:   e2822002    add r2, r2, #2
 570:   e59f101c    ldr r1, [pc, #28]   ; 594 <show+0x60>
 574:   e7933001    ldr r3, [r3, r1]
 578:   e5832000    str r2, [r3]
 57c:   ebffffe2    bl  50c <bar>
 580:   ebffff9f    bl  404 <module_dump@plt>
 584:   e8bd8800    pop {fp, pc}
 588:   00001a98    muleq   r0, r8, sl
 58c:   00001ab8            ; <UNDEFINED> instruction: 0x00001ab8
 590:   00001aa8    andeq   r1, r0, r8, lsr #21
 594:   fffffffc            ; <UNDEFINED> instruction: 0xfffffffc


Disassembly of section .data:                                                                                                                              

00002008 <local_global_init>:
    2008:   0000000a    andeq   r0, r0, sl

类型一:模块内部数据访问

        local_global_init +=1;对应的反汇编代码

//r2是地址58c处的值即:00001ab8 
544:   e59f2040    ldr r2, [pc, #64]   ; 58c <show+0x58>
//r2 = pc + r2 = 550 + 1ab8 = 2008
548:   e08f2002    add r2, pc, r2
//r2 = 地址2008处的值:0x0a ,即r2 = 10
 54c:   e5922000    ldr r2, [r2]
//r1 = r2 + 1 = 10 + 1 = 11
 550:   e2821001    add r1, r2, #1


00002008 <local_global_init>:                                                                                                                              
    2008:   0000000a    andeq   r0, r0, sl

根据上面汇编代码看,为何跳转到 local_global_init地址处,show代码段计算出来当前pc值和.data代码段中local_global_init相对偏移offset(1ab8),pc + offset即可访问到local_global_init变量,这是相对跳转。

类型二:模块内函数调用

        四种情况中最简单的情况,因为被调用的函数与调用者之间处于同一个模块,它们之间的相对位置是固定的,所以这种情况比较简单。

      57c:   ebffffe2    bl  50c <bar> //bl即相对跳转指令

bl指令可以参考:arm push/pop/b/bl汇编指令_nginux的博客-CSDN博客

类型三:模块外部变量访问

        模块间数据访问比模块内稍微复杂一点,因为模块间的数据访问目标地址需要等到装载时才能确定,比如上面例子中global_module变量,他被定义在外部模块,即动态库module.so中,并且该地址要动态库装载时才确定。前面提到地址无关代码,基本思想就是将地址相关的部分放到数据段,ELF的做法是在数据段里面建立一个指向这些变量的数据 ,也被称为全局偏移表(Global Offset Table, GOT)。当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用。

 //先计算.got表的函数表(_GLOBAL_OFFSET_TABLE_)的相对偏移
 //r3 = 588地址处的值,即 1a98
 53c:   e59f3044    ldr r3, [pc, #68]   ; 588 <show+0x54>

 //r3 = pc + r3 = 548 + 1a98 = 1FE0                                                                                                 
 540:   e08f3003    add r3, pc, r3
 ....

 //r2 = 地址594处的值,即ffffffc,这是-4的补码。
 560:   e59f202c    ldr r2, [pc, #44]   ; 594 <show+0x60>

 //r2 = r3 + r2地址处的值,即1FE0 - 4 = 1FDC,这个地址存储的是global_module变量的地址
 564:   e7932002    ldr r2, [r3, r2]

 //获取global_module数值,放到r2寄存器
 568:   e5922000    ldr r2, [r2]
 56c:   e2822002    add r2, r2, #2
 570:   e59f101c    ldr r1, [pc, #28]   ; 594 <show+0x60>
 574:   e7933001    ldr r3, [r3, r1]
 578:   e5832000    str r2, [r3]
 57c:   ebffffe2    bl  50c <bar>
 580:   ebffff9f    bl  404 <module_dump@plt>
 584:   e8bd8800    pop {fp, pc}
 588:   00001a98    muleq   r0, r8, sl
 58c:   00001ab8            ; <UNDEFINED> instruction: 0x00001ab8
 590:   00001aa8    andeq   r1, r0, r8, lsr #21
 594:   fffffffc            ; <UNDEFINED> instruction: 0xfffffffc

Disassembly of section .got:                                                                                                                               

//变量对应的GOT表
00001fc8 <_GLOBAL_OFFSET_TABLE_-0x18>:
    1fc8:   00001eb0            ; <UNDEFINED> instruction: 0x00001eb0
    1fcc:   00001ea0    andeq   r1, r0, r0, lsr #29
    1fd0:   00001e98    muleq   r0, r8, lr
    1fd4:   00001ea8    andeq   r1, r0, r8, lsr #29
    1fd8:   000005e4    andeq   r0, r0, r4, ror #11
    1fdc:   00000000    andeq   r0, r0, r0

//函数对应的GOT表
00001fe0 <_GLOBAL_OFFSET_TABLE_>:
    ...
    1fec:   000003cc    andeq   r0, r0, ip, asr #7
    1ff0:   000003cc    andeq   r0, r0, ip, asr #7
    1ff4:   000003cc    andeq   r0, r0, ip, asr #7
    1ff8:   000003cc    andeq   r0, r0, ip, asr #7
    1ffc:   000003cc    andeq   r0, r0, ip, asr #7

注意:arm elf平台变量和函数的got表都在.got section,而x86平台是分为了两个section。

图解上述流程:

流程描述:

        当要访问global_module外部变量时,程序先找到GOT,然后根据GOT中变量对应的表项找到global_module虚拟地址。arm32每个变量都对应一个4字节的地址,链接器装载模块的时候会查找变量的地址,然后填充GOT表中的各个项。由于GOT没有放在.text代码段,所以模块装载时可以动态修改,并且每个进程都可以有独立的副本,相互不受影响。

        GOT是如何做到跟指令的地址无关的?通过汇编代码我们可以看到,GOT表的和当前指令的offset偏移是可以确定的,通过offset相对偏移可解决使用绝对地址的问题。再根据外部变量在GOT中的偏移就可以确定变量的地址。当然GOT每个表项对应哪个变量的地址是由编译器自行决定的,通过objdump工具可以看到每个表项分别对应哪个变量。

        objdump -r elf(readelf -r也可以)可以读取rel.dyn变量重定位段,获取需要重定位的变量的相关信息,其中offset字段就是该变量的重定位地址对应的.got(GOT)的表项,比如global_module的offset = 1fdc(正如上面汇编计算的结果),对应的是.got(GOT)表中global_module的表项地址。

objdump -R elf

offset:global_module从elf头开始的offset,对应到某个.got表项
00001fdc UNKNOWN           global_module

readelf -S elf

...
17] .got              PROGBITS        00001fc8 000fc8 000038 00  WA  0   0  4
...

.got section的地址是1fc8,global_module对应的的.got表项地址:1fdc。

类型四:模块外部函数调用

延迟绑定(PLT)

        通过上面类型三外部变量通过GOT表跳转,很自然的想到外部函数调用也可以使用相同的方式来实现,不过现实中编译器并没有这么简单的实现,其中主要涉及一个性能问题:动态链接下,程序模块包含了大量的函数引用(全局变量相对较少,不然程序模块间耦合很大),程序的运行过程中,很多函数在程序结束前都不会被调用到(比如一些错误处理或者很少用到的功能模块),如果程序装载时就把所有的函数都链接好,解析出来实际的地址将是一种浪费。所以EFL采用了一种延迟绑定(Lazy Binding)的做法,基本思想:函数第一次调用时绑定(符号查找,重定位等),如果没有调用,则不进行绑定。所以程序开始时,模块间的函数调用都没有进行绑定,而是需要用到时才由动态链接器来负责绑定。这样的做法可以大大加速程序的启动速度。

PLT基本原理和实现

        外部函数引用时不直接跳转到GOT表,而是先间接跳转到叫做PLT项的结构。每个外部函数在PLT中都有一个相应的项,比如module_dump函数在PLT中的地址称为module_dump@plt。

module_dump@plt会跳转到对应的GOT表项,函数未执行前,该表项指向了plt section首地址,该首地址会跳转到符号解析函数,解析出来module_dump实际的地址,填入GOT对应的表项,GOT表项填充以后就可以跳转到module_dump函数。


00000404 <module_dump@plt>: 
 //ip = pc + 0 = 40c                                                                                                                               
 404:   e28fc600    add ip, pc, #0, 12

 //ip = ip + 0x1000 = 40c + 0x1000 = 0x140c
 408:   e28cca01    add ip, ip, #4096   ; 0x1000

 //pc = [0x140c + 0xbec] = [1ff8]
 //1ff8地址指向了GOT表
 40c:   e5bcfbec    ldr pc, [ip, #3052]!    ; 0xbec

readelf -r elf指令可以读取plt具体的内容,比如:
Relocation section '.rel.plt' at offset 0x458 contains 8 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00001fe0  00000316 R_ARM_JUMP_SLOT   00000000   __libc_init@LIBC
00001fe4  00000216 R_ARM_JUMP_SLOT   00000000   __cxa_atexit@LIBC
00001fe8  00000416 R_ARM_JUMP_SLOT   00000000   __register_atfork@LIBC
00001fec  00000116 R_ARM_JUMP_SLOT   00000000   printf@LIBC
00001ff0  00000716 R_ARM_JUMP_SLOT   00000000   malloc@LIBC
00001ff4  00000a16 R_ARM_JUMP_SLOT   00000000   xhook_register
00001ff8  00000916 R_ARM_JUMP_SLOT   00000000   xhook_refresh
00001ffc  00000816 R_ARM_JUMP_SLOT   00000000   say_hello

malloc@LIBC代表,在ELF的1ff0地址处理,存放的是libc库malloc地址。

Disassembly of section .got:

00001fc8 <_GLOBAL_OFFSET_TABLE_-0x18>:
    1fc8:   00001eb0            ; <UNDEFINED> instruction: 0x00001eb0
    1fcc:   00001ea0    andeq   r1, r0, r0, lsr #29
    1fd0:   00001e98    muleq   r0, r8, lr
    1fd4:   00001ea8    andeq   r1, r0, r8, lsr #29
    1fd8:   000005e4    andeq   r0, r0, r4, ror #11
    1fdc:   00000000    andeq   r0, r0, r0

//可以看到3cc指向了plt section首地址
00001fe0 <_GLOBAL_OFFSET_TABLE_>:
    ...
    1fec:   000003cc    andeq   r0, r0, ip, asr #7
    1ff0:   000003cc    andeq   r0, r0, ip, asr #7
    1ff4:   000003cc    andeq   r0, r0, ip, asr #7
    1ff8:   000003cc    andeq   r0, r0, ip, asr #7                                                                                                         
    1ffc:   000003cc    andeq   r0, r0, ip, asr #7



Disassembly of section .plt:

//这段汇编实现跳转到符号解析函数,函数地址解析完比填入1ff8地址中
//最终跳转到GOT表+8偏移处,即链接器符号解析函数,编译阶段无法确定,加载时填充,初始化0.
000003cc <__libc_init@plt-0x14>:                                                                                                                           
 3cc:   e52de004    push    {lr}        ; (str lr, [sp, #-4]!)
 3d0:   e59fe004    ldr lr, [pc, #4]    ; 3dc <note_end+0x1fc>
 3d4:   e08fe00e    add lr, pc, lr
 3d8:   e5bef008    ldr pc, [lr, #8]!
 3dc:   00001c04    andeq   r1, r0, r4, lsl #24

000003e0 <__libc_init@plt>:
 3e0:   e28fc600    add ip, pc, #0, 12
 3e4:   e28cca01    add ip, ip, #4096   ; 0x1000
 3e8:   e5bcfc04    ldr pc, [ip, #3076]!    ; 0xc04

图解流程:

 跳转到外部函数module_dump函数流程:

1. bl module_dump@plt

        先跳转到module_dump@plt汇编代码处,即PLT机制的间接跳转

2. module_dump@plt代码

        module_dump@plt会跳转到GOT表中module_dump的表项,如果时非首次调用。0x3cc处已经填充了实际的函数地址,直接跳转到函数地址执行即可。如果是首次调用module_dump函数,GOT表项中默认填充的是.plt的首地址(比如示例代码代码.plt section首地址0x3cc),跳转到3cc处执行

3. .plt section

        跳转到.plt section首地址处执行代码,该处代码会会跳转到GOT表项的第三项,存储的是链接器的符号解析函数(装载时才能确定该地址,初始化时为0)

4. 跳转到符号解析函数

        .plt section首地址处代码执行会跳转到链接器符号解析函数,函数解析到module_dump地址后填充到GOT module_dump的表项,即3cc值被替换成实际的函数地址。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值