ARM64上的动态链接

多年前的一个傍晚,在知春路上的一家饭店,我约潘爱民老师共进晚餐。我到饭店不久,潘老师如约而至,坐下后,拿出一本书送给我。书的名字就叫《程序员的自我修养》。

8e90e879a5c4224a5257ece7b6e25e8f.jpeg

我把这本书带回上海后,放到我的书架里。坦率说,我没有花过很大块时间读这本书,只是偶尔拿出来翻一下。为什么呢?不是因为书的内容不好,而是因为书的内容和我的知识库高度重叠,很多东西已经在我的大脑里了。

16938c753fa143ccf7b1c5b183edfbdc.jpeg

最近因为要解决一个问题,需要深入理解Linux ARM64上的动态链接实现,探索一番,收获颇多。考虑到潘老师等人的书是以X86为例,所以特别把ARM64上的实现整理出来,分享给大家。在整理文章时,特别又拿出潘老师等人的书,翻看了第7章《动态链接》。

ae03536a05f80f74eadf031418361ec3.jpeg

为了方便试验和讲解,我特意写了一小段测试程序。

#include <stdio.h>
#include <unistd.h>


int main(int argc, const char* argv[], const char* envp[])
{
        int loop = 10;


        do {
                usleep(100);
        } while(loop);


        return 0;
}

这个小程序故意调用了libc的usleep函数,用于观察动态调用libc函数的过程。

在ARM64上,它的汇编指令为:

