GDB

GDB 调试器

​ GDB( GNU 项目调试器)可以让您了解程序在执行时“内部” 究竟在干些什么,以及在程序发生崩溃的瞬间正在做什么。

​ GDB 做以下 4 件主要的事情来帮助您捕获程序中的 bug:

  • 在程序启动之前指定一些可以影响程序行为的变量或条件
  • 在某个指定的地方或条件下暂停程序
  • 在程序停止时检查已经发生了什么
  • 在程序执行过程中修改程序中的变量或条件,这样就可以体验修复一个bug 的成果,并继续了解其他 bug

一般要调试某个程序,为了能清晰地看到调试的每一行代码、调用的堆栈信息、变量名和函数名等信息,需要调试程序含有调试符号信息。使用 gcc 编译程序时,如果加上 -g 选项即可在编译后的程序中保留调试符号信息。

gcc -g -o hello hello.c

在实际生成调试程序时,一般不仅要加上 -g 选项,也建议关闭编译器的程序优化选项。编译器的程序优化选项一般有五个级别,从 O0 ~ O4 ( 注意第一个 O0 ,是字母 O 加上数字 0 ), O0 表示不优化,从 O1 ~ O4 优化级别越来越高, O4 最高。这样做的目的是为了调试的时候,符号文件显示的调试变量等能与源代码完全对应起来。

启动 GDB 调试

GDB 调试主要有三种方式:

  1. gdb filename 直接调试目标程序 ( gdb ./hello)
  2. gdb attach pid 附加进程
  3. gdb filename corename 调试 core 文件

退出 gdb

输入 q 或者 Ctrl-d 来退出

常用命令

命令名称 命令缩写 命令说明
run r 运行一个程序
continue c 让暂停的程序继续运行
next n 运行到下一行
step s 如果有调用函数,进入调用的函数内部,相当于 step into
until u 运行到指定行停下来
finish fi 结束当前调用函数,到上一层函数调用处
return return 结束当前调用函数并返回指定值,到上一层函数调用处
jump j 将当前程序执行流跳转到指定行或地址
print p 打印变量或寄存器值
backtrace bt 查看当前线程的调用堆栈
frame f 切换到当前调用线程的指定堆栈,具体堆栈通过堆栈序号指定
thread thread 切换到指定线程
break b 添加断点
tbreak tb 添加临时断点
delete del 删除断点
enable enable 启用某个断点
命令名称 命令缩写 命令说明
disable disable 禁用某个断点
watch watch 监视某一个变量或内存地址的值是否发生变化
list l 显示源码
info info 查看断点 / 线程等信息
ptype ptype 查看变量类型
disassemble dis 查看汇编代码
set args 设置程序启动命令行参数
show args 查看设置的命令行参数

命令简介

1 run 命令

默认情况下, gdb filename 命令只是附加的一个调试文件,并没有启动这个程序,需要输入 run 命令(简写为 r)启动这个程序

假设程序已经启动,再次输入 run 命令则是重启程序。我们在 GDB 界面按 Ctrl + C 快捷键让 GDB 中断下来,再次输入 run 命令, GDB会询问我们是否重启程序,输入 yes 确认重启。

2 continue 命令

当 GDB 触发断点或者使用 Ctrl + C 命令中断下来后,想让程序继续运行,只要输入 continue 命令即可(简写为 c)。当然,如果 continue 命令继续触发断点, GDB 就会再次中断下来

3 break 命令

break 命令(简写为 b)即我们添加断点的命令,可以使用以下方式添加断点:

  • break functionname,在函数名为 functionname 的入口处添加一个断点;(C++调试类方法的话,得写上类名)
  • break LineNo,在当前文件行号为 LineNo 处添加一个断点;
  • break filename:LineNo,在 filename 文件行号为 LineNo 处添加一个断点。

这三种方式都是我们常用的添加断点的方式。

4 backtrace 与 frame 命令

backtrace 命令(简写为 bt)用来查看当前调用堆栈。

