WoW进程的异常分发过程
以WoW方式运行在64位Windows系统的32位进程(简称WoW进程)与普通的32位进程相比,很多行为都保持不变,但是也有个别时候会显露出差异,让人感觉困惑,比如发生异常时,可能就把一些本来藏而不漏的“
内丑”曝露出来了。
举例来说,当我的AcsVio小程序(熟悉《软件调试》的朋友都知道)在64位环境下运行,JIT机制启动WinDBG64后,本来“自动定位到异常”的功能就不工作了,执行k命令看到的是下面一幕:
0:000:x86> k
ChildEBP RetAddr 0018fa80 773c014d ntdll_773b0000!ZwRaiseException+0x12
0018fa90 c0000005 ntdll_773b0000!KiUserExceptionDispatcher+0x29
WARNING: Frame IP not in any known module. Following frames may be wrong.
0018ff88 763933aa 0xc0000005
0018ff94 773e9ef2 KERNEL32!BaseThreadInitThunk+0xe
0018ffd4 773e9ec5 ntdll_773b0000!__RtlUserThreadStart+0x70
0018ffec 00000000 ntdll_773b0000!_RtlUserThreadStart+0x1bv
有一次在武汉讲课,曾有同行对这个问题大倒苦水,而且讲出一大堆有关的故事来。本来想在《软件调试》的第二版中详细讨论WoW的问题。但最近又有网友在论坛中提出希望了解wow的异常分发细节,于是就先
写篇博客吧,也算给书中的内容打个草稿。
从哪里讲起呢? 就从NTDLL说起吧。要理解下面的内容,首先必须清楚的是,在WoW进程中,有两个NTDLL.DLL,一个是64位的,一个是32位的。正常情况下,WinDBG会把32位建立个别名叫ntdll32,也就是这个样
子:
00000000`771d0000 00000000`77379000 ntdll
00000000`773b0000 00000000`77530000 ntdll32
注意它们都在2GB地址空间中,这样是为了方便32位代码中的32位指针可以直接指向64位的NTDLL。
某些情况下,比如JIT调试,WinDBG的别名机制可能不工作,于是32位的ntdll的名字就会被附加上基地址以便区别,也就是:
771d0000 77379000 ntdll (deferred)
773b0000 77530000 ntdll_773b0000 (pdb symbols)
另一个背景是,对于WoW线程来说,每个线程都有两个栈,一个是32位代码使用的,另一个是64位的转接层代码所使用的。
32 bit, StackBase : 0x190000
StackLimit : 0x18e000
Deallocation: 0x90000
64 bit, StackBase : 0x8fd20
StackLimit : 0x8c000
Deallocation: 0x50000
有了以上基础后,我们开始讲异常分发过程,假设以WoW方式直接运行(不在调试器中)的AcsVio进程中访问违例,CPU飞进内核态,开始执行异常处理和分发的代码。主要的步骤如下:
1)内核函数KiDispatchException分发这个异常,先试图分发给调试器,但如果没有调试器,便会将这个异常的信息复制到用户态栈,然后将程序指针指向NTDLL64中的KiUserExceptionDispatcher。从这一步来
看,内核对WoW进程没有什么特殊处理,就是按照一般规则来做,准备回到64位NTDLL64中寻找用户态的异常处理器(try/catch)。
2)当CPU刚开始回到用户态执行时,虽然代码是64位的KiUserExceptionDispatcher,但使用的栈却是32位代码用的,因此,在这个函数一开始,就有一个专门针对WoW的特殊动作,伪代码如下:
VOID
KiUserExceptionDispatcher(
__in PCONTEXT ContextRecord,
__in PEXCEPTION_RECORD ExceptionRecord,
)
{
if (Wow64PrepareForException)
Wow64PrepareForException(
ExceptionRecord,
ContextRecord
);
Wow64PrepareForException函数并不长,主要调用了两个函数,第一个是memcpy,将内核态复制上来的信息从当前使用32位栈复制到64位栈,另一个是wow64!CpuResetToConsistentState。后者主要是将异常信
息机构复制到wow64cpu!RecoverException64变量中,将异常对应的上下文结果复制到wow64cpu!RecoverContext64,然后切换栈,转向使用64位代码的专用栈。
3)接下来,KiUserExceptionDispatcher开始执行通用的代码,也就是调用RtlDispatchException来寻找和执行异常处理器。奥秘其实也就在这个过程中。
执行!exchain观察当前线程中有效的异常处理器,可以看到:
0:000> !exchain
8 stack frames, scanning for handlers...
Frame 0x02: wow64cpu!CpupReturnFromSimulatedCode (00000000`7456271e)
ehandler wow64cpu!CpupSimulateHandler (00000000`74562560)
Frame 0x03: wow64!RunCpuSimulation+0xa (00000000`745dd07e)
ehandler wow64!_C_specific_handler (00000000`745fe24e)
Frame 0x04: wow64!Wow64LdrpInitialize+0x429 (00000000`745dc549)
ehandler wow64!_GSHandlerCheck (00000000`745fe190)
Frame 0x05: ntdll!LdrpInitializeProcess+0x17e4 (00000000`77214956)
ehandler ntdll!_GSHandlerCheck (00000000`771e9818)
Frame 0x06: ntdll! ?? ::FNODOBFM::`string'+0x29220 (00000000`77211a17)
ehandler ntdll!_C_specific_handler (00000000`771e850c)
RtlDispatchException会依次询问这些处理器是否处理异常,如果有人回答处理(1),那么就执行它的异常处理器。
4)RunCpuSimulation函数的名为_C_specific_handler的异常处理器起着关键作用,在它得到执行机会时,它会调用Pass64bitExceptionTo32Bit函数将异常信息传递到32位代码使用的栈,并将32位上下文中的
程序指针设置为NTDLL32中的KiUserExceptionDispatcher。执行过程如下:
0:000> kn
# Child-SP RetAddr Call Site
00 00000000`0008db90 00000000`745dc9a5 wow64!Wow64SetupExceptionDispatch+0x1b7
01 00000000`0008dd00 00000000`745fea2c wow64!Pass64bitExceptionTo32Bit+0x105
02 00000000`0008e210 00000000`771e85a8 wow64!Wow64pLongJmp+0x66c
03 00000000`0008e240 00000000`771f9d0d ntdll!_C_specific_handler+0x8c
04 00000000`0008e2b0 00000000`771e91af ntdll!RtlpExecuteHandlerForException+0xd
05 00000000`0008e2e0 00000000`77221278 ntdll!RtlDispatchException+0x45a
06 00000000`0008e9c0 00000000`7456271e ntdll!KiUserExceptionDispatcher+0x2e
07 00000000`0008f100 00000000`745dd07e wow64cpu!CpupReturnFromSimulatedCode
08 00000000`0008f1c0 00000000`745dc549 wow64!RunCpuSimulation+0xa
09 00000000`0008f210 00000000`7724e707 wow64!Wow64LdrpInitialize+0x429
0a 00000000`0008f760 00000000`771fc32e ntdll! ?? ::FNODOBFM::`string'+0x29364
0b 00000000`0008f7d0 00000000`00000000 ntdll!LdrInitializeThunk+0xe
其中的Wow64SetupExceptionDispatch函数的目的便是将32位上下文中的程序指针飞到全局变量wow64!Ntdll32KiUserExceptionDispatcher飞到32位NTDLL中的KiUserExceptionDispatcher。
00000000`745dc707 8b0d73e80200 mov ecx,dword ptr [wow64!Ntdll32KiUserExceptionDispatcher (00000000`7460af80)] ds:00000000`7460af80=773c0124
00000000`745dc70d ff151d58ffff call qword ptr [wow64!_imp_CpuSetInstructionPointer (00000000`745d1f30)]
5)接下来做栈展开,准备执行_C_specific_handler的异常处理代码,也就是要回到RunCpuSimulation函数中执行,细节上便是执行RtlUnwindEx,后者再调用RtlRestoreContext
ntdll!RtlRestoreContext
0:000> k
Child-SP RetAddr Call Site
00000000`0008d6a8 00000000`771e897b ntdll!RtlRestoreContext
00000000`0008d6b0 00000000`771e57c4 ntdll!RtlUnwindEx+0x42d
00000000`0008dd50 00000000`771f9d0d ntdll!_C_specific_handler+0xcc
00000000`0008ddc0 00000000`771e91af ntdll!RtlpExecuteHandlerForException+0xd
00000000`0008ddf0 00000000`77221278 ntdll!RtlDispatchException+0x45a
00000000`0008e4d0 00000000`7456271e ntdll!KiUserExceptionDispatcher+0x2e
00000000`0008ec10 00000000`745dd07e wow64cpu!CpupReturnFromSimulatedCode
00000000`0008ecd0 00000000`745dc549 wow64!RunCpuSimulation+0xa
00000000`0008ed20 00000000`77214956 wow64!Wow64LdrpInitialize+0x429
00000000`0008f270 00000000`77211a17 ntdll!LdrpInitializeProcess+0x17e4
00000000`0008f760 00000000`771fc32e ntdll! ?? ::FNODOBFM::`string'+0x29220
00000000`0008f7d0 00000000`00000000 ntdll!LdrInitializeThunk+0xe
在RtlRestoreContext,会把当前线程的执行上下文设置成适合执行RunCpuSimulation的状态,也就是“飞”回以上栈回溯的 wow64!RunCpuSimulation+0xa这一栈帧。
RtlRestoreContext的最后两条指令是:
00000000`77220bcb 488b8980000000 mov rcx,qword ptr [rcx+80h]
00000000`77220bd2 48cf iretq
其中的iretq一旦执行,CPU便从栈顶取出接下来要执行的位置,观察栈顶:
0:000> dd 000000000008d660
00000000`0008d660 745dd080 00000000 0
其值果然位于RunCpuSimulation函数中:
wow64!RunCpuSimulation+0xc
6)在RunCpuSimulation中,调用CpuSimulate,准备去执行32位代码:
wow64!RunCpuSimulation:
00000000`745dd074 4883ec48 sub rsp,48h
00000000`745dd078 ff155a4effff call qword ptr [wow64!_imp_CpuSimulate (00000000`745d1ed8)] ds:00000000`745d1ed8={wow64cpu!CpuSimulate (00000000`745625b0)}
00000000`745dd07e eb00 jmp wow64!RunCpuSimulation+0xc (00000000`745dd080)
00000000`745dd080 ebf6 jmp wow64!RunCpuSimulation+0x4 (00000000`745dd078)
00000000`745dd082 4883c448 add rsp,48h
00000000`745dd086 c3 ret
在CpuSimulate中,将32位的线程上下文加载到物理寄存器,然后一个长跳转,跳转到32位世界:
0:000> p
wow64cpu!CpuSimulate+0x16b:
00000000`7456271b 41ff2e jmp fword ptr [r14] ds:00000000`0008ec80=0023773c0124
因为前面已经做好准备工作,所以这一跳,便跳到了ntdll32!KiUserExceptionDispatcher函数:
0:000> p
ntdll32!KiUserExceptionDispatcher:
773c0124 fc cld
至此,64位的异常处理代码将异常顺利移交到32位代码,完成了交接工作,接下来32位的异常代码仍然按老的逻辑继续按32位的规则分发异常。