【问题标题】:Inverse Heisenbug - Unit test fails only when debugger is attachedInverse Heisenbug - 仅在附加调试器时单元测试才会失败
【发布时间】:2011-05-15 10:46:37
【问题描述】:

我最近修复了我们产品中的一个缺陷,其症状是访问悬空指针导致的访问冲突。

为了获得良好的实践,我添加了一个单元测试以确保错误不会再次出现。在编写单元测试时,我总是会退出我的缺陷修复并确保单元测试失败,否则我知道它没有正确地完成它的工作。

退出缺陷修复后,我发现我的单元测试仍然通过(不好)。当我将调试器附加到单元测试以查看其通过的原因时,测试失败(即引发了异常),我可以中断并观察到调用堆栈与我修复的原始缺陷中的堆栈匹配。

我没有修改 Visual Studio 2005 中的“异常中断”设置,这确实是导致测试工具终止的关键 Win32 异常(即没有正常的异常处理程序)。

异常的文字是:

Unhandled exception at 0x0040fc59 in _testcase.exe: 0xC0000005:
Access violation reading location 0xcdcdcdcd.

注意:位置并不总是0xcdcdcdcd (allocated but unwritten Win32 heap memory)。有时是0x00000000,有时是另一个地址。

这似乎与传统的 Heisenbug 相反,通过调试器观察问题时问题就会消失。就我而言,通过调试器观察它会导致问题出现!

我最初的想法是,这是调试器中的时间差异所暴露的竞争条件。但是,当我将跟踪添加到代码并与调试器分开运行时,我打印出来的数据向我表明,应用程序应该以与在调试器下运行时类似的方式中止。但事实并非如此!

关于可能导致此问题的任何建议?


更新:我正在缩小这个问题的原因。有关详细信息,请参阅this question。如果我找到答案,我会用答案更新这个问题。

【问题讨论】:

  • 我曾经遇到过这样一个有趣的错误,即在附加调试器时程序崩溃了。我终于发现我有一个线程在调用sem_wait 时被阻塞;调试器在附加时中断了线程,导致sem_wait 返回错误EINTR。然后线程继续执行并发生了坏事。可以说,我明白了为什么检查错误代码很重要......

标签: c++ unit-testing debugging debug-build


【解决方案1】:

通常,当您删除指向该内存的指针时,VC++ 调试器将使用某个已知值填充堆分配的内存。自从我使用 Visual Studio 以来已经有一段时间了,但对我来说 0xcdcdcdcd 可能是这样一个值似乎是合理的。在我看来,应用程序在调试器中运行时最有可能正确崩溃。在发布模式下运行时,运行时不会浪费时间覆盖已释放的内存,因此有时您会“幸运”并且存储在该内存中的数据仍然有效。

您可以修改您的构建设置,以在发布模式下启用以已知值填充已释放内存的选项(完成后不要忘记再次将其关闭)。我猜如果你这样做,你的应用程序会在发布模式下崩溃。

我明白该值并不总是 0xcdcdcdcd,这可能意味着我错了,或者可能意味着您有多个指向悬空指针的路径。

【讨论】:

  • 为了清楚起见,我在这两种情况下都在运行调试模式。我是否使用 attached 的调试器运行它来确定缺陷是否可重现。我在连接调试器的情况下发生崩溃,但如果我通过命令行手动执行测试工具(即c:\> testHarness.exe),则不会发生崩溃。
【解决方案2】:

我在几年前遇到过这个问题:只有当调试器没有附加时才会出现问题。

事实证明,代码破坏了先前方法激活的堆栈帧,并且使用调试器引入了一个中间堆栈帧。

你可能有类似的情况。

【讨论】:

  • 这听起来更像是一个更传统的 Heisenbug。我的理论上应该更容易弄清楚,但这让我感到困惑。
【解决方案3】:

我不知道这是否对您有任何帮助,但我曾经遇到过一个错误,如果程序在 Visual Studio 调试器下运行,或者程序在外部运行,然后附加了调试器,则会出现不同的表现。

【讨论】:

  • 我看不出这有什么帮助。这很有趣,但非常无用:)
  • @OJ:实际上我也没有。就我而言,它可能在程序启动时做了一些小改动。 (我知道 C 运行时会在初始化时检查调试器,可能会进行一些额外的分配或类似的操作)
  • @Matthew:嗯,这是一个答案,只是一个可能没有帮助的答案。我可能会在短时间内删除它。
【解决方案4】:

我已查明此问题的原因 - 详情请参阅 this question

在调试器下运行我的测试工具时,调试环境消耗的内存意味着同一对象的后续分配/解除分配总是分配在不同的内存部分。这意味着当我的测试工具试图访问一个悬空指针时,它会导致测试崩溃(从技术上讲,这是未定义的行为,但这是测试代码,它似乎可以满足我的需要)。

从命令行运行我的测试工具时,同一对象的后续分配/解除分配总是重新使用 same 内存块。这种巧合的行为意味着当我在测试用例中访问实际上是一个悬空指针时,碰巧悬空指针仍然指向一个有效对象。这就是我没有看到崩溃的原因。

【讨论】:

  • 这不是很可靠。是的,它们被分配在内存的不同部分,但是如果在对象被删除后,内存区域仍然映射到进程地址空间呢?你试图建立在非常不稳定的基础上——即使是测试也太不稳定了。
猜你喜欢
  • 1970-01-01
  • 2011-05-22
  • 2021-08-16
  • 2017-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多