WinDBG扩展命令的工作原理
要理解WinDBG扩展命令的原理应该先理解WinDBG软件的架构,下图是以模块为单位所画出的架构示意图。最上层是四个EXE模块,它们提供了不同形式的用户接口,简称UI层。中间是调试引擎模块,它是WinDBG调试器的核心模块,大多数调试器的工作逻辑都包含在这个模块中。调试引擎下面是几个支撑模块。内核态的部分是操作系统提供的调试支持。
因此以调试引擎为中心,向上看是调试器与用户的接口,向下看便是与系统的接口。调试器作为一种相对独立的软件产品,保持其核心代码的稳定性和广泛的适用性是很重要的。像WinDBG这样的调试器,它要支持很多种版本的内核和应用程序,因此,保持这两个接口的稳定性是很重要的。向下的接口基本上是靠API的稳定性来保证的。
图1 WinDBG调试器的模块架构(摘自《软件调试》图29-13)
那么调试引擎和UI层之间使用的是什么样的借口呢?答案是类似COM接口的函数指针表。之所以说类似COM接口,是因为借鉴了COM接口的做法,但是又不是使用严格意义上的COM技术。
使用Depends工具观察dbgeng.dll(图2),可以看到这个DLL只输出了三个函数,分别是DebugConnect、DebugConnectWide和DebugCreate。
图2 DbgEng.dll模块所输出的函数
前两者只是参数中字符串类型(是否UNICODE)的差异,因此可以统一称为DebugConnect。事实上,DbgEng输出的这两组函数都是用于获取一组用户接口(Client Interface),DebugCreate用于创建一个本地调试引擎提供的服务接口,DebugConnect用于获取一个远程调试引擎提供的服务接口,或者说后者是为了支持远程调试而设计的。因此,它们的函数原型也只是DebugConnect多一个用来指定远程调试选项的参数RemoteOptions:
HRESULT DebugCreate( IN REFIID InterfaceId, OUT PVOID * Interface );
HRESULT DebugConnect( IN PCSTR RemoteOptions, IN REFIID InterfaceId, OUT PVOID * Interface );
其中的InterfaceId是要获取服务接口的接口ID(IID),随着WinDBG的发展,目前共有五个版本的接口,分别是IDebugClient,IDebugClient2~IDebugClient5。WinDBG SDK的头文件dbgeng.h( 位于sdk\inc目录中)中给出了这些接口的详细定义。
举例来说,下面的代码便申明了一个指向IDebugClient接口的指针,然后调用DebugCreate“获得”了一套服务接口。
IDebugClient * debugClient;
HRESULT Hr = DebugCreate( __uuidof(IDebugClient), (void **)&debugClient );
事实上,DebugCreate内部实现并不复杂,可以简单的理解为把debugClient指向了调试引擎已经创建好的全局变量。因为这个原因,这样获得的接口根本不需要释放,也不会有任何内存泄漏问题。
获得了IDebugClient接口后,便可以进一步获得其它接口,或者使用的它的方法。
讲到这里,我们就大概知道了UI层的写法,它们根据是否是远程调试来使用DebugConnect或者DebugCreate获得调试引擎的服务接口,然后把用户的命令传递给调试引擎,并把调试引擎的输出呈现给用户。这样看来,在调试引擎的基础上,写一个MyWinDBG.exe也就是几天的工夫。
写到这里该切切题了,我们要介绍的是WinDBG的扩展命令。首先,为什么要提供扩展命令这种机制呢?因为UI层提供的功能不够用,因此WinDBG允许用户写一个扩展模块来弥补UI层的不足。这个扩展模块被加载到UI层的EXE进程当中,与UI层的代码有着同等的地位,可以使用调试引擎的服务做更多的事情。
接下来的问题是如何写?答案是有多种方法。
第一种方法是直接使用上面的介绍的调试引擎服务接口,这样可以访问调试引擎的全部功能,所以这是最强大的一种方式。WinDBG的SDK将这种方式称为DbgEng扩展。SDK目录中的sdk\samples\exts\dbgexts示例使用的就是这种方式。
使用这种方法的扩展命令模块一定要输出一个以下原型和名称的函数:
HRESULT CALLBACK DebugExtensionInitialize(PULONG Version, PULONG Flags);
当WinDBG加载扩展命令时会调用这个函数来初始化这个命令。在这个函数内部,通常应该调用DebugConnect或者DebugCreate获得调试引擎的服务接口。
上面的方式要求对调试引擎有比较深入的理解,对COM接口定义不晕(有些程序员一见IXXX就晕)。这两个要求对于很多人有些太高,因此便有了简单一些的第二种方法。
与第一种方法所使用的接口形式不同,第二种方法使用的是函数形式。WinDBG SDK中公布了一系列函数,让扩展命令模块来调用。比如dprintf函数用于输出信息;GetContext/SetContext用于访问上下文;ReadMemory/WriteMemory用于访问内存;等等。这种方法是WinDBG的早期版本就开始使用的方法,被称为WdbgExts。sdk\samples\simplext目录中的示例和《软件调试》中的LBR命令使用的都是这种方法。
使用第二种方法编写的扩展模块一定要输出以下函数:
VOID WinDbgExtensionDllInit(
PWINDBG_EXTENSION_APIS lpExtensionApis,
USHORT MajorVersion, USHORT MinorVersion )
当WinDBG加载这个扩展模块时,会调用这个函数,将一个函数地址表的地址通过lpExtensionApis参数传递进来。这个函数内部通常应该将这个地址保存到全局变量中。
VOID WinDbgExtensionDllInit(
PWINDBG_EXTENSION_APIS lpExtensionApis,
USHORT MajorVersion, USHORT MinorVersion )
{
ExtensionApis = *lpExtensionApis;
SavedMajorVersion = MajorVersion;
SavedMinorVersion = MinorVersion;
return;
}
事实上,上面的WdbgExts函数其实就是针对这个结构中函数的宏,例如:
#define dprintf (ExtensionApis.lpOutputRoutine)
#define ReadMemory (ExtensionApis.lpReadProcessMemoryRoutine)
第二种方法的优点是相对简单,但是也有明显的缺点,那就是只能使用ExtensionApis数组中定义的函数,因此完全发挥调试引擎的功能。
前面两种方法一种是COM接口方法,一种是C函数方法,前者似乎太新潮(姑且如此称呼),后者似乎太古老。于是在2005年10月,WinDBG的6.5.3.8版本引入了编写扩展模块的第三种方法,称为EngExtCpp(《软件调试》表29-2)。
简单来说,EngExtCpp方法使用的C++类框架思想,与MFC、ATL等是一个套路。EngExtCpp的基类是ExtException。扩展模块应该从这个基类来派生一个子类,然后实现自己的扩展命令。sdk/samples/extcpp目录下的extcpp模块就是使用的这种方法。以下是它的类定义:
class EXT_CLASS : public ExtExtension
{
public:
EXT_COMMAND_METHOD(ummods);
};
其中EXT_CLASS 是一个宏,其定义为:
#ifndef EXT_CLASS
#define EXT_CLASS Extension
#endif
因此Extension是默认的类名,如果要使用其它名称,那么应该在包含头文件engextcpp.hpp前定义这个宏,比如:
#define EXT_CLASS AdvDBGExtension
EngExtCpp的类框架中定义了一些类以满足典型的需要,它的全部源代码包含在sdk\inc\engextcpp.cpp文件中。它的代码已经被编译成一个lib文件放在sdk\lib目录下,会被链接到使用这种方法的扩展模块中:
LINKLIBS = $(DBGLIB_LIB_PATH)\engextcpp.lib
归纳一下,WinDBG为我们提供了三种编写扩展模块的方法,各有特色和优缺点。我们可以根据需要和自己的偏好作出选择。喜欢用C++类库的可以使用第三种(EngExtCpp),喜欢用C函数的可以用第二种(WdbgExts),喜欢强大和COM接口的可以用第一种(DbgEng)。
无论哪一种,其基本做法都是编写一个动态链接库模块,并根据规定输出必要的函数。然后WinDBG在用户执行扩展命令时会寻找和加载这个扩展模块,然后寻找里面的输出函数,并执行。
例如,图3显示的是LBR扩展模块所输出的所有函数。
图3 LBR扩展模块所输出所有函数
其中的WinDbgExtensionDllInit就是必须输出的初始化函数。help和lbr是两个命令的输出函数。前面带有问号的函数是VC创建DLL时自动定义的类和函数,与WinDBG扩展模块无关。
图4 扩展模块ACPIKD所输出的函数
图4显示的是WinDBG的ACPI扩展命令模块ACPIKD.DLL所输出的函数。从函数DebugExtensionInitialize可以看出,它是使用第一种方法来编写的。