GDB(GNU Debugger)是UNIX及UNIX-like下的强大调试工具,可以调试ada, c, c++, asm, minimal, d, fortran, objective-c, go, java,pascal等语言。

笔记:微信公众号,编程珠玑。

哪些程序可以被调试

对于C程序来说,需要在编译时加上-g参数,保留调试信息,否则不能使用GDB进行调试。
但如果不是自己编译的程序,并不知道是否带有-g参数,如何判断一个文件是否带有调试信息呢?

gdb file_name

  • 不能调试。

    Linux-GDB-1

  • 可以调试

2. readelf 查看段信息

  • 不能调试

  • 可以调试

开始调试

调试无参程序

  1. gdb file_name
  2. run(简写r) 运行。

调试带参程序

  1. gdb file_name
  2. run 参数

或者

  1. gdb file_name
  2. set args 参数
  3. runr

list(可简写为l),它可以将源码列出来。

调试 core 文件

当程序core dump时,可能会产生core文件,它能够很大程序帮助我们定位问题。前提是系统没有限制core文件的产生。

1
ulimit -a	# 查看

1
2
core file size          (blocks, -c) 0
# 如果结果是0,即便程序core dump了也不会有core文件留下。

取消系统限制:

1
2
ulimit -c unlimied # 取消限制core文件大小。
ulimit -c 10 # 设置最大大小,单位为块,一块默认为512字节

调试 core dump 文件:

1
gdb 程序文件名 core文件名

调试已运行程序

使用ps命令找到进程id

1
ps -ef | grep 进程名

attach方式

假设获取到进程id为20829,则用下面的方式调试进程:

1
2
gdb
(gdb) attach 20829

如果有下面的错误提示:

1
2
3
4
Could not attach to process.  If your uid matches the uid of the target
process, check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
again as the root user. For more details, see /etc/sysctl.d/10-ptrace.conf
ptrace: Operation not permitted.

解决方法,切换到root用户:

1
2
3
4
# /etc/sysctl.d/10-ptrace.conf
kernel.yama.ptrace_scope = 1
# 修改为
kernel.yama.ptrace_scope = 0

直接调试相关id进程

1
2
3
gdb program pid
# or
gdb hello --pid 20829

已运行程序没有调试信息

为了节省磁盘空间,已经运行的程序通常没有调试信息。但如果又不能停止当前程序重新启动调试,那怎么办呢?还有办法,那就是同样的代码,再编译出一个带调试信息的版本。然后使用和前面提到的方式操作。对于attach方式,在attach之前,使用file命令即可:

1
2
3
4
$ gdb
(gdb) file hello
Reading symbols from hello...done.
(gdb)attach 20829

断点

查看断点

1
info breakpoints

设置断点

  1. 根据行号设置断点

    1
    2
    3
    b 9  #break 可简写为b
    # 或者
    b test.c:9
  2. 根据函数名设置断点

    1
    b fun_name
  3. 根据条件设置断点

    1
    2
    3
    4
    5
    break test.c:23 if b==0
    # 当在b等于0时,程序将会在第23行断住。
    # condition有着类似的作用,假设上面的断点号为1
    condition 1 b==0
    # b等于0时,产生断点1。
  4. 根据规则设置断点

    1
    2
    3
    4
    5
    6
    7
    rbreak printNum*
    # 对所有调用printNum函数都设置断点

    # 下面是对所有函数设置断点
    rbreak .
    rbreak test.c:. # 对test.c中的所有函数设置断点
    rbreak test.c:^print # 对以print开头的函数设置断点
  5. 设置临时断点

    1
    2
    tbreak test.c:l0  # 在第10行设置临时断点
    # 某处的断点只想生效一次,那么可以设置临时断点,这样断点后面就不复存在了
  6. 跳过多次设置断点

    1
    2
    3
    # ignore next 30 hits
    ignore 1 30
    # 1是要忽略的断点号,可以通过前面的方式查找到,30是需要跳过的次数。这样设置之后,会跳过前面30次。
  7. 根据表达式值变化产生断点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 有时候我们需要观察某个值或表达式,知道它什么时候发生变化了,这个时候我们可以借助watch命令。
    watch a
    # 这个时候,让程序继续运行,如果a的值发生变化,则会打印相关内容
    Hardware watchpoint 2: a
    Old value = 12
    New value = 11
    # 但是这里要特别注意的是,程序必须运行起来,否则会出现:
    No symbol "a" in current context.
    # 因为程序没有运行,当前上下文也就没有相关变量信息。
    # rwatch和awatch同样可以设置观察点,前者是当变量值被读时断住,后者是被读或者被改写时断住。

