RISU

Table of Contents

1. RISU

1.1. Overview

RISU (Random Instruction Sequence generator for Userspace testing) 可以用来对比两个平台执行同样的的指令时结果是否一致, 例如可以用来测试 qemu 结果是否正确.

以 arm 平台为例, 它的工作流程是:

  1. 使用 risugen 脚本随机生成一些指令, 放在 test.out 中
  2. 在 native arm 平台上执行 risu (做为 master), 它会执行 test.out 中的指令
  3. 在另一个平台例如 qemu 上执行 risu (做为 apprentice), 它同样执行 test.out 中的指令
  4. test.out 中的指令除了正常的 arm 指令, 还会插入一些特殊的 risuop, 这些 op 会触发 SIGILL
  5. master 和 apprentice 的 sighandler 会 catch SIGILL, 双方的 sighandler 通过 socket 把数据 (当前寄存器和内存) 集中到 apprentice, 然后 apprentice 根据 risuop 的不同执行不同的检查, 例如 OP_COMPARE (检查寄存器), OP_COMPAREMEM (检查内存)
  6. 如果检查发现双方不一致, 说明 apprentice 实现有问题

risu 主要用来检查每条指令执行后双方的寄存器以及内存是否一致, 所以它无法检查跳转指令, 也不支持特权指令.

另外, risu 官方目前并不支持 mips 和 risc-v

1.2. Usage

$> cd risu; mkdir build; cd build
$> CROSS_PREFIX=arm-linux-gnueabihf- ../configure --static
$> make
$> ../risugen --numinsns 10 --pattern 'VSTM.*' ../arm.risu vstm.out
  • 使用 socket 来通信

    # 正常情况下应该在一个 native arm 上执行, 这里只是测试, 所以用 qemu 
    $> qemu-arm ./risu --master vstm.out
    master port 9191
    master: waiting for connection on port 9191...
    
    # 在另一个 shell 中
    $> qemu-arm ./risu --host 127.0.0.1 vstm.out
    apprentice host 127.0.0.1 port 9191
    loading test image vstm.out...
    starting apprentice image at 0xff7ec000
    starting image
    
  • 使用 trace 文件来通信

    $> qemu-arm ./risu --master vstm.out -t test.trace
    $> qemu-arm ./risu vstm.out -t test.trace
    

1.3. Implementation details

risu 实现分为三部分:

  1. risu.c
  2. risugen perl 脚本
  3. risugen 针对不同平台的指令配置, 例如 arm.risu, 以及少量的支持性的平台相关代码,例如 risu_reginfo_arm.c, risu_arm.c

1.3.1. risu.c

risu.c 同时实现了 master 和 apprentice 部分, 它的主要功能是:

  1. master 和 apprentice 通信
  2. 通过 sighandler 触发通信和检查, 并且用 sighandler 的 ucontext 读写寄存器 (参考 kernel signal, signal handling in DBT)
  3. 检查寄存器, 内存是否一致
main():
  /* load_image 直接用 mmap 把 xxx.out load 进行, 且入口地址保存在
   * image_start */
  load_image();
  if (ismaster):
    return master();
  else:
    return apprentice();
1.3.1.1. master
master:
  set_sigill_handler(&master_sigill);
  image_start();

void master_sigill(int sig, siginfo_t *si, void *uc):
  send_register_info(uc);
  advance_pc(uc);

