Подтвердить что ты не робот

Когда деление на ноль не является делением на ноль? Головоломка в отладчике (статические переменные проблемы)

Я очень смущен, и я думаю, что мой отладчик лжет мне. В моем коде есть следующий цикл:

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

Константы 60 и 5 точно так, как я ожидал. Итак, где деление на ноль??? Является ли мой отладчик лживым? Разумеется, деление на ноль было сброшено аппаратными средствами, поэтому он не мог ошибиться в этом? И если это было деление на ноль в другом месте моего кода, каковы шансы, что отладчик покажет указатель инструкции именно в этом месте? Признаюсь, я в тупике.

4b9b3361

Ответ 1

Поскольку код является частью функции-члена, и вы вызываете эту функцию из нескольких потоков, переменные static не являются потокобезопасными, если используют компилятор, который не соответствует стандартам С++ 11. Таким образом, вы можете получить расы данных при инициализации этих двух статических переменных.

Для стандартного совместимого компилятора С++ 11 статические переменные теперь будут инициализированы первым потоком, а последующие потоки ожидают, пока статичность не будет инициализирована.

Для Visual Studio 2010 и ниже статические локальные переменные не гарантируются потокобезопасностью, поскольку эти компиляторы соответствуют стандарту С++ 03 и С++ 98.

Для Visual Studio 2013 я не уверен в уровне поддержки С++ 11 в терминах статической локальной инициализации. Поэтому для Visual Studio 2013 вам может потребоваться правильная синхронизация, чтобы гарантировать правильную инициализацию статических локальных переменных.

Для Visual Studio 2015 этот элемент был адресован, и правильная статическая локальная инициализация полностью реализована, поэтому код, который вы сейчас используете, должен корректно работать для VS 2015 и выше.


Изменить: для Visual Studio 2013 статическая локальная потоковая инициализация не реализована ( "Magic Statics" ), как описано здесь.

Поэтому мы можем с осторожностью проверить, что причиной исходной проблемы является проблема инициализации статической локализации и потоки. Таким образом, решение (если вы хотите придерживаться VS 2013) - использовать правильную синхронизацию или перепроектировать ваше приложение, чтобы статические переменные больше не нужны.

Ответ 2

Проблема может быть связана с многопотоком.

  • Поток входит в функцию
  • Проверяет скрытую статическую переменную is_initialized, чтобы убедиться, что инициализация уже выполнена.
  • var равен 0, поэтому он устанавливает переменную в 1 и продолжает чтение реестра
  • В этот момент другой поток входит в функцию
  • Второй поток видит переменные как уже инициализированные и пропускает код инициализации
  • Деление выполняется, когда знаменатель остается 0 (первый поток все еще читает реестр)
  • Сбой программы, но в то же время первый поток завершает выполнение, устанавливая переменные, которые вы видите на дампе.
  • Вы теряете сон, думая, как произошло невозможное.