How to get current value of EIP in managed code?

2019-02-17 02:39发布

问题:

The question seems like a dirty hack you should not do but let me explain first. The ultimate goal is to have method local statics like in C++.

void Func()
{
   static methodLocalObject = new ExpensiveThing();
   // use methodlocal Object
}

What has this to do with the instruction pointer? I want to cache data depending on my caller. To make this fast I walk back in the stack to get the address of my caller and use this as unique key for a dictionary to store the data. That would allow to create a reflection based tracer which does not use Reflection every time to get the name of the current method and type but only once and store the reflection infos in a hash table.

The answers so far were only mono based. I want to try a generic solution which works on .NET 3.5/4.0 32/64 bit. I do know that the calling convention for 64 bit is quite different so it could get challenging to get something reliable. But on the other hand I have full control inside my method how the stack does look like. The stack does look very different between .NET 3.5 and 4.0 and it differs of course also between release builds. I still have to check if NGen does create code with a different stack layout as well. One possibility would be to use a C++ helper method which takes 5 magic integer arguments (on x64 only the 5th will be on the stack) and check where I can find them on the stack. Another possiblity would be to simply use the whole stack until I find my magic marker on the stack as a key and use this part of the stack as unique enough key. But I am not sure if this approach can work at all or if there are better alternatives. I do know I can walk the stack in a safe way via the profiling or debugging apis but neither of them are fast.

For a tracing library the usual approach is to walk the stack using reflection to get the current method name and type.

class Tracer
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    public Tracer()
    {
        StackFrame frame = new StackTrace().GetFrame(1); // get caller
        Console.WriteLine("Entered method {0}.{1}", frame.GetMethod().DeclaringType.FullName, frame.GetMethod().Name);
    }

}

But this is very slow. The other solution is to pass the data directly via strings which is much faster but it needs more typing. The alternate solution would be to use the instruction pointer of the calling function (if this can be determined in a very fast way) to get around the expensive reflection calls. Then this would be possible:

class Tracer
{
    static Dictionary<Int64, string> _CachedMethods = new Dictionary<Int64, string>();

    [MethodImpl(MethodImplOptions.NoInlining)]
    public Tracer()
    {
        Int64 eip = GetEIpOfParentFrame();
        string name;
        lock (_CachedMethods)
        {
            if (!_CachedMethods.TryGetValue(eip, out name))
            {
                var callingMethod = new StackTrace().GetFrame(1).GetMethod();
                name =  callingMethod.DeclaringType + "." + callingMethod.Name;
                _CachedMethods[eip] = name;
            }
        }
        Console.WriteLine("Entered method {0}", name);

    }

    Int64 GetEIpOfParentFrame()
    {
        return 0; // todo this is the question how to get it
    }

}

I know that the solution needs to be unmanaged. In C++ there is a compiler intrinsic called _ReturnAddress but according to the docs it does not work with managed code. Another way to ask the same question: Does anybody know the calling convention and stack layout for managed methods for .NET 3.5/4 x32/x64?

Yours, Alois Kraus

回答1:

With C# 5.0 there is a new, well-hidden feature that enables this.

Caller Info attributes

Note Apparently, there is also the Microsoft BCL Portability Pack 1.1.3 Nuget package so you can use the Caller Info Attributes in .NET 4.0.

What this does, is make your optional parameters magically have caller-dependent default values. It has

  • CallerFilePathAttribute Full path of the source file that contains the caller. This is the file path at compile time.
  • CallerLineNumberAttribute Line number in the source file at which the method is called.
  • CallerMemberNameAttribute Method or property name of the caller. See Member Names later in this topic.

It has some pretty nifty features:

  • Caller Info values are emitted as literals into the Intermediate Language (IL) at compile time.
  • Unlike the results of the StackTrace property for exceptions, the results aren't affected by obfuscation.

The documentation sample looks like this:

// using System.Runtime.CompilerServices 
// using System.Diagnostics; 

