当前位置: 首页 > news >正文

RISCV64应用符号解析的实现机制

Linux应用程序的动态链接

在嵌入式应用开发过程中,可能会遇到一些应用程序的运行错误,例如某个符号不能成功解析:

./testapp: symbol lookup error: ./testapp: undefined symbol: cJSON_Parse

这通常是由依赖动态库版本不一致导致的。出现以上错误时,可能程序已经运行了一段时间,这说明应用依赖外部符号解析是一个动态的过程,并不是在应用启动时完成的;这个功能特点称为Lazy Binding。可以通过配置环境变量LD_BIND_NOW使应用在启动时就完成所有的动态链接符号解析(当然dlopen解析的除外):

export LD_BIND_NOW=1

本文记录了笔者在RISCV64 Linux环境下分析简单应用的符号动态解析的大致过程,使用的简单应用代码如下:

/* testapp.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>typedef int (* pfunc)(FILE *, const char *, ...);
int main(int argc, char *argv[])
{pfunc func;func = fprintf;func(stdout, "`fprintf entry address: %#lx\n", (unsigned long) func);fflush(stdout);func(stdout, "Application will now terminte: %#lx\n", (unsigned long) getpid());fflush(stdout);return 0;
}

应用内部的C语言函数地址

以上代码编译得到testapp,运行结果如下:

root@OpenWrt:~# ./testapp
`fprintf entry address: 0x10470
Application will now terminte: 0x125b

依笔者开发嵌入式应用的经验,fprintf函数定义于glibc库中,而通常动态库代码段的地址会比较高,这一点与上面的运行结果不符。对应用testapp反汇编可得:

0000000000010470 <fprintf@plt>:10470:       00002e17                auipc   t3,0x210474:       ba8e3e03                ld      t3,-1112(t3) # 12018 <fprintf@GLIBC_2.27>10478:       000e0367                jalr    t1,t31047c:       00000013                nop

这说明在应用内部,会对外部引用的(函数)符号创建一小段PLT代码。接下来我们要分析这个代码段是如何解析并跳转到glibc中的fprintf函数中的。

可执行文件ELF的PLT段和GOT.PLT

注意到,上面反汇编得到的fprintf@plt函数位于plt代码段,这个代码段是只读的;与之相应的,还存在一个got.plt段,该段是可读写的(以下结果由IDA反汇编工具得到):

.got:0000000000012008 __TMC_END__:    .dword -1               # DATA XREF: .plt:0000000000010458↑r
.got:0000000000012008                                         # .plt:0000000000010460↑o ...
.got:0000000000012010 qword_12010:    .dword 0                # DATA XREF: .plt:0000000000010468↑r
.got:0000000000012018 fprintf_ptr:    .dword __imp_fprintf    # DATA XREF: fprintf↑r
.got:0000000000012020 __libc_start_main_ptr:.dword __imp___libc_start_main
.got:0000000000012020                                         # DATA XREF: __libc_start_main↑r
.got:0000000000012028 fflush_ptr:     .dword __imp_fflush     # DATA XREF: fflush↑r
.got:0000000000012030 getpid_ptr:     .dword __imp_getpid     # DATA XREF: getpid↑r
.got:0000000000012038 _GLOBAL_OFFSET_TABLE_:.dword _DYNAMIC
.got:0000000000012040 fprintf_ptr_0:  .dword __imp_fprintf    # DATA XREF: main+10↑r
.got:0000000000012048 stdout_ptr:     .dword stdout           # DATA XREF: main+8↑r

GLIBC的动态链接解析后,会将这个表中的fprintf_ptr写入实际的fprintf函数在libc.so.6中实际加载的虚拟地址。在调试分析动态库的解析大致流程前,我们先了解一下plt代码段的实现。应用程序在链接时,静态链接器会应用文件中写入plt段,这一点在binutils中的代码可以得到确认:

/* binutils-2.33.1/bfd/elfnn-riscv.c */
/* Generate a PLT header. */
static bfd_boolean riscv_make_plt_header (bfd *output_bfd, bfd_vma gotplt_addr, bfd_vma addr, uint32_t *entry)
{
.../* auipc  t2, %hi(.got.plt)sub    t1, t1, t3           # shifted .got.plt offset + hdr size + 12l[w|d] t3, %lo(.got.plt)(t2)    # _dl_runtime_resolveaddi   t1, t1, -(hdr size + 12) # shifted .got.plt offsetaddi   t0, t2, %lo(.got.plt)    # &.got.pltsrli   t1, t1, log2(16/PTRSIZE) # .got.plt offsetl[w|d] t0, PTRSIZE(t0)      # link mapjr     t3 */entry[0] = RISCV_UTYPE (AUIPC, X_T2, gotplt_offset_high);entry[1] = RISCV_RTYPE (SUB, X_T1, X_T1, X_T3);entry[2] = RISCV_ITYPE (LREG, X_T3, X_T2, gotplt_offset_low);...return TRUE;
}

相应的,笔者反汇编testapp可以得到以下结果:

