windows-kernel-programming 中文机翻-第五章

就像任何软件一样,内核驱动程序往往有错误。与用户模式调试相比,调试驱动程序更具挑战性。驱动程序调试本质上是调试整台机器,而不仅仅是特定的进程或进程。这需要不同的心态。本章讨论了使用WinDbg调试器进行内核调试。

目录

Windows的调试工具

WinDbg简介

教程:用户模式调试基础知识

内核调试

本地内核调试

本地内核调试教程

完整的内核调试

配置目标

配置主机

内核驱动程序调试教程

总结

Windows的调试工具

Windows调试工具包包含一组专注于包中的调试器、工具和文档。此软件包可以作为Windows SDK或WDK的一部分安装,但没有完成真正的“安装”。安装只是复制文件,但不接触注册表,这意味着软件包仅取决于自己的模块和Windows DLL。这可以轻松地将整个目录复制到任何其他目录,包括可移动媒体。

该软件包包含四个调试器:Cdb.exe、Ntsd.Exe、Kd.exe和WinDbg.exe。以下是每个调试器基本功能的概要:

        •Cdb和Ntsd是用户模式,基于控制台的调试器。这意味着他们可以像任何其他用户模式调                                      试器一样附加到进程。两者都有控制台用户界面-键入命令,获取响应并重复。两者之间的唯一区别是,如果从控制台窗口启动,Cdb使用相同的控制台,而Ntsd会打开一个新的控制台窗口。除此之外,它们是相同的。

        • Kd是一个具有控制台用户界面的内核调试器。它可以连接到本地内核(本地内核调试,下节所述)或另一台机器。

        •WinDbg是唯一具有图形用户界面的调试器。它可以在用户模式调试或内核调试中操作,具体取决于其菜单执行的选择或启动的命令行参数。

经典WinDbg的最新替代品是Windbg Preview,可通过微软商店获得。这是经典调试器的翻拍,具有更好的用户界面和体验。它可以安装在Windows 10 1607或更高版本上。从功能的角度来看,它类似于经典的WinDbg。但由于现代、方便的用户界面,它有点容易使用,事实上也解决了经典调试器中的一些错误。我们将在本章后面看到的所有命令都同样适用于任一调试器。

虽然这些调试器可能看起来彼此不同,但事实上用户模式调试器本质上是相同的,内核调试器也是如此。它们都基于作为DLL(DbgEng.Dll)实现的单个调试器引擎。各种调试器能够使用扩展DLL,这些DLL提供了调试器的大部分功能。

调试器引擎在Windows文档的调试工具中得到了公平的记录,因此可以编写使用相同引擎的新调试器。

软件包中的其他工具包括(部分列表):

        • Gflags.exe - 全球标志工具,允许设置一些内核标志和图像标志。

        • ADPlus.exe - 为进程崩溃或挂起生成转储文件。

        • Kill.exe - 基于进程ID、名称或模式终止进程的简单工具。

        • Dumpchk.exe - 对转储文件进行一些常规检查的工具。

        • TList.exe - 列出系统上运行的进程和各种选项。

        • Umdh.exe - 分析用户模式进程中的堆分配。

        • UsbView.exe - 显示USB设备和集线器的分层视图。

WinDbg简介

本节描述了WinDbg的基本原理,但请记住,除了GUI窗口外,控制台调试器的一切都基本相同。

WinDbg是围绕命令构建的。用户输入命令,调试器回复描述命令结果的文本。使用GUI,其中一些结果被描绘在专用窗口中,如本地、堆栈、线程等。

WinDbg支持三种类型的命令:

•内在命令-这些命令内置在调试器中,它们对正在调试的目标进行操作。

•元命令-这些命令以句点(.)开始,它们对调试过程本身进行操作,而不是直接对正在调试的目标进行操作。

•爆炸(扩展)命令-这些命令以感叹号(!)开始,提供调试器的大部分功能。所有扩展命令都在扩展DLL中实现。默认情况下,调试器加载一组预定义的扩展DLL,但可以从调试器目录或其他来源加载更多。

编写扩展DLL是可能的,并在调试器文档中充分记录。事实上,许多这样的DLL已经创建,可以从各自的来源加载。这些DLL提供了增强调试体验的新命令,通常针对特定场景。

教程:用户模式调试基础知识

如果您有WinDbg的经验,您可以安全地跳过此部分。

本教程旨在基本了解WinDbg以及如何使用它进行用户模式调试。内核调试将在下一节中描述。

通常有两种方法可以启动用户模式调试——要么启动可执行文件并附加到它,要么附加到现有的进程。我们将在本教程中使用后一种方法,但除了第一步外,所有其他操作都是相同的。

        •启动记事本。

        •启动WinDbg(预览版或经典版)。以下屏幕截图使用预览)。

        •选择文件/附件进行处理,并在列表中找到记事本进程(见图5-1)。然后点击附加。您应该会看到类似于图5-2的输出。

命令窗口是感兴趣的主窗口-它应该始终打开。这是显示命令的各种响应的那个。通常,调试会话中的大部分时间都用于与此窗口交互。

该过程现已暂停——我们正处于调试器引发的断点。

        •我们将使用的第一个命令是∼,它显示调试过程中所有线程的信息:

0:003> ~
   0  Id: 874c.18068 Suspend: 1 Teb: 00000001`2229d000 Unfrozen
   1  Id: 874c.46ac Suspend: 1 Teb: 00000001`222a5000 Unfrozen
   2  Id: 874c.152cc Suspend: 1 Teb: 00000001`222a7000 Unfrozen
.  3  Id: 874c.bb08 Suspend: 1 Teb: 00000001`222ab000 Unfrozen