public void DoProcessing()
{
    TraceMessage("Something happened.");
}

public void TraceMessage(string message,
        [CallerMemberName] string memberName = "",
        [CallerFilePath] string sourceFilePath = "",
        [CallerLineNumber] int sourceLineNumber = 0)
{
    Trace.WriteLine("message: " + message);
    Trace.WriteLine("member name: " + memberName);
    Trace.WriteLine("source file path: " + sourceFilePath);
    Trace.WriteLine("source line number: " + sourceLineNumber);
}

// Sample Output: 
//  message: Something happened. 
//  member name: DoProcessing 
//  source file path: c:\Users\username\Documents\Visual Studio 2012\Projects\CallerInfoCS\CallerInfoCS\Form1.cs 
//  source line number: 31 


回答2:

Update This answer is now obsolete for recent version of .NET: see here How to get current value of EIP in managed code?

The real short answer is: the CLR VM is a stack machine, so no EIP there. The slightly longer answer is: if you rely on undocumented implementation-specific details, you could extrapolate a useable ID from the CPU EIP in unmanaged code.

Proof Of Concept

I just managed the following proof of concept, using mono 2.11 on Linux 32-bit. I hope the information might help. This implements unmanaged functions:

extern static string CurrentMethodDisplay();
extern static uint CurrentMethodAddress();

Native source: tracehelper.c [1]:

#include <string.h>

void* CurrentMethodAddress()
{
    void* ip;
    asm ("movl 4(%%ebp),%0" : "=r"(ip) );
    return ip;
}

const char* const MethodDisplayFromAddress(void* ip);
const char* const CurrentMethodDisplay()
{
    return MethodDisplayFromAddress(CurrentMethodAddress());
}

#ifndef USE_UNDOCUMENTED_APIS
extern char * mono_pmip (void *ip);

const char* const MethodDisplayFromAddress(void* ip)
{
    const char* text = mono_pmip(ip);
    return strdup(text? text:"(unknown)");
}
#else

/* 
 * undocumented structures, not part of public API
 *
 * mono_pmip only returns a rather ugly string representation of the stack frame
 * this version of the code tries establish only the actual name of the method
 *
 * mono_pmip understands call trampolines as well, this function skips those
 */
struct _MonoDomain; // forward
struct _MonoMethod; // forward
typedef struct _MonoDomain  MonoDomain;
typedef struct _MonoMethod  MonoMethod;
struct _MonoJitInfo { MonoMethod* method; /* rest ommitted */ };

typedef struct _MonoJitInfo MonoJitInfo;

MonoDomain *mono_domain_get(void);
char* mono_method_full_name(MonoMethod *method, int signature);
MonoJitInfo *mono_jit_info_table_find(MonoDomain *domain, char *addr);

const char* const MethodDisplayFromAddress(void* ip)
{
    MonoJitInfo *ji = mono_jit_info_table_find (mono_domain_get(), ip);
    const char* text = ji? mono_method_full_name (ji->method, 1) : 0;
    return text? text:strdup("(unknown, trampoline?)");
}

#endif

C# Source (client.cs) to call this native library function:

using System;
using System.Runtime.InteropServices;

namespace PoC
{
    class MainClass
    {
        [DllImportAttribute("libtracehelper.so")] extern static string CurrentMethodDisplay();
        [DllImportAttribute("libtracehelper.so")] extern static uint CurrentMethodAddress();

        static MainClass()
        {
            Console.WriteLine ("TRACE 0 {0:X8} {1}", CurrentMethodAddress(), CurrentMethodDisplay());
        }

        public static void Main (string[] args)
        {
            Console.WriteLine ("TRACE 1 {0:X8} {1}", CurrentMethodAddress(), CurrentMethodDisplay());
            {
                var instance = new MainClass();
                instance.OtherMethod();
            }
            Console.WriteLine ("TRACE 2 {0:X8} {1}", CurrentMethodAddress(), CurrentMethodDisplay());
            {
                var instance = new MainClass();
                instance.OtherMethod();
            }
            Console.WriteLine ("TRACE 3 {0:X8} {1}", CurrentMethodAddress(), CurrentMethodDisplay());
            Console.Read();
        }

