📜 ⬆️ ⬇️

Age of JIT compiling. Part I. Genesis

The runtime theme of the .NET platform is covered in great detail. However, the work of the JIT itself, the resulting code and interaction with the runtime environment is not very.

Well, fix it!

We learn the reasons for the lack of inheritance from structures, the nature of unbound delegates.
')
And also ... calling any methods on any objects without reflection.

â–Ś Genesis of Value-types


Structures in .NET are, on the one hand, structures in the classical understanding of a given word (layout, mutability, etc.), on the other hand, they support OOP and the .NET environment in principle (methods ToString, GetHashCode; inheritance from System.ValueType, which in turn from System.Object; etc.).

To better understand why structures cannot be inherited from other types, it is necessary to go to the level of organization of methods in the CLR.

Instance-level methods have an implicit argument this. In fact, it is clear. JIT, by compiling code, creates a signature of the following form:

ReturnType MethodName(Type this, …arguments…) 

But this is for reference types.

For significant:

 ReturnType MethodName(ref Type this, …arguments…) 

Yes Yes! This is done to support the variability of structures, i.e. so we can modify this.

So why not inherit structures from other types?

Answer the question: what if this is a virtual method of the base reference class? How to be a JIT compiler? No Constantly guessing and generating various code specializations (with semantics byval and byref), besides dispatching a table of virtual methods, is inefficient. Boxing is added to properly serve the virtual method.

But ... Methods ToString , GetHashCode , Equals are virtual methods of the reference ancestor class System.Object ?!

These are exceptions. JIT knows this and generates a binding and specialization only for these methods.

â–Ś Unbound Delegates


Reflection in .NET allows us to create a delegate for both static methods and instances.
However, there is a small problem - for instances you need to create a delegate in a new way.

Consider an example:
 class Program { static void Main(string[] args) { var calc = new Calc() { FirstOperand = 2 }; var addMethodInfo = typeof(Calc).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); var addDelegate = (Func<int, int>)Delegate.CreateDelegate( typeof(Func<int, int>), calc, addMethodInfo); Console.WriteLine(addDelegate(2)); // 4 } } class Calc { public int FirstOperand = 0; public int Add(int secondOperand) { return FirstOperand + secondOperand; } } 


Unbound delegates come to the rescue , i.e. unbound However, they have one feature: a different signature where they add (yes, you guessed correctly) the first argument - a link to the instance.

Those. unbound delegates are references to the “real” method.

So, the signature Add (int secondOperand) turns into Add ( Calc this , int secondOperand) .

Check:
 class Program { static void Main(string[] args) { var addMethodInfo = typeof(Calc).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); var addDelegate = (Func<Calc, int, int>)Delegate.CreateDelegate( typeof(Func<Calc, int, int>), null, addMethodInfo); Console.WriteLine(addDelegate(new Calc(), 2)); // 2 } } class Calc { public int FirstOperand = 0; public int Add(int secondOperand) { return FirstOperand + secondOperand; } } 


Remember the question about the signatures of the structure methods? Declare the type Calc as a struct and run. ArgumentException? Yes?

We need to pass the argument to this byref to Func <Calc, int, int>, but how ?!

Declare your delegate FuncByRef
 delegate TResult FuncByRef<T1, in T2, out TResult>(ref T1 arg1, T2 arg2); 


Change the code:
 class Program { delegate TResult FuncByRef<T1, in T2, out TResult>(ref T1 arg1, T2 arg2); static void Main(string[] args) { var addMethodInfo = typeof(Calc).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); var addDelegate = (FuncByRef<Calc, int, int>)Delegate.CreateDelegate( typeof(FuncByRef<Calc, int, int>), null, addMethodInfo); var calc = new Calc(); calc.FirstOperand = 123; Console.WriteLine(addDelegate(ref calc, 2)); // 125 } } struct Calc { public int FirstOperand; public int Add(int secondOperand) { return FirstOperand + secondOperand; } } 


â–Ś Verification evasion


Consider a simple application:
 class Program { static void Main(string[] args) { CallTest(new object()); CallTestWithExlicitCasting(new object()); Console.Read(); } static void CallTest(object target) { Program p = target as Program; p.Test(); } static void CallTestWithExlicitCasting(object target) { Program p = (Program)target; p.Test(); } public void Test() { Console.WriteLine("Test"); } } 

As you can see, the application will fall from a NullReferenceException when calling CallTest ().

Well, correct this situation. To do this, run ildasm .

Visual Studio Command Promt -> ildasm



Next File -> Dump -> Save as dialog -> msiltricks_patch.il

Open the saved file msiltricks_patch.il in your favorite editor and go to the body of the CallTest method:
 .method private hidebysig static void CallTest(object target) cil managed { // Code size 14 (0xe) .maxstack 1 .locals init ([0] class MSILTricks.Program p) IL_0000: ldarg.0 IL_0001: isinst MSILTricks.Program IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: callvirt instance void MSILTricks.Program::Test() IL_000d: ret } // end of method Program::CallTest 

Delete the term IL_0001: isinst MSILTricks.Program, i.e. call the isinst op code (also known as as in C #).

Do the same with the CallTestWithExlicitCasting method:
 .method private hidebysig static void CallTestWithExlicitCasting(object target) cil managed { // Code size 14 (0xe) .maxstack 1 .locals init ([0] class MSILTricks.Program p) IL_0000: ldarg.0 IL_0001: castclass MSILTricks.Program IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: callvirt instance void MSILTricks.Program::Test() IL_000d: ret } // end of method Program::CallTestWithExlicitCasting 

Remove the term IL_0001: castclass MSILTricks.Program , i.e. a call to the castclass opcode (also known as an explicit cast operator in C #).

Visual Studio Command Promt -> cd [your saved file dir]
Visual Studio Command Promt -> ilasm msiltricks_patch.il

Run msiltricks_patch.exe

Not a single exception, even AccessViolationException .
Haha

The fact is that our Test method has no side effects, and also does not use this in its body.

Conclusion: we are working with “hardware” and the variables of reference types are just addresses in memory, i.e. DWORD ; type casting, etc. are nothing more than an abstraction and “protection” at the compilation stage. The central processor works with addresses in memory. The CLR provides these addresses, the JIT compiles the code considering them.

Your KO :)

And, yes, the callvirt instruction does not check for the “correctness” of the object.
To get AccessViolationException, you can add, for example, a virtual method to the Program class and call it in the Test method.

Source: https://habr.com/ru/post/248775/


All Articles