从release version的困境中走出(一)
翻译:cppbug cpp_bug@hotmail.com
简介
你的程序已经可以正常工作。你已经认真的测试了程序的方方面面。该是把它卖出去的时候了。于是你作出release version。怎么回事?一些在debug version中从没发生过的错误却从天而降。内存访问错误,没有按预期跳出对话框,结果也不正确。这究竟是为什么?ok,这篇文章将会告诉你原因。
Compiler bugs
当我们遇到上面所描述的问题时,“编译器存在bugs”通常会是我们的第一反应:原本一直正常的程序怎么会突然变得不正常?我只是从debug version变成了release version呀!!虽然编译器存在bugs的可能性是存在的,但我们还是应该把它作为最后的求助。首先应该想到的是程序自身一定还存在问题。后面我们会回到“compiler bugs”的问题上来,但是现在先认定编译器是正确的。
Storage allocator Issues
MFC debug version的运行期内存配置与release version是不同的。通常,debug version会在每个存储块的开始和末尾进行内存分配。在内存分配上的差异可能会导致一些在debug version中从未出现过的问题突然使你不知所措。因此,当某些问题在debug version中没有被检测出来的话,那么这些错误往往就是极其隐蔽和致命的。不过你不用过度恐慌,这种情况通常很少发生。
为什么这种情况很少呢?因为MFC debug version的allocator会把所有的内存都初始化为一定的伪值(bogus values),所以在debug version下任何试图使用一个未分配成功的存储块的行为都会立即得到一个访问错误通知。另外,当一块内存被释放,它将会被设置为另一种模式,如果你还想要通过指针对它进行访问的话,同样会给你一个访问错误通知。
Debug allocator任何时候都会检查它所分配的内存块的开头和末尾是否被破坏。典型的例子就是你分配了一个含有n个元素的内存块作为数组,接着你访问元素0到元素n,而不是从0到n-1,因此这就破坏了数组的末端。这种情况下基本上会产生一个访问错误。但是请你记住,任何东西都不是绝对的,这里也不例外。并且一个极其隐蔽的错误或许已经开始在你的眼皮底下滋生。
内存往往是以量化的块(chunk)的形式进行分配,虽然块的大小没有明确的规定,但通常都是16或32字节。因此,如果你分配一个含有6个元素的DWORD数组(size = 6 * sizeof(DWORD) bytes = 24 bytes),那么allocator实际上会给你32字节而不是24字节(一个32字节的块或两个16字节块)。这时如果你写element[6](第7个元素), 被称为“dead space”的区域会被覆盖,但是错误却没有被检测出来。在release version中却不一定这样,由于优化的存在,块的大小可能是8字节,这样3个8字节会被分配,如果你写element[6]的话,将会覆盖下一个内存块。真是一个令人沮丧的消息。这些错误可能并不会立即显现出来直到你发现程序突然意外终止!你可以用任意大小的块构建近似的“boundary condition”环境。因为对于两种版本的allocator来说可以分配相同大小的块,但是debug version的allocator会出于自己的目的添加隐藏区域,所以在debug version和release version中你将会得到两种不同的内存分配模式。
未初始化的局部变量
可能最会引起release-vs-debug错误的是未初始化局部变量的存在。考虑一个简单的例子:
thing * search(thing * something)
BOOL found;
for(int i = 0; i < whatever.GetSize(); i++)
{
if(whatever[i]->field == something->field)
{ /* found it */
found = TRUE;
break;
} /* found it */
}
if(found)
return whatever[i];
else
return NULL;
}
这是一段如此优雅直观的程序,唯一的缺陷就在于它没有把found变量初始化为FALSE。但是这个bug在debug version中永远不会被发现!那么在release version中将会发生什么? 你一定想到了,问题就是出在那个数组身上,它拥有n个元素,所以无论什么时候element[n]被返回都是一个明显的违规。但这个错误为什么在debug version中不会出现呢?因为在debug version中,found的值通常会被初始化为0(FALSE),因此当fail-search,循环结束的时候,它将会正确的报告查找失败,并且NULL被返回。然而在release version中却往往不会进行初始化。
为什么堆栈在二者之间会有所不同呢?在debug version中,frame pointer 通常会在程序入口处被压入栈中,并且每个变量也都会在栈中占有一席之地。然而在release version中却不是这样,编译器的优化可能会检测到frame pointer并不需要,或者变量的位置可以从stack pointer中获得(一种技术被称为frame pointer simulation),所以frame pointer并不会被压入到栈中。另外,编译器还会把一些大量使用变量放入寄存器而不是压入栈中。因此,一个变量的初始化会取决于很多因素。
除了仔细的阅读代码,还可以求助于一些高级的编译器诊断工具,否则基本上没有办法检测到未初始化的局部变量。我非常喜欢Gimpel Lint,这是一个非常出色的工具,我强烈推荐给你们。
Bounds Errors
许多有效的优化可以使曾经在debug version中隐藏的错误浮现出来。是的,有时这是编译器的错误,但是在99%的时间里,这是一个在没有优化的情况下不会显现出来的逻辑错误。例如,
void func()
{
char buffer[10];
int counter;
lstrcpy(buffer, "abcdefghik"); // 11-byte copy, including NULL
...
在debug version中,字符串尾的NULL字节会覆盖变量counter的高位字节,这里并不会有什么危害。然而在优化编译器中,counter往往被移入一个寄存器,并且不会被压入栈,这样NULL字节就会覆盖跟在buffer后面的数据,那个数据很可能是函数的返回地址,这样在函数返回时就会引起一个访问错误。
当然,这种情况对程序的布局很敏感。如果换成以下程序:
void func()
{
char buffer[10];
int counter;
char result[20];
wsprintf(result, _T("Result = %d"), counter);
lstrcpy(buffer, _T("abcdefghik")); // 11-byte copy, including NUL
在debug version中NULL字节会覆盖counter的高字节(但是这里并不会有什么不良影响,因为counter在使用wsprintf的语句后面就不再需要了),但是在release version中它会覆盖result的第一个字节,不过result的使命已经完成,那也就没有什么关系了,但是如果result是一个char*指针或者其他的指针,只要你通过它进行访问的话就会得到一个访问错误。注意:程序在debug version下会正常工作,但是不知不觉中错误已经被隐藏。
在这种情况下,你需要建立一个有调试信息的可执行体版本,然后使用
break-on-value-changed的方法来寻找错误的覆盖。有时你还必须非常有创造力的来追踪这些错误。