This is the seventeenth part of the Custom memory allocation series. For your convenience you can find other parts in the table of contents in Part 1 — Allocating object on a stack
I was rewriting my method hijacking samples to .NET 5 and I found an interesting behavior. Let’s take this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
using System; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; namespace OverridingSealedMethodNetCore { class Program { static void Main(string[] args) { Console.WriteLine($"Calling StaticString method before hacking:\t{TestClass.StaticString()}"); HijackMethod(typeof(TestClass), nameof(TestClass.StaticString), typeof(Program), nameof(StaticStringHijacked)); Console.WriteLine($"Calling StaticString method after hacking:\t{TestClass.StaticString()}"); Console.WriteLine(); var instance = new TestClass(); Console.WriteLine($"Calling InstanceString method before hacking:\t{instance.InstanceString()}"); HijackMethod(typeof(TestClass), nameof(TestClass.InstanceString), typeof(Program), nameof(InstanceStringHijacked)); Console.WriteLine($"Calling InstanceString method after hacking:\t{instance.InstanceString()}"); Console.WriteLine(); Vector2 v = new Vector2(9.856331f, -2.2437377f); for (int i = 1; i <= 35; i++) { MultiTieredClass.Test(v, i); Thread.Sleep(100); } } public static void HijackMethod(Type sourceType, string sourceMethod, Type targetType, string targetMethod) { // Get methods using reflection var source = sourceType.GetMethod(sourceMethod); var target = targetType.GetMethod(targetMethod); // Prepare methods to get machine code (not needed in this example, though) RuntimeHelpers.PrepareMethod(source.MethodHandle); RuntimeHelpers.PrepareMethod(target.MethodHandle); var sourceMethodDescriptorAddress = source.MethodHandle.Value; var targetMethodMachineCodeAddress = target.MethodHandle.GetFunctionPointer(); // Pointer is two pointers from the beginning of the method descriptor Marshal.WriteIntPtr(sourceMethodDescriptorAddress, 2 * IntPtr.Size, targetMethodMachineCodeAddress); } [MethodImpl(MethodImplOptions.NoInlining)] public static string StaticStringHijacked() { return "Static string hijacked"; } [MethodImpl(MethodImplOptions.NoInlining)] public string InstanceStringHijacked() { return "Instance string hijacked"; } } class TestClass { [MethodImpl(MethodImplOptions.NoInlining)] public static string StaticString() { return "Static string"; } [MethodImpl(MethodImplOptions.NoInlining)] public string InstanceString() { return "Instance string"; } } class MultiTieredClass { [MethodImpl(MethodImplOptions.NoInlining)] public static void Test(Vector2 v, int i) { v = Vector2.Normalize(v); Console.WriteLine($"Vector iteration {i:0000}:\t{v}\t{TestClass.StaticString()}"); } } } |
If you follow my blog then there is nothing new here. We try to hijack method by modifying its runtime metadata. The MultiTiered part is only to show recompilation of the code. I’m running this on W10 x64 in Release x64 mode and I’m getting this output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
Calling StaticString method before hacking: Static string Calling StaticString method after hacking: Static string Calling InstanceString method before hacking: Instance string Calling InstanceString method after hacking: Instance string Vector iteration 0001: <0.9750545, -0.22196561> Static string Vector iteration 0002: <0.9750545, -0.22196561> Static string Vector iteration 0003: <0.9750545, -0.22196561> Static string Vector iteration 0004: <0.9750545, -0.22196561> Static string Vector iteration 0005: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0006: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0007: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0008: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0009: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0010: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0011: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0012: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0013: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0014: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0015: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0016: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0017: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0018: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0019: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0020: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0021: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0022: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0023: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0024: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0025: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0026: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0027: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0028: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0029: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0030: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0031: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0032: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0033: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0034: <0.9750545, -0.22196561> Static string hijacked Vector iteration 0035: <0.97505456, -0.22196563> Static string |
And this is nice. Notice that first two lines of the output show that even though we hacked the method, we’re still not getting the new behavior. That’s first why.
Next, we see that we start calling the example of multitiered compilation method and first 4 instances are consistent. However, in fifth one we see that a hijacked method was called instead of the original one. That’s second why. This lasts until iteration 35 when multitiered compilation kicks in and recompiles things.
I don’t know the answer why it works this way but I presume there is this new code cache thing which was implemented around .NET Core 2.1 to support multitiered compilation. I may be wrong, though.