From 893d502f7c794882c347eac3412c914c78ec2503 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:27:03 +0800 Subject: [PATCH 01/34] add 20230701-qemu-system-decode-analyse.md and images --- .../20230701-qemu-system-decode-analyse.md | 224 ++++++++++++++++++ .../gen_set_gpr.svg | 4 + .../tcg_gen_code.svg | 4 + .../translation_and_execution_loop.svg | 4 + 4 files changed, 236 insertions(+) create mode 100644 articles/20230701-qemu-system-decode-analyse.md create mode 100644 articles/images/qemu-system-decode-analyse/gen_set_gpr.svg create mode 100644 articles/images/qemu-system-decode-analyse/tcg_gen_code.svg create mode 100644 articles/images/qemu-system-decode-analyse/translation_and_execution_loop.svg diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md new file mode 100644 index 0000000..8f339a8 --- /dev/null +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -0,0 +1,224 @@ +> 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是一个具有跨平台的特性、可执行硬件虚拟化的开源托管虚拟机,可通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。 + +QEMU支持以下两种方式进行模拟: + +- 用户模式(User Mode Emulation):在该模式下,QEMU 可以在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。 +- 系统模拟(System Emulation):在该模式下,QEMU 提供运行客户机所需的完整环境,包括 CPU,内存和外围设备。 + +## TCG概述 + +### 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` 对应的内存位置,并根据目标平台的指令单元大小更新该指针的值。 + +到这里,中间码所描述的对客户机CPU数据状态的更改已经全部翻译为主机CPU指令,从前端到后端,从解码到翻译的逻辑链条就完整了。 + +## 参考资料 + +1. [Decodetree Specification](https://www.qemu.org/docs/master/devel/decodetree.html) +2. [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 0000000..50deb8a --- /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 0000000..312dd1e --- /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 0000000..e08f81a --- /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 -- Gitee From 8b1e72f6dfb9eb08933de7a8a366f305588037a9 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:43:29 +0800 Subject: [PATCH 02/34] Update 20230701-qemu-system-decode-analyse.md --- .../20230701-qemu-system-decode-analyse.md | 449 +++++++++--------- 1 file changed, 225 insertions(+), 224 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index 8f339a8..fa2cb93 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,224 +1,225 @@ -> 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是一个具有跨平台的特性、可执行硬件虚拟化的开源托管虚拟机,可通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。 - -QEMU支持以下两种方式进行模拟: - -- 用户模式(User Mode Emulation):在该模式下,QEMU 可以在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。 -- 系统模拟(System Emulation):在该模式下,QEMU 提供运行客户机所需的完整环境,包括 CPU,内存和外围设备。 - -## TCG概述 - -### 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` 对应的内存位置,并根据目标平台的指令单元大小更新该指针的值。 - -到这里,中间码所描述的对客户机CPU数据状态的更改已经全部翻译为主机CPU指令,从前端到后端,从解码到翻译的逻辑链条就完整了。 - -## 参考资料 - -1. [Decodetree Specification](https://www.qemu.org/docs/master/devel/decodetree.html) -2. [TCG Intermediate Representation](https://www.qemu.org/docs/master/devel/tcg-ops.html) +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [tounix]
+> 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是一个具有跨平台的特性、可执行硬件虚拟化的开源托管虚拟机,可通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。 + +QEMU支持以下两种方式进行模拟: + +- 用户模式(User Mode Emulation):在该模式下,QEMU 可以在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。 +- 系统模拟(System Emulation):在该模式下,QEMU 提供运行客户机所需的完整环境,包括 CPU,内存和外围设备。 + +## TCG概述 + +### 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` 对应的内存位置,并根据目标平台的指令单元大小更新该指针的值。 + +到这里,中间码所描述的对客户机CPU数据状态的更改已经全部翻译为主机CPU指令,从前端到后端,从解码到翻译的逻辑链条就完整了。 + +## 参考资料 + +1. [Decodetree Specification](https://www.qemu.org/docs/master/devel/decodetree.html) +2. [TCG Intermediate Representation](https://www.qemu.org/docs/master/devel/tcg-ops.html) -- Gitee From fdf02c7a6b2eb4cdcd320cfb893229766fb047a3 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:44:49 +0800 Subject: [PATCH 03/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-spaces Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index fa2cb93..892bf04 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [tounix]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -39,7 +39,7 @@ TCG 定义了一系列中间码,将已经翻译的代码以代码块的形式 从更加抽象的视角来看,TCG 模式下所谓客户机CPU的运行,实际上就是根据指令不断客户机CPU的状态,即改变描述客户机CPU的状态的数据结构中的有关变量。因为实际的代码执行过程实在主机CPU上完成的,因此客户机CPU的指令必须被翻译为主机CPU指令才能被执行,才能改变客户机CPU的数据状态。 -qemu为了解耦把客户机CPU指令先解码为中间码,中间码其实就是一组描述如何改变客户机CPU数据状态且架构无关的语句,所以目标CPU状态参数会被传入中间码描述语句。中间码实际上是改变客户机CPU状态的抽象的描述,部分架构的CPU上的状态难以抽象成一般的描述就用 `helper` 函数进行补充。将中间码翻译为主机CPU代码时, TCG后端使用 `tcg_gen_xxx ` 函数描述具体某条客户机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`函数执行具体的翻译工作。 @@ -59,7 +59,7 @@ qemu编译的时候会解析`.decode`文件,使用脚本 `scripts/decodetree.p (insn & fixedmask) == fixedbits ``` -对于每种 `instruction pattern` ,`scripts/decodetree.py`脚本定义了具体描述形式,下面进行简要分析: +对于每种 `instruction pattern`,`scripts/decodetree.py`脚本定义了具体描述形式,下面进行简要分析: - **Fields:**CPU 在解码的时候需要把指令中的特性 `field` 中的数据取出作为传入参数 (寄存器编号,立即数,操作码等),`field` 描述一个指令编码中特定的字段,根据描述可以生成取对应字段的函数。 @@ -73,7 +73,7 @@ qemu编译的时候会解析`.decode`文件,使用脚本 `scripts/decodetree.p 在上面的例子中,一个数据,如一个立即数,可能是多个字段拼成的,所以就有相应的移位操作,或者有些立即数是由编码字段的数值取出后再经过简单运算得到,`field` 定义中所带函数就负责完成这样的计算。 -- **Argument Sets:**定义数据结构。比如,`target/riscv/insn32.decode` 中定义的`&b imm rs2 rs1` 在编译后的 `decode-insn32.c.inc` 中生成的数据结构如下,这个结构将作为 `trans_xxx` 函数的传入参数。 +- **Argument Sets:**定义数据结构。比如,`target/riscv/insn32.decode` 中定义的`&b imm rs2 rs1` 在编译后的 `decode-insn32.c.inc` 中生成的数据结构如下,这个结构将作为 `trans_xxx` 函数的传入参数。 ```c typedef struct { @@ -211,7 +211,7 @@ void riscv_translate_init(void) 后端的代码主要负责将中间码翻译成主机指令,中间码中的 `TCGv` 变量直接映射到主机 CPU 的寄存器上,实际上,翻译得到的主机CPU代码修改中间码中 `TCGv` 变量对应的内存。这里的基本思想是 qemu 在生成的中间码中以及 TB 执行后做了主机CPU寄存器到客户机CPU描述结构对应内存区域之间的同步。 -下面以 `add` 指令为例, 给出后端代码调用过程的详细分析: +下面以 `add` 指令为例,给出后端代码调用过程的详细分析: ![img](images/qemu-system-decode-analyse/tcg_gen_code.svg) -- Gitee From 324345ef7cb8b140c14573af2b46b516cf5b2471 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:45:19 +0800 Subject: [PATCH 04/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-header Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index 892bf04..4dcd142 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,10 +1,10 @@ -> 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 +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces header]
+> 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为例 -- Gitee From 010ccc6aa0d61ea0370802c7213c6eb6ce37f864 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:55:21 +0800 Subject: [PATCH 05/34] Update 20230701-qemu-system-decode-analyse.md --- articles/20230701-qemu-system-decode-analyse.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index 4dcd142..c5ed1e9 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -8,7 +8,7 @@ # QEMU系统模式下指令解码模块简析:以RISC-V为例 -## QEMU简介 +## 前言 QEMU是一个具有跨平台的特性、可执行硬件虚拟化的开源托管虚拟机,可通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。 @@ -17,7 +17,7 @@ QEMU支持以下两种方式进行模拟: - 用户模式(User Mode Emulation):在该模式下,QEMU 可以在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。 - 系统模拟(System Emulation):在该模式下,QEMU 提供运行客户机所需的完整环境,包括 CPU,内存和外围设备。 -## TCG概述 +## 概述 ### QEMU模拟的基本逻辑 @@ -45,7 +45,7 @@ qemu为了解耦把客户机CPU指令先解码为中间码,中间码其实就 ## TCG细节 -### 前端解码概述 +### 前端解码 qemu定义了 `instruction pattern` 来描述客户机CPU指令,一个 `instruction pattern` 是指一组相同或相近的指令,risc-v架构的指令描述位于`target/riscv`目录下的`insn16.decode`、`insn32.decode`文件中。 @@ -217,7 +217,11 @@ void riscv_translate_init(void) `tcg_gen_code` 是整个后端翻译的入口,负责寄存器和内存区域之间的同步逻辑并根据不同指令类型调用相关函数将中间码翻译为主机CPU指令。默认情况下,`tcg_gen_code` 会调用 `tcg_reg_alloc_op` 函数,该函数会生成用主机CPU指令描述的同步逻辑,存放在 `TB` 中,最后调用不同架构的开发者提供的 `tcg_out_op` 函数完成具体指令的翻译工作。针对 `add` 指令,最终会调用 `tcg_out32()` 函数,该函数负责将一个32位无符号整数 `v` 写入到指针 `s->code_ptr` 对应的内存位置,并根据目标平台的指令单元大小更新该指针的值。 -到这里,中间码所描述的对客户机CPU数据状态的更改已经全部翻译为主机CPU指令,从前端到后端,从解码到翻译的逻辑链条就完整了。 +## 总结 + +qemu将客户机CPU指令解码为中间码,中间码是对指令如何改变客户机CPU数据状态的抽象描述,TCG后端将中间码翻译为主机CPU指令,也就是将中间码所描述的对客户机CPU数据状态的更改用主机CPU指令的形式进行描述,执行完成后就达到了模拟客户机CPU运行的效果。 + +至此,从前端到后端,从解码到翻译的逻辑链条就完整了。 ## 参考资料 -- Gitee From 82da2daa30e9c5a65f62f2f36dde5b1c694df3c6 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:55:56 +0800 Subject: [PATCH 06/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-codeinline Signed-off-by: jl-jiang --- .../20230701-qemu-system-decode-analyse.md | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index c5ed1e9..a06cbc2 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces header]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -23,7 +23,7 @@ QEMU支持以下两种方式进行模拟: QEMU提供两种CPU模拟实现方式,一种基于架构无关的中间码实现,另一种基于KVM实现。 -第一种方式,QEMU使用`Tiny Code Generator`(下文简称`TCG`)将客户机指令动态翻译为主机指令执行。这种方式的主要思想是使用纯软件的方法将客户机CPU指令先解码为架构无关的中间码(即 `Intermediate Representation`),然后再把中间码翻译为主机CPU指令执行。其中,由客户机CPU指令解码为中间码的过程被称为前端,由中间码翻译为主机CPU指令的过程被称为后端。以RISC-V为例,指令的前端解码逻辑位于 `target/riscv` 中,后端翻译逻辑位于 `tcg/riscv` 中,外设及其他硬件模拟代码位于 `hw/riscv` 中。 +第一种方式,QEMU使用 `Tiny Code Generator`(下文简称 `TCG`)将客户机指令动态翻译为主机指令执行。这种方式的主要思想是使用纯软件的方法将客户机CPU指令先解码为架构无关的中间码(即 `Intermediate Representation`),然后再把中间码翻译为主机CPU指令执行。其中,由客户机CPU指令解码为中间码的过程被称为前端,由中间码翻译为主机CPU指令的过程被称为后端。以RISC-V为例,指令的前端解码逻辑位于 `target/riscv` 中,后端翻译逻辑位于 `tcg/riscv` 中,外设及其他硬件模拟代码位于 `hw/riscv` 中。 第二种方式,基于KVM实现,直接使用主机CPU执行客户机指令,可以达到接近真实机器的运行性能。 @@ -31,25 +31,25 @@ QEMU提供两种CPU模拟实现方式,一种基于架构无关的中间码实 ### QEMU翻译执行过程 -TCG 定义了一系列中间码,将已经翻译的代码以代码块的形式储存在 `Translation Block` 中,通过跳转指令将主机处理器的指令集和客户机处理器的指令集链接。当 `Hypervisor` 执行代码时,存放于`Translation Block`中的链接指令可以跳转到指定的代码块,目标二进制代码可不断调用已翻译代码块来运行,直到需要翻译新块为止。执行的过程中,如果遇到了需要翻译的代码块,执行会暂停并跳回到 `Hypervisor`,`Hypervisor` 使用 TCG 对需要进行二进制翻译的客户机处理器指令集进行转换和翻译并存储到`Translation Block`中。 +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指令,整个过程动态执行。为了提高翻译效率,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数据状态的改变。 +qemu为了解耦把客户机CPU指令先解码为中间码,中间码其实就是一组描述如何改变客户机CPU数据状态且架构无关的语句,所以目标CPU状态参数会被传入中间码描述语句。中间码实际上是改变客户机CPU状态的抽象的描述,部分架构的CPU上的状态难以抽象成一般的描述就用 `helper` 函数进行补充。将中间码翻译为主机CPU代码时,TCG后端使用 `tcg_gen_xxx` 函数描述具体某条客户机CPU指令对客户机CPU数据状态的改变。 -翻译过程中`gen_intermediate_code `函数负责前端解码,把客户机的指令翻译成中间码。而 `tcg_gen_code`负责后端翻译,将中间码翻译成主机CPU上的指令,其中`tcg_out_xxx`函数执行具体的翻译工作。 +翻译过程中 `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定义了 `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将客户机指令翻译成中间码的时候需要调用这两个解码函数。 +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 @@ -59,7 +59,7 @@ qemu编译的时候会解析`.decode`文件,使用脚本 `scripts/decodetree.p (insn & fixedmask) == fixedbits ``` -对于每种 `instruction pattern`,`scripts/decodetree.py`脚本定义了具体描述形式,下面进行简要分析: +对于每种 `instruction pattern`,`scripts/decodetree.py` 脚本定义了具体描述形式,下面进行简要分析: - **Fields:**CPU 在解码的时候需要把指令中的特性 `field` 中的数据取出作为传入参数 (寄存器编号,立即数,操作码等),`field` 描述一个指令编码中特定的字段,根据描述可以生成取对应字段的函数。 @@ -73,7 +73,7 @@ qemu编译的时候会解析`.decode`文件,使用脚本 `scripts/decodetree.p 在上面的例子中,一个数据,如一个立即数,可能是多个字段拼成的,所以就有相应的移位操作,或者有些立即数是由编码字段的数值取出后再经过简单运算得到,`field` 定义中所带函数就负责完成这样的计算。 -- **Argument Sets:**定义数据结构。比如,`target/riscv/insn32.decode` 中定义的`&b imm rs2 rs1` 在编译后的 `decode-insn32.c.inc` 中生成的数据结构如下,这个结构将作为 `trans_xxx` 函数的传入参数。 +- **Argument Sets:**定义数据结构。比如,`target/riscv/insn32.decode` 中定义的 `&b imm rs2 rs1` 在编译后的 `decode-insn32.c.inc` 中生成的数据结构如下,这个结构将作为 `trans_xxx` 函数的传入参数。 ```c typedef struct { @@ -83,7 +83,7 @@ qemu编译的时候会解析`.decode`文件,使用脚本 `scripts/decodetree.p } arg_b; ``` -- **Formats:**定义指令的格式,例如下面的例子是对一个 32bit 指令编码的描述,其中`.` 表示一个 bit 位。 +- **Formats:**定义指令的格式,例如下面的例子是对一个 32bit 指令编码的描述,其中 `.` 表示一个 bit 位。 ```c @opr ...... ra:5 rb:5 ... 0 ....... rc:5 @@ -134,7 +134,7 @@ static bool trans_add(DisasContext *ctx, arg_add *a) } ``` -函数`gen_arith`被定义在文件 `target/riscv/translate.c` 中: +函数 `gen_arith` 被定义在文件 `target/riscv/translate.c` 中: ```c static bool gen_arith(DisasContext *ctx, arg_r *a, DisasExtend ext, @@ -164,7 +164,7 @@ static bool gen_arith(DisasContext *ctx, arg_r *a, DisasExtend ext, } ``` -注意到函数中 `func` 指向的函数是由 `trans_add` 传入的 `tcg_gen_add_tl` 函数,而此函数又在 `inluce/tcg/tcg-op.h` 中以宏定义的形式被定义为`tcg_gen_add_i64`或`tcg_gen_add_i32`函数,下面给出`tcg_gen_add_i64`函数的定义: +注意到函数中 `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) @@ -181,15 +181,15 @@ void tcg_gen_addi_i64(TCGv_i64 ret, TCGv_i64 arg1, int64_t arg2) } ``` -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` 指令解码。 +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指令为例,追踪该函数的调用链: +接着,我们针对 `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[] ` 数组的初始化逻辑: +解决该问题的基本思路是,只要TCG前端和后端约定描述客户机CPU状态数据结构相同,同时确保 `cpu_gpr[reg_num]` 指向的就是相关寄存器在这个数据结构中的位置。具体可以查看 `cpu_gpr[]` 数组的初始化逻辑: ```c void riscv_translate_init(void) -- Gitee From 4e9e808d38b9b4b7f679ad0e247a67ad1491d8fe Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:56:20 +0800 Subject: [PATCH 07/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-tables Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index a06cbc2..a12b134 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline tables]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -63,13 +63,13 @@ qemu编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetree - **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) | + | 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)) | + | %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` 定义中所带函数就负责完成这样的计算。 -- Gitee From a5ef7420d5174bca67a29678d05ade8c8ef9e805 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:56:51 +0800 Subject: [PATCH 08/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-urls Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index a12b134..87e5cf2 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline tables]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline tables urls]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -225,5 +225,8 @@ qemu将客户机CPU指令解码为中间码,中间码是对指令如何改变 ## 参考资料 -1. [Decodetree Specification](https://www.qemu.org/docs/master/devel/decodetree.html) -2. [TCG Intermediate Representation](https://www.qemu.org/docs/master/devel/tcg-ops.html) +1. [Decodetree Specification][001] +2. [TCG Intermediate Representation][002] + +[001]: https://www.qemu.org/docs/master/devel/decodetree.html +[002]: https://www.qemu.org/docs/master/devel/tcg-ops.html -- Gitee From a79e4c2cd0a0cf6cf0c4f3e4b616e35db458ad45 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:57:07 +0800 Subject: [PATCH 09/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-refs Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index 87e5cf2..459925b 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline tables urls]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline tables urls refs]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -225,8 +225,8 @@ qemu将客户机CPU指令解码为中间码,中间码是对指令如何改变 ## 参考资料 -1. [Decodetree Specification][001] -2. [TCG Intermediate Representation][002] +- . [Decodetree Specification][001] +- . [TCG Intermediate Representation][002] [001]: https://www.qemu.org/docs/master/devel/decodetree.html [002]: https://www.qemu.org/docs/master/devel/tcg-ops.html -- Gitee From a98a3a7972a18f050b8ceb4a406da31aa53e9937 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 13:57:49 +0800 Subject: [PATCH 10/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-pangu Signed-off-by: jl-jiang --- .../20230701-qemu-system-decode-analyse.md | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index 459925b..f4a7bc5 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline tables urls refs]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline tables urls refs pangu]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -6,50 +6,50 @@ > Proposal: [【老师提案】QEMU 系统模拟模式分析](https://gitee.com/tinylab/riscv-linux/issues/I61KIY)
> Sponsor: PLCT Lab, ISCAS -# QEMU系统模式下指令解码模块简析:以RISC-V为例 +# QEMU 系统模式下指令解码模块简析:以 RISC-V 为例 ## 前言 -QEMU是一个具有跨平台的特性、可执行硬件虚拟化的开源托管虚拟机,可通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。 +QEMU 是一个具有跨平台的特性、可执行硬件虚拟化的开源托管虚拟机,可通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。 -QEMU支持以下两种方式进行模拟: +QEMU 支持以下两种方式进行模拟: - 用户模式(User Mode Emulation):在该模式下,QEMU 可以在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。 - 系统模拟(System Emulation):在该模式下,QEMU 提供运行客户机所需的完整环境,包括 CPU,内存和外围设备。 ## 概述 -### QEMU模拟的基本逻辑 +### QEMU 模拟的基本逻辑 -QEMU提供两种CPU模拟实现方式,一种基于架构无关的中间码实现,另一种基于KVM实现。 +QEMU 提供两种 CPU 模拟实现方式,一种基于架构无关的中间码实现,另一种基于 KVM 实现。 -第一种方式,QEMU使用 `Tiny Code Generator`(下文简称 `TCG`)将客户机指令动态翻译为主机指令执行。这种方式的主要思想是使用纯软件的方法将客户机CPU指令先解码为架构无关的中间码(即 `Intermediate Representation`),然后再把中间码翻译为主机CPU指令执行。其中,由客户机CPU指令解码为中间码的过程被称为前端,由中间码翻译为主机CPU指令的过程被称为后端。以RISC-V为例,指令的前端解码逻辑位于 `target/riscv` 中,后端翻译逻辑位于 `tcg/riscv` 中,外设及其他硬件模拟代码位于 `hw/riscv` 中。 +第一种方式,QEMU 使用 `Tiny Code Generator`(下文简称 `TCG`)将客户机指令动态翻译为主机指令执行。这种方式的主要思想是使用纯软件的方法将客户机 CPU 指令先解码为架构无关的中间码(即 `Intermediate Representation`),然后再把中间码翻译为主机 CPU 指令执行。其中,由客户机 CPU 指令解码为中间码的过程被称为前端,由中间码翻译为主机 CPU 指令的过程被称为后端。以 RISC-V 为例,指令的前端解码逻辑位于 `target/riscv` 中,后端翻译逻辑位于 `tcg/riscv` 中,外设及其他硬件模拟代码位于 `hw/riscv` 中。 -第二种方式,基于KVM实现,直接使用主机CPU执行客户机指令,可以达到接近真实机器的运行性能。 +第二种方式,基于 KVM 实现,直接使用主机 CPU 执行客户机指令,可以达到接近真实机器的运行性能。 -本文只关注系统模式下TCG方式的分析,不讨论KVM的逻辑。 +本文只关注系统模式下 TCG 方式的分析,不讨论 KVM 的逻辑。 -### QEMU翻译执行过程 +### 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 指令,整个过程动态执行。为了提高翻译效率,qemu 将翻译成的主机 CPU 指令块做了缓存,即上文提到的 `Translation Block`,CPU 执行的时候,先在缓存中查找对应的 `TB`,如果查找成功就直接执行,否则进入翻译流程。 -从更加抽象的视角来看,TCG 模式下所谓客户机CPU的运行,实际上就是根据指令不断客户机CPU的状态,即改变描述客户机CPU的状态的数据结构中的有关变量。因为实际的代码执行过程实在主机CPU上完成的,因此客户机CPU的指令必须被翻译为主机CPU指令才能被执行,才能改变客户机CPU的数据状态。 +从更加抽象的视角来看,TCG 模式下所谓客户机 CPU 的运行,实际上就是根据指令不断客户机 CPU 的状态,即改变描述客户机 CPU 的状态的数据结构中的有关变量。因为实际的代码执行过程实在主机 CPU 上完成的,因此客户机 CPU 的指令必须被翻译为主机 CPU 指令才能被执行,才能改变客户机 CPU 的数据状态。 -qemu为了解耦把客户机CPU指令先解码为中间码,中间码其实就是一组描述如何改变客户机CPU数据状态且架构无关的语句,所以目标CPU状态参数会被传入中间码描述语句。中间码实际上是改变客户机CPU状态的抽象的描述,部分架构的CPU上的状态难以抽象成一般的描述就用 `helper` 函数进行补充。将中间码翻译为主机CPU代码时,TCG后端使用 `tcg_gen_xxx` 函数描述具体某条客户机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` 函数执行具体的翻译工作。 +翻译过程中 `gen_intermediate_code` 函数负责前端解码,把客户机的指令翻译成中间码。而 `tcg_gen_code` 负责后端翻译,将中间码翻译成主机 CPU 上的指令,其中 `tcg_out_xxx` 函数执行具体的翻译工作。 -## TCG细节 +## TCG 细节 ### 前端解码 -qemu定义了 `instruction pattern` 来描述客户机CPU指令,一个 `instruction pattern` 是指一组相同或相近的指令,risc-v架构的指令描述位于 `target/riscv` 目录下的 `insn16.decode`、`insn32.decode` 文件中。 +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将客户机指令翻译成中间码的时候需要调用这两个解码函数。 +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 @@ -61,7 +61,7 @@ qemu编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetree 对于每种 `instruction pattern`,`scripts/decodetree.py` 脚本定义了具体描述形式,下面进行简要分析: -- **Fields:**CPU 在解码的时候需要把指令中的特性 `field` 中的数据取出作为传入参数 (寄存器编号,立即数,操作码等),`field` 描述一个指令编码中特定的字段,根据描述可以生成取对应字段的函数。 +- **Fields:**CPU 在解码的时候需要把指令中的特性 `field` 中的数据取出作为传入参数(寄存器编号,立即数,操作码等),`field` 描述一个指令编码中特定的字段,根据描述可以生成取对应字段的函数。 | Input | Generated code | |------------------------------------------|-----------------------------------------------------------------------| @@ -90,7 +90,7 @@ qemu编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetree @opi ...... ra:5 lit:8 1 ....... rc:5 ``` -- **Patterns:**用来定义具体指令。这里借助RV32I基础指令集中的 `lui` 指令进行详细分析: +- **Patterns:**用来定义具体指令。这里借助 RV32I 基础指令集中的 `lui` 指令进行详细分析: ```c lui .................... ..... 0110111 @u @@ -109,9 +109,9 @@ qemu编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetree %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` 寄存器的标号。 + 可以看到 `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` 函数的实现如下: + 可以看到 `target/riscv/insn_trans/trans_rvi.c.inc` 中对应的 `trans_lui` 函数的实现如下: ```c static bool trans_lui(DisasContext *ctx, arg_lui *a) @@ -121,7 +121,7 @@ qemu编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetree } ``` -### trans_xxx函数 +### trans_xxx 函数 `trans_xxx` 函数负责将具体的客户机指令转换为中间码指令,下面以 risc-v 架构的 `add` 指令为例进行分析。 @@ -181,15 +181,15 @@ void tcg_gen_addi_i64(TCGv_i64 ret, TCGv_i64 arg1, int64_t arg2) } ``` -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` 指令解码。 +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指令为例,追踪该函数的调用链: +接着,我们针对 `gen_set_gpr` 进行深入分析,以 RV32 指令为例,追踪该函数的调用链: ![img](images/qemu-system-decode-analyse/gen_set_gpr.svg) -分析上述调用链的参数可以发现最后生成生成了一条 `mov_i32/i64 t0, t1` 指令,该指令先被挂到了一个链表里,此后的后端翻译会把这些指令翻译成主机指令。到这里,前端解码的逻辑就基本上打通了。还有最后一个问题需要解决:`cpu_gpr[reg_num]` 这个全局变量是如何索引到客户机CPU寄存器的? +分析上述调用链的参数可以发现最后生成生成了一条 `mov_i32/i64 t0, t1` 指令,该指令先被挂到了一个链表里,此后的后端翻译会把这些指令翻译成主机指令。到这里,前端解码的逻辑就基本上打通了。还有最后一个问题需要解决:`cpu_gpr[reg_num]` 这个全局变量是如何索引到客户机 CPU 寄存器的? -解决该问题的基本思路是,只要TCG前端和后端约定描述客户机CPU状态数据结构相同,同时确保 `cpu_gpr[reg_num]` 指向的就是相关寄存器在这个数据结构中的位置。具体可以查看 `cpu_gpr[]` 数组的初始化逻辑: +解决该问题的基本思路是,只要 TCG 前端和后端约定描述客户机 CPU 状态数据结构相同,同时确保 `cpu_gpr[reg_num]` 指向的就是相关寄存器在这个数据结构中的位置。具体可以查看 `cpu_gpr[]` 数组的初始化逻辑: ```c void riscv_translate_init(void) @@ -205,28 +205,28 @@ void riscv_translate_init(void) } ``` -客户机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]` 就可以在前端和后端之间建立连接。 +客户机 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描述结构对应内存区域之间的同步。 +后端的代码主要负责将中间码翻译成主机指令,中间码中的 `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` 对应的内存位置,并根据目标平台的指令单元大小更新该指针的值。 +`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运行的效果。 +qemu 将客户机 CPU 指令解码为中间码,中间码是对指令如何改变客户机 CPU 数据状态的抽象描述,TCG 后端将中间码翻译为主机 CPU 指令,也就是将中间码所描述的对客户机 CPU 数据状态的更改用主机 CPU 指令的形式进行描述,执行完成后就达到了模拟客户机 CPU 运行的效果。 至此,从前端到后端,从解码到翻译的逻辑链条就完整了。 ## 参考资料 -- . [Decodetree Specification][001] -- . [TCG Intermediate Representation][002] +-. [Decodetree Specification][001] +-. [TCG Intermediate Representation][002] [001]: https://www.qemu.org/docs/master/devel/decodetree.html [002]: https://www.qemu.org/docs/master/devel/tcg-ops.html -- Gitee From 321574e8652c47d4acae736364150940f6361869 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 14:09:12 +0800 Subject: [PATCH 11/34] Update 20230701-qemu-system-decode-analyse.md and gen_set_gpr.svg --- articles/20230701-qemu-system-decode-analyse.md | 7 +++---- articles/images/qemu-system-decode-analyse/gen_set_gpr.svg | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index f4a7bc5..bb43cd9 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -225,8 +225,7 @@ qemu 将客户机 CPU 指令解码为中间码,中间码是对指令如何改 ## 参考资料 --. [Decodetree Specification][001] --. [TCG Intermediate Representation][002] +- [Decodetree Specification][https://www.qemu.org/docs/master/devel/decodetree.html ] +- [TCG Intermediate Representation][https://www.qemu.org/docs/master/devel/tcg-ops.html] -[001]: https://www.qemu.org/docs/master/devel/decodetree.html -[002]: 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 index 50deb8a..94e82de 100644 --- a/articles/images/qemu-system-decode-analyse/gen_set_gpr.svg +++ b/articles/images/qemu-system-decode-analyse/gen_set_gpr.svg @@ -1,4 +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 +
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 -- Gitee From 4d9b02af26e44bdffc5aa3a89f385e0ec2a4c78d Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Sat, 1 Jul 2023 14:09:42 +0800 Subject: [PATCH 12/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-spaces Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index bb43cd9..96a83d9 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline tables urls refs pangu]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -225,7 +225,5 @@ qemu 将客户机 CPU 指令解码为中间码,中间码是对指令如何改 ## 参考资料 -- [Decodetree Specification][https://www.qemu.org/docs/master/devel/decodetree.html ] +- [Decodetree Specification][https://www.qemu.org/docs/master/devel/decodetree.html] - [TCG Intermediate Representation][https://www.qemu.org/docs/master/devel/tcg-ops.html] - - -- Gitee From 2bd51e1ea70d9e4a793f465cf2599ef4eddc9a5d Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Tue, 4 Jul 2023 11:29:43 +0800 Subject: [PATCH 13/34] revise article and images --- .../20230701-qemu-system-decode-analyse.md | 102 +++++++++++------- .../gen_set_gpr.svg | 2 +- .../tcg_gen_code.svg | 2 +- .../translation_and_execution_loop.svg | 2 +- 4 files changed, 68 insertions(+), 40 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index 96a83d9..82cbbaf 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -10,46 +10,46 @@ ## 前言 -QEMU 是一个具有跨平台的特性、可执行硬件虚拟化的开源托管虚拟机,可通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。 +QEMU是一个通用的、开源的模拟器,通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。QEMU 亦可藉由硬件虚拟化变身为虚拟机。 QEMU 支持以下两种方式进行模拟: -- 用户模式(User Mode Emulation):在该模式下,QEMU 可以在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。 -- 系统模拟(System Emulation):在该模式下,QEMU 提供运行客户机所需的完整环境,包括 CPU,内存和外围设备。 +- 用户模式(User Mode Emulation):在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。在该模式下,QEMU作为进程级虚拟机,只模拟系统调用前的用户态代码,系统调用进入内核后,由宿主机操作系统原生执行,QEMU 提供不同架构系统调用映射的转换。 +- 系统模拟(System Emulation):在该模式下,QEMU 作为系统级虚拟机提供运行客户机所需的完整环境,包括 CPU,内存和外围设备等。 ## 概述 ### QEMU 模拟的基本逻辑 -QEMU 提供两种 CPU 模拟实现方式,一种基于架构无关的中间码实现,另一种基于 KVM 实现。 +QEMU 提供两种 CPU 模拟实现方式,一种基于架构无关的中间码实现,另一种基于硬件虚拟化技术实现。 -第一种方式,QEMU 使用 `Tiny Code Generator`(下文简称 `TCG`)将客户机指令动态翻译为主机指令执行。这种方式的主要思想是使用纯软件的方法将客户机 CPU 指令先解码为架构无关的中间码(即 `Intermediate Representation`),然后再把中间码翻译为主机 CPU 指令执行。其中,由客户机 CPU 指令解码为中间码的过程被称为前端,由中间码翻译为主机 CPU 指令的过程被称为后端。以 RISC-V 为例,指令的前端解码逻辑位于 `target/riscv` 中,后端翻译逻辑位于 `tcg/riscv` 中,外设及其他硬件模拟代码位于 `hw/riscv` 中。 +第一种方式,QEMU 使用 `Tiny Code Generator`(下文简称 `TCG`)将客户机指令动态翻译为宿主机指令执行。这种方式的主要思想是使用纯软件的方法将客户机 CPU 指令先解码为架构无关的中间码(即 `Intermediate Representation`),然后再把中间码翻译为宿主机 CPU 指令执行。其中,由客户机 CPU 指令解码为中间码的过程被称为前端,由中间码翻译为宿主机 CPU 指令的过程被称为后端。以 RISC-V 为例,指令的前端解码逻辑位于 `target/riscv` 中,后端翻译逻辑位于 `tcg/` 中,外设及其他硬件模拟代码位于 `hw/riscv` 中。 -第二种方式,基于 KVM 实现,直接使用主机 CPU 执行客户机指令,可以达到接近真实机器的运行性能。 +第二种方式,基于硬件虚拟化技术实现,直接使用宿主机 CPU 执行客户机指令,可以达到接近真实机器的运行性能。 -本文只关注系统模式下 TCG 方式的分析,不讨论 KVM 的逻辑。 +本文只关注系统模式下 TCG 方式的分析,不讨论基础硬件虚拟化技术的运行逻辑。 ### QEMU 翻译执行过程 -TCG 定义了一系列中间码,将已经翻译的代码以代码块的形式储存在 `Translation Block` 中,通过跳转指令将主机处理器的指令集和客户机处理器的指令集链接。当 `Hypervisor` 执行代码时,存放于 `Translation Block` 中的链接指令可以跳转到指定的代码块,目标二进制代码可不断调用已翻译代码块来运行,直到需要翻译新块为止。执行的过程中,如果遇到了需要翻译的代码块,执行会暂停并跳回到 `Hypervisor`,`Hypervisor` 使用 TCG 对需要进行二进制翻译的客户机处理器指令集进行转换和翻译并存储到 `Translation Block` 中。 +TCG 定义了一系列中间码,将已经翻译的代码以代码块的形式储存在 `Translation Block` 中,通过跳转指令将宿主机 CPU 的指令集和客户机 CPU 的指令集链接。下图以 RISC-V 为例,给出了 QEMU v8.0.0 系统模拟的翻译执行流程。 ![img](images/qemu-system-decode-analyse/translation_and_execution_loop.svg) -通过上图,可以发现 TCG 前端解码和后端翻译都按照指令块的粒度进行,将一个客户机指令块翻译成中间码,然后把中间码翻译成主机 CPU 指令,整个过程动态执行。为了提高翻译效率,qemu 将翻译成的主机 CPU 指令块做了缓存,即上文提到的 `Translation Block`,CPU 执行的时候,先在缓存中查找对应的 `TB`,如果查找成功就直接执行,否则进入翻译流程。 +我们可以发现 TCG 前端解码和后端翻译都按照指令块的粒度进行,将一个客户机指令块翻译成中间码,然后把中间码翻译成宿主机 CPU 指令,整个过程动态执行。为了提高翻译效率,QEMU 将翻译成的宿主机 CPU 指令块做了缓存,即上文提到的 `Translation Block`,CPU 执行的时候,先在缓存中查找对应的 `TB`,如果查找成功就直接执行,否则进入翻译流程。 -从更加抽象的视角来看,TCG 模式下所谓客户机 CPU 的运行,实际上就是根据指令不断客户机 CPU 的状态,即改变描述客户机 CPU 的状态的数据结构中的有关变量。因为实际的代码执行过程实在主机 CPU 上完成的,因此客户机 CPU 的指令必须被翻译为主机 CPU 指令才能被执行,才能改变客户机 CPU 的数据状态。 +从更加抽象的视角来看,TCG 模式下所谓客户机 CPU 的运行,实际上就是根据指令不断客户机 CPU 的状态,即改变描述客户机 CPU 的状态的数据结构中的有关变量。因为实际的代码执行过程实在宿主机 CPU 上完成的,因此客户机 CPU 的指令必须被翻译为宿主机 CPU 指令才能被执行,才能改变客户机 CPU 的数据状态。 -qemu 为了解耦把客户机 CPU 指令先解码为中间码,中间码其实就是一组描述如何改变客户机 CPU 数据状态且架构无关的语句,所以目标 CPU 状态参数会被传入中间码描述语句。中间码实际上是改变客户机 CPU 状态的抽象的描述,部分架构的 CPU 上的状态难以抽象成一般的描述就用 `helper` 函数进行补充。将中间码翻译为主机 CPU 代码时,TCG 后端使用 `tcg_gen_xxx` 函数描述具体某条客户机 CPU 指令对客户机 CPU 数据状态的改变。 +QEMU 为了解耦把客户机 CPU 指令先解码为中间码,中间码其实就是一组描述如何改变客户机 CPU 数据状态且架构无关的语句,所以目标 CPU 状态参数会被传入中间码描述语句。中间码实际上是改变客户机 CPU 状态的抽象的描述,对于部分难以抽象成一般描述的指令就用 `helper` 函数进行补充。例如 RV32FD 指令集中的 `fadd.d rd, rs1, rs2` 指令,表示双精度浮点加,将 `rs1` 和 `rs2` 寄存器中的双进度浮点数相加并将舍入后的结果送到 `rd` 寄存器。对于该指令的行为,TCG 中间码并没有合适的描述,因此 QEMU 使用 `helper_fmadd_d` 函数根据情况直接调用模拟 FPU 解决问题。最后,将中间码翻译为宿主机 CPU 代码时,TCG 后端使用 `tcg_gen_xxx` 函数描述具体某条客户机 CPU 指令对客户机 CPU 数据状态的改变。 -翻译过程中 `gen_intermediate_code` 函数负责前端解码,把客户机的指令翻译成中间码。而 `tcg_gen_code` 负责后端翻译,将中间码翻译成主机 CPU 上的指令,其中 `tcg_out_xxx` 函数执行具体的翻译工作。 +翻译过程中 `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 定义了 `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 将客户机指令翻译成中间码的时候需要调用这两个解码函数。 +QEMU 编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetree.py` 生成对应指令描述函数的声明并存放于 `/libqemu-riscv64-softmmu.fa.p` 目录下的 `decode-insn32.c.inc` 和 `decode-insn16.c.inc` 文件中。在这两个文件中还定义两个较为关键的解码函数 `decode_insn32` 和 `decode_insn16` ,QEMU 将客户机指令翻译成中间码的时候需要调用这两个解码函数。需要注意的是,脚本 `scripts/decodetree.py` 生成的只是 `trans_xxx` 函数的声明,其定义需要开发者实现,RISC-V 对应的实现位于 `target/riscv/insn_trans/` 目录中。 ### Decode Tree @@ -61,19 +61,19 @@ qemu 编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetre 对于每种 `instruction pattern`,`scripts/decodetree.py` 脚本定义了具体描述形式,下面进行简要分析: -- **Fields:**CPU 在解码的时候需要把指令中的特性 `field` 中的数据取出作为传入参数(寄存器编号,立即数,操作码等),`field` 描述一个指令编码中特定的字段,根据描述可以生成取对应字段的函数。 +- **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)) | + | 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` 定义中所带函数就负责完成这样的计算。 + 上表给出了一些例子,如第一行的 `%disp 0:s16` 表示指令编码第0位起的16位构成了一个带符号数,因此生成代码 `sextract(i, 0, 16)`,意即从指令 `i` 的编码第0位开始取16位解释为带符号数返回。还有第三行的 `%disp12 0:s1 1:1 2:10` 表示该立即数由三个部分拼接而成,因此生成的代码中就包含了相应的移位、拼接运算。由 `field` 定义所生成的函数就负责完成这种与从指令编码中取数有关的计算。 -- **Argument Sets:**定义数据结构。比如,`target/riscv/insn32.decode` 中定义的 `&b imm rs2 rs1` 在编译后的 `decode-insn32.c.inc` 中生成的数据结构如下,这个结构将作为 `trans_xxx` 函数的传入参数。 +- **Argument Sets:** 定义数据结构。比如,`target/riscv/insn32.decode` 中定义的 `&b imm rs2 rs1` 在编译后的 `decode-insn32.c.inc` 中生成的数据结构如下,这个结构将作为 `trans_xxx` 函数的传入参数。 ```c typedef struct { @@ -83,14 +83,14 @@ qemu 编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetre } arg_b; ``` -- **Formats:**定义指令的格式,例如下面的例子是对一个 32bit 指令编码的描述,其中 `.` 表示一个 bit 位。 +- **Formats:** 定义指令的格式,例如下面的例子是对一个 32-bit 指令编码的描述,其中 `.` 表示一个 bit 位。 ```c @opr ...... ra:5 rb:5 ... 0 ....... rc:5 @opi ...... ra:5 lit:8 1 ....... rc:5 ``` -- **Patterns:**用来定义具体指令。这里借助 RV32I 基础指令集中的 `lui` 指令进行详细分析: +- **Patterns:** 用来定义具体指令。这里借助 RV32I 基础指令集中的 `lui` 指令进行详细分析: ```c lui .................... ..... 0110111 @u @@ -123,7 +123,7 @@ qemu 编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetre ### trans_xxx 函数 -`trans_xxx` 函数负责将具体的客户机指令转换为中间码指令,下面以 risc-v 架构的 `add` 指令为例进行分析。 +`trans_xxx` 函数负责将具体的客户机指令转换为中间码指令,若转换成功则返回 `true`,否则返回 `false`。下面以 RISC-V 架构的 `add` 指令为例进行分析。 如下是 `target/riscv/insn_trans/trans_rvi.c.inc` 文件中对 `add` 指令的模拟。 @@ -181,15 +181,15 @@ void tcg_gen_addi_i64(TCGv_i64 ret, TCGv_i64 arg1, int64_t arg2) } ``` -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` 指令解码。 +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 寄存器的? +分析上述调用链的参数可以发现最后生成生成了一条 `mov_i32 t0, t1` 指令,意思是将 `t1` 寄存器中的数移动到 `t0` 寄存器中。该指令先被挂到了一个链表里,此后的后端翻译会把这些指令翻译成宿主机指令。到这里,前端解码的逻辑就基本上打通了。还有最后一个问题需要解决:`cpu_gpr[reg_num]` 这个全局变量是如何索引到客户机 CPU 寄存器的? -解决该问题的基本思路是,只要 TCG 前端和后端约定描述客户机 CPU 状态数据结构相同,同时确保 `cpu_gpr[reg_num]` 指向的就是相关寄存器在这个数据结构中的位置。具体可以查看 `cpu_gpr[]` 数组的初始化逻辑: +解决该问题的基本思路是,只要 TCG 前端和后端约定描述客户机 CPU 状态数据结构相同,确保 `cpu_gpr[reg_num]` 指向的就是相关寄存器在这个数据结构中的位置即可,这一点在 `cpu_gpr[]` 数组的初始化过程中具体体现: ```c void riscv_translate_init(void) @@ -205,25 +205,53 @@ void riscv_translate_init(void) } ``` -客户机 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]` 就可以在前端和后端之间建立连接。 +`cpu_gpr[]` 数组在初始化时调用 `tcg_global_mem_new` 函数在TCG上下文 `tcg_ctx` 中分配空间并返回其相对地址,而后段翻译时访问 `cpu_gpr[]` 数组就是在访问TCG上下文中描述寄存器的变量,这样 `cpu_gpr[reg_name]` 就在前端和后端之间建立了连接。 ### 后端翻译 -后端的代码主要负责将中间码翻译成主机指令,中间码中的 `TCGv` 变量直接映射到主机 CPU 的寄存器上,实际上,翻译得到的主机 CPU 代码修改中间码中 `TCGv` 变量对应的内存。这里的基本思想是 qemu 在生成的中间码中以及 TB 执行后做了主机 CPU 寄存器到客户机 CPU 描述结构对应内存区域之间的同步。 +后端的代码主要负责将中间码翻译成宿主机指令,本质上就是根据中间码的描述使用宿主机指令来改变内存中表示的客户机CPU的数据结构以及客户机内存的状态。考虑以下两条 RISC-V 汇编指令: -下面以 `add` 指令为例,给出后端代码调用过程的详细分析: +```assembly +addi sp,sp,-32 +sd s0,24(sp) +``` + +经过前端解码,可以得到以下中间码: + +```assembly +add_i64 x2/sp,x2/sp,$0xffffffffffffffe0 +add_i64 tmp4,x2/sp,$0x18 +qemu_st_i64 x8/s0,tmp4,leq,0 +``` + +注意到 `sd` 指令被翻译成了两条中间码,第一条 `add_i64` 是用来计算 `sd ` 指令的目标地址,计算结果保存在 `tmp4` 这个虚拟寄存器里,第二条中间码把 `s0` 的值储存到虚拟寄存器 `tmp4 ` 描述的内存上。在中间码中,`x2/sp` 和 `x8/s0` 仍然是客户机CPU上寄存器的名字,但是逻辑上已经全部映射为QEMU虚拟寄存器。TCG 前端将 RISC-V 汇编指令解码为中间码和虚拟寄存器的表示,后端翻译则基于中间码和虚拟寄存器进行。再次审视上述两条指令,`addi` 的中间码表示要把客户机的 `sp` 寄存器加上-32,`sd`的中间码表示要将客户机的`s0`寄存器中的值送到`sp`寄存器加24后得到的地址处。对于这些中间码,在ARM架构的宿主机上可能被翻译为以下指令: + +```assembly +ldr x20, [x19, #0x10] +sub x20, x20, #0x20 +str x20, [x19, #0x10] +add x21, x20, #0x18 +ldr x22, [x19, #0x40] +str x22, [x21, xzr] +``` + +这段指令主要进行了以下操作:把客户机CPU的 `sp` 寄存器装载到宿主机CPU的 `x20` 寄存器,使用 `sub` 指令完成客户机CPU `sp` 寄存器值的计算并进行更新;使用 `add` 指令计算客户机CPU的 `sd` 指令的目标地址并保存到宿主机CPU的 `x21` 寄存器,接着把客户机CPU的 `s0 ` 寄存器装载到宿主机 CPU 的 `x22` 寄存器,最后使用 `str` 指令更新目标地址处的值。 + +通过以上案例可以发现,TCG后端主要完成三件事情:分配宿主机CPU寄存器、生成宿主机CPU指令以及宿主机CPU和客户机CPU之间的状态同步。其中,状态同步实际上通过两次映射完成:第一次是TCG前端解码时将客户机CPU寄存器映射为QEMU虚拟寄存器,第二次是TCG后端分配宿主机CPU寄存器时将QEMU虚拟寄存器映射为宿主机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` 对应的内存位置,并根据目标平台的指令单元大小更新该指针的值。 +`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 运行的效果。 +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] +- [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 index 94e82de..c458520 100644 --- a/articles/images/qemu-system-decode-analyse/gen_set_gpr.svg +++ b/articles/images/qemu-system-decode-analyse/gen_set_gpr.svg @@ -1,4 +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 +
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 definition
tcg_gen_op2 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)...
tcg_gen_op2(INDEX_op_mov_i32, ret, arg)
tcg_gen_op2(INDEX_op_mov_i32, ret, arg)
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 index 312dd1e..b0009c6 100644 --- a/articles/images/qemu-system-decode-analyse/tcg_gen_code.svg +++ b/articles/images/qemu-system-decode-analyse/tcg_gen_code.svg @@ -1,4 +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 +
tcg_gen_code()
tcg_gen_code()
tcg_reg_alloc_op()
tcg_reg_alloc_op()
tcg_out_op()
tcg_out_op()
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 index e08f81a..d530d6f 100644 --- a/articles/images/qemu-system-decode-analyse/translation_and_execution_loop.svg +++ b/articles/images/qemu-system-decode-analyse/translation_and_execution_loop.svg @@ -1,4 +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 +
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 Back End
TCG Back 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 -- Gitee From 774744c2676f95b04f8de194316b94ada7d23e9e Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Tue, 4 Jul 2023 11:31:20 +0800 Subject: [PATCH 14/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-spaces Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index 82cbbaf..fbccff2 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -49,7 +49,7 @@ QEMU 为了解耦把客户机 CPU 指令先解码为中间码,中间码其实 QEMU 定义了 `instruction pattern` 来描述客户机 CPU 指令,一个 `instruction pattern` 是指一组相同或相近的指令,RISC-V 架构的指令描述位于 `target/riscv` 目录下的 `insn16.decode`、`insn32.decode` 文件中。 -QEMU 编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetree.py` 生成对应指令描述函数的声明并存放于 `/libqemu-riscv64-softmmu.fa.p` 目录下的 `decode-insn32.c.inc` 和 `decode-insn16.c.inc` 文件中。在这两个文件中还定义两个较为关键的解码函数 `decode_insn32` 和 `decode_insn16` ,QEMU 将客户机指令翻译成中间码的时候需要调用这两个解码函数。需要注意的是,脚本 `scripts/decodetree.py` 生成的只是 `trans_xxx` 函数的声明,其定义需要开发者实现,RISC-V 对应的实现位于 `target/riscv/insn_trans/` 目录中。 +QEMU 编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetree.py` 生成对应指令描述函数的声明并存放于 `/libqemu-riscv64-softmmu.fa.p` 目录下的 `decode-insn32.c.inc` 和 `decode-insn16.c.inc` 文件中。在这两个文件中还定义两个较为关键的解码函数 `decode_insn32` 和 `decode_insn16`,QEMU 将客户机指令翻译成中间码的时候需要调用这两个解码函数。需要注意的是,脚本 `scripts/decodetree.py` 生成的只是 `trans_xxx` 函数的声明,其定义需要开发者实现,RISC-V 对应的实现位于 `target/riscv/insn_trans/` 目录中。 ### Decode Tree -- Gitee From 854a2227f2100856653e8d2c67a81f692e6b004a Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Tue, 4 Jul 2023 11:32:00 +0800 Subject: [PATCH 15/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-codeinline Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index fbccff2..2abeb92 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces codeinline]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -224,7 +224,7 @@ add_i64 tmp4,x2/sp,$0x18 qemu_st_i64 x8/s0,tmp4,leq,0 ``` -注意到 `sd` 指令被翻译成了两条中间码,第一条 `add_i64` 是用来计算 `sd ` 指令的目标地址,计算结果保存在 `tmp4` 这个虚拟寄存器里,第二条中间码把 `s0` 的值储存到虚拟寄存器 `tmp4 ` 描述的内存上。在中间码中,`x2/sp` 和 `x8/s0` 仍然是客户机CPU上寄存器的名字,但是逻辑上已经全部映射为QEMU虚拟寄存器。TCG 前端将 RISC-V 汇编指令解码为中间码和虚拟寄存器的表示,后端翻译则基于中间码和虚拟寄存器进行。再次审视上述两条指令,`addi` 的中间码表示要把客户机的 `sp` 寄存器加上-32,`sd`的中间码表示要将客户机的`s0`寄存器中的值送到`sp`寄存器加24后得到的地址处。对于这些中间码,在ARM架构的宿主机上可能被翻译为以下指令: +注意到 `sd` 指令被翻译成了两条中间码,第一条 `add_i64` 是用来计算 `sd` 指令的目标地址,计算结果保存在 `tmp4` 这个虚拟寄存器里,第二条中间码把 `s0` 的值储存到虚拟寄存器 `tmp4` 描述的内存上。在中间码中,`x2/sp` 和 `x8/s0` 仍然是客户机CPU上寄存器的名字,但是逻辑上已经全部映射为QEMU虚拟寄存器。TCG 前端将 RISC-V 汇编指令解码为中间码和虚拟寄存器的表示,后端翻译则基于中间码和虚拟寄存器进行。再次审视上述两条指令,`addi` 的中间码表示要把客户机的 `sp` 寄存器加上-32,`sd` 的中间码表示要将客户机的 `s0` 寄存器中的值送到 `sp` 寄存器加24后得到的地址处。对于这些中间码,在ARM架构的宿主机上可能被翻译为以下指令: ```assembly ldr x20, [x19, #0x10] @@ -235,7 +235,7 @@ ldr x22, [x19, #0x40] str x22, [x21, xzr] ``` -这段指令主要进行了以下操作:把客户机CPU的 `sp` 寄存器装载到宿主机CPU的 `x20` 寄存器,使用 `sub` 指令完成客户机CPU `sp` 寄存器值的计算并进行更新;使用 `add` 指令计算客户机CPU的 `sd` 指令的目标地址并保存到宿主机CPU的 `x21` 寄存器,接着把客户机CPU的 `s0 ` 寄存器装载到宿主机 CPU 的 `x22` 寄存器,最后使用 `str` 指令更新目标地址处的值。 +这段指令主要进行了以下操作:把客户机CPU的 `sp` 寄存器装载到宿主机CPU的 `x20` 寄存器,使用 `sub` 指令完成客户机CPU `sp` 寄存器值的计算并进行更新;使用 `add` 指令计算客户机CPU的 `sd` 指令的目标地址并保存到宿主机CPU的 `x21` 寄存器,接着把客户机CPU的 `s0` 寄存器装载到宿主机 CPU 的 `x22` 寄存器,最后使用 `str` 指令更新目标地址处的值。 通过以上案例可以发现,TCG后端主要完成三件事情:分配宿主机CPU寄存器、生成宿主机CPU指令以及宿主机CPU和客户机CPU之间的状态同步。其中,状态同步实际上通过两次映射完成:第一次是TCG前端解码时将客户机CPU寄存器映射为QEMU虚拟寄存器,第二次是TCG后端分配宿主机CPU寄存器时将QEMU虚拟寄存器映射为宿主机CPU的物理寄存器。 -- Gitee From c0a5522003342d8594ca4ce22a79e6e866e3f8eb Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Tue, 4 Jul 2023 11:32:41 +0800 Subject: [PATCH 16/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-tables Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index 2abeb92..fdc62d3 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces codeinline]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces codeinline tables]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -63,13 +63,13 @@ QEMU 编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetre - **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) | + | 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)) | + | %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)) | 上表给出了一些例子,如第一行的 `%disp 0:s16` 表示指令编码第0位起的16位构成了一个带符号数,因此生成代码 `sextract(i, 0, 16)`,意即从指令 `i` 的编码第0位开始取16位解释为带符号数返回。还有第三行的 `%disp12 0:s1 1:1 2:10` 表示该立即数由三个部分拼接而成,因此生成的代码中就包含了相应的移位、拼接运算。由 `field` 定义所生成的函数就负责完成这种与从指令编码中取数有关的计算。 -- Gitee From d5371fcd4d4bde145b9ecfc1479e9d9dc356ffc6 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Tue, 4 Jul 2023 11:34:04 +0800 Subject: [PATCH 17/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-pangu Signed-off-by: jl-jiang --- .../20230701-qemu-system-decode-analyse.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index fdc62d3..43ae935 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces codeinline tables]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces codeinline tables urls refs pangu]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
@@ -10,11 +10,11 @@ ## 前言 -QEMU是一个通用的、开源的模拟器,通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。QEMU 亦可藉由硬件虚拟化变身为虚拟机。 +QEMU 是一个通用的、开源的模拟器,通过纯软件方式实现硬件的虚拟化,模拟外部硬件,为用户提供抽象、虚拟的硬件环境。QEMU 亦可藉由硬件虚拟化变身为虚拟机。 QEMU 支持以下两种方式进行模拟: -- 用户模式(User Mode Emulation):在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。在该模式下,QEMU作为进程级虚拟机,只模拟系统调用前的用户态代码,系统调用进入内核后,由宿主机操作系统原生执行,QEMU 提供不同架构系统调用映射的转换。 +- 用户模式(User Mode Emulation):在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。在该模式下,QEMU 作为进程级虚拟机,只模拟系统调用前的用户态代码,系统调用进入内核后,由宿主机操作系统原生执行,QEMU 提供不同架构系统调用映射的转换。 - 系统模拟(System Emulation):在该模式下,QEMU 作为系统级虚拟机提供运行客户机所需的完整环境,包括 CPU,内存和外围设备等。 ## 概述 @@ -71,7 +71,7 @@ QEMU 编译的时候会解析 `.decode` 文件,使用脚本 `scripts/decodetre | %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)) | - 上表给出了一些例子,如第一行的 `%disp 0:s16` 表示指令编码第0位起的16位构成了一个带符号数,因此生成代码 `sextract(i, 0, 16)`,意即从指令 `i` 的编码第0位开始取16位解释为带符号数返回。还有第三行的 `%disp12 0:s1 1:1 2:10` 表示该立即数由三个部分拼接而成,因此生成的代码中就包含了相应的移位、拼接运算。由 `field` 定义所生成的函数就负责完成这种与从指令编码中取数有关的计算。 + 上表给出了一些例子,如第一行的 `%disp 0:s16` 表示指令编码第 0 位起的 16 位构成了一个带符号数,因此生成代码 `sextract(i, 0, 16)`,意即从指令 `i` 的编码第 0 位开始取 16 位解释为带符号数返回。还有第三行的 `%disp12 0:s1 1:1 2:10` 表示该立即数由三个部分拼接而成,因此生成的代码中就包含了相应的移位、拼接运算。由 `field` 定义所生成的函数就负责完成这种与从指令编码中取数有关的计算。 - **Argument Sets:** 定义数据结构。比如,`target/riscv/insn32.decode` 中定义的 `&b imm rs2 rs1` 在编译后的 `decode-insn32.c.inc` 中生成的数据结构如下,这个结构将作为 `trans_xxx` 函数的传入参数。 @@ -205,11 +205,11 @@ void riscv_translate_init(void) } ``` -`cpu_gpr[]` 数组在初始化时调用 `tcg_global_mem_new` 函数在TCG上下文 `tcg_ctx` 中分配空间并返回其相对地址,而后段翻译时访问 `cpu_gpr[]` 数组就是在访问TCG上下文中描述寄存器的变量,这样 `cpu_gpr[reg_name]` 就在前端和后端之间建立了连接。 +`cpu_gpr[]` 数组在初始化时调用 `tcg_global_mem_new` 函数在 TCG 上下文 `tcg_ctx` 中分配空间并返回其相对地址,而后段翻译时访问 `cpu_gpr[]` 数组就是在访问 TCG 上下文中描述寄存器的变量,这样 `cpu_gpr[reg_name]` 就在前端和后端之间建立了连接。 ### 后端翻译 -后端的代码主要负责将中间码翻译成宿主机指令,本质上就是根据中间码的描述使用宿主机指令来改变内存中表示的客户机CPU的数据结构以及客户机内存的状态。考虑以下两条 RISC-V 汇编指令: +后端的代码主要负责将中间码翻译成宿主机指令,本质上就是根据中间码的描述使用宿主机指令来改变内存中表示的客户机 CPU 的数据结构以及客户机内存的状态。考虑以下两条 RISC-V 汇编指令: ```assembly addi sp,sp,-32 @@ -224,7 +224,7 @@ add_i64 tmp4,x2/sp,$0x18 qemu_st_i64 x8/s0,tmp4,leq,0 ``` -注意到 `sd` 指令被翻译成了两条中间码,第一条 `add_i64` 是用来计算 `sd` 指令的目标地址,计算结果保存在 `tmp4` 这个虚拟寄存器里,第二条中间码把 `s0` 的值储存到虚拟寄存器 `tmp4` 描述的内存上。在中间码中,`x2/sp` 和 `x8/s0` 仍然是客户机CPU上寄存器的名字,但是逻辑上已经全部映射为QEMU虚拟寄存器。TCG 前端将 RISC-V 汇编指令解码为中间码和虚拟寄存器的表示,后端翻译则基于中间码和虚拟寄存器进行。再次审视上述两条指令,`addi` 的中间码表示要把客户机的 `sp` 寄存器加上-32,`sd` 的中间码表示要将客户机的 `s0` 寄存器中的值送到 `sp` 寄存器加24后得到的地址处。对于这些中间码,在ARM架构的宿主机上可能被翻译为以下指令: +注意到 `sd` 指令被翻译成了两条中间码,第一条 `add_i64` 是用来计算 `sd` 指令的目标地址,计算结果保存在 `tmp4` 这个虚拟寄存器里,第二条中间码把 `s0` 的值储存到虚拟寄存器 `tmp4` 描述的内存上。在中间码中,`x2/sp` 和 `x8/s0` 仍然是客户机 CPU 上寄存器的名字,但是逻辑上已经全部映射为 QEMU 虚拟寄存器。TCG 前端将 RISC-V 汇编指令解码为中间码和虚拟寄存器的表示,后端翻译则基于中间码和虚拟寄存器进行。再次审视上述两条指令,`addi` 的中间码表示要把客户机的 `sp` 寄存器加上-32,`sd` 的中间码表示要将客户机的 `s0` 寄存器中的值送到 `sp` 寄存器加 24 后得到的地址处。对于这些中间码,在 ARM 架构的宿主机上可能被翻译为以下指令: ```assembly ldr x20, [x19, #0x10] @@ -235,9 +235,9 @@ ldr x22, [x19, #0x40] str x22, [x21, xzr] ``` -这段指令主要进行了以下操作:把客户机CPU的 `sp` 寄存器装载到宿主机CPU的 `x20` 寄存器,使用 `sub` 指令完成客户机CPU `sp` 寄存器值的计算并进行更新;使用 `add` 指令计算客户机CPU的 `sd` 指令的目标地址并保存到宿主机CPU的 `x21` 寄存器,接着把客户机CPU的 `s0` 寄存器装载到宿主机 CPU 的 `x22` 寄存器,最后使用 `str` 指令更新目标地址处的值。 +这段指令主要进行了以下操作:把客户机 CPU 的 `sp` 寄存器装载到宿主机 CPU 的 `x20` 寄存器,使用 `sub` 指令完成客户机 CPU `sp` 寄存器值的计算并进行更新;使用 `add` 指令计算客户机 CPU 的 `sd` 指令的目标地址并保存到宿主机 CPU 的 `x21` 寄存器,接着把客户机 CPU 的 `s0` 寄存器装载到宿主机 CPU 的 `x22` 寄存器,最后使用 `str` 指令更新目标地址处的值。 -通过以上案例可以发现,TCG后端主要完成三件事情:分配宿主机CPU寄存器、生成宿主机CPU指令以及宿主机CPU和客户机CPU之间的状态同步。其中,状态同步实际上通过两次映射完成:第一次是TCG前端解码时将客户机CPU寄存器映射为QEMU虚拟寄存器,第二次是TCG后端分配宿主机CPU寄存器时将QEMU虚拟寄存器映射为宿主机CPU的物理寄存器。 +通过以上案例可以发现,TCG 后端主要完成三件事情:分配宿主机 CPU 寄存器、生成宿主机 CPU 指令以及宿主机 CPU 和客户机 CPU 之间的状态同步。其中,状态同步实际上通过两次映射完成:第一次是 TCG 前端解码时将客户机 CPU 寄存器映射为 QEMU 虚拟寄存器,第二次是 TCG 后端分配宿主机 CPU 寄存器时将 QEMU 虚拟寄存器映射为宿主机 CPU 的物理寄存器。 下面仍然以 `add` 指令为例,给出后端代码调用过程的详细分析: -- Gitee From 0b929cf1e9ae1e3efab397aed0e4d87a42393f04 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Tue, 4 Jul 2023 11:36:32 +0800 Subject: [PATCH 18/34] Update 20230701-qemu-system-decode-analyse.md --- articles/20230701-qemu-system-decode-analyse.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index 43ae935..daa2d03 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,10 +1,10 @@ > Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces codeinline tables urls refs pangu]
-> Author: jl-jiang
-> Date: 2023/07/01
-> Revisor: Bin Meng
-> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> 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 +> Sponsor: PLCT Lab, ISCAS # QEMU 系统模式下指令解码模块简析:以 RISC-V 为例 -- Gitee From 0ea2f1ec019a0352bdcc90f9bc9365158b75bd27 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Tue, 4 Jul 2023 11:37:08 +0800 Subject: [PATCH 19/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-header Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index daa2d03..b879eae 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,10 +1,10 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces codeinline tables urls refs pangu]
-> Author: jl-jiang
-> Date: 2023/07/01
-> Revisor: Bin Meng
-> Project: RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [header]
+> 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 +> Sponsor: PLCT Lab, ISCAS # QEMU 系统模式下指令解码模块简析:以 RISC-V 为例 -- Gitee From 0d87bc6fa563eaacbb4b2653bc8da787ce3307dd Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Tue, 4 Jul 2023 11:38:00 +0800 Subject: [PATCH 20/34] qemu-system-decode-analyse.md: commit correct result of tinycorrect-pangu Signed-off-by: jl-jiang --- articles/20230701-qemu-system-decode-analyse.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/articles/20230701-qemu-system-decode-analyse.md b/articles/20230701-qemu-system-decode-analyse.md index b879eae..14205f3 100644 --- a/articles/20230701-qemu-system-decode-analyse.md +++ b/articles/20230701-qemu-system-decode-analyse.md @@ -1,8 +1,8 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [header]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [header urls refs pangu]
> Author: jl-jiang
> Date: 2023/07/01
> Revisor: Bin Meng
-> Project: RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Project: RISC-V Linux 内核剖析】(https://gitee.com/tinylab/riscv-linux)
> Proposal: [【老师提案】QEMU 系统模拟模式分析](https://gitee.com/tinylab/riscv-linux/issues/I61KIY)
> Sponsor: PLCT Lab, ISCAS -- Gitee From a44f4dc5c1c77ff5195342a82c43b2d68cf163f2 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Thu, 13 Jul 2023 00:02:13 +0800 Subject: [PATCH 21/34] Upload 20230707-qemu-system-parameters.md and images --- articles/20230707-qemu-system-parameters.md | 443 ++++++++++++++++++ .../qemu-system-parameters/data_relation.svg | 4 + .../qemu-system-parameters/linked_list.svg | 4 + .../machine_parameters.svg | 4 + 4 files changed, 455 insertions(+) create mode 100644 articles/20230707-qemu-system-parameters.md create mode 100644 articles/images/qemu-system-parameters/data_relation.svg create mode 100644 articles/images/qemu-system-parameters/linked_list.svg create mode 100644 articles/images/qemu-system-parameters/machine_parameters.svg diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md new file mode 100644 index 0000000..3aae4f4 --- /dev/null +++ b/articles/20230707-qemu-system-parameters.md @@ -0,0 +1,443 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [header urls refs pangu]
+> Author: jl-jiang
+> Date: 2023/07/07
+> 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 参数解析机制简析 + +## 前言 + +QEMU 使用命令行来配置模拟器的启动参数,本文将简要梳理 QEMU 常用参数的含义与用法,重点分析 QEMU 的参数解析机制。 + +## 常用参数 + +QEMU 的全部可用参数定义于项目目录下的 `qemu-options.hx` 文件中,各选项的具体用法可以通过指令 `qemu-system-riscv64 [options] help` 查看。下面以一条常见的 QEMU 启动命令为例,分析 QEMU 常用参数的含义: + +```bash +qemu-system-riscv64 -M virt -m 256M -nographic \ + -kernel linux-kernel/arch/riscv/boot/Image \ + -drive file=rootfs.img,format=raw,id=hd0 \ + -device virtio-blk-device,drive=hd0 \ + -append "root=/dev/vda rw console=ttyS0" +``` + +- **-M:** 指定模拟器运行时的设备类型,在上面的例子中指定了 `qemu-system-riscv64` 的模拟设备为 `virt`,即RISC-V架构下的“通用虚拟平台” +- **-m:** 指定虚拟机内部的内存大小 +- **-kernel:** 指定内核镜像 +- **-append:** 指定内核命令行代码 +- **-drive:** 定义磁盘,`file` 表示磁盘镜像的文件路径,`format` 表示磁盘格式,`id` 表示磁盘号 +- **-device:** 添加设备到虚拟机中 + +## 参数解析 + +### 流程概述 + +QEMU 参数解析的主体逻辑位于 `softmmu/vl.c` 文件的 `qemu_init` 函数中,主要分为两个阶段,第一阶段负责检查传入参数的合法性,并不具体解析,第二阶段执行具体的解析操作,并通过 `switch` 语句跳转不同为分支完成对应选项的设置。QEMU 在不同的抽象层次上定义了多种数据结构来对不同的参数进行描述,这些数据结构都在参数解析之前完成初始化。 + +### 数据结构及初始化 + +QEMU 在 `softmmu/vl.c` 文件中定义了 `QEMUOption` 结构体来描述不同的命令行参数,其代码如下: + +```c +typedef struct QEMUOption { + const char *name; + int flags; + int index; + uint32_t arch_mask; +} QEMUOption; +``` + +其中 `name` 表示参数名称,`flags` 表示参数属性,比如是否拥有子参数,`index` 表示指令索引,`arch_mask` 表示参数支持的架构。在 `softmmu/vl.c` 文件中还定义了一个全局 `QEMUOption` 数组 `qemu_options` 来描述 QEMU 的全部可用参数,具体如下: + +```c +static const QEMUOption qemu_options[] = { + { "h", 0, QEMU_OPTION_h, QEMU_ARCH_ALL }, + +#define DEF(option, opt_arg, opt_enum, opt_help, arch_mask) \ + { option, opt_arg, opt_enum, arch_mask }, +#define DEFHEADING(text) +#define ARCHHEADING(text, arch_mask) + +#include "qemu-options.def" + { /* end of list */ } +}; +``` + +可以看到,`qemu_options` 数组中首先定义了一个参数 `h`,其使用方法为 `qemu-system-riscv64 -h`,作用是打印帮助信息。其余所有的可用参数都都通过 `DEF` 宏定义在 `/qemu-options.def` 文件中。需要注意的是,`qemu-options.def` 文件是由 `scripts/hxtool` 脚本在编译时根据 `qemu-options.hx` 文件生成的,因此不在 QEMU 源代码目录中。 + +对于含有多个子选项的参数,QEMU 在文件 `include/qemu/option_int.h` 中定义了两个结构体 `QemuOpt` 对子参数进行描述: + +```c +struct QemuOpt { + char *name; + char *str; + + const QemuOptDesc *desc; + union { + bool boolean; + uint64_t uint; + } value; + + QemuOpts *opts; + QTAILQ_ENTRY(QemuOpt) next; +}; +``` + +其中 `QemuOpt` 用于存储子选项,每个 `QemuOpt` 结构体都有一个 `QemuOptDesc` 类型的成员变量来描述子选项的信息。QEMU 在文件 `include/qemu/option.h` 中定义了 `QemuOptsList` 结构体用于进一步抽象描述一个参数的所有子选项,有关代码如下: + +```c +enum QemuOptType { + QEMU_OPT_STRING = 0, /* no parsing (use string as-is) */ + QEMU_OPT_BOOL, /* on/off */ + QEMU_OPT_NUMBER, /* simple number */ + QEMU_OPT_SIZE, /* size, accepts (K)ilo, (M)ega, (G)iga, (T)era postfix */ +}; + +typedef struct QemuOptDesc { + const char *name; + enum QemuOptType type; + const char *help; + const char *def_value_str; +} QemuOptDesc; + +struct QemuOptsList { + const char *name; + const char *implied_opt_name; + bool merge_lists; /* Merge multiple uses of option into a single list? */ + QTAILQ_HEAD(, QemuOpts) head; + QemuOptDesc desc[]; +}; +``` + +上述代码中,首先定义了枚举类型 `QemuOptType` 用于描述不同的参数类型,包括字符串、布尔值、数字和空间大小四种。接着定义了结构体 `QemuOptDesc` 用于存放对参数的描述信息,包括名称、类型、帮助信息和参数默认值。最后定义了结构体 `QemuOptsList`,每一个 `QemuOptsList` 结构体就对应了一个参数。但是,这里需要特别注意的是,`QemuOptsList` 并不直接与 `QemuOpt` 联系,中间需要通过结构体 `QemuOpts` 进行转接: + +```c +struct QemuOpts { + char *id; + QemuOptsList *list; + Location loc; + QTAILQ_HEAD(, QemuOpt) head; + QTAILQ_ENTRY(QemuOpts) next; +}; +``` + +这样设计的目的在于避免发生参数混淆,QEMU 可以通过命令行指定创建两个相同的设备,这个时候这两个设备的选项都挂在 `QemuOptsList` 上,但是分别属于两个独立的 `QemuOpts`(`id` 不同),每个 `QemuOpts` 都维护自己的 `QemuOpt` 链表,成员变量 `head` 是 `QemuOpts` 下的 `QemuOpt` 链表头,成员变量 `next` 用来连接同一 `QemuOptsList` 下的其他 `QemuOpts` ,具体如下图所示: + +![](images/qemu-system-parameters/data_relation.svg) + +QEMU 在 `util/qemu-config.c` 中定义了一个全局的 `QemuOptsList` 数组 `vm_config_groups` 来储存所有可用的参数: + +```c +static QemuOptsList *vm_config_groups[48]; +static QemuOptsList *drive_config_groups[5]; +``` + +这两行代码说明了 QEMU 最多支持48个参数,5个驱动器参数。这两个全局数组由位于 `softmmu/vl.c` 文件的 `qemu_init` 函数负责初始化: + +```c + qemu_add_opts(&qemu_drive_opts); + qemu_add_drive_opts(&qemu_legacy_drive_opts); + qemu_add_drive_opts(&qemu_common_drive_opts); + qemu_add_drive_opts(&qemu_drive_opts); + qemu_add_drive_opts(&bdrv_runtime_opts); + qemu_add_opts(&qemu_chardev_opts); + qemu_add_opts(&qemu_device_opts); + qemu_add_opts(&qemu_netdev_opts); + qemu_add_opts(&qemu_nic_opts); + qemu_add_opts(&qemu_net_opts); + qemu_add_opts(&qemu_rtc_opts); + qemu_add_opts(&qemu_global_opts); + qemu_add_opts(&qemu_mon_opts); + qemu_add_opts(&qemu_trace_opts); + qemu_plugin_add_opts(); + qemu_add_opts(&qemu_option_rom_opts); + qemu_add_opts(&qemu_accel_opts); + qemu_add_opts(&qemu_mem_opts); + qemu_add_opts(&qemu_smp_opts); + qemu_add_opts(&qemu_boot_opts); + qemu_add_opts(&qemu_add_fd_opts); + qemu_add_opts(&qemu_object_opts); + qemu_add_opts(&qemu_tpmdev_opts); + qemu_add_opts(&qemu_overcommit_opts); + qemu_add_opts(&qemu_msg_opts); + qemu_add_opts(&qemu_name_opts); + qemu_add_opts(&qemu_numa_opts); + qemu_add_opts(&qemu_icount_opts); + qemu_add_opts(&qemu_semihosting_config_opts); + qemu_add_opts(&qemu_fw_cfg_opts); + qemu_add_opts(&qemu_action_opts); +``` + +其中,`qemu_add_opts` 函数的实现位于 `util/qemu-config.c` 文件中,该函数主要负责将参数中传入的 `OemuOptsList` 添加到全局数组 `vm_config_groups` 中: + +```c +void qemu_add_opts(QemuOptsList *list) +{ + int entries, i; + + entries = ARRAY_SIZE(vm_config_groups); + entries--; /* keep list NULL terminated */ + for (i = 0; i < entries; i++) { + if (vm_config_groups[i] == NULL) { + vm_config_groups[i] = list; + return; + } + } + fprintf(stderr, "ran out of space in vm_config_groups"); + abort(); +} +``` + +### 第一阶段 + +QEMU 参数解析的第一阶段,遍历参数数组,通过 `lookup_opt` 函数来得到一个 `QEMUOption` ,判断这个 `QEMUOption` 是否在之前初始化的的数组中。这一步骤的主要作用验证参数的合法性,下面结合源码具体分析: + +```c + /* first pass of option parsing */ + optind = 1; + while (optind < argc) { + if (argv[optind][0] != '-') { + /* disk image */ + optind++; + } else { + const QEMUOption *popt; + + popt = lookup_opt(argc, argv, &optarg, &optind); + switch (popt->index) { + case QEMU_OPTION_nouserconfig: + userconfig = false; + break; + } + } + } + + machine_opts_dict = qdict_new(); + if (userconfig) { + qemu_read_default_config_file(&error_fatal); + } +``` + +首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,调用 `lookup_opt` 函数查询读取的参数名称,然后初始化 `machine_opts_dict` 数组,并根据实际情况加载用户配置。其中,`lookup_opt` 函数的定义如下: + +```c +static const QEMUOption *lookup_opt(int argc, char **argv, const char **poptarg, int *poptind) +{ + const QEMUOption *popt; + int optind = *poptind; + char *r = argv[optind]; + const char *optarg; + + loc_set_cmdline(argv, optind, 1); + optind++; + /* Treat --foo the same as -foo. */ + if (r[1] == '-') + r++; + popt = qemu_options; + for(;;) { + if (!popt->name) { + error_report("invalid option"); + exit(1); + } + if (!strcmp(popt->name, r + 1)) + break; + popt++; + } + if (popt->flags & HAS_ARG) { + if (optind >= argc) { + error_report("requires an argument"); + exit(1); + } + optarg = argv[optind++]; + loc_set_cmdline(argv, optind - 2, 2); + } else { + optarg = NULL; + } + + *poptarg = optarg; + *poptind = optind; + + return popt; +} +``` + +`lookup_opt` 函数首先调用 `loc_set_cmdline` 函数在终端传入的命令行参数中根据参数索引 `optind` 进行定位,然后通过参数名称的比较在全局数组 `qemu_options` 中寻找对应的 `QEMUOption`。同时,`lookup_opt` 函数会将参数后面的子选项保存到 `optarg` 中。 + +### 第二阶段 + +QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设置的阶段,主要逻辑如下: + +```c + /* second pass of option parsing */ + optind = 1; + for(;;) { + if (optind >= argc) + break; + if (argv[optind][0] != '-') { + loc_set_cmdline(argv, optind, 1); + drive_add(IF_DEFAULT, 0, argv[optind++], HD_OPTS); + } else { + const QEMUOption *popt; + + popt = lookup_opt(argc, argv, &optarg, &optind); + if (!(popt->arch_mask & arch_type)) { + error_report("Option not supported for this target"); + exit(1); + } + switch(popt->index) { + case QEMU_OPTION_cpu: + ... + break; + ... + default: + ... + } + } + } +``` + +第二阶段的逻辑就是按照下标顺序依次遍历终端传入的参数数组,调用 `lookup_opt` 函数找到对应的 `QEMUOption`,然后检查对应选项在当前架构下是否支持,最后使用 `switch` 语句根据 `QEMUOption` 的成员变量 `index` 的不同来执行不同的分支完成具体的设置。 + +下面将以不同的参数为例,详细说明参数解析第二阶段 `switch` 语句中的分支执行流程。首先关注最为简单的 `-version` 参数,其含义就是打印 QEMU 版本信息,然后退出程序,有关代码如下: + +```c + case QEMU_OPTION_version: + version(); + exit(0); + break; +``` + +可以看到,由于其含义最为简单,因此解析实现也非常简洁,直接调用 `version` 函数打印版本信息,随后退出程序。接着分析 `-kernel` 参数,通过对 `-kernel` 参数解析流程的考察,我们可以一窥与机器设置有关的参数的解析机制。首先给出 `-kernel` 参数在解析第二阶段分支执行的主要代码: + +```c + case QEMU_OPTION_kernel: + qdict_put_str(machine_opts_dict, "kernel", optarg); + break; +``` + +我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,这里的 `machine_opts_dict` 是在参数解析第一阶段末尾初始化的 `QDict` 类型的键值对集合,用于存储终端传入的参数数组中所有与机器设置有关的参数选项。最终参数的设置落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_matchine_options` 函数和 `qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制“的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: + +![](images/qemu-system-parameters/machine_parameters.svg) + +最后,观察 `-device` 参数,同样给出分支执行代码: + +```c + case QEMU_OPTION_device: + if (optarg[0] == '{') { + QObject *obj = qobject_from_json(optarg, &error_fatal); + DeviceOption *opt = g_new0(DeviceOption, 1); + opt->opts = qobject_to(QDict, obj); + loc_save(&opt->loc); + assert(opt->opts != NULL); + QTAILQ_INSERT_TAIL(&device_opts, opt, next); + } else { + if (!qemu_opts_parse_noisily(qemu_find_opts("device"), + optarg, true)) { + exit(1); + } + } + break; +``` + +分支执行的主要逻辑如下:首先检查 `optarg`(`-device` 选项的参数)是否以 `{` 开始。如果是,说明 `optarg` 是一个 JSON 对象,那么就需要调用 `qobject_from_json` 函数将 JSON 对象转换为一个 `QObject` 对象。然后,创建一个新的 `DeviceOption` 对象,并将 `QObject` 对象转换为一个 `QDict` 并存储在 `DeviceOption` 中,最后将 `DeviceOption` 添加到 `device_opts` 队列中。如果 `optarg` 不是一个 JSON 对象,那么就调用 `qemu_opts_parse_noisily` 函数将 `optarg` 解析为一个 `QemuOpts` 对象,并将这个对象添加到 "device" 选项的列表中。如果解析失败,则退出程序。下面我们重点关注 `else` 分支的部分,首先给出 `qemu_find_opts` 函数的定义,它位于 `util/qemu-config.c` 文件中: + +```c +QemuOptsList *qemu_find_opts(const char *group) +{ + QemuOptsList *ret; + Error *local_err = NULL; + + ret = find_list(vm_config_groups, group, &local_err); + if (local_err) { + error_report_err(local_err); + } + + return ret; +} +``` + +`qemu_find_opts` 函数从全局数组 `vm_config_groups` 中找到刚才插入的 `-device` 选相对应的 `QemuOptsList` 并返回,而 `qemu_opts_parse_noisily` 函数只是简单调用了 `opts_parse` 函数,后者会解析出一个 `QemuOpts`,每一个大类的参数都会在相应的`QemuOptsList` 中构造 `QEMUOpts`。继续分析 `opts_parse` 函数: + +```c +static QemuOpts *opts_parse(QemuOptsList *list, const char *params, + bool permit_abbrev, + bool warn_on_flag, bool *help_wanted, Error **errp) +{ + const char *firstname; + char *id = opts_parse_id(params); + QemuOpts *opts; + + assert(!permit_abbrev || list->implied_opt_name); + firstname = permit_abbrev ? list->implied_opt_name : NULL; + + opts = qemu_opts_create(list, id, !list->merge_lists, errp); + g_free(id); + if (opts == NULL) { + return NULL; + } + + if (!opts_do_parse(opts, params, firstname, + warn_on_flag, help_wanted, errp)) { + qemu_opts_del(opts); + return NULL; + } + + return opts; +} +``` + +`opts_parse` 函数代码中最重要的两行是对 `qemu_opts_create` 函数和 ` opts_do_parse` 函数的调用,前者用来创建 `QEMUopts` 并将它插入到对应的 `QemuOptsList` 上,后者则负责解析出 `QemuOpt`。函数 `opts_do_parse` 的作用是解析参数的值,如本文开头例子中的命令行参数 `-device virtio-blk-device,drive=hd0`。QEMU 的参数可能存在多种情况,比如上面的例子中 `virtio-blk-device` 表示开启一个标志,也有可能类似于 `drive=hd0` 的参数赋值语句。`opts_do_parse` 函数需要处理各种情况,并对每一个值生成一个 `QemuOpt`,关键代码如下: + +```c +static bool opts_do_parse(QemuOpts *opts, const char *params, + const char *firstname, + bool warn_on_flag, bool *help_wanted, Error **errp) +{ + char *option, *value; + const char *p; + QemuOpt *opt; + + for (p = params; *p;) { + p = get_opt_name_value(p, firstname, warn_on_flag, help_wanted, &option, &value); + if (help_wanted && *help_wanted) { + g_free(option); + g_free(value); + return false; + } + firstname = NULL; + + if (!strcmp(option, "id")) { + g_free(option); + g_free(value); + continue; + } + + opt = opt_create(opts, option, value); + g_free(option); + if (!opt_validate(opt, errp)) { + qemu_opt_del(opt); + return false; + } + } + + return true; +} +``` + +`opts_do_parse` 函数是 QEMU 参数解析过程的核心部分,它接受六个参数:`opts` 是一个 `QemuOpts` 对象,用于存储解析后的选项;`params` 是一个字符串,包含需要解析的参数;`firstname` 是第一个选项的名字;`warn_on_flag` 是一个布尔值,如果为 `true`,那么当遇到一个标志选项时,函数会打印一个警告消息;`help_wanted` 是一个指向布尔值的指针,如果函数解析到一个 `help` 选项,则设置 `*help_wanted` 为 `true`;`errp` 是一个错误指针,如果函数遇到错误,则设置 `*errp`。`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。此外,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt` 。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 + +仍然以命令行参数 `-device virtio-blk-device,drive=hd0` 为例,经过上述过程将解析出2个 `QemuOpt` 并形成如下图所示的链表: + +![](images/qemu-system-parameters/linked_list.svg) + +## 总结 + +本文以 QEMU 中引导 RISC-V 架构 LInux 内核启动的指令为例,总结归纳了 QEMU 常用参数的用法与含义,分析阐述了描述不同参数的各种数据结构的定义、作用以及相互关系,并按照 QEMU 的执行顺序详细梳理了数据结构初始化、参数解析第一阶段、参数解析第二阶段的代码逻辑。 + +## 参考资料 + +- [《QEMU启动方式分析(1):QEMU及RISC-V启动流程简介》](https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220816-introduction-to-qemu-and-riscv-upstream-boot-flow.md) +- [Invocation](https://www.qemu.org/docs/master/system/invocation.html) +- 《QEMU/KVM源码解析与应用》李强,机械工业出版社 \ No newline at end of file diff --git a/articles/images/qemu-system-parameters/data_relation.svg b/articles/images/qemu-system-parameters/data_relation.svg new file mode 100644 index 0000000..8f971a2 --- /dev/null +++ b/articles/images/qemu-system-parameters/data_relation.svg @@ -0,0 +1,4 @@ + + + +QemuOptsList
const char *name;
const char *name;
const char *implied_opt_name;
const char *implied_opt_name;
bool merge_lists;  
bool merge_lists;  
QTAILQ_HEAD(, QemuOpts) head;
QTAILQ_HEAD(, QemuOpts) head;
QemuOptDesc desc[];
QemuOptDesc desc[];
QemuOpts
char *id
char *id
QemuOptsList *list
QemuOptsList *list
Location loc
Location loc
QTAILQ_HEAD(, QemuOpt) head
QTAILQ_HEAD(, QemuOpt) head
QTAILQ_ENTRY(QemuOpts) next
QTAILQ_ENTRY(QemuOpts) next
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOptDesc
QemuOptDesc
QemuOptDesc
QemuOptDesc
……
……
QemuOptDesc
QemuOptDesc
QemuOptDesc
QemuOptDesc
QemuOpt
char *name
char *name
char *str
char *str
const QemuOptDesc *desc
const QemuOptDesc *desc
union {bool boolean; uint64_t uint;} value
union {bool boolean; uint64_t uint;} value
QemuOpts     *opts
QemuOpts     *opts
QTAILQ_ENTRY(QemuOpt) next;
QTAILQ_ENTRY(QemuOpt) next;
QemuOpt
QemuOpt
QemuOpt
QemuOpt
QemuOptDesc
const char *name
const char *name
enum QemuOptType type
enum QemuOptType type
const char *help
const char *help
const char *def_value_str
const char *def_value_str
Text is not SVG - cannot display
\ No newline at end of file diff --git a/articles/images/qemu-system-parameters/linked_list.svg b/articles/images/qemu-system-parameters/linked_list.svg new file mode 100644 index 0000000..b5633f1 --- /dev/null +++ b/articles/images/qemu-system-parameters/linked_list.svg @@ -0,0 +1,4 @@ + + + +
...
...
...
...
qemu_device_opts
qemu_device_opts
...
...
...
...
vm_config_groups
vm_config_groups
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpt
name=driver
name=driver
str=virtio-blk-device
str=virtio-blk-device
QemuOpt
name=drive
name=drive
str=hd0
str=hd0
Text is not SVG - cannot display
\ No newline at end of file diff --git a/articles/images/qemu-system-parameters/machine_parameters.svg b/articles/images/qemu-system-parameters/machine_parameters.svg new file mode 100644 index 0000000..d238c53 --- /dev/null +++ b/articles/images/qemu-system-parameters/machine_parameters.svg @@ -0,0 +1,4 @@ + + + +
 qdict_new()
 qdict_new()
lookup_opt()
lookup_opt()
switch(popt->index)
switch(popt->index)
case QEMU_OPTION_kernel
case QEMU_OPTION_kernel
qdict_put_str()
qdict_put_str()
qemu_validate_options()
qemu_validate_options()
qemu_apply_legacy_machine_options()
qemu_apply_legacy_machine_options()
qemu_apply_machine_options()
qemu_apply_machine_options()
Creat QDict
Creat QDict
Decode Parameters
Decode Parameters
Apply Options
Apply Options
Text is not SVG - cannot display
\ No newline at end of file -- Gitee From c211e18ad19862c12071666368e6478a13368196 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Thu, 13 Jul 2023 00:03:23 +0800 Subject: [PATCH 22/34] qemu-system-parameters.md: commit correct result of tinycorrect-spaces Signed-off-by: jl-jiang --- articles/20230707-qemu-system-parameters.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index 3aae4f4..b063db4 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [header urls refs pangu]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces]
> Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
@@ -124,7 +124,7 @@ struct QemuOpts { }; ``` -这样设计的目的在于避免发生参数混淆,QEMU 可以通过命令行指定创建两个相同的设备,这个时候这两个设备的选项都挂在 `QemuOptsList` 上,但是分别属于两个独立的 `QemuOpts`(`id` 不同),每个 `QemuOpts` 都维护自己的 `QemuOpt` 链表,成员变量 `head` 是 `QemuOpts` 下的 `QemuOpt` 链表头,成员变量 `next` 用来连接同一 `QemuOptsList` 下的其他 `QemuOpts` ,具体如下图所示: +这样设计的目的在于避免发生参数混淆,QEMU 可以通过命令行指定创建两个相同的设备,这个时候这两个设备的选项都挂在 `QemuOptsList` 上,但是分别属于两个独立的 `QemuOpts`(`id` 不同),每个 `QemuOpts` 都维护自己的 `QemuOpt` 链表,成员变量 `head` 是 `QemuOpts` 下的 `QemuOpt` 链表头,成员变量 `next` 用来连接同一 `QemuOptsList` 下的其他 `QemuOpts`,具体如下图所示: ![](images/qemu-system-parameters/data_relation.svg) @@ -193,7 +193,7 @@ void qemu_add_opts(QemuOptsList *list) ### 第一阶段 -QEMU 参数解析的第一阶段,遍历参数数组,通过 `lookup_opt` 函数来得到一个 `QEMUOption` ,判断这个 `QEMUOption` 是否在之前初始化的的数组中。这一步骤的主要作用验证参数的合法性,下面结合源码具体分析: +QEMU 参数解析的第一阶段,遍历参数数组,通过 `lookup_opt` 函数来得到一个 `QEMUOption`,判断这个 `QEMUOption` 是否在之前初始化的的数组中。这一步骤的主要作用验证参数的合法性,下面结合源码具体分析: ```c /* first pass of option parsing */ @@ -426,7 +426,7 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, } ``` -`opts_do_parse` 函数是 QEMU 参数解析过程的核心部分,它接受六个参数:`opts` 是一个 `QemuOpts` 对象,用于存储解析后的选项;`params` 是一个字符串,包含需要解析的参数;`firstname` 是第一个选项的名字;`warn_on_flag` 是一个布尔值,如果为 `true`,那么当遇到一个标志选项时,函数会打印一个警告消息;`help_wanted` 是一个指向布尔值的指针,如果函数解析到一个 `help` 选项,则设置 `*help_wanted` 为 `true`;`errp` 是一个错误指针,如果函数遇到错误,则设置 `*errp`。`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。此外,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt` 。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 +`opts_do_parse` 函数是 QEMU 参数解析过程的核心部分,它接受六个参数:`opts` 是一个 `QemuOpts` 对象,用于存储解析后的选项;`params` 是一个字符串,包含需要解析的参数;`firstname` 是第一个选项的名字;`warn_on_flag` 是一个布尔值,如果为 `true`,那么当遇到一个标志选项时,函数会打印一个警告消息;`help_wanted` 是一个指向布尔值的指针,如果函数解析到一个 `help` 选项,则设置 `*help_wanted` 为 `true`;`errp` 是一个错误指针,如果函数遇到错误,则设置 `*errp`。`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。此外,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 仍然以命令行参数 `-device virtio-blk-device,drive=hd0` 为例,经过上述过程将解析出2个 `QemuOpt` 并形成如下图所示的链表: @@ -440,4 +440,4 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, - [《QEMU启动方式分析(1):QEMU及RISC-V启动流程简介》](https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220816-introduction-to-qemu-and-riscv-upstream-boot-flow.md) - [Invocation](https://www.qemu.org/docs/master/system/invocation.html) -- 《QEMU/KVM源码解析与应用》李强,机械工业出版社 \ No newline at end of file +- 《QEMU/KVM源码解析与应用》李强,机械工业出版社 -- Gitee From 3480e282bb2c2397e705836857c3782b7e45aec3 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Thu, 13 Jul 2023 00:03:57 +0800 Subject: [PATCH 23/34] qemu-system-parameters.md: commit correct result of tinycorrect-comments Signed-off-by: jl-jiang --- articles/20230707-qemu-system-parameters.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index b063db4..f5a7ce2 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments]
> Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
@@ -90,9 +90,9 @@ struct QemuOpt { ```c enum QemuOptType { - QEMU_OPT_STRING = 0, /* no parsing (use string as-is) */ - QEMU_OPT_BOOL, /* on/off */ - QEMU_OPT_NUMBER, /* simple number */ + QEMU_OPT_STRING = 0, /* no parsing (use string as-is) */ + QEMU_OPT_BOOL, /* on/off */ + QEMU_OPT_NUMBER, /* simple number */ QEMU_OPT_SIZE, /* size, accepts (K)ilo, (M)ega, (G)iga, (T)era postfix */ }; @@ -232,7 +232,7 @@ static const QEMUOption *lookup_opt(int argc, char **argv, const char **poptarg, loc_set_cmdline(argv, optind, 1); optind++; - /* Treat --foo the same as -foo. */ + /* Treat --foo the same as -foo. */ if (r[1] == '-') r++; popt = qemu_options; -- Gitee From 230a63672970eefbc969c2dd344643555927a674 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Thu, 13 Jul 2023 00:04:20 +0800 Subject: [PATCH 24/34] qemu-system-parameters.md: commit correct result of tinycorrect-codeinline Signed-off-by: jl-jiang --- articles/20230707-qemu-system-parameters.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index f5a7ce2..2f63376 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments codeinline]
> Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
@@ -358,7 +358,7 @@ QemuOptsList *qemu_find_opts(const char *group) } ``` -`qemu_find_opts` 函数从全局数组 `vm_config_groups` 中找到刚才插入的 `-device` 选相对应的 `QemuOptsList` 并返回,而 `qemu_opts_parse_noisily` 函数只是简单调用了 `opts_parse` 函数,后者会解析出一个 `QemuOpts`,每一个大类的参数都会在相应的`QemuOptsList` 中构造 `QEMUOpts`。继续分析 `opts_parse` 函数: +`qemu_find_opts` 函数从全局数组 `vm_config_groups` 中找到刚才插入的 `-device` 选相对应的 `QemuOptsList` 并返回,而 `qemu_opts_parse_noisily` 函数只是简单调用了 `opts_parse` 函数,后者会解析出一个 `QemuOpts`,每一个大类的参数都会在相应的 `QemuOptsList` 中构造 `QEMUOpts`。继续分析 `opts_parse` 函数: ```c static QemuOpts *opts_parse(QemuOptsList *list, const char *params, @@ -388,7 +388,7 @@ static QemuOpts *opts_parse(QemuOptsList *list, const char *params, } ``` -`opts_parse` 函数代码中最重要的两行是对 `qemu_opts_create` 函数和 ` opts_do_parse` 函数的调用,前者用来创建 `QEMUopts` 并将它插入到对应的 `QemuOptsList` 上,后者则负责解析出 `QemuOpt`。函数 `opts_do_parse` 的作用是解析参数的值,如本文开头例子中的命令行参数 `-device virtio-blk-device,drive=hd0`。QEMU 的参数可能存在多种情况,比如上面的例子中 `virtio-blk-device` 表示开启一个标志,也有可能类似于 `drive=hd0` 的参数赋值语句。`opts_do_parse` 函数需要处理各种情况,并对每一个值生成一个 `QemuOpt`,关键代码如下: +`opts_parse` 函数代码中最重要的两行是对 `qemu_opts_create` 函数和 `opts_do_parse` 函数的调用,前者用来创建 `QEMUopts` 并将它插入到对应的 `QemuOptsList` 上,后者则负责解析出 `QemuOpt`。函数 `opts_do_parse` 的作用是解析参数的值,如本文开头例子中的命令行参数 `-device virtio-blk-device,drive=hd0`。QEMU 的参数可能存在多种情况,比如上面的例子中 `virtio-blk-device` 表示开启一个标志,也有可能类似于 `drive=hd0` 的参数赋值语句。`opts_do_parse` 函数需要处理各种情况,并对每一个值生成一个 `QemuOpt`,关键代码如下: ```c static bool opts_do_parse(QemuOpts *opts, const char *params, @@ -426,7 +426,7 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, } ``` -`opts_do_parse` 函数是 QEMU 参数解析过程的核心部分,它接受六个参数:`opts` 是一个 `QemuOpts` 对象,用于存储解析后的选项;`params` 是一个字符串,包含需要解析的参数;`firstname` 是第一个选项的名字;`warn_on_flag` 是一个布尔值,如果为 `true`,那么当遇到一个标志选项时,函数会打印一个警告消息;`help_wanted` 是一个指向布尔值的指针,如果函数解析到一个 `help` 选项,则设置 `*help_wanted` 为 `true`;`errp` 是一个错误指针,如果函数遇到错误,则设置 `*errp`。`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。此外,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 +`opts_do_parse` 函数是 QEMU 参数解析过程的核心部分,它接受六个参数:`opts` 是一个 `QemuOpts` 对象,用于存储解析后的选项;`params` 是一个字符串,包含需要解析的参数;`firstname` 是第一个选项的名字;`warn_on_flag` 是一个布尔值,如果为 `true`,那么当遇到一个标志选项时,函数会打印一个警告消息;`help_wanted` 是一个指向布尔值的指针,如果函数解析到一个 `help` 选项,则设置 `*help_wanted` 为 `true`;`errp` 是一个错误指针,如果函数遇到错误,则设置 `*errp`。`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。此外,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 仍然以命令行参数 `-device virtio-blk-device,drive=hd0` 为例,经过上述过程将解析出2个 `QemuOpt` 并形成如下图所示的链表: -- Gitee From 38fc131aa2ca3a0cddb997a42c21602ffc719e3f Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Thu, 13 Jul 2023 00:04:49 +0800 Subject: [PATCH 25/34] qemu-system-parameters.md: commit correct result of tinycorrect-images Signed-off-by: jl-jiang --- articles/20230707-qemu-system-parameters.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index 2f63376..7ab27f4 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments codeinline]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments codeinline images]
> Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
@@ -126,7 +126,7 @@ struct QemuOpts { 这样设计的目的在于避免发生参数混淆,QEMU 可以通过命令行指定创建两个相同的设备,这个时候这两个设备的选项都挂在 `QemuOptsList` 上,但是分别属于两个独立的 `QemuOpts`(`id` 不同),每个 `QemuOpts` 都维护自己的 `QemuOpt` 链表,成员变量 `head` 是 `QemuOpts` 下的 `QemuOpt` 链表头,成员变量 `next` 用来连接同一 `QemuOptsList` 下的其他 `QemuOpts`,具体如下图所示: -![](images/qemu-system-parameters/data_relation.svg) +![data_relation.svg](images/qemu-system-parameters/data_relation.svg) QEMU 在 `util/qemu-config.c` 中定义了一个全局的 `QemuOptsList` 数组 `vm_config_groups` 来储存所有可用的参数: @@ -319,7 +319,7 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,这里的 `machine_opts_dict` 是在参数解析第一阶段末尾初始化的 `QDict` 类型的键值对集合,用于存储终端传入的参数数组中所有与机器设置有关的参数选项。最终参数的设置落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_matchine_options` 函数和 `qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制“的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: -![](images/qemu-system-parameters/machine_parameters.svg) +![machine_parameters.svg](images/qemu-system-parameters/machine_parameters.svg) 最后,观察 `-device` 参数,同样给出分支执行代码: @@ -430,7 +430,7 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, 仍然以命令行参数 `-device virtio-blk-device,drive=hd0` 为例,经过上述过程将解析出2个 `QemuOpt` 并形成如下图所示的链表: -![](images/qemu-system-parameters/linked_list.svg) +![linked_list.svg](images/qemu-system-parameters/linked_list.svg) ## 总结 -- Gitee From 94665ab9c660f07470c8e0ea071d48eb1cdf6872 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Thu, 13 Jul 2023 00:07:06 +0800 Subject: [PATCH 26/34] qemu-system-parameters.md: commit correct result of tinycorrect-pangu Signed-off-by: jl-jiang --- articles/20230707-qemu-system-parameters.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index 7ab27f4..f246da5 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -1,8 +1,8 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments codeinline images]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments codeinline images urls refs pangu]
> Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
-> Project: RISC-V Linux 内核剖析】(https://gitee.com/tinylab/riscv-linux)
+> Project: RISC-V Linux 内核剖析』(https://gitee.com/tinylab/riscv-linux)
> Proposal: [【老师提案】QEMU 系统模拟模式分析](https://gitee.com/tinylab/riscv-linux/issues/I61KIY)
> Sponsor: PLCT Lab, ISCAS @@ -24,7 +24,7 @@ qemu-system-riscv64 -M virt -m 256M -nographic \ -append "root=/dev/vda rw console=ttyS0" ``` -- **-M:** 指定模拟器运行时的设备类型,在上面的例子中指定了 `qemu-system-riscv64` 的模拟设备为 `virt`,即RISC-V架构下的“通用虚拟平台” +- **-M:** 指定模拟器运行时的设备类型,在上面的例子中指定了 `qemu-system-riscv64` 的模拟设备为 `virt`,即 RISC-V 架构下的“通用虚拟平台” - **-m:** 指定虚拟机内部的内存大小 - **-kernel:** 指定内核镜像 - **-append:** 指定内核命令行代码 @@ -135,7 +135,7 @@ static QemuOptsList *vm_config_groups[48]; static QemuOptsList *drive_config_groups[5]; ``` -这两行代码说明了 QEMU 最多支持48个参数,5个驱动器参数。这两个全局数组由位于 `softmmu/vl.c` 文件的 `qemu_init` 函数负责初始化: +这两行代码说明了 QEMU 最多支持 48 个参数,5 个驱动器参数。这两个全局数组由位于 `softmmu/vl.c` 文件的 `qemu_init` 函数负责初始化: ```c qemu_add_opts(&qemu_drive_opts); @@ -428,7 +428,7 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, `opts_do_parse` 函数是 QEMU 参数解析过程的核心部分,它接受六个参数:`opts` 是一个 `QemuOpts` 对象,用于存储解析后的选项;`params` 是一个字符串,包含需要解析的参数;`firstname` 是第一个选项的名字;`warn_on_flag` 是一个布尔值,如果为 `true`,那么当遇到一个标志选项时,函数会打印一个警告消息;`help_wanted` 是一个指向布尔值的指针,如果函数解析到一个 `help` 选项,则设置 `*help_wanted` 为 `true`;`errp` 是一个错误指针,如果函数遇到错误,则设置 `*errp`。`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。此外,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 -仍然以命令行参数 `-device virtio-blk-device,drive=hd0` 为例,经过上述过程将解析出2个 `QemuOpt` 并形成如下图所示的链表: +仍然以命令行参数 `-device virtio-blk-device,drive=hd0` 为例,经过上述过程将解析出 2 个 `QemuOpt` 并形成如下图所示的链表: ![linked_list.svg](images/qemu-system-parameters/linked_list.svg) @@ -438,6 +438,6 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, ## 参考资料 -- [《QEMU启动方式分析(1):QEMU及RISC-V启动流程简介》](https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220816-introduction-to-qemu-and-riscv-upstream-boot-flow.md) +- [《QEMU 启动方式分析(1):QEMU 及 RISC-V 启动流程简介》](https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220816-introduction-to-qemu-and-riscv-upstream-boot-flow.md) - [Invocation](https://www.qemu.org/docs/master/system/invocation.html) -- 《QEMU/KVM源码解析与应用》李强,机械工业出版社 +- 《QEMU/KVM 源码解析与应用》李强,机械工业出版社 -- Gitee From aed9199803773ae6d987459e70210b06720d8c09 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Thu, 13 Jul 2023 00:08:03 +0800 Subject: [PATCH 27/34] qemu-system-parameters.md: commit correct result of tinycorrect-epw Signed-off-by: jl-jiang --- articles/20230707-qemu-system-parameters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index f246da5..1a09d20 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments codeinline images urls refs pangu]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments codeinline images urls refs pangu autocorrect epw]
> Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
@@ -317,7 +317,7 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 break; ``` -我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,这里的 `machine_opts_dict` 是在参数解析第一阶段末尾初始化的 `QDict` 类型的键值对集合,用于存储终端传入的参数数组中所有与机器设置有关的参数选项。最终参数的设置落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_matchine_options` 函数和 `qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制“的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: +我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,这里的 `machine_opts_dict` 是在参数解析第一阶段末尾初始化的 `QDict` 类型的键值对集合,用于存储终端传入的参数数组中所有与机器设置有关的参数选项。最终参数的设置落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_matchine_options` 函数和 `qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制”的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: ![machine_parameters.svg](images/qemu-system-parameters/machine_parameters.svg) -- Gitee From 3a43c6f58094b04452b87b8c90ac57da8aecc61b Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Thu, 13 Jul 2023 00:09:45 +0800 Subject: [PATCH 28/34] Update 20230707-qemu-system-parameters.md --- articles/20230707-qemu-system-parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index 1a09d20..2e34913 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -2,7 +2,7 @@ > Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
-> Project: RISC-V Linux 内核剖析』(https://gitee.com/tinylab/riscv-linux)
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
> Proposal: [【老师提案】QEMU 系统模拟模式分析](https://gitee.com/tinylab/riscv-linux/issues/I61KIY)
> Sponsor: PLCT Lab, ISCAS -- Gitee From bf58e83d7d89b12bdbb7b3e97af0cd9010a1873a Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Mon, 31 Jul 2023 10:42:47 +0800 Subject: [PATCH 29/34] revise article --- articles/20230707-qemu-system-parameters.md | 38 ++++++++++++++----- .../qemu-system-parameters/data_relation.svg | 2 +- .../qemu-system-parameters/linked_list.svg | 2 +- .../machine_parameters.svg | 2 +- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index 2e34913..c7a625b 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -10,11 +10,11 @@ ## 前言 -QEMU 使用命令行来配置模拟器的启动参数,本文将简要梳理 QEMU 常用参数的含义与用法,重点分析 QEMU 的参数解析机制。 +QEMU 使用命令行来配置模拟器的启动参数,本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例,简要梳理 QEMU 常用参数的含义与用法,重点分析 QEMU 的参数解析机制。 ## 常用参数 -QEMU 的全部可用参数定义于项目目录下的 `qemu-options.hx` 文件中,各选项的具体用法可以通过指令 `qemu-system-riscv64 [options] help` 查看。下面以一条常见的 QEMU 启动命令为例,分析 QEMU 常用参数的含义: +QEMU 各选项的具体用法可以通过指令 `qemu-system-riscv64 [options] help` 查看。下面以在 QEMU 中启动 RISC-V Linux 内核的命令为例,简要介绍 QEMU 常用参数的含义: ```bash qemu-system-riscv64 -M virt -m 256M -nographic \ @@ -35,7 +35,7 @@ qemu-system-riscv64 -M virt -m 256M -nographic \ ### 流程概述 -QEMU 参数解析的主体逻辑位于 `softmmu/vl.c` 文件的 `qemu_init` 函数中,主要分为两个阶段,第一阶段负责检查传入参数的合法性,并不具体解析,第二阶段执行具体的解析操作,并通过 `switch` 语句跳转不同为分支完成对应选项的设置。QEMU 在不同的抽象层次上定义了多种数据结构来对不同的参数进行描述,这些数据结构都在参数解析之前完成初始化。 +QEMU 的全部可用参数定义于项目目录下的 `qemu-options.hx` 文件中,而参数解析的主体逻辑位于 `softmmu/vl.c` 文件的 `qemu_init` 函数中,主要分为两个阶段,第一阶段负责检查传入参数的合法性,并不具体解析,第二阶段执行具体的解析操作,并通过 `switch` 语句跳转不同为分支完成对应选项的设置。QEMU 在不同的抽象层次上定义了多种数据结构来对不同的参数进行描述,这些数据结构都在参数解析之前完成初始化。 ### 数据结构及初始化 @@ -50,7 +50,7 @@ typedef struct QEMUOption { } QEMUOption; ``` -其中 `name` 表示参数名称,`flags` 表示参数属性,比如是否拥有子参数,`index` 表示指令索引,`arch_mask` 表示参数支持的架构。在 `softmmu/vl.c` 文件中还定义了一个全局 `QEMUOption` 数组 `qemu_options` 来描述 QEMU 的全部可用参数,具体如下: +其中 `name` 表示参数名称,`flags` 表示参数属性,为 1 表示拥有子参数,为 0 则表示无子参数,`index` 表示命令索引 (QEMU_OPTION_cmd),`arch_mask` 表示参数支持的架构。在 `softmmu/vl.c` 文件中还定义了一个全局 `QEMUOption` 数组 `qemu_options` 来描述 QEMU 的全部可用参数,具体如下: ```c static const QEMUOption qemu_options[] = { @@ -220,7 +220,9 @@ QEMU 参数解析的第一阶段,遍历参数数组,通过 `lookup_opt` 函 } ``` -首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,调用 `lookup_opt` 函数查询读取的参数名称,然后初始化 `machine_opts_dict` 数组,并根据实际情况加载用户配置。其中,`lookup_opt` 函数的定义如下: +首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,调用 `lookup_opt` 函数查询读取的参数名称,然后初始化 `machine_opts_dict` 数组,并根据实际情况加载用户配置。这里的 `machine_opts_dict` 是一个字典结构,主要用于存储终端传入的参数数组中的虚拟机选项和参数,包括CPU数量、内存大小、设备配置等。`machine_opts_dict` 的存在使得参数解析机制能够以一种结构化的方式管理和访问虚拟机参数,而不是使用分散的单独变量或者凌乱的数据结构。 + +下面给出 `lookup_opt` 函数的定义: ```c static const QEMUOption *lookup_opt(int argc, char **argv, const char **poptarg, int *poptind) @@ -265,6 +267,8 @@ static const QEMUOption *lookup_opt(int argc, char **argv, const char **poptarg, `lookup_opt` 函数首先调用 `loc_set_cmdline` 函数在终端传入的命令行参数中根据参数索引 `optind` 进行定位,然后通过参数名称的比较在全局数组 `qemu_options` 中寻找对应的 `QEMUOption`。同时,`lookup_opt` 函数会将参数后面的子选项保存到 `optarg` 中。 +至此,我们不难发现,第一阶段的工作并没有涉及实际的参数解析,而是完成了从命令行读取用户的配置参数并对所有参数进行有效性验证,为第二阶段的正式解析做准备。QEMU 参数解析机制的两补设计,实现了验证逻辑和执行逻辑的有效分离,减少了出错的风险,使代码逻辑更加清晰。 + ### 第二阶段 QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设置的阶段,主要逻辑如下: @@ -317,7 +321,7 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 break; ``` -我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,这里的 `machine_opts_dict` 是在参数解析第一阶段末尾初始化的 `QDict` 类型的键值对集合,用于存储终端传入的参数数组中所有与机器设置有关的参数选项。最终参数的设置落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_matchine_options` 函数和 `qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制”的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: +我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,最终参数设置的落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_machine_options` 函数和 `qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制”的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: ![machine_parameters.svg](images/qemu-system-parameters/machine_parameters.svg) @@ -388,7 +392,7 @@ static QemuOpts *opts_parse(QemuOptsList *list, const char *params, } ``` -`opts_parse` 函数代码中最重要的两行是对 `qemu_opts_create` 函数和 `opts_do_parse` 函数的调用,前者用来创建 `QEMUopts` 并将它插入到对应的 `QemuOptsList` 上,后者则负责解析出 `QemuOpt`。函数 `opts_do_parse` 的作用是解析参数的值,如本文开头例子中的命令行参数 `-device virtio-blk-device,drive=hd0`。QEMU 的参数可能存在多种情况,比如上面的例子中 `virtio-blk-device` 表示开启一个标志,也有可能类似于 `drive=hd0` 的参数赋值语句。`opts_do_parse` 函数需要处理各种情况,并对每一个值生成一个 `QemuOpt`,关键代码如下: +`opts_parse` 函数代码中最重要的两行是对 `qemu_opts_create` 函数和 `opts_do_parse` 函数的调用,前者用来创建 `QemuOpts` 并将它插入到对应的 `QemuOptsList` 上,后者则负责解析出 `QemuOpt`。函数 `opts_do_parse` 的作用是解析参数的值,如本文开头例子中的命令行参数 `-device virtio-blk-device,drive=hd0`。QEMU 的参数可能存在多种情况,比如上面的例子中 `virtio-blk-device` 表示开启一个标志,也有可能类似于 `drive=hd0` 的参数赋值语句。`opts_do_parse` 函数需要处理各种情况,并对每一个值生成一个 `QemuOpt`,关键代码如下: ```c static bool opts_do_parse(QemuOpts *opts, const char *params, @@ -426,7 +430,21 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, } ``` -`opts_do_parse` 函数是 QEMU 参数解析过程的核心部分,它接受六个参数:`opts` 是一个 `QemuOpts` 对象,用于存储解析后的选项;`params` 是一个字符串,包含需要解析的参数;`firstname` 是第一个选项的名字;`warn_on_flag` 是一个布尔值,如果为 `true`,那么当遇到一个标志选项时,函数会打印一个警告消息;`help_wanted` 是一个指向布尔值的指针,如果函数解析到一个 `help` 选项,则设置 `*help_wanted` 为 `true`;`errp` 是一个错误指针,如果函数遇到错误,则设置 `*errp`。`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。此外,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 +`opts_do_parse` 函数是 QEMU 参数解析过程的核心部分,它接受如下六个参数: + +- `opts` 是一个 `QemuOpts` 对象,用于存储解析后的选项 + +- `params` 是一个字符串,包含需要解析的参数 + +- `firstname` 是第一个选项的名字 + +- `warn_on_flag` 是一个布尔值,如果为 `true`,那么当遇到一个标志选项时,函数会打印一个警告消息 + +- `help_wanted` 是一个指向布尔值的指针,如果函数解析到一个 `help` 选项,则设置 `*help_wanted` 为 `true`;`errp` 是一个错误指针,如果函数遇到错误,则设置 `*errp` + +- `errp` 是一个错误指针,如果函数遇到错误,那么它会设置 `*errp` + +`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。注意,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 仍然以命令行参数 `-device virtio-blk-device,drive=hd0` 为例,经过上述过程将解析出 2 个 `QemuOpt` 并形成如下图所示的链表: @@ -434,10 +452,10 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, ## 总结 -本文以 QEMU 中引导 RISC-V 架构 LInux 内核启动的指令为例,总结归纳了 QEMU 常用参数的用法与含义,分析阐述了描述不同参数的各种数据结构的定义、作用以及相互关系,并按照 QEMU 的执行顺序详细梳理了数据结构初始化、参数解析第一阶段、参数解析第二阶段的代码逻辑。 +本文以 QEMU 中引导 RISC-V 架构 Linux 内核启动的指令为例,总结归纳了 QEMU 常用参数的用法与含义,分析阐述了描述不同参数的各种数据结构的定义、作用以及相互关系,并按照 QEMU 的执行顺序详细梳理了数据结构初始化、参数解析第一阶段、参数解析第二阶段的代码逻辑。 ## 参考资料 - [《QEMU 启动方式分析(1):QEMU 及 RISC-V 启动流程简介》](https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220816-introduction-to-qemu-and-riscv-upstream-boot-flow.md) -- [Invocation](https://www.qemu.org/docs/master/system/invocation.html) +- [System Emulation: Invocation](https://www.qemu.org/docs/master/system/invocation.html) - 《QEMU/KVM 源码解析与应用》李强,机械工业出版社 diff --git a/articles/images/qemu-system-parameters/data_relation.svg b/articles/images/qemu-system-parameters/data_relation.svg index 8f971a2..f3fbbc0 100644 --- a/articles/images/qemu-system-parameters/data_relation.svg +++ b/articles/images/qemu-system-parameters/data_relation.svg @@ -1,4 +1,4 @@ -QemuOptsList
const char *name;
const char *name;
const char *implied_opt_name;
const char *implied_opt_name;
bool merge_lists;  
bool merge_lists;  
QTAILQ_HEAD(, QemuOpts) head;
QTAILQ_HEAD(, QemuOpts) head;
QemuOptDesc desc[];
QemuOptDesc desc[];
QemuOpts
char *id
char *id
QemuOptsList *list
QemuOptsList *list
Location loc
Location loc
QTAILQ_HEAD(, QemuOpt) head
QTAILQ_HEAD(, QemuOpt) head
QTAILQ_ENTRY(QemuOpts) next
QTAILQ_ENTRY(QemuOpts) next
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOptDesc
QemuOptDesc
QemuOptDesc
QemuOptDesc
……
……
QemuOptDesc
QemuOptDesc
QemuOptDesc
QemuOptDesc
QemuOpt
char *name
char *name
char *str
char *str
const QemuOptDesc *desc
const QemuOptDesc *desc
union {bool boolean; uint64_t uint;} value
union {bool boolean; uint64_t uint;} value
QemuOpts     *opts
QemuOpts     *opts
QTAILQ_ENTRY(QemuOpt) next;
QTAILQ_ENTRY(QemuOpt) next;
QemuOpt
QemuOpt
QemuOpt
QemuOpt
QemuOptDesc
const char *name
const char *name
enum QemuOptType type
enum QemuOptType type
const char *help
const char *help
const char *def_value_str
const char *def_value_str
Text is not SVG - cannot display
\ No newline at end of file +QemuOptsList
const char *name;
const char *name;
const char *implied_opt_name;
const char *implied_opt_name;
bool merge_lists;  
bool merge_lists;  
QTAILQ_HEAD(, QemuOpts) head;
QTAILQ_HEAD(, QemuOpts) head;
QemuOptDesc desc[];
QemuOptDesc desc[];
QemuOpts
char *id
char *id
QemuOptsList *list
QemuOptsList *list
Location loc
Location loc
QTAILQ_HEAD(, QemuOpt) head
QTAILQ_HEAD(, QemuOpt) head
QTAILQ_ENTRY(QemuOpts) next
QTAILQ_ENTRY(QemuOpts) next
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOptDesc
QemuOptDesc
QemuOptDesc
QemuOptDesc
……
……
QemuOptDesc
QemuOptDesc
QemuOptDesc
QemuOptDesc
QemuOpt
char *name
char *name
char *str
char *str
const QemuOptDesc *desc
const QemuOptDesc *desc
union {bool boolean; uint64_t uint;} value
union {bool boolean; uint64_t uint;} value
QemuOpts *opts
QemuOpts *opts
QTAILQ_ENTRY(QemuOpt) next
QTAILQ_ENTRY(QemuOpt) next
QemuOpt
QemuOpt
QemuOpt
QemuOpt
QemuOptDesc
const char *name
const char *name
enum QemuOptType type
enum QemuOptType type
const char *help
const char *help
const char *def_value_str
const char *def_value_str
Text is not SVG - cannot display
\ No newline at end of file diff --git a/articles/images/qemu-system-parameters/linked_list.svg b/articles/images/qemu-system-parameters/linked_list.svg index b5633f1..0c3a33d 100644 --- a/articles/images/qemu-system-parameters/linked_list.svg +++ b/articles/images/qemu-system-parameters/linked_list.svg @@ -1,4 +1,4 @@ -
...
...
...
...
qemu_device_opts
qemu_device_opts
...
...
...
...
vm_config_groups
vm_config_groups
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpt
name=driver
name=driver
str=virtio-blk-device
str=virtio-blk-device
QemuOpt
name=drive
name=drive
str=hd0
str=hd0
Text is not SVG - cannot display
\ No newline at end of file +
...
...
...
...
qemu_device_opts
qemu_device_opts
...
...
...
...
vm_config_groups
vm_config_groups
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpts
QemuOpt
name=driver
name=driver
str=virtio-blk-device
str=virtio-blk-device
QemuOpt
name=drive
name=drive
str=hd0
str=hd0
Text is not SVG - cannot display
\ No newline at end of file diff --git a/articles/images/qemu-system-parameters/machine_parameters.svg b/articles/images/qemu-system-parameters/machine_parameters.svg index d238c53..5b75022 100644 --- a/articles/images/qemu-system-parameters/machine_parameters.svg +++ b/articles/images/qemu-system-parameters/machine_parameters.svg @@ -1,4 +1,4 @@ -
 qdict_new()
 qdict_new()
lookup_opt()
lookup_opt()
switch(popt->index)
switch(popt->index)
case QEMU_OPTION_kernel
case QEMU_OPTION_kernel
qdict_put_str()
qdict_put_str()
qemu_validate_options()
qemu_validate_options()
qemu_apply_legacy_machine_options()
qemu_apply_legacy_machine_options()
qemu_apply_machine_options()
qemu_apply_machine_options()
Creat QDict
Creat QDict
Decode Parameters
Decode Parameters
Apply Options
Apply Options
Text is not SVG - cannot display
\ No newline at end of file +
 qdict_new()
 qdict_new()
lookup_opt()
lookup_opt()
Create QDict
Create QDict
Decode Parameters
Decode Parameters
switch(popt->index)
switch(popt->index)
case QEMU_OPTION_kernel
case QEMU_OPTION_kernel
qdict_put_str()
qdict_put_str()
Apply Options
Apply Options
qemu_apply_legacy_machine_options()
qemu_apply_legacy_machine_options()
qemu_apply_machine_options()
qemu_apply_machine_options()
qemu_validate_options()
qemu_validate_options()
Text is not SVG - cannot display
\ No newline at end of file -- Gitee From 243b220798358179622b352d2d22669efa9d2830 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Mon, 31 Jul 2023 10:50:24 +0800 Subject: [PATCH 30/34] qemu-system-parameters.md: commit correct result of tinycorrect-spaces Signed-off-by: jl-jiang --- articles/20230707-qemu-system-parameters.md | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index c7a625b..6cb5e04 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces comments codeinline images urls refs pangu autocorrect epw]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces]
> Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
@@ -10,11 +10,11 @@ ## 前言 -QEMU 使用命令行来配置模拟器的启动参数,本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例,简要梳理 QEMU 常用参数的含义与用法,重点分析 QEMU 的参数解析机制。 +QEMU 使用命令行来配置模拟器的启动参数,本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例,简要梳理 QEMU 常用参数的含义与用法,重点分析QEMU 的参数解析机制。 ## 常用参数 -QEMU 各选项的具体用法可以通过指令 `qemu-system-riscv64 [options] help` 查看。下面以在 QEMU 中启动 RISC-V Linux 内核的命令为例,简要介绍 QEMU 常用参数的含义: +QEMU 各选项的具体用法可以通过指令 `qemu-system-riscv64 [options] help` 查看。下面以在 QEMU 中启动 RISC-V Linux 内核的命令为例,简要介绍QEMU 常用参数的含义: ```bash qemu-system-riscv64 -M virt -m 256M -nographic \ @@ -35,7 +35,7 @@ qemu-system-riscv64 -M virt -m 256M -nographic \ ### 流程概述 -QEMU 的全部可用参数定义于项目目录下的 `qemu-options.hx` 文件中,而参数解析的主体逻辑位于 `softmmu/vl.c` 文件的 `qemu_init` 函数中,主要分为两个阶段,第一阶段负责检查传入参数的合法性,并不具体解析,第二阶段执行具体的解析操作,并通过 `switch` 语句跳转不同为分支完成对应选项的设置。QEMU 在不同的抽象层次上定义了多种数据结构来对不同的参数进行描述,这些数据结构都在参数解析之前完成初始化。 +QEMU 的全部可用参数定义于项目目录下的 `qemu-options.hx` 文件中,而参数解析的主体逻辑位于`softmmu/vl.c` 文件的 `qemu_init` 函数中,主要分为两个阶段,第一阶段负责检查传入参数的合法性,并不具体解析,第二阶段执行具体的解析操作,并通过 `switch` 语句跳转不同为分支完成对应选项的设置。QEMU 在不同的抽象层次上定义了多种数据结构来对不同的参数进行描述,这些数据结构都在参数解析之前完成初始化。 ### 数据结构及初始化 @@ -50,7 +50,7 @@ typedef struct QEMUOption { } QEMUOption; ``` -其中 `name` 表示参数名称,`flags` 表示参数属性,为 1 表示拥有子参数,为 0 则表示无子参数,`index` 表示命令索引 (QEMU_OPTION_cmd),`arch_mask` 表示参数支持的架构。在 `softmmu/vl.c` 文件中还定义了一个全局 `QEMUOption` 数组 `qemu_options` 来描述 QEMU 的全部可用参数,具体如下: +其中 `name` 表示参数名称,`flags` 表示参数属性,为 1 表示拥有子参数,为 0 则表示无子参数,`index` 表示命令索引 (QEMU_OPTION_cmd),`arch_mask` 表示参数支持的架构。在 `softmmu/vl.c` 文件中还定义了一个全局`QEMUOption` 数组 `qemu_options` 来描述 QEMU 的全部可用参数,具体如下: ```c static const QEMUOption qemu_options[] = { @@ -112,7 +112,7 @@ struct QemuOptsList { }; ``` -上述代码中,首先定义了枚举类型 `QemuOptType` 用于描述不同的参数类型,包括字符串、布尔值、数字和空间大小四种。接着定义了结构体 `QemuOptDesc` 用于存放对参数的描述信息,包括名称、类型、帮助信息和参数默认值。最后定义了结构体 `QemuOptsList`,每一个 `QemuOptsList` 结构体就对应了一个参数。但是,这里需要特别注意的是,`QemuOptsList` 并不直接与 `QemuOpt` 联系,中间需要通过结构体 `QemuOpts` 进行转接: +上述代码中,首先定义了枚举类型 `QemuOptType` 用于描述不同的参数类型,包括字符串、布尔值、数字和空间大小四种。接着定义了结构体 `QemuOptDesc` 用于存放对参数的描述信息,包括名称、类型、帮助信息和参数默认值。最后定义了结构体 `QemuOptsList`,每一个 `QemuOptsList` 结构体就对应了一个参数。但是,这里需要特别注意的是,`QemuOptsList` 并不直接与`QemuOpt` 联系,中间需要通过结构体 `QemuOpts` 进行转接: ```c struct QemuOpts { @@ -124,7 +124,7 @@ struct QemuOpts { }; ``` -这样设计的目的在于避免发生参数混淆,QEMU 可以通过命令行指定创建两个相同的设备,这个时候这两个设备的选项都挂在 `QemuOptsList` 上,但是分别属于两个独立的 `QemuOpts`(`id` 不同),每个 `QemuOpts` 都维护自己的 `QemuOpt` 链表,成员变量 `head` 是 `QemuOpts` 下的 `QemuOpt` 链表头,成员变量 `next` 用来连接同一 `QemuOptsList` 下的其他 `QemuOpts`,具体如下图所示: +这样设计的目的在于避免发生参数混淆,QEMU 可以通过命令行指定创建两个相同的设备,这个时候这两个设备的选项都挂在 `QemuOptsList` 上,但是分别属于两个独立的 `QemuOpts`(`id` 不同),每个 `QemuOpts` 都维护自己的 `QemuOpt` 链表,成员变量`head` 是 `QemuOpts` 下的 `QemuOpt` 链表头,成员变量`next` 用来连接同一`QemuOptsList` 下的其他`QemuOpts`,具体如下图所示: ![data_relation.svg](images/qemu-system-parameters/data_relation.svg) @@ -135,7 +135,7 @@ static QemuOptsList *vm_config_groups[48]; static QemuOptsList *drive_config_groups[5]; ``` -这两行代码说明了 QEMU 最多支持 48 个参数,5 个驱动器参数。这两个全局数组由位于 `softmmu/vl.c` 文件的 `qemu_init` 函数负责初始化: +这两行代码说明了 QEMU 最多支持48 个参数,5 个驱动器参数。这两个全局数组由位于`softmmu/vl.c` 文件的 `qemu_init` 函数负责初始化: ```c qemu_add_opts(&qemu_drive_opts); @@ -171,7 +171,7 @@ static QemuOptsList *drive_config_groups[5]; qemu_add_opts(&qemu_action_opts); ``` -其中,`qemu_add_opts` 函数的实现位于 `util/qemu-config.c` 文件中,该函数主要负责将参数中传入的 `OemuOptsList` 添加到全局数组 `vm_config_groups` 中: +其中,`qemu_add_opts` 函数的实现位于`util/qemu-config.c` 文件中,该函数主要负责将参数中传入的 `OemuOptsList` 添加到全局数组 `vm_config_groups` 中: ```c void qemu_add_opts(QemuOptsList *list) @@ -220,7 +220,7 @@ QEMU 参数解析的第一阶段,遍历参数数组,通过 `lookup_opt` 函 } ``` -首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,调用 `lookup_opt` 函数查询读取的参数名称,然后初始化 `machine_opts_dict` 数组,并根据实际情况加载用户配置。这里的 `machine_opts_dict` 是一个字典结构,主要用于存储终端传入的参数数组中的虚拟机选项和参数,包括CPU数量、内存大小、设备配置等。`machine_opts_dict` 的存在使得参数解析机制能够以一种结构化的方式管理和访问虚拟机参数,而不是使用分散的单独变量或者凌乱的数据结构。 +首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,调用 `lookup_opt` 函数查询读取的参数名称,然后初始化`machine_opts_dict` 数组,并根据实际情况加载用户配置。这里的 `machine_opts_dict` 是一个字典结构,主要用于存储终端传入的参数数组中的虚拟机选项和参数,包括CPU数量、内存大小、设备配置等。`machine_opts_dict` 的存在使得参数解析机制能够以一种结构化的方式管理和访问虚拟机参数,而不是使用分散的单独变量或者凌乱的数据结构。 下面给出 `lookup_opt` 函数的定义: @@ -302,7 +302,7 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 } ``` -第二阶段的逻辑就是按照下标顺序依次遍历终端传入的参数数组,调用 `lookup_opt` 函数找到对应的 `QEMUOption`,然后检查对应选项在当前架构下是否支持,最后使用 `switch` 语句根据 `QEMUOption` 的成员变量 `index` 的不同来执行不同的分支完成具体的设置。 +第二阶段的逻辑就是按照下标顺序依次遍历终端传入的参数数组,调用 `lookup_opt` 函数找到对应的 `QEMUOption`,然后检查对应选项在当前架构下是否支持,最后使用 `switch` 语句根据 `QEMUOption` 的成员变量`index` 的不同来执行不同的分支完成具体的设置。 下面将以不同的参数为例,详细说明参数解析第二阶段 `switch` 语句中的分支执行流程。首先关注最为简单的 `-version` 参数,其含义就是打印 QEMU 版本信息,然后退出程序,有关代码如下: @@ -313,7 +313,7 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 break; ``` -可以看到,由于其含义最为简单,因此解析实现也非常简洁,直接调用 `version` 函数打印版本信息,随后退出程序。接着分析 `-kernel` 参数,通过对 `-kernel` 参数解析流程的考察,我们可以一窥与机器设置有关的参数的解析机制。首先给出 `-kernel` 参数在解析第二阶段分支执行的主要代码: +可以看到,由于其含义最为简单,因此解析实现也非常简洁,直接调用 `version` 函数打印版本信息,随后退出程序。接着分析`-kernel` 参数,通过对 `-kernel` 参数解析流程的考察,我们可以一窥与机器设置有关的参数的解析机制。首先给出 `-kernel` 参数在解析第二阶段分支执行的主要代码: ```c case QEMU_OPTION_kernel: @@ -321,11 +321,11 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 break; ``` -我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,最终参数设置的落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_machine_options` 函数和 `qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制”的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: +我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,最终参数设置的落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_machine_options` 函数和`qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制”的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: ![machine_parameters.svg](images/qemu-system-parameters/machine_parameters.svg) -最后,观察 `-device` 参数,同样给出分支执行代码: +最后,观察`-device` 参数,同样给出分支执行代码: ```c case QEMU_OPTION_device: @@ -345,7 +345,7 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 break; ``` -分支执行的主要逻辑如下:首先检查 `optarg`(`-device` 选项的参数)是否以 `{` 开始。如果是,说明 `optarg` 是一个 JSON 对象,那么就需要调用 `qobject_from_json` 函数将 JSON 对象转换为一个 `QObject` 对象。然后,创建一个新的 `DeviceOption` 对象,并将 `QObject` 对象转换为一个 `QDict` 并存储在 `DeviceOption` 中,最后将 `DeviceOption` 添加到 `device_opts` 队列中。如果 `optarg` 不是一个 JSON 对象,那么就调用 `qemu_opts_parse_noisily` 函数将 `optarg` 解析为一个 `QemuOpts` 对象,并将这个对象添加到 "device" 选项的列表中。如果解析失败,则退出程序。下面我们重点关注 `else` 分支的部分,首先给出 `qemu_find_opts` 函数的定义,它位于 `util/qemu-config.c` 文件中: +分支执行的主要逻辑如下:首先检查 `optarg`(`-device` 选项的参数)是否以 `{` 开始。如果是,说明`optarg` 是一个 JSON 对象,那么就需要调用 `qobject_from_json` 函数将 JSON 对象转换为一个 `QObject` 对象。然后,创建一个新的 `DeviceOption` 对象,并将 `QObject` 对象转换为一个 `QDict` 并存储在 `DeviceOption` 中,最后将 `DeviceOption` 添加到 `device_opts` 队列中。如果 `optarg` 不是一个 JSON 对象,那么就调用 `qemu_opts_parse_noisily` 函数将 `optarg` 解析为一个 `QemuOpts` 对象,并将这个对象添加到 "device" 选项的列表中。如果解析失败,则退出程序。下面我们重点关注 `else` 分支的部分,首先给出 `qemu_find_opts` 函数的定义,它位于`util/qemu-config.c` 文件中: ```c QemuOptsList *qemu_find_opts(const char *group) @@ -362,7 +362,7 @@ QemuOptsList *qemu_find_opts(const char *group) } ``` -`qemu_find_opts` 函数从全局数组 `vm_config_groups` 中找到刚才插入的 `-device` 选相对应的 `QemuOptsList` 并返回,而 `qemu_opts_parse_noisily` 函数只是简单调用了 `opts_parse` 函数,后者会解析出一个 `QemuOpts`,每一个大类的参数都会在相应的 `QemuOptsList` 中构造 `QEMUOpts`。继续分析 `opts_parse` 函数: +`qemu_find_opts` 函数从全局数组 `vm_config_groups` 中找到刚才插入的 `-device` 选相对应的 `QemuOptsList` 并返回,而`qemu_opts_parse_noisily` 函数只是简单调用了 `opts_parse` 函数,后者会解析出一个 `QemuOpts`,每一个大类的参数都会在相应的 `QemuOptsList` 中构造 `QEMUOpts`。继续分析`opts_parse` 函数: ```c static QemuOpts *opts_parse(QemuOptsList *list, const char *params, @@ -392,7 +392,7 @@ static QemuOpts *opts_parse(QemuOptsList *list, const char *params, } ``` -`opts_parse` 函数代码中最重要的两行是对 `qemu_opts_create` 函数和 `opts_do_parse` 函数的调用,前者用来创建 `QemuOpts` 并将它插入到对应的 `QemuOptsList` 上,后者则负责解析出 `QemuOpt`。函数 `opts_do_parse` 的作用是解析参数的值,如本文开头例子中的命令行参数 `-device virtio-blk-device,drive=hd0`。QEMU 的参数可能存在多种情况,比如上面的例子中 `virtio-blk-device` 表示开启一个标志,也有可能类似于 `drive=hd0` 的参数赋值语句。`opts_do_parse` 函数需要处理各种情况,并对每一个值生成一个 `QemuOpt`,关键代码如下: +`opts_parse` 函数代码中最重要的两行是对 `qemu_opts_create` 函数和`opts_do_parse` 函数的调用,前者用来创建 `QemuOpts` 并将它插入到对应的 `QemuOptsList` 上,后者则负责解析出 `QemuOpt`。函数 `opts_do_parse` 的作用是解析参数的值,如本文开头例子中的命令行参数 `-device virtio-blk-device,drive=hd0`。QEMU 的参数可能存在多种情况,比如上面的例子中 `virtio-blk-device` 表示开启一个标志,也有可能类似于`drive=hd0` 的参数赋值语句。`opts_do_parse` 函数需要处理各种情况,并对每一个值生成一个 `QemuOpt`,关键代码如下: ```c static bool opts_do_parse(QemuOpts *opts, const char *params, @@ -444,7 +444,7 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, - `errp` 是一个错误指针,如果函数遇到错误,那么它会设置 `*errp` -`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。注意,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 +`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析`params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。注意,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 仍然以命令行参数 `-device virtio-blk-device,drive=hd0` 为例,经过上述过程将解析出 2 个 `QemuOpt` 并形成如下图所示的链表: @@ -452,7 +452,7 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, ## 总结 -本文以 QEMU 中引导 RISC-V 架构 Linux 内核启动的指令为例,总结归纳了 QEMU 常用参数的用法与含义,分析阐述了描述不同参数的各种数据结构的定义、作用以及相互关系,并按照 QEMU 的执行顺序详细梳理了数据结构初始化、参数解析第一阶段、参数解析第二阶段的代码逻辑。 +本文以 QEMU 中引导RISC-V 架构 Linux 内核启动的指令为例,总结归纳了 QEMU 常用参数的用法与含义,分析阐述了描述不同参数的各种数据结构的定义、作用以及相互关系,并按照 QEMU 的执行顺序详细梳理了数据结构初始化、参数解析第一阶段、参数解析第二阶段的代码逻辑。 ## 参考资料 -- Gitee From d3c5d2ad066a4fdc21656f50adf05305abe5de23 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Mon, 31 Jul 2023 11:00:35 +0800 Subject: [PATCH 31/34] qemu-system-parameters.md: commit correct result of tinycorrect-codeinline Signed-off-by: jl-jiang --- articles/20230707-qemu-system-parameters.md | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index 6cb5e04..da4414a 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline]
> Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
@@ -35,7 +35,7 @@ qemu-system-riscv64 -M virt -m 256M -nographic \ ### 流程概述 -QEMU 的全部可用参数定义于项目目录下的 `qemu-options.hx` 文件中,而参数解析的主体逻辑位于`softmmu/vl.c` 文件的 `qemu_init` 函数中,主要分为两个阶段,第一阶段负责检查传入参数的合法性,并不具体解析,第二阶段执行具体的解析操作,并通过 `switch` 语句跳转不同为分支完成对应选项的设置。QEMU 在不同的抽象层次上定义了多种数据结构来对不同的参数进行描述,这些数据结构都在参数解析之前完成初始化。 +QEMU 的全部可用参数定义于项目目录下的 `qemu-options.hx` 文件中,而参数解析的主体逻辑位于 `softmmu/vl.c` 文件的 `qemu_init` 函数中,主要分为两个阶段,第一阶段负责检查传入参数的合法性,并不具体解析,第二阶段执行具体的解析操作,并通过 `switch` 语句跳转不同为分支完成对应选项的设置。QEMU 在不同的抽象层次上定义了多种数据结构来对不同的参数进行描述,这些数据结构都在参数解析之前完成初始化。 ### 数据结构及初始化 @@ -50,7 +50,7 @@ typedef struct QEMUOption { } QEMUOption; ``` -其中 `name` 表示参数名称,`flags` 表示参数属性,为 1 表示拥有子参数,为 0 则表示无子参数,`index` 表示命令索引 (QEMU_OPTION_cmd),`arch_mask` 表示参数支持的架构。在 `softmmu/vl.c` 文件中还定义了一个全局`QEMUOption` 数组 `qemu_options` 来描述 QEMU 的全部可用参数,具体如下: +其中 `name` 表示参数名称,`flags` 表示参数属性,为 1 表示拥有子参数,为 0 则表示无子参数,`index` 表示命令索引 (QEMU_OPTION_cmd),`arch_mask` 表示参数支持的架构。在 `softmmu/vl.c` 文件中还定义了一个全局 `QEMUOption` 数组 `qemu_options` 来描述 QEMU 的全部可用参数,具体如下: ```c static const QEMUOption qemu_options[] = { @@ -112,7 +112,7 @@ struct QemuOptsList { }; ``` -上述代码中,首先定义了枚举类型 `QemuOptType` 用于描述不同的参数类型,包括字符串、布尔值、数字和空间大小四种。接着定义了结构体 `QemuOptDesc` 用于存放对参数的描述信息,包括名称、类型、帮助信息和参数默认值。最后定义了结构体 `QemuOptsList`,每一个 `QemuOptsList` 结构体就对应了一个参数。但是,这里需要特别注意的是,`QemuOptsList` 并不直接与`QemuOpt` 联系,中间需要通过结构体 `QemuOpts` 进行转接: +上述代码中,首先定义了枚举类型 `QemuOptType` 用于描述不同的参数类型,包括字符串、布尔值、数字和空间大小四种。接着定义了结构体 `QemuOptDesc` 用于存放对参数的描述信息,包括名称、类型、帮助信息和参数默认值。最后定义了结构体 `QemuOptsList`,每一个 `QemuOptsList` 结构体就对应了一个参数。但是,这里需要特别注意的是,`QemuOptsList` 并不直接与 `QemuOpt` 联系,中间需要通过结构体 `QemuOpts` 进行转接: ```c struct QemuOpts { @@ -124,7 +124,7 @@ struct QemuOpts { }; ``` -这样设计的目的在于避免发生参数混淆,QEMU 可以通过命令行指定创建两个相同的设备,这个时候这两个设备的选项都挂在 `QemuOptsList` 上,但是分别属于两个独立的 `QemuOpts`(`id` 不同),每个 `QemuOpts` 都维护自己的 `QemuOpt` 链表,成员变量`head` 是 `QemuOpts` 下的 `QemuOpt` 链表头,成员变量`next` 用来连接同一`QemuOptsList` 下的其他`QemuOpts`,具体如下图所示: +这样设计的目的在于避免发生参数混淆,QEMU 可以通过命令行指定创建两个相同的设备,这个时候这两个设备的选项都挂在 `QemuOptsList` 上,但是分别属于两个独立的 `QemuOpts`(`id` 不同),每个 `QemuOpts` 都维护自己的 `QemuOpt` 链表,成员变量 `head` 是 `QemuOpts` 下的 `QemuOpt` 链表头,成员变量 `next` 用来连接同一 `QemuOptsList` 下的其他 `QemuOpts`,具体如下图所示: ![data_relation.svg](images/qemu-system-parameters/data_relation.svg) @@ -135,7 +135,7 @@ static QemuOptsList *vm_config_groups[48]; static QemuOptsList *drive_config_groups[5]; ``` -这两行代码说明了 QEMU 最多支持48 个参数,5 个驱动器参数。这两个全局数组由位于`softmmu/vl.c` 文件的 `qemu_init` 函数负责初始化: +这两行代码说明了 QEMU 最多支持48 个参数,5 个驱动器参数。这两个全局数组由位于 `softmmu/vl.c` 文件的 `qemu_init` 函数负责初始化: ```c qemu_add_opts(&qemu_drive_opts); @@ -171,7 +171,7 @@ static QemuOptsList *drive_config_groups[5]; qemu_add_opts(&qemu_action_opts); ``` -其中,`qemu_add_opts` 函数的实现位于`util/qemu-config.c` 文件中,该函数主要负责将参数中传入的 `OemuOptsList` 添加到全局数组 `vm_config_groups` 中: +其中,`qemu_add_opts` 函数的实现位于 `util/qemu-config.c` 文件中,该函数主要负责将参数中传入的 `OemuOptsList` 添加到全局数组 `vm_config_groups` 中: ```c void qemu_add_opts(QemuOptsList *list) @@ -220,7 +220,7 @@ QEMU 参数解析的第一阶段,遍历参数数组,通过 `lookup_opt` 函 } ``` -首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,调用 `lookup_opt` 函数查询读取的参数名称,然后初始化`machine_opts_dict` 数组,并根据实际情况加载用户配置。这里的 `machine_opts_dict` 是一个字典结构,主要用于存储终端传入的参数数组中的虚拟机选项和参数,包括CPU数量、内存大小、设备配置等。`machine_opts_dict` 的存在使得参数解析机制能够以一种结构化的方式管理和访问虚拟机参数,而不是使用分散的单独变量或者凌乱的数据结构。 +首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,调用 `lookup_opt` 函数查询读取的参数名称,然后初始化 `machine_opts_dict` 数组,并根据实际情况加载用户配置。这里的 `machine_opts_dict` 是一个字典结构,主要用于存储终端传入的参数数组中的虚拟机选项和参数,包括CPU数量、内存大小、设备配置等。`machine_opts_dict` 的存在使得参数解析机制能够以一种结构化的方式管理和访问虚拟机参数,而不是使用分散的单独变量或者凌乱的数据结构。 下面给出 `lookup_opt` 函数的定义: @@ -302,7 +302,7 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 } ``` -第二阶段的逻辑就是按照下标顺序依次遍历终端传入的参数数组,调用 `lookup_opt` 函数找到对应的 `QEMUOption`,然后检查对应选项在当前架构下是否支持,最后使用 `switch` 语句根据 `QEMUOption` 的成员变量`index` 的不同来执行不同的分支完成具体的设置。 +第二阶段的逻辑就是按照下标顺序依次遍历终端传入的参数数组,调用 `lookup_opt` 函数找到对应的 `QEMUOption`,然后检查对应选项在当前架构下是否支持,最后使用 `switch` 语句根据 `QEMUOption` 的成员变量 `index` 的不同来执行不同的分支完成具体的设置。 下面将以不同的参数为例,详细说明参数解析第二阶段 `switch` 语句中的分支执行流程。首先关注最为简单的 `-version` 参数,其含义就是打印 QEMU 版本信息,然后退出程序,有关代码如下: @@ -313,7 +313,7 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 break; ``` -可以看到,由于其含义最为简单,因此解析实现也非常简洁,直接调用 `version` 函数打印版本信息,随后退出程序。接着分析`-kernel` 参数,通过对 `-kernel` 参数解析流程的考察,我们可以一窥与机器设置有关的参数的解析机制。首先给出 `-kernel` 参数在解析第二阶段分支执行的主要代码: +可以看到,由于其含义最为简单,因此解析实现也非常简洁,直接调用 `version` 函数打印版本信息,随后退出程序。接着分析 `-kernel` 参数,通过对 `-kernel` 参数解析流程的考察,我们可以一窥与机器设置有关的参数的解析机制。首先给出 `-kernel` 参数在解析第二阶段分支执行的主要代码: ```c case QEMU_OPTION_kernel: @@ -321,11 +321,11 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 break; ``` -我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,最终参数设置的落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_machine_options` 函数和`qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制”的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: +我们可以发现,与机器设置有关的参数在整个解析过程中并不会真正执行具体设置,而是将对应的参数选项添加到 `machine_opts_dict` 中,最终参数设置的落实工作由机器创建及初始化阶段的 `qemu_apply_legacy_machine_options` 函数和 `qemu_apply_machine_options` 函数完成,这一部分将在“QEMU 机器创建及初始化机制”的分析文章中具体阐述,本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程: ![machine_parameters.svg](images/qemu-system-parameters/machine_parameters.svg) -最后,观察`-device` 参数,同样给出分支执行代码: +最后,观察 `-device` 参数,同样给出分支执行代码: ```c case QEMU_OPTION_device: @@ -345,7 +345,7 @@ QEMU 参数解析的第二阶段,是真正解析具体参数并执行相应设 break; ``` -分支执行的主要逻辑如下:首先检查 `optarg`(`-device` 选项的参数)是否以 `{` 开始。如果是,说明`optarg` 是一个 JSON 对象,那么就需要调用 `qobject_from_json` 函数将 JSON 对象转换为一个 `QObject` 对象。然后,创建一个新的 `DeviceOption` 对象,并将 `QObject` 对象转换为一个 `QDict` 并存储在 `DeviceOption` 中,最后将 `DeviceOption` 添加到 `device_opts` 队列中。如果 `optarg` 不是一个 JSON 对象,那么就调用 `qemu_opts_parse_noisily` 函数将 `optarg` 解析为一个 `QemuOpts` 对象,并将这个对象添加到 "device" 选项的列表中。如果解析失败,则退出程序。下面我们重点关注 `else` 分支的部分,首先给出 `qemu_find_opts` 函数的定义,它位于`util/qemu-config.c` 文件中: +分支执行的主要逻辑如下:首先检查 `optarg`(`-device` 选项的参数)是否以 `{` 开始。如果是,说明 `optarg` 是一个 JSON 对象,那么就需要调用 `qobject_from_json` 函数将 JSON 对象转换为一个 `QObject` 对象。然后,创建一个新的 `DeviceOption` 对象,并将 `QObject` 对象转换为一个 `QDict` 并存储在 `DeviceOption` 中,最后将 `DeviceOption` 添加到 `device_opts` 队列中。如果 `optarg` 不是一个 JSON 对象,那么就调用 `qemu_opts_parse_noisily` 函数将 `optarg` 解析为一个 `QemuOpts` 对象,并将这个对象添加到 "device" 选项的列表中。如果解析失败,则退出程序。下面我们重点关注 `else` 分支的部分,首先给出 `qemu_find_opts` 函数的定义,它位于 `util/qemu-config.c` 文件中: ```c QemuOptsList *qemu_find_opts(const char *group) @@ -362,7 +362,7 @@ QemuOptsList *qemu_find_opts(const char *group) } ``` -`qemu_find_opts` 函数从全局数组 `vm_config_groups` 中找到刚才插入的 `-device` 选相对应的 `QemuOptsList` 并返回,而`qemu_opts_parse_noisily` 函数只是简单调用了 `opts_parse` 函数,后者会解析出一个 `QemuOpts`,每一个大类的参数都会在相应的 `QemuOptsList` 中构造 `QEMUOpts`。继续分析`opts_parse` 函数: +`qemu_find_opts` 函数从全局数组 `vm_config_groups` 中找到刚才插入的 `-device` 选相对应的 `QemuOptsList` 并返回,而 `qemu_opts_parse_noisily` 函数只是简单调用了 `opts_parse` 函数,后者会解析出一个 `QemuOpts`,每一个大类的参数都会在相应的 `QemuOptsList` 中构造 `QEMUOpts`。继续分析 `opts_parse` 函数: ```c static QemuOpts *opts_parse(QemuOptsList *list, const char *params, @@ -392,7 +392,7 @@ static QemuOpts *opts_parse(QemuOptsList *list, const char *params, } ``` -`opts_parse` 函数代码中最重要的两行是对 `qemu_opts_create` 函数和`opts_do_parse` 函数的调用,前者用来创建 `QemuOpts` 并将它插入到对应的 `QemuOptsList` 上,后者则负责解析出 `QemuOpt`。函数 `opts_do_parse` 的作用是解析参数的值,如本文开头例子中的命令行参数 `-device virtio-blk-device,drive=hd0`。QEMU 的参数可能存在多种情况,比如上面的例子中 `virtio-blk-device` 表示开启一个标志,也有可能类似于`drive=hd0` 的参数赋值语句。`opts_do_parse` 函数需要处理各种情况,并对每一个值生成一个 `QemuOpt`,关键代码如下: +`opts_parse` 函数代码中最重要的两行是对 `qemu_opts_create` 函数和 `opts_do_parse` 函数的调用,前者用来创建 `QemuOpts` 并将它插入到对应的 `QemuOptsList` 上,后者则负责解析出 `QemuOpt`。函数 `opts_do_parse` 的作用是解析参数的值,如本文开头例子中的命令行参数 `-device virtio-blk-device,drive=hd0`。QEMU 的参数可能存在多种情况,比如上面的例子中 `virtio-blk-device` 表示开启一个标志,也有可能类似于 `drive=hd0` 的参数赋值语句。`opts_do_parse` 函数需要处理各种情况,并对每一个值生成一个 `QemuOpt`,关键代码如下: ```c static bool opts_do_parse(QemuOpts *opts, const char *params, @@ -444,7 +444,7 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, - `errp` 是一个错误指针,如果函数遇到错误,那么它会设置 `*errp` -`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析`params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。注意,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 +`opts_do_parse` 函数使用一个 `for` 循环一次遍历解析 `params` 中的每个选项:首先调用 `get_opt_name_value` 函数来获取下一个选项的名字和值,然后创建一个新的 `QemuOpt` 对象并添加到 `opts` 中,如果解析到一个 `help` 选项,则会设置 `*help_wanted` 为 `true`,并立即返回 `false`。注意,如果解析到的选项的名称是 `id`,则会跳过这个选项,因为参数 `id` 是用于设置 `QemuOpts` 对象的 ID 的,不能当作普通参数解析为 `QemuOpt`。最后,调用 `opt_validate` 函数来验证新创建的 `QemuOpt` 的有效性并返回解析结果。 仍然以命令行参数 `-device virtio-blk-device,drive=hd0` 为例,经过上述过程将解析出 2 个 `QemuOpt` 并形成如下图所示的链表: -- Gitee From ed004eb66f597ffb80d94186b553ae4633b136a6 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Mon, 31 Jul 2023 11:06:33 +0800 Subject: [PATCH 32/34] fix typo --- articles/20230707-qemu-system-parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index da4414a..83b01d0 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -267,7 +267,7 @@ static const QEMUOption *lookup_opt(int argc, char **argv, const char **poptarg, `lookup_opt` 函数首先调用 `loc_set_cmdline` 函数在终端传入的命令行参数中根据参数索引 `optind` 进行定位,然后通过参数名称的比较在全局数组 `qemu_options` 中寻找对应的 `QEMUOption`。同时,`lookup_opt` 函数会将参数后面的子选项保存到 `optarg` 中。 -至此,我们不难发现,第一阶段的工作并没有涉及实际的参数解析,而是完成了从命令行读取用户的配置参数并对所有参数进行有效性验证,为第二阶段的正式解析做准备。QEMU 参数解析机制的两补设计,实现了验证逻辑和执行逻辑的有效分离,减少了出错的风险,使代码逻辑更加清晰。 +至此,我们不难发现,第一阶段的工作并没有涉及实际的参数解析,而是完成了从命令行读取用户的配置参数并对所有参数进行有效性验证,为第二阶段的正式解析做准备。QEMU 参数解析机制的两步设计,实现了验证逻辑和执行逻辑的有效分离,减少了出错的风险,使代码逻辑更加清晰。 ### 第二阶段 -- Gitee From 6061d075cb3b94d48907331d3372632e22045286 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Mon, 31 Jul 2023 13:17:00 +0800 Subject: [PATCH 33/34] fix typos --- articles/20230707-qemu-system-parameters.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index 83b01d0..6e6debe 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -135,7 +135,7 @@ static QemuOptsList *vm_config_groups[48]; static QemuOptsList *drive_config_groups[5]; ``` -这两行代码说明了 QEMU 最多支持48 个参数,5 个驱动器参数。这两个全局数组由位于 `softmmu/vl.c` 文件的 `qemu_init` 函数负责初始化: +这两行代码说明了 QEMU 最多支持 48 个参数,5 个驱动器参数。这两个全局数组由位于 `softmmu/vl.c` 文件的 `qemu_init` 函数负责初始化: ```c qemu_add_opts(&qemu_drive_opts); @@ -220,7 +220,7 @@ QEMU 参数解析的第一阶段,遍历参数数组,通过 `lookup_opt` 函 } ``` -首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,调用 `lookup_opt` 函数查询读取的参数名称,然后初始化 `machine_opts_dict` 数组,并根据实际情况加载用户配置。这里的 `machine_opts_dict` 是一个字典结构,主要用于存储终端传入的参数数组中的虚拟机选项和参数,包括CPU数量、内存大小、设备配置等。`machine_opts_dict` 的存在使得参数解析机制能够以一种结构化的方式管理和访问虚拟机参数,而不是使用分散的单独变量或者凌乱的数据结构。 +首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,调用 `lookup_opt` 函数查询读取的参数名称,然后初始化 `machine_opts_dict` 数组,并根据实际情况加载用户配置。这里的 `machine_opts_dict` 是一个字典结构,主要用于存储终端传入的参数数组中的虚拟机选项和参数,包括 CPU 数量、内存大小、设备配置等。`machine_opts_dict` 的存在使得参数解析机制能够以一种结构化的方式管理和访问虚拟机参数,而不是使用分散的单独变量或者凌乱的数据结构。 下面给出 `lookup_opt` 函数的定义: @@ -452,7 +452,7 @@ static bool opts_do_parse(QemuOpts *opts, const char *params, ## 总结 -本文以 QEMU 中引导RISC-V 架构 Linux 内核启动的指令为例,总结归纳了 QEMU 常用参数的用法与含义,分析阐述了描述不同参数的各种数据结构的定义、作用以及相互关系,并按照 QEMU 的执行顺序详细梳理了数据结构初始化、参数解析第一阶段、参数解析第二阶段的代码逻辑。 +本文以 QEMU 中引导 RISC-V 架构 Linux 内核启动的指令为例,总结归纳了 QEMU 常用参数的用法与含义,分析阐述了描述不同参数的各种数据结构的定义、作用以及相互关系,并按照 QEMU 的执行顺序详细梳理了数据结构初始化、参数解析第一阶段、参数解析第二阶段的代码逻辑。 ## 参考资料 -- Gitee From ffe998d0927909440949f34d89595d6665ee1bc3 Mon Sep 17 00:00:00 2001 From: jl-jiang Date: Mon, 31 Jul 2023 13:27:31 +0800 Subject: [PATCH 34/34] qemu-system-parameters.md: commit correct result of tinycorrect-pangu Signed-off-by: jl-jiang --- articles/20230707-qemu-system-parameters.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/articles/20230707-qemu-system-parameters.md b/articles/20230707-qemu-system-parameters.md index 6e6debe..b52154d 100644 --- a/articles/20230707-qemu-system-parameters.md +++ b/articles/20230707-qemu-system-parameters.md @@ -1,4 +1,4 @@ -> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [codeinline]
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [urls refs pangu]
> Author: jl-jiang
> Date: 2023/07/07
> Revisor: Bin Meng
@@ -10,11 +10,11 @@ ## 前言 -QEMU 使用命令行来配置模拟器的启动参数,本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例,简要梳理 QEMU 常用参数的含义与用法,重点分析QEMU 的参数解析机制。 +QEMU 使用命令行来配置模拟器的启动参数,本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例,简要梳理 QEMU 常用参数的含义与用法,重点分析 QEMU 的参数解析机制。 ## 常用参数 -QEMU 各选项的具体用法可以通过指令 `qemu-system-riscv64 [options] help` 查看。下面以在 QEMU 中启动 RISC-V Linux 内核的命令为例,简要介绍QEMU 常用参数的含义: +QEMU 各选项的具体用法可以通过指令 `qemu-system-riscv64 [options] help` 查看。下面以在 QEMU 中启动 RISC-V Linux 内核的命令为例,简要介绍 QEMU 常用参数的含义: ```bash qemu-system-riscv64 -M virt -m 256M -nographic \ -- Gitee