C# - DbgEng.dll을 이용한 간단한 디버거 제작
이에 관해서 아주 좋은 내용의 글이 있습니다.
Writing a Simple Debugger with DbgEng.Dll
; http://blogs.microsoft.co.il/pavely/2015/07/27/writing-a-simple-debugger-with-dbgeng-dll/
사실 DbgEng.dll의 기능이 워낙 훌륭해서 조금만 코드를 추가하면 cdb 정도의,
CDB Command-Line Options
; https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/cdb-command-line-options
디버거는 쉽게 만들 수 있습니다. 어느 정도로 쉬운지, ^^ C#으로 간단하게 구현해 보겠습니다.
우선 필요한 Win32 구조체/인터페이스들은 이미 다음의 프로젝트에 구현되어 있으니,
DbgShell/ClrMemDiag/Debugger/
; https://github.com/microsoft/DbgShell/tree/master/ClrMemDiag/Debugger
그대로 다운로드해 프로젝트에 추가한 다음, "Writing a Simple Debugger with DbgEng.Dll" 글에서 정리한 내용에 따라 Debugger.cs 소스 코드를 다음과 같이 간단하게 구성할 수 있습니다.
using Microsoft.Diagnostics.Runtime.Interop;
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace SimpleDebugger
{
// DbgShell/ClrMemDiag/Debugger/
// https://github.com/microsoft/DbgShell/tree/master/ClrMemDiag/Debugger
// Writing a Simple Debugger with DbgEng.Dll
// http://blogs.microsoft.co.il/pavely/2015/07/27/writing-a-simple-debugger-with-dbgeng-dll/
public class UserDebugger : IDebugOutputCallbacks, IDebugEventCallbacksWide, IDisposable
{
[DllImport("dbgeng.dll")]
internal static extern int DebugCreate(ref Guid InterfaceId, [MarshalAs(UnmanagedType.IUnknown)] out object Interface);
IDebugClient5 _client;
IDebugControl4 _control;
public bool BreakpointHit { get; set; }
public bool StateChanged { get; set; }
public delegate void ModuleLoadedDelegate(ModuleInfo modInfo);
public ModuleLoadedDelegate ModuleLoaded;
public delegate void ExceptionOccurredDelegate(ExceptionInfo exInfo);
public ExceptionOccurredDelegate ExceptionOccurred;
public UserDebugger()
{
Guid guid = new Guid("27fe5639-8407-4f47-8364-ee118fb08ac8");
object obj = null;
int hr = DebugCreate(ref guid, out obj);
if (hr < 0)
{
Console.WriteLine("SourceFix: Unable to acquire client interface");
return;
}
_client = obj as IDebugClient5;
_control = _client as IDebugControl4;
_client.SetOutputCallbacks(this);
_client.SetEventCallbacksWide(this);
}
public bool AttachTo(int pid)
{
int hr = _client.AttachProcess(0, (uint)pid, DEBUG_ATTACH.DEFAULT);
return hr >= 0;
}
public int GetExecutionStatus(out DEBUG_STATUS status)
{
return _control.GetExecutionStatus(out status);
}
public void OutputCurrentState(DEBUG_OUTCTL outputControl, DEBUG_CURRENT flags)
{
_control.OutputCurrentState(outputControl, flags);
}
public void OutputPromptWide(DEBUG_OUTCTL outputControl, string format)
{
_control.OutputPromptWide(outputControl, format);
}
public int Execute(DEBUG_OUTCTL outputControl, string command, DEBUG_EXECUTE flags)
{
return _control.Execute(DEBUG_OUTCTL.THIS_CLIENT, command, DEBUG_EXECUTE.NOT_LOGGED);
}
public int ExecuteWide(DEBUG_OUTCTL outputControl, string command, DEBUG_EXECUTE flags)
{
return _control.ExecuteWide(outputControl, command, flags);
}
public int WaitForEvent(DEBUG_WAIT flag = DEBUG_WAIT.DEFAULT, int timeout = Timeout.Infinite)
{
unchecked
{
return _control.WaitForEvent(flag, (uint)timeout);
}
}
public void SetInterrupt(DEBUG_INTERRUPT flag = DEBUG_INTERRUPT.ACTIVE)
{
_control.SetInterrupt(flag);
}
public void Detach()
{
_client.DetachProcesses();
}
public void Dispose()
{
if (_control != null)
{
Marshal.ReleaseComObject(_control);
_control = null;
}
if (_client != null)
{
Marshal.ReleaseComObject(_client);
_client = null;
}
}
public int Output([In] DEBUG_OUTPUT Mask, [In, MarshalAs(UnmanagedType.LPStr)] string Text)
{
switch (Mask)
{
case DEBUG_OUTPUT.DEBUGGEE:
Console.ForegroundColor = ConsoleColor.Gray;
break;
case DEBUG_OUTPUT.PROMPT:
Console.ForegroundColor = ConsoleColor.Magenta;
break;
case DEBUG_OUTPUT.ERROR:
Console.ForegroundColor = ConsoleColor.Red;
break;
case DEBUG_OUTPUT.EXTENSION_WARNING:
case DEBUG_OUTPUT.WARNING:
Console.ForegroundColor = ConsoleColor.Yellow;
break;
case DEBUG_OUTPUT.SYMBOLS:
Console.ForegroundColor = ConsoleColor.Cyan;
break;
default:
Console.ForegroundColor = ConsoleColor.White;
break;
}
Console.Write(Text);
return 0;
}
public int GetInterestMask([Out] out DEBUG_EVENT Mask)
{
Mask = DEBUG_EVENT.BREAKPOINT | DEBUG_EVENT.CHANGE_DEBUGGEE_STATE
| DEBUG_EVENT.CHANGE_ENGINE_STATE | DEBUG_EVENT.CHANGE_SYMBOL_STATE
| DEBUG_EVENT.CREATE_PROCESS | DEBUG_EVENT.CREATE_THREAD | DEBUG_EVENT.EXCEPTION | DEBUG_EVENT.EXIT_PROCESS
| DEBUG_EVENT.EXIT_THREAD | DEBUG_EVENT.LOAD_MODULE | DEBUG_EVENT.SESSION_STATUS | DEBUG_EVENT.SYSTEM_ERROR
| DEBUG_EVENT.UNLOAD_MODULE;
return 0;
}
public int Breakpoint([In, MarshalAs(UnmanagedType.Interface)] IDebugBreakpoint2 Bp)
{
BreakpointHit = true;
StateChanged = true;
return (int)DEBUG_STATUS.BREAK;
}
public int Breakpoint([In, MarshalAs(UnmanagedType.Interface)] IDebugBreakpoint Bp)
{
BreakpointHit = true;
StateChanged = true;
return (int)DEBUG_STATUS.BREAK;
}
public int Exception([In] ref EXCEPTION_RECORD64 Exception, [In] uint FirstChance)
{
if (ExceptionOccurred != null)
{
ExceptionInfo exInfo = new ExceptionInfo(Exception, FirstChance);
ExceptionOccurred(exInfo);
}
return (int)DEBUG_STATUS.BREAK;
}
public int CreateThread([In] ulong Handle, [In] ulong DataOffset, [In] ulong StartOffset)
{
return 0;
}
public int ExitThread([In] uint ExitCode)
{
return 0;
}
public int CreateProcess([In] ulong ImageFileHandle, [In] ulong Handle, [In] ulong BaseOffset, [In] uint ModuleSize,
[In, MarshalAs(UnmanagedType.LPStr)] string ModuleName, [In, MarshalAs(UnmanagedType.LPStr)] string ImageName,
[In] uint CheckSum, [In] uint TimeDateStamp, [In] ulong InitialThreadHandle, [In] ulong ThreadDataOffset, [In] ulong StartOffset)
{
return (int)DEBUG_STATUS.NO_CHANGE;
}
public int ExitProcess([In] uint ExitCode)
{
return 0;
}
public int LoadModule([In] ulong ImageFileHandle, [In] ulong BaseOffset, [In] uint ModuleSize, [In, MarshalAs(UnmanagedType.LPStr)] string ModuleName,
[In, MarshalAs(UnmanagedType.LPStr)] string ImageName, [In] uint CheckSum, [In] uint TimeDateStamp)
{
if (ModuleLoaded != null)
{
ModuleInfo modInfo = new ModuleInfo(ImageFileHandle, BaseOffset, ModuleSize, ModuleName, ImageName, CheckSum, TimeDateStamp);
ModuleLoaded(modInfo);
}
return 0;
}
public int UnloadModule([In, MarshalAs(UnmanagedType.LPStr)] string ImageBaseName, [In] ulong BaseOffset)
{
return 0;
}
public int SystemError([In] uint Error, [In] uint Level)
{
return 0;
}
public int SessionStatus([In] DEBUG_SESSION Status)
{
return 0;
}
public int ChangeDebuggeeState([In] DEBUG_CDS Flags, [In] ulong Argument)
{
return 0;
}
public int ChangeEngineState([In] DEBUG_CES Flags, [In] ulong Argument)
{
return 0;
}
public int ChangeSymbolState([In] DEBUG_CSS Flags, [In] ulong Argument)
{
return 0;
}
}
}
IDebugControl interface
; https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/dbgeng/nn-dbgeng-idebugcontrol
위의 UserDebugger 타입을 이용하면 이제 간단한 명령행 디버거로 작동하는 콘솔 프로젝트를 구현할 수 있습니다.
using Microsoft.Diagnostics.Runtime.Interop;
using SimpleDebugger;
using System;
using System.Diagnostics;
namespace DbgShell
{
// DbgShell/ClrMemDiag/Debugger/
// https://github.com/microsoft/DbgShell/tree/master/ClrMemDiag/Debugger
// Writing a Simple Debugger with DbgEng.Dll
// http://blogs.microsoft.co.il/pavely/2015/07/27/writing-a-simple-debugger-with-dbgeng-dll/
class Program
{
static void Main(string[] args)
{
using (UserDebugger debugger = new UserDebugger())
{
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
debugger.SetInterrupt();
};
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = "DummyApp.exe";
psi.UseShellExecute = true;
Process child = Process.Start(psi);
try
{
if (debugger.AttachTo(child.Id) == false)
{
Console.WriteLine("Failed to attach");
return;
}
int hr = debugger.WaitForEvent();
while (true)
{
hr = debugger.GetExecutionStatus(out DEBUG_STATUS status);
if (hr != (int)HResult.S_OK)
{
break;
}
if (status == DEBUG_STATUS.NO_DEBUGGEE)
{
Console.WriteLine("No Target");
break;
}
if (status == DEBUG_STATUS.GO || status == DEBUG_STATUS.STEP_BRANCH ||
status == DEBUG_STATUS.STEP_INTO || status == DEBUG_STATUS.STEP_OVER)
{
hr = debugger.WaitForEvent();
continue;
}
if (debugger.StateChanged)
{
Console.WriteLine();
debugger.StateChanged = false;
if (debugger.BreakpointHit)
{
debugger.OutputCurrentState(DEBUG_OUTCTL.THIS_CLIENT, DEBUG_CURRENT.DEFAULT);
debugger.BreakpointHit = false;
}
}
debugger.OutputPromptWide(DEBUG_OUTCTL.THIS_CLIENT, null);
Console.Write(" ");
Console.ForegroundColor = ConsoleColor.Gray;
string command = Console.ReadLine();
debugger.ExecuteWide(DEBUG_OUTCTL.THIS_CLIENT, command, DEBUG_EXECUTE.DEFAULT);
}
}
finally
{
try
{
debugger.Detach();
} catch { }
try
{
child.Kill();
}
catch { }
}
}
}
}
}
참고로, 디버거 대상 프로세스 예제로 실행하는 DummayApp.exe의 소스 코드는 이렇습니다.
using System;
namespace DummyApp
{
class Program
{
static void Main(string[] args)
{
while (true)
{
Console.ReadLine();
try
{
throw new ApplicationException("TEST");
}
catch (Exception e)
{
}
}
}
}
}
따라서 DbgShell.exe를 실행하면 다음과 같은 출력 결과로 실행되고,
No .natvis files found at C:\WINDOWS\SYSTEM32\Visualizers.
No .natvis files found at C:\Users\%USERPROFILE%\AppData\Local\Dbg\Visualizers.
Microsoft (R) Windows Debugger Version 10.0.18362.1 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
*** wait with pending attach
************* Path validation summary **************
Response Time (ms) Location
Deferred SRV*c:\Symbols*http://msdl.microsoft.com/download/symbols
Symbol search path is: SRV*c:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00000000`00550000 00000000`00558000 c:\DotNetSamples\WinConsole\Debugger\DbgShell\bin\Debug\DummyApp.exe
ModLoad: 00007ffe`19c60000 00007ffe`19e50000 C:\WINDOWS\SYSTEM32\ntdll.dll
ModLoad: 00007ffe`022f0000 00007ffe`02354000 C:\WINDOWS\SYSTEM32\MSCOREE.DLL
ModLoad: 00007ffe`18e20000 00007ffe`18ed2000 C:\WINDOWS\System32\KERNEL32.dll
ModLoad: 00007ffe`16e70000 00007ffe`17113000 C:\WINDOWS\System32\KERNELBASE.dll
(54d8.860c): Break instruction exception - code 80000003 (first chance)
0:001>
보는 바와 같이 "0:001>"이라는 명령행 프롬프트까지
IDebugControl4::OutputPromptWide에 의해 제공되어 windbg에서의 경험 그대로 명령을 실행할 수 있습니다.
0:001> kv
Child-SP RetAddr : Args to Child : Call Site
00000000`00c5f858 00007ffe`19d2d4db : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!DbgBreakPoint
00000000`00c5f860 00007ffe`18e37bd4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!DbgUiRemoteBreakin+0x4b
00000000`00c5f890 00007ffe`19ccced1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
00000000`00c5f8c0 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
0:001> ~0s
ntdll!RtlNtStatusToDosError+0x86:
00007ffe`19cb3696 8b8cc300601200 mov ecx,dword ptr [rbx+rax*8+126000h] ds:00007ffe`19d864b8=c000a2a1
0:000> kv
Child-SP RetAddr : Args to Child : Call Site
00000000`008ff360 00007ffe`16e959ed : 00000000`c0000100 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlNtStatusToDosError+0x86
00000000`008ff390 00007ffe`16e95399 : 00000000`00000000 00000000`00000000 00000000`00992c02 00000000`00000000 : KERNELBASE!SleepConditionVariableSRW+0x7d
00000000`008ff3c0 00007ffe`022fb133 : 00000000`00000000 00007ffe`02339c08 00000200`002b0000 00000000`00000000 : KERNELBASE!GetEnvironmentVariableW+0x59
00000000`008ff400 00007ffe`022fb243 : 00000000`00000000 00000000`008ff820 00000000`00000000 00000000`00000000 : MSCOREE!CLRCreateInstance+0x7b3
00000000`008ff4c0 00007ffe`022f12bc : 00000000`008ff820 00000000`008ff620 00000000`00000000 00000000`00000000 : MSCOREE!CLRCreateInstance+0x8c3
00000000`008ff520 00007ffe`022f1574 : 00000000`008ff820 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!Ordinal141+0x12bc
00000000`008ff7c0 00007ffe`022fa516 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!Ordinal141+0x1574
00000000`008ff7f0 00007ffe`18e37bd4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain+0x16
00000000`008ff820 00007ffe`19ccced1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
00000000`008ff850 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
정말 쉽죠?!!! ^^
이 글의 완전한 예제 코드는 다음의 github에 실려 있습니다.
DotNetSamples/WinConsole/Debugger/SimpleDebugger
; https://github.com/stjeong/DotNetSamples/tree/master/WinConsole/Debugger/SimpleDebugger
물론, "!threads"와 같은 확장 명령어를 실행하려면 ext.dll, exts.dll, uext.dll, ntsdexts.dll 등의 모듈을 필요로 합니다. 그리고 (심벌 파일이 아닌) 심벌 서버와 연동을 하려면 "
pdb 파일을 다운로드하기 위한 symchk.exe 실행에 필요한 최소 파일" 글에서 정리한 DLL들이 필요합니다.
달리 말하면, 확장 DLL이라고 공개된 것들은 DbgEng.dll을 이용한 여러분들의 사용자 정의 디버거에서도 잘 동작한다는 것입니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]