send_register_info(void *uc):
  /* uc 是 ucontext, 包含 sigill 发生时 kernel stack 里的寄存器信息 */
  reginfo_init(&ri[MASTER], uc);
  /* 发生 sigill 正常情况下是因为 image 中的 risuop 导致的, 通过
   * ri->faulting_insn 获得这个 risuop */
  op = get_risuop(&ri[MASTER]);

  header.magic = RISU_MAGIC;
  header.pc = get_pc(&ri[MASTER]);
  header.risu_op = op;

  switch (op):
    /* OP_COMPARE 和 OP_COMPAREMEM 需要 master 把 ri (register info) 或 memblock
     * (memory) 发送给 apprentice */      
    case OP_TESTEND:
    case OP_COMPARE:
    case OP_SIGILL:
        header.size = reginfo_size(&ri[MASTER]);
        extra = &ri[MASTER];
        break;
    case OP_COMPAREMEM:
        header.size = MEMBLOCKLEN;
        extra = memblock;
        break;
    /* OP_SETMEMBLOCK 和 OP_GETMEMBLOCK 这两个并不需要发送信息给 apprentice:
     * 它们是负责让 memory 相关指令能读写到正确的内存地址 */
    case OP_SETMEMBLOCK:
    case OP_GETMEMBLOCK:
        header.size = 0;
        extra = NULL;
        break;
    default:
        abort();


  res = write_buffer(&header, sizeof(header));

  if (extra):
    res = write_buffer(extra, header.size);

  switch (op):
    /* 这里会为了处理 OP_SETMEMBLOCK 和 OP_GETMEMBLOCK */
    case OP_COMPARE:
    case OP_SIGILL:
    case OP_COMPAREMEM:
        break;
    case OP_TESTEND:
        return RES_END;
    case OP_SETMEMBLOCK:
        paramreg = get_reginfo_paramreg(&ri[MASTER]);
        memblock = (void *)(uintptr_t)paramreg;
        break;
    case OP_GETMEMBLOCK:
        paramreg = get_reginfo_paramreg(&ri[MASTER]);
        set_ucontext_paramreg(uc, paramreg + (uintptr_t)memblock);
        break;
    default:
        abort();
1.3.1.1.1. memblock

所谓 memblock, 是指有一些指令需要读写 memory (例如 load/store 指令), 为了测试这些指令是否正确, 需要 risu 想办法给这些指令提供一块可以读写的 memory, 称为 memblock.

risu 的做法是:

  1. risugen 时通过 write_memblock_setup 函数在 image 开头预留了一段空间做为 memblock, 并且插入一个 OP_SETMEMBLOCK 在运行时把该 memblock 的真实地址通过 paramreg (例如 r0) 通知给 risu
  2. 由于这个 memblock 是运行时才知道, 所以测试 memory 指令时, risugen 会先生成一个随机的 offset 在 paramreg 中, 然后通过一个 OP_GETMEMBLOCK 对 paramreg 进行修正: 把memblock 的真实地址加到 paramreg 中, 后续 memory 指令就可以读写 paramreg 这个地址了
  3. 每次 memory 相关指令后, 直接检查整个 memblock 就可以了
1.3.1.2. apprentice
apprentice_sigill(int sig, siginfo_t *si, void *uc):
  recv_and_compare_register_info(uc);
  advance_pc(uc);

recv_and_compare_register_info(void *uc):
  /* ri[APPRENTICE] 是 apprentice 自己的 reg */
  /* ri[MASTER] 是 master 传过来的 reg */
  reginfo_init(&ri[APPRENTICE], uc);
  recv_register_info(&ri[MASTER]);
  op = get_risuop(&ri[APPRENTICE]);

  switch (op):
    case OP_COMPARE:
    case OP_TESTEND:
    case OP_SIGILL:
      reginfo_is_eq(&ri[MASTER], &ri[APPRENTICE])      
      break;

    /* OP_SETMEMBLOCK/OP_GETMEMBLOCK 的实现与 master 一样 */
    case OP_SETMEMBLOCK:
        paramreg = get_reginfo_paramreg(&ri[APPRENTICE]);
        memblock = (void *)(uintptr_t)paramreg;
        break;

    case OP_GETMEMBLOCK:
        paramreg = get_reginfo_paramreg(&ri[APPRENTICE]);
        set_ucontext_paramreg(uc, paramreg + (uintptr_t)memblock);
        break;

    case OP_COMPAREMEM:
        /* 用 memcpy memblock 来比较 memory */
        if (memcmp(memblock, other_memblock, MEMBLOCKLEN) != 0) {
            res = RES_MISMATCH_MEM;
        }
        break;

1.3.2. risugen

risugen 通过读 xxx.risu 生成包含多条随机指令和相应的 risuop 的 image.

write_test_code:
  $numinsns = $params->{ 'numinsns' };
  if (grep { defined($insn_details{$_}->{blocks}->{"memory"}) } @keys):
    write_memblock_setup();
  for my $i (1..$numinsns):
    gen_one_insn($forcecond, $insn_details{$insn_enc});
    write_risuop($OP_COMPARE);

