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: /* syscall */
/* XXX: is it usable in real mode ? */
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));
/* TF handling for the syscall insn is different. The TF bit is checked
after the syscall insn completes. This allows #DB to not be
generated after one has entered CPL0 if TF is set in FMASK. */
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_64
DEF_HELPER_2(syscall, void, env, int)
DEF_HELPER_2(sysret, void, env, int)
#endif

之后需要在某个位置(target/i386/helper.ctarget/i386/seg_helper.c等)实现 helper_syscall 函数,而且参数需要匹配使用 DEF_HELPER_2 宏的定义。这里 DEF_HELPER_2(syscall, void, env, int) 的2表示函数有2个参数,syscall 是helper的名称,void是返回类型,envint是2个参数的类型,这与helper_syscall的定义也是相符的。

QEMU的helper实现中,有时会直接用 helper_syscall 的形式,有时会借助 HELPER 宏,写成 void HELPER(syscall) 的形式,二者的效果是一样的。

TCG的定义机制

通过上面的分析我们知道,要添加一个helper需要实现两个函数 gen_helper_xxxxhelper_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_xxxxgen_helper_xxxx(没有在 helper.h 或者 tcg-runtime.h 中声明),并想用 tcg_gen_callN 来生成调用helper代码的话,就会因为在hashtable中找不到对应的helper而导致QEMU崩溃。

小结

QEMU在tcg helper这块的设计还是挺trick的,本来C工程的原则是得避免同一个header包含多次,这里反而利用了这一点来进行多样化的声明,有点意思。

然而学会了插入helper才只是插桩之路的第一步,接下来还得深入了解TCG的实现机制和有关的函数,才能定制化自己的分析功能。

Reference