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






4.92/5 (75投票s)
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。