QEMU技术分析2

发布于 2022-09-18 02:31:03 字数 6529 浏览 13 评论 0

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[0], val); // 相当于 cpu_T[0] = val
}

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

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

static inline void gen_op_movl_T0_T1(void)
{
    tcg_gen_mov_tl(cpu_T[0], cpu_T[1]); // cpu_T[0] = cpu_T[1]
}

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

其中,cpu_T[0]、cpu_T[1]和前面讲过的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[args[0]];
val = args[1];

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 [ebp+50h],eax
099D004E C1 E0 04         shl         eax,4
099D0051 89 45 54         mov         dword ptr [ebp+54h],eax
099D0054 B8 5B E0 00 00   mov         eax,0E05Bh
099D0059 89 45 20         mov         dword ptr [ebp+20h],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 编辑 ]

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

初见终念 2022-09-25 02:31:03

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

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

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

作者是这样处理:

(1) 将 f000 放入 buf

(2) 将 e05b 放入 buf

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

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

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

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

经过等价效果转换为 ----->  opcode 为 FF /05 ( jmp [mem16:16])

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

我所认为的分解应该是:

像下面这条简单的 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[OP_ID(source_reg)|OP_ID(dest_reg)](op1, op2);

      ... ...

}

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

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

只有一腔孤勇 2022-09-25 02:31:03

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

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

不过你说的

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

(1) 将 f000 放入 buf

(2) 将 e05b 放入 buf

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

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

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

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

经过等价效果转换为 ----->  opcode 为 FF /05 ( jmp [mem16:16])

这种说法是不对的,对于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 编辑 ]

吻安 2022-09-25 02:31:03

谢谢楼主分享  学习

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文