0% found this document useful (0 votes)
40 views20 pages

Inside Out Part 1 - Virtual and Non-Virtual Calls in C# - Random IT Utensils

The document discusses how static, instance, and virtual functions are called in .NET and C#. It provides theory on the different types of functions and how they are called. It then has code examples and disassembled code to demonstrate how each type is called at the IL and machine code levels.

Uploaded by

Vishal
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
40 views20 pages

Inside Out Part 1 - Virtual and Non-Virtual Calls in C# - Random IT Utensils

The document discusses how static, instance, and virtual functions are called in .NET and C#. It provides theory on the different types of functions and how they are called. It then has code examples and disassembled code to demonstrate how each type is called at the IL and machine code levels.

Uploaded by

Vishal
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

Random IT Utensils

IT, operating systems, maths, and more.

Menu

.NET Inside Out Part 1 — Virtual and non-virtual calls


in C#
MAY 21, 2016


This is the rst part of the .NET Inside Out series where I play with CLR internals. For
your convenience you can nd 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 Over ow 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 StackOver owException 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#

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, nally,
we will see the jitted machine code. Let’s go!

Table of Contents 
1. Theory
1.1. Static functions
1.2. Instance functions
1.3. Virtual functions
1.4. Calling instance functions other way
2. Practice
3. Static functions
4. Non-virtual instance function
5. How does null check work?
6. Virtual function
7. Dynamic call
8. Summary

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
1 using System;
2 using System.Runtime.CompilerServices;
3  
4 namespace Calls
5 {
6     class Program
7     {
8         static void Main()
9         {
10             dynamic instance = new Functions();
11             instance.ToString();
12         }
13     }
14  
15     public class Functions
16     {
17         [MethodImpl(MethodImplOptions.NoInlining)]
18         public override string ToString()
19         {
20             var message = "Virtual instance";
21             Console.WriteLine(message);
22             return message;
23         }
24     }
25 }

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 .method private hidebysig static


2 void Main () cil managed
3 {
4 // Method begins at RVA 0x2050
5 // Code size 87 (0x57)
6 .maxstack 9
7 .entrypoint
8 .locals init (
9 [0] object
10 )
11  
12 IL_0000: newobj instance void Calls.Functions::.ctor()
13 IL_0005: stloc.0
14 IL_0006: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<clas
s [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSit
e, object>> Calls.Program/'<>o__0'::'<>p__0'
IL_000b: brtrue.s IL_0041
15  
16 IL_000d: ldc.i4 256
17 IL_0012: ldstr "ToString"
18 IL_0017: ldnull
19 IL_0018: ldtoken Calls.Program
20 IL_001d: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle
21 (valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0022: ldc.i4.1
IL_0023: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInf
22 o
23 IL_0028: dup
IL_0029: ldc.i4.0
24 IL_002a: ldc.i4.0
25 IL_002b: ldnull
26 IL_002c: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumen
27 tInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valu
28 etype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string
)
IL_0031: stelem.ref
20         }
21     }
22 }

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 .method private hidebysig static


2 void Main () cil managed
3 {
4 // Method begins at RVA 0x2050
5 // Code size 6 (0x6)
6 .maxstack 8
7 .entrypoint
8  
9 IL_0000: call void Calls.Functions::Static()
10 IL_0005: ret
11 } // 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 Microsoft (R) Windows Debugger Version 6.3.9600.17298 X86


2 Copyright (c) Microsoft Corporation. All rights reserved.
3  
4 CommandLine: "C:\Users\user\Documents\Visual Studio 2015\Projects\Calls\Calls\bin\Rele
ase\Calls.exe"
5 Symbol search path is: *** Invalid ***
6 ****************************************************************************
* Symbol loading may be unreliable without a symbol search path.           *
7 * Use .symfix to have the debugger choose a symbol path.                   *
* After setting your symbol path, use .reload to refresh symbol locations. *
8 ****************************************************************************
Executable search path is:
9 ModLoad: 008f0000 008f8000   Calls.exe
ModLoad: 77a30000 77ba9000   ntdll.dll
10 ModLoad: 73830000 73889000   C:\windows\SysWOW64\MSCOREE.DLL
ModLoad: 776b0000 777a0000   C:\windows\SysWOW64\KERNEL32.dll
11 ModLoad: 74b20000 74c96000   C:\windows\SysWOW64\KERNELBASE.dll
12 ModLoad: 62430000 624c1000   C:\windows\SysWOW64\apphelp.dll
13 (3e54.36a4): Break instruction exception - code 80000003 (first chance)
14 *** ERROR: Symbol file could not be found.  Defaulted to export symbols for ntdll.dll
15 -
16 eax=00000000 ebx=00000000 ecx=48430000 edx=00000000 esi=008f0080 edi=ff2f9000
17 eip=77ad3c85 esp=00a8f9f8 ebp=00a8fa24 iopl=0         nv up ei pl zr na pe nc
18 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrInitShimEngineDynamic+0x715:
19 77ad3c85 cc              int     3

