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

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

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

在上一回,我们已经介绍了足够多背景知识和方向,现在可以开始把 LoongArch 寄存器规范的拼图完成了!

首先我们先从简单的问题入手,先来尝试回答 saved registers 序列包含哪些寄存器。

在整个工作过程中,为了提高效率,我们一直伴随开发着一个反汇编工具。因为我们现在已经认识构成函数 prologue 的所有指令了,所以直接反汇编整个 .text 段,找到保存寄存器最多的一串 prologue,就能知道哪些寄存器构成 saved registers 了。

这不就找到了吗!

libpython2.7_d.so.1.0 代码(-O0 编译,无优化)

000000000012a750 <PyUnicode_BuildEncodingMap>:

   12a750:      02f3c063        addi    sp, sp, -784
   12a754:      29cc2061        sd      ra, 776(sp)
   12a758:      29cc0076        sd      fp, 768(sp)
   12a75c:      29cbe077        sd      r23, 760(sp)
   12a760:      29cbc078        sd      r24, 752(sp)
   12a764:      29cba079        sd      r25, 744(sp)
   12a768:      29cb807a        sd      r26, 736(sp)
   12a76c:      29cb607b        sd      r27, 728(sp)
   12a770:      29cb407c        sd      r28, 720(sp)
   12a774:      29cb207d        sd      r29, 712(sp)
   12a778:      29cb007e        sd      r30, 704(sp)
   12a77c:      29cae07f        sd      r31, 696(sp)
   12a780:      02cc4076        addi    s9, sp, 784

...

   12afc4:      28cc2061        ld      ra, 776(sp)
   12afc8:      28cc0076        ld      fp, 768(sp)
   12afcc:      28cbe077        ld      r23, 760(sp)
   12afd0:      28cbc078        ld      r24, 752(sp)
   12afd4:      28cba079        ld      r25, 744(sp)
   12afd8:      28cb807a        ld      r26, 736(sp)
   12afdc:      28cb607b        ld      r27, 728(sp)
   12afe0:      28cb407c        ld      r28, 720(sp)
   12afe4:      28cb207d        ld      r29, 712(sp)
   12afe8:      28cb007e        ld      r30, 704(sp)
   12afec:      28cae07f        ld      r31, 696(sp)
   12aff0:      02cc4063        addi    sp, sp, 784
   12aff4:      4c000020        ret
libpython2.7.so.1.0 代码(-O2 编译,有优化)

00000000000e8f20 <PyUnicode_BuildEncodingMap@@Base>:

   e8f20:       02f58063        addi    sp, sp, -672
   e8f24:       29c9a07b        sd      r27, 616(sp)
   e8f28:       29ca6061        sd      ra, 664(sp)
   e8f2c:       29ca4076        sd      fp, 656(sp)
   e8f30:       29ca2077        sd      r23, 648(sp)
   e8f34:       29ca0078        sd      r24, 640(sp)
   e8f38:       29c9e079        sd      r25, 632(sp)
   e8f3c:       29c9c07a        sd      r26, 624(sp)
   e8f40:       29c9807c        sd      r28, 608(sp)
   e8f44:       29c9607d        sd      r29, 600(sp)
   e8f48:       29c9407e        sd      r30, 592(sp)
   e8f4c:       29c9207f        sd      r31, 584(sp)

...

   e9128:       28ca6061        ld      ra, 664(sp)
   e912c:       28ca4076        ld      fp, 656(sp)
   e9130:       28ca2077        ld      r23, 648(sp)
   e9134:       28ca0078        ld      r24, 640(sp)
   e9138:       28c9e079        ld      r25, 632(sp)
   e913c:       28c9c07a        ld      r26, 624(sp)
   e9140:       28c9a07b        ld      r27, 616(sp)
   e9144:       28c9807c        ld      r28, 608(sp)
   e9148:       28c9607d        ld      r29, 600(sp)
   e914c:       28c9407e        ld      r30, 592(sp)
   e9150:       28c9207f        ld      r31, 584(sp)
   e9154:       02ca8063        addi    sp, sp, 672
   e9158:       4c000020        ret

这个函数的 C 代码开头长这样,可以看到有茫茫多的局部变量。而且它的篇幅很长,有茫茫多的函数调用,想必需要保存的东西也会很多。实际上也确实是这样的;再找不到比这更长的 prologue 了。

PyObject*
PyUnicode_BuildEncodingMap(PyObject* string)
{
    Py_UNICODE *decode;
    PyObject *result;
    struct encoding_map *mresult;
    int i;
    int need_dict = 0;
    unsigned char level1[32];
    unsigned char level2[512];
    unsigned char *mlevel1, *mlevel2, *mlevel3;
    int count2 = 0, count3 = 0;

    /* ... */
}

开优化的代码可能由于 instruction scheduling 等等原因,prologue 的指令不一定连续,保存和恢复的具体顺序也不一定。我们按照顺序排列这里看到的一堆寄存器,很显然从 r23 到 r31 都是 saved registers,按顺序分配。如果换成按 stack slot(也就是距离 sp 的偏移量)从高到低排序,也是一样的顺序。(为何要从高到低排呢?因为和大多数架构一样,LoongArch 的栈是从高地址向低地址增长的,因此地址越高,越接近“栈底”,也就是越先被分配。)

搞明白主力的 saved registers 是哪些之后,接下来我们还要研究 fp 应该位于 saved registers 序列的头还是尾。找一些比较小的函数,看下是不是总会出现 fp 就知道了。因为不开优化的代码总会用到 fp,我们只好去开优化的代码里翻翻。私有(static)的函数看不到函数名,也只能硬着头皮看:

不知道哪个函数

   56a20:       02fec063        addi    sp, sp, -80
   56a24:       29c10077        sd      r23, 64(sp)
   56a28:       29c0e078        sd      r24, 56(sp)
   56a2c:       29c0a07a        sd      r26, 40(sp)
   56a30:       29c0807b        sd      r27, 32(sp)
   56a34:       29c0607c        sd      r28, 24(sp)
   56a38:       29c0407d        sd      r29, 16(sp)
   56a3c:       29c0207e        sd      r30, 8(sp)
   56a40:       29c12061        sd      ra, 72(sp)
   56a44:       29c0c079        sd      r25, 48(sp)

还是不知道哪个函数

   5a480:       02ff0063        addi    sp, sp, -64
   5a484:       29c0607a        sd      r26, 24(sp)
   5a488:       29c0e061        sd      ra, 56(sp)
   5a48c:       29c0c077        sd      r23, 48(sp)
   5a490:       29c0a078        sd      r24, 40(sp)
   5a494:       29c08079        sd      r25, 32(sp)
   5a498:       29c0407b        sd      r27, 16(sp)

这样我们就发现了,如果需要保存的寄存器不多,那么是轮不到 fp 上场的,虽然如果 fp 被用到了它会出现在这一批寄存器最底下的 stack slot,只比 ra 靠后。整理一下:

  • 分配顺序(从先到后):r23-r31 fp
  • 栈上顺序(从底到顶):(ra) fp r23-r31

按照分配顺序起名字,r23-r31 应该叫做 s0-s8。至于 fp,虽然可选它作为帧指针,但无奈并没有什么上苍的旨意强制所有程序都要正确维护帧指针,因此它正确的归宿仍然是 saved register 序列,r22 = s9 = fp。它与其他 saved registers 的唯一区别,也就是在栈上排在 ra 之后 s0 之前而已。

我们的知识又扩充了一行!

编号别名保存方备注
1raCaller返回地址
3spCallee栈指针
22s9/fpCallee保证不被过程调用覆盖;可用作帧指针
23-31s0-s8Callee保证不被过程调用覆盖

我们还有好几个问题没有回答;欲知后事如何,请看下回分解。