52

我们最近尝试将我们的一些 Visual Studio 项目分解为库,并且在一个测试项目中,一切似乎都可以很好地编译和构建,其中一个库项目作为依赖项。但是,尝试运行该应用程序给了我们以下令人讨厌的运行时错误消息:

运行时检查失败 #0 - ESP 的值未在函数调用中正确保存。这通常是调用使用不同调用约定声明的函数指针的结果。

我们甚至从未为我们的函数指定调用约定(__cdecl 等),将所有编译器开关保留为默认值。我检查了项目设置对于跨库和测试项目的调用约定是一致的。

更新:我们的一位开发人员将“Basic Runtime Checks”项目设置从“Both (/RTC1, equiv. to /RTCsu)”更改为“Default”,运行时消失了,程序显然运行正常。我完全不相信这一点。这是一个适当的解决方案,还是一个危险的黑客攻击?

4

19 回答 19

53

这个调试错误意味着堆栈指针寄存器在函数调用之后没有返回到它的原始值,即函数调用之前的push次数没有跟在调用之后相等的pops次数。

我知道这有两个原因(都是动态加载的库)。#1 是 VC++ 在错误消息中描述的内容,但我认为这不是导致错误的最常见原因(参见 #2)。

1)不匹配的调用约定:

调用者和被调用者在谁将要做什么方面没有适当的协议。例如,如果您正在调用 DLL 函数_stdcall,但由于某种原因,您在调用中将其声明为_cdecl(VC++ 中的默认值)。如果您在不同的模块等中使用不同的语言,这会发生很多。

您必须检查有问题的函数的声明,并确保它没有被声明两次,而且是不同的。

2)不匹配的类型:

调用者和被调用者不是用相同的类型编译的。例如,一个公共标头定义了 API 中的类型并且最近发生了变化,一个模块被重新编译,但另一个没有 - 即某些类型在调用者和被调用者中可能具有不同的大小。

在这种情况下,调用者推送一个大小的参数,但被调用者(如果您使用_stdcall被调用者清理堆栈的地方)弹出不同大小的参数。因此,ESP 没有返回到正确的值。

(当然,这些参数以及它们下面的其他参数在被调用函数中似乎是乱码,但有时您可以在没有明显崩溃的情况下幸存下来。)

如果您可以访问所有代码,只需重新编译它。

于 2009-08-26T07:15:13.707 回答
20

我在其他论坛读到这个

我遇到了同样的问题,但我只是修复了它。我从以下代码中得到了同样的错误:

HMODULE hPowerFunctions = LoadLibrary("Powrprof.dll");
typedef bool (*tSetSuspendStateSig)(BOOL, BOOL, BOOL);

tSetSuspendState SetSuspendState = (tSuspendStateSig)GetProcAddress(hPowerfunctions, "SetSuspendState");

result = SetSuspendState(false, false, false); <---- This line was where the error popped up. 

经过一番调查,我将其中一行更改为:

typedef bool (WINAPI*tSetSuspendStateSig)(BOOL, BOOL, BOOL);

这解决了这个问题。如果您查看找到 SetSuspendState 的头文件(powrprof.h,SDK 的一部分),您会看到函数原型定义为:

BOOLEAN WINAPI SetSuspendState(BOOLEAN, BOOLEAN, BOOLEAN);

所以你们也有类似的问题。当您从 .dll 调用给定函数时,其签名可能已关闭。(在我的情况下,它是缺少 WINAPI 关键字)。

希望对未来的人有所帮助!:-)

干杯。

于 2010-07-26T08:07:12.963 回答
11

使支票静音不是正确的解决方案。您必须弄清楚您的调用约定出了什么问题。

有很多方法可以在不明确指定的情况下更改函数的调用对流。extern "C" 会这样做,STDMETHODIMP/IFACEMETHODIMP 也会这样做,其他宏也可能会这样做。

