1.介绍

TCG(微型代码生成器)最初是C编译器的通用后端。 它被简化为可以在QEMU中使用。 它也起源于Paul Brook编写的QOP代码生成器。

2.定义

TCG吸收类似于RISC的“ TCG ops”定义,并对它们执行一些优化,包括活动性分析(liveness analysis)和琐碎的常量表达式评估(trivial constant expression evaluation)。然后,在宿主CPU后端(也称为TCG“target”)中实现TCG操作。

TCG “target”:是我们为其生成代码的体系结构。当然,它与QEMU的“target”(即模拟体系结构)不同。当TCG开始作为用于交叉编译的通用C后端时,尽管QEMU从来没有这样的假设,但TCG目标不同于主机。

在本文档中,我们使用“ guest”来指定我们正在仿真的体系结构。 “target”始终表示TCG目标,即运行QEMU的计算机。

TCG“function”:对应于QEMU转换块(TB)。

TCG“temporary”:是仅存在于基本块中的变量。在每个函数中显式分配了临时项。

TCG“local temporary”:是仅存在于函数中的变量。在每个函数中显式分配了本地临时变量。

TCG“global”:是所有函数中都存在的变量(等效于C全局变量)。它们在定义的功能之前定义。 TCG全局可以是内存地址(例如QEMU CPU寄存器),固定宿主机器寄存器(例如QEMU CPU状态指针)或存储在QEMU TB之外的寄存器中的内存地址(尚未实现)。

TCG“basic block”对应于由分支指令终止的指令列表。

具有“unspecified behavior”:的操作可能会导致崩溃。

具有“unspecified behavior”的操作不应崩溃。但是,结果可能是几种可能性之一,因此可以认为是“不确定的结果”。

3.中间表示

3.1 介绍

TCG指令对临时变量,局部临时变量或全局变量进行操作。 TCG指令和变量是强类型的。 支持两种类型:32位整数和64位整数。 根据TCG目标字的大小,指针被定义为32位或64位整数的别名。

每个指令都有固定数量的输出变量操作数、输入变量操作数、始终为常数的操作数。

值得注意的例外是具有可变数量的输出和输入的调用指令。

以文本形式,输出操作数通常是第一个,然后是输入操作数,然后是常量操作数。 输出类型包含在指令名称中。 常量以“ $”为前缀。

add_i32 t0,t1,t2(表示t0 <-t1 + t2)

3.2 假设

基本块

  • 基本块在分支(例如brcond_i32指令),goto_tb和exit_tb指令之后结束。
  • 基本块在上一个基本块结束后或在set_label指令后开始。

基本块结束后,临时对象的内容将被破坏,但本地临时对象和全局变量将被保留。

尚不支持浮点类型

指针:取决于TCG目标,指针大小为32位或64位。 TCG_TYPE_PTR类型是TCG_TYPE_I32或TCG_TYPE_I64的别名。

帮助函数:

使用tcg_gen_helper_x_y可以调用任何采用i32,i64或指针类型的函数。默认情况下,在调用帮助函数前,所有全局变量都存储在其规范位置,并且假定该函数可以对其进行修改。默认情况下,允许帮助函数修改CPU状态或引发异常。

可以使用以下功能修饰符来覆盖它:

  • TCG_CALL_NO_READ_GLOBALS表示帮助程序不会直接或通过异常读取全局变量。在调用帮助函数之前,不会将它们保存到规范位置。
  • TCG_CALL_NO_WRITE_GLOBALS表示帮助程序不会修改任何全局变量。它们只会在调用帮助函数之前保存到规范位置,但之后将不会重新加载。
  • TCG_CALL_NO_SIDE_EFFECTS表示如果不使用返回值,则对该函数的调用将被删除。

请注意,TCG_CALL_NO_READ_GLOBALS意味着TCG_CALL_NO_WRITE_GLOBALS

在某些TCG目标(例如x86)上,支持多种调用约定。

分支:

  • 使用指令br跳转到标签。

3.3 代码优化

