Qemu TCG
时间轴
2025-11-23
- init
环境
1 | wget https://download.qemu.org/qemu-10.1.2.tar.xz |
创建.clangd
1 | CompileFlags: |
gdb
1 | gdb -args ./build/qemu-system-riscv64 -M virt -device edu,id=edu1 -nographic |
QEMU 支持多种 accel,但大体可以分为两种:指令模拟技术(TCG)、虚拟化技术(KVM、HVF)等。
常见翻译技术:
- 解释器:Interpreter,每次解析并执行一条 Guest 指令,循环往复。
- 静态二进制翻译:Static Binary Translation,在程序运行前进行翻译。运行时没有翻译开销,优化幅度有限。
- 动态二进制翻译:Dynamic Binary Translation,在程序运行时动态翻译。一般按照程序 trace 翻译,不会全量翻译,能对热点代码进行深度优化。
TCG(Tiny Code Generator) 最初是一个 C 语言的编译器后端,后面演化为 QEMU 的二进制动态编译(翻译)引擎。
Target & Guest
Guest(虚拟机/被模拟的架构)
指的是 QEMU 模拟的 CPU 架构,也就是你运行的系统的 CPU 类型。
例如:
riscv64(RISC-V 64 位)aarch64(ARM 64 位)i386(x86 32 位)
在启动 QEMU 时指定 Guest 架构,例如:
1
qemu-system-riscv64 -machine virt -kernel kernel.elf
Guest 决定 QEMU 模拟出的指令集、寄存器等。
TCG Target(TCG 目标架构)
- TCG(Tiny Code Generator)是 QEMU 的动态翻译器,用来把 Guest 指令 转换成 Host CPU 可执行的本地指令。
- TCG Target 就是 宿主机 CPU 的架构,决定 QEMU 生成的机器码要在什么 CPU 上运行。
- 例如:
- Host =
x86_64 - Guest =
riscv64 - TCG Target =
x86_64(因为 QEMU 最终生成 x86_64 机器码在宿主机执行)
- Host =
TCR Translation
TCG IR
类似 LLVM,QEMU 也定义有自己的 IR,流程如下:
1 | +---------------+ +----------------+ +---------------+ |
把Guest指令翻译成QEMU IR,这是前端,后端把QEMU IR翻译成宿主机指令
优势:
- 拓展性好:支持新的前端(Guest),只需要实现 source -> IR;
- 易于流程化:类似 LLVM,可以引入各种 pass,对不同环节进行优化。
劣势:
- 性能普遍不高(相对而言)
TCG翻译流程
基本流程
TCG 的二进制转换是以代码块(Basic Block)为基本单元,翻译的产物为 Translation Block。
Basic Block 的划分规则:分支指令;特权指令/异常;代码段跨页。
- Guest PC 指向一个 Basic Block
- Basic Block(BB)是连续的、没有分支的指令序列。
- 例如 x86/RISC-V 中的一段顺序执行的指令。
- 检查 TB Cache(翻译缓存)
- TB(Translated Block) 是 Basic Block 被 DBT 翻译成 宿主可执行代码 后的结果。
- DBT 会维护一个哈希表或者缓存,键通常是 Guest PC。
- 命中 TB Cache(Y)
- 如果 Guest PC 对应的 TB 已经生成过,就直接跳到 Exec TB 执行。
- 无需再次翻译,提高效率。
- 未命中 TB Cache(N)
- 调用 translation 模块,把 Guest 的 Basic Block 转换成 Translation Block(TB)。
- 翻译完成后,将 TB 保存到缓存中,以便下次快速执行。
- Direct Block Chaining
- 每个 TB 会保存 其对应的 Guest PC,以及通常还会记录 下一个 Guest PC(末尾指令的跳转目标)
- 执行完一个 TB 后,如果下一个 Guest PC 已经有 TB,就直接跳转执行下一个 TB,而不回到
Check TB Cache。 - 通过在 TB 末尾生成直接跳转指令实现。
- 优化了执行效率,避免每次都回到解释器循环,尤其是循环密集型代码。
1 | +---------------+ |
- 当一个 Basic Block 被 DBT 转换为 TB 以后,下次再执行到相同的 Basic Block 直接从缓存中获取 TB 执行即可,无需再经过转换:
TranslationBlock
TCG 的二进制转换是以代码块(Basic Block)为基本单元,翻译的产物为 Translation Block。
TCG有三种类型的变量:temporary, local temporary和global
global: 一般系统寄存器是global变量,global变量又分为寄存器和memory两类,memory一般定义的是CPU里的寄存器
memory这种在后端翻译时,会把guest CPU寄存器的值先load到host寄存器里,计算完后再store回guest CPU结构体里,模拟过程会有访存行为。
寄存器这种TCGv在后端翻译时,会直接映射到host的寄存器上,每次就可以直接访问,一般用来存放guest CPU env的指针。
temporary: 变量的生命只在一个BB内
local temporary: 变量的生命在一个TB内,可以跨越BB。
Basic Block 的划分规则:分支指令;特权指令/异常;代码段跨页。
BB从上一个BB的结尾或者一个set_label指令开始, BB以分支指令(brcond_xxx)、goto_tb以及exit_tb结束,
Prologue(前置代码):
保存 Guest CPU 状态(寄存器、标志位等)到宿主寄存器或内存
设置执行环境
Epilogue(后置代码):
恢复 Guest CPU 状态
跳回 Dispatcher 或直接跳到下一个 TB
1 | +---------------------+ |
Direct block chaining
拿 x86_64 平台举例,每次执行上下文切换需要执行大约 20 条指令 (指令还会进行内存的读写),因此 DBT 的优化措施之一就是减少上下文切换,实现 TB 之间的直接链接:(比如直接跳转指令,可以直接连接两个TB块,但间接跳转指令不行,因为依赖运行时计算)
1 | 1) +---------------------+ |
PS: 两个 chained tb 对应的 Guest 指令需要在同一个 Guest page。
Code Buffer
- code_buffer 是 TCG(Tiny Code Generator)在宿主机器上存放 翻译后的 Guest 指令(TB) 的连续内存区域。
- 所有 Translation Block(TB) 都在这个 buffer 中生成、存储和执行。
- 后续的 Prologue / TB.code / Epilogue 都位于 code_buffer 中。
1 | code_buffer = mmap() |
tcg_code_gen_epilogue:
- 生成 TB 的 epilogue 部分,并更新
TB.tc.ptr
tcg_qemu_tb_exec:
- 根据 TB.struct 获取 TB.code,并跳转到TB.code
TB.struct:
- 记录 TB 的元信息,例如:
- Guest PC(入口地址)
- TB 长度
- 指向 TB.code 的指针
- 下一块 TB(Direct Block Chaining)
TB.code:
- 经过 TCG 翻译生成的宿主机器码
- 实际执行时从这里开始
epilogue执行后回到qemu世界
在 qemu 启动的早期会执行一个函数叫 tcg_init_machine, 完成 code_buffer 的申请和初始化。
accel/tcg/tcg-all.c
1 | static int tcg_init_machine(AccelState *as, MachineState *ms) |
- 后续所有代码翻译和执行的工作,都围绕 code_buffer 展开
- TCGContext 的后端管理工作,也是围绕 code_buffer 进行
DecodeTree
1 | +---------------+ +----------------+ |
Decodetree 则是由 Bastian Koppelmann 于 2017 年在移植 RISC-V QEMU 的时候所提出来的机制。提出该机制主要是因为过往的 instruction decoders (如:ARM) 都是采用一堆 switch-case 来做判断。不仅难阅读,也难以维护。
因此 Bastian Koppelmann 就提出了 Decodetree 的机制,开发者只需要通过 Decodetree 的语法定义各个指令的格式,便可交由 Decodetree 来动态生成对应包含 switch case 的 instruction decoder.c。
Decodetree 本质是一个 python 脚本,输入定义了体系结构指令格式的文件,输出指令解码器源码文件。
1 | +-----------+ +-----------+ +-------------------+ |
- input: 体系结构定义的指令编码格式文件
- output: 指令解码器的源代码(参与 QEMU 编译)
Decodetree 语法
Decodetree 的语法共分为:Fields、Argument Sets、Formats、Patterns 四部分。
- Fields,描述指令编码中的寄存器、立即数等字段;
- Argument Sets,描述用来保存从指令中所截取出来各字段的值;
- Formats,描述指令的格式,并生成相应的 decode function;
- Pattern,描述一个指令的 decode 方式。
Decodetree Field
Field 定义如何取出一指令中,各字段 (eg: rd, rs1, rs2, imm) 的值。
1 | field_def := '%' identifier ( unnamed_field )* ( !function=identifier )? |
- %identifier
- 字段的名字,由开发者定义
- 例:
%rd,%rs1,%imm
- unnamed_field
- 指定字段在指令中的比特位置
- 格式:
high_bit : low_bit - 可选
s表示符号扩展(sign extension) - 例:
7:5表示指令的 bit[7:5] - 例:
31:s20表示 bit[31:20] 且需要符号扩展
- !function=identifier
- 从指令中取出字段值之后,调用一个函数做进一步处理
- 比如立即数需要符号扩展、位拼接或地址转换
1 | 以 RISC-V 的 U-type 指令为例: |
Decodetree Argument Sets
Argument Set 定义用来保存从指令中所截取出来各字段的值。
1 | args_def := '&' identifier ( args_elt )+ ( !extern )? |
- &identifier
- 定义一个 Argument Set 的名字,由开发者自定义
- 例:
®s,&loadstore
- args_elt
- Argument Set 中包含的元素,通常是之前定义好的字段(Field)
- 例:
rd,rs1,imm - 意思是:把这些字段提取的值保存到这个 Argument Set 里
- !extern
- 表示该 Argument Set 已经在其他 Decoder 中定义过,如果有该字段,就不会再次生成对应的 argument set struct
- 避免重复生成结构体
1 | // U-type 指令格式示例 |
Decodetree Format
Format 定义了指令的格式 (如 RISC-V 中的 R、I、S、B、U、J-type),并会生成对应的 decode function。
1 | fmt_def := '@' identifier ( fmt_elt )+ |
identifier 可由开发者自定义,如:opr、opi… 等。
fmt_elt 则可以采用以下不同的语法:
- fixedbit_elt 包含一个或多个
0、1、.、-,每一个代表指令中的 1 个 bit。.代表该 bit 可以用 0 或是 1 来表示。-代表该 bit 完全被忽略。
- 用 Field 的语法来声明,Eg:ra:5、rb:5、lit:8
- fixedbit_elt 包含一个或多个
field_ref 有下列两种格式 (以下范例参考上文所定义之 Field):
'%' identifier:直接参考一个被定义过的 Field。如:
%rd,会生成:1
a->rd = extract32(insn, 7, 5);
identifier '=' '%' identifier:直接参考一个被定义过的 Field,但通过第一个 identifier 来重命名其所对应的 argument 名称。此方式可以用来指定不同的 argument 名称来参考至同一个 Field如:
my_rd=%rd,会生成:1
a->my_rd = extract32(insn, 7, 5)
args_ref 指定所传入 decode function 的 Argument Set。若没有指定 args_ref 的话,Decodetree 会根据 field_elt 或 field_ref 自动生成一个 Argument Set。此外,一个 Format 最多只能包含一个 args_ref
当 fixedbit_elt 或 field_ref 被定义时,该 Format 的所有的 bits 都必须被定义(可通过 fixedbit_elt 或 . 来定义各个 bits,空格会被忽略)。
1 | @opi ...... ra:5 lit:8 1 ....... rc:5 |
- insn[31:26] 可为 0 或 1
- insn[25:21] 为 ra
- insn[20:13] 为 lit
- insn[12] 固定为 1
- insn[11:5] 可为 0 或 1
- insn[4:0] 为 rc
此 Format 会生成以下的 decode function:
1 | // 由于我们没有指定 args_ref,因此 Decodetree 根据了 field_elt 的定义,自动生成了 arg_decode_insn320 这个 Argument Set |
以 RISC-V I-type 指令为例:
1 | 31 20 19 15 14 12 11 7 6 0 |
此范例会生成以下的 decode function:
1 | typedef struct { |
回到先前的 RISC-V U-type 指令,我们可以如同 I-type 指令定义其格式:
1 | # Fields: |
会生成以下的 decode function:
1 | typedef struct { |
Decodetree Pattern
Pattern 实际定义了一个指令的 decode 方式。Decodetree 会根据 Patterns 的定义,来动态产生出对应的 switch-case decode 判断分支。
1 | pat_def := identifier ( pat_elt )+ |
- identifier 可由开发者自定义,如:addl_r、addli … 等。
- pat_elt 则可以采用以下不同的语法:
- fixedbit_elt 与在 Format 中 fixedbit_elt 的定义相同。
- field_elt 与在 Format 中 field_elt 的定义相同。
- field_ref 与在 Format 中 field_ref 的定义相同。
- args_ref 与在 Format 中 args_ref 的定义相同。
- fmt_ref 直接参考一个被定义过的 Format。
- const_elt 可以直接指定某一个 argument 的值。
Pattern 示例:
1 | addl_i 010000 ..... ..... .... 0000000 ..... @opi |
定义了 addl_i 这个指令的 Pattern,其中:
- insn[31:26] 为 010000。
- insn[11:5] 为 0000000。
- 参考了 Format 示例中 定义的 @opi Format。
- 由于 Pattern 的所有 bits 都必须明确的被定义,因此 @opi 必须包含其余 insn[25:12] 及 insn[4:0] 的格式定义,否则 Decodetree 便会报错。
最后 addl_i 的 decoder 还会调用 trans_addl_i() 这个 translator function
示例
设计一条 RISC-V 的算数指令 cube,指令编码格式遵循 R-type,语义为:rd = [rs1] * [rs1] * [rs1]。(通过helper实现)
1 | 31 25 24 20 19 15 14 12 11 7 6 0 |
QEMU 的 TCG 流程是:
- 从 Guest 指令生成 TB(Translation Block)
- 翻译 Guest 指令到宿主指令序列
有些指令比较复杂,或者 QEMU 没有现成的 TCG 操作码可以直接生成。这时就需要 helper:
- 作用:把指令语义用宿主可执行的 C 代码实现
- TCG 翻译阶段,遇到该指令时生成一个调用 helper 的代码片段
- 执行阶段,直接执行 helper 的实现
示例 C 代码:
1 | static int custom_cube(uintptr_t addr) |
在 QEMU 中添加 cube 的指令译码:
1 | // target/riscv/insn32.decode |
DEF_HELPER_3表示这是一个带 3 个参数的 helper参数:
env→ CPU 状态结构体 (CPURISCVState *env)tl→ 指令操作数(比如 rd)tl→ 指令操作数(比如 rs1)
添加 cube 的指令模拟逻辑(采用 helper 实现):
1 | // target/riscv/helper.h |
DEF_HELPER_3表示有三个参数
| 位置 | 意义 |
|---|---|
cube |
helper 名字 → 最终对应的函数名是 helper_cube |
void |
helper 的返回类型 |
env |
第一个参数类型:CPURISCVState *env(即 CPU 状态) |
tl |
第二个参数类型:target_ulong |
tl |
第三个参数类型:target_ulong |
env->gpr[rs1] → 从寄存器文件读取 rs1 的值
cpu_ldq_mmu(...) → 模拟从内存加载 rs1 地址处的值
env->gpr[rd] = val * val * val; → 执行 cube 计算,把结果写入 rd
arg_cube是生成的Argument Sets
编写一个简单的示例程序:
1 | int main(void) { |
编译运行测试:
1 | $ riscv64-linux-musl-gcc main.c -o cube_demo --static |
TCG IR
前面我们讲了如何使用 qemu 的 helper 函数来模拟指令的功能,但是一般情况下,helper 主要用于 IR 实现不太方便的情况。
如果想要获得更好的性能,推荐使用 IR 来实现。
TCG 的前端负责将目标架构的指令转换为 TCG op,而 TCG 的后端则负责将 TCG op 转换为目标架构的指令。
这里我们主要关注 TCG 的前端,讨论常用的 TCG op 的用法。
推荐阅读:
TCG op 的基本格式如下:
1 | tcg_gen_<op>[i]_<reg_size>(TCGv<reg_size> args, ...) |
Registers
1 | TCGv reg = tcg_global_mem_new(TCG_AREG0, offsetof(CPUState, reg), "reg"); |
Temporaries
1 | // Create a new temporary register |
labels
1 | // Create a new label |
Ops
操作单个寄存器:
1 | // ret = arg1 |
操作两个寄存器:
1 | // ret = arg1 + arg2 |
Bit Operations
Logic operations on a single register:
1 | // ret = !arg1 |
Logic operations on two registers:
1 | // ret = arg1 & arg2 |
Shift
1 | // ret = arg1 >> arg2 /* Sign fills vacant bits */ |
Rotation
1 | // ret = arg1 rotl arg2 |
Byte
1 | // ret = ((arg1 & 0xff00) >> 8) // ((arg1 & 0xff) << 8) |
Load/Store
These are for moving data between registers and arbitrary host memory.
Typically used for funky CPU state that is not represented by dedicated registers already and thus infrequently used.
These are not for accessing the target’s memory space;
see the QEMU_XX helpers below for that.
1 | // Load an 8bit quantity from host memory and sign extend |
These are for moving data between registers and arbitrary target memory.
The address to load/store via is always the second argument while the first argument is always the value to be loaded/stored.
The third argument (memory index) only makes sense for system targets; user targets will simply specify 0 all the time.
1 | // ret = *(int8_t *)addr |
Code Flow
1 | // if (arg1 <condition> arg2) goto label |
示例
我们使用 IR 来实现 cube 指令:
1 | // target/riscv/insn_trans/trans_rvi.c.inc |
打印IR
1 | $ ./build/qemu-system-riscv64 -M virt -d in_asm,op,out_asm -nographic -D cpu.log |
- -d表示输出日志
- in_asm表示输入汇编
- op表示中间IR
- out_asm表示输出汇编
- -D表示输出到文件或终端
参考:




