Saturday, April 11, 2015

Отладка stack overrun в c++

Трудноуловимый баг. Но не для 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