QEMU开源实战(六)

3,062 阅读6分钟

本周工作概览

本周将工作重心转移到trace模块中:

  • 将QEMU版本同步为当前Master分支
  • 使用trace
  • patch example
  • tricks learned

将QEMU版本更新当前master对应版本

Master直接从QEMU GitHub仓库拉取即可。之前提到过,配置git代理之后对于仓库的clone依旧无效。原因是之配置的http/https代理,clone的时候却用的ssh协议。同时,当前master版本在make时涉及到子模块的更新,在不禁用更新的情况下需要从GitHub通过ssh协议进行更新,若不配置ssh代理,则速度极慢,故先配置本地ssh代理。

Git ssh代理

修改 ~/.ssh/config 文件(不存在则新建):

# 必须是 github.com
Host github.com
   HostName github.com
   User git
   # 走 socks5 代理
   ProxyCommand nc -v -x 127.0.0.1:1080 %h %p

源码编译、安装镜像及使用Clion

源码编译

之后按照之前的文章中的步骤配置即可。 由于我们要使用trace模块,故进行configure的时候需要添加命令:

--enable-trace-backends=simple

详见使用tracing中的说明。

另外,在configure model devices时需要更新子模块,可能较慢。

安装镜像

将镜像替换成了Ubuntu Server

设置参数,启动QEMU,安装系统即可

使用Clion

按照之前的方法配置即可。不同之处在于,使用trace需要增加参数:

--trace events=/tmp/events

此处events可以根据需要进行更换,详见使用tracing中的说明。

使用tracing

参考文档

什么是trace

简单来说,就是一种debug机制,类似printf,但是更加易用和强大。有一些第三方trace工具,但是QEMU开发并可以使用自己的trace工具

简单实例

编译时启用tracing

在编译make之前的配置configure时启用tracing,并指定使用的后端,在通常情况下,使用simple后端

./configure --enable-trace-backends=simple

创建events

需要知道开发者想要trace哪些事件,故需要指定一个events文件。此处写入/tmp/events文件

echo memory_region_ops_read >/tmp/events

运行QEMU,生成trace file

在正常的参数后加

--trace events=/tmp/events

即可。events参数内容可改为其它指定的events文件

格式化输出trace内容

运行QEMU时会产生一个trace-*文件,其中*QEMU的进程id,可以通过如下指令获取:

pgrep -f qemu

之后使用脚本将文件中的内容格式化输出:

./scripts/simpletrace.py trace-events-all trace-*

输出内容如下:

memory_region_ops_read 6.598 pid=25536 cpu_index=0x0 mr=0x55e37bf0a0c0 addr=0x608 value=0x75f13f size=0x4
memory_region_ops_read 6.462 pid=25536 cpu_index=0x0 mr=0x55e37bf0a0c0 addr=0x608 value=0x75f156 size=0x4
memory_region_ops_read 6.578 pid=25536 cpu_index=0x0 mr=0x55e37bf0a0c0 addr=0x608 value=0x75f16e size=0x4
memory_region_ops_read 6.619 pid=25536 cpu_index=0x0 mr=0x55e37bf0a0c0 addr=0x608 value=0x75f186 size=0x4
memory_region_ops_read 58.428 pid=25536 cpu_index=0x0 mr=0x55e37bf0a0c0 addr=0x608 value=0x75f255 size=0x4
memory_region_ops_read 11.675 pid=25536 cpu_index=0x0 mr=0x55e37bf0a0c0 addr=0x608 value=0x75f280 size=0x4
memory_region_ops_read 15.101 pid=25536 cpu_index=0x0 mr=0x55e37c00a800 addr=0xfe001000 value=0x0 size=0x1
memory_region_ops_read 26.407 pid=25536 cpu_index=0x0 mr=0x55e37bf0a420 addr=0xb2 value=0xb5 size=0x1
......

添加trace event

假设现在源码中有这样的一个函数:

void *qemu_vmalloc(size_t size)
{
    void *ptr;
    size_t align = QEMU_VMALLOC_ALIGN;
 
    if (size < align) {
        align = getpagesize();
    }
    ptr = qemu_memalign(align, size);
    return ptr;
}

我们希望在函数内跟踪sizeptr变量。传统的方式是使用DPRINTF,而traceDPRINTF相比具有很多优势。此处我们使用trace方法实现。

