第一章:GDB入门——从零开始认识这个调试利器
1.1 GDB是什么?
GDB,全称GNU Debugger,是Linux平台上最经典的调试器。说白了,它就是一个能让你「看穿」程序运行过程的工具。
我刚开始接触GDB时,觉得它就是个命令行工具,黑乎乎的界面,远不如IDE的图形化调试来得直观。但用久了你会发现——GDB才是真正能解决问题的硬核工具。IDE做不到的,它能做;IDE看不到的,它能看。
GDB能让你做到三件事:
- 控制程序执行——想停就停,想跑就跑
- 观察程序状态——变量、内存、寄存器,一览无余
- 修改程序行为——运行时改值、改流程,现场修复
一句话总结:GDB就是程序员的「显微镜」和「手术刀」。
1.2 GDB能做什么?
我遇到过不少同事,觉得调试就是「加printf打印日志」。嗯,这确实是最原始的方法。但你想过没有——
- 生产环境不能随便加日志怎么办?
- 段错误(Segmentation Fault)发生时,你根本不知道在哪崩溃?
- 多线程死锁了,日志里看不出任何线索?
这些场景,GDB都能搞定。具体来说:
| 功能 | 说明 | 我常用的场景 |
|---|---|---|
| 断点调试 | 在指定行、函数、条件处暂停 | 排查逻辑错误,逐行看变量变化 |
| 查看调用栈 | 程序崩溃时,看是谁调了谁 | 段错误后,bt命令直接定位 |
| 内存检查 | 查看指针指向的内容、堆栈数据 | 排查野指针、内存泄漏 |
| 修改变量 | 运行时改值,绕过错误分支 | 测试异常路径,不用重新编译 |
| 反汇编 | 看CPU到底在执行什么指令 | 优化热点代码时,看汇编级效率 |
我的经验:GDB最强大的地方不是「调试」,而是「诊断」。尤其是线上问题,用GDB attach到正在运行的进程上,现场分析,这才是它的杀手锏。
1.3 GDB的安装与启动
安装GDB非常简单。不同系统略有差异:
# Ubuntu/Debian
sudo apt-get install gdb
# CentOS/RHEL
sudo yum install gdb
# macOS (通过Homebrew)
brew install gdb
# 验证安装
gdb --version
安装完成后,启动GDB有两种方式:
- 直接调试程序:
gdb ./myprogram - 分析core dump:
gdb ./myprogram core.12345 - 附加到运行中的进程:
gdb -p 12345
注意:用GDB调试程序,编译时一定要加 -g 选项。否则你看到的只有汇编代码,没有源码行号。我曾经帮一个同事排查问题,他编译时没加 -g,我折腾了半天才发现——嗯,这个坑踩过一次就不会忘了。
1.4 第一个GDB调试会话
光说不练假把式。我们来写一个简单的C程序,亲手走一遍GDB调试流程。
先准备一个示例程序 hello.c:
#include <stdio.h>
void say_hello(const char *name) {
printf("Hello, %s!\n", name);
}
int main() {
char *names[] = {"Alice", "Bob", "Charlie"};
int i;
for (i = 0; i <= 3; i++) {
say_hello(names[i]);
}
return 0;
}
注意看,这个程序有个明显的bug——循环条件是 i <= 3,但数组只有3个元素(下标0、1、2)。当i=3时,访问 names[3] 会越界。
编译并启动GDB:
gcc -g -o hello hello.c
gdb ./hello
进入GDB后,你会看到类似这样的提示符:(gdb)。接下来我们一步步操作:
(gdb) list # 查看源码
1 #include <stdio.h>
2
3 void say_hello(const char *name) {
4 printf("Hello, %s!\n", name);
5 }
6
7 int main() {
8 char *names[] = {"Alice", "Bob", "Charlie"};
9 int i;
10
11 for (i = 0; i <= 3; i++) {
12 say_hello(names[i]);
13 }
14
15 return 0;
16 }
(gdb) break 12 # 在第12行设置断点
Breakpoint 1 at 0x40054f: file hello.c, line 12.
(gdb) run # 运行程序
Starting program: /home/user/hello
Breakpoint 1, main () at hello.c:12
12 say_hello(names[i]);
(gdb) print i # 查看当前i的值
$1 = 0
(gdb) print names[i] # 查看names[i]的值
$2 = 0x4006e8 "Alice"
(gdb) continue # 继续运行
Hello, Alice!
Breakpoint 1, main () at hello.c:12
12 say_hello(names[i]);
(gdb) print i
$3 = 1
(gdb) continue # 继续,重复几次...
Hello, Bob!
Breakpoint 1, main () at hello.c:12
12 say_hello(names[i]);
(gdb) print i
$4 = 2
(gdb) continue
Hello, Charlie!
Breakpoint 1, main () at hello.c:12
12 say_hello(names[i]);
(gdb) print i
$5 = 3
(gdb) print names[3] # 看!越界了
$6 = 0x0
(gdb) step # 单步进入函数
say_hello (name=0x0) at hello.c:4
4 printf("Hello, %s!\n", name);
(gdb) print name
$7 = 0x0 # 传入的是NULL指针!
(gdb) quit # 退出GDB
看到了吗?当i=3时,names[3] 是NULL。程序虽然没立刻崩溃,但传入NULL给printf,在某些系统上会直接段错误。这就是GDB帮我们抓到的bug。
我的习惯:每次写循环,我都会先用GDB跑一遍边界条件。尤其是 i <= n 这种写法,十有八九会出问题。改成 i < n 就安全了。
1.5 本章知识体系
下面这张图,帮你理清GDB入门的核心脉络:
1.6 本章小结
这一章我们走完了GDB的入门流程。你学会了:
- GDB是什么——一个能让你「看穿」程序的调试器
- GDB能做什么——断点、调用栈、内存检查、反汇编
- 怎么安装和启动——apt/yum/brew,三种方式
- 第一个调试会话——亲手抓到了一个数组越界的bug
说实话,GDB的学习曲线确实有点陡。但别怕,多用几次就习惯了。我刚开始用GDB时,连 quit 命令都记不住,每次都是Ctrl+D退出。现在?闭着眼睛都能敲出常用的调试流程。
下一章,我们会深入GDB的断点机制——条件断点、硬件断点、数据断点,这些才是真正提升调试效率的利器。