禁用断点

1
2
3
4
5
disable  # 禁用所有断点
disable bnum # 禁用标号为bnum的断点
enable # 启用所有断点
enable bnum # 启用标号为bnum的断点
enable delete bnum # 启动标号为bnum的断点,并且在此之后删除该断点

清除断点

1
2
3
4
5
6
7
clear   # 删除当前行所有breakpoints
clear function # 删除函数名为function处的断点
clear filename:function # 删除文件filename中函数function处的断点
clear lineNum # 删除行号为lineNum处的断点
clear f:lename:lineNum # 删除文件filename中行号为lineNum处的断点
delete # 删除所有breakpoints,watchpoints和catchpoints
delete bnum # 删除断点号为bnum的断点

查看变量

打印基本类型变量,数组,字符数组

print(可简写为p)

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) p a
$1 = 10
(gdb) p b
$2 = {1, 2, 3, 5}
(gdb) p c
$3 = "hello,gdb"

# 有时候,多个函数或者多个文件会有同一个变量名,这个时候可以在前面加上文件名或者函数名来区分:
(gdb) p 'testGdb.h'::a
$1 = 11
(gdb) p 'main'::b
$2 = {1, 2, 3, 5}

打印指针指向内容

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) p d
1 = (int *) 0x602010

# 打印指针指向的内容
(gdb) p *d
$2 = 0
(gdb) p *d@10
$3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

# 使用*只能打印第一个值,如果要打印多个值,后面跟上@并加上要打印的长度。或者@后面跟上变量值
(gdb) p *d@a
$2 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
# a的值为10,并且是作为整型指针数据长度,因此后面可以直接跟着a,也可以打印出所有内容。

$

1
2
3
4
5
6
7
8
9
10
11
12
13
# $可表示上一个变量,而假设此时有一个链表linkNode,它有next成员代表下一个节点,则可使用下面方式不断打印链表内容:
(gdb) p *linkNode
(这里显示linkNode节点内容)
(gdb) p *$.next
(这里显示linkNode节点下一个节点的内容)
# 如果想要查看前面数组的内容,你可以将下标一个一个累加,还可以定义一个类似UNIX环境变量,例如:
(gdb) set $index=0
(gdb) p b[$index++]
$11 = 1
(gdb) p b[$index++]
$12 = 2
(gdb) p b[$index++]
$13 = 3

按照特定格式打印变量

  • x 按十六进制格式显示变量。
  • d 按十进制格式显示变量。
  • u 按十六进制格式显示无符号整型。
  • o 按八进制格式显示变量。
  • t 按二进制格式显示变量。
  • a 按十六进制格式显示变量。
  • c 按字符格式显示变量。
  • f 按浮点数格式显示变量。
1
2
3
4
5
6
7
8
9
10
11
(gdb) p c
$18 = "hello,shouwang"
# 查看它的十六进制格式打印
(gdb) p/x c
$19 = {0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x73, 0x68, 0x6f, 0x75, 0x77, 0x61, 0x6e, 0x67, 0x0}
# 如果想用这种方式查看浮点数的二进制格式是怎样的是不行的,因为直接打印它首先会被转换成整型,因此最终会得到8
(gdb) p e
$1 = 8.5
(gdb) p/t e
$2 = 1000
# 那么就需要另外一种查看方式了。

查看内存内容

examine(简写为x)可以用来查看内存地址中的值。语法如下:

1
x/[n][f][u] addr
  • n 表示要显示的内存单元数,默认值为1
  • f 表示要打印的格式,前面已经提到了格式控制字符
  • u 要打印的单元长度
  • addr 内存地址

