奇怪的MSC 8.0错误:" ESP的值未在函数调用中正确保存..."
最近,我们试图将一些Visual Studio项目分解为多个库,并且在其中一个库项目作为依赖项的测试项目中,所有内容似乎都可以编译并正常构建。但是,尝试运行该应用程序给了我们以下讨厌的运行时错误消息:
Run-Time Check Failure #0 - The value of ESP was not properly saved across a function call. This is usually a result of calling a function pointer declared with a different calling convention.
我们甚至从未为函数指定调用约定(__cdecl等),而将所有编译器开关都保留为默认设置。我检查了一下,项目设置对于在库和测试项目中调用约定是一致的。
更新:我们的一位开发人员将"基本运行时检查"项目设置从"两者(/ RTC1,等同于/ RTCsu)"更改为"默认",并且运行时消失了,从而使程序显然可以正确运行。我一点都不相信。这是正确的解决方案还是危险的破解?
解决方案
ESP是堆栈指针。因此,根据编译器,堆栈指针被弄乱了。很难说,在没有看到一些代码的情况下这是如何发生的(或者是否发生)。
重现此代码的最小代码段是多少?
沉默检查不是正确的解决方案。我们必须弄清楚是什么与调用约定有关。
有很多方法可以在不显式指定的情况下更改函数的调用对流。 extern" C"将执行此操作,STDMETHODIMP / IFACEMETHODIMP也将执行此操作,其他宏也可能执行该操作。
我相信,如果在WinDBG(http://www.microsoft.com/whdc/devtools/debugging/default.mspx)下运行程序,则运行时应该在遇到该问题时中断。我们可以查看调用堆栈并找出哪个函数有问题,然后查看其定义和调用方使用的声明。
我们要创建静态库还是DLL?如果是DLL,如何定义导出?如何创建导入库?
库中的函数原型是否与定义函数的函数声明完全相同?
我们是否有任何类型定义的函数原型(例如int(* fn)(int a,int b))
如果我们注册了dom,则可能会弄错原型。
ESP是在调用参数时不匹配的函数(在调试器中能分辨出哪个)时出错,即堆栈已恢复到调用该函数时的初始状态。
如果要加载需要外部声明的C ++函数,也可以得到此代码。C C使用cdecl,C ++默认使用stdcall调用约定(IIRC)。在导入的函数原型周围放置一些extern C包装器,即可对其进行修复。
如果可以在调试器中运行它,则会立即看到该函数。如果没有,我们可以设置DrWtsn32创建一个小型转储,我们可以将其加载到windbg中,以在发生错误时查看调用堆栈(不过,我们需要使用符号或者映射文件来查看函数名称)。
esp可能被弄乱的另一种情况是缓冲区的无意溢出,通常是由于错误地使用了指针来超出数组的边界。假设我们有一些看起来像的C函数
int a, b[2];
写到b [3]可能会改变a,并且在任何可能拖移堆栈中保存的esp的地方都可能会改变。
如果我们在Windows API中使用任何回调函数,则必须使用CALLBACK
和/或者WINAPI
进行声明。这将应用适当的修饰,以使编译器生成可正确清理堆栈的代码。例如,在Microsoft的编译器上,它添加了__stdcall。
Windows一直使用__stdcall
约定,因为它导致(略微)较小的代码,清理发生在被调用的函数中,而不是在每个调用位置。但是,它与varargs函数不兼容(因为只有调用者才知道他们推送了多少个参数)。
如果该函数是使用与调用约定不同的调用约定来调用的,则将收到此错误。
Visual Studio使用默认的调用约定设置,该设置在项目的选项中进行了缩放。检查原始项目设置和新库中的该值是否相同。一个过于野心勃勃的开发人员可能在原始版本中将其设置为_stdcall / pascal,因为与默认的cdecl相比,它减小了代码大小。因此,基本过程将使用此设置,而新库将获取默认的cdecl,这会导致问题
因为我们已经说过我们没有使用任何特殊的调用约定,所以这似乎是一个很好的可能性。
还对标头进行比较,以查看该进程看到的声明/文件是否与编译库时所用的声明/文件相同。
ps:使警告消失是BAAAD。基本错误仍然存在。
当代码尝试在非预期类型的对象上调用函数时,我看到了此错误。
因此,类层次结构:有孩子的父母:Child1和Child2
Child1* pMyChild = 0; ... pMyChild = pSomeClass->GetTheObj();// This call actually returned a Child2 object pMyChild->SomeFunction(); // "...value of ESP..." error occurs here
此调试错误意味着函数调用后堆栈指针寄存器未返回其原始值,即函数调用之前的推入次数与调用之后的弹出次数不相等。
我知道有两个原因(都包含动态加载的库)。 #1是VC ++在错误消息中描述的内容,但我认为这不是导致错误的最常见原因(请参见#2)。
1)呼叫约定不匹配:
呼叫者和被呼叫者对于谁将要做什么没有正确的协议。例如,如果我们要调用的是_stdcall的DLL函数,但是由于某种原因,我们在调用中将其声明为_cdecl(在VC ++中为默认值)。如果我们在不同的模块等中使用不同的语言,这会发生很多。
我们将必须检查有问题的函数的声明,并确保没有两次声明它,并且有所不同。
2)类型不匹配:
调用方和被调用方的编译类型不同。例如,一个通用标头定义了API中的类型,并且最近已更改,并且一个模块被重新编译,而另一个模块则未编译,即某些类型的呼叫者和被呼叫者的大小可能有所不同。
在这种情况下,调用方将推入一个大小的参数,但是被调用方(如果我们使用的是_stdcall,则被调用方将清理堆栈)将弹出不同大小的参数。因此,ESP没有返回到正确的值。
(当然,这些参数以及它们下面的其他参数在被调用的函数中似乎是乱码,但有时我们可以在没有可见崩溃的情况下幸免于难。)
如果我们有权访问所有代码,只需重新编译即可。
我在其他论坛上读过
我遇到了同样的问题,但是我已经解决了。我从以下代码中得到了相同的错误:
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关键字)。
希望对未来的人们有所帮助! :-)
干杯。