3.6 — 使用集成调试器:单步执行

运行程序时,执行从 main 函数的顶部开始,然后语句接语句地顺序执行,直到程序结束。在程序运行的任何时候,程序都会跟踪很多事情:正在使用的变量的值、已调用的函数(以便当这些函数返回时,程序知道返回到哪里),以及程序中当前的执行点(以便知道接下来要执行哪个语句)。所有这些被跟踪的信息都称为程序状态(简称状态)。

在之前的课程中,我们探讨了各种修改代码以帮助调试的方法,包括打印诊断信息或使用日志记录器。这些是检查程序运行状态的简单方法。尽管这些方法如果使用得当可以有效,但它们仍然存在缺点:它们需要修改代码,这既耗时又可能引入新的错误,并且它们会使代码变得杂乱,使现有代码更难理解。

到目前为止,我们展示的技术背后有一个未说明的假设:一旦我们运行代码,它将运行完成(只在接受输入时暂停),我们无法在任何我们想要的点干预和检查程序的结果。

然而,如果我们能够消除这个假设呢?幸运的是,大多数现代 IDE 都带有一个称为调试器的集成工具,它正是为此而设计的。

调试器

调试器是一种计算机程序,它允许程序员控制另一个程序的执行并检查该程序运行时的程序状态。例如,程序员可以使用调试器逐行执行程序,并在此过程中检查变量的值。通过比较变量的实际值与预期值,或者观察代码的执行路径,调试器可以极大地帮助查找语义(逻辑)错误。

调试器的强大之处在于两方面:精确控制程序执行的能力,以及查看(如果需要,还可以修改)程序状态的能力。

最初,调试器(例如 gdb)是独立的程序,具有命令行界面,程序员必须输入晦涩的命令才能使其工作。后来的调试器(例如 Borland turbo 调试器的早期版本)仍然是独立的程序,但提供了一个“图形化”前端,使它们更容易使用。如今,许多现代 IDE 都具有集成调试器——也就是说,一个使用与代码编辑器相同界面的调试器,因此您可以使用与编写代码相同的环境进行调试(而无需切换程序)。

虽然集成调试器非常方便且推荐给初学者,但命令行调试器得到了很好的支持,并且仍然在不支持图形界面的环境中(例如嵌入式系统)常用。

几乎所有现代调试器都包含相同的基本功能集——但是,访问这些功能的菜单排列方式几乎没有一致性,键盘快捷键的一致性更低。尽管我们的示例将使用 Microsoft Visual Studio 的屏幕截图(我们也将介绍如何在 Code::Blocks 中完成所有操作),但无论您使用哪种 IDE,您都应该能够轻松找到我们讨论的每个功能的访问方式。

提示

调试器键盘快捷键仅在 IDE/集成调试器为活动窗口时才有效。

本章的其余部分将用于学习如何使用调试器。

提示

不要忽视学习使用调试器。随着您的程序变得越来越复杂,您投入学习如何有效使用集成调试器的时间将与您查找和修复问题所节省的时间相形见绌。

警告

在继续本课(以及与使用调试器相关的后续课程)之前,请确保您的项目使用调试构建配置进行编译(有关更多信息,请参阅0.9 -- 配置编译器:构建配置)。

如果您使用发布配置编译项目,调试器的功能可能无法正常工作(例如,当您尝试单步进入程序时,它只会运行程序)。

对于 Code::Blocks 用户

如果您使用的是 Code::Blocks,您的调试器可能设置正确,也可能不正确。让我们检查一下。

首先,转到设置菜单 > 调试器...。接下来,打开左侧的GDB/CDB 调试器树,然后选择默认。应该会打开一个对话框,看起来像这样

如果“可执行文件路径”处出现一个大的红色条,那么您需要找到您的调试器。为此,请单击“可执行文件路径”字段右侧的“…”按钮。接下来,在您的系统上找到“gdb32.exe”文件——我的文件位于 C:\Program Files (x86)\CodeBlocks\MinGW\bin\gdb32.exe。然后单击确定

对于 Code::Blocks 用户

有报告称 Code::Blocks 集成调试器(GDB)可能无法识别包含空格或非英文字符的某些文件路径。如果您在学习这些课程时调试器似乎出现故障,这可能是一个原因。

对于 VS Code 用户

