Comment obtenir la valeur actuelle de EIP en code managé?

La question semble être un sale coup que vous ne devriez pas faire mais laissez-moi d’abord vous expliquer. Le but ultime est d’avoir une statique locale de la méthode comme en C ++.

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

Qu’est-ce que cela a à voir avec le pointeur d’instruction? Je souhaite mettre en cache des données en fonction de l’appelant. Pour accélérer cette procédure, je retourne dans la stack pour obtenir l’adresse de mon correspondant et l’utilise comme clé unique pour un dictionnaire pour stocker les données. Cela permettrait de créer un traceur basé sur la reflection qui n’utilise pas Reflection à chaque fois pour obtenir le nom de la méthode et du type actuels, mais une seule fois, et stocke les informations de reflection dans une table de hachage.

Jusqu’à présent, les réponses n’étaient que mono. Je veux essayer une solution générique qui fonctionne sur .NET 3.5 / 4.0 32/64 bit. Je sais que la convention d’appel 64 bits est très différente, il peut donc être difficile d’obtenir quelque chose de fiable. Mais d’autre part, j’ai le plein contrôle de ma méthode sur l’apparence de la stack. La stack a une apparence très différente entre .NET 3.5 et 4.0 et bien sûr aussi entre les versions de versions. Il me rest à vérifier si NGen crée également du code avec une disposition de stack différente. Une possibilité serait d’utiliser une méthode d’assistance C ++ qui prend 5 arguments magiques entiers (sur x64, seul le 5ème sera sur la stack) et de vérifier où je peux les trouver sur la stack. Une autre possibilité serait simplement d’utiliser toute la stack jusqu’à ce que je trouve mon marqueur magique sur la stack en tant que clé et d’utiliser cette partie de la stack comme clé unique. Mais je ne suis pas sûr si cette approche peut fonctionner du tout ou s’il existe de meilleures alternatives. Je sais que je peux parcourir la stack de manière sécurisée via les apis de profilage ou de débogage, mais aucune d’entre elles n’est rapide.

Pour une bibliothèque de traçage, l’approche habituelle consiste à parcourir la stack en utilisant la reflection pour obtenir le nom et le type de la méthode en cours.

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

Mais c’est très lent. L’autre solution consiste à transmettre les données directement via des chaînes, ce qui est beaucoup plus rapide mais nécessite davantage de frappe. La solution alternative serait d’utiliser le pointeur d’instruction de la fonction appelante (si cela peut être déterminé très rapidement) pour contourner les appels de reflection coûteux. Alors ce serait possible:

 class Tracer { static Dictionary _CachedMethods = new Dictionary(); [MethodImpl(MethodImplOptions.NoInlining)] public Tracer() { Int64 eip = GetEIpOfParentFrame(); ssortingng 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 } } 

Je sais que la solution doit être non gérée. En C ++, il existe un compilateur insortingnsèque appelé _ReturnAddress mais, selon la documentation, il ne fonctionne pas avec le code managé. Une autre façon de poser la même question: quelqu’un connaît-il la convention d’appel et la structure de stack des méthodes gérées pour .NET 3.5 / 4 x32 / x64?

Bien à vous, Alois Kraus

Avec C # 5.0, il existe une nouvelle fonctionnalité bien cachée qui permet cela.

Atsortingbuts Info appelant

Remarque Apparemment, il existe également le package Nuget Microsoft BCL Portability Pack 1.1.3 vous permettant d’utiliser les atsortingbuts de l’appelant dans .NET 4.0.

Cela permet de faire en sorte que vos parameters facultatifs aient des valeurs par défaut dépendantes de l’appelant . Il a

  • CallerFilePathAtsortingbute Chemin complet du fichier source contenant l’appelant. C’est le chemin du fichier au moment de la compilation.
  • CallerLineNumberAtsortingbute Numéro de ligne dans le fichier source auquel la méthode est appelée.
  • CallerMemberNameAtsortingbute Nom de méthode ou de propriété de l’appelant. Voir Noms des membres plus loin dans cette rubrique.

Il a quelques fonctionnalités intéressantes:

  • Les valeurs de l’appelant sont émises littéralement dans le langage intermédiaire (IL) au moment de la compilation .
  • Contrairement aux résultats de la propriété StackTrace pour les exceptions, les résultats ne sont pas affectés par l’obscurcissement .

L’exemple de documentation ressemble à ceci:

 // using System.Runtime.ComstackrServices // using System.Diagnostics; public void DoProcessing() { TraceMessage("Something happened."); } public void TraceMessage(ssortingng message, [CallerMemberName] ssortingng memberName = "", [CallerFilePath] ssortingng 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 

Mise à jour Cette réponse est maintenant obsolète pour la version récente de .NET: voir ici Comment obtenir la valeur actuelle de EIP dans le code managé?

La vraie réponse est la suivante: la machine virtuelle CLR est une machine à stack, donc pas d’EIP . La réponse un peu plus longue est la suivante: si vous vous basez sur des détails non documentés spécifiques à l’implémentation, vous pouvez extrapoler un ID utilisable à partir de l’EIP du CPU en code non managé.

Preuve de concept