        private void OtherMethod()
        {
            ThirdMethod();
            Console.WriteLine ("TRACE 4 {0:X8} {1}", CurrentMethodAddress(), CurrentMethodDisplay());
        }

        private void ThirdMethod()
        {
            Console.WriteLine ("TRACE 5 {0:X8} {1}", CurrentMethodAddress(), CurrentMethodDisplay());
        }
    }
}

Compile and link using Makefile:

CFLAGS+=-DUSE_UNDOCUMENTED_APIS
CFLAGS+=-fomit-frame-pointer
CFLAGS+=-save-temps
CFLAGS+=-g -O3

all: client.exe libtracehelper.so

client.exe: client.cs | libtracehelper.so
    gmcs -debug+ -optimize- client.cs 

tracehelper.s libtracehelper.so: tracehelper.c
    gcc -shared $(CFLAGS) -lmono -o $@ tracehelper.c 
#   gcc -g -O0 -shared -fomit-frame-pointer -save-temps -lmono -o $@ tracehelper.c 

test: client.exe
    LD_LIBRARY_PATH=".:..:/opt/mono/lib/" valgrind --tool=memcheck --leak-check=full --smc-check=all --suppressions=mono.supp mono --gc=sgen --debug ./client.exe

clean:
    rm -fv *.so *.exe a.out *.[iso] *.mdb

Running this with LD_LIBRARY_PATH=. ./client.exe results in:

TRACE 0 B57EF34B PoC.MainClass:.cctor ()
TRACE 1 B57EF1B3 PoC.MainClass:Main (string[])
TRACE 5 B57F973B PoC.MainClass:ThirdMethod ()
TRACE 4 B57F96E9 PoC.MainClass:OtherMethod ()
TRACE 2 B57EF225 PoC.MainClass:Main (string[])
TRACE 5 B57F973B PoC.MainClass:ThirdMethod ()
TRACE 4 B57F96E9 PoC.MainClass:OtherMethod ()
TRACE 3 B57EF292 PoC.MainClass:Main (string[])

Note that this is on Mono 2.11. It works on 2.6.7 as well, with and without optimization.

[1] I learned GNU extended asm for this purpose; thanks SO!

Conclusions ?

Delivered a proof of concept; this implementation is specific to Mono. A similar 'trick' could be delivered on MS .Net (using a ::LoadLibrary of SOS.dll, perhaps?) but is left as an exercise for the reader :)

I would personally still go with my other answer, but I suppose I succumbed to the challenge and, like I've said before: YMMV, Here be dragons, TIMTOWTDI, KISS etc.

Good night



回答3:

Update This answer is now obsolete for recent version of .NET: see here How to get current value of EIP in managed code?

Your best bet will be StackFrame(Int32):

Console.WriteLine(new System.Diagnostics.StackFrame(0).GetMethod().Name);
Console.WriteLine(new System.Diagnostics.StackFrame(0).GetNativeOffset());

More ideas

  • Use AOP (attribute based) instrumentation
  • Use Linfu or Cecil to dynamically emit the useful IDs

If you must, you can use code generator that will fill in the ids before compilation.



回答4:

I would use the profiler API's, but if you want more performance try Enter/Leave Hooks.

I think your trying to have your cake and eat it too, performance + portability don't always go together. Link in some MASM64 for performance :)



回答5:

I have another, (though highly experimental) idea, that is based on using expression trees to do the invocations to your method via a invoker and a facade.

Instead of normally calling your method, you would create an expression tree to invoke th facade from a given location at your code. This expression tree is passed to an invoker, that caches the compiled expression tree along with the caller information. The caller information can be retrieved once via StackTrace.GetMethod and cached against the expression tree.

From personal experience, since you only need a key for your invocation, you should store a MethodHandle only, instead of the full MethodBase object (dramatically reduces memory consumption).

