最后更新2021/12/26
这几天研究qemu linux-user有关的问题,此地立个桩,把我认为值得记录的研究结果记录在此。qemu整体的编译不细说了,只针对我需要的ppc64相关关键记录,很大众特别是没遇到问题的过程就不记录了,也许以后整理全本说明的时候填充一下。另外由于我的最终目标是搞aix-user,与aix-user相关的内容也许会在这里有关联,但全部的内容应在另一个专题中,此处都是linux-user的东西。
前置
通过configure定义目标,我用的是:
./configure --target-list="ppc64-softmmu ppc64-linux-user" --disable-curses --disable-kvm --disable-sdl
除了disable-curses是为了填wsl上libtinfow.so.6 missing的老坑,别的随意。
然后make,如果一切正常,会搞出两个可执行文件,一个用于虚拟ppc64 machine,另一个可作为ppc64 linux elf程序的加载器:
ppc64-softmmu/qemu-system-ppc64
ppc64-linux-user/qemu-ppc64
本系列只研究linux-user。
顺序结构
主代码在linux-user/main.c
从main()开始,都是各种初始化,涉及到的时候会更新本文。我喜欢直达关键点,其它的东西遇到再说。虽然这种方法不太适合一层层抽丝剥茧,特别是没有任何概念的时候,一下子跳得太远,会乱。但对我现在来说,如果走太多岔路,最后都不知道转到哪里去了。以走迷宫比喻,一种方法是从入口遍历每个岔路;我喜欢先到出口,反向走逆推。所以,我们就直接翻到main.c的最后几行:
cpu_loop(env);
cpu_loop里面是个死循环
到了cpu_loop,则所有初始化等准备工作都已经完成,包括待加载执行文件格式审核、加载到内存等等,马上需要做的只是解码执行了。
在很多文件中都有cpu_loop调用,真正被使用的是对应目标cpu架构下的cpu_loop,我这里用的是linux-user/ppc/cpu_loop.c
cpu_loop()看起来也很简单,进来就是个for死循环,在里面完成这么几个操作:
for (;;) {
cpu_exec_start(cs);
trapnr = cpu_exec(cs);
cpu_exec_end(cs);
process_queued_cpu_work(cs);
...
switch (trapnr) {
case POWERPC_EXCP_NONE:
...
}
process_pending_signals(env);
}
这里有个背景知识,qemu执行目标代码是以jump block为单位,每个jump block(可能不是这个专用词,以后更新)是一组被连续执行的代码,直到需要跳转到其它地址或者发生意外中断等等情况。
那么,这个结构就很清晰了,代码从cs指定位置开始做准备,(解码、生成tcg代码等等,注:其细节待确认,具体解码在cpu_exec_start还是在cpu_exec)解码顺序执行,直到本块执行完或者发生意外(此意外包含所有意外还是只包含guest代码意外待确认),返回后,进行块后处理,此块将被保留,下次再跑到这个地址,就无需解码,更快了。
这里有个潜在的情况,就是代码块的连续性,会存在代码块解码时是连续的,运行时会提前中断跳出,或者虽然是跳转,本应两个代码块,但前后两个代码块其实可以合并到一起。这些问题qemu都有考虑,后续深入分析的时候能开大相应的处理。
代码块后处理完成后就是意外处理,一个switc,case一堆的意外类型。这里也是我要关注的syscall问题。
case POWERPC_EXCP_SYSCALL_USER:
/* system call in user-mode emulation */
/* WARNING:
* PPC ABI uses overflow flag in cr0 to signal an error
* in syscalls.
*/
env->crf[0] &= ~0x1;
env->nip += 4;
ret = do_syscall(env, env->gpr[0], env->gpr[3], env->gpr[4],
env->gpr[5], env->gpr[6], env->gpr[7],
env->gpr[8], 0, 0);
if (ret == -TARGET_ERESTARTSYS) {
env->nip -= 4;
break;
}
至此,那基本确定了linux-user是通过生成syscall意外,由guest代码中断来,转到host代码执行syscall的。下一步要看看如何触发syscall意外。
在switch case循环里其实还有另一个syscall意外:
case POWERPC_EXCP_SYSCALL: /* System call exception */
cpu_abort(cs, "Syscall exception while in user mode. "
"Aborting\n");
break;
看起来这个syscall意外是”真正“的syscall时发生意外,而不是用于跳转代码的,以后再分析。
继续cpu_exec_start(cs)
cpu_exec_start(cs)在cpus_common.c,
-
进来首先搞个原子操作: atomic_set(&cpu->running, true); 可以理解,如果两个线程搞到一起,同时执行(解码)这块代码,肯定冲突啊!所以先上锁。如果以后不需要解码,应该就没这个问题,待确认,我想对应的代码块也会被扩展合并。具体如何慢慢研究。
-
再下面是smp_mb(),据说是控制内存一致性的,让编译器不要跨区域把内存读写搞乱。具体为何要放在这,怎么生效的先不管,至少不影响执行逻辑。
-
然后是个atom_read,果然需要读内存,还要原子读,而且是需要有空闲cpu(guest)才允许读。其实,我觉得如果为了性能考虑,这里可以优化一下,不去管guest是否有空闲cpu。当然,本身linux-user不是为性能而设计,只是为了虚拟,而且需要控制,分配个guest多少cpu,与这个guest相关的操作都应该在这些cpu之内,这么设计也可以理解。
好了,cpu_exec_start返回了。至此,说明cpu_exec_start啥都没干,只是把一部分代码读入,前面所说的解码什么都,都不在这里。具体atom_read是怎么个原子读,以后再看。
主菜cpu_exec(cs)
累了,休息一会。。。
饭后甜点cpu_exec_end(cs)
各种意外处理
POWERPC_EXCP_SYSCALL
关于各种意外的处理。。。待续
各种意外处理完,该abort的abort,该继续的继续,又会返回for循环,开始下一个代码块处理。
随机发现、确认
- guest代码在被执行时,通过生成syscall意外跳出guest代码,返回host代码,触发执行host syscall调用;