Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 57. C# - double 값에 대한 windbg 확인 [링크 복사], [링크+제목 복사],
조회: 23199
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 3개 있습니다.)
디버깅 기술: 57. C# - double 값에 대한 windbg 확인
; https://www.sysnet.pe.kr/2/0/1526

디버깅 기술: 97. windbg - 메모리 덤프로부터 DateTime 형식의 값을 알아내는 방법
; https://www.sysnet.pe.kr/2/0/11313

디버깅 기술: 202. windbg - ASP.NET MVC Web Application (.NET Framework) 응용 프로그램의 덤프 분석 시 요령
; https://www.sysnet.pe.kr/2/0/13741




C# - double 값에 대한 windbg 확인

아래와 같이 간단하게 프로그램을 만들고,

class Program
{
    static void Main(string[] args)
    {
        double xd = -2.12200016492843E-314;
        Output(xd);
    }

    private static void Output(double xd)
    {
        Console.WriteLine(xd);
        Console.ReadLine();
    }
}

Console.ReadLine까지 실행되었을 때 windbg로 attach시켜 봅니다.

Microsoft (R) Windows Debugger Version 6.2.9200.20512 X86
Copyright (c) Microsoft Corporation. All rights reserved.

*** wait with pending attach
Symbol search path is: SRV*e:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is: 
ModLoad: 00e70000 00e78000   d:\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
ModLoad: 76fc0000 77128000   C:\WINDOWS\SYSTEM32\ntdll.dll
ModLoad: 73e20000 73e76000   C:\WINDOWS\SYSTEM32\MSCOREE.DLL
ModLoad: 76770000 768b0000   C:\WINDOWS\SYSTEM32\KERNEL32.dll
...[생략]...
\b23c1312ec0a64893e596e2fc2aa875b\System.Core.ni.dll
(1e0c.2c60): Break instruction exception - code 80000003 (first chance)
eax=7e4a3000 ebx=00000000 ecx=00000000 edx=7706fdc4 esi=00000000 edi=00000000
eip=76fe879c esp=056bf934 ebp=056bf960 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!DbgBreakPoint:
76fe879c cc              int     3

0:006> .loadby sos clr

오호... 운이 나쁘게도 6번 스레드를 windbg가 잡아버렸군요. 우리가 원하는 CLR 스레드로 이동하기 위해서 우선 스레드 목록을 확인합니다.

0:006> !threads
ThreadCount:      2
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 3b1c 013c66e0     2a020 Preemptive  02EC52D8:00000000 013bd818 1     MTA 
   3    2  f1c 013d54b0     2b220 Preemptive  00000000:00000000 013bd818 0     MTA (Finalizer) 

windbg에서는 OS의 ID가 중요하므로 3b1c 값으로 스레드 변경을 할 수 있습니다.

0:006> ~~[3b1c]s
eax=00000000 ebx=00000024 ecx=00000000 edx=00000000 esi=00fff34c edi=00000000
eip=76ffbc5c esp=00fff234 ebp=00fff294 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!NtReadFile+0xc:
76ffbc5c c22400          ret     24h

0:000>

0:000으로 바뀌었죠. ^^ 이제 !clrstack 명령을 내리면 다음과 같이 나옵니다.

0:000> !clrstack
OS Thread Id: 0x3b1c (0)
Child SP       IP Call Site
00fff2b4 76ffbc5c [InlinedCallFrame: 00fff2b4] 
00fff2b0 7292b8cf DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
00fff2b4 7307a5f4 [InlinedCallFrame: 00fff2b4] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
00fff318 7307a5f4 System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
00fff34c 7307a4fb System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
00fff36c 728c6768 System.IO.StreamReader.ReadBuffer()
00fff380 7288838f System.IO.StreamReader.ReadLine()
00fff39c 730806ed System.IO.TextReader+SyncTextReader.ReadLine()
00fff3ac 72f88fb2 System.Console.ReadLine()
00fff3b4 012d0a34 ConsoleApplication1.Program.Output(Double) [d:\ConsoleApplication1\Program.cs @ 91]
00fff3c8 012d0597 ConsoleApplication1.Program.Main(System.String[]) [d:\ConsoleApplication1\Program.cs @ 81]
00fff660 73702652 [GCFrame: 00fff660] 

각 메서드의 호출에 전달된 파라미터 값도 확인하는 것이 가능합니다. 단지 -p 옵션만 추가해 주면 됩니다. ^^

0:000> !clrstack -p
OS Thread Id: 0x3b1c (0)
Child SP       IP Call Site
00fff2b4 76ffbc5c [InlinedCallFrame: 00fff2b4] 

