调试引入的不确定性:必现的BUG神秘消失

文章探讨了在调试过程中,由于单步执行和断点调试引入的不确定性可能导致原本重现的代码问题变得难以复现。作者通过实例分析了GDB单步调试与普通断点问题,并介绍了如何利用GDB的动态打印功能有效定位问题。
摘要由CSDN通过智能技术生成

引言 - 调试引入的不确定性

定位问题的时候,肯定遇到过这种情况:

  1. 一个100%必现的代码BUG,在单步跟踪或者使用断点进行调试时,问题却再也无法重现,或者是变得很难重现(本文将实例演示这种情况)。
  2. 一个运行正常的程序,在使用调试器跟踪它的代码逻辑时,只要一进行单步运行或者断点调试,就会遇到很多莫名奇妙的问题。

这是为什么呢?

有经验的童鞋,会立即想到,这是因为在调试器中运行程序时,单步执行和断点调试为程序执行的时序引入了不确定性,改变了代码的执行逻辑。

尤其在复杂的多进程或多线程的系统中,即便你用调试器只跟踪你感兴趣的其中一个进程或线程,但这仍然可能会对影响到程序的正常执行逻辑。

这个时候,我们就应该调整一下思路,换一种调试方法了。

示例

示例源码如下:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int global_variable = 4;

void alarm_handler(int signo)
{
    global_variable ++;
}

int main(int argc, char *argv[])
{
    int a, b , c;

    signal(SIGALRM, alarm_handler);
    alarm(1);

    a = 2;
    b = 3;
    c = 4;

    c = (a + b) / (global_variable - c);

    return 0;
}

简单解释下:

  • 程序中定义了一个全局变量global_variable,初始值是4。
  • main函数中捕获SIGALRM信号,并设置一个1秒钟的闹钟。
  • 闹钟超时会调用处理函数alarm_handler,把global_variable加1。

下面,编译执行一下:

root@ubuntu:debug# gcc -g alarm.c -o alarm
root@ubuntu:debug# ./alarm 
Floating point exception
root@ubuntu:debug# ./alarm 
Floating point exception
root@ubuntu:debug# ./alarm 
Floating point exception
root@ubuntu:debug# 

运行三次,每次都报浮点异常错误。

下面我们用GDB来单步调试一下。

GDB单步调试

先在main()函数入口设置断点,然后单步执行:

奇怪的是,我们单步调试的时候,程序却能够正常执行结束,没有任何异常。

这是为什么呢?

当然,这个示例程序很简单,我们一眼就看出,浮点异常肯定就出在第23行代码。

接下来,我们试试看在GDB中手动把变量打印出来。

GDB中手动打印变量

我们把断点直接设置在第23行上,然后把每个变量打印出来:

当代码断在23行时,此时用print命令打印出来c的值是4,global_variable值也是4,那么执行第23行肯定会出现除零错误。

事实上,我们在GDB中尝试用print命令打印(a + b) / (global_variable - c)的结果时,也确实报了除零的错误。

可是,当我们用continue命令继续执行程序时,程序却依然正常结束了。

这又是为什么呢?

普通断点的问题

我们前面尝试了单步执行,以及直接在第23行设置断点并打印变量的方式,可程序都能够正常执行结束,浮点异常错误无法重现。

这两种方式存在的共同问题是,在程序触发断点后,需要和用户进行交互,用户必须手动输入命令并恢复程序的执行。而和用户交互,势必引入延迟。

实际上,无论是单步执行,还是在断点触发后打印a、b、c、global_variable的值,都无法b保证程序在在1秒钟内执行完毕。因此,第17行设置的闹钟就会到期。

尽管我们用GDB调试程序时,被调试程序本身的执行被暂时停止,但是alarm函数设置的闹钟是由底层OS内核提供的服务。无论我们的程序执行是否被暂停,OS内核仍然会在设置的闹钟到期后,向应用程序发送SIGALRM信号。

如此以来,无论我们是用step命令还是continue命令恢复程序执行,程序都会首先处理SIGALRM信号,然后才去执行接下来的代码。

在SIGALRM的处理函数alarm_handler()中,会把global_variable加1,它的值变成了5,。接下来执行第23行代码时,global_variable - c的值就变成了1,当然不会再触发除零错误了。

接下来,我们用GDB的动态打印功能来调试一下。

GDB动态打印

所谓GDB动态打印,是GDB的一种特殊断点。它允许用户在调试程序时,在代码的任意位置添加一条格式化打印语句。当断点触发时,GDB会自动这条预设的格式化打印语句,并自动恢复程序的执行。

这样,我们就可以在不修改源码、不重新编译和部署的前提下,任意打印程序中的变量、内存数据等信息。

上篇文章有详细介绍:GDB动态打印:让你随时随地printf,不需修改代码,不需重新编译

更多关于程序调试、性能调优、编译器、操作系统等内容,欢迎关注公众号【原点技术】

使用GDB动态打印调试

我们用下面的命令,在第23行设置一个动态打印断点:

dprintf 23,"a = %d, b = %d, c = %d, global_variable = %d",a,b,c,global_variable

然后,用run命令执行程序:

我们看到,程序在执行第23行代码之前,先执行了我们预设的格式化打印语句,把a、b、c、global_variable的值都打印了出来。

并且,和预期一样,程序在执行过程中,果然发生了除零错误,也就是GDB中显示的SIGFPE信号导致的“Arithmetic exception”。

我们根据打印出来的变量的值,很简单就可以找到bug的真正所在!


欢迎关注微信公众号:【原点技术】,分享真正有用的东西!

进技术交流群,欢迎添加作者微信:CreCoding

原创文章,未经允许禁止转载,转载请联系作者:CreCoding

  • 46
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值