Inverse Heisenbug — модульный тест завершается с ошибкой только при подключении отладчика

Недавно я исправил дефект в нашем продукте, симптомом которого было нарушение прав доступа, вызванное обращением к оборванному указателю.

Для хорошей практики я добавил модульный тест, чтобы убедиться, что ошибка не вернется. При написании модульного теста я всегда отказываюсь от исправления дефекта и гарантирую, что модульный тест не работает, иначе я знаю, что он не выполняет свою работу должным образом.

После отказа от исправления дефекта я обнаружил, что мой модульный тест все еще проходит (не очень хорошо). Когда я прикрепил отладчик к модульному тесту, чтобы увидеть, почему он проходит, тест провалился (т. е. было выдано исключение), и я мог сломаться и увидеть, что стек вызовов совпадает со стеком исходного дефекта, который я исправил.

Я не менял настройки «Разрыв при исключении» в Visual Studio 2005, и это действительно критическое исключение Win32, которое приводит к завершению работы тестовой системы (т. е. изящного обработчика исключений нет).

Текст исключения:

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

Примечание. Местоположение не всегда 0xcdcdcdcd (выделенная, но незаписанная память кучи Win32). Иногда это 0x00000000, а иногда другой адрес.

Это похоже на обратную сторону традиционного Heisenbug, когда проблема исчезает при наблюдении за ней через отладчик. В моем случае наблюдение за ним через отладчик приводит к появлению проблемы!

Моя первоначальная мысль заключалась в том, что это состояние гонки, выявляемое разницей во времени в отладчике. Однако, когда я добавил трассировку в код и запустил его отдельно от отладчика, данные, которые я распечатывал, указывали мне, что приложение должно быть прервано так же, как при работе под отладчиком. Но это не так!

Любые предложения относительно того, что может быть причиной этого?


Обновление: я выясняю причину этой проблемы. См. manager">этот вопрос для более подробной информации. Обновлю этот вопрос ответом, если найду его.


person LeopardSkinPillBoxHat    schedule 25.11.2010    source источник
comment
Однажды у меня была такая забавная ошибка, когда программа вылетала при подключении отладчика. Наконец я понял, что у меня есть поток, который был заблокирован при вызове sem_wait; отладчик прервал поток при его присоединении, в результате чего sem_wait вернулся с ошибкой EINTR. Затем поток продолжил выполнение, и случилось что-то плохое. Достаточно сказать, что я узнал, почему важно проверять коды ошибок...   -  person James McNellis    schedule 25.11.2010


Ответы (4)


Как правило, отладчик VC++ заполняет память, выделенную в куче, некоторым известным значением, когда вы удаляете указатель на эту память. Прошло довольно много времени с тех пор, как я использовал Visual Studio, но мне кажется разумным, что 0xcdcdcdcd может быть таким значением. Мне кажется наиболее вероятным, что приложение нормально падает при запуске в отладчике. При работе в режиме Release среда выполнения не тратит время на перезапись освобожденной памяти, поэтому иногда вам «везет», и данные, хранящиеся в этой памяти, все еще действительны.

Вы можете изменить настройки сборки, чтобы включить опцию заполнения освобожденной памяти известным значением в режиме Release (не забудьте снова отключить ее, когда закончите). Я предполагаю, что если вы сделаете это, ваше приложение выйдет из строя в режиме выпуска.

Я понимаю, что значение не всегда равно 0xcdcdcdcd, что может означать, что я ошибаюсь, или может означать, что у вас есть более одного пути к оборванному указателю.

person Adam Milligan    schedule 25.11.2010
comment
Просто для ясности, я запускаю режим отладки в обоих случаях. То, запускаю ли я его с подключенным отладчиком, определяет, воспроизводим ли дефект. Я получаю сбой с подключенным отладчиком, но не сбой, если я запускаю тестовую обвязку вручную через командную строку (т.е. c:\> testHarness.exe). - person LeopardSkinPillBoxHat; 25.11.2010

Я столкнулся с этим несколько лет назад в обратном порядке: проблема возникала только тогда, когда отладчик был не подключен.

Оказалось, что код искажал стек-фрейм активации предыдущего метода и с помощью отладчика вводил промежуточный стек-фрейм.

Возможно у вас аналогичная ситуация.

person Adrian Pronk    schedule 25.11.2010
comment
Звучит как более традиционный Heisenbug. Мое теоретически должно быть легче понять, но это сбивает меня с толку. - person LeopardSkinPillBoxHat; 25.11.2010

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

person Community    schedule 25.11.2010
comment
Я не понимаю, как это может помочь. Это интересно, но очень бесполезно :) - person OJ.; 25.11.2010
comment
@OJ: На самом деле я тоже. В моем случае это могло быть небольшое изменение при запуске программы. (Я знаю, что среда выполнения C проверяет наличие отладчика во время инициализации, возможно, были сделаны некоторые дополнительные выделения или что-то подобное) - person Hasturkun; 25.11.2010
comment
@Matthew: Ну, это было задумано как ответ, просто, вероятно, бесполезный. Я, вероятно, удалю это через некоторое время. - person Hasturkun; 25.11.2010

Я выделил причину этой проблемы - см. этот вопрос для получения подробной информации.

При выполнении моей тестовой программы под отладчиком память, потребляемая средой отладки, означала, что последующие выделения/освобождения одного и того же объекта всегда распределялись в разных частях памяти. Это означало, что когда моя тестовая программа пыталась получить доступ к висячему указателю, она приводила к сбою теста (технически это неопределенное поведение, но это тестовый код, и, похоже, он делает то, что мне нужно).

При запуске моей тестовой программы из командной строки последующие выделения/освобождения одного и того же объекта всегда повторно использовали один и тот же блок памяти. Это случайное поведение означало, что когда я обращался к тому, что на самом деле было висячим указателем в моем тестовом примере, случалось, что висячий указатель все еще указывал на действительный объект. Вот почему я не видел аварии.

person LeopardSkinPillBoxHat    schedule 25.11.2010
comment
Это не очень надежно. Да, они размещены в разных частях памяти, но что, если после удаления объекта область памяти все еще отображается в адресное пространство процесса? Вы пытаетесь строить на очень зыбкой почве — слишком зыбкой даже для теста. - person sharptooth; 25.11.2010