OutputDebugString 的机制
编写一个监控应用程序来捕获调试消息
引言
本文重点介绍如何捕获应用程序调用 Win32 API OutputDebugString 输出的调试字符串。如果您使用过 Sysinternals 制作的 DebugView,那么您对这个工具的使用会很熟悉。
背景
- 对象 
创建应用程序和 monitor或调试器之间交互通信机制的内核对象有 4 个- DBWinMutex: 一个- Mutex对象。用于创建- OutputDebugString的独占执行。
- DBWIN_BUFFER_READY: 这是一个- Event对象。当共享内存准备好时,- Monitor或 Debugger 会向- OutputDebugString发送信号。
- DBWIN_DATA_READY: 这是一个- Event对象。当- OutputDebugString例程完成写入共享内存时,此事件将被触发。
- DBWIN_BUFFER: 这是一个共享内存的名称。大小为 4K 字节,前 4 个字节表示进程 ID,后面是调试字符串的内容。
 
- 工作流 
以下是 monitor和OutputDebugString之间通信机制的工作流程。
- 捕获调试消息
- UI 应用程序中要小心 
当您编写像上面这样的 UI 应用程序时要小心,如果您在 UI 线程中连续两次调用 OutputDebugString,您的应用程序将阻塞 10 秒钟,然后第一个调试字符串才会被输出。原因是:第一次调用OutputDebugString后,monitor线程捕获调试字符串,并将其发送到 UI 的Editbox并等待响应,但此时 UI 线程转到第二个OutputDebugString并等待BufferReady事件,而此事件需要由monitor线程在其数据处理完成后触发。这意味着这两个线程,UI线程和Monitor线程,处于死锁状态。如何解决这种情况:当您有大量连续的调试字符串需要输出时,您需要将 UI 线程保持在一个简单的独立线程中,并在其他线程中运行 OutputDebugString。
如果您想在应用程序中捕获调试消息,您需要创建或打开这 4 个内核对象,并创建一个监控共享内存的任务。当数据准备好时,任务会将其发送出去。下图显示了由应用程序本身捕获的调试消息
Using the Code
接口声明
类 CWinDebugMonitor 只有一个 public 虚方法:OutputWinDebugString,开发人员可以声明一个派生自 CWinDebugMonitor 的类,并实现该虚方法来处理所有捕获的调试字符串。
//////////////////////////////////////////////////////////////
//
//         File: WinDebugMonitor.h
//  Description: Interface of class CWinDebugMonitor
//      Created: 2007-12-6
//       Author: Ken Zhang
//       E-Mail: cpp.china@hotmail.com
//
//////////////////////////////////////////////////////////////
#ifndef __WIN_DEBUG_BUFFER_H__
#define __WIN_DEBUG_BUFFER_H__
#include <windows.h>
#include <atlbase.h>
#include <atlstr.h>
class CWinDebugMonitor
{
private:
    enum {
        TIMEOUT_WIN_DEBUG    =    100,
    };
    struct dbwin_buffer
    {
        DWORD   dwProcessId;
        char    data[4096-sizeof(DWORD)];
    };
private:
    HANDLE m_hDBWinMutex;
    HANDLE m_hDBMonBuffer;
    HANDLE m_hEventBufferReady;
    HANDLE m_hEventDataReady;
    HANDLE m_hWinDebugMonitorThread;
    BOOL m_bWinDebugMonStopped;
    struct dbwin_buffer *m_pDBBuffer;
private:
    DWORD Initialize();
    void Unintialize();
    DWORD WinDebugMonitorProcess();
    static DWORD WINAPI WinDebugMonitorThread(void *pData);
public:
    CWinDebugMonitor();
    ~CWinDebugMonitor();
public:
    virtual void OutputWinDebugString(const char *str) {};
};
#endif
CWinDebugMonitor 的初始化
Initialize 方法打开前面描述的所有内核对象,并创建一个监控和处理调试消息的线程。
DWORD CWinDebugMonitor::Initialize()
{
    DWORD errorCode = 0;
    BOOL bSuccessful = FALSE;
    SetLastError(0);
    // Mutex: DBWin
    // ---------------------------------------------------------
    CComBSTR DBWinMutex = L"DBWinMutex";
    m_hDBWinMutex = ::OpenMutex(
        MUTEX_ALL_ACCESS,
        FALSE,
        DBWinMutex
        );
    if (m_hDBWinMutex == NULL) {
        errorCode = GetLastError();
        return errorCode;
    }
    // Event: buffer ready
    // ---------------------------------------------------------
    CComBSTR DBWIN_BUFFER_READY = L"DBWIN_BUFFER_READY";
    m_hEventBufferReady = ::OpenEvent(
        EVENT_ALL_ACCESS,
        FALSE,
        DBWIN_BUFFER_READY
        );
    if (m_hEventBufferReady == NULL) {
        m_hEventBufferReady = ::CreateEvent(
            NULL,
            FALSE,    // auto-reset
            TRUE,    // initial state: signaled
            DBWIN_BUFFER_READY
            );
        if (m_hEventBufferReady == NULL) {
            errorCode = GetLastError();
            return errorCode;
        }
    }
    // Event: data ready
    // ---------------------------------------------------------
    CComBSTR DBWIN_DATA_READY = L"DBWIN_DATA_READY";
    m_hEventDataReady = ::OpenEvent(
        SYNCHRONIZE,
        FALSE,
        DBWIN_DATA_READY
        );
    if (m_hEventDataReady == NULL) {
        m_hEventDataReady = ::CreateEvent(
            NULL,
            FALSE,    // auto-reset
            FALSE,    // initial state: nonsignaled
            DBWIN_DATA_READY
            );
        if (m_hEventDataReady == NULL) {
            errorCode = GetLastError();
            return errorCode;
        }
    }
    // Shared memory
    // ---------------------------------------------------------
    CComBSTR DBWIN_BUFFER = L"DBWIN_BUFFER";
    m_hDBMonBuffer = ::OpenFileMapping(
        FILE_MAP_READ,
        FALSE,
        DBWIN_BUFFER
        );
    if (m_hDBMonBuffer == NULL) {
        m_hDBMonBuffer = ::CreateFileMapping(
            INVALID_HANDLE_VALUE,
            NULL,
            PAGE_READWRITE,
            0,
            sizeof(struct dbwin_buffer),
            DBWIN_BUFFER
            );
        if (m_hDBMonBuffer == NULL) {
            errorCode = GetLastError();
            return errorCode;
        }
    }
    m_pDBBuffer = (struct dbwin_buffer *)::MapViewOfFile(
        m_hDBMonBuffer,
        SECTION_MAP_READ,
        0,
        0,
        0
        );
    if (m_pDBBuffer == NULL) {
        errorCode = GetLastError();
        return errorCode;
    }
    // Monitoring thread
    // ---------------------------------------------------------
    m_bWinDebugMonStopped = FALSE;
    m_hWinDebugMonitorThread = ::CreateThread(
        NULL,
        0,
        WinDebugMonitorThread,
        this,
        0,
        NULL
        );
    if (m_hWinDebugMonitorThread == NULL) {
        m_bWinDebugMonStopped = TRUE;
        errorCode = GetLastError();
        return errorCode;
    }
    // set monitor thread's priority to highest
    // ---------------------------------------------------------
    bSuccessful = ::SetPriorityClass(
        ::GetCurrentProcess(),
        REALTIME_PRIORITY_CLASS
        );
    bSuccessful = ::SetThreadPriority(
        m_hWinDebugMonitorThread,
        THREAD_PRIORITY_TIME_CRITICAL
        );
    return errorCode;
}
取消初始化
Uninitialize 是一个 private 方法,它在析构函数中自动调用。它停止 monitor 线程并释放所有打开的内核对象。
void CWinDebugMonitor::Unintialize()
{
    if (m_hWinDebugMonitorThread != NULL) {
        m_bWinDebugMonStopped = TRUE;
        ::WaitForSingleObject(m_hWinDebugMonitorThread, INFINITE);
    }
    if (m_hDBWinMutex != NULL) {
        CloseHandle(m_hDBWinMutex);
        m_hDBWinMutex = NULL;
    }
    if (m_hDBMonBuffer != NULL) {
        ::UnmapViewOfFile(m_pDBBuffer);
        CloseHandle(m_hDBMonBuffer);
        m_hDBMonBuffer = NULL;
    }
    if (m_hEventBufferReady != NULL) {
        CloseHandle(m_hEventBufferReady);
        m_hEventBufferReady = NULL;
    }
    if (m_hEventDataReady != NULL) {
        CloseHandle(m_hEventDataReady);
        m_hEventDataReady = NULL;
    }
    m_pDBBuffer = NULL;
}
处理捕获的调试字符串
此方法由 monitor 线程调用,当有调试字符串可用时,在这里调用用户实现的 OutputWinDebugString 方法。
DWORD CWinDebugMonitor::WinDebugMonitorProcess()
{
    DWORD ret = 0;
    // wait for data ready
    ret = ::WaitForSingleObject(m_hEventDataReady, TIMEOUT_WIN_DEBUG);
    if (ret == WAIT_OBJECT_0) {
        OutputWinDebugString(m_pDBBuffer->data);
        // signal buffer ready
        SetEvent(m_hEventBufferReady);
    }
    return ret;
} 
监控线程
monitor 线程在 Initialize 中自动启动(如果所有初始化工作都成功完成),并通过 Uninitialize 停止。
DWORD WINAPI CWinDebugMonitor::WinDebugMonitorThread(void *pData)
{
    CWinDebugMonitor *_this = (CWinDebugMonitor *)pData;
    if (_this != NULL) {
        while (!_this->m_bWinDebugMonStopped) {
            _this->WinDebugMonitorProcess();
        }
    }
    return 0;
}
示例代码
源代码的使用非常简单,我们可以创建两个项目,一个是以 monitor 方式捕获调试消息,另一个项目负责通过调用 OutputDebugString 发送连续的调试消息。首先,运行 monitor.exe,然后运行 output.exe,您将看到 output.exe 发送的所有消息都将被 monitor.exe 捕获。
注意:请勿在 Visual Studio 调试模式下测试,因为 Visual Studio 会在调试消息到达 WinDebugMonitor 之前捕获它们。
监控应用程序
monitor 捕获所有调试消息并打印它们。如果按下任意键,它将退出。
#include "WinDebugMonitor.h"
#include <conio.h>
class Monitor : public CWinDebugMonitor
{
public:
    virtual void OutputWinDebugString(const char *str)
    {
        printf("%s", str);
    };
};
void main()
{
    printf("Win Debug Monitor Tool\n");
    printf("----------------------\n");
    Monitor mon;
    getch();
}
输出应用程序
此应用程序不断发送大量调试消息,如果按下任意键,它将退出循环。
#include "stdafx.h"
#include <conio.h>
#include <windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
    int i = 0;
    printf("Press any key to stop calling OutputDebugString......\n");
    while (!kbhit()) {
        TCHAR buf[64];
        _stprintf(buf, _T("Message from process %d, msg id: %d\n"),
                ::GetCurrentProcessId(), ++i);
        OutputDebugString(buf);
    }
    printf("Total %d messages sent.\n", i);
    return 0;
}
运行这两个应用程序
启动两个命令行控制台,在一个控制台中运行 monitor.exe,在另一个控制台中运行 output.exe,output.exe 发送的所有调试消息都将被 monitor.exe 捕获并显示。
关注点
源代码是初学者学习 Win32 多线程和内核对象(如互斥体、事件和共享内存)的一个很好的示例。当我查找 OutputDebugString 的源代码时,我发现 ReactOS 有。在研究之后,我写了这篇文章。
注意:ReactOS 是一个开源操作系统,它实现了另一个 Windows XP。
参考文献
历史
- 2008-2-21: 文章创建
- 2008-2-21: 文章更新 - 删除了与当前进程 ID 比较的进程 ID
- 2008-2-21: 文章更新 - 添加了示例部分和演示项目下载




