玩转Linux GDB & pdb

一、GDB调试🐯

  1. watch -n指令的使用:

    # 每隔1s在终端打印一次当前系统内存使用情况
    watch -n 1 "cat /proc/meminfo"
    
    # 每隔1s查看当前系统中所有正在运行的进程
    # ps:查看系统进程; -e:显示所有进程;-f:全格式
    # ps -aux指令也用于查看进程。两者的输出风格不同,内容几乎无差别,一般推荐使用-elf
    watch -n 1 "ps -elf"
    
  2. nm指令的使用:

    # 查看可执行文件或者动态链接库的符号表(函数、变量等)
    nm ***.exe 
    
    # 加上grep可以精确定位
    nm ***.exe | grep 待查名称
    
  3. gdb指令:

    # 打开一个可视化的gdb调试终端,开始调试程序文件
    gdb -tui **.exe
    
    # 此外,也可以对正在运行中的进程,切入gdb调试
    gdb -p 进程PID
    # or
    gdb attach 进程PID
    
    • 进入调试:
    (gdb)l 数字n     # (小写L)显示出代码从第n行开始的内容,默认显示10行;之后再次输入l,会再往后输出10行
    (gdb)set args model/yolo4_tf.xmodel 0 -t 8  # set args:设置程序启动参数
    (gdb)run    # run指令:进入主程序,立即开始执行,直到遇到断点或者程序结束
    (gdb)start  # start指令:进入主程序,停在main()主函数入口处;等待下一步指示(手动打断点等)
    
    (gdb)break n (简写b n) # 在第n行处设置断点(可以带上代码路径和代码名称: b /usr/codeprj/OAGUPDATE.cpp:578)
    
    (gdb)break func       # 在函数func()的入口处设置断点,如:break cb_button
    (gdb)info b           # 显示当前程序的断点设置情况
    
    (gdb)delete 断点号n    # 删除第n个断点
    (gdb)clear 行号n       # 清除第n行的断点,每行可能有多个不同的断点
    
    (gdb)disable 断点号n   # 暂停第n个断点
    (gdb)enable 断点号n    # 开启第n个断点
    (gdb)delete breakpoints     # 清除所有断点
    (gdb)quit        # 使用 quit 命令退出当前gdb调试进程;之后如果再次启动调试,会消除上一次调试操作中建立的所有断点
    
    (gdb)continue    # 继续执行代码,直到遇到下一个断点或者代码执行结束!
    
    # GDB 调试器共提供了3种可实现单步调试程序的方法,即使用 next、step 和 until 命令
    (gdb)next           # 当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next指令都会一步执行完
    (gdb)step           # 当step命令所执行的代码行中包含函数时,会进入该函数内部,在函数第一行代码处停止执行
    (gdb)finish/return  # 结束当前执行的函数:finish命令会执行函数到正常退出;而return命令是立即结束执行当前函数并返回,即剩下未执行的也不管了
    
    • 打印变量:
    (gdb)print num                  # 输出或者修改指定变量或者表达式的值
    (gdb)print file::variable       # file用于指定具体的文件名,variable表示要查看的目标变量或表达式
    (gdb)print function::variable   # funciton 用于指定具体所在函数的函数名
    
    (gdb)p/x variable           # 按十六进制格式显示变量
    (gdb)p/d variable           # 按十进制显示
    (gdb)p/u variable           # 按十六进制显示无符号整型
    (gdb)p/o variable           # 按八进制显示变量
    (gdb)p/t variable           # 按二进制显示变量
    (gdb)p/c variable           # 按字符格式显示变量
    (gdb)p/f variable           # 按浮点数显示变量
    
    • 打印数组:
    # 打印数组内容的同时显示出数组下标
    set print array-indexes on
    
    # 数组默认打印200个元素,但也可以设置具体个数
    set print elements 具体数字num   # num为0时,即不限制元素个数
    
    # 选择打印数组范围
    print *(arr+起始地址)@num
    # 打印arr+128后的6个元素
    print *(arr+128)@6
    
    print arr  # 打印arr数组
    
    • 使用backtrace查看栈桢信息(函数调用的顺序):
      • 当我们阅读代码和查找BUG时,往往有一个烦恼。就是我们不知道函数的调用顺序。而这些函数调用顺序对应我们理解程序结构,程序运行过程是很有帮助的。关于函数的信息都存放在栈中。

        # 栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等  bt:backtrace
        (gdb)bt           # 查看当前所处函数栈帧的数据
        (gdb)bt full      # 打印当前所处函数栈帧的所有参数信息
        
         # 定位栈异常的指令:
        (gdb)frame N       # 切换到栈编号为N的栈帧  N:bt对应的栈编号
        (gdb)info frame    # 打印当前函数(N)所处的栈桢信息
        (gdb)info locals   # 打印当前函数内的局部变量
        (gdb)info args     # 查看当前函数参数
        
      • 示例:

        //frame.c
        #include <stdio.h>
        
        
        int sum(int n)
        {
            int ret = 0;
        
            if( n > 0 )
            {
                ret = n + sum(n-1);
            }
        
            return ret;
        }
        
        
        int main()
        {
            int s = 0;
        
            s = sum(10);
        
            printf("sum = %d\n", s);
        
            return 0;
        }
        
        • 示例代码的调试过程:

          1. 设置断点:设置到递归结束标志的位置

          (gdb) start
          The program being debugged has been started already.
          Start it from the beginning? (y or n) y
          Temporary breakpoint 4 at 0x80483f9: file frame.c, line 19.
          Starting program: /home/delphi/workspace/test.out 
          Temporary breakpoint 4, main () at frame.c:19
          19       int s = 0;
          
          (gdb) break sum if n==0  # 设置sum函数中, n==0 时的数据断点。
          Breakpoint 5 at 0x80483ca: file frame.c, line 6.
          
          (gdb) info break         # 查看断点信息
          Num     Type           Disp Enb Address    What
          5       breakpoint     keep y   0x080483ca in sum at frame.c:6
           stop only if n==0
          

          2. 查看函数调用过程

          (gdb) continue 
          Continuing.
          
          Breakpoint 5, sum (n=0) at frame.c:6
          6       int ret = 0;
          
          # 通过栈桢号判断函数的调用顺序:上一行的函数被下一行的函数调用
          (gdb) backtrace  # 查看函数调用的顺序
          #0  sum (n=0) at frame.c:6
          #1  0x080483e5 in sum (n=1) at frame.c:10
          #2  0x080483e5 in sum (n=2) at frame.c:10
          #3  0x080483e5 in sum (n=3) at frame.c:10
          #4  0x080483e5 in sum (n=4) at frame.c:10
          #5  0x080483e5 in sum (n=5) at frame.c:10
          #6  0x080483e5 in sum (n=6) at frame.c:10
          #7  0x080483e5 in sum (n=7) at frame.c:10
          #8  0x080483e5 in sum (n=8) at frame.c:10
          #9  0x080483e5 in sum (n=9) at frame.c:10
          #10 0x080483e5 in sum (n=10) at frame.c:10
          #11 0x0804840d in main () at frame.c:21
          

          3. 分析函数调用过程

          (gdb) next        # 单步执行,不进入函数
          8       if( n > 0 )
          
          (gdb) next
          13      return ret;
          
          (gdb) info args   # 查看当前函数参数的值
          n = 0
          
          (gdb) frame 7     # 切换栈编号为7的上下文中
          #7  0x080483e5 in sum (n=7) at frame.c:10
          10          ret = n + sum(n-1);
          
          (gdb) info args   # 查看栈编号为7时函数参数的值
          n = 7
          
          (gdb) info locals  # 查看当前局部变量ret的值
          ret = 0  # 计算结果
          
          (gdb) frame 0
          #0  sum (n=0) at frame.c:13
          13      return ret;
          
          (gdb) info registers  # 查看当前寄存器的值
          eax            0x0  0
          ecx            0x241be83d   605808701
          edx            0x1  1
          ebx            0x287ff4 2654196
          esp            0xbffff070   0xbffff070
          ebp            0xbffff098   0xbffff098
          esi            0x0  0
          edi            0x0  0
          eip            0x80483eb    0x80483eb <sum+39>
          eflags         0x200246 [ PF ZF IF ID ]
          cs             0x73 115
          ss             0x7b 123
          ds             0x7b 123
          es             0x7b 123
          fs             0x0  0
          gs             0x33 51
          
          (gdb) info frame           # 查看当前栈帧的详细信息
          Stack level 0, frame at 0xbffff0a0:
           eip = 0x80483eb in sum (frame.c:13); saved eip 0x80483e5
           called by frame at 0xbffff0d0
           source language c.
           Arglist at 0xbffff098, args: n=0
           Locals at 0xbffff098, Previous frame's sp is 0xbffff0a0    # 上一个栈指针地址
           Saved registers:          # 将下面值保存到寄存器中
            ebp at 0xbffff098, eip at 0xbffff09c
            
          (gdb) x /1wx 0xbffff098    # 查看ebp地址中的值
          0xbffff098: 0xbffff0c8
          
          (gdb) next
          14  }
          
          (gdb) next
          13      return ret;
          
          (gdb) info args
          n = 1
          
          (gdb) info registers        # 查看栈帧编号为1的寄存器值
          eax            0x1  1
          ecx            0x241be83d   605808701
          edx            0x1  1
          ebx            0x287ff4 2654196
          esp            0xbffff0a0   0xbffff0a0
          ebp            0xbffff0c8   0xbffff0c8
          esi            0x0  0
          edi            0x0  0
          eip            0x80483eb    0x80483eb <sum+39>
          eflags         0x200202 [ IF ID ]
          cs             0x73 115
          ss             0x7b 123
          ds             0x7b 123
          es             0x7b 123
          fs             0x0  0
          gs             0x33 51
          
          (gdb) info locals
          ret = 1      # 计算结果
          
  4. 调试执行异常崩溃的程序:

    • 当然,如果直接使用gdb **.exe的方式调试程序,就不用这种定位崩溃方式了!

    • 在Linux操作系统中,当程序执行发生异常崩溃时,系统可以将发生崩溃时的内存数据、调用堆栈情况等信息自动记录下载,并存储到一个文件中,该文件通常称为core文件,Linux系统所具备的这种功能又称为核心转储(core dump)。幸运的是,GDB对core文件的分析和调试提供有非常强大的功能支持,当程序发生异常崩溃时,通过GDB调试产生的core文件,往往可以更快速的解决问题。

    • 如何设置core dump文件在系统的生成存放目录:Linux-Coredump分析基础 ubuntu如何生成core文件 core文件去哪了

    • 写个代码验证一下:

      #include <stdio.h>
      
      int main() {
          char *a = NULL;
          *a = 2;  // 这行一看就是有BUG
          
          return 0;
      }
      
    • 编译:

      # -g:一定不能少,否则后面用gdb调试时候不能直观地显示出BUG所在位置
      $ g++ -g -o test core.cpp 
      $ ./test
      Segmentation fault (core dumped)      <-- 发生段错误,并生成了 core 文件
      
    • 可以根据生成时间查找core dump文件:

      # 这里假设core文件被存放在/home/homework/coresave路径下
      ls /home/homework/coresave -hl | grep test
      -rw-rw-rw- 1 root      root      400K Mar 13 15:08 core.test.27725.1615619332
      -rw-rw-rw- 1 root      root      400K Mar 13 15:26 core.test.7791.1615620408
      -rw-rw-rw- 1 root      root      540K Mar 11 10:29 core.test.1868.1615429740
      -rw-rw-rw- 1 root      root      400K Mar 13 15:07 core.test.26880.1615619264
      -rw-rw-rw- 1 root      root      404K Mar  3 19:42 core.test.28802.1614771771
      
    • gdb进行调试:

      # test: 要调试的可执行文件的名称
      $ gdb test /home/homework/coresave/core.test1.7791.1615620408
      
      Reading symbols from /home/zhudi/project/linux/blog/gdb/test...done.
      
      warning: core file may not match specified executable file.
      [New LWP 7791]
      Core was generated by `./test'.
      Program terminated with signal 11, Segmentation fault.
      #0  0x00000000004005bd in main () at core.cpp:5
      5	    *a = 2;
      
    • 由此可见,程序崩溃了在第五行,定位到了出现问题的代码位置。

二、pdb调试😸

  • 注意,因为 Python 是一种解释型语言,可以通过 pdb shell 执行命令。 ipdb 是一种增强型的 pdb ,它使用IPython 作为 REPL并开启了 tab 补全、语法高亮、更好的回溯和更好的内省,同时还保留了pdb 模块相同的接口。

  • 调试python代码,可以使用ipdb调试工具

    python -m ipdb bubble.py
    
    • 进入调试过程后,基本操作指令和gdb大同小异:

      # 显示当前行附近的11行或继续执行之前的源码显示
      $ l(ist)
      
      # 打断点
      $ b 6  # 在第六行
      
      # 一步一步执行,遇到函数就进入
      $ step
      
      # 继续执行直到当前函数的下一条语句或者 return 语句
      $ next
      
      # 继续运行,直到执行结束/出现报错/遇到断点
      $ c
      
      # 代码因某种原因停下后,打印数据;可以配合step使用
      $ p 变量名
      $ p locals()  # 打印当前时刻所有变量
      
      # 继续执行直到当前函数返回
      $ return
      
      # 停止调试
      $ q
      
      # 重启调试
      $ restart
      
  • 对于更底层的编程语言,您可能需要了解一下 gdb ( 以及它的改进版 pwndbg) 和 lldb

  • 它们都对类 C 语言的调试进行了优化,它允许您探索任意进程及其机器状态:寄存器、堆栈、程序计数器等。