Skip to content

Latest commit

 

History

History
435 lines (319 loc) · 25.6 KB

File metadata and controls

435 lines (319 loc) · 25.6 KB

七、调试代码并解决错误

在前一章中,我们成功地开发了一个服务器-客户端程序。我们还顺利地运行了我们创建的程序。然而,有时我们在运行应用时会面临一些问题,比如收到意外结果或应用在运行时崩溃。在这种情况下,调试工具有能力帮助我们解决这些问题。在本章讨论调试工具时,我们将讨论以下主题:

  • 选择我们使用的调试工具,并保持它的简单和轻量级
  • 设置调试工具,准备待调试的可执行文件
  • 熟悉调试工具中使用的命令

选择调试工具

身边很多调试工具都自带编程语言的集成开发环境 ( IDE )。比如 Visual Studio 有一个调试工具针对 C、C++、C#、Visual Basic。或者,您可能听说过代码块和流血开发-C++,它们也有自己的调试工具。但是,如果你还记得我们在第 1 章中讨论的用 C++ 简化你的网络编程的话,我们决定不使用 IDE,因为它的重载不会给我们的计算机加载太多资源。我们需要一个轻量级的工具来开发我们的网络应用。

我们选择的工具是 GNU 调试器 ( GDB )。GDB 是一个基于命令行工具的强大调试工具;这意味着我们不需要复杂的图形用户界面 ( GUI )。换句话说,我们只需要一个键盘,甚至不需要鼠标,所以系统也变得轻量级了。

GDB 可以做四件主要的事情来帮助我们解决代码问题,如下所示:

  • 逐行运行我们的代码:当 GDB 运行我们的程序时,我们可以看到此刻正在执行哪一行
  • 在特定的一行停止我们的代码:当我们怀疑某一行导致了错误时,这很有用
  • 检查可疑线:当我们成功停在可疑线时,我们可以继续检查,例如,通过检查涉及的变量的值
  • 改变变量的值:如果我们发现了导致错误的意外变量值,我们可以用我们的期望值替换 GDB 运行时的值,以确保值的改变会解决问题

安装调试工具

幸运的是,如果您遵循了第 1 章、中与安装 MinGW-w64 相关的所有步骤,您就不需要安装其他任何东西了,因为安装包中还包含了 GDB 工具。我们现在需要做的是在命令控制台中运行 GDB 工具,检查它是否正常运行。

在我们命令提示符的任何活动目录中,键入以下命令:

gdb

我们应该在控制台窗口中获得以下输出:

C:\CPP>gdb
GNU gdb (GDB) 7.8.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)_

正如我们在控制台上看到的前面的输出,我们有 7.8.1 版本(这不是最新版本,因为我们刚刚从 MinGW-w64 安装程序包中获得它)。最后一行还有(gdb),旁边有一个闪烁的光标;这意味着 GDB 已经准备好接受命令。但是,目前我们需要知道的命令是quit(或者,我们可以用q作为捷径)退出 GDB。只需键入q并按进入,您将返回命令提示符。

准备调试文件

GDB 至少需要一个可执行文件进行调试。出于这个目的,我们将回到上一章,从那里借用源代码。还记得我们在第一章用 C++ 简化你的网络编程中创建的一个游戏吗,在这个游戏中,我们要猜测电脑想到的随机数?如果你记得的话,我们有源代码,我们在第一章中保存为rangen.cpp,我们通过添加Boost库进行了修改,在第三章中保存为rangen_boost.cpp,介绍了 Boost C++ 库。在下一节中,我们将使用rangen_boost.cpp源代码来演示 GDB 的使用。还有,对于那些已经忘记了源代码的人,我在这里为大家重写了一下:

/* rangen_boost.cpp */
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_int_distribution.hpp>
#include <iostream>

int main(void) {
  int guessNumber;
  std::cout << "Select number among 0 to 10: ";
  std::cin >> guessNumber;
  if(guessNumber < 0 || guessNumber > 10) {
    return 1;
  }
  boost::random::mt19937 rng;
  boost::random::uniform_int_distribution<> ten(0,10);
  int randomNumber = ten(rng);

  if(guessNumber == randomNumber) {
    std::cout << "Congratulation, " << guessNumber << " is your lucky number.\n";
  }
  else {
    std::cout << "Sorry, I'm thinking about number " << randomNumber << "\n"; 
  }
  return 0;
}

我们将修改编译命令,以便在 GDB 使用。我们将使用-g选项,这样创建的可执行文件将包含 GDB 将读取的调试信息和符号。我们将使用以下命令从包含调试信息和符号的rangen_boost.cpp文件生成rangen_boost_gdb.exe可执行文件:

