65.9K
CodeProject 正在变化。 阅读更多。
Home

DbMon.NET - 一个简单的 .NET OutputDebugString 捕获器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (75投票s)

2006年3月7日

CPOL

5分钟阅读

viewsIcon

213931

downloadIcon

3911

VC++ 6.0 示例 'dbmon' 的 .NET 移植版。

引言

`.NET` 类 `System.Diagnostics.Debug` 提供了一组帮助您调试代码的方法和属性。例如,可能是 `System.Diagnostics.Debug.WriteLine(""Program loaded.")`。您可以通过将调试器附加到程序或在“Debug”模式下启动程序来捕获此输出。但是,如果您在没有附加调试器的情况下启动程序怎么办?这些消息会去哪里?

为了回答这个问题,我们需要知道所有以 `Debug.Write*` 开头的方法调用都被重定向到 `kernel32.dll` 函数 `OutputDebugString`。因此,像 SysInternals 著名且备受喜爱的 DebugView 这样的工具,它们会捕获此内核调用的输出,即使它们不是 .NET 程序也能显示调试文本。

仍然没有答案,如果未附加调试器,`Debug.WriteLine` 调用会发生什么。也许 DebugView 的主页可以为我们带来一些光明,但唯一透露的信息是

  • 您无需调试器即可捕获应用程序的调试输出
  • 您也无需修改您的应用程序 [...] 来使用非标准调试输出 API。

根据这一点,可以在不修改任何函数表或进行任何“非法”进程内存修改的情况下捕获这些调用。

让我们回到我们确切知道的内容。`Debug.Write*` 方法调用被重定向到 `OutputDebugString`。此函数的 MSDN API 文档对此没有帮助,您将获得的唯一信息是*“如果应用程序没有调试器 [...],`OutputDebugString` 将不执行任何操作。”*,这一定有某种错误,因为当我们使用 DebugView 时,它至少会执行一些操作。

经过几次 Google 搜索后,我偶然发现了一个名为“DbMon - Implements a Debug Monitor”的 Visual C++ 6.0 示例,它解释了构建 .NET `OutputDebugString` 捕获器所需的一切。借助此示例,可以解释在未附加调试器时这些消息的去向。

OutputDebugString 内部机制

`kernel32.dll` 函数 `OutputDebugString` 使用两种技术帮助我们捕获调试消息

通过 CreateFileMapping 进行进程间内存共享

传递给 `OutputDebugString` 的数据存储在一个共享内存段中,同一台机器上运行的每个进程都可以访问该内存段。此共享内存段的名称是 `DBWIN_BUFFER`。要读取此内存,您只需创建一个新的文件映射并映射此段。然后,您就可以读取此内存,尽管它在您的进程范围之外。

通过 CreateEvent/SetEvent 进行进程间同步

为了在我们的 `DBWIN_BUFFER` 中有新消息时收到通知,Microsoft 使用了两个进程间事件,称为 `"DBWIN_BUFFER_READY"` 和 `"DBWIN_DATA_READY"`。第一个事件用于让应用程序知道有人正在监听共享缓冲区段。第二个事件用于通知捕获应用程序数据可用。

`OutputDebugString` 中使用的共享内存段相当简单。第一个 `DWORD`(4 字节)是调用 `OutputDebugString` 的客户端应用程序的进程 ID,缓冲区其余部分是一个以 null(\0)结尾的字符串,包含调试文本。

PID
(4 字节)
文本
(n 字节,以 \0 结尾)
                         

定义 .NET 接口

既然我们知道了 `Debug.Write*` 的内部机制,我们就可以开始围绕它构建一个 .NET 框架。它应该可以通过委托使用,并且应该可以打开或关闭。这就是我们让它公开可见所需要的一切,其余的对于构建捕获应用程序来说并不重要。

一个示例控制台应用程序可能如下所示

public static void Main(string[] args) {
    DebugMonitor.Start();
    DebugMonitor.OnOutputDebugString += new 
          OnOutputDebugStringHandler(OnOutputDebugString);
    Console.WriteLine("Press 'Enter' to exit.");
    Console.ReadLine();
    DebugMonitor.Stop();
}

private static void OnOutputDebugString(int pid, string text) {
    Console.WriteLine(DateTime.Now + ": [" + pid + "] " + text);
}

