本文章是由機器翻譯。

偵錯工具引擎 API

撰寫適合 Windows 延伸模組的偵錯工具,第 3 篇:用戶端和回呼

Andrew Richards

下載代碼示例

在關於調試器 API 的第三部分中,我將深入探討調試器擴展與調試器之間可能存在的關係。我將概要介紹調試器用戶端和調試器回檔。在此過程中,我將探討 DEBUG_OUTPUT_XXX 和 DEBUG_OUTCTL_XXX 常量的本質問題。

我將在此基礎上實現 Son of Strike (SOS) 調試器擴展的封裝。我將利用調試器標記語言 (DML) 來增強 SOS 輸出,並演示如何利用內置調試器命令和其他擴展來檢索擴展所需的資訊。

在繼續前,確保您已閱讀前兩個部分的內容,瞭解什麼是調試器擴展(包括我如何構建和測試本文中的示例)以及如何正確生成輸出。這些內容可在 msdn.microsoft.com/magazine/gg650659msdn.microsoft.com/magazine/hh148143中找到。

輸出控制

可以通過兩種方法來在擴展中生成輸出:IDebugControl::Output* 函數和 IDebugControl::ControlledOutput* 函數。Output 函數僅僅是 ControlledOutput 函數的簡化。這兩種函數可檢索當前的輸出控制設置,並將其傳遞給 ControlledOutput 作為第一個參數:

HRESULT ControlledOutput(
  [in]  ULONG OutputControl,
  [in]  ULONG Mask,
  [in]  PCSTR Format,
         ...
);

在第二篇文章中,我概述了用於 OutputControl 參數的 DEBUG_OUTCTL_XXX 常量。 在本文中,我將重點討論控制輸出範圍的低位。 對於低位,有一個基於 DEBUG_OUTCTL_XXX 範圍的常量是必須使用的。 該值指示將輸出到何處。 該值可以指示:輸出到所有調試器用戶端 (DEBUG_OUTCTL_ALL_CLIENTS),只輸出到與 IDebugControl 介面相關聯的 IDebugClient (DEBUG_OUTCTL_THIS_CLIENT),輸出到其他所有用戶端 (DEBUG_OUTCTL_ALL_OTHER_CLIENTS),不輸出到任何用戶端 (DEBUG_OUTCTL_IGNORE),或者只輸出到日誌檔 (DEBUG_OUTCTL_LOG_ONLY)。

在隨附代碼下載(與前一篇文章相同)的 Example06 中,!outctlpassed 命令(參見圖 1)基於傳入的 IDebugClient 介面。

圖 1 !outctlpassed

HRESULT CALLBACK 
outctlpassed(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  UNREFERENCED_PARAMETER(args);

  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->
    QueryInterface(__uuidof(IDebugControl), (void **)&pDebugControl)))
  {
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_THIS_CLIENT,           
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_THIS_CLIENT\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS,       
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_ALL_CLIENTS\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS,  
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_ALL_OTHER_CLIENTS\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_IGNORE,             
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_IGNORE\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_LOG_ONLY,          
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_LOG_ONLY\n");

    pDebugControl->Release();
  }
  return S_OK;
}

該函數可為(傳入的 IDebugClient 的)IDebugControl 介面執行 QueryInterface。 調用 ControlledOutput 時將使用每一個輸出控制範圍。 將 Mask 設置為 Normal (DEBUG_OUTPUT_NORMAL),這樣它就不會被排除在輸出之外。 預設情況下,只有值為 Verbose 的 Mask (DEBUG_OUTPUT_VERBOSE) 會被排除在 WinDBG“輸出”視窗之外。 為了測試命令,我使用了 test_windbg.cmd 腳本。 在啟動的 WinDBG 中,打開日誌檔 (.logopen),運行 !outctlpassed 命令,然後關閉日誌檔 (.logclose),如下所示:

0:000> .logopen outctlpassed.txt
打開日誌檔“outctlpassed.txt”
0:000> !outctlpassed
DEBUG_OUTCTL_THIS_CLIENT
DEBUG_OUTCTL_ALL_CLIENTS
0:000> .logclose
關閉打開的日誌檔 outctlpassed.txt

!outctlpassed 命令只包括 DEBUG_OUTCTL_THIS_CLIENT 輸出和 DEBUG_OUTCTL_ALL_CLIENTS 輸出。 在“輸出”視窗中省略了 DEBUG_OUTCTL_ALL_OTHER_CLIENTS、DEBUG_OUTCTL_IGNORE 以及 DEBUG_OUTCTL_LOG_ONLY 的輸出控制。 但如果您查看日誌檔,會有不同的結果,如下所示:

