62

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?

valiano
  • 16,433
  • 7
  • 64
  • 79
dud3
  • 621
  • 4
  • 3
  • Very good question!!! Did you try (when you get "dynamic_pinvoke") to skip GetPinvokeMap and to switch to StackWalk64 family functions? (http://msdn.microsoft.com/en-us/library/windows/desktop/ms680650(v=vs.85).aspx) – Adriano Repetti Jun 07 '12 at 17:23
  • 1
    Document the HRESULT return values, on *all* of these calls. – Hans Passant Jun 07 '12 at 19:56
  • @HansPassant: all calls returns S_OK but GetPinvokeMap which ends up with 0x80131130 (CLDB_E_RECORD_NOTFOUND). – dud3 Jun 08 '12 at 08:49
  • I am not familiar with the profiling interfaces, but I have an idea. Take the `FunctionId` you get in the callback, pass it to `ICorProfilerInfo::GetCodeInfo` to get the entry point of the function (I think...). With that address, [use `VirtualQuery` to get the handle of the module in which it is located](http://stackoverflow.com/q/6734095/151292) then use [`GetModuleFileName`](http://msdn.microsoft.com/en-us/library/windows/desktop/ms683197(v=vs.85).aspx) to get the name of the module. – James McNellis Jul 05 '12 at 03:48
  • I don't know of any API to get an export name from a function pointer, but you could [enumerate the exports of the module](http://stackoverflow.com/questions/1128150/win32-api-to-enumerate-dll-export-functions) and build your own reverse lookup table. Again, I'm not familiar with these APIs; it's just a thought I had when I read your question. Let me know if it helped and I can post this as an answer (or, if it doesn't help, I'd like to know too :-D). – James McNellis Jul 05 '12 at 03:49
  • Does this error associated with `GetPinvokeMap` happen with all the functions for the DLLs that are called? If so, are you sure you're using the right calling conventions in the code? Left for me however, I'd help the profiler by adding a helper method to dynamically store the name of the module for each function so it can be easily accessed or extracted from the profiler. – Chibueze Opata Jul 05 '12 at 04:44
  • Also, [Walking the call stack](http://www.codeproject.com/Articles/11132/Walking-the-callstack) as Adriano suggested could be an alternative... – Chibueze Opata Jul 05 '12 at 04:46
  • Is this a language-agnostic question? Why are there so many language tags? Please retag with only *one* language. – Drise Jul 16 '12 at 18:53

1 Answers1

5

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);
        }
    }
}
joncham
  • 1,584
  • 10
  • 14
  • Really interesting answer. Although it could make sense to use `AssemblyBuilderAccess.RunAndCollect` to avoid memory leaks. – svick Jul 26 '12 at 09:44
  • That's true for longer running applications. Ideally, one would also create more than one pinvoke at a time. Perhaps all for a given library in a single ModuleBuilder or TypeBuilder. – joncham Jul 26 '12 at 13:46