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