如果调试器可以显示程序源代码的当前执行点或者程序错误的位置,这种调试器就叫做源代码调试器/符号调试器,比如说GDB和Visual Studio
程序
#include <stdio.h>
#include <stdlib.h>
int factorial(int n){
int result = 1;
if(n == 0){
return result;
}
result = factorial(n - 1) * n;
return result;
}
int main(int argc, char** argv)
{
int n , result;
if(argc != 2){
fprintf(stderr, "usage: main n, n >= 0\n");
}
n = atoi(argv[1]);
result = factorial(n);
printf("factorial %d = %d\n", n, result);
return 0;
}
使调试器与程序一起运行
为了开始进行调试,需要使得程序与调试器一起运行。必须指示编译器将调试信息放到程序的对象代码中。这些调试信息也叫做调试符号或符号信息。它们包括函数和变量的名称以及CPU指令、源文件和行号之间的关系。注意,大多数编译器在默认或者优化模式下未启用调试,因为对象代码中的调试会使得程序变得更大。此外,大多数编译器优化在调试模式下是禁用的,因此程序会运行的较慢。
对于GNU编译器GCC/G++以及大部分其他编译器,进行调试的编译器标识是-g,因此:
g++ -g -o factorial main.cpp
下一步是将程序载入调试器,并运行它。调试器将一直出于运行模式,直到被打断、程序崩溃或者程序退出。
对于GDB调试器,需要键入命令gdb,并输入程序名称作为它的第一个参数。GDB将启用一个命令行解释器,在这里可以输入命令来控制调试器。运行程序的命令是run
,后跟着要传递给程序的命令行参数:
$ gdb factorial
一大堆免责信息
(gdb) run 1
Starting program: /home/oceanstar/CLionProjects/debug_learning/factorial 1
factorial 1 = 1
[Inferior 1 (process 30667) exited normally]
从上面可以看出:
(gdb) run 1
开始进程: 程序二进制完整路径 程序参数为1
print打印
[Inferior 1 (进程30667) 正常退出]
接下来我们调试一个异常:用参数n=-1来运行程序,浙江导致递归不能正常停止:
(gdb) run -1
Starting program: /home/oceanstar/CLionProjects/debug_learning/factorial -1
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400615 in factorial (n=<error reading variable: Cannot access memory at address 0x7fffff7feffc>) at main.cpp:4
4 int factorial(int n){
从上面可以看出:
(gdb) run 1
开始进程: 程序二进制完整路径 程序参数为-1
进程收到SIGSEGV,段错误
错误发生在在main.cpp的第4行的地址0x0000000000400615 处(n=<读取变量时发生错误>:不能读取地址0x7fffff7feffc)
4 发生错误的实际代码行
分析:该程序的第4行引起段错误,这是一个内存错误。
如果是使用VS调试,将打印:
在程序崩溃时执行栈跟踪
VS已经说明了崩溃原因:C/C++程序的栈是一个内存片段,用来存储每个活动的函数调用的栈帧(stack frame)。栈帧由返回地址、函数的参数和局部变量组成。栈跟踪(stack stace)是一个实际的栈帧链,这个链从调试器当前停止或者暂停的最顶部函数开始,向下一直到main函数。当嵌套函数调用的链过长,造成栈没有足够内存来存储当前栈帧时,就发生了栈溢出。
除了在源代码中显示程序崩溃的位置之外,调试器还显示栈帧和崩溃的栈跟踪。栈跟踪是用于调试崩溃位置的有用信息,因为它可以告诉我们导致崩溃的函数调用链
GDB调试器通过编号来引用栈帧,其中当前栈帧的编号是0,main()函数的栈帧编号最高。这个数字也是调用栈的大小。GDB的栈跟踪命令是bt、backtrace、where
(全是一个程序,只是不同的名字)。栈跟踪确定本例中的程序是程序的栈溢出,原因是对factorial()函数进行了过多的递归调用。
可以在调用栈中进行上下导航,并检查函数参数和局部变量的值。在GDB中,可以使用命令up
或者down
在栈中移动。
使用断点
交互式调试要求能够在程序终止前挂起程序的运行,并且能够以可控制的方式在程序代码中导航,这是通过断点实现的。调试器提供了一系列断点命令如下:
- 行断点:当到达源代码中的指定行时,暂停程序
- 函数断点:当到达指定函数的第一行时,暂停程序
- 条件断点:如果特定条件保持为真,暂停程序
- 事件断点:当发生特定事件时,暂停程序。支持的事件包括来自操作系统的signals和C++异常
调试
一些用于暂停和运行程序的命令:
run
:run命令将开始程序。可以通过命令行参数或者环境变量来控制和更改运行程序的环境start
:start命令将允许程序,直到main()的第一行,然后停止程序的执行。这样就不会所示包含main()函数的文件,并在其第一行设置显示的断点了pause
:pause命令将中断一个正在运行的程序。在某些调试器中,键入Ctrl-C或单击Pause键将起到相同的效果continue
:调试器命令continue使暂停的程序恢复执行
为了理解复杂代码片段的行为,需要逐行遍历源代码。调试器提供了逐行步进(stepping)的功能。由三种不同的步进模式:
step-into
:调试器命令step-into
(在GDB中是step
)的作用是移动到下一个可执行的代码行。如果当前行是一个函数调用,则调试器将进入函数,并停止在函数体的第一行。step-over
:调试器命令step-over
(在GDB中是next
)的作用是在同一个调用栈层中移动到下一个可执行的代码行。如果当前行是一个函数调用,则调用器将在函数调用之后的下一条语句停止。调试器不会进入函数体。如果当前行是函数的最后一行,则step-over将进入下一个函数栈层,并在调用函数的下一行停止。step-out
:调试器命令step-out
(在GDB中是finish
)的作用是在栈中前进到下一层,并在调用函数的下一行停止。
看个例子
目的:用调试器来查明为什么当n>=13时,程序返回错误值。(程序13!的返回结果是1932053504,而正确的值应该是6227020800)
我们需要检测递归调用是否正常工作,因此在第5行设置一个断点,以便检测当n=0时,函数不再调用自己。另外,在第11行设置一个断点,以便能够查看每次调用factorial的返回值。这里不再使用步进,而是是程序继续第一个断点:
…
…