diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md new file mode 100644 index 0000000000000000000000000000000000000000..96a83d9ac4a396e4950dd3c385191da21ea58856 --- /dev/null +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -0,0 +1,229 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces]
+> Author: jl-jiang
+> Date: 2023/07/01
+> Revisor: Bin Meng
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [【老师提案】QEMU 系统模拟模式分析](https://gitee.com/tinylab/riscv-linux/issues/I61KIY)
+> Sponsor: PLCT Lab, ISCAS + +# QEMU 系统模式下指令解码模块简析:以 RISC-V 为例 + +## 前言 + +QEMU 是一个具有跨平台的特性、可执行硬件虚拟化的开源托管虚拟机,可通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。 + +QEMU 支持以下两种方式进行模拟: + +- 用户模式(User Mode Emulation):在该模式下,QEMU 可以在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。 +- 系统模拟(System Emulation):在该模式下,QEMU 提供运行客户机所需的完整环境,包括 CPU,内存和外围设备。 + +## 概述 + +### QEMU 模拟的基本逻辑 + +QEMU 提供两种 CPU 模拟实现方式,一种基于架构无关的中间码实现,另一种基于 KVM 实现。 + +第一种方式,QEMU 使用 `Tiny Code Generator`(下文简称 `TCG`)将客户机指令动态翻译为主机指令执行。这种方式的主要思想是使用纯软件的方法将客户机 CPU 指令先解码为架构无关的中间码(即 `Intermediate Representation`),然后再把中间码翻译为主机 CPU 指令执行。其中,由客户机 CPU 指令解码为中间码的过程被称为前端,由中间码翻译为主机 CPU 指令的过程被称为后端。以 RISC-V 为例,指令的前端解码逻辑位于 `target/riscv` 中,后端翻译逻辑位于 `tcg/riscv` 中,外设及其他硬件模拟代码位于 `hw/riscv` 中。 + +第二种方式,基于 KVM 实现,直接使用主机 CPU 执行客户机指令,可以达到接近真实机器的运行性能。 + +本文只关注系统模式下 TCG 方式的分析,不讨论 KVM 的逻辑。 + +### QEMU 翻译执行过程 + +TCG 定义了一系列中间码,将已经翻译的代码以代码块的形式储存在 `Translation Block` 中,通过跳转指令将主机处理器的指令集和客户机处理器的指令集链接。当 `Hypervisor` 执行代码时,存放于 `Translation Block` 中的链接指令可以跳转到指定的代码块,目标二进制代码可不断调用已翻译代码块来运行,直到需要翻译新块为止。执行的过程中,如果遇到了需要翻译的代码块,执行会暂停并跳回到 `Hypervisor`,`Hypervisor` 使用 TCG 对需要进行二进制翻译的客户机处理器指令集进行转换和翻译并存储到 `Translation Block` 中。 + +![img](images/qemu-system-decode-analyse/translation_and_execution_loop.svg) + +通过上图,可以发现 TCG 前端解码和后端翻译都按照指令块的粒度进行,将一个客户机指令块翻译成中间码,然后把中间码翻译成主机 CPU 指令,整个过程动态执行。为了提高翻译效率,qemu 将翻译成的主机 CPU 指令块做了缓存,即上文提到的 `Translation Block`,CPU 执行的时候,先在缓存中查找对应的 `TB`,如果查找成功就直接执行,否则进入翻译流程。 + +从更加抽象的视角来看,TCG 模式下所谓客户机 CPU 的运行,实际上就是根据指令不断客户机 CPU 的状态,即改变描述客户机 CPU 的状态的数据结构中的有关变量。因为实际的代码执行过程实在主机 CPU 上完成的,因此客户机 CPU 的指令必须被翻译为主机 CPU 指令才能被执行,才能改变客户机 CPU 的数据状态。 + +qemu 为了解耦把客户机 CPU 指令先解码为中间码,中间码其实就是一组描述如何改变客户机 CPU 数据状态且架构无关的语句,所以目标 CPU 状态参数会被传入中间码描述语句。中间码实际上是改变客户机 CPU 状态的抽象的描述,部分架构的 CPU 上的状态难以抽象成一般的描述就用 `helper` 函数进行补充。将中间码翻译为主机 CPU 代码时,TCG 后端使用 `tcg_gen_xxx` 函数描述具体某条客户机 CPU 指令对客户机 CPU 数据状态的改变。 + +翻译过程中 `gen_intermediate_code` 函数负责前端解码,把客户机的指令翻译成中间码。而 `tcg_gen_code` 负责后端翻译,将中间码翻译成主机 CPU 上的指令,其中 `tcg_out_xxx` 函数执行具体的翻译工作。 + +## TCG 细节 + +### 前端解码 + +qemu 定义了 `instruction pattern` 来描述客户机 CPU 指令,一个 `instruction pattern` 是指一组相同或相近的指令,risc-v 架构的指令描述位于 `target/riscv` 目录下的 `insn16.decode`、`insn32.decode` 文件中。 + +qemu 编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetree.py` 生成对应指令描述函数的定义并存放于 `qemu/build/libqemu-riscv64-softmmu.fa.p` 目录下的 `decode-insn32.c.inc` 和 `decode-insn16.c.inc` 文件中。需要注意的是,脚本 `scripts/decodetree.py` 生成的只是 `trans_xxx` 函数的定义,其定义需要开发者实现,RISC-V 对应的实现位于 `target/riscv/insn_trans/` 目录中。此外,在 `decode-insn32.c.inc` 和 `decode-insn16.c.inc` 文件中有两个解码函数 `decode_insn32` 和 `decode_insn16` 较为关键,qemu 将客户机指令翻译成中间码的时候需要调用这两个解码函数。 + +### Decode Tree + +每种 `instruction pattern` 都有固定位和固定掩码,它们的组合构成了模式匹配的条件: + +```c +(insn & fixedmask) == fixedbits +``` + +对于每种 `instruction pattern`,`scripts/decodetree.py` 脚本定义了具体描述形式,下面进行简要分析: + +- **Fields:**CPU 在解码的时候需要把指令中的特性 `field` 中的数据取出作为传入参数(寄存器编号,立即数,操作码等),`field` 描述一个指令编码中特定的字段,根据描述可以生成取对应字段的函数。 + + | Input | Generated code | + |------------------------------------------|-----------------------------------------------------------------------| + | %disp 0:s16 | sextract(i, 0, 16) | + | %imm9 16:6 10:3 | extract(i, 16, 6) << 3 \| extract(i, 10, 3) | + | %disp12 0:s1 1:1 2:10 | sextract(i, 0, 1) << 11 \|extract(i, 1, 1) << 10 \| extract(i, 2, 10) | + | %shimm8 5:s8 13:1!function=expand_shimm8 | expand_shimm8(sextract(i, 5, 8) << 1 \|extract(i, 13, 1)) | + | %sz_imm 10:2 sz:3!function=expand_sz_imm | expand_sz_imm(extract(i, 10, 2) << 3 \|extract(a->sz, 0, 3)) | + + 在上面的例子中,一个数据,如一个立即数,可能是多个字段拼成的,所以就有相应的移位操作,或者有些立即数是由编码字段的数值取出后再经过简单运算得到,`field` 定义中所带函数就负责完成这样的计算。 + +- **Argument Sets:**定义数据结构。比如,`target/riscv/insn32.decode` 中定义的 `&b imm rs2 rs1` 在编译后的 `decode-insn32.c.inc` 中生成的数据结构如下,这个结构将作为 `trans_xxx` 函数的传入参数。 + + ```c + typedef struct { + int imm; + int rs2; + int rs1; + } arg_b; + ``` + +- **Formats:**定义指令的格式,例如下面的例子是对一个 32bit 指令编码的描述,其中 `.` 表示一个 bit 位。 + + ```c + @opr ...... ra:5 rb:5 ... 0 ....... rc:5 + @opi ...... ra:5 lit:8 1 ....... rc:5 + ``` + +- **Patterns:**用来定义具体指令。这里借助 RV32I 基础指令集中的 `lui` 指令进行详细分析: + + ```c + lui .................... ..... 0110111 @u + ``` + + 另外列出相关的 format、argument、field 的定义,以便分析: + + ```c + # Argument sets: + &u imm rd + # Formats 32: + @u .................... ..... ....... &u imm=%imm_u %rd + # Fields: + %rd 7:5 + # immediates: + %imm_u 12:s20 !function=ex_shift_12 + ``` + + 可以看到 `lui` 指令的操作码是 `0110111`,指令的格式定义是 `@u`,使用的参数定义是 `&u`,而 `&u` 就是 `trans_lui` 函数的传入参数结构体里的变量定义,其中定义的变量名字是 `imm`、`rd`,这个 `imm` 实际的格式是 `%imm_u`,它是一个由指令编码 31-12 位定义的立即数,将指令编码 31-12 位的数值左移 12 位即可得到最终结果,`rd` 实际的格式是 `%rd`,是一个在指令编码 7-5 位定义的 `rd` 寄存器的标号。 + + 可以看到 `target/riscv/insn_trans/trans_rvi.c.inc` 中对应的 `trans_lui` 函数的实现如下: + + ```c + static bool trans_lui(DisasContext *ctx, arg_lui *a) + { + gen_set_gpri(ctx, a->rd, a->imm); + return true; + } + ``` + +### trans_xxx 函数 + +`trans_xxx` 函数负责将具体的客户机指令转换为中间码指令,下面以 risc-v 架构的 `add` 指令为例进行分析。 + +如下是 `target/riscv/insn_trans/trans_rvi.c.inc` 文件中对 `add` 指令的模拟。 + +```c +static bool trans_add(DisasContext *ctx, arg_add *a) +{ + return gen_arith(ctx, a, EXT_NONE, tcg_gen_add_tl, tcg_gen_add2_tl); +} +``` + +函数 `gen_arith` 被定义在文件 `target/riscv/translate.c` 中: + +```c +static bool gen_arith(DisasContext *ctx, arg_r *a, DisasExtend ext, + void (*func)(TCGv, TCGv, TCGv), + void (*f128)(TCGv, TCGv, TCGv, TCGv, TCGv, TCGv)) +{ + TCGv dest = dest_gpr(ctx, a->rd); + TCGv src1 = get_gpr(ctx, a->rs1, ext); + TCGv src2 = get_gpr(ctx, a->rs2, ext); + + if (get_ol(ctx) < MXL_RV128) { + func(dest, src1, src2); + gen_set_gpr(ctx, a->rd, dest); + } else { + if (f128 == NULL) { + return false; + } + + TCGv src1h = get_gprh(ctx, a->rs1); + TCGv src2h = get_gprh(ctx, a->rs2); + TCGv desth = dest_gprh(ctx, a->rd); + + f128(dest, desth, src1, src1h, src2, src2h); + gen_set_gpr128(ctx, a->rd, dest, desth); + } + return true; +} +``` + +注意到函数中 `func` 指向的函数是由 `trans_add` 传入的 `tcg_gen_add_tl` 函数,而此函数又在 `inluce/tcg/tcg-op.h` 中以宏定义的形式被定义为 `tcg_gen_add_i64` 或 `tcg_gen_add_i32` 函数,下面给出 `tcg_gen_add_i64` 函数的定义: + +```c +void tcg_gen_addi_i64(TCGv_i64 ret, TCGv_i64 arg1, int64_t arg2) +{ + if (arg2 == 0) { + tcg_gen_mov_i64(ret, arg1); + } else if (TCG_TARGET_REG_BITS == 64) { + tcg_gen_add_i64(ret, arg1, tcg_constant_i64(arg2)); + } else { + tcg_gen_add2_i32(TCGV_LOW(ret), TCGV_HIGH(ret), + TCGV_LOW(arg1), TCGV_HIGH(arg1), + tcg_constant_i32(arg2), tcg_constant_i32(arg2 >> 32)); + } +} +``` + +risc-v 的 `add` 指令内容是从 CPU 的 `rs1` 和 `rs2` 寄存器中取操作数,相加后送入 `rd` 寄存器中。宏观上看,`gen_arith` 函数首先调用 `dest_gpr` 和 `get_gpr` 这两个寄存器操作封装函数获取 `rs1` 和 `rs2` 寄存器的值,并准备 `rd` 寄存器。然后通过 `func(dest, src1, src2)` 最终调用 `tcg_gen_addi_i64` 函数完成两数相加,最后使用 `gen_set_gpr` 将结果传送至 `rd` 寄存器,完成 `add` 指令解码。 + +接着,我们针对 `gen_set_gpr` 进行深入分析,以 RV32 指令为例,追踪该函数的调用链: + +![img](images/qemu-system-decode-analyse/gen_set_gpr.svg) + +分析上述调用链的参数可以发现最后生成生成了一条 `mov_i32/i64 t0, t1` 指令,该指令先被挂到了一个链表里,此后的后端翻译会把这些指令翻译成主机指令。到这里,前端解码的逻辑就基本上打通了。还有最后一个问题需要解决:`cpu_gpr[reg_num]` 这个全局变量是如何索引到客户机 CPU 寄存器的? + +解决该问题的基本思路是,只要 TCG 前端和后端约定描述客户机 CPU 状态数据结构相同,同时确保 `cpu_gpr[reg_num]` 指向的就是相关寄存器在这个数据结构中的位置。具体可以查看 `cpu_gpr[]` 数组的初始化逻辑: + +```c +void riscv_translate_init(void) +{ + int i; + // ... + for (i = 1; i < 32; i++) { + cpu_gpr[i] = tcg_global_mem_new(cpu_env, + offsetof(CPURISCVState, gpr[i]), riscv_int_regnames[i]); + // ... + } + // ... +} +``` + +客户机 CPU 在函数 `tcg_context_init(unsigned max_cpus)` 中初始化,得到是 `tcg_ctx` 里 `TCGTemp temps` 的地址。`tcg_global_mem_new` 在 `tcg_ctx` 中从 `TCGTemp temps` 上分配空间,返回空间 `tcg_ctx` 上的相对地址。这样 `cpu_gpr[reg_name]` 就可以在前端和后端之间建立连接。 + +### 后端翻译 + +后端的代码主要负责将中间码翻译成主机指令,中间码中的 `TCGv` 变量直接映射到主机 CPU 的寄存器上,实际上,翻译得到的主机 CPU 代码修改中间码中 `TCGv` 变量对应的内存。这里的基本思想是 qemu 在生成的中间码中以及 TB 执行后做了主机 CPU 寄存器到客户机 CPU 描述结构对应内存区域之间的同步。 + +下面以 `add` 指令为例,给出后端代码调用过程的详细分析: + +![img](images/qemu-system-decode-analyse/tcg_gen_code.svg) + +`tcg_gen_code` 是整个后端翻译的入口,负责寄存器和内存区域之间的同步逻辑并根据不同指令类型调用相关函数将中间码翻译为主机 CPU 指令。默认情况下,`tcg_gen_code` 会调用 `tcg_reg_alloc_op` 函数,该函数会生成用主机 CPU 指令描述的同步逻辑,存放在 `TB` 中,最后调用不同架构的开发者提供的 `tcg_out_op` 函数完成具体指令的翻译工作。针对 `add` 指令,最终会调用 `tcg_out32()` 函数,该函数负责将一个 32 位无符号整数 `v` 写入到指针 `s->code_ptr` 对应的内存位置,并根据目标平台的指令单元大小更新该指针的值。 + +## 总结 + +qemu 将客户机 CPU 指令解码为中间码,中间码是对指令如何改变客户机 CPU 数据状态的抽象描述,TCG 后端将中间码翻译为主机 CPU 指令,也就是将中间码所描述的对客户机 CPU 数据状态的更改用主机 CPU 指令的形式进行描述,执行完成后就达到了模拟客户机 CPU 运行的效果。 + +至此,从前端到后端,从解码到翻译的逻辑链条就完整了。 + +## 参考资料 + +- [Decodetree Specification][https://www.qemu.org/docs/master/devel/decodetree.html] +- [TCG Intermediate Representation][https://www.qemu.org/docs/master/devel/tcg-ops.html] diff --git a/articles/images/qemu-system-decode-analyse/gen_set_gpr.svg b/articles/images/qemu-system-decode-analyse/gen_set_gpr.svg new file mode 100644 index 0000000000000000000000000000000000000000..94e82deae2c7764eed8212597436001eeb480255 --- /dev/null +++ b/articles/images/qemu-system-decode-analyse/gen_set_gpr.svg @@ -0,0 +1,4 @@ + + + +
gen_set_gpr(ctx, a->rd, dest)
gen_set_gpr(ctx, a->rd, dest)
tcg_gen_ext32s_tl(cpu_gpr[reg_num], t)
tcg_gen_ext32s_tl(cpu_gpr[reg_num], t)
tcg_gen_mov_i32(cpu_gpr[reg_num], t)
tcg_gen_mov_i32(cpu_gpr[reg_num], t)
tcg_gen_op2_i32(INDEX_op_mov_i32, ret, arg)
tcg_gen_op2_i32(INDEX_op_mov_i32, ret, arg)
tcg_gen_op2_i32 definition
tcg_gen_op2_i32 definition
void tcg_gen_op2(TCGOpcode opc, TCGArg a1, TCGArg a2)
{
    TCGOp *op = tcg_emit_op(opc, 2);
    op->args[0] = a1;
    op->args[1] = a2;
}
void tcg_gen_op2(TCGOpcode opc, TCGArg a1, TCGArg a2)...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/articles/images/qemu-system-decode-analyse/tcg_gen_code.svg b/articles/images/qemu-system-decode-analyse/tcg_gen_code.svg new file mode 100644 index 0000000000000000000000000000000000000000..312dd1ebd7b428509ed802c0b017226e7a0db4d0 --- /dev/null +++ b/articles/images/qemu-system-decode-analyse/tcg_gen_code.svg @@ -0,0 +1,4 @@ + + + +
tcg_gen_code()
tcg_gen_code()
tcg_reg_alloc_op()
tcg_reg_alloc_op()
tcg_out_op()
tcg_out_op()
tcg_out_opc_reg()
tcg_out_opc_reg()
tcg_out32()
tcg_out32()
case INDEX_op_add_i64
case INDEX_op_add_i64
tcg_out32 definition
tcg_out32 definition
static __attribute__((unused)) inline void tcg_out32(TCGContext *s, uint32_t v)
{
    if (TCG_TARGET_INSN_UNIT_SIZE == 4) {
        *s->code_ptr++ = v;
    } else {
        tcg_insn_unit *p = s->code_ptr;
        memcpy(p, &v, sizeof(v));
        s->code_ptr = p + (4 / TCG_TARGET_INSN_UNIT_SIZE);
    }
}
static __attribute__((unused)) inline void tcg_out32(TCGContext *s, uint32_t v)...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/articles/images/qemu-system-decode-analyse/translation_and_execution_loop.svg b/articles/images/qemu-system-decode-analyse/translation_and_execution_loop.svg new file mode 100644 index 0000000000000000000000000000000000000000..e08f81adf89b56e3cdc69e9e8eeb6ab5637be72a --- /dev/null +++ b/articles/images/qemu-system-decode-analyse/translation_and_execution_loop.svg @@ -0,0 +1,4 @@ + + + +
qemu init
qemu init
main()
main()
tcg_cpus_exec()
tcg_cpus_exec()
cpu_exec()
cpu_exec()
cpu_exec_loop()
cpu_exec_loop()
TB lookup
TB lookup
cpu_loop_exec_tb()
cpu_loop_exec_tb()
cpu_tb_exec()
cpu_tb_exec()
Found
Found
tb_gen_code()
tb_gen_code()
Null
Null
setjmp_gen_code()
setjmp_gen_code()
TCG Front End
TCG Front End
gen_intermediate_code()
gen_intermediate_code()
translator_loop()
translator_loop()
riscv_tr_translate_insn()
riscv_tr_translate_insn()
decode_opc()
decode_opc()
decode_insn32()
decode_insn32()
TCG Front End
TCG Front End
tcg_gen_code()
tcg_gen_code()
tcg_out_op()
tcg_out_op()
tcg_out_xxx()
tcg_out_xxx()
update buffer
update buffer
Text is not SVG - cannot display
\ No newline at end of file