QEMU作为一款emulator进行模拟的主要方式是binary translation,将目标代码转换成TCG IR再转换成宿主机的代码执行,于是在中间TCG生成时就可以通过插入一些代码来完成插桩的任务。而要完成这一任务首先我们得知道如何在TCG中插入一个helper.
TCG中的helper函数 TCG全称是Tiny Code Generator,实际规定的操作并不多。为了能够实现比较复杂的CPU功能,除了JIT出的宿主机代码本身外,qemu自身还带有一些与相关架构关系比较紧密的函数供JIT的代码调用,这部分函数代码就是helper函数。
以QEMU 4.2.0版本为例,对x86指令进行翻译的代码位于 target/i386/translate.c
,7235行
1 2 3 4 5 6 7 8 9 10 case 0x105 : gen_update_cc_op(s); gen_jmp_im(s, pc_start - s->cs_base); gen_helper_syscall(cpu_env, tcg_const_i32(s->pc - pc_start)); gen_eob_worker(s, false , true ); break ;
这一段是对syscall
指令的翻译,其中有一条 gen_helper_syscall
函数调用,该函数会在tcg代码中插入一条call的backend-ops,目标是 helper_syscall
函数。该函数位于 target/i386/seg_helper.c
中
1 2 3 4 5 6 7 8 9 10 11 void helper_syscall (CPUX86State *env, int next_eip_addend) { int selector; if (!(env->efer & MSR_EFER_SCE)) { raise_exception_err_ra(env, EXCP06_ILLOP, 0 , GETPC()); } selector = (env->star >> 32 ) & 0xffff ; if (env->hflags & HF_LMA_MASK) { int code64; ...
所以当程序执行到syscall指令时,就会进入到 helper_syscall
函数中,该函数根据CPU的状态,寻找syscall的入口点,并将eip设置过去,进入内核态执行。
如果类比 PIN 的话,gen_helper_syscall
就相当于 INS_InsertCall
,是在翻译过程中使用的;而 helper_syscall
则相当于分析函数,是在运行时使用的。
添加helper 举个例子,我们想为x86加入一个helper函数,首先需要修改 target/i386/helper.h
,为syscall的helper定义如下
1 2 3 4 5 6 DEF_HELPER_1 (sysenter, void, env) DEF_HELPER_2 (sysexit, void, env, int) #ifdef TARGET_X86_64DEF_HELPER_2 (syscall, void, env, int) DEF_HELPER_2 (sysret, void, env, int) #endif
之后需要在某个位置(target/i386/helper.c
或target/i386/seg_helper.c
等)实现 helper_syscall
函数,而且参数需要匹配使用 DEF_HELPER_2
宏的定义。这里 DEF_HELPER_2(syscall, void, env, int)
的2表示函数有2个参数,syscall
是helper的名称,void
是返回类型,env
与int
是2个参数的类型,这与helper_syscall
的定义也是相符的。
QEMU的helper实现中,有时会直接用 helper_syscall
的形式,有时会借助 HELPER
宏,写成 void HELPER(syscall)
的形式,二者的效果是一样的。
TCG的定义机制 通过上面的分析我们知道,要添加一个helper需要实现两个函数 gen_helper_xxxx
和 helper_xxxx
(xxxx是helper的名称),而仅仅在 helper.h
中添加一行的定义就声明了两个函数,这是如何做到的呢?
我们看 translate.c
的头部29-30行
1 2 #include "exec/helper-proto.h" #include "exec/helper-gen.h"
在 exec/helper-proto.h
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define DEF_HELPER_FLAGS_5(name, flags, ret, t1, t2, t3, t4, t5) \ dh_ctype(ret) HELPER(name) (dh_ctype(t1), dh_ctype(t2), dh_ctype(t3), \ dh_ctype(t4), dh_ctype(t5)); #define DEF_HELPER_FLAGS_6(name, flags, ret, t1, t2, t3, t4, t5, t6) \ dh_ctype(ret) HELPER(name) (dh_ctype(t1), dh_ctype(t2), dh_ctype(t3), \ dh_ctype(t4), dh_ctype(t5), dh_ctype(t6)); #include "helper.h" #include "trace/generated-helpers.h" #include "tcg-runtime.h" #include "plugin-helpers.h" #undef DEF_HELPER_FLAGS_0 #undef DEF_HELPER_FLAGS_1 #undef DEF_HELPER_FLAGS_2 #undef DEF_HELPER_FLAGS_3 #undef DEF_HELPER_FLAGS_4 #undef DEF_HELPER_FLAGS_5 #undef DEF_HELPER_FLAGS_6
首先定义 DEF_HELPER_FLAGS_N
的宏,这些宏展开后就能够声明 helper_xxxx
,接下来再包含 helper.h
就完成了它的声明。在文件结束时使用 #undef
再将这些宏给取消了。
接着 exec/helper-gen.h
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define DEF_HELPER_FLAGS_6(name, flags, ret, t1, t2, t3, t4, t5, t6) \ static inline void glue(gen_helper_, name)(dh_retvar_decl(ret) \ dh_arg_decl(t1, 1), dh_arg_decl(t2, 2), dh_arg_decl(t3, 3), \ dh_arg_decl(t4, 4), dh_arg_decl(t5, 5), dh_arg_decl(t6, 6)) \ { \ TCGTemp *args[6] = { dh_arg(t1, 1), dh_arg(t2, 2), dh_arg(t3, 3), \ dh_arg(t4, 4), dh_arg(t5, 5), dh_arg(t6, 6) }; \ tcg_gen_callN(HELPER(name), dh_retvar(ret), 6, args); \ } #include "helper.h" #include "trace/generated-helpers.h" #include "trace/generated-helpers-wrappers.h" #include "tcg-runtime.h" #include "plugin-helpers.h" #undef DEF_HELPER_FLAGS_0 #undef DEF_HELPER_FLAGS_1 #undef DEF_HELPER_FLAGS_2 #undef DEF_HELPER_FLAGS_3 #undef DEF_HELPER_FLAGS_4 #undef DEF_HELPER_FLAGS_5 #undef DEF_HELPER_FLAGS_6
这里又将 DEF_HELPER_FLAGS_N
展开为了 gen_helper_xxxx
的定义,并且在直接实现了该函数,使用 tcg_gen_callN
来插入对helper函数的调用。下面再次包含了 helper.h
,这就完成了2个函数的定义,然后用户自己再实现 helper_xxxx
就可以了。
值得一提的是,上面分析仅仅是x86的helper函数,每个架构都有自己的 helper.h
。如果想添加所有架构通用的helper函数,可以在 tcg-runtime.h
中添加,位于 accel/tcg/tcg-runtime.h
.
另外,tcg_gen_callN
是在 tcg/tcg.c
中实现的,这个函数在开头会从一个helper的hashtable来获得相关的信息
1 2 3 4 5 6 7 8 9 10 void tcg_gen_callN (void *func, TCGTemp *ret, int nargs, TCGTemp **args) { int i, real_args, nb_rets, pi; unsigned sizemask, flags; TCGHelperInfo *info; TCGOp *op; info = g_hash_table_lookup(helper_table, (gpointer)func); flags = info->flags; sizemask = info->sizemask;
这个hashtable是在tcg初始化的时候填的,在同一文件中
1 2 3 4 static const TCGHelperInfo all_helpers[] = {#include "exec/helper-tcg.h" }; static GHashTable *helper_table;
又包含了 exec/helper-tcg.h
,不出意外地这个header跟前面同样的套路,只不过这次是展开成数组元素。
1 2 3 4 5 6 7 8 9 10 #define DEF_HELPER_FLAGS_6(NAME, FLAGS, ret, t1, t2, t3, t4, t5, t6) \ { .func = HELPER(NAME), .name = str(NAME), \ .flags = FLAGS | dh_callflag(ret), \ .sizemask = dh_sizemask(ret, 0) | dh_sizemask(t1, 1) \ | dh_sizemask(t2, 2) | dh_sizemask(t3, 3) | dh_sizemask(t4, 4) \ | dh_sizemask(t5, 5) | dh_sizemask(t6, 6) }, #include "helper.h" #include "trace/generated-helpers.h" #include "tcg-runtime.h"
这就意味着,如果自己定义和实现了 helper_xxxx
和 gen_helper_xxxx
(没有在 helper.h
或者 tcg-runtime.h
中声明),并想用 tcg_gen_callN
来生成调用helper代码的话,就会因为在hashtable中找不到对应的helper而导致QEMU崩溃。
小结 QEMU在tcg helper这块的设计还是挺trick的,本来C工程的原则是得避免同一个header包含多次,这里反而利用了这一点来进行多样化的声明,有点意思。
然而学会了插入helper才只是插桩之路的第一步,接下来还得深入了解TCG的实现机制和有关的函数,才能定制化自己的分析功能。
Reference