我已经教给你一些关于我的强大的调试宏的技巧,并且你已经开始用它们了。当我调试代码时,我使用debug()
宏,分析发生了什么以及跟踪问题。在这个练习中我打算教给你一些使用gdb的技巧,用于监视一个不会退出的简单程序。你会学到如何使用gdb附加到运行中的进程,并挂起它来观察发生了什么。在此之后我会给你一些用于gdb的小提示和小技巧。
我主要按照一种“科学方法”的方式来调试,我会提出可能的所有原因,之后排除它们或证明它们导致了缺陷。许多程序员拥有的问题是它们对解决bug的恐慌和急躁使他们觉得这种方法会“拖慢”他们。它们并没有注意到,它们已经失败了,并且在收集无用的信息。我发现日志(调试输出)会强迫我科学地解决bug,并且在更多情况下易于收集信息。
此外,使用调试输出来作为我的首要调试工具的理由如下:
debug("Yo, dis right? %d", my_stuff);
就没有那么麻烦。尽管所有原因显示我更倾向于debug
而不是gdb
,我还是在少数情况下回用到gdb
,并且我认为你应该选择有助于你完成工作的工具。有时,你只能够连接到一个崩溃的程序并且四处转悠。或者,你得到了一个会崩溃的服务器,你只能够获得一些核心文件来一探究竟。这些货少数其它情况中,gdb是很好的办法。你最好准备尽可能多的工具来解决问题。
接下来我会通过对比gdb、调试输出和Valgrind来详细分析,像这样:
这一过程适用于你打算使用任何调试技巧,无论是Valgrind、调试输出,或者使用调试器。我打算以使用gdb
的形式来描述他,因为似乎人们在使用调试器是会跳过它。但是应当对每个bug使用它,直到你只需要在非常困难的bug上用到。
notes.txt
,并且将它用作记录想法、bug和问题的“实验记录”。gdb
之前,写下你打算修复的bug,以及可能的产生原因。gdb
并且使用file:function
挑选最可能的因素,之后在那里设置断点。gdb
运行程序,并且确认它是否是真正原因。查明它的最好方式就是看看你是否可以使用set
命令,简单修复问题或者重现错误。notes.txt
中标记它不是,以及理由。移到下一个可能的原因,并且使最易于调试的,之后记录你收集到的信息。这里你并没有注意到,它是最基本的科学方法。你写下一些假设,之后调试来证明或证伪它们。这让你洞察到更多可能的因素,最终使你找到他。这个过程有助于你避免重复步入同一个可能的因素,即使你发现它们并不可能。
你也可以使用调试输出来执行这个过程。唯一的不同就是你实际在源码中编写假设来推测问题所在,而不是notes.txt
中。某种程度上,调试输出强制你科学地解决bug,因为你需要将假写为打印语句。
我将在这个练习中调试下面这个程序,它只有一个不会正常终止的while
循环。我在里面放置了一个usleep
调用,使它循环起来更加有趣。
#include <unistd.h>
int main(int argc, char *argv[])
{
int i = 0;
while(i < 100) {
usleep(3000);
}
return 0;
}
像往常一样编译,并且在gdb
下启动它,例如:gdb ./ex31
。
一旦它运行之后,我打算让你使用这些gdb
命令和它交互,并且观察它们的作用以及如何使用它们。
help COMMAND
获得COMMAND
的简单帮助。
break file.c:(line|function)
在你希望暂停之星的地方设置断点。你可以提供行号或者函数名称,来在文件中的那个地方暂停。
run ARGS
运行程序,使用ARGS
作为命令行参数。
cont
继续执行程序,直到断点或错误。
step
单步执行代码,但是会进入函数内部。使用它来跟踪函数内部,来观察它做了什么。
next
就像是step
,但是他会运行函数并步过它们。
backtrace (or bt)
执行“跟踪回溯”,它会转储函数到当前执行点的执行轨迹。对于查明如何执行到这里非常有用,因为它也打印出传给每个函数的参数。它和Valgrind报告内存错误的方式很接近。
set var X = Y
将变量X
设置为Y
。
print X
打印出X
的值,你通常可以使用C的语法来访问指针的值或者结构体的内容。
ENTER
重复上一条命令。
quit
退出gdb
。
这些都是我使用gdb
时的主要命令。你现在的任务是玩转它们和ex31
,你会对它的输出更加熟悉。
一旦你熟悉了gdb
之后,你会希望多加使用它。尝试在更复杂的程序,例如devpkg
上使用它,来观察你是否能够改函数的执行或分析出程序在做什么。
gdb
最实用的功能就是附加到运行中的程序,并且就地调试它的能力。当你拥有一个崩溃的服务器或GUI程序,你通常不需要像之前那样在gdb
下运行它。而是可以直接启动它,希望它不要马上崩溃,之后附加到它并设置断点。练习的这一部分中我会向你展示怎么做。
当你退出gdb
之后,如果你停止了ex31
我希望你重启它,之后开启另一个中断窗口以便于启动gdb
并附加。进程附加就是你让gdb
连接到已经运行的程序,以便于你实时监测它。它会挂起程序来让你单步执行,当你执行完之后程序会像往常一样恢复运行。
下面是一段会话,我对ex31
做了上述事情,单步执行它,之后修改while
循环并使它退出。
$ ps ax | grep ex31
10026 s000 S+ 0:00.11 ./ex31
10036 s001 R+ 0:00.00 grep ex31
$ gdb ./ex31 10026
GNU gdb 6.3.50-20050815 (Apple version gdb-1705) (Fri Jul 1 10:50:06 UTC 2011)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin"...Reading symbols for shared libraries .. done
/Users/zedshaw/projects/books/learn-c-the-hard-way/code/10026: No such file or directory
Attaching to program: `/Users/zedshaw/projects/books/learn-c-the-hard-way/code/ex31', process 10026.
Reading symbols for shared libraries + done
Reading symbols for shared libraries ++........................ done
Reading symbols for shared libraries + done
0x00007fff862c9e42 in __semwait_signal ()
(gdb) break 8
Breakpoint 1 at 0x107babf14: file ex31.c, line 8.
(gdb) break ex31.c:11
Breakpoint 2 at 0x107babf1c: file ex31.c, line 12.
(gdb) cont
Continuing.
Breakpoint 1, main (argc=1, argv=0x7fff677aabd8) at ex31.c:8
8 while(i < 100) {
(gdb) p i
$1 = 0
(gdb) cont
Continuing.
Breakpoint 1, main (argc=1, argv=0x7fff677aabd8) at ex31.c:8
8 while(i < 100) {
(gdb) p i
$2 = 0
(gdb) list
3
4 int main(int argc, char *argv[])
5 {
6 int i = 0;
7
8 while(i < 100) {
9 usleep(3000);
10 }
11
12 return 0;
(gdb) set var i = 200
(gdb) p i
$3 = 200
(gdb) next
Breakpoint 2, main (argc=1, argv=0x7fff677aabd8) at ex31.c:12
12 return 0;
(gdb) cont
Continuing.
Program exited normally.
(gdb) quit
$
注
在OSX上你可能会看到输入root密码的GUI输入框,并且即使你输入了密码还是会得到来自
gdb
的“Unable to access task for process-id XXX: (os/kern) failure.”的错误。这种情况下,你需要停止gdb
和ex31
程序,并重新启动程序使它工作,只要你成功输入了root密码。
我会遍历整个会话,并且解释我做了什么:
gdb:1
使用ps
来寻找我想要附加的ex31
的进程ID。
gdb:5
我使用gdb ./ex31 PID
来附加到进程,其中PID
替换为我所拥有的进程ID。
gdb:6-19
gdb
打印出了一堆关于协议的信息,接着它读取了所有东西。
gdb:21
程序被附加,并且在当前执行点上停止。所以现在我在文件中的第8行使用break
设置了断点。我假设我这么做的时候,已经在这个我想中断的文件中了。
gdb:24
执行break
的更好方式,是提供file.c line
的格式,便于你确保定位到了正确的地方。我在这个break
中这样做。
gdb:27
我使用cont
来继续运行,直到我命中了断点。
gdb:30-31
我已到达断点,于是gdb
打印出我需要了解的变量(argc
和argv
),以及停下来的位置,之后打印出断点的行号。
gdb:33-34
我使用print
的缩写p
来打印出i
变量的值,它是0。
gdb:36
继续运行来查看i
是否改变。
gdb:42
再次打印出i
,显然它没有变化。
gdb:45-55
使用list
来查看代码是什么,之后我意识到它不可能退出,因为我没有自增i
。
gdb:57
确认我的假设是正确的,即i
需要使用set
命令来修改为i = 200
。这是gdb
最优秀的特性之一,让你“修改”程序来让你快速知道你是否正确。
gdb:59
打印i
来确保它已改变。
gdb:62
使用next
来移到下一段代码,并且我发现命中了ex31.c:12
的断点,所以这意味着while
循环已退出。我的假设正确,我需要修改i
。
gdb:67
使用cont
来继续运行,程序像往常一样退出。
gdb:71
最后我使用quit
来退出gdb
。
下面是你可以用于GDB的一些小技巧:
gdb --args
通常gdb
获得你提供的变量并假设它们用于它自己。使用--args
来向程序传递它们。
thread apply all bt
转储所有线程的执行轨迹,非常有用。
gdb --batch --ex r --ex bt --ex q --args
运行程序,当它崩溃时你会得到执行轨迹。
?
如果你有其它技巧,在评论中写下它吧。
gdb
相比。它们在本地调试程序时非常有用,但是对于在服务器上调试没有任何意义。ex31.c
使它在几个迭代之后崩溃,之后尝试得到它的核心转储并分析。