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() {
m_AckEvent = CreateEvent(ref sa, false,
false, "DBWIN_BUFFER_READY");
if (m_AckEvent == IntPtr.Zero) {
throw CreateApplicationException("Failed to create" +
" event 'DBWIN_BUFFER_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'");
}
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
.
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