GDB调试学习总结
2021-09-15 / ryanxw

GDB调试学习总结

命令概览

命令名称 命令缩写 命令说明 备注
run r 运行一个程序
break b 添加断点 -b xx.cpp:line
delete del 删除断点
list l 显示源代码
continue c 让暂停的程序继续运行
next n 运行到下一行源代码,不进入函数内部
step s 运行到下一行源代码,进入函数内部
print p 打印变量或者寄存器的值
until u 运行到指定行停止下来
jump j 将当前程序执行流跳转到指定行或地址
backtrace bt 查看当前线程的调用堆栈
thread thread 切换到指定线程
frame f 切换到当前调用线程的指定堆栈,具体堆栈通过堆栈序号指定
info info 查看断点/线程等信息
ptype ptype 查看变量类型
disassemble dis 查看汇编代码
watch watch 监视某一个变量或内存地址的值是否发生变化
finish fin 结束当前调用函数,返回到上一层函数调用处
return return 结束当前调用函数并返回指定值,到上一层函数调用处

调试方法

直接调试

  • gdb filename
    开发过程中,编译生成目标二进制文件后,可以直接gdb filename开始调试,输入runr运行程序。比如下图所示:
    gdb1
    启动后可以通过netstat -nltp | grep 6379来查看程序已经运行起来了
    gdb2

附加进程调试

  • gdb attach pid
    如果当程序已经启动后想调试,但是不想重启程序。或者当程序在测试过程中,不能重启,一旦重启则会丢失当前的状态信息。这种情况下就使用附加进程进行调试。
    • 获取进程pid
      gdb3
    • 生成附加进程
      gdb4
      提示 “Attaching to process 8380 时即已成功将 GDB 附加到目标进程,结束调试后输入detach离开且不对当前进程有影响,让程序和gdb调试器分离,如下图:
      gdb5

core文件调试

coredump即核心转储,是进程运行崩溃瞬间的一个内存快照。操作系统在程序发生异常而异常在进程内部又没有被捕获的情况下,会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存到core文件,该文件是个二进制文件,可以使用gdb进行分析。

  • 产生原因

    • 内存访问越界
    • 多线程程序使用了线程不安全的函数
    • 多线程读写数据未加锁保护,对于被多个线程同时访问的全局数据,要加锁保护否则会coredump
    • 非法指针
    • 堆栈溢出
  • 查看系统产生core文件后存放的位置

    1
    2
    3
    4
    5
    6
    7
    [xw@null coredump]$ cat /proc/sys/kernel/core_pattern 
    /data/coredump/core_%e_%t
    - 表示产生的core文件都以core_%e_%t格式保存到/data/coredump/目录下
    -e 程序文件名
    -t 时间戳
    -p 进程ID
    -s 使进程崩溃的信号signal
  • 修改core文件的存储位置

    1
    echo "/data/coredump/core_%e_%t" > /proc/sys/kernel/core_pattern
  • 修改core文件大小
    (1) ulimit -c 查看参数core file size的值,如果为0表示不会生成core文件,一般修改为unlimited

    1
    2
    [xw@null ~]$ ulimit -c
    0

    (2) 修改core文件大小

    1
    2
    3
    [xw@null ~]$ ulimit -c unlimited
    [xw@null ~]$ ulimit -c
    unlimited
  • readelf -h corefile查看core文件头
    core8

  • gdb调试core文件

    1
    2
    gdb 执行文件 core文件
    where/bt 查看堆栈信息进一步分析程序
  • 参考链接

命令详解

run

1
[xxxx@gzqc-172_24_21_96-null src]$ gdb redis-server

gdb6
此时只是附加了一个调试文件,程序还未启动,输入run命令即可运行程序。
gdb7
Ctrl+C可以中断gdb,再次输入r后输入y重启程序
gdb8

continue

Ctrl+C可以中断gdb,输入c可以使程序继续运行

1
2
3
4
5
6
^C
Program received signal SIGINT, Interrupt.
0x00007ffff71e9d43 in epoll_wait () from /lib64/libc.so.6
(gdb) c
Continuing.

