程序员肖邦的博客 享受编程和技术所带来的快乐

使用GDB调试程序

2015-04-12
肖邦

使用 GDB 调试程序

GDB的主要功能

一般来说,GDB主要帮忙你完成下面四个方面的功能:

  • 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序
  • 可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
  • 当程序被停住时,可以检查此时你的程序中所发生的事
  • 动态的改变你程序的执行环境

如何使用GDB

一般来说 GDB 主要调试的是 C/C++ 的程序。要调试 C/C++ 的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器 cc/gcc/g++ 的 -g 参数可以做到这一点。如:

$ cc -g hello.c -o hello
$ g++ -g hello.cpp -o hello

启动 GDB 调试程序,一般有 3 种方式:

$ gdb program
# program 就是执行文件,一般在当前目录下

$ gdb attach pid
# 调试一个正在运行的进程

$ gdb program core
# 同时调试运行程序和 core 文件

如果你的程序编译时开启了优化选项,那么在用 GDB 调试被优化过的程序时,可能会发生某些变量不能访问,或是取值错误码的情况。因为优化程序会删改你的程序,整理你程序的语句顺序,剔除一些无意义的变量等,所以在 GDB 调试这种程序时,运行时的指令和你所编写指令就有不一样,也就会出现你所想象不到的结果。对付这种情况时,需要在编译程序时关闭编译优化。GCC 可以使用 -gstabs 选项来解决这个问题。

运行前可做的事情

程序的运行,你有可能需要设置下面四方面的事。

  • 程序运行的参数

    set args  可指定运行时参数。(如:set args 10 20 30 40 50)
    show args 命令可以查看设置好的运行参数。
    
  • 运行环境

    path <dir> 可设定程序的运行路径。
    show paths 查看程序的运行路径。
    set environment varname [=value] 设置环境变量。如:set env USER=hchen
    show environment [varname] 查看环境变量
    
  • 工作目录

    cd <dir> 相当于shell的cd命令
    pwd 显示当前的所在目录
    
  • 程序的输入输出

    info terminal 显示你程序用到的终端的模式
    使用重定向控制程序输出。如:run > outfile
    tty命令可以指写输入输出的终端设备。如:tty /dev/ttyb
    

常用命令概览

命令帮助文档的使用:

(gdb) help
# help 例出 gdb 的命令种类

(gdb) help breakpoints
# 可以使用 help <class> 命令,如 help breakpoints 查看断点所有命令

常用命令的概览如下:

命令名称 命令缩写 命令说明
run r 运行程序
continue c 让暂停的程序继续运行
next n 运行到一行
step s 如果有调用函数,进入调用的函数内部
util u 运行到指定行停下来
finish fi 结束当前调用函数,到上一层函数调用处
return return 结束当前调用函数并返回指定值,到上一层函数调用处
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 * 查看程序的命令行参数

设置断点

调试程序中,暂停程序运行是必须的,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表示条件,在条件成立时停住。

设置观察点

观察点一般来观察某个表达式 ( 变量也是一种表达式 ) 的值是否有变化了,如果有变化,马上停止程序,我们有下面的几种方法来设置观察点:

  • watch <expr> :为表达式 expr 设置一个观察点,一旦表达式值有变化,马上停止程序。
  • rwatch <expr> :当表达式 expr 被读时,停住程序。
  • awatch <expr> :当表达式的值被读或被写时,停住程序。
  • info watchpoints :列出当前所设置了的所有观察点。

设置捕捉点

你可设置捕捉点来补捉程序运行时的一些事件。如:载入共享库(动态链接库)或是 C++ 的异常。设置捕捉点的格式为:

catch <event>

当 event 发生时,停住程序。event 可以是下面的内容:

  • throw 一个 C++ 抛出的异常。
  • catch 一个 C++ 捕捉到的异常。
  • exec 调用系统调用 exec 时。
  • fork 调用系统调用 fork 时。
  • vfork 调用系统调用 vfork 时。
  • loadload <libname> 载入共享库时。
  • unloadunload <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 提供的 command 命令来设置停止点的运行命令。也就是说,当运行的程序在被停止住时,我们可以让其自动运行一些别的命令,这很有利行自动化调试。对基于 GDB 的自动化调试是一个强大的支持。

commands [bnum]
... command-list ...
end
# 为断点号 bnum 指写一个命令列表。当程序被该断点停住时,gdb 会依次运行命令列表中的命令。

