vxasm 发表于 2009-08-25 18:16

QEMU技术分析2 - TCG(Tiny Code Generator)基本原理

title: QEMU技术分析2 - TCG(Tiny Code Generator)基本原理
author: vxasm (http://hi.baidu.com/vxasm)

从QEMU-0.10.0开始,TCG成为QEMU新的翻译引擎,做到了“真正”的动态翻译(从某种意义上说,旧版本是从编译后的目标文件中复制二进制指令)。TCG的全称为“Tiny Code Generator”,QEMU的作者Fabrice Bellard在TCG的说明文件中写到,TCG起源于一个C编译器后端,后来被简化为QEMU的动态代码生成器(Fabrice Bellard以前还写过一个很牛的编译器TinyCC)。实际上TCG的作用也和一个真正的编译器后端一样,主要负责分析、优化Target代码以及生成Host代码。

        Target指令 ----> TCG ----> Host指令

以下的讲述以X86平台为例(Host和Target都是X86)。

我在上篇文章中讲到,动态翻译的基本思想就是把每一条Target指令切分成为若干条微操作,每条微操作由一段简单的C代码来实现,运行时通过一个动态代码生成器把这些微操作组合成一个函数,最后执行这个函数,就相当于执行了一条Target指令。这种思想的基础是因为CPU指令都是很规则的,每条指令的长度、操作码、操作数都有固定格式,根据前面就可推导出后面,所以只需通过反汇编引擎分析出指令的操作码、输入参数、输出参数等,剩下的工作就是编码为目标指令了。

那么现在的CPU指令这么多,怎么知道要分为哪些微操作呢?其实CPU指令看似名目繁多,异常复杂,实际上多数指令不外乎以下几大类:数据传送、算术运算、逻辑运算、程序控制;例如,数据传送包括:传送指令(如MOV)、堆栈操作(PUSH、POP)等,程序控制包括:函数调用(CALL)、转移指令(JMP)等;

基于此,TCG就把微操作按以上几大类定义(见tcg/i386/tcg-target.c),例如:其中一个最简单的函数 tcg_out_movi 如下:

// tcg/tcg.c
static inline void tcg_out8(TCGContext *s, uint8_t v)
{
    *s->code_ptr++ = v;
}

static inline void tcg_out32(TCGContext *s, uint32_t v)
{
    *(uint32_t *)s->code_ptr = v;
    s->code_ptr += 4;
}

// tcg/i386/tcg-target.c
static inline void tcg_out_movi(TCGContext *s, TCGType type,
                              int ret, int32_t arg)
{
    if (arg == 0) {
      /* xor r0,r0 */
      tcg_out_modrm(s, 0x01 | (ARITH_XOR << 3), ret, ret);
    } else {
      tcg_out8(s, 0xb8 + ret); // 输出操作码,ret是寄存器索引
      tcg_out32(s, arg); // 输出操作数
    }
}

0xb8 - 0xbf 正是x86指令中的 mov R, Iv 系列操作的16进制码,所以,tcg_out_movi 的功能就是输出 mov 操作的指令码到缓冲区中。可以看出,TCG在生成目标指令的过程中是采用硬编码的,因此,要让TCG运行在不同的Host平台上,就必须为不同的平台编写微指令函数。

接下来,我还是以一条Target指令 jmp f000:e05b 来讲述它是如何被翻译成Host指令的。其中几个关键变量的定义如下:

gen_opc_buf:操作码缓冲区
gen_opparam_buf:参数缓冲区
gen_code_buf:存放翻译后指令的缓冲区
gen_opc_ptr、gen_opparam_ptr、gen_code_ptr三个指针变量分别指向上述缓冲区。

jmp f000:e05b 的编码是:EA 5B E0 00 F0,首先是disas_insn()函数翻译指令,当碰到第1个字节EA,分析可知这是一条16位无条件跳转指令,因此依次从后续字节中得到offset和selector,然后分为如下微指令操作:

gen_op_movl_T0_im(selector);
gen_op_movl_T1_imu(offset);
gen_op_movl_seg_T0_vm(R_CS);
gen_op_movl_T0_T1();
gen_op_jmp_T0();

这几个微操作的函数定义如下(功能可看注释):

static inline void gen_op_movl_T0_im(int32_t val)
{
    tcg_gen_movi_tl(cpu_T, val); // 相当于 cpu_T = val
}

static inline void gen_op_movl_T1_imu(uint32_t val)
{
    tcg_gen_movi_tl(cpu_T, val); // 相当于 cpu_T = val
}

static inline void gen_op_movl_seg_T0_vm(int seg_reg)
{
    tcg_gen_andi_tl(cpu_T, cpu_T, 0xffff); // cpu_T = cpu_T&0xffff
    tcg_gen_st32_tl(cpu_T, cpu_env,
                  offsetof(CPUX86State,segs.selector)); // the value of cpu_T store to the 'offset' of cpu_env
    tcg_gen_shli_tl(cpu_T, cpu_T, 4); // cpu_T = cpu_T<<4
    tcg_gen_st_tl(cpu_T, cpu_env,
                  offsetof(CPUX86State,segs.base)); // the value of cpu_T store to the 'offset' of cpu_env
}

static inline void gen_op_movl_T0_T1(void)
{
    tcg_gen_mov_tl(cpu_T, cpu_T); // cpu_T = cpu_T
}

static inline void gen_op_jmp_T0(void)
{
    tcg_gen_st_tl(cpu_T, cpu_env, offsetof(CPUState, eip)); // // the value of cpu_T store to the 'offset' of cpu_env
}

其中,cpu_T、cpu_T和前面讲过的T0、T1功能一样,都是用来临时存储的变量。在32位目标机上,tcg_gen_movi_tl 就是 tcg_gen_op2i_i32 函数,它的定义如下:

static inline void tcg_gen_op2i_i32(int opc, TCGv_i32 arg1, TCGArg arg2)
{
    *gen_opc_ptr++ = opc;
    *gen_opparam_ptr++ = GET_TCGV_I32(arg1);
    *gen_opparam_ptr++ = arg2;
}

static inline void tcg_gen_movi_i32(TCGv_i32 ret, int32_t arg)
{
    tcg_gen_op2i_i32(INDEX_op_movi_i32, ret, arg);
}

gen_opparam_buf 是用来存放操作数的缓冲区,它的存放顺序是:第1个4字节代表s->temps(用来存放目标值的数组,即输出参数)的索引,第2个4字节及之后字节代表输入参数,对它的具体解析过程可见 tcg_reg_alloc_movi 函数,示例代码如下:

TCGTemp *ots;
tcg_target_ulong val;

ots = &s->temps];
val = args;

