Предисловие

Изменение .NET метода MSIL кода во время выполнения приложения – это очень круто. Это настолько круто, что можно перехватывать вызовы функций (hooking), сделать защиту своего ПО и другие удивительные вещи. Именно поэтому мне уже давно хотелось это осуществить, но была одна проблема – MSIL код компилируется в машинный код с помощью JIT перед тем, как мы сможем что-либо с этим кодом сделать. А так как .NET CLR не документирована и изменяется от версии к версии, то мы и будем искать стабильный и надёжный путь, независимый от точного расположения адресов в памяти.
Наконец, спустя целую неделю исследований, я сделал это. Вашему вниманию предоставляется простой метод:
protected string CompareOneAndTwo() { int a = 1; int b = 2; if (a < b) { return "Number 1 is less than 2"; } else { return "Number 1 is greater than 2 (O_o)"; } }
Как вы видите, он возвращает «Number 1 is less than 2». Давайте попробуем исправить это недоразумение и постараемся изменить этот метод таким образом, чтобы возвращаемый результат был «Number 1 is greater than 2 (О_о)».
Посмотрев MSIL код этого метода, мы можем достичь нашей цели заменой опкода Bge_S на Blt_S.

И если вы запустите демонстрационное приложение, то оно покажет вам неверный результат.

