QEMU gdbstub:-s -S 冻结启动与 GDB 远程调试协议详解
调试普通程序时,gdb 直接用操作系统提供的 ptrace 接口接管进程;调试一台虚拟机时这条路走不通——虚拟 CPU 不是宿主机内核认识的进程线程,客户机里跑的固件也没有任何调试服务可以连接。QEMU 的解法是在自己进程里内嵌一个 GDB 远程调试服务端,也就是 gdbstub:宿主机上的 gdb 不直接碰 CPU,而是通过一条 TCP 连接向 QEMU 发调试报文,由 QEMU 代为读写虚拟 CPU 的寄存器与内存、布置断点、控制单步。对 gdb 而言,远程调试一台裸机虚拟机和调试一个远程进程,用的是同一套命令。
本篇是 gdb 调试系列的第一篇,只解决「连上之前发生了什么」:在一台无界面的 RISC-V virt 虚拟平台上,用 -s -S 把机器冻结在第一条指令之前,分别从 QMP 与 gdb 两个视角核对状态,再把连接过程拆成协议时序。环境搭建见 QEMU 安装与第一个 RISC-V 虚拟平台,同属控制面的 monitor 通道见 QEMU monitor:不进客户机观测运行状态、设备与快照的控制通道,下断点与单步的实操放在续篇 QEMU gdb 第一个断点:从连接到命中 0x80000000。
本文要回答的问题
-s和-S只差一个字母,它们各自让 QEMU 做了什么,为什么教程里总是两个一起出现?- gdbstub 说的是什么协议,宿主机 gdb 与 QEMU 之间的报文长什么样?
- QMP 报出的
prelaunch和运行中途停下的paused有什么本质区别? - gdb 连上后读到的 PC 为什么是
0x1000,全部通用寄存器为零又说明了什么? - gdb 退出连接之后,被冻结的虚拟机会一直停着吗?
一、两个一字开关:-s 开出调试端口,-S 冻结启动
QEMU 命令行里最容易被混为一谈的就是这两个短开关,实际上它们管的是两件完全独立的事。-s 是一个简写,完整展开等于 -gdb tcp::1234,意思是让 QEMU 在宿主机的 TCP 1234 端口上开一个 gdbstub 服务端,等待 gdb 接入;它只负责「开门」,不改变虚拟机的运行节奏。-S 则与网络无关,它告诉 QEMU:进程启动之后不要立刻让虚拟 CPU 取指执行,整机冻结在复位状态,直到调试器发来继续运行的命令。
两个开关正交组合,才有了下面四种局面,这也是为什么入门教程几乎总是把它们写在一起:既要能连上去,又要保证连上去时代码一步都还没跑。
读这张概念卡抓住两组颜色:蓝色块描述「通道」,解决 gdb 怎么连进来;红色与紫色块描述「运行节奏」,解决 CPU 什么时候开始跑。只给 -s 不给 -S 完全合法,虚拟机正常启动,gdb 随时可以 later-attach;只给 -S 不给 -s 则机器冻住却没有调试入口,只能靠 monitor 的 cont 解救,实际中不会这么用。
| 开关 | 完整形态 | 管什么 | 缺了它会怎样 |
|---|---|---|---|
-s | -gdb tcp::1234 | 开 gdbstub 服务端,监听 TCP 1234 | gdb 无端口可连,target remote 被拒 |
-S | 无简写展开,独立选项 | 启动后冻结 CPU,不执行任何指令 | 虚拟机直接跑起来,调试器只能中途挂接 |
二、gdbstub 是什么:嵌在 QEMU 里的 GDB 远程调试服务端
gdbstub 这个名字可以拆成两半理解:gdb 指它服务的客户端是 GNU 调试器,stub 表示它是一个「桩」——自身不提供调试界面,只实现协议、代为操作被调目标。QEMU 把这个桩嵌在自己的进程内部,对外表现为一个 TCP 服务端,对内则能直接触碰虚拟 CPU 的执行循环和物理内存。它说的协议叫 GDB Remote Serial Protocol(GDB 远程串行协议,简称 RSP),最早为串口调试嵌入式板子设计,跑在 TCP 上只是换了承载介质,报文语法一字未改。
整条调试链路上有三个角色,关系如下图。蓝色是宿主机上的 gdb 进程,灰色是承载报文的回环端口与 QEMU 进程边界,紫色 gdbstub 是协议解释者,绿色虚拟 CPU 是真正被调试的对象,橙色块是它赖以读写的 CPU 现场。
读图抓住一条往返主线:gdb 发出的每个请求都先到紫色 gdbstub,由它翻译成对绿色 vCPU 的具体操作,结果再沿原路包成响应报文。gdb 全程不直接访问虚拟机内存,QEMU 也不实现任何调试交互逻辑——分工就在紫色这一层切开。
RSP 报文是纯文本的,格式为一个美元符号开头、井号加两位十六进制校验和结尾,宿主机 gdb 收到后用单个 + 或 - 字符确认。下面是几个与本篇直接相关的命令报文,后文的时序图会看到它们按这个次序出场。
$?#xx # 查询目标当前的停机原因
$g#xx # 读取全部寄存器的值
$p1#xx # 只读取编号为 1 的寄存器
$m1000,4#xx # 从物理地址 0x1000 读 4 字节内存
$Z0,80000000,4#xx # 在指定地址插入一个软件断点协议报文里没有「架构」字段:寄存器布局靠连接时双方约定,所以 gdb 命令行先执行 set architecture riscv:rv64 再 target remote,否则 gdb 不知道按 64 位 RISC-V 布局解码返回内容。
三、冻结语义:prelaunch 与 paused 不是一回事
实验脚本用下面的命令行启动虚拟机,与前面的概念卡逐一对应:-s -S 同时给出,另外挂一条 QMP 通道作为独立观测视角,-display none 关掉图形窗口。
qemu-system-riscv64 -machine virt -bios default -display none \
-s -S -qmp unix:./output/01/qmp.sock,server,nowait启动完成后脚本先轮询确认 1234 端口已经监听,再走 QMP 通道发 qmp_capabilities 与 query-status。此时 gdb 还没有连上来,QMP 返回的状态是 prelaunch、running 为 false。下面的终端实录覆盖版本基座、冻结启动与这次状态查询的完整过程,命令回显以 + 开头。
prelaunch 是理解 -S 的关键词,它与调试中常见的 paused 长得像、含义却不同。两者都满足 running=false,但分界在于虚拟机「是否曾经运行过」。
| 维度 | prelaunch | paused |
|---|---|---|
| 进入条件 | 带 -S 启动后的初始态,CPU 从未取指 | 至少运行过,之后被调试器或 monitor 停下 |
| 典型触发 | 开机即冻结,等待 gdb 接管 | 断点命中、stop 命令、快照存盘停顿 |
| CPU 现场 | 复位值:PC 指向复位向量,通用寄存器全零 | 停下那一刻的真实运行现场 |
| 如何离开 | gdb 连上后 continue,或 monitor 执行 cont | continue/cont 恢复,或直接退出 |
这个区分不是文字游戏:预启动冻结保证你看到的是硬件复位后的确定状态,寄存器值不受任何固件代码影响;而 paused 现场里满是运行留下的中间值。调试固件启动代码、排查「第一条指令之前」的问题时,只有 prelaunch 态能提供干净的起点。
四、连接握手与读寄存器:一次 RSP 往返的完整时序
接下来让 gdb 以批处理模式接入。命令行里 -q -nx 表示安静启动且不加载用户配置,-batch 让 gdb 执行完 -ex 指定的命令后自动退出,架构声明与连接命令的顺序不能颠倒:
riscv64-elf-gdb -q -nx -batch \
-ex 'set architecture riscv:rv64' \
-ex 'target remote :1234' \
-ex 'info registers pc' \
-ex 'info registers'连接建立后的交互按时序拆成下图:gdb 先发 ? 询问停机原因,gdbstub 回报陷阱信号;再发 g 读全部寄存器,gdbstub 向虚拟 CPU 取复位现场回传;批处理跑完后 gdb 主动断开,触发 gdbstub 的解冻逻辑。

