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

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

비밀번호

댓글 작성자
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13616정성태5/3/202473닷넷: 2256. ASP.NET Core 웹 사이트의 HTTP/HTTPS + Dual mode Socket (IPv4/IPv6) 지원 방법파일 다운로드1
13615정성태5/3/2024105닷넷: 2255. C# 배열을 Numpy ndarray 배열과 상호 변환
13614정성태5/2/2024174닷넷: 2254. C# - COM 인터페이스의 상속 시 중복으로 메서드를 선언
13613정성태5/1/2024264닷넷: 2253. C# - Video Capture 장치(Camera) 열거 및 지원 포맷 조회파일 다운로드1
13612정성태4/30/2024250오류 유형: 902. Visual Studio - error MSB3021: Unable to copy file
13611정성태4/29/2024485닷넷: 2252. C# - GUID 타입 전용의 UnmanagedType.LPStruct - 두 번째 이야기파일 다운로드1
13610정성태4/28/2024581닷넷: 2251. C# - 제네릭 인자를 가진 타입을 생성하는 방법 - 두 번째 이야기
13609정성태4/27/2024760닷넷: 2250. PInvoke 호출 시 참조 타입(class)을 마샬링하는 [IN], [OUT] 특성파일 다운로드1
13608정성태4/26/20241011닷넷: 2249. C# - 부모의 필드/프로퍼티에 대해 서로 다른 자식 클래스 간에 Reflection 접근이 동작할까요?파일 다운로드1
13607정성태4/25/20241051닷넷: 2248. C# - 인터페이스 타입의 다중 포인터를 인자로 갖는 C/C++ 함수 연동
13606정성태4/24/2024997닷넷: 2247. C# - tensorflow 연동 (MNIST 예제)파일 다운로드1
13605정성태4/23/2024977닷넷: 2246. C# - Python.NET을 이용한 파이썬 소스코드 연동파일 다운로드1
13604정성태4/22/2024976오류 유형: 901. Visual Studio - Unable to set the next statement. Set next statement cannot be used in '[Exception]' call stack frames.
13603정성태4/21/20241025닷넷: 2245. C# - IronPython을 이용한 파이썬 소스코드 연동파일 다운로드1
13602정성태4/20/20241014닷넷: 2244. C# - PCM 오디오 데이터를 연속(Streaming) 재생 (Windows Multimedia)파일 다운로드1
13601정성태4/19/20241025닷넷: 2243. C# - PCM 사운드 재생(NAudio)파일 다운로드1
13600정성태4/18/20241066닷넷: 2242. C# - 관리 스레드와 비관리 스레드
13599정성태4/17/2024973닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/20241011닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드2
13597정성태4/15/20241050닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/20241134닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/20241087닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/20241112닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20241120닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241473C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...