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

OutputDebugString 的机制

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (16投票s)

2008年2月20日

CPOL

4分钟阅读

viewsIcon

119310

downloadIcon

3542

编写一个监控应用程序来捕获调试消息

引言

本文重点介绍如何捕获应用程序调用 Win32 API OutputDebugString 输出的调试字符串。如果您使用过 Sysinternals 制作的 DebugView,那么您对这个工具的使用会很熟悉。

背景

  • 对象

    创建应用程序和 monitor 或调试器之间交互通信机制的内核对象有 4 个

    1. DBWinMutex: 一个 Mutex 对象。用于创建 OutputDebugString 的独占执行。
    2. DBWIN_BUFFER_READY: 这是一个 Event 对象。当共享内存准备好时,Monitor 或 Debugger 会向 OutputDebugString 发送信号。
    3. DBWIN_DATA_READY: 这是一个 Event 对象。当 OutputDebugString 例程完成写入共享内存时,此事件将被触发。
    4. DBWIN_BUFFER: 这是一个共享内存的名称。大小为 4K 字节,前 4 个字节表示进程 ID,后面是调试字符串的内容。

  • 工作流

    以下是 monitorOutputDebugString 之间通信机制的工作流程。

  • 捕获调试消息
  • 如果您想在应用程序中捕获调试消息,您需要创建或打开这 4 个内核对象,并创建一个监控共享内存的任务。当数据准备好时,任务会将其发送出去。下图显示了由应用程序本身捕获的调试消息

  • UI 应用程序中要小心

    当您编写像上面这样的 UI 应用程序时要小心,如果您在 UI 线程中连续两次调用 OutputDebugString,您的应用程序将阻塞 10 秒钟,然后第一个调试字符串才会被输出。原因是:第一次调用 OutputDebugString 后,monitor 线程捕获调试字符串,并将其发送到 UI 的 Editbox 并等待响应,但此时 UI 线程转到第二个 OutputDebugString 并等待 BufferReady 事件,而此事件需要由 monitor 线程在其数据处理完成后触发。这意味着这两个线程,UI 线程和 Monitor 线程,处于死锁状态。

    如何解决这种情况:当您有大量连续的调试字符串需要输出时,您需要将 UI 线程保持在一个简单的独立线程中,并在其他线程中运行 OutputDebugString

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.exeoutput.exe 发送的所有调试消息都将被 monitor.exe 捕获并显示。

关注点

源代码是初学者学习 Win32 多线程和内核对象(如互斥体、事件和共享内存)的一个很好的示例。当我查找 OutputDebugString 的源代码时,我发现 ReactOS 有。在研究之后,我写了这篇文章。

注意:ReactOS 是一个开源操作系统,它实现了另一个 Windows XP。

参考文献

历史

  • 2008-2-21: 文章创建
  • 2008-2-21: 文章更新 - 删除了与当前进程 ID 比较的进程 ID
  • 2008-2-21: 文章更新 - 添加了示例部分和演示项目下载
© . All rights reserved.