使用 GDB 调试

调试器可以让你暂停程序、检查和更改变量、单步执行代码。花几个小时学习一个调试器,你就能避免将来数十小时的挫败。这是一份快速指南,更多信息请见:

入门:启动与停止

  • gcc -g myprogram.c
    • 使用调试选项(-g)编译myprogram.c。你仍然得到a.out,但它包含调试信息,让你在GDB内使用变量和函数名,而不是原始内存地址(不好玩)。
  • gdb a.out
    • 打开GDB并加载文件a.out,但不运行程序。你会看到一个提示符。 (gdb) – 所有示例都基于这个提示符。
  • r
  • r arg1 arg2
  • r < file1
    • 运行之前加载的“a.out”的三种方式。你可以直接运行(r),传递参数(r arg1 arg2),或输入文件。通常在运行之前设置断点。
  • help
  • h breakpoints
    • 列出帮助主题(help)或获取特定主题的帮助(h breakpoints)。GDB有完善的文档。
  • q – 退出GDB

单步执行代码(Stepping Through Code)

单步执行允许您追踪程序的路径,并聚焦于导致崩溃或返回无效输入的代码。

  • l
  • l 50
  • l myfunction
    • 列出当前行(l)、特定行(l 50)或函数(l myfunction)的10行源代码。
  • next
    • 运行程序直到下一行,然后暂停。如果当前行是一个函数,则执行整个函数,然后暂停。Next 适合快速浏览代码。
  • step
    • 执行下一条指令,而不是下一行。如果当前指令是设置变量,则等同于 next。如果是一个函数,则会跳入该函数,执行第一条语句,然后暂停。Step 适合深入代码细节。
  • finish
    • 完成当前函数的执行,然后暂停(也称为 step out)。如果意外进入了一个函数,这很有用。

断点(Breakpoint)与观察点(Watchpoint)

断点是调试的关键之一。当程序到达某个位置时,它们会暂停(中断)程序。您可以检查并更改变量,然后继续执行。这在查看为什么某些输入失败或测试输入时很有帮助。

  • break 45
  • break myfunction
    • 在第45行或 myfunction 处设置断点。程序在到达断点时会暂停。
  • watch x == 3
    • 设置一个监视点,当条件发生变化时(例如 x == 3 变化)暂停程序。监视点对于特定输入(如 myPtr != NULL)非常有用,无需在 每一个 函数调用处设置断点。
  • continue
    • 在断点/监视点暂停后恢复执行。程序将继续运行,直到遇到下一个断点/监视点。
  • delete N
    • 删除断点 N(创建时断点会编号)。

设置变量与调用函数

在运行时查看和改变变量是调试的重要组成部分。尝试给函数输入无效参数或运行其他测试用例以找到问题的根源。通常,您会在程序暂停时查看/设置变量。

  • print x
    • 打印变量 x 的当前值。能够使用原始变量名正是需要 (-g) 标志的原因;常规编译的程序会移除这些信息。
  • set x = 3
  • set x = y
    • 将 x 设置为某个值(3)或另一个变量(y)
  • call myfunction()
  • call myotherfunction(x)
  • call strlen(mystring)
    • 调用用户自定义或系统函数。这非常有用,但要小心调用有 bug 的函数。
  • display x
  • undisplay x
    • 持续显示变量 x 的值,每次单步执行或暂停后都会显示。如果经常检查某个特定值,这很有用。使用 undisplay 移除持续显示。

回溯(Backtrace)与切换帧(Frame)

这个 是当前函数调用的列表——它显示您在程序中的位置。 存储单个函数调用的详细信息,例如参数。

  • bt

    • 回溯(Backtrace),即打印当前函数堆栈以显示您在当前程序中的位置。如果 main 调用函数 a(),该函数又调用 b(),该函数又调用 c(),则回溯为

      c <= current location
      b
      a
      main
      
  • bt full – 回溯,包括局部变量
  • up
  • down
    • 在函数栈中向上或向下移动到下一个帧。如果你在 c,你可以移动到 b 或者 a 来检查局部变量。
  • return
    • 从当前函数返回。

崩溃(Crash)与核心转储(Core Dump)

“核心转储(core dump)”是程序崩溃时内存的快照,通常保存在名为“core”的文件中。GDB 可以读取核心转储并给出崩溃的行号、传递的参数等信息。这非常有用,但请记得使用 (-g) 编译,否则核心转储将难以调试。

  • gdb myprogram core
    • 使用“core”作为核心转储文件来调试 myprogram。
  • bt
    • 打印崩溃时的回溯(函数栈)。使用上述技术检查变量。

信号处理

信号是特定事件(如定时器或错误)后发送的消息。GDB 可能在遇到信号时暂停;您可能希望忽略它们。

  • handle [signalname] [action]
  • handle SIGUSR1 nostop
  • handle SIGUSR1 noprint
  • handle SIGUSR1 ignore
    • 告诉 GDB 忽略某个信号(SIGUSR1)当它发生时。有不同级别的忽略。

与 Emacs 的集成

Emacs 文本编辑器与 GDB 集成得很好。直接在编辑器中调试很棒,因为您可以一次看到整个屏幕的代码。使用 M-x gdb 启动一个新窗口运行 GDB 并 在此了解更多.

技巧

  • 我常常更喜欢观察点(Watchpoint)而不是断点(Breakpoint)。与其在每个循环中断并检查变量,不如设置一个观察点,当变量达到所需值时触发(i == 25, ptr != null 等)。
  • printf 适用于跟踪(tracing)。但将 printf 在一个log 函数中以获得灵活性。
  • 尝试为你的消息传递一个日志级别(1为最重要,3为最不重要)。你可以调整日志函数,使其在关键错误时发送邮件、记录到文件等。
  • 代码胜于雄辩,直接看代码吧。使用 #define LOG_LEVEL LOG_WARN 来显示警告及以上级别。使用 #define LOG_LEVEL LOG_NONE 来关闭调试。

#include 

#define LOG_NONE 0
#define LOG_ERROR 1
#define LOG_WARN 2
#define LOG_INFO 3
#define LOG_LEVEL LOG_WARN


// shows msg if allowed by LOG_LEVEL
int log(char *msg, int level){
  if (LOG_LEVEL >= level){
    printf("LOG %d: %s\n", level, msg);
    // could also log to file
  }

  return 0;
}

int main(int argc, char** argv){
  printf("Hi there!\n");

  log("Really bad error!", LOG_ERROR);
  log("Warning, not so serious.", LOG_WARN);
  log("Just some info, not that important.", LOG_INFO);

  return 0;
}

  • 花时间学习 GDB(或其他调试工具)吧!我知道,这就像让人吃蔬菜一样,但它真的对你有益——你以后会感谢我的。

加入 45 万月度读者

喜欢这篇文章?还有更多内容能帮你建立持久、直观的数学理解。加入通讯以获取额外内容和最新更新。