gdb基础

gdb

这篇文章基本上是摘自gdb手册,除此之外就是加了实际的代码样例,这样可以更清楚的看到一些命令的执行效果。当然,这儿不会涉及到所有的gdb命令,而只是一些常用的。

我这儿使用的gdb的版本是6.8。不过因为只是一些常用命令,因此在老版本上应该也没问题。操作系统是Windows XP。

一.概述

调试器的目的在于让你查看程序运行时的内部状态;或者在程序崩溃的时候,查看程序异常的原因。它们可以在一下几个方面提供帮助:

1)启动程序,按照你的意愿影响程序的行为

2)让程序在特定条件发生时停止运行

3)当程序停止的时候,检查程序正在做什么

4)改变程序的某些东西,这样就可以运行时修正bug,然后继续测试其他的问题。

gdb支持多种语言的调试,不过最成熟的应该就是c/c++了。这儿也以c++程序为例来说明。如果想知道其他语言的支持情况,可以看gdb手册。

二.gdb的起停

gdb最常用的启动方式就是:

gdb program

或者在程序异常终止的时候这样:

gdb program core

也或者,你可以attach到一个已经运行的进程来调试它,就像这样

gdb program PID

当然,也可以在命令行设置gdb的启动参数,例如args:

gdb —args gcc -O2 -c foo.c

这样就可以调试gcc,同时将参数"-O2 -c foo.c"传给gcc。

可以用gdb -help查看gdb的所有选项。

要想中止gdb的运行,可以输入quit或者q,也可以按ctrl-d组合键。

三.gdb命令

命令语法

gdb命令的形式为:command [arg1...argn]。每个命令单独一行。行的长度没有限制(当然,一般应该也不会有多长)。

许多gdb的命令都会有缩写的形式,例如break可以缩写为b。

如果直接按回车,gdb会执行刚刚被执行命令。

如果命令中有#字符,那么从#开始的字符都会被当成是注释。这个一般用在命令文件中。后面会有介绍。

命令补全

gdb具有补全功能。功能键是<TAB>。例如break,在输入bre之后按<TAB> 键,gdb就会补全为break。如果只输入b,然后按<TAB>,gdb会响一声,这说明有多个以b开始的命令。这种情况下再按一 次<TAB>,gdb就会把所有以b开始的命令输出出来。下面是此时的屏幕截图:

如果只是想看一下以某个(些)字符开始的命令,可以按<META>?,而不用按两次<TAB>。在没 有<META>键的电脑上,可以用<ESC>键代替。这个命令按起来有点麻烦,比按两次<TAB>要麻烦多了,如果 不怕<TAB>键被按坏,我建议你还是按<TAB>吧。

命令补全可以用于gdb命令,gdb子命令,程序中的符号名(例如函数名等)。

在调试c++程序时,基本上肯定会遇到的问题就是重载函数。例如,在设置断点的时候,假设有两个名为overload的函数,这时为了区分到底是那 个函数,就需要加上函数参数类型(函数名加上参数列表作为逻辑上的一个词)。为了使gdb能将参数列表两边的括号作为这个词的一部分,需要用单引号'将函 数整个的括起来。看下图:

获取帮助

启动gdb后,可以输入命令help得到gdb的命令列表。注意的是这时输出的每个条目都是一类命令。上个图:

得到命令类别后,可以用命令 help class得到此类别的所有命令。上图:

上面的图中只显示了部分breakpoints命令。

其他跟帮助相关的命令有:

a)help command

用于显示命令command的帮助信息。

b)apropos args

这里的args可以使正则表达式。显示所有匹配args的命令的简要说明。

c)complete args

显示所有以args开始的命令。

另外还有两个很有用的命令: infoshow

a)info

用于显示被调试程序的信息。例如传递到当前函数的参数-info args;或者查看当前寄存器的值-info registers;也可以查看断点-info breakpoints。可以用help info查看info的说明。

b)show

这个命令用于显示gdb的信息。也就是gdb的一些属性值。可以用help show查看帮助信息。这个命令通常应该是配合set用的(用于设定gdb的属性)。

好了,现在对gdb应该有个大概的认识了,下面我们就要拿几个小例子来验证一些常用命令的效果。Let's go!

四.示例1

首先说明一点,要想高效的使用gdb的功能,需要在编译程序的时候要加上-g选项,这个选项会把调试信息加到可执行文件中。

下面说一下示例文件,包括三个:gdb.h, gdb.cpp, test.cpp

gdb.h
#ifndef _GDB_H
#define _GDB_H 1

