笨办法学C 练习31:代码调试

练习31:代码调试

原文:Exercise 31: Debugging Codehtml

译者:飞龙git

我已经教给你一些关于个人强大的调试宏的技巧,而且你已经开始用它们了。当我调试代码时,我使用debug()宏,分析发生了什么以及跟踪问题。在这个练习中我打算教给你一些使用gdb的技巧,用于监视一个不会退出的简单程序。你会学到如何使用gdb附加到运行中的进程,并挂起它来观察发生了什么。在此以后我会给你一些用于gdb的小提示和小技巧。程序员

调试输出、GDB或Valgrind

我主要按照一种“科学方法”的方式来调试,我会提出可能的全部缘由,以后排除它们或证实它们致使了缺陷。许多程序员拥有的问题是它们对解决bug的恐慌和急躁使他们以为这种方法会“拖慢”他们。它们并无注意到,它们已经失败了,而且在收集无用的信息。我发现日志(调试输出)会强迫我科学地解决bug,而且在更多状况下易于收集信息。github

此外,使用调试输出来做为个人首要调试工具的理由以下:编程

  • 你可使用变量的调试输出,来看到程序执行的整个轨迹,它让你跟踪变量是如何产生错误的。使用gdb的话,你必须为每一个变量放置查看和调试语句,而且难以得到执行的实际轨迹。服务器

  • 调试输出存在于代码中,当你须要它们是你能够从新编译使它们回来。使用gdb的话,你每次调试都须要从新配置相同的信息。app

  • 当服务器工做不正常时,它的调试日志功能易于打开,而且在它运行中能够监视日志来查看哪里不对。系统管理员知道如何处理日志,他们不知道如何使用gdb。函数

  • 打印信息更加容易。调试器一般因为它奇特的UI和先后矛盾显得难用且古怪。debug("Yo, dis right? %d", my_stuff);就没有那么麻烦。工具

  • 编写调试输出来发现缺陷,强迫你实际分析代码,而且使用科学方法。你能够认为它是,“我假设这里的代码是错误的”,你能够运行它来验证你的假设,若是这里没有错误那么你能够移动到其它地方。这看起来须要更长时间,可是实际上更快,由于你经历了“鉴别诊断”的过程,并排除全部可能的缘由,直到你找到它。单元测试

  • 调试输入更适于和单元测试一块儿运行。你能够实际上老是编译调试语句,单元测试时能够随时查看日志。若是你用gdb,你须要在gdb中重复运行单元测试,并跟踪他来查看发生了什么。

  • 使用Valgrind能够获得和调试输出等价的内存相关的错误,因此你并不须要使用相似gdb的东西来寻找缺陷。

尽管全部缘由显示我更倾向于debug而不是gdb,我仍是在少数状况下回用到gdb,而且我认为你应该选择有助于你完成工做的工具。有时,你只可以链接到一个崩溃的程序而且四处转悠。或者,你获得了一个会崩溃的服务器,你只可以得到一些核心文件来一探究竟。这些货少数其它状况中,gdb是很好的办法。你最好准备尽量多的工具来解决问题。

接下来我会经过对比gdb、调试输出和Valgrind来详细分析,像这样:

  • Valgrind用于捕获全部内存错误。若是Valgrind中含有错误或Valgrind会严重拖慢程序,我会使用gdb。

  • 调试输出用于诊断或修复有关逻辑或使用上的缺陷。在你使用Valgrind以前,这些共计90%的缺陷。

  • 使用gdb解决剩下的“谜之bug”,或如要收集信息的紧急状况。若是Valgrind不起做用,而且我不能打印出所需信息,我就会使用gdb开始四处搜索。这里我仅仅使用gdb来收集信息。一旦我弄清发生了什么,我会回来编程单元测试来引起缺陷,以后编程打印语句来查找缘由。

调试策略

这一过程适用于你打算使用任何调试技巧,不管是Valgrind、调试输出,或者使用调试器。我打算以使用gdb的形式来描述他,由于彷佛人们在使用调试器是会跳过它。可是应当对每一个bug使用它,直到你只须要在很是困难的bug上用到。

  • 建立一个小型文本文件叫作notes.txt,而且将它用做记录想法、bug和问题的“实验记录”。

  • 在你使用gdb以前,写下你打算修复的bug,以及可能的产生缘由。

  • 对于每一个缘由,写下你所认为的,问题来源的函数或文件,或者仅仅写下你不知道。

  • 如今启动gdb而且使用file:function挑选最可能的因素,以后在那里设置断点。

  • 使用gdb运行程序,而且确认它是不是真正缘由。查明它的最好方式就是看看你是否可使用set命令,简单修复问题或者重现错误。

  • 若是它不是真正缘由,则在notes.txt中标记它不是,以及理由。移到下一个可能的缘由,而且使最易于调试的,以后记录你收集到的信息。

这里你并无注意到,它是最基本的科学方法。你写下一些假设,以后调试来证实或证伪它们。这让你洞察到更多可能的因素,最终使你找到他。这个过程有助于你避免重复步入同一个可能的因素,即便你发现它们并不可能。

你也可使用调试输出来执行这个过程。惟一的不一样就是你实际在源码中编写假设来推测问题所在,而不是notes.txt中。某种程度上,调试输出强制你科学地解决bug,由于你须要将假写为打印语句。

使用 GDB

我将在这个练习中调试下面这个程序,它只有一个不会正常终止的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.”的错误。这种状况下,你须要中止gdbex31程序,并从新启动程序使它工做,只要你成功输入了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打印出我须要了解的变量(argcargv),以及停下来的位置,以后打印出断点的行号。

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的一些小技巧:

gdb --args

一般gdb得到你提供的变量并假设它们用于它本身。使用--args来向程序传递它们。

thread apply all bt

转储全部线程的执行轨迹,很是有用。

gdb --batch --ex r --ex bt --ex q --args

运行程序,当它崩溃时你会获得执行轨迹。

?

若是你有其它技巧,在评论中写下它吧。

附加题

  • 找到一个图形化的调试器,将它与原始的gdb相比。它们在本地调试程序时很是有用,可是对于在服务器上调试没有任何意义。

  • 你能够开启OS上的“核心转储”,当程序崩溃时你会获得一个核心文件。这个核心文件就像是对程序的解剖,便于你了解崩溃时发生了什么,以及由什么缘由致使。修改ex31.c使它在几个迭代以后崩溃,以后尝试获得它的核心转储并分析。

相关文章
相关标签/搜索