调试器基础:GDB/LLDB 简介与核心概念
调试器这东西,说白了就是程序员的「时光机」。
代码崩了?变量值不对?逻辑走到岔路上去了?调试器能让你停下来,往回看,一步步走。我做了十几年嵌入式开发和系统软件,可以很负责任地告诉你:不会用调试器,等于上战场不带枪。
这一章,咱们先把地基打牢。GDB 和 LLDB 是什么?断点、单步、堆栈这些核心概念到底怎么用?调试符号表又是干嘛的?嗯,一个一个来。
1.1 GDB 与 LLDB:两大调试器
目前主流的调试器就两个:GDB(GNU Debugger)和 LLDB(LLVM Debugger)。
GDB 是老牌劲旅,Linux 下的标配。LLDB 是后起之秀,macOS 和 iOS 开发的首选,也是 Xcode 的默认调试器。两者理念相似,命令也高度兼容。
| 特性 | GDB | LLDB |
|---|---|---|
| 所属项目 | GNU | LLVM |
| 主要平台 | Linux、类 Unix | macOS、iOS、Linux |
| 语言支持 | C/C++、Rust、Go 等 | C/C++、Objective-C、Swift |
| 命令风格 | 传统、稳定 | 更现代、支持 Python 脚本 |
| 启动方式 | gdb ./a.out |
lldb ./a.out |
1.2 核心概念:断点、单步、堆栈
这三个概念,是调试器的灵魂。你想想看,没有它们,你只能靠 printf 大法,一行行猜问题在哪。
1.2.1 断点(Breakpoint)
断点就是让程序在指定位置暂停。暂停后,你可以查看变量、修改内存、或者一步步走。
设置断点的方式:
- 行号断点:
b main.c:42—— 在第 42 行停下。 - 函数断点:
b my_function—— 进入函数时停下。 - 条件断点:
b main.c:42 if x > 5—— 只有x > 5时才停。
if x = 5 写成了赋值,而不是比较。记住,条件表达式是 ==,不是 =。
1.2.2 单步(Stepping)
单步执行,让你像放慢镜头一样,看每一行代码的执行效果。
- step(s):步入。遇到函数调用,会进入函数内部。
- next(n):步过。遇到函数调用,直接执行完,不进去。
- finish:跳出。从当前函数直接执行到返回。
- continue(c):继续执行,直到下一个断点。
我个人习惯是:不确定函数内部逻辑时,用 step 进去看;确定没问题时,用 next 跳过。
1.2.3 堆栈(Stack)
堆栈是函数调用的「路线图」。程序崩溃时,第一件事就是看堆栈。
常用命令:
bt(backtrace):打印当前调用堆栈。frame N:切换到第 N 层栈帧。info locals:查看当前栈帧的局部变量。
main。崩溃时,先看最上面几层,那里通常藏着罪魁祸首。
1.3 调试符号表(Debug Symbols)
调试符号表,是连接二进制代码和源代码的桥梁。
没有符号表,你看到的只有一堆地址和汇编指令。有了符号表,你才能看到函数名、变量名、行号。
符号表的核心内容:
- 函数名与地址的映射
- 变量名、类型、作用域
- 源代码行号与指令地址的对应关系
编译时如何生成符号表?
- GCC/Clang:加上
-g选项。例如:gcc -g -o myapp main.c - 优化级别:
-O0(无优化)配合-g效果最好。优化后的代码可能行号对应不准。
-g -O2 生成带符号表的二进制,然后用 strip 命令剥离符号表,单独保存。这样线上崩溃时,可以用符号表解析堆栈。我曾经因为没保存符号表,线上崩溃堆栈全是地址,根本没法定位。
1.4 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作调试器学习的「地图」。
1.5 快速上手:一个完整的调试会话
光说不练假把式。咱们用一个最简单的例子,走一遍流程。
假设有段代码:
// demo.c
#include <stdio.h>
int divide(int a, int b) {
return a / b;
}
int main() {
int x = 10;
int y = 0;
int result = divide(x, y);
printf("Result: %d\n", result);
return 0;
}
编译并调试:
# 编译,带符号表
gcc -g -o demo demo.c
# 启动 GDB
gdb ./demo
# 在 divide 函数设断点
(gdb) b divide
# 运行
(gdb) run
# 程序停在 divide 入口,查看参数
(gdb) info args
a = 10
b = 0
# 单步执行
(gdb) n
# 程序崩溃了?看堆栈
(gdb) bt
#0 0x... in divide (a=10, b=0) at demo.c:4
#1 0x... in main () at demo.c:10
# 发现问题:除零错误
b=0 的那一刻,问题就清楚了。这就是调试器的威力——让 bug 无处遁形。
嗯,这一章的内容就到这里。调试器的基础概念,说白了就是「停、看、走」。断点让你停,单步让你走,堆栈让你看全局。符号表则是这一切的前提。把这些搞明白,后面的章节咱们再深入各种高级技巧。