...[생략]..

00fff39c 730806ed System.IO.TextReader+SyncTextReader.ReadLine()
    PARAMETERS:
        this (<CLR reg>) = 0x02ec52c8

00fff3ac 72f88fb2 System.Console.ReadLine()

00fff3b4 012d0a34 ConsoleApplication1.Program.Output(Double) [d:\ConsoleApplication1\Program.cs @ 91]
    PARAMETERS:
        xd (0x00fff3c0) = 0x00002295

00fff3c8 012d0597 ConsoleApplication1.Program.Main(System.String[]) [d:\ConsoleApplication1\Program.cs @ 81]
    PARAMETERS:
        args (0x00fff4cc) = 0x02ec2448

00fff660 73702652 [GCFrame: 00fff660] 

보시는 것처럼, Output 메서드에 전달된 double 인자의 값이 0x00002295입니다. double 인자는 IEEE 부동 소수점 표준을 따르므로 -2.12200016492843E-314 값이 0x2295로 나온 것입니다. (double은 8바이트이긴 한데 !clrstack 명령어는 타입 고려를 하지 않아 4바이트 내용만 출력합니다.)

바이너리 표기를 직접 확인하는 것도 가능합니다. 위의 출력에서 "xd (0x00fff3c0)"라고 되어 있는데 0x00fff3c0 값이 바로 double 값이 담겨있는 메모리의 주소입니다. 따라서 다음과 같이 덤프할 수 있습니다.

0:000> db 0x00fff3c0
00fff3c0  95 22 00 00 01 00 00 80-5c 4e ec 02 b0 4d ec 02  ."......\N...M..
00fff3d0  08 4d ec 02 94 4c ec 02-a4 4b ec 02 54 4b ec 02  .M...L...K..TK..
00fff3e0  1c 4b ec 02 08 4b ec 02-bc 4a ec 02 84 4a ec 02  .K...K...J...J..
00fff3f0  70 4a ec 02 b8 49 ec 02-68 49 ec 02 ec 48 ec 02  pJ...I..hI...H..
00fff400  9c 48 ec 02 20 48 ec 02-d0 47 ec 02 e0 24 ec 02  .H.. H...G...$..
00fff410  90 24 ec 02 5c 4e ec 02-08 4b ec 02 84 4a ec 02  .$..\N...K...J..
00fff420  70 4a ec 02 95 22 00 00-01 00 00 80 70 4e ec 02  pJ..."......pN..
00fff430  d3 ff ff ff 1c 00 00 00-c4 4d ec 02 24 d7 12 00  .........M..$...

아하~~~ 엔디안 문제가 있으니 8바이트씩 묶어서 출력하는 것이 더 낫겠군요.

0:000> dq 0x00fff3c0
00fff3c0  80000001`00002295 02ec4db0`02ec4e5c
00fff3d0  02ec4c94`02ec4d08 02ec4b54`02ec4ba4
00fff3e0  02ec4b08`02ec4b1c 02ec4a84`02ec4abc
00fff3f0  02ec49b8`02ec4a70 02ec48ec`02ec4968
00fff400  02ec4820`02ec489c 02ec24e0`02ec47d0
00fff410  02ec4e5c`02ec2490 02ec4a84`02ec4b08
00fff420  00002295`02ec4a70 02ec4e70`80000001
00fff430  0000001c`ffffffd3 0012d724`02ec4dc4

실제로 이 값이 맞는지 C#코드로 확인해 볼 수 있습니다.

ulong xd2 = 0x8000000100002295;
byte [] tmpBytes = BitConverter.GetBytes(xd2);
Console.WriteLine(BitConverter.ToDouble(tmpBytes, 0));

// 출력결과
-2.12200016492843E-314




"clrstack -p"로 쉽게 인자 값을 확인할 수 있지만 가끔은 unmanaged로 접근해야 하는 경우가 있습니다. 예를 들어, RCW를 이용해 COM 개체의 메서드를 호출하는 경우에는 "clrstack -p"로 호출 인자가 출력되지 않습니다.

그런 경우 windbg에서 "View" / "Call Stack" 메뉴를 선택하면 다음과 같은 목록을 볼 수 있습니다.

windbg_ebp_1.png

Output 메서드가 COM 개체의 호출이라고 가정한 경우 !clrstack의 출력으로부터 IP(Instruction Pointer) 주소를 얻을 수 있고,

