9

我正在尝试使用 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);

}
4

5 回答 5

10

您的typedef函数指针需要与您正在使用的库的调用约定相匹配。例如,如果InitMotoBee使用,cdecltypedef将如下所示:

typedef bool (__cdecl *Type_InitMotoBee)(void)

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

于 2012-04-09T21:12:56.517 回答
6

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

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

堆栈指针必须始终恢复到函数调用之前的相同值。

调用约定

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

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

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

作为参考,请查看这个 StackOverflow 问题

Raymond Chen 有一篇关于这个主题的好博文。

您应该使用哪种调用约定?

这超出了这个答案的范围,但是如果你正在做 C# 到 C 的互操作,那么了解什么调用约定是很重要的。

在 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# 互操作包装器将知道生成 cdecl 调用。

还有什么问题?

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

编组结构

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

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

如果您在 C# 中有 stdcall 委托,则如下所示:

public delegate void SampleTimeChangedCallback(SampleTime sampleTime);

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

typedef void(__stdcall *SampleTimeChangedCallback)(SampleTime sampleTime);

一切都应该没问题。您在双方都使用相同的调用约定(C# 互操作默认使用 stdcall,我们在本机代码中明确设置 __stdcall)。

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

本机结构看起来像这样:

struct SampleTime
{
    __int64 displayTime;
    __int64 playbackTime;
}

C# 结构如下所示:

[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 中,被调用者(即被调用的函数)负责恢复堆栈。

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

然后执行该函数 - 请记住,实际上这是 ac# 委托。

由于我们使用 stdcall,被调用者(c# 委托)负责恢复堆栈。但是在 C# 领域,我们对编译器撒了谎,告诉它 SampleTime 结构的大小是 32 字节,而实际上它只有 16 个字节。

我们违反了单一定义规则

C# 编译器别无选择,只能相信我们告诉它的内容,因此它将堆栈指针“恢复”32 字节。

当我们返回调用站点(在本地)时,堆栈指针尚未正确恢复,所有赌注都已关闭。

如果幸运的话,您会遇到运行时检查 #0。如果您不走运,该程序可能不会立即崩溃。您可以确定的一件事:您的程序不再执行您认为的代码。

于 2016-05-10T23:28:06.220 回答
1

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

于 2014-11-21T00:05:44.750 回答
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++ 如何处理其在内存中的成员函数调用(静态内存分配、动态内存分配)有关。您可以看看静态内存分配和动态内存分配之间的区别

于 2017-01-26T13:30:30.170 回答
0

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

TExternalProcCallbackDetectorError = procedure(dwError: DWORD); cdecl;

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

在摸索了一段时间后,我偶然发现了这个答案,尤其是@Rob's(感谢Rob!)。Delphi 不支持__thiscall,但将 Delphi 原型更改为以下解决了该问题:

TExternalProcCallbackDetectorError = procedure(dwError: DWORD); stdcall;
于 2020-04-15T14:04:49.840 回答