gdb 调试 OpenSBI:在 QEMU 里打下第一个硬件级断点
应用程序的调试从 main 开始,而固件的调试从「上电那一刻」开始。用 QEMU 学习 RISC-V 固件时,最迷人的一件事就是可以让 CPU 在执行第一条指令之前就停住,然后亲手在 OpenSBI 的入口地址 0x80000000 打下断点,一条指令一条指令地看着它启动。本篇面向第一次做这件事的读者,把 gdb 调试 OpenSBI 的完整操作序列走一遍:为什么是这个地址、怎么让虚拟机冻在起跑线上、断点命中后如何反汇编和单步,以及一个新手几乎必踩的坑——断点为什么会永远不命中。
gdbstub 这条调试通道本身的协议与暂停语义,在姊妹篇 gdb 调试 OpenSBI 的底层通道:QEMU gdbstub 是怎么工作的 里展开;本篇专注操作,默认你已经按 用 gdb 调试 OpenSBI 的环境准备:安装第一台 RISC-V virt 虚拟机 装好了 QEMU 和 riscv64-elf-gdb。
本文要回答的问题
- virt 机器上电后第一条指令在哪个地址,为什么 OpenSBI 的断点要下在
0x80000000而不是0x1000? -s -S两个参数各自做了什么,怎样让虚拟机冻在复位向量上一条指令都不跑?- gdb 连上后那句
No executable has been specified警告是不是出错了? break、continue、x/4i、stepi这套组合拳按什么顺序打,命中现场能读到什么?- 为什么断点、continue、单步必须放在同一个 gdb 会话里,分开做为什么会挂死?
这张流程卡就是本篇的主线,四步必须严格按序、且在同一个 gdb 会话内完成。前两步解决「让 CPU 停在正确的地方」,后两步解决「停下来之后看什么」。蓝色是输入条件,红色是全篇最关键的决策点——断点地址的选择,橙色与绿色分别是放行执行和观测验证。
一、两条地址线:复位向量 0x1000 与 OpenSBI 入口 0x80000000
下断点之前先要回答「断在哪」。RISC-V 架构规定,CPU 上电复位后程序计数器 PC 被硬件置为复位向量,具体值由平台设计决定。QEMU 的 virt 机器把复位向量设在 0x1000:这个地址落在机器内置的一小片 ROM 区域里,存放的是 QEMU 生成的早期引导代码,它的任务很简单——做最起码的初始化,然后一条跳转指令把控制权交给 DRAM 里真正的固件。
DRAM 在 virt 机器的地址映射里从 0x80000000 开始。用 -bios default 启动时,QEMU 自带的 OpenSBI 固件被加载到这片 DRAM 的起始位置,固件的第一条指令就正好位于 0x80000000。于是开机的控制流是一条两跳的接力:复位后 CPU 站在 0x1000,执行 ROM 里的跳转,落到 0x80000000,OpenSBI 从这里开始它的生命。我们要调试的是 OpenSBI,断点自然下在它的入口,而不是只存放跳板代码的复位向量。
读这张图抓住颜色的语义:蓝色是硬件给定的起点,紫色是 ROM 里的中转动作,红色是全篇的核心地址 0x80000000,橙色是 gdb 放行执行后的断点拦截,绿色是停下来之后的单步观测。地址本身不需要背,理解这条「ROM 复位向量 → DRAM 固件入口」的两跳接力,面对任何开发板时都能照同一思路找到固件入口。
顺带回答一个自然的疑问:能不能直接把断点下在 0x1000?技术上可以,-S 冻结后 CPU 本来就停在那里,什么都不用下;但那片 ROM 里是 QEMU 内置的跳板代码,不属于 OpenSBI,也没有对应源码,停在那里只能看到几条跳转,学不到固件逻辑。调试目标是 OpenSBI,观察点就应该设在它自己的第一条指令上。
二、让虚拟机冻在起跑线上:-s -S 与 gdb 连接
正常启动的虚拟机会在几毫秒内跑完整个 OpenSBI,根本来不及干预。需要两个开关把它冻住:-s 是 -gdb tcp::1234 的简写,让 QEMU 在本机 1234 端口开一个 gdbstub 调试服务;-S 让 CPU 在复位后保持暂停,不自动取指。两个合起来的效果是:虚拟机进程照常启动、1234 端口开始监听,但 vCPU 一条指令都不执行,PC 原样停在 0x1000。
本次实验的启动命令如下,-display none 表示不需要图形窗口,-qmp 挂一个 QMP 套接字供脚本查询状态:
qemu-system-riscv64 -machine virt -bios default -display none \
-s -S -qmp unix:qmp.sock,server,nowait另开一个终端启动交叉版 gdb。-q 关掉版本横幅,-nx 不读用户目录下的 .gdbinit 以免污染实验环境,-batch 让命令执行完自动退出,每条 -ex 携带一条 gdb 命令:
riscv64-elf-gdb -q -nx -batch \
-ex 'set architecture riscv:rv64' \
-ex 'target remote :1234' \
-ex 'info registers pc'先声明架构是因为 gdb 手里没有任何可执行文件,无法从 ELF 头推断目标是 RV64;target remote :1234 连上 gdbstub 之后,读到的第一个现场就是复位状态。监视器与 QMP 的分工如果想补背景,可以读 QEMU 监视器入门:QMP/HMP 与 info 命令族。
三、打下第一个断点并让 continue 命中
连接建立后,先确认起点 PC,再在固件入口下断点,然后 continue 放行——CPU 从 0x1000 起跑,执行 ROM 跳板,在踏进 0x80000000 第一条指令的瞬间被 gdbstub 拦回调试态。下面是本次实验的原始实录前半段。
逐行读这段输出。连接成功后 gdb 打印 The target architecture is set to "riscv:rv64",确认架构声明生效。紧接着的两行警告值得专门解释:
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.这不是错误,而是裸机调试的常态。gdb 平时调试程序时会加载一个 ELF 文件,从中取得函数名、行号、变量类型等符号信息;这次我们只告诉它「去 1234 端口连一个远程目标」,没有给任何 ELF,所以它坦白自己没有符号表,后续回溯里出现 in ?? () 也是同一原因——它只知道地址,不知道这个地址对应什么函数。等手头有带符号的 OpenSBI 编译产物时,用 file fw_jump.elf 加载即可,?? 就会变成函数名。
警告之后是两个关键现场。info registers pc 显示起点 pc 0x1000,印证了第一节的复位向量;break *0x80000000 回显 Breakpoint 1 at 0x80000000,注意地址前的星号不能省——裸机调试里星号表示「在这个绝对地址下断点」,不带星号时 gdb 会把参数当符号名解析,而我们恰好没有符号表。continue 之后输出 Breakpoint 1, 0x0000000080000000 in ?? (),再次读 PC 已是 0x80000000:ROM 跳板执行完毕,CPU 精准停在 OpenSBI 的第一条指令上。
四、反汇编、单步与寄存器:亲眼看 PC 每次加 4
断点命中只证明「停对了地方」,要证明这里确实是可执行的固件代码,还得看指令本身。x/4i $pc 让 gdb 从当前 PC 开始反汇编四条指令(x 是 examine,4i 是四条 instruction),stepi 则每次精确执行一条机器指令——注意是 stepi 不是源码级的 step,没有符号和行号时只有指令级单步可用。
反汇编给出的四条指令是一段典型的裸机入口序言:
0x80000000: add s0, a0, zero
0x80000004: add s1, a1, zero
0x80000008: add s2, a2, zero
0x8000000c: jal 0x80000678RISC-V 的 SBI 规范约定,前一级引导程序把三个启动参数放在 a0、a1、a2 三个寄存器里(分别是硬件 ID、设备树地址等),OpenSBI 入口做的第一件事就是把它们原样搬运到 callee-saved 的 s0、s1、s2 保存起来,免得后续函数调用把参数冲掉;保存完用一条 jal 跳转到 0x80000678 的正式初始化流程。三条 add rd, rs, zero 等价于寄存器拷贝,是 RISC-V 没有原生 mov 指令的标志性写法。
两次 stepi 之后读 PC,结果是 0x80000008,正好前进了 8 个字节、每次 4 字节。这背后是 RV64I 基础指令集的定长编码:每条指令占 32 位即 4 字节,指令地址天然四字节对齐。需要补一句边界知识:RISC-V 的 C 压缩扩展允许 16 位指令,开启后单步可能只前进 2 字节;而这里四条指令都是 32 位宽,所以观测到稳定的 +4。实录最后一行 [Inferior 1 (process 1) detached] 是 -batch 会话结束时 gdb 主动脱离目标,下一节就讲这个脱离动作带来的坑。

