In the next few “under the covers” posts I am going to put up a bit about something that most people take for granted, how does that method call actually work (note in this article we are looking at a very basic IL "call"? While it seems mundane, this is actually very important information as it can greatly affect code performance and offer you a greater understanding of how various items work in the CLR. Let’s start by putting up a small bit of test code.
|
class Foo {
public void Test() {
for (int i = 0; i < 10; i++) {
Console.WriteLine("Test");
}
}
}
class Program {
static void Main(string[] args) {
Foo f = new Foo();
f.Test();
f.Test();
f.Test();
}
} |
|
Listing 1: A simple example for discussion |
The JIT must deal with some huge problems just to run a simple bit of code like this. Let’s step into the process to get a better idea of what happens. Just before control is given to our code the Main method is JIT compiled, control is then passed to the main method to begin execution. We can see the disassembly for this code in Listing 2.
|
static void Main(string[] args) {
Foo f = new Foo();
00000000 push esi
00000001 mov ecx,913080h
00000006 call FFB21FAC
0000000b mov esi,eax
f.Test();
0000000d mov ecx,esi
0000000f cmp dword ptr [ecx],ecx
00000011 call dword ptr ds:[009130B8h]
f.Test();
00000017 mov ecx,esi
00000019 cmp dword ptr [ecx],ecx
0000001b call dword ptr ds:[009130B8h]
f.Test();
00000021 mov ecx,esi
00000023 cmp dword ptr [ecx],ecx
00000025 call dword ptr ds:[009130B8h]
0000002b pop esi
}
0000002c ret |
|
Listing 2: Disassembly of the main method |
From looking at the disassembled code we can see that the generated code is dereferencing a pointer in order to make the call. There are a few reasons for this but before we get into them let’s look a bit at how this works. It’s time to break out our trusty debugger to see what’s going on. If you have not yet read it, now would be a good time to go back and read my first post here at CodeBetter Viewing Unmanaged Code in VS.NET as you need this and SOS (Son Of Strike) setup in order to follow along.
I will put all debugger commands in bold fixed width and the output in normal fixed width.
Put a break point on the first line of code and start debugging. All commands for SOS should be typed in your immediate window.
.load SOS
extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\SOS.dll loaded
This first command loads the SOS debugging tool making it available for our use.
!Name2EE ConsoleApplication29.exe ConsoleApplication29.Foo.Test
PDB symbol for mscorwks.dll not loaded
Module: 00912c14 (ConsoleApplication29.exe)
Token: 0x06000001
MethodDesc: 00913070
Name: ConsoleApplication29.Foo.Test()
Not JITTED yet. Use !bpmd -md 00913070 to break on run.
This command gives us more information on our method. There is a lot of useful information here but the most important bit of informatio is the MethodDesc address. We can use this to find out more information about our method.
!DumpMD 00913070
Method Name: ConsoleApplication29.Foo.Test()
Class: 009113b8
MethodTable: 00913080
mdToken: 06000001
Module: 00912c14
IsJitted: no
m_CodeOrIL: ffffffffffffffff
Here we can get our method table, we can examine our full method table using this address.
!DumpMT -md 00913080
EEClass: 009113b8
Module: 00912c14
Name: ConsoleApplication29.Foo
mdToken: 02000002 (C:\Documents and Settings\Greg\My Documents\Visual Studio 2005\Projects\ConsoleApplication29\ConsoleApplication29\bin\Release\ConsoleApplication29.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
79354bec 7913bd48 PreJIT System.Object.ToString()
793539c0 7913bd50 PreJIT System.Object.Equals(System.Object)
793539b0 7913bd68 PreJIT System.Object.GetHashCode()
7934a4c0 7913bd70 PreJIT System.Object.Finalize()
009130c8 00913070 NONE ConsoleApplication29.Foo.Test()
009130d4 00913078 NONE ConsoleApplication29.Foo..ctor()
As I am sure many people have noticed by now (it has been in every one of these listings J), our method has not yet been JIT’ed. This is one of the reasons why we dereference in order to make the call above, at the time that the main method was compiled it had no idea where it should call. This leads us to an interesting question.
How does the JIT know when to compile a method?
Essentially the JIT is lazy loading our compilation for us… It uses a concept known as a “thunk” in order to catch the first time we try to call into our method. The thunk is a little piece of unmanaged code that is emitted by the CLR when it first loads the type. The thunk as we will see shortly contains code to call the JIT or code to call the method.
|

|
|
Figure 1 : Process of JIT’ing a method |
The process as shown in Figure 1 is almost deceptively simple; in practice this would be far too slow to create functional systems with. The difference between this drawing and how it actually works has to deal with the decision. The picture would lead you to believe that the thunk has branching code; it does not. Instead the JIT uses a concept known as back patching.
The term back patching may be familiar to you as it is also used in garbage collection, it means to go back and update a pointer to reflect new information. When the method is called for the first time it reads from the method table an address that points to our thunk and calls it. Our thunk forwards us off to the JIT. The key is that when the JIT has completed we go back and change our method table to now directly point to the JIT’ed code (we update the table). Figure 2 & Figure 3 illustrate a before and after view of this process. Note that the call moves directly in the pictures while in reality they are actually reading a memory address that changes.
Now that we have a high level view of what happens, let’s verify it by watching it in the debugger. You can use either your memory window (debug->windows->memory and enter the memory address of your call (i.e. 009130B8h from the listing)) or you can use the registers window (debug->windows->registers (make sure effective address is enabled)) to view the data we need.
Showing stuff like this to your friends in the debugger is also good for asserting your role as alpha nerd in the office.
Using the debugger step to line 0011 (the first call). We can see in either location that the memory address 009130B8 (the address we are dereferencing for our call) contains 009130C8 just before we make our call. This memory address should look familiar as it was in our method table listing earlier as the entry for our Test method. This is pointing to our thunk, we can even look at our thunk as unmanaged code by disassembling that address with the !u command.
!u 009130C8
Unmanaged code
009130C8 B870309100 mov eax,913070h
009130CD 89ED mov ebp,ebp
009130CF E938EEA2FF jmp 00341F0C
009130D4 B878309100 mov eax,913078h
009130D9 89ED mov ebp,ebp
009130DB E92CEEA2FF jmp 00341F0C
009130E0 0000 add byte ptr [eax],al
009130E2 0000 add byte ptr [eax],al
009130E4 0000 add byte ptr [eax],al
009130E6 0000 add byte ptr [eax],al
This listing is a bit confusing because it is actually two thunks followed by nothing. So our call that we were looking at is set to jump to the first instruction here. That 913070 being loaded should also look familiar it is the address of our method desc, it is being pushed onto EAX to be passed as a parameter to the JIT compiler (so the JIT compiler knows what to compile!). Let’s set a break point in our other method hit f5 and take a look at our environment after to see what has changed.
!DumpMT -md 00913080
EEClass: 009113b8
Module: 00912c14
Name: ConsoleApplication29.Foo
mdToken: 02000002 (C:\Documents and Settings\Greg\My Documents\Visual Studio 2005\Projects\ConsoleApplication29\ConsoleApplication29\bin\Release\ConsoleApplication29.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
79354bec 7913bd48 PreJIT System.Object.ToString()
793539c0 7913bd50 PreJIT System.Object.Equals(System.Object)
793539b0 7913bd68 PreJIT System.Object.GetHashCode()
7934a4c0 7913bd70 PreJIT System.Object.Finalize()
00de00b0 00913070 JIT ConsoleApplication29.Foo.Test()
009130d4 00913078 NONE ConsoleApplication29.Foo..ctor()
We can see here that our method has now been JIT’ed (as a normal JIT) and that our method table entry has been updated to reflect a new location (00de00b0). This is the location that our unmanaged code has been created by the JIT. We can verify this by looking at our current stack as we are currently in the method.
!CLRStack
OS Thread Id: 0x8e8 (2280)
ESP EIP
0012f47c 00de00b0 ConsoleApplication29.Foo.Test()
0012f480 00de0087 ConsoleApplication29.Program.Main(System.String[])
0012f69c 79e88f63 [GCFrame: 0012f69c]
Let’s step out of this method and back out to our main method. Our code is again setting up the same call, by stepping to line 1b and re-checking our pointer we can see that our code will now call the newly generated unmanaged code directly as opposed to passing through the thunk.
This will all stay just like this, unless our code is pitched which will be the next discussion!