# 例如
break foo if x>0
commands
printf "x is %d/n",x
continue
end

如果你要清除断点上的命令序列,那么只要简单的执行一下 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 :单步跟踪一条机器指令!一条程序代码有可能由数条机器指令完成,stepinexti 可以单步执行机器指令。

信号(Signals)

GDB 有能力在你调试程序的时候处理任何一种信号,你可以告诉 GDB 需要处理哪一种信号。你可以要求 GDB 收到你所指定的信号时,马上停住正在运行的程序,以供你进行调试。你可以用 GDB 的 handle 命令来完成这一功能。

handle <signal> <keywords...>

在GDB中定义一个信号处理,一旦被调试的程序接收到信号,运行程序马上会被 GDB 停住,以供调试。其 <keywords> 可以是以下几种关键字的一个或多个。

  • nostop :当被调试的程序收到信号时,GDB 不会停住程序的运行,但会打出消息告诉你收到这种信号。
  • stop :当被调试的程序收到信号时,GDB 会停住你的程序。
  • print :当被调试的程序收到信号时,GDB 会显示出一条信息。
  • noprint :当被调试的程序收到信号时,GDB 不会告诉你收到信号的信息。
  • passnoignore :当被调试的程序收到信号时,GDB 不处理信号。这表示,GDB 会把这个信号交给被调试程序会处理。
  • nopassignore :当被调试的程序收到信号时,GDB 不会让被调试程序来处理这个信号。
  • info signalsinfo handle :查看有哪些信号在被 GDB 检测中。

线程(Thread Stops)

如果你程序是多线程的话,你可以定义你的断点是否在所有的线程上,或是在某个特定的线程。GDB 很容易帮你完成这一工作。

break <linespec> thread <threadno>
break <linespec> thread <threadno> if ...

linespec 指定了断点设置在的源程序的行号。threadno 指定了线程的 ID,注意,这个 ID 是 GDB 分配的,你可以通过 info threads 命令来查看正在运行程序中的线程信息。如果你不指定 thread <threadno> 则表示你的断点设在所有线程上面。你还可以为某线程指定断点条件。如:

(gdb) break frik.c:13 thread 28 if bartab > lim

当你的程序被 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 命令的格式是:

print <expr>
print /<f> <expr>

<expr> 是表达式,是你所调试的程序的语言的表达式(GDB可以调试多种编程语言),<f> 是输出的格式,比如,如果要把表达式按 16 进制的格式输出,那么就是 /x

一、表达式

GDB 会根据当前的程序运行的数据来计算这个表达式,既然是表达式,那么就可以是当前程序运行中的 const 常量、变量、函数等内容。可惜的是 GDB 不能使用你在程序中所定义的宏。表达式的语法应该是当前所调试的语言的语法。

  • @ :是一个和数组有关的操作符,在后面会有更详细的说明。
  • :: :指定一个在文件或是一个函数中的变量。
  • {<type>} <addr> :表示一个指向内存地址 <addr> 的类型为 type 的一个对象。

二、程序变量

在 GDB 中,你可以随时查看以下三种变量的值:

  • 全局变量(所有文件可见的)
  • 静态全局变量(当前文件可见的)
  • 局部变量(当前 Scope 可见的)

print 显示出的变量的值会默认是函数中的局部变量的值,如果此时你想查看同名的全局变量的值时,你可以使用 :: 操作符

file::variable
function::variable

gdb) print 'f2.c'::x

当然,:: 操作符会和 C++ 中的发生冲突,GDB 能自动识别 :: 是否 C++ 的操作符,所以你不必担心在调试 C++ 程序时会出现异常。    

三、数组

有时候,你需要查看一段连续的内存空间的值。比如数组的一段,或是动态分配的数据的大小。你可以使用 GDB 的 @ 操作符。

int *array = (int *) malloc(len * sizeof(int));

在 GDB 调试过程中,你可以以如下命令显示出这个动态数组的取值:

(gdb) p *array@len
$1 = {2, 4, 6, 8, 10, 12, ... 34, 36, 38, 40}

如果是静态数组的话,可以直接用 print 数组名,就可以显示数组中所有数据的内容了。

四、输出格式