记录里的地址变化只有三个数,却串起了整条启动路径。会话开始时 pc 还是复位向量 0x1000,break *0x80000000 加 continue 之后断点命中,PC 精确停在 OpenSBI 入口;随后的反汇编窗口给出四条入口指令,箭头 => 标出当前停在第一条 add s0,a0,zero 上;每执行一次 stepi,下一次读 PC 就加 4,两次之后落在 0x80000008 的第三条指令处。画面末尾的 detach 提示提醒读者:这些动作全部发生在同一个 gdb 会话里,会话一断虚拟机就会继续跑。
五、同一个会话铁律:gdb 一退出,虚拟机就恢复奔跑
本次实验把看 PC、下断点、continue、反汇编、单步全部塞进同一条 gdb 命令里,这不是脚本作者的洁癖,而是被 gdbstub 的暂停语义逼出来的。远程调试的生命周期是这样的:gdb 连接期间 vCPU 处于调试态,听 gdb 指挥;gdb 退出并脱离(detach)时,gdbstub 会把 vCPU 恢复为自由运行状态,虚拟机从被打断的地方继续全速奔跑。
设想一个看似合理的拆法:第一个 gdb 会话只连接、看一眼 0x1000 然后退出;第二个会话再连上、在 0x80000000 下断点、continue。第一个会话退出的瞬间虚拟机就解冻了,OpenSBI 的入口代码在第二个会话敲命令之前早已执行过去——0x80000000 是一条只会经过一次的单程路,断点下在一个已经跑过的地址上,CPU 再也不会回来,continue 只能永久挂住,表现为终端毫无反应的假死。
所以正确纪律只有一条:从 target remote 到最后一次 stepi,所有动作放在同一个会话里完成。交互式调试时就保持一个 gdb 窗口不退出;脚本化时用 -batch 配合多条 -ex,让 gdb 在单次进程内按顺序执行完再脱离。如果确实需要中途换一个 gdb 接手,替代办法是不要 detach,而是用 QEMU 监视器的 stop/cont 或在第一个会话里先把 vCPU 保持暂停,具体握手细节见 gdb 调试 OpenSBI 的底层通道:QEMU gdbstub 是怎么工作的。
万一已经遇到 continue 挂死,判定也简单:gdb 长时间无任何回显、按 Ctrl-C 中断后读 PC 发现已经远超 0x80000000(甚至跑到更高地址的内核阶段),就说明断点下晚了。这种现场没有补救意义,直接结束 gdb、杀掉这台虚拟机重新用 -s -S 起一台即可——虚拟机是一次性的实验环境,重开的成本只有几秒,比重追跑飞的现场便宜得多。
六、本篇命令速查与常见现象
把实操中用到的命令和第一次连接时容易发懵的现象收成一张表,调试时按行对照即可。
| 命令或现象 | 含义 | 备注 |
|---|---|---|
-s | QEMU 在 tcp::1234 开 gdbstub | -gdb tcp::1234 的简写 |
-S | 复位后不自动运行 | 与 -s 搭配才能冻在 0x1000 |
set architecture riscv:rv64 | 手动声明目标架构 | 无 ELF 时必须先声明 |
target remote :1234 | 连接 gdbstub | 主机本机回环,端口与 -s 对应 |
break *0x80000000 | 在绝对地址下断点 | 星号不可省,否则按符号名解析 |
continue | 放行到下一个断点 | 断点跑空时会永久挂住,查会话纪律 |
x/4i $pc | 反汇编 PC 起四条指令 | i 可换数量,如 x/8i |
stepi | 执行一条机器指令 | 无源码时用它,不用 step |
info registers pc | 读程序计数器 | 看通用寄存器换 info registers |
No executable has been specified | 没有符号文件的正常警告 | 有 ELF 后用 file xxx.elf 消除 |
实验环境
- 机器/系统:MacBook Pro(Intel x86_64)/ macOS 26(Darwin 25.5)
- 工具版本:QEMU 11.1.2(qemu-system-riscv64)、riscv64-elf-gdb 18.1
- 日期:2026-11-08
- 完整命令序列:执行
02-first-breakpoint.sh一条脚本完成全部步骤——以-s -S启动冻结的 virt 虚拟机并等待 1234 端口就绪,随后在单个 gdb-batch会话内依次执行info registers pc、break *0x80000000、continue、info registers pc、x/4i $pc、两次stepi、再次info registers pc,最后收掉虚拟机,输出即本篇引用的完整实录。
代码获取:本篇实验的可复现脚本见公开仓库 derekzhuo/geek-doc.cn 的
experiments/qemu-gdbstub-basics目录。克隆后进入该目录执行./02-first-breakpoint.sh,终端会依次打印复位向量现场、断点命中、入口反汇编与单步结果,与本篇实录逐条对应;同目录的01-gdbstub-handshake.sh是 gdbstub 连接握手的配套实验。
这一篇在知识体系里的位置
本篇是 SoC 与 QEMU 入门路线里 gdb 调试系列的实操首篇,与原理篇 gdb 调试 OpenSBI 的底层通道:QEMU gdbstub 是怎么工作的 构成一对:那篇回答 gdbstub 报文怎么收发、暂停语义怎么实现,本篇回答手该怎么敲、地址怎么选。virt 机器的安装与 OpenSBI 固件来源在 用 gdb 调试 OpenSBI 的环境准备:安装第一台 RISC-V virt 虚拟机,运行期控制虚拟机的另一条通道 QMP/HMP 在 QEMU 监视器入门:QMP/HMP 与 info 命令族。再往后走,可以加载带符号的 OpenSBI ELF 做源码级调试,或者把同样的方法用到 U-Boot、Linux 内核的入口断点上——地址映射各有不同,「找入口、冻 CPU、单会话打断点」的方法论完全一致。
总结
gdb 调试 OpenSBI 的第一次实操围绕一条地址接力展开:virt 机器复位后 PC 站在 ROM 复位向量 0x1000,一条跳转把控制权交给 DRAM 起始处 0x80000000 的 OpenSBI 固件入口,断点就下在这个绝对地址上。用 -s -S 让虚拟机冻在起跑线,单会话内依次完成 target remote :1234、break *0x80000000、continue、x/4i $pc 与 stepi,就能看到入口处保存 a0/a1/a2 的三条 add 与一条 jal,并实测到 RV64I 定长指令下单步每次 PC 加 4。No executable has been specified 只是没有符号表的正常警告,而断点永不命中的头号原因是 gdb 脱离后虚拟机恢复自由运行、入口代码单程跑过——调试动作始终锁在同一个会话里,这个坑即可避开。