7

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.

Tadeusz
  • 6,453
  • 9
  • 40
  • 58
  • 2
    Clearly, the CLR has a lot of Windows API knowledge built in. You can, for example, P/Invoke API calls that take character strings and still omit the trailing `A` or `W` from the symbol name. I would not be surprised if this were another case of the CLR trying to be helpful (read: opaque in what it's doing). – IInspectable Mar 12 '21 at 08:50
  • 2
    Seems to be remapped internally, see [this](https://github.com/dotnet/pinvoke/issues/431): "*.NET Framework has special case logic to ensure that MoveMemory, CopyMemory, FillMemory, and ZeroMemory are correctly mapped to RtlMoveMemory, RtlCopyMemory, RtlFillMemory, and RtlZeroMemory respectively*". – dxiv Mar 12 '21 at 08:54
  • @dxiv Thanks for your link! Strange, that this case is not documented in .NET documentation. Special case...Hmm. – Tadeusz Mar 12 '21 at 08:59
  • @dxiv Post that as an answer. This is a good one, I never knew this – Charlieface Mar 12 '21 at 09:49
  • 1
    You can see that voodoo here in clr.dll (unfortunately that part of .NET Framework is not open source): https://i.imgur.com/SQtTpMo.png – Simon Mourier Mar 12 '21 at 09:55
  • @SimonMourier Great thanks!! Post this as an answer. – Tadeusz Mar 12 '21 at 10:21
  • 1
    @dxiv - you should post the answer, see also this: https://stackoverflow.com/questions/47014586/different-p-invoke-entry-point-for-net-vs-net-core-2/47016390 – Simon Mourier Mar 12 '21 at 10:24
  • I shoud add to @SimonMourier answer that CLR calls Rtl* functions from kernel32.dll (from assembly listing), but in kernel32.dll those functions are just aliases for eponymous functions from ntdll.dll (export forwarding). – Tadeusz Mar 12 '21 at 10:32
  • *"Strange, that this case is not documented"* - I don't see anything strange here, that's just .NET's way to go about things. The amount of damage .NET has done to Windows as a native development platform is staggering. – IInspectable Mar 12 '21 at 11:47
  • @SimonMourier I was hoping someone more familiar with the .NET internals could track it down in the sources and post a more satisfying answer, but apparently this happens in the closed-source part. – dxiv Mar 12 '21 at 21:10
  • @dxiv - well, this is still good as an answer (you can reuse my disassembly bitmap if you want) – Simon Mourier Mar 13 '21 at 06:23
  • @SimonMourier Thanks, that disassembly was too tempting to resist ;-) – dxiv Mar 13 '21 at 07:24

1 Answers1

4

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

dxiv
  • 16,984
  • 2
  • 27
  • 49
  • 1
    I was lucky to grab an early copy of CoreCLR from github, that code was still in method.cpp, NDirectMethodDesc::FindEntryPoint(). Lost when they hacked out all of the non-core features from the codebase. Might as well mention other functions that get mapped, Get/SetWindowLong and Get/SetClassLong go to their Ptr flavor in 64-bit mode. – Hans Passant Mar 13 '21 at 09:27
  • @HansPassant Thanks for the pointer. On a quick look, however, that remapping doesn't seem to be done automatically by the framework ([1](https://stackoverflow.com/questions/54833997/i-keep-getting-unable-to-find-an-entry-point-named-getwindowlongptra-in-dll)). Your answer ([2](https://stackoverflow.com/a/3344276)) suggests that it needs to be done in the client code explicitly, the same way WinForms does it for example Still quite the black magic, just a slightly different flavor of it. – dxiv Mar 14 '21 at 01:53