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
不能调试。

可以调试

2. readelf 查看段信息
不能调试

可以调试

开始调试
调试无参程序
gdb file_name-
run(简写r)运行。
调试带参程序
gdb file_namerun 参数
或者
gdb file_nameset args 参数run或r


list(可简写为l),它可以将源码列出来。
调试 core 文件
当程序core dump时,可能会产生core文件,它能够很大程序帮助我们定位问题。前提是系统没有限制core文件的产生。
1 | ulimit -a # 查看 |

1 | core file size (blocks, -c) 0 |
取消系统限制:
1 | ulimit -c unlimied # 取消限制core文件大小。 |
调试 core dump 文件:
1 | gdb 程序文件名 core文件名 |
调试已运行程序
使用ps命令找到进程id
1 | ps -ef | grep 进程名 |
attach方式
假设获取到进程id为20829,则用下面的方式调试进程:
1 | gdb |
如果有下面的错误提示:
1 | Could not attach to process. If your uid matches the uid of the target |
解决方法,切换到root用户:
1 | /etc/sysctl.d/10-ptrace.conf |
直接调试相关id进程
1 | gdb program pid |
已运行程序没有调试信息
为了节省磁盘空间,已经运行的程序通常没有调试信息。但如果又不能停止当前程序重新启动调试,那怎么办呢?还有办法,那就是同样的代码,再编译出一个带调试信息的版本。然后使用和前面提到的方式操作。对于attach方式,在attach之前,使用file命令即可:
1 | gdb |
断点
查看断点
1 | info breakpoints |
设置断点
根据行号设置断点
1
2
3b 9 #break 可简写为b
或者
b test.c:9根据函数名设置断点
1
b fun_name
根据条件设置断点
1
2
3
4
5break test.c:23 if b==0
当在b等于0时,程序将会在第23行断住。
condition有着类似的作用,假设上面的断点号为1
condition 1 b==0
b等于0时,产生断点1。根据规则设置断点
1
2
3
4
5
6
7rbreak printNum*
对所有调用printNum函数都设置断点
下面是对所有函数设置断点
rbreak .
rbreak test.c:. # 对test.c中的所有函数设置断点
rbreak test.c:^print # 对以print开头的函数设置断点设置临时断点
1
2tbreak test.c:l0 # 在第10行设置临时断点
某处的断点只想生效一次,那么可以设置临时断点,这样断点后面就不复存在了跳过多次设置断点
1
2
3ignore next 30 hits
ignore 1 30
1是要忽略的断点号,可以通过前面的方式查找到,30是需要跳过的次数。这样设置之后,会跳过前面30次。根据表达式值变化产生断点
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 | disable # 禁用所有断点 |
清除断点
1 | clear # 删除当前行所有breakpoints |
查看变量
打印基本类型变量,数组,字符数组
print(可简写为p)
1 | (gdb) p a |
打印指针指向内容
1 | (gdb) p d |
$
1 | $可表示上一个变量,而假设此时有一个链表linkNode,它有next成员代表下一个节点,则可使用下面方式不断打印链表内容: |
按照特定格式打印变量
- x 按十六进制格式显示变量。
- d 按十进制格式显示变量。
- u 按十六进制格式显示无符号整型。
- o 按八进制格式显示变量。
- t 按二进制格式显示变量。
- a 按十六进制格式显示变量。
- c 按字符格式显示变量。
- f 按浮点数格式显示变量。
1 | (gdb) p c |
查看内存内容
examine(简写为x)可以用来查看内存地址中的值。语法如下:
1 | x/[n][f][u] addr |
- n 表示要显示的内存单元数,默认值为1
- f 表示要打印的格式,前面已经提到了格式控制字符
- u 要打印的单元长度
- addr 内存地址
单元类型常见有如下:
- b 字节
- h 半字,即双字节
- w 字,即四字节
- g 八字节
1 | 把float变量e按照二进制方式打印,并且打印单位是一字节: |
自动显示变量内容
假设希望程序断住时,就显示某个变量的值,可以使用display命令。
1 | (gdb) display e |
那么每次程序断住时,就会打印e的值。要查看哪些变量被设置了display,可以使用:
1 | (gdb)info display |
想要清除可以使用:
1 | delete display num # num为前面变量前的编号,不带num时清除所有。 |
查看寄存器内容
1 | (gdb)info registers |
单步调试
单步执行-next
不会进入到函数内部。
next命令(可简写为n)用于在程序断住后,继续执行下一条语句,假设已经启动调试,并在第12行停住,如果要继续执行,则使用n执行下一条语句,如果后面跟上数字num,则表示执行该命令num次,就达到继续执行n行的效果了。
单步进入-step
跟踪函数内部的情况,可以使用step命令(可简写为s),它可以单步跟踪到函数内部,但前提是该函数有调试信息并且有源码信息。
s命令会尝试进入函数,但是如果没有该函数源码,需要跳过该函数执行,可使用finish命令,继续后面的执行。如果没有函数调用,s的作用与n的作用并无差别,仅仅是继续执行下一行。它后面也可以跟数字,表明要执行的次数。
当然它还有一个选项,用来设置当遇到没有调试信息的函数,s命令是否跳过该函数,而执行后面的。默认情况下,它是会跳过的,即step-mode值是off:
1 | (gdb) show step-mode |
还有一个与step相关的命令是stepi(可简写为si),它与step不同的是,每次执行一条机器指令:
继续执行到下一个断点-continue
我们可能打了多处断点,或者断点打在循环内,这个时候,想跳过这个断点,甚至跳过多次断点继续执行该怎么做呢?可以使用continue命令(可简写为c)或者fg,它会继续执行程序,直到再次遇到断点处。
继续运行到指定位置-until
假如我们在25行停住了,现在想要运行到29行停住,就可以使用until命令(可简写为u):
1 | gdb gdbStep |
跳过执行–skip
skip可以在step时跳过一些不想关注的函数或者某个文件的代码:
1 | gdb gdbStep |
可以看到,再使用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。