如何在托管代码中获取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的托管方法的调用约定和堆栈布局吗?
你的,阿洛伊斯·克劳斯
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
你最好的选择是StackFrame(Int32):Update这个答案在最近的。net版本中已经过时了:参见这里如何在托管代码中获得EIP的当前值?
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