如果想切换到其他堆栈处,可以使用 frame 命令( 简写为 f),该命令的使用方法是“ frame 堆栈编号(编号不加 #)”。

5 info break、 enable、 disable 和 delete 命令

在程序中加了很多断点,而我们想查看加了哪些断点时,可以使用 info break 命令(简写为 info b)

如果我们想禁用某个断点,使用“ disable 断点编号” 就可以禁用这个断点了,被禁用的断点不会再被触发;同理,被禁用的断点也可以使用“ enable 断点编号” 重新启用。

使用 disable 1 以后,第一个断点的 Enb 一栏的值由 y 变成 n,重启程序也不会再次触发

如果 disable 命令和 enable 命令不加断点编号,则分别表示禁用和启用所有断点

使用“ delete 编号”可以删除某个断点,如 delete 2 3 则表示要删除的断点 2 和断点 3

如果输入 delete 不加命令号,则表示删除所有断点

6 list 命令

(gdb) list

如果不带任何参数的话,该命令会接着打印上次 list 命令打印出代码后面的代码。如果是第一次执行 list 命令则会显示当前正在执行代码位置附近的代码。

(gdb) list -

如果参数是一个减号的话,则和前面刚好相反,会打印上次 list 命令打印出代码前面的代码。

(gdb) list LOATION

list 命令还可以带一个代码位置作为参数,顾名思义,这样的话就会打印出该代码位置附近的代码。这个代码位置的定义和在 break 命令中定义的相同,可以是一个行号:

(gdb) list 100

列出当前代码文件中第 100 行附近代码

(gdb) sample.c:29

列出 sample.c 文件中第 29行附近代码

(gdb) list  main

列出当前代码文件中 main 函数附近代码

(gdb) list sample.c:add

列出 sample.c 代码文件中指定函数附近代码

(gdb) list FIRST,LAST

list 命令还可以指定要显示代码的具体范围。这里 FIRST 和 LAST 都是具体的代码位置,此时该命令将显示 FIRST 到 LAST 之间的代
码。可以不指定 FIRST 或者 LAST 参数,这样的话就将显示 LAST 之前或者 FIRST 之后的代码。注意,即使只指定一个参数也要带逗号,否则就编程前面的命令,显示代码位置附近的代码了。

list 命令默认只会打印出 10 行源代码,如果觉得不够,可以使用如下命令修改:

(gdb) set listsize COUNT

这样的话,下次 list 命令就会显示 COUNT 行源代码了。如果想查看这个参数当前被设置成多少,可以使用如下命令:

(gdb) show listsize  

还有一个非常有用的命令,如果你想看程序中一共定义了哪些函数,可以使用下面的命令:

(gdb) info functions

这个命令会显示程序中所有函数的名词,参数格式,返回值类型以及函数处于哪个代码文件中。

list 命令(简写为 l)可以查看当前断点处的代码。

再次输入 list 命令试一下,则往后查阅代码

7 print 和 ptype 命令

通过 print 命令(简写为 p)我们可以在调试过程中方便地查看变量的值,也可以修改当前内存中的变量值。

假设要查看的变量名是 sum,则命令为p sump &sum可以输出 sum 的地址值。如果在 C++对象中,可以通过 p this 来显示当前对象的地址,也可以通过 p *this 来列出当前对象的各个成员变量值,如果有三个变量可以相加( 假设变量名分别叫 a、 b、 c ),可以使用 p a + b + c 来打印这三个变量的结果值。

假设 func() 是一个可以执行的函数, p func() 命令可以输出该变量的执行结果。举一个最常用的例子,某个时刻,某个系统函数执行失败了,通过系统变量 errno 得到一个错误码,则可以使用 p strerror(errno) 将这个错误码对应的文字信息打印出来,这样就不用费劲地去 man 手册上查找这个错误码对应的错误含义了。

print 命令不仅可以输出表达式结果,同时也可以修改变量的值。 如p sum=100,可以临时把sum的值改为100。

8 ptype 命令

ptype ,顾名思义,其含义是“ print type”,就是输出一个变量的类型

(gdb) ptype sum
type = int
(gdb) 

9 info 和 thread 命令

info 命令是一个复合指令,还可以用来查看当前进程的所有线程运行情况

(gdb) info threads
  Id   Target Id                                  Frame 
* 1    Thread 0x7ffff7a6d3c0 (LWP 9135) "test_tp" test1 () at /home/dark/mylearn/c++/valgrindtest/main.cpp:36
  2    Thread 0x7ffff7a69640 (LWP 9211) "test_tp" 0x00007ffff7b46a55 in clock_nanosleep@GLIBC_2.2.5 () from /usr/lib/libc.so.6
  3    Thread 0x7ffff7268640 (LWP 9212) "test_tp" 0x00007ffff7af815a in __futex_abstimed_wait_common () from /usr/lib/libc.so.6
  4    Thread 0x7ffff6a67640 (LWP 9213) "test_tp" 0x00007ffff7af815a in __futex_abstimed_wait_common () from /usr/lib/libc.so.6
  5    Thread 0x7ffff6266640 (LWP 9214) "test_tp" 0x00007ffff7af8330 in __lll_lock_wait () from /usr/lib/libc.so.6
  6    Thread 0x7ffff5a65640 (LWP 9215) "test_tp" 0x00007ffff7af8330 in __lll_lock_wait () from /usr/lib/libc.so.6
  7    Thread 0x7ffff5264640 (LWP 9216) "test_tp" 0x00007ffff7af8330 in __lll_lock_wait () from /usr/lib/libc.so.6
  8    Thread 0x7ffff4a63640 (LWP 9217) "test_tp" 0x00007ffff7af8330 in __lll_lock_wait () from /usr/lib/libc.so.6
  9    Thread 0x7ffff4262640 (LWP 9218) "test_tp" 0x00007ffff7af8330 in __lll_lock_wait () from /usr/lib/libc.so.6
  10   Thread 0x7ffff3a61640 (LWP 9219) "test_tp" 0x00007ffff7af8330 in __lll_lock_wait () from /usr/lib/libc.so.6
  11   Thread 0x7ffff3260640 (LWP 9220) "test_tp" 0x00007ffff7af8330 in __lll_lock_wait () from /usr/lib/libc.so.6
(gdb) 

注意 虽然第一栏的名称叫 Id,但第一栏的数值不是线程的 Id,第三栏括号里的内容(如 LWP 9135)中, 9135这样的数值才是当前线程真正的 Id。 Light Weight Process(轻量级进程),即是我们所说的线程。

可以通过“ thread 线程编号”切换到具体的线程上去。例如,想切换到线程 2 上去,只要输入 thread 2 即可,然后输入 bt 就能查看这个线程的调用堆栈了

请注意,当把 GDB当前作用的线程切换到线程 2 上之后, 线程 2 前面就被加上了星号

info 命令还可以用来查看当前函数的参数值,组合命令是 info args

10 next、 step、 until、 finish、 return 和 jump 命令

next

next命令(简写为 n)是让 GDB 调到下一条命令去执行,这里的下一条命令不一定是代码的下一行,而是根据程序逻辑跳转到相应的位置。

如果当前 GDB 中断在上述代码第 6 行,此时输入 next 命令 GDB 将调到第 11 行,因为这里的 if 条件并不满足。

这里有一个小技巧,在 GDB 命令行界面如果直接按下回车键,默认是将最近一条命令重新执行一遍,因此, 当使用 next 命令单步调试时,不必反复输入 n 命令,直接回车就可以了。

next 命令用调试的术语叫“单步步过”( step over),即遇到函数调用直接跳过,不进入函数体内部。

step

step 命令(简写为 s)就是“单步步入”( step into),顾名思义,就是遇到函数调用,进入函数内部。

在26行打断点,运行到断点处后输出s就可以进入add函数

(gdb) b 26
Breakpoint 1 at 0x11ee: file sample.cpp, line 26.
(gdb) r
Starting program: /home/dark/mylearn/c++/valgrindtest/sample 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
a:1, b:2 
c:3

Breakpoint 1, main (argc=1, argv=0x7fffffffe658) at sample.cpp:26
26          int s = add((a + c), b);
(gdb) s
add (a=4, b=2) at sample.cpp:13
13          int sum = a + b;
(gdb) bt
#0  add (a=4, b=2) at sample.cpp:13
#1  0x0000555555555202 in main (argc=1, argv=0x7fffffffe658) at sample.cpp:26
(gdb) 

return 和 finish

实际调试时,我们在某个函数中调试一段时间后,不需要再一步步执行到函数返回处,希望直接执行完当前函数并回到上一层调用处,就可以使用 finish 命令。与 finish命令类似的还有 return 命令, return 命令的作用是结束执行当前函数,还可以指定该函数的返回值。

这里需要注意一下二者的区别: finish 命令会执行函数到正常退出该函数;而 return命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。

until 命令

实际调试时,还有一个 until 命令( 简写为 u)可以指定程序运行到某一行停下来

(gdb) b main
Breakpoint 2 at 0x555555555199: file sample.cpp, line 19.
(gdb) r
Starting program: /home/dark/mylearn/c++/valgrindtest/sample 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".

Breakpoint 2, main (argc=1, argv=0x7fffffffe658) at sample.cpp:19
19          int a, b = 0;
(gdb) u 29
a:1, b:2 
c:3

Breakpoint 1, main (argc=1, argv=0x7fffffffe658) at sample.cpp:26
26          int s = add((a + c), b);
(gdb) u 29
s:6
main (argc=1, argv=0x7fffffffe658) at sample.cpp:29
29          return 0;
(gdb) 

当停在第19行,想直接跳到第29行,可以直接输入 u 29,这样就能快速执行完中间的代码(中间遇到断点的话会停下)。

Jump 命令

jump 命令基本用法是:

jump

  • 该命令会带一个参数,即要跳转到的代码位置,可以是源代码的行号:

    (gdb) jump 555 #跳转到源代码的第 555 行的位置

  • 可以是相对当前代码位置的偏移量:

    (gdb) jump +10 #跳转到距当前代码下 10 行的位置

  • 也可以是代码所处的内存地址:

    (gdb) jump *0x12345678 #跳转到位于该地址的代码处

注意,在内存地址前面要加“ *”。还有, jump 命令不会改变当前程序调用栈的内容,所以当你从一个函数跳到另一个函数时,当函数运行完返回进行退栈操作时就会发生错误,因此最好还是在同一个函数中进行跳转。

location 可以是程序的行号或者函数的地址, jump 会让程序执行流跳转到指定位置执行,当然其行为也是不可控制的,例如您跳过了某个对象的初始化代码,直接执行操作该对象的代码,那么可能会导致程序崩溃或其他意外行为。 jump 命令可以简写成 j,但是不可以简写成 jmp,其使用有一个注意事项,即如果 jump 跳转到的位置后续没有断点,那么 GDB 会执行完跳转处的代码会继续执行。举个例子:

1 int somefunc()
2 {
3 //代码 A
4 //代码 B
5 //代码 C
6 //代码 D
7 //代码 E
8 //代码 F
9 }

假设我们的断点初始位置在行号 3 处(代码 A),这个时候我们使用 jump 6,那么程序会跳过代码 B 和 C 的执行,执行完代码 D( 跳转点),程序并不会停在代码 6 处,而是继续执行后续代码,因此如果我们想查看执行跳转处的代码后的结果,需要在行号6、 7 或 8 处设置断点。

11 disassemble 命令

​ 当进行一些高级调试时,我们可能需要查看某段代码的汇编指令去排查问题,或者是在调试一些没有调试信息的发布版程序时,也只能通过反汇编代码去定位问题,那么disassemble 命令就派上用场了。

12 set args 和 show args 命令

​ 很多程序需要我们传递命令行参数。在 GDB 调试中,很多人会觉得可以使用 gdb filename args 这种形式来给 GDB 调试的程序传递命令行参数,这样是不行的。正确的做法是在用 GDB 附加程序后,在使用 run 命令之前,使用“ set args 参数内容”来设置命令行参数。

如果想清除掉已经设置好的命令行参数,使用 set args 不加任何参数即可

(gdb) show args
Argument list to give program being debugged when it is started is "".
(gdb) set args "hahaha" "xixixi"
(gdb) show args
Argument list to give program being debugged when it is started is ""hahaha" "xixixi"".
(gdb) set args
(gdb) show args
Argument list to give program being debugged when it is started is "".
(gdb) 

13 tbreak 命令

​ tbreak 命令也是添加一个断点,第一个字母“ t”的意思是 temporarily(临时的),也就是说这个命令加的断点是临时的,所谓临时断点,就是一旦该断点触发一次后就会自动删除。添加断点的方法与上面介绍的 break 命令一模一样

14 watch 命令

​ watch 命令是一个强大的命令,它可以用来监视一个变量或者一段内存,当这个变量或者该内存处的值发生变化时, GDB 就会中断下来。被监视的某个变量或者某个内存地址会产生一个 watch point(观察点)。

watch 命令的使用方式是“ watch 变量名或内存地址”,一般有以下几种形式:

  • 形式一:整型变量s

    int i;

    watch i

  • 形式二:指针类型

    char *p;

    watch p 与 watch *p

    注意: watch p 与 watch *p 是有区别的,前者是查看 *(&p),是 p 变量本身;后者是 p 所指内存的内容。我们需要查看地址,因为目的是要看某内存地址上的数据是怎样变化的。

  • 形式三: watch 一个数组或内存区间

    char buf[128];

    watch buf

    这里是对 buf 的 128 个数据进行了监视,此时不是采用硬件断点,而是用软中断实现的。用软中断方式去检查内存变量是比较耗费 CPU 资源的,精确地指明地址是硬件中断。

注意:当设置的观察点是一个局部变量时,局部变量无效后,观察点也会失效。在观察点失效时 GDB 可能会提示如下信息:
Watchpoint 2 deleted because the program has left the block in which its expression is valid.

#include <stdio.h>
#include <string.h>

char mem[8];
char buf[128];

void initBuf(char *pBuf)
{
    int i, j;
    mem[0] = '0';
    mem[1] = '1';
    mem[2] = '2';
    mem[3] = '3';
    mem[4] = '4';
    mem[5] = '5';
    mem[6] = '6';
    mem[7] = '7';
    //ascii table first 32 is not printable
    for (i = 2; i < 8; i++)
    {
        for (j = 0; j < 16; j++)
            pBuf[i * 16 + j] = i * 16 + j;
    }
}

void prtBuf(char *pBuf)
{
    int i, j;
    for (i = 2; i < 8; i++)
    {
        for (j = 0; j < 16; j++)
            printf("%c  ", pBuf[i * 16 + j]);
        printf("\n");
    }
}

int main()
{
    initBuf(buf);
    prtBuf(buf);
    return 0;
}

测试

(gdb) b main
Breakpoint 1 at 0x1253: file sample.cpp, line 39.
(gdb) watch mem
Hardware watchpoint 2: mem
(gdb) r
Starting program: /home/dark/mylearn/c++/valgrindtest/sample 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".

Breakpoint 1, main () at sample.cpp:39
39          initBuf(buf);
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "\000\000\000\000\000\000\000"
New value = "0\000\000\000\000\000\000"
initBuf (pBuf=0x555555558080 <buf> "") at sample.cpp:11
11          mem[1] = '1';
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "0\000\000\000\000\000\000"
New value = "01\000\000\000\000\000"
initBuf (pBuf=0x555555558080 <buf> "") at sample.cpp:12
12          mem[2] = '2';
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "01\000\000\000\000\000"
New value = "012\000\000\000\000"
initBuf (pBuf=0x555555558080 <buf> "") at sample.cpp:13
13          mem[3] = '3';
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "012\000\000\000\000"
New value = "0123\000\000\000"
initBuf (pBuf=0x555555558080 <buf> "") at sample.cpp:14
14          mem[4] = '4';
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "0123\000\000\000"
New value = "01234\000\000"
initBuf (pBuf=0x555555558080 <buf> "") at sample.cpp:15
15          mem[5] = '5';
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "01234\000\000"
New value = "012345\000"
initBuf (pBuf=0x555555558080 <buf> "") at sample.cpp:16
16          mem[6] = '6';
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "012345\000"
New value = "0123456"
initBuf (pBuf=0x555555558080 <buf> "") at sample.cpp:17
17          mem[7] = '7';
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "0123456"
New value = "01234567"
initBuf (pBuf=0x555555558080 <buf> "") at sample.cpp:19
19          for (i = 2; i < 8; i++)
(gdb) c
Continuing.
   !  "  #  $  %  &  '  (  )  *  +  ,  -  .  /  
0  1  2  3  4  5  6  7  8  9  :  ;  <  =  >  ?  
@  A  B  C  D  E  F  G  H  I  J  K  L  M  N  O  
P  Q  R  S  T  U  V  W  X  Y  Z  [  \  ]  ^  _  
`  a  b  c  d  e  f  g  h  i  j  k  l  m  n  o  
p  q  r  s  t  u  v  w  x  y  z  {  |  }  ~    
[Inferior 1 (process 11337) exited normally]
(gdb) 

watch i 的问题:是可以同时去 watch,只是局部变量需要进入到相应的起作用范围才能 watch。 比如 initBuf 函数的 i。

问题来了,如果要取消 watch 怎么办?

先用 info watch 查看 watch 的变量,然后根据编号使用 delete 删除相应的 watch 变量。

15 display 命令

​ display 命令监视的变量或者内存地址,每次程序中断下来都会自动输出这些变量或内存的值。例如,假设程序有一些全局变量,每次断点停下来我都希望 GDB 可以自动输出这些变量的最新值,那么使用“ display 变量名”设置即可。

调试技巧

1 将 print 打印结果显示完整

​ 当使用 print 命令打印一个字符串或者字符数组时,如果该字符串太长, print 命令默认显示不全的,我们可以通过在 GDB 中输入 set print element 0 命令设置一下,这样再次使用 print 命令就能完整地显示该变量的所有字符串了。

2 多线程下禁止线程切换

​ 假设现在有 5 个线程,除了主线程,工作线程都是下面这样的一个函数:

void thread_proc(void* arg)
{
    //代码行 1
    //代码行 2
    //代码行 3
    //代码行 4
    //代码行 5
    //代码行 6
    //代码行 7
    //代码行 8
    //代码行 9
    //代码行 10
    //代码行 11
    //代码行 12
    //代码行 13
    //代码行 14
    //代码行 15
}

为了能说清楚这个问题,我们把四个工作线程分别叫做 A、 B、 C、 D。

假设 GDB 当前正在处于线程 A 的代码行 3 处,此时输入 next 命令,我们期望的是调试器跳到代码行 4 处;或者使用“ u 代码行 10”,那么我们期望输入 u 命令后调试器可以跳转到代码行 10 处。

但是在实际情况下, GDB 可能会跳转到代码行 1 或者代码行 2 处,甚至代码行13、代码行 14 这样的地方也是有可能的,这不是调试器 bug,这是多线程程序的特点,当我们从代码行 4 处让程序 continue 时,线程 A 虽然会继续往下执行,但是如果此时系统的线程调度将 CPU 时间片切换到线程 B、 C 或者 D 呢?那么程序最终停下来的时候,处于代码行 1 或者代码行 2 或者其他地方就不奇怪了,而此时打印相关的变量值,可能就不是我们需要的线程 A 的相关值。

为了解决调试多线程程序时出现的这种问题, GDB 提供了一个在调试时将程序执行流锁定在当前调试线程的命令: set scheduler-locking on。当然也可以关闭这一选项,使用 set scheduler-locking off。

3 条件断点

​ 在实际调试中,我们一般会用到三种断点:普通断点、条件断点和硬件断点。

​ 硬件断点又叫数据断点,这样的断点其实就是前面课程中介绍的用 watch 命令添加的部分断点(为什么是部分而不是全部,前面介绍原因了, watch 添加的断点有部分是通过软中断实现的,不属于硬件断点)。硬件断点的触发时机是监视的内存地址或者变量值发生变化。

​ 普通断点就是除去条件断点和硬件断点以外的断点。

​ 下面重点来介绍一下条件断点,所谓条件断点,就是满足某个条件才会触发的断点,这里先举一个直观的例子:

void do_something_func(int i)
{
    i ++;
    i = 100 * i;
}
int main()
{
    for(int i = 0; i < 10000; ++i)
    {
    	do_something_func(i);
    }
    
    return 0;
}

在上述代码中,假如我们希望当变量 i=5000 时,进入 do_something_func() 函数追踪一下这个函数的执行细节。此时可以修改代码增加一个 i=5000 的 if 条件,然后重新编译链接调试,这样显然比较麻烦,尤其是对于一些大型项目,每次重新编译链接都需要花一定的时间,而且调试完了还得把程序修改回来。

有了条件断点就不需要这么麻烦了,添加条件断点的命令是 break [lineNo] if [condition],其中 lineNo 是程序触发断点后需要停下的位置, condition 是断点触发的条件。这里可以写成 break 11 if i==5000,其中, 11 就是调用 do_something_fun() 函数所在的行号。当然这里的行号必须是合理行号,如果行号非法或者行号位置不合理也不会触发这个断点。

(gdb) break 11 if i==5000
Breakpoint 2 at 0x400514: file test1.c, line 10.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/testgdb/test1
Breakpoint 1, main () at test1.c:9
9 for(int i = 0; i < 10000; ++i)
(gdb) c
Continuing.
Breakpoint 2, main () at test1.c:11
11 do_something_func(i);
(gdb) p i
$1 = 5000

把 i 打印出来, GDB 确实是在 i=5000 时停下来了。

添加条件断点还有一个方法就是先添加一个普通断点,然后使用“ condition 断点编号断点触发条件”这样的方式来添加。添加一下上述断点:

gdb) b 11
Breakpoint 1 at 0x400514: file test1.c, line 11.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400514 in main at test1.c:11
(gdb) condition 1 i==5000
(gdb) r
Starting program: /root/testgdb/test1
y
Breakpoint 1, main () at test1.c:11
11 do_something_func(i);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64
(gdb) p i
$1 = 5000
(gdb)

4 使用 GDB 调试多进程程序

这里说的多进程程序指的是一个进程使用 Linux 系统调用 fork() 函数产生的子进程,没有相互关联的进程就是普通的 GDB 调试

在实际的应用中,如有这样一类程序,如 Nginx,对于客户端的连接是采用多进程模型,当 Nginx 接受客户端连接后,创建一个新的进程来处理这一路连接上的信息来往,新产生的进程与原进程互为父子关系,那么如何用 GDB 调试这样的父子进程呢?一般有两种方法:

( 1) 用 GDB 先调试父进程,等子进程 fork 出来后,使用 gdb attach 到子进程上去,当然这需要重新开启一个 session 窗口用于调试

( 2) GDB 调试器提供了一个选项叫 follow-fork,可以使用 show follow-fork mode查看当前值,也可以通过 set follow-fork mode 来设置是当一个进程 fork 出新的子进程时, GDB 是继续调试父进程还是子进程(取值是 child),默认是父进程( 取值是 parent)。

(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)

参考文档

官方参考文档: http://valgrind.org/docs/manual/QuickStart.html
官方参考文档: http://www.gnu.org/software/gdb/documentation/
官方参考文档: http://sourceware.org/gdb/onlinedocs/gdb/TUI.html
GDB 常用命令 https://blog.csdn.net/Roland_Sun/article/details/42460663

posted @ 2022-05-22 22:37  DarkH  阅读(621)  评论(0编辑  收藏  举报