错误消息告诉您 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。如果您不走运,该程序可能不会立即崩溃。您可以确定的一件事:您的程序不再执行您认为的代码。