使用GDB调试程序
GDB的主要功能
一般来说,GDB主要帮忙你完成下面四个方面的功能:
- 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序
- 可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
- 当程序被停住时,可以检查此时你的程序中所发生的事
- 动态的改变你程序的执行环境
如何使用GDB
一般来说 GDB 主要调试的是 C/C++ 的程序。要调试 C/C++ 的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器 cc/gcc/g++ 的 -g
参数可以做到这一点。如:
1 | $ cc -g hello.c -o hello |
启动 GDB 调试程序,一般有 3
种方式:
1 | $ gdb program |
如果你的程序编译时开启了优化选项,那么在用 GDB 调试被优化过的程序时,可能会发生某些变量不能访问,或是取值错误码的情况。因为优化程序会删改你的程序,整理你程序的语句顺序,剔除一些无意义的变量等,所以在 GDB 调试这种程序时,运行时的指令和你所编写指令就有不一样,也就会出现你所想象不到的结果。对付这种情况时,需要在编译程序时关闭编译优化。GCC 可以使用 -gstabs
选项来解决这个问题。
运行前可做的事情
程序的运行,你有可能需要设置下面四方面的事。
程序运行的参数
1
2set args # 可指定运行时参数。(如:set args 10 20 30 40 50)
show args # 命令可以查看设置好的运行参数。运行环境
1
2
3
4path <dir> # 可设定程序的运行路径。
show paths # 查看程序的运行路径。
set environment varname [=value] # 设置环境变量。如:set env USER=hchen
show environment [varname] # 查看环境变量工作目录
1
2cd <dir> # 相当于shell的cd命令
pwd # 显示当前的所在目录程序的输入输出
1
2
3info terminal # 显示你程序用到的终端的模式
run > outfile # 使用重定向控制程序输出
tty # 命令可以指写输入输出的终端设备。如:tty /dev/ttyb
命令行参数
有时我们调试的程序需要有命令行参数,其实有三种方式:
- 命令行参数(–args)
1
$ gdb --args ./test 1 2 3
- GDB命令设置参数
1
2$ gdb ./test
(gdb) set args 1 2 3 - 运行时指定参数
1
2$ gdb ./test
(gdb) run 1 2 3
常用命令概览
命令帮助文档的使用:
1 | (gdb) help |
常用命令的概览如下:
命令名称 | 命令缩写 | 命令说明 |
---|---|---|
run | r | 运行程序 |
continue | c | 让暂停的程序继续运行 |
next | n | 运行到一行 |
step | s | 如果有调用函数,进入调用的函数内部 |
util | u | 运行到指定行停下来 |
finish | fi | 结束当前调用函数,到上一层函数调用处 |
return | return | 结束当前调用函数并返回指定值,到上一层函数调用处 |
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 | * | 查看程序的命令行参数 |
设置断点
调试程序中,暂停程序运行是必须的,GDB 可以方便地暂停程序的运行。你可以设置程序的在哪行停住,在什么条件下停住,在收到什么信号时停往等等。以便于你查看运行时的变量,以及运行时的流程。当进程被 GDB 停住时,你可以使用 info program
来查看程序的是否在运行,进程号,被暂停的原因。
在 GDB 中,我们可以有以下几种暂停方式:断点BreakPoint
、观察点WatchPoint
、捕捉点CatchPoint
、信号Signals
、线程停止Thread Stops
。如果要恢复程序运行,可以使用 continue
命令。
我们用 break 命令来设置断点,有几种设置断点的方法:
break function
:在指定函数时停住。break linenum
:在指定行号停住。break +/-offset
:在当前行前面/后面的 offset 行停住。break filename:function
:在源文件 filename 的 function 函数的入口处停住break *address
:在程序运行的内存地址处停住。break ... if <condition>
:...
可以是上述的参数,condition
表示条件,在条件成立时停住。
条件断点
条件断点很简单,但在实际调试过程中很有用处,下边看下最简单的断点示例
1 | # 基本命令语法 |
由于 C 语言的字符串的条件判断方式不太一样,不能直接使用 ==
来判断。
1 | # 调用libc函数,加上强制类型转换,防止有些GDB不能识别 |
还可以使用 GDB 提供的条件判断方法
1 | (gdb) condition 4 $_streq(name, "Hello") |
常用的几个 GDB 内置方法:
$_memeq(buf1, buf2, length)
:内存对比$_regex(str, regex)
:正则匹配$_streq(str1, str2)
:字符串比较$_strlen(str)
:计算字符串长度
设置观察点
观察点一般来观察某个表达式 ( 变量也是一种表达式 ) 的值是否有变化了,如果有变化,马上停止程序,我们有下面的几种方法来设置观察点:
watch <expr>
:为表达式expr
设置一个观察点,一旦表达式值有变化,马上停止程序。rwatch <expr>
:当表达式expr
被读时,停住程序。awatch <expr>
:当表达式的值被读或被写时,停住程序。info watchpoints
:列出当前所设置了的所有观察点。
设置捕捉点
你可设置捕捉点来补捉程序运行时的一些事件。如:载入共享库(动态链接库)或是 C++ 的异常。设置捕捉点的格式为:
1 | catch <event> |
当 event 发生时,停住程序。event 可以是下面的内容:
throw
一个C++
抛出的异常。catch
一个C++
捕捉到的异常。exec
调用系统调用exec
时。fork
调用系统调用fork
时。vfork
调用系统调用vfork
时。load
或load <libname>
载入共享库时。unload
或unload <libname>
卸载共享库时。
停止点的维护
在 GDB 中,如果你觉得已定义好的停止点没有用了,你可以使用 delete、clear、disable、enable 这几个命令来进行维护。
clear
:清除所有已定义的停止点。clear <function>
/clear <filename:function>
:清除所有设置在函数上的停止点。clear <linenum>
/clear <filename:linenum>
:清除所有设置在指定行上的停止点。delete [breakpoints] [range...]
:删除指定的断点,breakpoints 为断点号。如果不指定断点号,则表示删除所有的断点。range
表示断点号的范围 (如;3-7)。disable [breakpoints] [range...]
:disable
所指定的停止点,breakpoints
为停止点号。如果什么都不指定,表示disable
所有的停止点。enable [breakpoints] [range...]
:enable
所指定的停止点,breakpoints
为停止点号。enable [breakpoints] once range...
:enable
所指定的停止点一次,当程序停止后,该停止点马上被 GDB 自动disable
。enable [breakpoints] delete range...
:enable
所指定的停止点一次,当程序停止后,该停止点马上被 GDB 自动删除。
停止条件维护
一般来说,为断点设置一个条件,我们使用 if
关键词,后面跟其断点条件。并且,条件设置好后,我们可以用 condition
命令来修改断点的条件。(只有 break 和 watch 命令支持 if
,catch 目前暂不支持 if
)。
condition <bnum> <expression>
:修改断点号为bnum
的停止条件为expression
。condition <bnum>
:清除断点号为bnum
的停止条件。ignore <bnum> <count>
:表示忽略断点号为bnum
的停止条件count
次。
为停止点设定运行命令
我们可以使用 GDB 提供的 commands
命令来设置停止点的运行命令。也就是说,当运行的程序在被停止住时,我们可以让其自动运行一些别的命令,这很有利行自动化调试。对基于 GDB 的自动化调试是一个强大的支持。
1 | (gdb) break foo if x>0 |
如果你要清除断点上的命令序列,那么只要简单的执行一下 commands
命令,并直接在打个 end
就行了。
恢复程序运行和单步调试
当程序被停住了,你可以用 continue
命令恢复程序的运行直到程序结束,或下一个断点到来。
continue [ignore-count]
:ignore-count
表示忽略其后的断点次数。step <count>
:单步跟踪,如果有函数调用,他会进入该函数。进入函数的前提是,此函数被编译有 debug 信息。后面可以加count
也可以不加,不加表示一条条地执行,加表示执行后面的count
条指令,然后再停住。next <count>
:同样单步跟踪,如果有函数调用,它不会进入该函数。set step-mode on/off
:打开step-mode
模式,于是,在进行单步跟踪时,程序不会因为没有 debug 信息而不停住。finish
:运行程序,直到当前函数完成返回。并打印函数返回时的堆栈地址和返回值及参数值等信息。until
:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。stepi / nexti
:单步跟踪一条机器指令!一条程序代码有可能由数条机器指令完成,stepi
和nexti
可以单步执行机器指令。
信号(Signals)
GDB 有能力在你调试程序的时候处理任何一种信号,你可以告诉 GDB 需要处理哪一种信号。你可以要求 GDB 收到你所指定的信号时,马上停住正在运行的程序,以供你进行调试。你可以用 GDB 的 handle
命令来完成这一功能。
1 | handle <signal> <keywords...> |
在GDB中定义一个信号处理,一旦被调试的程序接收到信号,运行程序马上会被 GDB 停住,以供调试。其 <keywords>
可以是以下几种关键字的一个或多个。
nostop
:当被调试的程序收到信号时,GDB 不会停住程序的运行,但会打出消息告诉你收到这种信号。stop
:当被调试的程序收到信号时,GDB 会停住你的程序。print
:当被调试的程序收到信号时,GDB 会显示出一条信息。noprint
:当被调试的程序收到信号时,GDB 不会告诉你收到信号的信息。pass
或noignore
:当被调试的程序收到信号时,GDB 不处理信号。这表示,GDB 会把这个信号交给被调试程序会处理。nopass
或ignore
:当被调试的程序收到信号时,GDB 不会让被调试程序来处理这个信号。info signals
或info handle
:查看有哪些信号在被 GDB 检测中。
线程(Thread Stops)
- 查看当前的线程信息
1
info thread
- 切换调试线程
1
thread <threadID>
- 线程设置断点
1
2
3
4
5
6# 为某个线程设置断点
break file.c:100 thread <threadID>
# 为所有线程设置断点(默认断点设置在所有线程上)
break file.c:100 thread all
# 设置断点时可以同时设置条件表达式
break file.c:100 thread <threadID> if (expression) - 多线程的运行控制。在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。
1
2
3set scheduler-locking off # 不锁定任何线程,所有线程都执行,这是默认值
set scheduler-locking on # 只有当前被调试线程执行
set scheduler-locking step # 在单步调试时,除了 next 过一个函数的情况以外,只有当前线程会执行。
当你的程序被 GDB 停住时,所有的运行线程都会被停住。这方便你你查看运行程序的总体情况。而在你恢复程序运行时,所有的线程也会被恢复运行。那怕是主进程在被单步调试时。
查看栈的信息
当程序被停住了,你需要做的第一件事就是查看程序是在哪里停住的。当你的程序调用了一个函数,函数的地址,函数参数,函数内的局部变量都会被压入 栈
中。你可以用 GDB 命令来查看当前的栈中的信息。
backtrace(简写:bt)
:打印当前的函数调用栈的所有信息。backtrace <n>
:表示只打印栈顶上 n 层的栈信息。backtrace <-n>
:表示只打印栈底下 n 层的栈信息。
如果你要查看某一层的信息,你需要在切换当前的栈,一般来说,程序停止时,最顶层的栈就是当前栈,如果你要查看栈下面层的详细信息,首先要做的是切换当前栈。
frame <n>
:n 是一个从 0 开始的整数,是栈中的层编号。up <n>
:表示向栈的上面移动 n 层,可以不打 n,表示向上移动一层。down <n>
:表示向栈的下面移动 n 层,可以不打 n,表示向下移动一层。frame
:会打印出这些信息:栈的层编号,当前的函数名,函数参数值,函数所在文件及行号,函数执行到的语句。info frame
:这个命令会打印出更为详细的当前栈层的信息。info args
:打印出当前函数的参数名及其值。info locals
:打印出当前函数中所有局部变量及其值。info catch
:打印出当前的函数中的异常处理信息。
查看运行时数据
在你调试程序时,当程序被停住时,你可以使用 print
命令来查看当前程序的运行数据。print
命令的格式是:
1 | print <expr> |
<expr>
是表达式,是你所调试的程序的语言的表达式(GDB可以调试多种编程语言),<f>
是输出的格式,比如,如果要把表达式按 16 进制的格式输出,那么就是 /x
。
一、表达式
GDB 会根据当前的程序运行的数据来计算这个表达式,既然是表达式,那么就可以是当前程序运行中的 const 常量、变量、函数等内容。可惜的是 GDB 不能使用你在程序中所定义的宏。表达式的语法应该是当前所调试的语言的语法。
@
:是一个和数组有关的操作符,在后面会有更详细的说明。::
:指定一个在文件或是一个函数中的变量。{<type>} <addr>
:表示一个指向内存地址<addr>
的类型为type
的一个对象。
二、程序变量
在 GDB 中,你可以随时查看以下三种变量的值:
- 全局变量(所有文件可见的)
- 静态全局变量(当前文件可见的)
- 局部变量(当前 Scope 可见的)
用 print
显示出的变量的值会默认是函数中的局部变量的值,如果此时你想查看同名的全局变量的值时,你可以使用 ::
操作符
1 | file::variable |
当然,::
操作符会和 C++ 中的发生冲突,GDB 能自动识别 ::
是否 C++ 的操作符,所以你不必担心在调试 C++ 程序时会出现异常。
三、宏变量
在 GDB 中默认我们无法 print 宏定义,因为宏是预编译的。在 GCC 编译程序的时候,加上 -ggdb3
参数,就可以调试宏了。另外,你可以使用下述的 GDB 的宏调试命令来查看相关的宏。
1 | (gdb) info macro MAX_VALUE # 查看这个宏在哪些文件里被引用了,以及宏定义是什么样的 |
四、数组
有时候,你需要查看一段连续的内存空间的值。比如数组的一段,或是动态分配的数据的大小。你可以使用 GDB 的 @
操作符。
1 | int *array = (int *) malloc(len * sizeof(int)); |
在 GDB 调试过程中,你可以以如下命令显示出这个动态数组的取值:
1 | (gdb) p *array@len |
如果是静态数组的话,可以直接用 print 数组名
,就可以显示数组中所有数据的内容了。
五、输出格式
一般来说,GDB 会根据变量的类型输出变量的值。但你也可以自定义 GDB 的输出的格式。例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量的中的位的情况。要做到这样,你可以使用 GDB 的数据显示格式:
x
按十六进制格式显示变量。d
按十进制格式显示变量。u
按十六进制格式显示无符号整型。o
按八进制格式显示变量。t
按二进制格式显示变量。a
按十六进制格式显示变量。c
按字符格式显示变量。f
按浮点数格式显示变量。
1 | (gdb) p i |
p 命令总是需要变量名的,而 x 命令是用来查看内存的
1 | (gdb) x/x 0x7fffffffe524 # 以十六进制输出 |
六、查看内存
你可以使用 examine
命令来查看内存地址中的值。命令的语法如下所示:
1 | x/<n/f/u> <addr> # n、f、u是可选的参数。 |
n
是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。f
表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是 i。u
表示从当前地址往后请求的字节数,如果不指定的话,GDB 默认是 4 个 bytes。u 参数可以用下面的字符来代替,b 表示单字节,h 表示双字节,w 表示四字节,g 表示八字节。当我们指定了字节长度后,GDB 会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。<addr>
表示一个内存地址。
n/f/u
三个参数可以一起使用。例如命令:x/3uh 0x54320
表示,从内存地址 0x54320 读取内容,h 表示以双字节为一个单位,3 表示三个单位,u 表示按十六进制显示。
七、自动显示
你可以设置一些自动显示的变量,当程序停住时,或是在你单步跟踪时,这些变量会自动显示。相关的 GDB 命令是 display
。
1 | display <expr> |
expr
是一个表达式fmt
表示显示的格式addr
表示内存地址
当你用 display
设定好了一个或多个表达式后,只要你的程序被停下来,GDB 会自动显示你所设置的这些表达式的值。格式 i
和 s
同样被 display
支持,一个非常有用的命令是:
1 | display/i $pc |
$pc
是 GDB 的环境变量,表示着指令的地址,/i
则表示输出格式为机器指令码,也就是汇编。于是当程序停下后,就会出现源代码和机器指令码相对应的情形,这是一个很有意思的功能。下面是一些和 display
相关的 GDB 命令:
1 | undisplay <dnums...> |
删除自动显示,dnums
意为所设置好了的自动显式的编号。如果要同时删除几个,编号可以用空格分隔,如果要删除一个范围内的编号,可以用减号表示(如:2-5
)。
1 | disable display <dnums...> |
info display
查看display
设置的自动显示的信息。GDB 会打出一张表格,向你报告当然调试中设置了多少个自动显示设置,其中包括,设置的编号,表达式,是否 enable。
改变程序的执行
一旦使用GDB挂上被调试程序,当程序运行起来后,你可以根据自己的调试思路来动态地在GDB中更改当前被调试程序的运行线路或是其变量的值,这个强大的功能能够让你更好的调试你的程序,比如,你可以在程序的一次运行中走遍程序的所有分支。
一、修改变量值
修改被调试程序运行时的变量值,在 GDB 中很容易实现,使用 GDB 的 print 命令即可完成。如:
1 | (gdb) print x=4 |
x=4
这个表达式是 C/C++ 的语法,意为把变量 x 的值修改为 4。在某些时候,很有可能你的变量和 GDB 中的参数冲突,如:
1 | (gdb) whatis width |
因为,set width
是 GDB 的命令,所以,出现了 “Invalid syntax in expression” 的设置错误,此时,你可以使用 set var
命令来告诉 GDB,width 不是你 GDB 的参数,而是程序的变量名,如:
1 | (gdb) set var width=47 |
在 GDB 环境中自定义变量
1 | (gdb) set $i=0 |
另外,还可能有些情况,GDB 并不报告这种错误,所以保险起见,在你改变程序变量取值时,最好都使用 set var
格式的 GDB 命令。
二、跳转执行
一般来说,被调试程序会按照程序代码的运行顺序依次执行。GDB 提供了乱序执行的功能,也就是说,GDB 可以修改程序的执行顺序,可以让程序执行随意跳跃。这个功能可以由 GDB 的 jump
命令来完:
1 | jump <linespec> |
指定下一条语句的运行点。<linespce>
可以是文件的行号,可以是 file:line
格式,可以是 +num
这种偏移量格式。表式着下一条运行语句从哪里开始。
1 | jump <address> |
注意,jump
命令不会改变当前的程序栈中的内容,所以,当你从一个函数跳到另一个函数时,当函数运行完返回时进行弹栈操作时必然会发生错误,可能结果还是非常奇怪的,甚至于产生程序 Core Dump
。所以最好是同一个函数中进行跳转。
熟悉汇编的人都知道,程序运行时,有一个寄存器用于保存当前代码所在的内存地址。所以,jump
命令也就是改变了这个寄存器中的值。于是,你可以使用 set $pc
来更改跳转执行的地址。如:set $pc = 0x485
。
三、产生信号
使用 singal
命令,可以产生一个信号给被调试的程序。如:中断信号 Ctrl+C
。这非常方便于程序的调试,可以在程序运行的任意位置设置断点,并在该断点用 GDB 产生一个信号量,这种精确地在某处产生信号非常有利程序的调试。
语法是:signal <singal>
,UNIX 的系统信号量通常从 1 到 15。所以 <singal>
取值也在这个范围。single
命令和 shell 的 kill
命令不同,系统的 kill
命令发信号给被调试程序时,是由 GDB 截获的,而 single
命令所发出一信号则是直接发给被调试程序的。
四、强制函数返回
如果你的调试断点在某个函数中,并还有语句没有执行完。你可以使用 return
命令强制函数忽略还没有执行的语句并返回。
1 | return |
使用 return
命令取消当前函数的执行,并立即返回,如果指定了 <expression>
,那么该表达式的值会被认作函数的返回值。
五、强制调用函数
call <expr>
表达式中可以一是函数,以此达到强制调用函数的目的。并显示函数的返回值,如果函数返回值是 void
,那么就不显示。
另一个相似的命令也可以完成这一功能 print
,print
后面可以跟表达式,所以也可以用他来调用函数,print
和 call
的不同是,如果函数返回 void
,call
则不显示,print
则显示函数返回值,并把该值存入历史数据中。
GDB 其它技巧
- 使用
layout
调试代码。使用layout
可以实现一边看代码,一边调试,并且将窗口分开显示。主要使用方式如下:1
2
3(gdb) layout src # 进入 layout 模式
(gdb) focus next # 将光标定位到下边的调试窗口
(gdb) ctrl + x ; a # 退出 layout 模式:先按组合键 ctrl + x ,然后按键 a
注:转载自左耳朵耗子陈皓的 CSDN 系列文章《用GDB调试程序》