生成说明时,至少可以依靠以下优化:

  • 简化了单个指令,例如下面代码将被压缩

    and_i32 t0,t0,$ 0xffffffff

在基本块级别进行存活分析。 该信息用于禁止从死变量移动到另一个变量。 它还用于删除计算无效结果的指令。 后者对于QEMU中的条件代码优化特别有用。

在以下示例中:

1
2
3
add_i32 t0,t1,t2
add_i32 t0,t0,$ 1
mov_i32 t0,$ 1

仅保留最后一条指令。

3.4 指令参考

3.4.1 函数调用

call <ret> <params> ptr

调用函数 ‘ptr’ (指针类型)

<ret> 可选地 32 bit or 64 bit 返回值
<params> 可选地 32 bit or 64 bit 参数

3.4.2 Jumps/Labels

set_label $label

在当前程序点定义标签’label’

br $label

跳转到标签

brcond_i32/i64 t0, t1, cond, label

当t0与t1的条件运算cond的结果为true时跳转到label

条件运算cond可以是:

1
2
3
4
5
6
7
8
9
10
11
TCG_COND_EQ
TCG_COND_NE
TCG_COND_LT /* signed */
TCG_COND_GE /* signed */
TCG_COND_LE /* signed */
TCG_COND_GT /* signed */
TCG_COND_LTU /* unsigned */
TCG_COND_GEU /* unsigned */
TCG_COND_LEU /* unsigned */
TCG_COND_GTU /* unsigned */

3.4.3 数学运算

add_i32/i64 t0, t1, t2

t0=t1+t2

sub_i32/i64 t0, t1, t2

t0=t1-t2

neg_i32/i64 t0, t1

t0=-t1 (补码形式)

mul_i32/i64 t0, t1, t2

t0=t1*t2

div_i32/i64 t0, t1, t2

t0=t1/t2 (有符号). 如果被零除或溢出,则行为不确定。

divu_i32/i64 t0, t1, t2

t0=t1/t2 (无符号). 如果被零除,则行为不确定。

rem_i32/i64 t0, t1, t2

t0=t1%t2 (有符号). 如果被零除或溢出,则行为不确定。

remu_i32/i64 t0, t1, t2

t0=t1%t2 (无符号). 如果被零除,则行为不确定。

3.4.4 逻辑运算

and_i32/i64 t0, t1, t2

t0=t1&t2

or_i32/i64 t0, t1, t2

t0=t1|t2

xor_i32/i64 t0, t1, t2

t0=t1^t2

not_i32/i64 t0, t1

t0=~t1

andc_i32/i64 t0, t1, t2

t0=t1&~t2

eqv_i32/i64 t0, t1, t2

t0=(t1^t2), or equivalently, t0=t1^t2

nand_i32/i64 t0, t1, t2

t0=~(t1&t2)

nor_i32/i64 t0, t1, t2

t0=~(t1|t2)

orc_i32/i64 t0, t1, t2

t0=t1|~t2

clz_i32/i64 t0, t1, t2

t0 = t1 ? clz(t1) : t2

ctz_i32/i64 t0, t1, t2

t0 = t1 ? ctz(t1) : t2

3.4.5 移位/旋转

shl_i32/i64 t0, t1, t2

t0=t1 << t2. Unspecified behavior if t2 < 0 or t2 >= 32 (resp 64)

shr_i32/i64 t0, t1, t2

t0=t1 >> t2 (unsigned). Unspecified behavior if t2 < 0 or t2 >= 32 (resp 64)

sar_i32/i64 t0, t1, t2

t0=t1 >> t2 (signed). Unspecified behavior if t2 < 0 or t2 >= 32 (resp 64)

rotl_i32/i64 t0, t1, t2

t1向左旋转t2位
Unspecified behavior if t2 < 0 or t2 >= 32 (resp 64)

rotr_i32/i64 t0, t1, t2

t1向右旋转t2位
Unspecified behavior if t2 < 0 or t2 >= 32 (resp 64)

3.4.6 杂项