g++ -Wall -ansi -I ../boost_1_58_0 rangen_boost.cpp -o rangen_boost_gdb -g

正如我们在前面的命令中看到的,我们在编译命令中添加了-g选项,以便在可执行文件中记录调试信息和符号。现在,我们的活动目录中应该有名为rangen_boost_gdb.exe的文件。在下一节中,我们将使用 GDB 调试它。

类型

我们只能调试使用-g选项编译的可执行文件。换句话说,如果没有调试信息和符号,我们将无法调试可执行文件。此外,我们无法调试源代码文件(*.cpp文件)或头文件(*.h文件)。

在 GDB 的领导下运行程序

准备好包含调试信息和符号的可执行文件后,让我们运行 GDB 从文件中读取所有符号并进行调试。运行以下命令启动调试过程:

gdb rangen_boost_gdb

我们的输出如下:

C:\CPP>gdb rangen_boost_gdb
GNU gdb (GDB) 7.8.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from rangen_boost_gdb...done.
(gdb)_

除了(gdb)之前的最后一行,我们得到了与之前的 GDB 输出相同的输出。这一行告诉我们,GDB 已经成功读取了所有调试符号,并准备启动调试过程。在这一步中,如果我们的程序需要,我们还可以指定参数。由于我们的程序不需要指定任何参数,我们现在可以忽略它。

开始调试过程

要启动调试过程,我们可以调用runstart命令。前者将在 GDB 下启动我们的程序,而后者将表现类似,但将逐行执行代码。不同的是,如果我们还没有设置断点,如果我们调用run命令,程序将照常运行,而如果我们用start命令启动,调试器将自动在主代码块中设置断点,如果断点到达该点,程序将停止。

现在,让我们使用start命令进行调试。只需在 GDB 提示符下键入start,控制台将追加以下输出:

(gdb) start
Temporary breakpoint 1 at 0x401506: file rangen_boost.cpp, line 10.
Starting program: C:\CPP\rangen_boost_gdb.exe
[New Thread 10856.0x213c]

Temporary breakpoint 1, main () at rangen_boost.cpp:10
10              std::cout << "Select number among 0 to 10: ";

调试过程开始。从输出中,我们可以发现在第 10 行的main块内自动创建了一个断点。当没有断点时,调试器将选择主块中的第一条语句。这就是为什么我们把line 10作为我们的自动断点。

继续和步进调试过程

在我们在 GDB 的带领下成功启动我们的计划之后,下一步就是继续和迈步。我们可以使用以下命令之一来继续并逐步执行调试过程:

  • continue:这个命令将恢复程序的执行,直到我们的程序正常完成。如果找到断点,执行将在设置断点的行停止。
  • step:这个命令将执行我们程序的最后一步。步骤可能意味着一行源代码或一条机器指令。如果它找到了函数的调用,它将进入函数并在函数内部运行一个步骤。
  • next:这个命令的行为类似于step命令,但是它只延续到当前堆栈帧的下一行。换句话说,如果next命令发现一个函数的调用,它将不会进入该函数。

现在,让我们使用next命令。在我们调用start命令后,在 GDB 提示符下键入next命令。我们应该得到以下输出:

(gdb) next
Select number among 0 to 10: 11         std::cin >> guessNumber;

GDB 执行第 10 行,然后继续执行第 11 行。我们将再次调用next命令继续调试过程。然而,如果我们只是按下进入键,GDB 将执行我们之前的命令。这就是为什么我们现在只需要按下进入键,它会给我们一个闪烁的光标。现在,我们必须输入我们猜测要存储在guessNumber变量中的数字。我会输入号码4,但是你可以输入你喜欢的号码。再次按下进入键,根据需要继续调试多次,正常退出程序。将附加以下输出:

(gdb)
4
12              if(guessNumber < 0 || guessNumber > 10)
(gdb)
17              boost::random::mt19937 rng;
(gdb)
19              boost::random::uniform_int_distribution<> ten(0,10);
(gdb)
20              int randomNumber = ten(rng);
(gdb)
22              if(guessNumber == randomNumber)
(gdb)
28                      std::cout << "Sorry, I'm thinking about number " << randomNumber << "\n";
(gdb)
Sorry, I'm thinking about number 8
30              return 0;
(gdb)
31      }(gdb)
0x00000000004013b5 in __tmainCRTStartup ()
(gdb)
Single stepping until exit from function __tmainCRTStartup, which has no line number information.
[Inferior 1 (process 11804) exited normally]

