Трудноуловимый баг. Но не для Visual Studio.
Столкнулся с такой проблемой, что код, скомпилированный MinGW, работает отлично, а вот скомпилированный Visual Studio - падает с Unhandled Exception. Долго думал, как может быть Unhandled Exception, если инструкция throw находится в try...catch блоке. Избавившись от исключений, получил более вменяемое сообщение об ошибке: stack overrun. Да, мне повезло, в Visual Studio по умолчанию включена опция /GS, которая позволяет отслеживать некоторые ошибки переполнения.
Однако /GS позволяет только определить, в какой функции произошла ошибка, но не указать на ошибочный код. Его нужно как-то найти. В случае перетирания кода возврата функции, сделать это относительно просто. В самом начале функции, при возврате из которой происходит ошибка, можно создать массив на стеке, размером, например, 128 байт. Далее отлаживаться по шагам и следить за состоянием этого массива. Если после очередного шага массив был изменен - мы нашли функцию, которая портит стек.
Рассмотрим небольшой пример. Путь есть функция foo(), при возврате из которой /GS говорит нам, что произошел stack overrun.
void foo() { auto v1 = bar1(); auto v2 = bar2(); auto v3 = bar3(); }
Модифицируем функцию:
void foo() { char buf[128] = {0}; auto v1 = bar1(); auto v2 = bar2(); auto v3 = bar3(); }
Теперь наша программа не падает. Но это связано не с тем, что мы исправили ошибку. Просто вместо перезатирания стека, наш код перезатирает значение массива buf.
Изначально buf заполнен нулями. При отладке я заметил, что после возврата из функции bar2(), масив изменил свое значение, первые 8 байт стали отличными от 0. Это значит, что переменная v2 занимает больше места, чем ожидается. Возникнуть такое может, если функция bar2() скомпилирована с заголовочным файлом, отличным от того, который используется в нашей программе. Проще говоря, нарушена бинарная совместимость интерфейсов класса. Добиться такой бинарной несовместимости, как оказалось, достаточно просто. Например, в классе может быть опциональный член, который появляется в зависимости от настроек препроцессора. Библиотека с bar2() скомпилирована с этой опцией, а наше приложение - без нее. Вот и получаем, что заголовочный файл один, а бинарной совместимости нет.
Подробнее про /GS можно почитать тут
No comments:
Post a Comment