具有非导出函数的 DllImport。为什么它有效?

DllImport with non-exported functions. Why is it working?

我刚遇到 C# 中 DllImport 的奇怪行为,我无法解释。我想知道它是如何实现的以及我可以在哪里阅读它。案例是通过 DllImport 可以调用不真正导出表单 dll 的函数。在我的例子中,它是 kernel32.dll 和函数 ZeroMemory(但具有 Copy/Move/Fill 内存这种行为)。所以,我的代码:

[DllImport("kernel32", EntryPoint = "LoadLibraryW", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr LoadLibrary(string libName);

[DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr GetProcAddress(IntPtr module, string procName);

//WTF???
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ZeroMemory(IntPtr address, int size);

static void TestMemory()
{
    IntPtr mem = Marshal.AllocHGlobal(100); //Allocate memory block of 100 bytes size
    Marshal.WriteByte(mem, 55);            //Write some value in the first byte
    ZeroMemory(mem, 100);                   //Clearing block of memory
    byte firstByte = Marshal.ReadByte(mem); //Read the first byte of memory block
    Console.WriteLine(firstByte);           //Output 0 (not 55) - ZeroMemory is working

    //Getting address of ZeroMemory from kernel32.dll
    IntPtr kernelHandle = LoadLibrary("kernel32.dll");
    IntPtr address = GetProcAddress(kernelHandle, "ZeroMemory");
    Console.WriteLine(address.ToString("X"));   //Output 0 - Library kernel32.dll DOESN'T export function ZeroMemory!!!

    //Testing GetProcAddress via getting address of some exported function
    Console.WriteLine(GetProcAddress(kernelHandle, "AllocConsole").ToString("X"));  //Output some address value - all is OK.
}

没有抛出 EntryPointNotFoundException - 代码工作正常。如果将 ZeroMemory 的名称更改为 ZeroMemory1 或类似名称 - 将抛出异常。但是在 kernel32.dll 的导出 table 中,我们看到:

根本没有 ZeroMemory 函数! 如果我们查看 msdn,我们会读到 ZeroMemory 只是 WinBase.h C++ 头文件中的一个宏。在里面我们看到:

#define RtlMoveMemory memmove
#define RtlCopyMemory memcpy
#define RtlFillMemory(d,l,f) memset((d), (f), (l))
#define RtlZeroMemory(d,l) RtlFillMemory((d),(l),0)
#define MoveMemory RtlMoveMemory
#define CopyMemory RtlCopyMemory
#define FillMemory RtlFillMemory
#define ZeroMemory RtlZeroMemory

显然,在 C++ ZeroMemory 中实际上是通过 ntdll.dll 中的 RtlFillMemory 工作的。但它是在 C++ 中!!!为什么它在 C# 中工作?在 DllImport 属性 here 的官方文档中,我们可以阅读下一个:

As a minimum requirement, you must supply the name of the DLL containing the entry point.

但在那种情况下 kernel32.dll 无法连接 ZeroMemory 的入口点。到底是怎么回事??请帮忙。

OP 是正确的,因为 kernel32.dll 没有导出 ZeroMemory 导出,但是 C# DllImport 以某种方式成功地神奇地将 ZeroMemory 引用解析为正确的 RtlZeroMemory 在针对框架的 .NET 应用程序中导出(但 不是 在核心)。

事实证明,框架专门检查了少数记录为 inlines/macros(MoveMemoryCopyMemory FillMemoryZeroMemory)的 Win32 API代码并在内部重新路由到正确的出口。虽然没有正式记录,但已在 MS 认可的 comment under a .NET Runtime 问题中得到确认。

As an FYI, there were a few special cased P/Invoke names in .NET Framework: MoveMemory, CopyMemory, FillMemory, and ZeroMemory. All of these will work when pointing at kernel32 on .NET Framework, but will fail on .NET Core. Please use the EntryPoint property in the DllImport attribute to get the desired behavior. The proper export names can be found using dumpbin /exports kernel32.dll from a Visual Studio command prompt.

上面建议添加一个明确的 EntryPoint 声明以在 Framework 和 Core 中工作,例如:

[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
public static extern void ZeroMemory(IntPtr address, IntPtr count);

神奇的名称重新映射发生在开源 .NET 代码之外,但可以在 Simon Mourier 在评论中提供的反汇编中清楚地看到。