mov_i32/i64 t0, t1

t0 = t1

t1移动到t0 (两个操作符类型必须相同).

1
2
3
4
5
6
ext8s_i32/i64 t0, t1
ext8u_i32/i64 t0, t1
ext16s_i32/i64 t0, t1
ext16u_i32/i64 t0, t1
ext32s_i64 t0, t1
ext32u_i64 t0, t1

8, 16 or 32 bit sign/zero 扩展 (both operands must have the same type)

bswap16_i32/i64 t0, t1

16 bit byte swap on a 32/64 bit value. It assumes that the two/six high order
bytes are set to zero.

bswap32_i32/i64 t0, t1

32 bit byte swap on a 32/64 bit value. With a 64 bit value, it assumes that
the four high order bytes are set to zero.

bswap64_i64 t0, t1

64 bit byte swap

discard_i32/i64 t0

指示t0的值之后不再使用,这对强制删除无效代码很有效。

deposit_i32/i64 dest, t1, t2, pos, len

将T2作为位域存入T1,并将结果放入DEST。 位字段由POS / LEN描述,它们是立即值:

LEN - 位域长度
POS - 从LSB开始计算的第一位的位置

例如, deposit_i32 dest, t1, t2, 8, 4 表示 从bit8开始的4-bit位域. 此操作等效于

dest = (t1 & ~0x0f00) | ((t2 << 8) & 0x0f00)

1
2
extract_i32/i64 dest, t1, pos, len
sextract_i32/i64 dest, t1, pos, len

从T1中提取一个位域,并将结果放入DEST。 位字段由POS / LEN描述,它们是即时值,如上用于存款。 对于extract_ *,结果将用零扩展到左侧;否则,结果将扩展为0。 对于sextract_ *,结果将扩展到左侧,并带有pos + len-1的位域符号位的副本。

例如”sextract_i32 dest, t1, 8, 4” 表示从bit8开始的一段4-bit位域. 操作等效于

dest = (t1 << 20) >> 28.

extract2_i32/i64 dest, t1, t2, pos

对于N = {32,64},从t2:t1的串联中提取一个N位数,从pos开始。 tcg_gen_extract2_ {i32,i64}扩展器接受0 <= pos <= N作为输入。 后端代码生成器将看不到0或N作为这些操作码的输入。

extrl_i64_i32 t0, t1

仅对于64位主机,提取输入T1的低32位并将其放入32位输出T0。 根据主机,这可能是一个简单的举动,或者可能需要其他规范化。

extrh_i64_i32 t0, t1

仅对于64位主机,提取输入T1的高32位并将其放入32位输出T0。 根据主机的不同,这可能是简单的转换,或者可能需要其他规范化。

3.4.7 条件移动

setcond_i32/i64 dest, t1, t2, cond

dest = (t1 cond t2)

当t1与t2在条件运算cond的结果为True则设置dest为1,否则为0

movcond_i32/i64 dest, c1, c2, v1, v2, cond

dest = (c1 cond c2 ? v1 : v2)

当t1与t2在条件运算cond的结果为True则设置dest为V1,否则为V2

3.4.8 类型转换

ext_i32_i64 t0, t1
将t1(32位)转换为t0(64位)并进行符号扩展

extu_i32_i64 t0, t1
将t1(32位)转换为t0(64位)并进行0扩展

trunc_i64_i32 t0, t1
将t1(64位)截断为t0(32位)

concat_i32_i64 t0, t1, t2
构造t0(64位),取t1(32位)的低半部分和t2(32位)的高半部分。

concat32_i64 t0, t1, t2
构造t0(64位),取t1(64位)的低半部分和t2(64位)的高半部分。

3.4.9 加载/存放

1
2
3
4
5
6
7
ld_i32/i64 t0, t1, offset
ld8s_i32/i64 t0, t1, offset
ld8u_i32/i64 t0, t1, offset
ld16s_i32/i64 t0, t1, offset
ld16u_i32/i64 t0, t1, offset
ld32s_i64 t0, t1, offset
ld32u_i64 t0, t1, offset