打開日誌檔“outctlpassed.txt”
0:000> !outctlpassed
DEBUG_OUTCTL_THIS_CLIENT
DEBUG_OUTCTL_ALL_CLIENTS
DEBUG_OUTCTL_ALL_OTHER_CLIENTS
DEBUG_OUTCTL_LOG_ONLY
0:000> .logclose
關閉打開的日誌檔 outctlpassed.txt

日誌檔中唯一缺少的輸出控制為 DEBUG_OUTCTL_IGNORE。

調試器體系結構

要瞭解 WinDBG“輸出”視窗和日誌檔中缺少的輸出所發生的情況,我們需要深入瞭解調試器體系結構。 調試基於四個層次:調試器助手、調試器引擎、調試器用戶端和調試器擴展。

最底層為調試器助手 (dbghelp.dll)。 該庫包含所有用於解析符號的功能。

接下來一層為調試器引擎 (dbgeng.dll)。 該庫可處理調試會話,尤其是可為遠端目標的調試提供支援。 該庫中包含各種調試器介面(如 IDebugClient、IDebugControl 等)的處理。

接下來一層為調試器用戶端層。 在該層上,輸入和輸出的處理按照客戶認為適當的方式進行。 WinDBG 和 NTSD 調試器均位於這一層。 用戶端使用調試器引擎 DebugCreate 和 DebugConnect 函數來創建附加到所需目標的 IDebugClient 物件。 目標可以是本地的,也可以是遠端的(通過調試器引擎代理支援)。

最後一層為調試器擴展。 用戶端可調用由調試器擴展 DLL 匯出的命令。 調試器用戶端收到的來自調試器引擎的 IDebugClient 介面與用戶端傳遞給擴展的介面相同。 儘管由用戶端調用,但擴展僅與調試器引擎進行交互。 只要擴展不損壞用戶端的配置,其就可以與用戶端共用 IDebugClient 介面。 此外,兩者也可以設置其自身的用戶端(通過 IDebugClient::CreateClient),並根據需要進行配置。

多個調試器用戶端

可將多個用戶端(IDebugClient 物件)附加到同一個調試會話;會話是唯一的,而用戶端並不唯一。 每個用戶端都有連接到調試器引擎的專用介面,我們將充分利用這一功能。

最初,當您將 WinDBG 或 NTSD 附加到流程時,僅存在一個用戶端 - 由 DebugCreate 或 DebugConnect 返回的 IDebugClient 介面。 當擴展針對該返回介面調用 IDebugClient::CreateClient 時,調試器引擎會設置另一個與會話關聯的用戶端。 新用戶端將連接到同一個調試會話,但其處於預設狀態。 沒有在新用戶端上設置回檔,且輸出遮罩為預設值(除 Verbose 外的所有內容)。

圖 2中,!outctlcreate 的輸出與 !outctlpassed 的輸出相同,但是前者基於新創建的 IDebugClient 物件。

圖 2 !outctlcreate

HRESULT CALLBACK 
outctlcreate(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  UNREFERENCED_PARAMETER(args);

  IDebugClient* pDebugCreated;
  if (SUCCEEDED(pDebugClient->CreateClient(&pDebugCreated)))
  {
    IDebugControl* pDebugControl;
    if (SUCCEEDED(pDebugCreated->QueryInterface(__uuidof(IDebugControl), 
      (void **)&pDebugControl)))
    {
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_THIS_CLIENT,        
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_THIS_CLIENT\n");
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS,       
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_ALL_CLIENTS\n");
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_ALL_OTHER_CLIENTS\n");
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_IGNORE,            
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_IGNORE\n");
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_LOG_ONLY,          
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_LOG_ONLY\n");
      pDebugControl->Release();
    }
    pDebugCreated->Release();
  }
  return S_OK;
}

如上所述,為了測試命令,我使用了 test_windbg.cmd 腳本。 在啟動的 WinDBG 中,打開日誌檔 (.logopen),運行 !outctlcreate 命令,然後關閉日誌檔 (.logclose),如下所示:

0:000> .logopen outctlcreate.txt
打開日誌檔“outctlcreate.txt”
0:000> !outctlcreate
DEBUG_OUTCTL_ALL_CLIENTS
DEBUG_OUTCTL_ALL_OTHER_CLIENTS
0:000> .logclose
關閉打開的日誌檔 outctlcreate.txt

!outctlcreate 命令只包括 DEBUG_OUTCTL_ALL_CLIENTS 輸出和 DEBUG_OUTCTL_ALL_OTHER_CLIENTS 輸出。 “所有其他”輸出替代了“當前”輸出,因為此時 WinDBG 用戶端為“其他”用戶端。 日誌檔結果保持不變,如下所示:

