Inside Out Part 1 - Virtual and Non-Virtual Calls in C# - Random IT Utensils
Inside Out Part 1 - Virtual and Non-Virtual Calls in C# - Random IT Utensils
Menu
”
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:
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:
We can see that we indeed call the method using call opcode. Let’s now execute the app and
see the generated machine code:
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.
We have symbols loaded. Let’s nd machine code for Main function. We can do it for instance
by nding assemblies:
36
37
38
39
40
41
We can see that we have two interesting method tables. Let’s dump the one for Program class:
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.
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:
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 }
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:
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.
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:
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
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:
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:
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
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