Чуть ниже находится код для замены IL. Я считаю, что для понимания кода, комментариев достаточно.
//Сначала получаем наш метод Type type=this.GetType(); MethodInfo methodInfo=type.GetMethod("CompareOneAndTwo", BindingFlags.NonPublic|BindingFlags.Instance); //Следующая строка, на самом деле, необязательна //Делаем мы для это для того, чтобы вызывать метод, скомпилированный JIT //Так что мы можем быть уверены, что всё будет работать для не JIT-компилированных методов :) RuntimeHelpers.PrepareMethod(methodInfo.MethodHandle); //Получаем оригинальные IL-опкоды для метода byte[] ilCodes=methodInfo.GetMethodBody().GetILAsByteArray(); //Это не очень хороший способ поиска опкодов //Но для нашего примера он работает for (int i=0; i<ilCodes.Length; i++) { if (ilCodes[i]==OpCodes.Bge_S.Value) { //Заменяем Bge_S на Blt_S ilCodes[i]=(byte)OpCodes.Blt_S.Value; } } //Обновляем IL-код InjectionHelper.UpdateILCodes(methodInfo, ilCodes);
Вы можете загрузить себе демонстрационную программу и попробовать.
- Поддерживает .NET от 2.0 до 4.0
- Поддерживает множество типов методов, включая dynamic и generic методы.
- Поддерживает релизные версии .NET приложений
- Поддерживает x86 и x64
Использование кода
Скопируйте InjectionHelper.cs, в котором находятся необходимые методы, в свой проект.
public static class InjectionHelper { // Load the unmanaged injection.dll, the initlaization happens in a background thread // you can check if the initialization is completed by GetStatus() public static void Initialize() // Unload the unmanaged injection.dll public static void Uninitialize() // Update the IL Code of a Method. public static void UpdateILCodes(MethodInfo method, byte[] ilCodes) // The method returns until the initialization is completed public static Status WaitForIntializationCompletion() // Query the current status of the unmanaged dll, returns immediately. public static Status GetStatus() }
Метод Injectionhelper::Initialize загружает injection.dll, состоящую из неуправляемого (unmanaged) кода, из директории, в которой лежит сборка, поэтому все файлы, которые вы хотите модифицировать, должны лежать там же. Либо вы можете поправить исходники, кому как удобнее :)
Список файлов:
| Имя файла | Описание |
| Injection32.dll | Неуправляемая dll, выполняющая нашу задачу (версия x86) |
| Injection64.dll | Неуправляемая dll, выполняющая нашу задачу (версия x64) |
| EasyHook32.dll | x86 EasyHook DLL (http://easyhook.codeplex.com/) (используется Injection32.dll) |
| EasyHook64.dll | X64 EasyHook DLL (http://easyhook.codeplex.com/) (используется Injection64.dll) |
| x86/* | Windows Debug Tool for x86 |
| x64/* | Windows Debug Tool for x64 |
| PDB_symbols/* | Файлы PDB. Их можно удалить, но это замедлит инициализацию |
За кулисами
Давайте сначала посмотрим, как работают CLR и JIT.

Библиотека, описывающая JIT (clr.dll для .NET 4.0 / mscorwks.dll для .NET 2.0+), предоставляет _stdcall метод getJit, который возвращает интерфейс ICorJitCompiler.
Библиотека, описывающая CLR (clr.dll для .NET 4.0 / mscorwks.dll для .NET 2.0+), вызывает метод getJit для получения интерфейса ICorJitCompiler
CorJitResult compileMethod(ICorJitInfo * pJitInfo, CORINFO_METHOD_INFO * pMethodInfo, UINT nFlags, LPBYTE * pEntryAddress, ULONG * pSizeOfCode);
Эта часть лёгкая, надо просто найти адрес метода compileMethod и подменить его с помощью EasyHook.
// ICorJitCompiler interface from JIT dll class ICorJitCompiler { public: typedef CorJitResult (__stdcall ICorJitCompiler::*PFN_compileMethod)(ICorJitInfo * pJitInfo, CORINFO_METHOD_INFO * pMethodInfo, UINT nFlags, LPBYTE * pEntryAddress, ULONG * pSizeOfCode); CorJitResult compileMethod(ICorJitInfo * pJitInfo, CORINFO_METHOD_INFO * pMethodInfo, UINT nFlags, LPBYTE * pEntryAddress, ULONG * pSizeOfCode) { return (this->*s_pfnComplieMethod)( pJitInfo, pMethodInfo, nFlags, pEntryAddress, pSizeOfCode); } private: static PFN_compileMethod s_pfnComplieMethod; }; // сохраняем настоящий адрес LPVOID pAddr = tPdbHelper.GetJitCompileMethodAddress(); LPVOID* pDest = (LPVOID*)&ICorJitCompiler::s_pfnComplieMethod; *pDest = pAddr; // а это мой compileMethod CorJitResult __stdcall CInjection::compileMethod(ICorJitInfo * pJitInfo , CORINFO_METHOD_INFO * pCorMethodInfo , UINT nFlags , LPBYTE * pEntryAddress , ULONG * pSizeOfCode ) { ICorJitCompiler * pCorJitCompiler = (ICorJitCompiler *)this; // TO DO: заменить IL-код перед вызовом настоящего метода CorJitResult result = pCorJitCompiler->compileMethod( pJitInfo, pCorMethodInfo, nFlags, pEntryAddress, pSizeOfCode); return result; } // перехватить и заменить JIT-скомпилированный метод на мой NTSTATUS ntStatus = LhInstallHook( (PVOID&)ICorJitCompiler::s_pfnComplieMethod , &(PVOID&)CInjection::compileMethod , NULL , &s_hHookCompileMethod );
Изменение IL-кода для JIT-скомпилированных методов
Метод compileMethod, описанный выше, не будет вызываться CLR для JIT-скомпилированного метода. Для решения этой проблемы, я сделал сохранение структур данных CLR и последующее их восстановление перед JIT-компиляцией. И в этом случае, как только compileMethod будет вызван снова, мы можем заменить IL.
Таким образом, нам надо немного посмотреть на реализацию CLR, SSCLI (Shared Source Common Language Infrastructure / Инфраструктура общего языка) является хорошим источником информации, но так как она довольно устарела, мы не можем использовать её в нашем коде.

Да-да, именно эта диаграмма устарела, но общая структура сохранилась. Каждый класс в .NET имеет как минимум одну структуру MethodTable в памяти. А каждая структура MethodTable связана с EEClass, который хранит информацию времени выполнения для рефлексии и других целей.
Для каждого метода, есть как минимум одна MethodDesc структура, содержащая информацию о флагах, адресах слота, адресе входа и т.п.
Перед тем, как метода будет JIT-скомрилирован, слот указывает на JMI преобразователь, который переключает JIT-компиляцию; Когда IL-код будет скомпилирован, в слот будет записан указатель на JMI, и код при выполнении будет попадать прямо на скомпилированный код.
Чтобы восстановить структуру информации, сначала необходимо очистить флаги, затем модифицировать адрес точки входа на временный и т.д. Во время тестирования, я делал это, изменяя память напрямую. Но это грязно, как минимум, потому что есть зависимость от адресов структур данных и код разных .NET версий отличается.
Я искал правильных способ, и к счастью, я нашёл метод MethodDesc::Reset в исходном коде SSCLI (vm/method.cpp).
void MethodDesc::Reset() { CONTRACTL { THROWS; GC_NOTRIGGER; } CONTRACTL_END // Этот метод непотокобезопасный с тех пор, как мы начали его изменять вручную. // Используйте это только если вы уверены в потокобезопасности вызова этого метода _ASSERTE(IsEnCMethod() || // Процесс находится под отладкой IsDynamicMethod() || GetLoaderModule()->IsReflection()); // Очищаем все флаги ClearFlagsOnUpdate(); if (HasPrecode()) { GetPrecode()->Reset(); } else { // Здесь мы должны обрабатывать только Reflection-методы _ASSERTE(GetLoaderModule()->IsReflection()); InterlockedUpdateFlags2(enum_flag2_HasStableEntryPoint | enum_flag2_HasPrecode, FALSE); *GetAddrOfSlotUnchecked() = GetTemporaryEntryPoint(); } _ASSERTE(!HasNativeCode()); }
Как вы видите, этот код делает что надо. Поэтому мне просто надо его вызвать для MethodDesc перед JIT-компиляцией.
Строго говоря, я не могу использовать MethodDesc из SSCLI, так как MethodDesc используется внутри Microsoft, и никто не знает, что из-за этого может быть.
К счастью, адрес этого внутреннего метода существует в PDB c сервера Microsoft Symbol, и это решает мою проблему. Адрес метода Reset() в CLR DLL можно найти просто распарсив PDB!
Теперь остался один важный параметр – это указатель this на MethodDesc. Получить его не так трудно. Вообще, MethodBase.MethodHandle.Value == CORINFO_METHOD_HANDLE == адрес MethodDesc == указатель this на MethodDesc.
class MethodDesc { typedef void (MethodDesc::*PFN_Reset)(void); typedef BOOL (MethodDesc::*PFN_IsGenericMethodDefinition)(void); typedef ULONG (MethodDesc::*PFN_GetNumGenericMethodArgs)(void); typedef MethodDesc * (MethodDesc::*PFN_StripMethodInstantiation)(void); typedef BOOL (MethodDesc::*PFN_HasClassOrMethodInstantiation)(void); typedef BOOL (MethodDesc::*PFN_ContainsGenericVariables)(void); typedef MethodDesc * (MethodDesc::*PFN_GetWrappedMethodDesc)(void); typedef AppDomain * (MethodDesc::*PFN_GetDomain)(void); typedef Module * (MethodDesc::*PFN_GetLoaderModule)(void); public: void Reset(void) { (this->*s_pfnReset)(); } BOOL IsGenericMethodDefinition(void) { return (this->*s_pfnIsGenericMethodDefinition)(); } ULONG GetNumGenericMethodArgs(void) { return (this->*s_pfnGetNumGenericMethodArgs)(); } MethodDesc * StripMethodInstantiation(void) { return (this->*s_pfnStripMethodInstantiation)(); } BOOL HasClassOrMethodInstantiation(void) { return (this->*s_pfnHasClassOrMethodInstantiation)(); } BOOL ContainsGenericVariables(void) { return (this->*s_pfnContainsGenericVariables)(); } MethodDesc * GetWrappedMethodDesc(void) { return (this->*s_pfnGetWrappedMethodDesc)(); } AppDomain * GetDomain(void) { return (this->*s_pfnGetDomain)(); } Module * GetLoaderModule(void) { return (this->*s_pfnGetLoaderModule)(); } private: static PFN_Reset s_pfnReset; static PFN_IsGenericMethodDefinition s_pfnIsGenericMethodDefinition; static PFN_GetNumGenericMethodArgs s_pfnGetNumGenericMethodArgs; static PFN_StripMethodInstantiation s_pfnStripMethodInstantiation; static PFN_HasClassOrMethodInstantiation s_pfnHasClassOrMethodInstantiation; static PFN_ContainsGenericVariables s_pfnContainsGenericVariables; static PFN_GetWrappedMethodDesc s_pfnGetWrappedMethodDesc; static PFN_GetDomain s_pfnGetDomain; static PFN_GetLoaderModule s_pfnGetLoaderModule; };
Статические переменные хранят адреса внутренних методов MethodDesc и они инициализируются, когда загружается неуправляемая DLL. А public методы просто вызывают внутренние методы.
Теперь мы можем запросто вызывать внутренние методы Microsoft:
MethodDesc * pMethodDesc = (MethodDesc*)pMethodHandle; pMethodDesc->Reset();
Поиск адресов внутренних методов в PDB-файле
Когда загружается неуправляемая DLL, она проверяет версию среды CLR/JIT, в которой она загружена. И она также пытается получить адреса внутренних методов из PDB-файла. Если найти их не удалось, она попытается запустить symchk.exe из Windows Debug Tools, чтобы загрузить соответствующие PDB-файлы с сервера Microsoft Symbol. Эта процедура занимает довольно длительное время, от нескольких секунд до нескольких минут. Возможно, мы можем ускорить этот процесс, кэшируя адрес CLR/JIT библиотек, подсчитывая их хэши.
Восстановление метода к не JIT-компилированному
Теперь всё готово. Неуправляемая библиотека экспортирует методы для управляемого кода, принимает IL-коды и MethodBase.MethodHandle.Value из управляемого кода.
// structure to store the IL code for replacement typedef struct _ILCodeBuffer { LPBYTE pBuffer; DWORD dwSize; } ILCodeBuffer, *LPILCodeBuffer; // method to be called by managed code BOOL CInjection::StartUpdateILCodes( MethodTable * pMethodTable , CORINFO_METHOD_HANDLE pMethodHandle , mdMethodDef md , LPBYTE pBuffer , DWORD dwSize ) { MethodDesc * pMethodDesc = (MethodDesc*)pMethodHandle; // reset this MethodDesc pMethodDesc->Reset(); ILCodeBuffer tILCodeBuffer; tILCodeBuffer.pBuffer = pBuffer; tILCodeBuffer.dwSize = dwSize; tILCodeBuffer.bIsGeneric = FALSE; // save the IL code for the method s_mpILBuffers.insert( std::pair< CORINFO_METHOD_HANDLE, ILCodeBuffer>( pMethodHandle, tILCodeBuffer) ); return TRUE; }
Этот код просто вызывает Reset() и сохраняет IL-коды в map, которая будет использоваться compileMethod, когда метод будет компилироваться.
И в compileMethod просто заменим IL-код:
CorJitResult __stdcall CInjection::compileMethod(ICorJitInfo * pJitInfo , CORINFO_METHOD_INFO * pCorMethodInfo , UINT nFlags , LPBYTE * pEntryAddress , ULONG * pSizeOfCode ) { ICorJitCompiler * pCorJitCompiler = (ICorJitCompiler *)this; LPBYTE pOriginalILCode = pCorMethodInfo->ILCode; unsigned int nOriginalSize = pCorMethodInfo->ILCodeSize; ILCodeBuffer tILCodeBuffer = {0}; MethodDesc * pMethodDesc = (MethodDesc*)pCorMethodInfo->ftn; // find the method to be replaced std::map< CORINFO_METHOD_HANDLE, ILCodeBuffer>::iterator iter = s_mpILBuffers.find((CORINFO_METHOD_HANDLE)pMethodDesc); if( iter != s_mpILBuffers.end() ) { tILCodeBuffer = iter->second; pCorMethodInfo->ILCode = tILCodeBuffer.pBuffer; pCorMethodInfo->ILCodeSize = tILCodeBuffer.dwSize; } CorJitResult result = pCorJitCompiler->compileMethod( pJitInfo, pCorMethodInfo, nFlags, pEntryAddress, pSizeOfCode); return result; }
Generic метод
Generic метод отображается в памяти в MethodDesc. Но вызов Generic метода с различающимися типами параметров могут заставить CLR создать различные сущности одного метода.
Строчка ниже – это простой generic из демонстрационной программы.
string GenericMethodToBeReplaced<T, K>(T t, K k)
Вызывая GenericMethodToBeReplaced<string, int>(“11”, 2) в первый раз, CLR создаёт объект типа InstantiatedMethodDesc (дочерний от MethodDesc, и его флаг помечается как mcInstantied), который хранит в InstMethodHashTable структуру данных метода.
И вызывая GenericMethodToBeReplaced<long, int>(1, 2), CLR создаёт другой объект InstantiatedMethodDesc.
Поэтому нам надо найти все InstantiatedMethodDesc generic метода и выполнить им Reset.
В исходном коде SSCLI (vm/proftoeeinterfaceimpl.cpp) есть класс LoadedMethodDescIterator, который мы можем использовать. Он получает на вход три параметра и ищет методы по идентификатору метода (MethodToken).
LoadedMethodDescIterator MDIter(ADIter.GetDomain(), pModule, methodId); while(MDIter.Next()) { MethodDesc * pMD = MDIter.Current(); if (pMD) { _ASSERTE(pMD->IsIL()); pMD->SetRVA(rva); } }
Заметим, что адреса конструктора, методов Next, Current мы можем получить из PDB-файла.

Не так страшно, что мы не знает точного размера LoadedMethodDescIterator, просто выделим под его хранения большой блок памяти.
class LoadedMethodDescIterator { private: BYTE dummy[10240]; };
Ещё хотелось бы обратить внимание, что произошли небольшие изменения в методе Next() при переходе .NET от версии 2.0 к 4.5.
// .Net 2.0 & 4.0 LoadedMethodDescIterator(AppDomain * pAppDomain, Module *pModule, mdMethodDef md) BOOL LoadedMethodDescIterator::Next(void) // .Net 4.5 LoadedMethodDescIterator(AppDomain * pAppDomain, Module *pModule, mdMethodDef md,enum AssemblyIterationMode mode) BOOL LoadedMethodDescIterator::Next(CollectibleAssemblyHolder<DomainAssembly *> *)
Поэтому нам нужно определять текущую версию .NET фрэймворка для того, чтобы корректно вызывать метод.
// detect the version of CLR BOOL DetermineDotNetVersion(void) { WCHAR wszPath[MAX_PATH] = {0}; ::GetModuleFileNameW( g_hClrModule, wszPath, MAX_PATH); CStringW strPath(wszPath); int nIndex = strPath.ReverseFind('\\'); if( nIndex <= 0 ) return FALSE; nIndex++; CStringW strFilename = strPath.Mid( nIndex, strPath.GetLength() - nIndex); if( strFilename.CompareNoCase(L"mscorwks.dll") == 0 ) { g_tDotNetVersion = DotNetVersion_20; return TRUE; } if( strFilename.CompareNoCase(L"clr.dll") == 0 ) { DWORD dwHandle = NULL; UINT nSize = 0; LPBYTE lpBuffer = NULL; BYTE szTempBuf[2048] = {0}; DWORD dwSize = GetFileVersionInfoSizeW( wszPath, &dwHandle); if (dwSize != NULL) { LPVOID pData = szTempBuf; if (GetFileVersionInfo( wszPath, dwHandle, dwSize, pData)) { if (VerQueryValueW( pData, L"\\",(VOID FAR* FAR*)&lpBuffer,&nSize)) { if (nSize) { VS_FIXEDFILEINFO * pVerInfo = (VS_FIXEDFILEINFO *)lpBuffer; if (pVerInfo->dwSignature == 0xfeef04bd) { int nMajor = HIWORD(pVerInfo->dwFileVersionMS); int nMinor = LOWORD(pVerInfo->dwFileVersionMS); int nBuildMajor = HIWORD(pVerInfo->dwFileVersionLS); int nBuildMinor = LOWORD(pVerInfo->dwFileVersionLS); if( nMajor == 4 && nMinor == 0 && nBuildMajor == 30319 ) { if( nBuildMinor < 10000 ) g_tDotNetVersion = DotNetVersion_40; else g_tDotNetVersion = DotNetVersion_45; return TRUE; } } } } } return FALSE; } } return FALSE; }
Теперь мы можем объявить наш LoadMethodDescIterator, который будет работать вместе с CLR.
enum AssemblyIterationMode { AssemblyIterationMode_Default = 0 }; class LoadedMethodDescIterator { typedef void (LoadedMethodDescIterator::*PFN_LoadedMethodDescIteratorConstructor)(AppDomain * pAppDomain, Module *pModule, mdMethodDef md); typedef void (LoadedMethodDescIterator::*PFN_LoadedMethodDescIteratorConstructor_v45)(AppDomain * pAppDomain, Module *pModule, mdMethodDef md, AssemblyIterationMode mode); typedef void (LoadedMethodDescIterator::*PFN_Start)(AppDomain * pAppDomain, Module *pModule, mdMethodDef md); typedef BOOL (LoadedMethodDescIterator::*PFN_Next_v4)(LPVOID pParam); typedef BOOL (LoadedMethodDescIterator::*PFN_Next_v2)(void); typedef MethodDesc* (LoadedMethodDescIterator::*PFN_Current)(void); public: LoadedMethodDescIterator(AppDomain * pAppDomain, Module *pModule, mdMethodDef md) { memset( dummy, 0, sizeof(dummy)); memset( dummy2, 0, sizeof(dummy2)); if( s_pfnConstructor ) (this->*s_pfnConstructor)( pAppDomain, pModule, md); if( s_pfnConstructor_v45 ) (this->*s_pfnConstructor_v45)( pAppDomain, pModule, md, AssemblyIterationMode_Default); } void Start(AppDomain * pAppDomain, Module *pModule, mdMethodDef md) { (this->*s_pfnStart)( pAppDomain, pModule, md); } BOOL Next() { if( s_pfnNext_v4 ) return (this->*s_pfnNext_v4)(dummy2); if( s_pfnNext_v2 ) return (this->*s_pfnNext_v2)(); return FALSE; } MethodDesc* Current() { return (this->*s_pfnCurrent)(); } private: // we don't know the exact size of LoadedMethodDescIterator, so add enough memory here BYTE dummy[10240]; // class CollectibleAssemblyHolder<class DomainAssembly *> parameter for Next() in .Net4.0 and above BYTE dummy2[10240]; // constructor for .Net2.0 & .Net 4.0 static PFN_LoadedMethodDescIteratorConstructor s_pfnConstructor; // constructor for .Net4.5 static PFN_LoadedMethodDescIteratorConstructor_v45 s_pfnConstructor_v45; static PFN_Start s_pfnStart; static PFN_Next_v4 s_pfnNext_v4; static PFN_Next_v2 s_pfnNext_v2; static PFN_Current s_pfnCurrent; public: static void MatchAddress(PSYMBOL_INFOW pSymbolInfo) { LPVOID* pDest = NULL; if( wcscmp( L"LoadedMethodDescIterator::LoadedMethodDescIterator", pSymbolInfo->Name) == 0 ) { switch(g_tDotNetVersion) { case DotNetVersion_20: case DotNetVersion_40: pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnConstructor); break; case DotNetVersion_45: pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnConstructor_v45); break; default: ATLASSERT(FALSE); return; } } else if( wcscmp( L"LoadedMethodDescIterator::Next", pSymbolInfo->Name) == 0 ) { switch(g_tDotNetVersion) { case DotNetVersion_20: pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnNext_v2); break; case DotNetVersion_40: case DotNetVersion_45: pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnNext_v4); break; default: ATLASSERT(FALSE); return; } } else if( wcscmp( L"LoadedMethodDescIterator::Start", pSymbolInfo->Name) == 0 ) pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnStart); else if( wcscmp( L"LoadedMethodDescIterator::Current", pSymbolInfo->Name) == 0 ) pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnCurrent); if( pDest ) *pDest = (LPVOID)pSymbolInfo->Address; } };
И наконец, используем LoadedMethodDescIterator для вызова Reset() у MethodDesc для generic методов.
Module * pModule = pMethodDesc->GetLoaderModule(); AppDomain * pAppDomain = pMethodDesc->GetDomain(); if( pModule ) { LoadedMethodDescIterator * pLoadedMethodDescIter = new LoadedMethodDescIterator( pAppDomain, pModule, md); while(pLoadedMethodDescIter->Next()) { MethodDesc * pMD = pLoadedMethodDescIter->Current(); if( pMD ) pMD->Reset(); } delete pLoadedMethodDescIter; }
Компиляция и оптимизация
Я обнаружил, что если метод очень мал и его размер составляет всего несколько байт, то он будет компилироваться в inline режиме. Поэтому MethodDesc::Reset() не поможет, потому что во время выполнения, до вызова этого метода даже дело не дойдёт. Немного больше информации можно найти в CEEInfo::canInline (vm.\/jitinterface.cpp в SSCLI)
Динамические методы
При добавлении в IL-код динамических методов, надо быть очень осторожным. Добавление неверного IL-кода для других типов методов лишь вызовет InvalidApplicationException, но добавление некорректного IL-кода в динамический метод может привести к полному сбою CLR и нашего процесса. IL-код динамических методов отличается от остальных. Самый лучший выход – генерировать IL-код из другого динамического метода, а потом копировать и вставлять.