我相信如果在 WinDBG ( http://www.microsoft.com/whdc/devtools/debugging/default.mspx ) 下运行您的程序,运行时应该会在您遇到该问题时中断。您可以查看调用堆栈并找出哪个函数有问题,然后查看它的定义和调用者使用的声明。

于 2008-09-27T00:53:57.737 回答
6

当代码尝试在不属于预期类型的​​对象上调用函数时,我看到了这个错误。

所以,类层次结构: 有孩子的父母: Child1 和 Child2

Child1* pMyChild = 0;
...
pMyChild = pSomeClass->GetTheObj();// This call actually returned a Child2 object
pMyChild->SomeFunction();          // "...value of ESP..." error occurs here
于 2009-05-14T14:53:09.767 回答
5

我从 VC++ 程序调用的 AutoIt API 遇到了类似的错误。

    typedef long (*AU3_RunFn)(LPCWSTR, LPCWSTR);

但是,当我按照线程前面的建议更改包含 WINAPI 的声明时,问题就消失了。

没有任何错误的代码如下所示:

typedef long (WINAPI *AU3_RunFn)(LPCWSTR, LPCWSTR);

AU3_RunFn _AU3_RunFn;
HINSTANCE hInstLibrary = LoadLibrary("AutoItX3.dll");
if (hInstLibrary)
{
  _AU3_RunFn = (AU3_RunFn)GetProcAddress(hInstLibrary, "AU3_WinActivate");
  if (_AU3_RunFn)
     _AU3_RunFn(L"Untitled - Notepad",L"");
  FreeLibrary(hInstLibrary);
}
于 2012-06-17T09:00:34.850 回答
3

我在调用 DLL 中的函数时遇到此错误,该 DLL 是使用 2005 年之前版本的 Visual C++ 从较新版本的 VC(2008)编译的。该函数具有以下签名:

LONG WINAPI myFunc( time_t, SYSTEMTIME*, BOOL* );

问题是time_t2005 之前的版本中 的大小为 32 位,但自 VS2005 以来为 64 位(定义为_time64_t)。函数的调用需要一个 32 位变量,但从 VC >= 2005 调用时会得到一个 64 位变量。由于在使用WINAPI调用约定时函数的参数是通过堆栈传递的,这会破坏堆栈并生成上述错误消息 ( “运行时检查失败 #0 ...”)。

要解决此问题,可以

#define _USE_32BIT_TIME_T

在包含 DLL 的头文件之前或 - 更好 - 根据 VS 版本更改头文件中函数的签名(2005 之前的版本不知道_time32_t!):

#if _MSC_VER >= 1400
LONG WINAPI myFunc( _time32_t, SYSTEMTIME*, BOOL* );
#else
LONG WINAPI myFunc( time_t, SYSTEMTIME*, BOOL* );
#endif

请注意,您当然需要在调用程序中使用_time32_t而不是。time_t

于 2014-05-21T06:41:50.507 回答
3

值得指出的是,这也可能是 Visual Studio 错误。

我在 VS2017、Win10 x64 上遇到了这个问题。起初它是有道理的,因为我正在做一些奇怪的事情,将其转换为派生类型并将其包装在 lambda 中。但是,我将代码还原为之前的提交,但仍然出现错误,即使它以前不存在。

我尝试重新启动然后重建项目,然后错误消失了。

于 2018-03-13T17:45:27.820 回答
2

在将函数移动到 dll 并使用 LoadLibrary 和 GetProcAddress 动态加载 dll 后,我遇到了同样的错误。由于装饰,我已经为 dll 中的函数声明了 extern "C"。所以这也将调用约定更改为 __cdecl。我在加载代码中将函数指针声明为 __stdcall。一旦我在加载代码中将函数指针从 __stdcall 更改为 __cdecl,运行时错误就消失了。

于 2015-01-10T19:35:08.887 回答
1

您是在创建静态库还是 DLL?如果是 DLL,如何定义导出;导入库是如何创建的?

库中函数的原型是否与定义函数的函数声明完全相同?

于 2008-09-27T00:55:49.900 回答
1

你有任何类型定义的函数原型(例如 int (*fn)(int a, int b) )

如果你 dom 你可能弄错了原型。

ESP 是调用函数时的错误(你能分辨出调试器中的哪个函数吗?),它的参数不匹配 - 即堆栈已恢复到调用函数时开始的状态。

如果您正在加载需要声明的 C++ 函数 extern C - C 使用 cdecl,C++ 默认使用 stdcall 调用约定 (IIRC),您也可以获得此信息。在导入的函数原型周围放置一些 extern C 包装器,您可以修复它。

如果您可以在调试器中运行它,您将立即看到该功能。如果没有,您可以设置 DrWtsn32 以创建一个小型转储,您可以将其加载到 windbg 以查看错误发生时的调用堆栈(尽管您需要符号或映射文件来查看函数名称)。

于 2008-09-27T01:02:43.753 回答
1

如果函数是使用不同于它编译成的调用约定调用的,您将收到此错误。

Visual Studio 使用项目选项中标明的默认调用约定设置。检查此值在原始项目设置和新库中是否相同。过于雄心勃勃的开发人员可能会在原始版本中将其设置为 _stdcall/pascal,因为与默认的 cdecl 相比,它减少了代码大小。因此,基本进程将使用此设置,并且新库会获得导致问题的默认 cdecl

既然你说过你不使用任何特殊的调用约定,这似乎是一个很好的可能性。

还要对标头进行比较,以查看进程看到的声明/文件是否与编译库时使用的相同。

ps:让警告消失是BAAD。潜在的错误仍然存​​在。

于 2008-09-28T18:13:21.770 回答
1

另一种esp可能会搞砸的情况是无意的缓冲区溢出,通常是由于错误地使用指针超出了数组的边界。假设你有一些看起来像的 C 函数

int a, b[2];

写到b[3]可能会改变a,并且任何过去的地方都可能会破坏esp堆栈中保存的内容。

于 2008-09-27T03:32:34.000 回答
1

这在访问 COM 对象(Visual Studio 2010)时发生在我身上。我在对 QueryInterface 的调用中传递了另一个接口 A 的 GUID,但随后我将检索到的指针转换为接口 B。这导致对具有完全签名的接口进行函数调用,这说明了堆栈(和 ESP)是弄乱。

传递接口 B 的 GUID 解决了该问题。

于 2014-04-17T22:04:27.473 回答
1
于 2016-08-26T16:47:58.040 回答
0

ESP 是堆栈指针。因此,根据编译器的说法,您的堆栈指针变得混乱了。在没有看到一些代码的情况下,很难说这是如何(或是否)发生的。

重现此代码的最小代码段是多少?

于 2008-09-27T00:53:12.387 回答
0

如果您在 Windows API 中使用任何回调函数,则必须使用CALLBACK和/或声明它们WINAPI。这将应用适当的修饰以使编译器生成正确清理堆栈的代码。例如,在 Microsoft 的编译器中,它添加了__stdcall.

Windows 一直使用该__stdcall约定,因为它会导致(稍微)更小的代码,清理发生在被调用的函数中,而不是在每个调用站点。但是,它与可变参数函数不兼容(因为只有调用者知道他们推送了多少参数)。

于 2008-09-27T10:40:41.337 回答
0

这是一个产生该错误的精简 C++ 程序。使用 (Microsoft Visual Studio 2003) 编译会产生上述错误。

#include "stdafx.h"
char* blah(char *a){
  char p[1];
  strcat(p, a);
  return (char*)p;
}
int main(){
  std::cout << blah("a");
  std::cin.get();
}

错误:“运行时检查失败 #0 - ESP 的值未在函数调用中正确保存。这通常是调用使用一种调用约定声明的函数而使用另一种调用约定声明的函数指针的结果。”

于 2012-05-24T21:16:25.000 回答
0

我在工作中遇到了同样的问题。我正在更新一些调用 FARPROC 函数指针的非常旧的代码。如果您不知道,FARPROC 是具有零类型安全性的函数指针。它是 typdef 函数指针的 C 等价物,没有编译器类型检查。例如,假设您有一个带有 3 个参数的函数。您将 FARPROC 指向它,然后使用 4 个参数而不是 3 个参数调用它。额外的参数将额外的垃圾推入堆栈,当它弹出时,ESP 现在与启动时不同。所以我通过删除调用 FARPROC 函数调用的额外参数来解决它。

于 2014-10-30T21:46:01.413 回答
0

不是最好的答案,但我只是从头开始重新编译我的代码(在 VS 中重建),然后问题就消失了。

于 2017-03-02T10:12:59.210 回答