65.9K
CodeProject 正在变化。 阅读更多。
Home

往返或逐步进行 JIT Thunk 层

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (19投票s)

2005年2月11日

5分钟阅读

viewsIcon

61311

downloadIcon

398

本文考察了代码首次运行时所执行的 JIT 갔层。

引言

本文考察了代码首次运行时所执行的 JIT 갔层,也就是说,它需要即时编译(jitting),并且在后续调用时运行。我在这篇文章中包含了一个小的 C# WinForms 应用程序,其名称奇特地让人联想到比尔博·巴金斯在《霍比特人》一书中的冒险。示例程序不是控制台应用程序,因为我们将需要第二次单步执行代码。为了进行这次探索,我使用的是我自己的调试器 PEBrowse Interactive,您可以从我的网站下载。

在构建和编译了示例程序之后,使用 PEBrowse Interactive 启动调试,方法是选择“文件/启动调试”。程序应该停止执行并中断,出现四个子窗口,看起来大致如下:

Click to see large image

我的调试器停在第一个 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)

我们又进入了一个 갔!单步执行并仔细记录 ECXEAX 寄存器的内容后,您将看到我们命中 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)的旅程,那里潜伏着黑蜘蛛!

© . All rights reserved.