/MT和/MD构建会崩溃,但只有在未连接调试器时才会发生:如何调试?

9
我有一个小型的单线程C++应用程序,使用Visual Studio 2005编译和链接,其中使用了boost(crc、program_options和tokenizer)、少量STL和其他系统头文件。(它的主要目的是读取.csv并生成自定义二进制.dat以及一对.h结构声明,用于“解释”.dat的格式。)这个工具在非调试模式下崩溃(访问空指针异常),仅在发布版本中出现。例如,按F5不会导致工具崩溃,而Ctrl-F5会导致崩溃。当我重新附加调试器时,我得到了这个堆栈:
ntdll.dll!_RtlAllocateHeap@12()  + 0x26916 bytes    
csv2bin.exe!malloc(unsigned int size=0x00000014)  Line 163 + 0x63 bytes C
csv2bin.exe!operator new(unsigned int size=0x00000014)  Line 59 + 0x8 bytes C++
>csv2bin.exe!Record::addField(const char * string=0x0034aac8)  Line 62 + 0x7 bytes  C++
csv2bin.exe!main(int argc=0x00000007, char * * argv=0x00343998)  Line 253   C++
csv2bin.exe!__tmainCRTStartup()  Line 327 + 0x12 bytes  C

它崩溃的那一行代码是一个看起来很普通的分配操作:
pField = new NumberField(this, static_cast<NumberFieldInfo*>(pFieldInfo));

我认为它还没有到达构造函数,只是在跳转到构造函数之前分配内存。通常情况下,在崩溃之前该代码段已经执行了数十次,通常在一个一致的(但不可疑的)位置崩溃。

当使用/MTd或/MDd(调试运行时)编译时,问题消失了,并且当使用/MT或/MD时,问题会重新出现。

NULL从堆栈加载,并且我可以在内存视图中看到它。_RtlAllocateHeap@12 + 0x26916字节似乎是一个巨大的偏移量,就像做了一个错误的跳转。

我尝试在调试版本中使用“_HAS_ITERATOR_DEBUGGING”,但没有找到任何可疑的问题。

在Record::addField的开头和结尾放置HeapValidate显示,直到崩溃时都有一个OK堆。

这以前曾经有效 - 我不确定现在和上次我们编译工具之间发生了什么变化(可能是几年前,也许是在较旧的VS下)。我们尝试过一个较旧版本的boost(1.36 vs 1.38)。

在手动检查代码或将其馈送给PC-Lint并仔细查看输出之前,您有关于如何有效地调试这个问题的任何建议吗?

(如果您在评论中请求信息,我很乐意更新问题。)


我曾经有过一次半反向工程RtlHeap的经历,用于解决类似的错误。不要被巨大的偏移量所困扰 - 这是正常的。调试符号(您似乎正在使用)显然缺少一些私有函数(可能在源文件中声明为“静态”),并且对于RtlAllocateHeap或RtlAllocateHeapSlowly获得巨大的偏移量只意味着它找到的最接近的符号。 - Chris Walton
@arke:是的,我发帖后不久就意识到了这一点。(应该回去编辑一下。)在此之前,我曾经在工作中编写过一个查找工具,用于解析Codewarrior生成的xMAP文件,并偶尔遇到了同样的问题——只是没有想到我不一定会拥有所有这里的符号,出现了这种情况。 - leander
3个回答

13

运行时是否附加调试器有一个鲜为人知的区别,那就是操作系统的调试堆(参见为什么我在使用调试器时代码会运行缓慢?)。您可以使用环境变量_NO_DEBUG_HEAP关闭调试堆。您可以在计算机属性中或在Visual Studio的项目设置中指定此设置。

一旦关闭调试堆,即使附加了调试器,您也应该看到相同的崩溃。

尽管如此,请注意,内存破坏可能很难调试,因为破坏的真正原因(例如某些缓冲区溢出)通常可能与您看到的症状(崩溃)非常远。


1
+1:谢谢,我之前不知道有_NO_DEBUG_HEAP这个选项。现在试一下看看。(我曾经有过一个有趣的经历,追踪内存破坏问题只发生在没有连接调试器的嵌入式硬件上,所以我理解你说的“可能与症状相去甚远”部分。) - leander
没错,调试器中出现了崩溃。 =)祝我好运... - leander
1
哦,调试堆隐藏了损坏?那真是倒霉... - Michael Burr
1
@Michael:是的,这是一个一字符缓冲区溢出。我想是由于不同的填充方式,调试堆没有表现出来。 - leander
谢谢!我按照你的建议,在Visual Studio中成功复现了一个问题。 - undefined

3

在我将环境设置为_NO_DEBUG_HEAP=1后,应用程序验证器对于解决这个问题非常有用,可以查看此处的已接受答案:如何找到最后释放内存的位置?

值得一提的是PageHeap,我在查看应用程序验证器时发现了它。看起来它涵盖了一些类似的内容。

(FYI,这是一个单字符缓冲区溢出问题:

m_pEnumName = (char*)malloc(strlen(data) /* missing +1 here */);
strcpy(m_pEnumName, data);

...另一个极好的理由是不要直接使用strcpy


3

在new或malloc内部崩溃通常意味着malloc实现的(内部)结构已被破坏。这往往是通过写入先前分配的内存空间(缓冲区溢出)而完成的。然后,在下一次调用new或malloc时,应用程序崩溃,因为内部结构现在包含无效数据。

检查是否可能覆盖了任何先前分配的空间。

如果您的应用程序是可移植的,可以尝试在Linux上构建它,并在Valgrind下运行。


是的,我也这么猜。现在是挖掘electricfence或dmalloc的时候了,特别是现在_NO_DEBUG_HEAP允许我在调试器内崩溃。 - leander
是的,我之前考虑将它移植到Linux上,只是为了使用Valgrind!=)Memcheck模块非常好用,我甚至曾经用它来调试过MMORPG服务器。应用程序验证器似乎在Windows中涵盖了很多相同的领域,所幸我找到了它。 - leander

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接