打開日誌檔“outctlcreate.txt”
0:000> !outctlcreate
DEBUG_OUTCTL_THIS_CLIENT
DEBUG_OUTCTL_ALL_CLIENTS
DEBUG_OUTCTL_ALL_OTHER_CLIENTS
DEBUG_OUTCTL_LOG_ONLY
0:000> .logclose
關閉打開的日誌檔 outctlcreate.txt

在調試引擎中,可以有條件地執行 ControlledOutput 調用。 引擎可調用所有符合輸出控制標準的用戶端上的輸出回檔。 因此,如果您設置您自身的用戶端,並使用 DEBUG_OUTCTL_THIS_CLIENT 輸出控制,輸出將留在本地(即不會顯示在 WinDBG 中)。 請注意,輸出仍會轉入日誌檔。 如果您添加“未記錄”高位 (DEBUG_OUTCTL_NOT_LOGGED),也可以阻止輸出轉入到日誌檔。

再次重申,!outctlthis 命令只執行已創建用戶端上的“當前”輸出和“未記錄”輸出 (DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED),如圖 3所示。

圖 3 !outctlthis

HRESULT CALLBACK 
outctlthis(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  UNREFERENCED_PARAMETER(args);

  IDebugClient* pDebugCreated;
  if (SUCCEEDED(pDebugClient->CreateClient(&pDebugCreated)))
  {
    IDebugControl* pDebugControl;
    if (SUCCEEDED(pDebugCreated->QueryInterface(__uuidof(IDebugControl), 
      (void **)&pDebugControl)))
    {
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_THIS_CLIENT |  
        DEBUG_OUTCTL_NOT_LOGGED, DEBUG_OUTPUT_NORMAL, 
        "DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED\n");
      pDebugControl->Release();
    }
    pDebugCreated->Release();
  }
  return S_OK;
}

如上所述,為了測試命令,我使用了 test_windbg.cmd 腳本。 在啟動的 WinDBG 中,打開日誌檔 (.logopen),運行 !outctlthis 命令,然後關閉日誌檔 (.logclose),如下所示:

0:000> .logopen outctlthis.txt
打開日誌檔“outctlthis.txt”
0:000> !outctlthis
0:000> .logclose
關閉打開的日誌檔 outctlthis.txt

日誌檔這次沒有記錄命令的輸出。 請注意,日誌檔不記錄該執行是因為 WinDBG 用戶端在擴展調用之前已創建提示輸出,如下所示:

打開日誌檔“outctlthis.txt”
0:000> !outctlthis
0:000> .logclose
關閉打開的日誌檔 outctlthis.txt

實際上,我已經將輸出重定向到 NULL,因為創建的用戶端並沒有任何關聯的輸出回檔 - 不一定有用。 但如果我更改代碼以改用 IDebugControl::Execute 函數,那麼此時我就可以執行任何命令,而不會在 WinDBG 中顯示或記錄,如下所示:

pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED, 
  args, DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);

以這種方式使用 Execute 時,您需要設置 DEBUG_EXECUTE_NOT_LOGGED 標記和 DEBUG_EXECUTE_NO_REPEAT 標記,從而使日誌檔中不會生成提示輸出,且不會保存命令以供重複使用(例如,在使用者在命令提示符中按下 Enter 鍵以執行空命令的情況下)。

圖 4所示,最後 !outctlexecute 命令無提示地執行了提供的參數。

圖 4 !outctlexecute

HRESULT CALLBACK 
outctlexecute(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  IDebugClient* pDebugCreated;
  if (SUCCEEDED(pDebugClient->CreateClient(&pDebugCreated)))
  {
    IDebugControl* pDebugControl;
    if (SUCCEEDED(pDebugCreated->QueryInterface(__uuidof(IDebugControl), 
      (void **)&pDebugControl)))
  {
    pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED, 
      args, DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);
    pDebugControl->Release();
  }
  pDebugCreated->Release();
  }
  return S_OK;
}

我正在執行符號的強制性重新載入 (.reload /f)。 這一操作通常會生成大量的符號輸出,但 !outctlexecute 已隱藏這些輸出,如下所示:

0:000> !outctlexecute .reload /f

輸出回檔

當調試器引擎將輸出請求路由到用戶端(由輸出控制指示)時,應用了兩個附加約束:

  • 類型(遮罩)的輸出是您想要的嗎?
  • 是否有關聯的輸出回檔?

調用 IDebugControl::Output 或 IDebugControl::ControlledOutput 時,調試引擎會將提供的遮罩與用戶端的輸出遮罩 (IDebugClient::GetOutputMask) 進行比較。 兩個遮罩需要與待調用的輸出回檔相匹配。