Je viens de gérer la preuve de concept suivante, en utilisant mono 2.11 sur Linux 32 bits. J’espère que l’information pourrait aider. Ceci implémente des fonctions non managées:

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

Source native: tracehelper.c [1]:

 #include  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 ssortingng representation of the stack frame * this version of the code sortinges 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) pour appeler cette fonction de bibliothèque native:

 using System; using System.Runtime.InteropServices; namespace PoC { class MainClass { [DllImportAtsortingbute("libtracehelper.so")] extern static ssortingng CurrentMethodDisplay(); [DllImportAtsortingbute("libtracehelper.so")] extern static uint CurrentMethodAddress(); static MainClass() { Console.WriteLine ("TRACE 0 {0:X8} {1}", CurrentMethodAddress(), CurrentMethodDisplay()); } public static void Main (ssortingng[] 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()); } } } 

Comstackz et liez en utilisant 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 

En LD_LIBRARY_PATH=. ./client.exe ceci avec LD_LIBRARY_PATH=. ./client.exe LD_LIBRARY_PATH=. ./client.exe résulte en:

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

Notez que ceci est sur Mono 2.11. Cela fonctionne également sur 2.6.7, avec et sans optimisation.

[1] J’ai appris que GNU extended asm était utilisé à cette fin. Merci beaucoup!

Des conclusions?

Livré une preuve de concept; cette implémentation est spécifique à Mono. Une “astuce” similaire pourrait être livrée sur MS .Net (en utilisant une :: LoadLibrary de SOS.dll , peut-être?) Mais est laissée comme exercice pour le lecteur 🙂

Personnellement, j’y allais quand même avec mon autre réponse , mais je suppose que j’ai succombé au défi et, comme je l’ai déjà dit: YMMV, Here be dragons, TIMTOWTDI , KISS etc.

Bonne nuit

Mise à jour Cette réponse est maintenant obsolète pour la version récente de .NET: voir ici Comment obtenir la valeur actuelle de EIP dans le code managé?

Votre meilleur pari sera StackFrame (Int32):

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

Plus d’idées

  • Utiliser une instrumentation AOP (basée sur les atsortingbuts)
  • Utilisez Linfu ou Cecil pour émettre dynamicment les identifiants utiles

Si vous le devez, vous pouvez utiliser un générateur de code qui remplira les identifiants avant la compilation.

J’utiliserais l’ API du profileur , mais si vous voulez plus de performances, essayez Entrée / Laisser les crochets .

Je pense que vous essayez d’avoir votre gâteau et de le manger aussi, la performance et la portabilité ne vont pas toujours ensemble. Lien dans certains MASM64 pour la performance 🙂

J’ai une autre idée (bien que très expérimentale), basée sur l’utilisation d’arbres d’expression pour effectuer les invocations de votre méthode via un invocateur et une façade.

Au lieu d’appeler normalement votre méthode, vous créez un arbre d’expression pour appeler la façade à partir d’un emplacement donné de votre code. Cette arborescence d’expression est transmise à un appelant qui met en cache l’arborescence d’expression compilée avec les informations de l’appelant. Les informations sur l’appelant peuvent être extraites une fois via StackTrace.GetMethod et mises en cache dans l’arborescence des expressions.

D’après votre expérience personnelle, puisque vous n’avez besoin que d’une clé pour votre invocation, vous ne devez stocker qu’un object MethodHandle au lieu de l’object MethodBase complet (réduit considérablement la consommation de mémoire).

Pour effectuer l’appel réel, vous pouvez maintenant examiner l’arborescence d’expression et en créer un nouveau pour appeler l’implémentation réelle avec un dictionnaire contenant les statistiques de votre méthode ou transmettez-lui la clé de méthode de l’appelant.


Wow, c’est vraiment cool et rapide comme l’enfer. S’il vous plaît fournir des commentaires sur l’essentiel: 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( ssortingng[] args ) { Expression call = () => Func( "hello" ); Invoke( call ); Invoke( call ); } // caches handles for expresisons, as they are expensive to find. static Dictionary handleCache = new Dictionary(); // static locals are managed per method handle static Dictionary> staticLocals = new Dictionary>(); // redirects are individual for each expression tree static Dictionary redirects = new Dictionary(); static void Invoke( Expression 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 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( ssortingng ), default( Dictionary ) ) ); LambdaExpression redirectedExpression = Expression.Lambda( Expression.Call( callExpression.Object, redirect, arguments ), new ParameterExpression[0] ); redirectedInvocation = redirectedExpression.Comstack(); redirects.Add( call, redirectedInvocation ); } return redirectedInvocation; } private static RuntimeMethodHandle SetupCaller( Expression call ) { RuntimeMethodHandle caller; if (!handleCache.TryGetValue( call, out caller )) { caller = new StackFrame( 1 ).GetMethod().MethodHandle; handleCache.Add( call, caller ); staticLocals.Add( caller, new Dictionary() ); } return caller; } public static MethodInfo MethodOf( Expression expression ) { MethodCallExpression body = (MethodCallExpression)expression.Body; return body.Method; } [Obsolete( "do not call directly" )] public static void Func( ssortingng arg ) { } private static void Func( ssortingng arg, Dictionary 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 ); } } } 

Le résultat est:

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