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

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

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

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

前面我们搞了个土办法,已经能借助 MIPS 的工具链方便地检查单个函数的代码了。现在找两个小函数开刀:

const char *
_Py_gitversion(void)
{
    return GITVERSION;
}
0000000000052b94 <_Py_gitversion>:
   52b94:       02ffc063        0x2ffc063
   52b98:       29c02076        slti    zero,t2,8310
   52b9c:       02c04076        0x2c04076
   52ba0:       1c004b6c        bgtz    zero,65954 <_PyObject_CallMethod_SizeT+0x60>
   52ba4:       02fb418c        syscall 0xbed06
   52ba8:       00150184        0x150184
   52bac:       28c02076        slti    zero,a2,8310
   52bb0:       02c04063        0x2c04063
   52bb4:       4c000020        0x4c000020

以及这个也比较小:

void
PyNode_ListTree(node *n)
{
    listnode(stdout, n);
}
0000000000053368 <PyNode_ListTree>:
   53368:       02ff8063        0x2ff8063
   5336c:       29c06061        slti    zero,t2,24673
   53370:       29c04076        slti    zero,t2,16502
   53374:       02c08076        0x2c08076
   53378:       29ffa2c4        slti    ra,t3,-23868
   5337c:       1c0076ac        bgtz    zero,70e30 <stringlib_isspace+0xd8>
   53380:       28dab18c        slti    k0,a2,-20084
   53384:       2600018c        addiu   zero,s0,396
   53388:       28ffa2c5        slti    ra,a3,-23867
   5338c:       00150184        0x150184
   53390:       54001800        0x54001800
   53394:       03400000        0x3400000
   53398:       28c06061        slti    zero,a2,24673
   5339c:       28c04076        slti    zero,a2,16502
   533a0:       02c08063        0x2c08063
   533a4:       4c000020        0x4c000020

这两个函数第一眼看上去,有什么相似之处?

其实多看几个函数就会发现,好像所有函数都是以 02ffxx63 开头,以 02c0xx63 4c000020 结尾。

还有,紧贴着开头和结尾,往往是相同数量的 29c0xxxx28c0xxxx

根据一般的人类认知,很显然,4c000020 应该是从子程序调用返回的意思。而 02 开头的指令,更像是起到了调整栈指针——sp 的作用。

我们把这三个指令用 LoongArch 的指令格式试着分析一下!

02ff8063 = 0000 0010 1111 1111 1000 0000 0110 0011
         = 000000  1011111111100000   00011  00011

02c08063 = 0000 0010 1100 0000 1000 0000 0110 0011
         = 000000  1011000000100000   00011  00011

4c000020 = 0100 1100 0000 0000 0000 0000 0010 0000
         = 010011  0000000000000000   00001  00000

我们先来看前两条指令。看上去像是同一个操作,而且大量连续的 1 和 0,怎么看怎么像两个互为相反数的立即数!

日常生活中常见架构的栈,无一例外都是往下增长的,要支持相反的增长方向,从工具链到内核到各种底层工具,需要填平相当大量的坑。因此我们有足够的理由相信 LoongArch 也不会例外。

鉴于我们处理的会是栈指针 sp 的增减这样一件事,那么显然变化量不会很大。事实上,几百字节的栈帧对绝大多数函数来讲,已经算“巨大”了。这样的话,我们应该能精确“抠”出前两条指令的立即数域了。

000000 1011 111111100000 00011 00011
OPC    OPC2 IMM          RJ    RD

000000 1011 000000100000 00011 00011
OPC    OPC2 IMM          RJ    RD

中间一段的断句一定在 1011 处,不能更靠后,因为两个指令的相同前缀止于此;不能更靠前,因为否则下面这个数的最高位就会为 1,两个数就都变成负数了。

那么这就是 LoongArch 的标准指令格式之一——双寄存器操作数 12 位立即数。两个寄存器都是 r3,两个立即数确实是相反数(正负 32,大家可以复习一下 2 的补码表示),第一个是负数。

于是我们现在有足够的信息,假定这个指令就是 LoongArch 的立即数有符号 64 位加法了。为啥一定是 64 位呢?因为 loongarch64 剧透了啊 ;-)

000000 1011 IMM12 RJ RD  =>  addi rd, rj, imm12

这里借用了 RISC-V 的指令名(所以以后发现的 32 位立即数加法会叫 addiw)。另外,r3 寄存器应该就是 LoongArch 约定的栈指针了。

为了一篇文章不要太长,剩下的指令,就留到下回分解吧~