【问题标题】:C++ Run-Time Check Failure #0 - The value of ESP was not properly saved across a function callC++ 运行时检查失败 #0 - ESP 的值未在函数调用中正确保存
【发布时间】:2012-04-22 04:54:55
【问题描述】:

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

}

【问题讨论】:

  • 你能告诉我们Type_InitMotoBee等人的声明吗?
  • typedef bool (*Type_InitMotoBee)(void)
  • 定义为 const 的函数也有同样的问题。 Visual Studio 并没有像我想象的那样构建一切。似乎所有标记为 const 的东西都没有被触及。这可能导致集线器下的函数签名不匹配?

标签: c++ dll


【解决方案1】:

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

typedef bool (__cdecl *Type_InitMotoBee)(void)

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

【讨论】:

  • 调用约定可以是别的东西,这可以帮助您确定调用约定 stackoverflow.com/questions/4162400/… 。只需在依赖遍历器之类的应用程序中打开 mtb.dll 即可查看您正在调用的方法的符号。
【解决方案2】:

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

【讨论】:

  • 这可能只是删除了诊断,问题仍然存在
【解决方案3】:

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

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

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

调用约定

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

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

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

参考请看this StackOverflow question

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

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

这超出了此答案的范围,但如果您正在执行 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# interop 默认使用 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 中,被调用者 - 即被调用的函数 - 负责恢复堆栈。

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

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

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

我们违反了One Definition Rule

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

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

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

【讨论】:

  • 对于上述SampleTimeChangedCallback 委托的示例,要将其更改为匹配__cdecl 非托管指针,请使用委托定义上方的属性[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
【解决方案4】:

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

我通过以下方式解决了它。 就我而言,当我尝试将成员函数作为回调传递给线程以执行异步调用时,问题就出现了。类本身是可执行项目调用的 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

【讨论】:

    【解决方案5】:

    使用 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's(感谢 Rob!)。 Delphi 不支持__thiscall,但是将 Delphi 原型更改为以下解决了该问题:

    TExternalProcCallbackDetectorError = procedure(dwError: DWORD); stdcall;
    

    【讨论】:

      猜你喜欢
      • 2023-03-06
      • 2012-01-25
      • 1970-01-01
      • 2011-01-26
      • 2010-11-30
      • 1970-01-01
      • 1970-01-01
      • 2011-10-06
      相关资源
      最近更新 更多