Writing Windows Debugger in C#

By | December 5, 2017

Actually C# is not the best language to call Windows Debugging functions. Several pinvokes and debugging structures definitions are required. They may be taken for winbase.h include file. In attached projects these pinvoke and structures definitions are located in PInvokes.cs file.
Debugger is a program to debug another process. The debugger may start new process by itself or attached to already running process.
However in the attaching case some priority restrictions. I will not discuss this restrictions now in this article may be sometime later.
One debugger application may debug one or several processes. The separate debugging thread should be created for every debugged process.

The debugging thread start debugged applications (CreateProcess pinvoke is used with one of the following flags DEBUG_ONLY_THIS_PROCESS or DEBUG_PROCESS in arguiments) or attached to already running process (DebugActiveProcess pinvoke) and after executing debugging loop which is based on 2 debugging functions: WaitForDebugEvent and ContinueDebugEvent:
Debugger diagram

Presented project is 64-bit C# console application.
The full debugger project source files may be downloaded from here.
It should be started with arguments: be started with arguments: the first arguments is debugged process ID for debugging already running process or full path to executable file to start new process to debug. The second argument is optional and used to open new separate console if you want to start new console process, without this arguments both processes (debugger and debugged process) with share the same console. The value of the second argument should be always “nc” (new console).
In the first step the debugger thread is started:

            t.Start(args[0]);
            Console.ReadKey();
            bContinueDebugging = false;   // used in debugging loop