读这张时序图抓住两个时刻:前半程所有交互都发生在 CPU 冻结期间,gdbstub 只是替 gdb 翻看静止的现场;最后一行 detach 之后方向反转,gdbstub 主动放行虚拟 CPU。下面的终端实录就是这次往返的真实输出。
实录里有两个值得逐一解释的数字。第一,pc 0x1000 是 RISC-V virt 机器的复位向量:QEMU 在物理地址 0x1000 放了一小段掩膜 ROM,复位后 CPU 从这里取第一条跳转指令,再奔赴 0x80000000 处的 OpenSBI 固件。PC 停在这里而不是固件主体,正好证明一条指令都还没执行。第二,从 ra、sp 到 t6 的 31 个通用寄存器全部为零,这是 RISC-V 规范规定的硬件复位值,与 prelaunch 态的判定互相印证——只要有任何一条指令跑过,sp、gp 这类启动代码最先改写的寄存器就不可能保持零值。

这张记录按五个编号小节展开,正好对应调试链路建立的先后次序。开头先列出 QEMU 11.1.2 与 riscv64-elf-gdb 18.1 的版本,随后冻结启动的虚拟机在 QMP 视角下报出 status: prelaunch、running: false;画面中段 gdb 批处理连上 1234 端口,架构声明之后那条 No executable 警告夹在连接成功的输出之间,警告下面寄存器读取照常完成——pc 停在 0x1000,整列通用寄存器全是 0x0,末尾 [Inferior detached] 标记会话结束。把警告与正常返回放在同一屏里看,最能说明它只是无符号提示而非连接失败。
五、No executable 警告与 detach:无符号调试与断开后的恢复
实录中紧接着架构声明有一条两行警告,初次使用 gdbstub 的人很容易把它误判为连接失败:
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.它的含义是:当前 gdb 没有用 file 命令加载任何 ELF 可执行文件,而远程目标又是一台裸机虚拟机,不会主动告诉 gdb 自己在跑什么镜像,所以 gdb 手里没有符号表。这是无符号调试的正常现象,不影响连接,也不影响寄存器与内存读取——证据就在警告下面,info registers 照常返回了完整结果。代价只在符号层面:输出里的 0x0000000000001000 in ?? () 中两个问号表示该地址无法对应任何函数名,下断点也只能按裸地址写。后续加载固件 ELF 之后,警告消失,地址就会显示成函数名与源码行。
再看实录最后一行 [Inferior 1 (process 1) detached]。批处理结束时 gdb 没有杀掉目标,而是分离连接——这与 target remote 的语义一致:远程模式下 gdb 退出默认 detach,被调目标继续存活。对 QEMU gdbstub 而言,detach 还携带一个额外动作:连接断开后,原本因调试而停住的虚拟 CPU 会被解除冻结、恢复取指执行。也就是说,-S 制造的冻结只保证「第一条指令之前没人催它跑」,并不保证机器永远停着;调试器一走,虚拟机就自行启动。本实验的脚本在 gdb 退出后立刻收掉了 QEMU 进程,日志没有展示恢复后的状态,读者在交互式 gdb 里手动 detach 后,再通过 QMP 执行 query-status 就能看到状态从停机变为 running。
若要在 gdb 断开后仍保持停机,就不要依赖 detach 的默认行为:让 gdb 会话保持连接,或改由 monitor 通道管理生命周期。
六、gdbstub 与 monitor:两条控制通道如何分工
讲完 gdbstub 的完整行为,可以把它和前一篇的 monitor 并排放在一起。两者都是 QEMU 开在控制面上的侧门,都不需要客户机系统配合,但抽象层级和擅长的问题完全不同,混用只会事倍功半。
| 维度 | QEMU monitor(QMP/HMP) | gdbstub(RSP) |
|---|---|---|
| 接入工具 | nc、libvirt 等 JSON 客户端 | gdb 及 RSP 兼容调试器 |
| 抽象层级 | 整机:状态、设备、快照、关机 | CPU:寄存器、内存、断点、单步 |
| 典型动作 | query-status、info block、savevm | info registers、break、stepi、continue |
| 对运行节奏的影响 | 多数查询只读,快照类动作短暂停顿 | 连接与命中断点时 CPU 停在调试态 |
| 适用场景 | 运维观测、脚本编排、状态存档 | 固件与内核开发、指令级排错 |
两条通道还能同时使用、互相作证:本篇就是用 monitor 侧的 QMP 确认 prelaunch,再用 gdbstub 侧的寄存器视图确认复位现场,一个回答「机器在什么状态」,一个回答「CPU 现场具体长什么样」。下一篇 QEMU gdb 第一个断点:从连接到命中 0x80000000 会在本篇的连接基础上往前迈一步:加载固件符号、在 OpenSBI 入口下软件断点、continue 命中后用 stepi 观察 PC 每次前进 4 字节,把 RSP 报文中的 Z0 断点命令落到实处。
这一篇在知识体系里的位置
本篇是 SoC 与 QEMU 入门路线上调试分支的第一篇,向上承接 QEMU 安装与第一个 RISC-V 虚拟平台 的环境准备与 认识 QEMU:从零看懂虚拟机、镜像与快照 的基本概念,横向与 QEMU monitor:不进客户机观测运行状态、设备与快照的控制通道 构成控制面双通道,向下为断点实操、固件源码级调试与外设单步观测打底。读完本篇应建立的核心认识是:-s 管通道、-S 管节奏,gdbstub 说 RSP 协议,prelaunch 是从未运行的复位态,调试器断开后虚拟机会恢复运行。
代码获取:本篇全部实验(冻结启动、QMP 状态查询、gdbstub 握手与寄存器读取)的可复现脚本见公开仓库 derekzhuo/geek-doc.cn 的
experiments/qemu-gdbstub-basics/目录。克隆后执行./01-gdbstub-handshake.sh即可重新跑出完整实验日志。
实验环境
机器/系统:MacBook Pro / macOS 26(Darwin 25.5)
工具版本:QEMU 11.1.2(Homebrew,
qemu-system-riscv64 --version实测)、riscv64-elf-gdb 18.1(riscv64-elf-gdb --version实测)日期:2026-11-08
完整命令序列:
bash./01-gdbstub-handshake.sh脚本内部依次完成:带
-s -S -qmp的qemu-system-riscv64 -machine virt -bios default -display none后台冻结启动并轮询 1234 端口;经 Unix 套接字发送qmp_capabilities、query-status确认 prelaunch;执行riscv64-elf-gdb -q -nx -batch -ex 'set architecture riscv:rv64' -ex 'target remote :1234' -ex 'info registers pc' -ex 'info registers'完成握手与寄存器读取;最后收掉 QEMU 进程。
总结
QEMU gdbstub 是嵌在虚拟机进程里的 GDB 远程调试服务端,本篇在 RISC-V virt 平台上把连接前的机制完整走了一遍:-s 等价于 -gdb tcp::1234,负责在 TCP 1234 上开出说 RSP 协议的调试端口,-S 负责让虚拟 CPU 冻结在第一条指令之前,两者一个管通道、一个管节奏;QMP 在 gdb 接入前报出 prelaunch 且 running=false,它与运行中途停机的 paused 的分界在于虚拟机是否曾经运行;gdb 经 target remote :1234 完成 RSP 握手后读到 PC 为复位向量 0x1000、31 个通用寄存器全零,证明冻结发生在任何指令执行之前;No executable 警告只表示缺少符号表而不是连接失败,gdb 批处理退出时 detach 连接,gdbstub 随即解除冻结、虚拟机恢复取指。