您将看到的线程的确切数量可能与此处显示的不同。

非常重要的事情之一是适当的符号的存在。微软提供了一个公共符号服务器,可用于查找微软大多数模块的符号。这在任何低级调试中都是必不可少的。

        •要快速设置符号,请输入.symfix命令。

        •更好的方法是设置一次符号,并将其用于未来的所有调试会话。为此,请添加一个名为_NT_SYMBOL_PATH的系统环境变量,并将其设置为如下字符串:

SRV*c:\Symbols*http://msdl.microsoft.com/download/symbols

中间部分(星号之间)是在本地机器上缓存符号的本地路径;您可以选择任何您喜欢的路径。一旦设置了此环境变量,调试器的下一次调用将自动找到符号,并根据需要从微软符号服务器加载它们。

        •为了确保您有正确的符号,请输入lm(加载模块)命令:

0:007> lm
start             end                 module name
00000000`56960000 00000000`56a0f000   sysfer     (no symbols)           
00007ff6`09ef0000 00007ff6`09f28000   notepad    (pdb symbols)          C:\ProgramData\Dbg\sym\notepad.pdb\E94DD4FE49E3B454BC7E54A35C324C341\notepad.pdb
00007ffe`93920000 00007ffe`939fd000   efswrt     (deferred)             
00007ffe`b9550000 00007ffe`b9649000   textinputframework   (deferred)             
00007ffe`bd040000 00007ffe`bd0ec000   TextShaping   (deferred)             
00007ffe`c7d80000 00007ffe`c7de6000   oleacc     (deferred)             
00007ffe`d5350000 00007ffe`d55ea000   COMCTL32   (deferred)             
00007ffe`d7e50000 00007ffe`d7f44000   MrmCoreR   (deferred)             
00007ffe`e8940000 00007ffe`e895d000   MPR        (deferred)             
00007ffe`ecd50000 00007ffe`ecf53000   twinapi_appcore   (deferred)             
00007ffe`eeb20000 00007ffe`eec75000   wintypes   (deferred)             
00007ffe`efb00000 00007ffe`efe5b000   CoreUIComponents   (deferred)             
00007ffe`f01e0000 00007ffe`f02d2000   CoreMessaging   (deferred)             
00007ffe`f0600000 00007ffe`f069e000   uxtheme    (deferred)             
00007ffe`f0d00000 00007ffe`f149b000   windows_storage   (deferred)             
00007ffe`f19a0000 00007ffe`f19b2000   kernel_appcore   (deferred)             
00007ffe`f2290000 00007ffe`f22c3000   ntmarta    (deferred)             
00007ffe`f2a80000 00007ffe`f2aad000   Wldp       (deferred)             
00007ffe`f3190000 00007ffe`f32aa000   gdi32full   (deferred)             
00007ffe`f3300000 00007ffe`f35f6000   KERNELBASE   (deferred)             
00007ffe`f3760000 00007ffe`f37e2000   bcryptPrimitives   (deferred)             
00007ffe`f3910000 00007ffe`f3932000   win32u     (deferred)             
00007ffe`f3940000 00007ffe`f39dd000   msvcp_win   (deferred)             
00007ffe`f3a10000 00007ffe`f3b10000   ucrtbase   (deferred)             
00007ffe`f3b10000 00007ffe`f3e64000   combase    (private pdb symbols)  C:\ProgramData\Dbg\sym\combase.pdb\A552B427FA3F6A4EC85674114824E9B14\combase.pdb
00007ffe`f3f00000 00007ffe`f4026000   RPCRT4     (deferred)             
00007ffe`f4160000 00007ffe`f420f000   ADVAPI32   (deferred)             
00007ffe`f4210000 00007ffe`f42bd000   shcore     (deferred)             
00007ffe`f4320000 00007ffe`f43dd000   KERNEL32   (pdb symbols)          C:\ProgramData\Dbg\sym\kernel32.pdb\B07C97792B439ABC0DF83499536C7AE51\kernel32.pdb
00007ffe`f4590000 00007ffe`f462e000   msvcrt     (deferred)             
00007ffe`f4640000 00007ffe`f47de000   USER32     (deferred)             
00007ffe`f47e0000 00007ffe`f48f4000   MSCTF      (deferred)             
00007ffe`f4a10000 00007ffe`f4add000   OLEAUT32   (deferred)             
00007ffe`f4b40000 00007ffe`f4bab000   WS2_32     (deferred)             
00007ffe`f4bb0000 00007ffe`f4c05000   shlwapi    (deferred)             
00007ffe`f4ca0000 00007ffe`f4d3c000   sechost    (deferred)             
00007ffe`f4d40000 00007ffe`f4de9000   clbcatq    (deferred)             
00007ffe`f52c0000 00007ffe`f5a05000   SHELL32    (deferred)             
00007ffe`f5a10000 00007ffe`f5a40000   IMM32      (deferred)             
00007ffe`f5a40000 00007ffe`f5a6c000   GDI32      (deferred)             
00007ffe`f5ab0000 00007ffe`f5ca8000   ntdll      (pdb symbols)          C:\ProgramData\Dbg\sym\ntdll.pdb\A372A05ADCAA11CA41FD4A931AB93BB33\ntdll.pdb