Disassembly of section .plt:
0000000000010450 <_PROCEDURE_LINKAGE_TABLE_>:10450:       00002397                auipc   t2,0x210454:       41c30333                sub     t1,t1,t310458:       bb83be03                ld      t3,-1096(t2) # 12008 <__TMC_END__>1045c:       fd430313                addi    t1,t1,-44 # ffffffffffffcfd4 <__global_pointer$+0xfffffffffffea7d4>10460:       bb838293                addi    t0,t2,-109610464:       00135313                srli    t1,t1,0x110468:       0082b283                ld      t0,8(t0)1046c:       000e0067                jr      t30000000000010470 <fprintf@plt>:10470:       00002e17                auipc   t3,0x210474:       ba8e3e03                ld      t3,-1112(t3) # 12018 <fprintf@GLIBC_2.27>10478:       000e0367                jalr    t1,t31047c:       00000013                nop

接下来我们着重调试这两段反汇编代码。

fprintf@plt的跳转调试

笔者在地址0x10474加入断点,可见fprintf@plt的第一条指令执行后,会到当前指令的一个偏移地址(即0x12470);在它的-1112字节处是符号fprintf@got.plt地址(即0x12018地址,上面被IDA反汇编工具命名为fprintf_ptr),它应当保存glibc中函数fprintf的虚拟地址,但却得到了0x10450(这是plt解析的入口地址,对应符号_PROCEDURE_LINKAGE_TABLE_):

Breakpoint 1, 0x0000000000010474 in fprintf@plt ()
(gdb) i r t3
t3             0x12470  74864
(gdb) x/1xg 0x12470-1112
0x12018 <fprintf@got.plt>:      0x0000000000010450
(gdb) stepi
0x0000000000010478 in fprintf@plt ()
(gdb) i r t3
t3             0x10450  66640`

换句话说,当符号解析完成后,地址0x12018处应当被写入fprintf函数的实际地址。fprintf@plt的第三条指令jalr t1, t3,会跳转到t3寄存器中的地址,并将下一条指令地址(即0x1047c)写入t1寄存器。这个寄存器在接下来的跳转后会参与计算。接着继续调试,得到以下结果。

(gdb) tbreak *0x10468
Temporary breakpoint 2 at 0x10468
(gdb) c
Continuing.
Temporary breakpoint 2, 0x0000000000010468 in _PROCEDURE_LINKAGE_TABLE_ ()
(gdb) i r t0 t1 t2 t3
t0             0x12008  73736
t1             0x0      0
t2             0x12450  74832
t3             0x3ff7ff37bc     274743637948
(gdb)

这个结果需要结合汇编和注释来说明:

0000000000010450 <_PROCEDURE_LINKAGE_TABLE_>:10450:   auipc   t2,0x2      # t2 = 指令寄存器偏移,0x12450 (用于GOT.PLT地址访问)10454:   sub t1,t1,t3        # 该XXXX@plt的nop指令到PLT的地址偏移10458:   ld  t3,-1096(t2)    # GOT.PLT的第一个8字节,指向 _dl_runtime_resolve函数地址1045c:   addi    t1,t1,-44   # PLT到fprintf@plt的nop指令偏移量,共44字节10460:   addi    t0,t2,-1096 # t0 = GOT.PLT 的起始地址,在应用链时确定该固定值10464:   srli    t1,t1,0x1   # t1 = N * (16 / 2),对于 fprintf@plt,N值为0,16为xxx@plt的大小10468:   ld  t0,8(t0)        # GOT.PLT的第二个8字节,是 `link_map的指针1046c:   jr  t3              # 跳转到_dl_runtime_resolve函数

上面的汇编注释说明了,GOT.PLT的前两个8字节分别对应_dl_runtime_resolve函数地址,以及struct link_map指针。本文不深入讨论link_map及其符号解析的过程。这两个指针是在应用启动时,由动态链接器写入的(在ELF文件中,两者为-1和0):

/* glibc-2.29/ysdeps/riscv/dl-machine.h */
auto inline int __attribute__ ((always_inline))
elf_machine_runtime_setup (struct link_map *l, int lazy, int profile)
{/* If using PLTs, fill in the first two entries of .got.plt.  */...gotplt[0] = (ElfW(Addr)) &_dl_runtime_resolve;gotplt[1] = (ElfW(Addr)) l;...return lazy;
}

动态链接符号解析后更新fprintf@got.plt

根据上面的调试结果,我们可以直接跟踪0x12018地址处(fprintf@got.plt)内存监视,查看何时被更新了:

(gdb) watch *((unsigned long *) 0x12018)
Watchpoint 3: *((unsigned long *) 0x12018)
(gdb) c
Continuing.Watchpoint 3: *((unsigned long *) 0x12018)Old value = 66640
New value = 274742727904
0x0000003ff7fef0d8 in _dl_fixup (l=<optimized out>, reloc_arg=<optimized out>) at dl-runtime.c:149
149     dl-runtime.c: No such file or directory.
(gdb) bt
#0  0x0000003ff7fef0d8 in _dl_fixup (l=<optimized out>, reloc_arg=<optimized out>) at dl-runtime.c:149
#1  0x0000003ff7ff37f2 in _dl_runtime_resolve () at ../sysdeps/riscv/dl-trampoline.S:61
Backtrace stopped: frame did not save the PC
(gdb) info address fprintf
Symbol "fprintf" is at 0x3ff7f154e0 in a file compiled without debugging.

