windbg - 메모리 덤프로부터 DateTime 형식의 값을 알아내는 방법
예를 들어, 다음과 같이 DateTime을 사용하는 프로그램의
using System;
namespace ConsoleApp1
{
class Program
{
DateTime _now = DateTime.Now;
DateTime _utcNow = DateTime.UtcNow;
DateTime _uninitialized;
static void Main(string[] args)
{
Program pg = new Program();
Console.ReadLine();
}
}
}
메모리 덤프를 windbg에 로드하면 객체의 DateTime 필드를 다음과 같이 접근할 수 있습니다.
0:000> !DumpObj /d 030230dc
Name: ConsoleApp1.Program
MethodTable: 01174d4c
EEClass: 011713c4
Size: 32(0x20) bytes
File: C:\temp\ConsoleApp1\bin\Debug\ConsoleApp1.exe
Fields:
MT Field Offset Type VT Attr Value Name
5c75f748 4000001 4 System.DateTime 1 instance 030230e0 _now
5c75f748 4000002 c System.DateTime 1 instance 030230e8 _utcNow
5c75f748 4000003 14 System.DateTime 1 instance 030230f0 _uninitialized
그다음, 개별적으로 값을 덤프할 수 있습니다.
0:000> !DumpVC /d 5c75f748 030230e0
Name: System.DateTime
MethodTable: 5c75f748
EEClass: 5c2fe6c4
Size: 16(0x10) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
5c75d1d0 40002ce 0 System.UInt64 1 instance 9859791360354292701 dateData
5c763bc8 40002bf 68 System.Int32[] 0 shared static DaysToMonth365
>> Domain:Value 011eb430:0302315c <<
5c763bc8 40002c0 6c System.Int32[] 0 shared static DaysToMonth366
>> Domain:Value 011eb430:0302319c <<
5c75f748 40002c1 60 System.DateTime 1 shared static MinValue
>> Domain:Value 011eb430:04021088 <<
5c75f748 40002c2 64 System.DateTime 1 shared static MaxValue
>> Domain:Value 011eb430:0402108c <<
0:000> !DumpVC /d 5c75f748 030230e8
Name: System.DateTime
MethodTable: 5c75f748
EEClass: 5c2fe6c4
Size: 16(0x10) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
5c75d1d0 40002ce 0 System.UInt64 1 instance 5248105017926914794 dateData
...[생략]...
0:000> !DumpVC /d 5c75f748 030230f0
Name: System.DateTime
MethodTable: 5c75f748
EEClass: 5c2fe6c4
Size: 16(0x10) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
5c75d1d0 40002ce 0 System.UInt64 1 instance 0 dateData
...[생략]...
리플렉션으로 보면, ulong dateData 필드가 DateTime 타입의 유일한 인스턴스 데이터임을 알 수 있습니다.
[Serializable, __DynamicallyInvokable]
public struct DateTime : IComparable, IFormattable, IConvertible, ISerializable, IComparable<DateTime>, IEquatable<DateTime>
{
// Fields
private ulong dateData;
...[생략]...
// Methods
...[생략]...
[__DynamicallyInvokable]
public DateTime(long ticks)
{
if ((ticks < 0L) || (ticks > 0x2bca2875f4373fffL))
{
throw new ArgumentOutOfRangeException("ticks", Environment.GetResourceString("ArgumentOutOfRange_DateTimeBadTicks"));
}
this.dateData = (ulong) ticks;
}
...[생략]...
}
따라서, (예제 코드의 _uninitialized 멤버처럼) dateData 값이 0이라는 점은 값 형식에 한 번도 값이 할당된 적이 없는 것입니다. 그리고 각각 UTC, Local에 대해 다음과 같이 dateData 값이 할당됩니다.
dateData == 5248105017926914794 (UTC)
dateData == 9859791360354292701 (Local)
이 값으로부터 어떻게 원래 시간 정보를 알 수 있을까요? DateTime 생성자에 보면 인자로 전달한 값(ticks)을 그대로 dateData 변수에 대입하는 것을 볼 수 있는데요. 실제로 그렇게 해보면 0x2bca2875f4373fffL 한계에 걸려 ArgumentOutOfRangeException 예외가 발생합니다.
Unhandled Exception: System.ArgumentOutOfRangeException: Ticks must be between DateTime.MinValue.Ticks and DateTime.MaxValue.Ticks.
Parameter name: ticks
at System.DateTime..ctor(Int64 ticks)
at ConsoleApp1.Program.Main(String[] args)
9859791360354292701, 5248105017926914794 값은 0x2bca2875f4373fff(10진수 3155378975999999999)값보다 크기 때문인데, 이유를 알기 위해 좀 더 추적해 보면 DateTime.Now에서 단서를 찾을 수 있습니다.
[__DynamicallyInvokable]
public static DateTime Now
{
[__DynamicallyInvokable]
get
{
DateTime utcNow = UtcNow;
bool isAmbiguousLocalDst = false;
long ticks = TimeZoneInfo.GetDateTimeNowUtcOffsetFromUtc(utcNow, out isAmbiguousLocalDst).Ticks;
long num2 = utcNow.Ticks + ticks;
if (num2 > 0x2bca2875f4373fffL)
{
return new DateTime(0x2bca2875f4373fffL, DateTimeKind.Local);
}
if (num2 < 0L)
{
return new DateTime(0L, DateTimeKind.Local);
}
return new DateTime(num2, DateTimeKind.Local, isAmbiguousLocalDst);
}
}
[__DynamicallyInvokable]
public DateTime(long ticks, DateTimeKind kind)
{
if ((ticks < 0L) || (ticks > 0x2bca2875f4373fffL))
{
throw new ArgumentOutOfRangeException("ticks", Environment.GetResourceString("ArgumentOutOfRange_DateTimeBadTicks"));
}
if ((kind < DateTimeKind.Unspecified) || (kind > DateTimeKind.Local))
{
throw new ArgumentException(Environment.GetResourceString("Argument_InvalidDateTimeKind"), "kind");
}
this.dateData = (ulong) (ticks | (((long) kind) << 0x3e));
}
보는 바와 같이 DateTime.dateData의 값은 원래의 시간 정보를 담은 ticks 값에 DateTimeKind 값을 OR 연산한 값이기 때문입니다.
따라서, 해당 값의 DateTimeKind를 알 수 있다면 다음과 같이 변환해서 원래의 시간 정보를 알아낼 수 있습니다.
long u = 5248105017926914794;
long mask = (long)DateTimeKind.Utc; // 또는 DateTimeKind.Local
u = (u ^ (mask << 0x3e));
DateTime dt = new DateTime(u, (DateTimeKind)mask);
Console.WriteLine(dt);
결국 DateTimeKind 값에 의해 dateData 값이 바뀌므로 이를 자동화하면 다음과 같은 식으로 코딩할 수 있습니다.
private static DateTime ToDateTime(long u)
{
bool tryGetDateTime(long ticks, DateTimeKind kind, out DateTime result) // 로컬 함수 C# 7.0
{
result = DateTime.MinValue;
long checkMask = (long)kind << 0x3e;
long masked = ticks & checkMask;
if (masked == checkMask)
{
result = new DateTime(ticks ^ checkMask, kind);
return true;
}
return false;
};
switch (u)
{
// 패턴 매칭 C# 7.0
case long value when tryGetDateTime(value, DateTimeKind.Utc, out DateTime time) == true: // out 사용 개선 C# 7.0
return time;
case long value when tryGetDateTime(value, DateTimeKind.Local, out DateTime time) == true: // out 사용 개선 C# 7.0
return time;
}
return new DateTime(u, DateTimeKind.Unspecified);
}
이를 이용해 windbg에서 알아낸 값을 다음과 같이 친숙한 시간 값으로 바꿔줄 수 있습니다.
long u = 5248105017926914794;
DateTime dt = ToDateTime(u);
Console.WriteLine(dt); // 2017-09-25 오전 1:32:29
u = (long)9859791360354292701;
dt = ToDateTime(u);
Console.WriteLine(dt); // 2017-09-25 오전 10:32:29
참고로, DateTime 생성자를 통해 dateData 값을 넣어줄 필요 없이 Reflection을 이용하는 방법도 있습니다.
// C# - 구조체(값 형식)의 필드를 리플렉션을 이용해 값을 바꾸는 방법
// ; https://www.sysnet.pe.kr/2/0/11312
unchecked
{
long u = 5248105017926914794;
DateTime dt = new DateTime();
FieldInfo fi = typeof(DateTime).GetField("dateData", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
TypedReference tr = __makeref(dt);
fi.SetValueDirect(tr, (ulong)u);
Console.WriteLine(dt); // 2017-09-25 오전 1:32:29
u = (long)9859791360354292701;
fi.SetValueDirect(tr, (ulong)u);
Console.WriteLine(dt); // 2017-09-25 오전 10:32:29
}
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]