要设置调试,请按 Ctrl+Shift+P 并选择 “C/C++: 添加调试配置”,然后选择 “C/C++: g++ 构建并调试活动文件”。这应该会创建并打开 launch.json 配置文件。将 “stopAtEntry” 设置为 true
"stopAtEntry": true,

然后打开 main.cpp 并通过按 F5 或按 Ctrl+Shift+P 并选择 “调试:启动调试并停在入口” 来开始调试。

单步执行

我们将从探索一些调试工具开始,这些工具允许我们控制程序的执行方式。

单步执行是一组相关的调试器功能的名称,它们允许我们逐语句执行(单步通过)我们的代码。

我们将依次介绍许多相关的单步执行命令。

单步进入

单步进入命令执行程序正常执行路径中的下一条语句,然后暂停程序的执行,以便我们可以使用调试器检查程序的状态。如果正在执行的语句包含函数调用,单步进入会导致程序跳转到被调用函数的顶部,并在那里暂停。

我们来看一个非常简单的程序

#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);

    return 0;
}

让我们使用 step into 命令调试这个程序。

首先,找到并执行一次 step into 调试命令。

对于 Visual Studio 用户

在 Visual Studio 中,可以通过 调试菜单 > 单步进入 或按下 F11 快捷键来访问 单步进入 命令。

对于 Code::Blocks 用户

在 Code::Blocks 中,step into 命令可以通过 Debug menu > Step into 或按 Shift-F7 组合键来访问。

对于 VS Code 用户

在 VS Code 中,可以通过 运行 > 单步进入 访问 单步进入 命令。

对于其他编译器/IDE

如果使用不同的 IDE,您很可能会在“调试”或“运行”菜单下找到“单步进入”命令。

当您的程序未运行且您执行第一个调试命令时,您可能会看到一些事情发生

  • 如果需要,程序将重新编译。
  • 程序将开始运行。由于我们的应用程序是控制台程序,因此将打开一个控制台输出窗口。它将是空的,因为我们还没有输出任何内容。
  • 您的 IDE 可能会打开一些诊断窗口,它们可能名为“诊断工具”、“调用堆栈”和“监视”。我们稍后会介绍其中一些是什么——现在您可以忽略它们。

因为我们执行了单步进入,所以您现在应该会在函数 main 的左大括号(第 9 行)左侧看到某种标记。在 Visual Studio 中,此标记是黄色箭头(Code::Blocks 使用黄色三角形)。如果您使用的是不同的 IDE,您应该会看到具有相同目的的标记。

这个箭头标记表示指向的行将是下一条要执行的行。在这种情况下,调试器告诉我们下一条要执行的行是函数 main 的左大括号(第 9 行)。

选择单步进入(使用您 IDE 的相应命令,如上所述)执行开大括号,箭头将移动到下一条语句(第 10 行)。

这意味着下一行将执行对函数 printValue 的调用。

再次选择 单步进入。因为此语句包含对 printValue 的函数调用,我们将单步进入该函数,并且箭头将移动到 printValue 函数体的顶部(第 4 行)。

再次选择“单步进入”以执行函数 printValue 的左括号,这将使箭头前进到第 5 行。

再次选择单步进入,这将执行语句std::cout << value << '\n'并将箭头移动到第6行。

警告

用于输出的 operator<< 版本是作为函数实现的。因此,您的 IDE 可能会单步进入 operator<< 函数的实现。

如果发生这种情况,您会看到 IDE 打开一个新代码文件,并且箭头标记将移动到名为 operator<< 的函数顶部(这是标准库的一部分)。关闭刚刚打开的代码文件,然后找到并执行 step out 调试命令(如果您需要帮助,说明在下面的“step out”部分)。

现在因为 std::cout << value << '\n' 已经执行,我们应该在控制台窗口中看到值 5 出现。

提示

在之前的课程中,我们提到 std::cout 是带缓冲的,这意味着您要求 std::cout 打印值与它实际打印值之间可能会有延迟。因此,您可能在此刻看不到值 5 出现。为确保 std::cout 的所有输出立即输出,您可以暂时将以下语句添加到 main() 函数的顶部

std::cout << std::unitbuf; // enable automatic flushing for std::cout (for debugging)

出于性能原因,此语句在调试后应删除或注释掉。

如果您不想不断添加/删除/注释/取消注释上述语句,您可以将该语句包装在一个条件编译预处理器指令中(在课程2.10 -- 预处理器简介中介绍)

#ifdef DEBUG
std::cout << std::unitbuf; // enable automatic flushing for std::cout (for debugging)
#endif