20

21

22

23
24

We load executable and we are ready to execute it. Let’s start it and let it work till the end.
1 0:000> g
2 ModLoad: 75240000 752bb000   C:\windows\SysWOW64\ADVAPI32.dll
3 ModLoad: 76900000 769be000   C:\windows\SysWOW64\msvcrt.dll
4 ModLoad: 774b0000 774f3000   C:\windows\SysWOW64\sechost.dll
5 ModLoad: 77920000 779cc000   C:\windows\SysWOW64\RPCRT4.dll
6 ModLoad: 74b00000 74b1e000   C:\windows\SysWOW64\SspiCli.dll
7 ModLoad: 74af0000 74afa000   C:\windows\SysWOW64\CRYPTBASE.dll
8 ModLoad: 74a90000 74ae9000   C:\windows\SysWOW64\bcryptPrimitives.dll
ModLoad: 737b0000 73829000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscoreei.dl
9 l
ModLoad: 750b0000 750f4000   C:\windows\SysWOW64\SHLWAPI.dll
10 ModLoad: 76bd0000 76d8a000   C:\windows\SysWOW64\combase.dll
11 ModLoad: 74cb0000 74dfd000   C:\windows\SysWOW64\GDI32.dll
12 ModLoad: 76d90000 76ed0000   C:\windows\SysWOW64\USER32.dll
13 ModLoad: 75440000 7546b000   C:\windows\SysWOW64\IMM32.DLL
14 ModLoad: 769c0000 76ae0000   C:\windows\SysWOW64\MSCTF.dll
15 ModLoad: 749c0000 749eb000   C:\windows\SysWOW64\nvinit.dll
16 ModLoad: 74a80000 74a88000   C:\windows\SysWOW64\VERSION.dll
17 ModLoad: 74970000 749b6000   C:\PROGRA~2\Sophos\SOPHOS~1\SOPHOS~1.DLL
18 ModLoad: 75480000 75486000   C:\windows\SysWOW64\PSAPI.DLL
ModLoad: 74ca0000 74cac000   C:\windows\SysWOW64\kernel.appcore.dll
19 ModLoad: 68a80000 69131000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
20 ModLoad: 73540000 73635000   C:\windows\SysWOW64\MSVCR120_CLR0400.dll
(3e54.36a4): Unknown exception - code 04242420 (first chance)
21 ModLoad: 67930000 68a7a000   C:\windows\assembly\NativeImages_v4.0.30319_32\mscorlib\2
25759bb87c854c0fff27b1d84858c21\mscorlib.ni.dll
22 ModLoad: 777a0000 7788a000   C:\windows\SysWOW64\ole32.dll
ModLoad: 73730000 737ae000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll
23 ModLoad: 77410000 774a2000   C:\windows\SysWOW64\OLEAUT32.dll
24 *** 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
25 eip=77a98dcc esp=00a8f908 ebp=00a8f918 iopl=0         nv up ei pl nz na po nc
26 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!ZwTerminateProcess+0xc:
27 77a98dcc c20800          ret     8
28

29

30

31

32
33

We can see that our process is about to terminate. Let’s load all symbols and SOS.

1 0:000> .loadby sos clr


2 0:000> .symfix
3 0:000> .reload
4 Reloading current modules
5 ...............................

We have symbols loaded. Let’s nd machine code for Main function. We can do it for instance
by nding assemblies:

1 0:000> !name2ee * Program


2 Module:      67931000
3 Assembly:    mscorlib.dll
4 --------------------------------------
5 Module:      00dc3fbc
6 Assembly:    Calls.exe
We have our assembly. Let’s dump its method tables:

1 0:000> !dumpmodule -mt 00dc3fbc


