Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작 [링크 복사], [링크+제목 복사]
조회: 11503
글쓴 사람
정성태 (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)
13254정성태2/10/20234384Windows: 223. (WMI 쿼리를 위한) PowerShell 문자열 escape 처리
13253정성태2/9/20235160Windows: 222. C# - 다른 윈도우 프로그램이 실행되었음을 인식하는 방법파일 다운로드1
13252정성태2/9/20233997오류 유형: 844. ssh로 명령어 수행 시 멈춤 현상
13251정성태2/8/20234441스크립트: 44. 파이썬의 3가지 스레드 ID
13250정성태2/8/20236257오류 유형: 843. System.InvalidOperationException - Unable to configure HTTPS endpoint
13249정성태2/7/20235089오류 유형: 842. 리눅스 - You must wait longer to change your password
13248정성태2/7/20234160오류 유형: 841. 리눅스 - [사용자 계정] is not in the sudoers file. This incident will be reported.
13247정성태2/7/20235060VS.NET IDE: 180. Visual Studio - 닷넷 소스 코드 디버깅 중 "Decompile source code"가 동작하는 않는 문제
13246정성태2/6/20234181개발 환경 구성: 664. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 - 두 번째 이야기
13245정성태2/6/20234753.NET Framework: 2093. C# - PEM 파일을 이용한 RSA 개인키/공개키 설정 방법파일 다운로드1
13244정성태2/5/20234102VS.NET IDE: 179. Visual Studio - External Tools에 Shell 내장 명령어 등록
13243정성태2/5/20234961디버깅 기술: 190. windbg - Win32 API 호출 시점에 BP 거는 방법 [1]
13242정성태2/4/20234409디버깅 기술: 189. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.UnauthorizedAccessException
13241정성태2/3/20233911디버깅 기술: 188. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.IO.FileNotFoundException
13240정성태2/1/20234068디버깅 기술: 187. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.Web.HttpException
13239정성태2/1/20233728디버깅 기술: 186. C# - CacheDependency의 숨겨진 예외 - System.Web.HttpException
13238정성태1/31/20235804.NET Framework: 2092. IIS 웹 사이트를 TLS 1.2 또는 TLS 1.3 프로토콜로만 운영하는 방법
13237정성태1/30/20235465.NET Framework: 2091. C# - 웹 사이트가 어떤 버전의 TLS/SSL을 지원하는지 확인하는 방법
13236정성태1/29/20235075개발 환경 구성: 663. openssl을 이용해 인트라넷 IIS 사이트의 SSL 인증서 생성
13235정성태1/29/20234635개발 환경 구성: 662. openssl - 윈도우 환경의 명령행에서 SAN 적용하는 방법
13234정성태1/28/20235719개발 환경 구성: 661. dnSpy를 이용해 소스 코드가 없는 .NET 어셈블리의 코드를 변경하는 방법 [1]
13233정성태1/28/20237036오류 유형: 840. C# - WebClient로 https 호출 시 "The request was aborted: Could not create SSL/TLS secure channel" 예외 발생
13232정성태1/27/20234801스크립트: 43. uwsgi의 --processes와 --threads 옵션
13231정성태1/27/20233723오류 유형: 839. python - TypeError: '...' object is not callable
13230정성태1/26/20234097개발 환경 구성: 660. WSL 2 내부로부터 호스트 측의 네트워크로 UDP 데이터가 1개의 패킷으로만 제한되는 문제
13229정성태1/25/20235089.NET Framework: 2090. C# - UDP Datagram의 최대 크기
1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...