单元类型常见有如下:

  • b 字节
  • h 半字,即双字节
  • w 字,即四字节
  • g 八字节
1
2
3
4
# 把float变量e按照二进制方式打印,并且打印单位是一字节:
(gdb) x/4tb &e
0x7fffffffdbd4: 00000000 00000000 00001000 01000001
# 看到,变量e的四个字节都以二进制的方式打印出来了。

自动显示变量内容

假设希望程序断住时,就显示某个变量的值,可以使用display命令。

1
2
(gdb) display e
1: e = 8.5

那么每次程序断住时,就会打印e的值。要查看哪些变量被设置了display,可以使用:

1
2
3
4
5
(gdb)info display
Auto-display expressions now in effect:
Num Enb Expression
1: y b
2: y e

想要清除可以使用:

1
2
3
delete display num # num为前面变量前的编号,不带num时清除所有。
# 或者
disable display num

查看寄存器内容

1
2
3
4
5
6
7
8
(gdb)info registers
rax 0x0 0
rbx 0x0 0
rcx 0x7ffff7dd1b00 140737351850752
rdx 0x0 0
rsi 0x7ffff7dd1b30 140737351850800
rdi 0xffffffff 4294967295
rbp 0x7fffffffdc10 0x7fffffffdc10

单步调试

单步执行-next

不会进入到函数内部。

next命令(可简写为n)用于在程序断住后,继续执行下一条语句,假设已经启动调试,并在第12行停住,如果要继续执行,则使用n执行下一条语句,如果后面跟上数字num,则表示执行该命令num次,就达到继续执行n行的效果了。

单步进入-step

跟踪函数内部的情况,可以使用step命令(可简写为s),它可以单步跟踪到函数内部,但前提是该函数有调试信息并且有源码信息。

s命令会尝试进入函数,但是如果没有该函数源码,需要跳过该函数执行,可使用finish命令,继续后面的执行。如果没有函数调用,s的作用与n的作用并无差别,仅仅是继续执行下一行。它后面也可以跟数字,表明要执行的次数。

当然它还有一个选项,用来设置当遇到没有调试信息的函数,s命令是否跳过该函数,而执行后面的。默认情况下,它是会跳过的,即step-mode值是off:

1
2
3
4
(gdb) show step-mode 
Mode of the step operation is off.
(gdb) set step-mode on
(gdb) set step-mode off

还有一个与step相关的命令是stepi(可简写为si),它与step不同的是,每次执行一条机器指令:

继续执行到下一个断点-continue

我们可能打了多处断点,或者断点打在循环内,这个时候,想跳过这个断点,甚至跳过多次断点继续执行该怎么做呢?可以使用continue命令(可简写为c)或者fg,它会继续执行程序,直到再次遇到断点处。

继续运行到指定位置-until

假如我们在25行停住了,现在想要运行到29行停住,就可以使用until命令(可简写为u):

1
2
3
4
5
6
7
8
$ gdb gdbStep
(gdb)b 25
(gdb)run
(gdb) u 29
it will calc a + b
3 + 7 = 10
main () at gdbStep.c:29
29 count(c);

跳过执行–skip

skip可以在step时跳过一些不想关注的函数或者某个文件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ gdb gdbStep
(gdb) b 27
Breakpoint 1 at 0x4005e4: file gdbStep.c, line 27.
(gdb) skip function add #step时跳过add函数
Function add will be skipped when stepping.
(gdb) info skip #查看step情况
Num Type Enb What
1 function y add
(gdb) run
Starting program: /home/hyb/workspaces/gdb/gdbStep
it will calc a + b

Breakpoint 1, main () at gdbStep.c:27
27 int c = add(a,b);
(gdb) s
28 printf("%d + %d = %d\n",a,b,c);
(gdb)

可以看到,再使用skip之后,使用step将不会进入add函数。
step也后面也可以跟文件:

1
(gdb)skip file gdbStep.c

其他相关命令:

  • skip delete [num] 删除skip
  • skip enable [num] 使能skip
  • skip disable [num] 去使能skip

其中num是前面通过info skip看到的num值,上面可以带或不带该值,如果不带num,则针对所有skip,如果带上了,则只针对某一个skip。