3. CPU 执行日志分析:-d in_asm 参数详解、指令流追踪与反汇编输出解读

调试 RISC-V 程序,最怕什么?

怕代码跑飞了,你却不知道它飞去了哪里。

怕程序死循环,你却不知道它卡在哪条指令上。

怕中断处理异常,你却不知道 CPU 到底执行了什么。

这时候,-d in_asm 就是你的救命稻草。它能让你看到 CPU 执行的每一条指令,一个不落。

3.1 -d in_asm 到底输出了什么?

先看一个最简单的例子。假设我们有一段 RISC-V 汇编:

addi    sp, sp, -16
sw      ra, 12(sp)
li      a0, 42
call    print_num
lw      ra, 12(sp)
addi    sp, sp, 16
ret

用 QEMU 跑起来,加上 -d in_asm 参数:

qemu-riscv64 -d in_asm test_program

你会看到类似这样的输出:

----------------
IN:
0x00010000:  ff010113        addi    sp,sp,-16
0x00010004:  00112623        sw      ra,12(sp)
0x00010008:  02800513        li      a0,42
0x0001000c:  010000ef        jal     ra,0x0001001c
0x00010010:  00c12083        lw      ra,12(sp)
0x00010014:  01010113        addi    sp,sp,16
0x00010018:  00008067        ret

嗯,这里要注意几个关键信息:

  • 分隔线:---------------- 表示一个翻译块的开始。QEMU 不是一条一条翻译的,它会把连续的指令打包成一个块(Translation Block)。
  • IN::表示接下来是输入的指令流,也就是原始的反汇编。
  • 地址 + 机器码 + 反汇编:比如 0x00010000: ff010113 addi sp,sp,-16,地址、十六进制指令、助记符,一目了然。
我的习惯:每次调试新程序,我都会先跑一遍 -d in_asm,把前几百条指令的日志存下来。这样程序崩溃时,我能快速定位到“最后一条正常执行的指令”是什么。

3.2 指令流追踪:看 CPU 如何“走路”

指令流追踪,说白了就是看 CPU 的“脚印”。

你想想看,程序执行本质上就是 PC(程序计数器)不断变化的过程。而 -d in_asm 把每一步都记录了下来。

我遇到过一个问题:一个嵌入式系统启动后,串口没有任何输出。我怀疑是 bootloader 根本没执行到串口初始化代码。于是我用 -d in_asm 跑了一遍,发现 CPU 在地址 0x80000040 处反复执行一条 j 0x80000040——说白了,就是个死循环。原来 bootloader 在等待某个外设就绪,但那个外设根本没初始化。

指令流追踪的核心价值在于:

  • 验证控制流:函数调用、跳转、分支是否按预期执行。
  • 发现死循环:如果某个地址反复出现,那就是卡住了。
  • 检查异常处理:看看 CPU 是否进入了异常向量表。
注意:-d in_asm 的输出量非常大。一个简单的 Hello World 程序可能产生几千行日志。我建议用 tee 或重定向保存到文件,然后用 grep 搜索关键地址。

3.3 反汇编输出解读:从机器码到指令

反汇编输出看起来简单,但里面藏着很多细节。我们来看一个 RISC-V 指令的典型格式:

地址 机器码 反汇编 说明
0x00010000 ff010113 addi sp,sp,-16 R-type 立即数加法
0x00010004 00112623 sw ra,12(sp) S-type 存储指令
0x0001000c 010000ef jal ra,0x0001001c J-type 跳转并链接
0x00010018 00008067 ret I-type 返回(jalr 的别名)

这里有个坑,我踩过好几次。

你看 ret 指令,反汇编显示的是 ret,但机器码是 00008067。实际上,RISC-V 的 ret 是伪指令,它被汇编器翻译成了 jalr x0, ra, 0。如果你只看反汇编,可能会忽略这个细节。但在调试时,如果你看到 jalr 而不是 ret,别慌——它们是一回事。

关键点:反汇编输出是 QEMU 内部的反汇编器生成的,它和 GNU objdump 的输出可能略有不同。比如,QEMU 可能会把某些伪指令展开成真实指令。我个人建议以 QEMU 的输出为准,因为它是实际执行的指令。

3.4 实战:用 -d in_asm 定位一个典型 Bug

假设你写了一个 RISC-V 程序,功能是计算斐波那契数列。但结果总是不对。你怀疑是函数调用出了问题。

用 -d in_asm 跑一下,输出如下:

----------------
IN: fib
0x00010050:  fe010113        addi    sp,sp,-32
0x00010054:  00112e23        sw      ra,28(sp)
0x00010058:  00812c23        sw      s0,24(sp)
0x0001005c:  02010413        addi    s0,sp,32
0x00010060:  00050793        mv      a5,a0
0x00010064:  00078513        mv      a0,a5
0x00010068:  00a00593        li      a1,10
0x0001006c:  00b50533        add     a0,a0,a1
0x00010070:  00000073        ecall
----------------
IN: main
0x00010020:  ff010113        addi    sp,sp,-16
0x00010024:  00112623        sw      ra,12(sp)
0x00010028:  00a00513        li      a0,10
0x0001002c:  020000ef        jal     ra,0x00010050
0x00010030:  00c12083        lw      ra,12(sp)
0x00010034:  01010113        addi    sp,sp,16
0x00010038:  00008067        ret

发现问题了吗?

在 fib 函数中,地址 0x00010070 有一条 ecall 指令。这明显不对——斐波那契函数里不应该有系统调用。原来是我在写代码时,不小心把调试用的 ecall 留在了函数里。

如果没有指令流追踪,这种错误很难发现。你可能会花几个小时调试算法逻辑,结果发现是多了条指令。

3.5 知识结构图:-d in_asm 的核心逻辑

下面这张图总结了 -d in_asm 的工作流程和输出结构:

-d in_asm 指令流追踪核心逻辑 QEMU 启动 翻译块 (Translation Block) 输出:地址 | 机器码 | 反汇编指令 验证控制流 发现死循环 检查异常处理 提示:输出量可能很大,建议重定向到文件后用 grep 分析 常用命令:qemu-riscv64 -d in_asm program 2>&1 | tee trace.log

3.6 实用技巧与避坑指南

最后,分享几个我这些年积累下来的实用技巧:

  • 结合地址范围过滤:如果程序很大,可以用 -d in_asm,nochain 让每个翻译块都输出,避免 QEMU 的链式翻译优化导致日志不完整。
  • 对比两次运行:我曾经调试一个随机崩溃的问题,把正常和异常两次运行的 -d in_asm 日志用 diff 对比,很快就找到了差异点。
  • 注意伪指令展开:QEMU 的反汇编器可能会展开伪指令。比如 nop 可能显示为 addi x0, x0, 0。别被吓到,这是正常的。
我曾经踩过的坑:有一次我调试一个多核 RISC-V 系统,用 -d in_asm 只看到了一个核的指令流。后来才发现,-d in_asm 默认只跟踪第一个 CPU。要跟踪所有核,需要加 -d cpu 参数。这个坑让我浪费了整整一个下午。

好了,关于 -d in_asm 的内容就到这里。记住,这个工具是你调试 RISC-V 程序的第一道防线。下次程序跑飞了,别急着加打印,先看看 CPU 到底执行了什么。