一般来说,GDB 会根据变量的类型输出变量的值。但你也可以自定义 GDB 的输出的格式。例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量的中的位的情况。要做到这样,你可以使用 GDB 的数据显示格式:

  • x  按十六进制格式显示变量。
  • d  按十进制格式显示变量。
  • u  按十六进制格式显示无符号整型。
  • o  按八进制格式显示变量。
  • t  按二进制格式显示变量。
  • a  按十六进制格式显示变量。
  • c  按字符格式显示变量。
  • f  按浮点数格式显示变量。
(gdb) p i
$21 = 101

(gdb) p/a i
$22 = 0x65

(gdb) p/c i
$23 = 101 'e'

(gdb) p/f i
$24 = 1.41531145e-43

(gdb) p/x i
$25 = 0x65

(gdb) p/t i
$26 = 1100101

五、查看内存

你可以使用 examine 命令来查看内存地址中的值。命令的语法如下所示:

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

display <expr> 
display/<fmt> <expr> 
display/<fmt> <addr>
  • expr 是一个表达式
  • fmt 表示显示的格式
  • addr 表示内存地址

当你用 display 设定好了一个或多个表达式后,只要你的程序被停下来,GDB 会自动显示你所设置的这些表达式的值。格式 is 同样被 display 支持,一个非常有用的命令是:

display/i $pc

$pc 是 GDB 的环境变量,表示着指令的地址,/i 则表示输出格式为机器指令码,也就是汇编。于是当程序停下后,就会出现源代码和机器指令码相对应的情形,这是一个很有意思的功能。下面是一些和 display 相关的 GDB 命令:

undisplay <dnums...>
delete display <dnums...>

删除自动显示,dnums 意为所设置好了的自动显式的编号。如果要同时删除几个,编号可以用空格分隔,如果要删除一个范围内的编号,可以用减号表示(如:2-5)。

disable display <dnums...>
enable display <dnums...>
# disable 和 enalbe 不删除自动显示的设置,而只是让其失效和恢复。
  • info display 查看 display 设置的自动显示的信息。GDB 会打出一张表格,向你报告当然调试中设置了多少个自动显示设置,其中包括,设置的编号,表达式,是否 enable。

改变程序的执行

一旦使用GDB挂上被调试程序,当程序运行起来后,你可以根据自己的调试思路来动态地在GDB中更改当前被调试程序的运行线路或是其变量的值,这个强大的功能能够让你更好的调试你的程序,比如,你可以在程序的一次运行中走遍程序的所有分支。

一、修改变量值

修改被调试程序运行时的变量值,在 GDB 中很容易实现,使用 GDB 的 print 命令即可完成。如:

(gdb) print x=4

x=4 这个表达式是 C/C++ 的语法,意为把变量 x 的值修改为 4。在某些时候,很有可能你的变量和 GDB 中的参数冲突,如:

(gdb) whatis width
      type = double
(gdb) p width
      $4 = 13
(gdb) set width=47
      Invalid syntax in expression.

因为,set width 是 GDB 的命令,所以,出现了 “Invalid syntax in expression” 的设置错误,此时,你可以使用 set var 命令来告诉 GDB,width 不是你 GDB 的参数,而是程序的变量名,如:

(gdb) set var width=47

另外,还可能有些情况,GDB 并不报告这种错误,所以保险起见,在你改变程序变量取值时,最好都使用 set var 格式的 GDB 命令。

二、跳转执行

一般来说,被调试程序会按照程序代码的运行顺序依次执行。GDB 提供了乱序执行的功能,也就是说,GDB 可以修改程序的执行顺序,可以让程序执行随意跳跃。这个功能可以由 GDB 的 jump 命令来完:

jump <linespec>

指定下一条语句的运行点。<linespce> 可以是文件的行号,可以是 file:line 格式,可以是 +num 这种偏移量格式。表式着下一条运行语句从哪里开始。

jump <address>
# 这里的 <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 命令强制函数忽略还没有执行的语句并返回。

return
return <expression>

使用 return 命令取消当前函数的执行,并立即返回,如果指定了 <expression>,那么该表达式的值会被认作函数的返回值。

五、强制调用函数

call <expr> 表达式中可以一是函数,以此达到强制调用函数的目的。并显示函数的返回值,如果函数返回值是 void,那么就不显示。

另一个相似的命令也可以完成这一功能 printprint 后面可以跟表达式,所以也可以用他来调用函数,printcall 的不同是,如果函数返回 voidcall 则不显示,print 则显示函数返回值,并把该值存入历史数据中。


Comments

Content