如何在托管代码中获取EIP的当前值?

时间:2011-04-17 19:19:30

标签: c# .net clr

问题似乎是一个你不应该做的肮脏的黑客,但让我先解释一下。最终目标是在C ++中使用方法局部静态。

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

这与指令指针有什么关系?我想根据我的调用者缓存数据。为了快速实现这一点,我回到堆栈中以获取调用者的地址,并将其用作字典的唯一键来存储数据。 这将允许创建一个基于反射的跟踪器,它不会每次都使用Reflection来获取当前方法和类型的名称,但只有一次,并将反射信息存储在哈希表中。

到目前为止,答案仅以单声道为基础。我想尝试一个适用于.NET 3.5 / 4.0 32/64位的通用解决方案。我知道64位的calling convention是完全不同的,因此获得可靠的东西可能会遇到挑战。但另一方面,我在我的方法中完全控制堆栈的外观。在.NET 3.5和4.0之间,堆栈确实看起来非常不同,当然它在发布版本之间也有所不同。我仍然需要检查NGen是否确实创建了具有不同堆栈布局的代码。一种可能性是使用C ++帮助器方法,该方法需要5个魔术整数参数(在x64上只有第5个将在堆栈上)并检查我在堆栈中可以找到它们的位置。 另一种可能性是简单地使用整个堆栈,直到我在堆栈上找到我的魔术标记作为键并使用堆栈的这一部分作为唯一足够的密钥。但我不确定这种方法是否可行,或者是否有更好的选择。我知道我可以通过分析或调试apis以安全的方式走栈,但它们都不是很快。

对于跟踪库,通常的方法是使用反射遍历堆栈以获取当前方法名称和类型。

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);
    }

}

但这很慢。另一种解决方案是直接通过字符串传递数据,速度要快得多,但需要更多的输入。另一种解决方案是使用调用函数的指令指针(如果可以以非常快的方式确定)来绕过昂贵的反射调用。 那么这是可能的:

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
    }

}

我知道解决方案需要不受管理。在C ++中有一个名为_ReturnAddress的编译器内在函数,但根据文档,它不适用于托管代码。另一种提出相同问题的方法:是否有人知道.NET 3.5 / 4 x32 / x64托管方法的调用约定和堆栈布局?

此致,   Alois Kraus

5 个答案:

答案 0 :(得分:7)

  

更新对于最新版本的.NET,此答案现已过时:请参阅此处How to get current value of EIP in managed code?

真正的简短回答是: CLR VM是一个堆栈计算机,因此没有EIP 。 稍微长一点的答案是:如果您依赖于未记录的特定于实现的详细信息,则可以在非托管代码中从CPU EIP中推断出可用的ID。

概念证明

我刚刚在Linux 32位上使用mono 2.11进行了以下概念验证。我希望这些信息可能有所帮助。这实现了非托管函数:

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

原生资源: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)调用此本机库函数:

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());
        }
    }
}

使用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

使用LD_LIBRARY_PATH=. ./client.exe运行此结果:

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[])

请注意,这是在Mono 2.11上。它也适用于2.6.7,有和没有优化。

[1] 我为此目的学习了GNU extended asm;谢谢!

结论?

提供概念证明;此实现特定于Mono。类似的“技巧”可以在MS .Net上传递(使用::LoadLibrary of SOS.dll,也许?)但是留给读者练习:)

我个人仍然go with my other answer,但我想我已经屈服于挑战,就像我之前说过的那样:YMMV,这里有龙, TIMTOWTDI ,KISS等

晚安

答案 1 :(得分:2)

  

更新对于最新版本的.NET,此答案现已过时:请参阅此处How to get current value of EIP in managed code?

你最好的选择是StackFrame(Int32):

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

更多想法

  • 使用AOP(基于属性的)工具
  • 使用Linfu或Cecil动态发出有用的ID

如果必须,您可以使用代码生成器在编译之前填入ID。

答案 2 :(得分:2)

使用C#5.0,有一个新的,隐藏得很好的功能可以实现这一点。

  

Caller Info attributes

注意显然,还有Microsoft BCL Portability Pack 1.1.3 Nuget包,因此您可以使用.NET 4.0中的来电者信息属性。

这样做,是使您的可选参数神奇地具有 调用者相关的默认值 。它有

它有一些非常漂亮的功能:

  • 调用者信息值 在编译时作为文字发送到中间语言(IL)
  • 与异常的StackTrace属性的结果不同, 结果不受模糊处理的影响

文档示例如下所示:

// 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 

答案 3 :(得分:0)

我会使用profiler API's,但如果您想要更高的效果,请尝试Enter/Leave Hooks

我认为你试图吃蛋糕并且吃它,性能+便携性并不总是在一起。链接某些MASM64以获得性能:)

答案 4 :(得分:0)

我有另一个(尽管是高度实验性的)想法,它基于使用表达式树来通过调用者和外观对您的方法进行调用。

您可以创建一个表达式树来代替通常调用您的方法,而不是从代码中的给定位置调用th facade。该表达式树被传递给调用者,该调用者将编译的表达式树与调用者信息一起缓存。可以通过StackTrace.GetMethod检索一次调用者信息,并对表达式树进行缓存。

从个人经验来看,由于您只需要一个调用密钥,因此您应该只存储一个MethodHandle,而不是完整的MethodBase对象(显着减少内存消耗)。

要执行实际调用,您现在可以检查表达式树并构建一个新表达式,以使用包含方法级别静态的字典调用实际实现,或者将调用方法键传递给它。


哇,这真的很酷,很快就像地狱一样。请提供有关要点的反馈: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 );
        }
    }
}

其输出为:

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