This is the third part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#
A word of warning! Things presented here depend on the implementation details and require an extensive knowledge about the internals. There are multiple things in place which may change the outcome — multi-tiered compilation, optimizations, JIT vs AoT compilation, and others. I wrote an article covering some aspects but YMMV.
Warning — clickbait title!
We are not going to override sealed function, however, we are going to substitute one method with another using some machine code. Let’s go.
Introduction
Sometimes we would like to stub a function in order to use different one in tests. There are dozens of mocking libraries in C# which easily can do that, however, they usually require the method to be virtual. There are also libraries like Fakes able to modify event sealed types. We are going to do something similar.
Every method is a piece of machine code. If we simply modify this code to jump somewhere else, it will do that. If we jump to some other method with matching signature, we can easily “override sealed function”. Enough of theory, let’s see the code.
Implementation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public static void HijackMethod(MethodInfo source, MethodInfo target) { RuntimeHelpers.PrepareMethod(source.MethodHandle); RuntimeHelpers.PrepareMethod(target.MethodHandle); var sourceAddress = source.MethodHandle.GetFunctionPointer(); var targetAddress = (long)target.MethodHandle.GetFunctionPointer(); int offset = (int)(targetAddress - (long)sourceAddress - 4 - 1); // four bytes for relative address and one byte for opcode byte[] instruction = { 0xE9, // Long jump relative instruction (byte)(offset & 0xFF), (byte)((offset >> 8) & 0xFF), (byte)((offset >> 16) & 0xFF), (byte)((offset >> 24) & 0xFF) }; Marshal.Copy(instruction, 0, sourceAddress, instruction.Length); } |
This method takes two method descriptors: source method to override and target method to use as a jump target. We first prepare the methods (so they are compiled to machine code), next we extract their addresses and calculate the jump. Finally, we generate simple machine code which does the jump. First byte (0xE9
) is an opcode of jump instruction, next four bytes represent relative destination address. Finally, we copy the code in place.
We can use the following test program:
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 |
using System; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace MethodHijacker { class TestClass { public static string ReturnString() { return "Original string"; } public static string ReturnStringHijacked() { return "Modified string"; } public static string StringProperty { get; set; } public string NonStaticReturnStringHijacked() { return "Nonstatic modified string"; } public string NonStaticStringProperty { get; set; } } class Program { static void Main() { // String method Console.WriteLine(TestClass.ReturnString()); MethodHijacker.HijackMethod(typeof (TestClass).GetMethod(nameof(TestClass.ReturnString)), typeof (TestClass).GetMethod(nameof(TestClass.ReturnStringHijacked))); Console.WriteLine(TestClass.ReturnString()); // String property TestClass.StringProperty = "Test"; Console.WriteLine(TestClass.StringProperty); MethodHijacker.HijackMethod(typeof(TestClass).GetProperty(nameof(TestClass.StringProperty)).GetMethod, typeof(TestClass).GetMethod(nameof(TestClass.ReturnStringHijacked))); Console.WriteLine(TestClass.StringProperty); // Nonstatic property var instance = new TestClass(); instance.NonStaticStringProperty = "Test nonstatic"; Console.WriteLine(instance.NonStaticStringProperty); MethodHijacker.HijackMethod(typeof(TestClass).GetProperty(nameof(TestClass.NonStaticStringProperty)).GetMethod, typeof(TestClass).GetMethod(nameof(TestClass.NonStaticReturnStringHijacked))); Console.WriteLine(instance.NonStaticStringProperty); } } } |
The code was tested on x86 and x64 Windows 10 with .NET 4.5.