qemu plugin基本概念以及使用
qemu tcg支持用插件的方式为qemu新增功能,qemu源码里作为示例自带了几个插件,我们先
编译使用下qemu自带的cache插件。
一般情况下qemu tcg是不模拟cache的,qemu自带了一个简单模拟cache的插件。我们在配置
qemu的时候要带上–enable-plugins,编译完qemu后,进入qemu/build/contrib/plugins/,
在这个目录下运行make,之后可以看见qemu的自带的plugin都编译出了,其中就有cache的
plugin: libcache.so。
按如下运行命令,可以带着cache plugin运行qemu,-d plugin输出plugin里的打印信息,
-D指定打印信息输出的文件:
1 | qemu-system-riscv64 -m 256m -nographic -machine virt \ |
如上运行qemu后,linux内核启动从1s到了30s,可见qemu带plugin运行的速度是很慢的。
qemu plugin机制分析
qemu要实现plugin就必然向外,也就是向plugin提供一组API接口,plugin使用这组API向qemu
注册以及获取qemu模拟的guest的信息。qemu内部为了支持plugin机制也会增加plugin的核心
实现代码。
首先我们从qemu user mode入手看下qemu是如何解析命令行里输入的plugin so的。
1 | main |
可以看见qemu user mode会把解析到的每个plugin的信息放到qemu_plugin_desc,再把所有
的qemu_plugin_desc保存到一个叫plugins的全局链表里。
main函数里随后会使用qemu_plugin_load_list加载plugin so:
1 | qemu_plugin_load_list() |
qemu内部对一个plugin的信息保存在qemu_plugin_ctx里,qemu全局的plugin信息保存到
struct qemu_plugin_state plugin里,注意上面的qemu_plugin_desc保存的只是plugin对应
的文件路径、参数等信息。struct qemu_plugin_state plugin里会用链表和哈希表分别记录
所有plugin。
qemu针对每个plugin,打开对应的动态库,执行动态库里名为qemu_plugin_install的函数。
每个plugin必须实现这个注册接口。
我们以libcache看看plugin怎么实现qemu_plugin_install。
1 | /* qemu/contrib/plugins/cache.c */ |
这个函数做了一些自己plugin的初始化后,调用了qemu plugin API注册了vcpu_tb_trans和
plugin_exit两个函数。所谓注册,就是把这两个函数保存到了plugin对应qemu_plugin_ctx
(后面简称ctx)的qemu_plugin_cb(后面简称cb)里,注意ctx里的cb是个数组,不同的数组项
描述不同的event,比如如上的QEMU_PLUGIN_EV_VCPU_TB_TRANS就是一个event,qemu在翻译
执行主流程里会调用这些注册的回调函数。
我们可以先观察下如上vcpu_tb_trans的行为,去掉其中业务相关的逻辑,其主逻辑如下:
1 | vcpu_tb_trans() |
这个函数对于一个tb,调用qemu plugin的API得到guest指令数目、每个指令相关信息,并
针对每条guest指令使用qemu plugin API注册相关的回调函数,比如这里对每条guest指令
注册了vcpu_mem_access和vcpu_insn_exec两个函数。不同注册函数注册的回调函数在qemu
翻译执行主循环的不同位置被触发,plugin需要根据API的语意使用API注册回调函数。
我们进一步分析qemu内部是怎么实现回调函数的注册和触发的。
1 | void qemu_plugin_register_vcpu_mem_cb(struct qemu_plugin_insn *insn, |
从qemu_plugin_insn的内部结构可以看出来,针对一个guest指令,plugin可以注册两大类
回调函数:PLUGIN_CB_MEM和PLUGIN_CB_INSN,每一类里又分: PLUGIN_CB_REGULAR和PLUGIN_CB_INLINE。
被注册的函数以及相关的参数统统保存在insn的对应cb里(qemu_plugin_dyn_cb)。这里只是
保存了相关注册函数信息,被注册的函数还没有和qemu主流程关联在一起,和qemu主流程关
联的过程还是在qemu主流程里实现。
整个qemu翻译的主流程中被插入了plugin的桩函数以及桩函数的替换逻辑:
1 | /* qemu/accel/tcg/translator.c */ |
plugin_gen_tb_start/plugin_gen_insn_start/plugin_gen_insn_end用来插入桩函数,
plugin_gen_tb_end主要用来做桩函数的替换。
我们顺序看一个plugin_gen_insn_start的处理。可以看见,这里插入了一些中间码,大概的
情况是:
1 | plugin_cb_start PLUGIN_GEN_FROM_INSN, PLUGIN_GEN_ENABLE_MEM_HELPER |
如上的plugin_vcpu_udata_cb是一个空的桩函数。
qemu在plugin_gen_tb_end把plugin注册的回调函数插入qemu翻译执行逻辑里,使用的基本方
法就是使用qemu_plugin_insn中保存的回调函数替换如上的桩函数。
1 | plugin_gen_tb_end |
qemu_plugin_tb_trans_cb扫描qemu全局的plugin,针对每个plugin,调用之前注册的
QEMU_PLUGIN_EV_VCPU_TB_TRANS event对应的回调函数。具体到上面的cache plugin就是
其中的vcpu_tb_trans函数。
我们就用cache的vcpu_tb_trans继续分析,这个函数里最主要的针对tb里的每个guest指令
调用qemu plugin API注册回调函数,如上,其实这里的注册就是把回调函数保存到guest指
令insn结构体里,我们具体看其中一个:
1 | qemu_plugin_register_vcpu_mem_cb(insn, vcpu_mem_access, QEMU_PLUGIN_CB_NO_REGS, rw, data); |
plugin_gen_inject用注册的回调函数替换掉如上call中间码里的函数地址,并对输入参数做
必要的调整:
1 | plugin_gen_inject(ptb) |
如上代码识别plugin_tb_start开头的一段中间码,然后做匹配位置的函数替换以及参数生成。
还是看如上PLUGIN_GEN_FROM_INSN对应的处理(语意是获取指令执行之前的信息),如上插入
了三段以plugin_cb_start开头的中间码,这里的三个case分别处理相关的中间码。
我们深入看下plugin_gen_enable_mem_helper的处理。
1 | plugin_gen_enable_mem_helper |
经过上述操作,最终结果是把plugin中注册的回调函数保存到了CPUState的plugin_mem_cbs。
原来的中间码序列:
1 | plugin_cb_start PLUGIN_GEN_FROM_INSN, PLUGIN_GEN_ENABLE_MEM_HELPER |
变成了:
1 | movi_64 ptr, arr地址 |
在load/store的实现里(其中调用helper函数里),qemu调用qemu_plugin_vcpu_mem_cb的到
CPUState中保存的memory相关回调,并执行。
如上我们分析了一个qemu处理plugin的特例情况,其它plugin插桩以及替换的原理也是一样
的,比如对于plugin_cb_start PLUGIN_GEN_FROM_TB, PLUGIN_GEN_CB_UDATA的情况,qemu
直接用call IR插入了一个空helper函数,后面的替换直接修改call IR里保存的函数地址以及
函数入参就好了。
如上我们大致根据plugin的执行流程分析其工作原理,下面在横向的维度上把plugin的基本
概念再展开下。
从plugin_gen_from的定义上看,qemu plugin的插桩点包括:
1 | /* TB翻译前 */ |
指令和TB的插桩点在qemu翻译执行的主循环里,如上的分析中已经有涉及。PLUGIN_GEN_FROM_MEM
在load/store的公共实现代码里插桩时会用到,在qemu/tcg/tcg-op.c里load/store IR的实现
tcg_gen_qemu_ld/st_i32/i64会调用plugin_gen_mem_callbacks插入参数为PLUGIN_GEN_FROM_MEM
的plugin_cb_start/end,以及空的helper桩函数。
可以看到,qemu对应中间码使用插入helper桩函数再替换的方式支持plugin。对于helper函数
里需要支持plugin时,qemu把回调函数先保存到CPUState里,然后在helper里直接调用回调
函数,这个就是我们上面重点分析的例子里的情形。
写一个自己的qemu plugin
如上分析了qemu plugin的逻辑,我们再从plugin的角度看看qemu都提供的那些API出来,以
及他们的大概用法。我们还是从cache plugin入手,然后横向展开看看。
如上分析里,cache plugin首先使用qemu_plugin_register_vcpu_tb_trans_cb注册了在tb
翻译结束会调用的回调函数(vcpu_tb_trans),这个回调函数可以得到tb里guest指令的句柄,
从而plugin里可以继续针对guest指令注册回调函数。
我们先看下第一层,也就是除了tb翻译完成可以注册回调,还有那些地方可以注册回调。qemu
里还提供了如下注册plugin的地方:
1 | QEMU_PLUGIN_EV_VCPU_INIT |
如上的每个地方,qemu都提供了一个API来注册回调,比如QEMU_PLUGIN_EV_VCPU_INIT对应
的API就是qemu_plugin_register_vcpu_init_cb。如上回调点大概意思可以猜出来,但是要
知道确切意思还的去看qemu的代码,qemu并没有把自己执行的模型表述的很清楚。
cache plugin里针对tb里的每个guest指令注册了vcpu_mem_access和vcpu_insn_exec,我们
看看针对指令都可以怎么注册回调函数。针对guest指令可以注册内存读写相关的回调和指令
执行相关的回调,每种类型又分为cb和inline:
1 | qemu_plugin_register_vcpu_mem_cb |
对于指令执行相关的回调,qemu会在每个指令执行前调用,对于内存读写相关的回调,qemu
会在访存指令的helper实现函数以及访存指令完成时调用。cb类型的回调是plugin里实现回调
函数,qemu主流程里调用plugin里定义的函数来实现信息记录的,而所谓inline并没有调用
helper函数记录信息,而是在plugin里定义操作指令和操作的目的地址,qemu主流程里每当
到了调用点就对目的地址做相关的操作,目前qemu定义的操作还只有add,可以看出,inline
是一种轻量级的记录方式,qemu内部实现上,只需要根据plugin提供的操作地址稍微调整下
主流程里的桩中间码就可以做到。
qemu针对tb执行也提供了可以注册回调的入口:
1 | qemu_plugin_register_vcpu_tb_exec_cb |
使用如上API注册回调,qemu会在执行tb前调用回调或者像上面分析中提到的那样更新注册
地址上的数据。
qemu plugin里需要获得guest指令或者tb的一些参数,为此qemu还对外提供了一组获取guest
指令或者tb的信息的辅助函数。这些辅助函数以及上述所提到的API在include/qemu/qemu-plugin.h
里均有定义。