This is the first part of the .NET Inside Out series where I play with CLR internals. For your convenience you can find other parts using the links below (or by guessing the address):
Part 1 — Virtual and non-virtual calls in C#
Part 2 — Handling and rethrowing exceptions
Part 3 — How to override sealed function
Part 4 — How to override sealed function revisited
Part 5 — Capture thread creation to handle exceptions
Part 6 — Proxy handling casting
Part 7 — Generating Func from a bunch of bytes
Part 8 — Handling Stack Overflow Exception in C# with VEH
Part 9 — Generating Func from a bunch of bytes in C# revisited
Part 10 — Using type markers for low level optimizations
Part 11 — Using structs for devirtualization
Part 12 — Modifying managed library on an IL level
Part 13 — Bypassing license checks
Part 14 — Calling virtual method without dynamic dispatch
Part 15 — Starting process on different desktop
Part 16 — Abusing type system
Part 17 — Abusing types to serialize non-serializable type
Part 18 — Handling StackOverflowException with custom CLR host
Part 19 – Creating structure instance without calling a constructor
Part 20 – Try doing nothing but decreasing performance
Part 21 – Using is broken
Part 22 – Your application is always multithreaded and it’s not easy to exit properly
Part 23 – Machine code address of any .NET Core method
Part 24 – Synchronous waiting for the Task in the same frame
Part 25 – Using is broken revisited
Part 26 – Multiple identity inheritance in C#
Part 27 – Rerouting a running thread to some other method
Part 28 – Terminating some existing thread
Part 29 – Terminating some existing thread with jumps
Today we are going to dive into function invocation mechanism in .NET. We will use C# language to prepare few applications, then we will examine IL for these applications, finally, we will see the jitted machine code. Let’s go!
Table of Contents
Theory
In C# we have multiple types of functions. We have static functions which we need to call using class name. We have virtual functions which we can override using inheritance. We also have instance functions which are not virtual. Syntax for invoking all these functions in C# is the same — we simply use parenthesis after the full name and we are done. However, in IL there are different instructions for calling different methods. Let’s see the difference.
Static functions
It is usually said that static functions (e.g., class functions) are called using call opcode. This is available because we are able to determine address of a function during compilation so we can hardcode the address in the IL code.
Instance functions
Non-virtual instance functions works almost the same as static functions, however, they need another thing — instance of a class for which we call the method. This is why we cannot use the same opcode — we need to verify whether the reference is null or not. In former case we need to throw appropriate exception. To call non-virtual instance functions we use callvirt opcode. This opcode is also capable of calling virtual instance functions.
Virtual functions
In order to be able to call virtual functions .NET uses dispatch table. This is the most common way of implementing this mechanism, it is also used in most C++ compilers. Basically, every type contains it’s own table of functions with pointers to actual implementation. Imagine that we have a function called Foo
in base class, and we override this function in derived class. Both types will contain this method in their tables, however, the pointers to implementation will differ. Having that CLR is able to invoke function basing on the actual type, because it simply examines the dispatch table and calls function. However, this is slower than calling static function because CLR needs to extract the function address from the dispatch table.
Calling instance functions other way
In theory it is possible to call virtual function using call
opcode — without checking for null. If we wouldn’t use this
in method then everything should work fine.
Practice
Let’s see some xamples. I will use .NET 4.5.2 on Windows 10 x64. I will compile codes as Release with Any CPU and debug them using WinDBG x86. Let’s begin.
Static functions
Let’s start with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System; using System.Runtime.CompilerServices; namespace Calls { class Program { static void Main() { Functions.Static(); } } public class Functions { [MethodImpl(MethodImplOptions.NoInlining)] public static void Static() { Console.WriteLine("Static"); } } } |
Nothing fancy here. We simply call a static method from other class. We also add attribute which will disable inlining. Let’s disassemble the code using ILSpy:
1 2 3 4 5 6 7 8 9 10 11 |
.method private hidebysig static void Main () cil managed { // Method begins at RVA 0x2050 // Code size 6 (0x6) .maxstack 8 .entrypoint IL_0000: call void Calls.Functions::Static() IL_0005: ret } // end of method Program::Main |
We can see that we indeed call the method using call
opcode. Let’s now execute the app and see the generated machine 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 |
Microsoft (R) Windows Debugger Version 6.3.9600.17298 X86 Copyright (c) Microsoft Corporation. All rights reserved. CommandLine: "C:\Users\user\Documents\Visual Studio 2015\Projects\Calls\Calls\bin\Release\Calls.exe" Symbol search path is: *** Invalid *** **************************************************************************** * Symbol loading may be unreliable without a symbol search path. * * Use .symfix to have the debugger choose a symbol path. * * After setting your symbol path, use .reload to refresh symbol locations. * **************************************************************************** Executable search path is: ModLoad: 008f0000 008f8000 Calls.exe ModLoad: 77a30000 77ba9000 ntdll.dll ModLoad: 73830000 73889000 C:\windows\SysWOW64\MSCOREE.DLL ModLoad: 776b0000 777a0000 C:\windows\SysWOW64\KERNEL32.dll ModLoad: 74b20000 74c96000 C:\windows\SysWOW64\KERNELBASE.dll ModLoad: 62430000 624c1000 C:\windows\SysWOW64\apphelp.dll (3e54.36a4): Break instruction exception - code 80000003 (first chance) *** ERROR: Symbol file could not be found. Defaulted to export symbols for ntdll.dll - eax=00000000 ebx=00000000 ecx=48430000 edx=00000000 esi=008f0080 edi=ff2f9000 eip=77ad3c85 esp=00a8f9f8 ebp=00a8fa24 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 ntdll!LdrInitShimEngineDynamic+0x715: 77ad3c85 cc int 3 |
We load executable and we are ready to execute it. Let’s start it and let it work till the end.
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 |
0:000> g ModLoad: 75240000 752bb000 C:\windows\SysWOW64\ADVAPI32.dll ModLoad: 76900000 769be000 C:\windows\SysWOW64\msvcrt.dll ModLoad: 774b0000 774f3000 C:\windows\SysWOW64\sechost.dll ModLoad: 77920000 779cc000 C:\windows\SysWOW64\RPCRT4.dll ModLoad: 74b00000 74b1e000 C:\windows\SysWOW64\SspiCli.dll ModLoad: 74af0000 74afa000 C:\windows\SysWOW64\CRYPTBASE.dll ModLoad: 74a90000 74ae9000 C:\windows\SysWOW64\bcryptPrimitives.dll ModLoad: 737b0000 73829000 C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscoreei.dll ModLoad: 750b0000 750f4000 C:\windows\SysWOW64\SHLWAPI.dll ModLoad: 76bd0000 76d8a000 C:\windows\SysWOW64\combase.dll ModLoad: 74cb0000 74dfd000 C:\windows\SysWOW64\GDI32.dll ModLoad: 76d90000 76ed0000 C:\windows\SysWOW64\USER32.dll ModLoad: 75440000 7546b000 C:\windows\SysWOW64\IMM32.DLL ModLoad: 769c0000 76ae0000 C:\windows\SysWOW64\MSCTF.dll ModLoad: 749c0000 749eb000 C:\windows\SysWOW64\nvinit.dll ModLoad: 74a80000 74a88000 C:\windows\SysWOW64\VERSION.dll ModLoad: 74970000 749b6000 C:\PROGRA~2\Sophos\SOPHOS~1\SOPHOS~1.DLL ModLoad: 75480000 75486000 C:\windows\SysWOW64\PSAPI.DLL ModLoad: 74ca0000 74cac000 C:\windows\SysWOW64\kernel.appcore.dll ModLoad: 68a80000 69131000 C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll ModLoad: 73540000 73635000 C:\windows\SysWOW64\MSVCR120_CLR0400.dll (3e54.36a4): Unknown exception - code 04242420 (first chance) ModLoad: 67930000 68a7a000 C:\windows\assembly\NativeImages_v4.0.30319_32\mscorlib\225759bb87c854c0fff27b1d84858c21\mscorlib.ni.dll ModLoad: 777a0000 7788a000 C:\windows\SysWOW64\ole32.dll ModLoad: 73730000 737ae000 C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll ModLoad: 77410000 774a2000 C:\windows\SysWOW64\OLEAUT32.dll *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\windows\SysWOW64\KERNEL32.dll - eax=00000000 ebx=00000001 ecx=00000000 edx=00000000 esi=00000000 edi=77b38920 eip=77a98dcc esp=00a8f908 ebp=00a8f918 iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202 ntdll!ZwTerminateProcess+0xc: 77a98dcc c20800 ret 8 |
We can see that our process is about to terminate. Let’s load all symbols and SOS.
1 2 3 4 5 |
0:000> .loadby sos clr 0:000> .symfix 0:000> .reload Reloading current modules ............................... |
We have symbols loaded. Let’s find machine code for Main
function. We can do it for instance by finding assemblies:
1 2 3 4 5 6 |
0:000> !name2ee * Program Module: 67931000 Assembly: mscorlib.dll -------------------------------------- Module: 00dc3fbc Assembly: Calls.exe |
We have our assembly. Let’s dump its method tables:
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 |
0:000> !dumpmodule -mt 00dc3fbc Name: C:\Users\user\Documents\Visual Studio 2015\Projects\Calls\Calls\bin\Release\Calls.exe Attributes: PEFile Assembly: 00e5a4e0 LoaderHeap: 00000000 TypeDefToMethodTableMap: 00dc0038 TypeRefToMethodTableMap: 00dc0048 MethodDefToDescMap: 00dc0090 FieldDefToDescMap: 00dc00a4 MemberRefToDescMap: 00000000 FileReferencesMap: 00dc00ac AssemblyReferencesMap: 00dc00b0 MetaData start address: 008f206c (1504 bytes) Types defined in this module MT TypeDef Name ------------------------------------------------------------------------------ 00dc4cec 0x02000002 Calls.Program 00dc4d54 0x02000003 Calls.Functions Types referenced in this module MT TypeRef Name ------------------------------------------------------------------------------ 67d6cd10 0x02000001 System.Runtime.CompilerServices.CompilationRelaxationsAttribute 67d6d708 0x02000002 System.Runtime.CompilerServices.RuntimeCompatibilityAttribute 67d6cdb0 0x02000003 System.Diagnostics.DebuggableAttribute 67d6d220 0x02000005 System.Reflection.AssemblyTitleAttribute 67d6d2a0 0x02000006 System.Reflection.AssemblyDescriptionAttribute 67d6d260 0x02000007 System.Reflection.AssemblyConfigurationAttribute 67d6cca4 0x02000008 System.Reflection.AssemblyCompanyAttribute 67d6cc64 0x02000009 System.Reflection.AssemblyProductAttribute 67d6d3d4 0x0200000a System.Reflection.AssemblyCopyrightAttribute 67d6d414 0x0200000b System.Reflection.AssemblyTrademarkAttribute 67d6d00c 0x0200000c System.Runtime.InteropServices.ComVisibleAttribute 67d6cdf0 0x0200000d System.Runtime.InteropServices.GuidAttribute 67d6d454 0x0200000e System.Reflection.AssemblyFileVersionAttribute 67d70470 0x0200000f System.Runtime.Versioning.TargetFrameworkAttribute 67d6dbd4 0x02000010 System.Object 67d56980 0x02000011 System.Console |
We can see that we have two interesting method tables. Let’s dump the one for Program
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
0:000> !dumpmt -md 00dc4cec EEClass: 00dc138c Module: 00dc3fbc Name: Calls.Program mdToken: 02000002 File: C:\Users\user\Documents\Visual Studio 2015\Projects\Calls\Calls\bin\Release\Calls.exe BaseSize: 0xc ComponentSize: 0x0 Slots in VTable: 6 Number of IFaces in IFaceMap: 0 -------------------------------------- MethodDesc Table Entry MethodDe JIT Name 67cd19c8 679361fc PreJIT System.Object.ToString() 67cd7850 67936204 PreJIT System.Object.Equals(System.Object) 67cdbd80 67936224 PreJIT System.Object.GetHashCode() 67c2dbe8 67936238 PreJIT System.Object.Finalize() 0120003d 00dc4ce4 NONE Calls.Program..ctor() 01200450 00dc4cd8 JIT Calls.Program.Main() |
We can see that Main
method is already jitted (because it was executed). Let’s dump its machine code:
1 2 3 4 5 6 7 8 9 10 11 |
0:000> !U 01200450 Normal JIT generated code Calls.Program.Main() Begin 01200450, size 7 *** WARNING: Unable to verify checksum for Calls.exe c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10: >>> 01200450 ff15484ddc00 call dword ptr ds:[0DC4D48h] (Calls.Functions.Static(), mdToken: 06000003) c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11: 01200456 c3 ret |
We can see that we are calling method directly using call
instruction and passing the hardcoded address.
Non-virtual instance function
Let’s modify the code in the following way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System; using System.Runtime.CompilerServices; namespace Calls { class Program { static void Main() { new Functions().NonVirtualInstance(); } } public class Functions { [MethodImpl(MethodImplOptions.NoInlining)] public void NonVirtualInstance() { Console.WriteLine("Non-virtual instance"); } } } |
We changed the method to non-virtual instance method. In our Main
we create object of the class and directly call a method. Let’s see the IL:
1 2 3 4 5 6 7 8 9 10 11 12 |
.method private hidebysig static void Main () cil managed { // Method begins at RVA 0x2050 // Code size 11 (0xb) .maxstack 8 .entrypoint IL_0000: newobj instance void Calls.Functions::.ctor() IL_0005: call instance void Calls.Functions::NonVirtualInstance() IL_000a: ret } // end of method Program::Main |
We still use call
instruction here. Let’s examine the machine code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
0:000> !U 00bf0448 Normal JIT generated code Calls.Program.Main() Begin 00bf0448, size 13 *** WARNING: Unable to verify checksum for Calls.exe c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10: >>> 00bf0448 b9544dba00 mov ecx,0BA4D54h (MT: Calls.Functions) 00bf044d e8a22cfaff call 00b930f4 (JitHelp: CORINFO_HELP_NEWSFAST) 00bf0452 8bc8 mov ecx,eax 00bf0454 ff15484dba00 call dword ptr ds:[0BA4D48h] (Calls.Functions.NonVirtualInstance(), mdToken: 06000003) c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11: 00bf045a c3 ret |
We can see few interesting things. First, we start by calling constructor for the object. Next, we store this
reference in ecx
register. Finally, we call method directly using hardcoded address.
This might look a bit strange since in theory we should use callvirt
opcode. Let’s modify code a bit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System; using System.Runtime.CompilerServices; namespace Calls { class Program { static void Main() { var instance = new Functions(); instance.NonVirtualInstance(); } } public class Functions { [MethodImpl(MethodImplOptions.NoInlining)] public void NonVirtualInstance() { Console.WriteLine("Non-virtual instance"); } } } |
We simply store instance in a variable. Let’s see the IL:
1 2 3 4 5 6 7 8 9 10 11 12 |
.method private hidebysig static void Main () cil managed { // Method begins at RVA 0x2050 // Code size 11 (0xb) .maxstack 8 .entrypoint IL_0000: newobj instance void Calls.Functions::.ctor() IL_0005: callvirt instance void Calls.Functions::NonVirtualInstance() IL_000a: ret } // end of method Program::Main |
And now we can see that we are indeed using callvirt
instruction. Interesting! Let’s see the machine code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
0:000> !U 00e60448 Normal JIT generated code Calls.Program.Main() Begin 00e60448, size 13 *** WARNING: Unable to verify checksum for Calls.exe c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10: >>> 00e60448 b9544de100 mov ecx,0E14D54h (MT: Calls.Functions) 00e6044d e8a22cfaff call 00e030f4 (JitHelp: CORINFO_HELP_NEWSFAST) c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11: 00e60452 8bc8 mov ecx,eax 00e60454 ff15484de100 call dword ptr ds:[0E14D48h] (Calls.Functions.NonVirtualInstance(), mdToken: 06000003) c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 12: 00e6045a c3 ret |
As we can see, the machine code is exactly the same. There is no null check, so in this situation both call
and callvirt
instructions were jitted to the same code. Let’s modify the program a little more:
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 |
using System; using System.Runtime.CompilerServices; namespace Calls { class Program { static void Main() { var instance = new Functions(); Call(instance); } [MethodImpl(MethodImplOptions.NoInlining)] static void Call(Functions instance) { instance.NonVirtualInstance(); } } public class Functions { [MethodImpl(MethodImplOptions.NoInlining)] public void NonVirtualInstance() { Console.WriteLine("Non-virtual instance"); } } } |
We do almost the same, however, we pass object to another function and then we call instance function. The IL is as follows:
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 |
.method private hidebysig static void Main () cil managed { // Method begins at RVA 0x2050 // Code size 11 (0xb) .maxstack 8 .entrypoint IL_0000: newobj instance void Calls.Functions::.ctor() IL_0005: call void Calls.Program::Call(class Calls.Functions) IL_000a: ret } // end of method Program::Main .method private hidebysig static void Call ( class Calls.Functions 'instance' ) cil managed noinlining { // Method begins at RVA 0x205c // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: callvirt instance void Calls.Functions::NonVirtualInstance() IL_0006: ret } // end of method Program::Call |
And the machine code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
0:000> !U 02ba0448 Normal JIT generated code Calls.Program.Main() Begin 02ba0448, size 13 *** WARNING: Unable to verify checksum for Calls.exe c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10: >>> 02ba0448 b9604db502 mov ecx,2B54D60h (MT: Calls.Functions) 02ba044d e8a22cfaff call 02b430f4 (JitHelp: CORINFO_HELP_NEWSFAST) c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11: 02ba0452 8bc8 mov ecx,eax 02ba0454 ff15ec4cb502 call dword ptr ds:[2B54CECh] (Calls.Program.Call(Calls.Functions), mdToken: 06000002) c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 12: 02ba045a c3 ret |
We create an object, put it in the register and call a method. Let’s move on:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
0:000> !U 02ba0470 Normal JIT generated code Calls.Program.Call(Calls.Functions) Begin 02ba0470, size d c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 17: >>> 02ba0470 55 push ebp 02ba0471 8bec mov ebp,esp 02ba0473 3909 cmp dword ptr [ecx],ecx 02ba0475 ff15544db502 call dword ptr ds:[2B54D54h] (Calls.Functions.NonVirtualInstance(), mdToken: 06000004) c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 18: 02ba047b 5d pop ebp 02ba047c c3 ret |
And here we have what we wanted to see. First, we prepare a stack frame by storing ebp
register. Next, we compare ecx
register and perform a null check. Finally, we call a method directly using hardcoded address. Next, we can see a cleanup and exit instruction.
How does null check work?
You might ask what is going on. I said that there is a null check, however, there is neither branch instruction nor null handler. Let’s see the instruction:
1 |
02ba0473 3909 cmp dword ptr [ecx],ecx |
Here we compare a register to some extracted value. cmp
instruction sets CPU flags so we can later perform conditional jumps based on them. However, in our listing we simply ignore the comparison result so how does it work?
First, let’s assume that we passed a correct reference. We try to compare ecx
(which has correct value) with dword ptr [ecx]
. The latter tries to dereference the pointer and since it is valid, it extracts some value. We then perform a comparison and store flags in the CPU.
However, imagine that ecx
is a null reference (which means that it is equal to zero). If we try to dereference it, we will try to read something from the zero address. Since this is a null pointer memory partition, we will be blocked by the MMU and there will be a hardware interrupt. CLR will handle it and convert to NullReferenceException
.
So it looks like we can safely ignore CPU flags after the comparison, because in case of having null reference the CPU will notify us about the problem. Clever — we can perform a null check using one CPU instruction.
Virtual function
Let us now call a virtual function. Let’s use 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 |
using System; using System.Runtime.CompilerServices; namespace Calls { class Program { static void Main() { var instance = new Functions(); instance.ToString(); } } public class Functions { [MethodImpl(MethodImplOptions.NoInlining)] public override string ToString() { var message = "Virtual instance"; Console.WriteLine(message); return message; } } } |
We will utilize ToString
method, since it is virtual in System.Object
class. IL for this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
.method private hidebysig static void Main () cil managed { // Method begins at RVA 0x2050 // Code size 12 (0xc) .maxstack 8 .entrypoint IL_0000: newobj instance void Calls.Functions::.ctor() IL_0005: callvirt instance string [mscorlib]System.Object::ToString() IL_000a: pop IL_000b: ret } // end of method Program::Main |
We use callvirt
instruction. Please also notice that we are calling method from System.Object
and not from our class. Right now we expect to see invocation using dispatch table. Let’s check it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
0:000> !U 02a30448 Normal JIT generated code Calls.Program.Main() Begin 02a30448, size 18 *** WARNING: Unable to verify checksum for Calls.exe c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10: >>> 02a30448 55 push ebp 02a30449 8bec mov ebp,esp 02a3044b b9504d2701 mov ecx,1274D50h (MT: Calls.Functions) 02a30450 e89f2c83fe call 012630f4 (JitHelp: CORINFO_HELP_NEWSFAST) c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11: 02a30455 8bc8 mov ecx,eax 02a30457 8b01 mov eax,dword ptr [ecx] 02a30459 8b4028 mov eax,dword ptr [eax+28h] 02a3045c ff10 call dword ptr [eax] c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 12: 02a3045e 5d pop ebp 02a3045f c3 ret |
And we can indeed verify that there is a dispatch table used. These three lines are doing that:
1 2 3 |
02a30457 8b01 mov eax,dword ptr [ecx] 02a30459 8b4028 mov eax,dword ptr [eax+28h] 02a3045c ff10 call dword ptr [eax] |
We first dereference the pointer to an object and store it in the eax
register. Since .NET reference points to pointer to type descriptor, we end up with pointer to type descriptor in eax
. Next, we dereference the value which is stored 40 bytes after the beginning of the type descriptor and store it in the eax
register. This is an address of the method descriptor of implementation of ToString
in our custom class. Finally, we call the method using the register value. So we can see that it is indeed using dynamic address instead of hardcoded one.
Dynamic call
For now we were only calling methods using ordinary mechanisms which can be checked during compilation time. However, there is also a dynamic
keyword which allows us to defer the call and perform it in runtime. Let’s modify the code a bit and see how it works:
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 |
using System; using System.Runtime.CompilerServices; namespace Calls { class Program { static void Main() { dynamic instance = new Functions(); instance.ToString(); } } public class Functions { [MethodImpl(MethodImplOptions.NoInlining)] public override string ToString() { var message = "Virtual instance"; Console.WriteLine(message); return message; } } } |
Only one change in here. We replaced the var
with dynamic
so now the compiler should emit code for using DLR mechanisms. Let’s decompile the 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 |
.method private hidebysig static void Main () cil managed { // Method begins at RVA 0x2050 // Code size 87 (0x57) .maxstack 9 .entrypoint .locals init ( [0] object ) IL_0000: newobj instance void Calls.Functions::.ctor() IL_0005: stloc.0 IL_0006: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSite, object>> Calls.Program/'<>o__0'::'<>p__0' IL_000b: brtrue.s IL_0041 IL_000d: ldc.i4 256 IL_0012: ldstr "ToString" IL_0017: ldnull IL_0018: ldtoken Calls.Program IL_001d: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) IL_0022: ldc.i4.1 IL_0023: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo IL_0028: dup IL_0029: ldc.i4.0 IL_002a: ldc.i4.0 IL_002b: ldnull IL_002c: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string) IL_0031: stelem.ref IL_0032: call class [System.Core]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::InvokeMember(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, string, class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Type>, class [mscorlib]System.Type, class [mscorlib]System.Collections.Generic.IEnumerable`1<class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo>) IL_0037: call class [System.Core]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSite, object>>::Create(class [System.Core]System.Runtime.CompilerServices.CallSiteBinder) IL_003c: stsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSite, object>> Calls.Program/'<>o__0'::'<>p__0' IL_0041: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSite, object>> Calls.Program/'<>o__0'::'<>p__0' IL_0046: ldfld !0 class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSite, object>>::Target IL_004b: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSite, object>> Calls.Program/'<>o__0'::'<>p__0' IL_0050: ldloc.0 IL_0051: callvirt instance void class [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSite, object>::Invoke(!0, !1) IL_0056: ret } // end of method Program::Main |
And indeed we can see, that calling dynamic method is much more difficult. We use things like CallSite
and lots of DLR magic here.
Summary
In this post we saw how different functions are called. The actual opcode used for invocation depends on type of a method and even on a way of storing the variable which we use to call the method. However, even using different opcode might not result in different machine code since the CLR is able to perform optimizations when jitting the code.