NEMU与QEMU联调经验
本文属于 《RISC-V指令集差分测试(DiffTest)系列教程》之一,欢迎查看其它文章。
当我们搭建NEMU与QEMU的DiffTest环境时,修改了很多代码,免不了需要两边一起调试,解决一些问题。
这里,对这些调试经验做下小结。
1 打印日志
1.1 QEMU日志
启动QEMU时,添加以下参数,QEMU可输出指令日志:
-d in_asm -D ./qemu.log
如果需要,打印CPU寄存器,可以加上cpu参数:
-d in_asm,cpu
。
比如:x0 ~ 31寄存器、csr寄存器等
1.2 NEMU日志
编译NEMU时,make menuconfig
配置以下内容:
Testing and Debugging -> [*] Enable debug features: instruction tracing and watchpoint
Testing and Debugging -> [*] Enable rich Log for tracing instructions, pc, inst and dasm
在启动NEMU时,再添加以下参数,NEMU可输出指令,到日志文件:
-l nemu.log
不加
-l nemu.log
时,日志默认输出到屏幕。
两边都输出指令日志,便于随时查看两边跑到什么位置了。
2 独立调试NEMU与QEMU
我们在《搭建NEMU与QEMU的DiffTest环境(Socket方式)》文中,是通过NEMU启动QEMU,然后再连接QEMU的,这样虽然方便,但是没法调试QEMU代码。
因此,如果需要调试NEMU与QEMU之间socket数据包交互,需要独立运行。
gdb调试启动QEMU:
gdb --args ./qemu-system-riscv64 -S -gdb tcp::1234 -nographic -d in_asm -D ./qemu.log
对于NEMU,在NEMU/tools/qemu-socket-diff/src/diff-test.c文件中difftest_init函数,将int pid = fork();
修改为int pid = 2; //fork();
,这样NEMU就不会去启动QEMU了,NEMU只管连接QEMU,并收发socket数据包就行。
gdb调试启动NEMU:
gdb --args ./riscv64-nemu-interpreter -b benos_payload.bin -d ./riscv64-qemu-so -l nemu.log
3 几个关键的断点
3.1 QEMU断点
3.1.1 取指令
打断点:b translate.c:1052
在qemu-7.1.0/target/riscv/translate.c中,如下:
static void decode_opc(CPURISCVState *env, DisasContext *ctx, uint16_t opcode)
{
...
opcode32 = deposit32(opcode32, 16, 16, // 第1052行
translator_lduw(env, &ctx->base,
ctx->base.pc_next + 2));
ctx->opcode = opcode32;
ctx->pc_succ_insn = ctx->base.pc_next + 4; // 第1056行
...
}
- opcode32:表示取到指令的二进制码
- ctx->base.pc_next:表示当前取指的PC地址
3.1.2 解析处理命令
打断点:b gdbstub.c:2565
在qemu-7.1.0/gdbstub.c中,如下:
static int gdb_handle_packet(const char *line_buf)
{
const GdbCmdParseEntry *cmd_parser = NULL;
trace_gdbstub_io_command(line_buf); // 第2565行
...
}
NEMU发送的所有数据包,被解析成字符串命令后,都会调用gdb_handle_packet函数,在gdb_handle_packet函数中根据line_buf,进行cmd_parser赋值,以便后续进行对应操作。
也就是说,只要在此断点处,打印line_buf的值(p line_buf
),就可以看到所有NEMU发送给QEMU的命令字符串。
3.1.3 读寄存器
打断点:b gdbstub.c:1864
在qemu-7.1.0/gdbstub.c中,如下:
static void handle_read_all_regs(GArray *params, void *user_ctx)
{
target_ulong addr, len;
cpu_synchronize_state(gdbserver_state.g_cpu); // 第1864行
...
}
在handle_read_all_regs函数中,读取所有寄存器;最终会在riscv_cpu_gdb_read_register函数中读取CPU寄存器值。
3.1.4 写寄存器
打断点:b gdbstub.c:1847
在qemu-7.1.0/gdbstub.c中,如下:
static void handle_write_all_regs(GArray *params, void *user_ctx)
{
...
cpu_synchronize_state(gdbserver_state.g_cpu); // 第1847行
...
}
在handle_write_all_regs函数中,写入所有寄存器;最终会在riscv_cpu_gdb_write_register函数中写入CPU寄存器。
3.1.5 写内存
打断点:b gdbstub.c:1783
在qemu-7.1.0/gdbstub.c中,有个handle_write_mem函数。
3.1.6 读内存
打断点:b gdbstub.c:1810
在qemu-7.1.0/gdbstub.c中,有个handle_read_mem函数。
3.2 NEMU断点
3.2.1 取指令
打断点:
b cpu-exec.c:532
b cpu-exec.c:560
在NEMU/src/cpu/cpu-exec.c中,如下:
static int execute(int n) {
...
fetch_decode(&s, cpu.pc); // 第532行
cpu.debug.current_pc = s.pc;
cpu.pc = s.snpc;
#ifdef CONFIG_TVAL_EX_II
cpu.instr = s.isa.instr.val; // 第536行
#endif
...
IFDEF(CONFIG_DIFFTEST, difftest_step(s.pc, cpu.pc)); // 第560行
...
}
- s.isa.instr.val:表示取到指令的二进制码
- cpu.pc:表示当前取指的PC地址
- difftest_step:此函数会触发QEMU单步执行一次,并读取QEMU执行结果,进行比较