本周工作概览
本周将工作重心转移到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;
}
我们希望在函数内跟踪size和ptr变量。传统的方式是使用DPRINTF,而trace与DPRINTF相比具有很多优势。此处我们使用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()函数在使用时传递参数是不定的,为解决这种传递可变个数参数的问题,可以用...代替省略参数