はじめに
この記事はUE4AdventCalendar6日目の記事です
今回はUE4の機能でありながら、ほぼ語られることのないにもかかわらず、結構色々なことをやっているクラッシュレポーターについて解説したいと思います
開発環境
UE4.26系
Windows10
Visual Studio Community 2017
Crash Report Client(CRC)とは
Crash Report Client(以下CRC)とはUE4がなんらかの理由によりクラッシュすると出てくる親の顔よりもみたであろうウィンドウです
普段何気なくUE4を触っている人は無言でCloseボタンを押しているのではないのでしょうか?
CRCはUE4のリポジトリに属していながらもUE4自体とは全く別プロセスで動作するUE4でありながら、UE4そのものとは別に動作する不思議な立ち位置のプログラムになっています
クラッシュレポーターの主な機能は以下の通り
- クラッシュレポートの出力
- クラッシュレポートを指定のURLへ送信
- クラッシュレポートに追加メッセージなどをつける
ここまでだと「ふーん、そうか」くらいに思うのではないでしょうか
実はこれだけの機能にみえてその裏では色々行っています
それを理解するためにも、まずはCRCの全体像から掘り下げていきます
一般的にCRCを開発現場で使用したい際の基本的な使い方は本記事では言及しないので、サーバの送信先の変更方法などを知りたい場合は以下のSentry様のドキュメントを確認していただけると幸いです
CRC関連クラスの概要
CRCは大きく分けて三つのModuleで構成されている
- CrashReportClientモジュール
- CrashReportCoreモジュール
- CrashReportClientEditorモジュール
重要なのは「CrashReportCore」と「CrashReportClient」の2つ
CrashReportClient モジュール
「CrashReportClient」では主にCRCのウィンドウプロセス
CRCはゲームがクラッシュしたら呼ばれたら出てくるので当たり前ではあるが、ゲームとは全く別のプロセスで動作しており、UE4が持つ他のModuleと大きく異なる点はここです
このModuleは主に別プロセスで動作するクラッシュレポーターに関するプログラム群の集まりです
CrashReportCore モジュール
「CrashReportCore」はCrashReportClientモジュールとは異なり、ゲーム側のプロセスで動作し、同プロセス内のCrashthreadで使用されるプログラム群です
また、ここではCRC本体のプロセス呼び出しなどが行われます
他にもCRC本体側からファイルの入出力などをする場合このモジュールのプログラムを呼び出すこともあります
CrashReportClientEditor モジュール
EditorやPIEなどでゲームを動作させていた時にクラッシュした時に呼ばれるモジュール
実際の処理はCrashReportClientモジュールが担っているが、PrivateDefinitionsなどにEditorが起動したとわかるためのDefineを追加することで、CrashReportClientモジュール内で処理を分岐させています
実は知られていないCRCがやっていること
ここからが本題です
CRCは一見簡単に行っているようにみえるが、実はUE4ユーザが快適に開発業務を行うために様々なことを行っていますので、その一部をソースを追いながら解説していきます
UnrealEditorのクラッシュ情報収集
クラッシュ情報を収集するのは当たり前だと思いましたか?
いいえ、致命的なクラッシュ以外にもCRCは仕事をしてくれているのです
実はCRCはUnrealEditorが起動したときに同時に起動してbackgroundでUnrealEditorとプロセス間通信を行っています
その処理を追いかけるにはまずWindowsPlatformCrashContextというクラスを見てみましょう
Editor起動時に呼ばれるCRC
WindowsPlatformCrashContextクラスはCrashReportCoreモジュールのクラスで、主な役割はCRCのプロセスを呼び出す役目をになっています
Editorが起動した際にCRCを呼ぶための関数はFCrashReportingThreadのコンストラクタ内から呼ばれます
そしてその中で以下のようにLaunchCrashReportClient関数が呼び出されます
#if USE_CRASH_REPORTER_MONITOR
if (!FPlatformProperties::IsServerOnly())
{
CrashClientHandle = LaunchCrashReportClient(&CrashMonitorWritePipe, &CrashMonitorReadPipe, &CrashMonitorPid);
FMemory::Memzero(SharedContext);
}
#endif
そしてさらに深く探っていくと以下の処理に行きつきます
static FORCEINLINE bool CreatePipeWrite(void*& ReadPipe, void*& WritePipe)
{
SECURITY_ATTRIBUTES Attr = { sizeof(SECURITY_ATTRIBUTES), NULL, true };
if (!::CreatePipe(&ReadPipe, &WritePipe, &Attr, 0))
{
return false;
}
このCreatePipe関数はWinAPIの関数で、プロセス間通信を行うためのパイプ関数となります
また、LaunchCrashReportClient関数の最後のほうでCRCのプロセス呼び出しも確認できます
Handle = FPlatformProcess::CreateProc(
CrashReporterClientPath,
CrashReporterClientArgs,
true, false, false,
OutCrashReportClientProcessId, 0,
nullptr,
PipeChildInRead, //Pass this to allow inherit handles in child proc
nullptr);
これでCRCとUnrealEditor側がつながっていることがわかりました
Ensureマクロがヒットした時の情報を収集している
CRCとUnrealEditorがパイプでつながっていることを前述しました
ここではそれをどう活かしているのかの紹介をします
WindowsPlatformCrashContext.cpp1228行目OnEnsure関数がそれにあたります
OnEnsure関数はReportEnsureUsingCrashReportClient関数内から呼び出されます
#if !PLATFORM_SEH_EXCEPTIONS_DISABLED
__except (ReportEnsureUsingCrashReportClient( GetExceptionInformation(), NumStackFramesToIgnore, ErrorMessage, IsInteractiveEnsureMode() ? EErrorReportUI::ShowDialog : EErrorReportUI::ReportInUnattendedMode))
CA_SUPPRESS(6322)
{
}
#endif
これはEnsureで呼ばれる時に限らないのですが、CRCが呼び出される関数はパイプがつながっている場合とそうでない場合で、二つの関数に分岐します
・パイプがつながっている場合
⇒ReportCrashForMonitor関数
・パイプがつながっていない場合
⇒ReportCrashUsingCrashReportClient関数
Xmlの書き換え
CRCが作成するXMLは作成された瞬間とレポート送信時でかなり大きく動的に書き換えられています
それをわかりやすくするために生成直後のXMLをブレイクポイントで止めて別途コピーし、CRCが立ち上がった後と比較しました
大きく内容が変化していることがわかります
その中でも大きな役割を占める部分を紹介します
Legacy callstack の生成
LegacyCallstackとはなんだ?と思った方も多いのではないでしょうか
ここでいうLegacyCallstackとはつまり、人間が読むことができるCallstackのことです
このようなCallstackはよく見ているのではないでしょうか
Fatal error: [File:C:/UnrealEngineSource/UnrealEngine/Engine/Source/Runtime/Engine/Private/UnrealEngine.cpp] [Line: 8714] Crashing the gamethread at your request
UE4Editor_Engine!UEngine::PerformError() [C:\UnrealEngineSource\UnrealEngine\Engine\Source\Runtime\Engine\Private\UnrealEngine.cpp:8754]
UE4Editor_Engine!UEngine::HandleDebugCommand() [C:\UnrealEngineSource\UnrealEngine\Engine\Source\Runtime\Engine\Private\UnrealEngine.cpp:6751]
UE4Editor_Engine!UEngine::Exec() [C:\UnrealEngineSource\UnrealEngine\Engine\Source\Runtime\Engine\Private\UnrealEngine.cpp:4328]
作成直後のXMLにはPortableCallstackという、DLL名やアドレスが書かれた人間では読むことは難しいCallstackのみしか書かれていません
これではエラーレポートを受けてもどうすることもできずに困ってしまいます(以下参照)
<PCallStack>ntdll 0x00007ffdc1170000 + 9cdf4 KERNELBASE 0x00007ffdbeb80000 + 21a5e UE4Editor-Core 0x00007ffd49000000 + 5ed1ed UE4Editor 0x00007ff6d9420000 + 3b13f VCRUNTIME140 0x00007ffdafa20000 + e3e0 ntdll 0x00007ffdc1170000 + a20cf ntdll 0x00007ffdc1170000 + 51454 ntdll 0x00007ffdc1170000 + 511a5 KERNELBASE 0x00007ffdbeb80000 + 34f69 UE4Editor-Core 0x00007ffd49000000 + 5ed136 UE4Edi
このLegacyCallstackはFCrashDebugHelperWindows::CreateMinidumpDiagnosticReport関数で生成されています
// Get the build version and modules paths.
FCrashModuleInfo ExeFileVersion;
WindowsStackWalkExt.GetExeFileVersionAndModuleList(ExeFileVersion);
// Init Symbols
WindowsStackWalkExt.InitSymbols();
// Set the symbol path based on the loaded modules
WindowsStackWalkExt.SetSymbolPathsFromModules();
// Get all the info we should ever need about the modules
WindowsStackWalkExt.GetModuleInfoDetailed();
// Get info about the system that created the minidump
WindowsStackWalkExt.GetSystemInfo();
// Get all the thread info
WindowsStackWalkExt.GetThreadInfo();
// Get exception info
WindowsStackWalkExt.GetExceptionInfo();
// Get the callstacks for each thread
WindowsStackWalkExt.GetCallstacks();
この処理群はDebugSymbolファイル(pdb)を読み取って様々な処理を一括で行っています
Callstackの生成そのものはFWindowsPlatformStackWalkExt::GetCallstacks関数で行われていることが確認できます
GetCallstack関数で生成処理を追うことが出来る最後の処理は以下のようになっています
UE_LOG(LogCrashDebugHelper, Log, TEXT("Running GetContextStackTrace()"));
HRESULT HR = Control->GetContextStackTrace( Context, ContextUsed, StackFrames, MaxFrames, ContextData, MaxFramesSize, ContextUsed, &Count );
UE_LOG(LogCrashDebugHelper, Log, TEXT("GetContextStackTrace() got %d frames"), Count);
これ以上先はWindowsAPIを参照しており、UE4もDebugSymbolを読み取る処理全般は外部ライブラリを頼っていることがわかります
ちなみにそのライブラリは以下のライブラリなのでDebugSymbolを読み取る方法が気になる方は調べてみてください
また、この生成処理は非常に重い処理のためCRCのウィンドウが立ち上がった直後はまだ生成されていません
CRCのウィンドウが立ち上がるとCallstackの欄がLoadingしているかのようなアニメーションをしているのを覚えていますか?
あれはDbgeng.dllでCallstackの生成処理を非同期で行っているからです
よってLegacyCallstackの生成フローはXmlやLogファイルの生成フローとは全く異なります
具体的にはFCrashReportClientクラスのコンストラクタで別threadを作成し実行を完全に非同期にしています
if (!ErrorReport.TryReadDiagnosticsFile() && !FParse::Param( FCommandLine::Get(), TEXT( "no-local-diagnosis" ) ))
{
DiagnoseReportTask = new FAsyncTask<FDiagnoseReportWorker>( this );
DiagnoseReportTask->StartBackgroundTask();
StartTicker();
}
このFDiagnoseReportWorkerクラスがFCrashDebugHelperWindows::CreateMinidumpDiagnosticReport関数をbackgroundで呼び出すようになっていることが確認できます
if (!CrashDebugHelper->CreateMinidumpDiagnosticReport(ReportDirectory / DumpFilename))
{
return LOCTEXT("NoDebuggingSymbols", "You do not have any debugging symbols required to display the callstack for this crash.");
}
#さいごに
あまり知られていないCrashRportClientについてまとめてみました
正直、まだまだ紹介していない機能や実は行っているすごい処理はたくさんありますが、キリがないので一旦これで終わりにしたいと思います
CRCはEpicGames内でもあまり詳しい人は少ないのか、ほとんど話題に上がることは少ないですが興味のある方はCRC沼にハマってみてはいかがでしょうか