t0 = read(t1 + offset)
从宿主机器内存加载8位,16位,32位或64位带或不带符号扩展名。 偏移量必须为常数。

1
2
3
4
st_i32/i64 t0, t1, offset
st8_i32/i64 t0, t1, offset
st16_i32/i64 t0, t1, offset
st32_i64 t0, t1, offset

write(t0, t1 + offset)
将8、16、32或64位写入宿主机器内存。

所有这些操作码均假定指向的主机内存与全局内存不对应。 在后一种情况下,行为是不可预测的。

3.4.10 多字(multiword)算术

1
2
add2_i32/i64 t0_low, t0_high, t1_low, t1_high, t2_low, t2_high
sub2_i32/i64 t0_low, t0_high, t1_low, t1_high, t2_low, t2_high

与add / sub相似,不同之处在于双字输入T1和T2由两个单字参数组成,并且双字输出T0在两个单字输出中返回。

mulu2_i32/i64 t0_low, t0_high, t1, t2

与mul相似,除了两个无符号输入T1和T2产生完整的双字乘积T0。 后者以两个单字输出返回。

muls2_i32/i64 t0_low, t0_high, t1, t2

与mulu2相似,除了两个输入T1和T2是带符号的。

1
2
mulsh_i32/i64 t0, t1, t2
muluh_i32/i64 t0, t1, t2

分别提供有符号或无符号乘法的高部分。 如果后端未提供mulu2 / muls2,则tcg-op生成器可以获得相同的结果,方法是发出一对操作码mul + muluh / mulsh。

3.4.11 内存屏障

mb <$ arg>

生成target内存屏障指令,以确保由相应的Host内存屏障指令强制执行的内存排序。 后端执行的排序可能比guest要求的排序严格。 它不能更弱。 该操作码采用一个常量参数,该参数是生成适当的屏障指令所必需的。 后端应注意仅在必要时(即,对于SMP来宾和启用MTTCG时)发出目标屏障指令。

guest翻译器应为所有具有顺序副作用的guest指令生成此操作码。

请参阅docs / devel / atomics.txt了解有关内存屏障的更多信息。

3.4.12 32位宿主机器模拟64位客户机器

以下操作码是TCG内部的。 因此,它们将由32位主机代码生成器实现,而不由来宾转换器发出。 它们由“ tcg-op.h”中的内联函数根据需要发出。

brcond2_i32 t0_low,t0_high,t1_low,t1_high,cond,label

与brcond相似,不同之处在于64位值T0和T1由两个32位自变量形成。

setcond2_i32 dest,t1_low,t1_high,t2_low,t2_high,cond

与setcond相似,除了64位值T1和T2由两个32位自变量形成。 结果是一个32位值。

3.4.13 QEMU特定指令

exit_tb t0

退出当前TB,并返回值t0(字类型word type)。

goto_tb index

如果当前TB已链接到该TB,则退出当前TB并跳转到TB索引“index”(常量)。否则,执行下一条指令。只有索引0和1有效,并且每TB每个插槽索引最多只能发布一次tcg_gen_goto_tb。

lookup_and_goto_ptr tb_addr

查找TB地址(’tb_addr’),如果有效,则跳转到该地址。如果无效,请跳至TCG尾声以返回到exec循环。

此操作是可选的。如果TCG后端未实现goto_ptr操作码,则发出此操作等同于发出exit_tb(0)。

qemu_ld_i32 / i64 t0,t1,flag,memidx

qemu_st_i32 / i64 t0,t1,flag,memidx

将guest地址t1的数据加载到t0,或将数据存储在guest地址t1的t0中。 _i32 / _i64大小仅适用于输入/输出寄存器t0的大小。地址t1始终根据来宾的大小而定,并且存储器操作的宽度由标志控制。

如果处理32位主机上的64位数量,则t0和t1都可以分成低位序排序的寄存器对。

