Appearance

GDB 调试 & VSCode 调试

byml2025-03-29OS

使用 GDB 调试

GDB 是一个强大的调试工具,但由于是命令行形式,有一定的上手门槛。下面结合实验程序,介绍一些常用的命令。更多命令可以参考官方新手教程open in new window官方文档open in new window

连接

  • target remote :1234:由于我们需要使用 QEMU 模拟器,只能采用这种方式将 GDB 接入 QEMU。运行 make debug 后,QEMU 窗口会等待 GDB 连接,因此会有一行灰色的提示。此时接入 GDB,可以看到蓝色的当前 EIP 位置是一个初始值。
  • file <file_path>:加载符号表。GDB 需要符号表将二进制代码地址和具体的函数名对应起来。在编译时,需要添加 -g 命令创建符号表,否则即使加载了也会提示无符号表。

信息查看

  • info registers [name]:通过 info registersi r 指令,可以查看 CPU 寄存器的值。当后面不带其他参数的时候,打印的是全体寄存器的值。带参数则会打印指定名字的寄存器的值。
  • monitor info registers:为什么 i r 中没有找到 GDTRIDTR 之类的寄存器?因为这些寄存器并不是 CPU 寄存器。我们需要通过 monitor info registers 命令,通过 QEMU monitor 获得这些寄存器的值。值得注意的是,这个命令不接受其他参数,会打印出所有系统寄存器的值;而且,这些值是解析过的,以 GS 为例,0018 是其值,000b8000 是其地址,ffffffff 是其段限,后面是权限等信息。更多含义请阅读官方文档open in new window
  • 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、反复跳转的 tuilayout,都给我们的调试带来了一定的困难(主要是重复输入太浪费时间了 😢)。 有没有更方便快捷的方式呢?有的,兄弟,有的!那就是 VSCode 自带的调试工具!

基础调试框架

从实现原理角度,VSCode 和我们一样,都是开了个 gdb 进程然后输命令。因此,我们只需要将我们常用的启动命令写入 VSCode 的配置文件,封装在 VSCode 能处理的 Task 和 Launch 结构中就可以了。 工作目录文件结构:请根据自己的文件目录结构,适当调整配置文件中的路径(所有涉及 ${config:lab.id} 的)

.
├── .vscode
│   ├── launch.json
│   └── settings.json
├── Lab1
└── Lab2
    ├── ...
    └── Makefile

配置步骤:

  1. 在根目录下创建 .vscode 文件夹,进入文件夹
  2. 新建一个 settings.json 文件,写入以下内容:(如果原本就有内容,直接添加一项即可)
{
    "others(if exists)": {},
    "lab.id": 2  // 以后可以根据实验号进行调整,用于指定文件夹
}
  1. 新建一个 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}" // 请自行调整
            }
        }
    ]
}

使用方法

  1. 配置完成后,在左侧“运行和调试”选项中,会出现 qemu-debug 的选项。需要调试时,请先通过 make debug 启动 QEMU 窗口。然后,在调试窗口中点击绿色三角开始调试。
  1. (如果配置了 ${input:symbolFile} )会出现下拉菜单。在此处选择合适的符号表。
  1. 观察到下方栏变色,即代表调试已经正常启动了。此时,可以在文件中添加断点,并通过上方按钮执行。
  1. 可以在左边的调试界面看到局部变量、CPU 寄存器等的值,还可以查看调用堆栈、监视指定表达式。
  1. 通过在“调用堆栈”的列表项上右击鼠标,可以打开反汇编视图,查看汇编代码的执行情况。
  1. 可以通过在断点红点处右击选择“编辑断点”,或在未打断点的行号数字上右击选择“添加条件断点”,将表达式加入断点,断点会在满足条件表达式的时候才暂停。
  1. 可以通过右上角按钮打开底部面板,选择“调试控制台”,在其中以 -exec <gdb 命令> 的形式,运行命令行形式的 GDB,这与直接使用 GDB 调试是完全相同的。在符号表无效的时候可以使用此方法。
无法调试 `uMain.elf`?

这是由于 GDB 默认寻址从 0x0 开始的问题,导致符号表无法加载在正确的位置上。以下为一些个人见解:

  • 进入内核时,系统的 CS.base = 0x0, SS.base = 0x0,因此在内核态下所有的地址均与物理地址相同
  • loadUMain 执行中,内核将用户程序加载在物理地址 0x200000 上,并设置了 CS.base = 0x200000, SS.base = 0x200000
  • 执行 iret 后,CPU 以 0x200000CSSS 寻址的基地址,EIP 此时赋值为 0x0,代表其实际访问物理内存为 0x200000 + 0x0,因此加载在 0x200000 且以 0x0 为静态地址编译链接的用户程序得以正常运行(程序运行时按照 EIPESP 中的虚拟地址)
  • 问题来了,GDB 默认 PC = 0x0 + EIP,这就导致了 GDB 无法正常调试用户模式的程序!

解决方法:在 initSeg 中设置用户模式段选择子为 CS.base = 0x0, SS.base = 0x0,加载以 0x200000 为静态地址编译链接(修改 app/Makefile)的用户程序。这样程序就运行在物理地址 0x20000,GDB 也可以正常调试了。此时程序会从 EIP = 0x200000 开始执行。