uf
ndbgee!main [/gewu/nanocode/nd3/tests/ndbgee.c @ 5]:
5561030754 a9bc7bfd stp x29, x30, [sp, #-0x40]!
5561030758 910003fd mov x29, sp
556103075c b9002fe0 str w0, [sp, #0x2c]
5561030760 f90013e1 str x1, [sp, #0x20]
5561030764 f9000fe2 str x2, [sp, #0x18]
5561030768 52800140 mov w0, #0xa
556103076c b9003fe0 str w0, [sp, #0x3c]
5561030770 52800c80 mov w0, #0x64
5561030774 97ffffaf bl  #0x5561030630
5561030778 b9403fe0 ldr w0, [sp, #0x3c]
556103077c 7100001f cmp w0, #0
5561030780 54ffff81 b.ne        #0x5561030770
5561030784 52800000 mov w0, #0
5561030788 a8c47bfd ldp x29, x30, [sp], #0x40
556103078c d65f03c0 ret

上面的反汇编是使用NDB调试器的uf命令产生的,第一列是内存地址,第二列是机器码。第11行是调用usleep函数。

5561030774 97ffffaf bl  #0x5561030630

其中,5561030774 是指令地址,97ffffaf是指令的机器码,bl是指令的操作符,而bl的操作数0x5561030630便是指向PLT(Procedure Linkage Table)表的目标地址。

PLT是用于动态链接的一张表,它的每一个表项是一小团指令。对于ARM64而言,每个表项是16个字节,刚好容纳4条A64指令。比如,usleep表项的四条指令如下:

u 0x5561030630
ndbgee!__abi_tag+3b8:
5561030630 90000090 adrp        x16, #0x5561040000
5561030634 f947e611 ldr x17, [x16, #0xfc8]
5561030638 913f2210 add x16, x16, #0xfc8
556103063c d61f0220 br  x17

关于动态链接,细讲起来涉及的东西比较多,但是在运行期,关键就是两张表,一张是已经提到的PLT表,另一张便是GOT(Global Offset Table, 全局偏移表)。

使用readelf -S或者ndb的x !**S命令都可以观察elf文件的各个节列表。(下面命令的结果不完整)

x ndbgee!**S


Section Headers:
  [Nr] Name                 Type            Addr             Off    Size   ES Flg Lk Inf Al
  [ 0] <none>               NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp              PROGBITS        0000000000000238 000238 00001b 00   A  0   0  1
  [ 2] .note.gnu.build-id   NOTE            0000000000000254 000254 000024 00   A  0   0  4
  [ 3] .note.ABI-tag        NOTE            0000000000000278 000278 000020 00   A  0   0  4
  [ 4] .gnu.hash            GNU_HASH        0000000000000298 000298 00001c 00   A  5   0  8
  [ 5] .dynsym              DYNSYM          00000000000002b8 0002b8 0000f0 18   A  6   3  8
  [ 6] .dynstr              STRTAB          00000000000003a8 0003a8 000094 00   A  0   0  1
  [ 7] .gnu.version         VERSYM          000000000000043c 00043c 000014 02   A  5   0  2
  [ 8] .gnu.version_r       VERNEED         0000000000000450 000450 000030 00   A  6   1  8
  [ 9] .rela.dyn            RELA            0000000000000480 000480 0000c0 18   A  5   0  8
  [10] .rela.plt            RELA            0000000000000540 000540 000078 18  AI  5  21  8
  [11] .init                PROGBITS        00000000000005b8 0005b8 000018 00  AX  0   0  4
  [12] .plt                 PROGBITS        00000000000005d0 0005d0 000070 00  AX  0   0 16
  [13] .text                PROGBITS        0000000000000640 000640 000150 00  AX  0   0 64

继续刚才的实例,在ARM64上的调用过程分为两个动作。动作1是从GOT表中查找usleep函数的实际地址。动作2便是使用br指令转移到usleep函数。

动作1就是上面四条指令中的前两条:

5561030630 90000090 adrp        x16, #0x5561040000
5561030634 f947e611 ldr x17, [x16, #0xfc8]

第一条是把GOT表的基地址0x5561040000放到x16寄存器,第二条就是使用ldr指令查表,把查到的结果放到x17寄存器。

比较关键的是用LDR指令来查GOT表,反汇编出来的指令是:

ldr x17, [x16, #0xfc8]

其中的立即数0xfc8便是GOT表中的偏移。而GOT表的表项内容就是更新好的目标函数地址,比如usleep表项对应的内容就是usleep函数的起始地址:0x0000007ff7ebf050。

(gdb) x /2gx 0x5555560000+0xfc8
0x5555560fc8 <usleep@got.plt>:  0x0000007ff7ebf050      0x0000000000010da0
(gdb) info symbol 0x5555560fc8
usleep@got[plt] in section .got of /home/geduer/gelabs/gedl/gedl

使用gdb的info symbol指令可以看到,0x0000007ff7ebf050就是libc里的usleep函数【入口】。

(gdb) info symbol 0x0000007ff7ebf050
usleep in section .text of /lib/aarch64-linux-gnu/libc.so.6

看过上面的内容,大家应该明白了在普通运行阶段,跨模块动态调用的执行过程。但要完全理解动态理解,最好还要知道加载阶段所发生的动态修补过程。也就是ld(loader)程序在加载程序时是如何更新GOT表的。

要理解这个原理,就需要了解动态链接所需要的另外一张表,重定位表,它的实际名字比较多,可能是如下名字之一:

.rel.plt
.rela.plt
.rel.dyn
.rela.dyn

其中以.rela命名的比.rel要新,支持rela方式的一般称为rela架构。ARM64是支持rela架构的。

用NDB的x !**r命令可以显示rela表的所有表项:

x ndbgee!**r
Loading symbols for 00000055`8d560000           ndbgee ->   ndbgee


Relocation section .rela.dyn at offset 0x480 contains 8 entries:
  Offset            Info             Type                Sym. Value       Sym. Name              + Addend
0000000000010d90  0000000000000403 R_AARCH64_RELATIVE    0000000000000000  <null>                750
0000000000010d98  0000000000000403 R_AARCH64_RELATIVE    0000000000000000  <null>                700
0000000000010ff0  0000000000000403 R_AARCH64_RELATIVE    0000000000000000  <null>                754
0000000000011008  0000000000000403 R_AARCH64_RELATIVE    0000000000000000  <null>                11008
0000000000010fd8  0000000400000401 R_AARCH64_GLOB_DAT    0000000000000000  _ITM_deregisterTMClone0
0000000000010fe0  0000000500000401 R_AARCH64_GLOB_DAT    0000000000000000  __cxa_finalize        0
0000000000010fe8  0000000600000401 R_AARCH64_GLOB_DAT    0000000000000000  __gmon_start__        0
0000000000010ff8  0000000900000401 R_AARCH64_GLOB_DAT    0000000000000000  _ITM_registerTMCloneTa0


Relocation section .rela.plt at offset 0x540 contains 5 entries:
  Offset            Info             Type                Sym. Value       Sym. Name              + Addend
0000000000010fa8  0000000300000402 R_AARCH64_JUMP_SLOT   0000000000000000  __libc_start_main     0
0000000000010fb0  0000000500000402 R_AARCH64_JUMP_SLOT   0000000000000000  __cxa_finalize        0
0000000000010fb8  0000000600000402 R_AARCH64_JUMP_SLOT   0000000000000000  __gmon_start__        0
0000000000010fc0  0000000700000402 R_AARCH64_JUMP_SLOT   0000000000000000  abort                 0
0000000000010fc8  0000000800000402 R_AARCH64_JUMP_SLOT   000000000000000

rela表的每个表项是如下数据结构:

typedef struct tagElf64_Raw_Rela{
  unsigned char r_offset[8];  /* Location at which to apply the action */
  unsigned char  r_info[8];  /* index and type of relocation */
  unsigned char  r_addend[8];  /* Constant addend used to compute value */
} Elf64_Raw_Rela;

其中的r_offset字段是偏移值,r_info包含符号索引和重定位类型,高32位是对应函数的符号,可以从.dynamic表查到符号的名字。

#define ELF64_R_SYM(i)    ((i) >> 32)
#define ELF64_R_TYPE(i)    ((i) & 0xffffffff)

r_info的低32位是重定位类型,用来指定该如何计算目标地址。

下面是ELF标准中对以上两个字段的解释。

r_offset
This member gives the location at which to apply the relocation action. For a relocatable
file, the value is the byte offset from the beginning of the section to the storage unit affected
by the relocation. For an executable file or a shared object, the value is the virtual address of
the storage unit affected by the relocation.
r_info
This member gives both the symbol table index with respect to which the relocation must be
made, and the type of relocation to apply. For example, a call instruction’s relocation entry
would hold the symbol table index of the function being called. If the index is STN_UNDEF,
the undefined symbol index, the relocation uses 0 as the ‘‘symbol value.’’ Relocation types
are processor-specific. When the text refers to a relocation entry’s relocation type or symbol
table index, it means the result of applying ELF32_R_TYPE or ELF32_R_SYM, respectively,
to the entry’s r_info member

这个夏季,我把大部分时间都投在NDB上,全面支持DWARF5,提升符号解析能力,开启ASAN,挤出深藏的BUG,提高测试标准,完善实现不全的命令......上周,我开始着手改进反汇编结果的可读性,主要是增加符号注释。我把这个小任务取名为“NDB反汇编符号增强”。为了查找类似usleep@plt这样的符号,触发我做上面的试验。花了2天时间,这个功能开始工作了。

2cbd4fdf048669ddcb2dbe75798988c3.png

理解动态链接,是程序员自我修养的一部分。

(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)

*************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

4e6af14f250f5b5a27ca00460fc847c2.png

也欢迎关注格友公众号

0a5a2e748ee25fd18e85b58b7ad416f7.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值