GNU项目调试器(GDB)
了解 GNU 调试器(GDB),这是一个功能强大的 C 语言调试工具,学习如何运行它并查看堆栈跟踪。
调试 是识别代码中错误或缺陷来源并分析其原因的过程。它有助于修复程序崩溃、消除逻辑错误,并使我们的程序更加高效。
什么是符号化调试器?
调试器 是一个运行其他程序并提供控制该程序执行能力的程序,例如逐行执行并检查程序运行时的变量。当程序崩溃且 UNIX 给出模糊的错误信息时,调试器能帮助我们确定崩溃位置,并(结合我们的思考)弄清崩溃原因。
为什么不使用 printf?
一种快速且简单的调试方法,无需额外的调试程序,就是向代码中插入 printf 语句,将我们认为与崩溃(或其他错误)相关的变量值输出到屏幕上。有些人称这种方法为在程序中添加“追踪代码”。
使用追踪代码进行调试的缺点是,可能需要在程序各处添加大量 printf 语句,插入和移除都很麻烦。此外,符号化调试器能完成远超简单追踪代码的功能,例如:
- 暂停程序执行。
- 查看变量的当前值。
- 跳转到任意代码行。
- 计算表达式。
- 从断点处重新开始执行。
使用调试器可以实现更精细的控制。最后,我们可以使用 GDB 调试一个已经崩溃的程序,而无需重新启动程序。这样我们就能查看程序在崩溃瞬间的状态和变量情况,从而检查变量值、崩溃发生的位置等信息。
为 GDB 编译程序
GNU 调试器(GDB)是 C 开发者广泛使用的调试工具。为了让 GDB 能够运行我们的程序,必须使用特殊编译标志 -g 进行编译。下面是一个用于演示 GDB 使用方法的简单程序 go.c:
1#include <stdio.h> 2#include <stdlib.h> 3 4char *getWord(int maxsize) 5{ 6 char *s; 7 s = calloc(sizeof(char), maxsize); 8 printf("Enter a string (max %d chars): ", maxsize); 9 scanf("%s", s); 10 return s; 11} 12 13void repeatWord(char *s, int n) 14{ 15 int i; 16 for (i = 0; i < n; i++) { 17 printf("%s\n", s); 18 } 19} 20 21int *getVec(int vecsize) 22{ 23 int *vec = malloc(sizeof(int)*vecsize); 24 int i; 25 for (i = 0; i < vecsize; i++) { 26 printf("Enter value of vec[%d]: ", i); 27 scanf("%d", &vec[i]); 28 } 29 return vec; 30} 31 32void printVec(int *vec, int size) 33{ 34 int i; 35 printf("vec = {"); 36 for (i = 0; i < size - 1; i++) { 37 printf("%d, ", vec[i]); 38 } 39 printf("%d}\n", vec[size - 1]); 40} 41 42int main(int argc, char *argv[]) 43{ 44 char *myWord = getWord(256); 45 repeatWord(myWord, 3); 46 47 int *myVec = getVec(5); 48 printVec(myVec, 5); 49 50 free(myWord); 51 free(myVec); 52 53 return 0; 54}
该文件包含一个输入字符串的函数,另一个用于重复打印字符串若干次的函数;还包含一个输入数值的函数和一个打印输入数值的函数。
以下是程序执行时的示例输出:
1$ gcc -g -o go go.c 2$ ./go 3Enter a string (max 256 chars): TheCat 4TheCat 5TheCat 6TheCat 7Enter value of vec[0]: 3 8Enter value of vec[1]: 1 9Enter value of vec[2]: 4 10Enter value of vec[3]: 1 11Enter value of vec[4]: 5 12vec = {3, 1, 4, 1, 5}
注意我们使用了 -g 编译标志。这使得程序能够被 GDB 使用。
栈帧
我们在讨论 C 中变量分配方式时已经提到过栈。这一概念再次出现,同时引入了一个称为 帧 的概念。现在我们简要回顾重点和核心思想。
栈实际上由多个 栈帧 构成。每个帧代表一次函数调用。因此,随着程序调用函数,栈帧数量增加,栈也随之增长。当函数返回(退出)时,栈会缩小,栈帧被弹出。
每当程序中调用一个函数时,栈中就会为其分配一块内存空间(即栈帧)。这块内存包含函数内部声明的变量存储空间、调用当前函数的上一级函数的行号以及该函数的输入参数。这些信息都可以在 GDB 中进行检查。
特别是当程序崩溃时,我们可以让 GDB 告诉我们崩溃的位置,并请求获取一个 栈回溯(stack trace),即列出当前栈中所有的栈帧。
使用 gdb 命令检查栈
作为示例,让我们修改上述代码,引入一个导致程序崩溃的错误。请注释掉第 7 行代码:
1// s = calloc(sizeof(char), maxsize);
现在运行程序,会出现如下结果:
1$ gcc -g -o go go.c 2$ ./go 3Enter a string (max 256 chars): TheCat 4Segmentation fault (core dumped)
接下来,我们通过 gdb 命令运行程序:
1/usercode$ gdb go 2GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2 3Copyright (C) 2020 Free Software Foundation, Inc. 4License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> 5This is free software: you are free to change and redistribute it. 6There is NO WARRANTY, to the extent permitted by law. 7Type "show copying" and "show warranty" for details. 8This GDB was configured as "x86_64-linux-gnu". 9Type "show configuration" for configuration details. 10For bug reporting instructions, please see: 11<http://www.gnu.org/software/gdb/bugs/>. 12Find the GDB manual and other documentation resources online at: 13 <http://www.gnu.org/software/gdb/documentation/>. 14 15For help, type "help". 16Type "apropos word" to search for commands related to "word"... 17Reading symbols from go... 18(gdb) run 19Starting program: /usercode/go 20warning: Error disabling address space randomization: Operation not permitted 21Enter a string (max 256 chars): TheCat 22 23Program received signal SIGSEGV, Segmentation fault. 24__vfscanf_internal (s=<optimized out>, format=<optimized out>, argptr=argptr@entry=0x7ffc47048fc0, 25 mode_flags=mode_flags@entry=2) at vfscanf-internal.c:1100 261100 vfscanf-internal.c: No such file or directory. 27(gdb)
我们使用命令 gdb go 启动调试器,然后输入 run 命令从 GDB 内部运行程序。与之前一样,程序崩溃了,但错误信息略显晦涩。此时可以输入 backtrace 命令查看栈帧列表:
1(gdb) backtrace 2#0 _IO_vfscanf_internal (s=<optimized out>, format=<optimized out>, 3 argptr=0x7fffffffe048, errp=0x0) at vfscanf.c:1095 4#1 0x00007ffff7a79fdd in __isoc99_scanf (format=<optimized out>) 5 at isoc99_scanf.c:37 6#2 0x000000000040067f in getWord (maxsize=256) at go.c:9 7#3 0x00000000004007cc in main (argc=1, argv=0x7fffffffe258) at go.c:44 8(gdb)
我们看到的是一个编号的栈帧列表。编号为 #3 的对应 main 函数,#2 对应 getWord 函数,#1 对应 scanf 函数,而 #0 是 scanf 内部使用的某个函数。
这个栈帧列表让我们了解了程序当前所处的位置以及函数之间的调用关系。请注意,每个栈帧条目都包含了源文件名和行号,这非常有用。例如,我们知道崩溃发生在 go.c 文件的第 9 行(见 #2 条目的末尾显示 go.c:9),并且从 #1 条目可知,问题出在对 scanf 的调用。
现在我们可以查看代码,尝试找出 scanf 失败的原因。我们只传入了两个参数:格式字符串和用于存放数据的缓冲区指针。格式字符串 "%s" 看起来没问题,因此我们可以使用 GDB 的 print 命令检查缓冲区内容,这将在下一课中介绍。