class gdb
{
public:
explicit gdb(int v);
void overload(int one);
void overload(int one, int two);
void catch_ex(int ex); //exception
void loop();
private:
int value;
int array[10];
};

#endif

gdb.cpp
#include "gdb.h"
#include <iostream>
using namespace std;

gdb::gdb(int v)
{
value = v;
for(int i=0; i<10; i++)
{
array[i] = i;
}
}

void gdb::overload(int one)
{
cout<<"function overload with one parameter: "<<one<<endl;
}

void gdb::overload(int one, int two)
{
cout<<"function overload with two paremeters: "<<one<<" "<<two<<endl;
}

void gdb::loop()
{
int loop_array[10];
for(int i=0; i<10; i++)
{
loop_array[i] = i;
}

int v=3;
v=3;
v=4;
}

void gdb::catch_ex(int ex)
{
int e = ex;
try
{
if(e <= 0)
{
throw e;
}
else
{
cout<<"function catch_ex: "<<ex<<endl;
}
}
catch(int x)
{
cout<<"exception: "<<x<<endl;
}
}
test.cpp
#include "gdb.h"

int main()
{
gdb g(5);
g.overload(1);
g.overload(1, 2);
g.loop();

g.catch_ex(3);
g.catch_ex(-1);

return 0;
}

好了,现在开始调试。首先启动gdb,指定要调试的可执行文件。前面已经说过了,可以简单地使用gdb program来启动。或者可以首先启动gdb,然后用file命令指定要调试的文件。下面是仅启动gdb后的画面:

现在用file命令指定要调试的文件:

然后就可以用 run 或者 r 命令来运行程序。在运行之前,你可能需要为程序设定一些信息,这些信息有一下四种:

1)程序参数

可以用set args命令设定程序的参数。设定完后可以用show args查看设置的是否正确。如果set args后面不带任何参数,则向程序传递的参数为空。

2)环境

这儿的环境就是在系统/用户配置文件中设置的环境变量,像HOME, PATH之类的.GDB提供了在调试的时候改变这些变量值的方式,这样当需要的时候就不用退出gdb来重新设置.GDB提供的命令有:

a) path directory -- 将 directory 加到环境变量PATH前面. 注意 对PATH的改变只对调试的程序有效, GDB使用的PATH不会有改变.1

b) show paths -- 显示PATH的值。

c) show environment [varname] -- 显示环境变量varname的值,如果不指定varname,则显示所有环境变量的值。

d) set environment varname [= value] -- 设置环境变量varname的值为value。这个改变只是对调试的程序生效。如果不提供value,则将varname的值置为空。

e) unset environment varname -- 从环境中移除传递给程序的变量varname。

3)工作目录

在启动gdb调试程序的时候,被调试的程序会从gdb继承工作目录。当然gdb也提供了命令来修改工作目录:

a) cd directory -- 将 directory 设为新的工作目录。

b) pwd -- 显示当前工作目录。

4)标准输入输出

还没找到在windows里面这个东西有啥用,现在也没有linux可用,不好多说。有需要的自己看gdb手册吧。我简单抄一下手册吧。

在gdb中,可以将run命令的输入输出重定向到文件或者其他终端。也可以通过tty命令设置被调试程序输入输出的设备。命令格式是:

tty terminal 或者 set inferior-tty terminal.

tty 就是 set inferior-tty 的别名。


咚咚咚咚,下面正式开始!

上面我们已经启动了程序, 也知道了如何运行程序。可是如果你直接执行run命令会发现,程序直接运行结束了。如果你想在某一行或者某个函数调用的地方,或者当某个变量/表达式的值改变的时候,也或者在某些事件发生的时候--例如抛出异常、加载动态库,或者创建子进程--的时候停止程序运行,那应该怎么办呢?

有了gdb,一切就都好办了:), 利用下面这三个强大的武器,你可以任意的停止程序。小心了,大家小心了,偶要祭出这三件宝物了,它们是:

断点

断点就是指定一个位置,使得程序运行到这个位置的时候会停下来(当然,还可以设置条件断点,当运行到指定位置时,只有满足了设置的条件,程序才会停下来),这样便于观察程序的内部状态。断点相关的命令主要有:

a)break location

在指定位置 location 处设置断点,这里的 location 可以是函数名,行号,指令地址等(关于如何指定 location ,可以看这里)。

b)break

如果不指定任何参数,break会在选定的栈帧的下一条指令处设置断点。

c)break ... if cond

设置条件断点。每次到达断点的时候都会对表达式 cond 求值,只有当结果为非0的时候程序才会在这个断点停下来。

d)tbreak args

设置一个只生效一次的断点。args跟break命令里的参数意义相同(也就是说,可以为location,为空,或者条件)。

e)hbreak args

设置硬件断点。

f)thbreak args

设置只生效一次的硬件断点。

g)rbreak regex

在所有匹配正则表达式 regex 的函数上设置断点。

h)info breakpoints [n]

i)info break [n]

j)info watchpoints [n]

上面三个命令都是列出当前的断点、观察点和捕捉点,如果指定参数n,则仅列出第n个的信息。

来试验一把吧。首先用gdb启动程序,假设我们想在test.cpp的 g.overload(1) 这一行添加一个断点,那就执行命令:b test.cpp:6,执行完后:

这时可以用info break等命令查看断点信息:

然后我们运行程序,看看有什么效果。

看到了吧,程序停在了断点所在的行。这时可以用where或者frame查看当前的栈帧信息,也可以用 print 查看一些变量或者表达式的信息,或者info args查看参数信息,等等。

其他的命令大家可以自己尝试一下。


有时候我们并不确定要在哪里加断点,例如当我们想在某个变量被改变或者被读、被写的时候让程序停下来,可能由于访问变量的地方比较多,要想每个地方都加上断点比较麻烦,而且很可能有遗漏,这时候我们就需要依赖另一个强大的命令了,也就是观察点。

观察点

观察点是一类特殊的断点,如果针对某个变量或者表达式指定一个观察点,那么当它们的值被读/写的时候,gdb会停止程序的执行。你不需要像设置断点时那样明确指定这个观察点在程序中的位置。观察点相关的命令有:

a)watch expr [thread threadnum]

expr 设置一个观察点。当 expr 的值被改变的时候,gdb会停止程序的运行。

如果指定了线程参数thread threadnum ,则 只有 在线程 threadnum 改变 expr 的值时,程序才会停止。

b)rwatch expr [thread threadnum]

expr 设置一个读观察点。当程序读 expr 的值时,gdb会停止程序的运行。

c)awatch expr [thread threadnum]

expr 设置一个访问观察点。当程序读或者写 expr 时,gdb会停止程序的运行。

d)info watchpoints

显示所有的断点、观察点、捕捉点。跟info break 相同。

下面再来看看观察点的使用。

首先我们设置一个断点在g.loop()这一行,然后运行到这里。step进入loop()函数。这一串命令的执行如下:

这时我们已经进入loop()函数,现在我们设置几个观察点。设置命令序列和设置完后的效果如小:

可以看到有5个停止点,其中前两个是我们设置的断点,后面三个是观察点。分别为watch, rwatch 和 awatch。另外还有一个地方就是,x86上默认是硬件观察点。

好了,来运行一下试试。

到达第一个观察点的时候程序停止运行,同时打印出了变量的旧值和新值,以及观察点的位置。

下面我们接着运行,看看遇到后面的观察点的时候又会怎样。

程序在第三个观察点停了下来(第二个观察点为读观察点),同样打印出了变量的新旧值和观察点的位置。第二个观察点由于是读观察点,而程序中没有读这个变量的地方,因此运行的时候被跳过了。为了看一下读观察点的效果,我们再设置一个读观察点:

i是循环内的迭代器,它会被复制给loop_array[i]变量,也就是会被读。可以看到,程序会停止运行,输出i的值。注意:每次对i的读操作都会使得程序停止。下面是两次执行continue命令后的输出:

观察点的内容差不多就这些了。另外有个需要注意的地方就是: watch命令设置的观察点只有在变量或者表达式的值被改变得时候才会使得程序停止运行,如果只是被写,但是值没有改变,则程序不会停止。


上面已经讲了断点、观察点,而对于某些情况,这两种停止点并不是最有效的方式。例如想在c++程序中跑出异常的时候停 止程序,这时候用断点就不够有效了,因为程序中可能好多异常处理的地方,如果一个个设置断点,那就太麻烦了(当然如果只处理某几个异常,用断点也无不可, 甚至用起来更灵活);而观察点就更不可用了。

这种情况就需要用到捕捉点了。

捕捉点

捕捉点也是一类特殊的断点,它可以使得程序在某种事件发生时停止运行,例如c++异常,或者加载动态库、创建子进程等。设置捕捉点的命令是catch.

catch event

其中 event 可以是:

a)throw

c++抛出异常。

b)catch

c++捕捉异常。

c)exception

Ada异常。

d)exception unhandled

程序中未处理的异常。

e)assert

失败的Ada断言。

f)exec

