《开局一个二进制,从零开始的 LoongArch 指令集推导》——第九回 序与跋(五)

本 LoongArch 指令集研究工作在百度贴吧龙芯吧同步连载。

本研究中涉及的逆向工程仅出于学习、研究目的。本研究工作未得到任何龙芯、麒麟等软硬件厂商的任何形式帮助。

本研究属于个人行为,与本人雇主或任何其他主体无关。

上一回我们找出了 ld sd 两条指令,看到了几个寄存器的序号,r4 r5 r12 这样的;但我们并不知道为何一定是这几个寄存器。这一回我们就来一探究竟。为了对初学者友好一些(也为了水篇幅),还是先介绍一些背景资料吧。

回顾 MIPS 的寄存器规范,这里取 n64 ABI:

编号 别名 保存方 备注
0 zero - 读取时固定为 0,写入为空操作(但不影响可能产生的副作用)
1 at Caller 伪指令专用/临时量
2-3 v0-v1 Caller 返回值/临时量
4-11 a0-a7 Caller 入参/临时量
12-15 t0-t3 Caller 临时量
16-23 s0-s7 Callee 保证不被过程调用覆盖
24 t8 Caller 临时量
25 t9 Caller/- 临时量;启用 abicall 的情况下永远代表本函数的入口地址,不可使用
26-27 k0-k1 - 内核专用,其值随时可能被中断处理程序覆盖,不可使用
28 gp - 全局指针;用户态不应修改
29 sp Callee 栈指针
30 s8/fp Callee 保证不被过程调用覆盖,也可用作帧指针
31 ra Caller 返回地址

以及 RISC-V(不带 E 扩展)的寄存器规范:

编号 别名 保存方 备注
0 zero - 读取时固定为 0,写入为空操作(但不影响可能产生的副作用)
1 ra Caller 返回地址
2 sp Callee 栈指针
3 gp - 全局指针;用户态不应修改
4 tp - 线程指针;用户态不应修改
5 t0 Caller 临时量/备选返回地址
6-7 t1-t2 Caller 临时量
8 s0/fp Callee 保证不被过程调用覆盖,也可用作帧指针
9 s1 Callee 保证不被过程调用覆盖
10-11 a0-a1 Caller 入参/返回值/临时量
12-17 a2-a7 Caller 入参/临时量
18-27 s2-s11 Callee 保证不被过程调用覆盖
28-31 t3-t6 Caller 临时量

我们可以看到,RISC 架构一般会有一个寄存器固定为 0(位置不一定,比如 Alpha 架构的 zero 寄存器就是 r31)。返回地址也有独立的寄存器,这一点不像 x86/AMD64。帧指针在开启优化的代码一般都不用,这时候就相当于多了一个被调用方保存的寄存器。其余的寄存器用途也大同小异,无非栈指针,一堆临时量,一堆过程调用后不会被覆盖的量,一堆入参,一到两个返回值。这都是支撑现当代程序语言所必须的,因此可以认为所有架构都一样。

不同的 RISC 架构相互之间也存在一定的区别,例如 MIPS 就保留了两个通用寄存器给中断处理程序,因为不一定有 KScratch 寄存器可用,而中断处理程序必须完整保留、恢复现场,又必须占用寄存器;之后的架构往往吸收了历史教训,例如我们看到 RISC-V 就不再需要抠掉两个寄存器给内核专用了。再或者 MIPS 没有专门的线程指针供快速访问 TLS(thread local storage,线程本地存储,此处非 TLS 协议)区域,而 RISC-V 设计了有。(MIPS 会使用 rdhwr xx, $29 从 RDHWR 寄存器空间的 UserLocal 寄存器拉出这个指针;AMD64 则使用 fs 段寄存器表示这个语义。)

那么我们回到 LoongArch,怎么才能找出哪个寄存器代表什么含义呢?我们现在知道的信息,用表格整理出来,大概是这样:

编号 别名 保存方 备注
1 ra Caller 返回地址
3 sp Callee 栈指针
22 s?/fp Callee 保证不被过程调用覆盖;可用作帧指针

我们有充分的理由相信 LoongArch 的帧指针也是可选利用的,但不清楚它的分配顺序在其他正常 s 系列寄存器之前还是之后。

那么我们接下来可以怎么搞明白其他的部分呢?

当然,我们不能看见手头代码没有用到的指令和用法。还好 Python 这个软件足够通用,该用到的东西基本都会用到。

为了搞明白 zero 寄存器是哪一个(虽然大概率是 0 号寄存器),需要找到一些语义很明显的指令;这个肯定有。

为了搞明白哪些寄存器归被调用方保存,需要找到临时量需求非常大,过程调用很多的函数,也就是大函数;这个肯定有。

为了搞明白哪些寄存器是入参,需要找到参数特别多的函数;虽然一个函数一大堆参数是一种坏味道,但相信也会有。

为了搞明白哪些寄存器放返回值,需要找到,emmmm,有返回值的函数;是个程序都有。

为了搞明白哪个寄存器是线程指针,需要有地方访问 thread-local 变量;这个倒不一定有,但 C 语言的 errno 就是最典型的这么一个量,碰碰运气吧!

至于全局指针,说实话,我们不一定能在 LoongArch 中看到,因为如果 LoongArch 确实如介绍 PPT 中所说的,添加了 PC-relative 的加载、跳转功能,那么就不像 MIPS,对 gp(在 MIPS 指向 GOT)的依赖会削弱很多很多。也碰碰运气吧!

那么我们能找到多少函数和指令来验证我们的想法呢?请看下回分解。