memidx选择要使用的qemu tlb索引(例如,用户或内核访问权限)。这些标志是MemOp位,用于选择内存访问的符号,宽度和字节序。

对于32位主机,保证qemu_ld / st_i64仅与标志中指定的64位内存访问。

宿主机器向量操作

所有向量操作都有两个参数:TCGOP_VECLTCGOP_VECE。前者以log2 64位为单位指定向量的长度。后者以log2 8位为单位指定元素的长度(如果适用)。

例如: VECL = 1-> 64 << 1-> v128,而VECE = 2-> 1 << 2-> i32

1
2
3
mov_vec v0,v1
ld_vec v0,t1
st_vec v0,t1

移动,加载和存储。

dup_vec v0,r1

通过V0将R1的低N位复制到VECL / VECE副本中。

dupi_vec v0,c

同样,对于常量。
较小的值将由扩展器复制到主机寄存器的大小。

dup2_vec v0,r1,r2

跨V0将r2:r1复制到VECL / 64副本中。该操作码仅适用于32位主机。

add_vec v0,v1,v2

v0 = v1 + v2,在向量中的元素中。

sub_vec v0,v1,v2

同样,v0 = v1-v2。

mul_vec v0,v1,v2

同样,v0 = v1 * v2。

neg_vec v0,v1

同样,v0 = -v1。

abs_vec v0,v1

同样,v0 = v1 <0? -v1:v1,位于向量中的元素中。

smin_vec:

umin_vec:

同样,对于有符号和无符号元素类型,v0 = MIN(v1,v2)。

smax_vec:

umax_vec:

同样,对于有符号和无符号元素类型,v0 = MAX(v1,v2)。

1
2
3
4
ssadd_vec:
sssub_vec:
usadd_vec:
ussub_vec:

有符号和无符号饱和加法和减法。如果在元素类型内无法表示真实结果,则将元素设置为该类型的最小值或最大值。

1
2
3
4
5
6
and_vec v0,v1,v2
or_vec v0,v1,v2
xor_vec v0,v1,v2
andc_vec v0,v1,v2
orc_vec v0,v1,v2
not_vec v0,v1

同样,有和没有补码的逻辑运算。请注意,VECE未使用。

shli_vec v0,v1,i2

shls_vec v0,v1,s2

将所有元素从v1移至标量i2 / s2。即

1
2
3
4
for(i = 0; i <VECL / VECE; ++ i)
{
v0 [i] = v1 [i] << s2;
}
1
2
3
4
shri_vec v0,v1,i2
sari_vec v0,v1,i2
shrs_vec v0,v1,s2
sars_vec v0,v1,s2

对于逻辑和算术右移也是如此。

shlv_vec v0,v1,v2

将元素从v1移到v2。即

1
2
3
4
for(i = 0; i <VECL / VECE; ++ i)
{
v0 [i] = v1 [i] << s2;
}

shrv_vec v0,v1,v2

sarv_vec v0,v1,v2

对于逻辑和算术右移也是如此。

cmp_vec v0,v1,v2,cond

按元素比较向量,将-1表示为true,将0表示为false。

bitsel_vec v0,v1,v2,v3

按位选择,v0 =(v2&v1)| (v3&〜v1),遍及整个向量。

cmpsel_vec v0,c1,c2,v3,v4,cond

根据比较结果选择元素:

1
2
3
for(i = 0; i <n; ++ i){
v0 [i] =(c1 [i] cond c2 [i])? v3 [i]:v4 [i]。
}

注意1:当已知最后一个操作数为常量时,会定义一些快捷方式(例如,addi用于add,movi用于mov)。

注意2:使用TCG时,绝不能直接生成操作码,因为其中某些操作码可能无法用作“实际”操作码。 始终使用函数tcg_gen_xxx(args)。

4. 后端

tcg-target.h包含目标特定的定义。 tcg-target.inc.c包含目标特定代码; 它由tcg / tcg.c包含,而不是独立的C文件。

4.1 假设

目标字大小(TCG_TARGET_REG_BITS)预计为32位或64位。期望指针具有与word相同的大小。

