Click here to Skip to main content
15,881,730 members
Articles / Programming Languages / C#
Article

DbMon.NET - A simple .NET OutputDebugString capturer

Rate me:
Please Sign up or sign in to vote.
4.92/5 (75 votes)
7 Mar 2006CPOL5 min read 208.8K   3.9K   88   31
A .NET port of the VC++ 6.0 sample, 'dbmon'.

Image 1

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.

Image 2

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.

Image 3

An example console application might look like this:

C#
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.

C#
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.

C#
    // ...

    // 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.

C#
private static void Capture() {                    
    // ...
    
    while (true) {        
        SetEvent(m_AckEvent);    
        // ...
        WaitForSingleObject(m_ReadyEvent, INFINITE);                
        // ...        
        if (OnOutputDebugString != null)
            OnOutputDebugString(pid, text);        
    }    
                    
    // ...
}

Image 4

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:

C++
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:

C#
int pid = Marshal.ReadInt32(SharedMem);

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

C#
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:

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
heuerm11-Jan-21 5:30
heuerm11-Jan-21 5:30 
GeneralMy vote of 5 Pin
User 110609797-Nov-14 5:22
User 110609797-Nov-14 5:22 
QuestionNice article, help create a new 'debugviewer' ? Pin
JanWilmans16-Oct-13 9:19
JanWilmans16-Oct-13 9:19 
GeneralThanks! Pin
shuizuan20-Oct-12 3:04
shuizuan20-Oct-12 3:04 
GeneralRe: Thanks! Pin
ryowu27-Nov-13 19:54
professionalryowu27-Nov-13 19:54 
QuestionHow to catch KdPrint() output from kernel? Pin
George1189-Oct-12 8:08
George1189-Oct-12 8:08 
QuestionThank you! This is great! Pin
ryowu18-May-12 15:07
professionalryowu18-May-12 15:07 
GeneralMessage filtering based on TraceLevel Pin
Member 240217113-Sep-10 22:32
Member 240217113-Sep-10 22:32 
Generaloriginal DBMON source code Pin
umeca744-Mar-10 22:14
umeca744-Mar-10 22:14 
GeneralRe: original DBMON source code Pin
Garth J Lancaster4-Apr-10 12:42
professionalGarth J Lancaster4-Apr-10 12:42 
QuestionWhat license is your code under...? Pin
Wing Flanagan18-Dec-08 12:21
Wing Flanagan18-Dec-08 12:21 
AnswerRe: What license is your code under...? Pin
Christian Birkl22-Dec-08 11:46
Christian Birkl22-Dec-08 11:46 
GeneralRe: What license is your code under...? Pin
chrisbhmg8-Aug-09 9:58
chrisbhmg8-Aug-09 9:58 
GeneralAnother alternative Pin
chaiguy133718-Nov-08 5:35
chaiguy133718-Nov-08 5:35 
QuestionHow to capture under windows 2003 terminal service Pin
Silent Night28-Jul-07 3:05
Silent Night28-Jul-07 3:05 
AnswerRe: How to capture under windows Vista Ultimate Pin
Oliman25-Jun-08 13:31
Oliman25-Jun-08 13:31 
GeneralA bug Pin
Igorp7113-Jul-07 5:21
Igorp7113-Jul-07 5:21 
You're passing an uninitialized SECURITY_ATTRIBUTES structure to CreateEvent calls. This leads to problems under non-admin user.

Should be something like this:

sdPtr = Marshal.AllocHGlobal(Marshal.SizeOf(sd));
Marshal.StructureToPtr(sd, sdPtr, false);

SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
sa.bInheritHandle = true;
sa.nLength = Marshal.SizeOf(sa);
sa.lpSecurityDescriptor = sdPtr;

QuestionCapture Global Win32 Pin
Igorp7111-Jul-07 13:08
Igorp7111-Jul-07 13:08 
AnswerRe: Capture Global Win32 Pin
Igorp7111-Jul-07 16:06
Igorp7111-Jul-07 16:06 
AnswerRe: Capture Global Win32 Pin
Siva Chandran P7-Jan-12 6:07
Siva Chandran P7-Jan-12 6:07 
QuestionUsing in a windows service? Pin
melchizedek19-Jun-07 11:15
melchizedek19-Jun-07 11:15 
AnswerRe: Using in a windows service? Pin
melchizedek20-Jun-07 5:30
melchizedek20-Jun-07 5:30 
AnswerRe: Using in a windows service? Pin
Siva Chandran P7-Jan-12 6:09
Siva Chandran P7-Jan-12 6:09 
GeneralNice! Pin
melchizedek12-Jun-07 10:58
melchizedek12-Jun-07 10:58 
GeneralNot capturing from ASP.NET Pin
Jacquers19-Apr-07 8:24
Jacquers19-Apr-07 8:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.