时间轴

2025-11-23

  1. init

环境

1
2
3
4
5
6
wget https://download.qemu.org/qemu-10.1.2.tar.xz
tar xvJf qemu-10.1.2.tar.xz
cd qemu-10.1.2
mkdir -p output
./configure --prefix=$PWD/output --target-list=aarch64-softmmu,riscv64-softmmu --enable-debug
bear -- make -j$(nproc)

创建.clangd

1
2
3
CompileFlags:
Add: -Wno-unknown-warning-option
Remove: [-m*, -f*]

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 机器码在宿主机执行)

TCR Translation

TCG IR

类似 LLVM,QEMU 也定义有自己的 IR,流程如下:

1
2
3
4
5
6
7
+---------------+      +----------------+      +---------------+
| | | | | |
| Source binary | ---> | QEMU IR | ---> | Target binary |
| code | | | | code |
| | | | | |
+---------------+ +----------------+ +---------------+
Guest Host

把Guest指令翻译成QEMU IR,这是前端,后端把QEMU IR翻译成宿主机指令

优势

  • 拓展性好:支持新的前端(Guest),只需要实现 source -> IR;
  • 易于流程化:类似 LLVM,可以引入各种 pass,对不同环节进行优化。

劣势

  • 性能普遍不高(相对而言)

TCG翻译流程

基本流程

TCG 的二进制转换是以代码块(Basic Block)为基本单元,翻译的产物为 Translation Block。

Basic Block 的划分规则:分支指令特权指令/异常代码段跨页

  1. Guest PC 指向一个 Basic Block
    • Basic Block(BB)是连续的、没有分支的指令序列。
    • 例如 x86/RISC-V 中的一段顺序执行的指令。
  2. 检查 TB Cache(翻译缓存)
    • TB(Translated Block) 是 Basic Block 被 DBT 翻译成 宿主可执行代码 后的结果。
    • DBT 会维护一个哈希表或者缓存,键通常是 Guest PC。
  3. 命中 TB Cache(Y)
    • 如果 Guest PC 对应的 TB 已经生成过,就直接跳到 Exec TB 执行。
    • 无需再次翻译,提高效率。
  4. 未命中 TB Cache(N)
    • 调用 translation 模块,把 Guest 的 Basic Block 转换成 Translation Block(TB)。
    • 翻译完成后,将 TB 保存到缓存中,以便下次快速执行。
  5. Direct Block Chaining
    • 每个 TB 会保存 其对应的 Guest PC,以及通常还会记录 下一个 Guest PC(末尾指令的跳转目标)
    • 执行完一个 TB 后,如果下一个 Guest PC 已经有 TB,就直接跳转执行下一个 TB,而不回到 Check TB Cache
    • 通过在 TB 末尾生成直接跳转指令实现。
    • 优化了执行效率,避免每次都回到解释器循环,尤其是循环密集型代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                              +---------------+                    
+----------------------| Do something |-------------------+
| +---------------+ |
v |
+--------------+ +----------------+ Y +---------+ |
| Guest PC +------>| Check TB Cache +-------->| Exec TB +-----+
+--------------+ +------+---------+ +---------+
| N ^
v |
+-------------+ |
| translation | |
+-----+-------+ |
v |
+-----------------+ |
|Save TB to Cache +-------------+
+-----------------+
  • 当一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
                        +---------------------+                                     
1) | |
+----------------+ QEMU TCG engine +---------------+
| +---->| |<---+ |
| | +----------+---^------+ | |
| | | | 4) | | 5)
| | 3) | +------+ | |
v |2) v | | 6) v
+---------------+ | +---------------+ | | +---------------+
| prologue | | | prologue | | | | prologue |
+---------------+ | +---------------+ | | +---------------+
| | | | | | | | |
| Translation | | | Translation | | | | Translation |
| Block1 | | | Block2 | | | | Block3 |
| | | | | | | | |
+---------------+ | +---------------+- | | +---------------+
| epilogue | | | epilogue | | | | epilogue |
+------+--------+ | +-------+-------+ | | +------+--------+
+----------+ +----------+ +---------+

