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

来来回回 - 重奏

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2012年4月11日

CPOL

9分钟阅读

viewsIcon

14051

downloadIcon

115

这是我多年前撰写的文章《去而复返,或步入 JIT 穿梭层》的全新版本,但这次它将审视 64 位进程和 Microsoft .NET 运行时 V4.0。

引言

几年前,我写了一篇题为《去而复返,或步入 JIT 穿梭层》(https://codeproject.org.cn/Articles/9557/There-and-Back-Again-or-Stepping-Through-the-JIT-T)的小文章,简要介绍了 Microsoft .NET V1.1 即时 (JIT) 编译和 32 位进程的穿梭层。现在是时候更新这个故事了,但这次我们将针对 64 位技术和更高版本的 .NET 运行时。为了这次重访,我使用了 Visual Studio 11 Beta,在安装了 Windows Ultimate 7 的虚拟机中运行,并使用了我自己的 64 位调试器 PEBrowseDbg64 Interactive Professional(http://www.smidgeonsoft.prohosting.com/pebrowse-pro-interactive-debugger.html)。注意:如果您还没有获取 VS11 Beta 版本并安装,早期版本的 Visual Studio 配合 64 位技术应该同样适用,但以下叙述可能会略有差异。

创建测试程序

打开 Visual Studio,创建一个名为 Wilderland 的 C# Windows 窗体应用程序(这让人想起我早些文章中引用的《霍比特人》一书http://en.wikipedia.org/wiki/The_Hobbit),并以此向 J. R. R. 托尔金和即将上映的电影致敬)。将窗体的 Text 属性更改为“Wilderland”,然后添加一个名为 LonelyMountain_button 的按钮,其 Text 属性为“Lonely Mountain”。在按钮的 Click 事件中,添加以下代码片段

int TheOneRing = 0x9731;
LonelyMountain(TheOneRing);

添加私有方法 LonelyMountain

private void LonelyMountain(int MagicRing)
{
    LonelyMountain_button.Text = "Bilbo Lives!";
}

最后,在窗体的 FormClosing 事件中添加

MessageBox.Show("The adventure is ending!", "Farewell");

请确保您将生成目标设置为 64 位,即 Active solution platform 为“x64”,而不是“Any CPU”,并编译 Debug 和 Release 版本的程序(我们将在旅途中使用 Release 版本)。运行程序后,您应该会看到类似于下图的画面

下载并安装 PEBrowseDbg64 后,我们现在就可以开始我们的冒险了。

设置环境

理解本文解释的绝对首要条件是能够访问正确的调试符号。作为 PEBrowseDbg64 安装的一部分,您会找到一个名为 _NT_SYMBOL_PATH.txt 的小文件。如果您尚未设置,请创建一个名为该文件名(不含文件扩展名)的环境变量,并将其值设置为该文件的内容。做好这个小准备后,我们可以继续。启动 PEBrowseDbg64,您会看到这个画面

对于使用过我旧的 32 位调试器 PEBrowse Interactive Professional 的各位来说,这个窗口应该很熟悉。

开始旅程

如果您使用的是 VS11 Beta,则需要检查 Tools/VS11 Beta 菜单项。然后,从菜单中选择 File/Start Program(或 Ctl-S),并在出现的对话框中按如下方式填写

(Wilderland 程序可能位于您系统中的其他目录。)按 Start 按钮,现在您应该会看到

调试器在这里暂停了您的程序,因为选中了 _Debug loader startup code?_ 复选框。如果您曾经好奇过 Windows 程序是如何启动的,也就是说,在您的程序的 main 例程被调用之前,您可以在这里开始单步执行(F11),并进入 _LdrpInitialize_ 的调用。目前,按 F5,或 Debug/Go,下一个“正常”断点将在 NTDLL 例程 _LdrpDoDebuggerBreak_ 中触发。这是您使用其他调试器(例如 WinDbg)时遇到的相同初始断点。再次按 F5 将会得到(您可能会看到一个可以安全忽略的中间异常)

正如例程的名称所示,.NET 运行时正在构建您的托管代码运行所在的 AppDomain 环境。我们有机会调查这一点,是因为我们在 _Debug Session Start_ 对话框中选择了 _Break on 1st JIT event?_ 复选框。可以通过在左侧的树状视图索引中找到 _MSCORLIB.NI.DLL_ 中的 _Intermediate Language_ (IL) 例程,并显示 .NET 方法来访问该例程的 IL - 稍后会详细介绍。

再次按 F5 将会显示

树状视图中的一些节点现在显示为红色,表示新分配或已更改的内存。现在,展开 _WILDERLAND.EXE_ 节点将获得对文件内部的浏览访问权限,以及 .NET 方法和元数据、导出(如果存在)和调试符号(如果可用)。导航到 _.NET Methods_ 节点,然后选择菜单项 _View/Display.NET Methods_,并展开 _Wilderland::Form1_ 节点。新窗口应该看起来像这样

选择 _LonelyMountain_button_click_ 项,然后选择菜单项 _Edit/Add Breakpoint at 06000003_,这恰好是按钮单击事件背后的 _MethodDef_,即 _LonelyMountain_。此活动对应于在 Visual Studio 等开发环境中,在托管代码内部设置断点的正常机会。但是,如果您选择菜单项 _View/Debug Windows/.NET JIT Events_

您会发现您的程序已经发生了大量活动。这当然就是本文前面提到的 AppDomain 设置过程。在这些断点中的任何一个上设置断点并重新启动调试会话,您就有机会探索更多的启动活动。

进入墨林或 JIT-穿梭层

设置单击事件的断点后,让程序继续运行,直到您看到 Wilderland 窗口,然后按 Lonely Mountain 按钮。调试器现在将停止您的程序并显示

(我正在为 Wilderland 程序的 Release 版本截图 - 如果您使用的是 Debug 版本,可能会看到略有不同的画面。)如果您准备进入神秘的墨林,并愿意跟随神奇的路径,请按 _F10_ 直到您到达跳转语句

JMP EAX

请注意,您的局部变量 _TheOneRing_ 存在于 _RDX_ 寄存器中。另外,选择 Register Contents 窗口,然后选择 _Edit/Watch Contents of RAX_

按 _F10_ 一次,您应该会看到

PID: 0x0E10 Disassembly for THUNK at 0x000007FE8CC8C040
  ;********************************************************************************
 000007FE8CC8C040: CALL    PrecodeFixupThunk ; (0x000007FEEC442110)
 000007FE8CC8C045: POP     RSI
 000007FE8CC8C046: ADD     AL,BYTE PTR [RCX+RBP*8]
 000007FE8CC8C049: XOR     ESP,DWORD PTR [RDX+0x045F0018]
 000007FE8CC8C04F: ADD     EBP,EAX
 000007FE8CC8C051: MOV     EBX,0x5E5F7B60
...

注意对 _PrecodeFixupThunk_ 的调用。请按 _F11_,即“进入调用”,而不是 _F10_,“跳过”。如果您不小心按了 _F10_,程序将继续运行而没有进一步交互的机会,就像墨林中河里的危险一样,您的调试会话将永远无法唤醒(因为接下来的 _POP RSI_ 指令将不会被执行)。

如果您像 13 个矮人与霍比特人一样,小心地使用了 _F11_ 之舟,那么接下来的将是您的奖励

PID: 0x0E10 Disassembly for clr.dll!PrecodeFixupThunk (0x000007FEEC442110)
  ;********************************************************************************
  ; PrecodeFixupThunk (0x000007FEEC442110)
  ;    Debug Symbol:  PrecodeFixupThunk
 000007FEEC442110: POP     RAX
 000007FEEC442111: MOVZX   R10,BYTE PTR [RAX+0x02]
 000007FEEC442116: MOVZX   R11,BYTE PTR [RAX+0x01]
 000007FEEC44211B: MOV     RAX,QWORD PTR [RAX+R10*8+0x03]
 000007FEEC442120: LEA     R10,QWORD PTR [RAX+R11*8]
 000007FEEC442124: JMP     0x000007FEEC442450
 ;
 ; ThePreStub
 000007FEEC442450: LEA     RAX,QWORD PTR [RSP+0x08]
 000007FEEC442455: PUSH    R10
 000007FEEC442457: PUSH    R15
 000007FEEC442459: PUSH    R14
 000007FEEC44245B: PUSH    R13
 000007FEEC44245D: PUSH    R12
 000007FEEC44245F: PUSH    RBP
 000007FEEC442460: PUSH    RBX
 000007FEEC442461: PUSH    RSI
 000007FEEC442462: PUSH    RDI
 000007FEEC442463: PUSH    RAX
 000007FEEC442464: SUB     RSP,0x78
 000007FEEC442468: MOV     QWORD PTR [RSP+0x000000D0],RCX
 000007FEEC442470: MOV     QWORD PTR [RSP+0x000000D8],RDX
 000007FEEC442478: MOV     QWORD PTR [RSP+0x000000E0],R8
 000007FEEC442480: MOV     QWORD PTR [RSP+0x000000E8],R9
 000007FEEC442488: MOVQ    XMMWORD PTR [RSP+0x20],XMM0
 000007FEEC44248E: MOVQ    XMMWORD PTR [RSP+0x30],XMM1
 000007FEEC442494: MOVQ    XMMWORD PTR [RSP+0x40],XMM2
 000007FEEC44249A: MOVQ    XMMWORD PTR [RSP+0x50],XMM3
 000007FEEC4424A0: LEA     RCX,QWORD PTR [RSP+0x68]
 000007FEEC4424A5: CALL    clr.dll!PreStubWorker ; (0x000007FEEC4BE1B0)
 000007FEEC4424AA: MOVDQA  XMM0,XMMWORD PTR [RSP+0x20]
 000007FEEC4424B0: MOVDQA  XMM1,XMMWORD PTR [RSP+0x30]
 000007FEEC4424B6: MOVDQA  XMM2,XMMWORD PTR [RSP+0x40]
 000007FEEC4424BC: MOVDQA  XMM3,XMMWORD PTR [RSP+0x50]
 000007FEEC4424C2: MOV     RCX,QWORD PTR [RSP+0x000000D0]
 000007FEEC4424CA: MOV     RDX,QWORD PTR [RSP+0x000000D8]
 000007FEEC4424D2: MOV     R8,QWORD PTR [RSP+0x000000E0]
 000007FEEC4424DA: MOV     R9,QWORD PTR [RSP+0x000000E8]
 000007FEEC4424E2: NOP     
 000007FEEC4424E3: ADD     RSP,0x0000000000000080
 000007FEEC4424EA: POP     RDI
 000007FEEC4424EB: POP     RSI
 000007FEEC4424EC: POP     RBX
 000007FEEC4424ED: POP     RBP
 000007FEEC4424EE: POP     R12
 000007FEEC4424F0: POP     R13
 000007FEEC4424F2: POP     R14
 000007FEEC4424F4: POP     R15
 000007FEEC4424F6: POP     R10
 000007FEEC4424F8: JMP     RAX

不要被所有这些汇编代码吓倒 - 其中大部分是为了在即将调用 JIT 编译器之前保存您的托管代码的状态,并在调用之后恢复其状态。顺便说一句,_JIT Events_ 窗口中出现的所有 JIT 事件都已经通过这段代码。在寄存器窗口中,当您单步执行代码时,最需要注意的两个寄存器是 _RAX_ 和 _R10_。另外,请注意第一条指令,即 _POP RAX_。如果您仔细思考并记住上一条指令——一个调用语句,您就会意识到调用的返回地址将从堆栈弹出到 _RAX_ 寄存器中,而这正是调用之后指令的地址!这个小技巧为代码提供了穿梭内存中的地址,并帮助以下神秘的代码将 _R10_ 加载另一个穿梭地址,而这个地址在即将调用 JIT 编译器时将变得非常重要。

现在,按 _F10_ 直到您到达 _CLR!PreStubWorker_ 的调用。如果您想深入了解 .NET JIT 进程的内部,现在可以通过按 _F11_ 来进入此调用,但要准备好面对代码中的蜘蛛和其他更糟的东西 - 那是棘手的东西!相反,使用 _F10_ 跳过调用,并观察您之前为 _RAX_ 寄存器打开的内存窗口是如何变化的

内存位置(例如,000007FE8CC8C040)的内容已更改(以红色显示)!如果您使用 _View/Disassemble At_ 来反汇编该地址,您可能还会注意到对 _PrecodeFixupThunk_ 的调用已被跳转语句替换!_LonelyMountain_ 私有方法的 C# 代码刚刚被编译,并且通过这段自我修改的代码魔术,您的程序获得了对其的访问权限。继续按 _F10_ 直到您到达 _JMP RAX_ 指令,然后再次按 _F10_

PID: 0x0E10 Disassembly for THUNK at 0x000007FE8CE16360
 ;********************************************************************************
 000007FE8CE16360: SUB     RSP,0x28
 000007FE8CE16364: TEST    RCX,RCX
 000007FE8CE16367: JE      0x000007FE8CE163B2
 000007FE8CE16369: MOV     RAX,QWORD PTR [RCX]
 000007FE8CE1636C: MOV     R10D,0x7FFFFF10018
 000007FE8CE16376: CMP     RAX,R10
 000007FE8CE16379: JNE     0x000007FE8CE163B2
 000007FE8CE1637B: MOV     QWORD PTR [RSP+0x30],RCX
 000007FE8CE16380: MOV     QWORD PTR [RSP+0x38],RDX
 000007FE8CE16385: MOV     QWORD PTR [RSP+0x40],R8
 000007FE8CE1638A: MOV     QWORD PTR [RSP+0x48],R9
 000007FE8CE1638F: MOV     RAX,QWORD PTR [RCX+0x28]
 000007FE8CE16393: MOV     RCX,QWORD PTR [RCX+0x10]
 000007FE8CE16397: CALL    RAX
 000007FE8CE16399: MOV     RCX,QWORD PTR [RSP+0x30]
 000007FE8CE1639E: MOV     RDX,QWORD PTR [RSP+0x38]
 000007FE8CE163A3: MOV     R8,QWORD PTR [RSP+0x40]
 000007FE8CE163A8: MOV     R9,QWORD PTR [RSP+0x48]
 000007FE8CE163AD: TEST    RAX,RAX
 000007FE8CE163B0: JNE     0x000007FE8CE163C3
 000007FE8CE163B2: MOV     RAX,0x7FE8CDA0390 ; (06000002) ; (0x000007FE8CDA0390)
 000007FE8CE163BC: ADD     RSP,0x28
 000007FE8CE163C0: JMP     RAX
  ;
  000007FE8CA45BE3: MOV     R10D,0x7FE8C8B6560
  000007FE8CA45BED: MOV     RAX,0x7FEEC0DBCB0 ; (0x000007FEEC0DBCB0)
  000007FE8CA45BF7: ADD     RSP,0x28
  000007FE8CA45BFB: JMP     RAX

我怀疑大部分代码是为了支持 .NET 仪器化和其他性能分析活动,因为按 _F10_ 直到第一个 _JMP RAX_ 语句(有趣的是,这个指令一直在出现)可以绕过其中的大部分。(地址 0x7FEEC0DBCB0 指的是 _CLR.DLL_ 中的一个名为 _TransparentProxyStubRCX_ 的例程。)再次按 _F10_ 将我们安全地带到了 _LonelyMountain_ 私有方法

让程序继续运行(按 _F5_)将导致按钮的文本向世界宣布我们已成功到达目的地

回家之路

我们的冒险即将结束,但如果您按下新更改的按钮 **Bilbo Lives!**,程序将在单击事件中第二次暂停,您可以重复上述步骤,亲眼看看代码在后续传递中是如何表现的。您应该注意到,程序再次执行了第二个穿梭层,并且只要程序处于活动状态(或未因重新 JIT 请求或其他注入代码而修改),它就会继续这样做。就像比尔博和魔戒一样,我们的代码会因反复的活动而受到微妙的改变。

更有冒险精神(喜欢图克家)的各位,可能会想在 _FormClosing_ 事件上设置一个断点,并在调用本机例程(例如 _MessageBox_)时,走一条稍有不同的 JIT 路径。

PID: 0x0E10 Disassembly for Wilderland.exe!
Wilderland::Form1::Form1_FormClosing (06000004) at 0x000007FE8CDA03D0
  ;********************************************************************************
 000007FE8CDA03D0: MOV     RCX,0x12A03238
 000007FE8CDA03DA: MOV     RCX,QWORD PTR [RCX]
 000007FE8CDA03DD: MOV     RDX,0x12A03240
 000007FE8CDA03E7: MOV     RDX,QWORD PTR [RDX]
 000007FE8CDA03EA: LEA     RAX,QWORD PTR [0x000007FEE8920F30]
 000007FE8CDA03F1: JMP     RAX
  ;
  000007FE8CDA03F4: RET

第一个穿梭层是相同的;但第二个穿梭层丢失了,因为您正在 _System.Windows.Forms.ni.DLL_ 中执行 NGEN 后的代码。

对于我们其他人来说,我们可以查看 Wilderland 的最后一部分

并在关闭最后一个对话框后,宣布冒险结束。

结论

如果您将之前的文章与这篇文章进行比较,您应该会意识到,尽管在从 32 位、早期版本的通用语言运行时迁移到其 64 位后继者期间,细节已经发生了变化,但其本质仍然相同。当您的 .NET 可执行文件加载时,会为代码中的每个 _MethodDef_ 创建指向包装 JIT 编译过程的通用穿梭的小存根。成功编译方法的 IL 后,JIT 编译器会分配内存来保存新的本机指令,并创建一个第二个定制的存根,将原始调用链接到新代码。如果您尝试走 _MessageBox_ 路径,您也会意识到在将您的代码链接到本机 Windows API 时,也发生了类似的过程。此外,我希望通过向您介绍我的新 64 位调试器,您将获得一个新的工具,用于探索和理解 .NET Wilderland。

© . All rights reserved.