往返或逐步进行 JIT Thunk 层






4.88/5 (19投票s)
2005年2月11日
5分钟阅读

61311

398
本文考察了代码首次运行时所执行的 JIT 갔层。
引言
本文考察了代码首次运行时所执行的 JIT 갔层,也就是说,它需要即时编译(jitting),并且在后续调用时运行。我在这篇文章中包含了一个小的 C# WinForms 应用程序,其名称奇特地让人联想到比尔博·巴金斯在《霍比特人》一书中的冒险。示例程序不是控制台应用程序,因为我们将需要第二次单步执行代码。为了进行这次探索,我使用的是我自己的调试器 PEBrowse Interactive,您可以从我的网站下载。
在构建和编译了示例程序之后,使用 PEBrowse Interactive 启动调试,方法是选择“文件/启动调试”。程序应该停止执行并中断,出现四个子窗口,看起来大致如下:
我的调试器停在第一个 JIT 编译的方法上,即 System.AppDomain::SetupDomain
。展开“Wilderland.exe”和“.NET Methods”节点,找到 Hobbiton_Button_Click
方法,并通过选择“视图/添加断点”在其上设置一个断点。然后,让调试器继续运行,直到应用程序以其全部光彩出现。
按下标有“Lonely Mountain”的按钮,PEBrowse Interactive 将会显示一个反汇编窗口,其中包含该方法的 x86 和 IL 代码:Wilderland.WilderlandForm::Hobbiton_Button_Click
JIT 编译的 Wilderland.WilderlandForm::Hobbiton_Button_Click
(06000006) 在 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
我们穿越 JIT 갔层的旅程将从一个看起来像“CALL DWORD PTR [0x6ED8168]
”的 call
语句开始,所以请按 **F10** 键进行**单步执行**,直到调试器定位到这条语句。请注意,在此过程中,值 0x9731(在源代码中称为 TheOneRing
)被移入 ESI
寄存器,然后是 EDX
寄存器。在继续之前,选择“工具/配置/内存”选项,并将“默认对齐”更改为 DWord
。通过按 **F4** 并输入 call
语句中的地址来检查调用的目标地址。
+0x06ED8168 06ED7B6B ..{k
按 **F11** 键**进入**这个 call
语句。
0x06ED7B6B 处的 THUNK 反汇编
>; 0x6ED7B6B: E8 A0 2C 27 F9 CALL 0x14A810
再次按 **F11** 键进入这条语句,反汇编窗口将显示类似以下内容。注意:按 **F11** 而不是 **F10** 至关重要,因为代码路径将**永远不会**返回到 call
语句之后的语句。
0x0014A810 处的 THUNK 反汇编
>; 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
现在,单步执行到 call
语句,并通过查找“寄存器内容”窗口并双击 ESP 行来检查 ESP
的内容。
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
如果您留意了反汇编的执行过程,您会发现大多数寄存器的内容以及初始 call
语句的返回地址(即 ESP-0x18
)都被推到了堆栈上。我们不会单步进入那个 call
语句,尽管这个调用实际上触发了 JIT 编译器,因为探索和解释那里发生的事情超出了本文的范围。值得指出的是,ESP-0x24
的地址已加载到 ESI
寄存器中,并且这是传递给编译器的唯一参数。最后,我们的局部变量 TheOneRing
出现在堆栈两次。现在,按 **F10** 键单步跳过 call
语句,并重新检查 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
如果您仔细比较前后内容,您会发现堆栈值**没有任何变化**!这是怎么回事?为了回答这个问题,我们将继续单步执行,直到到达 POP DWORD PTR [ESP]
语句,然后检查将从堆栈中弹出什么。对于那些更敏锐的人来说,您可能会注意到,在我们单步执行的过程中,一个 DWORD
值已经从
+0x0012F304 0012F5B4 to 06ED7B6B
被调用后的第二个 move
语句改变了。再单步执行一次,这个地址将成为 return
语句的目标!我们的旅程到此结束了吗?不!在 return
语句处按 **F11**,您将看到类似以下内容:
0x06ED7B6B 处的 THUNK 反汇编
>; 0x6ED7B6B: E8 28 A9 2C 00 CALL 0x71A2498
此外,这个地址应该有些熟悉,因为我们在 Wilderland.WilderlandForm::Hobbiton_Button_Click
的反汇编中看到过它作为 call
语句的目标。现在 call
语句已经改变了!
再次按 **F11** 键。
0x071A2498 处的 THUNK 反汇编
+ 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)
我们又进入了一个 갔!单步执行并仔细记录 ECX
和 EAX
寄存器的内容后,您将看到我们命中 JNZ 0x71A24AF
语句。再单步执行两次将得到以下反汇编:
JIT 编译的 Wilderland.WilderlandForm::LonelyMountain
(06000005) 在 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
我们终于到达了目的地 Wilderland.WilderlandForm::LonelyMountain
,它刚刚被 JIT 编译。您可以通过选择 PEBrowse Interactive 中的“视图/JIT 事件”并检查列表中的最后一项来亲眼看到这一点。单步执行直到到达 return
语句,再执行一个语句(按 **F10** 或 **F11**)。Wilderland.WilderlandForm::Hobbiton_Button_Click
的反汇编再次显示,但现在定位在我们最初进入的第一个 call
语句之后。就像比尔博在他的冒险中一样,我们已经“往返了一次”。此时让调试器继续运行将演示按钮的标题现在已更改为“Bilbo Lives!”。
如果我们想重复这次旅程并再次按下“Bilbo Lives!”按钮,会发生什么?调试器再次在 Wilderland.WilderlandForm::Hobbiton_Button_Click
方法的开头停止执行。单步执行到我们之前检查过的 call
语句,我们将发现该调用的目标将是
0x06ED7B6B 处的 THUNK 反汇编
>; 0x6ED7B6B: E8 28 A9 2C 00 CALL 0x71A2498
这是我们在从 JIT 编译器调用返回后在上面看到的同一条语句。单步进入此调用,然后单步执行直到再次命中 Wilderland.WilderlandForm::LonelyMountain
方法,这将证明代码不会再次 JIT 编译,但我们仍然会经过我们之前看到的某个 갔。所以,就像比尔博拥有的魔戒一直影响他一样,我们的代码继续通过一个 JIT 编译器 갔。
结论
希望这次对代码经过的 갔的考察能激发您进一步探究 .NET 环境中动态生成代码的机制。我的解释只是简要地提到了单个参数 TheOneRing
,但没有强调它在堆栈上的生命周期。此外,我们也没有深入 JIT 编译器本身——对于那些更大胆的人来说,您可能想跟随这条路径,但我会警告您:这是一条穿越幽暗密林(Mirkwood)的旅程,那里潜伏着黑蜘蛛!