Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 57. C# - double 값에 대한 windbg 확인 [링크 복사], [링크+제목 복사],
조회: 17091
글쓴 사람
정성태 (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)
13579정성태3/13/20241958오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/20242174닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/20242477닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
13576정성태3/8/20241936닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/20242118닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
13574정성태3/6/20242002닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
13573정성태3/5/20241900닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
13572정성태3/4/20241936닷넷: 2224. C# - WPF의 Dispatcher Queue로 알아보는 await 호출의 hang 현상파일 다운로드1
13571정성태3/1/20241979닷넷: 2223. C# - await 호출과 WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13570정성태2/29/20241909닷넷: 2222. C# - WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13569정성태2/28/20241835닷넷: 2221. C# - LoadContext, LoadFromContext 그리고 GAC파일 다운로드1
13568정성태2/27/20241956닷넷: 2220. C# - .NET Framework 프로세스의 LoaderOptimization 설정을 확인하는 방법파일 다운로드1
13567정성태2/27/20241910오류 유형: 898. .NET Framework 3.5 이하에서 mscoree.tlb 참조 시 System.BadImageFormatException파일 다운로드1
13566정성태2/27/20241974오류 유형: 897. Windows 7 SDK 설치 시 ".NET Development" 옵션이 비활성으로 선택이 안 되는 경우
13565정성태2/23/20241842닷넷: 2219. .NET CLR2 보안 모델에서의 개별 System.Security.Permissions 제어
13564정성태2/22/20242058Windows: 259. Hyper-V Generation 1 유형의 VM을 Generation 2 유형으로 바꾸는 방법
13563정성태2/21/20242022디버깅 기술: 196. windbg - async/await 비동기인 경우 메모리 덤프 분석의 어려움
13562정성태2/21/20242044오류 유형: 896. ASP.NET - .NET Framework 기본 예제에서 System.Web에 대한 System.IO.FileNotFoundException 예외 발생
13561정성태2/20/20242162닷넷: 2218. C# - (예를 들어, Socket) 비동기 I/O에 대한 await 호출 시 CancellationToken을 이용한 취소파일 다운로드1
13560정성태2/19/20242162디버깅 기술: 195. windbg 분석 사례 - Semaphore 잠금으로 인한 Hang 현상 (닷넷)
13559정성태2/19/20243031오류 유형: 895. ASP.NET - System.Security.SecurityException: 'Requested registry access is not allowed.'
13558정성태2/18/20242271닷넷: 2217. C# - 최댓값이 1인 SemaphoreSlim 보다 Mutex 또는 lock(obj)를 선택하는 것이 나은 이유
13557정성태2/18/20242019Windows: 258. Task Scheduler의 Author 속성 값을 변경하는 방법
13556정성태2/17/20242078Windows: 257. Windows - Symbolic (hard/soft) Link 및 Junction 차이점
13555정성태2/15/20242402닷넷: 2216. C# - SemaphoreSlim 사용 시 주의점
13554정성태2/15/20242096VS.NET IDE: 189. Visual Studio - 닷넷 소스코드 디컴파일 찾기가 안 될 때
1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...