Direct block chaining

拿 x86_64 平台举例,每次执行上下文切换需要执行大约 20 条指令 (指令还会进行内存的读写),因此 DBT 的优化措施之一就是减少上下文切换,实现 TB 之间的直接链接:(比如直接跳转指令,可以直接连接两个TB块,但间接跳转指令不行,因为依赖运行时计算)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
            1)          +---------------------+                                 
+----------------+ QEMU TCG engine +---------------------------+
| +---------------------+ |
v |
+---------------+ +---------------+ +---------------+ |
| prologue | | prologue | 3) | prologue | |
+---------------+ +------> +---------------+ +-----> +---------------+ |
| | | | | | | | | 5)
| Translation | | | Translation | | | Translation | |
| Block1 | | | Block2 | | | Block3 | |
| | |2) | | | | | |
+---------------+-+ +---------------+--+ +---------------+---+
| epilogue | | epilogue | | epilogue |
+------+--------+ +-------+-------+ +------+--------+

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
2
3
4
5
6
7
8
9
10
11
12
13
code_buffer = mmap()                                               
| TCGContext.code_ptr
v v
+-----------+----------+-------------+---------+------------------+
| | | | | |
| prologue | epilogue | TB.struct | TB.code | ... | size = Host / dynamic_code_size
| | | | | |
+-----------+----------+-------------+---------+------------------+
^ ^ ^
| | |
| tcg_code_gen_epilogue |
| tb.tc.ptr
tcg_qemu_tb_exec

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
static int tcg_init_machine(AccelState *as, MachineState *ms)
{
TCGState *s = TCG_STATE(as);
unsigned max_threads = 1;

#ifndef CONFIG_USER_ONLY
CPUClass *cc = CPU_CLASS(object_class_by_name(target_cpu_type()));
bool mttcg_supported = cc->tcg_ops->mttcg_supported;

switch (s->mttcg_enabled) {
case ON_OFF_AUTO_AUTO:
/*
* We default to false if we know other options have been enabled
* which are currently incompatible with MTTCG. Otherwise when each
* guest (target) has been updated to support:
* - atomic instructions
* - memory ordering primitives (barriers)
* they can set the appropriate CONFIG flags in ${target}-softmmu.mak
*
* Once a guest architecture has been converted to the new primitives
* there is one remaining limitation to check:
* - The guest can't be oversized (e.g. 64 bit guest on 32 bit host)
*/
if (mttcg_supported && !icount_enabled()) {
s->mttcg_enabled = ON_OFF_AUTO_ON;
max_threads = ms->smp.max_cpus;
} else {
s->mttcg_enabled = ON_OFF_AUTO_OFF;
}
break;
case ON_OFF_AUTO_ON:
if (!mttcg_supported) {
warn_report("Guest not yet converted to MTTCG - "
"you may get unexpected results");
}
max_threads = ms->smp.max_cpus;
break;
case ON_OFF_AUTO_OFF:
break;
default:
g_assert_not_reached();
}
#endif

tcg_allowed = true;

page_init();
tb_htable_init();
tcg_init(s->tb_size * MiB, s->splitwx_enabled, max_threads);

#if defined(CONFIG_SOFTMMU)
/*
* There's no guest base to take into account, so go ahead and
* initialize the prologue now.
*/
tcg_prologue_init();
#endif

#ifdef CONFIG_USER_ONLY
qdev_create_fake_machine();
#endif

return 0;
}

  • 后续所有代码翻译和执行的工作,都围绕 code_buffer 展开
  • TCGContext 的后端管理工作,也是围绕 code_buffer 进行

DecodeTree

1
2
3
4
5
6
7
+---------------+      +----------------+
| | | |
| Source binary | ---> | QEMU IR |
| code | | |
| | | |
+---------------+ +----------------+
Guest

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
2
3
4
+-----------+           +-----------+            +-------------------+
| arch-insn | input | scripts/ | output | decode-@BASENAME@ |
| .decode +---------->| decode.py +----------->| .c.in |
+-----------+ +-----------+ +-------------------+
  • 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