2 Name:       C:\Users\user\Documents\Visual Studio 2015\Projects\Calls\Calls\bin\Releas
e\Calls.exe
3 Attributes: PEFile
4 Assembly:   00e5a4e0
5 LoaderHeap:              00000000
6 TypeDefToMethodTableMap: 00dc0038
7 TypeRefToMethodTableMap: 00dc0048
8 MethodDefToDescMap:      00dc0090
9 FieldDefToDescMap:       00dc00a4
10 MemberRefToDescMap:      00000000
11 FileReferencesMap:       00dc00ac
12 AssemblyReferencesMap:   00dc00b0
13 MetaData start address:  008f206c (1504 bytes)
14  
15 Types defined in this module
16  
17       MT  TypeDef Name
18 ------------------------------------------------------------------------------
00dc4cec 0x02000002 Calls.Program
19 00dc4d54 0x02000003 Calls.Functions
20  
21 Types referenced in this module
22  
23       MT    TypeRef Name
24 ------------------------------------------------------------------------------
25 67d6cd10 0x02000001 System.Runtime.CompilerServices.CompilationRelaxationsAttribute
67d6d708 0x02000002 System.Runtime.CompilerServices.RuntimeCompatibilityAttribute
26 67d6cdb0 0x02000003 System.Diagnostics.DebuggableAttribute
67d6d220 0x02000005 System.Reflection.AssemblyTitleAttribute
27 67d6d2a0 0x02000006 System.Reflection.AssemblyDescriptionAttribute
67d6d260 0x02000007 System.Reflection.AssemblyConfigurationAttribute
28 67d6cca4 0x02000008 System.Reflection.AssemblyCompanyAttribute
29 67d6cc64 0x02000009 System.Reflection.AssemblyProductAttribute
30 67d6d3d4 0x0200000a System.Reflection.AssemblyCopyrightAttribute
67d6d414 0x0200000b System.Reflection.AssemblyTrademarkAttribute
31 67d6d00c 0x0200000c System.Runtime.InteropServices.ComVisibleAttribute
67d6cdf0 0x0200000d System.Runtime.InteropServices.GuidAttribute
32 67d6d454 0x0200000e System.Reflection.AssemblyFileVersionAttribute
33 67d70470 0x0200000f System.Runtime.Versioning.TargetFrameworkAttribute
34 67d6dbd4 0x02000010 System.Object
67d56980 0x02000011 System.Console
35

36

37

38

39

40
41

We can see that we have two interesting method tables. Let’s dump the one for Program class:

1 0:000> !dumpmt -md 00dc4cec


2 EEClass:         00dc138c
3 Module:          00dc3fbc
4 Name:            Calls.Program
5 mdToken:         02000002
6
7 File:            C:\Users\user\Documents\Visual Studio 2015\Projects\Calls\Calls\bin\R
8 elease\Calls.exe
9 BaseSize:        0xc
ComponentSize:   0x0
10
11 Slots in VTable: 6
12 Number of IFaces in IFaceMap: 0
13 --------------------------------------
14 MethodDesc Table
   Entry MethodDe    JIT Name
15
16 67cd19c8 679361fc PreJIT System.Object.ToString()
17 67cd7850 67936204 PreJIT System.Object.Equals(System.Object)
18 67cdbd80 67936224 PreJIT System.Object.GetHashCode()
19 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 0:000> !U 01200450
2 Normal JIT generated code
3 Calls.Program.Main()
4 Begin 01200450, size 7
5 *** WARNING: Unable to verify checksum for Calls.exe
6  
7 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10:
>>> 01200450 ff15484ddc00    call    dword ptr ds:[0DC4D48h] (Calls.Functions.Static()
8 , mdToken: 06000003)
 
9 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11:
10 01200456 c3              ret

11

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 using System;
2 using System.Runtime.CompilerServices;
3  
4 namespace Calls
5 {
6     class Program
7     {
8         static void Main()
9         {
10             new Functions().NonVirtualInstance();
11         }
12     }
13  
14     public class Functions
15     {
16         [MethodImpl(MethodImplOptions.NoInlining)]
17         public void NonVirtualInstance()
18         {
19             Console.WriteLine("Non-virtual instance");
20         }
21     }
22 }

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 .method private hidebysig static


2 void Main () cil managed
3 {
4 // Method begins at RVA 0x2050
5 // Code size 11 (0xb)
6 .maxstack 8
7 .entrypoint
8  
9 IL_0000: newobj instance void Calls.Functions::.ctor()
10 IL_0005: call instance void Calls.Functions::NonVirtualInstance()
IL_000a: ret
11 } // end of method Program::Main
12

We still use call instruction here. Let’s examine the machine code:

1 0:000> !U 00bf0448
2 Normal JIT generated code
3 Calls.Program.Main()
4 Begin 00bf0448, size 13
5 *** WARNING: Unable to verify checksum for Calls.exe
6  
7 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10:
>>> 00bf0448 b9544dba00      mov     ecx,0BA4D54h (MT: Calls.Functions)
8 00bf044d e8a22cfaff      call    00b930f4 (JitHelp: CORINFO_HELP_NEWSFAST)
00bf0452 8bc8            mov     ecx,eax
9 00bf0454 ff15484dba00    call    dword ptr ds:[0BA4D48h] (Calls.Functions.NonVirtualIn
stance(), mdToken: 06000003)
10  
11 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11:
00bf045a c3              ret
12
13

14

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 using System;
2 using System.Runtime.CompilerServices;
3  
4 namespace Calls
5 {
6     class Program
7     {
8         static void Main()
9         {
10             var instance = new Functions();
11             instance.NonVirtualInstance();
12         }
13     }
14  
15     public class Functions
16     {
17         [MethodImpl(MethodImplOptions.NoInlining)]
18         public void NonVirtualInstance()
19         {
20             Console.WriteLine("Non-virtual instance");
21         }
22     }
23 }

We simply store instance in a variable. Let’s see the IL:

1 .method private hidebysig static


2 void Main () cil managed
3 {
4 // Method begins at RVA 0x2050
5 // Code size 11 (0xb)
6 .maxstack 8
7 .entrypoint
8  
9 IL_0000: newobj instance void Calls.Functions::.ctor()
10 IL_0005: callvirt instance void Calls.Functions::NonVirtualInstance()
IL_000a: ret
11 } // end of method Program::Main
12

And now we can see that we are indeed using callvirt instruction. Interesting! Let’s see the
machine code:

1 0:000> !U 00e60448
2 Normal JIT generated code
3 Calls.Program.Main()
4 Begin 00e60448, size 13
5 *** WARNING: Unable to verify checksum for Calls.exe
6  
7 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10:
>>> 00e60448 b9544de100      mov     ecx,0E14D54h (MT: Calls.Functions)
8 00e6044d e8a22cfaff      call    00e030f4 (JitHelp: CORINFO_HELP_NEWSFAST)
 
9 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11:
00e60452 8bc8            mov     ecx,eax
10 00e60454 ff15484de100    call    dword ptr ds:[0E14D48h] (Calls.Functions.NonVirtualIn
11 stance(), mdToken: 06000003)
 
12 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 12:
13 00e6045a c3              ret

14
15

16

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 using System;
2 using System.Runtime.CompilerServices;
3  
4 namespace Calls
5 {
6     class Program
7     {
8         static void Main()
9         {
10             var instance = new Functions();
11             Call(instance);
12         }
13  
14         [MethodImpl(MethodImplOptions.NoInlining)]
15         static void Call(Functions instance)
16         {
17             instance.NonVirtualInstance();
18         }
19     }
20  
21     public class Functions
22     {
23         [MethodImpl(MethodImplOptions.NoInlining)]
24         public void NonVirtualInstance()
25         {
26             Console.WriteLine("Non-virtual instance");
27         }
28     }
29 }

We do almost the same, however, we pass object to another function and then we call instance
function. The IL is as follows:

1 .method private hidebysig static


2 void Main () cil managed
3 {
4 // Method begins at RVA 0x2050
5 // Code size 11 (0xb)
6 .maxstack 8
7 .entrypoint
8  
9 IL_0000: newobj instance void Calls.Functions::.ctor()
10 IL_0005: call void Calls.Program::Call(class Calls.Functions)
IL_000a: ret
11 } // end of method Program::Main
12  
13 .method private hidebysig static
14 void Call (
15 class Calls.Functions 'instance'
16 ) cil managed noinlining
17 {
18 // Method begins at RVA 0x205c
19 // Code size 7 (0x7)
20 .maxstack 8
21  
22 IL_0000: ldarg.0
23 IL_0001: callvirt instance void Calls.Functions::NonVirtualInstance()
24 IL_0006: ret
} // end of method Program::Call
25
26

And the machine code:


1 0:000> !U 02ba0448
2
Normal JIT generated code
3
Calls.Program.Main()
4
Begin 02ba0448, size 13
5
*** WARNING: Unable to verify checksum for Calls.exe
6
 
7
c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10:
>>> 02ba0448 b9604db502      mov     ecx,2B54D60h (MT: Calls.Functions)
8
02ba044d e8a22cfaff      call    02b430f4 (JitHelp: CORINFO_HELP_NEWSFAST)
 
