学习使用 GNU 调试器调试代码

使用 GNU 调试器排除代码故障。下载我们的新速查表。
67 位读者喜欢这篇文章。
magnifying glass on computer screen, finding a bug in the code

Opensource.com

GNU 调试器,更常用其命令 gdb 称呼,是一个交互式控制台,帮助您单步执行源代码,分析执行的内容,并从本质上逆向工程找出有缺陷的应用程序中出现的问题。

故障排除的麻烦之处在于其复杂性。GNU 调试器并非真正复杂的应用程序,但如果您不知道从哪里开始,甚至不知道何时以及为何需要求助于 GDB 进行故障排除,它可能会让人感到不知所措。如果您一直在使用 print、 echo 或 printf 语句 来调试代码,但您开始怀疑可能存在更强大的工具,那么本教程适合您。

代码存在缺陷

要开始使用 GDB,您需要一些代码。这是一个用 C++ 编写的示例应用程序(如果您通常不使用 C++ 编写代码也没关系,原理在所有语言中都是相同的),它源自 Opensource.com 上的猜谜游戏系列中的一个示例。

#include <iostream>
#include <stdlib.h> //srand
#include <stdio.h>  //printf

using namespace std;

int main () {

srand (time(NULL));
int alpha = rand() % 8;
cout << "Hello world." << endl;
int beta = 2;

printf("alpha is set to is %s\n", alpha);
printf("kiwi is set to is %s\n", beta);

 return 0;
} // main

此代码示例中存在一个错误,但它可以编译(至少在 GCC 5 中是这样)。如果您熟悉 C++,您可能已经看到了它,但这是一个简单的问题,可以帮助 GDB 新用户理解调试过程。编译并运行它以查看错误

$ g++ -o buggy example.cpp
$ ./buggy
Hello world.
Segmentation fault

排除段错误故障

从这个输出中,您可以推测变量 alpha 已正确设置,因为否则,您不会期望看到之后的代码行。当然,这并非总是如此,但这是一个很好的工作理论,并且本质上与您使用 printf 作为日志和调试器时可能得出的结论相同。从这里,您可以假设错误位于成功打印的行之后的某一行中。但是,尚不清楚错误是在紧随其后的行还是在几行之后。

GNU 调试器是一个交互式故障排除工具,因此您可以使用 gdb 命令运行有缺陷的代码。为了获得最佳结果,您应该从包含调试符号的源代码重新编译有缺陷的应用程序。首先,看看在不重新编译的情况下 GDB 可以提供哪些信息

$ gdb ./buggy
Reading symbols from ./buggy...done.
(gdb) start
Temporary breakpoint 1 at 0x400a44
Starting program: /home/seth/demo/buggy 

Temporary breakpoint 1, 0x0000000000400a44 in main ()
(gdb)

当您使用二进制可执行文件作为参数启动 GDB 时,GDB 会加载应用程序,然后等待您的指令。因为这是您第一次在此可执行文件上运行 GDB,所以尝试重现错误以希望 GDB 可以提供更深入的见解是有意义的。GDB 用于启动已加载应用程序的命令非常直观,即 start。默认情况下,GDB 中内置了一个断点,以便当它遇到应用程序的 main 函数时,它会暂停执行。要允许 GDB 继续执行,请使用命令 continue

(gdb) continue
Continuing.
Hello world.

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff71c0c0b in vfprintf () from /lib64/libc.so.6
(gdb)

这里没有意外:应用程序在打印 “Hello world,” 后不久崩溃了,但 GDB 可以提供崩溃发生时正在发生的函数调用。这可能就是您找到导致崩溃的错误所需的全部信息,但为了更好地了解 GDB 的功能和一般调试过程,假设问题尚未明确,并且您想更深入地探究此代码中发生的事情。

使用调试符号编译代码

要充分利用 GDB,您需要在可执行文件中编译调试符号。您可以使用 GCC 中的 -g 选项生成它

$ g++ -o debuggy example.cpp
$ ./debuggy
Hello world.
Segmentation fault

将调试符号编译到可执行文件中会导致文件大得多,因此通常不会为了增加便利性而分发它们。但是,如果您正在调试开源代码,则为了测试而使用调试符号重新编译是有意义的

$ ls -l *buggy* *cpp
-rw-r--r--    310 Feb 19 08:30 debug.cpp
-rwxr-xr-x  11624 Feb 19 10:27 buggy*
-rwxr-xr-x  22952 Feb 19 10:53 debuggy*

使用 GDB 调试

