Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

另一种 WinDbg 插件编写方法 - Debugger Engine Extension

Posted on 2004-07-08 11:53  Flier Lu  阅读(1648)  评论(0编辑  收藏  举报
http://www.blogcn.com/user8/flier_lu/index.html?id=2178387&run=.0F8ED5D

在仔细阅读 scz 的《MSDN系列(11)--给SoftICE写插件》一文后,忍不住自己动手试试 WinDbg 插件的编写,呵呵。不过我选择的是与小四不同的另一种 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)时才会显示,非常适合对插件就行调试跟踪。

    在了解了调试接口函数的大致使用流程后,接着编写一个有实际意义的功能,也就是小四文章中的 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 类型插件的必要内容已经基本上介绍完了,以后有机会再详细介绍调试接口的具体使用方法,呵呵

btw: 不要找我要可直接编译的代码。我跟小四习惯不同,只提供编写完整代码所有必要功能点的详细介绍。如果要想实际 run 起来,自己努力吧,呵呵