9
c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11:
02ba0452 8bc8            mov     ecx,eax
10
02ba0454 ff15ec4cb502    call    dword ptr ds:[2B54CECh] (Calls.Program.Call(Calls.Fun
11
ctions), mdToken: 06000002)
 
12
c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 12:
13
02ba045a c3              ret
14
15

16

We create an object, put it in the register and call a method. Let’s move on:

1 0:000> !U 02ba0470
2 Normal JIT generated code
3 Calls.Program.Call(Calls.Functions)
4 Begin 02ba0470, size d
5  
6 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 17:
>>> 02ba0470 55              push    ebp
7 02ba0471 8bec            mov     ebp,esp
8 02ba0473 3909            cmp     dword ptr [ecx],ecx
9 02ba0475 ff15544db502    call    dword ptr ds:[2B54D54h] (Calls.Functions.NonVirtualIn
10 stance(), mdToken: 06000004)
 
11 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 18:
12 02ba047b 5d              pop     ebp
02ba047c c3              ret
13
14

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 ags 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 ags 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 ags 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 using System;
2 using System.Runtime.CompilerServices;
3  
4 namespace Calls
5 {
6     class Program
7     {
8         static void Main()
9         {
10             var instance = new Functions();
11             instance.ToString();
12         }
13     }
14  
15     public class Functions
16     {
17         [MethodImpl(MethodImplOptions.NoInlining)]
18         public override string ToString()
19         {
20             var message = "Virtual instance";
21             Console.WriteLine(message);
22             return message;
23         }
24     }
25 }

We will utilize ToString method, since it is virtual in System.Object class. IL for this code:

1 .method private hidebysig static


2 void Main () cil managed
3 {
4 // Method begins at RVA 0x2050
5 // Code size 12 (0xc)
6 .maxstack 8
7 .entrypoint
8  
9 IL_0000: newobj instance void Calls.Functions::.ctor()
10 IL_0005: callvirt instance string [mscorlib]System.Object::ToString()
IL_000a: pop
11 IL_000b: ret
12 } // end of method Program::Main
13

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 0:000> !U 02a30448
2 Normal JIT generated code
3 Calls.Program.Main()
4 Begin 02a30448, size 18
5 *** WARNING: Unable to verify checksum for Calls.exe
6  
7 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 10:
>>> 02a30448 55              push    ebp
8 02a30449 8bec            mov     ebp,esp
9 02a3044b b9504d2701      mov     ecx,1274D50h (MT: Calls.Functions)
10 02a30450 e89f2c83fe      call    012630f4 (JitHelp: CORINFO_HELP_NEWSFAST)
 
11 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 11:
02a30455 8bc8            mov     ecx,eax
12 02a30457 8b01            mov     eax,dword ptr [ecx]
13 02a30459 8b4028          mov     eax,dword ptr [eax+28h]
02a3045c ff10            call    dword ptr [eax]
14  
15 c:\users\user\documents\visual studio 2015\Projects\Calls\Calls\Program.cs @ 12:
16 02a3045e 5d              pop     ebp
17 02a3045f c3              ret
18
19

20
21

And we can indeed verify that there is a dispatch table used. These three lines are doing that:

1 02a30457 8b01            mov     eax,dword ptr [ecx]


2 02a30459 8b4028          mov     eax,dword ptr [eax+28h]
3 02a3045c ff10            call    dword ptr [eax]

We rst 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 using System;
2 using System.Runtime.CompilerServices;
3  
4 namespace Calls
5 {
6     class Program
7     {
8         static void Main()
9         {
10             dynamic instance = new Functions();
11             instance.ToString();
12         }
13     }
14  
15     public class Functions
16     {
17         [MethodImpl(MethodImplOptions.NoInlining)]
18         public override string ToString()
19         {
20             var message = "Virtual instance";
21             Console.WriteLine(message);
22             return message;
23         }
24     }
25 }

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 .method private hidebysig static