接下來,調試引擎將調用 IDebugClient::GetOutputCallbacks 以獲取輸出回檔介面(IDebugOutputCallbacks、IDebugOutputCallbacks2 或 IDebugOutputCallbacksWide)。 如果用戶端上已設定輸出回檔(事先通過 IDebugClient::SetOutputCallbacks 完成的),調試引擎將調用該回檔 - 例如 IDebugOutputCallbacks::Output。

在隨附代碼下載的 Example07 中,我已對 IDebugOutputCallbacks 實現進行編碼,可支援 1MB 的正常輸出和 1MB 的錯誤輸出。 (标题见图 5,代码见图 6。)

圖 5 OutputCallbacks.h

#ifndef __OUTPUTCALLBACKS_H__
#define __OUTPUTCALLBACKS_H__

#include "dbgexts.h"

class COutputCallbacks : public IDebugOutputCallbacks
{
  private:
    long m_ref;
    PCHAR m_pBufferNormal;
    size_t m_nBufferNormal;
    PCHAR m_pBufferError;
    size_t m_nBufferError;

  public:
    COutputCallbacks()
     {
      m_ref = 1;
      m_pBufferNormal = NULL;
      m_nBufferNormal = 0;
      m_pBufferError = NULL;
      m_nBufferError = 0;
    }

    ~COutputCallbacks()
    {
      Clear();
    }

    // IUnknown
    STDMETHOD(QueryInterface)(__in REFIID InterfaceId, __out PVOID* Interface);
    STDMETHOD_(ULONG, AddRef)();
    STDMETHOD_(ULONG, Release)();

    // IDebugOutputCallbacks
    STDMETHOD(Output)(__in ULONG Mask, __in PCSTR Text);

    // Helpers
    ULONG SupportedMask() { return DEBUG_OUTPUT_NORMAL | DEBUG_OUTPUT_ERROR; }
    PCHAR BufferNormal() { return m_pBufferNormal; }
    PCHAR BufferError() { return m_pBufferError; }
    void Clear();   
};

#endif // #ifndef __OUTPUTCALLBACKS_H__

圖 6 OutputCallbacks.cpp

#include "dbgexts.h"
#include "outputcallbacks.h"

#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;
}

STDMETHODIMP COutputCallbacks::Output(__in ULONG Mask, __in PCSTR Text)
{
  if ((Mask & DEBUG_OUTPUT_NORMAL) == DEBUG_OUTPUT_NORMAL)
  {
    if (m_pBufferNormal == NULL)
    {
      m_nBufferNormal = 0;
      m_pBufferNormal = (PCHAR)malloc(sizeof(CHAR)*(MAX_OUTPUTCALLBACKS_BUFFER));
      if (m_pBufferNormal == NULL) return E_OUTOFMEMORY;
      m_pBufferNormal[0] = '\0';
      m_pBufferNormal[MAX_OUTPUTCALLBACKS_LENGTH] = '\0';
    }
    size_t len = strlen(Text);
    if (len > (MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferNormal))
    {
      len = MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferNormal;
    }
    if (len > 0)
    {
      memcpy(&m_pBufferNormal[m_nBufferNormal], Text, len);
      m_nBufferNormal += len;
      m_pBufferNormal[m_nBufferNormal] = '\0';
    }
  }
  if ((Mask & DEBUG_OUTPUT_ERROR) == DEBUG_OUTPUT_ERROR)
  {
    if (m_pBufferError == NULL)
    {
      m_nBufferError = 0;
      m_pBufferError = (PCHAR)malloc(sizeof(CHAR)*(MAX_OUTPUTCALLBACKS_BUFFER));
      if (m_pBufferError == NULL) return E_OUTOFMEMORY;
      m_pBufferError[0] = '\0';
      m_pBufferError[MAX_OUTPUTCALLBACKS_LENGTH] = '\0';
    }
    size_t len = strlen(Text);
    if (len >= (MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferError))
    {
      len = MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferError;
    }
    if (len > 0)
    {
      memcpy(&m_pBufferError[m_nBufferError], Text, len);
      m_nBufferError += len;
      m_pBufferError[m_nBufferError] = '\0';
    }
  }
  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;
  }
}

每次調用 Output 後,類會將字串附加到正常或錯誤緩衝區。 除 IUnknown 和 IDebugOutputCallbacks 介面所要求的函數之外,還有其他可提供到緩衝區(BufferNormal 和 BufferError)的存取權限從而清除緩衝區的函數,以及用來獲取所支援的輸出類型 (SupportedMask) 的函數。

每次調用 Output 或 ControlledOutput(同時符合 IDebugClient 標準)時,系統會調用回檔的 Output 函數。 傳入的遮罩與原始輸出調用上設置的遮罩相同。 Output 函數會對傳入的遮罩執行位元遮罩比較,從而確保文本與正確的緩衝區關聯。 輸出以單獨方式還是組合方式存儲還是被忽略,具體取決於回檔的實現。 單個 IDebugClient::Execute 調用可能產生數百種不同類型的 Output 調用。 因為我想解釋一下整個 Execute 操作的正常輸出和錯誤輸出,所以我將這些字串連接起來存入兩個關聯的緩衝區。