您需要确保已定义 DEBUG 预处理器宏,无论是在此语句上方,还是作为编译器设置的一部分。

再次选择单步进入以执行函数 printValue 的右大括号。此时,printValue 已执行完毕,控制权返回到 main

您会注意到箭头再次指向 printValue

虽然您可能认为调试器打算再次调用 printValue,但实际上调试器只是告诉您它正在从函数调用返回。

再选择单步进入三次。此时,我们已经执行了程序中的所有行,所以我们完成了。一些调试器会在此刻自动终止调试会话,另一些则可能不会。如果您的调试器没有,您可能需要在菜单中找到“停止调试”命令(在 Visual Studio 中,此命令位于调试 > 停止调试下)。

请注意,停止调试可以在调试过程中的任何时候使用,以结束调试会话。

恭喜,您现在已经单步执行了一个程序,并观察了每一行的执行!

提示

在未来的课程中,我们将探索其他调试器命令,其中一些可能只有在调试器已运行时才可用。如果所需的调试命令不可用,请单步进入您的代码以启动调试器,然后重试。

单步跳过

单步进入 类似,单步跳过命令执行程序正常执行路径中的下一条语句。但是,单步进入 会进入函数调用并逐行执行它们,而 单步跳过 会执行整个函数而不停止,并在函数执行完毕后将控制权返回给您。

对于 Visual Studio 用户

在 Visual Studio 中,单步跳过 命令可以通过 调试菜单 > 单步跳过 或按下 F10 快捷键来访问。

对于 Code::Blocks 用户

在 Code::Blocks 中,step over 命令被命名为 Next line,可以通过 Debug menu > Next line 或按下 F7 快捷键来访问。

对于 VS Code 用户

在 VS Code 中,可以通过 运行 > 单步跳过 或按下 F10 快捷键来访问 单步跳过 命令。

让我们看一个单步跳过函数调用 printValue 的例子

#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);

    return 0;
}

首先,使用 单步进入 您的程序,直到执行标记位于第 10 行

现在,选择单步跳过。调试器将执行函数(在控制台输出窗口中打印值5),然后将控制权返回到下一条语句(第12行)。

当您确定函数已经正常工作或暂时不想调试它们时,单步跳过命令提供了一种方便的跳过函数的方式。

跳出

与其他两个单步执行命令不同,跳出不仅仅执行下一行代码。相反,它会执行当前正在执行的函数中所有剩余的代码,然后在函数返回后将控制权返回给您。

对于 Visual Studio 用户

在 Visual Studio 中,单步跳出 命令可以通过 调试菜单 > 单步跳出 或按下 Shift-F11 快捷组合键来访问。

对于 Code::Blocks 用户

在 Code::Blocks 中,step out 命令可以通过 Debug menu > Step out 或按下 Ctrl-F7 快捷组合键来访问。

对于 VS Code 用户

在 VS Code 中,可以通过 运行 > 跳出 或按下 shift+F11 快捷组合键访问 跳出 命令。

我们来看一个使用上面相同程序的例子

#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);

    return 0;
}

单步进入 程序,直到您位于函数 printValue 内部,执行标记位于第 4 行。

然后选择跳出。您会注意到值5出现在输出窗口中,并且在函数终止后(在第10行),调试器将控制权返回给您。

当你不小心进入了一个你不想调试的函数时,这个命令最有用。

走得太远

在程序中单步执行时,通常只能向前单步。很容易不小心单步越过(超过)你想要检查的地方。

如果您不小心走过了目标位置,通常的做法是停止调试并重新启动调试,这次要更小心一点,不要错过目标。

后退一步

一些调试器(例如 Visual Studio Enterprise Edition 和 rr)引入了一种通常称为后退一步反向调试的单步执行功能。后退一步的目标是回溯上一步,以便您可以将程序恢复到之前的状态。如果您单步执行过度,或者您想重新检查刚刚执行的语句,这会很有用。

实现后退一步需要调试器的高度复杂性(因为它必须为每个步骤跟踪单独的程序状态)。由于复杂性,此功能尚未标准化,并且因调试器而异。截至撰写本文时(2019 年 1 月),Visual Studio Community 版和最新版 Code::Blocks 均不支持此功能。希望将来,它会渗透到这些产品中并得到更广泛的使用。

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
401 评论
最新
最早 最多投票
内联反馈
查看所有评论