调试器可以让你暂停程序、检查和更改变量、单步执行代码。花几个小时学习一个调试器,你就能避免将来数十小时的挫败。这是一份快速指南,更多信息请见:
入门:启动与停止
gcc -g myprogram.c- 使用调试选项(-g)编译myprogram.c。你仍然得到a.out,但它包含调试信息,让你在GDB内使用变量和函数名,而不是原始内存地址(不好玩)。
gdb a.out- 打开GDB并加载文件a.out,但不运行程序。你会看到一个提示符。
(gdb)– 所有示例都基于这个提示符。
- 打开GDB并加载文件a.out,但不运行程序。你会看到一个提示符。
rr arg1 arg2r < file1- 运行之前加载的“a.out”的三种方式。你可以直接运行(r),传递参数(r arg1 arg2),或输入文件。通常在运行之前设置断点。
helph breakpoints- 列出帮助主题(help)或获取特定主题的帮助(h breakpoints)。GDB有完善的文档。
q– 退出GDB
单步执行代码(Stepping Through Code)
单步执行允许您追踪程序的路径,并聚焦于导致崩溃或返回无效输入的代码。
ll 50l myfunction- 列出当前行(l)、特定行(l 50)或函数(l myfunction)的10行源代码。
next- 运行程序直到下一行,然后暂停。如果当前行是一个函数,则执行整个函数,然后暂停。Next 适合快速浏览代码。
step- 执行下一条指令,而不是下一行。如果当前指令是设置变量,则等同于
next。如果是一个函数,则会跳入该函数,执行第一条语句,然后暂停。Step 适合深入代码细节。
- 执行下一条指令,而不是下一行。如果当前指令是设置变量,则等同于
finish- 完成当前函数的执行,然后暂停(也称为 step out)。如果意外进入了一个函数,这很有用。
断点(Breakpoint)与观察点(Watchpoint)
断点是调试的关键之一。当程序到达某个位置时,它们会暂停(中断)程序。您可以检查并更改变量,然后继续执行。这在查看为什么某些输入失败或测试输入时很有帮助。
break 45break myfunction- 在第45行或 myfunction 处设置断点。程序在到达断点时会暂停。
watch x == 3- 设置一个监视点,当条件发生变化时(例如 x == 3 变化)暂停程序。监视点对于特定输入(如 myPtr != NULL)非常有用,无需在 每一个 函数调用处设置断点。
continue- 在断点/监视点暂停后恢复执行。程序将继续运行,直到遇到下一个断点/监视点。
delete N- 删除断点 N(创建时断点会编号)。
设置变量与调用函数
在运行时查看和改变变量是调试的重要组成部分。尝试给函数输入无效参数或运行其他测试用例以找到问题的根源。通常,您会在程序暂停时查看/设置变量。
print x- 打印变量 x 的当前值。能够使用原始变量名正是需要 (-g) 标志的原因;常规编译的程序会移除这些信息。
set x = 3set x = y- 将 x 设置为某个值(3)或另一个变量(y)
call myfunction()call myotherfunction(x)call strlen(mystring)- 调用用户自定义或系统函数。这非常有用,但要小心调用有 bug 的函数。
display xundisplay x- 持续显示变量 x 的值,每次单步执行或暂停后都会显示。如果经常检查某个特定值,这很有用。使用 undisplay 移除持续显示。
回溯(Backtrace)与切换帧(Frame)
这个 栈 是当前函数调用的列表——它显示您在程序中的位置。 帧 存储单个函数调用的详细信息,例如参数。
bt回溯(Backtrace),即打印当前函数堆栈以显示您在当前程序中的位置。如果
main调用函数a(),该函数又调用b(),该函数又调用c(),则回溯为c <= current location b a main
bt full– 回溯,包括局部变量updown- 在函数栈中向上或向下移动到下一个帧。如果你在
c,你可以移动到b或者a来检查局部变量。
- 在函数栈中向上或向下移动到下一个帧。如果你在
return- 从当前函数返回。
崩溃(Crash)与核心转储(Core Dump)
“核心转储(core dump)”是程序崩溃时内存的快照,通常保存在名为“core”的文件中。GDB 可以读取核心转储并给出崩溃的行号、传递的参数等信息。这非常有用,但请记得使用 (-g) 编译,否则核心转储将难以调试。
gdb myprogram core- 使用“core”作为核心转储文件来调试 myprogram。
bt- 打印崩溃时的回溯(函数栈)。使用上述技术检查变量。
信号处理
信号是特定事件(如定时器或错误)后发送的消息。GDB 可能在遇到信号时暂停;您可能希望忽略它们。
handle [signalname] [action]handle SIGUSR1 nostophandle SIGUSR1 noprinthandle SIGUSR1 ignore- 告诉 GDB 忽略某个信号(
SIGUSR1)当它发生时。有不同级别的忽略。
- 告诉 GDB 忽略某个信号(
与 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(或其他调试工具)吧!我知道,这就像让人吃蔬菜一样,但它真的对你有益——你以后会感谢我的。