C ++运行时检查失败#0 - ESP的值未在函数调用中正确保存

时间:2012-04-09 20:45:46

标签: c++ dll

我正在尝试使用c ++编程motorbee

当我运行代码时,我收到以下错误:

  

运行时检查失败#0 - ESP的值是   没有在函数调用中正确保存。    这通常是调用声明的函数的结果    一个调用约定,声明了一个函数指针    与不同的召集惯例。

这是我的代码。

#include "stdafx.h"
#include <iostream>
#include "windows.h"
#include "mt.h"
using namespace std;

HINSTANCE BeeHandle= LoadLibrary("mtb.dll"); 
Type_InitMotoBee InitMotoBee;
Type_SetMotors SetMotors;
Type_Digital_IO Digital_IO;
void main ()
{   
    InitMotoBee = (Type_InitMotoBee)GetProcAddress( BeeHandle,"InitMotoBee");
    SetMotors =(Type_SetMotors)GetProcAddress(BeeHandle,"SetMotors");
    Digital_IO =(Type_Digital_IO)GetProcAddress(BeeHandle,"Digital_IO ");
    InitMotoBee();

    SetMotors(0, 50, 0, 0, 0, 0, 0, 0, 0);

}

5 个答案:

答案 0 :(得分:9)

您的typedef函数指针需要与您正在使用的库的calling convention匹配。例如,如果InitMotoBee使用cdecl,则typedef将如下所示:

typedef bool (__cdecl *Type_InitMotoBee)(void)

SetMotors函数接受参数,因此也需要正确设置调用约定(这可能是应用程序失败的地方)。

答案 1 :(得分:4)

错误消息告诉您ESP寄存器(堆栈指针)未正确维护&#34;。它没有应有的价值。

当您使用非托管语言(如C或C ++)进行函数调用时,函数的参数将被推送到堆栈 - 增加堆栈指针。当函数调用返回时,弹出参数 - 减少堆栈指针。

必须始终将堆栈指针恢复为函数调用之前的值。

召集会议

调用约定精确指定应如何维护堆栈,以及调用者或被调用者是否负责从堆栈中弹出参数。

例如,在stdcall调用约定中,调用 ee 负责在函数返回之前恢复堆栈指针。在cdecl调用约定中,调用 er 负责。

显然混合调用约定很糟糕!如果呼叫 er 正在使用stdcall,则它希望呼叫 ee 来维护堆栈。如果调用 ee 正在使用cdecl,那么它希望调用 er 来维护堆栈。最终结果:没有人维护堆栈!或者相反的例子:每个人都在维护堆栈,这意味着它会被恢复两次并最终出错。

供参考,请查看this StackOverflow question

Raymond Chen在这个问题上有一个很好的blog post

您应该使用哪个电话会议?

这超出了本答案的范围,但如果您正在进行C#到C interop,那么了解调用约定是很重要的。

在Visual Studio中,C / C ++项目的默认调用约定是cdecl。

在.Net中,使用DllImport进行互操作调用的默认调用约定是stdcall。这也适用于代表。 (大多数本机Windows功能都使用stdcall。)

请考虑以下(不正确的)互操作。

