多年前的一个傍晚,在知春路上的一家饭店,我约潘爱民老师共进晚餐。我到饭店不久,潘老师如约而至,坐下后,拿出一本书送给我。书的名字就叫《程序员的自我修养》。
我把这本书带回上海后,放到我的书架里。坦率说,我没有花过很大块时间读这本书,只是偶尔拿出来翻一下。为什么呢?不是因为书的内容不好,而是因为书的内容和我的知识库高度重叠,很多东西已经在我的大脑里了。
最近因为要解决一个问题,需要深入理解Linux ARM64上的动态链接实现,探索一番,收获颇多。考虑到潘老师等人的书是以X86为例,所以特别把ARM64上的实现整理出来,分享给大家。在整理文章时,特别又拿出潘老师等人的书,翻看了第7章《动态链接》。
为了方便试验和讲解,我特意写了一小段测试程序。
#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天时间,这个功能开始工作了。
理解动态链接,是程序员自我修养的一部分。
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号