Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DbMon.NET - A simple .NET OutputDebugString capturer

0.00/5 (No votes)
7 Mar 2006 1  
A .NET port of the VC++ 6.0 sample, 'dbmon'.

Introduction

The .NET class System.Diagnostics.Debug provides a set of methods and properties that help debug your code. An example for this might be System.Diagnostics.Debug.WriteLine("Program loaded."). You can capture this output by attaching a debugger to your program, or starting it in 'Debug' mode. But what if you launch your program without a debugger attached? Where do these messages go?

To answer this question, one needs to know that all method calls prefixed with Debug.Write* are redirected to the kernel32.dll function OutputDebugString. Therefore, tools like the famous and all beloved DebugView from SysInternals, which are capturing the output of this kernel call, can display debugging texts even if they are not .NET programs.

Still no answer to what happens to Debug.WriteLine calls if no debugger is attached. Perhaps DebugView's homepage can bring some light into our darkness, but the only information revealed is:

  • you don�t need a debugger to catch the debug output of your applications
  • nor do you need to modify your applications [...] to use non-standard debug output APIs.

According to this, it is possible to capture those calls without modifying any function tables or doing some "illegal" process memory modifications.

Let's go back to what we know for sure. Debug.Write* method calls are redirected to OutputDebugString. The MSDN API documentation for this function doesn't help us, the only information you will get is "If the application has no debugger [...], OutputDebugString does nothing.", which must be somehow wrong since it does at least something as we can see when using DebugView.

A few Google searches later, I stumbled upon a Visual C++ 6.0 sample called "DbMon - Implements a Debug Monitor" which explained everything needed to build a .NET OutputDebugString capturer. With the help of this sample, one can explain where those messages go when no debugger is attached.

OutputDebugString Internals

The kernel32.dll function OutputDebugString uses two techniques that help us capture debug messages:

Interprocess memory sharing via CreateFileMapping

The data passed to OutputDebugString is stored in a shared memory segment which can be accessed by every process running on the same machine. The name of this shared memory segment is DBWIN_BUFFER. To read this memory, you just have to create a new file mapping to and a view this segment. Then you are prepared to read from this memory although it is outside your process scope.

Interprocess synchronization via CreateEvent/SetEvent

To get notified when a new message is available in our DBWIN_BUFFER, Microsoft uses two interprocess events called "DBWIN_BUFFER_READY" and "DBWIN_DATA_READY". The first event is used to let application(s) know that someone is listening to the shared buffer segment. The second event is used to notify the capturing application that data is available.

The shared memory segment used in OutputDebugString is rather trivial. The first DWORD (4 bytes) is the process ID of the client application which called OutputDebugString, the rest of the buffer is a null (\0) terminated string contain the debugging text.

PID
(4 bytes)
text
(n bytes terminated with \0)
                         

Defining the .NET interface

Now that we know the internals of Debug.Write*, we can start building a .NET framework around it. It should be usable via delegates and one shall turn it on or off. That's all we need to make it publicly visible, the rest is not relevant for building a capturing application.

An example console application might look like this:

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);
}

Implementing the .NET monitor

void DbMon.NET.DebugMonitor.Start()

To make this debug monitor listen on OutputDebugString calls, we need to setup our two events mentioned above and create a file mapping to the shared buffer.

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'");
    }
        
    //...

Also, create a new thread where we can listen to the event DBWIN_DATA_READY for not blocking the executing thread when calling 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()

The Capture method is an endless loop which waits for a signal to the event DBWIN_DATA_READY. If it gets notified, it starts reading the shared memory segment DBWIN_BUFFER, extracts the information, and calls the .NET event DebugMonitor.OnOutputDebugString.

private static void Capture() {                    
    // ...

    
    while (true) {        
        SetEvent(m_AckEvent);    
        // ...

        WaitForSingleObject(m_ReadyEvent, INFINITE);                
        // ...        

        if (OnOutputDebugString != null)
            OnOutputDebugString(pid, text);        
    }    
                    
    // ...

}

Control flow of the method Capture().

Reading the shared memory segment

The Visual Studio C++ sample is simple to port to .NET. You just need to P/Invoke all external function calls. But two lines of code are a bit trickier since they use C's ability of manipulating pointers:

LPSTR String = (LPSTR)SharedMem + sizeof(DWORD);
DWORD pThisPid = SharedMem;

The first line assigns to the variable String a new pointer of SharedMem starting at the offset sizeof(DWORD) (4 bytes). Since we know how this buffer is structured, we can use the helper class System.Runtime.InteropServices.Marshal to extract the PID, which can be done by reading the first 4 bytes as an Int32:

int pid = Marshal.ReadInt32(SharedMem);

To extract the text, we need to skip the first 4 bytes.

IntPtr sharedMemAfterPid = new IntPtr(SharedMem.ToInt32() + 
                           Marshal.SizeOf(typeof(Int32)));

Now we can use Marshal.PtrToStringAnsi to copy the content of this pointer to a .NET string:

string text = Marshal.PtrToStringAnsi(sharedMemAfterPid);

Conclusion

This article is not meant to be a replacement to SysInternals' DebugView. It is the best tool for debugging programs when attaching to a debugger is not an option. The intent is to show how OutputDebugString works, and I hope I could help in bringing some light into this darkness.

References

  • DebugView: An (the) OutputDebugString capturer.
  • PInvoke: Porting Win32 API calls to .NET.
  • DbMon: Implements a Debug Monitor.

API References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here