[DllImport("MyDll", EntryPoint = "MyDll_Init"]
public static extern void Init();

它正在使用stdcall调用约定,因为它是.Net的默认值。如果您还没有更改MyDLL项目的Visual Studio项目设置,您很快就会发现这不起作用。 C / C ++ DLL项目的默认值为cdecl。

正确的互操作电话将是:

[DllImport("MyDll", EntryPoint = "MyDll_Init", CallingConvention = CallingConvention.Cdecl)]
public static extern void Init();

请注意显式的CallingConvention属性。 C#interop包装器将知道生成cdecl调用。

还有什么可能出错?

如果您确定您的呼叫约定是正确的,您可能仍会遇到运行时检查失败#0。

编组结构

回想一下,函数参数在函数调用开始时被压入堆栈,然后在结束时再次弹出。为了确保正确维护堆栈,参数的大小必须在push和pop之间保持一致。

在本机代码中,编译器将为您处理此问题。你永远不需要考虑。当谈到C和C#之间的互操作时,你可能会被咬伤。

如果你在C#中有一个stdcall委托,就像这样:

public delegate void SampleTimeChangedCallback(SampleTime sampleTime);

对应于C函数指针,如下所示:

typedef void(__stdcall *SampleTimeChangedCallback)(SampleTime sampleTime);
一切都应该没问题。您在双方使用相同的调用约定(默认情况下,C#interop使用stdcall,我们在本机代码中明确设置__stdcall)。

但请看一下这些参数:SampleTime结构。它们都具有相同的名称,但一个是本机结构,另一个是C#结构。

本机结构看起来像这样:

struct SampleTime
{
    __int64 displayTime;
    __int64 playbackTime;
}

C#struct看起来像这样:

[StructLayout(LayoutKind.Explicit, Size = 32)]
public struct SampleTime
{
    [FieldOffset(0)]
    private long displayTime;

    [FieldOffset(8)]
    private long playbackTime;
}

查看C#结构的Size属性 - 它错了!两个8字节长度意味着16字节大小。也许有人删除了一些字段并且无法更新Size属性。

现在,当本机代码使用stdcall调用SampleTimeChangedCallback函数时,我们遇到了问题。

回想一下,在stdcall中,被调用者 - 即被调用的函数 - 负责恢复堆栈。

所以:调用者将参数推送到堆栈。在此示例中,这是在本机代码中发生的。参数的大小是编译器已知的,因此保证堆栈指针递增的值是正确的。

然后执行该功能 - 请记住,实际上这是一个c#委托。

由于我们正在使用stdcall,因此被调用者(c#delegate)负责恢复堆栈。但是在C#land中,我们对编译器撒了谎,告诉它SampleTime结构的大小是32字节,当它真的只有16时。

我们违反了One Definition Rule

C#编译器别无选择,只能相信我们告诉它的内容,因此它将会恢复&#34;堆栈指针由32字节组成。

当我们返回呼叫站点(在本地)时,堆栈指针未正确恢复,所有投注均已关闭。

如果运气好的话,您将遇到#0的运行时检查。如果你不走运,程序可能不会马上崩溃。您可以确定的一件事是:您的程序不再执行您认为的代码。

答案 2 :(得分:0)

我最终将编译器选项从/ RTC1(实际上是/ RTC和/ RTCu)更改为/ RTCu。 http://support.microsoft.com/kb/822039

答案 3 :(得分:0)

我遇到了类似的问题,其中出现了相同的错误消息。

我用以下方式解决了这个问题。 在我的情况下出现问题,当我试图将成员函数作为回调传递给线程以执行异步调用。该类本身是由可执行项目调用的DLL(子组件)的一部分。

OGLModel::~OGLModel() {
  std::thread delVertexThread(&OGLModel::AsyncDisposeVertices, this, vertices);
  delVertexThread.join();
}

void OGLModel::AsyncDisposeVertices(std::vector<OGLVertex> *vertices)
{

  std::cout << "OGLModel garbage collection active..";
  if (vertices != 0) {
    std::vector<OGLVertex> *swap = new std::vector<OGLVertex>();
    vertices->swap(*swap);
    delete vertices;
  }
  std::cout << "OGLModel garbage collection finished..";
} 

成员函数OGLModel::AsyncVertexDispose的声明是通过在标题中使用virtual来执行的。删除virtual限定符后,ESP错误消息消失。

我没有有效的解释,但有些想法。我认为c ++如何处理内存中的成员函数调用(静态内存分配,动态内存分配)。您可以查看Difference between static memory allocation and dynamic memory allocation

答案 4 :(得分:0)

使用Visual Studio 2019 DLL出现类似的问题,该DLL内部使用Visual Studio 2017中编写的第3方库,该库使用Microsoft特定的__thiscall调用约定。我需要在Delphi 7应用程序中调用回调。在早期版本的MSVC中,DLL使用__cdecl调用约定,因此我的回调在Delphi中定义为:

TExternalProcCallbackDetectorError = procedure(dwError: DWORD); cdecl;

过去,这种类型的原型已与许多VS2003 DLL一起使用,没有任何问题。但是,当VS2019 C ++ DLL调用回调时,将调用Delphi代码...,然后引发Run-Time Check Failure #0异常。灾难!

挠了一下头之后,我偶然发现了这个答案,尤其是@Rob的答案(感谢Rob!)。 Delphi不支持__thiscall,但是将Delphi原型更改为以下内容可以解决此问题:

TExternalProcCallbackDetectorError = procedure(dwError: DWORD); stdcall;