Introduction
I have always been interested in the internal workings of the CLR. One thing of particular interest is the Just In Time Compiler or JIT. Today, we are going
to look at how the JIT compiles MSIL, and create a utility that allows us to programmatically replace any JIT'ed method with
another method at runtime. We will also create a debugging utility that will intercept JIT calls and print diagnostics information to the console.
Just In Time Compiler
Microsoft Intermediate Language or MSIL (properly know as Common Intermediate Language (CIL)) is a low level assembly type language. All .NET languages
are compiled into MSIL (some exception with C++/CLI). Processors cannot run MSIL directly (maybe an ARM Jazelle
like technology for .NET in the future). The Just In Time compiler is used to turn MSIL into machine code.
A method will only be compiled once by the JIT, the CLR will cache the machine code the JIT outputs for future calls.
The compilation process needs to be extremely fast since it happens at runtime. Because MSIL is a low level language, its OP codes translate very
easily to machine specific OP codes. The compilation process itself uses something called a JITStub. A JITStub is a chunk of machine code,
each method will have one. The JIT stub initially contains code that will invoke the JIT for the method. After the method is JIT'ed, the stub
is replaced with code that calls the machine code the JIT created directly.
000EFA60 E86488D979 call 79E882C9
000EFA60 E97BC1CB00 jmp 00DABBE0
Every class has a method table. A method table has the address of all the JITStubs for a class' methods. The method table is used
at JIT time to resolve method JIT stubs. A method that has already been JIT'ed will not reference this table; the machine code created
by the JIT will call the stub address directly. Below is the method table output from SOS. The entry column contains the stub address.
MethodDesc Table
Entry MethodDesc JIT Name
79371278 7914b928 PreJIT System.Object.ToString()
7936b3b0 7914b930 PreJIT System.Object.Equals(System.Object)
7936b3d0 7914b948 PreJIT System.Object.GetHashCode()
793624d0 7914b950 PreJIT System.Object.Finalize()
000efa60 00d77ce0 JIT ReplaceExample.StaticClassB.A()
000efa70 00d77ce8 NONE ReplaceExample.StaticClassB.B()
It is possible to invoke the JIT from managed code without actually invoking a method. The System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod
method
will force the JIT to compile the method.
Debugging
Debugging the JIT process can be quite difficult. Luckily, there are a number of great tools we can use. We will briefly go over them.
Debugging Using SOS
SOS stands for Son of Strike. It is a debugging extension for the CLR that you can use in Visual Studio or WinDbg. It is a really neat tool that
comes with the .NET framework. I have used it several times to debug .NET exceptions on machines that don't have Visual Studio installed. We can also use
it to locate and view structures in memory used by the CLR, view assembly and IL, and many other things. It has been my primary debugging tool
for writing this. SOS only works when unmanaged debugging is enabled. For more info, please click here.
Rotor
Rotor is an Open Source (released under Microsoft Shared Source license) CLR released by Microsoft. Rotor is not the same as the CLR that Microsoft ships,
but it is a complete CLR. The amount of code is enormous, and it can be quite difficult to find what you are looking for. We are using some
of the headers from Rotor for our JIT Logger.
JIT Logger
The JITLogger is a tool that logs JIT calls to the console. It can be enabled and disabled. I used Daniel Pistelli's code from
his .NET Internals and Code Injection article to create the JITLogger. Below are the JITLogger
signatures and some sample output. For more information, please look at the attached code or read Daniel's excellent
article.
public class JitLogger
{
public static bool Enabled { get; set; }
public static int JitCompileCount { get; }
}
Output:
JIT : 0xdd20a8 Program.StaticTests
JIT : 0xdd217b Program.TestStaticReplaceJited
JIT : 0x749c205c MethodUtil.ReplaceMethod
JIT : 0x749c2270 MethodUtil.MethodSignaturesEqual
JIT : 0x749c231c MethodUtil.GetMethodReturnType
JIT : 0x749c20e4 MethodUtil.GetMethodAddress
JIT : 0xdd23e6 StaticClassB.A
Method Injection
I want the replace code to be very basic. We will take two methods, a source and destination, and replace the destination with the source. Below is the signature
of our replacement method:
public static void ReplaceMethod(MethodBase source, MethodBase dest)
IL Injection
I originally wanted to replace the IL, but I ran into some issues. The CLR seems to behave different depending
on the build mode and if a debugger is attached. Also, the JIT calls are cached (stubs replaced). We would need to get the CLR to somehow invalidate
the JIT cache. I was able to get this working, but only in debug mode, and I had to persist some state for each method to be able to get the CLR to re-JIT it.
I also tried using Daniel Pistelli's hooking method, but ran into some issues. I wanted to programmatically replace methods from managed code.
I thought I could hook the JIT, use RuntimeHelpers.PrepareMethod
to cause the method to get JITed, and then modify the CORINFO_METHOD_INFO
structure that
gets passed into our hooked method. Passing state between managed and unmanaged hooked methods was an issue. If we call any managed method
from the hooked method, we get a stack overflow. Also, invalidating the JIT cache is a pain, and again, I could only get it working in debug mode.
Another approach I attempted was using the unmanaged metadata APIs. I would read the RVA from the metadata method table,
use the RVA and module base address to find the IL address in memory, and just write over it. This was problematic because the length of the source
IL in bytes must be less than the detestation. There is more than just the IL. We also have the tiny or fat IL header and possibly SEH structures, etc.
After the method is invoked, once the JITStub is replaced, we run into the same problem as the other method.
After running into a wall with the different IL approaches, I decided to try a different method. Instead of replacing the IL, we will replace
the assembly code that JIT outputs. With this approach, we don't have to worry about invaliding the cache, IL headers, SEH, etc.
Post JIT Injection
Our new approach will ensure both the source and destination methods are compiled, locate the method table in memory for both methods,
and replace the destination's JITStub address with that of the source. We can replace a method as many times as we like, and do not
have to worry about the method being cached. We need to locate a couple things in memory first.
We are going to use the RuntimeTypeHandle
and RuntimeMethodHandle
to locate the method table and a method slot in memory.
The RuntimeMethodHandle
points to an 8 byte structure in memory called a MethodDescription
. This is the same address we see in the MethodDesc column using
the SOS !DumpMT -MD
command. This structure contains the index of the method in the method table. We can then use RuntimeTypeHandle
to locate the method table itself. The method table starts 40 bytes after the RuntimeTypeHandle
address.
Dynamic methods work differently. I could not really find any documentation, but I was able to find the JITStub address using
the memory debugger. A dynamic method does not expose its RuntimeMethodHandle
so we need to use Reflection to get it. I found the JITStub address
24 bytes after the address of the runtime method handle.
public static IntPtr GetMethodAddress(MethodBase method)
{
if ((method is DynamicMethod))
{
unsafe
{
byte* ptr = (byte*)GetDynamicMethodRuntimeHandle(method).ToPointer();
if (IntPtr.Size == 8)
{
ulong* address = (ulong*)ptr;
address += 6;
return new IntPtr(address);
}
else
{
uint* address = (uint*)ptr;
address += 6;
return new IntPtr(address);
}
}
}
RuntimeHelpers.PrepareMethod(method.MethodHandle);
unsafe
{
int skip = 10;
UInt64* location = (UInt64*)(method.MethodHandle.Value.ToPointer());
int index = (int)(((*location) >> 32) & 0xFF);
if (IntPtr.Size == 8)
{
ulong* classStart = (ulong*)method.DeclaringType.TypeHandle.Value.ToPointer();
ulong* address = classStart + index + skip;
return new IntPtr(address);
}
else
{
uint* classStart = (uint*)method.DeclaringType.TypeHandle.Value.ToPointer();
uint* address = classStart + index + skip;
return new IntPtr(address);
}
}
}
private static IntPtr GetDynamicMethodRuntimeHandle(MethodBase method)
{
if (method is DynamicMethod)
{
FieldInfo fieldInfo = typeof(DynamicMethod).GetField("m_method",
BindingFlags.NonPublic|BindingFlags.Instance);
return ((RuntimeMethodHandle)fieldInfo.GetValue(method)).Value;
}
return method.MethodHandle.Value;
}
After we get the location of the JITStub addresses, we simply need to change the value. Shown below is our replace method:
public static void ReplaceMethod(IntPtr srcAdr, MethodBase dest)
{
IntPtr destAdr = GetMethodAddress(dest);
unsafe
{
if (IntPtr.Size == 8)
{
ulong* d = (ulong*)destAdr.ToPointer();
*d = *((ulong*)srcAdr.ToPointer());
}
else
{
uint* d = (uint*)destAdr.ToPointer();
*d = *((uint*)srcAdr.ToPointer());
}
}
}
public static void ReplaceMethod(MethodBase source, MethodBase dest)
{
if (!MethodSignaturesEqual(source, dest))
{
throw new ArgumentException("The method signatures are not the same.",
"source");
}
ReplaceMethod(GetMethodAddress(source), dest);
}
Example Code
In our example code, we are going to try several things. We are going to replace a static method from one class with a static method from another class. We will do the same
thing with an instance method. We will also replace a static method with a DynamicMethod. Some of our test methods were being inlined
in Release mode. I had to add MethodImpl
attributes to several of the methods to prevent inlining.
If we step through the code, we will notice that Visual Studio gets tricked too. After the method is replaced, Visual Studio will step into the new method instead of the old method.
Below is the output from our tests:
Enabling JIT debugging.
JIT : 0x10720a8 Program.StaticTests
JIT : 0x107217b Program.TestStaticReplaceJited
Replacing StaticClassA.A() with StaticClassB.A()
JIT : 0x71ac205c MethodUtil.ReplaceMethod
JIT : 0x71ac2270 MethodUtil.MethodSignaturesEqual
JIT : 0x71ac231c MethodUtil.GetMethodReturnType
JIT : 0x71ac20e4 MethodUtil.GetMethodAddress
JIT : 0x10723e6 StaticClassB.A
JIT : 0x71ac2094 MethodUtil.ReplaceMethod
JIT : 0x1072426 StaticClassA.A
Call StaticClassA.A() from a method that has already been jited
StaticClassA.A
Call StaticClassA.A() from a method that has not been jited
JIT : 0x1072172 Program.TestStaticReplace
StaticClassB.A
JIT : 0x1072190 Program.InstanceTests
JIT : 0x1072284 Program.TestInstanceReplaceJited
Replacing InstanceClassA.A() with InstanceClassB.A()
JIT : 0x10723c2 InstanceClassB.A
JIT : 0x1072402 InstanceClassA.A
Call InstanceClassA.A() from a method that has already been jited
JIT : 0x107241e InstanceClassA..ctor
InstanceClassA.A
Call InstanceClassA.A() from a method that has not been jited
JIT : 0x1072268 Program.TestInstanceReplace
InstanceClassB.A
JIT : 0x10722a0 Program.DynamicTests
JIT : 0x1072344 Program.CreateTestMethod
Created new dynamic metbod StaticClassA.C
JIT : 0x107232e Program.TestDynamicReplaceJited
Replacing StaticClassA.B() with dynamic StaticClassA.C()
JIT : 0x71ac2210 MethodUtil.GetDynamicMethodRuntimeHandle
JIT : 0x1072434 StaticClassA.B
Call StaticClassA.B() from a method that has already been jited
StaticClassA.B
Call StaticClassA.B() from a method that has not been jited
JIT : 0x1072325 Program.TestDynamicReplace
JIT : 0x10c318 DynamicClass.C
StaticClassA.C
Conclusions
The practical uses of this code are limited. If you want to modify a library you are using and don't have access to the source code,
don't want to decompile recompile or use a hex editor, then this might be helpful to you. Might be possible to create some AOP library that
modifies existing types at runtime instead of creating wrappers or build time approaches.
There are some limitations with this code. As we can see from the example code, once a method has been JIT'ed, it will no longer refer
to the method table address we are changing. Replacement should happen before a calling method gets JIT'ed. This has not been tested at all
on an x86-64 machine. Zapped or NGen-ed assemblies also do not work.
We need to keep in mind that we are directly manipulating the CLR memory in ways not intended. This code might not work with newer versions
of the .NET framework. This was tested with .NET 3.5 on a Vista x86, and might not work on your machine.
It might be cool to write a class that could detect the processor features and reemit a more optimized version that takes advantages
of technologies such as SIMD. My knowledge of the x86 assembler is a little bit limited. I will see if I can throw something together later though.
Update
It looks like the memory layout changed in .NET 2.0 SP2 which I was forced to install when installing .NET 3.5 SP1. I updated the code to detect
the framework and act appropriately. Please let me know if there are any issues: ziadelmalki@hotmail.com.
Links