2
field_def     := '%' identifier ( unnamed_field )* ( !function=identifier )?
unnamed_field := number ':' ( 's' ) number
  • %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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RISC-VU-type 指令为例:
31 12 11 7 6 0
+----------------------------------+--------------------+------+
| imm[31:12] | rd |opcode| U-type
+----------------------------------+--------------------+------+

可以声明为:
%rd 7:5
%imm_u 12:s20 !function=ex_shift_12

最后会生成如下的代码:
static void decode_insn32_extract_u(DisasContext *ctx, arg_u *a, uint32_t insn)
{
a->imm = ex_shift_12(ctx, sextract32(insn, 12, 20)); // 是由 insn[31:12] 所取得并做符号扩展,且会再调用 ex_shift_12() 来左移 12 个 bits
a->rd = extract32(insn, 7, 5); // 由 insn[11:7] 所取得
}

Decodetree Argument Sets

Argument Set 定义用来保存从指令中所截取出来各字段的值

1
2
args_def    := '&' identifier ( args_elt )+ ( !extern )?
args_elt := identifier
  • &identifier
    • 定义一个 Argument Set 的名字,由开发者自定义
    • 例:&regs, &loadstore
  • args_elt
    • Argument Set 中包含的元素,通常是之前定义好的字段(Field)
    • 例:rd, rs1, imm
    • 意思是:把这些字段提取的值保存到这个 Argument Set 里
  • !extern
    • 表示该 Argument Set 已经在其他 Decoder 中定义过,如果有该字段,就不会再次生成对应的 argument set struct
    • 避免重复生成结构体
1
2
3
4
5
6
7
8
// U-type 指令格式示例
// &u imm rd

// 生成如下代码
typedef struct {
int imm;
int rd;
} arg_u;

Decodetree Format

Format 定义了指令的格式 (如 RISC-V 中的 R、I、S、B、U、J-type),并会生成对应的 decode function

1
2
3
4
5
6
fmt_def      := '@' identifier ( fmt_elt )+
fmt_elt := fixedbit_elt | field_elt | field_ref | args_ref
fixedbit_elt := [01.-]+
field_elt := identifier ':' 's'? number
field_ref := '%' identifier | identifier '=' '%' identifier
args_ref := '&' identifier
  • identifier 可由开发者自定义,如:opr、opi… 等。

  • fmt_elt 则可以采用以下不同的语法:

    • fixedbit_elt 包含一个或多个 01.-每一个代表指令中的 1 个 bit
      • . 代表该 bit 可以用 0 或是 1 来表示。
      • - 代表该 bit 完全被忽略。
    • 用 Field 的语法来声明,Eg:ra:5、rb:5、lit:8
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
// 由于我们没有指定 args_ref,因此 Decodetree 根据了 field_elt 的定义,自动生成了 arg_decode_insn320 这个 Argument Set
typedef struct {
int lit;
int ra;
int rc;
} arg_decode_insn320;

static void decode_insn32_extract_opi(DisasContext *ctx, arg_decode_insn320 *a, uint32_t insn)
{
a->ra = extract32(insn, 21, 5);
a->lit = extract32(insn, 13, 8);
a->rc = extract32(insn, 0, 5);
}

以 RISC-V I-type 指令为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
31           20 19    15 14     12  11                 7  6    0
+--------------+--------+----------+--------------------+------+
| imm[11:0] | rs1 | funct3 | rd |opcode| I-type
+--------------+--------+----------+--------------------+------+

# Fields:
%rs1 15:5
%rd 7:5

# immediates:
%imm_i 20:s12

# Argment sets:
&i imm rs1 rd

@i ........ ........ ........ ........ &i imm=%imm_i %rs1 %rd

此范例会生成以下的 decode function:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
int imm;
int rd;
int rs1;
} arg_i;

static void decode_insn32extract_i(DisasContext *ctx, arg_i *a, uint32_t insn)
{
a->imm = sextract32(insn, 20, 12);
a->rs1 = extract32(insn, 15, 5);
a->rd = extract32(insn, 7, 5);
}

