另一种 WinDbg 插件编写方法 - Debugger Engine Extension作者:Flier Lu <flier @ nsfocus.com>
出处:http://flier_lu.blogone.net/?id=2178387
主页:http://www.nsfocus.com
日期:2004-07-06
在仔细阅读上期月刊中 scz 的《MSDN系列(11)--给SoftICE写插件》一文后,忍不住自己动手试试 WinDbg 插件的编写,呵呵。不过我选择的是与 scz 不同的另一种 WinDbg 插件编写方法。
WinDbg 最新版本的 sdkhelp 目录下有一个 debugext.chm 文件,里面有很详细的 WinDbg 插件编写文档。其中提到 WinDbg 支持两种类型的插件:DbgEng 扩展和 WdbgExts 扩展。前者是使用在 dbgeng.h 中定义的针对 Debugger Engine API 的调试扩展;后者则是使用在 wdbgexts.h 中定义的专门针对 WinDbg 的调试扩展。小四文章中使用的就是后者的接口,较为简明,也可以被 SoftIce 很好支持;我则选择前一种插件类型,功能更强大,而且可以被除 WinDbg 外的其他支持 Debugger Engine API 的工具,如 Visual Studio.NET 支持。
与 WdbgExts 类型扩展插件类似,DbgEng 类型扩展插件必须实现一个初始化回调函数:
HRESULT CALLBACK DebugExtensionInitialize(OUT PULONG Version, OUT PULONG Flags);
此函数在使用 .load 命令载入插件时被调用,返回插件的版本信息。如
const int EXTS_VERSION_MAJOR = 1;
const int EXTS_VERSION_MINOR = 0;
extern "C" HRESULT CALLBACK DebugExtensionInitialize(OUT PULONG Version, OUT PULONG Flags)
{
*Version = DEBUG_EXTENSION_VERSION(EXTS_VERSION_MAJOR, EXTS_VERSION_MINOR);
*Flags = 0;
return S_OK;
}
定义插件回调函数时,必须使用 extern "C" 指定此函数的函数名使用与 C 兼容的命名格式,并建立一个 .def 文件定义入口名字,如
LIBRARY ClrExts
EXPORTS
DebugExtensionInitialize
DebugExtensionUninitialize
DebugExtensionNotify
KnownStructOutput
help
showcontext
这里建立一个新的 DbgEng 类型插件 ClrExts 完成对 CLR 调试支持扩展功能,并导出四个标准回调函数。除 DebugExtensionInitialize 必须有以外,其他三个回调函数是可选的。
void CALLBACK DebugExtensionNotify(IN ULONG Notify, IN ULONG64 Argument);
DebugExtensionNotify 函数在调试会话的状态转换的时候被调用,以通知插件调整自己的状态。Notify 参数可以有四个值:
DEBUG_NOTIFY_SESSION_ACTIVE: 调试会话被激活
DEBUG_NOTIFY_SESSION_INACTIVE: 没有被激活的调试会话
DEBUG_NOTIFY_SESSION_ACCESSIBLE: 调试会话被中断并可访问
DEBUG_NOTIFY_SESSION_INACCESSIBLE: 调试会话恢复执行并不能访问
调试会话的概念,表示是否正在调试一个进程中;而根据调试状态,中断目标程序运行并由调试器获得控制权时,调试会话被中断并可访问(DEBUG_NOTIFY_SESSION_ACCESSIBLE)。
调试插件可以通过跟踪这几个状态的改变,调整自己对目标调试进程的控制方法。
void CALLBACK DebugExtensionUninitialize(void);
DebugExtensionUninitialize函数则是在插件被 .unload 命令卸载的时候被调用。
HRESULT CALLBACK KnownStructOutput(IN ULONG Flag, IN ULONG64 Address, IN PSTR StructName, OUT PSTR Buffer, IN OUT PULONG BufferSize);
最后一个 KnownStructOutput 回调函数较少被用到,用于提供此调试插件支持打印的结构列表,并可打印指定地址的指定结构内容。
与 WdbgExts 类型插件不太一样,DbgEng 类型插件的调试接口可以通过 DebugCreate 函数,自行获取其 COM 接口
HRESULT DebugCreate(IN REFIID InterfaceId, OUT PVOID *Interface);
也可以通过插件命令的参数获得。插件的通用命令接口如下:
HRESULT CALLBACK (* PDEBUG_EXTENSION_CALL)(IN IDebugClient *Client, IN OPTIONAL PCSTR Args);
第一个参数 Client 就是调试接口,另外一个则是命令的参数字符串。
可以使用一个简单的包装类 CDebugClient 对 IDebugClient 接口就行包装,其构造函数自动获取调试接口
class CDebugClient
{
private:
HRESULT m_hr;
CComPtr<IDebugClient> m_spDebugClient;
CComQIPtr<IDebugControl> m_spDebugControl;
WINDBG_EXTENSION_APIS32 m_extensionApis;
public:
CDebugClient(void);
};
CDebugClient::CDebugClient(void)
{
m_hr = DebugCreate(__uuidof(IDebugClient), (PVOID *)&m_spDebugClient);
if(SUCCEEDED(m_hr))
{
m_spDebugControl = m_spDebugClient;
m_extensionApis.nSize = sizeof(m_extensionApis);
m_hr = m_spDebugControl->GetWindbgExtensionApis32(&m_extensionApis);
}
}
DebugCreate 函数构造一个新的 IDebugClient 接口实例,并放入 ATL 接口包装类 CComPtr<IDebugClient> 的对象 m_spDebugClient 中,并可从此接口查询获取 IDebugControl 接口实例。IDebugControl::GetWindbgExtensionApis32 则可以获取与 WdbgExts 类型插件兼容的调试接口函数集。不过我们后面将看到,DbgEng 的相应接口,比 WinDbg 的传统函数集功能要强大得多。
对于插件命令的入口直接给出的 IDebugClient 实例,则可以省去构造过程,如
CDebugClient::CDebugClient(IDebugClient *dbg)
: m_outLevel(olDefault), m_hr(S_OK), m_spDebugClient(dbg)
{
if(dbg)
{
m_spDebugControl = m_spDebugClient;
m_extensionApis.nSize = sizeof(m_extensionApis);
m_hr = m_spDebugControl->GetWindbgExtensionApis32(&m_extensionApis);
}
}
在了解了调试接口的创建和包装方法后,可以建立第一个插件命令,help,显示一个帮助字符串给调试器
extern "C" HRESULT CALLBACK help(IN IDebugClient *Client, IN OPTIONAL PCSTR Args)
{
UNREFERENCED_PARAMETER(Args);
CDebugClient DebugClient(Client);
if(FAILED(DebugClient.getLastHResult())) return DebugClient.getLastHResult();
DebugClient.Info("Help for %s "
" help - Show this help ", EXTS_NAME);
return DebugClient.getLastHResult();
}
UNREFERENCED_PARAMETER 是一个宏,用于显式引用一次不会用到的函数参数,避免编译器警告;
然后使用命令参数构造 CDebugClient 实例,并判断其构造过程是否有效;
接着调用 CDebugClient 封装的 Info 函数打印一堆帮助字符串;
最后返回 DebugClient 的最后调用状态。
函数逻辑非常简单,就不罗嗦了,下面看看对字符串的输出
enum OutputLevel
{
olAll,
olDebug,
olInfo,
olWarning,
olError,
#ifdef _DEBUG
olDefault = olAll
#else
olDefault = olInfo
#endif
};
class CDebugClient
{
private:
OutputLevel m_outLevel;
};
首先定义了5个缺省的输出级别,所有、调试、信息、警告和错误;然后定义调试接口的信息显示级别。
class CDebugClient
{
public:
void OutputString(OutputLevel lvl, const char *fmt, va_list args) const;
void OutputString(OutputLevel lvl, const char *fmt, ...) const;
void DoLog(OutputLevel level, const char *fmt, va_list args) const
{
if(m_outLevel <= level) OutputString(level, fmt, args);
}
#define DEF_LOG_LEVEL(name) void name(const char *fmt, ...) const
{
va_list args;
va_start(args, fmt);
DoLog(ol ## name, fmt, args);
va_end(args);
}
DEF_LOG_LEVEL(Debug);
DEF_LOG_LEVEL(Info);
DEF_LOG_LEVEL(Warning);
DEF_LOG_LEVEL(Error);
};
实际的信息输出放在 OutputString 函数中完成,而 DoLog 则根据当前调试接口的信息级别判断是否需要输出信息。并使用 DEF_LOG_LEVEL 宏定义四种常用的信息输出函数。
void CDebugClient::OutputString(OutputLevel lvl, const char *fmt, va_list args) const
{
#if 1
static ULONG OutputMask[] = {
0,
DEBUG_OUTPUT_VERBOSE,
DEBUG_OUTPUT_NORMAL,
DEBUG_OUTPUT_WARNING,
DEBUG_OUTPUT_ERROR
};
m_spDebugControl->OutputVaList(OutputMask[lvl], fmt, args);
#else
std::string str;
str.resize(_vscprintf(fmt, args)+1, 0);
_vsnprintf(const_cast<char *>(str.c_str()), str.size(), fmt, args);
m_extensionApis.lpOutputRoutine(str.c_str());
#endif
}
void CDebugClient::OutputString(OutputLevel lvl, const char *fmt, ...) const
{
va_list args;
va_start(args, fmt);
OutputString(lvl, fmt, args);
va_end(args);
}
OutputString 可以通过 IDebugControl 的 OutputVaList 方法输出,也可以通过传统的 WdbgExts 调试接口的 lpOutputRoutine 函数输出。前者的优点是可以根据信息输出级别,设定相应的输出掩码。如 olDebug 对应于 DEBUG_OUTPUT_VERBOSE,此类型信息只有在 WinDbg 打开了 Verbose 模式(菜单 View/Verbose Output)时才会显示,非常适合对插件就行调试跟踪。
在了解了调试接口函数的大致使用流程后,接着编写一个有实际意义的功能,也就是 scz 文章中的 showcontext 函数,代码如下:
#define OFFSETOF(TYPE, MEMBER) ((size_t)&((TYPE)0)->MEMBER)
extern "C" HRESULT CALLBACK showcontext(IN IDebugClient *Client, IN OPTIONAL PCSTR Args)
{
CDebugClient DebugClient(Client);
if(FAILED(DebugClient.getLastHResult())) return DebugClient.getLastHResult();
DebugClient.Debug("%s: call externsion function showcontext with arguments - %s ", EXTS_NAME, Args);
std::string buf;
DWORD dwSize = OFFSETOF(PCONTEXT, ExtendedRegisters);
buf.resize(dwSize);
DWORD dwAddress = DebugClient.Evaluate(Args);
DebugClient.Debug("%s: get expression "%s" 's value %x ", EXTS_NAME, Args, dwAddress);
if(DebugClient.ReadMemory(dwAddress, buf) == dwSize)
{
PCONTEXT pCtxt = (PCONTEXT)buf.c_str();
DebugClient.Info("EAX=%08X EBX=%08X ECX=%08X EDX=%08X ESI=%08X "
"EDI=%08X EBP=%08X ESP=%08X EIP=%08X EFLAGS=%08X "
"CS=%04X DS=%04X SS=%04X ES=%04X FS=%04X GS=%04X ",
pCtxt->Eax, pCtxt->Ebx, pCtxt->Ecx, pCtxt->Edx, pCtxt->Esi,
pCtxt->Edi, pCtxt->Ebp, pCtxt->Esp, pCtxt->Eip, pCtxt->EFlags,
(WORD)pCtxt->SegCs, (WORD)pCtxt->SegDs, (WORD)pCtxt->SegSs,
(WORD)pCtxt->SegEs, (WORD)pCtxt->SegFs, (WORD)pCtxt->SegGs);
}
else
{
DebugClient.Warning("%s: Cannot read process memory @ %x ", EXTS_NAME, dwAddress);
}
return DebugClient.getLastHResult();
}
代码逻辑很简单:首先获取调试接口;然后调用 DebugClient.Evaluate 函数分析命令参数的表达式,获取目标地址;然后调用 DebugClient.ReadMemory 函数从指定地址读取 CONTEXT 结构的部分内容;最后调用 DebugClient.Info 函数输出信息。
OFFSETOF 宏是一个获取结构部分内容长度的小技巧,通过将 0 地址强制转换为结构指针,来获得指定字段在结构中的相对偏移。
size_t CDebugClient::Evaluate(const char *lpExpression)
{
DEBUG_VALUE value;
m_hr = m_spDebugControl->Evaluate(lpExpression, DEBUG_VALUE_INT32, &value, NULL);
return value.I32;
}
CDebugClient::Evaluate 函数简单调用 IDebugControl 接口的 Evaluate 函数,完成表达式的计算工作。例如敲入 "showcontext *(esp+4)"这条命令,命令行参数 Args 的内容就是 "*(esp+4)",而 Evaluate 函数可以将这个表达式计算得到一个确定的地址。DEBUG_VALUE_INT32参数指定需要获取一个 32 位整数;DEBUG_VALUE 则是一个类似 VARIANT 的联合类型,用户保存各种可能类型的参数。
size_t CDebugClient::ReadMemory(size_t offset, void *buf, size_t size) const
{
ULONG readBytes;
#if 1
CComQIPtr<IDebugDataSpaces> spDebugDataSpaces(m_spDebugClient);
spDebugDataSpaces->ReadVirtual(offset, buf, size, &readBytes);
#else
m_extensionApis.lpReadProcessMemoryRoutine(offset, buf, size, &readBytes);
#endif
return readBytes;
}
size_t CDebugClient::ReadMemory(size_t offset, std::string& buf) const
{
return ReadMemory(offset, const_cast<char *>(buf.c_str()), buf.size());
}
而 ReadMemory 则比较简单,通过 IDebugDataSpaces 接口或 WdbgExts 兼容接口都能读取目标进程的虚拟内存。
至此,编写一个 DbgEng 类型插件的必要内容已经基本上介绍完了,以后有机会再详细介绍调试接口的具体使用方法,呵呵
参考引用:
MSDN系列(11)--给SoftICE写插件,scz,http://www.nsfocus.net/index.php?act=magazine&do=view&mid=2204