RISC-V 调试规范(The RISC-V Debug Specification)1.0 版本已于 2025 年 02 月 21 日 修订完毕,最近我在 QEMU 上正在支持这个规范,正好可以和大家聊聊。
概述
当设计从仿真转入硬件实现后,用户对系统状态的控制和理解显著降低,硬件内置调试支持对底层软硬件调试至关重要。
一般情况下,在 RISC-V 硬件上运行成熟 OS 时,软件可处理多数调试任务。但在启动阶段、无 OS 环境(如裸机固件)或硬件故障时,硬件调试支持不可替代。
RISC-V 的调试规范,为 Hart 增加了一个新的模式叫 Debug Mode,它使用 DCSR 寄存器控制在哪些权限态下陷入 Debug Mode。为了方便管理 Hart 的调试状态(如暂停/步进/恢复),规范中新增了一个硬件模块叫 DM(Debug Module),它的特点是使用非侵入式的调试手段来控制 Hart 的行为,并对外提供统一的调试接口。
DM 通过 DTM (Debug Transport Module)连接外部调试器(JTAG,或直接支持 GDB 调试协议)。
下面给出 RISC-V 调试规范的整体框架图:
如图所示,调试的基本流程是,外部调试器通过 DTM 发送 DMI 请求,DM 响应后执行对应的操作及抽象命令,抽象命令会转换成一组 RISC-V 指令,交由单个或者一组 Hart 执行,比如访问 Hart 的 CSR,并返回状态码(如果超时或者错误需要 DTM 重试或者通知调试器)。
下面给出正常情况和调试模式下 Hart 的状态机对比图:
以上提到的是调试规范的必要模块,还有一些可选的模块,比如上图在 Hart 中展示的 Trigger Module ,用来支持硬件断点和监视点。
接下来我们对重要模块进行讨论(寄存器配置请阅读手册,会更准确,下文主要讲解流程和原理),最后再和大家聊聊在 QEMU 上实现这个规范的注意事项。
Hart 的 Debug Mode(Sdext 扩展)
Sdext 扩展为外部调试器提供了必要支持:Debug Mode、专用 CSR 。该扩展必须与 DM 协同工作,单独实现无意义(因此建议在 QEMU 上实现这个扩展时,需要检查是否支持 DM )。
我们先聊聊 Debug Mode,它的权限高于 M 模式,且不受 PMP/PMA 限制。进入条件如下:
-
执行
ebreak指令(配置dcsr.ebreakm/s/u触发); -
外部调试请求(DM发起
haltreq); -
触发模块事件(如硬件断点)。
执行dret指令返回原模式(从 dpc CSR 恢复)。
扩展新增的 CSR 如下:
这里我们先讲 dscratch0/1 CSR。
由于 DM 主要采用非侵入式调试的方法,所以要实现对 Hart 的控制,需要 DM 将其翻译成对应的 RISC-V 指令序列,比如暂停、恢复。所谓暂停,就是用一组 RISC-V 指令实现 park loop(可以理解成一个 while 循环,里面不停的检查标志位,是否需要恢复或者执行调试器的命令)。
为了避免陷入 Debug Mode 执行 park loop 时,破坏 Hart 的现场,可以使用 dscratch0/1 临时保存 park loop 中占用的寄存器,退出 Debug Mode 时再恢复。
如何实现步进执行?
通过设置 dcsr.step,DM 在执行 dret 后恢复到 Hart 原来正常状态时,执行 dcsr.step 条指令后,再次陷入 Debug Mode。
如何实现断点?
外部调试器可以修改 Hart 被调试地址的指令为 ebreak, 当 Hart 执行到 ebreak 时,不再陷入普通异常,而是进入 Debug Mode。 可通过 dcsr.ebreak<u/s/m> 按特权级配置是否触发调试。
如何处理中断?
Debug Mode 的优先级高于所有中断(包括 NMI ),调试期间中断被挂起,退出调试模式后按优先级处理, dcsr.nmip 标志指示调试期间发生的 NMI 。
如何与特权模式交互?
Debug Module
DM 独立于指令集架构,通过内存映射寄存器(而非指令)实现控制,DM 会根据外部调试器配置的抽象命令,将其生成对应的 RISC-V 指令序列,除了基本的暂停和恢复执行命令,还需要支持访问 GPR/FPR/CSR 以及内存(结果写入 data0/1 寄存器)。
DM 除了抽象命令,还允许在抽象命令执行完毕以后,执行外部调试器输入的自定义指令序列(写入 program buffer),提供更高的灵活性和扩展性。
一些重要的寄存器罗列如下:
一个最小的 DM 实现,仅支持 GPR 和基础 CSR 访问(无内存访问/程序缓冲区),但需要声明兼容性(dmstatus.version 标识)。
当 DM 发起对 Hart 的调试信号时,会生成一个特殊的中断,告知 Hart 需要陷入 Debug Mode。这个特殊的中断,类似异常的处理流程,但是不属于异常。
Hart 在接受到来自 DM 的中断以后,需要将当前现场的下一条指令的 PC 地址,写入 DPC 寄存器,然后将陷入 Debug Mode 的原因更新到 DCSR 寄存器,之后更新 PC 寄存器指向 park loop 指令段的起始地址。
park loop 由 DM 实现,一般放在地址空间的起始区域。
进入 park loop 时会先保存现场,一般是把 park loop 内会用的寄存器保存到 dcsrach0/1 寄存器,然后进入一个 loop 里面循环检查 flag,来决定是 going 执行抽象命令,还是 resume 恢复到被调试前的现场。
如何执行抽象指令?
抽象命令必须以 ebreak 指令结尾,以便执行完以后再回到 park loop。
如何执行 Program Buffer?
外部调试器可以配置 DM 的寄存器,选择是先执行抽象命令再执行 Program buffer,还是只执行 Program Buffer。同样的,Program Buffer 的最后一条指令也必须是 ebreak 指令,但是允许在 Program Buffer 中存在跳出到外部的 jump 指令。同时 DM 也允许配置为执行完 Program Buffer 自动执行 ebreak 的行为,这样可以节省一条指令给用户使用。
如果抽象命令生成的指令结尾以及 Program Buffer 的结尾不是 ebreak,那么 DM 将失去对 Hart 的控制,导致行为不可控。
DM 也支持外部调试访问 DM 的 data0/1 寄存器时,自动触发上一次配置的抽象命令。
QEMU 模拟 RISC-V 调试规范
QEMU 本身支持通过 gdbstub 远程调试 Hart,如果 QEMU accelerator 使用 TCG,那么 gdbstub 的介入信号会被当做 TCG 的中断/异常来处理,如果 gdbstub 要暂停 Hart,可以将其 cs->halted 配置为 1。
由于 DM 的实现是非侵入式的,可以设计成和 gdbstub 兼容的方案。但需要注意以下几点:
- DM 发起抽象命令时,需要通过 qemu_irq 通知 Hart 切换到 Debug Mode;
- DM ROM 中存放的抽象命令对应的 RISC-V 指令序列每次都会发生修改,属于代码自修改行为,需要清 TB cache;
- 注意 DM 状态机的维护,与 Hart 状态相对应;
- DM 的 hartsel 必须是连续的,但是 Hart 的 mhartid 可以是不连续的,需要注意两者的映射关系。




