Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 57. C# - double 값에 대한 windbg 확인 [링크 복사], [링크+제목 복사],
조회: 17087
글쓴 사람
정성태 (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)
13504정성태12/27/20232842닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/20232762Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232766닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232584개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232722디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20233407닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232718오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232925Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232863Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20233015Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20233131닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232795개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232530Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232668개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232440개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232385오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232701개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232513개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232379오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232557개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232755닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20233391닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232760개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20233152개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232656개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232891닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...