正如我们在前面的输出中所看到的,在我们输入猜测的号码后,程序执行的if语句,以确保我们输入的号码没有超出范围。如果我们的猜测数字有效,程序会继续生成一个随机数。然后将我们的猜测数字与程序生成的随机数进行比较。无论两个数字是否相同,程序都会给出不同的输出。不幸的是,我的猜测数字不同于随机数。如果您能够正确猜测数字,您可能会获得不同的输出。

打印源代码

有时,我们可能想在运行调试过程时检查我们的源文件。由于调试信息和符号都记录在我们的程序中,GDB 可以打印源代码,即使它是一个可执行文件。要打印源代码,我们可以在 GDB 提示符下键入list(或快捷方式为l命令)。默认情况下,每次调用命令时,GDB 都会打印十行。但是,我们可以使用set listsize命令更改该设置。另外,要知道list命令将显示的行数,我们可以调用show listsize命令。让我们看看下面的命令行输出:

(gdb) show listsize
Number of source lines gdb will list by default is 10.
(gdb) set listsize 20
(gdb) show listsize
Number of source lines gdb will list by default is 20.
(gdb)_

我们使用list命令增加要显示的行数。现在,每次调用list命令,输出都会显示二十行源代码。

以下是最常见的几种list命令的形式:

  • list:这个命令将显示列表大小定义的所有行的源代码。如果我们再次调用它,它将显示列表大小定义的剩余行数。
  • list [linenumber]:该命令将显示以linenumber为中心的线条。命令list 10将显示第 5 行到第 14 行,因为第 10 行在中间。
  • list [functionname]:该命令将显示以functionname变量开头为中心的行。命令list main将在列表中心显示int main(void)功能。
  • list [first,last]:该命令将显示从第一行到最后一行。命令list 15,16将只显示第 15 行和第 16 行。
  • list [,last]:该命令将显示以last结尾的行。命令list ,5将显示第 1 行到第 5 行。
  • list [first,]:此命令将显示以指定行为第一行开始的所有行。如果行数超过指定的行数,命令list 5,将显示第 5 行到其余行。否则,它将显示与列表大小设置一样多的行。
  • list +:该命令将显示最后显示的行之后的所有行。
  • list -:该命令将显示最后显示的行之前的所有行。

设置和删除断点

如果我们怀疑某一行出错,我们可以在该行设置一个断点,以便调试器在该行停止调试过程。要设置断点,我们可以调用break [linenumber]命令。考虑我们想在第 20 行停止,它包含以下代码:

int randomNumber = ten(rng);

在这里,我们必须在 GDB 下加载我们的程序之后调用break 20命令,以便在第 20 行设置断点。以下输出控制台说明了这一点:

(gdb) break 20
Breakpoint 1 at 0x401574: file rangen_boost.cpp, line 20.
(gdb) run
Starting program: C:\CPP\rangen_boost_gdb.exe
[New Thread 1428.0x13f4]
Select number among 0 to 10: 2

Breakpoint 1, main () at rangen_boost.cpp:20
20              int randomNumber = ten(rng);
(gdb) next
22              if(guessNumber == randomNumber)
(gdb)
28                      std::cout << "Sorry, I'm thinking about number " << randomNumber << "\n";
(gdb)
Sorry, I'm thinking about number 8
30              return 0;
(gdb)
31      }(gdb)
0x00000000004013b5 in __tmainCRTStartup ()
(gdb)
Single stepping until exit from function __tmainCRTStartup,
which has no line number information.
[Inferior 1 (process 1428) exited normally]
(gdb)_

在前面的输出控制台中,就在我们的程序加载到 GDB 下之后,我们调用break 20命令。调试器然后在第 20 行设置一个新的断点。我们没有像以前那样调用start命令,而是调用run命令来执行程序,并让它在找到断点时停止。例如,在我们输入猜测数字2后,调试器停在第 20 行,也就是我们期望它停的那一行。然后,我们调用next命令继续调试器,并多次按下进入键,直到程序退出。

如果我们想删除一个断点,只需使用delete N命令,其中N是设置所有断点的顺序。如果我们没有记住我们设置的断点的所有位置,我们可以调用info break命令来获得所有断点的列表。我们也可以使用delete命令(没有N,将删除所有断点)。

打印变量值

我们已经能够停在我们想要的线上了。我们还可以发现我们在程序中使用的变量的值。我们可以调用print [variablename]命令打印任意变量的值。使用上一个断点,我们将打印变量randomNumber的值。就在调试器遇到第 20 行的断点后,我们将调用 print randomNumber命令。然后,我们调用next命令,再次打印randomNumber变量。请看下面的命令调用示例:

(gdb) break 20
Breakpoint 1 at 0x401574: file rangen_boost.cpp, line 20.
(gdb) run
Starting program: C:\CPP\rangen_boost_gdb.exe
[New Thread 5436.0x1b04]
Select number among 0 to 10: 3

Breakpoint 1, main () at rangen_boost.cpp:20
20              int randomNumber = ten(rng);
(gdb) print randomNumber
$1 = 0
(gdb) next
22              if(guessNumber == randomNumber)
(gdb) print randomNumber
$2 = 8
(gdb)_

正如我们在前面的输出中看到的,下面一行是设置断点的地方:

int randomNumber = ten(rng);

在该行被执行之前,我们查看randomNumber变量的值。变量的值为0。然后,我们调用next命令指示调试器执行该行。之后,我们再次窥视变量的值,这次是8。当然,在这个实验中,你可能会得到不同的值,而不是 8。

修改变量值

我们将通过修改其中一个变量的值来欺骗我们的程序。可以使用set var [variablename]=[newvalue]命令重新分配变量值。为了保证我们要修改的变量的类型,我们可以调用whatis [variablename]命令来获取所需的变量类型。

现在,让我们在程序给变量赋值一个随机数后,改变randomNumber变量的值。我们将重新启动调试过程,删除所有已经设置的断点,在第 22 行设置一个新的断点,并通过键入continue命令继续调试过程,直到调试器命中第 22 行的断点。在这种情况下,我们可以重新分配randomNumber变量的值,使其与guessNumber变量的值完全相同。现在,再次调用continue命令。之后,我们会因为猜中了正确的数字而受到祝贺。

有关更多详细信息,让我们看看下面的输出控制台,它将说明前面的步骤:

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Temporary breakpoint 2 at 0x401506: file rangen_boost.cpp, line 10.
Starting program: C:\CPP\rangen_boost_gdb.exe
[New Thread 6392.0x1030]

Temporary breakpoint 2, main () at rangen_boost.cpp:10
10              std::cout << "Select number among 0 to 10: ";
(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000401574 in main()
 at rangen_boost.cpp:20
(gdb) delete 1
(gdb) info break
No breakpoints or watchpoints.
(gdb) break 22
Breakpoint 3 at 0x40158d: file rangen_boost.cpp, line 22.
(gdb) continue
Continuing.
Select number among 0 to 10: 5

Breakpoint 3, main () at rangen_boost.cpp:22
22              if(guessNumber == randomNumber)
(gdb) whatis randomNumber
type = int
(gdb) print randomNumber
$3 = 8
(gdb) set var randomNumber=5
(gdb) print randomNumber
$4 = 5
(gdb) continue
Continuing.
Congratulation, 5 is your lucky number.
[Inferior 1 (process 6392) exited normally]
(gdb)_

正如我们在前面的输出中看到的,当我们调用start命令时,调试器要求我们停止前面的调试过程,因为它仍然在运行。只需输入 Y 键,按回车键即可回答查询。我们可以使用info break命令列出所有可用的断点,然后根据我们从info break命令获得的顺序删除所需的断点。我们调用continue命令恢复调试过程,当调试器到达断点时,我们用guessNumber变量的值重新分配randomNumber变量。我们继续调试过程,并在运行时成功修改randomNumber变量的值,因为我们受到了程序的祝贺。

如果程序中有多个变量,不用逐个打印所有变量,可以使用info locals命令打印所有变量的值。

调用命令提示符

我偶尔会在 GDB 提示符里面调用 Windows shell 命令,比如cls命令到清除屏幕,dir命令列出活动目录的内容,甚至还有编译命令。如果还想执行 Windows shell 命令,可以使用的 GDB 命令是shell [Windows shell command]。它实际上只是在 Windows shell 命令之前添加shell命令,并在需要时添加参数。让我们看看下面的控制台输出,以了解在 GDB 提示符下执行 Windows shell 命令。让我们看看下面的输出:

C:\CPP>gdb
GNU gdb (GDB) 7.8.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) shell dir rangen_boost* /w
 Volume in drive C is SYSTEM
 Volume Serial Number is 8EA6-1DBE

 Directory of C:\CPP

rangen_boost.cpp       rangen_boost.exe       rangen_boost_gdb.exe
 3 File(s)        190,379 bytes
 0 Dir(s)  141,683,314,688 bytes free