break

  • break用于添加断点,添加完成后重启程序即可触发,停在断点处

    • break func在函数名入口处添加断点
    • break line在当前文件行号line处添加断点
    • break filename:line在文件filename行号line处添加断点
      gdb9
  • redis-server端口6379,端口号肯定是通过bind()函数创建的,搜索文件位于anet.c 401 行。
    gdb10

    • anet.c:401处添加断点后重启触发断点1, continue后触发断点2。如下所示:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      (gdb) b anet.c:401
      (gdb) info b
      Num Type Disp Enb Address What
      1 breakpoint keep y 0x0000000000442962 in main at server.c:6151
      2 breakpoint keep y 0x00000000004340e2 in anetListen at anet.c:401
      (gdb) r
      Starting program: /data/home/xxxx/redis-6.2.3/src/redis-server
      [Thread debugging using libthread_db enabled]
      Using host libthread_db library "/lib64/libthread_db.so.1".
      Breakpoint 1, main (argc=1, argv=0x7fffffffdf58) at server.c:6151
      6151 char config_from_stdin = 0;
      (gdb) c
      Breakpoint 2, anetListen (err=0xbcee50 <server+752> "", s=10, sa=0xdf79a0, len=16, backlog=511) at anet.c:401
      401 if (bind(s,sa,len) == -1) {

    • 分别在anet.c 404 410 412处添加断点,用于判断从该函数从何处返回。如下图所示:
      gdb12
    • continue可以看到触发断点5。如下图所示:
      gdb13

backtrace 和 frame

  • backtrace用于查看堆栈信息。frame用于切换到某个堆栈处。上面的gdb程序中断在anet.c:412

    • bt查看此时的堆栈信息共有6层堆栈#0 ~ #5,如下图所示:
      gdb15

    • frame 堆栈编号切换堆栈信息,如下图所示:
      gdb14
      可以得到调用关系

      • main()函数在6322行调用initServer()函数
      • initServer()函数在3195行调用listenToPort()函数
      • listenToPort()函数在3052行调用anetTcpServer()函数
      • anetTcpServer()函数在468行调用_anetTcpServer()函数
      • _anetTcpServer()函数在450行调用anetListen()函数
      • 此时断点正停在anetListen()函数中

info break、enable、disable、delete

  • info b查看断点个数
    gdb16
  • enable num启用编号为num的断点,不加参数表示全部启用
  • disable num禁用编号为num的断点,不加参数表示全部禁用
  • delete num删除编号为num的断点,不加参数表示全部删除
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    (gdb) info b
    Num Type Disp Enb Address What
    1 breakpoint keep y 0x0000000000442962 in main at server.c:6151
    breakpoint already hit 1 time
    2 breakpoint keep y 0x00000000004340e2 in anetListen at anet.c:401
    breakpoint already hit 2 times
    3 breakpoint keep y 0x000000000043412c in anetListen at anet.c:404
    4 breakpoint keep y 0x0000000000434178 in anetListen at anet.c:410
    5 breakpoint keep y 0x000000000043417f in anetListen at anet.c:412
    breakpoint already hit 1 time
    (gdb) disable 4
    (gdb) info b
    Num Type Disp Enb Address What
    1 breakpoint keep y 0x0000000000442962 in main at server.c:6151
    breakpoint already hit 1 time
    2 breakpoint keep y 0x00000000004340e2 in anetListen at anet.c:401
    breakpoint already hit 2 times
    3 breakpoint keep y 0x000000000043412c in anetListen at anet.c:404
    4 breakpoint keep n 0x0000000000434178 in anetListen at anet.c:410
    5 breakpoint keep y 0x000000000043417f in anetListen at anet.c:412
    breakpoint already hit 1 time
    (gdb) delete 3
    (gdb) info b
    Num Type Disp Enb Address What
    1 breakpoint keep y 0x0000000000442962 in main at server.c:6151
    breakpoint already hit 1 time
    2 breakpoint keep y 0x00000000004340e2 in anetListen at anet.c:401
    breakpoint already hit 2 times
    4 breakpoint keep n 0x0000000000434178 in anetListen at anet.c:410
    5 breakpoint keep y 0x000000000043417f in anetListen at anet.c:412
    breakpoint already hit 1 time
    (gdb) disable
    (gdb) info b
    Num Type Disp Enb Address What
    1 breakpoint keep n 0x0000000000442962 in main at server.c:6151
    breakpoint already hit 1 time
    2 breakpoint keep n 0x00000000004340e2 in anetListen at anet.c:401
    breakpoint already hit 2 times
    4 breakpoint keep n 0x0000000000434178 in anetListen at anet.c:410
    5 breakpoint keep n 0x000000000043417f in anetListen at anet.c:412
    breakpoint already hit 1 time
    (gdb) enable
    (gdb) info b
    Num Type Disp Enb Address What
    1 breakpoint keep y 0x0000000000442962 in main at server.c:6151
    breakpoint already hit 1 time
    2 breakpoint keep y 0x00000000004340e2 in anetListen at anet.c:401
    breakpoint already hit 2 times
    4 breakpoint keep y 0x0000000000434178 in anetListen at anet.c:410
    5 breakpoint keep y 0x000000000043417f in anetListen at anet.c:412
    breakpoint already hit 1 time
    (gdb) delete
    Delete all breakpoints? (y or n) y
    (gdb) info b
    No breakpoints or watchpoints.

list

  • list用于显示源码,默认显示10行。如图所示:
    gdb17

print、ptype

  • print用于查看变量的值,ptype用于查看变量类型。如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    (gdb) f 4
    #4 0x000000000043b304 in initServer () at server.c:3195
    3195 listenToPort(server.port,&server.ipfd) == C_ERR) {
    (gdb) p server.port
    $1 = 6379
    (gdb) ptype server
    type = struct redisServer {
    pid_t pid;
    pthread_t main_thread_id;
    char *configfile;
    char *executable;
    char **exec_argv;
    int dynamic_hz;
    int config_hz;
    mode_t umask;
    ....}
    (gdb) ptype server.port
    type = int

info 和 thread

  • info thread查看redis-server启动后一共产生5个线程,1个主线程和4个工作线程。主线程阻塞在epoll_wait()处,其他工作线程阻塞在pthread_cond_wait()处。*表示的是当前gdb作用于哪个线程上,而不是*就代表指向主线程。
    gdb18

  • bt查看当前线程的调用堆栈,如下图所示:
    gdb19
    堆栈#4上显示的调用main()函数表示当前线程即主线程。

  • thread num切换到其他线程
    gdb20

next 和 step

  • next单步步过(step over),遇到函数直接跳过不会进入函数内部。(回车键默认是将最近的一条命令执行)
  • step单步步入(step into),遇到函数进入函数内部。

until 和 jump

  • until可以指定程序运行到某一行停下来。
  • jump可以指定程序执行流跳转到指定位置执行,若跳转的地方没有断点则会继续执行,可以用于测试异常分支代码。

finish 和 return

  • finish命令会执行函数到正常退出该函数。
  • return命令是立即结束执行当前函数并返回,即使当前函数还有剩余的代码未执行完毕,也不会执行。

disassemble

  • disassemble命令查看汇编代码,一般用于高级调试选哟查看某段代码的汇编指令,或调试没有调式信息的发版程序。
    gdb21

set args 和 show args

  • set args用于传递命令行参数。做法:gdb附加程序,在run之前,使用set args 参数内容来传递命令行参数。
  • show args用于查看命令行参数是否设置成功。
    gdb22

watch

用于监视一个变量或者一段内存,当其值发生变化时,gdb会中断下来。被监视的变量或内存会产生一个watch point(观察点)。

  • watch命令时通过添加硬件断点来实现监视数据变化,格式:watch 变量名/内存地址

    • 整形变量

      1
      2
      int i;
      watch i
    • 指针类型

      1
      2
      3
      char *p;
      watch p
      watch *p
    • 数组/内存区间(这里是对buf的128个数据监视,不是采用硬件中断,是用软中断实现)

      1
      2
      char buf[10];
      watch buf
    • 程序例子,可见watch的变量a的值在发生变化时会中断

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      #include <iostream>
      using namespace std;

      int main() {
      int a = 1;
      while (a <= 5)
      {
      a = a*2;
      }
      cout << a << endl;
      return 0;
      }

      gdb23

多进程/线程调试

  • 调试父子进程

    • gdb调试父进程,等子进程fork完成后,使用gdb attach pid进入子进程,需要重新开启一个窗口用于调试。
    • gdb调试器提供选项follow-fork,可以使用show follow-fork mode查看当前值,也可以通过set follow-fork mode来设置是当一个进程fork出新的子进程时,gdb时继续调试父进程还是子进程(取值child),默认是父进程(取值parent
    1
    2
    3
    4
    5
    6
    (gdb) show follow-fork mode     
    Debugger response to a program call of fork or vfork is "parent".
    (gdb) set follow-fork child
    (gdb) show follow-fork mode
    Debugger response to a program call of fork or vfork is "child".
    (gdb)
  • spp 线程

    • tool目录下将服务停止
    • 进入bin目录
      1
      2
      3
      4
      5
      ./spp_xxx_proxy ../etc/spp_proxy.xml
      gdb --args ./spp_xxx_worker ../etc/spp_worker1.xml
      set follow-fork-mode child
      b 设置断点
      r 运行程序