sub write_memblock_setup():
  my $align = $MAXALIGN;
  my $datalen = 8192 + $align;
  write_pc_adr(0, (4 * 4) + ($align - 1)); # insn 1
  write_align_reg(0, $align);              # insn 2
  # NOTE: 通过 OP_SETMEMBLOCK 把 memblock 真实地址通知 risu
  write_risuop($OP_SETMEMBLOCK);           # insn 3
  write_jump_fwd($datalen);                # insn 4

  # NOTE: 在 image 预留空间
  for (my $i = 0; $i < $datalen / 4; $i++):
    insn32(rand(0xffffffff));

sub gen_one_insn($$):
  # gen_one_insn 时前面已经通过 pattern 或 key 确定了要生成哪一条指令
  INSN: while(1):
  # NOTE: 指令内容是随机填充的
  my $insn = int(rand(0xffffffff));
  $insn &= ~$fixedbitmask;
  $insn |= $fixedbits;
  if (defined $constraint):
    my $v = eval_with_fields($insnname, $insn, $rec, "constraints", $constraint);
    # NOTE: 如果不满足 constraint, 则重新开始
    if (!$v):
      next INSN;
  # ....
  # 这条指令是 memory 指令
  if (defined $memblock):
    # 在 arm.risu 中 memory 后是一段 perl 代码, 例如:
    #
    # VLD1_stoa A1a 1111 0100 1 d 10 rn:4 vd:4 11 00 sz:2 t a xm:4 \
    # !constraints { ($d == 0 || $t == 0 || $vd != 0xf) && $sz != 3 && ($sz != 0 || $a != 1) && ($rn != $xm); } \
    # !memory { reg($rn); }
    #
    # 其中 reg($rn) 这个 perl 代码在这里会用来生成特定的指令
    #
    $basereg = eval_with_fields($insnname, $insn, $rec, "memory", $memblock);
  # 输出 insn 到 image 中
  insn32($insn);
  if (defined $memblock):
    # NOTE: OP_COMPAREMEM
    write_risuop($OP_COMPAREMEM);

sub reg($@):
  my ($base, @trashed) = @_;
  write_get_offset();
  # 把 r0 中的地址 mov 到 $base 寄存器以便指令能使用这段内存
  write_mov_rr($base, 0);
  write_mov_ri(0, 0);

sub write_get_offset():
  # 随机生成一个 memblock 范围由的 offset, 运行时通过 OP_GETMEMBLOCK 会得到真正的地址
  my $offset = (rand(2048 - 512) + 256) & ~($alignment_restriction - 1);
  write_mov_ri(0, $offset);
  # NOTE: OP_GETMEMBLOCK
  write_risuop($OP_GETMEMBLOCK);

1.3.3. xxx.risu

以 arm.risu 为例:

risu 配置用来枚举所有的指令的名字, 格式, constraints 以及是否需要访问内存. 它的结构与 qemu 的 decodetree 使用的 insn32.decode 有些类似.

例如:

VSTM A1a cond:4 110 p 1 d w 0 rn:4 vd:4 1011 00 imm:5 x \
 !constraints { $p != 1 && $imm != 0 && $imm <= 16 && ((($d << 4) | $vd) + $imm) <= 32; } \
 !memory { reg($rn); }

VADD A1 1111 0010 0 d sz:2 vn:4 vd:4 1000 n q m 0 vm:4

以 VADD 为例, 它与 arm 手册中的定义是完全一致的:

arm_vadd.png

如果 risugen 生成指令时选择了 vadd 指令, 它会:

  1. 首先生成一个随机的 32 bit 指令 insn
  2. 根据上面的指令模板中的 0/1 把 insn 的相应 bit 置位/复位
  3. 解析出 vn, vd, n, q, m, vm 等 field 的值, 如果指令模板中有指定 constraint, 则使用 field 值去测试 constraint 是否满足
  4. 如果指令模板有指定 memory, 则使用 reg 等函数生成随机的地址, 并插入一些 memblock 相关的 risuop: OP_SETMEMBLOCK, OP_GETMEMBLOCK 和 OP_COMPAREMEM

Author: [email protected]
Date: 2023-01-10 Tue 14:16
Last updated: 2023-01-12 Thu 13:53

知识共享许可协议