0:000> !clrstack
OS Thread Id: 0x3b1c (0)
Child SP       IP Call Site
..[생략]...
00fff3ac 72f88fb2 System.Console.ReadLine()
00fff3b4 012d0a34 ConsoleApplication1.Program.Output(Double) [d:\ConsoleApplication1\Program.cs @ 91]
00fff3c8 012d0597 ConsoleApplication1.Program.Main(System.String[]) [d:\ConsoleApplication1\Program.cs @ 81]
00fff660 73702652 [GCFrame: 00fff660]

"Call Stack" 뷰에서는 !clrstack 출력이 아닌 네이티브 호출 스택을 보여주므로 012d0a34 값 자체를 메서드 명 위치에서 찾으면 됩니다. 그럼 다음의 프레임이 우리가 찾는 Output 메서드에 해당하죠.

ChildEBP RetAddr  Args to Child              Method Name
...[생략]...
00fff3b8 012d0597 00002295 80000001 02ec4e5c 0x12d0a34
...[생략]...

친절하게도 Args 목록이 나오는데 windbg는 해당 데이터 타입을 모르기 때문에 32비트 응용 프로그램인 경우 4바이트씩 잘라서 인자를 보여주고 있습니다. 기본적으로 3개의 인자를 이렇게 보여주고 있는데, EBP 레지스터를 이용하면 좀 더 덤프를 하는 것도 가능합니다. C/C++에 익숙한 분들이라면 (32비트인 경우) EBP + 8의 주소를 시작으로 인자가 나열된다는 것을 잘 아시겠죠! ^^ 따라서 ChildEBP 주소로 이 처리를 할 수 있습니다.

0:000> dq 00fff3b8 + 8
00fff3c0  80000001`00002295 02ec4db0`02ec4e5c
00fff3d0  02ec4c94`02ec4d08 02ec4b54`02ec4ba4
00fff3e0  02ec4b08`02ec4b1c 02ec4a84`02ec4abc
00fff3f0  02ec49b8`02ec4a70 02ec48ec`02ec4968
00fff400  02ec4820`02ec489c 02ec24e0`02ec47d0
00fff410  02ec4e5c`02ec2490 02ec4a84`02ec4b08
00fff420  00002295`02ec4a70 02ec4e70`80000001
00fff430  0000001c`ffffffd3 0012d724`02ec4dc4

당연히 이 주소는 이전의 !clrstack -p로 살펴봤던 파라미터의 주소값과 일치하므로 출력 결과가 똑같습니다.

(사실, 위의 과정은 "Call Stack" 뷰가 아니라 단순히 k 명령어를 통해서도 할 수 있습니다. ^^)




double 값이 일반적인 숫자 데이터와는 인코딩 방식이 다르기 때문에 한가지 유의할 사항이 있습니다. 가령 다음과 같이 덤프된 결과를 얻은 경우,

0:000> dq 12cea8 + 8
0012ceb0  ffffffe7`00000004 0012d728`0012d724
0012cec0  0000001c`ffffffd3 80000001`00002295
0012ced0  79f3b1a2`0012d248 79e992fb`ffffffff
0012cee0  00000010`79e96be6 00000000`0e2cfcb8
0012cef0  0012d254`80000000 79e7f1a8`79e96bfb
0012cf00  13f37b88`79e96a55 0012d3b8`0012d560
0012cf10  00000000`0012d560 79e9d605`00000000
0012cf20  00000000`00000000 00000000`01566394

결과가 "ffffffe7`00000004"로 나왔는데 이 값이 일반적인 숫자라면 넘어갈 수 있는 문제이지만 double 형 타입인 경우에는 그렇지 않습니다. 값을 직접 확인해 보면 어떨까요? ^^

ulong xd2 = 0xffffffe700000004;
byte [] tmpBytes = BitConverter.GetBytes(xd2);
Console.WriteLine(BitConverter.ToDouble(tmpBytes, 0));

// 출력결과
NaN

NaN이 나왔으니 유효한 double 값이 아니었던 것입니다. 이 문제는 실제로 한 고객사의 덤프를 분석해서 나온 결과입니다. double 값이 이렇게 전달되는 것을 모르고 그동안 응용 프로그램이 실행되다가 어느 순간 이런 잘못된 입력이 들어왔을 때 프로그램이 오동작을 일으키게 된 것입니다. (입력값을 검사하는 코드가 왜 중요한지 알게 되는 순간입니다. ^^)

마지막으로 C#의 double에 대한 특이값들은 다음과 같습니다.

double t = 0;
double t1 = double.NaN;
double t2 = double.PositiveInfinity;
double t3 = double.NegativeInfinity;

Console.WriteLine(BitConverter.ToString(BitConverter.GetBytes(t)));
Console.WriteLine(t);

