160516 닷넷 어플리케이션 디버깅
간략요약
닷넷 어플리케이션 디버깅의 실용적이며 다양한 디버깅 방법에 대해서 서술한다.
- 전역 예외처리
- 가장 일반적인 예외처리
- 사후 디버깅으로 주로 사용됨
- 주로 예외처리 루틴에서 콜스텍정보만 텍스트로 로그서버에 업로드하여 분석함.
- 콜스텍 정보 정도면 80~90% 정도 오류상황의 원인을 분석할 수 있음.
- 하지만 정확한 변수값을 파악할 수 없는 한계가 있음.
- 실행중 VS디버거 접합
- 개발PC(혹은 원격디버깅환경이 셋팅된PC)에 VS디버거를 연결하여 정지점 기반으로 디버깅
- 정지점 기반 디버깅이라 가장 기능이 강력함.
- 하지만 사후디버깅이 가능하지 않고, 개발PC에 준하는 환경셋팅 노력이 요구됨.
- 덤프파일 생성후 windbg로 분석하기
- 실행중 혹은 크래시발생시 덤프파일 생성후 windbg로 디버깅.
- 콜스텍정보, 메모리내용을 모두 확인할 수 있음.
- windbg의 강력한 기능을 모두 사용할 수 있음.
- windbg 명령어를 잘 알고 있어야 함.
- windbg가 편리한 GUI를 제공하지 않음.
- 디버깅은 visualization이 중요한데 이게 안되는 것은 큰 단점…
- 덤프파일 생성후 VS디버거로 분석하기
- 덤프파일 분석의 모든 장점을 똑같이 갖음.
- VS디버거를 사용하기 때문에 어려운 명령어가 필요없고 편리한 GUI도 제공됨!
테스트를 위한 간단한 어플리케이션
- AppDomain.CurrentDomain.UnhandledException 를 이용해서 전역예외처리를 함.
- 지역변수, 전역변수를 할당하는 간단한 코드도 추가.
- Q, D, E 키로 종료, 덤프파일생성, 예외 발생을 구현함.
class Program
{
private static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += _AppDomain_UnhandledException;
var p = new MyPerson();
p.id = 101;
p.name = "william";
MySingleton.Current.id = 12345;
MySingleton.Current.name = "william";
MySingleton.Current.friends = new List<string> { "brandon", "kevin" };
Console.WriteLine($@"
commands
----------------
q : exit program
d : create dump
e : create exception
enter command :
");
while (true)
{
var key = Console.ReadKey();
if (key.Key == ConsoleKey.D)
{
Console.WriteLine("create dump...");
MiniDumpWriter.Write();
}
else if (key.Key == ConsoleKey.Q)
{
Console.WriteLine("exit program...");
break;
}
else if (key.Key == ConsoleKey.E)
{
Console.WriteLine("create exception...");
throw new Exception("my exception is occured!!!");
break;
}
}
}
private static void _AppDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Console.WriteLine($@"e : {e}");
Console.WriteLine($@"e.ExceptionObject : {e.ExceptionObject}");
}
}
public class MyPerson
{
public int id { get; set; }
public string name { get; set; }
}
public class MySingleton
{
public int id { get; set; }
public string name { get; set; }
public List<string> friends { get; set; }
#region 싱글톤
static MySingleton _Current;
static public MySingleton Current
{
get
{
if (_Current == null)
{
_Current = new MySingleton();
}
return _Current;
}
}
MySingleton()
{
}
#endregion
}
예외발생시 예외정보, 콜스텍 출력(기존 닷넷 어플리케이션 디버깅)
- 콜스텍 정보가 텍스트로 잘 출력됨!!!
- 이정도로 오류상황을 분석하기에 충분하다면 아래에 기술되는 글들을 계속 읽을 필요는 없음.
commands
----------------
q : exit program
d : create dump
e : create exception
enter command :
e
create exception...
e : System.UnhandledExceptionEventArgs
e.ExceptionObject : System.Exception: my exception is occured!!!
위치: MySimpleConApp.Program.Main(String[] args) 파일 C:\temp\MySimpleConApp\
Program.cs:줄 53
덤프파일 수동작성
- 덤프파일을 가장하는 가장 간단한 방법으로 작업관리자를 열고 해당 프로세스를 찾아서 우클릭해서 덤프파일을 생성할 수 있음.
- 작업관리자로 생성하는 덤프파일은 풀덤프파일이며 콜스텍정보, 변수값정보(=메모리정보)가 모두 포함된다.
- 작업관리자 대신 ProcessExplorer를 이용하면 풀덤프 미니덤프 파일을 각각 생성할 수 있으며, 이때 미니덤프로는 콜스텍정보까지만 분석할 수 있다.
- 물론 이때문에 풀덤프파일은 용량이 매우 크다.
PS C:\Users\Hyundong\AppData\Local\Temp> ls *.dmp
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2016-05-16 오후 3:45 97156600 MySimpleConApp.DMP
windbg 설치
- 다운로드 사이트 : https://msdn.microsoft.com/en-us/windows/hardware/hh852365.aspx
- WDK10을 설치할때 함께 설치됨.
- C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe
PS C:\Users\Hyundong\AppData\Local\Temp> windbg -z .\MySimpleConApp.DMP
Microsoft (R) Windows Debugger Version 10.0.10586.567 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\Users\Hyundong\AppData\Local\Temp\MySimpleConApp.DMP]
User Mini Dump File with Full Memory: Only application data is available
Symbol search path is: srv*
Executable search path is:
Windows 10 Version 10586 MP (4 procs) Free x64
Product: WinNt, suite: SingleUserTS
Built by: 10.0.10586.0 (th2_release.151029-1700)
Machine Name:
Debug session time: Mon May 16 15:45:27.000 2016 (UTC + 9:00)
System Uptime: 0 days 5:39:46.399
Process Uptime: 0 days 0:04:06.000
........................................
Loading unloaded module list
.
ntdll!NtDeviceIoControlFile+0x14:
00007ffd`734351c4 c3 ret
0:000>
- sos.dll clr.dll을 로드함.
- windbg에서 닷넷어플리케이션 디버깅에 필요한 명령어들이 포함된 dll임.
.loadby sos clr
0:000> .loadby sos clr
- 콜스텍정보를 출력함
- 모든 디버깅은 콜스텍 정보를 출력하는 것부터 시작!
- 닷넷디버깅
!clrstack -a
0:000> !clrstack -a
OS Thread Id: 0x1dc4 (0)
Child SP IP Call Site
0000003a9acfed28 00007ffd734351c4 [InlinedCallFrame: 0000003a9acfed28] Microsoft.Win32.Win32Native.ReadConsoleInput(IntPtr, InputRecord ByRef, Int32, Int32 ByRef)
0000003a9acfed28 00007ffd416893a1 [InlinedCallFrame: 0000003a9acfed28] Microsoft.Win32.Win32Native.ReadConsoleInput(IntPtr, InputRecord ByRef, Int32, Int32 ByRef)
0000003a9acfecf0 00007ffd416893a1 DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr, InputRecord ByRef, Int32, Int32 ByRef)
PARAMETERS:
<no data>
<no data>
<no data>
<no data>
0000003a9acfee00 00007ffd4177e8d9 System.Console.ReadKey(Boolean)
PARAMETERS:
intercept (0x0000003a9acfef08) = 0x0000000000000000
LOCALS:
<no data>
<no data>
<no data>
<no data>
<no data>
0x0000003a9acfee40 = 0x000001963d847c98
<no data>
<no data>
<no data>
0000003a9acfef00 00007ffce4f7063c *** WARNING: Unable to verify checksum for MySimpleConApp.exe
MySimpleConApp.Program.Main(System.String[]) [C:\project\160516_DotNetDebugging\MySimpleConApp\Program.cs @ 35]
PARAMETERS:
args (0x0000003a9acfefd0) = 0x000001963d8443f8
LOCALS:
0x0000003a9acfef68 = 0x000001963d844640
0x0000003a9acfefa0 = 0x0000000000000000
0x0000003a9acfef9c = 0x0000000000000000
0x0000003a9acfef98 = 0x0000000000000000
0x0000003a9acfef94 = 0x0000000000000000
0x0000003a9acfef90 = 0x0000000000000001
0000003a9acff200 00007ffd445d4073 [GCFrame: 0000003a9acff200]
- 윗결과에서 Program.Main 함수의 로컬변수인 0x000001963d844640 를 클릭하면 오브젝트의 덤프를 출력할 수 있음.
- 이 변수는 코드상의 var p = new MyPerson(); 변수임.
- p.id = 101; 로 할당된 변수값을 확인할 수 있음.
!DumpObj /d 000001963d844640
0:000> !DumpObj /d 000001963d844640
Name: MySimpleConApp.MyPerson
MethodTable: 00007ffce4e65b10
EEClass: 00007ffce4fb1068
Size: 32(0x20) bytes
File: C:\project\160516_DotNetDebugging\MySimpleConApp\bin\Debug\MySimpleConApp.exe
Fields:
MT Field Offset Type VT Attr Value Name
00007ffd4115af60 4000001 10 System.Int32 1 instance 101 <id>k__BackingField
00007ffd41158538 4000002 8 System.String 0 instance 000001963d844410 <name>k__BackingField
- p변수의 name의 값을 알아보기위해 000001963d844410 를 덤프시도
- william 이라는 텍스트를 확인할 수 있음.
!DumpObj /d 000001963d844410
0:000> !DumpObj /d 000001963d844410
Name: System.String
MethodTable: 00007ffd41158538
EEClass: 00007ffd40aa4ab8
Size: 40(0x28) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: william
Fields:
MT Field Offset Type VT Attr Value Name
00007ffd4115af60 4000243 8 System.Int32 1 instance 7 m_stringLength
00007ffd411596e8 4000244 c System.Char 1 instance 77 m_firstChar
00007ffd41158538 4000248 80 System.String 0 shared static Empty
>> Domain:Value 000001963b9500e0:NotInit <<
- 이것외에도 무작정 힙에 할당된 모든 메모리의 값을 알고 싶다면…
- 주소별로 정렬되고 타입별로 통계내어서 깔끔하게 출력된다.
- 그렇지만 짧은 프로그램인데도 시스템 내부 변수 때문에 결과가 엄청 길다. ㅠㅠ;;
!dumpheap
0:000> !dumpheap
Address MT Size
000001963d841000 000001963b9560a0 24 Free
000001963d841018 000001963b9560a0 24 Free
000001963d841030 000001963b9560a0 24 Free
000001963d841048 00007ffd41158768 160
000001963d8410e8 00007ffd41158950 160
000001963d841188 00007ffd411589c8 160
000001963d841228 00007ffd41158a40 160
...
Statistics:
MT Count TotalSize Class Name
00007ffd4117a140 1 24 System.Reflection.Missing
00007ffd41179ff8 1 24 System.__Filters
00007ffd41179890 1 24 System.IntPtr
00007ffd4115fea0 1 24 System.Security.HostSecurityManager
...
Total 417 objects
- 정적변수로 잡았던 MySingleton에 대한 값을 확인해 볼려고 함.
- 일단 정적변수초기화에 대한 코드를 다시 한번보면
MySingleton.Current.id = 12345;
MySingleton.Current.name = "william";
MySingleton.Current.friends = new List<string> { "brandon", "kevin" };
- MySingleton으로 타입을 지정해서 힙에서 변수를 덤프.
!dumpheap -type MySingleton
0:000> !dumpheap -type MySingleton
Address MT Size
000001963d844660 00007ffce4e65c78 40
Statistics:
MT Count TotalSize Class Name
00007ffce4e65c78 1 40 MySimpleConApp.MySingleton
Total 1 objects
0:000> !DumpObj /d 000001963d844660
Name: MySimpleConApp.MySingleton
MethodTable: 00007ffce4e65c78
EEClass: 00007ffce4fb10e0
Size: 40(0x28) bytes
File: C:\project\160516_DotNetDebugging\MySimpleConApp\bin\Debug\MySimpleConApp.exe
Fields:
MT Field Offset Type VT Attr Value Name
00007ffd4115af60 4000003 18 System.Int32 1 instance 12345 <id>k__BackingField
00007ffd41158538 4000004 8 System.String 0 instance 000001963d844410 <name>k__BackingField
00007ffd40abd6c8 4000005 10 ...tring, mscorlib]] 0 instance 000001963d844688 <friends>k__BackingField
00007ffce4e65c78 4000006 8 ...onApp.MySingleton 0 static 000001963d844660 _Current
- 일단 name 속성을 확인.
!DumpObj /d 000001963d844410
0:000> !DumpObj /d 000001963d844410
Name: System.String
MethodTable: 00007ffd41158538
EEClass: 00007ffd40aa4ab8
Size: 40(0x28) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: william
Fields:
MT Field Offset Type VT Attr Value Name
00007ffd4115af60 4000243 8 System.Int32 1 instance 7 m_stringLength
00007ffd411596e8 4000244 c System.Char 1 instance 77 m_firstChar
00007ffd41158538 4000248 80 System.String 0 shared static Empty
>> Domain:Value 000001963b9500e0:NotInit <<
- friends 는 List타입이기 때문에 몇단계를 거쳐야 한다.
- 일단 friends를 덤프하고,
!DumpObj /d 000001963d844688
0:000> !DumpObj /d 000001963d844688
Name: System.Collections.Generic.List`1[[System.String, mscorlib]]
MethodTable: 00007ffd40abd6c8
EEClass: 00007ffd40b6b310
Size: 40(0x28) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffd41147288 4001820 8 System.__Canon[] 0 instance 000001963d8446c8 _items
00007ffd4115af60 4001821 18 System.Int32 1 instance 2 _size
00007ffd4115af60 4001822 1c System.Int32 1 instance 2 _version
00007ffd41158b18 4001823 10 System.Object 0 instance 0000000000000000 _syncRoot
00007ffd41147288 4001824 0 System.__Canon[] 0 shared static _emptyArray
>> Domain:Value dynamic statics NYI 000001963b9500e0:NotInit <<
- 다시 _items를 덤프함.
- _items를 덤프할때는 변수덤프가 아닌 배열덤프를 사용함.
!DumpArray /d 000001963d8446c8
0:000> !DumpArray /d 000001963d8446c8
Name: System.String[]
MethodTable: 00007ffd41159880
EEClass: 00007ffd40b624a8
Size: 56(0x38) bytes
Array: Rank 1, Number of elements 4, Type CLASS
Element Methodtable: 00007ffd41158538
[0] 000001963d844438
[1] 000001963d844460
[2] null
[3] null
- 배열이 2개만 할당된것이 확인됨.
- 이중 0번째 값을 덤프해보면 brandon이라는 값이 정확히 확인됨.
- 이중 1번째 값을 덤프해보면 kevin이라는 값이 정확히 확인됨.
!DumpObj /d 000001963d844438
!DumpObj /d 000001963d844460
0:000> !DumpObj /d 000001963d844438
Name: System.String
MethodTable: 00007ffd41158538
EEClass: 00007ffd40aa4ab8
Size: 40(0x28) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: brandon
Fields:
MT Field Offset Type VT Attr Value Name
00007ffd4115af60 4000243 8 System.Int32 1 instance 7 m_stringLength
00007ffd411596e8 4000244 c System.Char 1 instance 62 m_firstChar
00007ffd41158538 4000248 80 System.String 0 shared static Empty
>> Domain:Value 000001963b9500e0:NotInit <<
0:000> !DumpObj /d 000001963d844460
Name: System.String
MethodTable: 00007ffd41158538
EEClass: 00007ffd40aa4ab8
Size: 36(0x24) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: kevin
Fields:
MT Field Offset Type VT Attr Value Name
00007ffd4115af60 4000243 8 System.Int32 1 instance 5 m_stringLength
00007ffd411596e8 4000244 c System.Char 1 instance 6b m_firstChar
00007ffd41158538 4000248 80 System.String 0 shared static Empty
>> Domain:Value 000001963b9500e0:NotInit <<
- Program.Main 함수의 소스코드가 화면에 띄워짐
- 소스코드에서 정지점에 노란색 화살표도 표시됨.
- 정지점 기준으로 로컯변수 p와 MySingleton.Current 같은 정적변수에 접근하는 것도 가능함.
VS디버거 사용시 주의할 점
- 분석대상 dmp파일을 발생시킨 어플리케이션의 pdb파일이 분석시 꼭 같은 디렉토리에 존재해야 함.
- 역시 같은버전의 소스코드도 어디엔가는 존재해야 하는데, VS가 발견하지 못하면 소스코드 찾기 대화상자가 유도되므로 소스코드가 준비가 되어 있어야 함.
- pdb파일버전은 dmp파일과 정확히 같아야 하지만, 소스코드는 분석대상 이외의 부분이 수정된것은 상관없음.
- 실용적으로는 CBT, OBT등 본격 테스트에 앞서서 버전별로 pdb를 모아두어, 필요할때마다 연결해서 사용하는것이 좋겠음.
닷넷 어플리케이션 내에서 덤프파일 생성하기
- 어플리케이션 크래시상황이나, 실행중이라도 특정시점에 즉시 덤프를 수행하는 방법이 필요할 때가 많음.
- 크래시 이후 디버깅
- 사용자환경에서 실행중 디버깅
- 사용자환경개선 프로그램(?)
- 당연히 덤프파일 생성기능이 API로 제공됨.
- 아쉽게도 Win32 API이므로 pinvoke 사용해야 함.
namespace MySimpleConApp
{
public static class MiniDumpWriter
{
[Flags]
public enum MINIDUMP_TYPE
{
MiniDumpNormal = 0x00000000,
MiniDumpWithDataSegs = 0x00000001,
MiniDumpWithFullMemory = 0x00000002,
MiniDumpWithHandleData = 0x00000004,
MiniDumpFilterMemory = 0x00000008,
MiniDumpScanMemory = 0x00000010,
MiniDumpWithUnloadedModules = 0x00000020,
MiniDumpWithIndirectlyReferencedMemory = 0x00000040,
MiniDumpFilterModulePaths = 0x00000080,
MiniDumpWithProcessThreadData = 0x00000100,
MiniDumpWithPrivateReadWriteMemory = 0x00000200,
MiniDumpWithoutOptionalData = 0x00000400,
MiniDumpWithFullMemoryInfo = 0x00000800,
MiniDumpWithThreadInfo = 0x00001000,
MiniDumpWithCodeSegs = 0x00002000
}
[DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam);
public static bool Write()
{
var currentProcess = Process.GetCurrentProcess();
var currentProcessHandle = currentProcess.Handle;
var currentProcessId = (uint)currentProcess.Id;
{
var fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $@"{DateTime.Now.ToString("yyMMdd-HHmmss")}.mini.dmp");
var options = MINIDUMP_TYPE.MiniDumpNormal;
using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Write))
{
MiniDumpWriteDump(currentProcessHandle, currentProcessId, fs.SafeFileHandle, (uint)options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}
}
{
var fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $@"{DateTime.Now.ToString("yyMMdd-HHmmss")}.full.dmp");
var options =
MINIDUMP_TYPE.MiniDumpNormal |
MINIDUMP_TYPE.MiniDumpWithFullMemory |
MINIDUMP_TYPE.MiniDumpWithHandleData |
MINIDUMP_TYPE.MiniDumpWithProcessThreadData |
MINIDUMP_TYPE.MiniDumpWithThreadInfo; ;
using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Write))
{
MiniDumpWriteDump(currentProcessHandle, currentProcessId, fs.SafeFileHandle, (uint)options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}
}
return true;
}
}
}
- 이렇듯 dbghelp.dll의 MiniDumpWriteDump 함수를 사용하여 구현을 하는데 미니덤프와 풀덤프를 함께 만들도록 구현했음.
- 상기 언급했듯 미니덤프는 콜스텍정보만, 풀덤프는 콜스텍과 메모리정보를 모두 포함함.
- 사용하는 코드는 아래와 같음.
- d를 클릭하면 수행하게 되어 있음.
if (key.Key == ConsoleKey.D)
{
Console.WriteLine("create dump...");
MiniDumpWriter.Write();
}
이 글은 Evernote에서 작성되었습니다. Evernote는 하나의 업무 공간입니다. Evernote를 다운로드하세요. |
댓글
댓글 쓰기