启动 GDB 并加载您的新可执行文件(在本例中为 debuggy

$ gdb ./debuggy
Reading symbols from ./debuggy...done.
(gdb) start
Temporary breakpoint 1 at 0x400a44
Starting program: /home/seth/demo/debuggy 

Temporary breakpoint 1, 0x0000000000400a44 in main ()
(gdb)

和以前一样,使用 start 命令继续

(gdb) start
Temporary breakpoint 1 at 0x400a48: file debug.cpp, line 9.
Starting program: /home/sek/demo/debuggy 

Temporary breakpoint 1, main () at debug.cpp:9
9	srand (time(NULL));
(gdb)

这次,自动 main 断点可以指定 GDB 暂停在哪个行号以及该行包含的代码。您可以使用 continue 恢复正常操作,但您已经知道应用程序在完成之前崩溃,因此,您可以改用 next 关键字逐行单步执行代码

(gdb) next
10  int alpha = rand() % 8;
(gdb) next
11  cout << "Hello world." << endl;
(gdb) next
Hello world.
12  int beta = 2;
(gdb) next
14	printf("alpha is set to is %s\n", alpha);
(gdb) next

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff71c0c0b in vfprintf () from /lib64/libc.so.6
(gdb)

通过此过程,您可以确认崩溃不是发生在设置 beta 变量时,而是发生在执行 printf 行时。本文中已多次暴露该错误(剧透:为 printf 提供了错误的数据类型),但假设解决方案仍然不明确,并且需要进一步调查。

设置断点

一旦您的代码加载到 GDB 中,您可以向 GDB 询问代码到目前为止生成的数据。要尝试一些数据内省,请通过再次发出 start 命令来重新启动您的应用程序,然后继续到第 11 行。快速到达第 11 行的简单方法是设置一个查找特定行号的断点

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x400a48: file debug.cpp, line 9.
Starting program: /home/sek/demo/debuggy 

Temporary breakpoint 2, main () at debug.cpp:9
9	srand (time(NULL));
(gdb) break 11
Breakpoint 3 at 0x400a74: file debug.cpp, line 11.

建立断点后,使用 continue 继续执行

(gdb) continue
Continuing.

Breakpoint 3, main () at debug.cpp:11
11	cout << "Hello world." << endl;
(gdb)

您现在暂停在第 11 行,正好在 alpha 变量设置之后,以及在 beta 设置之前。

使用 GDB 进行变量内省

要查看变量的值,请使用 print 命令。在此示例代码中,alpha 的值是随机的,因此您的实际结果可能与我的不同

(gdb) print alpha
$1 = 3
(gdb)

当然,您无法查看尚未建立的变量的值

(gdb) print beta
$2 = 0

使用流程控制

要继续,您可以单步执行代码行,直到到达 beta 设置为值的位置

(gdb) next
Hello world.
12  int beta = 2;
(gdb) next
14  printf("alpha is set to is %s\n", alpha);
(gdb) print beta
$3 = 2

或者,您可以设置一个监视点。监视点就像断点一样,是一种控制 GDB 如何执行代码流程的方法。在本例中,您知道 beta 变量应设置为 2,因此您可以设置一个监视点,以便在 beta 的值更改时提醒您

(gdb) watch beta > 0
Hardware watchpoint 5: beta > 0
(gdb) continue
Continuing.

Breakpoint 3, main () at debug.cpp:11
11	cout << "Hello world." << endl;
(gdb) continue
Continuing.
Hello world.

Hardware watchpoint 5: beta > 0

Old value = false
New value = true
main () at debug.cpp:14
14	printf("alpha is set to is %s\n", alpha);
(gdb)

您可以手动使用 next 单步执行代码,也可以使用断点、监视点和捕获点来控制代码的执行方式。

使用 GDB 分析数据

您可以以不同的格式查看数据。例如,要将 beta 的值视为八进制值

(gdb) print /o beta
$4 = 02

要查看其在内存中的地址

(gdb) print /o beta
$5 = 0x2

您还可以查看变量的数据类型

(gdb) whatis beta
type = int

使用 GDB 解决错误

这种内省可以更好地告知您不仅执行了哪些代码,还执行了代码的方式。在本例中,对变量使用 whatis 命令会给您一个线索,即您的 alphabeta 变量是整数,这可能会唤起您对 printf 语法的记忆,使您意识到在 printf 语句中,您必须使用 %d 说明符而不是 %s。进行此更改后,应用程序将按预期运行,并且不再存在明显的错误。

当代码编译后却显示存在错误时,尤其令人沮丧,但这就是最棘手的错误的工作方式。如果它们很容易被捕获,它们就不会是错误了。使用 GDB 是查找并消除它们的一种方法。

下载我们的速查表

即使在最基本的编程形式中,代码存在错误也是一个不争的事实。并非所有错误都严重到会阻止应用程序运行(甚至编译),也并非所有错误都是由不正确的代码引起的。有时,错误会间歇性地发生,这是由于特别有创意的用户所做的意外选择组合造成的。有时,程序员会从他们在自己的代码中使用的库中继承错误。无论原因是什么,错误基本上无处不在,找到并消除它们是程序员工作的一部分。

GNU 调试器是查找错误的有用工具。您可以使用它做的事情比我在本文中演示的要多得多。您可以使用 GNU Info 阅读器阅读有关其许多功能的信息

$ info gdb

无论您是刚开始学习 GDB 还是已经是专家,了解有哪些命令可供您使用以及这些命令的语法总是没有坏处的。

立即下载我们的 GDB 速查表。

接下来阅读
标签
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算机行业工作,而且经常同时从事这两个行业。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.