• 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: 模块外部的函数调用。

    示例代码:

    1. module.so:
    2. module.h
    3. void module_dump();
    4. module.c
    5. #include"module.h"
    6. int global_module = 10;
    7. void module_dump() {
    8. global_module += 1;
    9. }
    10. ~
    11. elf.c:
    12. int local_global_init = 10;
    13. extern global_module;
    14. void bar() {
    15. int a = 0;
    16. int b = 1;
    17. }
    18. void show() {
    19. //Type 1
    20. local_global_init +=1;
    21. //Type3
    22. global_module += 2;
    23. //Type2
    24. bar();
    25. //Type4
    26. module_dump();
    27. }

     编译:
     

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

    show函数的反汇编代码:

    1. 00000534 <show>:
    2. 534: e92d4800 push {fp, lr}
    3. 538: e28db004 add fp, sp, #4
    4. 53c: e59f3044 ldr r3, [pc, #68] ; 588
    5. 540: e08f3003 add r3, pc, r3
    6. 544: e59f2040 ldr r2, [pc, #64] ; 58c
    7. 548: e08f2002 add r2, pc, r2
    8. 54c: e5922000 ldr r2, [r2]
    9. 550: e2821001 add r1, r2, #1
    10. 554: e59f2034 ldr r2, [pc, #52] ; 590
    11. 558: e08f2002 add r2, pc, r2
    12. 55c: e5821000 str r1, [r2]
    13. 560: e59f202c ldr r2, [pc, #44] ; 594
    14. 564: e7932002 ldr r2, [r3, r2]
    15. 568: e5922000 ldr r2, [r2]
    16. 56c: e2822002 add r2, r2, #2
    17. 570: e59f101c ldr r1, [pc, #28] ; 594
    18. 574: e7933001 ldr r3, [r3, r1]
    19. 578: e5832000 str r2, [r3]
    20. 57c: ebffffe2 bl 50c <bar>
    21. 580: ebffff9f bl 404 <module_dump@plt>
    22. 584: e8bd8800 pop {fp, pc}
    23. 588: 00001a98 muleq r0, r8, sl
    24. 58c: 00001ab8 ; <UNDEFINED> instruction: 0x00001ab8
    25. 590: 00001aa8 andeq r1, r0, r8, lsr #21
    26. 594: fffffffc ; <UNDEFINED> instruction: 0xfffffffc
    27. Disassembly of section .data:
    28. 00002008 <local_global_init>:
    29. 2008: 0000000a andeq r0, r0, sl

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

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

    1. //r2是地址58c处的值即:00001ab8
    2. 544: e59f2040 ldr r2, [pc, #64] ; 58c
    3. //r2 = pc + r2 = 550 + 1ab8 = 2008
    4. 548: e08f2002 add r2, pc, r2
    5. //r2 = 地址2008处的值:0x0a ,即r2 = 10
    6. 54c: e5922000 ldr r2, [r2]
    7. //r1 = r2 + 1 = 10 + 1 = 11
    8. 550: e2821001 add r1, r2, #1
    9. 00002008 :
    10. 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 //bl即相对跳转指令

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

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

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

    1. //先计算.got表的函数表(_GLOBAL_OFFSET_TABLE_)的相对偏移
    2. //r3 = 588地址处的值,即 1a98
    3. 53c: e59f3044 ldr r3, [pc, #68] ; 588
    4. //r3 = pc + r3 = 548 + 1a98 = 1FE0
    5. 540: e08f3003 add r3, pc, r3
    6. ....
    7. //r2 = 地址594处的值,即ffffffc,这是-4的补码。
    8. 560: e59f202c ldr r2, [pc, #44] ; 594
    9. //r2 = r3 + r2地址处的值,即1FE0 - 4 = 1FDC,这个地址存储的是global_module变量的地址
    10. 564: e7932002 ldr r2, [r3, r2]
    11. //获取global_module数值,放到r2寄存器
    12. 568: e5922000 ldr r2, [r2]
    13. 56c: e2822002 add r2, r2, #2
    14. 570: e59f101c ldr r1, [pc, #28] ; 594
    15. 574: e7933001 ldr r3, [r3, r1]
    16. 578: e5832000 str r2, [r3]
    17. 57c: ebffffe2 bl 50c
    18. 580: ebffff9f bl 404
    19. 584: e8bd8800 pop {fp, pc}
    20. 588: 00001a98 muleq r0, r8, sl
    21. 58c: 00001ab8 ; instruction: 0x00001ab8
    22. 590: 00001aa8 andeq r1, r0, r8, lsr #21
    23. 594: fffffffc ; instruction: 0xfffffffc
    24. Disassembly of section .got:
    25. //变量对应的GOT表
    26. 00001fc8 <_GLOBAL_OFFSET_TABLE_-0x18>:
    27. 1fc8: 00001eb0 ; instruction: 0x00001eb0
    28. 1fcc: 00001ea0 andeq r1, r0, r0, lsr #29
    29. 1fd0: 00001e98 muleq r0, r8, lr
    30. 1fd4: 00001ea8 andeq r1, r0, r8, lsr #29
    31. 1fd8: 000005e4 andeq r0, r0, r4, ror #11
    32. 1fdc: 00000000 andeq r0, r0, r0
    33. //函数对应的GOT表
    34. 00001fe0 <_GLOBAL_OFFSET_TABLE_>:
    35. ...
    36. 1fec: 000003cc andeq r0, r0, ip, asr #7
    37. 1ff0: 000003cc andeq r0, r0, ip, asr #7
    38. 1ff4: 000003cc andeq r0, r0, ip, asr #7
    39. 1ff8: 000003cc andeq r0, r0, ip, asr #7
    40. 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的表项地址。

    1. objdump -R elf
    2. offset:global_module从elf头开始的offset,对应到某个.got表项
    3. 00001fdc UNKNOWN global_module
    4. readelf -S elf
    5. ...
    6. 17] .got PROGBITS 00001fc8 000fc8 000038 00 WA 0 0 4
    7. ...

    .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函数。

    1. 00000404 :
    2. //ip = pc + 0 = 40c
    3. 404: e28fc600 add ip, pc, #0, 12
    4. //ip = ip + 0x1000 = 40c + 0x1000 = 0x140c
    5. 408: e28cca01 add ip, ip, #4096 ; 0x1000
    6. //pc = [0x140c + 0xbec] = [1ff8]
    7. //1ff8地址指向了GOT表
    8. 40c: e5bcfbec ldr pc, [ip, #3052]! ; 0xbec
    9. //readelf -r elf
    10. Disassembly of section .got:
    11. 00001fc8 <_GLOBAL_OFFSET_TABLE_-0x18>:
    12. 1fc8: 00001eb0 ; instruction: 0x00001eb0
    13. 1fcc: 00001ea0 andeq r1, r0, r0, lsr #29
    14. 1fd0: 00001e98 muleq r0, r8, lr
    15. 1fd4: 00001ea8 andeq r1, r0, r8, lsr #29
    16. 1fd8: 000005e4 andeq r0, r0, r4, ror #11
    17. 1fdc: 00000000 andeq r0, r0, r0
    18. //可以看到3cc指向了plt section首地址
    19. 00001fe0 <_GLOBAL_OFFSET_TABLE_>:
    20. ...
    21. 1fec: 000003cc andeq r0, r0, ip, asr #7
    22. 1ff0: 000003cc andeq r0, r0, ip, asr #7
    23. 1ff4: 000003cc andeq r0, r0, ip, asr #7
    24. 1ff8: 000003cc andeq r0, r0, ip, asr #7
    25. 1ffc: 000003cc andeq r0, r0, ip, asr #7
    26. Disassembly of section .plt:
    27. //这段汇编实现跳转到符号解析函数,函数地址解析完比填入1ff8地址中
    28. //最终跳转到GOT表+8偏移处,即链接器符号解析函数,编译阶段无法确定,加载时填充,初始化0.
    29. 000003cc <__libc_init@plt-0x14>:
    30. 3cc: e52de004 push {lr} ; (str lr, [sp, #-4]!)
    31. 3d0: e59fe004 ldr lr, [pc, #4] ; 3dc
    32. 3d4: e08fe00e add lr, pc, lr
    33. 3d8: e5bef008 ldr pc, [lr, #8]!
    34. 3dc: 00001c04 andeq r1, r0, r4, lsl #24
    35. 000003e0 <__libc_init@plt>:
    36. 3e0: e28fc600 add ip, pc, #0, 12
    37. 3e4: e28cca01 add ip, ip, #4096 ; 0x1000
    38. 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值被替换成实际的函数地址。

  • 相关阅读:
    ceph 原理
    STL初始、容器遍历方式及嵌套
    计算机毕业设计django基于python研究生备考互助系统
    [JavaScript 刷题] 树 - 二叉搜索树中的插入操作, leetcode 701
    2022-09-09 Unity InputSystem4——输入配置文件
    【数据结构入门_数组】 Leetcode 350. 两个数组的交集 II
    【SQL server速成之路】——身份验证及建立和管理用户账户
    Apache文件管理自学笔记——映射文件夹和基于单ip多域名配置apache虚拟机
    Nodejs入门实战一篇精通
    思科交换机VLAN基本配置
  • 原文地址:https://blog.csdn.net/GetNextWindow/article/details/126155878