ots->val_type = TEMP_VAL_CONST;
ots->val = val; // 把输入值暂时存放在ots结构中

接下来,根据 gen_opc_buf 保存的操作码列表,gen_opparam_buf 保存的参数列表,以及TCGContext结构,经过 tcg_gen_code_common 函数调用,jmp f000:e05b 生成的最终指令如下:

099D0040 B8 00 F0 00 00   mov         eax,0F000h
099D0045 81 E0 FF FF 00 00 and         eax,0FFFFh
099D004B 89 45 50         mov         dword ptr ,eax
099D004E C1 E0 04         shl         eax,4
099D0051 89 45 54         mov         dword ptr ,eax
099D0054 B8 5B E0 00 00   mov         eax,0E05Bh
099D0059 89 45 20         mov         dword ptr ,eax
099D005C 31 C0            xor         eax,eax
099D005E E9 25 5D CA 06   jmp         _code_gen_prologue+8 (10675D88h) /* 返回 */

从上面可以看出,生成的Host代码很简洁,在调试中,把QEMU执行Target指令的过程和Bochs比较是一件很有趣的事情,当然,这只是设计理念的不同,而并没有技术上的优劣之分。

[ 本帖最后由 vxasm 于 2009-8-26 10:30 编辑 ]

mik 发表于 2009-08-25 23:25

  你不应该使用“微指令”这一词,使人容易产生混淆,感觉和 processor 的微码混在一起。
