При отладке какого-то сбоя я столкнулся с некоторым кодом, который упрощается до следующего случая:
#include <cmath>
#pragma intrinsic (sqrt)
class MyClass
{
public:
MyClass() { m[0] = 0; }
double& x() { return m[0]; }
private:
double m[1];
};
void function()
{
MyClass obj;
obj.x() = -sqrt(2.0);
}
int main()
{
function();
return 0;
}
При построении в Debug | Win32 с VS2012 (версия Pro версии 11.0.61030.00 Update 4 и Express для Windows Desktop версии 11.0.61030.00 Update 4) код запускает проверки проверки времени выполнения в конце выполнения function
, которые отображаются как (случайным образом):
Ошибка проверки времени выполнения # 2 - поврежден стек вокруг объекта obj.
или
В Test.exe произошло переполнение буфера, которое повредило внутреннее состояние программы. Нажмите "Разрыв", чтобы отладить программу или "Продолжить", чтобы завершить работу программы.
Я понимаю, что это обычно означает какой-то переполнение/переполнение буфера для объектов в стеке. Возможно, я что-то пропускаю, но я не вижу нигде в этом коде на С++, где может произойти переполнение буфера. После игры с различными настройками кода и перехода через сгенерированный ассемблерный код функции (см. Раздел "подробности" ниже), у меня возникнет соблазн сказать, что это выглядит как ошибка в Visual Studio 2012, но, возможно, я слишком глубоко и чего-то не хватает.
Существуют ли требования к использованию встроенных функций или другие стандартные требования к С++, которые не соответствуют этому коду, что может объяснить это поведение?
Если нет, отключает функцию intrinsic единственный способ получить правильное поведение проверки во время выполнения (кроме описанного ниже обходного пути, например 0-sqrt
, которое может легко потеряться)?
Детали
Играя вокруг кода, я заметил, что ошибки проверки времени выполнения исчезают, когда я отключу встроенный sqrt
, комментируя строку #pragma
.
В противном случае с sqrt
внутренней прагмой (или опцией компилятора /Oi ):
- Использование установщика, такого как
obj.setx(double x) { m[0] = x; }
, не удивительно также порождает ошибки проверки времени выполнения. - Замена
obj.x() = -sqrt(2.0)
наobj.x() = +sqrt(2.0)
илиobj.x() = 0.0-sqrt(2.0)
к моему удивлению не приводит к ошибкам проверки времени выполнения. - Аналогично заменяя
obj.x() = -sqrt(2.0)
наobj.x() = -1.4142135623730951;
, не генерируется ошибка проверки времени выполнения. - Замена элемента
double m[1];
наdouble m;
(наряду сm[0]
образами) только, похоже, генерирует ошибку "Ошибка проверки времени выполнения №2" (даже приobj.x() = -sqrt(2.0)
) и иногда работает нормально. - Объявление
obj
в качестве экземпляраstatic
или выделение его в куче не приводит к ошибкам проверки времени выполнения. - Установка предупреждений компилятора на уровень 4 не дает никаких предупреждений.
- Компиляция того же кода с VS2005 Pro или VS2010 Express не создает ошибок проверки времени выполнения.
- В этом стоит отметить проблему с Windows 7 (с процессором Intel Xeon) и с машиной Windows 8.1 (с процессором Intel Core i7).
Затем я просмотрел сгенерированный код сборки. В целях иллюстрации я буду ссылаться на "неудачную версию" как на версию, полученную из приведенного выше кода, тогда как я создал "рабочую версию", просто комментируя строку #pragma intrinsic (sqrt)
. Ниже показан вид сбоку полученного сгенерированного кода сборки с "неудачной версией" слева, а "рабочая версия" справа:
Сначала я заметил, что вызов _RTC_CheckStackVars
отвечает за ошибки "Ошибка проверки времени выполнения" 2 и проверяет, в частности, всякий раз, когда волшебные файлы cookie 0xCCCCCCCC
по-прежнему остаются неповрежденными вокруг объекта obj
на стек (который начинается со смещения -20 байт относительно исходного значения ESP
). На следующих снимках экрана я выделил местоположение объекта зеленым цветом, а местоположение "волшебное печенье" - красным. В начале функции в "рабочей версии" это выглядит так:
а затем прямо перед вызовом _RTC_CheckStackVars
:
Теперь в "неудачной версии" преамбула включает дополнительную строку (строка 3415)
and esp,0FFFFFFF8h
который по существу выравнивает obj
на границе 8 байтов. В частности, всякий раз, когда вызывается функция с начальным значением ESP
, заканчивающимся 0
или 8
nibble, сохраняется obj
, начиная со смещения -24 байта относительно начального значения ESP
.
Проблема в том, что _RTC_CheckStackVars
по-прежнему ищет те волшебные куки 0xCCCCCCCC
в тех же местах, что и исходное значение ESP
, как в приведенной выше "рабочей версии" (т.е. Смещения -24 и -12 байтов), В этом случае obj
первые 4 байта фактически перекрывают одно из местоположений волшебного печенья. Это показано на скриншотах ниже в начале "неудачной версии" :
а затем прямо перед вызовом _RTC_CheckStackVars
:
Мы можем заметить, что фактические данные, соответствующие obj.m[0]
, идентичны между "рабочей версией" и "неудачной версией" ( "cd 3b 7f 66 9e a0 f6 bf" или ожидаемое значение - 1.4142135623730951 при интерпретации double
).
Кстати, проверки _RTC_CheckStackVars
действительно проходят каждый раз, когда начальное значение ESP
заканчивается на 4
или C
nibble (в этом случае obj
начинается со смещения -20 байт, как в "рабочая версия" ).
После завершения проверки _RTC_CheckStackVars
(при условии, что она проходит), есть дополнительная проверка, что восстановленное значение ESP
соответствует исходному значению. Эта проверка, когда она терпит неудачу, несет ответственность за сообщение "Переполнение буфера произошло в...".
В "рабочей версии" оригинал ESP
скопирован в EBP
в начале преамбулы (строка 3415), и это значение, которое используется для вычисления контрольной суммы путем xoring с помощью ___security_cookie
(строка 3425)). В "неудачной версии" вычисление контрольной суммы основано на ESP
(строка 3425) после того, как ESP
уменьшилось на 12 при нажатии некоторых регистров (строки 3417-3419), но соответствующая проверка с восстановленным ESP
выполняется в той же точке, где эти регистры были восстановлены.
Итак, короче говоря, и если бы я не понял этого, похоже, что "рабочая версия" соответствует стандартным учебникам и учебным пособиям по обработке стека, тогда как "неудачная версия" помешает проверкам времени выполнения.
P.S.: "Debug build" относится к стандартному набору параметров компилятора конфигурации "Debug" из нового шаблона проекта "Win32 Console Application".