GDB简单实用教程
GDB是G++附带的调试工具,提供了设置断点、打印变量、单步调试等功能,可以让你在调试程序时事半功倍…
GDB可以
- 启动你的程序,可以按照你的定制要求随心所欲的运行程序。
- 可让被调试的程序在你所指定的调置的断点处停住。
- 当程序被停住时,可以检查此时你的程序中所发生的事,以及内存状态等。
- 动态的改变你程序的执行环境。
当然,GDB不是这个东西:
(图形化的GDB缺少一些必要的回显,例如函数中断点不会显示当前函数名和参数,有些时候很麻烦)。。。
Windows下GDB使用
一般情况下GDB在Windows系统下会在MinGW或MinGW64文件夹下的bin目录中,如图:
双击即可开始运行,如果你想要用命令也可以cd
到这个目录然后.\gdb
(注:笔者使用的是CMDer,与普通CMD效果可能略有出入)
GDB使用的前置条件是你的程序在编译时包含了调试信息,如果你在使用命令行编译,那么请在指令的后面加入-g
;如果你在使用IDE的话,也需要将IDE中的编译指令加入-g
,如果你的IDE中有默认的“Debug”属性的话,那么用默认的就行:
在加入-g
时,值得注意的时-g
一共有4个等级:
-g0
等于不加-g。即不包含任何信息-g1
只包含最小信息,一般来说只有你不需要debug,只需要backtrace信息,并且真的很在意程序大小,或者有其他保密/特殊需求时才会使用-g1。-g2
为gdb默认等级,包含绝大多数你需要的信息。-g3
包含一些额外信息,例如包含宏定义信息。当你需要调试宏定义时,请使用-g3
进入gdb后,输入file {你的程序}
。然后使用set args {arg1} {arg2} … {argN}
设定好你的程序参数,再运行run
.
GDB指令
help指令很强大!多用help!help里面总会有你需要的信息。如果你不知道如何使用help,请在gdb里面输入:help all
这里挑选一些常用的指令给大家说明一下:(指令需要在控制权在GDB下时(如遇到断点)
backtrace
显示栈信息。简写为bt
;frame x
切换到栈的第x层。其中x会在bt
命令中显示,从0开始。0表示栈顶。简写为f
;up/down x
往栈顶/栈底移动x帧。当不输入x时,默认为1;print x
打印x的信息,x可以是变量,也可以是对象或者数组。简写为p
;print */&x
打印x的内容/地址(就相当于C++的*
和&
运算符);call
调用函数。注意此命令需要一个正在运行的程序;break x
在的第x行设置断点,然后gdb会给出断点编号y。简写为b
。后面会对断点进行更详细的解释;command m
设置程序执行到断点m时要看的内容,例如:1
2
3
4
5
6
7command n
>printf "x is %d\n",x
>c
>end如果command后面没有参数n,则命令被赋给最后一个breakpoint,这其实是说break和command连在一起用,在脚本里用就非常方便了;
continue
继续运行程序。进入调试模式后,若你已经获取了你需要的信息或者需要程序继续运行时使用。可简写为c
until
执行到当前循环完成。可简写为u
;step
单步调试,步入当前函数。可简写为s
;next
单步调试,步过当前函数。可简写为n
;finish
执行到当前函数返回;set var x=10
改变当前变量x的值。也可以这样用:set {int}0x83040 = 10把内存地址0x83040的值强制转换为int并赋值为10(慎用);info locals
打印当前栈帧的本地变量;jump
使当前执行的程序跳转到某一行;return
强制函数返回。可以指定返回值;display x
设置对表达式的展示,表达式x的值将在每次控制权转移到GDB下时打印一次;delete y
删除编号为y的断点,简写为d
;
GDB点位:
**监视点(watchpoint)**。监视点是监视内存中某个地址,当该地址的数据被改变(或者被读取)时,程序交出控制权进入调试器。注意监视点分为软件模式和硬件模式:GDB 使用软件监视点的方式是在单步执行你的程序的同时测试变量的值,所以执行程序的速度会变慢。同时,软件监视点仅在当前线程有效。幸运的是,32 位的 Intel x86 处理器提供了 4 个特殊的调试寄存器用来方便调试程序,GDB 可以使用这些寄存器建立硬件监视点。GDB 总是会优先使用硬件监视点,因为这样不会减慢程序的执行速度。然而,可用的(enable的)硬件监视点的个数是有限的。如果你设置了过多的硬件监视点,当程序从中断的状态变为执行的状态(例如continue,until或者finish)时,GDB 可能无法把它们全部激活。另外,活动的硬件监视点的数量只有在试图继续执行程序时才能知道,也就是说,即使你设置了过多的硬件监视点,gdb在你运行程序之前也不会警告你。
设置监视点的命令有3个:
- watch(写监视)
- rwatch(读监视)
- awatch(读写监视)
他们的使用方法一样,皆为以下几种:
- (r/a)watch x x是一个变量名。当x的值改变/被读取时,程序交出控制权进入调试器。
- (r/a)watch 0xN N为一个有效地址。当该地址的内容变化/被读取时,程序交出控制权进入调试器。
- (r/a)watch *(int *)0xN N为一个有效地址。当该地址的中的int指针指向的内容变化/被读取时,程序交出控制权进入调试器。
- (r/a)watch -l *(int *)0xN N为一个有效地址。当该地址的中的int指针指向的内容变化/被读取,或者该地址的内容变化/被读取时,程序交出控制权进入调试器。
注意3和4的区别在于,当加入-l选项后,会同时监视表达式本身以及表达式指向的内容。
**断点(breakpoint)**。 断点是指当执行到程序某一步时,程序交出控制权进入调试器。值得注意的是,break会有一些变体:tbreak,hbreak,thbreak与rbreak。tbreak与break功能相同,只是所设置的断点在触发一次后自动删除。hbreak是一个硬件断点。thbreak则既是一个临时的硬件断点。注意硬件断点需要硬件支持,某些硬件可能不支持这种类型的断点。rbreak稍微特殊一些,它会在匹配正则表达式的全部位置加上断点。break家族的使用方法如下:
- (t/h)break x.cpp:y 。在代码x.cpp的第y行加入断点。x.cpp若不指定,则会以当前执行的文件作为断点文件。若程序未执行,则以包含main函数的源代码文件作为断点文件。若x.cpp和y都不指定,则以当前debugger的点作为断点处。
- (t/h)break 0xN。在地址N处加入断点。N必须为一个有效的代码段(code segment)地址。
- (t/h)break x.cpp:func。在x.cpp的func函数入口处加入断点。x.cpp可以不提供直接使用break func。注意由于重载(overload)的存在,因此gdb可能会询问你希望在哪个函数加上断点。你也可以通过指定参数类型来避免该问题,例如break func(int ,char *)
- (t/h)break +/-N。在当前运行处的第N行后/前加入断点。
- rbreak REGEXP。 在所有符合正则表达式REGEXP的函数入口加入断点。例如rbeak EX_* 表示在所有符合以EX_开头的函数入口处加入断点。
注意break后面还有2个可选参数,线程id和条件。线程id指在info threads中的线程序号,而非系统提供的tid。例如break x.cpp:y 2 if (a==24),表示在2号线程的x.cpp的第y行加入断点,并且只有当a的值为24时,程序才会交出控制权进入调试器。
另外,breakpoints可以通过save命令保存,以方便使用者下次再次进入程序调试时不需要重设断点。
**捕捉点(catchpoint)**。捕捉点是当某些事件发生时,程序交出控制权进入调试器。例如catch一个exception,assert,signal,fork甚至syscall。tcatch与catch功能一样,只是所设置的捕捉点在触发一次后自动删除。
**跟踪点(tracepoint)**。跟踪点与上面三个断点不同之处在于,它只是跟踪记录信息而不会中断程序的运行。当你的程序是realtime程序(如GUI),或者与其他的程序有交互时,你可能会希望使用跟踪点达到监视程序而又不破坏程序自身行为的目的。与断点相同的是,跟踪点会保存下在跟踪点时的一些内存信息供使用者查阅,例如数组或者对象。
另外,tracepoints可以通过save命令保存,以方便使用者下次再次进入程序调试时不需要重设这些跟踪点。
**检查点(checkpoint)**:
gdb可以保存某一个时间点的程序状态或者说是程序映像,并且稍后又可以返回到这个状态。这个称之为checkpoint(就如同你玩游戏时的检查点。。。)。
每个检查点是进程的一个拷贝。这样当一个bug很难重现,而又担心调试过头了又要从头开始重现时,可以在估计要重现这个bug之前,做一个checkpoint,这样即使debug过头了,也可以从这个checkpoint开始,而不用重启整个程序并且期待它重现这个bug(也许需要很久!!)。
但是每个checkpoint有一个唯一的进程id,这个pid与原始程序的pid不同,因此如果程序需要使用pid的信息时,需要慎重考虑。
如果你觉得这篇教程很好的话,请将其分享出去!