2. GDB基础:GDB的安装与配置、断点设置与管理、单步执行与变量查看

调试,说白了就是让程序停下来,让你看看它到底在想什么。GDB 就是干这个的。我最早接触 GDB 是在调试一个用户态驱动的时候,那会儿程序跑着跑着就崩了,加了一堆 printk 也没用。后来同事扔给我一句「用 GDB 啊」,我才开始认真学这玩意儿。嗯,今天就把我这些年用 GDB 的经验,挑最核心的讲给你听。

2.1 GDB 的安装与配置

安装 GDB 其实很简单。大多数 Linux 发行版都自带,如果没有,一行命令搞定:

# Ubuntu/Debian
sudo apt-get install gdb

# CentOS/RHEL
sudo yum install gdb

# Fedora
sudo dnf install gdb

装完之后,验证一下:

$ gdb --version
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2

我个人习惯在 ~/.gdbinit 里加点配置,省得每次都要敲重复命令。举个例子:

# 设置汇编风格为 Intel(我更喜欢这个,看着顺眼)
set disassembly-flavor intel

# 显示源代码行号
set print pretty on

# 历史记录保存
set history save on

# 自动加载 .gdbinit 文件(安全考虑,默认是关闭的)
set auto-load safe-path /
小提示: 如果你在公司环境里调试,记得把 auto-load safe-path 设得严格一点。我曾经见过有人因为加载了恶意的 .gdbinit 文件,导致调试信息泄露。安全无小事。

2.2 断点设置与管理

断点是调试的灵魂。没有断点,你就像在黑暗里摸象。GDB 的断点种类很多,我挑几个最常用的讲。

2.2.1 普通断点

最基础的用法,就是在函数入口或某一行停下:

(gdb) break main          # 在 main 函数入口设断点
(gdb) break driver_open   # 在 driver_open 函数入口设断点
(gdb) break 42            # 在当前文件的第 42 行设断点
(gdb) break file.c:100    # 在 file.c 的第 100 行设断点

你想想看,如果驱动有几十个文件,你一个个找行号多累?我一般先用 list 命令看看代码,再设断点:

(gdb) list driver_open, +20   # 显示 driver_open 函数的前 20 行
(gdb) break 128               # 然后在关键行设断点

2.2.2 条件断点

条件断点是个好东西。比如你只想知道当某个错误码出现时,程序是怎么走的:

(gdb) break driver_ioctl if error_code == -EINVAL
(gdb) break 256 if count > 1024 && flag == 1

我在项目中遇到过一个问题:一个驱动在特定条件下才会崩溃,但每次跑几千次才触发一次。用条件断点,我只在 count > 10000 时停下来,省去了大量手动检查的时间。

2.2.3 临时断点

有时候你只想停一次,之后自动删除。用 tbreak:

(gdb) tbreak driver_read   # 只停一次
(gdb) tbreak 88            # 第 88 行只停一次

2.2.4 断点管理

断点多了,管理就变得重要。我常用的命令:

(gdb) info breakpoints     # 查看所有断点
(gdb) disable 2            # 禁用 2 号断点(不删除,但暂时不用)
(gdb) enable 2             # 重新启用
(gdb) delete 2             # 删除 2 号断点
(gdb) delete               # 删除所有断点

输出示例:

(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000555555555189 in main at test.c:10
2       breakpoint     keep y   0x00005555555551a0 in driver_open at driver.c:42
        stop only if error_code == -EINVAL
3       breakpoint     keep n   0x00005555555551c0 in driver_read at driver.c:88
重要: 注意看第三行,Enb 列是 n,表示这个断点被禁用了。这在调试多个场景时非常有用——你可以保留所有断点,只启用当前需要的。

2.3 单步执行与变量查看

断点停下来了,然后呢?你得一步一步走,看看变量是怎么变的。

2.3.1 单步执行

GDB 提供了几种单步方式,我习惯这样用:

(gdb) step    # 步入:进入函数内部
(gdb) next    # 步过:不进入函数,直接执行完当前行
(gdb) finish  # 跳出:执行完当前函数,返回到调用处
(gdb) until   # 运行到指定行(比如跳出循环)

举个例子,假设你在 driver_open 函数里停住了:

(gdb) step
driver_open () at driver.c:45
45          ret = check_permission();

(gdb) step
check_permission () at driver.c:120
120         if (user_id == 0) {

(gdb) finish
Run till exit from #0  check_permission () at driver.c:120
0x00005555555551b0 in driver_open () at driver.c:46
46          if (ret != 0) {

你看,step 进入了 check_permission,finish 又跳出来了。这样你就能看到函数内部是怎么跑的。

注意: 如果函数没有调试符号(比如是库函数),step 会直接跳过。我曾经在调试一个第三方库时,怎么 step 都进不去,后来才发现人家编译时没加 -g 选项。白忙活半小时。

2.3.2 变量查看

单步执行的同时,你得盯着变量。GDB 的变量查看命令很直观:

(gdb) print ret          # 打印 ret 的值
$1 = 0

(gdb) print user_id      # 打印 user_id
$2 = 1000

(gdb) print &user_id     # 打印 user_id 的地址
$3 = (int *) 0x7fffffffe4a0

(gdb) print buffer       # 打印数组或指针
$4 = 0x5555555592a0 "Hello World"

(gdb) print *buffer      # 解引用指针
$5 = 'H'

(gdb) print buffer[0]    # 打印数组第一个元素
$6 = 'H'

我常用的一个技巧是 display 命令,让 GDB 每次停下来都自动显示某个变量:

(gdb) display ret
1: ret = 0

(gdb) display user_id
2: user_id = 1000

(gdb) display buffer
3: buffer = 0x5555555592a0 "Hello World"

这样你每次 next 或 step 之后,GDB 都会自动打印这些变量,省得你手动敲 print。嗯,这个习惯我用了好多年,效率提升很明显。

2.3.3 查看复杂数据结构

驱动开发中经常遇到结构体。比如:

struct device {
    int id;
    char name[32];
    struct device *parent;
    void *private_data;
};

(gdb) print dev
$7 = {id = 1, name = "usb0", parent = 0x5555555592e0, private_data = 0x0}

(gdb) print dev->parent
$8 = (struct device *) 0x5555555592e0

(gdb) print *dev->parent
$9 = {id = 0, name = "usb_bus", parent = 0x0, private_data = 0x555555559300}

如果结构体嵌套很深,可以用 set print pretty on 让输出更易读:

(gdb) set print pretty on
(gdb) print dev
$10 = {
  id = 1,
  name = "usb0",
  parent = 0x5555555592e0,
  private_data = 0x0
}

2.4 实战小技巧

最后分享几个我在实际调试中积累的小技巧:

  • 用 backtrace 看调用栈: 程序崩了之后,先 bt 看看是怎么走到这一步的。我见过太多人一上来就乱设断点,其实看看调用栈就能定位问题。
  • 用 frame 切换栈帧: frame 2 可以跳到调用栈的第二层,查看那里的局部变量。这在分析多层调用时特别有用。
  • 用 watch 监视变量变化: watch ret 会在 ret 的值发生变化时停下来。我曾经用它抓到一个内存越界修改的问题,省了我两天时间。
  • 用 info registers 看寄存器: 调试底层代码时,寄存器里的值往往能告诉你更多信息。
我的习惯: 每次开始调试前,我都会先 list 一下代码,心里有个谱。然后设 2-3 个关键断点,用 display 盯住核心变量。这样调试起来思路清晰,不会乱。

好了,GDB 的基础就讲到这里。下一章我们会深入聊聊如何用 GDB 调试用户态驱动的具体场景,包括内存泄漏、死锁、性能问题等。嗯,那些才是真正考验功力的时候。