【问题标题】:When is a divide by zero not a divide by zero? A puzzle in the debugger (static variable issues)什么时候除以零而不是除以零?调试器中的一个谜题(静态变量问题)
【发布时间】:2015-01-22 19:20:23
【问题描述】:

我很困惑,我认为我的调试器在骗我。我的代码中有以下循环:

MyClass::UploadFile(CString strFile)
{
  ...
  static DWORD dwLockWaitTime = EngKey::GetDWORD(DNENG_SERVER_UPLOAD_LOCK_WAIT_TIME, DNENG_SERVER_UPLOAD_LOCK_WAIT_TIME_DEFAULT);
  static DWORD dwLockPollInterval = EngKey::GetDWORD(DNENG_SERVER_UPLOAD_LOCK_POLL_INTERVAL, DNENG_SERVER_UPLOAD_LOCK_POLL_INTERVAL_DEFAULT);

  LONGLONG llReturnedOffset(0LL);
  BOOL bLocked(FALSE);
  for (DWORD sanity = 0; (sanity == 0 || status == RESUMABLE_FILE_LOCKED) && sanity < (dwLockWaitTime / dwLockPollInterval); sanity++) 
    {
      ...

在我的程序过程中,这个循环已经执行了数百次,并且两个静态变量在代码中的任何地方都没有改变,当它们被静态初始化并在循环条件中读取时,它们只被写入一次在另一个地方。由于它们是从 Windows 注册表中读取的用户设置,因此它们几乎总是具有 dwLockWaitTime = 60 和 dwLockPollInterval = 5 的常量值。所以循环总是执行 60 / 5。

很少,我得到一个崩溃转储,它表明这行代码已经引发了除以零错误。我检查了 WinDbg 的内容并显示:

FAULTING_IP: 
procname!CServerAgent::ResumableUpload+54a [serveragent.cpp @ 725]
00000001`3f72d74a f73570151c00    div     eax,dword ptr [proc!dwLockPollInterval (00000001`3f8eecc0)]

EXCEPTION_RECORD:  ffffffffffffffff -- (.exr 0xffffffffffffffff)
ExceptionAddress: 000000013f72d74a (proc!CServerAgent::ResumableUpload+0x000000000000054a)
   ExceptionCode: c0000094 (Integer divide-by-zero)
  ExceptionFlags: 00000000
NumberParameters: 0

ERROR_CODE: (NTSTATUS) 0xc0000094 - {EXCEPTION}  Integer division by zero.

我检查了汇编代码,它显示崩溃发生在这个 div 指令上。

00000001`3f72d744 8b0572151c00    mov     eax,dword ptr [dwLockWaitTime (00000001`3f8eecbc)]
00000001`3f72d74a f73570151c00    div     eax,dword ptr [dwLockPollInterval (00000001`3f8eecc0)]

如您所见,000000013f8eecbc 的值被移动到eax,然后eax 除以000000013f8eecc0 的值。

你问的这两个值是什么?