在32位目标上,所有64位操作都将转换为32位。必须执行一些特定的操作才能允许它(请参阅add2_i32,sub2_i32,brcond2_i32)。

在64位目标上,使用以下操作在32和64位寄存器之间传输值:

  • trunc_shr_i64_i32
  • ext_i32_i64
  • extu_i32_i64

它们可确保在将值从32位寄存器移到64位寄存器时正确截断或扩展这些值,反之亦然。请注意,trunc_shr_i64_i32是可选操作。如果满足以下所有条件,则无需实施:

  • 64位寄存器可以保存32位值
  • 64位寄存器中的32位值不需要保持零或符号扩展
  • 所有32位TCG操作都忽略了64位寄存器的高位

此版本不支持浮点运算。代码生成器的先前版本完全支持它们,但是最好先集中于整数运算。

4.2 约束条件

类似于GCC的约束用于定义每条指令的约束。此版本不支持内存限制。别名在输入操作数中与GCC一样指定。

即使没有显式别名,同一寄存器也可用于输入和输出。如果操作扩展到多个目标指令,则必须注意避免破坏输入值。支持GCC样式的“早期内容early clobber”输出,并带有’&’。

目标target可以定义特定的寄存器或常量约束。如果操作使用不允许所有常量的常量输入约束,则它还必须接受寄存器以进行回退。一般将约束“ i”定义为接受任何常数。约束“ r”不是通用定义的,但是每个后端始终使用它来表示所有寄存器。

movi_i32movi_i64操作必须接受任何常量。

mov_i32mov_i64操作必须接受任何相同类型的寄存器。

ld / st / sti指令必须接受带符号的32位常量偏移量。这可以通过保留特定寄存器来实现,如果偏移太大,则可以在其中计算地址。

ld / st指令必须接受任何目标(ld)或源(st)寄存器。

如果sti指令无法存储给定常量,则它可能会失败。

4.3 函数调用假设

  • 参数和返回值的唯一支持类型为:32位和64位整数和指针。
  • 堆栈向下生长。
  • 前N个参数在寄存器中传递。
  • 下一个参数通过将它们存储为words在堆栈上传递。
  • 调用期间某些寄存器被破坏。
  • 该函数可以在寄存器中返回0或1的值。 在32位目标上,函数必须能够为64位返回类型返回寄存器中的2个值。

5 建议编码规则

  • 使用全局变量来表示QEMU CPU状态的经常修改的部分,例如整数寄存器和条件代码。 TCG将能够使用宿主机器寄存器来存储它们。

  • 避免将全局变量存储在固定寄存器中。它们只能用于存储指向CPU状态的指针,也可以用于存储指向寄存器窗口的指针。

  • 使用临时变量。仅在真正需要时使用本地临时变量,例如当您需要在跳转后使用一个值时。本地临时对象在当前TCG实现中带来了性能上的损失:它们的内容在每个基本块的末尾保存到内存中。

  • 不再使用的免费临时文件和本地临时文件(tcg_temp_free)。由于tcg_const_x()还会创建一个临时文件,因此在使用它后应将其释放。释放临时文件不会生成更好的代码,但是会减少TCG的内存使用量和翻译速度。

  • 不要为复杂或很少使用的guest指令而使用帮助函数。使用TCG实现guest指令时,使用大约二十多个TCG指令几乎没有性能优势。请注意,这种经验法则更适用于执行复杂逻辑或算术的帮助函数,其中C编译器有能力很好地进行优化。在指令主要执行加载和存储的情况下,它就不那么重要了,在那些情况下,对于较长的序列,内联TCG可能仍然更快。

  • exec-all.h中的MAX_OP_PER_INSTR设置了每个来宾指令可生成的TCG指令数量的硬限制-不能超过此上限,而不会冒缓冲区溢出的风险。

  • 如果您知道TCG无法证明给定的全局变量在给定的程序点处“死亡”,则使用“丢弃”指令。 x86 guest虚拟机使用它来改善条件代码的优化。