Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작 [링크 복사], [링크+제목 복사]
조회: 11366
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 3개 있습니다.)

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을 이용한 여러분들의 사용자 정의 디버거에서도 잘 동작한다는 것입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/3/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-03-30 07시02분
Writing a Debugger From Scratch - DbgRs Part 1 - Attaching to a Process
; https://www.timdbg.com/posts/writing-a-debugger-from-scratch-part-1/

Writing a Debugger From Scratch - DbgRs Part 2 - Register State and Stepping
; https://www.timdbg.com/posts/writing-a-debugger-from-scratch-part-2/

Writing a Debugger From Scratch - DbgRs Part 3 - Reading Memory
; https://www.timdbg.com/posts/writing-a-debugger-from-scratch-part-3/
정성태

1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13551정성태2/12/20242017닷넷: 2213. ASP.NET/Core 웹 응용 프로그램 - 2차 스레드의 예외로 인한 비정상 종료
13550정성태2/11/20242107Windows: 256. C# - Server socket이 닫히면 Accept 시켰던 자식 소켓이 닫힐까요?
13549정성태2/3/20242479개발 환경 구성: 706. C# - 컨테이너에서 실행하기 위한 (소켓) 콘솔 프로젝트 구성
13548정성태2/1/20242309개발 환경 구성: 705. "Docker Desktop for Windows" - ASP.NET Core 응용 프로그램의 소켓 주소 바인딩(IPv4/IPv6 loopback, Any)
13547정성태1/31/20242056개발 환경 구성: 704. Visual Studio - .NET 8 프로젝트부터 dockerfile에 추가된 "USER app" 설정
13546정성태1/30/20241896Windows: 255. (디버거의 영향 등으로) 대상 프로세스가 멈추면 Socket KeepAlive로 연결이 끊길까요?
13545정성태1/30/20241828닷넷: 2212. ASP.NET Core - 우선순위에 따른 HTTP/HTTPS 호스트:포트 바인딩 방법
13544정성태1/30/20241846오류 유형: 894. Microsoft.Data.SqlClient - Could not load file or assembly 'System.Security.Permissions, ...'
13543정성태1/30/20241826Windows: 254. Windows - 기본 사용 중인 5357 포트 비활성화는 방법
13542정성태1/30/20241876오류 유형: 893. Visual Studio - Web Application을 실행하지 못하는 IISExpress - 두 번째 이야기
13541정성태1/29/20241922VS.NET IDE: 188. launchSettings.json의 useSSL 옵션
13540정성태1/29/20242051Linux: 69. 리눅스 - "Docker Desktop for Windows" Container 환경에서 IPv6 Loopback Address 바인딩 오류
13539정성태1/26/20242146개발 환경 구성: 703. Visual Studio - launchSettings.json을 이용한 HTTP/HTTPS 포트 바인딩
13538정성태1/25/20242214닷넷: 2211. C# - NonGC(FOH) 영역에 .NET 개체를 생성파일 다운로드1
13537정성태1/24/20242268닷넷: 2210. C# - Native 메모리에 .NET 개체를 생성파일 다운로드1
13536정성태1/23/20242374닷넷: 2209. .NET 8 - NonGC Heap / FOH (Frozen Object Heap) [1]
13535정성태1/22/20242206닷넷: 2208. C# - GCHandle 구조체의 메모리 분석
13534정성태1/21/20242039닷넷: 2207. C# - SQL Server DB를 bacpac으로 Export/Import파일 다운로드1
13533정성태1/18/20242239닷넷: 2206. C# - TCP KeepAlive의 서버 측 구현파일 다운로드1
13532정성태1/17/20242149닷넷: 2205. C# - SuperSimpleTcp 사용 시 주의할 점파일 다운로드1
13531정성태1/16/20242026닷넷: 2204. C# - TCP KeepAlive에 새로 추가된 Retry 옵션파일 다운로드1
13530정성태1/15/20242010닷넷: 2203. C# - Python과의 AES 암호화 연동파일 다운로드1
13529정성태1/15/20241895닷넷: 2202. C# - PublishAot의 glibc에 대한 정적 링킹하는 방법
13528정성태1/14/20242032Linux: 68. busybox 컨테이너에서 실행 가능한 C++, Go 프로그램 빌드
13527정성태1/14/20241959오류 유형: 892. Visual Studio - Failed to launch debug adapter. Additional information may be available in the output window.
13526정성태1/14/20242048닷넷: 2201. C# - Facebook 연동 / 사용자 탈퇴 처리 방법
1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...