Files
UnrealEngine/Engine/Source/Developer/CrashDebugHelper/Private/Windows/WindowsPlatformStackWalkExt.cpp
2025-05-18 13:04:45 +08:00

574 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "WindowsPlatformStackWalkExt.h"
#include "CrashDebugHelper.h"
#include "CrashDebugHelperPrivate.h"
#include "GenericPlatform/GenericPlatformStackWalk.h"
#include "GenericPlatform/GenericPlatformCrashContext.h"
#include "Misc/Parse.h"
#include "Misc/CommandLine.h"
#include "Misc/MemStack.h"
#include "Misc/Paths.h"
#include "HAL/PlatformProcess.h"
#include "HAL/FileManager.h"
#include "Windows/AllowWindowsPlatformTypes.h"
#include "dbgeng.h"
#include <DbgHelp.h>
#include "Windows/HideWindowsPlatformTypes.h"
#pragma comment( lib, "dbgeng.lib" )
// Use _NT_SYMBOL_PATH for non development builds. We don't want shipping crash reporter to try to
// access build servers for example.
#if !UE_BUILD_SHIPPING
#define ALLOW_UNREAL_ACCESS_TO_NT_SYMBOL_PATH 1
#else
#define ALLOW_UNREAL_ACCESS_TO_NT_SYMBOL_PATH 0
#endif
static IDebugClient5* Client = NULL;
static IDebugControl4* Control = NULL;
static IDebugSymbols3* Symbol = NULL;
static IDebugAdvanced3* Advanced = NULL;
FWindowsPlatformStackWalkExt::FWindowsPlatformStackWalkExt( FCrashInfo& InCrashInfo )
: CrashInfo( InCrashInfo )
{}
FWindowsPlatformStackWalkExt::~FWindowsPlatformStackWalkExt()
{
ShutdownStackWalking();
}
bool FWindowsPlatformStackWalkExt::InitStackWalking()
{
if (!FWindowsPlatformMisc::CoInitialize())
{
return false;
}
check( DebugCreate( __uuidof( IDebugClient5 ), ( void** )&Client ) == S_OK );
check( Client->QueryInterface( __uuidof( IDebugControl4 ), ( void** )&Control ) == S_OK );
check( Client->QueryInterface( __uuidof( IDebugSymbols3 ), ( void** )&Symbol ) == S_OK );
check( Client->QueryInterface( __uuidof( IDebugAdvanced3 ), ( void** )&Advanced ) == S_OK );
return true;
}
void FWindowsPlatformStackWalkExt::ShutdownStackWalking()
{
Advanced->Release();
Symbol->Release();
Control->Release();
Client->Release();
FWindowsPlatformMisc::CoUninitialize();
}
/**
*
*/
void FWindowsPlatformStackWalkExt::InitSymbols()
{
ULONG SymOpts = 0;
// Load line information
SymOpts |= SYMOPT_LOAD_LINES;
SymOpts |= SYMOPT_OMAP_FIND_NEAREST;
// Fail if a critical error is encountered
SymOpts |= SYMOPT_FAIL_CRITICAL_ERRORS;
// Always load immediately; no deferred loading
SymOpts |= SYMOPT_DEFERRED_LOADS;
// Require an exact symbol match
SymOpts |= SYMOPT_EXACT_SYMBOLS;
// This option allows for undecorated names to be handled by the symbol engine.
SymOpts |= SYMOPT_UNDNAME;
#ifdef _DEBUG
// Disable by default as it can be very spammy/slow. Turn it on if you are debugging symbol look-up!
SymOpts |= SYMOPT_DEBUG;
#endif // _DEBUG
Symbol->SetSymbolOptions( SymOpts );
}
FString FWindowsPlatformStackWalkExt::ExtractRelativePath( const TCHAR* BaseName, TCHAR* FullName )
{
FString FullPath = FString( FullName ).ToLower();
FullPath.ReplaceInline( TEXT( "\\" ), TEXT( "/" ) );
TArray<FString> Components;
int32 Count = FullPath.ParseIntoArray( Components, TEXT( "/" ), true );
FullPath = TEXT( "" );
for( int32 Index = 0; Index < Count; Index++ )
{
if( Components[Index] == BaseName )
{
if( Index > 0 )
{
for( int32 Inner = Index - 1; Inner < Count; Inner++ )
{
FullPath += Components[Inner];
if( Inner < Count - 1 )
{
FullPath += TEXT( "/" );
}
}
}
break;
}
}
return FullPath;
}
void FWindowsPlatformStackWalkExt::GetExeFileVersionAndModuleList( FCrashModuleInfo& out_ExeFileVersion )
{
// The the number of loaded modules
ULONG LoadedModuleCount = 0;
ULONG UnloadedModuleCount = 0;
Symbol->GetNumberModules( &LoadedModuleCount, &UnloadedModuleCount );
UE_LOG( LogCrashDebugHelper, Log, TEXT( "Modules loaded: %i, unloaded: %i" ), LoadedModuleCount, UnloadedModuleCount );
// Find the relative names of all the modules so we know which files to sync
int ExecutableIndex = -1;
for( uint32 ModuleIndex = 0; ModuleIndex < LoadedModuleCount; ModuleIndex++ )
{
ULONG64 ModuleBase = 0;
Symbol->GetModuleByIndex( ModuleIndex, &ModuleBase );
// Get the full path of the module name
TCHAR ModuleName[MAX_PATH] = {0};
Symbol->GetModuleNameStringWide( DEBUG_MODNAME_IMAGE, ModuleIndex, ModuleBase, ModuleName, MAX_PATH, NULL );
const FString RelativeModuleName = ExtractRelativePath( TEXT( "binaries" ), ModuleName );
// Get the exe, which we extract the version number, so we know what label to sync to
if (RelativeModuleName.Len() > 0 && RelativeModuleName.EndsWith( TEXT( ".exe" ) ))
{
ExecutableIndex = ModuleIndex;
}
// Add only modules in Binaries folders
if (RelativeModuleName.Len() > 0)
{
CrashInfo.ModuleNames.Add( ModuleName );
}
else
{
SystemModuleNames.Add(ModuleName);
}
}
// Get the executable version info.
if( ExecutableIndex > -1 )
{
VS_FIXEDFILEINFO VersionInfo = {0};
Symbol->GetModuleVersionInformationWide( ExecutableIndex, 0, TEXT( "\\" ), &VersionInfo, sizeof( VS_FIXEDFILEINFO ), NULL );
out_ExeFileVersion.Major = HIWORD( VersionInfo.dwProductVersionMS );
out_ExeFileVersion.Minor = LOWORD( VersionInfo.dwProductVersionMS );
out_ExeFileVersion.Patch = HIWORD( VersionInfo.dwProductVersionLS );
}
else
{
UE_LOG( LogCrashDebugHelper, Warning, TEXT( "Unable to locate the executable" ) );
}
}
void FWindowsPlatformStackWalkExt::SetSymbolPathsFromModules()
{
FString CombinedPath = TEXT( "" );
{
// Use symbol cache from command line
FString DebugSymbols;
if (FParse::Value(FCommandLine::Get(), TEXT("DebugSymbols="), DebugSymbols))
{
CombinedPath += TEXT("SRV*");
CombinedPath += DebugSymbols;
CombinedPath += TEXT(";");
}
// Iterate over all loaded modules.
TSet<FString> SymbolPaths;
for (const FString& Filename : CrashInfo.ModuleNames)
{
const FString Path = FPaths::GetPath(Filename);
if (Path.Len() > 0)
{
SymbolPaths.Add(Path);
}
}
for (const FString& SymbolPath : SymbolPaths)
{
CombinedPath += SymbolPath;
CombinedPath += TEXT(";");
}
#if ALLOW_UNREAL_ACCESS_TO_NT_SYMBOL_PATH
FString SymbolPathEnvironmentVariable = FPlatformMisc::GetEnvironmentVariable(L"_NT_SYMBOL_PATH");
if (!SymbolPathEnvironmentVariable.IsEmpty())
{
CombinedPath += SymbolPathEnvironmentVariable;
CombinedPath += ";";
}
#endif
}
TSet<FString> ImagePathSet;
FString ImagePathnames(CombinedPath);
for (const FString& SysImagePathname : SystemModuleNames)
{
FString Path = FPaths::GetPath(SysImagePathname);
if (Path.Len() > 0)
{
ImagePathSet.Add(Path);
}
}
for (const FString& SysImgPath : ImagePathSet)
{
ImagePathnames += SysImgPath;
ImagePathnames += TEXT(";");
}
// Set the symbol path
Symbol->SetImagePathWide( *ImagePathnames );
Symbol->SetSymbolPathWide( *CombinedPath );
// Add in syncing of the Microsoft symbol servers if requested
if( FParse::Param( FCommandLine::Get(), TEXT( "SyncMicrosoftSymbols" ) ) )
{
FString BinariesDir = FString(FPlatformProcess::BaseDir());
if ( !FPaths::FileExists( FPaths::Combine(*BinariesDir, TEXT("symsrv.dll")) ) )
{
UE_LOG( LogCrashDebugHelper, Warning, TEXT( "Error: symsrv.dll was not detected in: %s. Microsoft symbols will not be downloaded!" ), *BinariesDir );
}
if ( !FPaths::FileExists( FPaths::Combine(*BinariesDir, TEXT("symsrv.yes")) ) )
{
UE_LOG( LogCrashDebugHelper, Warning, TEXT( "symsrv.yes was not detected in: %s. This will cause a popup to confirm the license." ), *BinariesDir );
}
if ( !FPaths::FileExists( FPaths::Combine(*BinariesDir, TEXT("dbghelp.dll")) ) )
{
UE_LOG( LogCrashDebugHelper, Warning, TEXT( "Error: dbghelp.dll was not detected in: %s. Microsoft symbols will not be downloaded!" ), *BinariesDir );
}
Symbol->AppendImagePathWide( TEXT( "SRV*..\\..\\Intermediate\\SymbolCache*http://msdl.microsoft.com/download/symbols" ) );
Symbol->AppendSymbolPathWide( TEXT( "SRV*..\\..\\Intermediate\\SymbolCache*http://msdl.microsoft.com/download/symbols" ) );
}
TCHAR SymbolPath[16384 * 2] = { 0 }; // On large projects, we usually need more than 16K because the list of module is long.
Symbol->GetSymbolPathWide( SymbolPath, UE_ARRAY_COUNT(SymbolPath), NULL );
TArray<FString> SymbolPaths;
FString( SymbolPath ).ParseIntoArray(SymbolPaths, TEXT(";"), true );
UE_LOG( LogCrashDebugHelper, Log, TEXT( "Symbol paths" ) );
for( const auto& It : SymbolPaths )
{
UE_LOG( LogCrashDebugHelper, Log, TEXT( " %s" ), *It );
}
Symbol->GetImagePathWide( SymbolPath, UE_ARRAY_COUNT( SymbolPath ), NULL );
TArray<FString> ImagePaths;
FString( SymbolPath ).ParseIntoArray( ImagePaths, TEXT( ";" ), true );
UE_LOG( LogCrashDebugHelper, Log, TEXT( "Image paths" ) );
for( const auto& It : ImagePaths )
{
UE_LOG( LogCrashDebugHelper, Log, TEXT( " %s" ), *It );
}
}
/** Helper class used to sort modules by name. */
class FSortModulesByName
{
public:
bool operator()( const FCrashModuleInfo& A, const FCrashModuleInfo& B ) const
{
if( A.Extension == TEXT( ".exe" ) )
{
return true;
}
if( A.Extension != B.Extension )
{
// Sort any exe modules to the top
return ( A.Extension > B.Extension );
}
// alphabetise all the dlls
return ( A.Name < B.Name );
}
};
void FWindowsPlatformStackWalkExt::GetModuleInfoDetailed()
{
// The the number of loaded modules
ULONG LoadedModuleCount = 0;
ULONG UnloadedModuleCount = 0;
Symbol->GetNumberModules( &LoadedModuleCount, &UnloadedModuleCount );
CrashInfo.Modules.Empty( LoadedModuleCount );
for( uint32 ModuleIndex = 0; ModuleIndex < LoadedModuleCount; ModuleIndex++ )
{
FCrashModuleInfo* CrashModule = new ( CrashInfo.Modules ) FCrashModuleInfo();
ULONG64 ModuleBase = 0;
Symbol->GetModuleByIndex( ModuleIndex, &ModuleBase );
// Get the full path of the module name
TCHAR ModuleName[MAX_PATH] = { 0 };
Symbol->GetModuleNameStringWide( DEBUG_MODNAME_IMAGE, ModuleIndex, ModuleBase, ModuleName, MAX_PATH, NULL );
FString RelativeModuleName = ExtractRelativePath( TEXT( "binaries" ), ModuleName );
if( RelativeModuleName.Len() > 0 )
{
CrashModule->Name = RelativeModuleName;
}
else
{
CrashModule->Name = ModuleName;
}
CrashModule->Extension = CrashModule->Name.Right( 4 ).ToLower();
CrashModule->BaseOfImage = ModuleBase;
DEBUG_MODULE_PARAMETERS ModuleParameters = { 0 };
Symbol->GetModuleParameters( 1, NULL, ModuleIndex, &ModuleParameters );
CrashModule->SizeOfImage = ModuleParameters.Size;
VS_FIXEDFILEINFO VersionInfo = { 0 };
Symbol->GetModuleVersionInformationWide( ModuleIndex, ModuleBase, TEXT( "\\" ), &VersionInfo, sizeof( VS_FIXEDFILEINFO ), NULL );
CrashModule->Major = HIWORD( VersionInfo.dwProductVersionMS );
CrashModule->Minor = LOWORD( VersionInfo.dwProductVersionMS );
CrashModule->Patch = HIWORD( VersionInfo.dwProductVersionLS );
CrashModule->Revision = LOWORD( VersionInfo.dwProductVersionLS );
// NOTE: The function below was commented because in some occasions (unknown reason), it triggered a search for modules on the user disk,
// scanning all projects assets for each module, taking an unacceptable long time in very large projects. This could be observed with
// SysInternal ProcessMonitor. The original intent for calling ReloadWide() is unknown, but normally, this function is meant
// to control when the modules are reloaded, for example if the .pdb were about to be deleted, we would want to reload and cache the
// symbols before losing them. Since we configured the debug engine with SYMOPT_DEFERRED_LOADS to only load symbols if needed,
// reloading them all here looks counter-productive. Commenting it did not show any obvious side effects other than fixing the
// very long module search time issue.
// Ensure all the images are synced - need the full path here
//Symbol->ReloadWide( *CrashModule->Name );
}
CrashInfo.Modules.Sort( FSortModulesByName() );
}
bool FWindowsPlatformStackWalkExt::IsOffsetWithinModules( uint64 Offset )
{
for( int ModuleIndex = 0; ModuleIndex < CrashInfo.Modules.Num(); ModuleIndex++ )
{
FCrashModuleInfo& CrashModule = CrashInfo.Modules[ModuleIndex];
if( Offset >= CrashModule.BaseOfImage && Offset < CrashModule.BaseOfImage + CrashModule.SizeOfImage )
{
return true;
}
}
return false;
}
void FWindowsPlatformStackWalkExt::GetSystemInfo()
{
FCrashSystemInfo& SystemInfo = CrashInfo.SystemInfo;
ULONG PlatformId = 0;
ULONG Major = 0;
ULONG Minor = 0;
ULONG Build = 0;
ULONG Revision = 0;
Control->GetSystemVersionValues( &PlatformId, &Major, &Minor, &Build, &Revision );
SystemInfo.OSMajor = ( uint16 )Major;
SystemInfo.OSMinor = ( uint16 )Minor;
SystemInfo.OSBuild = ( uint16 )Build;
SystemInfo.OSRevision = ( uint16 )Revision;
ULONG ProcessorType = 0;
Control->GetActualProcessorType( &ProcessorType );
switch( ProcessorType )
{
case IMAGE_FILE_MACHINE_I386:
// x86
CrashInfo.SystemInfo.ProcessorArchitecture = PA_X86;
break;
case IMAGE_FILE_MACHINE_ARM:
// ARM
CrashInfo.SystemInfo.ProcessorArchitecture = PA_ARM;
break;
case IMAGE_FILE_MACHINE_AMD64:
// x64
CrashInfo.SystemInfo.ProcessorArchitecture = PA_X64;
break;
default:
break;
};
ULONG ProcessorCount = 0;
Control->GetNumberProcessors( &ProcessorCount );
SystemInfo.ProcessorCount = ProcessorCount;
}
void FWindowsPlatformStackWalkExt::GetExceptionInfo()
{
FCrashExceptionInfo& Exception = CrashInfo.Exception;
ULONG ExceptionType = 0;
ULONG ProcessID = 0;
ULONG ThreadId = 0;
TCHAR Description[MAX_PATH] = { 0 };
Control->GetLastEventInformationWide( &ExceptionType, &ProcessID, &ThreadId, NULL, 0, NULL, Description, UE_ARRAY_COUNT( Description ), NULL );
Exception.Code = ExceptionType;
Exception.ProcessId = ProcessID;
Exception.ThreadId = ThreadId;
Exception.ExceptionString = Description;
}
void FWindowsPlatformStackWalkExt::GetCallstacks()
{
const int32 MAX_NAME_LENGTH = FProgramCounterSymbolInfo::MAX_NAME_LENGTH;
FCrashExceptionInfo& Exception = CrashInfo.Exception;
FMemMark Mark(FMemStack::Get());
//const float int int32 FString
const int32 ContextSize = 4096;
byte* Context = new(FMemStack::Get()) byte[ContextSize];
ULONG DebugEvent = 0;
ULONG ProcessID = 0;
ULONG ThreadID = 0;
ULONG ContextUsed = 0;
// Get the context of the crashed thread
HRESULT hr = Control->GetStoredEventInformation(&DebugEvent, &ProcessID, &ThreadID, Context, ContextSize, &ContextUsed, NULL, 0, 0);
if( FAILED(hr) )
{
return;
}
// Some magic number checks
if( ContextUsed == 716 )
{
UE_LOG( LogCrashDebugHelper, Log, TEXT( "Context size matches x86 sizeof( CONTEXT )" ) );
}
else if( ContextUsed == 1232 )
{
UE_LOG( LogCrashDebugHelper, Log, TEXT( "Context size matches x64 sizeof( CONTEXT )" ) );
}
// Get the entire stack trace
const uint32 MaxFrames = 8192;
const uint32 MaxFramesSize = MaxFrames * ContextUsed;
DEBUG_STACK_FRAME* StackFrames = new(FMemStack::Get()) DEBUG_STACK_FRAME[MaxFrames];
ULONG Count = 0;
bool bFoundSourceFile = false;
void* ContextData = FMemStack::Get().PushBytes( MaxFramesSize, 0 );
FMemory::Memzero( ContextData, MaxFramesSize );
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);
int32 AssertOrEnsureIndex = -1;
for( uint32 StackIndex = 0; StackIndex < Count; StackIndex++ )
{
const uint64 Offset = StackFrames[StackIndex].InstructionOffset;
if( IsOffsetWithinModules( Offset ) )
{
// Get the module, function, and offset
uint64 Displacement = 0;
TCHAR NameByOffset[MAX_PATH] = {0};
Symbol->GetNameByOffsetWide( Offset, NameByOffset, ARRAYSIZE( NameByOffset ) - 1, NULL, &Displacement );
FString ModuleAndFunction = NameByOffset;
// Don't care about any more entries higher than this
if (ModuleAndFunction.Contains( TEXT( "tmainCRTStartup" ) ) || ModuleAndFunction.Contains( TEXT( "FRunnableThreadWin::GuardedRun" ) ))
{
break;
}
// Look for source file name and line number
TCHAR SourceName[MAX_PATH] = { 0 };
ULONG LineNumber = 0;
Symbol->GetLineByOffsetWide( Offset, &LineNumber, SourceName, ARRAYSIZE( SourceName ) - 1, NULL, NULL );
// Remember the top of the stack to locate in the source file
if( !bFoundSourceFile && FCString::Strlen( SourceName ) > 0 && LineNumber > 0 )
{
CrashInfo.SourceFile = ExtractRelativePath( TEXT( "source" ), SourceName );
CrashInfo.SourceLineNumber = LineNumber;
bFoundSourceFile = true;
}
FString ModuleName;
FString FunctionName;
// According to MSDN, the symbol name will include an ! if the function name could be discovered, delimiting it from the module name
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff547186(v=vs.85).aspx
if( ModuleAndFunction.Contains( TEXT( "!" ) ) )
{
ModuleAndFunction.Split( TEXT( "!" ), &ModuleName, &FunctionName );
FunctionName += TEXT( "()" );
}
else
{
ModuleName = ModuleAndFunction;
}
// FString InModuleName, FString InFunctionName, FString InFilename, uint32 InLineNumber, uint64 InSymbolDisplacement, uint64 InOffsetInModule, uint64 InProgramCounter
FProgramCounterSymbolInfoEx SymbolInfo( ModuleName, FunctionName, SourceName, LineNumber, Displacement, Offset, 0 );
FString GenericFormattedCallstackLine;
FGenericPlatformStackWalk::SymbolInfoToHumanReadableStringEx( SymbolInfo, GenericFormattedCallstackLine );
Exception.CallStackString.Add( GenericFormattedCallstackLine );
UE_LOG( LogCrashDebugHelper, Log, TEXT( "%3u: %s" ), StackIndex, *GenericFormattedCallstackLine );
}
}
}
bool FWindowsPlatformStackWalkExt::OpenDumpFile( const FString& InCrashDumpFilename )
{
const bool bFound = IFileManager::Get().FileSize( *InCrashDumpFilename ) != INDEX_NONE;
if( !bFound )
{
UE_LOG( LogCrashDebugHelper, Warning, TEXT( "Failed to find minidump file: %s" ), *InCrashDumpFilename );
return false;
}
HRESULT hr = Client->OpenDumpFileWide(*InCrashDumpFilename, NULL);
if(FAILED(hr) )
{
UE_LOG( LogCrashDebugHelper, Warning, TEXT( "Failed to open minidump file: %s" ), *InCrashDumpFilename );
return false;
}
if( Control->WaitForEvent( 0, INFINITE ) != S_OK )
{
UE_LOG( LogCrashDebugHelper, Warning, TEXT( "Failed while waiting for minidump to load: %s" ), *InCrashDumpFilename );
return false;
}
UE_LOG( LogCrashDebugHelper, Log, TEXT( "Successfully opened minidump: %s" ), *InCrashDumpFilename );
return true;
}