Как получить текущее значение EIP в управляемом коде? - PullRequest
7 голосов
/ 17 апреля 2011

Вопрос выглядит как грязный взлом, который вы не должны делать, но позвольте мне сначала объяснить. Конечная цель состоит в том, чтобы иметь метод локальной статики, как в C ++.

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

Какое это имеет отношение к указателю инструкции? Я хочу кешировать данные в зависимости от моего абонента. Чтобы сделать это быстро, я возвращаюсь в стек, чтобы получить адрес моего абонента и использую его как уникальный ключ для словаря для хранения данных. Это позволило бы создать трассировщик на основе отражения, который не использует Reflection каждый раз, чтобы получить имя текущего метода и типа, а только один раз и сохранить информацию об отражении в хэш-таблице.

Пока ответы были только моно. Я хочу попробовать универсальное решение, которое работает на .NET 3.5 / 4.0 32/64 бит. Я знаю, что соглашение о вызовах для 64-битных систем сильно отличается, поэтому может быть сложно получить что-то надежное. Но с другой стороны, в моем методе есть полный контроль над тем, как выглядит стек. Стек действительно выглядит очень по-разному между .NET 3.5 и 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?

С уважением, Алоис Краус

Ответы [ 5 ]

7 голосов
/ 19 апреля 2011

Обновление Этот ответ устарел для последней версии .NET: см. Здесь Как получить текущее значение EIP в управляемом коде?

Очень краткий ответ: виртуальная машина CLR - это стековая машина, поэтому EIP там нет .Немного более длинный ответ: если вы полагаетесь на недокументированные подробности, относящиеся к реализации, вы можете экстраполировать пригодный идентификатор из EIP ЦП в неуправляемый код.

Подтверждение концепции

Я простосправился со следующим подтверждением концепции, используя моно 2.11 в 32-битной LinuxЯ надеюсь, что информация может помочь.Это реализует неуправляемые функции:

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, расширенный asm для этой цели;спасибо SO!

Выводы?

Предоставлено подтверждение концепции;эта реализация специфична для Mono.Подобный «трюк» может быть реализован в MS .Net (с использованием :: LoadLibrary SOS.dll , возможно?), Но оставлен в качестве упражнения для читателя:)

Я бы лично все равно пошел бы с другим моим ответом , но я полагаю, что я уступил вызову, и, как я уже говорил: YMMV, здесь будут драконы, TIMTOWTDI , KISSи т.д.

Спокойной ночи

2 голосов
/ 06 августа 2013

В C # 5.0 есть новая, хорошо скрытая функция, которая позволяет это.

Атрибуты Caller Info

Примечание По-видимому, существует также пакет Nuget Microsoft BCL Portability Pack 1.1.3 , так что вы можете использовать Атрибуты информации о вызывающем абоненте в .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 
2 голосов
/ 18 апреля 2011

Обновление Этот ответ устарел для последней версии .NET: см. Здесь Как получить текущее значение EIP в управляемом коде?

Лучшей ставкой будет StackFrame (Int32):

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

Больше идей

  • Использовать AOP (на основе атрибутов) инструментарий
  • Используйте Linfu или Cecil, чтобы динамически выдать полезные идентификаторы

Если необходимо, вы можете использовать генератор кода, который будет заполнять идентификаторы перед компиляцией.

0 голосов
/ 26 июня 2011

У меня есть другая (хотя и очень экспериментальная) идея, которая основана на использовании деревьев выражений для выполнения вызовов вашего метода через invoker и фасад.

Вместо того, чтобы нормально вызывать ваш метод, вы должны создать дерево выражений для вызова фасада из заданного места в вашем коде.Это дерево выражений передается вызывающему, который кэширует скомпилированное дерево выражений вместе с информацией о вызывающем.Информация о вызывающем абоненте может быть получена один раз через 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
0 голосов
/ 27 апреля 2011

Я бы использовал API профилировщика , но если вы хотите большей производительности, попробуйте Enter / Leave Hooks .

Я думаю, что вы пытаетесь получить свой торт и съесть его, производительность + портативность не всегда идут вместе. Ссылка в некоторых MASM64 для производительности:)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...