QEMU TCG
Table of Contents
1. QEMU TCG
https://www.linaro.org/blog/the-evolution-of-the-qemu-translator/
https://github.com/airbus-seclab/qemu_blog/blob/main/tcg_p1.md
本文基于 qemu 7.1.0
TCG (tiny code generator) 是 qemu 执行 dynamic binary translation (DBT) 的组件, 和编译器类似, 它也包括 frontend, backend 和中间表示.
以 x86 (host) 上执行 riscv (target) 指令为例, frontend 负责把 binary 中的 riscv 指令翻译成中间表示, backend 负责把中间表示翻译成 x86 指令.
当 host 与 target 相同时, qemu 会利用 kvm 等虚拟化技术而不再使用 TCG.
1.1. decodetree
decodetree 是一个 python 程序, 用来生成 tcg 的 frontend 入口. 以 x86 上执行 riscv 指令为例, 针对每条 riscv 指令, 生成的代码会调用 target/riscv/translate.c 中对应的代码, 生成对应的 tcg 中间表示. 然后 x86 后端负责把 tcg 中间表示翻译成 x86 指令.
$> cd qemu-7.1.0/target/riscv $> python3 ../../scripts/decodetree.py insn32.decode --static-decode=decode_insn32
生成的代码大约是这样:
static bool decode_insn32(DisasContext *ctx, uint32_t insn) { switch (insn & 0x0000007f) { case 0x00000003: /* ........ ........ ........ .0000011 */ decode_insn32_extract_i(ctx, &u.f_i, insn); switch ((insn >> 12) & 0x7) { case 0x0: /* ........ ........ .000.... .0000011 */ /* insn32.decode:125 */ if (trans_lb(ctx, &u.f_i)) return true; break; /* ... */ } /* ... */ } }
decodetree.py 根据 insn32.decode 中写的指令格式来生成代码, insn32.decode 大约是这样:
addi ............ ..... 000 ..... 0010011 @i vle8_v ... 000 . 00000 ..... 000 ..... 0000111 @r2_nfvm mret 0011000 00010 00000 000 00000 1110011
如果要添加新的 riscv target 指令, 需要:
- 修改 insn32.decode
- 在 target/riscv/translate.c 中实现对应的 trans_xxx 函数
qemu 的 decodetree 和 opcodes 功能类似
Backlinks
RISU (RISU > Implementation details > xxx.risu): risu 配置用来枚举所有的指令的名字, 格式, constraints 以及是否需要访问内存. 它的 结构与 qemu 的 decodetree 使用的 insn32.decode 有些类似.
1.2. helper
https://fulcronz27.wordpress.com/2014/06/09/qemu-call-a-custom-function-from-tcg/
并非所有的 target 指令在 tcg 中都走 `target insn -> tcg ir -> host insn` 这条路径, 有些 target insn 是 tcg ir 不支持的, 主要是一些 riscv extenstion 例如 fpu, bitmanip, m128, vector 等以及一些 privileged insn 例如 mret, wfi, tlb flush 以及 csr 相关指令, 这些 target insn 都会通过 helper 直接以外部函数的形式执行.
riscv target 使用的 helper 有:
- fpu helper
- bitmanip helper
- crypto helper
- m128 helper
- vector helper
- op helper
以 riscv fmadd_s 指令为例:
trans_fmadd_s(DisasContext *ctx, arg_fmadd_s *a): TCGv_i64 dest = dest_fpr(ctx, a->rd); TCGv_i64 src1 = get_fpr_hs(ctx, a->rs1); TCGv_i64 src2 = get_fpr_hs(ctx, a->rs2); TCGv_i64 src3 = get_fpr_hs(ctx, a->rs3); gen_set_rm(ctx, a->rm); gen_helper_fmadd_s(dest, cpu_env, src1, src2, src3); gen_set_fpr_hs(ctx, a->rd, dest); mark_fs_dirty(ctx); return true; /* gen_helper_fmadd_s 无法直接找到定义, 它是一系列宏展开的结果 */ /* include/exec/helper-gen.h */ #define DEF_HELPER_FLAGS_0(name, flags, ret) \ static inline void glue(gen_helper_, name)(dh_retvar_decl0(ret)) \ { \ tcg_gen_callN(HELPER(name), dh_retvar(ret), 0, NULL); \ } /* 根据前面的宏, gen_helper_xxx 会展开成 tcg_gen_call_N(helper_xxx) */ gen_helper_fmadd_s: tcg_gen_callN(helper_fmadd_s, ...); /* target/riscv/fpu_helper.c */ helper_fmadd_s(CPURISCVState *env, uint64_t frs1, uint64_t frs2,uint64_t frs3): /* do_fmadd_s 会直接调用到 host 的 fmaf 函数 (如果 host 支持 hard float) */ return do_fmadd_s(env, frs1, frs2, frs3, 0);
以 riscv rvv 指令为例:
tcg 并不会把 rvv 翻译成 x86 的 SSE/AVX, tcg 的作法相当于用一个 array 模拟 rvv 操作.
以 vadd_vv 为例, 宏展开结果大约是:
helper_vadd_vv_b : do_vext_vv(vd, v0, vs1, vs2, env, desc, do_vadd_vv_b, ESZ); for (i = env->vstart; i < vl; i++): /* vd, vs1, vs2 这些 vector register 实际是通过 buffer 来模拟的 */ /* fn 是 do_vadd_vv_b */ fn(vd, vs1, vs2, i); do_vadd_vv_b(void *vd, void *vs1, void *vs2, int i): TX1 s1 = *((T1 *)vs1 + HS1(i)); TX2 s2 = *((T2 *)vs2 + HS2(i)); *((TD *)vd + HD(i)) = s2 + s1;
一般情况下给 target 添加自定义指令时都会采用 helper 的形式去实现 trans_xxx 函数, 因为 target 自定义指令通常没有对应的 tcg ir.
使用 helper 添加 riscv 指令的步骤是:
- 参考 decodetree 部分, 添加新的 riscv 指令
- 修改 `target/riscv/helper.h` 添加新的 helper 声明
- 修改 `target/riscv/translate.c`, 实现 trans_xxx, 让它调用 gen_helper_xxx
- 修改 `target/riscv/xxx_helper.c`, 实现 helper_xxx 函数
也可以用 `include/exec/helper-head.h` 中的 HELPER 宏简化上面的一些操作.
1.3. backtrace
cpu_exec: while (!cpu_handle_exception(cpu, &ret)): while (!cpu_handle_interrupt(cpu, &last_tb)): tb = tb_lookup(cpu, pc, cs_base, flags, cflags); if (tb == NULL): tb = tb_gen_code(cpu, pc, cs_base, flags, cflags); cpu_loop_exec_tb(cpu, tb, &last_tb, &tb_exit); tb_gen_code: gen_intermediate_code(cpu, tb, max_insns); /* target/riscv/translate.c */ gen_intermediate_code: translator_loop(&riscv_tr_ops, &ctx.base, cs, tb, max_insns); /* accel/tcg/translator.c */ translator_loop: while (true): ops->insn_start(db, cpu); ops->translate_insn(db, cpu); /* translate_insn 是 target tr ops 里的函数指针 */ static const TranslatorOps riscv_tr_ops = { /* ... */ .translate_insn = riscv_tr_translate_insn, /* ... */ }; /* target/riscv/translate.c */ riscv_tr_translate_insn: decode_opc(env, ctx, opcode16); decode_insn32(ctx, opcode32); /* decode_insn32 是 decodetree.py 生成的函数 */ decode_insn32(DisasContext *ctx, uint32_t insn): switch (insn & 0x0000007f): case 0x00000013: /* ........ ........ ........ .0010011 */ switch ((insn >> 12) & 0x7) { case 0x0: /* ........ ........ .000.... .0010011 */ /* insn32.decode:133 */ decode_insn32_extract_i(ctx, &u.f_i, insn); if (trans_addi(ctx, &u.f_i)) return true; break; /* target/riscv/insn_trans/trans_rvi.c.inc */ trans_addi(DisasContext *ctx, arg_addi *a): /* tcg_gen_xxx 用来生成 tcg IR */ return gen_arith_imm_fn(ctx, a, EXT_NONE, tcg_gen_addi_tl, gen_addi2_i128);
1.4. debug
$> cat test.c int main(int argc, char *argv[]) { return argc + 1; } $> riscv64-linux-gnu-gcc test.c -O0 -g -static $> qemu-riscv64 -d in_asm,op,out_asm ./a.out IN: main ... 0x0000000000010444: 2785 addiw a5,a5,1 ~~~~~~~~~~~~~~~~~~~~~~~ binary 中的 riscv 指令 0x0000000000010446: 2781 sext.w a5,a5 ... OP: ... ---- 0000000000010444 add_i64 tmp4,x15/a5,$0x1 ~~~~~~~~~~~~~~~~~~~~~~~~ addiw 通过 riscv frontend 翻译后的中间表示 ext32s_i64 x15/a5,tmp4 ---- 0000000000010446 mov_i64 tmp4,x15/a5 ext32s_i64 x15/a5,tmp4 ... OUT: [size=160] -- guest addr 0x0000000000010430 + tb prologue ... -- guest addr 0x0000000000010440 0x7fc4e8027207: 49 83 c4 ec addq $-0x14, %r12 0x7fc4e802720b: 4d 63 24 24 movslq (%r12), %r12 -- guest addr 0x0000000000010444 0x7fc4e802720f: 49 ff c4 incq %r12 ~~~~~~~~~~~~~ add_i64 通过 x86 backend 翻译生成的 x86 指令 0x7fc4e8027212: 4d 63 e4 movslq %r12d, %r12 0x7fc4e8027215: 4c 89 65 78 movq %r12, 0x78(%rbp) ...
Backlinks
Hello KVM (Hello KVM > qemu): qemu kvm 与 QEMU TCG 属于不同的 accelerator, 都需要实现 AccelOpsClass, kvm 对应的 ops->create_vcpu_thread 为 kvm_start_vcpu_thread, 最终会执行 kvm_cpu_exec
RISC-V Toolchain Patch (RISC-V Toolchain Patch > T-Head > qemu): t-head 主要是支持 p/zfh 和 t-head 自定义扩展. 主要修改部分是 QEMU TCG, 包括 insn32.decode, translate.c, xxx_helper.c
RISU (RISU > Overview): RISU (Random Instruction Sequence generator for Userspace testing) 可以用来对比 两个平台执行同样的的指令时结果是否一致, 例如可以用来测试 qemu 结果是否正确.
Spike (Spike > interpreter): spike 和 qemu 类似, 但它不像 QEMU TCG 那样执行二进制翻译: 它通过一个 interpreter 在 host 上执行 riscv 指令
mctoll (mctoll > 总结): mctoll 把寄存器做为 llvm 局部变量的做法, 和 QEMU TCG 类似. 不同的是, mctoll 可以 使用 llvm opt 和 llc 对 llvm ir 进行优化和 lower, 最终这些局部变量极可能会重新变 成寄存器. 另外, llvm opt 也可以去掉一些没有用到的 llvm ir (例如针对 flag 寄存器 的处理)
opcodes (binutils > opcodes > riscv_opcodes): as/objdump/gdb 都使用了 opcodes, qemu/spike/gem5 则定义了它们自己的一套类似的机制