Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작 [링크 복사], [링크+제목 복사]
조회: 11363
글쓴 사람
정성태 (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)
13423정성태10/6/20233063스크립트: 58. 파이썬 - async/await 기본 사용법
13422정성태10/5/20233204닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
13421정성태10/4/20233243닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
13420정성태9/26/20235327스크립트: 57. 파이썬 - UnboundLocalError: cannot access local variable '...' where it is not associated with a value
13419정성태9/25/20233092스크립트: 56. 파이썬 - RuntimeError: dictionary changed size during iteration
13418정성태9/25/20233759닷넷: 2146. C# - ConcurrentDictionary 자료 구조의 동기화 방식
13417정성태9/19/20233336닷넷: 2145. C# - 제네릭의 형식 매개변수에 속한 (매개변수를 가진) 생성자를 호출하는 방법
13416정성태9/19/20233152오류 유형: 877. redis-py - MISCONF Redis is configured to save RDB snapshots, ...
13415정성태9/18/20233639닷넷: 2144. C# 12 - 컬렉션 식(Collection Expressions)
13414정성태9/16/20233390디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법
13413정성태9/14/20233582닷넷: 2143. C# - 시스템 Time Zone 변경 시 이벤트 알림을 받는 방법
13412정성태9/14/20236853닷넷: 2142. C# 12 - 인라인 배열(Inline Arrays) [1]
13411정성태9/12/20233364Windows: 252. 권한 상승 전/후 따로 관리되는 공유 네트워크 드라이브 정보
13410정성태9/11/20234863닷넷: 2141. C# 12 - Interceptor (컴파일 시에 메서드 호출 재작성) [1]
13409정성태9/8/20233721닷넷: 2140. C# - Win32 API를 이용한 모니터 전원 끄기
13408정성태9/5/20233714Windows: 251. 임의로 만든 EXE 파일을 포함한 ZIP 파일의 압축을 해제할 때 Windows Defender에 의해 삭제되는 경우
13407정성태9/4/20233473닷넷: 2139. C# - ParallelEnumerable을 이용한 IEnumerable에 대한 병렬 처리
13406정성태9/4/20233426VS.NET IDE: 186. Visual Studio Community 버전의 라이선스
13405정성태9/3/20233841닷넷: 2138. C# - async 메서드 호출 원칙
13404정성태8/29/20233364오류 유형: 876. Windows - 키보드의 등호(=, Equals sign) 키가 눌리지 않는 경우
13403정성태8/21/20233189오류 유형: 875. The following signatures couldn't be verified because the public key is not available: NO_PUBKEY EB3E94ADBE1229CF
13402정성태8/20/20233250닷넷: 2137. ILSpy의 nuget 라이브러리 버전 - ICSharpCode.Decompiler
13401정성태8/19/20233525닷넷: 2136. .NET 5+ 환경에서 P/Invoke의 성능을 높이기 위한 SuppressGCTransition 특성 [1]
13400정성태8/10/20233351오류 유형: 874. 파이썬 - pymssql을 윈도우 환경에서 설치 불가
13399정성태8/9/20233373닷넷: 2135. C# - 지역 변수로 이해하는 메서드 매개변수의 값/참조 전달
13398정성태8/3/20234135스크립트: 55. 파이썬 - pyodbc를 이용한 SQL Server 연결 사용법
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...