使用DebugView实时显示Log

DebugView简介

DebugView是一个监视本地系统或者通过tcp/ip连接的网络系统的OutputDebugString输出的应用程序。DebugView不仅能够监视Win32应用的debug输出,还可以监视内核模型的debug输出。因此,如果使用OutputDebugString来打印调试信息的话,就可以在程序运行时候通过DebugView来实时显示程序的调试信息。
这种方式在某种意义上,比将Log打印到文件中,关闭程序后再查看Log输出的方式更加方便。而且可以将这两种调试程序的方式结合起来,既使用DebugView来实时显示调试信息,又将调试信息输出到Log文件中,方便以后分析。

安装DebugView

下载地址:DebugView
下载后面后解压压缩包,发现里面有三个文件:Dbgview.exe、dbgview.chm、Eula.txt。
Dbgview.exe就是我们要使用的实时显示Log工具。dbgview.chm是自带的文档,有不懂的地方可以查阅该文档。
现在可以将DebugView.exe放到任何你喜欢的目录,比如桌面。

配置DebugView

配置Capture

如下图所示,要记得勾选Capture Win32和Capture Global Win32。Capture Global Win32用于网络模式下捕获网络主机的Debug输出的时候。如果需要捕获内核模式的调试输出,记得勾选Capture Kernel Win32。
enter description here
如果点击Capture Global Win32菜单出现提示:
enter description here
重新以管理员的身份启动DebugView。
enter description here

配置Filter

如下图所示,打开Filter对话框,
enter description hereenter description here
然后在Include中输入要包含的字符串,比如”hankpcxiao”,多个字符串用;分隔,比如”hankpcxiao;xpc”。
这样就只会捕获包括过滤字符串hankpcxiao或者xpc的OutputDebugString输出。
如果我们在每个OutputDebugString输出前自动加上过滤字符串,那么DebugView就只会输出我们的Log信息了。

开启捕获

最后确保开启了捕获,如下图所示:
enter description here

如何在程序中输出Log信息?

默认情况下,DebugView会捕获函数OutputDebugString的输出,但是这个函数的参数是个字符串指针,不太方便。下面我们通过一些列步骤来创建一个方便使用的Log类。

格式化输出

我们习惯使用printf这样的函数来格式化输出信息,因此这次我们也把OutputDebugString包装成可变参数形式的格式化输出函数。

1
2
3
4
5
6
7
8
9
10
11
void DebugViewOutput(const char* fm, ...)
{
static char szMsg[MAX_PATH];

va_list argList;
va_start(argList, fm);
vsprintf_s(szMsg, fm, argList);
va_end(argList);

OutputDebugString(szMsg);
}

让DebugView在VS调试程序时候也能够捕获Log

网上有不少介绍DebugView使用的文章,但是都忽略了一个事实,那就是默认情况下,使用VS运行程序时候,OutputDebugString的输出是到VS的输出窗口中,DebugView中并没有任何信息。只有单独运行程序的时候,DebugView才能够捕捉到信息。
但是这样就不能结合打断点调试和DebugView两个强大的调试方法了。不过,还是有解决办法的。通过一个叫做DBWin通信机制可以实现调试程序时候,把OutputDebugString的输出信息显示到DebugView窗口中。这套机制的本质是通过内存映射文件来跨进程交换数据。
具体参考以下的类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
    class XpcDebugView
{
public:
XpcDebugView() {}
~XpcDebugView() {}
void XpcDebugViewOutput(const char* fm, ...);

private:
struct DBWinBuffer
{
DWORD ProcessId;
char Data[4096 - sizeof(DWORD)];
};
bool Initialize();
void UnInitialize();

HANDLE m_mutex;
HANDLE m_fileMapping;
HANDLE m_bufferReadyEvent;
HANDLE m_dataReadyEvent;
DBWinBuffer* m_buffer;
};

bool XpcDebugView::Initialize()
{
m_mutex = OpenMutex(SYNCHRONIZE, FALSE, TEXT("DBWinMutex"));

//打开DBWIN_BUFFER
m_fileMapping = OpenFileMapping(FILE_MAP_WRITE, FALSE, TEXT("DBWIN_BUFFER"));

if (m_fileMapping == NULL) return false;

//打开DBWIN_BUFFER_READY
m_bufferReadyEvent = OpenEvent(SYNCHRONIZE, FALSE, TEXT("DBWIN_BUFFER_READY"));

//打开DBWIN_DATA_READY
m_dataReadyEvent = OpenEvent(EVENT_MODIFY_STATE, FALSE, TEXT("DBWIN_DATA_READY"));

//等待DBWIN_BUFFER就绪
WaitForSingleObject(m_bufferReadyEvent, INFINITE);

//把DBWIN_BUFFER映射到某个地址
m_buffer = (DBWinBuffer*)MapViewOfFile(m_fileMapping, FILE_MAP_WRITE, 0, 0, 0);
m_buffer->ProcessId = GetCurrentProcessId();
}

void XpcDebugView::UnInitialize()
{
//释放和关闭DBWIN_BUFFER
FlushViewOfFile(m_buffer, 0);
UnmapViewOfFile(m_buffer);

//触发DBWIN_DATA_READY
SetEvent(m_dataReadyEvent);

CloseHandle(m_fileMapping);

//清理
CloseHandle(m_dataReadyEvent);
CloseHandle(m_bufferReadyEvent);
ReleaseMutex(m_mutex);
CloseHandle(m_mutex);
}

