Ошибка проверки времени выполнения стека с встроенным 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)
  1. Отключите встроенные функции, удалив строку #pragma intrinsics (sqrt) и добавив #pragma function (sqrt) (см. msdn для получения дополнительной информации).
    Если intrinsics были активированы для всех файлов с помощью свойства проекта Enable Intrinsic Functions (опция компилятора /Oi ), вам необходимо отключить это свойство проекта. Затем вы можете включить встроенные функции по отдельности для определенных функций, проверяя, что на них не влияет ошибка (с директивами #pragma intrinsics для каждой требуемой встроенной функции).
  2. Измените код с помощью обходных методов, таких как 0-sqrt(2.0), -1*sqrt(2.0) (которые удаляют унарный оператор минус) в попытке обмануть компилятор в использовании другого пути генерации кода. Обратите внимание, что это, скорее всего, сломается, казалось бы, с небольшими изменениями кода.