引言 - 调试引入的不确定性
定位问题的时候,肯定遇到过这种情况:
- 一个100%必现的代码BUG,在单步跟踪或者使用断点进行调试时,问题却再也无法重现,或者是变得很难重现(本文将实例演示这种情况)。
- 一个运行正常的程序,在使用调试器跟踪它的代码逻辑时,只要一进行单步运行或者断点调试,就会遇到很多莫名奇妙的问题。
这是为什么呢?
有经验的童鞋,会立即想到,这是因为在调试器中运行程序时,单步执行和断点调试为程序执行的时序引入了不确定性,改变了代码的执行逻辑。
尤其在复杂的多进程或多线程的系统中,即便你用调试器只跟踪你感兴趣的其中一个进程或线程,但这仍然可能会对影响到程序的正常执行逻辑。
这个时候,我们就应该调整一下思路,换一种调试方法了。
示例
示例源码如下:
#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