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

本文关键字:EIP 获取 托管代码 | 更新日期: 2023-09-27 17:50:20

这个问题似乎是你不应该做的,但让我先解释一下。最终目标是拥有像c++那样的方法局部静态。

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

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

到目前为止,答案都是基于单一性的。我想尝试一个通用的解决方案,它可以在。net 3.5/4.0 32/64位上工作。我确实知道64位的调用约定是完全不同的,所以它可能是一个挑战,以获得可靠的东西。但另一方面,我可以在方法中完全控制堆栈的样子。堆栈在。net 3.5和。net 4.0之间确实有很大的不同,当然在不同的发行版本之间也有所不同。我仍然需要检查NGen是否使用不同的堆栈布局创建代码。一种可能性是使用一个c++助手方法,它接受5个神奇的整数参数(在x64上,只有第5个参数会在堆栈上),并检查我可以在堆栈上找到它们。另一种可能性是简单地使用整个堆栈,直到我在堆栈上找到我的魔法标记作为密钥,并使用堆栈的这一部分作为唯一的密钥。但我不确定这种方法是否有效,或者是否有更好的替代方案。我知道我可以通过分析或调试api以一种安全的方式遍历堆栈,但它们都不是很快的。

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

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的托管方法的调用约定和堆栈布局吗?

你的,阿洛伊斯·克劳斯

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

Update这个答案在最近的。net版本中已经过时了:参见这里如何在托管代码中获得EIP的当前值?

真正简短的答案是: 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#源代码(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扩展asm就是为了这个目的;谢谢你如此!

结论?

交付概念证明;这个实现是针对Mono的。类似的"技巧"可以在MS .Net上传递(也许使用SOS.dll的::LoadLibrary ?),但留给读者作为练习:)

我个人仍然会用我的其他答案,但我想我屈服于挑战,就像我之前说过的:YMMV,这里是龙,TIMTOWTDI, KISS等。

晚安

在c# 5.0中,有一个新的、隐藏良好的特性可以实现这一点。

来电者信息属性

注意显然,还有一个Microsoft BCL可移植性包1.1.3 Nuget包,所以你可以在。net 4.0中使用调用者信息属性。

的作用是使你的可选参数神奇地具有依赖于调用者的默认值。它有

  • CallerFilePathAttribute包含调用者的源文件的完整路径。这是编译时的文件路径
  • CallerLineNumberAttribute在源文件中调用方法的行号
  • CallerMemberNameAttribute调用方的方法或属性名。请参阅本主题后面的成员名称。

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

  • 调用者信息值是在编译时作为文字发送到中间语言(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 

Update这个答案在最近的。net版本中已经过时了:参见这里如何在托管代码中获得EIP的当前值?

你最好的选择是StackFrame(Int32):
Console.WriteLine(new System.Diagnostics.StackFrame(0).GetMethod().Name);
Console.WriteLine(new System.Diagnostics.StackFrame(0).GetNativeOffset());

更多的想法

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

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

我会使用分析器API,但如果你想要更多的性能,请尝试Enter/Leave Hooks。

我认为你想要鱼与熊掌兼得,性能和可移植性并不总是在一起的。

我有另一个(虽然是高度实验性的)想法,这是基于使用表达式树通过调用程序和facade对您的方法进行调用。

您将创建一个表达式树来从代码中的给定位置调用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