GDB 调试 & VSCode 调试
使用 GDB 调试
GDB 是一个强大的调试工具,但由于是命令行形式,有一定的上手门槛。下面结合实验程序,介绍一些常用的命令。更多命令可以参考官方新手教程和官方文档。
连接
target remote :1234
:由于我们需要使用 QEMU 模拟器,只能采用这种方式将 GDB 接入 QEMU。运行make debug
后,QEMU 窗口会等待 GDB 连接,因此会有一行灰色的提示。此时接入 GDB,可以看到蓝色的当前 EIP 位置是一个初始值。

file <file_path>
:加载符号表。GDB 需要符号表将二进制代码地址和具体的函数名对应起来。在编译时,需要添加-g
命令创建符号表,否则即使加载了也会提示无符号表。


信息查看
info registers [name]
:通过info registers
或i r
指令,可以查看 CPU 寄存器的值。当后面不带其他参数的时候,打印的是全体寄存器的值。带参数则会打印指定名字的寄存器的值。

monitor info registers
:为什么i r
中没有找到GDTR
、IDTR
之类的寄存器?因为这些寄存器并不是 CPU 寄存器。我们需要通过monitor info registers
命令,通过 QEMU monitor 获得这些寄存器的值。值得注意的是,这个命令不接受其他参数,会打印出所有系统寄存器的值;而且,这些值是解析过的,以 GS 为例,0018
是其值,000b8000
是其地址,ffffffff
是其段限,后面是权限等信息。更多含义请阅读官方文档。

x
:输出指定位置的内容。语法为x/<num><i(asm)|x(hex)|...> <addr/function_name/...>
。

运行
continue
:继续执行,直到遇到下一次断点si
:向下执行一行汇编代码n
:向下执行一行代码(需要加载符号表)break <addr/function_name/file_name:line>
:设置断点。可以在指定地址、指定函数、指定文件的指定代码行打断点。注意,后两者需要先加载符号表。

info breakpoints
:查看当前的断点信息

tui enable/disable
:开启/关闭图形界面

layout asm/src
:在汇编界面和代码界面切换

使用 VSCode 调试
GDB 命令行虽然直接,但使用起来仍然存在着一些不方便,例如每次都要输入的 target remote localhost:1234
、反复跳转的 tui
和 layout
,都给我们的调试带来了一定的困难(主要是重复输入太浪费时间了 😢)。 有没有更方便快捷的方式呢?有的,兄弟,有的!那就是 VSCode 自带的调试工具!
基础调试框架
从实现原理角度,VSCode 和我们一样,都是开了个 gdb 进程然后输命令。因此,我们只需要将我们常用的启动命令写入 VSCode 的配置文件,封装在 VSCode 能处理的 Task 和 Launch 结构中就可以了。 工作目录文件结构:请根据自己的文件目录结构,适当调整配置文件中的路径(所有涉及 ${config:lab.id} 的)
.
├── .vscode
│ ├── launch.json
│ └── settings.json
├── Lab1
└── Lab2
├── ...
└── Makefile
配置步骤:
- 在根目录下创建
.vscode
文件夹,进入文件夹 - 新建一个
settings.json
文件,写入以下内容:(如果原本就有内容,直接添加一项即可)
{
"others(if exists)": {},
"lab.id": 2 // 以后可以根据实验号进行调整,用于指定文件夹
}
- 新建一个
launch.json
文件,写入以下内容:
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "qemu-debug",
"type": "cppdbg",
"request": "launch",
// ${input:symbolFile} 需安装扩展 Tasks Shell Input
// 也可直接写成符号表的路径,如 kernel/kMain.elf
"program": "${input:symbolFile}",
"cwd": "${workspaceFolder}/Lab${config:lab.id}", // 请自行调整
"miDebuggerPath": "/usr/bin/gdb",
"miDebuggerServerAddress": "localhost:1234",
"stopAtConnect": true,
"MIMode": "gdb"
}
],
// 不安装扩展的话不需要写下面这个
"inputs": [
{
"id": "symbolFile",
"type": "command",
"command": "shellCommand.execute",
"args": {
"command": "find . | awk '/\\.elf/'",
"cwd": "${workspaceFolder}/Lab${config:lab.id}" // 请自行调整
}
}
]
}
使用方法
- 配置完成后,在左侧“运行和调试”选项中,会出现
qemu-debug
的选项。需要调试时,请先通过make debug
启动 QEMU 窗口。然后,在调试窗口中点击绿色三角开始调试。

- (如果配置了
${input:symbolFile}
)会出现下拉菜单。在此处选择合适的符号表。

- 观察到下方栏变色,即代表调试已经正常启动了。此时,可以在文件中添加断点,并通过上方按钮执行。

- 可以在左边的调试界面看到局部变量、CPU 寄存器等的值,还可以查看调用堆栈、监视指定表达式。

- 通过在“调用堆栈”的列表项上右击鼠标,可以打开反汇编视图,查看汇编代码的执行情况。

- 可以通过在断点红点处右击选择“编辑断点”,或在未打断点的行号数字上右击选择“添加条件断点”,将表达式加入断点,断点会在满足条件表达式的时候才暂停。

- 可以通过右上角按钮打开底部面板,选择“调试控制台”,在其中以
-exec <gdb 命令>
的形式,运行命令行形式的 GDB,这与直接使用 GDB 调试是完全相同的。在符号表无效的时候可以使用此方法。

无法调试 `uMain.elf`?
这是由于 GDB 默认寻址从 0x0
开始的问题,导致符号表无法加载在正确的位置上。以下为一些个人见解:
- 进入内核时,系统的
CS.base = 0x0, SS.base = 0x0
,因此在内核态下所有的地址均与物理地址相同 - 在
loadUMain
执行中,内核将用户程序加载在物理地址0x200000
上,并设置了CS.base = 0x200000, SS.base = 0x200000
- 执行
iret
后,CPU 以0x200000
为CS
和SS
寻址的基地址,EIP
此时赋值为0x0
,代表其实际访问物理内存为0x200000 + 0x0
,因此加载在0x200000
且以0x0
为静态地址编译链接的用户程序得以正常运行(程序运行时按照EIP
和ESP
中的虚拟地址) - 问题来了,GDB 默认
PC = 0x0 + EIP
,这就导致了 GDB 无法正常调试用户模式的程序!
解决方法:在 initSeg
中设置用户模式段选择子为 CS.base = 0x0, SS.base = 0x0
,加载以 0x200000
为静态地址编译链接(修改 app/Makefile
)的用户程序。这样程序就运行在物理地址 0x20000
,GDB 也可以正常调试了。此时程序会从 EIP = 0x200000
开始执行。