In my previous post, I showed how PInvoke stubs are generated at run time in intermediate language (IL) by the Common Language Runtime (CLR), and compiled to machine code on the fly by the same Just-In-Time compiler (JIT) that compiles the IL streams inside your usual C# compiled assemblies.

We found that these stubs – not entirely surprisingly – do 3 basic things:

  • marshal the input arguments from managed to native
  • call the unmanaged function
  • marshal the native result (and possible out/ref arguments) back to managed

The only questions remaining were: how does the stub get the address of the unmanaged function, and what does this StubHelpers.GetStubContext() method do ?

The answer will come naturally if we take for example a simple program that has 2 PInvoke methods with the same input and output arguments:

[DllImport("kernel32.dll")]
static extern void ExitThread(uint dwExitCode);

[DllImport("kernel32.dll")]
static extern void Sleep(uint dwMilliseconds);

If I let the CLR generate an IL stub for both methods, I have exactly the same input and output marshalling, and even the unmanaged function call signature (not address) is the same.

That seems a bit of a waste, so how could one optimize this ?

Indeed, we would save on basically everything we care about (RAM, JIT compilation) by just generating one IL stub for every unique input+output argument signature, and injecting that stub with the unmanaged address it needs to call.

This is exactly how it works: when the CLR encounters a PInvoke method, it pushes a frame on the stack (InlinedCallFrame) with info about – among other things – the unmanaged function address just before calling the actual IL stub.

The stub in turn requests this information through StubHelpers.GetStubContext() (aka ‘gimme my callframe’), and calls into the unmanaged function.

To see this in action, consider the code:

namespace TestPInvoke
{
    class Program
    {
        [DllImport("kernel32.dll")]
        static extern void Sleep(uint dwMilliseconds);

        [DllImport("kernel32.dll", EntryPoint = "Sleep")]
        static extern void SleepAgain(uint dwMilliseconds);

        static void Main(string[] args)
        {
            Console.WriteLine("Press any key...");

            while (!Console.KeyAvailable)
            {
                Sleep(500);
                SleepAgain(500);
            }
        }
    }
}

I’ll run this from WinDbg+SOS, here’s the disassembly of the calls to Sleep and SleepAgain in main:

mov     ecx,1F4h
call    0042c04c (TestPInvoke.Program.Sleep(UInt32), mdToken: 06000001)
mov     ecx,1F4h
call    0042c058 (TestPInvoke.Program.SleepAgain(UInt32), mdToken: 06000002)

You see the calls to Sleep and SleepAgain are pointing to different addresses. If we dump the unmanaged code at these locations we have:

!u 0042c04c (Sleep)
Unmanaged code
mov     eax,42379Ch
jmp     006100d0 (DomainBoundILStubClass.IL_STUB_PInvoke(UInt32))

!u 0042c058 (SleepAgain)
Unmanaged code
mov     eax,4237C8h
jmp     006100d0 (DomainBoundILStubClass.IL_STUB_PInvoke(UInt32)

Indeed, we see in a few lines that some different value is loaded into eax, before jumping to the same address (the IL stub). Since the value in eax is the only thing seperating the two, this must be a pointer to our call frame.

So let’s consider these as memory addresses and check what’s there:

dd 42379Ch
0042379c  63000001 20ea0005 00000000 00192385
004237ac  001925ec 00423808 0042c010 00000000

dd 4237C8h
004237c8  630b0002 20ea0006 00000000 00192385
004237d8  001925ec 00423810 0042c01c 00000000

Now remember the offset in the IL in the previous post ? The unmanaged call was to a pointer reference at offset 20 (14h) in our stubcontext. Or in plain words: take the value at offset 20 in the callframe (emphasized), and dereference it. This gives us:

00423808 => 7747cf49 (KERNEL32!SleepStub)
00423810 => 7747cf49 (same)

And there we have it, PInvoke demystified.

Ruurd Keizer

Author Ruurd Keizer

Quantumphysics PhD disguised as software architect, developer, and cloud native platform greasemonkey. Analytic, pragmatic, result oriented, never forgetting the bottom line. Interested in the whole picture: from businessvalue down to the bare metal.

More posts by Ruurd Keizer
18 March 2014

Leave a Reply