模块列表显示了此时加载到调试流程中的所有模块(DLL和EXE)。您可以看到每个模块加载到的开始和结束虚拟地址。在模块名称之后,您可以看到此模块的符号状态(在括号中)。可能的值包括:

        •延迟-此调试会话中从未需要此模块的符号,因此目前无法加载。这些将在需要时加载。

        •pdb符号-这意味着适当的公共符号已加载。显示PDB文件的本地路径。

        •导出符号-此DLL仅可用于导出符号。这通常意味着此模块没有符号或尚未找到它们。

        •没有符号-试图定位此模块符号,但未找到任何东西,甚至没有导出的符号(此类模块没有导出符号,如可执行文件和驱动程序文件的情况)。

您可以使用命令.reload /f modulename.dll强制加载模块的符号。这将为该模块的符号可用性提供明确的证据。

也可以在调试器的设置对话框中配置符号路径。

        •打开文件/设置菜单并找到调试设置。然后,您可以添加更多路径进行符号搜索。如果调试您自己的代码,这非常有用,因此您希望调试器搜索可以找到相关PDB文件的目录(见图5-3)。

        •在继续之前,请确保您正确配置了符号。要诊断任何问题,你可以!Sym noisy命令,记录符号加载尝试的详细信息。

回到线程列表-请注意其中一个线程在其数据前面有一个点。就调试器而言,这是当前的线程。这意味着任何发布的涉及未指定线程的命令都将在该线程上工作。此“当前线程”也显示在提示符中-冒号右侧的数字是当前线程索引(在本例中为3)。

        •输入k命令,该命令显示当前线程的堆栈跟踪:

0:003> k
 # Child-SP          RetAddr           Call Site
00 00000001`224ffbd8 00007ffc`204aef5b ntdll!DbgBreakPoint
01 00000001`224ffbe0 00007ffc`1f647974 ntdll!DbgUiRemoteBreakin+0x4b
02 00000001`224ffc10 00007ffc`2044a271 KERNEL32!BaseThreadInitThunk+0x14
03 00000001`224ffc40 00000000`00000000 ntdll!RtlUserThreadStart+0x21

您可以看到此线程上的调用列表(当然仅限用户模式)。上述输出中的堆栈顶部是位于模块ntdll.dll中的函数DbgBreakPoint。带符号的地址的一般格式是模块名称!函数名+偏移。偏移量是可选的,如果它正是该函数的开始,则可能是零。另请注意,模块名称没有扩展名。

在上面的输出中,DbgBreakpoint由DbgUiRemoteBreakIn调用,由BaseThreadInitThunk等调用。

顺便说一句,这个线程是由调试器注入的,以便强行闯入目标。

        •要切换到其他线程,请使用以下命令:∼ns,其中n是线程索引。让我们切换到线程0,然后显示其调用堆栈:

0:003> ~0s
win32u!NtUserGetMessage+0x14:
00007ffc`1c4b1164 c3              ret
0:000> k
 # Child-SP          RetAddr           Call Site
00 00000001`2247f998 00007ffc`1d802fbd win32u!NtUserGetMessage+0x14
01 00000001`2247f9a0 00007ff7`5382449f USER32!GetMessageW+0x2d
02 00000001`2247fa00 00007ff7`5383ae07 notepad!WinMain+0x267
03 00000001`2247fb00 00007ffc`1f647974 notepad!__mainCRTStartup+0x19f
04 00000001`2247fbc0 00007ffc`2044a271 KERNEL32!BaseThreadInitThunk+0x14
05 00000001`2247fbf0 00000000`00000000 ntdll!RtlUserThreadStart+0x21

这是记事本的主要(第一个)线程。堆栈顶部显示了等待UI消息的线程。

        •在不切换到另一个线程的情况下显示另一个线程的调用堆栈的另一种方法是在实际命令之                            前使用波浪号和线程编号。以下输出适用于线程1的堆栈:

0:000> ~1k
 # Child-SP          RetAddr           Call Site
00 00000001`2267f4c8 00007ffc`204301f4 ntdll!NtWaitForWorkViaWorkerFactory+0x14
01 00000001`2267f4d0 00007ffc`1f647974 ntdll!TppWorkerThread+0x274
02 00000001`2267f7c0 00007ffc`2044a271 KERNEL32!BaseThreadInitThunk+0x14
03 00000001`2267f7f0 00000000`00000000 ntdll!RtlUserThreadStart+0x21

•让我们回到线程列表:​​​​​​​​​​​​​​

.  0  Id: 874c.18068 Suspend: 1 Teb: 00000001`2229d000 Unfrozen
   1  Id: 874c.46ac Suspend: 1 Teb: 00000001`222a5000 Unfrozen
   2  Id: 874c.152cc Suspend: 1 Teb: 00000001`222a7000 Unfrozen
#  3  Id: 874c.bb08 Suspend: 1 Teb: 00000001`222ab000 Unfrozen

