在VS2012中使用sqrt内部堆栈运行时检查失败

Sle*_*Eye 15 c++ visual-studio-2012

在调试一些崩溃时,我遇到了一些代码,简化了以下情况:

#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;
}
Run Code Online (Sandbox Code Playgroud)

当使用VS2012(Pro Version 11.0.61030.00 Update 4和Express for Windows Desktop版本11.0.61030.00 Update 4)在Debug | Win32中内置时,代码会在结束时触发运行时检查错误. function执行显示为(以随机方式):

运行时检查失败#2 - 变量'obj'周围的堆栈已损坏.

要么

Test.exe中发生缓冲区溢出,该程序已损坏程序的内部状态.按Break可调试程序或继续终止程序.

据我所知,这通常意味着堆栈上的对象存在某种缓冲区溢出/欠载.也许我忽略了一些东西,但是我无法在这个C++代码中看到任何可能发生这种缓冲区溢出的代码.在对代码进行各种调整并逐步执行函数生成的汇编代码之后(参见下面的"详细信息"部分),我很想说它看起来像Visual Studio 2012中的一个错误,但也许我是只是在太深,缺少一些东西.

是否有内部函数使用要求或此代码不符合的其他C++标准要求,这可以解释这种行为?

如果没有,禁用函数内在是获得正确的运行时检查行为的唯一方法(除了0-sqrt下面提到的可能很容易丢失的解决方法)?

细节

围绕代码,我注意到当我sqrt通过注释掉#pragma行来禁用内在函数时,运行时检查错误消失了.

否则使用sqrt内部编译指示(或/ Oi编译器选项):

  • 使用诸如此类的setter 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)),有时运行正常.
  • 声明objstatic实例或在堆上分配它不会生成运行时检查错误.
  • 将编译器警告设置为级别4不会产生任何警告.
  • 使用VS2005 Pro或VS2010 Express编译相同的代码不会产生运行时检查错误.
  • 值得一提的是,我在Windows 7(使用Intel Xeon CPU)和Windows 8.1机器(使用Intel Core i7 CPU)上注意到了这个问题.

然后我继续查看生成的汇编代码.出于说明的目的,我将把"失败的版本"称为从上面提供的代码中获得的版本,而我通过简单地注释该#pragma intrinsic (sqrt)行来生成"工作版本" .生成的汇编代码的并排差异视图如下所示,左侧是"失败版本",右侧是"工作版本": DIFF

首先,我注意到该_RTC_CheckStackVars调用负责"运行时检查失败#2"错误,并且特别是当魔术cookie 在堆栈上的对象0xCCCCCCCC周围仍然完好无损时进行检查obj(这恰好是从 - 的偏移开始 -相对于原始值的20个字节ESP).在下面的屏幕截图中,我突出显示了绿色的对象位置和红色的魔术cookie位置.在"工作版"中的函数开始时,它是这样的:

RTC-OK-启动的功能

然后在电话会议之前_RTC_CheckStackVars:

RTC-OK-右前方的RTC_CheckStackVars

现在在"失败的版本"中,序言包括一个额外的(第3415行)

and         esp,0FFFFFFF8h
Run Code Online (Sandbox Code Playgroud)

它基本上obj在8字节边界上对齐.具体来说,每当调用函数时,初始值ESP以a 08半字节结束obj,则从相对于初始值的-24字节的偏移量开始存储ESP.问题是_RTC_CheckStackVars仍然0xCCCCCCCC在相对于原始ESP值的相同位置寻找那些神奇的cookie,如上面描述的"工作版本"(即-24和-12字节的偏移).在这种情况下,obj前4个字节实际上与魔术cookie位置之一重叠.这在"失败版本"开头的下面的屏幕截图中显示:

RTC-故障启动的功能

然后在电话会议之前_RTC_CheckStackVars:

RTC-故障右前方的RTC_CheckStackVars

我们可以注意到,对应的实际数据obj.m[0]在"工作版本"和"失败版本"之间是相同的("cd 3b 7f 66 9e a0 f6 bf",或者当解释为a时预期值-1.4142135623730951 double) .

顺便说一下,_RTC_CheckStackVars只要初始值ESP以a 4C半字节结束(在这种情况下obj从-20字节偏移开始,就像在"工作版本"中那样),检查实际上就会通过.

_RTC_CheckStackVars完成检查(假设它通过),有一个附加的检查的恢复值ESP对应于原始值.该检查在失败时,负责"在......中发生缓冲区溢出"消息.

在"工作版本"中,原始文件ESP被复制到EBP前导码的早期(第3415行),并且它是用于通过xoring ___security_cookie(第3425行)计算校验和的值.在"失败的版本"中,校验和计算基于ESP(行3425) ESP推送一些寄存器(行3417-3419)之后递减12,但是与恢复的相应检查ESP是在那些寄存器的相同点处完成的.已经恢复.

所以,简而言之,除非我没有做到这一点,看起来"工作版本"遵循标准教科书和堆栈处理教程,而"失败版本"会混淆运行时检查.

PS:"Debug build"是指"Win32 Console Application"新项目模板中"Debug"配置的标准编译器选项集.

Sle*_*Eye 2

正如 Hans 在评论中指出的那样,该问题无法再使用 Visual Studio 2013 重现。同样,Microsoft connect bug 报告的官方答案是:

我们无法使用 VS2013 Update 4 RTM 重现它。产品团队本身不再直接接受对 Microsoft Visual Studio 2012 及早期产品的反馈。您可以通过访问以下链接中的资源之一来获取有关 Visual Studio 2012 及更早版本的问题的支持: http ://www.visualstudio.com/support/support-overview-vs

因此,鉴于该问题仅在 VS2012 上通过函数内在函数(/Oi 编译器选项)、运行时检查(/RTCs 或 /RTC1 编译器选项)一元减运算符的使用触发,请删除任何一个(或多个)这些条件应该可以解决该问题。

因此,可用的选项似乎是:

  1. 升级到最新的 Visual Studio(如果您的项目允许)
  2. #pragma runtime_check通过使用以下示例中的内容包围受影响的函数,禁用对受影响函数的运行时检查:
    #pragma runtime_check ("s", off)
    void function()
    {
      MyClass obj;
      obj.x() = -sqrt(2.0);
    }
    #pragma runtime_check ("s", restore)
Run Code Online (Sandbox Code Playgroud)
  1. #pragma intrinsics (sqrt)通过删除该行并添加来禁用内在函数#pragma function (sqrt)(有关详细信息, 请参阅msdn )。
    如果已通过“启用内部函数”项目属性(/Oi 编译器选项)为所有文件激活了内部函数,则您需要停用该项目属性。然后,您可以为特定函数逐个启用内在函数,同时检查它们是否不受错误影响(使用#pragma intrinsics每个所需内在函数的指令)。
  2. 使用诸如0-sqrt(2.0), -1*sqrt(2.0)(删除一元减号运算符)之类的变通方法来调整代码,试图欺骗编译器使用不同的代码生成路径。请注意,这很可能会因看似微小的代码更改而中断。