2023年1月

WinDbg的alias命令(as, aS)在script里面很有用,但是WinDbg的script不算是一种设计良好的语言,一般在写WinDbg script总会遇到各种坑,就包括alias命令的求值。

与变量相比,WinDbg的alias更像是C语言的宏。他可以把一个名字定义成指定的字符串,环境变量,给定地址的字符串,甚至表达式的值或者WinDbg命令的输出字符串。C语言的宏仅在定义点到文件结束有效(如果后面没有undef),WinDbg的alias的求值则 下面解释下这个过程。

假设已经有alias (aS foo bar), 在运行命令 .echo foo 或者 .echo ${foo} (使用后者的可以对alias name提供显示的tokenize和参数,比如${/v:foo},参考WinDbg帮助)。在运行 .echo foo 命令,WinDbg会对整个命令字符串执行alias替换,替换完成在执行整个命令,运行栈如下:


dbgeng!ReplaceAliases
dbgeng
!PreprocessExternalStrBuf
dbgeng
!PreprocessExternalString
dbgeng
!Execute
dbgeng
!DebugClient::ExecuteWide
...

工作区对我来说总是有点混乱。我知道如何说服他们做我需要做的工作,但他们仍然有点神秘。最近我决定解决这个问题,只是为了知道他们是如何在幕后工作的。但在我向您展示我的调查之前,让我们讨论不同类型的工作区。Windbg使用几种内置类型,包括Base、User、Kernel、Remote、Processor Architecture、Per Dump和Per Executable。它还使用命名工作空间(或用户定义的工作空间)。当您执行特定类型的调试(例如实时用户模式、事后转储分析等)时,这些工作区将合并到最终环境中。这里有一个图表来说明工作区的可能组合。

  • 绿线是使用WinDbg打开转储文件的情况。在这种情况下,将使用基本工作区+每个转储工作区。注意:每个转储只意味着打开的每个转储文件都有自己的工作区。
  • 蓝线是使用WinDbg使用Base+User模式工作区实时调试正在运行的应用程序的场景。
  • 橙色线是WinDbg的一个例子,它用于在x86机器上执行实时内核调试。在本例中,windbg使用的是Base+Kernel+x86工作区。

从图中可以看到windbg通常使用两个工作区的组合。当实时内核调试时,它使用三个工作区。

那么工作空间里是什么呢?

  • 会话信息
    包括所有断点(bp)和异常和事件处理信息(sx设置);所有打开的源文件;所有用户定义的别名。
  • 配置设置
    包括符号路径;可执行映像路径;源路径;使用l+,l-(设置源选项)设置的当前源选项;日志文件设置;COM或1394内核连接设置(如果连接是使用图形界面启动的);每个打开的对话框中的最新路径(不保存工作区文件和文本文件路径);当前的.enable_unicode、.force_radix_output和.enable_long_status设置。
  • WinDbg图形界面
    WinDbg窗口的标题;自动打开反汇编设置;默认字体;桌面上WinDbg窗口的大小和位置;打开了哪些调试信息窗口;每个打开的窗口的大小和位置,包括窗口的大小、其浮动或停靠状态、是否与其他窗口有选项卡,以及其快捷菜单中的所有相关设置;调试器命令窗口中窗格边界的位置以及该窗口中的换行设置;工具栏和状态栏以及每个调试信息窗口上的各个工具栏是否可见;“寄存器”窗口的自定义;Calls窗口、Locals窗口和Watch窗口中的标志;在监视窗口中查看的项目;每个源窗口中的光标位置。

所有这些设置(蓝色设置除外)都是累积应用的(首先是基本设置,然后是下一个工作区等)。上面的蓝色项仅从链中的最后一个工作区加载。为了在实际操作中显示这一点,我创建了一个简单的演练,以演示调试器工作区的使用。
首先,我没有使用任何命令行选项就打开了windbg。当它在此休眠状态下打开时(没有附加到任何内容,也没有打开任何内容),它将使用基本工作区。如果我没有更改任何内容(例如窗口位置),则在开始调试时不会提示我使用任何工作区对话框。但是,如果我将调试器的主窗口移动到任何位置(我们将调用此位置1),然后执行下面突出显示的任何操作-

我收到这个对话框的提示-

在上面的对话框中选择“是”将我的更改集成到“基本”工作区中,因此窗口位置1现在是基本工作区的一部分。

现在我要选择“Open Executable”并浏览到我们原来忠实的目标notepad.exe。打开二进制文件后,windbg使用Base+Notepad(每个可执行文件)。现在,我将再次移动调试器的主窗口(我们将调用此位置2),并选择“调试>停止调试”选项。由于窗口位置更改,系统将提示我以下内容-

调试问题时可能面临的一个常见任务是记录有关对一个或多个函数的调用的信息。如果你想知道你的程序中有一个你有源代码的函数,你可以添加一些调试打印和重建程序,有时这是不实际的。例如,您可能不总是能够重现一个问题,因此可能不可行的是,必须重新启动调试生成,因为您可能会吹走您的重现。或者,更重要的是,您可能需要记录对没有源代码的函数的调用(或者不作为程序的一部分构建,或者不想修改)。
例如,您可能希望记录对各种Windows api的调用,以便获得有关正在排除故障的问题的信息。现在,根据您正在做的工作,您可以在每次调用特定API之前和之后添加调试打印来完成这项工作。但是,这通常不太方便,如果您不是要记录的函数的直接调用方,那么无论如何,您都不能走这条路。
有许多API spy/API日志记录软件包(Windows发行版的调试工具甚至附带了一个名为Logger的软件包,尽管它往往相当脆弱——就我个人而言,我经常遇到它崩溃,而不是实际工作中遇到的问题)。尽管您可以使用其中的一个,但是“收缩包装”日志工具的一个很大的限制是,它们不知道如何正确地记录对自定义函数的调用,或者日志工具不知道的函数。更好的日志工具在一定程度上是用户可扩展的,因为它们通常提供某种脚本语言或编程语言,允许用户(即您)描述函数参数和调用约定,以便对它们进行日志记录。
但是,通常很难(甚至不可能)向这些工具描述许多类型的函数,例如包含指向包含指向其他结构的指针的结构的指针的函数,或其他此类不重要的结构。因此,在许多情况下,我倾向于建议不要在需要记录对函数的调用的情况下使用所谓的“收缩包装”API日志工具。

