VS2012 中的 sqrt 内部堆栈运行时检查失败

Stack runtime check failure with sqrt intrinsic in 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;
}

当使用 VS2012(专业版 11.0.61030.00 更新 4,Express 用于 Windows 桌面版 11.0.61030.00 更新 4)在 Debug|Win32 中构建时,代码触发 run-time 检查错误function 执行结束,显示为(以随机方式):

Run-Time Check Failure #2 - Stack around the variable 'obj' was corrupted.

A buffer overrun has occurred in Test.exe which has corrupted the program's internal state. Press Break to debug the program or Continue to terminate the program.

我知道这通常意味着堆栈中对象的某种缓冲区 overrun/underrun。也许我忽略了一些东西,但我看不到此 C++ 代码中可能发生此类缓冲区溢出的任何地方。在对代码进行各种调整并单步执行函数生成的汇编代码后(请参阅下面的 "details" 部分),我很想说它看起来像是 Visual Studio 2012 中的错误,但也许我只是陷入太深而错过了一些东西。

是否存在此代码不满足的内在函数使用要求或其他 C++ 标准要求,这可以解释此行为?

如果不是,禁用内部函数是否是获得正确 run-time 检查行为的唯一方法(除了下面提到的 0-sqrt 之类的变通方法,它很容易丢失)?

详情

玩代码,我注意到当我通过注释掉 #pragma 行禁用 sqrt 内在函数时,run-time 检查错误消失了。

否则使用 sqrt intrinsic pragma(或 /Oi 编译器选项):

然后我接着看生成的汇编代码。出于说明的目的,我将 "the failing version" 称为从上面提供的代码中获得的代码,而我通过简单地注释 #pragma intrinsic (sqrt) 行生成了 "working version"。生成的汇编代码的 side-by-side 差异视图如下所示,左侧是 "failing version",右侧是 "working version":

首先,我注意到 _RTC_CheckStackVars 调用是造成 "Run-Time Check Failure #2" 错误的原因,尤其是在魔术饼干 0xCCCCCCCC 在 [=27= 周围仍然完好无损时进行检查] 堆栈上的对象(恰好从相对于 ESP 的原始值的 -20 字节的偏移量开始)。在下面的屏幕截图中,我用绿色突出显示了对象位置,用红色突出显示了魔术饼干位置。在 "working version" 中函数的开头是这样的:

然后稍后在调用 _RTC_CheckStackVars 之前:

现在在 "failing version" 中,序言包含一个额外的(第 3415 行)

and         esp,0FFFFFFF8h

这实际上使 obj 在 8 字节边界上对齐。具体来说,无论何时使用以 08 半字节结尾的初始值 ESP 调用函数,都会从 -24 字节的偏移量开始存储 obj相对于 ESP 的初始值。 问题是 _RTC_CheckStackVars 仍然在相对于原始 ESP 值的相同位置寻找那些 0xCCCCCCCC 魔法饼干,如上面描述的 "working version" 所示(即偏移量-24 和 -12 字节)。在这种情况下,obj 的前 4 个字节实际上与魔术 cookie 位置之一重叠。这显示在下面 "failing version":

开头的屏幕截图中

然后稍后在调用 _RTC_CheckStackVars 之前:

我们可以顺便注意到对应于 obj.m[0] 的实际数据在 "working version" 和 "failing version" 之间是相同的("cd 3b 7f 66 9e a0 f6 bf" 或预期值-1.4142135623730951 当解释为 double).

顺便说一句,只要 ESP 的初始值以 4C 半字节结尾(在这种情况下 obj从 -20 字节偏移量开始,就像在 "working version").

中一样

_RTC_CheckStackVars 检查完成后(假设它通过),还有一个额外的检查 ESP 的恢复值对应于原始值。此检查失败时,负责 fr "A buffer overrun has occurred in ..." 消息。

在 "working version" 中,原始 ESP 在序言的早期被复制到 EBP (第 3415 行),这个值用于通过异或计算校验和___security_cookie(第 3425 行)。在 "failing version" 中,校验和计算基于 ESP(第 3425 行)after ESP has been decremented by 12 while push some registers (lines 3417-3419),但是对恢复的 ESP 的相应检查是在恢复这些寄存器的同一点完成的。

所以,简而言之,除非我没有做对,否则看起来 "working version" 遵循关于堆栈处理的标准教科书和教程,而 "failing version" 搞砸了 run-time 检查。

P.S.:"Debug build" 是指来自 "Win32 Console Application" 的 "Debug" 配置的标准编译器选项集新项目模板。

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

we are unable to reproduce it with VS2013 Update 4 RTM. The product team itself no longer directly accepting feedback for Microsoft Visual Studio 2012 and earlier products. You can get support for issues with Visual Studio 2012 and earlier by visiting one of the resources in the link below: http://www.visualstudio.com/support/support-overview-vs

因此,假设问题仅在具有函数内在函数(/Oi 编译器选项)、运行时检查(/RTCs 或 /RTC1 编译器选项)的 VS2012 上触发使用一元减号运算符,摆脱任何一个(或多个)这些条件应该可以解决这个问题。

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

  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)
  1. 通过删除 #pragma intrinsics (sqrt) 行并添加 #pragma function (sqrt) 来禁用内部函数(有关详细信息,请参阅 msdn)。
    如果已通过 "Enable Intrinsic Functions" 项目 属性(/Oi 编译器选项)为所有文件激活内部函数,则需要停用该项目 属性。然后,您可以逐个为特定函数启用内部函数,同时检查它们是否不受错误影响(每个必需的内部函数使用 #pragma intrinsics 指令)。
  2. 使用 0-sqrt(2.0)-1*sqrt(2.0)(删除一元减号运算符)等变通方法调整代码,试图欺骗编译器使用不同的代码生成路径。请注意,这很可能会因看似微小的代码更改而中断。