在不同的开发环境中,如Eclipse或Visual Studio,有许多关于如何访问调用栈的信息。然而,关于如何实际理解调用栈的信息却相对较少。本文将介绍一种使用调用栈进行调试的策略。将以一个用C++编写并在Visual Studio 2012中开发的个人项目为例,但会尽量提供适用于其他编程语言和IDE的通用策略和结论。对于经验丰富的程序员来说,这里提供的信息可能看起来微不足道,但对于没有从更资深的同事那里了解到这种技术的人来说,这些信息可能是至关重要且难以找到的。
在运行项目的一个测试时,抛出了以下异常:
异常清楚地表明它是在函数getPixelFromMiddle()
的第116行抛出的,该函数位于ColorBox.cpp
中。在第一步中,找到抛出异常的代码行是一个简单的任务。在定位到这行代码后,在其上设置一个断点。这是bug出现的地方,所以将需要使用调用栈深入代码中找到导致它的冲突状态。
如果程序崩溃而没有明确指出发生了意外,使用断点并在几次测试运行中改变它们的位置,以找到程序崩溃的代码行。
在正确的位置设置断点后,再次以调试模式运行程序。执行在断点处停止。导航到调用栈窗口(在VS中,通过菜单Debug -> Window -> Call Stack),将看到调用彼此的函数的堆叠列表。函数ColorBox::getPixelFromMiddle()
位于栈顶,因为这是程序执行停止时最后一个被调用的函数。ColorBox::getPixelFromMiddle()
下面的函数是它的调用者,VS标记了程序执行在从getPixelFromMiddle()
返回后将恢复的代码行。
示例中的调用栈有以下条目:
ColorBox::getPixelFromMiddle()
被Schlieren::colorFinalImageAt()
调用,它又被Schlieren::generateFinalImage()
调用,它又被Test::schlierenProcessing()
调用。
访问调用栈,可以检查程序执行在某一时刻冻结的情况,显示其所有内部细节。
现在可以使用调用栈调试的强大功能。在调试时,调用栈提供了两种不同类型的信息。首先,有程序执行序列,可以检查应用程序的实际业务逻辑是否符合意图(当调试自己的代码时)。第二件重要的信息是程序状态,即在程序执行达到断点时所有变量和对象持有的值。使用在编辑器中可用的最喜欢的技术来检查感兴趣的变量的值(在VS中,通过将鼠标悬停在它们上面或使用Autos、Locals或Watch窗口等)。
现在拥有所有信息,可以进行快速而简单的调查,找到导致异常触发的状态冲突(bug)。
在示例中,异常抛出是因为变量w
持有一个值为65531的值。w
是处理图像时的当前宽度值,因此它应该在0和图像宽度之间。它是uint16_t
类型,所以最大值是65535。65531的值非常接近uint16_t
类型的最大值,这表明试图将负值-5分配给这个无符号变量,结果为65531(65536-5)。宽度应该是一个非负数,所以状态冲突是在将值-5分配给w
的代码行。
使用调用栈回溯到上一个函数:Schlieren::colorFinalImageAt()
(在栈上点击getPixelFromMiddle()
下方的它)。
当鼠标指针悬停在用于调用getPixelFromMiddle()
的wOffset
上时,可以看到它确实有-5的值。已经识别出了bug。在调用代码(colorFinalImageAt()
)中,调用getPixelFromMiddle(wOffset = -5, hOffset = 4)
,而它的签名是getPixelFromMiddle(uint16_t w, uint16_t h)
。给无符号整型变量负值导致了状态冲突。
注意:请注意,bug可能比示例中显示的要远离它出现的地方。只使用这个简单但真实的例子来展示调用栈调试的策略,而不会过多地涉及特定代码体的技术细节。
找到导致状态冲突的代码逻辑不一致性几乎会自动产生解决方案。类ColorBox
持有一个大小为m_size X m_size
的正方形RGB图像(成员变量m_colorData
)。成员函数getPixelFromMiddle(uint16_t w, uint16_t h)
接收一个宽度和高度值。这些包含从图像中间偏移的宽度和高度值。因此,它们可以持有负值,所以它们应该是有符号整型。除了改变类型外,还有一个更微妙的变化要做:两个函数参数应该在getPixelFromMiddle()
中重命名为wOffsetFromMiddle
和hOffsetFromMiddle
。如果从一开始就使用这些名称,那么它们应该是有符号整型就会变得很明显。