qemu之linux-user备忘

2021/12/27 7:12:43

本文主要是介绍qemu之linux-user备忘,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

最后更新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,

  1. 进来首先搞个原子操作: atomic_set(&cpu->running, true); 可以理解,如果两个线程搞到一起,同时执行(解码)这块代码,肯定冲突啊!所以先上锁。如果以后不需要解码,应该就没这个问题,待确认,我想对应的代码块也会被扩展合并。具体如何慢慢研究。

  2. 再下面是smp_mb(),据说是控制内存一致性的,让编译器不要跨区域把内存读写搞乱。具体为何要放在这,怎么生效的先不管,至少不影响执行逻辑。

  3. 然后是个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循环,开始下一个代码块处理。

随机发现、确认

  1. guest代码在被执行时,通过生成syscall意外跳出guest代码,返回host代码,触发执行host syscall调用;

反思



这篇关于qemu之linux-user备忘的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程