IDebugOutputCallbacks 介面傳遞 ANSI 格式和 TEXT 格式的字串。 如果原始文本的格式是 Unicode 或 DML,引擎將在回檔之前執行格式轉換。 要捕獲 Unicode (wide) 內容,需改用 IDebugOutCallbacksWide 介面。 兩種介面的唯一區別在於 Text 參數是作為 PCWSTR 而不是作為 PCSTR 傳遞的。 這兩種介面僅支援基於 TEXT 輸出的通知。 它們不接收 DML 格式的文本。

若要獲取 DML 內容,您需要使用 IDebugOutputCallbacks2 介面。 使用該介面,您可以控制回檔所接收文本的格式化方式:TEXT、DML 或其中任何一個。 GetInterestMask 函數用於定義該格式。 在用戶端上通過 IDebugClient::SetOutputCallbacks 設置介面時會調用此函數。 返回是接收 TEXT (DEBUG_OUTCBI_TEXT)、DML (DEBUG_OUTCBI_DML) 還是其中任何一種輸出類型 (DEBUG_OUTCBI_ANY_FORMAT)。 請注意,這與 Output Mask 不同。

該介面上的 Output 函數沒有特定用途,只是 IDebugCallbacks 的(虛設)介面繼承的一種產物。 所有輸出回檔通知都是由 Output2 函數發出的:

STDMETHOD(Output2)(ULONG Which, ULONG Flags, ULONG64 Arg, PCWSTR Text)

Which 參數指定要傳入的是 TEXT (DEBUG_OUTCB_TEXT) 還是 DML (DEBUG_OUTCB_DML),或指定所需的清除 (DEBUG_OUTCB_EXPLICIT_FLUSH)。 請注意,這些是 CB* 常量;而不是用於相關遮罩的 CBI* 常量。

Flags 參數指定位元遮罩中的其他資訊。 尤其是針對 DML 內容,該參數指定內容是否包含超連結 (DEBUG_OUTCBF_DML_HAS_TAGS) 或是否具有編碼的特殊字元("、&、< 和 >)(DEBUG_OUTCBF_DML_HAS_SPECIAL_CHARACTERS)。 該參數也可以表明完成輸出處理之後需要清除 (DEBUG_OUTCBF_COMBINED_EXPLICIT_FLUSH)。 當引擎組合 Output 調用和 FlushCallbacks 調用時會出現這種情況。

Arg 參數包含 DML 回檔、TEXT 回檔和 FLUSH 回檔的輸出遮罩。

最後,Text 參數為要輸出的文本。 當針對清除 (DEBUG_OUTCB_EXPLICIT_FLUSH) 調用回檔時,該參數為 NULL。 文本通常以 Unicode 格式傳遞。

執行

將回檔添加到 !outctlexecute 命令並不需要很多代碼(參見圖 4)。 其他步驟用來分配回檔,並在調用 Execute 之前將該回檔與創建的 IDebugClient 介面相關聯。

我根據引用的計數堆物件實現了我的回檔操作。 如果您使用的是 Windows Driver Kit 生成環境,將需要禁用“驅動程式的 PREfast”警告 28197,以便您在每次編譯專案時不會收到關於“新”分配的 OACR(Microsoft 自動代碼審查)警告。 (PREfast 不會檢測類是否為引用計數。)

用戶端的回檔關聯分為兩部分:輸出遮罩 (SetOutputMask) 和輸出回檔 (SetOutputCallbacks)。 我的回檔類中有助手函數,可以返回支援的輸出遮罩 (COutputCallbacks::SupportedMask),因而我不需要對其進行硬編碼。 如果未設置遮罩,則可能無法獲得預期的輸出。

!callbackecho 命令(參見圖 7)是在隨附下載的 Example07 專案中實現的。

圖 7 !callbackecho

HRESULT CALLBACK 
callbackecho(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  IDebugClient* pDebugCreated;
  if (SUCCEEDED(pDebugClient->CreateClient(&pDebugCreated)))
  {
    IDebugControl* pDebugControl;
    if (SUCCEEDED(pDebugCreated->QueryInterface(__uuidof(IDebugControl), 
      (void **)&pDebugControl)))
    {
      // Create our IDebugOutputCallbacks-based object
      #pragma warning( disable : 28197 ) // PreFAST sees the 'new' as a leak due to 
                                     // the use of AddRef/Release
      COutputCallbacks* pOutputCallbacks = new COutputCallbacks;
      if (pOutputCallbacks != NULL)
      {
        // Apply IDebugOutputCallbacks to the IDebugClient
        if (SUCCEEDED(pDebugCreated->
          SetOutputMask(pOutputCallbacks->SupportedMask())) &&   
          SUCCEEDED(pDebugCreated->SetOutputCallbacks(pOutputCallbacks)))
      {
        // Execute and display 'vertarget'
        pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED, 
          "vertarget", DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);
        pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
          DEBUG_OUTPUT_NORMAL, "[Normal]\n%s\n", pOutputCallbacks->BufferNormal() ?
pOutputCallbacks->BufferNormal() : "");
        pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
          DEBUG_OUTPUT_ERROR , "[Error] \n%s\n", pOutputCallbacks->BufferError() ?
pOutputCallbacks->BufferError() : "");

        // Clear the callback buffers
        pOutputCallbacks->Clear();

        // Execute and display the passed command
        pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED, 
          args, DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);
        pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
          DEBUG_OUTPUT_NORMAL, "[Normal]\n%s\n", pOutputCallbacks->BufferNormal() ?
pOutputCallbacks->BufferNormal() : "");
        pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
          DEBUG_OUTPUT_ERROR , "[Error] \n%s\n", pOutputCallbacks->BufferError() ?
pOutputCallbacks->BufferError() : "");

        pDebugCreated->SetOutputCallbacks(NULL); // It's a best practice to 
                                                // release callbacks 
                                                // before the client release
        }
        pOutputCallbacks->Release();
      }
      pDebugControl->Release();
    }
    pDebugCreated->Release();
  }
  return S_OK;
}