但是,如果在源代码中实现调试打印不是一个可行的解决方案,那么表面上看,这会使一个没有可用的解决方案来记录调用。事实上并非如此——事实证明,通过谨慎地使用所谓的“条件断点”,您可以经常使用调试器(例如WinDbg/ntsd/cdb/kd,这是本文其余部分将要提到的)来提供这种调用日志记录。使用调试器有许多优点;例如,您可以“动态”执行此类API日志记录,并且在可以在进程启动后附加调试器的情况下,您甚至不需要特别启动程序来记录它。然而,更好的是,调试器对以有意义的形式向用户显示数据有广泛的支持。
如果你仔细想想,向用户显示数据实际上是调试器的主要功能之一。这也是调试器通过扩展具有高度可扩展性的主要原因之一,这样复杂的数据结构就可以以有意义的方式显示和解释。通过使用调试器来执行API日志记录,您可以利用丰富的功能来显示已烘焙到调试器中的数据(及其扩展,甚至是您自己编写的任何自定义扩展),从而兼作调用日志记录功能。
更好的是,因为调试器可以基于符号文件(如果您有私有符号,例如您编译或提供的程序)以有意义的方式读取和显示许多数据类型,而这些数据类型没有用于显示它们的特定调试器扩展名(如!把手!错误(错误代码)!devobj和soforth),通常可以利用调试器基于符号中的类型信息格式化数据的能力。这通常是通过dt命令完成的,并且通常为大多数自定义数据类型提供一个可行的显示,而不必像处理日志程序那样进行任何复杂的“训练”。(某些数据结构(如树和列表)可能需要比dt中提供的更多的智能来显示数据结构的所有部分。对于“container”数据类型,这通常是正确的,尽管即使对于那些类型,您仍然可以经常使用dt以有意义的方式显示容器中的实际成员。)利用符号文件(通过调试器)中包含的信息进行API日志记录也使您不必确保日志记录程序对所有结构和其他类型的定义与您的程序同步正在调试,因为调试器自动接收基于符号的正确定义(如果使用的符号服务器包含自己内部符号的索引版本,调试器甚至可以自己找到符号)。

这种方法的另一个优点是,如果您对调试器相当熟悉,那么您可能不必像使用API日志程序那样学习新的描述语言。这是因为您可能已经熟悉了调试器从每天的调试器使用中为显示数据而提供的许多命令。(即使您对调试器不太熟悉,但默认情况下调试器附带的大量文档说明了如何通过各种调试器命令格式化和显示数据。此外,还有许多示例描述了如何在Internet上使用大多数重要或有用的调试器命令。)
好吧,关于为什么要考虑使用调试器来执行调用日志记录已经足够了。下一次,快速浏览并逐步介绍如何做到这一点(正如前面提到的那样,这非常简单),以及一些可能需要注意的注意事项和问题。

使用调试器执行调用日志记录并没有那么困难。所涉及的基本思想是在您感兴趣的函数的开头设置一个“条件”断点(例如,通过bp命令)。从那里,断点可以有显示输入参数的命令。但是,在某些情况下(例如,显示返回值、输出参数中的值等),您也可以变得更聪明一些,尽管根据正在调试的程序的特性,这可能是问题,也可能不是问题。
举一个简单的例子,我的意思是,有一个经典的“显示通过Win32 CreateFile打开的所有文件”。为此,方法是在kernel32!CreateFileW上设置一个断点。(请记住,大多数“A”win32api都会跳到“W”api,因此您通常可以仅在“W”版本上设置一个断点来同时获取这两个api。当然,这并不总是正确的(有些bizzare api,比如WinInet,实际上是从“W”到“A”的重击),但作为一般的经验法则,情况往往是这样的。)kernel32断点需要具备如何根据所讨论例程的调用约定显示第一个参数的知识。因为CreateFile是stdcall,所以应该是[esp+4](对于x86)和rcx(对于x64)。

在最基本的情况下,breakpoint命令可能如下所示:

0:001> bp kernel32!CreateFileW "du poi(@esp+4) ; gc"

当我们用Windbg打开一个exe时,调试器第一次中断:

 

输入kb查看当前栈如下:

0:000>kb#ChildEBP RetAddr  Args to Child
00 00fff8bc 77d498e0 5e7dcb19 0105b000 00000000 ntdll!LdrpDoDebuggerBreak+0x2b
01 00fffb18 77d05257 5e7dcb71 00000000 00000000 ntdll!LdrpInitializeProcess+0x1b20
02 00fffb70 77d05151 00000000 00000000 00000000 ntdll!_LdrpInitialize+0xb0
03 00fffb7c 00000000 00fffb90 77ca0000 00000000 ntdll!LdrInitializeThunk+0x11