1. 概述

主要介绍常见的代码调试方法,解决开发阶段、运行阶段遇到的奇怪和奔溃问题。解决debug日志不全、频繁编译慢、复现难等问题,以及考虑性能通常会关闭debug日志,如果这个时候在运行过程中出现了奔溃等问题,需要通过以下方式分析问题。

主要涉及编译型语言,比如:C/C++、Rust、Go等

2. 调试工具

● 常见调试工具:gdb/cgdb/lldb/windbg/valgrind
● 开发阶段可以使用IDE自带调试器

3. 调试场景

  1. 开发阶段代码调试:问题容易复现和解决
  2. 运行阶段代码调试:问题不容易复现和解决, 需要生成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自动加载符号表表示

    无标题.png

  • 添加了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

    无标题.png

  • 进入gdb之后,一般情况下都是停在大概卡死位置,这个时候可以通过bt、print查看详细信息。

无标题.png

  • 调试完成之后,执行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

    无标题.png

  • 通过p jkc发现是null指针

    5.5 根据内核段错误日志调试

  • 如果忘记开启coredump日志,问题已经出现,且很难复现,这时候可以通过内核段错误日志调试,但是信息没有那么全,参考上面的案例

    5.5.1 查看内核日志

  • cat /var/log/kern.log | grep segfault

无标题.png

  • 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>反汇编查看地址范围

无标题.png

  • 通过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}')
查看所有线程堆栈

无标题.png

  • 通过上面的截图能看到线程3线程2都在等待lock

    打印关键变量和锁状态信息

无标题.png

单独调试某个线程
(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

image.png

  • 通过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

image.png

  • 默认情况下显示不是main函数,所以设置断点需要加文件名break main.rs:54

无标题.png


Jankin Cai
1 声望0 粉丝