Home » C++ » Profiling a dynamic pinvoke

Profiling a dynamic pinvoke

Posted by: admin November 29, 2017 Leave a comment

Questions:

I am working on MSIL profiler and encountered problems with ManagedToUnmanagedTransition and UnmanagedToManagedTransition callbacks of ICorProfilerCallback interface.

What I want to retrieve is an information about method being called (name and module name it resides in).

So far it was working fine. Until so called dynamic pinvoke occured (described in detail at: http://blogs.msdn.com/b/jonathanswift/archive/2006/10/03/dynamically-calling-an-unmanaged-dll-from-.net-_2800_c_23002900_.aspx)

In this scenario IMetaDataImport::GetPinvokeMap fails. Also IMetaDataAssemblyImport::GetAssemblyProps returns “dynamic_pinvoke” as a name of the assembly.

profiler_1_0->GetTokenAndMetaDataFromFunction(function_id, IID_IMetaDataImport, (IUnknown**) &imd_import, &md_token);
imd_import->GetPinvokeMap(md_token, &mapping, module_name, buffer_size, &chars_read, &md_module_ref);
// here the fail occurs

profiler_1_0->GetTokenAndMetaDataFromFunction(function_id, IID_IMetaDataAssemblyImport, (IUnknown**) &imd_assembly_import, &md_token);
imd_assembly_import->GetAssemblyFromScope(&md_assembly);
imd_assembly_import->GetAssemblyProps(md_assembly, 0, 0, 0, assembly_name, buffer_size, &chars_read, 0, 0);
// assembly_name is set to "dynamic_pinvoke"

How to obtain a module name (.dll) and a name of function being pinvoked through dynamic pinvoke?

Answers:

The profiler APIs are returning metadata specified in the managed code, normally via the DllImportAttribute. In the case of the ‘dynamic pinvoke’ which uses the Marshal.GetDelegateForFunctionPointer method, the module and function names were never specified as metadata and not available. An alternative approach to dynamic pinvoke declarations that includes the required metadata will probably avoid this issue. Try using System.Reflection.Emit APIs such as TypeBuilder.DefinePInvokeMethod as one solution.

Here is an example using System.Reflection.Emit that does work with the profiler APIs.

using System;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using System.Reflection;

namespace DynamicCodeCSharp
{
    class Program
    {
        [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]
        private delegate int MessageBoxFunc(IntPtr hWnd, string text, string caption, int options);

        static readonly Type[] MessageBoxArgTypes = new Type[] { typeof(IntPtr), typeof(string), typeof(string), typeof(int)};

        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string dllToLoad);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);

        [DllImport("kernel32.dll")]
        public static extern bool FreeLibrary(IntPtr hModule);

        static MethodInfo BuildMessageBoxPInvoke(string module, string proc)
        {
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(module), AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(module);
            TypeBuilder typeBuilder = moduleBuilder.DefineType(proc);

            typeBuilder.DefinePInvokeMethod(proc, module, proc,
                       MethodAttributes.Static | MethodAttributes.PinvokeImpl,
                       CallingConventions.Standard, typeof
                       (int), MessageBoxArgTypes, 
                       CallingConvention.StdCall, CharSet.Auto);

            Type type = typeBuilder.CreateType();

            return type.GetMethod(proc, BindingFlags.Static | BindingFlags.NonPublic); ;
        }

        static MessageBoxFunc CreateFunc()
        {
            MethodInfo methodInfo = BuildMessageBoxPInvoke("user32.dll", "MessageBox");
            return (MessageBoxFunc)Delegate.CreateDelegate(typeof(MessageBoxFunc), methodInfo);
        }

        static void Main(string[] args)
        {
            MessageBoxFunc func = CreateFunc();
            func(IntPtr.Zero, "Hello World", "From C#", 0);
        }
    }
}

A few examples to demonstrate the issues with the current approach.

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBox(IntPtr hWnd, string text, string caption, int options);

static void Main(string[] args)
{
     MessageBox(IntPtr.Zero, "Hello World", "From C#", 0);
}

There is no MessageBox function exported from user32.dll. It only contains MessageBoxA and MessageBoxW. As we did not specify the ExactSpelling=false in the DllImport attribute and our CharSet is Unicode, .Net will also search user32.dll for our entry point appended with a W. This means MessageBoxW is in fact the native function we are calling. However, GetPinvokeMap returns ‘MessageBox’ as the function name (module_name variable in your code).

Now lets instead invoke the function via ordinal number rather than name. Using the dumpbin program in the Windows SDK:

dumpbin /exports C:\Windows\SysWOW64\user32.dll

...
2046  215 0006FD3F MessageBoxW
...

2046 is the ordinal number for MessageBoxW. Adjusting our DllImport declaration to use the EntryPoint field we get:

[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "#2046")]
public static extern int MessageBox(IntPtr hWnd, string text, string caption, int options);

This time GetPInvokeMap returns “#2046”. We can see the profiler knows nothing about the ‘name’ of the native function being invoked.

Going even further, the native code being called may not even have a name. In the following example, an ‘Add’ function is created in executable memory at runtime. No function name or library has ever been associated with the native code being executed.

using System;
using System.Runtime.InteropServices;

namespace DynamicCodeCSharp
{
    class Program
    {
        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate int AddFunc(int a, int b);

        [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall)]
        private static extern IntPtr VirtualAlloc(IntPtr addr, IntPtr size, int allocType, int protectType);

        const int MEM_COMMIT = 0x1000;
        const int MEM_RESERVE = 0x2000;

        const int PAGE_EXECUTE_READWRITE = 0x40;

        static readonly byte[] buf = 
            {
                // push ebp
                0x55,
                // mov ebp, esp
                0x8b, 0xec,
                // mov eax, [ebp + 8]
                0x8b, 0x45, 0x08,
                // add eax, [ebp + 8]
                0x03, 0x45, 0x0c,
                // pop ebp
                0x5d,
                // ret
                0xc3
            };

        static AddFunc CreateFunc()
        {
            // allocate some executable memory
            IntPtr code = VirtualAlloc(IntPtr.Zero, (IntPtr)buf.Length, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
            // copy our add function implementation into the memory
            Marshal.Copy(buf, 0, code, buf.Length);
            // create a delegate to this executable memory
            return (AddFunc)Marshal.GetDelegateForFunctionPointer(code, typeof(AddFunc));
        }

        static void Main(string[] args)
        {
            AddFunc func = CreateFunc();
            int value = func(10, 20);
            Console.WriteLine(value);
        }
    }
}