上面的内存监视断点是比较消耗CPU的,因为笔者用的RISCV64芯片不支持硬件断点。地址0x12018处被写入了274742727904,即十六进制的0x3ff7f154e0,对应着fprintf函数在libc.so.6库中的虚拟地址。这样GLIBC动态链接器就完成了一个函数符号的解析。这里省略了_dl_fixup两个函数的细节,因为其中的内容太多了,不便展开。下面我们关注一下_dl_runtime_resolve的实现,因为这段汇编相对简单:

/* glibc-2.29/sysdeps/riscv/dl-trampoline.S */
ENTRY (_dl_runtime_resolve)
...# Update .got.plt and obtain runtime address of callee.slli a1, t1, 1mv a0, t0       # link mapadd a1, a1, t1  # reloc offset (== thrice the .got.plt offset)la a2, _dl_fixupjalr a2mv t1, a0...
END (_dl_runtime_resolve)

在上现对PLT代码断的注释中,有一行:

10464:   srli    t1,t1,0x1   # t1 = N * (16 / 2)

这里的N是指一个xxx@pltPLT代码段中的序号。对于fprintf@plt来说,它是第一个,即之前的计算结果使得t1的寄存器为0;16是一个xxx@plt段的大小,共4条指令16字节。以上代码又提到了:

add a1, a1, t1  # reloc offset (== thrice the .got.plt offset)

即将t1乘以3后赋给a1寄存器。为什么要这样操作?因为一个重定位结构体的大小是24个字节:

/* glibc-2.29/elf/elf.h */
typedef struct {Elf64_Addr    r_offset;       /* Address */Elf64_Xword   r_info;         /* Relocation type and symbol index */Elf64_Sxword  r_addend;       /* Addend */
} Elf64_Rela;

结合两者可以得到:

N * (16 / 2) * 3 = N * 24 = N * sizeof(struct Elf64_Rela)

这样就可以通过xxx@pltPLT代码段中的偏移得到与其对应的重定位结构体的偏移了(对应代码中的PLTREL,即结构体Elf64_Rela)。不过需要注意的是,PLT代码断与GOT.PLT段中的符号表,存在一个顺序对应的关系,否则以下代码不能成立:

/* glibc-2.29/elf/dl-runtime.c */
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{
...const PLTREL *const reloc= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); /* reloc_offset =  reloc_arg */...

最后,函数符号解析完成后,动态链接器还不忘了调用已解析的函数:

/* glibc-2.29/sysdeps/riscv/dl-trampoline.S */
ENTRY (_dl_runtime_resolve)...# Invoke the callee.jr t1
END (_dl_runtime_resolve)

总结

本文笔者对RISCV64平台,应用运行时的符号解析做了初步的探究,了解了RISCV64平台应用的PLT代码段和GOT.PLT段的结构。但并没有深入分析ELF可执行文件的格式及符号解析的查找、比对等功能实现(对应GLIBC代码中的_dl_fixup函数),感兴趣的伙伴可以深入调试分析。


http://www.mrgr.cn/news/27764.html

相关文章:

  • 算法魅力-二分查找实战
  • 蓝桥杯——数组
  • RHCE的学习(17)
  • git撤销、回退某个commit的修改
  • GPT-5 要来了:抢先了解其创新突破
  • 推荐一款优秀的Flash幻灯片制作软件:Flash Gallery Factory
  • 响应式CSS 媒体查询——WEB开发系列39
  • 艾里斑(Airy Disk)与瑞利判据(Rayleigh criterion)
  • 2024上半年国产操作系统卖疯了!麒麟4.9亿,统信1.9亿!
  • 41.在 CSS 中使用 clamp() 实现响应式排版
  • 【智路】智路OS Perception Fusion Service
  • 暗界正方形之谜
  • 复杂情感识别系统
  • CAD_Electrical 2022使用记录
  • 【加密算法基础——RSA加密特点分析及解密方式】
  • Java面向对象六大设计原则总结(超级详细,附有代码、图解以及案例)
  • 深入理解Python中的“_,”:一个实用的语法特性
  • 神经网络通俗理解学习笔记(3)注意力神经网络
  • 树莓派5上手
  • Java多线程1
  • 闲鱼 sign 阿里228滑块 分析
  • Spring Boot,在应用程序启动后执行某些 SQL 语句
  • QT模型视图结构1
  • 链式二叉树的基本操作(C语言版)
  • 【自动化测试】自动化测试的价值和误区以及如何高效实用地落地自动化测试
  • 2024/9/15 408“回头看”之应用层小总结(下)