Within debugger thread the new process is started using pinvoke call CreateProcess or debugger attached to already running process calling DebugActiveProcess pinvoke.
When the new process is started the CreateProcess call should use one of the following flags DEBUG_ONLY_THIS_PROCESS or DEBUG_PROCESS in arguiments.
If debugging thread successful attached to the already stated process or successfully started the new one it begins looping waiting for debug event calling WaitForDebugEvent pinvoke. When debug events captured the debugging thread analyzes it and does appropriate actions:

            while (bContinueDebugging)
            {
                IntPtr debugEventPtr = Marshal.AllocHGlobal(188);
                bool bb = PInvokes.WaitForDebugEvent(debugEventPtr, 1000);
                UInt32 dwContinueDebugEvent = PInvokes.DBG_CONTINUE;
                if (bb)
                {
                    PInvokes.DEBUG_EVENT DebugEvent = (PInvokes.DEBUG_EVENT)Marshal.PtrToStructure(debugEventPtr, typeof(PInvokes.DEBUG_EVENT));
                    IntPtr debugInfoPtr = GetIntPtrFromByteArray(DebugEvent.u);
                    switch(DebugEvent.dwDebugEventCode)
                    {
                        case PInvokes.CREATE_PROCESS_DEBUG_EVENT:
                            Console.WriteLine("CREATE_PROCESS_DEBUG_EVENT");
                            PInvokes.CREATE_PROCESS_DEBUG_INFO CreateProcessDebugInfo = (PInvokes.CREATE_PROCESS_DEBUG_INFO)Marshal.PtrToStructure(debugInfoPtr, typeof(PInvokes.CREATE_PROCESS_DEBUG_INFO));
                            hProcess = CreateProcessDebugInfo.hProcess;
                            break;
                        case PInvokes.CREATE_THREAD_DEBUG_EVENT:
                            Console.WriteLine("CREATE_THREAD_DEBUG_EVENT");
                /*              PInvokes.CREATE_THREAD_DEBUG_INFO CreateThreadDebugInfo;
                            CreateThreadDebugInfo.hThread = (IntPtr)BitConverter.ToUInt64(DebugEvent.u, 0);
                            CreateThreadDebugInfo.lpThreadLocalBase = (IntPtr)BitConverter.ToUInt64(DebugEvent.u, 8);
                            CreateThreadDebugInfo.lpStartAddress = (PInvokes.PTHREAD_START_ROUTINE)Marshal.GetDelegateForFunctionPointer((IntPtr)BitConverter.ToUInt64(DebugEvent.u, 8), typeof(PInvokes.PTHREAD_START_ROUTINE));
//                               CreateThreadDebugInfo.lpStartAddress = (PInvokes.PTHREAD_START_ROUTINE)BitConverter.ToUInt64(DebugEvent.u, 16);*/
                            break;
                        case PInvokes.EXCEPTION_DEBUG_EVENT:
                            PInvokes.EXCEPTION_DEBUG_INFO ExceptionDebugInfo = (PInvokes.EXCEPTION_DEBUG_INFO)Marshal.PtrToStructure(debugInfoPtr, typeof(PInvokes.EXCEPTION_DEBUG_INFO));
                            string exceptionDebugStr = String.Format("EXCEPTION_DEBUG_EVENT: Exception Address: 0x{0:x}, Exception code: 0x{1:x}",
                                (ulong)ExceptionDebugInfo.ExceptionRecord.ExceptionAddress, 					ExceptionDebugInfo.ExceptionRecord.ExceptionCode);
                            Console.WriteLine(exceptionDebugStr);
                            switch (ExceptionDebugInfo.ExceptionRecord.ExceptionCode)
                            {
                                case PInvokes.EXCEPTION_ACCESS_VIOLATION:
                                    Console.WriteLine("EXCEPTION_ACCESS_VIOLATION");
                                    PInvokes.ContinueDebugEvent(DebugEvent.dwProcessId,
                                           DebugEvent.dwThreadId,
                                           PInvokes.DBG_EXCEPTION_NOT_HANDLED);
                                    break;

                                case PInvokes.EXCEPTION_BREAKPOINT:
                                    Console.WriteLine("EXCEPTION_BREAKPOINT");
                                    break;

                                case PInvokes.EXCEPTION_DATATYPE_MISALIGNMENT:
                                    Console.WriteLine("EXCEPTION_DATATYPE_MISALIGNMENT");
                                    break;

                                case PInvokes.EXCEPTION_SINGLE_STEP:
                                    Console.WriteLine("EXCEPTION_SINGLE_STEP");
                                    break;

                                case PInvokes.DBG_CONTROL_C:
                                    Console.WriteLine("DBG_CONTROL_C");
                                    break;
                                case PInvokes.EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
                                    Console.WriteLine("EXCEPTION_ARRAY_BOUNDS_EXCEEDED");
                                    break;
                                case PInvokes.EXCEPTION_INT_DIVIDE_BY_ZERO:
                                    PInvokes.ContinueDebugEvent(DebugEvent.dwProcessId,
                                           DebugEvent.dwThreadId,
                                           PInvokes.DBG_EXCEPTION_NOT_HANDLED);
                                    Console.WriteLine("EXCEPTION_INT_DIVIDE_BY_ZERO");
                                    break;
                                // ............ 
                                // some other exception case see winbase.h include file for definition
                                // ............
                                default:
                                    Console.WriteLine("Handle other exceptions.");
                                    break;
                            }
                            break;
                        case PInvokes.EXIT_PROCESS_DEBUG_EVENT:
                            Console.WriteLine("EXIT_PROCESS_DEBUG_EVENT");
                            PInvokes.EXIT_PROCESS_DEBUG_INFO ExitProcessDebugInfo = (PInvokes.EXIT_PROCESS_DEBUG_INFO)Marshal.PtrToStructure(debugInfoPtr, typeof(PInvokes.EXIT_PROCESS_DEBUG_INFO));
                            bContinueDebugging = false;
                            break;
                        case PInvokes.EXIT_THREAD_DEBUG_EVENT:
                            Console.WriteLine("EXIT_THREAD_DEBUG_EVENT");
                            PInvokes.EXIT_THREAD_DEBUG_INFO ExitThreadDebugInfo = (PInvokes.EXIT_THREAD_DEBUG_INFO)Marshal.PtrToStructure				(debugInfoPtr, typeof(PInvokes.EXIT_THREAD_DEBUG_INFO));
                            break;
                        case PInvokes.LOAD_DLL_DEBUG_EVENT:
                            PInvokes.LOAD_DLL_DEBUG_INFO LoadDLLDebugInfo = (PInvokes.LOAD_DLL_DEBUG_INFO)Marshal.PtrToStructure				(debugInfoPtr, typeof(PInvokes.LOAD_DLL_DEBUG_INFO));
                            lpBaseOfDllLoad = LoadDLLDebugInfo.lpBaseOfDll;
                            break;
                        case PInvokes.OUTPUT_DEBUG_STRING_EVENT:
                            Console.WriteLine("OUTPUT_DEBUG_STRING_EVENT");
                            PInvokes.OUTPUT_DEBUG_STRING_INFO OutputDebugStringInfo = 				(PInvokes.OUTPUT_DEBUG_STRING_INFO)Marshal.PtrToStructure(debugInfoPtr, typeof(PInvokes.OUTPUT_DEBUG_STRING_INFO));
                            break;
                        case PInvokes.RIP_EVENT:
                            Console.WriteLine("RIP_EVENT");
                            PInvokes.RIP_INFO RipInfo = (PInvokes.RIP_INFO)Marshal.PtrToStructure(debugInfoPtr, typeof(PInvokes.RIP_INFO));
                            break;
                        case PInvokes.UNLOAD_DLL_DEBUG_EVENT:
                            PInvokes.UNLOAD_DLL_DEBUG_INFO UnloadDebugInfo = (PInvokes.UNLOAD_DLL_DEBUG_INFO)Marshal.PtrToStructure				(debugInfoPtr, typeof(PInvokes.UNLOAD_DLL_DEBUG_INFO));
                            Console.WriteLine("UNLOAD_DLL_DEBUG_EVENT: Dll name: " + FindUnloadModule((int)nPid, UnloadDebugInfo));
                            break;
                    }
                    // Resume executing the thread that reported the debugging event. 
                    bool bb1 = PInvokes.ContinueDebugEvent((uint)DebugEvent.dwProcessId,
                                (uint)DebugEvent.dwThreadId,
                                dwContinueDebugEvent);
                    if (lpBaseOfDllLoad != IntPtr.Zero)
                    {
                        Console.WriteLine("LOAD_DLL_DEBUG_EVENT: Dll name: " + FindModule((int)nPid, lpBaseOfDllLoad));
                        lpBaseOfDllLoad = IntPtr.Zero;
                    }
                }
                if (debugEventPtr != null)
                    Marshal.FreeHGlobal(debugEventPtr);
            }
            if (nPid > 0)
            {
                if (!PInvokes.DebugActiveProcessStop(nPid))
                {
                    Console.WriteLine("DebugActiveProcessStop failed");
                }
            }

The debugging thread breaks the loop the EXIT_PROCESS_DEBUG_EVENT event takes place. The event has been processed the debugging loop executes ContinueDebugEvent function to start waiting for new event (the third argument is DBG_CONTINUE) or cancel waiting (the third argument should be equal to DBG_EXCEPTION_NOT_HANDLED).
Below is output example of debugging crashing console application (crashing reason is division by zero):


D:\Debugger>DebuggerSharp64.exe d:\Debugger\ConsoleCrashApplication.exe nc
CREATE_PROCESS_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\ntdll.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\MSCOREE.DLL
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\KERNEL32.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\KERNELBASE.dll
CREATE_PROCESS_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\ntdll.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\KERNEL32.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\KERNELBASE.dll
EXCEPTION_DEBUG_EVENT: Exception Address: 0x7fffe4501b90, Exception code: 0x80000003
EXCEPTION_BREAKPOINT
CREATE_THREAD_DEBUG_EVENT
EXIT_THREAD_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT: Dll name:
CREATE_THREAD_DEBUG_EVENT
EXCEPTION_DEBUG_EVENT: Exception Address: 0x7fffe4501b90, Exception code: 0x80000003
EXCEPTION_BREAKPOINT
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\ADVAPI32.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\msvcrt.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\sechost.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\RPCRT4.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\SspiCli.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscoreei.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\SHLWAPI.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\combase.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\USER32.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\GDI32.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\IMM32.DLL
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\MSCTF.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\kernel.appcore.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\VERSION.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\MSVCR120_CLR0400.dll
EXCEPTION_DEBUG_EVENT: Exception Address: 0x7fffe16a8a5c, Exception code: 0x4242420
Handle other exceptions.
CREATE_THREAD_DEBUG_EVENT
CREATE_THREAD_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\assembly\NativeImages_v4.0.30319_64\mscorlib\fa8eef6f6cb67c660d71e15c5cad71b5\mscorlib.ni.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\ole32.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\CRYPTBASE.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\SYSTEM32\bcryptPrimitives.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clrjit.dll
LOAD_DLL_DEBUG_EVENT: Dll name: C:\windows\system32\OLEAUT32.dll
CREATE_THREAD_DEBUG_EVENT
EXCEPTION_DEBUG_EVENT: Exception Address: 0x7fff7a9805e1, Exception code: 0xc0000094
EXCEPTION_INT_DIVIDE_BY_ZERO
EXCEPTION_DEBUG_EVENT: Exception Address: 0x7fffe16a8a5c, Exception code: 0xc0000094
EXCEPTION_INT_DIVIDE_BY_ZERO
LOAD_DLL_DEBUG_EVENT: Dll name: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\diasymreader.dll
EXCEPTION_DEBUG_EVENT: Exception Address: 0x7fff7a9805e1, Exception code: 0xc0000094
EXCEPTION_INT_DIVIDE_BY_ZERO
EXIT_THREAD_DEBUG_EVENT
EXIT_THREAD_DEBUG_EVENT
EXIT_THREAD_DEBUG_EVENT
EXIT_THREAD_DEBUG_EVENT
EXIT_PROCESS_DEBUG_EVENT
D:\Debugger>

The second argument “nc” is used to open separate console for debugged application ConsoleCrashApplication.exe.
The source code of debugged ConsoleCrashAppliacation is below:

using System;
using System.Threading;
namespace ConsoleCrashAppliacation
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("The appliacation will crash in 10 seconds");
            Console.WriteLine("Wait for crash or press any key to exit");
            Thread thread = new Thread(CrashThread);
            thread.Start();
            Console.ReadKey();
        }
        static void CrashThread()
        {
            Console.WriteLine("Entering in crash thread. 10 second sleep");
            Thread.Sleep(10000);
            Console.WriteLine("Crashing now");
            int a = 1;
            int b = 0;
            int c = a / b;
        }
    }
}

One thought on “Writing Windows Debugger in C#

  1. amount

    Hi there! Do you use Twitter? I’d like to follow you if that would be okay.
    I’m definitely enjoying your blog and look forward to new posts.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *