使用GDB调试Linux软件

来源:互联网 发布:可以手绘的软件 编辑:程序博客网 时间:2024/04/27 12:21
<script type="text/javascript">google_ad_client = "pub-8800625213955058";/* 336x280, 创建于 07-11-21 */google_ad_slot = "0989131976";google_ad_width = 336;google_ad_height = 280;//</script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>

使用GDB调试Linux软件
GNU调试器介绍


作者:David Seager 写作时间:01 Feb 2001
翻译:王锐 翻译时间:5 November 2005


绝大部分Linux爱好者在shell中使用GNU调试器或者叫做gdb。Gdb让您能够看到一个程序的内部结构,指出变量的值,设置断点并在源码中进行单步执行。它是一个解决代码中的问题的非常好的工具。在这篇文章中,我将告诉您这个工具有多么酷和多么有用。
编译
在您开始之前,您要调试的程序必须先进行编译,并把调试信息编译进去。这样gdb才能找到变量、代码行和函数。要做到这一点,只要在使用gcc(或g )编译的时候额外加一个“-g”的属性即可,如:
gcc -g eg.c -o eg


运行gdb
Gdb从shell运行,使用命令“gdb”,后面跟上程序名字作为参数,例如“gdb eg”,或者您可以使用file命令加载一个程序用于调试,如“file eg”。这两种方法都假定您是在程序所在的文件下执行命令的。一旦加载成功,就可以在gdb中使用“run”命令来启动程序了。


调试例子
如果没有任何错误您的程序会完成执行,在某个点上gdb可以获得控制权。但是如果出错了呢?这种情况下gdb会取得控制权并中断程序,允许您检查每一样东西的状态并有希望找到原因。为了表现这一场景,我们使用一个例子程序:

#include
int wib(int no1, int no2)
{
int result, diff;
diff = no1 - no2;
result = no1 / diff;
return result;
}
int main(int argc, char *argv[])
{
int value, div, result, i, total;
value = 10;
div = 6;
total = 0;
for(i = 0; i < 10; i )
{
result = wib(value, div);
total = result;
div ;
value--;
}
printf("%d wibed by %d equals %d/n", value, div, total);
return 0;
}

这个程序运行10次循环,使用函数wib()计算一个累加值,最后打印出结果。
把这段程序输入到您最喜欢使用的文本编辑器中,注意空行数量也要一样多,存为“eg1.c”,使用“gcc -g eg1.c -o eg1”编译并使用“gdb eg1”启动gdb。使用“run”运行这个程序会导致输出如下消息:

Program received signal SIGFPE, Arithmetic exception.
0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7 result = no1 / diff;
(gdb)

Gdb指示程序在第7行获得一个算术异常并打印出了行号和函数wib()的参数值,这些值非常有用。使用命令“list”看看第7行周围的源代码,“list”命令每次显示10行代码。再打一次“list”(或按下回车键,回车键表示重复前一次的命令)会把下一个10行代码列出来。从gdb消息中可以看出在第7行进行变量“diff”除变量“no1”运算的时候出错了。
使用“print”命令再加上变量名称看看变量的值。我们可以通过输入“print no1”和“print diff”查看“no1”和“diff”的值,结果如下:

(gdb) print no1
$5 = 8
(gdb) print diff
$2 = 0

Gdb显示“no1”等于8而“diff”等于0。根据这些值和第7行代码我们可以推出这个算术异常是由于除0引起的。列表显示变量“diff”在第6行计算出来,我们可以计算一下这个值,使用“print no1-no2”。Gdb告诉我们wib函数的两个参数都是8,那么我们可能想要检查调用了wib()函数的main()函数以看看这件事是什么时候发生的。我们还可以允许我们的程序自然死亡,只要告诉gdb让程序继续执行就可以了,使用命令“continue”。

(gdb) continue
Continuing.
Program terminated with signal SIGFPE, Arithmetic exception.
The program no longer exists.

使用断点
为了看到main()函数中在发生什么,我们可以在特定的行上或程序代码的一个函数上设置断点,gdb会在执行到那里时中断。我们可以通过“break main”在main()函数中设置一个断点,或者指定任何一个我们感兴趣的函数名字。为了达到我们的目的,我们要使程序在执行到调用wib()函数前中断。使用“list main”命令会打印出从main()函数开始的源代码,再打一个回车键会在第21行看到对函数wib()的调用。我们使用“break 21”在第21行设置一个断点。Gdb会有如下响应:

(gdb) break 21
Breakpoint 1 at 0x8048428: file eg1.c, line 21.

这表示它已经在我们请求的行设置了一个断点。“run”命令会使程序执行到断点处返回。gdb会产生一个消息显示它在哪个断点处中断并且此处是程序中的哪个位置:

Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:21
21 result = wib(value, div);

使用“print value”和“print div”会显示对于第一次wib()调用来说变量是10和6,使用“print i”会显示0。您会很高兴地看到gdb显示所有变量只需使用“info locals”,为您省了许多次敲击键盘的动作。
从前面的研究知道问题发生在“value”和“div”相等的时候,所以使用“continue”继续执行直到再一次到达断点1。对于这次迭代“info locals”显示value等于9,div等于7。
我们可以不使用continue而使用“next”,这个命令可以单步执行程序,这样我们可以看看“value”和“div”是怎样改变的。Gdb会有如下响应:

(gdb) next
22 total = result;

多次使用会车键会显示一个加和一个减,如下:

(gdb)
23 div ;
(gdb)
24 value--;

另外两个回车把我们带到21行,准备调用wib()。“info locals”显示现在“div”等于“value”,预示着即将到来的问题。如果有兴趣还可以通过单步执行进入到wib()函数内部再次看看这个除法错误,使用“step”命令就可以了(与“step”相对的是“next”命令,这个命令会跳过函数调用),接下来使用“next”获得一个“result”的计算值。
既然我们已经完成了调试,可以通过“quit”命令退出gdb。因为程序还在运行,这个动作会终止程序,gdb会有提示以确认您想要终止。
更多断点和检查点
在上面的例子中我们在21行设置了一个断点,因为我们对调用wib()函数前的“value”值什么时候等于“div”值感兴趣。我们要连续执行程序两次才能到达这个点,然而通过给断点设置条件我们可以使gdb只在“value”确实等于“div”的时候中止。在定义断点的时候设置条件,我们可以指定“break <line number> if <conditional expression>”。把eg1重新载入gdb并使用如下命令:

(gdb) break 21 if value==div
Breakpoint 1 at 0x8048428: file eg1.c, line 21.

如果一个断点如1号断点已经在21行定义好了,我们可以使用“condition”命令在这个断点上设置条件:

(gdb) condition 1 value==div

使用“run”运行eg1.c,gdb会在“value”等于“div”时中断,避免了手工打印“continue”直到它们相等为止。当调试C程序时断点条件可以是任何合法的C表达式,事实上您用哪种语言哪种语言的合法表达式就可以使用。在条件中指定的变量必须在您设置断点的行的变量作用域范围内,否则这个表达式就没有意义。
可以使用“condition”命令指定一个断点断点但是不加表达式从而使这个断点成为非条件型表达式。例如“condition 1”设置断点1为非条件型。
要看看当前定义了哪些断点以及它们的条件,使用命令“info break”:

(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048428 in main at eg1.c:21
stop only if value == div
breakpoint already hit 1 time

要满足什么条件,多少次命中,以及这个断点是否是可用的在“Enb”列显示出来。可以使用命令“disable <breakpoint number>”把断点置为不可用,使用“enable <breakpoint number>”置为可用或者使用“delete <breakpoint number>”整个删除,例如“disable 1”阻止了在第1号断点上的中断。
如果我们对于什么时候“value”等于“div”更感兴趣,我们可以设置一个不同类型的断点,叫做检查点(watch)。一个检查点会在特定的表达式改变了值时中断程序执行,但是它必须是在表达式中的变量在作用范围之内时设置。要在作用范围内获得“value”和“div”,我们可以在main()函数上设置一个断点并且运行程序,当main()断点命中后设置我们的检查点。重启启动gdb并载入eg1,使用如下命令:

(gdb) break main
Breakpoint 1 at 0x8048402: file eg1.c, line 15.
(gdb) run
...
Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:15
15 value = 10;

当“div”改变时要保持跟踪,我们可以使用“watch div”,但是因为我们想要“div”等于“value”时中断,所以使用如下命令:

(gdb) watch div==value
Hardware watchpoint 2: div == value

当“div==value”从0(false)到1(true)变化时继续执行会导致Gdb中断:

(gdb) continue
Continuing.
Hardware watchpoint 2: div == value
Old value = 0
New value = 1
main (argc=1, argv=0xbffff954) at eg1.c:19
19 for(i = 0; i < 10; i )

“info locals”命令显示“value”确实等于“div”,并且都是8。
使用“info watch”(这个命令等同于“info break”)列出定义好的检查点,检查点可以使用和断点同样的语法变为可用状态、不可用状态和删除。
核心文件
在gdb下运行程序使得查找bug更加容易,但是在调试器之外程序经查会死掉,只留下一个核心文件。Gdb可以载入核心文件并让您可以在它死掉前检查程序状态。
在gdb外运行我们的例子程序eg1会导致内核崩溃:

$ ./eg1
Floating point exception (core dumped)

要带着一个核心文件启动gdb,通过shell使用命令“gdb eg1 core”或者“gdb eg1 -c core”。Gdb会载入核心文件,eg1的程序列表,显示程序怎样终止的并提供一条消息,非常像我们之前在gdb下运行程序那样。

...
Core was generated by `./eg1'.
Program terminated with signal 8, Floating point exception.
...
#0 0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7 result = no1 / diff;

这时我们可以使用“info locals”、“print”、“info args”和“list”查看由除0导致的值。命令“info variables”会打印出所有程序变量的值,但是要费很长时间因为gdb从C库开始打印变量而不是从我们的程序代码开始。为了更容易地发现在叫做“wib()”函数中发生了什么,我们可以使用gdb的栈命令。
栈跟踪
程序“call stack”是一个函数列表。每个函数和它的变量都指定了一个“frame”,而最近调用的函数则在0号frame中(叫做底层frame)。要打印栈,使用命令“bt”(是“backtrace”的缩写)。

(gdb) bt
#0 0x80483ea in wib (no1=8, no2=8) at eg1.c:7
#1 0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21

这显示了函数wib()是由main()在21行调用的(立即使用“list 21”可以证明这一点),wib()在0号frame中,而main()在1号frame中。因为wib()在0号frame中,这是程序内执行到发生算术错误的时候所在的函数。
当您使用“info locals”命令时gdb打印出当前frame的局部变量,默认情况下是中断的函数(0号frame)。当前的frame使用命令“frame”打印出来。要查看main函数中的变量(在1号frame中),我们可以通过命令“frame 1”转到1号frame下,然后再使用“info locals”:

(gdb) frame 1
#1 0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21
21 result = wib(value, div);
(gdb) info locals
value = 8
div = 8
result = 4
i = 2
total = 6

这显示了错误发生在第三次循环(i等于2)时,此时“value”等于“div”。
可以通过显式指定frame号或命令“frame up”或“frame down”来转换frame,使用“frame up”下上移,使用“frame down”向下移。要获得更多关于一个frame的信息诸如它的地址和程序语言,您可以使用命令“info frame”。
Gdb栈命令在程序执行时起作用,就像核心文件一样,所以对于复杂的程序您可以在它运行时跟踪到这个程序是怎样进入函数的。
绑定到其它进程上
为了调试核心文件或程序,gdb可以绑定到一个正在运行的进程上(这个进程的程序必须编译进了调试信息)并进入到进程内。这是通过指定您想要绑定gdb的程序进程ID而不是核心文件名实现的,这里有个例子程序,这个例子程序会进行循环并休眠:

#include
int main(int argc, char *argv[])
{
int i;
for(i = 0; i < 60; i )
{
sleep(1);
}
return 0;
}

使用“gcc -g eg2.c -o eg2”编译并使用“./eg2 &”运行它。当它在后台运行起来的时候把进程ID的信息找到,在这里是1283:

./eg2 &
[3] 1283

启动gdb并指定您的pid,在这个例子中使用“gdb eg2 1283”。Gdb会寻找一个叫做“1283”的核心文件并且当没有找到它时会绑定并进入到1283进程中,无论它在哪里运行(在这里可能是在sleep()中):

...
/home/seager/gdb/1283: No such file or directory.
Attaching to program: /home/seager/gdb/eg2, Pid 1283
...
0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
(gdb)

这里所有的常用gdb命令都可以用。我们可以使用“backtrace”查看我们程序所在位置与main()的关系以及main()的frame号是多少,然后转换到那个frame,并找出我们进入for循环多少次了:

(gdb) backtrace
#0 0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
#1 0x400a877d in __sleep (seconds=1) at ../sysdeps/unix/sysv/linux/sleep.c:78
#2 0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
(gdb) frame 2
#2 0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
7 sleep(1);
(gdb) print i
$1 = 50

当我们完成任务后可以使用“detach”命令离开而让进程继续执行或者使用“kill”命令终止这个进程。我们还可以在进程1283下绑定到eg2,首先使用“file eg2”载入文件,然后使用命令“attach 1283”绑定。
其它灵巧的跟踪
Gdb允许您在不退出调试环境的情况下运行shell命令,使用“shell [commandline]”实现,对于在调试时修改源代码是很有用的。
最后您可以在程序运行时修改变量值,使用“set”命令。再次在gdb下运行“eg1”,在第7行设置一个条件断点(这是计算result的地方),使用命令“break 7 if diff==0”并运行程序。当gdb中断执行后可以把“diff”置为非0以使程序能够运行完成:

Breakpoint 1, wib (no1=8, no2=8) at eg1.c:7
7 result = no1 / diff;
(gdb) print diff
$1 = 0
(gdb) set diff=1
(gdb) continue
Continuing.
0 wibed by 16 equals 10
Program exited normally.

结论
GNU调试器放到任何程序员的武器库中都是一个功能非常强大的工具。我只是讲述了它可以做的一小部分工作,要找到更多的话,我建议您阅读GNU调试器的手册。

原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 陶粒砖 陶粒回填 陶粒批发 陶粒生产厂家 陶粒回填厕所好处 陶粒图片 陶粒混凝土图片 广东陶粒厂 陶粒混凝土价格 陶粒的价格 陶粒工艺 长沙陶粒 陶粒有什么作用 陶粒多少钱 陶粒用途 贵阳陶粒 陶粒空心砌块 陶粒页岩 陶粒块 陶粒混凝土砖 陶粒板 陶粒滤料厂 陶粒土 填卫生间陶粒 页岩陶粒混凝土 陶粒机 广州陶粒厂 防城港陶粒 秦皇岛陶粒 水泥陶粒混凝土 陶粒怎么使用 陶粒混凝土配合比 南京陶粒价格 陶粒加气块 保温材料陶粒 陶粒滤料 广州番禺陶粒厂 鱼缸陶粒 湖南陶粒 卫生间陶粒价格 江门陶粒厂