其实,前面几篇文章已经可以写出一个调试器扩展,但是编写这样的调试器扩展,其所能提供的功能并不比windbg脚本多。通过这篇文章,我希望大家能从中了解一些调试器扩展所特有的能力(要不然还费神写什么代码)。
进阶篇中曾经提到过一个不怎么起眼的类:IDebugClient,当时我介绍说这个类的作用是"用户输入的来源和命令输出的目标"。这句话没错,当然更确切的定义还是需要读者移步到MSDN查阅。我这里想提的是:除了使用默认的输出目标,还可以创建新的运行在后台的IDebugClient,我们从后台获取中间结果,整理后显示到前台目标。当然,你可能会觉得多余,那我就通过一个实际的例子让你慢慢接受我的看法。来看一个场景:要求枚举系统所有进程打开的对象,并找出占用特定对象的进程(这是我在HLK测试时遇到的问题)。看到这个命题,大家可能会想到!handle 命令可以枚举进程所有的句柄(在内核中找到句柄),又会有人提出,!handle命令结合!for_each_process命令就可以枚举系统中所有进程打开的对象:
kd> !for_each_process "!handle 0 3 @#Process" ;简单解释一下:""中的@#Process最终会被进程EPROCESS地址代替,其他!for_each_xx命令也有相似的用法
Failed to get VAD root
PROCESS 89e34830 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 00b1f000 ObjectTable: e1000cc0 HandleCount: 186.
Image: System
Kernel handle table at e1000cc0 with 186 entries in use
0004: Object: 89e34830 GrantedAccess: 001f0fff Entry: e1002008
Object: 89e34830 Type: (89e34e70) Process
ObjectHeader: 89e34818 (old version)
HandleCount: 2 PointerCount: 51
0008: Object: 89e33020 GrantedAccess: 00000000 Entry: e1002010
Object: 89e33020 Type: (89e34ca0) Thread
ObjectHeader: 89e33008 (old version)
HandleCount: 1 PointerCount: 1
没错,上面的命令运行一段时间,的确可以把所有打开的对象全部枚举出来,但是,仍然没有找到指定对象。有人会说,把所有的windbg输出拷贝到文本中慢慢找呗~这不失为一个办法,但是由于输出过多,windbg都分屏了!!这时候,我猜到一定会有聪明的人说,可以用.logopen/.logclose命令把所有操作记录到调试日志中,然后慢慢比对。是的,这确实可以解决我提出的问题,但是丑陋了一点----需要借助中间文件来实现功能。如果满足上面的解决方案,windbg调试扩展倒也简单,下面的代码片就够了:
HRESULT CALLBACK
cmdsample(PDEBUG_CLIENT4 Client, PCSTR args)
{
CHAR Input[256];
int hndId;
char handlsBuf[256];
char cmdLine[256] = {0};
INIT_API();
UNREFERENCED_PARAMETER(args);
#define LogFilePath "C:\\dbglog.txt"
HRESULT status = g_ExtControl->OpenLogFile(LogFilePath,false);
if(status != S_OK)
return S_OK;
//
// list all the modules by executing lm command
//
g_ExtControl->Execute(DEBUG_OUTCTL_ALL_CLIENTS |
DEBUG_OUTCTL_OVERRIDE_MASK,
"!handle", // Command to be executed
DEBUG_EXECUTE_ECHO );
g_ExtControl->CloseLogFile();
FILE* fp = fopen(LogFilePath,"r");
if(!fp)
return S_OK;
fgets(handlsBuf,256,fp);
....
}
但是,如果你对这种带有中间文件的解决方式不满意,可以通过创建IDebugClient对象的方式完成目标:用windbg传给调试扩展导出接口HRESULT CALLBACK ExtensionCommandName(PDEBUG_CLIENT , PCSTR ); 的第一个参数DEBUG_CLIENT(默认的调试终端)创建一个全新的调试终端,然后由这个全新的调试终端创建相关的DEBUG_CONTROL对象backgroundCtrl,用于控制输出和执行命令(即调用backgroundCtrl->Output/backgroundCtrl->Execute函数)。这样,所有由backgroundCtrl对象执行的命令结果和输出的调试字符串都会输出到刚才全新创建的调试终端中。由于这个调试终端并不会显示到前台,所以这些调试信息全被丢弃了~
不过,这同时带来一个意外:我很可能需要借助这些中间结果做进一步的分析,而这些信息被丢弃了,分析岂不是进行不下去了?这个意外可以通过设置输出回调函数(OutputCallback)解决。windbg在输出字符串前会检查是否有输出回调函数,如果有,将字符串传给输出回调函数,最后传递给显示终端。也就是说,我们可以自定义一个输出回调函数,再此获得所有的输出:
#include "stdafx.h"
#include "outputCB.h"
#include "./windbgExtUtil.h"
#include
#define MAX_OUTPUTCALLBACKS_BUFFER 0x1000000 // 1Mb
#define MAX_OUTPUTCALLBACKS_LENGTH 0x0FFFFFF // 1Mb - 1
STDMETHODIMP COutputCallbacks::QueryInterface(__in REFIID InterfaceId,
__out PVOID* Interface)
{
*Interface = NULL;
if (IsEqualIID(InterfaceId, __uuidof(IUnknown)) || IsEqualIID(InterfaceId,
__uuidof(IDebugOutputCallbacks)))
{
*Interface = (IDebugOutputCallbacks *)this;
InterlockedIncrement(&m_ref);
return S_OK;
}
else
{
return E_NOINTERFACE;
}
}
STDMETHODIMP_(ULONG) COutputCallbacks::AddRef()
{
return InterlockedIncrement(&m_ref);
}
STDMETHODIMP_(ULONG) COutputCallbacks::Release()
{
if (InterlockedDecrement(&m_ref) == 0)
{
delete this;
return 0;
}
return m_ref;
}
extern std::string outputBuffer;
STDMETHODIMP COutputCallbacks::Output(__in ULONG Mask, __in PCSTR Text)
{
/*
if(strcmp(runningCmd,"!process 0 0")==0)
{
recordAllProcs(Text);
}
else if(strcmp(runningCmd, "!handle -1 7 Address of(Process Object)")==0)
{
recordAllHandlers(Text);
}
*/
outputBuffer.append(Text);
return S_OK;
}
void COutputCallbacks::Clear()
{
if (m_pBufferNormal)
{
free(m_pBufferNormal);
m_pBufferNormal = NULL;
m_nBufferNormal = 0;
}
if (m_pBufferError)
{
free(m_pBufferError);
m_pBufferError = NULL;
m_nBufferError = 0;
}
}
上面的代码定义了一个输出回调函数类,其中的COutputCallbacks::Output用于接受调试输出。我把所有的输出全部存放到全局变量std::string中。当前一个命令执行完毕后,就开始解析变量中的字符串:
extern std::string outputBuffer; //定义全局变量std::string
extern LIST_ENTRY procListHead;
ProcInfo* curProcess;
HRESULT match_process_object(PDEBUG_CLIENT DebugClient, PCSTR args)
{
IDebugClient *backgroundDebugClient;
PDEBUG_CONTROL backgroundDbgCtrl;
PDEBUG_CONTROL forgroundDbgCtrl;
HRESULT Hr;
COutputCallbacks* outputCBObj = NULL;
LIST_ENTRY* pos = NULL;
DWORD objAddr = 0;
outputCBObj = new COutputCallbacks; //创建输出回调
if(outputCBObj == NULL)
return S_FALSE;
if((args==NULL)||(strlen(args)==0))
{
return S_FALSE;
}
sscanf(args, "%x", &objAddr);
//search
do{
if (DebugClient->CreateClient(&backgroundDebugClient)!=S_OK)
break;
//用默认的调试终端创建全新的调试终端,并创建IDebugControl对象
if(DebugClient->QueryInterface(__uuidof(IDebugControl),(void **)&forgroundDbgCtrl)!=S_OK)
break;
//设置调试终端的输出回调函数
backgroundDebugClient->SetOutputCallbacks(outputCBObj);
if ((Hr = backgroundDebugClient->QueryInterface(__uuidof(IDebugControl),
(void **)&backgroundDbgCtrl)) != S_OK)
break;
outputBuffer.clear();
//开始执行命令,命令执行过程中COutputCallbacks::Output会不断接受调试输出
backgroundDbgCtrl->Execute(DEBUG_OUTCTL_THIS_CLIENT, "!process 0 0", DEBUG_EXECUTE_NO_REPEAT);
//命令执行结束,COutputCallbacks::Output接受的调试输出也完毕,可以执行分析过程
parseAllProcess(outputBuffer.c_str(),outputBuffer.length());
结合后台调试终端和输出回调函数,就可以在后台收集windbg输出的结果,最终拼凑出我们需要的结果并显示到前台,如下:
forgroundDbgCtrl->Output(DEBUG_OUTPUT_NORMAL, "Process: %s EProcess Address: %x"\
" has occupy object",
curProcess->procName,
curProcess->procEprocAddr);
恩,现在大家可以写出属于自己的windbg调试扩展了~
参考资料:
Debugger Engine API - Writing a Debugging Tools for Windows Extension, Part 3: Clients and Callbacks
windbg sdk sample:WinDDK\7600.16385.1\Debuggers\sdk\samples\exts
最后,我写的调试器扩展demo,实现本篇中提到的功能:链接:http://pan.baidu.com/s/1pLsOIiJ 密码:6e7v