其執行兩種命令(“vertarget”和傳遞的參數),並分別顯示它們的正常輸出和錯誤輸出。 使用 ControlledOutput 函數時,您將注意到我使用了 DEBUG_OUTCTL_ALL_OTHER_CLIENTS 輸出控制。 這一控制將輸出發送給其他所有用戶端,並且避免輸出被我的其他關聯輸出回檔捕獲。 若要遍歷所捕獲的輸出並生成相同遮罩的輸出,請確保您使用的是 DEBUG_OUTCTL_ALL_OTHER_CLIENTS 輸出控制,否則您可能很容易陷入無限迴圈之中。

如上所述,我使用了 test_windbg.cmd 腳本來測試命令。 在啟動的 WinDBG 中,運行 !callbackecho 命令,並查看“vertarget”的正常輸出和“r @rip”的錯誤輸出。(register 命令失敗,因為這是 x86 目標,而“rip”是 x64 寄存器。)請注意,在 Execute 調用之間,我調用了我的 Clear 助手函數以重置緩衝區。 這就是導致“vertarget”輸出從“r @rip”命令輸出中缺失的原因,如下所示:

0:000> !callbackecho r @rip
[Normal]
Windows 7 Version 7600 MP (2 procs) Free x86 compatible
產品: WinNt, suite: SingleUserTS
kernel32.dll version: 6.1.7600.16385 (win7_rtm.090713-1255)
電腦名稱:
調試會話時間: Sat Jan 29 22:01:59.080 2011 (UTC - 8:00)
系統正常執行時間: 0 days 6:22:48.483
流程正常執行時間: 0 days 0:01:05.792

核心程式的時間: 0 days 0:00:00.015
使用者時間: 0 days 0:00:00.000
 
[Error]

[Normal]
 
[Error]**
**^“r @rip”中嚴重的寄存器錯誤**

實際上您可以在不設置自己的用戶端的情況下實現相同的功能。 如果小心將您自己的值與傳入用戶端的輸出遮罩和輸出回檔進行交換,可以達到相同的結果。 但是,您執行這一操作時必須相當精確。 傳入的 IDebugClient 是 (WinDBG) 調試器針對“輸出”視窗的輸出所使用的用戶端。 如果您沒有將其返回到起始狀態,調試器將不再正常工作。

圖 8(來自代碼下載中的 Example07)顯示的是使用 !commandtoggle 命令的切換方法。

圖 8 !callbacktoggle

HRESULT CALLBACK 
callbacktoggle(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl)))
  {
    // Remember the original mask and callback
    ULONG ulOriginalMask;
    IDebugOutputCallbacks* pOriginalCallback;
    if (SUCCEEDED(pDebugClient->GetOutputMask(&ulOriginalMask)) &&
      SUCCEEDED(pDebugClient->GetOutputCallbacks(&pOriginalCallback)))
    {
      // Create our IDebugOutputCallbacks based object
      #pragma warning( disable : 28197 ) // PreFAST sees the 'new' as a leak due to 
                                     // the use of AddRef/Release
      COutputCallbacks* pOutputCallbacks = new COutputCallbacks;
      if (pOutputCallbacks != NULL)
      {
        // Apply IDebugOutputCallbacks to the IDebugClient
        if (SUCCEEDED(pDebugClient->SetOutputMask(pOutputCallbacks->
          SupportedMask())) && (pDebugClient->
          SetOutputCallbacks(pOutputCallbacks)))
        {
          // Execute 'vertarget'
          pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | 
            DEBUG_OUTCTL_NOT_LOGGED, "vertarget", 
            DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);

          // Revert the mask and callback so we can do some output
          pDebugClient->SetOutputMask(ulOriginalMask);
          pDebugClient->SetOutputCallbacks(pOriginalCallback);

          // Display 'vertarget'
          pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS,  
            DEBUG_OUTPUT_NORMAL, "[Normal]\n%s\n", pOutputCallbacks->
            BufferNormal() ?
pOutputCallbacks->BufferNormal() : "");
          pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS, 
            DEBUG_OUTPUT_ERROR , "[Error] \n%s\n", pOutputCallbacks->
            BufferError() ?
pOutputCallbacks->BufferError() : "");

          // Go back to our mask and callback so we can do the private callback
          pDebugClient->SetOutputMask(pOutputCallbacks->SupportedMask());
          pDebugClient->SetOutputCallbacks(pOutputCallbacks);

          // Clear the callback buffers
          pOutputCallbacks->Clear();

          // Execute the passed command
          pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | 
            DEBUG_OUTCTL_NOT_LOGGED, args, DEBUG_EXECUTE_NOT_LOGGED |  
            DEBUG_EXECUTE_NO_REPEAT);

          // Revert the mask and callback so we can do some output
          pDebugClient->SetOutputMask(ulOriginalMask);
          pDebugClient->SetOutputCallbacks(pOriginalCallback);

          // Display the passed command
          pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS, 
            DEBUG_OUTPUT_NORMAL, "[Normal]\n%s\n", pOutputCallbacks->
            BufferNormal() ?
pOutputCallbacks->BufferNormal() : "");
          pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS, 
            DEBUG_OUTPUT_ERROR , "[Error] \n%s\n", pOutputCallbacks->
            BufferError() ?
pOutputCallbacks->BufferError() : "");
        }

        // Revert the mask (again) for the case 
        // where the mask was set but the callback wasn't
        pDebugClient->SetOutputMask(ulOriginalMask);
        pOutputCallbacks->Release();
      }
    }
    pDebugControl->Release();
  }
  return S_OK;
}

該命令實現了與 !commandecho 完全相同的功能。為了能夠調用 Execute 和 ControlledOutput,我常常需要在傳入值和我的值之間切換輸出遮罩值和輸出回檔值,從而獲得所需的結果。請注意,在這種情況下,ControlledOutput 傳遞 DEBUG_OUTCTL_ALL_CLIENTS 輸出控制,因為關聯用戶端此時是所需的輸出目標。此外,您會注意到我在初始調用時(為了簡便起見)僅實現了錯誤處理。

在大多數情況下,用戶端的重新使用可能相當繁瑣;應謹慎使用此方法。

要執行什麼內容?

我使用該專用輸出技術的主要原因是為了實現基於 TEXT 的命令的 DML 版本,對於這類版本,我並不瞭解其內部資訊。我還特別對一些帶超連結的 SOS 調試器命令進行了“增值”操作。有關如何通過解析輸出來確定標記內容的資訊不在本文討論範圍之內。但是我會談談如何確定要執行的正確命令,以生成解析所需的輸出。

載入的調試器擴展及其關聯功能的枚舉不屬於調試器 API 的一部分。更糟糕的是,相同的調試器擴展可以從磁片上的同一位置載入多次。如果使用 .load myext 和 .chain 命令運行 test_windbg.cmd 腳本,您將能看到該情況的一個示例。MyExt 擴展載入了兩次,一次是作為 myext,第二次是作為 myext.dll:

0:000> .load myext
0:000> .chain
擴展 DLL 搜索路徑:
C:\Debuggers_x86\WINXP;C:\Debuggers_x86\winext;...
擴展 DLL 鏈:
myext:圖像 6.1.7600.16385,API 1.0.0,構建于 2011 年 1 月 29 日,星期六,22:00:48
[路徑: C:\Debuggers_x86\myext.dll]
myext.dll:圖像 6.1.7600.16385,API 1.0.0,構建于 2011 年 1 月 29 日,星期六,22:00:48
[路徑: C:\Debuggers_x86\myext.dll]
dbghelp:圖像 6.12.0002.633,API 6.1.6,構建于 2010 年 2 月 1 日,星期一,12:08:26 下午
[路徑: C:\Debuggers_x86\dbghelp.dll]
   ...

