1. 概述
主要介绍常见的代码调试方法,解决开发阶段、运行阶段遇到的奇怪和奔溃问题。解决debug日志不全、频繁编译慢、复现难等问题,以及考虑性能通常会关闭debug日志,如果这个时候在运行过程中出现了奔溃等问题,需要通过以下方式分析问题。
主要涉及编译型语言,比如:C/C++、Rust、Go等
2. 调试工具
● 常见调试工具:gdb/cgdb/lldb/windbg/valgrind
● 开发阶段可以使用IDE自带调试器
3. 调试场景
- 开发阶段代码调试:问题容易复现和解决
- 运行阶段代码调试:问题不容易复现和解决, 需要生成coredump文件后续分析
4. gdb常见调试命令
4.1 gdb启动命令
$ gdb <filename> # 直接调试目标程序
$ gdb --pid <pid> # 附加进程调试
$ gdb <filename> <coredump> # 调试coredump文件
4.2 常见调试命令
● break [func][:<num>]
: 设置断点
● tbreak [func][:<num>]
: 设置临时断点,程序只会停留一次
● r|run [args]
: 运行程序
● n|next <count>
: 单步跟踪,如果有函数调用,不会进入该函数。
● s|step <count>
: 单步跟踪, 如果有函数调用,会进入该函数。
● u|until
: 运行程序直到退出循环体
● c|continue [ignore-count]
: 继续运行程序, ignore-count: 表示忽略其后的断点次数。
● p|print <var>
: 打印变量值
● finish
: 退出函数
● q|quit
: 退出gdb
● 详细命令help [commands]
查看
4.3 栈命令
● backtrace <-n/+n>|bt <-n/+n>
: 显示调用栈信息, 可以不加n
● frame <n>|f <n>
: 切换栈
● up <n>
: 表示向上移动栈, 可以不加n
● down <n>
: 表示向下移动栈, 可以不加n
● select-frame <n>
: 不显示栈信息,对应于frame命令
● up-silently <n>
: 不显示栈信息, 对应于up命令
● down-silently <n>
: 不显示栈信息, 对应于 down 命令
4.4 设置断点
● break
: 设置断点
● tbreak [func][:<num>]
: 设置临时断点,程序只会停留一次
● info break|breakpoints
: 查看断点
● delete break|breakpoints [range...]
: 删除断点
● disable break|breakpoints [range...]
: 禁止断点
● enable break|breakpoints [range...]
: 启用断点
(gdb) break 17
Breakpoint 2 at 0x555555554699: file 01-example.c, line 17.
(gdb) break 23
Breakpoint 3 at 0x5555555546c0: file 01-example.c, line 23.
(gdb) info breakpoints
Num Type Disp Enb Address What
2 breakpoint keep y 0x0000555555554699 in main at 01-example.c:17
3 breakpoint keep y 0x00005555555546c0 in main at 01-example.c:23
(gdb) delete breakpoints 1
No breakpoint number 1.
(gdb) delete breakpoints 2
(gdb) info breakpoints
Num Type Disp Enb Address What
3 breakpoint keep y 0x00005555555546c0 in main at 01-example.c:23
(gdb)
4.5 print 命令
- print <expr>
- print /<f> <expr>
表达式
- @是一个核数组有关的操作符
- ::指定一个在文件或是一个函数中的变量
- {<type>} <addr>: 表示一个指向内存地址<addr>的类型为type的一个对象。
格式
- x: 十六进制
- d: 十进制
- u: 十六进制,无符号
- o: 八进制
- t: 二进制
- a: 十六进制
- c: 字符
f: 浮点数
数组
(gdb) p *argv@argc $8 = {0x7fffffffe6bf "/home/xxxx/works/code-debug/01-example", 0x7fffffffe6f5 "ss"}
格式
(gdb) p i $21 = 101 (gdb) p/a i $22 = 0x65 (gdb) p/c i $23 = 101 'e' (gdb) p/f i $24 = 1.41531145e-43 (gdb) p/x i $25 = 0x65 (gdb) p/t i $26 = 1100101
5. 代码调试案例
5.1 gdb调试程序
5.1.1 代码例子
/** * @Author: jankincai * @Date: 2024-02-18 18:07:12 * @Last Modified by: jankincai * @Last Modified time: 2024-02-21 10:52:59 */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> typedef struct { uint16_t key; const char *value; }jkc_t; void jankincai_b(jkc_t *value) { printf(">>> jankincai_b key:%d value:%s\n", value->key, value->value); } void jankincai_a(jkc_t *value) { printf(">>> jankincai_a key:%d value:%s\n", value->key, value->value); value->key = 2; value->value = "jankincai_b"; jankincai_b(value); } int main(int argc, char const *argv[]) { /* code */ if (argc >= 2) { jankincai_a(NULL); } else { jkc_t value; value.key = 1; value.value = "jankincai_a"; jankincai_a(&value); } return 0; }
5.1.2 gdb简单调试过程
$ gdb ./01-example (gdb) break 38 # 设置断点 (gdb) layout split # 显示源代码和汇编代码 (gdb) run # 运行程序 (gdb) info args # 显示当前函数参数 argc = 1 argv = 0x7fffffffe458 (gdb) p *argv@argc # 显示argv内容 $3 = {0x7fffffffe692 "/home/xxxx/works/code-debug/build/01-example"} (gdb) n # 单步调试 (gdb) n # 单步调试 (gdb) n # 单步调试 (gdb) p value.key # 打印key $1 = 1 (gdb) p value.value # 打印value $2 = 0x55555555488d "jankincai_a" (gdb) info locals # 查看当前局部变量 value = {key = 1, value = 0x55555555488d "jankincai_a"} (gdb) s # 单步调试进入函数, n不会进入函数 (gdb) n # 单步调试 (gdb) n # 单步调试 (gdb) n # 单步调试 (gdb) info args value = 0x7fffffffe350 (gdb) p value.key $1 = 2 (gdb) p value.value $2 = 0x555555554881 "jankincai_b"
5.2 gdb加载符号表调试程序
程序开发完成之后,通常会将符号表和调试信息剥离,主要为了减少文件大小和增加反汇编难度从,如果后续自己需要调试在安装调试信息
5.2.1 剥离符号表和调试信息
# 复制符号表和调试信息 $ objcopy --only-keep-debug <filename> <filename>.debug # 剥离符号表和调试信息 $ objcopy -S <filename> # $ strip -s <filename> # 添加.gnu_debuglink段, 指向xxx.debug文件 $ objcopy --add-gnu-debuglink=<filename>.debug <filename> # 查看.gnu_debuglink段 $ readelf -S <filename> | grep gnu_debuglink $ readelf -p .gnu_debuglink <filename>
5.2.2 gdb手动加载符号表调试
需要重命名
add-gnu-debuglink
指定的*.debug
文件,使系统默认查找不到,然后手动加载$ gdb <filename> (gdb) symbol-file <filename>.debug Reading symbols from example.debug...done.
5.2.3 gdb自动加载符号表表示
添加了
add-gnu-debuglink
会自动在上面那些路径查找调试文件, 只要把调试符号表文件放入对应路径,启动gdb会自动加载调试符号表。5.3 gdb调试正在进行的进程
主要应用于程序运行中突然卡死情况调试
5.3.1 代码例子
/** * @Author: jankincai * @Date: 2024-02-19 14:51:55 * @Last Modified by: jankincai * @Last Modified time: 2024-02-19 14:55:49 */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <unistd.h> void jankincai(void) { uint64_t count = 0; while (1) { printf("count: %lu\n", count); count++; sleep(1); } } int main(int argc, char const *argv[]) { /* code */ jankincai(); return 0; }
gcc -Wall -g -o 02-attach 02-attach.c
5.3.2 获取进程pid
$ ps -aux|grep <xxx>
5.3.3 gdb --pid
- 进入gdb之后,一般情况下都是停在大概卡死位置,这个时候可以通过bt、print查看详细信息。
调试完成之后,执行
detach
命令,恢复程序继续运行(gdb) detach Detaching from program: /home/xxxx/works/code-debug/build/02-attach, process 12090
5.4 gdb调试coredump文件
主要用于调试长时间运行的程序,且问题很难复现(随机出现),利用产生的
coredump
文件分析问题,该调试方法很重要5.4.1 代码例子
/** * @Author: jankincai * @Date: 2024-02-19 14:51:55 * @Last Modified by: jankincai * @Last Modified time: 2024-02-19 15:55:17 */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <unistd.h> typedef struct { uint16_t key; const char *value; }jkc_t; void jankincai(void) { uint64_t count = 0; jkc_t *jkc = NULL; while (1) { printf("count: %lu\n", count); if (count == 10) { jkc->key = 1; jkc->value = "jankincai\n"; } count++; sleep(1); } } int main(int argc, char const *argv[]) { /* code */ jankincai(); return 0; }
5.4.2 开启coredump
# sudo vim /etc/sysctl.conf kernel.core_pattern = /home/bolean/coredump/core-%e.%p
$ sudo sysctl -p $ ulimit -c unlimited # 只应用当前bash, 如果需要持久化可以写入/etc/profile文件
5.4.3 gdb调试
$ gdb ./03-coredump ~/coredump/core-03-coredump
通过
p jkc
发现是null
指针5.5 根据内核段错误日志调试
如果忘记开启
coredump
日志,问题已经出现,且很难复现,这时候可以通过内核段错误日志调试,但是信息没有那么全,参考上面的案例5.5.1 查看内核日志
cat /var/log/kern.log | grep segfault
ip
: 指令指针寄存器,出差时程序执行的位置sp
: 堆栈指针寄存器error
:错误码,由三个bit组成- bit2: 值为1表示是用户态程序内存访问越界,否则表示内核态程序内存访问越界。
- bit1: 值为1表示是写操作导致内存访问越界,否则表示是读操作导致内存访问越界。
- bit0: 值为1表示没有足够的权限访问非法地址的内容,否则表示无效地址
程序名后面紧跟着基地址,例如:5558ccfef000
5.5.2 addr2line 地址转换为代码中错误文件、行号及函数名
addr2line -e 进程名 0xIP指令地址 -f addr2line -e 进程名 0xIP指令地址 - 0x基地址 -f # 如果基地址是400000就不用减
Feb 19 15:56:27 bolean kernel: [3391050.849981] 02-attach[16616]: segfault at 0 ip 00005558ccfef6c5 sp 00007ffd08bd1f60 error 6 in 02-attach[5558ccfef000+1000]
0x00005558ccfef6c5 - 0x5558ccfef000
=0x6c5
- 可以通过
objdump -dS <filename>
反汇编查看地址范围
通过
addr2line
分析得出奔溃文件、函数、行号等gdb多线程调试命令
info threads
: 查看当前线程数量thread <num>
: 切换线程set scheduler-locking off|on|step 在使用step或者continue命令调试当前被调试线程的时候,可以设置其他线程的表现。
- off - 不锁定任何线程,也就是所有线程都执行,这是默认值。
- on - 只有当前被调试程序会执行。
- step - 在单步的时候,除了next过一个函数的情况以外,只有当前线程会执行。
thread apply all bt
: 显示所有线程栈信息。5.6 gdb调试死锁问题
由于出现死锁,通常程序表现为卡死,所以可以利用
gdb attach
方法调试运行的进程5.6.1 代码示例
/** * @Author: jankincai * @Date: 2024-02-19 17:11:51 * @Last Modified by: jankincai * @Last Modified time: 2024-02-19 18:14:05 */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <unistd.h> pthread_mutex_t mutex_lock; uint64_t count = 0; void* jankincai_a(void* ptr) { while (1) { pthread_mutex_lock(&mutex_lock); printf("[%ld] jankincai_a count=%lu\n", pthread_self(), count); count ++; pthread_mutex_unlock(&mutex_lock); sleep(1); } } void* jankincai_b(void* ptr) { while (1) { pthread_mutex_lock(&mutex_lock); printf("[%ld] jankincai_b count=%lu\n", pthread_self(), count); count ++; if (count < 5) { pthread_mutex_unlock(&mutex_lock); } sleep(1); } } int main(int argc, char const *argv[]) { pthread_setname_np(pthread_self(), "main"); pthread_mutex_init(&mutex_lock, NULL); pthread_t t1, t2; pthread_create(&t1, NULL, jankincai_a, NULL); pthread_setname_np(t1, "jankincai_a"); pthread_create(&t2, NULL, jankincai_b, NULL); pthread_setname_np(t2, "jankincai_b"); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex_lock); /* code */ return 0; }
5.6.2 gdb调试过程
调试命令
$ ps -aux|grep <xxx> # 获取进程PID
$ sudo gdb --pid <pid>
# 或者
$ sudo gdb --pid $(ps -aux | grep <xxxx> | grep -v 'color=' | awk '{print $2}')
查看所有线程堆栈
通过上面的截图能看到
线程3
和线程2
都在等待lock
打印关键变量和锁状态信息
单独调试某个线程
(gdb) layout split # 显示源代码和汇编代码
(gdb) tbreak 34 # 设置断点
(gdb) run [args] # 运行程序
(gdb) info threads # 查看线程信息
(gdb) thread 3 # 切换线程
(gdb) set scheduler-locking on # 设置只调试当前线程,其他线程不会运行
(gdb) n # 单步调试
(gdb) c # 继续运行, 观察线程是否卡死
valgrind
# 打印互斥锁lock/unlock过程
$ sudo valgrind --tool=drd --trace-mutex=yes ./04-deadlock
通过
valgrind drd
工具能分析出线程2
和线程3
同时给0x30a040
上锁,rc
加锁计数,owner
表示线程3
持有该锁并没有释放,还继续加锁操作。5.7 gdb 多进程调试命令
set follow-fork-mode parent|child
parent
:fork
之后继续调试父进程,子进程不受影响child
:fork
之后继续调试子进程,父进程不受影响
set detach-on-fork on|off
on
: 断开调试follow-fork-mode指定的进程off
: gdb将控制父进程和子进程。follow-fork-mode指定的进程将被调试,另一个进程置于暂停(suspended)状态。
info inferiors
:显示进程信息interior <id>
:切换进程detach interior <id>
: 允许进程正常运行5.8 远程调试(gdb+gdbserver)
- https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_130.html
- https://sourceware.org/gdb/current/onlinedocs/gdb.html/Server.html
一般用于嵌入式开发调试
6. rust代码调试案例
6.1 rust 代码奔溃
- 如果在开发阶段代码奔溃,这是很容易的事件, Rust提供了环境变量
RUST_BACKTRACE=1|full
显示调用栈,能看到奔溃位置 如果是在运行阶段,没有配置Rust生成doredump,程序奔溃之后Rust之后提升使用
RUST_BACKTRACE=1
, 但是如果遇到很难复现的问题,将无法分析原因,除非一直使用RUST_BACKTRACE=1
运行,但是也不利于后续通过rust-gdb/rust-lldb
打印参数和变量等信息6.2 rust代码奔溃产生coredump配置
通过监听
panic hook
事件,并通过kill
发送SIGQUIT
进程退出并会生成coredump
文件[dependencies] libc = "0.2"
pub fn register_panic_handler() { let default_panic = std::panic::take_hook(); std::panic::set_hook(Box::new(move |panic_info| { default_panic(panic_info); // Don't forget to enable core dumps on your shell with eg `ulimit -c unlimited` let pid = std::process::id(); eprintln!("dumping core for pid {}", std::process::id()); use libc::kill; use libc::SIGQUIT; // use std::convert::TryInto; unsafe { kill(pid.try_into().unwrap(), SIGQUIT) }; })); }
fn main() { register_panic_handler(); // TODO }
6.3 完整例子
#[derive(Debug)] pub struct Jkc { pub key: u16, pub value: String, } pub fn register_panic_handler() { let default_panic = std::panic::take_hook(); std::panic::set_hook(Box::new(move |panic_info| { default_panic(panic_info); // Don't forget to enable core dumps on your shell with eg `ulimit -c unlimited` let pid = std::process::id(); eprintln!("dumping core for pid {}", std::process::id()); use libc::kill; use libc::SIGQUIT; // use std::convert::TryInto; unsafe { kill(pid.try_into().unwrap(), SIGQUIT) }; })); } pub fn jankincai_c(value: &[u8]) { println!(">>> {}", value[4]); // let value2: Option<u8> = None; // let value3 = value2.unwrap(); } pub fn jankincai_b(value: &mut Jkc) { println!("jankincai_b {value:?}"); let vlist = [0; 3]; jankincai_c(&vlist); } pub fn jankincai_a(value: &mut Jkc) { println!("jankincai_a {value:?}"); value.key = 2; value.value = "jankincai_b".to_string(); jankincai_b(value); } fn main() { // RUST_BACKTRACE=1 cargo run register_panic_handler(); let mut value = Jkc { key: 1, value: "jankincai_a".to_string() }; jankincai_a(&mut value); }
6.4 调试过程
sudo rust-gdb ./target/debug/rust-example ~/coredump/core-rust-example
(gdb) bt #0 0x00007fb1c6412177 in kill () at ../sysdeps/unix/syscall-template.S:78 #1 0x00005590bbcd5cf5 in rust_example::register_panic_handler::{{closure}} (panic_info=0x7fff18c9f698) at src/main.rs:22 #2 0x00005590bbcf5c03 in <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call () at library/alloc/src/boxed.rs:2021 #3 std::panicking::rust_panic_with_hook () at library/std/src/panicking.rs:757 #4 0x00005590bbcf5981 in std::panicking::begin_panic_handler::{{closure}} () at library/std/src/panicking.rs:631 #5 0x00005590bbcf40c6 in std::sys_common::backtrace::__rust_end_short_backtrace () at library/std/src/sys_common/backtrace.rs:170 #6 0x00005590bbcf56c2 in rust_begin_unwind () at library/std/src/panicking.rs:619 #7 0x00005590bbcd4e15 in core::panicking::panic_fmt () at library/core/src/panicking.rs:72 #8 0x00005590bbcd4fd2 in core::panicking::panic_bounds_check () at library/core/src/panicking.rs:180 #9 0x00005590bbcd6b55 in rust_example::jankincai_c (value=&[u8](size=3) = {...}) at src/main.rs:28 #10 0x00005590bbcd6be9 in rust_example::jankincai_b (value=0x7fff18c9fa10) at src/main.rs:39 #11 0x00005590bbcd6cf6 in rust_example::jankincai_a (value=0x7fff18c9fa10) at src/main.rs:49 #12 0x00005590bbcd6d41 in rust_example::main () at src/main.rs:59 (gdb) frame 9 #9 0x00005590bbcd6b55 in rust_example::jankincai_c (value=&[u8](size=3) = {...}) at src/main.rs:28 println!(">>> {}", value[4]); (gdb) p value $1 = &[u8](size=3) = {0, 0, 0} (gdb)
sudo gdb ./target/debug/rust-example ~/coredump/core-rust-example
(gdb) bt #0 0x00007fb1c6412177 in kill () at ../sysdeps/unix/syscall-template.S:78 #1 0x00005590bbcd5cf5 in rust_example::register_panic_handler::{{closure}} (panic_info=0x7fff18c9f698) at src/main.rs:22 #2 0x00005590bbcf5c03 in <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call () at library/alloc/src/boxed.rs:2021 #3 std::panicking::rust_panic_with_hook () at library/std/src/panicking.rs:757 #4 0x00005590bbcf5981 in std::panicking::begin_panic_handler::{{closure}} () at library/std/src/panicking.rs:631 #5 0x00005590bbcf40c6 in std::sys_common::backtrace::__rust_end_short_backtrace () at library/std/src/sys_common/backtrace.rs:170 #6 0x00005590bbcf56c2 in rust_begin_unwind () at library/std/src/panicking.rs:619 #7 0x00005590bbcd4e15 in core::panicking::panic_fmt () at library/core/src/panicking.rs:72 #8 0x00005590bbcd4fd2 in core::panicking::panic_bounds_check () at library/core/src/panicking.rs:180 #9 0x00005590bbcd6b55 in rust_example::jankincai_c (value=...) at src/main.rs:28 #10 0x00005590bbcd6be9 in rust_example::jankincai_b (value=0x7fff18c9fa10) at src/main.rs:39 #11 0x00005590bbcd6cf6 in rust_example::jankincai_a (value=0x7fff18c9fa10) at src/main.rs:49 #12 0x00005590bbcd6d41 in rust_example::main () at src/main.rs:59 (gdb) frame 9 #9 0x00005590bbcd6b55 in rust_example::jankincai_c (value=...) at src/main.rs:28 warning: Source file is more recent than executable. println!(">>> {}", value[4]); (gdb) p value $1 = &[u8] {data_ptr: 0x7fff18c9f945 "\000", length: 3} (gdb) p *value.data_ptr@value.length $2 = [0, 0, 0] (gdb)
7. rust代码调试注意事项
rust-gdb ./target/debug/rust-example
- 默认情况下显示不是main函数,所以设置断点需要加文件名
break main.rs:54
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。