微码是 processor 内部的产物,x86 指令经过 processor 内部的 decode 单元译码后产生的最小 processor 执行命令。

  使用“微操作”一词比较合适一些。比较符合作者说的 “tiny code” 本质


  像你上面所说的 jmp far f000:e05b 指令

作者是这样处理:

(1) 将 f000 放入 buf

(2) 将 e05b 放入 buf

(3) jmp       ( buf 里存放着 f000:e05b)

-------------------------------------------------------------------

通过上述一些操作,实现 jmp f000:e05b 的结果。而并不是将 jmp far 指令分解为微指令或微码。

指令:jmp f000:e05b  ----> 原本的 opcode 是 EA

经过等价效果转换为 ----->opcode 为 FF /05 ( jmp )


不过,或许作者就是通过这样达到他期望的动态效果吧。





我所认为的分解应该是:


像下面这条简单的 c 语句:

   a = b + c;


1、 确定两个主要操作:

(1) "+" 加法操作。

(2) "=" 赋值操作。

所以,我若分解的话,会分解为:

int add_2ops(op1, op2)
{
      source_reg = get_reg();            /* 经过分析取得可用 reg ID */
      dest_reg = get_reg();               /* 得取可用的 reg ID */

      ... ...
      /* 调用由两个operands ID 合成而来的函数 */
      add_2ops_generator(op1, op2);

      ... ...

}

void move_dword()
{
  ... ....
}


比较实在的根据语句原意分解为相应的汇编码或机器码

vxasm 发表于 2009-08-26 10:27

原帖由 mik 于 2009-8-25 23:25 发表 http://linux.chinaunix.net/bbs/images/common/back.gif
  你不应该使用“微指令”这一词,使人容易产生混淆,感觉和 processor 的微码混在一起。
微码是 processor 内部的产物,x86 指令经过 processor 内部的 decode 单元译码后产生的最小 processor 执行命令。
...


感谢mik老大把加为精华贴。从思路上说,我觉得QEMU这种分解指令的做法和处理器的微码有相通之处,但有可能引起误解,感谢你的建议,用“微操作”来表示的确是个更好的词,我待会改一下。

不过你说的

jmp far f000:e05b 指令,作者是这样处理:

(1) 将 f000 放入 buf

(2) 将 e05b 放入 buf

(3) jmp       ( buf 里存放着 f000:e05b)

-------------------------------------------------------------------

通过上述一些操作,实现 jmp f000:e05b 的结果。而并不是将 jmp far 指令分解为微指令或微码。

指令:jmp f000:e05b  ----> 原本的 opcode 是 EA

经过等价效果转换为 ----->opcode 为 FF /05 ( jmp )

这种说法是不对的,对于Target机的JMP,Host不会去执行真正的跳转指令,而只是简单的将目标地址放到EIP中而已。QEMU源码中还有很多细节,说起来太啰嗦,所以前面我没有讲到。在QEMU中维护着一个称为 CPUState 的数据结构,用来保存Target机的状态,包括了Target机CPU的所有寄存器,像EAX,EBP,ESP,CS,EIP,EFLAGS等,像上面说的jmp f000:e05b指令,它分解为如下微操作:

gen_op_movl_T0_im(selector);
gen_op_movl_T1_imu(offset);
gen_op_movl_seg_T0_vm(R_CS);
gen_op_movl_T0_T1();
gen_op_jmp_T0();

我用env变量来表示 CPUState 结构,它总是代表着Target机的当前状态。那么上面这几条微操作的意义概括说,是:把selector放到env.cs,把offset放到env.eip。就是这么简单,每次解析指令时,总是以 env.cs+env.eip 为开始地址。

[ 本帖最后由 vxasm 于 2009-8-26 10:53 编辑 ]

linxer 发表于 2009-09-01 02:20

谢谢楼主分享学习
页: [1]
查看完整版本: QEMU技术分析2 - TCG(Tiny Code Generator)基本原理