调试器基础: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
我的习惯:在 Linux 下调试 C/C++ 项目,我几乎只用 GDB。但如果你做 iOS 逆向或 Swift 开发,LLDB 是绕不开的。两者学会一个,另一个上手很快。

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 效果最好。优化后的代码可能行号对应不准。
避坑指南:发布 Release 版本时,通常用 -g -O2 生成带符号表的二进制,然后用 strip 命令剥离符号表,单独保存。这样线上崩溃时,可以用符号表解析堆栈。我曾经因为没保存符号表,线上崩溃堆栈全是地址,根本没法定位。

1.4 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作调试器学习的「地图」。

调试器基础:知识体系 调试器核心 断点 Breakpoint 单步 Stepping 堆栈 Stack 行号断点 函数断点 条件断点 step(步入) next(步过) finish / continue bt(回溯) frame 切换 info locals 调试符号表(-g 选项)

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 无处遁形。

嗯,这一章的内容就到这里。调试器的基础概念,说白了就是「停、看、走」。断点让你停,单步让你走,堆栈让你看全局。符号表则是这一切的前提。把这些搞明白,后面的章节咱们再深入各种高级技巧。