可以QEMU根目录或任何子目录下建立trace-events文件,并将其目录在make时声明于trace-events-subdirs参数中:

Each directory in the source tree can declare a set of static trace events in a local "trace-events" file. All directories which contain "trace-events" files must be listed in the "trace-events-subdirs" make variable in the top level Makefile.objs. During build, the "trace-events" file in each listed subdirectory will be processed by the "tracetool" script to generate code for the trace events.

可以在trace-events文件中编写如下声明:

qemu_vmalloc(size_t size, void *ptr) "size %zu ptr %p"

trace工具会生成一个trace_qemu_vmalloc函数供调用:

#include "trace.h"  /* needed for trace event prototype */

void *qemu_vmalloc(size_t size)
{
    void *ptr;
    size_t align = QEMU_VMALLOC_ALIGN;
 
    if (size < align) {
        align = getpagesize();
    }
    ptr = qemu_memalign(align, size);
    trace_qemu_vmalloc(size, ptr);
    return ptr;
}

注意要在源代码最开始引用trace.h

对于一个新的trace-event的添加,还有一些特殊的注意事项,详见参考文档

patch example

为了尝试贡献patch,我们需要先找一些之前的人在这方面提交并被采纳的patch,如:

[PULL,49/87] hw/i386/pc: Convert DPRINTF() to trace events

在这个patch中,将最初用于调试的DPRINTF()改为了trace实现。只需要将对于DPRINTF()相关定义改为

#include "trace.h"

并在trace-events中定义输出函数:

pc_gsi_interrupt(int irqn, int level) "GSI interrupt #%d level:%d"

之后就可以引用此trace函数进行输出:

{
     GSIState *s = opaque;
 
    //DPRINTF("pc: %s GSI %d\n", level ? "raising" : "lowering", n);
    trace_pc_gsi_interrupt(n, level);
     if (n < ISA_NUM_IRQS) {
         qemu_set_irq(s->i8259_irq[n], level);
     }

其它DPRINTF的修改同理


贡献patch

如此我们可以从源码中找一些仍旧采用DPRINTF的地方,将其改为trace的方式,例如,在/hw/virtio/virtio-crypto.c中有十处仍使用DPRINTF,例如:

// /hw/virtio/virtio-crypto.c

{
    VirtIOCrypto *vcrypto = VIRTIO_CRYPTO(vdev);
    unsigned int num = *out_num;

    info->cipher_alg = ldl_le_p(&cipher_para->algo);
    info->key_len = ldl_le_p(&cipher_para->keylen);
    info->direction = ldl_le_p(&cipher_para->op);
    DPRINTF("cipher_alg=%" PRIu32 ", info->direction=%" PRIu32 "\n",
             info->cipher_alg, info->direction);
    
    ......
    
}

tricks learned

在宏定义中使用do{...}while(0)

在查看DPRINTF的使用方法时,发现其宏定义几乎都采取如下这种do{...}while(0)形式:

#define DPRINTF(fmt, ...) \
do { \
    if (DEBUG_VIRTIO_CRYPTO) { \
        fprintf(stderr, "virtio_crypto: " fmt, ##__VA_ARGS__); \
    } \
} while (0)

原因是,若想要宏定义的内容是具有两条以上顺序执行语句,或结尾不加分号(如此处if{}之后无分号)的情况时,直接定义会产生错误,如下面这个例子:

#define FOO(x) foo(x); bar(x)

if (condition)
    FOO(x);
else // syntax error here
    ...;

显然这种替换会导致bar(x)无法位于if(condition)分支内,若在宏定义中加上大括号也不是好的解决办法,因为这需要在实际写代码的时候省略分号,不符合编程习惯:

#define FOO(x) { foo(x); bar(x); }

if (condition)
    FOO(x)
else
    ...

但使用do{...}while(0)完美解决这个问题:

#define FOO(x) do { foo(x); bar(x); } while (0)

if (condition)
    FOO(x);
else
    ....

算是一个很有意思的trick

使用...代替省略参数

在宏定义DPRINTF的时候都写作:

#define DPRINTF(fmt, ...)\
...

由于DPRINTF类似printf(),而printf()函数在使用时传递参数是不定的,为解决这种传递可变个数参数的问题,可以用...代替省略参数

参考链接