crashdump(crash dump蓝屏怎么办)

张工 2022-05-31 18:56:29 阅读:48
  

  根据前面的介绍,NT内核会将操作系统的代码和数据映射到系统中所有进程的内核空间。这样,每个进程中的应用程序代码都可以方便地调用内核空间中的系统服务。这里的“非常方便”有多重含义。一方面,内核代码和用户代码在同一个地址空间,应用调用系统服务时不需要切换地址空间。另一方面,内核空间在整个系统中的地址是统一的,所以写内核空间的代码会简单很多。但是这种设计也带来了一个很大的问题,就是用户空间的程序指针可以指向内核空间的数据和代码,所以要防止用户代码对内核空间的操作系统造成损害。怎么做?答案是使用权限控制来保护内核空间。

2.6.1 访问模式

   Windows定义了两种访问模式――用户模式和内核模式。应用程序(代码)运行在用户态,操作系统代码运行在内核态。内核模式对应处理器的最高特权级别(不考虑虚拟机),内核模式下执行的代码可以访问所有系统资源,有权使用所有特权指令。相对而言,用户模式对应于较低的处理器优先级。用户态执行的代码只能访问系统允许的内存空间,无权使用特权指令。

  如本书第1卷所述,IA-32处理器定义了四个特权级别(或环),分别为0、1、2和3,优先级0(环0)的特权级别最高。在硬件层面,处理器确保高优先级的数据和代码不会被低优先级的代码破坏。Windows系统使用IA-32处理器定义的四个优先级中的两个。优先级3(环3)用于用户模式,优先级0用于内核模式。只用其中两个,主要是因为有些处理器只支持两个优先级,比如Compaq Alpha处理器。值得注意的是,对于x86处理器,没有寄存器来指示处理器当前所处的模式(或优先级)。优先级只是代码或数据所在的内存段或页面的一个属性。参见第1卷第2.6节和第2.7节。

  因为内核态的数据和代码有更高的优先级,所以用户态的代码不能直接访问内核空间的数据,也不能直接调用内核空间的任何函数或例程。任何此类尝试都将导致保护性错误。也就是说,即使用户空间中的代码指针正确指向要访问的数据或代码,一旦发生访问,处理器也会检测到该访问是非法的,停止访问并产生保护性异常(#GP)。

  虽然不能直接访问,但是用户程序可以通过调用系统服务,间接访问内核空间的数据或者间接调用并执行内核空间的代码。当调用一个系统服务时,调用线程会从用户态切换到内核态,调用结束后又回到用户态,这叫模式切换。在一个线程的KTHREAD结构中,定义了两个字段UserTime和KernelTime,分别记录这个线程在用户态和内核态下的运行时间(以时钟中断次数表示)。模式切换通过软中断或特殊快速系统调用指令实现。下面举例分别介绍这两种切换机制。

2.6.2 使用INT 2E切换到内核模式

  图2-4显示了在Windows 2000中通过INT 2E从应用程序调用ReadFile() API的过程。因为ReadFile() API是从Kernel32.dll导出的,所以我们可以看到,调用首先到了Kernel32.dll的ReadFile()函数,它在简单检查参数后调用了NtDll.dll的ntReadFile()函数。

Windows操作系统管理进程和线程:内核模式和用户模式

  图2-4通过INT 2E从应用程序调用ReadFile() API的过程

  从反汇编可以看出,NtDll.dll的NtReadFile()函数很短。首先将ReadFile()对应的系统服务号(0xa1,与版本相关)放入EAX寄存器,将参数指针放入EDX寄存器,然后通过INT n指令进行调用。这里需要注意的是,虽然每个系统服务都有一个唯一的编号,但是微软并没有公开这些服务编号,也不保证这些编号在不同的Windows版本中保持一致。

  ntdll!NtReadFile: //Windows 20

0077f8fb5d b8a1000000 mov eax,0xa177f8fb62 8d542404 lea edx,[esp+0x4]77f8fb66 cd2e int 2e77f8fb68 c22400 ret 0x24

  在WinDBG下通过!idt 2e命令可以看到2e号向量对应的服务例程是KiSystemService ()KiSystemService ()是内核态中专门用来分发系统调用的例程。

lkd> !idt 2eDumping IDT:2e:   804db1ed nt!KiSystemService

  Windows将2e号向量专门用于系统调用,在启动早期初始化中断描述符表(Interrupt Descriptor Table,IDT)时(见第11章)便注册好了合适的服务例程。因此当NTDll.DLL中的NtReadFile()发出INT 2E指令后,CPU便会通过IDT找到KiSystemService ()函数。因为KiSystemService ()函数是位于内核空间的,所以CPU在把执行权交给KiSystemService ()函数前,会做好从用户模式切换到内核模式的各种工作,包括:

  (1)权限检查,即检查源位置和目标位置所在的代码段权限,核实是否可以转移;

  (2)准备内核模式使用的栈,为了保证内核安全,所有线程在内核态执行时都必须使用位于内核空间的内核栈(kernel stack),内核栈的大小一般为8KB或12KB。

  KiSystemService ()会根据服务ID从系统服务分发表(System Service Dispatch Table)中查找到要调用的服务函数地址和参数描述,然后将参数从用户态栈复制到该线程的内核栈中,最后KiSystemService ()调用内核中真正的NtReadFile()函数,执行读文件的操作,操作结束后会返回到KiSystemService ()KiSystemService ()会将操作结果复制回该线程用户态栈,最后通过IRET指令将执行权交回给NtDll.dll中的NtReadFile()函数(继续执行INT 2E后面的那条指令)。

  通过INT 2E进行系统调用时,CPU必须从内存中分别加载门描述符和段描述符才能得到KiSystemService ()的地址,即使门描述符和段描述符已经在高速缓存中,CPU也需要通过“内存读(memory read)”操作从高速缓存中读出这些数据,然后进行权限检查。

2.6.3 快速系统调用

  因为系统调用是非常频繁的操作,所以如果能减少这些开销还是非常有意义的。可以从两个方面来降低开销:一是把系统调用服务例程的地址放到寄存器中以避免读IDT这样的内存操作,因为读寄存器的速度比读内存的速度要快很多;二是避免权限检查,也就是使用特殊的指令让CPU省去那些对系统服务调用来说根本不需要的权限检查。奔腾II处理器引入的SYSENTER/SYSEXIT指令正是按这一思路设计的。AMD K7引入的SYSCALL/SYSRETURN指令也是为这一目的而设计的。相对于INT 2E,使用这些指令可以加快系统调用的速度,因此利用这些指令进行的系统调用称为快速系统调用。

  下面我们介绍Windows系统是如何利用IA-32处理器的SYSENTER/SYSEXIT指令(从奔腾II开始)实现快速系统调用的[2]。首先,Windows 2000或之前的Windows系统不支持快速系统调用,它们只能使用前面介绍的INT 2E方式进行系统调用。Windows XP和Windows Server 2003或更新的版本在启动过程中会通过CPUID指令检测CPU是否支持快速系统调用指令(EDX寄存器的SEP标志位)。如果CPU不支持这些指令,那么仍使用INT 2E方式。如果CPU支持这些指令,那么Windows系统便会决定使用新的方式进行系统调用,并做好如下准备工作。

  (1)在全局描述符表(GDT)中建立4个段描述符,分别用来描述供SYSENTER指令进入内核模式时使用的代码段(CS)和栈段(SS),以及SYSEXIT指令从内核模式返回用户模式时使用的代码段和栈段。这4个段描述符在GDT中的排列应该严格按照以上顺序,只要指定一个段描述符的位置便能计算出其他的。

  (2)设置表2-1中专门用于系统调用的MSR(关于MSR的详细介绍见卷1的2.4.3节),SYSENTER_EIP_MSR用于指定新的程序指针,也就是SYSENTER指令要跳转到的目标例程地址。Windows系统会将其设置为KiFastCallEntry的地址,因为KiFastCallEntry例程是Windows内核中专门用来受理快速系统调用的。SYSENTER_CS_MSR用来指定新的代码段,也就是KiFastCallEntry所在的代码段。SYSENTER_ESP_MSR用于指定新的栈指针(ESP)。新的栈段是由SYSENTER_CS_MSR的值加8得来的。

  (3)将一小段名为SystemCallStub的代码复制到SharedUserData内存区,该内存区会被映射到每个Win32进程的进程空间中。这样当应用程序每次进行系统调用时,NTDll.DLL中的残根(stub)函数便调用这段SystemCallStub代码。SystemCallStub的内容因系统硬件的不同而不同,对于IA-32处理器,该代码使用SYSENTER指令,对于AMD处理器,该代码使用SYSCALL指令。

  表2-1 供SYSENTER指令使用的MSR(略)

  例如在配有Pentium M CPU的Windows XP系统上,以上3个寄存器的值分别为:

lkd> rdmsr 174msr[174] = 00000000`00000008lkd> rdmsr 175msr[175] = 00000000`bacd8000lkd> rdmsr 176msr[176] = 00000000`8053cad0

  其中SYSENTER_CS_MSR的值为8,这是Windows系统的内核代码段的选择子,即常量KGDT_R0_CODE的值。WinDBG帮助文件中关于dg命令的说明中列出了这个常量。SYSENTER_EIP_MSR的值是8053cad0,检查nt内核中KiFastCallEntry函数的地址。

lkd> x nt!KiFastCallEntry8053cad0 nt!KiFastCallEntry = <no type information>

  可见,Windows把快速系统调用的目标指向内核代码段中的KiFastCallEntry函数。

  通过反汇编Windows XP下NTDll.DLL中的NtReadFile ()函数,可以看到SystemCallStub被映射到进程的0x7ffe0300位置。与前面Windows 2000下的版本相比,容易看到该服务的系统服务号码在这两个版本间是不同的。

kd> u ntdll...ntdll!NtReadFile: // Windows XP77f5bfa8 b8b7000000       mov     eax,0xb777f5bfad ba0003fe7f       mov     edx,0x7ffe030077f5bfb2 ffd2             call edx {SharedUserData!SystemCallStub (7ffe0300)}77f5bfb4 c22400           ret     0x2477f5bfb7 90               nop

  观察本段下面反汇编SystemCallStub的结果,它只包含3条指令,分别用于将栈指针(ESP寄存器)放入EDX寄存器中、执行sysenter指令和返回。第一条指令有两个用途:一是向内核空间传递参数;二是指定从内核模式返回时的栈地址。因为笔者使用的是英特尔奔腾M处理器,所以此处是sysenter指令,对于AMD处理器,此处应该是syscall指令。

kd> u...SharedUserData!SystemCallStub:7ffe0300 8bd4             mov     edx,esp7ffe0302 0f34             sysenter7ffe0304 c3               ret

  下面让我们看一下KiFastCallEntry例程,其清单如下所示。

kd> u nt!KiFastCallEntry L20nt!KiFastCallEntry:804db1bb 368b0d40f0dfff   mov      ecx,ss:[ffdff040]804db1c2 368b6104         mov      esp,ss:[ecx+0x4]804db1c6 b90403fe7f       mov      ecx,0x7ffe0304804db1cb 3b2504f0dfff     cmp      esp,[ffdff004]804db1d1 0f84cc030000     je       nt!KiServiceExit2+0x13f (804db5a3)804db1d7 6a23             push     0x23804db1d9 52               push     edx804db1da 83c208           add      edx,0x8804db1dd 6802020000       push     0x202804db1e2 6a02             push     0x2804db1e4 9d               popfd804db1e5 6a1b             push     0x1b804db1e7 51               push     ecx // Fall Through,自然进入KiSystemService函数nt!KiSystemService:804db1e8 90               nop804db1e9 90               nop804db1ea 90               nop804db1eb 90               nop804db1ec 90                nopnt!KiSystemService:804db1ed 6a00             push      0x0804db1ef 55               push      ebp

  显而易见,KiFastCallEntry在做了些简单操作后,便下落(fall through)到KiSystemService函数了,也就是说,快速系统调用和使用INT 2E进行的系统调用在内核中的处理绝大部分是一样的。另外,请注意ecx寄存器,mov ecx,0x7ffe0304将其值设为0x7ffe0304,也就是SharedUserData内存区里SystemCallStub例程中ret指令的地址(参见上文的SystemCallStub代码)。在进入nt!KiSystemService之前,ecx连同其他一些参数被压入栈中。事实上,ecx用来指定SYSEXIT返回用户模式时的目标地址。当使用INT 2E进行系统调用时,由于INT n指令会自动将中断发生时的CS和EIP寄存器压入栈中,当中断处理例程通过执行iretd返回时,iretd指令会使用栈中保存的CS和EIP值返回合适的位置。因为sysenter指令不会向栈中压入要返回的位置,所以sysexit指令必须通过其他机制知道要返回的位置。这便是压入ECX寄存器的原因。通过反汇编KiSystemCallExit2例程,我们可以看到在执行sysexit指令之前,ecx寄存器的值又从栈中恢复出来了。

kd> u nt!KiSystemCallExit l20nt!KiSystemCallExit:804db3b4 cf            iretdnt!KiSystemCallExit2:804db3b5 5a            pop      edx804db3b6 83c408        add      esp,0x8804db3b9 59            pop      ecx804db3ba fb            sti804db3bb 0f35          sysexitnt!KiSystemCallExit3:804db3bd 59            pop      ecx804db3be 83c408        add      esp,0x8804db3c1 5c            pop      esp804db3c2 0f07          sysret

  以上代码中包含了3个从系统调用返回的例程,即KiSystemCallExitKiSystemCallExit2KiSystemCallExit3,它们分别对应于使用INT 2E、sysenter和syscall发起的系统调用,如表2-2所示。

  表2-2 系统调用(略)

  图2-5展示了使用sysenter/sysexit指令对进行系统调用的完整过程(以调用ReadFile服务为例)。

Windows操作系统管理进程和线程:内核模式和用户模式

  图2-5 快速系统调用(针对IA-32处理器)


Windows操作系统管理进程和线程:内核模式和用户模式

   格物

  下面通过一个小的实验来加深大家对系统调用的理解。首先启动WinDBG程序,选择File → Open Crash Dump,然后选择本书实验文件中的dumps\w732cf4.dmp文件。在调试会话建立后,先执行.symfix c:\symbols和.reload加载模块与符号,再执行k命令,便得到清单2-4所示的完美栈回溯。

  第22章将详细讲解栈回溯的原理,现在大家只要知道栈上记录着函数相互调用时的参数和返回地址等信息。栈回溯是从栈上找到这些信息,然后显示出来的过程,是追溯线程执行轨迹的一种便捷方法。

  清单2-4还显示了任务管理器程序(taskmgr)调用NtTerminateProcess系统服务时的执行过程。栈回溯的结果包含4列,第一列是序号,第二列是每个函数的栈帧基地址,第三列是返回地址,第四列是使用“函数名+字节偏移量”形式表达的执行位置。以00栈帧为例,它对应的函数是著名的蓝屏函数KeBugCheckEx,它的栈帧基地址是9796fb9c,它的返回地址是82b1ab51,翻译成符号便是PspCatchCriticalBreak+0x71。

  清单2-4 完美栈回溯

# ChildEBP RetAddr  00 9796fb9c 82b1ab51 nt!KeBugCheckEx+0x1e01 9796fbc0 82a6daa8 nt!PspCatchCriticalBreak+0x7102 9796fbf0 82a605b6 nt!PspTerminateAllThreads+0x2d03 9796fc24 8287c87a nt!NtTerminateProcess+0x1a204 9796fc24 77da7094 nt!KiFastCallEntry+0x12a05 001df4dc 77da68d4 ntdll!KiFastSystemCallRet06 001df4e0 76193c82 ntdll!NtTerminateProcess+0xc07 001df4f0 00bf57b9 KERNELBASE!TerminateProcess+0x2c08 001df524 00bf67ec taskmgr!CProcPage::KillProcess+0x11609 001df564 00bebc96 taskmgr!CProcPage::HandleWMCOMMAND+0x10f0a 001df5d8 76abc4e7 taskmgr!ProcPageProc+0x2750b 001df604 76ad5b7c USER32!InternalCallWinProc+0x230c 001df680 76ad59f3 USER32!UserCallDlgProcCheckWow+0x1320d 001df6c8 76ad5be3 USER32!DefDlgProcWorker+0xa80e 001df6e4 76abc4e7 USER32!DefDlgProcW+0x220f 001df710 76abc5e7 USER32!InternalCallWinProc+0x2310 001df788 76ab5294 USER32!UserCallWinProcCheckWow+0x14b11 001df7c8 76ab5582 USER32!SendMessageWorker+0x4d012 001df7e8 74e94601 USER32!SendMessageW+0x7c13 001df808 74e94663 COMCTL32!Button_NotifyParent+0x3d14 001df824 74e944ed COMCTL32!Button_ReleaseCapture+0x11315 001df884 76abc4e7 COMCTL32!Button_WndProc+0xa1816 001df8b0 76abc5e7 USER32!InternalCallWinProc+0x2317 001df928 76abcc19 USER32!UserCallWinProcCheckWow+0x14b18 001df988 76abcc70 USER32!DispatchMessageWorker+0x35e19 001df998 76ab41eb USER32!DispatchMessageW+0xf1a 001df9bc 00be16fc USER32!IsDialogMessageW+0x5881b 001dfdac 00be5384 taskmgr!wWinMain+0x5d11c 001dfe40 76bbed6c taskmgr!_initterm_e+0x1b11d 001dfe4c 77dc377b kernel32!BaseThreadInitThunk+0xe1e 001dfe8c 77dc374e ntdll!__RtlUserThreadStart+0x701f 001dfea4 00000000 ntdll!_RtlUserThreadStart+0x1b

  仔细观察清单2-4中的地址部分,很容易看出用户空间和内核空间的分界,也就是在栈帧04和栈帧05之间。栈帧05中的KiFastSystemCallRet函数属于ntdll模块,位于用户空间。栈帧04中的KiFastCallEntry函数属于nt模块,位于内核空间。栈帧04的基地址是9796fc24,属于内核空间;栈帧05的基地址是001df4dc,属于用户空间。它们分别来自这个线程的内核态栈和用户态栈。WinDBG的k命令穿越两个空间,遍历两个栈,显示出线程在用户空间和内核空间执行的完整过程,能产生如此完美的栈回溯显示了WinDBG的强大。


2.6.4 逆向调用

  前文介绍了从用户模式进入内核模式的两种方法,通过这两种方法,用户模式的代码可以“调用”位于内核模式的系统服务。那么内核模式的代码是否可以主动调用用户模式的代码呢?答案是肯定的,这种调用通常称为逆向调用(reverse call)。

  简单来说,逆向调用的过程是这样的。首先内核代码使用内核函数KiCallUserMode发起调用。接下来的执行过程与从系统调用返回(KiServiceExit)类似,不过进入用户模式时执行的是NTDll.DLL中的KiUserCallbackDispatcher。而后KiUserCallbackDispatcher会调用内核希望调用的用户态函数。当用户模式的工作完成后,执行返回动作的函数会执行INT 2B指令,也就是触发一个0x2B异常。这个异常的处理函数是内核模式的KiCallbackReturn函数。于是,通过INT 2B异常,CPU又跳回内核模式继续执行了。

lkd> !idt 2bDumping IDT:2b:   8053d070 nt!KiCallbackReturn

  以上是使用WinDBG的!idt命令观察到的0x2B异常的处理函数。

2.6.5 实例分析

  下面通过一个实际例子来进一步展示系统调用和逆向调用的执行过程。清单2-5显示了使用WinDBG的内核调试会话捕捉到的记事本进程发起系统调用进入内核和内核函数执行逆向调用的全过程(栈回溯)。

  清单2-5 记事本进程从发起系统调用进入内核和内核函数逆向调用的全过程

kd> kn # ChildEBP RetAddr  00 0006fe94 77fb4da6 USER32!XyCallbackReturn01 0006fe94 8050f8ae ntdll!KiUserCallbackDispatcher+0x1302 f4fc19b4 80595d2c nt!KiCallUserMode+0x403 f4fc1a10 bf871e98 nt!KeUserModeCallback+0x8704 f4fc1a90 bf8748d4 win32k!SfnDWORD+0xa005 f4fc1ad8 bf87148d win32k!xxxSendMessageToClient+0x17406 f4fc1b24 bf8714d3 win32k!xxxSendMessageTimeout+0x1a607 f4fc1b44 bf8635f6 win32k!xxxSendMessage+0x1a08 f4fc1b74 bf84a620 win32k!xxxMouseActivate+0x22d09 f4fc1c98 bf87a0c1 win32k!xxxScanSysQueue+0x8280a f4fc1cec bf87a8ad win32k!xxxRealInternalGetMessage+0x32c0b f4fc1d4c 804da140 win32k!NtUserGetMessage+0x270c f4fc1d4c 7ffe0304 nt!KiSystemService+0xc40d 0006feb8 77d43a21 SharedUserData!SystemCallStub+0x20e 0006febc 77d43c95 USER32!NtUserGetMessage+0xc0f 0006fed8 010028e4 USER32!GetMessageW+0x3110 0006ff1c 01006c54 notepad!WinMain+0xe311 0006ffc0 77e814c7 notepad!WinMainCRTStartup+0x17412 0006fff0 00000000 kernel32!BaseProcessStart+0x23

  根据执行的先后顺序,最下面一行(帧#12)对应的是进程的启动函数BaseProcessStart,而后是编译器生成的进程启动函数WinMainCRTStartup,以及记事本程序自己的入口函数WinMain。帧#0f表示记事本程序在调用GetMessage API进入消息循环。接下来GetMessage API调用Windows子系统服务的残根函数NtUserGetMessage。从第2列的栈帧基地址都小于0x800000000可以看出,帧#12~#0d都是在用户模式执行的。帧#0d执行我们前面分析过的SystemCallStub,而后(帧#0c)便进入了内核模式的KiSystemServiceKiSystemService根据系统服务号码,将调用分发给Windows子系统内核模块win32k中的NtUserGetMessage函数。

  帧#0a~#05表示内核模式的窗口消息函数在工作。帧#07~#05表示要把一个窗口消息发送到用户态。帧#04的SfnDWORD表示在将消息组织好后调用KeUserModeCallback函数,发起逆向调用。帧#02表明在执行KiCallUserMode函数,帧#01表明已经在用户模式下执行,这两行之间的部分过程没有显示出来。同样,帧#01 和帧#00 之间执行用户模式函数的过程没有完全体现出来。XyCallbackReturn函数是用于返回内核模式的,它的代码很简单,只有如下几条指令。

USER32!XyCallbackReturn:001b:77d44168 8b442404     mov   eax,dword ptr [esp+4] ss:0023:0006fe84=00000000001b:77d4416c cd2b          int   2Bh001b:77d4416e c20400        ret   4

  第1行把用户模式函数的执行结果赋给EAX寄存器,第2行执行INT 2B指令。执行过INT 2B后,CPU便转去执行异常处理程序KiCallbackReturn,回到了内核模式。

  本文摘自《软件调试(第2版)卷2:Windows平台调试(上、下册)》

Windows操作系统管理进程和线程:内核模式和用户模式

  本书是国内当前集中介绍软件调试主题的权威著作。本书第2卷分为5篇,共30章,主要围绕Windows系统展开介绍。第一篇(第1~4章)介绍Windows系统简史、进程和线程、架构和系统部件,以及Windows系统的启动过程,既从空间角度讲述Windows的软件世界,也从时间角度描述Windows世界的搭建过程。第二篇(第5~8章)描述特殊的过程调用、垫片、托管世界和Linux子系统。第三篇(第9~19章)深入探讨用户态调试模型、用户态调试过程、中断和异常管理、未处理异常和JIT调试、硬错误和蓝屏、错误报告、日志、事件追踪、WHEA、内核调试引擎和验证机制。第四篇(第20~25章)从编译和编译期检查、运行时库和运行期检查、栈和函数调用、堆和堆检查、异常处理代码的编译、调试符号等方面概括编译器的调试支持。第五篇(第26~30章)首先纵览调试器的发展历史、工作模型和经典架构,然后分别讨论集成在Visual Studio和Visual Studio(VS)Code中的调试器,最后深度解析WinDBG调试器的历史、结构和用法。

  本书理论与实践结合,不仅涵盖了相关的技术背景知识,还深入研讨了大量具有代表性的技术细节,是学习软件调试技术的珍贵资料。

二维码