(gdb) shell g++ -Wall -ansi -I ../boost_1_58_0 rangen_boost.cpp -o rangen_boost_gdb_2 -g
(gdb) shell dir rangen_boost* /w
 Volume in drive C is SYSTEM
 Volume Serial Number is 8EA6-1DBE

 Directory of C:\CPP

rangen_boost.cpp         rangen_boost.exe         rangen_boost_gdb.exe
rangen_boost_gdb_2.exe
 4 File(s)        259,866 bytes
 0 Dir(s)  141,683,249,152 bytes free

在前面的控制台输出中,我们调用dir命令列出活动目录中所有以rangen_boost开头的文件。然后,我们调用编译命令在活动目录中生成rangen_boost_gdb_2.exe可执行文件。然后,我们再次调用dir命令,以确保rangen_boost_gdb_2.exe可执行文件已成功创建。

类型

您可以使用apropos shell命令获取更多关于 shell 命令的信息。

解决错误

第五章中,深入研究助推.Asio 库,我们讨论了异常和错误的处理。如果我们遵循本书中所有的源代码,我们可能永远不会得到任何错误代码来迷惑我们。然而,如果我们试图修改源代码,即使只是一点点,可能会抛出一个错误代码,程序不会给我们任何描述。由于Boost库抛出的错误代码是基于 Windows 系统错误代码,超出了本书的范围,我们可以在msdn . Microsoft . com/en-us/library/Windows/desktop/ms 681381% 28v = vs . 85% 29 . aspx微软开发者网 ( MSDN )网站上找到的描述。在这里,我们可以找到从错误 0 到 15999 的所有错误代码的翻译。使用 GDB 和来自 MSDN 的错误代码翻译将成为解决我们程序中出现的错误的有力工具。

让我们回到第 6 章创建客户端-服务器应用并运行serverasync程序。当程序运行时,它在端口4444上监听127.0.0.1中的客户端,在我们的示例中,这将通过 telnet 来模拟。但是,如果客户端没有响应,会发生什么?为了进一步了解,让我们不运行 telnet 运行serverasync程序。由于客户端没有响应,将显示以下错误:

Solving the error

我们得到了系统错误代码995。现在,有了这个错误代码,我们可以访问 MSDN 系统错误代码并找到错误描述,这就是由于线程退出或应用请求,输入/输出操作已经中止。(错误 _ 操作 _ 中止)

接下来是什么?

我们熟悉基本的 GDB 命令。在 GDB 还有很多命令我们不能在这本书里讨论。GDB 有一个官方网站,我们可以在 www.gnu.org/software/gdb/documentation/访问。在这里,我们可以找到所有尚未讨论过的完整命令。

我们还可以在官方网站【www.boost.org】上获得关于 Boost C++ 库的更多详细信息,尤其是Boost.Asio库文档,可在www.boost.org/doc/libs/1_58_0/doc/html/boost_asio.html上获得。

总结

调试过程是我们可以通过一步一步运行程序来分析程序的一个基本过程。当我们的程序产生意想不到的结果或在执行过程中崩溃时,除了运行调试过程,没有其他选择。GDB 是我们的选择,因为它与 C++ 语言兼容,因为它带有 MinGW-w64 安装软件包,并且在加载时很轻。

GDB 只能运行使用-g选项编译的可执行文件。此选项将添加调试信息和符号,这在调试过程中很重要。如果没有-g选项,您将无法调试编译的可执行文件。

在 GDB 下成功加载程序后,我们可以选择runstart命令来执行调试过程。run命令将照常执行我们的程序,但是如果调试器发现断点,它将停止,而start命令将在第一次执行时在程序的main块停止。

当调试器在某一行停止时,我们必须决定是否继续调试过程。我们可以选择运行程序,直到它退出或者使用continue命令找到断点。或者,我们可以使用next命令逐步运行调试器。

要使调试器在调试过程执行时停止,请调用break [linenumber]命令来设置断点。如果我们想确保设置正确的行号,可以调用list命令打印源代码。调用delete N命令将删除N可以找到info break命令的断点。

当发现错误时,检索变量值也很重要。如果程序产生意外的输出,我们可以通过打印变量来跟踪它的值。我们可以通过使用print [variablename]命令来做到这一点。对于我们怀疑会导致错误的变量,我们可以使用set var [variablename]=[newvalue]命令为该变量重新分配一个新值。然后,我们可以再次运行调试器,直到获得预期的输出。当我们修复了所有的错误,并且确信一切都是完美的,我们可以通过使用shell [Windows shell command]命令在 GDB 提示符下调用编译命令来重新编译我们的程序。