Search code examples
c#.netwinapimarshallingdllimport

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


I just encountered with strange behaviour of DllImport in C#, which I can't explain. I want to know how It is possible and where I can read about It. Case is that via DllImport one can call function that doesn't really exported form dll. In my case It is kernel32.dll and function ZeroMemory (but with Copy/Move/Fill memory such behavior). So, my code:

[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.
}

No EntryPointNotFoundException is thrown - code works fine. If change name of ZeroMemory to ZeroMemory1 or something like that - exception will be thrown. But in export table of kernel32.dll we see:

There is NO ZeroMemory!!!

There is NO ZeroMemory function at all! If we look in msdn, we read that ZeroMemory is just a macro in WinBase.h header file for C++. Inside that we see:

#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

Obviously, that in C++ ZeroMemory actually works through RtlFillMemory from ntdll.dll. But it is in C++!!! Why is it work in C#?? On official documentation for DllImport attribute here we can read the next:

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

But in that case kernel32.dll CANNOT contating entry point for ZeroMemory. What is going on?? Help, please.


Solution

  • The OP is correct in that kernel32.dll has no export ZeroMemory export, yet the C# DllImport somehow succeeds to magically resolve the ZeroMemory reference to the correct RtlZeroMemory export in .NET apps targeted at the Framework (but not at Core).

    Turns out that a handful of Win32 APIs documented as inlines/macros (MoveMemory, CopyMemory FillMemory, ZeroMemory) are specifically checked by the Framework code and internally rerouted to the correct exports. While not formally documented, this was acknowledged in a MS-sanctioned comment under a .NET Runtime issue.

    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.

    The above suggests adding an explicit EntryPoint for the declaration to work in both Framework and Core, for example:

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

    The magic name remapping happens outside the open-sourced .NET code, but can be clearly seen in the disassembly contributed by Simon Mourier in a comment.

    enter image description here