4. QEMU虚拟平台搭建
调试环境搭建,说白了就是让宿主机上的GDB能跟QEMU里跑的“虚拟芯片”对上话。很多初学者觉得这步麻烦,其实搞清楚三个环节就行:交叉编译GDB、打开QEMU的gdbstub、建立网络或管道连接。我最早做汽车MCU调试时,就因为gdbstub端口配错,折腾了整整一下午。嗯,咱们一步步来。
4.1 为什么需要交叉编译GDB?
你想想看,宿主机是x86_64架构,目标机是ARM Cortex-R或Cortex-M。宿主机上的原生GDB只能调试本地x86程序,没法解析ARM指令集。所以我们需要一个在x86上运行、但能理解ARM指令的GDB——这就是交叉编译版本的GDB。
核心概念:交叉GDB = 运行在宿主机(x86) + 调试目标机(ARM)的二进制。它跟目标机上的gdbserver(或QEMU内置的gdbstub)配合,完成远程调试。
4.2 交叉编译GDB的完整步骤
我个人习惯用aarch64-none-elf或arm-none-eabi工具链。这里以ARM Cortex-A72(QEMU的virt平台常用)为例,展示交叉编译GDB 14.1的过程。
4.2.1 准备源码与工具链
# 下载GDB源码
wget https://ftp.gnu.org/gnu/gdb/gdb-14.1.tar.xz
tar -xf gdb-14.1.tar.xz
cd gdb-14.1
# 确保交叉编译器在PATH中
export PATH=/opt/gcc-arm-10.3-2021.07/bin:$PATH
# 验证
aarch64-none-elf-gcc --version
4.2.2 配置与编译
mkdir build && cd build
../configure \
--target=aarch64-none-elf \
--prefix=/opt/gdb-cross-aarch64 \
--disable-werror \
--with-python=no \
--with-expat=yes
make -j$(nproc)
make install
我的经验:--disable-werror能避免因编译器警告导致编译失败。另外,如果不需要Python脚本扩展,--with-python=no可以加快编译速度。我在项目里遇到过expat库缺失导致XML解析失败,所以建议加上--with-expat=yes并提前安装libexpat1-dev。
4.2.3 验证交叉GDB
/opt/gdb-cross-aarch64/bin/aarch64-none-elf-gdb --version
# 输出示例:
GNU gdb (GDB) 14.1
Copyright (C) 2023 Free Software Foundation, Inc.
...
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=aarch64-none-elf".
看到--target=aarch64-none-elf就对了。这说明这个GDB能解析ARM64的指令和符号。
4.3 QEMU的gdbstub配置
QEMU内置了一个轻量级的GDB服务器,叫gdbstub。它监听一个TCP端口,等待GDB连接。启动QEMU时加上-s或-gdb参数即可开启。
4.3.1 两种启动方式
| 参数 | 说明 | 示例 |
|---|---|---|
-s |
等价于-gdb tcp::1234,默认端口1234 |
qemu-system-aarch64 -s ... |
-gdb |
自定义监听地址和端口 | qemu-system-aarch64 -gdb tcp::2345 |
注意:如果同时使用-s和-S,QEMU会在启动时暂停CPU,等待GDB连接后再开始执行。这是调试bootloader或早期初始化代码的常用组合。我曾经因为忘了加-S,结果程序跑完了GDB才连上,啥也没抓到。
4.3.2 完整启动示例
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a72 \
-smp 4 \
-m 2G \
-kernel my_os.elf \
-nographic \
-s \ # 开启gdbstub,监听1234端口
-S # 启动时暂停CPU
4.4 建立宿主机-目标机调试链路
链路建立分三步:启动QEMU(带gdbstub)→ 启动交叉GDB → 远程连接。我画了一张流程图,帮你理清关系。
4.4.1 连接操作
# 终端1:启动QEMU(带gdbstub,暂停CPU)
qemu-system-aarch64 -machine virt -cpu cortex-a72 -kernel my_os.elf -nographic -s -S
# 终端2:启动交叉GDB并连接
/opt/gdb-cross-aarch64/bin/aarch64-none-elf-gdb my_os.elf
# 在GDB内部执行:
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000000040000000 in _start ()
# 成功!现在可以设置断点、单步执行了
(gdb) break kernel_main
(gdb) continue
避坑指南:如果连接时提示Connection refused,先检查QEMU是否真的在运行。我遇到过好几次,终端1的QEMU因为参数错误直接退出了,终端2还在傻等。另外,target remote后面不要加多余空格,localhost:1234中间是冒号。
4.5 调试链路的验证方法
连接成功后,我通常会做三个快速验证,确保链路真的通了:
- 查看寄存器:
info registers,确认能看到ARM的通用寄存器和PC值。 - 设置断点:
break *0x40000000,然后continue,看能否停在入口地址。 - 单步执行:
stepi,看PC是否按ARM指令长度递增。
(gdb) info registers
x0 0x0 0
x1 0x0 0
...
pc 0x40000000 0x40000000 <_start>
(gdb) break *0x40000004
Breakpoint 1 at 0x40000004
(gdb) continue
Continuing.
Breakpoint 1, 0x0000000040000004 in _start ()
(gdb) stepi
0x0000000040000008 in _start ()
看到PC从0x40000000走到0x40000004再到0x40000008,说明链路完全正常。这时候你就可以像调试本地程序一样,调试QEMU里的ARM代码了。
注意:如果单步时发现PC跳到了奇怪地址(比如0x0或0xffffxxxx),通常是中断向量表或异常处理没配置好。别慌,这是目标机软件问题,不是调试链路的问题。你可以用monitor system_reset让QEMU复位,重新来过。
4.6 进阶:多核调试与硬件断点
QEMU的gdbstub支持多核调试。用info threads查看所有虚拟CPU,用thread n切换到指定核心。
(gdb) info threads
Id Target Id Frame
1 Thread 1 (CPU#0) 0x40000000 in _start ()
2 Thread 2 (CPU#1) 0x40000000 in _start ()
3 Thread 3 (CPU#2) 0x40000000 in _start ()
4 Thread 4 (CPU#3) 0x40000000 in _start ()
(gdb) thread 3
[Switching to thread 3 (Thread 3)]
#0 0x40000000 in _start ()
硬件断点用hbreak命令。QEMU模拟器支持最多4个硬件断点,适合调试那些不能修改指令内存的场景(比如Flash中的bootloader)。
(gdb) hbreak *0x40000100
Hardware assisted breakpoint 1 at 0x40000100
好了,到这一步,你的GDB调试环境就彻底搭好了。后面章节我们会基于这个链路,做各种性能分析和故障定位。记住这个流程:交叉GDB → QEMU gdbstub → TCP连接,以后每次搭建新平台都是这个套路。