实现 .NET 监视器

void DbMon.NET.DebugMonitor.Start()

为了让这个调试监视器监听 `OutputDebugString` 调用,我们需要设置上面提到的两个事件,并为共享缓冲区创建一个文件映射。

public static void Start() {
    // ...
    
    // Create the event for slot 'DBWIN_BUFFER_READY'
    m_AckEvent = CreateEvent(ref sa, false, 
                   false, "DBWIN_BUFFER_READY");
    if (m_AckEvent == IntPtr.Zero) {
        throw CreateApplicationException("Failed to create" + 
                                         " event 'DBWIN_BUFFER_READY'");
    }    
    
    // Create the event for slot 'DBWIN_DATA_READY'
    m_ReadyEvent = CreateEvent(ref sa, false, false, "DBWIN_DATA_READY");
    if (m_ReadyEvent == IntPtr.Zero) {
        throw CreateApplicationException("Failed to create" + 
                                         " event 'DBWIN_DATA_READY'");
    }    
    
    // Get a handle to the readable shared memory at slot 'DBWIN_BUFFER'.
    m_SharedFile = CreateFileMapping(new IntPtr(-1), ref sa, 
                   PageProtection.ReadWrite, 0, 4096, "DBWIN_BUFFER");
    if (m_SharedFile == IntPtr.Zero) {
        throw CreateApplicationException("Failed to create" + 
                   " a file mapping to slot 'DBWIN_BUFFER'");
    }
        
    //...

此外,创建一个新线程,我们可以在其中监听 `DBWIN_DATA_READY` 事件,以免在调用 `Start` 时阻塞执行线程。

    // ...

    // Start a new thread where we can capture the output
    // of OutputDebugString calls so we don't block here.
    m_Capturer = new Thread(new ThreadStart(Capture));
    m_Capturer.Start();                
}

void DbMon.NET.DebugMonitor.Capture()

`Capture` 方法是一个无限循环,它等待 `DBWIN_DATA_READY` 事件的信号。如果收到通知,它将开始读取共享内存段 `DBWIN_BUFFER`,提取信息,并调用 .NET 事件 `DebugMonitor.OnOutputDebugString`。

private static void Capture() {                    
    // ...
    
    while (true) {        
        SetEvent(m_AckEvent);    
        // ...
        WaitForSingleObject(m_ReadyEvent, INFINITE);                
        // ...        
        if (OnOutputDebugString != null)
            OnOutputDebugString(pid, text);        
    }    
                    
    // ...
}

`Capture()` 方法的控制流。

读取共享内存段

Visual Studio C++ 示例很容易移植到 .NET。您只需要 P/Invoke 所有外部函数调用。但是有两行代码有点棘手,因为它们使用了 C 操作指针的能力

LPSTR String = (LPSTR)SharedMem + sizeof(DWORD);
DWORD pThisPid = SharedMem;

第一行将 `String` 变量赋值为一个新的 `SharedMem` 指针,该指针从偏移量 `sizeof(DWORD)`(4 字节)开始。由于我们知道此缓冲区的结构,我们可以使用辅助类 `System.Runtime.InteropServices.Marshal` 来提取 PID,这可以通过将前 4 个字节读取为 `Int32` 来完成

int pid = Marshal.ReadInt32(SharedMem);

要提取文本,我们需要跳过前 4 个字节。

IntPtr sharedMemAfterPid = new IntPtr(SharedMem.ToInt32() + 
                           Marshal.SizeOf(typeof(Int32)));

现在我们可以使用 `Marshal.PtrToStringAnsi` 将此指针的内容复制到 .NET string

string text = Marshal.PtrToStringAnsi(sharedMemAfterPid);

结论

本文无意取代 SysInternals 的 DebugView。它是调试程序在无法附加调试器时的最佳工具。目的是展示 `OutputDebugString` 的工作原理,我希望我已经帮助大家揭开了这层神秘的面纱。

参考文献

  • DebugView: 一个(经典的)`OutputDebugString` 捕获器。
  • PInvoke: 将 Win32 API 调用移植到 .NET。
  • DbMon: 实现一个 Debug Monitor。

API 参考

© . All rights reserved.