C# - 일부러 GC Heap을 깨뜨려 GC 수행 시 비정상 종료시키는 예제
물론, 일부러 그럴 필요는 없지만 ^^ 그래도 이것이 얼마나 간단할 수 있는지 살펴보겠습니다. 지난 글에 설명했듯이,
C#에서 확인해 보는 관리 힙의 인스턴스 구조
; https://www.sysnet.pe.kr/2/0/1176
관리 힙에 있는 객체는 메모리 구조가 OBJECT Header와 Method Table 주소 및 각 타입의 필드로 이뤄집니다.
int[] test = new int[1];
test[0] = 0xff;
Console.WriteLine("int [] ================== ");
fixed (int* p1 = &test[0])
{
Console.WriteLine("OBJECT Header(SyncBlock Index): " + (*(p1 - 3)).ToString("x"));
Console.WriteLine("MethodTable Address: " + (*(p1 - 2)).ToString("x"));
Console.WriteLine("배열 요소 크기: " + (*(p1 - 1)).ToString("x"));
Console.WriteLine("0번째 배열 값: " + (*(p1 - 0)).ToString("x"));
}
그래서, 만약 관리 힙의 시작 주소를 알 수 있다면 그 이후로 Method Table 주소를 이용해 해당 타입의 필드 정보를 보고 객체가 소유한 메모리의 크기를 구해 다음 객체가 할당된 시작점으로 이동할 수 있습니다. 실제로 GC는 이런 식으로 GC Heap을 열람하면서 객체 정리를 해나갑니다.
그렇다면, 당연히 "Method Table 주소"를 구할 수 없다면 GC는 해당 객체의 크기를 알 수 없어 패닉(?)에 빠지게 될 것입니다. ^^; 이것을 다음과 같이 간단한 코드로 테스트할 수 있습니다.
using System;
class Program
{
static unsafe void Main(string[] args)
{
int[] test = new int[1];
test[0] = 0xff;
Console.WriteLine("int [] ================== ");
fixed (int* p1 = &test[0])
{
Console.WriteLine("OBJECT Header(SyncBlock Index): " + (*(p1 - 3)).ToString("x"));
Console.WriteLine("MethodTable Address: " + (*(p1 - 2)).ToString("x"));
*(p1 - 2) = 0; // GC의 MethodTable 주소 정보를 제거
Console.WriteLine("배열 요소 크기: " + (*(p1 - 1)).ToString("x"));
Console.WriteLine("0번째 배열 값: " + (*(p1 - 0)).ToString("x"));
}
GC.Collect(); // 여기서 비정상 종료
Console.WriteLine("GCed!"); // 이 코드는 절대 수행되지 않음
}
}
이때 발생하는 이벤트 로그를 보면 다음과 같습니다.
Log Name: Application
Source: .NET Runtime
Date: 2019-11-15 오후 10:45:32
Event ID: 1023
Task Category: None
Level: Error
Keywords: Classic
User: N/A
Computer: TESTPC
Description:
Application: ConsoleApp1.exe
Framework Version: v4.0.30319
Description: The process was terminated due to an internal error in the .NET Runtime at IP 709E21D1 (70900000) with exit code 80131506.
Log Name: Application
Source: Application Error
Date: 2019-11-15 오후 10:45:32
Event ID: 1000
Task Category: (100)
Level: Error
Keywords: Classic
User: N/A
Computer: TESTPC
Description:
Faulting application name: ConsoleApp1.exe, version: 1.0.0.0, time stamp: 0xf39545aa
Faulting module name: clr.dll, version: 4.8.4042.0, time stamp: 0x5d7a9e00
Exception code: 0xc0000005
Fault offset: 0x000e21d1
Faulting process id: 0x5f1c
Faulting application start time: 0x01d59b565e367a5c
Faulting application path: c:\temp\ConsoleApp1\bin\Debug\ConsoleApp1.exe
Faulting module path: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
Report Id: b790601d-4a9d-470c-b8a9-144beabee7ab
Faulting package full name:
Faulting package-relative application ID:
Log Name: Application
Source: Windows Error Reporting
Date: 2019-11-15 오후 10:45:35
Event ID: 1001
Task Category: None
Level: Information
Keywords: Classic
User: N/A
Computer: TESTPC
Description:
Fault bucket 2275602419194088766, type 1
Event Name: APPCRASH
Response: Not available
Cab Id: 0
Problem signature:
P1: ConsoleApp1.exe
P2: 1.0.0.0
P3: f39545aa
P4: clr.dll
P5: 4.8.4042.0
P6: 5d7a9e00
P7: c0000005
P8: 000e21d1
P9:
P10:
Attached files:
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WER4A26.tmp.mdmp
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WER4A95.tmp.WERInternalMetadata.xml
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WER4AC5.tmp.xml
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WER4AC7.tmp.csv
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WER4AE7.tmp.txt
These files may be available here:
\\?\C:\ProgramData\Microsoft\Windows\WER\ReportArchive\AppCrash_ConsoleApp1.exe_205af63bf864821f555164beb5b9ebbca41c_870f1ae3_efc6993a-ca59-44d7-95e8-0749fcc35e9a
Analysis symbol:
Rechecking for solution: 0
Report Id: b790601d-4a9d-470c-b8a9-144beabee7ab
Report Status: 268435456
Hashed bucket: f856d214730f94afbf949057f6154d3e
Cab Guid: 0
비록 이번에도 "Heap"이 깨지긴 했지만 말 그대로 .NET CLR의 관리 Heap이기 때문에 Native Heap이 깨졌을 때처럼
FaultTolerantHeap과 같은 대우는 받지 못합니다.
어쨌든 이런 식으로 관리 힙이 깨져
비정상 종료하는 경우 메모리 덤프를 받으면, 다음과 같은 콜 스택을 확인할 수 있습니다.
clr!WKS::gc_heap::mark_object_simple+60
clr!WKS::GCHeap::Promote+a8
clr!GcEnumObject+37
clr!EECodeManager::EnumGcRefs+840
clr!GcStackCrawlCallBack+139
clr!Thread::StackWalkFramesEx+92
clr!Thread::StackWalkFrames+9d
clr!standalone::ScanStackRoots+43
clr!GCToEEInterface::GcScanRoots+db
clr!WKS::gc_heap::mark_phase+170
clr!WKS::gc_heap::gc1+ae
clr!WKS::gc_heap::garbage_collect+367
clr!WKS::GCHeap::GarbageCollectGeneration+1bd
clr!WKS::GCHeap::GarbageCollectTry+71
clr!WKS::GCHeap::GarbageCollect+ac
clr!GCInterface::Collect+69
mscorlib_ni!System.GC.Collect()+31
mscorlib_ni!System.GC.Collect()+31
Program.Main(System.String[])+188
clr!CallDescrWorkerInternal+34
clr!CallDescrWorkerWithHandler+6b
clr!MethodDescCallSite::CallTargetWorker+16a
clr!RunMain+1b3
clr!Assembly::ExecuteMainMethod+f7
clr!SystemDomain::ExecuteMainMethod+5ef
clr!ExecuteEXE+4c
clr!_CorExeMainInternal+dc
clr!_CorExeMain+4d
mscoreei!_CorExeMain+d6
mscoree!ShellShim__CorExeMain+9e
mscoree!_CorExeMain_Exported+8
kernel32!BaseThreadInitThunk+19
ntdll!__RtlUserThreadStart+2f
ntdll!_RtlUserThreadStart+1b
// ServerGC의 경우
clr!SVR::gc_heap::plan_phase+12bb
clr!SVR::gc_heap::gc1+c3
clr!SVR::gc_heap::garbage_collect+f0
clr!SVR::gc_heap::gc_thread_function+74
clr!SVR::gc_heap::gc_thread_stub+9a
clr!<lambda_5d4b8dc0ce3eb05b864e38505727a1c1>::<lambda_invoker_cdecl>+51
kernel32!BaseThreadInitThunk+14
ntdll!RtlUserThreadStart+21
참고로 이때의 예외 유형은 System.ExecutionEngineException입니다.
아무튼, 테스트를 통해 알 수 있겠지만 관리 힙의 데이터 주소를 native 측에 전달하거나, 또는 unsafe/fixed로 다룰 때는 그만큼 주의를 기울여야 합니다. 위의 사례와는 반대로 해당 데이터를 넘어서 쓰게 되면 다음 객체의 헤더를 덮어쓰게 될 것이고 그럴 때에도 GC는 헤어날 수 없는 충격에 빠지게 될 테니까요. ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]