基础介绍
qemu-riscv64 -d help可以看到支持的各种debug选项,我们一般用的有in_asm/exec/cpu
以及singlestep等,其他的参数,比如,out_asm是翻译后得到的host上的汇编,op是qemu的
中间码,这些都和qemu自己的调试有关。
我们看下前面的几个:in_asm是输出guest的汇编,exec是记录每个翻译块执行的trace,
cpu是在执行一个翻译块之前打印当前的CPU各个寄存器的值,在没有配置singlestep的时候,
上面的打印都是和翻译块(TB)相关的,singlestep可以看到每条guest指令模拟执行后的各种
调试信息。上面的调试配置可以配合使用,使用-D file,可以输出到文件file,需要注意
的是调试信息往往比较多。如果是用user mode调试小的程序,输出的log的大小还可以接受,
如果,比如你用system mode起Linux内核的时候加上了上面的调试选项,运行的速度就会
很慢,因为要输出的log太多了,qemu还有-dfilter可以只输出特定地址区间的log,这样就
可以先不加-dfilter运行一遍,大概知道需要debug的地方,然后再加上-dfilter仔细的跟踪。
qemu里还可以用-d mmu去跟踪mmu翻译,用-d int去跟踪中断和异常。
举个例子
qemu上运行的代码:(test.c)
1 2 3 4 5 6 7 8 9 10 11 12 13
| int __attribute__ ((noinline)) test_add(int a, int b) { return a + b; }
int main() { volatile int c = 0;
c = test_add(1, 2);
return 0; }
|
riscv64-linux-gnu-gcc test.c –static
riscv64-linux-gnu-objdump -D a.out > test.s
我们这样运行下: qemu-riscv64 -d in_asm,exec,cpu -D ./log a.out
然后看看test_add这个函数前后的各种调试信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| [...] ---------------- IN: main 0x0000000000010458: 1101 addi sp,sp,-32 0x000000000001045a: ec06 sd ra,24(sp) 0x000000000001045c: e822 sd s0,16(sp) 0x000000000001045e: 1000 addi s0,sp,32 0x0000000000010460: fe042623 sw zero,-20(s0) 0x0000000000010464: 4589 addi a1,zero,2 0x0000000000010466: 4505 addi a0,zero,1 0x0000000000010468: fc9ff0ef jal ra,-56 # 0x10430
Trace 0: 0xffff98026780 [0000000000000000/0000000000010458/00006100/00000000] main pc 0000000000010458 x0/zero 0000000000000000 x1/ra 0000000000010610 x2/sp 0000004000800360 x3/gp 0000000000071028 x4/tp 0000000000072710 x5/t0 0000000000072000 x6/t1 2f2f2f2f2f2f2f2f x7/t2 0000000000072000 x8/s0 000000000001094a x9/s1 00000000000109da x10/a0 0000000000000001 x11/a1 00000040008004a8 x12/a2 00000040008004b8 x13/a3 0000000000000000 x14/a4 0000004000800388 x15/a5 0000000000010458 x16/a6 0000000000071140 x17/a7 4a5e000112702f5b x18/s2 0000000000000000 x19/s3 0000000000000000 x20/s4 0000000000000000 x21/s5 0000000000000000 x22/s6 0000000000000000 x23/s7 0000000000000000 x24/s8 0000000000000000 x25/s9 0000000000000000 x26/s10 0000000000000000 x27/s11 0000000000000000 x28/t3 ffffffffffffffff x29/t4 000000000006ead0 x30/t5 0000000000000000 x31/t6 0000000000072000 ---------------- IN: test_add 0x0000000000010430: 1101 addi sp,sp,-32 0x0000000000010432: ec22 sd s0,24(sp) 0x0000000000010434: 1000 addi s0,sp,32 0x0000000000010436: 87aa mv a5,a0 0x0000000000010438: 872e mv a4,a1 0x000000000001043a: fef42623 sw a5,-20(s0) 0x000000000001043e: 87ba mv a5,a4 0x0000000000010440: fef42423 sw a5,-24(s0) 0x0000000000010444: fec42703 lw a4,-20(s0) 0x0000000000010448: fe842783 lw a5,-24(s0) 0x000000000001044c: 9fb9 addw a5,a5,a4 0x000000000001044e: 2781 sext.w a5,a5 0x0000000000010450: 853e mv a0,a5 0x0000000000010452: 6462 ld s0,24(sp) 0x0000000000010454: 6105 addi sp,sp,32 0x0000000000010456: 8082 ret
Linking TBs 0xffff98026780 [0000000000010458] index 0 -> 0xffff98026900 [0000000000010430] Trace 0: 0xffff98026900 [0000000000000000/0000000000010430/00006100/00000000] test_add pc 0000000000010430 x0/zero 0000000000000000 x1/ra 000000000001046c x2/sp 0000004000800340 x3/gp 0000000000071028 x4/tp 0000000000072710 x5/t0 0000000000072000 x6/t1 2f2f2f2f2f2f2f2f x7/t2 0000000000072000 x8/s0 0000004000800360 x9/s1 00000000000109da x10/a0 0000000000000001 x11/a1 0000000000000002 x12/a2 00000040008004b8 x13/a3 0000000000000000 x14/a4 0000004000800388 x15/a5 0000000000010458 x16/a6 0000000000071140 x17/a7 4a5e000112702f5b x18/s2 0000000000000000 x19/s3 0000000000000000 x20/s4 0000000000000000 x21/s5 0000000000000000 x22/s6 0000000000000000 x23/s7 0000000000000000 x24/s8 0000000000000000 x25/s9 0000000000000000 x26/s10 0000000000000000 x27/s11 0000000000000000 x28/t3 ffffffffffffffff x29/t4 000000000006ead0 x30/t5 0000000000000000 x31/t6 0000000000072000 ---------------- IN: main 0x000000000001046c: 87aa mv a5,a0 0x000000000001046e: fef42623 sw a5,-20(s0) 0x0000000000010472: 4781 mv a5,zero 0x0000000000010474: 853e mv a0,a5 0x0000000000010476: 60e2 ld ra,24(sp) 0x0000000000010478: 6442 ld s0,16(sp) 0x000000000001047a: 6105 addi sp,sp,32 0x000000000001047c: 8082 ret
Trace 0: 0xffff98026ac0 [0000000000000000/000000000001046c/00006100/00000000] main pc 000000000001046c x0/zero 0000000000000000 x1/ra 000000000001046c x2/sp 0000004000800340 x3/gp 0000000000071028 x4/tp 0000000000072710 x5/t0 0000000000072000 x6/t1 2f2f2f2f2f2f2f2f x7/t2 0000000000072000 x8/s0 0000004000800360 x9/s1 00000000000109da x10/a0 0000000000000003 x11/a1 0000000000000002 x12/a2 00000040008004b8 x13/a3 0000000000000000 x14/a4 0000000000000001 x15/a5 0000000000000003 x16/a6 0000000000071140 x17/a7 4a5e000112702f5b x18/s2 0000000000000000 x19/s3 0000000000000000 x20/s4 0000000000000000 x21/s5 0000000000000000 x22/s6 0000000000000000 x23/s7 0000000000000000 x24/s8 0000000000000000 x25/s9 0000000000000000 x26/s10 0000000000000000 x27/s11 0000000000000000 x28/t3 ffffffffffffffff x29/t4 000000000006ead0 x30/t5 0000000000000000 x31/t6 0000000000072000 [...]
|
如上,IN: xxx后面是guest的汇编,in_asm会显示这一部分。exec打印出来的是诸如,Linking TBs xxx
Trace 0:xxx的部分,这些都是qemu TB的信息,其中中括号里的第二个参数对调试guest
代码有用,比如,Trace 0: 0xffff98026900 [0000000000000000/0000000000010430/00006100/00000000] test_add
中的0000000000010430是这个TB对应的第一个guest指令的地址,中括号后面是符号名字。
exec可以用来跟踪程序执行到哪里了,运行时的跟踪。
qemu把若干条guest指令翻译到一个TB里,TB的划分一般是程序跳转的地方,可以看到main
函数的前几条指令放到了一个TB里,test_add的指令都放到一个TB,main的后面几条指令
放到了下一个TB里。
需要注意的一个地方是,CPU寄存器打印表示的是进入当前TB翻译执行代码之前寄存器的数值。
所以,可以看到test_add这一段log的a0是1、a1是2,表示进入test_add这段代码对应的TB
之前CPU寄存器的值。逻辑上,test_add调用之前先把1放到a0、2放到a1,和log是一致的。
使用singlestep可以看到一条guest指令执行后CPU的状态,我们只看CPU的状态,使用如下
的命令:qemu-riscv64 -d in_asm,cpu -singlestep -D ./log a.out
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| [...] ---------------- IN: main 0x0000000000010468: fc9ff0ef jal ra,-56 # 0x10430
pc 0000000000010468 x0/zero 0000000000000000 x1/ra 0000000000010610 x2/sp 0000004000800340 x3/gp 0000000000071028 x4/tp 0000000000072710 x5/t0 0000000000072000 x6/t1 2f2f2f2f2f2f2f2f x7/t2 0000000000072000 x8/s0 0000004000800360 x9/s1 00000000000109da x10/a0 0000000000000001 x11/a1 0000000000000002 x12/a2 00000040008004b8 x13/a3 0000000000000000 x14/a4 0000004000800388 x15/a5 0000000000010458 x16/a6 0000000000071140 x17/a7 4a5e000112702f5b x18/s2 0000000000000000 x19/s3 0000000000000000 x20/s4 0000000000000000 x21/s5 0000000000000000 x22/s6 0000000000000000 x23/s7 0000000000000000 x24/s8 0000000000000000 x25/s9 0000000000000000 x26/s10 0000000000000000 x27/s11 0000000000000000 x28/t3 ffffffffffffffff x29/t4 000000000006ead0 x30/t5 0000000000000000 x31/t6 0000000000072000 ---------------- IN: test_add 0x0000000000010430: 1101 addi sp,sp,-32
pc 0000000000010430 x0/zero 0000000000000000 x1/ra 000000000001046c x2/sp 0000004000800340 x3/gp 0000000000071028 x4/tp 0000000000072710 x5/t0 0000000000072000 x6/t1 2f2f2f2f2f2f2f2f x7/t2 0000000000072000 x8/s0 0000004000800360 x9/s1 00000000000109da x10/a0 0000000000000001 x11/a1 0000000000000002 x12/a2 00000040008004b8 x13/a3 0000000000000000 x14/a4 0000004000800388 x15/a5 0000000000010458 x16/a6 0000000000071140 x17/a7 4a5e000112702f5b x18/s2 0000000000000000 x19/s3 0000000000000000 x20/s4 0000000000000000 x21/s5 0000000000000000 x22/s6 0000000000000000 x23/s7 0000000000000000 x24/s8 0000000000000000 x25/s9 0000000000000000 x26/s10 0000000000000000 x27/s11 0000000000000000 x28/t3 ffffffffffffffff x29/t4 000000000006ead0 x30/t5 0000000000000000 x31/t6 0000000000072000 ---------------- IN: test_add 0x0000000000010432: ec22 sd s0,24(sp)
pc 0000000000010432 x0/zero 0000000000000000 x1/ra 000000000001046c x2/sp 0000004000800320 x3/gp 0000000000071028 x4/tp 0000000000072710 x5/t0 0000000000072000 x6/t1 2f2f2f2f2f2f2f2f x7/t2 0000000000072000 x8/s0 0000004000800360 x9/s1 00000000000109da x10/a0 0000000000000001 x11/a1 0000000000000002 x12/a2 00000040008004b8 x13/a3 0000000000000000 x14/a4 0000004000800388 x15/a5 0000000000010458 x16/a6 0000000000071140 x17/a7 4a5e000112702f5b x18/s2 0000000000000000 x19/s3 0000000000000000 x20/s4 0000000000000000 x21/s5 0000000000000000 x22/s6 0000000000000000 x23/s7 0000000000000000 x24/s8 0000000000000000 x25/s9 0000000000000000 x26/s10 0000000000000000 x27/s11 0000000000000000 x28/t3 ffffffffffffffff x29/t4 000000000006ead0 x30/t5 0000000000000000 x31/t6 0000000000072000 ---------------- IN: test_add 0x0000000000010434: 1000 addi s0,sp,32
pc 0000000000010434 x0/zero 0000000000000000 x1/ra 000000000001046c x2/sp 0000004000800320 x3/gp 0000000000071028 x4/tp 0000000000072710 x5/t0 0000000000072000 x6/t1 2f2f2f2f2f2f2f2f x7/t2 0000000000072000 [...]
|
如上,因为显示的寄存器是执行本条指令之前的寄存器的数值,比如,test_add的第一条指令
addi sp,sp,-32对sp寄存器的更新要在test_add的第二个指令(sd s0,24(sp))的打印log里
才能看到:x0/zero 0000000000000000 x1/ra 000000000001046c x2/sp 0000004000800320 x3/gp 0000000000071028。
如果你想用qemu跟踪指令指令的流程,只要用脚本处理下如上的-singlestep的反汇编log就好,
具体的处理方式可以参考下面代码分析中的介绍。
我们加上-dfilter运行下qemu-riscv64 -d in_asm,exec,cpu -dfilter 0x10430+0x26 -D ./log a.out
dfilter的这种写法表示只输出pc在[0x10430, 0x10430+0x26]区间的log,这段区间是test_add
函数的代码。全部log就只有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| ---------------- IN: test_add 0x0000000000010430: 1101 addi sp,sp,-32 0x0000000000010432: ec22 sd s0,24(sp) 0x0000000000010434: 1000 addi s0,sp,32 0x0000000000010436: 87aa mv a5,a0 0x0000000000010438: 872e mv a4,a1 0x000000000001043a: fef42623 sw a5,-20(s0) 0x000000000001043e: 87ba mv a5,a4 0x0000000000010440: fef42423 sw a5,-24(s0) 0x0000000000010444: fec42703 lw a4,-20(s0) 0x0000000000010448: fe842783 lw a5,-24(s0) 0x000000000001044c: 9fb9 addw a5,a5,a4 0x000000000001044e: 2781 sext.w a5,a5 0x0000000000010450: 853e mv a0,a5 0x0000000000010452: 6462 ld s0,24(sp) 0x0000000000010454: 6105 addi sp,sp,32 0x0000000000010456: 8082 ret
Trace 0: 0xffffa00265c0 [0000000000000000/0000000000010430/0x6100] test_add pc 0000000000010430 x0/zero 0000000000000000 x1/ra 000000000001046c x2/sp 0000004000800160 x3/gp 0000000000071028 x4/tp 0000000000072710 x5/t0 0000000000072000 x6/t1 2f2f2f2f2f2f2f2f x7/t2 0000000000072000 x8/s0 0000004000800180 x9/s1 00000000000109da x10/a0 0000000000000001 x11/a1 0000000000000002 x12/a2 00000040008002d8 x13/a3 0000000000000000 x14/a4 00000040008001a8 x15/a5 0000000000010458 x16/a6 0000000000071140 x17/a7 0112702f5b5a4001 x18/s2 0000000000000000 x19/s3 0000000000000000 x20/s4 0000000000000000 x21/s5 0000000000000000 x22/s6 0000000000000000 x23/s7 0000000000000000 x24/s8 0000000000000000 x25/s9 0000000000000000 x26/s10 0000000000000000 x27/s11 0000000000000000 x28/t3 ffffffffffffffff x29/t4 000000000006ead0 x30/t5 0000000000000000 x31/t6 0000000000072000
|
dfilter参数还有其他的描述格式,从qemu的自测试代码可以看到具体的使用方式:qemu/tests/test-logging.c。
还可以用: 基地址-区间大小、基地址..结尾地址,用逗号分开可以加多段不同的地址,比如:
-dfilter 0x1000+0x100,0x2100-0x100,0x3000..0x3100
加上-d mmu可以打印出qemu上MMU相关的行为,打印大概是这样的:
1 2
| riscv_cpu_tlb_fill ad ffffffe001003fe0 rw 0 mmu_idx 1 riscv_cpu_tlb_fill address=ffffffe001003fe0 ret 0 physical 0000000081203fe0 prot 3
|
上面表示MMU要查ffffffe001003fe0对应的物理地址,并且查到的结果是0000000081203fe0,
rw后面的数字表示访问类型,0表示数据load,1表示数据store,2表示指令fetch,ret后面
的数字,0表示翻译成功,1表示翻译失败。
加上-d int可以打印中断和异常的log,但是在5.1.50的版本上,这个打印还没有加上,在
7.0.50上, 这个打印大概是这样的:
1 2 3
| riscv_cpu_do_interrupt: hart:0, async:0, cause:0000000000000009, epc:0xffffffe00020455e, tval:0x0000000000000000, desc=supervisor_ecall [...] riscv_cpu_do_interrupt: hart:0, async:1, cause:0000000000000005, epc:0xffffffe000201558, tval:0x0000000000000000, desc=s_timer
|
hart表示异常或中断的cpu,async后面的数据,0表示异常,1表示中断,cause/epc/tval
就是相关寄存器中的值,最后一个desc是一个描述异常或中断的字符串。
qemu代码分析
in_asm的入口在:translator_loop的最后,可见guest汇编只在翻译的时候会打印,这些
打印不反应执行的流程。另外,qemu对于同一段代码是可能会翻译到不同的TB里的,这个
和翻译参数有关系。guest指令的反汇编是独立的逻辑,不在本文中描述。
exec和cpu的入口在:cpu_tb_exec的log_cpu_exec里,可见是在TB执行的时候打印。但是,
qemu为了提高执行效率,TB和TB之间可以直接跳过去,这个叫chained TB,而log_cpu_exec
是在翻译-执行的循环里加的打印,所以,要跟踪程序执行流程,必须断开qemu里的chained
TB,所以参数变成了:-d in_asm,cpu,nochain -singlestep,如果只要得到程序的执行流程,
cpu可以去掉,就变成了-d in_asm,exec,nochain –singlestep, in_asm可以打出反汇编,
但是,只是翻译的时候打一下,随后执行的时候不会打印,exec + nochain可以打印具体执行
的流程,但是没有反汇编,所以,我们可以对得到的log在exec的log下插入对应的汇编代码,
再把所有exec对应的汇编代码提取到一个文件里就得到了实际指令执行流程,可以写一个脚本
搞定这个事情。
qemu里有两个singlestep相关的flag,一个是全局变量singlestep,一个是cpu结构体里的
singlestep_enabled。后者应该和gdb单步运行有关系,不是我们考虑东西。singlestep
的相关逻辑在翻译执行主循环里,如果是singlestep就配置翻译flag(cflags)为CF_NO_GOTO_TB,
并且一个TB只进行一条guest指令的翻译执行,CF_NO_GOTO_TB禁止qemu在TB之间跳转。
这里有个点需要注意,GOTO_PTR也可能在TB之间跳转,这里之所没有连CF_NO_GOTO_PTR也
配置上,是因为GOTO_PTR的help函数里调用了log_cpu_exec,所以,在GOTO_PTR的时候是
可以打印trace的。
可以看到,singlestep和nochain改变了qemu翻译执行的粒度,效率上是变差了很多的。
实际在qemu system mode上用-d in_asm,cpu -singlestep跑,会慢的一塌糊涂,这个时候
就可以用-dfilter指定输出log的指令地址区间。