请注意,该点已移动到线程0(当前线程),显示线程3上的哈希符号(#)。标有(#)的线程是导致最后一个断点的线程(在这种情况下,这是我们最初的调试器附加)。

∼命令提供的线程的基本信息如图5-4所示。

WinDbg报告的大多数数字默认为十六进制。要将值转换为十进制,您可以使用?(评估表达式)命令。

        •键入以下内容以获取十进制进程ID(然后您可以与任务管理器中报告的PID进行比较):

0:000> ? 874c
Evaluate expression: 34636 = 00000000`0000874c

•您可以使用0n前缀表达十进制数字,因此也可以获得反向结果:

​​​​​​​​​​​​​​0:000> ? 0n34636

Evaluate expression: 34636 = 00000000`0000874c

•您可以使用!检查线程的TEB!Teb命令。使用!没有地址的teb显示了当前线程的TEB:

由!显示的一些数据Teb命令相对广为人知:

​​​​​​​

        •StackBase和StackLimit-线程的用户模式堆栈基础和限制。

        • ClientId - 进程和线程ID。

        • LastErrorValue - 最后一个Win32错误代码(GetLastError)。

        • TlsStorage - 此线程的线程本地存储(TLS)阵列(TLS的完整解释超出了本书的范围)。•PEB地址-过程环境块(PEB)的地址,可与!Peb命令。

        • !teb命令(和类似的命令)显示了幕后真实结构的一部分,在这种情况下是_TEB。您始终可以使用dt(显示类型)命令查看真实结构:

  1. 0:000> dt ntdll!_teb
       +0x000 NtTib            : _NT_TIB
       +0x038 EnvironmentPointer : Ptr64 Void
       +0x040 ClientId         : _CLIENT_ID
       +0x050 ActiveRpcHandle  : Ptr64 Void
       +0x058 ThreadLocalStoragePointer : Ptr64 Void
       +0x060 ProcessEnvironmentBlock : Ptr64 _PEB
    

    (truncated)

       +0x1808 LockCount        : Uint4B
       +0x180c WowTebOffset     : Int4B
       +0x1810 ResourceRetValue : Ptr64 Void
       +0x1818 ReservedForWdf   : Ptr64 Void
       +0x1820 ReservedForCrt   : Uint8B
       +0x1828 EffectiveContainerId : _GUID
    

请注意,WinDbg 在涉及符号时不区分大小写。 另外,请注意以下划线开头的结构名称; 这就是 Windows 中定义所有结构的方式(用户模式和内核模式)。 使用 typedef 名称(不带下划线)可能有效,也可能无效,因此建议始终使用下划线。

您如何知道哪个模块定义了您想要查看的结构?如果结构被记录在案,该模块将列在结构的文档中。您还可以尝试在没有模块名称的情况下指定结构,迫使调试器搜索它。一般来说,你“知道”结构是由经验定义的,有时还有上下文。

•如果您将地址附加到上一个命令,您可以获得数据成员的实际值:

0:000> dt ntdll!_teb 00000001`2229d000
   +0x000 NtTib            : _NT_TIB
   +0x038 EnvironmentPointer : (null)
   +0x040 ClientId         : _CLIENT_ID
   +0x050 ActiveRpcHandle  : (null)
   +0x058 ThreadLocalStoragePointer : 0x000001c9`3676c940 Void
   +0x060 ProcessEnvironmentBlock : 0x00000001`2229c000 _PEB
   +0x068 LastErrorValue   : 0

 (truncated)

​​​​​​​   +0x1808 LockCount        : 0
   +0x180c WowTebOffset.    : 0n0
   +0x1810 ResourceRetValue : 0x000001c9`3677fd00 Void
   +0x1818 ReservedForWdf   : (null)
   +0x1820 ReservedForCrt   : 0
   +0x1828 EffectiveContainerId : _GUID {00000000-0000-0000-0000-000000000000}

每个成员都显示其与结构开头的偏移量、名称和值。简单值直接显示,而结构值(如上面的NtTib)通常通过超链接显示。单击此超链接可提供结构的详细信息。

        •单击上面的NtTib成员以显示此数据成员的详细信息:

调试器使用较新的dx命令来查看数据。

如果您没有看到超链接,您可能使用的是非常旧的WinDbg,默认情况下调试器标记语言(DML)不打开。您可以使用.prefer_dml 1命令打开它。

现在让我们把注意力转向断点。让我们在记事本打开文件时设置一个断点。

        •键入以下命令以在CreateFile API函数中设置断点:

0:000> bp kernel32!createfilew

请注意,函数名称实际上是CreateFileW,因为没有名为CreateFile的函数。在代码中,这是一个基于名为UNICODE的编译常量扩展到CreateFileW(宽Unicode版本)或CreateFileA(ASCII或Ansi版本)的宏。WinDbg没有任何回应。这是一件好事。

大多数API有两组函数涉及字符串的原因是历史性的。无论如何,Visual Studio项目默认定义UNICODE常量,因此Unicode是规范。这是一件好事-A函数将其输入转换为Unicode并调用W函数。

        •您可以使用bl命令列出现有的断点:

0:000> bl
  0 e Disable Clear  00007ffc`1f652300  0001 (0001)  0:**** KERNEL32!CreateFileW

您可以看到断点索引(0),无论它是启用还是禁用(e=启用,d=禁用),并且您将获得禁用(bd命令)和删除(bc命令)断点的超链接。

现在让我们让记事本继续执行,直到断点击中:

        •键入g命令或按工具栏上的Go按钮或按F5:

您将看到调试器在提示符中显示Busy,命令区域显示Debuggee正在运行,这意味着您无法在下一次中断之前输入命令。

        •记事本现在应该还活着。转到其文件菜单,然后选择打开...调试器应该喷出模块负载的详细信息,然后打破:

Breakpoint 0 hit
KERNEL32!CreateFileW:
00007ffc`1f652300 ff25aa670500    jmp     qword ptr [KERNEL32!_imp_CreateFileW (0000\7ffc`1f6a8ab0)] ds:00007ffc`1f6a8ab0={KERNELBASE!CreateFileW (00007ffc`1c75e260)}

        •我们已经达到了断点!注意它发生的线程。让我们看看调用堆栈是什么样子的(可能需要一段时间才能显示调试器是否需要从微软的符号服务器下载符号):

 

在这一点上我们能做什么?您可能想知道正在打开什么文件。我们可以根据CreateFileW函数的调用约定获得这些信息。由于这是一个64位进程(处理器是英特尔/AMD),调用约定指出,第一个整数/指针参数在RCX、RDX、R8和R9寄存器中传递。由于CreateFileW中的文件名是第一个参数,因此相关的寄存器是RCX。

您可以在调试器文档(或多个Web资源)中获取有关调用约定的更多信息。

        •使用r命令显示RCX寄存器的值(您将获得不同的值):

0:002> r rcx
rcx=00000001226fabf8

        •我们可以使用各种d(显示)命令查看RCX指向的内存。
 

db命令以字节显示内存,右侧显示ASCII字符。文件名很清楚,但由于字符串是Unicode,所以查看起来并不超级方便。

        •使用du命令更方便地查看Unicode字符串:

0:002> du 00000001226fabf8
00000001`226fabf8  "C:\Windows\Microsoft.NET\Framewo"
00000001`226fac38  "rk64\\v2.0.50727\clr.dll"

        •您可以通过在注册名称前加上@直接使用寄存器值:

0:002> du @rcx
00000001`226fabf8  "C:\Windows\Microsoft.NET\Framewo"
00000001`226fac38  "rk64\\v2.0.50727\clr.dll"

现在让我们在原生API中设置另一个由CreateFileW-NtCreateFile调用的断点:

0:002> bp ntdll!ntcreatefile
0:002> bl
  0 e Disable Clear  00007ffc`1f652300  0001 (0001)  0:**** KERNEL32!CreateFileW
  1 e Disable Clear  00007ffc`20480120  0001 (0001)  0:**** ntdll!NtCreateFile

请注意,原生API从不使用W或A-它始终与Unicode字符串一起工作。

•使用g命令继续执行。调试器应该会崩溃:

•列出即将使用u(拆解)命令执行的后续8条指令:

 

请注意,值0x55被复制到EAX寄存器。这是NtCreateFile的系统服务号,如第1章所述。显示的syscall指令是导致过渡到内核,然后执行NtCreateFile系统服务本身的指令。

        •您可以使用p命令(步骤-点击F10作为替代方案)来执行下一个指令。您可以使用t命令(跟踪-点击F11作为替代方案)进入函数(在组装的情况下,这是调用指令):

•进入系统调用是不可能的,因为我们处于用户模式。当我们跨过/进入时,一切都完成了,我们得到了一个结果。

0:002> p
ntdll!NtCreateFile+0x14:
00007ffc`20480134 c3              ret

•x64调用惯例中函数的返回值存储在EAX或RAX中。对于系统调用,它是NTSTATUS,因此EAX包含返回的状态:

0:002> r eax
eax=c0000034

•我们手上有一个错误。我们可以了解细节!错误命令:

0:002> !error @eax

Error code: (NTSTATUS) 0xc0000034 (3221225524) - Object Name not found.

•禁用所有断点,并让记事本继续正常执行:

0:002> bd *
0:002> g

由于我们目前没有断点,我们可以通过单击工具栏上的Break按钮或按键盘上的Ctrl+Break来强制中断:

874c.16a54): Break instruction exception - code 80000003 (first chance)
ntdll!DbgBreakPoint:
00007ffc`20483080 cc              int     3

•注意提示中的线程编号。显示所有当前线程:

0:022> ~
   0  Id: 874c.18068 Suspend: 1 Teb: 00000001`2229d000 Unfrozen
   1  Id: 874c.46ac Suspend: 1 Teb: 00000001`222a5000 Unfrozen
   2  Id: 874c.152cc Suspend: 1 Teb: 00000001`222a7000 Unfrozen
   3  Id: 874c.f7ec Suspend: 1 Teb: 00000001`222ad000 Unfrozen
   4  Id: 874c.145b4 Suspend: 1 Teb: 00000001`222af000 Unfrozen

(truncated)

  18  Id: 874c.f0c4 Suspend: 1 Teb: 00000001`222d1000 Unfrozen
  19  Id: 874c.17414 Suspend: 1 Teb: 00000001`222d3000 Unfrozen
  20  Id: 874c.c878 Suspend: 1 Teb: 00000001`222d5000 Unfrozen
  21  Id: 874c.d8c0 Suspend: 1 Teb: 00000001`222d7000 Unfrozen
. 22  Id: 874c.16a54 Suspend: 1 Teb: 00000001`222e1000 Unfrozen
  23  Id: 874c.10838 Suspend: 1 Teb: 00000001`222db000 Unfrozen
  24  Id: 874c.10cf0 Suspend: 1 Teb: 00000001`222dd000 Unfrozen

很多线程,对吗?这些实际上是由常见的开放式对话框创建/调用的,因此不是记事本的直接错误。

        •继续以您想要的任何方式探索调试器!

了解NtWriteFile和NtReadFile的系统服务编号。

•如果您关闭记事本,您将在进程终止时遇到一个断点:

•你可以使用q命令来终止调试器。如果这个过程是静止的,它将被终止。另一种方法是使用.detach命令在不杀死目标的情况下断开与目标的连接。

内核调试

用户模式调试涉及调试器附加到进程,设置导致进程线程暂停的断点等。另一方面,内核模式调试涉及使用调试器控制整个机器。这意味着,如果设置断点,然后击中,整个机器就会冻结。显然,这不能用一台机器来实现。在完整的内核调试中,涉及两台机器:一台主机(调试器运行的地方)和一个目标(正在调试)。然而,目标可以是托管在调试器执行的同一台机器(主机)上的虚拟机。图5-5显示了通过某种连接介质连接的主机和目标。

在我们进入完整的内核调试之前,我们将看看其更简单的表亲-本地内核调试。

本地内核调试

本地内核调试(LKD)允许查看本地机器上的系统内存和其他系统信息。本地和全内核调试的主要区别在于,使用LKD无法设置断点,这意味着您总是在查看系统的当前状态。这也意味着即使在执行命令时,事情也会发生变化,因此一些信息可能不可靠。通过完整的内核调试,只有在目标系统处于断点时才能输入命令,因此系统状态保持不变。

要配置LKD,请在提升的命令提示符中输入以下内容,然后重新启动系统:

bcdedit /debug on

系统重新启动后,以更高的特权启动WinDbg。选择菜单文件/添加到内核(WinDbg预览)或文件/内核调试...(经典WinDbg)。选择本地选项卡,然后单击确定。您应该会看到类似于以下内容的输出:


Microsoft (R) Windows Debugger Version 10.0.18317.1001 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
Connected to Windows 10 18362 x64 target at (Sun Apr 21 08:50:59.964 2019 (UTC + 3:0\
0)), ptr64 TRUE
************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       SRV*c:\Symbols*http://msdl.microsoft.\
com/download/symbols
Symbol search path is: c:\temp;SRV*c:\Symbols*http://msdl.microsoft.com/download/sym\
bols
Executable search path is:
Windows 10 Kernel Version 18362 MP (12 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 18362.1.amd64fre.19h1_release.190318-1202
Machine Name:
Kernel base = 0xfffff806`466b8000 PsLoadedModuleList = 0xfffff806`46afb2d0
Debug session time: Sun Apr 21 08:51:00.702 2019 (UTC + 3:00)
System Uptime: 0 days 11:33:37.265

本地内核调试受Windows 10、Server 2016及更高版本上的安全启动保护。要激活LKD,您必须在机器的BIOS设置中禁用安全启动。如果出于任何原因,这是不可能的,可以使用Sysinternals LiveKd工具。将LiveKd.exe复制到Windows主目录的调试工具。然后使用LiveKd启动WinDbg,使用以下命令:livekd -w。

注意提示显示lkd。这表明本地内核调试处于活动状态。

本地内核调试教程

如果您熟悉内核调试命令,您可以安全地跳过本节。

        •您可以使用进程0 0命令显示系统上运行的所有进程的基本信息:

lkd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS ffff8d0e682a73c0
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001ad002  ObjectTable: ffffe20712204b80  HandleCount: 9542.
    Image: System
PROCESS ffff8d0e6832e140
    SessionId: none  Cid: 0058    Peb: 00000000  ParentCid: 0004
    DirBase: 03188002  ObjectTable: ffffe2071220cac0  HandleCount:   0.
    Image: Secure System
PROCESS ffff8d0e683f1080
    SessionId: none  Cid: 0098    Peb: 00000000  ParentCid: 0004
    DirBase: 003e1002  ObjectTable: ffffe20712209480  HandleCount:   0.
    Image: Registry
PROCESS ffff8d0e83099080
    SessionId: none  Cid: 032c    Peb: 5aba7eb000  ParentCid: 0004
    DirBase: 15fa39002  ObjectTable: ffffe20712970080  HandleCount:  53.
    Image: smss.exe
(truncated)

对于每个过程,都会显示以下信息:

        •进程文本所附地址是进程的EPROCESS地址(当然是在内核空间中)。

        • SessionId - 进程正在运行的会话。

        •Cid-(客户端ID)唯一的进程ID。

        • Peb - 过程环境块(PEB)的地址。当然,这个地址在用户空间中。

        • ParentCid - (父客户端ID)父进程的进程ID。请注意,父进程可能不再存在,此ID可以重复使用。

        • DirBase - 此过程中主页面目录的物理地址(没有下12位),用作虚拟寻址翻译的基础。在x64上,这被称为页面地图级别4,在x86上,它被称为页面目录指针表(PDPT)。

        • ObjectTable - 进程专用句柄表的指针。

        • HandleCount - 此过程中的句柄数量。

        •图像-可执行文件名称,或与可执行文件无关的特殊进程名称(例如:安全系统、系统、Mem压缩)。

!进程命令至少接受两个参数。第一个使用其EPROCESS地址表示感兴趣的过程,其中零表示“所有或任何过程”。第二个参数是所需的细节级别,其中零意味着最小的细节量(有点掩码)。可以添加第三个参数来搜索特定的可执行文件。

        •列出运行csrss.exe的所有进程:

lkd> !process 0 0 csrss.exe
PROCESS ffff8d0e83c020c0
    SessionId: 0  Cid: 038c    Peb: f599af6000  ParentCid: 0384
    DirBase: 844eaa002  ObjectTable: ffffe20712345480  HandleCount: 992.
    Image: csrss.exe
PROCESS ffff8d0e849df080
    SessionId: 1  Cid: 045c    Peb: e8a8c9c000  ParentCid: 0438
    DirBase: 17afc1002  ObjectTable: ffffe207186d93c0  HandleCount: 1146.
    Image: csrss.exe

•通过指定其地址和更高级别的细节来列出特定流程的更多信息:

从上述输出中可以看出,会显示有关该过程的更多信息。其中一些信息是超链接的,便于进一步检查。这个过程的一部分(如果有的话)的工作是超链接的。

        •单击工作地址超链接:

Job是一个包含一个或多个流程的对象,它可以应用各种限制并监控各种会计信息。对Jobs的详细讨论超出了本书的范围。更多信息可以在Windows内部书籍中找到。

        •像往常一样,一个命令,比如!工作隐藏了真实数据结构中的一些可用信息。在这种情况下,是EJOB。使用dt nt命令!_Ejob与工作地址查看所有细节。

        •通过单击其超链接也可以查看进程的PEB。这类似于!在用户模式下使用的peb命令,但这里的转折是必须首先设置正确的进程上下文,因为地址在用户空间中。点击Peb超链接。你应该看到这样的东西:

lkd> .process /p ffff8d0e849df080; !peb e8a8c9c000
Implicit process is now ffff8d0e`849df080
PEB at 000000e8a8c9c000
    InheritedAddressSpace:    No
    ReadImageFileExecOptions: No
    BeingDebugged:            No
    ImageBaseAddress:         00007ff62fc70000
    NtGlobalFlag:             4400
    NtGlobalFlag2:            0
    Ldr                       00007ffa0ecc53c0
    Ldr.Initialized:          Yes
    Ldr.InInitializationOrderModuleList:     000002021cc04dc0 . 000002021cc15f00
    Ldr.InLoadOrderModuleList:               000002021cc04f30 . 000002021cc15ee0
    Ldr.InMemoryOrderModuleList:             000002021cc04f40 . 000002021cc15ef0
                Base TimeStamp                        Module
7ff62fc70000 78facb67 Apr 27 01:06:31 2034 C:\WINDOWS\system32\csrss.exe
7ffa0eb60000 a52b7c6a Oct 23 22:22:18 2057 C:\WINDOWS\SYSTEM32\ntdll.dll
7ffa0ba10000 802fce16 Feb 24 11:29:58 2038 C:\WINDOWS\SYSTEM32\CSRSRV.dll
7ffa0b9f0000 94c740f0 Feb 04 23:17:36 2049 C:\WINDOWS\system32\basesrv.D\
LL
(truncated)

使用.process meta命令设置正确的进程上下文,然后显示PEB。这是您需要用来显示用户空间中信息的通用技术。

        •重复!再次处理命令,但这次没有细节级别。显示了有关该过程的更多信息:

该命令列出了进程中的所有线程。每个线程都由其附加到文本“THREAD”的ETHREAD地址表示。调用堆栈也列出了——模块前缀“nt”代表内核——无需使用“真实”内核模块名称。

使用“nt”而不是明确说明内核模块名称的原因之一是因为 64 位和 32 位系统之间的模块名称不同(64 位上为 ntoskrnl.exe,32 位上至少有两个变体)。 而且它短了很多。

        •默认情况下不会加载用户模式符号,因此跨越用户模式的线程堆栈仅显示数字地址。您可以使用.reload /user显式加载用户符号:

线程的信息可以通过!单独查看!线程命令和线程地址。查看调试器文档,了解此命令显示的各种信息的描述。

内核模式调试中其他通常有用/有趣的命令包括:

        • !Pcr - 显示指定为附加索引的处理器的过程控制区域(PCR)(如果没有指定索引,默认显示处理器0)。

        • !Vm - 显示系统和进程的内存统计信息。

        • !运行-显示系统上所有处理器上运行的线程的信息。

我们将在后面的章节中查看对调试驱动程序有用的更具体的命令。

完整的内核调试

完整的内核调试需要在主机和目标上进行配置。在本节中,我们将了解如何将虚拟机配置为内核调试的目标。这是内核驱动程序工作的推荐和最方便的设置(当不为硬件开发设备驱动程序时)。我们将完成配置Hyper-V虚拟机(VM)的步骤。如果您正在使用不同的虚拟化技术(例如VMWare或VirtualBox),请参阅该供应商的文档或网络,以获取正确的程序以获得相同的结果。

目标和主机必须使用一些连接介质进行通信。有几种选择可供选择。最好的选择是使用网络。不幸的是,这至少需要主机和目标运行Windows 8。由于Windows 7仍然是一个可行的目标,我们将使用另一个选项-COM(串行)端口。当然,大多数机器不再有串行端口,无论如何,我们连接到虚拟机,因此不涉及真正的电缆。所有虚拟化平台都允许将虚拟串行端口重定向到主机上的命名管道;这是我们将使用的配置。

配置目标

目标虚拟机必须配置为内核调试,类似于本地内核调试,但添加的连接介质设置为该机器上的虚拟串行端口。

配置的一种方法是在提升的命令窗口中使用bcdedit:

bcdedit /debug on
bcdedit /dbgsettings serial debugport:1 baudrate:115200

根据实际的虚拟序列号(通常为1)更改调试端口号。

必须重新启动虚拟机才能使这些配置生效。在此之前,我们可以将串行端口映射到命名管道。以下是Hyper-V虚拟机的程序:

        •如果Hyper-V VM是第1代(较旧),则VM的设置中有一个简单的UI来进行配置。如果没有定义,请使用“添加硬件”选项添加串行端口。然后将串行端口配置为映射到您选择的命名端口。图5-6显示了这个对话框。

        •对于第2代虚拟机,目前没有UI。要配置这一点,请确保虚拟机已关闭(尽管在最新的Windows 10版本中不是强制性的),并打开一个提升的PowerShell窗口。

        •键入以下内容以设置映射到命名管道的串行端口:

Set-VMComPort myvmname -Number 1 -Path \\.\pipe\debug

使用bcdedit适当更改虚拟机名称和之前在虚拟机内设置的COM端口号。确保管道路径是独一无二的。

        •您可以使用Get-VMComPort验证设置是否正常:

Get-VMComPort myvmname
VMName   Name  Path
------   ----  ----
myvmname COM 1 \\.\pipe\debug
myvmname COM 2

您可以启动虚拟机-目标现已准备就绪。

配置主机

内核调试器必须配置为与映射到主机上暴露的相同名称管道的同一串行端口上的虚拟机连接。

        •启动内核调试器并选择文件/附件到内核。导航到COM选项卡。填写目标上设置的正确细节。图5-7显示了这些设置的样子。

•单击OK。调试器应连接到目标。Ifitdoesnot,单击Breaktoolbar按钮。以下是一个典型的输出:

请注意,提示有一个索引和单词kd。索引是导致中断的当前处理器。此时,目标虚拟机已完全冻结。您现在可以正常调试,请记住,每当您在某个地方损坏时,整个机器都会暂停。

内核驱动程序调试教程

一旦主机和目标连接,就可以开始调试。我们将使用我们在第4章中开发的PriorityBooster驱动程序来演示完整的内核调试。

        •如第4章所做的那样,在目标上安装(但不要加载)驱动程序。确保将驱动程序的PDB文件与驱动程序SYS文件本身一起复制。这简化了为司机获取正确的符号。

        •让我们在DriverEntry中设置一个断点。我们无法加载驱动程序,因为这会导致DriverEntry执行,我们将错过在那里设置断点的机会。由于驱动程序尚未加载,我们可以使用bu命令(未解决的断点)来设置未来的断点。如果目标当前正在运行,请闯入目标,并键入以下命令:

0: kd> bu prioritybooster!driverentry
0: kd> bl
   0 e Disable Clear u        0001 (0001) (prioritybooster!driverentry)

断点尚未解决,因为我们的模块尚未加载。

        •发出g命令让目标继续,并用sc启动助推器加载驱动程序(假设驱动程序的名字是助推器)。如果一切顺利,断点应击中,源文件应自动加载,在命令窗口中显示以下输出:

0: kd> g
Breakpoint 0 hit
PriorityBooster!DriverEntry:
fffff801`358211d0 4889542410      mov     qword ptr [rsp+10h],rdx

图5-8显示了WinDbg预览源窗口的屏幕截图,并标记了正确的行。当地人窗口也按预期显示。

此时,您可以跨过源线,查看Locals窗口中的变量,甚至向Watch窗口添加表达式。您还可以使用本地窗口更改值,就像通常使用其他调试器一样。

命令窗口仍然一如既往地可用,但使用UI,一些操作只是更容易。例如,设置断点可以使用正常的bp命令完成,但您可以简单地打开源文件(如果它尚未打开),转到要设置断点的行,然后点击F9或单击工具栏上的相应按钮。无论哪种方式,bp命令都将在命令窗口中执行。断点窗口可以作为当前设置的断点的快速概述

        •发出k命令以查看DriverEntry是如何被调用的:

2: kd> k
 # Child-SP          RetAddr           Call Site
00 ffffad08`226df898 fffff801`35825020 PriorityBooster!DriverEntry [c:\dev\priorityb\
ooster\prioritybooster\prioritybooster.cpp @ 14]
01 ffffad08`226df8a0 fffff801`37111436 PriorityBooster!GsDriverEntry+0x20 [minkernel\
\tools\gs_support\kmodefastfail\gs_driverentry.c @ 47]
02 ffffad08`226df8d0 fffff801`37110e6e nt!IopLoadDriver+0x4c2
03 ffffad08`226dfab0 fffff801`36ab7835 nt!IopLoadUnloadDriver+0x4e
04 ffffad08`226dfaf0 fffff801`36b39925 nt!ExpWorkerThread+0x105
05 ffffad08`226dfb90 fffff801`36bccd5a nt!PspSystemThreadStartup+0x55
06 ffffad08`226dfbe0 00000000`00000000 nt!KiStartSystemThread+0x2a

如果断点似乎无法设置,这可能是一个符号问题。执行.reload命令,看看问题是否得到解决。在用户空间中设置断点也是可能的,但首先执行.reload /user来帮助做到这一点。

可能只有当特定进程是执行代码的进程时才应该击中断点。这可以通过将/p开关添加到断点来完成。在以下示例中,只有当进程是 explorer.exe时,才会设置断点:

2: kd> !process 0 0 explorer.exe
PROCESS ffffdd06042e4080
    SessionId: 2  Cid: 1df8    Peb: 00dee000  ParentCid: 1dd8
    DirBase: 1bf58a002  ObjectTable: ffff960a682133c0  HandleCount: 3504.
    Image: explorer.exe
2: kd> bp /p ffffdd06042e4080 prioritybooster!priorityboosterdevicecontrol
2: kd> bl
     0 e Disable Clear  fffff801`358211d0  [c:\dev\prioritybooster\prioritybooster\p\
rioritybooster.cpp @ 14]     0001 (0001) PriorityBooster!DriverEntry
     1 e Disable Clear  fffff801`35821040  [c:\dev\prioritybooster\prioritybooster\p\
rioritybooster.cpp @ 63]     0001 (0001) PriorityBooster!PriorityBoosterDeviceContro\
l
     Match process data ffffdd06`042e4080

让我们通过在源视图中击中F9来实现I/Ocontrolcode的正常断点,如图5-9所示(并通过在该线上击中F9来消除条件进程断点)。

        •使用一些线程ID和优先级运行测试应用程序:

booster 2000 30

断点应该击中。您可以通过源代码和命令的组合继续正常调试。

总结

在这一章中,我们研究了使用WinDbg调试的基础知识。这是开发的一项基本技能,因为包括内核驱动程序在内的各种软件都可能存在错误。

在下一章中,我们将深入研究一些我们需要熟悉的内核机制,因为这些机制在开发和调试驱动程序时经常出现。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值