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: 文章更新 - 添加了示例部分和演示项目下载