Console.WriteLine(BitConverter.ToString(BitConverter.GetBytes(t1)));
Console.WriteLine(t1);

Console.WriteLine(BitConverter.ToString(BitConverter.GetBytes(t2)));
Console.WriteLine(t2);

Console.WriteLine(BitConverter.ToString(BitConverter.GetBytes(t3)));
Console.WriteLine(t3);

// 출력결과:
0  : 00-00-00-00-00-00-00-00
NaN: 00-00-00-00-00-00-F8-FF (0xfff8000000000000)
+∞ : 00-00-00-00-00-00-F0-7F (0x7ff0000000000000)
-∞ : 00-00-00-00-00-00-F0-FF (0xfff0000000000000)

보시면 NaN 값이 0xfff8000000000000로 나오는데 이 값만을 기억하고 NaN 판정을 내리면 안된다는 사실!




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/10/2021]

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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  [128]  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1855정성태2/10/201520734개발 환경 구성: 256. WebDAV Redirector - Sysinternals 폴더 연결 시 "The network path was not found" 오류 해결 방법
1854정성태2/10/201521702Windows: 104. 폴더는 삭제할 수 없지만, 그 하위 폴더/파일은 생성/삭제/변경하는 보안 설정
1853정성태2/6/201552019웹: 29. 여신금융협회 웹 사이트의 "Netscape 6.0은 지원하지 않습니다." 오류 메시지 [5]
1852정성태2/5/201522409.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미파일 다운로드1
1851정성태2/5/201523359VC++: 88. 하룻밤의 꿈 - 인텔 하스웰의 TSX Instruction 지원 [2]
1850정성태2/4/201544166Windows: 103. 작업 관리자에서의 "Commit size"가 가리키는 메모리의 의미 [4]
1849정성태2/4/201524109기타: 51. DropBox의 CPU 100% 현상 [1]파일 다운로드1
1848정성태2/4/201519341.NET Framework: 491. 닷넷 Generic 타입의 메타 데이터 토큰 값 알아내는 방법 [2]
1847정성태2/3/201522679기타: 50. C# - 윈도우에서 dropbox 동기화 폴더 경로 및 종료하는 방법
1846정성태2/2/201531979Windows: 102. 제어판의 프로그램 추가/삭제 항목을 수동으로 실행하고 싶다면? [1]
1845정성태1/26/201532855Windows: 101. 제어판의 "Windows 자격 증명 관리(Manage your credentials)"를 금지시키는 방법
1844정성태1/26/201530799오류 유형: 269. USB 메모리의 용량이 비정상적으로 보여진다면? [7]
1843정성태1/24/201521850VC++: 87. 무시할 수 없는 Visual C++ 런타임 함수 성능
1842정성태1/23/201544327개발 환경 구성: 255. 노트북 키보드에 없는 BREAK 키를 다른 키로 대체하는 방법
1841정성태1/21/201519296오류 유형: 268. Win32 핸들 관련 CLR4 보안 오류 사례
1840정성태1/8/201527524오류 유형: 267. Visual Studio - CodeLens 사용 시 CPU 100% 현상
1839정성태1/5/201520425디버깅 기술: 69. windbg 분석 사례 - cpu 100% 현상 (2)
1838정성태1/4/201540184기타: 49. 윈도우 내레이터(Narrator) 기능 끄는 방법(윈도우에 파란색의 굵은 테두리 선이 나타난다면?) [4]
1837정성태1/4/201526294디버깅 기술: 68. windbg 분석 사례 - 메모리 부족 [1]
1836정성태1/4/201526305디버깅 기술: 67. windbg - 덤프 파일과 handle 정보
1835정성태1/3/201526775개발 환경 구성: 254. SQL 서버 역시 SSL 3.0/TLS 1.0만을 지원하는 듯!
1834정성태1/3/201551424개발 환경 구성: 253. TLS 1.2를 적용한 IIS 웹 사이트 구성
1833정성태1/3/201527471.NET Framework: 490. System.Data.SqlClient는 SSL 3.0/TLS 1.0만 지원하는 듯! [3]
1832정성태1/2/201520547오류 유형: 266. Azure에 응용 프로그램 게시 중 로그인 오류
1831정성태1/1/201528421디버깅 기술: 66. windbg 분석 사례 - cpu 100% 현상 (1) [1]
1830정성태1/1/201527467오류 유형: 265. svchost.exe 프로세스(IP Helper: IPHLPSVC)의 CPU 100% 현상
... 121  122  123  124  125  126  127  [128]  129  130  131  132  133  134  135  ...