你有没有经历过这样的崩溃时刻:程序跑着跑着,某个变量莫名其妙变成了 None,你以为它会是个 Some("hello"),结果它给了你一记现实的耳光?或者那该死的 Option::unwrap() 在凌晨两点把你从床上炸起来?
更惨的是,你 println! 大法一顿乱打,输出满屏都是,却还是找不到问题出在哪。等你好不容易找到,天都亮了。
如果你也经历过这种绝望,这篇文章就是为你准备的。

很多人觉得调试器太复杂,不如 println! 来得痛快。但 println! 有个致命问题:它只能告诉你「程序跑完了是什么样」,而调试器能告诉你「程序是怎么一步步变成这个样子的」。
想象一下,println! 就像是案发现场的照片,而 LLDB 是那台能回放整个过程的监控录像。哪个更有用,不言而喻。
而且 Rust 程序不像 C/C++ 那样,编译器会帮你挡住很多坑。Rust 虽然内存安全,但逻辑 Bug 它不管——Option 里面到底是 None 还是 Some,只有调试的时候才知道。
rust-lldb 是 Rust 官方提供的 wrapper(包装脚本),它会调用底层的 lldb,并额外加载 Rust 专用的 Python 脚本(pretty printers),让调试 Rust 程序时能更好地显示 Rust 的数据结构(Vec、String、Option、Enum 等)。
今天我们详细介绍下 rust-lldb 最实用的命令。
首先,用调试模式编译你的 Rust 程序:
cargo builddebug 模式下,Cargo 会自动生成调试符号,让 LLDB 能看懂你的代码。
然后启动 LLDB:
rust-lldb ./target/debug/your_program进去之后,你面对的是 (lldb) 提示符。这时候程序还没跑,你得告诉它从哪开始。
b / br断点是调试的核心。没有断点,程序一口气跑完,你还调试个啥?
b -- Set a breakpoint using one of several shorthand formats.
最常用的方式是按函数名打断点:
(lldb) b gdb_demo::main
(lldb) b your_module::your_function或者按文件行号:
(lldb) b src/main.rs:42小技巧:Rust 的函数名可能很长,不用打全。比如你的函数叫 my_module::process_data::parse_json,直接 b parse_json 往往就能命中。
(lldb) br list(lldb) br list
Current breakpoints:
1: name = 'main::main', locations = 0 (pending)
2: name = 'main::sum_vector', locations = 0 (pending)
3: name = 'sum_vector', locations = 1
3.1: where = gdb_demo`gdb_demo::sum_vector::h9c0f29ea8f72f3be + 33 at main.rs:13:19, address = gdb_demo[0x0000000100002261], unresolved, hit count = 0
4: name = 'gdb_demo::main', locations = 1
4.1: where = gdb_demo`gdb_demo::main::h5f24b280e42565ff + 11 at main.rs:2:19, address = gdb_demo[0x000000010000233b], unresolved, hit count = 0
5: file = 'src/main', line = 12, exact_match = 0, locations = 0 (pending)
6: file = 'src/main.rs', line = 12, exact_match = 0, locations = 1
6.1: where = gdb_demo`gdb_demo::sum_vector::h9c0f29ea8f72f3be + 33 at main.rs:13:19, address = gdb_demo[0x0000000100002261], unresolved, hit count = 0
7: name = 'list', locations = 0 (pending)(lldb) br delete 1 # 删除编号为1的断点
(lldb) br del # 删除所有断点(谨慎使用)(lldb) br disable 1 # 暂时禁用,以后还能用
(lldb) br enable 1 # 重新启用实战场景:你怀疑问题出在 process_request 函数,直接 b process_request。程序每次进这个函数都会停下来,你可以仔细检查输入参数对不对。
断点设置好了,现在让程序跑起来:
r(lldb) r(lldb) r
Process 31292 launched: 'gdb_demo/target/debug/gdb_demo' (x86_64)
Process 31292 stopped
* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
frame #0: 0x000000010000233b gdb_demo`gdb_demo::main::h5f24b280e42565ff at main.rs:2:19
1 fn main() {
-> 2 let numbers = vec![1, 2, 3, 4, 5];
3 let sum = sum_vector(&numbers);
4 println!("Sum: {}", sum);
5
6 let mut v = Vec::new();
7 v.push(42);
Target 0: (gdb_demo) stopped.程序会一直跑,直到撞上断点或者崩溃。
c停在断点后,想让它继续跑:
(lldb) cn 和 s这是最关键的两个命令:
n (next):执行当前行,如果当前行是函数调用,直接跑完整个函数,不进去s (step):执行当前行,如果当前行是函数调用,进入函数内部举个例子:
let result = calculate_total(items); // 第42行
println!("{:?}", result);如果你在 42 行停住了:
n,程序直接跑完 calculate_total,停在第 43 行s,程序进入 calculate_total 函数内部,你可以一步步看它怎么算的什么时候应该用哪个?
ns 进去看看finish进了函数之后,发现这里没问题,想赶紧出去:
(lldb) finish程序会执行完当前函数,回到调用它的地方。
bt 和 framebt程序崩溃了,或者你停在某处想知道「我是怎么到这里的」:
(lldb) bt输出类似这样:
(lldb) bt
* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
* frame #0: 0x000000010000233b gdb_demo`gdb_demo::main::h5f24b280e42565ff at main.rs:2:19
frame #1: 0x000000010000165e gdb_demo`core::ops::function::FnOnce::call_once::h6ddedf0346e35ea0((null)=0x0000000100002330, (null)=<unavailable>) at function.rs:250:5
frame #2: 0x0000000100001ea1 gdb_demo`std::sys::backtrace::__rust_begin_short_backtrace::h75ca11478f906fc3(f=0x0000000100002330) at backtrace.rs:158:18
frame #3: 0x0000000100001564 gdb_demo`std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hd134b1645c19cbe8 at rt.rs:206:18
frame #4: 0x000000010000c515 gdb_demo`std::rt::lang_start_internal::hba06c43da98f3f80 [inlined] core::ops::function::impls::_$LT$impl$u20$core..ops..function..FnOnce$LT$A$GT$$u20$for$u20$$RF$F$GT$::call_once::hf3ac46c438c11a2e at function.rs:287:21 [opt]
frame #5: 0x000000010000c50f gdb_demo`std::rt::lang_start_internal::hba06c43da98f3f80 [inlined] std::panicking::catch_unwind::do_call::h23b5400e9699d0e5 at panicking.rs:590:40 [opt]
frame #6: 0x000000010000c50f gdb_demo`std::rt::lang_start_internal::hba06c43da98f3f80 [inlined] std::panicking::catch_unwind::h455f28d6fd492edc at panicking.rs:553:19 [opt]
frame #7: 0x000000010000c50f gdb_demo`std::rt::lang_start_internal::hba06c43da98f3f80 [inlined] std::panic::catch_unwind::hfd8b99ae711af814 at panic.rs:359:14 [opt]
frame #8: 0x000000010000c50f gdb_demo`std::rt::lang_start_internal::hba06c43da98f3f80 [inlined] std::rt::lang_start_internal::_$u7b$$u7b$closure$u7d$$u7d$::hd633c016c2fbfa9f at rt.rs:175:24 [opt]
frame #9: 0x000000010000c16a gdb_demo`std::rt::lang_start_internal::hba06c43da98f3f80 [inlined] std::panicking::catch_unwind::do_call::h68b3f7dc5c3e0f1e at panicking.rs:590:40 [opt]
frame #10: 0x000000010000c16a gdb_demo`std::rt::lang_start_internal::hba06c43da98f3f80 [inlined] std::panicking::catch_unwind::h9d1581b2521a3be8 at panicking.rs:553:19 [opt]
frame #11: 0x000000010000c16a gdb_demo`std::rt::lang_start_internal::hba06c43da98f3f80 [inlined] std::panic::catch_unwind::h44c0363dfb2d6acc at panic.rs:359:14 [opt]
frame #12: 0x000000010000c16a gdb_demo`std::rt::lang_start_internal::hba06c43da98f3f80 at rt.rs:171:5 [opt]
frame #13: 0x0000000100001547 gdb_demo`std::rt::lang_start::h78b8d8026e105e75(main=0x0000000100002330, argc=1, argv=0x00007ff7bfefeaa8, sigpipe='\0') at rt.rs:205:5
frame #14: 0x0000000100002508 gdb_demo`main + 24
frame #15: 0x00007ff808bd3530 dyld`start + 3056这告诉你:main 调用了 handle,handle 调用了 parse,现在停在 parse 的第 87 行。
关键信息:调用栈是从下往上看的,最下面是程序入口,最上面是当前位置。
frame查看第 1 层的详细信息:
(lldb) frame select 1切换到第 1 层之后,你就可以查看那个层级的变量了。这在追查「参数传递过程」时特别有用。
(lldb) frame select 1
frame #1: 0x000000010000165e gdb_demo`core::ops::function::FnOnce::call_once::h6ddedf0346e35ea0((null)=0x0000000100002330, (null)=<unavailable>) at function.rs:250:5
247
248 /// Performs the call operation.
249 #[unstable(feature = "fn_traits", issue = "29625")]
-> 250 extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
251 }
252
253 mod impls {p / po / v现在到了最激动人心的部分:看变量值。
p(lldb) p some_variable
(lldb) n
Process 32258 stopped
* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x00000001000023d3 gdb_demo`gdb_demo::main::h5f24b280e42565ff at main.rs:3:26
1 fn main() {
2 let numbers = vec![1, 2, 3, 4, 5];
-> 3 let sum = sum_vector(&numbers);
4 println!("Sum: {}", sum);
5
6 let mut v = Vec::new();
7 v.push(42);
Target 0: (gdb_demo) stopped.
(lldb) p numbers
(alloc::vec::Vec<int, alloc::alloc::Global>) size=5 {
[0] = 1
[1] = 2
[2] = 3
[3] = 4
[4] = 5
}对于简单类型(整数、布尔值),直接就能看到值。
popo 是 "print object" 的意思,对于复杂类型会调用它的 Debug 或 Display 实现:
(lldb) po my_structv(lldb) v列出当前作用域所有局部变量,非常实用。
(lldb) v
(alloc::vec::Vec<int, alloc::alloc::Global>) numbers = size=5 {
[0] = 1
[1] = 2
[2] = 3
[3] = 4
[4] = 5
}命令 | 全称 | 作用 | 示例 |
|---|---|---|---|
b | break | 设置断点 | b main / b file.rs:10 |
br list | breakpoint list | 查看所有断点 | br list |
br del | breakpoint delete | 删除断点 | br del 1 |
r | run | 运行程序 | r |
c | continue | 继续运行 | c |
n | next | 单步执行(不进函数) | n |
s | step | 单步执行(进入函数) | s |
finish | - | 执行完当前函数 | finish |
bt | backtrace | 查看调用栈 | bt |
frame select | - | 切换栈帧 | frame select 1 |
p | 打印变量 | p x | |
po | print object | 打印对象(调用 Debug) | po my_struct |
v | variable | 查看所有局部变量 | v |
q | quit | 退出 LLDB | q |
建议保存这张表,调试的时候随时查阅。
Q1:LLDB 和 GDB 有什么区别?选哪个?
A:macOS 上 LLDB 是原生支持的,调试体验更好。Linux 上两者都可以。命令大同小异,学会一个,另一个很快就能上手。Rust 官方对 LLDB 的支持更完善一些。
Q2:为什么我的变量打印出来是乱码?
A:可能是编译时没开调试信息。确保用 cargo build 编译,不要加 --release。Release 模式会优化掉很多调试信息。
Q3:调试时程序运行很慢怎么办?
A:这是正常的。调试模式下程序性能会下降,因为要加载调试符号、响应断点等。如果只是看最终结果,可以用 release 模式运行;如果要调试,就耐心一点。
Q4:能在 IDE 里用这些命令吗?
A:当然可以。VS Code 的 CodeLLDB 插件、IntelliJ Rust 都集成了 LLDB。图形界面更直观,但命令行更灵活,建议两种都学会。
Q5:我的断点打不上,提示找不到符号怎么办?
A:检查函数名是否正确。Rust 的函数名可能被 mangling,用 lldb 的 image lookup 命令搜索:
(lldb) image lookup -r -n your_function掌握 LLDB 不是一蹴而就的事,但只要记住核心流程:
b 打断点r 运行n / s 单步执行p / po 查看变量bt 查调用栈这五个命令覆盖了 80% 的调试场景。剩下的技巧,在实际使用中慢慢积累就好。
调试本质上是一种「逆向推理」——从错误现象出发,一步步追溯原因。LLDB 就是你的放大镜和记录仪。用好它,那些凌晨两点的崩溃,可能就变成下午两点的「小意思」了。