回到先前的 RISC-V U-type 指令,我们可以如同 I-type 指令定义其格式:

1
2
3
4
5
6
7
8
9
10
# Fields:
%rd 7:5

# immediates:
%imm_u 12:s20 !function=ex_shift_12

# Argument sets:
&u imm rd

@u .................... ..... ....... &u imm=%imm_u %rd

会生成以下的 decode function:

1
2
3
4
5
6
7
8
9
10
typedef struct {
int imm;
int rd;
} arg_u;

static void decode_insn32_extract_u(DisasContext *ctx, arg_u *a, uint32_t insn)
{
a->imm = ex_shift_12(ctx, sextract32(insn, 12, 20));
a->rd = extract32(insn, 7, 5);
}

Decodetree Pattern

Pattern 实际定义了一个指令的 decode 方式。Decodetree 会根据 Patterns 的定义,来动态产生出对应的 switch-case decode 判断分支。

1
2
3
4
pat_def      := identifier ( pat_elt )+
pat_elt := fixedbit_elt | field_elt | field_ref | args_ref | fmt_ref | const_elt
fmt_ref := '@' identifier
const_elt := identifier '=' number
  • 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
2
3
4
5
6
7
8
31      25 24  20 19    15 14     12  11                7 6     0
+---------+--------+--------+----------+-------------------+-------+
| func7 | rs2 | rs1 | funct3 | rd | opcode| R-type
+---------+--------+--------+----------+-------------------+-------+
6 6 0x7b
+---------+--------+--------+----------+-------------------+-------+
| 000110 | 00000 | rs1 | 110 | rd |1111011| cube
+---------+--------+--------+----------+-------------------+-------+

QEMU 的 TCG 流程是:

  1. 从 Guest 指令生成 TB(Translation Block)
  2. 翻译 Guest 指令到宿主指令序列

有些指令比较复杂,或者 QEMU 没有现成的 TCG 操作码可以直接生成。这时就需要 helper

  • 作用:把指令语义用宿主可执行的 C 代码实现
  • TCG 翻译阶段,遇到该指令时生成一个调用 helper 的代码片段
  • 执行阶段,直接执行 helper 的实现

示例 C 代码:

1
2
3
4
5
6
7
8
9
static int custom_cube(uintptr_t addr)
{
int cube;
asm volatile (
".insn r 0x7b, 6, 6, %0, %1, x0"
:"=r"(cube) // 将结果存储在变量 cube 中
:"r"(addr)); // 将变量 addr 的值作为输入
return cube;
}

在 QEMU 中添加 cube 的指令译码:

1
2
3
// target/riscv/insn32.decode
@r_cube ....... ..... ..... ... ..... ....... %rs1 %rd
cube 0000110 00000 ..... 110 ..... 1111011 @r_cube
  • DEF_HELPER_3 表示这是一个带 3 个参数的 helper

  • 参数:

    • env → CPU 状态结构体 (CPURISCVState *env)

    • tl → 指令操作数(比如 rd)

    • tl → 指令操作数(比如 rs1)

