调试:源代码调试器

如果调试器可以显示程序源代码的当前执行点或者程序错误的位置,这种调试器就叫做源代码调试器/符号调试器,比如说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=<读取变量时发生错误>:不能读取地址0x7fffff7feffc4    发生错误的实际代码行

分析:该程序的第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的返回值。这里不再使用步进,而是是程序继续第一个断点:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值