2 void Main () cil managed
3 {
4 // Method begins at RVA 0x2050
5 // Code size 87 (0x57)
6 .maxstack 9
7 .entrypoint
8 .locals init (
9 [0] object
10 )
11  
12 IL_0000: newobj instance void Calls.Functions::.ctor()
13 IL_0005: stloc.0
14 IL_0006: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<clas
s [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSit
e, object>> Calls.Program/'<>o__0'::'<>p__0'
IL_000b: brtrue.s IL_0041
15  
16 IL_000d: ldc.i4 256
17 IL_0012: ldstr "ToString"
18 IL_0017: ldnull
19 IL_0018: ldtoken Calls.Program
20 IL_001d: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle
21 (valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0022: ldc.i4.1
IL_0023: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInf
22 o
23 IL_0028: dup
IL_0029: ldc.i4.0
24 IL_002a: ldc.i4.0
25 IL_002b: ldnull
26 IL_002c: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumen
27 tInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valu
28 etype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string
)
IL_0031: stelem.ref
IL_0032: call class [System.Core]System.Runtime.CompilerServices.CallSiteBinder [M
29 icrosoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::InvokeMember(valuetype [Microso
30 ft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, string, class [mscorlib]Sy
stem.Collections.Generic.IEnumerable`1<class [mscorlib]System.Type>, class [mscorlib]S
ystem.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> cl
ass [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Act
ion`2<class [System.Core]System.Runtime.CompilerServices.CallSite, object>>::Create(cl
ass [System.Core]System.Runtime.CompilerServices.CallSiteBinder)
31 IL_003c: stsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<clas
s [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSit
e, object>> Calls.Program/'<>o__0'::'<>p__0'
 
IL_0041: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<clas
s [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSit
32
e, object>> Calls.Program/'<>o__0'::'<>p__0'
IL_0046: ldfld !0 class [System.Core]System.Runtime.CompilerServices.CallSite`1<cl
ass [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallS
ite, object>>::Target
33 IL_004b: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<clas
34 s [mscorlib]System.Action`2<class [System.Core]System.Runtime.CompilerServices.CallSit
e, object>> Calls.Program/'<>o__0'::'<>p__0'
IL_0050: ldloc.0
IL_0051: callvirt instance void class [mscorlib]System.Action`2<class [System.Core
35
]System.Runtime.CompilerServices.CallSite, object>::Invoke(!0, !1)
IL_0056: ret
} // end of method Program::Main
36

37
38

39
40

And indeed we can see, that calling dynamic method is much more dif cult. 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.

POSTED IN CODING

.N E T C#

‹ PREVIOUS NEXT ›
ILP Part 31 — Students Placement Problem Part 10 — Parallel Execution for Contests Part 1 — Powershell
Fixing plan
Search … Search

Recent Posts

ILP Part 85 — Lying riddle

SAT Part 3 — Reducing ILP to SAT

SAT Part 2 — Arithmetic

SAT Part 1 — Boolean logic

Four types of experience in software engineering

Categories

Administration

Coding

Computer Science

Databases

Debugging

Math

Archive

November 2021 (3)

October 2021 (5)

September 2021 (4)
August 2021 (4)

July 2021 (5)

June 2021 (4)

May 2021 (5)

April 2021 (4)

March 2021 (4)

February 2021 (4)

January 2021 (5)

December 2020 (4)

November 2020 (4)

October 2020 (5)

September 2020 (4)

August 2020 (5)

July 2020 (4)

June 2020 (4)

May 2020 (5)

April 2020 (4)

March 2020 (4)

February 2020 (5)

January 2020 (4)

December 2019 (4)

November 2019 (5)

October 2019 (4)

September 2019 (4)
August 2019 (5)

July 2019 (4)

June 2019 (5)

May 2019 (4)

April 2019 (4)

March 2019 (5)

February 2019 (5)

January 2019 (4)

December 2018 (5)

November 2018 (4)

October 2018 (4)

September 2018 (5)

August 2018 (4)

July 2018 (4)

June 2018 (5)

May 2018 (4)

April 2018 (4)

March 2018 (5)

February 2018 (4)

January 2018 (4)

December 2017 (5)

November 2017 (4)

October 2017 (4)

September 2017 (5)
August 2017 (4)

July 2017 (5)

June 2017 (4)

May 2017 (4)

April 2017 (5)

March 2017 (4)

February 2017 (4)

January 2017 (4)

December 2016 (5)

November 2016 (4)

October 2016 (5)

September 2016 (4)

August 2016 (4)

July 2016 (5)

June 2016 (4)

May 2016 (4)

April 2016 (5)

March 2016 (4)

February 2016 (4)

January 2016 (5)

December 2015 (4)

November 2015 (4)

October 2015 (5)

September 2015 (4)
August 2015 (3)

Posts

ILP Part 85 — Lying riddle November 20, 2021

SAT Part 3 — Reducing ILP to SAT November 13, 2021

SAT Part 2 — Arithmetic November 6, 2021

You might also like