Click here to Skip to main content
Click here to Skip to main content

There and Back Again or Stepping Through the JIT Thunk Layer

, 10 Feb 2005
Rate this:
Please Sign up or sign in to vote.
This article examines the JIT thunk layers that your code executes when a method is run for the first time.

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:

Click to see large image

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:

  ; Stack Size (in BYTES): 24 (0x00000018)
  ; Number of Parameters: 1
  ; Local Variables Size (in BYTES): 12 (0x0000000C)
  ; Prologue Size (in BYTES): 23 (0x17)
  ; Standard Frame
> 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; VAR:0x8
  0x71DF02D: 8B F9                  MOV      EDI,ECX
  ; end of prologue
  0x71DF02F: 33 F6                  XOR      ESI,ESI
  ; IL_0000: ldc.i4 0x00009731
  ; IL_0005: stloc.0
  0x71DF031: BE 31 97 00 00         MOV      ESI,0x9731
  ; IL_0006: ldarg.0
  ; IL_0007: ldloc.0
  ; IL_0008: call  Wilderland.WilderlandForm::LonelyMountain()
  0x71DF036: 8B D6                  MOV      EDX,ESI
  0x71DF038: 8B CF                  MOV      ECX,EDI
  0x71DF03A: FF 15 68 81 ED 06      CALL     DWORD PTR [0x6ED8168] ;<=========
  ; IL_000D: ret
  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            ; (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       ; (0x71A24AF); (*+0x15)
  0x71A249C: 8B 01            MOV      EAX,DWORD PTR [ECX]
  0x71A249E: 3D 0C 00 F6 7F   CMP      EAX,0x7FF6000C  
  0x71A24A3: 75 0A            JNZ      0x71A24AF       ; (0x71A24AF); (*+0xC)
  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       ; (0x71A24B5); (*+0x8)
  0x71A24AF: 58               POP      EAX  ; <==0x071A24A3(*-0xC), 0x071A249A(*-0x15)
  0x71A24B0: E9 B3 CB 03 00   JMP      0x71DF068
  0x71A24B5: E9 66 59 FC F8   JMP      0x167E20  ; <==0x071A24AD(*-0x8)

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

  ; Stack Size (in BYTES): 16 (0x00000010)
  ; Number of Parameters: 0
  ; Local Variables Size (in BYTES): 8 (0x00000008)
  ; Prologue Size (in BYTES): 22 (0x16)
  ; Standard Frame
> 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; VAR:0x8
  0x71DF07C: 8B F1                  MOV      ESI,ECX
  ; end of prologue
  ; IL_0000: ldarg.0
  ; IL_0001: ldfld Hobbiton_Button
  ; IL_0006: ldstr "Bilbo Lives!"
  ; IL_000B: callvirt  System.Windows.Forms.Control::set_Text()
  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]
  ; IL_0010: ret
  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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Russ Osterlund

United States United States
My name is Russell Osterlund and I live in Merrimack, NH. I was an independent consultant and software developer but am now retired. I can be reached at: RussellOsterlund@comcast.net or via my website: www.smidgeonsoft.com.

Comments and Discussions

 
GeneralExcellent Article PinmemberRüdiger Klaehn13-Feb-05 5:39 
GeneralGood work Russ PinstaffNishant S11-Feb-05 21:31 
GeneralExcellent article Russ! PinmemberJohn Lyon-Smith11-Feb-05 5:03 
QuestionDid you write your own debugger? PinprotectorMarc Clifton11-Feb-05 1:35 
AnswerRe: Did you write your own debugger? PinsussAnonymous11-Feb-05 2:08 
This is a native debugger written using the Win32 APIs, so it can be used with native Win32 applications as well. It works very well in mixed mode, i.e., managed and native code, as well. It employs a .NET profiler using the ICorProfilerInfo interface to watch for JIT events and decode the metadata.
GeneralRe: Did you write your own debugger? PinmemberArtemis14-Feb-05 20:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140821.2 | Last Updated 11 Feb 2005
Article Copyright 2005 by Russ Osterlund
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid