Ошибка проверки времени выполнения стека с встроенным sqrt в VS2012
При отладке какого-то сбоя я столкнулся с некоторым кодом, который упрощается до следующего случая:
#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)
. Ниже показан вид сбоку полученного сгенерированного кода сборки с "неудачной версией" слева, а "рабочая версия" справа:
![Diff]()
Сначала я заметил, что вызов _RTC_CheckStackVars
отвечает за ошибки "Ошибка проверки времени выполнения" 2 и проверяет, в частности, всякий раз, когда волшебные файлы cookie 0xCCCCCCCC
по-прежнему остаются неповрежденными вокруг объекта obj
на стек (который начинается со смещения -20 байт относительно исходного значения ESP
). На следующих снимках экрана я выделил местоположение объекта зеленым цветом, а местоположение "волшебное печенье" - красным. В начале функции в "рабочей версии" это выглядит так:
![RTC-ok-start-of-function]()
а затем прямо перед вызовом _RTC_CheckStackVars
:
![RTC-ok-right-before-RTC_CheckStackVars]()
Теперь в "неудачной версии" преамбула включает дополнительную строку (строка 3415)
and esp,0FFFFFFF8h
который по существу выравнивает obj
на границе 8 байтов. В частности, всякий раз, когда вызывается функция с начальным значением ESP
, заканчивающимся 0
или 8
nibble, сохраняется obj
, начиная со смещения -24 байта относительно начального значения ESP
.
Проблема в том, что _RTC_CheckStackVars
по-прежнему ищет те волшебные куки 0xCCCCCCCC
в тех же местах, что и исходное значение ESP
, как в приведенной выше "рабочей версии" (т.е. Смещения -24 и -12 байтов), В этом случае obj
первые 4 байта фактически перекрывают одно из местоположений волшебного печенья. Это показано на скриншотах ниже в начале "неудачной версии" :
![RTC-fail-start-of-function]()
а затем прямо перед вызовом _RTC_CheckStackVars
:
![RTC-fail-right-before-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".
Ответы
Ответ 1
Как отметил Ханс в комментариях, этот вопрос больше не может быть воспроизведен с помощью Visual Studio 2013.
Аналогичным образом, официальный ответ Отчет об ошибке подключения к Microsoft:
мы не можем воспроизвести его с помощью RT2013 Update 4 RTM. Сама команда продуктов больше не принимает непосредственную обратную связь для Microsoft Visual Studio 2012 и более ранних продуктов. Вы можете получить поддержку для проблем с Visual Studio 2012 и ранее, посетив один из ресурсов по ссылке ниже: http://www.visualstudio.com/support/support-overview-vs
Таким образом, при условии, что проблема запускается только на VS2012 с функцией intrinsics (опция компилятора /Oi ), проверки времени выполнения (либо/RTC, либо/компилятор RTC1), так и использование унарного оператора минус, избавляясь от любого ( или более) этих условий должны решить проблему.
Таким образом, кажется, что доступны следующие опции:
- Обновление до последней версии Visual Studio (если позволяет ваш проект)
- Отключить проверки выполнения для затронутых функций, окружив их с помощью
#pragma runtime_check
, например, в следующем примере:
#pragma runtime_check ("s", off)
void function()
{
MyClass obj;
obj.x() = -sqrt(2.0);
}
#pragma runtime_check ("s", restore)
- Отключите встроенные функции, удалив строку
#pragma intrinsics (sqrt)
и добавив #pragma function (sqrt)
(см. msdn для получения дополнительной информации).
Если intrinsics были активированы для всех файлов с помощью свойства проекта Enable Intrinsic Functions (опция компилятора /Oi ), вам необходимо отключить это свойство проекта. Затем вы можете включить встроенные функции по отдельности для определенных функций, проверяя, что на них не влияет ошибка (с директивами #pragma intrinsics
для каждой требуемой встроенной функции).
- Измените код с помощью обходных методов, таких как
0-sqrt(2.0)
, -1*sqrt(2.0)
(которые удаляют унарный оператор минус) в попытке обмануть компилятор в использовании другого пути генерации кода. Обратите внимание, что это, скорее всего, сломается, казалось бы, с небольшими изменениями кода.