添加 cube 的指令模拟逻辑(采用 helper 实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// target/riscv/helper.h
DEF_HELPER_3(cube, void, env, tl, tl)

// target/riscv/op_helper.c
void helper_cube(CPURISCVState *env, target_ulong rd, target_ulong rs1)
{
MemOpIdx oi = make_memop_idx(MO_TEUQ, 0);
target_ulong val = cpu_ldq_mmu(env, env->gpr[rs1], oi, GETPC());
env->gpr[rd] = val * val * val;
}

// target/riscv/insn_trans/trans_rvi.c.inc
static bool trans_cube(DisasContext *ctx, arg_cube *a)
{
//gen_helper_cube(tcg_env, tcg_constant_tl(a->rd), tcg_constant_tl(a->rs1));//a->rs1只是寄存器标号
gen_helper_cube(tcg_env, get_gpr(a->rd), get_gpr(a->rs1));//get general purpose register
// tcg_env是一个全局变量
return true;
}

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
2
3
4
5
6
7
8
9
10
11
int main(void) {
int a = 3;
int ret = 0;
ret = custom_cube((uintptr_t)&a);
if (ret == a * a * a) {
printf("ok!\n");
} else {
printf("err! ret=%d\n", ret);
}
return 0;
}

编译运行测试:

1
2
3
$ riscv64-linux-musl-gcc main.c -o cube_demo --static
$ qemu-rsicv64 cube_demo
$ ok!

TCG IR

前面我们讲了如何使用 qemu 的 helper 函数来模拟指令的功能,但是一般情况下,helper 主要用于 IR 实现不太方便的情况。

如果想要获得更好的性能,推荐使用 IR 来实现。

TCG 的前端负责将目标架构的指令转换为 TCG op,而 TCG 的后端则负责将 TCG op 转换为目标架构的指令。

这里我们主要关注 TCG 的前端,讨论常用的 TCG op 的用法。

推荐阅读:

TCG op 的基本格式如下:

1
2
3
4
5
6
tcg_gen_<op>[i]_<reg_size>(TCGv<reg_size> args, ...)

op: 操作类型
i: 操作数数量
reg_size: 寄存器大小 (32/64/tl)
args: 操作数列表

Registers

1
TCGv reg = tcg_global_mem_new(TCG_AREG0, offsetof(CPUState, reg), "reg");

Temporaries

1
2
3
4
5
6
7
8
9
10
// Create a new temporary register
TCGv tmp = tcg_temp_new();

// Create a local temporary register.
// Simple temporary register cannot carry its value across jump/brcond,
// only local temporary can.
TCGv tmpl = tcg_temp_local_new();

// Free a temporary register
tcg_temp_free(tmp);

labels

1
2
3
4
5
// Create a new label
int l = gen_new_label();

// Label the current location.
gen_set_label(l);

Ops

操作单个寄存器:

1
2
3
4
5
6
7
// ret = arg1
// Assignment_(mathematical_logic): Assign one register to another
tcg_gen_mov_tl(ret, arg1);

// ret = - arg1
// Negation: Negate the sign of a register
tcg_gen_neg_tl(ret, arg1);

操作两个寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ret = arg1 + arg2
// Addition: Add two registers
tcg_gen_add_tl(ret, arg1, arg2);

// ret = arg1 - arg2
// Subtraction: Subtract two registers
tcg_gen_sub_tl(ret, arg1, arg2);

// ret = arg1 * arg2
// Multiplication: Multiply two signed registers and return the result
tcg_gen_mul_tl(ret, arg1, arg2);

// ret = arg1 * arg2
// Multiplication: Multiply two unsigned registers and return the result
tcg_gen_mulu_tl(ret, arg1, arg2);

// ret = arg1 / arg2
// Division_(mathematics): Divide two signed registers and return the result
tcg_gen_div_tl(ret, arg1, arg2);

// ret = arg1 / arg2
// Division_(mathematics): Divide two unsigned registers and return the result
tcg_gen_divu_tl(ret, arg1, arg2);

// ret = arg1 % arg2
// Division_(mathematics): Divide two signed registers and return the remainder
tcg_gen_rem_tl(ret, arg1, arg2);

// ret = arg1 % arg2
// Division_(mathematics) Divide two unsigned registers and return the remainder
tcg_gen_remu_tl(ret, arg1, arg2);

Bit Operations

Logic operations on a single register:

1
2
3
// ret = !arg1
// Negation: Logical NOT an register
tcg_gen_not_tl(ret, arg1);

Logic operations on two registers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ret = arg1 & arg2
// Logical_conjunction: Logical AND two registers
tcg_gen_and_tl(ret, arg1, arg2);

// ret = arg1 arg2
// Logical_disjunction: Logical OR two registers
tcg_gen_or_tl(ret, arg1, arg2);

// ret = arg1 ^ arg2
// Exclusive_or: Logical XOR two registers
tcg_gen_xor_tl(ret, arg1, arg2);

// ret = arg1 ↑ arg2
// Logical_NAND: Logical NAND two registers
tcg_gen_nand_tl(ret, arg1, arg2);

// ret = arg1 ↓ arg2
// Logical_NOR Logical NOR two registers
tcg_gen_nor_tl(ret, arg1, arg2);

// ret = !(arg1 ^ arg2)
// Logical_equivalence: Compute logical equivalent of two registers
tcg_gen_eqv_tl(ret, arg1, arg2);

// ret = arg1 & ~arg2
// Logical AND one register with the complement of another
tcg_gen_andc_tl(ret, arg1, arg2);

// ret = arg1 ~arg2
// Logical OR one register with the complement of another
tcg_gen_orc_tl(ret, arg1, arg2);

Shift

1
2
3
4
5
6
7
8
9
10
11
// ret = arg1 >> arg2 /* Sign fills vacant bits */
// Arithmetic shift right one operand by magnitude of another
tcg_gen_sar_tl(ret, arg1, arg2);

// ret = arg1 << arg2
// Logical_shift Logical shift left one registerby magnitude of another
tcg_gen_shl_tl(ret, arg1, arg2);

// ret = arg1 >> arg2
// Logical_shift Logical shift right one register by magnitude of another
tcg_gen_shr_tl(ret, arg1, arg2);

Rotation

1
2
3
4
5
6
7
// ret = arg1 rotl arg2
// Circular_shift: Rotate left one register by magnitude of another
tcg_gen_rotl_tl(ret, arg1, arg2);

// ret = arg1 rotr arg2
// Circular_shift Rotate right one register by magnitude of another
tcg_gen_rotr_tl(ret, arg1, arg2);

Byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ret = ((arg1 & 0xff00) >> 8) // ((arg1 & 0xff) << 8)
// Endianness Byte swap a 16bit register
tcg_gen_bswap16_tl(ret, arg1);

// ret = ...see bswap16 and extend to 32bits...
// Endianness Byte swap a 32bit register
tcg_gen_bswap32_tl(ret, arg1);


// ret = ...see bswap32 and extend to 64bits...
// Endianness Byte swap a 64bit register
tcg_gen_bswap64_tl(ret, arg1);

// ret = (int8_t)arg1
// Sign extend an 8bit register
tcg_gen_ext8s_tl(ret, arg1);

// ret = (uint8_t)arg1
// Zero extend an 8bit register
tcg_gen_ext8u_tl(ret, arg1);

// ret = (int16_t)arg1
// Sign extend an 16bit register
tcg_gen_ext16s_tl(ret, arg1);

// ret = (uint16_t)arg1
// Zero extend an 16bit register
tcg_gen_ext16u_tl(ret, arg1);

// ret = (int32_t)arg1
// Sign extend an 32bit register
tcg_gen_ext32s_tl(ret, arg1);

// ret = (uint32_t)arg1
// Zero extend an 32bit register
tcg_gen_ext32u_tl(ret, arg1);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Load an 8bit quantity from host memory and sign extend
tcg_gen_ld8s_tl(reg, cpu_env, offsetof(CPUState, reg));

// Load an 8bit quantity from host memory and zero extend
tcg_gen_ld8u_tl(reg, cpu_env, offsetof(CPUState, reg));

// Load a 16bit quantity from host memory and sign extend
tcg_gen_ld16s_tl(reg, cpu_env, offsetof(CPUState, reg));

// Load a 16bit quantity from host memory and zero extend
tcg_gen_ld16u_tl(reg, cpu_env, offsetof(CPUState, reg));

// Load a 32bit quantity from host memory and sign extend
tcg_gen_ld32s_tl(reg, cpu_env, offsetof(CPUState, reg));

// Load a 32bit quantity from host memory and zero extend
tcg_gen_ld32u_tl(reg, cpu_env, offsetof(CPUState, reg));

// Load a 64bit quantity from host memory
tcg_gen_ld64_tl(reg, cpu_env, offsetof(CPUState, reg));

// Alias to target native sized load
tcg_gen_ld_tl(reg, cpu_env, offsetof(CPUState, reg));

// Store a 8bit quantity to host memory
tcg_gen_st8_tl(reg, cpu_env, offsetof(CPUState, reg));

// Store a 16bit quantity to host memory
tcg_gen_st16_tl(reg, cpu_env, offsetof(CPUState, reg));

// Store a 32bit quantity to host memory
tcg_gen_st32_tl(reg, cpu_env, offsetof(CPUState, reg));

// Alias to target native sized store
tcg_gen_st_tl(reg, cpu_env, offsetof(CPUState, reg));

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ret = *(int8_t *)addr
// Load an 8bit quantity from target memory and sign extend
tcg_gen_qemu_ld8s(ret, addr, mem_idx);

// ret = *(uint8_t *)addr
// Load an 8bit quantity from target memory and zero extend
tcg_gen_qemu_ld8u(ret, addr, mem_idx);

// ret = *(int8_t *)addr
// Load a 16bit quantity from target memory and sign extend
tcg_gen_qemu_ld16s(ret, addr, mem_idx);

// ret = *(uint8_t *)addr
// Load a 16bit quantity from target memory and zero extend
tcg_gen_qemu_ld16u(ret, addr, mem_idx);

// ret = *(int8_t *)addr
// Load a 32bit quantity from target memory and sign extend
tcg_gen_qemu_ld32s(ret, addr, mem_idx);

// ret = *(uint8_t *)addr
// Load a 32bit quantity from target memory and zero extend
tcg_gen_qemu_ld32u(ret, addr, mem_idx);

// ret = *(uint64_t *)addr
// Load a 64bit quantity from target memory
tcg_gen_qemu_ld64(ret, addr, mem_idx);

// *(uint8_t *)addr = arg
// Store an 8bit quantity to target memory
tcg_gen_qemu_st8(arg, addr, mem_idx);

// *(uint16_t *)addr = arg
// Store a 16bit quantity to target memory
tcg_gen_qemu_st16(arg, addr, mem_idx);

// *(uint32_t *)addr = arg
// Store a 32bit quantity to target memory
tcg_gen_qemu_st32(arg, addr, mem_idx);

// *(uint64_t *)addr = arg
// Store a 64bit quantity to target memory
tcg_gen_qemu_st64(arg, addr, mem_idx);

Code Flow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// if (arg1 <condition> arg2) goto label
// Test two operands and conditionally branch to a label
tcg_gen_brcond_tl(TCG_COND_XXX, arg1, arg2, label);

// Goto translation block (TB chaining)
// Every TB can goto_tb to max two other different destinations. There are
// two jump slots. tcg_gen_goto_tb takes a jump slot index as an arg,
// 0 or 1. These jumps will only take place if the TB's get chained,
// you need to tcg_gen_exit_tb with (tb // index) for that to ever happen.
// tcg_gen_goto_tb may be issued at most once with each slot index per TB.
tcg_gen_goto_tb(num);

// Exit translation block
// num may be 0 or TB address ORed with the index of the taken jump slot.
// If you tcg_gen_exit_tb(0), chaining will not happen and a new TB
// will be looked up based on the CPU state.
tcg_gen_exit_tb(num);

// ret = arg1 <condition> arg2
// Compare two operands
tcg_gen_setcond_tl(TCG_COND_XXX, ret, arg1, arg2);

示例

我们使用 IR 来实现 cube 指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
// target/riscv/insn_trans/trans_rvi.c.inc
static bool trans_cube(DisasContext *ctx, arg_cube *a)
{
TCGv dest = tcg_temp_new(); // 申请一个临时变量
TCGv rd = get_gpr(ctx, a->rd, EXT_NONE); // 获取 rd 寄存器
// 读取 rs1 寄存器的值指向的内存的值,存储到 dest 中
tcg_gen_qemu_ld_tl(dest, get_gpr(ctx, a->rs1, EXT_NONE), ctx->mem_idx, MO_TEUQ);
// 计算 cube 并存储到 rd 寄存器中
tcg_gen_mul_tl(rd, dest, dest); // rd = dest * dest
tcg_gen_mul_tl(rd, rd, dest); // rd = rd * dest
gen_set_gpr(ctx, a->rd, rd);
return true;
}

打印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表示输出到文件或终端

参考: