Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작 [링크 복사], [링크+제목 복사],
조회: 11687
글쓴 사람
정성태 (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/
정성태

... 46  47  48  49  50  51  52  53  54  55  56  57  58  [59]  60  ...
NoWriterDateCnt.TitleFile(s)
12160정성태2/26/202010470개발 환경 구성: 469. Reg-free COM 개체 사용을 위한 manifest 파일 생성 도구 - COMRegFreeManifest
12159정성태2/26/20208603오류 유형: 596. Visual Studio - The project needs to include ATL support
12158정성태2/25/202010435디버깅 기술: 165. C# - Marshal.GetIUnknownForObject/GetIDispatchForObject 사용 시 메모리 누수(Memory Leak) 발생파일 다운로드1
12157정성태2/25/202010370디버깅 기술: 164. C# - Marshal.GetNativeVariantForObject 사용 시 메모리 누수(Memory Leak) 발생 및 해결 방법파일 다운로드1
12156정성태2/25/20209702오류 유형: 595. LINK : warning LNK4098: defaultlib 'nafxcw.lib' conflicts with use of other libs; use /NODEFAULTLIB:library
12155정성태2/25/20208989오류 유형: 594. Warning NU1701 - This package may not be fully compatible with your project
12154정성태2/25/20208796오류 유형: 593. warning LNK4070: /OUT:... directive in .EXP differs from output filename
12153정성태2/23/202011530.NET Framework: 898. Trampoline을 이용한 후킹의 한계파일 다운로드1
12152정성태2/23/202011275.NET Framework: 897. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 세 번째 이야기(Trampoline 후킹)파일 다운로드1
12151정성태2/22/202011759.NET Framework: 896. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 - 두 번째 이야기 (원본 함수 호출)파일 다운로드1
12150정성태2/21/202011633.NET Framework: 895. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 [1]파일 다운로드1
12149정성태2/20/202011389.NET Framework: 894. eBEST C# XingAPI 래퍼 - 연속 조회 처리 방법 [1]
12148정성태2/19/202012636디버깅 기술: 163. x64 환경에서 구현하는 다양한 Trampoline 기법 [1]
12147정성태2/19/202011274디버깅 기술: 162. x86/x64의 기계어 코드 최대 길이
12146정성태2/18/202011486.NET Framework: 893. eBEST C# XingAPI 래퍼 - 로그인 처리파일 다운로드1
12145정성태2/18/202010719.NET Framework: 892. eBEST C# XingAPI 래퍼 - Sqlite 지원 추가파일 다운로드1
12144정성태2/13/202010740.NET Framework: 891. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 두 번째 이야기파일 다운로드1
12143정성태2/13/20208779.NET Framework: 890. 상황별 GetFunctionPointer 반환값 정리 - x64파일 다운로드1
12142정성태2/12/202010535.NET Framework: 889. C# 코드로 접근하는 MethodDesc, MethodTable파일 다운로드1
12141정성태2/10/202010107.NET Framework: 888. C# - ASP.NET Core 웹 응용 프로그램의 출력 가로채기 [2]파일 다운로드1
12140정성태2/10/20209986.NET Framework: 887. C# - ASP.NET 웹 응용 프로그램의 출력 가로채기파일 다운로드1
12139정성태2/9/202011345.NET Framework: 886. C# - Console 응용 프로그램에서 UI 스레드 구현 방법
12138정성태2/9/202014107.NET Framework: 885. C# - 닷넷 응용 프로그램에서 SQLite 사용 [6]파일 다운로드1
12137정성태2/9/20209297오류 유형: 592. [AhnLab] 경고 - 디버거 실행을 탐지했습니다.
12136정성태2/6/20209699Windows: 168. Windows + S(또는 Q)로 뜨는 작업 표시줄의 검색 바가 동작하지 않는 경우
12135정성태2/6/202013345개발 환경 구성: 468. Nuget 패키지의 로컬 보관 폴더를 옮기는 방법 [2]
... 46  47  48  49  50  51  52  53  54  55  56  57  58  [59]  60  ...