杂项/ CKB-VM ASM 介绍

CKB-VM ASM 模式是一个使用手写汇编编写的 RISC-V 解释器.

Trace

Trace 是 CKB-VM 执行的基本块(Block). 根据定义, Trace 只有一个入口和出口, Trace 中出现的指令按照它们在代码中出现的顺序执行. 因此, 任何控制流指令, 例如跳转, 调⽤, 返回, 或系统调⽤将结束一个 Trace. 同时, 为了限制 Trace 的大小, 当一个 Trace 中包含的指令超出一定数目时, 也将强制结束该 Trace.

00000000000102b8:
   102b8:   1101                    addi    sp,sp,-32
   102ba:   67c5                    lui     a5,0x11
   102bc:   e822                    sd      s0,16(sp)
   102be:   6445                    lui     s0,0x11
   102c0:   39078713                addi    a4,a5,912 # 11390
   102c4:   39840413                addi    s0,s0,920 # 11398
   102c8:   8c19                    sub     s0,s0,a4
   102ca:   e426                    sd      s1,8(sp)
   102cc:   ec06                    sd      ra,24(sp)
   102ce:   840d                    srai    s0,s0,0x3
   102d0:   39078493                addi    s1,a5,912
   102d4:   e411                    bnez    s0,102e0 <----------------- Trace 0
   102d6:   60e2                    ld      ra,24(sp)
   102d8:   6442                    ld      s0,16(sp)
   102da:   64a2                    ld      s1,8(sp)
   102dc:   6105                    addi    sp,sp,32
   102de:   8082                    ret              <----------------- Trace 1
   102e0:   147d                    addi    s0,s0,-1
   102e2:   00341793                slli    a5,s0,0x3
   102e6:   97a6                    add     a5,a5,s1
   102e8:   639c                    ld      a5,0(a5)
   102ea:   9782                    jalr    a5       <----------------- Trace 2
   102ec:   b7e5                    j       102d4    <----------------- Trace 3

Trace 缓存

在执行流进入 ASM 代码之前, Rust 代码需要做一件事: 构建供 ASM 代码执行的 Trace. 因此 Rust 代码会在当前 PC 位置读取一个指令, 如果该指令不是分支/跳转等指令, 则继续读取下一个指令--直到构造出一个完整的 Trace.

通过构建缓存机制, 使得 Rust 不需要每次都重新生成 Trace. 假设试图从 pc 开始, 取一个 Trace, 其伪代码如下:

trace_cache = [0; 8192];

func gen_trace(pc) -> Trace {
    slot = (pc / 32) % 8192
    if trace_cache[slot].address == pc {
        return trace_cache[slot]
    } else {
        trace = build_new_trace()
        trace_cache[slot] = trace
        return trace
    }
}

Trace 执行

ASM 将 Trace 内的每个指令顺序执行, 直到到达 Trace 的结尾.

Trace 执行的尾递归

如何连接 Rust 代码与 ASM 代码, 一个直观的构建方法是:

    +<---------------------------+
    |                            |
主转码循环(Rust) -> Trace -> ASM 解释器

但是从 ASM 解释器返回到主转码循环会带来很⼤的性能损失, 这需要进行不必要的上下文切换和检查. 为了避免这些负⾯的性能影响,我们采⽤了尾递归方法:当 ASM 执行 Trace 到达 Trace 结尾时,我们跳转到 ASM 代码的入口处而不是退出 ASM 代码进入 Rust 代码. ASM 的入口处代码将会进行 Trace 缓存的检查, 如果缓存命中, 则继续执行新的 Trace; 如果缓存命中失败, 则进入 Rust 代码, 要求 Rust 代码开始新一轮的主转码循环.

    +<---------------------------+
    |                            |        1. 缓存未命中
主转码循环(Rust) -> Trace -> ASM 解释器
                               |    |     2. 缓存命中
                               +<---+

其实由上可知, ASM 主转码循环是惰性工作的, 未被使用到的指令不会被翻译, 频繁被使用到的指令则大概率是被缓存的.