对exec的调用(只在HP-UX和GNU/Linux中可用)。

g)fork

对fork的调用(只在HP-UX和GNU/Linux中可用)。

h)vfork

对vfork的调用(只在HP-UX和GNU/Linux中可用)。

i)load

动态加载共享库(只在HP-UX中可用)。

j)load libname

动态加载共享库 libname (只在HP-UX中可用)。

k)unload

卸载已加载的共享库(只在HP-UX中可用)。

l)unload libname

卸载已加载的共享库 libname (只在HP-UX中可用)。

还有一个设置只生效一次的捕捉点的命令是: tcatch event

下面再看一下捕捉点的使用。

首先在vtest.cpp的 g.catch_ex(-1); 设置一个断点,然后运行,进入此函数。

现在我们设置一个捕捉点。继续运行:

可以看到程序在抛出异常的地方停止了。


删除断点

当断点不再需要了,那就应该删除掉,否则每次执行到断点的位置程序都要停下来,会把人逼疯的。删除断点的命令有两个:clear和delete。

a)clear [ location ]

如果不指定 location ,则删除选择的栈帧中下一条要执行的指令上的任何断点。如果选择的是最内部的栈帧(也就是当前正执行的函数的栈帧),则clear会将刚刚使程序停止的断点被删除。 关于 location 的说明可以看这里

b)delete [breakpoints] [ range ... ]

删除指定范围 range 那的所有的断点、观察点、捕捉点。如果不指定参数 range ,则会删除所有的停止点。这里的 range 指定的是断点编号区间。可以用info break查看断点信息。


禁用断点

如果不想删除断点,只是想暂时使它失效,则可以使用disable命令。disable命令的形式如下:

a)disable [breakpoints] [ range ...]

使指定区间 range 内的断点失效。如果不指定 range ,则所有的断点都失效。

使断点生效的命令是enable,形式有:

a)enable [breakpoints] [ range ...]

使指定区间 range 内的断点或者所有断点生效。

b)enable [breakpoints] once range...

使指定区间内的断点生效一次。

c)enable [breakpoints] delete range...

使指定区间内的断点生效一次,然后删除。


断点条件

断点条件使得只有在相应的条件满足时,断点才有效。这里的条件表达式跟程序所用语言的逻辑表达式的语法相同,例如在c/c++语言里,可以用 a==b 或者 x&&y这种表达式。

断点条件可以在设置断点的时候指定,也可以在断点设置后通过condition命令来设置或者改变。 condition的形式为:

a)condition bnum expression

设置表达式 expression 为停止点 bnum 的条件。

b)condition bnum

删除停止点 bnum 的条件。

还有一个命令,可以使得gdb忽略断点的条件一定的次数,其形式为:

a)ingore bnum count


指定位置

许多gdb命令都接受一个用于指定程序位置的参数。位置的指定方式有下面几种:

a) linenum

当前源文件的行号。

b) -offset

当前行前面,跟当前行间隔为 offset 的行。当前行可以这样确定:使用list命令,打印出来的最后一行就是当前行;或者对于断点命令,选定的栈帧中,程序停止执行的位置就是当前行。

c) +offset

当前行后面,跟当前行间隔为 offset 的行。

d) filename:linenum

源文件 filename 中的行 linenum

e) function

当前源文件中的函数 function

f) filename:function

源文件 filename 中的函数 function

g) * address

指定程序地址 address。常用的 address 形式有:

expression -- 当前语言中有效的表达式。

funcaddr -- 函数的地址。在c/c++中就是函数名。

'filename'::funcaddr -- 源文件 filename 中的函数地址 funcaddr


后面懒得写了,暂时先放一放。发现就算是抄文档,内容多了也是个很累人的活。唉,懒了,不行了...

待添加


数据

待添加


五 示例2

待添加


六 后记

这篇文章只是捡了GDB中最常用的一些东西,而且还只是最常用的东西中的一小部分,有兴趣或者需要的可以直接看GDB的文档,可以在这里找到。


1. 不知道是不是因为windows和linux系统的不同,用gdb启动程序后,执行show paths后输出:Executables and object file path: 。也就是说输出的值是空的。

posted on 2008-08-18 13:31 季阳 阅读(4878) 评论(1)  编辑 收藏 引用

评论

# re: gdb基础 2012-08-09 16:25 怡红公子

写的很好,找了很长时间,LZ加油啊!  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2012年8月>
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

导航

统计

常用链接

留言簿(2)

随笔档案(12)

搜索

最新随笔

最新评论

阅读排行榜

评论排行榜