void XpcDebugView::XpcDebugViewOutput(const char* fm, ...)
{
if (Initialize() == false) return;

static char szMsg[MAX_PATH];

va_list argList;
va_start(argList, fm);
vsprintf_s(szMsg, fm, argList);
va_end(argList);

#pragma warning( push )
#pragma warning( disable: 4996 )
//向DBWIN_BUFFER写入数据
strcpy(m_buffer->Data, szMsg);
printf(szMsg);
#pragma warning( pop )

UnInitialize();
}

使用的时候直接调用类成员函数XpcDebugViewOutput即可。
关于这部分的内容更具体的可以参考文章,如何让OutputDebugString绕过调试器

输出到DebugView的同时输出到Log文件

我将上面的类改造成下面的样子,在初始化时候创建一个Log文件,在反初始化时候关闭Log文件,每次调用XpcDebugViewOutput使用调用fprintf将格式化字符串输出到文件中。这样就能达到输出Log信息到DebugView中的同时,又能够将Log信息持久化保存了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
    class XpcDebugView
{
public:
XpcDebugView() { m_pszLogName = "DefaultLog.txt"; Initialize(); }
XpcDebugView(const char* pszLogName) { m_pszLogName = pszLogName; Initialize();}
~XpcDebugView() { UnInitialize(); }
void XpcDebugViewOutput(const char* fm, ...);

private:
struct DBWinBuffer
{
DWORD ProcessId;
char Data[4096 - sizeof(DWORD)];
};

bool Initialize();
void UnInitialize();

bool InitializeDBWin();
void UnInitializeDBWin();

HANDLE m_mutex;
HANDLE m_fileMapping;
HANDLE m_bufferReadyEvent;
HANDLE m_dataReadyEvent;
DBWinBuffer* m_buffer;
const char* m_pszLogName;
FILE* m_pFileLog;
};

#pragma warning( push )
#pragma warning( disable: 4996 )
bool XpcDebugView::Initialize()
{
m_pFileLog = fopen(m_pszLogName, "w");
return m_pFileLog != NULL;
}

void XpcDebugView::UnInitialize()
{
if (m_pFileLog)
{
fclose(m_pFileLog);
}
}

bool XpcDebugView::InitializeDBWin()
{
m_mutex = OpenMutex(SYNCHRONIZE, FALSE, TEXT("DBWinMutex"));

//打开DBWIN_BUFFER
m_fileMapping = OpenFileMapping(FILE_MAP_WRITE, FALSE, TEXT("DBWIN_BUFFER"));

if (m_fileMapping == NULL) return false;

//打开DBWIN_BUFFER_READY
m_bufferReadyEvent = OpenEvent(SYNCHRONIZE, FALSE, TEXT("DBWIN_BUFFER_READY"));

//打开DBWIN_DATA_READY
m_dataReadyEvent = OpenEvent(EVENT_MODIFY_STATE, FALSE, TEXT("DBWIN_DATA_READY"));

//等待DBWIN_BUFFER就绪
WaitForSingleObject(m_bufferReadyEvent, INFINITE);

//把DBWIN_BUFFER映射到某个地址
m_buffer = (DBWinBuffer*)MapViewOfFile(m_fileMapping, FILE_MAP_WRITE, 0, 0, 0);
m_buffer->ProcessId = GetCurrentProcessId();
}

void XpcDebugView::UnInitializeDBWin()
{
//释放和关闭DBWIN_BUFFER
FlushViewOfFile(m_buffer, 0);
UnmapViewOfFile(m_buffer);

//触发DBWIN_DATA_READY
SetEvent(m_dataReadyEvent);

CloseHandle(m_fileMapping);

//清理
CloseHandle(m_dataReadyEvent);
CloseHandle(m_bufferReadyEvent);
ReleaseMutex(m_mutex);
CloseHandle(m_mutex);
}

void XpcDebugView::XpcDebugViewOutput(const char* fm, ...)
{
if (InitializeDBWin() == false) return;

static char szMsg[MAX_PATH];

va_list argList;
va_start(argList, fm);
vsprintf_s(szMsg, fm, argList);
va_end(argList);

//向DBWIN_BUFFER写入数据
strcpy(m_buffer->Data, szMsg);
if (m_pFileLog)
{
fprintf(m_pFileLog, szMsg);
}

UnInitializeDBWin();
}
#pragma warning( pop )

如何使用XpcDebugView类

最简单的方式是定义一个XpcDebugView的全局变量,比如:
XpcDebugView myDebugview(“myLog.txt”);
输出Log信息的时候调用函数myDebugview.XpcDebugViewOutput(“%d %d %s\n”, 1, 2, “log”):
为了方便使用,可以在XpcDebugViewOutput的输出后面添加换行符,这样每次调用后就会自动换行了。
并且加上过滤字符串前缀,这样DebugView就只会捕获我们的输出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void XpcDebugView::XpcDebugViewOutput(const char* fm, ...)
{
static char szMsg[MAX_PATH];
static char szOutput[MAX_PATH];

va_list argList;
va_start(argList, fm);
vsprintf_s(szMsg, fm, argList);
va_end(argList);

strcpy(szOutput, "[hankpcxiao] ");
strcat(szOutput, szMsg);
strcat(szOutput, "\n");
if (m_pFileLog)
{
fprintf(m_pFileLog, szOutput);
}

//向DBWIN_BUFFER写入数据
if (InitializeDBWin())
{
strcpy(m_buffer->Data, szOutput);
UnInitializeDBWin();
}
}

总结

首先需要下载好DebugView程序,然后配置capture选项,另外是Filter字符串。最后为了保证在VS中调试程序时候,能够将调试信息输出到DebugView,需要使用DBWin通信进制。为此,我封装了一个Log类,在将Log输出到DebugView的同时也将Log输出到日志文件中。

参考资料:

[1] Frequently asked questions on the Microsoft application DebugView.exe
[2] 如何让OutputDebugString绕过调试器