調用擴展命令以進行解析時,您需要確保調用的是目標命令,而不是其他擴展(或其他實例)中具有相同名稱的命令。

如果您完全限定命令,引擎將僅需要執行指定的命令即可。但是如果您沒有完全限定命令,引擎將需要進行搜索。引擎按載入順序查看每個擴展的匯出表,並執行第一個具有匹配命令名稱的擴展。.extmatch 命令顯示有關給定命令的當前搜索結果。.extmatch 命令通過“/e”開關支援副檔名和命令萬用字元,如下所示:

0:000> .extmatch callbackecho
!myext.callbackecho
!myext.dll.callbackecho
0:000> .extmatch /e *myext.dll* *back*
!myext.dll.callbackecho
!myext.dll.callbacktoggle

當您通過調用 .loadby sos.dll mscorwks 載入 SOS 擴展時,完全限定的路徑將為之寄存,也就是說,該擴展不會命名為“!sos.dll.<command>”。在以下示例中,我在 Windows 7 x64 中運行了 Test03 示例應用程式,並將調試器附加到該程式 (F6)。SOS 擴展是從 mscorwks 資料夾 (C:\Windows\Microsoft.NET\Framework64\v2.0.50727) 載入的:

0:004> .loadby sos.dll mscorwks
0:004> .chain
擴展 DLL 搜索路徑:
  C:\debuggers\WINXP;C:\debuggers\winext;...
擴展 DLL 鏈:
  C:\Windows\Microsoft.NET\Framework64\v2.0.50727\sos.dll:
圖像 2.0.50727.4952,API 1.0.0,構建于 2010 年 5 月 13 日,星期四,05:15:18
[路徑: C:\Windows\Microsoft.NET\Framework64\v2.0.50727\sos.dll]
    dbghelp:圖像 6.12.0002.633,API 6.1.6,構建于 2010 年 2 月 1 日,星期一,12:15:44
[路徑: C:\debuggers\dbghelp.dll]
    ...

安全調用 SOS 命令的唯一方法是採用完全限定。所以,當利用 SOS 擴展(或任何其他擴展)時,需要確定兩件事情:

  • 是否已載入 SOS 擴展?
  • 載入到什麼位置?

兩者均可通過擴展匹配來確定 (.extmatch /e *sos.dll* <command>)。在 LoadSOS 函數(Example08 的一部分;代碼未在此列出)中,我首先查找位於版本控制位置的命令 (\v2.0.50727\sos.dll.<command>)。如果找不到版本控制命令,我將返回查找不帶版本的命令 (\sos.dll.<command>)。如果此時我能找到命令,則會返回成功資訊,但返回的是 S_FALSE(與 S_OK 相對)。是否可能出現錯誤解析取決於 LoadSOS 的調用方。

如果找不到命令的 SOS 版本,那麼我假定它還沒有載入(或者已載入,但載入方式不符合我的要求),接著執行一次專用“.loadby”。如果這沒有生成任何錯誤,我將重複搜索。如果無法載入 SOS 或找不到命令,我將返回失敗資訊 (E_FAIL)。

找到命令後,我會立即執行和重寫該命令(例如:ParseDSO)並輸出 DML 版本(參考代碼下載中的 Example08)。

在 SOS 命令的 DML 包裝程式中,我在執行命令前進行該項檢查是為了確保 SOS 沒有在調用之間被卸載並且所需的命令已存在。圖 9圖 10顯示了我正在執行的 !dsox 命令,具有 x86 和 x64 目標。

圖 9 Test03 x86 !dsox Wrapping !dumpstackobjects

圖 10 Test03 x64 !dsox Wrapping !dumpstackobjects

命令將 !dumpobj 超連結添加到 !dumpstackobjects 中列出的每個位址;!dsox 的代碼位於 Example08 中。

結語

借助極少量的基礎結構代碼,可以利用所有內置或擴展命令的邏輯。我的 SOS 擴展解析與添加 DML 有關,但是它可能與收集託管類資訊有關。不必瞭解任何關於 CLR 內部結構的資訊即可實現目標。我只需要知道如何導航到我想要顯示的資料。

通過資料檢索的自動化和顯示的豐富化,調試器擴展簡化了調試分析。我希望本系列能為您帶來靈感,讓您能夠實現屬於自己的擴展,簡化您執行的調試工作。

如果您對調試感興趣並且希望深入瞭解,您應該看看“高級 Windows 調試和疑難排解”博客 (blogs.msdn.com/b/ntdebugging),該博客提供了大量培訓和案例研究文章。

Andrew Richards  是 Windows OEM 方面的 Microsoft 高級專家級工程師。他熱衷於支援工具,並且在不斷創造能夠簡化支援工程師工作的調試器擴展和應用程式。

衷心感謝以下技術專家對本文的審閱: Drew Bliss