Introduction
This article examines the JIT thunk layers that your code executes when a method is run for the first time, i.e., it needs just-in-time compilation or jitting, and is run with any subsequent invocations. I have included with this article a small C# WinForms application with names that whimsically recall Bilbo Baggin's adventure in the book, The Hobbit. The sample program is not a console application because we will want to step through the code a second time. For this exploration, I am using my own debugger, PEBrowse Interactive, which you can download from my website.
After you have built and compiled the sample program, start debugging it with PEBrowse Interactive by selecting File/Start Debugging. The program should stop executing and break with four child-windows that look something like the following:
My debugger stops on the first JITted method, or System.AppDomain::SetupDomain
. Expand Wilderland.exe and the .NET Methods node, and look for the Hobbiton_Button_Click
method, and set a breakpoint on the method by selecting View/Add Breakpoint. Then, let the debugger continue until the application appears in all its glory.
Press the button labeled "Lonely Mountain", and PEBrowse Interactive will present a disassembly window containing the x86 and IL for the method: Wilderland.WilderlandForm::Hobbiton_Button_Click
:
Disassembly of JITted Wilderland.WilderlandForm::Hobbiton_Button_Click
(06000006) at 0x071DF018:
> 0x71DF018: 55 PUSH EBP
0x71DF019: 8B EC MOV EBP,ESP
0x71DF01B: 83 EC 0C SUB ESP,0xC
0x71DF01E: 57 PUSH EDI
0x71DF01F: 56 PUSH ESI
0x71DF020: 68 80 7B ED 06 PUSH 0x6ED7B80
0x71DF025: E8 4A 25 E2 08 CALL 0x10001574
0x71DF02A: 89 55 F8 MOV DWORD PTR [EBP-0x8],EDX
0x71DF02D: 8B F9 MOV EDI,ECX
0x71DF02F: 33 F6 XOR ESI,ESI
0x71DF031: BE 31 97 00 00 MOV ESI,0x9731
0x71DF036: 8B D6 MOV EDX,ESI
0x71DF038: 8B CF MOV ECX,EDI
0x71DF03A: FF 15 68 81 ED 06 CALL DWORD PTR [0x6ED8168]
0x71DF040: 90 NOP
0x71DF041: EB 00 JMP 0x71DF043
0x71DF043: 68 80 7B ED 06 PUSH 0x6ED7B80
0x71DF048: E8 27 25 E2 08 CALL 0x10001574
0x71DF04D: 5E POP ESI
0x71DF04E: 5F POP EDI
0x71DF04F: 8B E5 MOV ESP,EBP
0x71DF051: 5D POP EBP
0x71DF052: C2 04 00 RET 0x4
The beginning of our journey through the JIT thunk layer will start at the call
statement looking something like "CALL DWORD PTR [0x6ED8168]
", so single step by pressing the F10 key until the debugger is positioned on this statement. Note that along the way, the value 0x9731, called the TheOneRing
in the source code, is moved into the ESI
and then the EDX
registers. Before continuing, select the option Tools/Configure/Memory, and change the Default Alignment to DWord
. Examine the destination of the call by pressing F4 and entering the address in the call
statement:
+0x06ED8168 06ED7B6B ..{k
Step into this call
statement by pressing F11.
Disassembly of THUNK at 0x06ED7B6B:
> 0x6ED7B6B: E8 A0 2C 27 F9 CALL 0x14A810
Let us step into this statement by pressing F11 again, and something like the following will be displayed in the disassembly window. Note: It is very important that you press F11 and not F10 because the code path will never return to the statement after the call
statement:
Disassembly of THUNK at 0x0014A810:
> 0x14A810: 52 PUSH EDX
0x14A811: 68 F0 30 1B 79 PUSH 0x791B30F0
0x14A816: 55 PUSH EBP
0x14A817: 53 PUSH EBX
0x14A818: 56 PUSH ESI
0x14A819: 57 PUSH EDI
0x14A81A: 8D 74 24 10 LEA ESI,DWORD PTR [ESP+0x10]
0x14A81E: 51 PUSH ECX
0x14A81F: 52 PUSH EDX
0x14A820: 64 8B 1D 2C 0E 00 00 MOV EBX,FS:[0xE2C]
0x14A827: 8B 7B 08 MOV EDI,DWORD PTR [EBX+0x8]
0x14A82A: 89 7E 04 MOV DWORD PTR [ESI+0x4],EDI
0x14A82D: 89 73 08 MOV DWORD PTR [EBX+0x8],ESI
0x14A830: 56 PUSH ESI
0x14A831: E8 14 C2 08 79 CALL 0x791D6A4A
0x14A836: 89 7B 08 MOV DWORD PTR [EBX+0x8],EDI
0x14A839: 89 46 04 MOV DWORD PTR [ESI+0x4],EAX
0x14A83C: 5A POP EDX
0x14A83D: 59 POP ECX
0x14A83E: 5F POP EDI
0x14A83F: 5E POP ESI
0x14A840: 5B POP EBX
0x14A841: 5D POP EBP
0x14A842: 83 C4 04 ADD ESP,0x4
0x14A845: 8F 04 24 POP DWORD PTR [ESP]
0x14A848: C3 RET
Now, single-step to the call
statement and examine the contents of ESP
by finding the Register Contents window and double-clicking on the ESP line:
ESP: 0x0012F2E4
+0x0012F2E4 0012F300 .... ESP
0x0012F2E8 00009731 ...1 -3C
+0x0012F2EC 04B71D10 .... -38
+0x0012F2F0 04B71D10 .... -34
0x0012F2F4 00009731 ...1 -30
+0x0012F2F8 0012F448 ...H -2C
+0x0012F2FC 0012F324 ...$ -28
+0x0012F300 791B30F0 y.0. -24 Ordinal79 + 0x30F0
+0x0012F304 0012F5B4 .... -20
+0x0012F308 06ED7B70 ..{p -1C
+0x0012F30C 071DF040 ...@ -18 Wilderland.WilderlandForm::Hobbiton_Button_Click
(06000006) + 0x0028
+0x0012F310 04B72E74 ...t -14
+0x0012F314 04B72FA4 ../. -10
+0x0012F318 0012F368 ...h -0C
+0x0012F31C 04B72E74 ...t -08
+0x0012F320 06ED7B7B ..{{ -04
+0x0012F324 0012F368 ...h EBP
*** Frame for 0x0014A831***
+0x0012F328 071DD4A2 .... RET System.Windows.Forms.Control::OnClick
(060005C4) + 0x0052
If you have paid attention to the execution of the disassembly, you will see that the contents of most of the registers have been pushed onto the stack as well as the return address from the initial call
statement, i.e., ESP-0x18
. We won't step into the call
statement even though this call actually invokes the JIT-compiler, because exploring and explaining what happens there is beyond the scope of this article. It is worthwhile to point out that the address of ESP-0x24
has been loaded into the ESI
register and that this is the only parameter passed into the compiler. Finally, our local variable, TheOneRing
, appears twice in the stack. Now, step over the call
statement by pressing F10 and reexamine the contents of ESP
:
ESP: 0x0012F2E8
0x0012F2E8 00009731 ...1 ESP
+0x0012F2EC 04B71D10 .... -38
+0x0012F2F0 04B71D10 .... -34
0x0012F2F4 00009731 ...1 -30
+0x0012F2F8 0012F448 ...H -2C
+0x0012F2FC 0012F324 ...$ -28
+0x0012F300 791B30F0 y.0. -24 Ordinal79 + 0x30F0
+0x0012F304 0012F5B4 .... -20
+0x0012F308 06ED7B70 ..{p -1C
+0x0012F30C 071DF040 ...@ -18 Wilderland.WilderlandForm::Hobbiton_Button_Click
(06000006) + 0x0028
+0x0012F310 04B72E74 ...t -14
+0x0012F314 04B72FA4 ../. -10
+0x0012F318 0012F368 ...h -0C
+0x0012F31C 04B72E74 ...t -08
+0x0012F320 06ED7B7B ..{{ -04
+0x0012F324 0012F368 ...h EBP
*** Frame for 0x0014A836***
+0x0012F328 071DD4A2 .... RET System.Windows.Forms.Control::OnClick
(060005C4) + 0x0052
If you carefully compare the contents before with the contents after, you will find no change in the stack values! What is going on here? In order to answer this question, we will continue single-stepping until we reach the statement, POP DWORD PTR [ESP]
, and then examine what will be popped off the stack. The more astute of you may have seen that while we were single-stepping, one of the DWORD
values was altered from:
+0x0012F304 0012F5B4 to 06ED7B6B
by the second move
statement after the call. Single-stepping one more time will make this address now the target of the return
statement! Is this the end of our journey? No! Press F11 at the return
statement and you will see something like the following:
Disassembly of THUNK at 0x06ED7B6B
> 0x6ED7B6B: E8 28 A9 2C 00 CALL 0x71A2498
Furthermore, this address should be somewhat familiar since we saw it as the target of the call
statement back in the disassembly for Wilderland.WilderlandForm::Hobbiton_Button_Click
. The call
statement has changed!
Press F11 again:
Disassembly of THUNK at 0x071A2498:
+ 0x71A2498: 85 C9 TEST ECX,ECX
0x71A249A: 74 13 JZ 0x71A24AF
0x71A249C: 8B 01 MOV EAX,DWORD PTR [ECX]
0x71A249E: 3D 0C 00 F6 7F CMP EAX,0x7FF6000C
0x71A24A3: 75 0A JNZ 0x71A24AF
0x71A24A5: 8B 41 08 MOV EAX,DWORD PTR [ECX+0x8]
0x71A24A8: FF 51 14 CALL DWORD PTR [ECX+0x14]
0x71A24AB: 85 C0 TEST EAX,EAX
0x71A24AD: 75 06 JNZ 0x71A24B5
0x71A24AF: 58 POP EAX
0x71A24B0: E9 B3 CB 03 00 JMP 0x71DF068
0x71A24B5: E9 66 59 FC F8 JMP 0x167E20
We are in another thunk! After single-stepping and carefully noting the contents of the ECX
and EAX
registers, you will see that we will hit the statement, JNZ 0x71A24AF
. Single-stepping two more times will yield the following disassembly:
Disassembly of JITted Wilderland.WilderlandForm::LonelyMountain
(06000005) at 0x071DF068
> 0x71DF068: 55 PUSH EBP
0x71DF069: 8B EC MOV EBP,ESP
0x71DF06B: 83 EC 08 SUB ESP,0x8
0x71DF06E: 56 PUSH ESI
0x71DF06F: 68 70 7B ED 06 PUSH 0x6ED7B70
0x71DF074: E8 FB 24 E2 08 CALL 0x10001574
0x71DF079: 89 55 F8 MOV DWORD PTR [EBP-0x8],EDX
0x71DF07C: 8B F1 MOV ESI,ECX
0x71DF07E: 8B 8E DC 00 00 00 MOV ECX,DWORD PTR [ESI+0xDC]
0x71DF084: 8B 15 B8 16 B7 05 MOV EDX,DWORD PTR [0x5B716B8]
0x71DF08A: 8B 01 MOV EAX,DWORD PTR [ECX]
0x71DF08C: FF 90 E8 00 00 00 CALL DWORD PTR [EAX+0xE8]
0x71DF092: 90 NOP
0x71DF093: EB 00 JMP 0x71DF095
0x71DF095: 68 70 7B ED 06 PUSH 0x6ED7B70
0x71DF09A: E8 D5 24 E2 08 CALL 0x10001574
0x71DF09F: 5E POP ESI
0x71DF0A0: 8B E5 MOV ESP,EBP
0x71DF0A2: 5D POP EBP
0x71DF0A3: C3 RET
We are finally at our destination, Wilderland.WilderlandForm::LonelyMountain
, which has just been JITted. You can see this for yourself by selecting View/JIT Events in PEBrowse Interactive and examining the last entry in the list. Step until you reach the return
statement, and execute one more statement by pressing F10 or F11. The disassembly for Wilderland.WilderlandForm::Hobbiton_Button_Click
is again displayed but positioned now after the first call
statement we entered. Just like Bilbo in his adventure, we have been "there and back again". Letting the debugger continue at this point will demonstrate that the caption for the button is now changed to "Bilbo Lives!".
What happens if we wish to repeat the journey and press the "Bilbo Lives!" button again? The debugger stops execution once again at the beginning of the Wilderland.WilderlandForm::Hobbiton_Button_Click
method. Stepping until the call
statement we first examined a while back, we will discover that the target of the call will be:
Disassembly of THUNK at 0x06ED7B6B:
> 0x6ED7B6B: E8 28 A9 2C 00 CALL 0x71A2498
which is the same statement we saw above after returning from the JIT compiler call. Stepping into this call and then single-stepping until we hit again the method, Wilderland.WilderlandForm::LonelyMountain
, will prove that the code is not JITted again, but that we still will pass through one of the thunks we saw earlier. So, just like Bilbo's continued possession of the One Ring affected him throughout his long life, our code continues to pass through one of the JIT compiler thunks.
Conclusion
Hopefully, this examination of the thunks your code passes through will inspire you to further investigate the mechanics of code being generated on the fly in the .NET environment. My explanation just briefly touched on the single parameter, TheOneRing
, but did not highlight its lifetime on the stack. Also, we did not venture into the JIT compiler itself -- the more adventurous of you might want to follow this path but I will warn you: It is a journey through Mirkwood and there are black spiders lurking!