To perform the real invocation, you can now examine the expression tree and build a new one to call the real implementation with either a dictionary containing your method level statics or pass it the caller method key.


Wow, this is really cool and fast as hell. Please provide feedback on the gist: https://gist.github.com/1047616

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace MethodStaticLocals
{
    class ExpensiveObject 
    {
        public ExpensiveObject()
        {
            Console.WriteLine( "Creating Expensive object" );
        }
    };

    class MainClass
    {
        public static void Main( string[] args )
        {
            Expression<Action> call = () => Func( "hello" );

            Invoke( call );
            Invoke( call );
        }

        // caches handles for expresisons, as they are expensive to find.
        static Dictionary<Expression, RuntimeMethodHandle> handleCache = new Dictionary<Expression, RuntimeMethodHandle>();
        // static locals are managed per method handle
        static Dictionary<RuntimeMethodHandle, Dictionary<string, object>> staticLocals = new Dictionary<RuntimeMethodHandle, Dictionary<string, object>>();
        // redirects are individual for each expression tree
        static Dictionary<Expression, Delegate> redirects = new Dictionary<Expression, Delegate>();

        static void Invoke( Expression<Action> call )
        {
            if (call.Parameters != null && call.Parameters.Any())
                throw new InvalidOperationException();

            if (call.Body.NodeType != ExpressionType.Call)
                throw new InvalidOperationException();

            Delegate redirectedInvocation = SetupRedirectedInvocation( call );

            redirectedInvocation.DynamicInvoke();

        }

        private static Delegate SetupRedirectedInvocation( Expression<Action> call )
        {
            Delegate redirectedInvocation;
            if (!redirects.TryGetValue( call, out redirectedInvocation ))
            {
                RuntimeMethodHandle caller = SetupCaller( call );

                Console.WriteLine( "Creating redirect for {0}", caller.Value );
                MethodCallExpression callExpression = (MethodCallExpression)call.Body;

                // add staticLocals dictionary as argument
                var arguments = callExpression.Arguments.ToList();
                arguments.Add( Expression.Constant( staticLocals[caller] ) );

                // todo: dynamically find redirect
                var redirect = MethodOf( () => Func( default( string ), default( Dictionary<string, object> ) ) );

                LambdaExpression redirectedExpression = Expression.Lambda( Expression.Call( callExpression.Object, redirect, arguments ), new ParameterExpression[0] );

                redirectedInvocation = redirectedExpression.Compile();
                redirects.Add( call, redirectedInvocation );
            }
            return redirectedInvocation;
        }

        private static RuntimeMethodHandle SetupCaller( Expression<Action> call )
        {
            RuntimeMethodHandle caller;
            if (!handleCache.TryGetValue( call, out caller ))
            {
                caller = new StackFrame( 1 ).GetMethod().MethodHandle;
                handleCache.Add( call, caller );
                staticLocals.Add( caller, new Dictionary<string, object>() );
            }
            return caller;
        }

        public static MethodInfo MethodOf( Expression<Action> expression )
        {
            MethodCallExpression body = (MethodCallExpression)expression.Body;
            return body.Method;
        }

        [Obsolete( "do not call directly" )]
        public static void Func( string arg )
        {
        }

        private static void Func( string arg, Dictionary<string, object> staticLocals )
        {
            if (!staticLocals.ContainsKey( "expensive"))
            {
                staticLocals.Add( "expensive", new ExpensiveObject() );
            }

            ExpensiveObject obj = (ExpensiveObject)staticLocals["expensive"];
            Console.WriteLine( "Func invoked: arg: {0}; expensive: {1}", arg, obj );
        }
    }
}

The output of which is:

Creating redirect for 92963900
Creating Expensive object
Func invoked: arg: hello; expensive: MethodStaticLocals.ExpensiveObject
Func invoked: arg: hello; expensive: MethodStaticLocals.ExpensiveObject


标签: c# .net clr