在qemu里增加指令的前端解码 | 您所在的位置:网站首页 › 用decode造句 › 在qemu里增加指令的前端解码 |
qemu基础逻辑 qemu虚拟机提供两种CPU实现的方式,一种是基于中间码的实现,一种是基于KVM的实现。 第一种方式,我们一般叫tcg(tiny code generator),这种方式的基本思路是用纯软件的 方式把target CPU的指令先翻译成所谓的中间码,然后再把中间码翻译成host CPU 的指令, 我们把target CPU指令翻译成中间码的过程叫整个过程的前端,中间码翻译成host CPU的 过程对应的叫做后端。给qemu增加一个新CPU的模型需要既增加前端也增加后端,如果要把 整个系统支持起来,还要增加基础设备以及user mode的支持,整个系统的支持的逻辑不在 本文档里展开,本文只聚焦于前端的相关逻辑。如果目的是在一个成熟的平台上验证另一个 新的CPU,比如在x86机器上跑riscv的虚拟机,验证riscv的逻辑,只需要加上riscv指令到 中间码这个前端支持就好,因为中间码到x86的后端已经存在;如果目的是,比如在一台riscv 的机器上模拟x86,就需要加中间码到riscv的后端支持,我们这里以riscv为例子,把它作为 一个需要支持的CPU构架。 一个完整的riscv到中间码前端支持的例子可以参考: https://lore.kernel.org/all/[email protected] 以riscv为例子,体系相关的前端的代码在:target/riscv/,后端的代码在:tcg/riscv/, 基础外设和machine的代码在hw/riscv/ 基于KVM的方式,直接使用host CPU执行target CPU的指令,性能接近host上的性能,但是 需要target CPU和host CPU是相同的构架。本文不讨论KVM的逻辑。 qemu tcg前端解码逻辑把target cpu指令翻译成host cpu指令有两种方式,一种是使用helper函数,一种是使用 tiny code generator函数的方式。helper函数的方式还没有分析,现在只看tcg的方式。 我们把逻辑拉高一层看问题,所谓target CPU的运行,实际上是根据target CPU指令流去 不断的改变target CPU软件描述结构里的数据状态,因为实际的代码要运行到host CPU上, 所以,target代码要被翻译成host代码,才可以执行,通过执行改变target CPU的数据状态。 qemu为了解耦把target CPU代码先翻译成中间码,那么翻译成的中间码的语义也就是改变 target CPU数据状态的一组描述语句,所以target CPU状态参数会被当做入参传入中间码 描述语句。这组中间码是改变CPU状态的抽象的描述,有些CPU上的状态不好抽象成一般的 描述就用helper函数的方式补充,所以helper函数也是改变target CPU状态的描述。 tcg的方式,我们要使用tcg_gen_xxx的函数组织逻辑描述target CPU指令对target CPU状态 的改变。一些公共的代码是可以自动生成的,qemu里使用decode tree的方式自动生成这一部 分代码。 以riscv的代码来具体说明。qemu定义了一组target CPU指令的描述格式,说明文档在: docs/devel/decodetree.rst,riscv的指令描述在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里。这些文件生成的trans_xxx函数需要自己实现,riscv的这部分实现 是放在了在target/riscv/insn_trans/*里。生成的文件里有两个很大的解码函数decode-insn32.c.inc和 decode-insn16.c.inc,qemu把target CPU指令翻译成中间码的时候就需要调用上面两个解码 函数。 我们用riscv user mode的代码来看看上层具体调用关系。qemu提供system mode和user mode 的模拟方式,其中system mode会完整模拟整个系统,一个完整的OS可以运行在这个模拟的 系统上,user mode只是支持加载一个target CPU构架的用户态程序来跑,对于一般指令 使用tcg的方式翻译执行,对于用户态程序里的系统调用,user mode代码里模拟实现了系统 调用的语意。linux user mode的代码在qemu/linux-user/*,具体的调用过程如下: 12345678910111213141516/* qemu/linux-user/main.c */main +-> cpu_loop +-> cpu_exec +-> tb_gen_code | /* qemu/target/riscv/trannslate.c */ | +-> gen_intermediate_code | | +-> translator_loop(&riscv_tr_ops, xxx) | | /* riscv_tr_translate_insn */ | | +-> ops->translator_insn | | +-> decode_ops | | +-> decode_insn16 | | +-> decode_insn32 | +-> tcg_gen_code | +-> tcg_out_xxx +-> cpu_loop_exec_tbgen_intermediate_code是前端的解码函数,把target CPU的指令翻译成tcg中间码。tcg_gen_code 是后端,把中间码翻译成host CPU上的指令,其中tcg_out_xxx的一组函数做具体的翻译工作。 基本逻辑就是这样。下面展开其中的各个细节看下,细节上大概有这么几块: tcg整个翻译流程构架分析 decode tree的语法 tcg trans_xxx函数的语法 tcg翻译流程整个tcg前后端的翻译流程按指令块的粒度来搞,收集一个指令块翻译成中间码,然后把 中间码翻译成host CPU指令,整个过程动态执行。为了加速翻译,qemu把翻译成的host CPU指令块做了缓存,tcg前端解码的时候,先在缓存里找,如果找见就直接执行。 大致的代码调用关系如上。 decode tree语法因为CPU指令编码总是一组一组的,就可以用decode去描述这些固定的结构,然后qemu根据 这些指令定义,使用一个脚本(scripts/decodetree.py)在编译的时候生成解码函数的框架。 decode tree里定义了几个描述:field,argument,format,pattern,group。依次看看 他们是怎么用的。只记录要注意的点,细节还是直接看decodetree.rst这个文档。 CPU在解码的时候总要把指令中的特性field中的数据取出作为入参(寄存器编号,立即数,操作码等), field描述一个指令编码中特定的域段,根据描述可以生成取对应域段的函数。 1234567891011121314+---------------------------+---------------------------------------------+| Input | Generated code |+===========================+=============================================+| %disp 0:s16 | sextract(i, 0, 16) |+---------------------------+---------------------------------------------+| %imm9 16:6 10:3 | extract(i, 16, 6) rs1); gen_get_gpr(source2, a->rs2); (*func)(source1, source1, source2); gen_set_gpr(a->rd, source1); tcg_temp_free(source1); tcg_temp_free(source2); return true;}void tcg_gen_addi_i64(TCGv_i64 ret, TCGv_i64 arg1, int64_t arg2);tcg_gen_addi_i64可以看到tcg_gen_add_tl的函数入参,riscv的add指令从target CPU的 rs1,rs2里取两个加数,相加后放到rd寄存里,所以上面gen_get_gpr就表示生成这样的 中间码:把rs1/2位置上的数据存到source1/2位置上,gen_get_gpr的实现就是: 1234567tcg_gen_mov_tl(t, cpu_gpr[reg_num]) -> tcg_gen_mov_i64 -> tcg_gen_op2_i64(INDEX_op_mov_i64, ret, arg) -> tcg_gen_op2(opc, tcgv_i64_arg(a1), tcgv_i64_arg(a2)) -> TCGOp *op = tcg_emit_op(opc); -> op->args[0] = a1; -> op->args[1] = a2;可以看到最后生成的mov指令先挂到了一个链表里,后面的后端解码会把这些指令翻译成 host指令,生成的指令就是qemu/tcg/README里介绍的mov_i32/i64 t0, t1这个指令。这里 有几个逻辑要打通: 1. tcg_temp_new创建的变量存在哪里; 2. cpu_gpr[reg_num]是一个 全局变量,它如何索引到target CPU的寄存器。 首先tcg_temp_new分配的空间是在TCGContext tcg_ctx里的,所谓创建一个这样的TCGv就是 在tcg_ctx里用去一个TCGTemp。cpu_gpr[reg_num]可以索引到target CPU寄存器的基本逻辑 是,其实只要在前端和后端约定好描述target CPU的软件结构,cpu_gpr[reg_num]描述的就 时相关寄存器在这个软件结构里的位置。我们再看下这个cpu_gpr[]的初始化逻辑和tcg_ctx 的初始化逻辑,以及后端的编码逻辑就可以打通整个逻辑。 12riscv_translate_init -> cpu_gpr[i] = tcg_global_mem_new(cpu_env, offsetof(CPURISCVState, gpr[i]), riscv_int_regnames[i]);cpu_env在tcg_context_init(unsigned max_cpus)里初始化,得到的是tcg_ctx里TCGTemp temps 的地址。tcg_global_mem_new一次在tcg_ctx里从TCGTemp temps上分配空间,返回空间在 tcg_ctx上的相对地址。这样cpu_gpr[reg_name]就可以作为标记在前端和后端之间建立连接。 后端的代码直接把中间码翻译成host指令,中间码中的TCGv直接映射到host CPU的寄存器上, 从逻辑上讲,应该是翻译得到的host代码修改中间码对应TCGv对应的内存才对。这里的基本 逻辑是qemu在生成的中间码中以及TB执行后做了host寄存器到target CPU描述内存之间的 同步。 123456/* qemu/tcg/riscv/tcg-target.c.inc *//* tcg_out_op是整个后端解码体系架构相关的入口函数,每个架构都要做具体实现 */tcg_out_op -> case INDEX_op_add_i64 -> tcg_out_opc_reg -> tcg_out32可以看到add_i64的中间码直接翻译到了host上的寄存器,这里后端的翻译还是拿riscv举例了。 123456tcg_gen_code /* 如上提到的同步代码逻辑在这个函数中 */ -> default: tcg_reg_alloc_op -> 生成用host指令描述的同步逻辑,放在TB里 /* 生成业务相关的host指令,后端译码的总入口 */ -> tcg_out_op如上是同步的一个大概逻辑,具体细节需要进一步分析。 |
CopyRight 2018-2019 实验室设备网 版权所有 |