0:048> dd 00000001`3f8eecbc
00000001`3f8eecbc  0000003c 00000005 00000001 00000000
00000001`3f8eeccc  00000000 00000002 00000000 00000000
00000001`3f8eecdc  00000000 7fffffff a9ad25cf 7fffffff
00000001`3f8eecec  a9ad25cf 00000000 00000000 00000000
00000001`3f8eecfc  00000000 00000000 00000000 00000000
00000001`3f8eed0c  00000000 00000000 00000000 00000000
00000001`3f8eed1c  00000000 00000000 00000000 00000000
00000001`3f8eed2c  00000000 00000000 00000000 00000000
0:048> dd 000000013f8eecc0
00000001`3f8eecc0  00000005 00000001 00000000 00000000
00000001`3f8eecd0  00000002 00000000 00000000 00000000
00000001`3f8eece0  7fffffff a9ad25cf 7fffffff a9ad25cf
00000001`3f8eecf0  00000000 00000000 00000000 00000000
00000001`3f8eed00  00000000 00000000 00000000 00000000
00000001`3f8eed10  00000000 00000000 00000000 00000000
00000001`3f8eed20  00000000 00000000 00000000 00000000
00000001`3f8eed30  00000000 00000000 00000000 00000000

常量605 完全符合我的预期。那么除以零在哪里???我的调试器在撒谎吗?肯定是硬件已经抛出了除以零,所以它不会犯错吗?如果它在我的代码中的不同位置被零除,那么调试器会在这个位置显示指令指针的几率是多少?我承认,我被难住了..

【问题讨论】:

  • 您在 minidump 中看到预期值的事实并不能证明它在指令执行时具有预期值。例如,线程竞赛的非常经典的行为,小型转储不是在引发异常的确切时间记录的瞬时快照。软 RAM 错误是此类无法解释的事故的另一个来源。因此,如果您确定线程不是一个因素,那么给客户的最佳建议是更换他的 RAM 模块。
  • 关于这一思路的两点。如果转储中的内容(顺便说一下完整转储而不是迷你转储)不正确,则它必须相对于执行的内容已经过时,而不是相反。这意味着如果这两个值被破坏了,那么之后就不得不将它们恢复为预期值......
  • @Benj - So the loop is always doing 60 / 5. 这可能无法回答您的问题,但为什么不预先计算此值并将其存储在变量中,然后在循环中使用该变量?
  • @Benj - 另外,如果您还没有这样做,我认为是时候考虑记录这些(重要的)值(dwLockWaitTime、dwLockPollInterval),而不是试图找出它们的来源一个小型转储。
  • @Benj - 你用的是什么编译器?根据您的描述,我认为您遇到了问题。

标签: c++ debugging windbg


【解决方案1】:

由于代码是成员函数的一部分,并且您从多个线程调用此函数,如果使用不符合 C++ 11 标准的编译器,static 变量不是线程安全的。因此,在初始化这两个静态变量时,您可能会遇到数据竞争。

对于符合 C++ 11 标准的编译器,静态变量现在保证由第一个线程初始化,而后续线程等待静态变量初始化。

对于Visual Studio 2010 及以下版本,不保证静态局部变量是线程安全的,因为这些编译器符合 C++ 03 和 C++ 98 标准。

对于Visual Studio 2013,我不确定 C++ 11 在静态本地初始化方面的支持水平。因此,对于 Visual Studio 2013,您可能必须使用适当的同步来确保正确初始化静态局部变量。

对于Visual Studio 2015,此项目已得到解决,并且已完全实现正确的静态本地初始化,因此您当前拥有的代码应该可以在 VS 2015 及更高版本中正常工作。


编辑:对于Visual Studio 2013,未实现静态本地线程安全初始化(“Magic Statics”),as described here

因此,我们可以谨慎地验证原始问题的原因是静态局部初始化问题和线程。所以解决方案(如果你想坚持使用 VS 2013)是使用适当的同步,或者重新设计你的应用程序,以便不再需要静态变量。

【讨论】:

  • 您是否拥有 Studio Studio 2010,您能否编写一个 SSCCE 并将二进制文件发布到某处?
  • 我使用 VS 2008 和 VS 2013。我跳过了 VS 2010。但是,由于这些编译器遵循 C++ 03 标准(及以下标准),因此无法保证静态本地变量会被多个线程初始化一种线程安全的方式。鉴于 Microsoft 承认最终在 Visual Studio 2015 中修复了此问题,我们可以假设 VS 2010 中使用静态局部变量并从多个线程调用此代码的任何代码都不是线程安全的。
  • 我毫不怀疑你是对的,我只想自己调试它,也许教别人更详细地调试它,例如通过查看汇编代码。我想这可以用 Express Edition 完成,所以我会寻找它的副本。
  • 感谢保罗的所有帮助! :-)
【解决方案2】:

问题可能与多线程有关。

  1. 线程进入函数
  2. 检查隐藏的“is_initialized”静态变量以查看是否已执行初始化
  3. var 为 0,因此它将变量设置为 1 并继续读取注册表
  4. 此时另一个线程进入函数
  5. 第二个线程将变量视为已初始化并跳过初始化代码
  6. 分母仍为0时进行除法(第一个线程仍在读取注册表)
  7. 程序崩溃,但同时第一个线程完成执行,设置您在转储中看到的变量。
  8. 想着不可能的事情是如何发生的,你会失眠

【讨论】:

  • 我是否正确理解它只能发生在多处理器 CPU 上,并且由于线程调度而不能发生在单核 CPU 上?如果是这样,将程序的亲和性设置为单个 CPU 是否可以证明这一点?
  • @ThomasW:从注册表读取可能是一个很长的操作。在此期间,即使是单核 CPU 也会终止第一个线程的时间片(Windows 上为 20 毫秒 IIRC)并安排第二个线程触发问题,这并非不可能。
  • 是的,问题还是会出现,但是在单CPU上,异常会不会停止进程,让调试器显示正确的值(本例为0)?
猜你喜欢
  • 2018-03-22
  • 1970-01-01
  • 1970-01-01
  • 2021-03-15
  • 2010-10-16
  • 1970-01-01
  • 2013-09-21
  • 2016-07-13
  • 2017-12-23
相关资源
最近更新 更多