// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "SymsLibDumpLog.h" #include "LaunchEngineLoop.h" // GEngineLoop #include "Algo/ForEach.h" #include "Async/MappedFileHandle.h" #include "Async/ParallelFor.h" #include "HAL/PlatformFileManager.h" #include "Hash/CityHash.h" #include "Serialization/Archive.h" #include "Templates/UniquePtr.h" #include "RequiredProgramMainCPPInclude.h" // required for ue programs #include #include "symslib.h" // Only HOST platforms will be running this, which Linux/Mac need/support this type of demangling #define SUPPORTS_CXXABI_DEMANGLE PLATFORM_LINUX || PLATFORM_MAC #if SUPPORTS_CXXABI_DEMANGLE #include // for demangling #endif DEFINE_LOG_CATEGORY(LogSymsLibDump); IMPLEMENT_APPLICATION(SymsLibDump, "SymsLibDump"); namespace { // On the rare chance we dont have a proper name parsed from the dwarf debug we will fallback to this const ANSICHAR* UnknownName = ""; struct FAutoMappedFile { TUniquePtr Handle; TUniquePtr Region; bool Load(const TCHAR* FileName) { FOpenMappedResult Result = FPlatformFileManager::Get().GetPlatformFile().OpenMappedEx(FileName); if (Result.HasValue()) { Handle = Result.StealValue(); Region.Reset(Handle->MapRegion(0, Handle->GetFileSize())); } else { Handle.Reset(); Region.Reset(); } return Region.IsValid(); } SYMS_String8 GetData() const { if (Region.IsValid()) { return syms_str8((SYMS_U8*)Region->GetMappedPtr(), Region->GetMappedSize()); } return syms_str8(nullptr, 0); } }; // cxa_demangle allocates enough room with malloc if it can demangle. If we we must call free on that pointer ANSICHAR* Demangle(const ANSICHAR* MangledName) { #if SUPPORTS_CXXABI_DEMANGLE return abi::__cxa_demangle(MangledName, nullptr, nullptr, nullptr); #endif return nullptr; } struct FSymsLine { uint64 Address; uint64 Size; uint32 Line; uint32 FileId; }; struct FSymsUnit { SYMS_SpatialMap1D ProcMap; SYMS_String8Array FileTable; SYMS_LineTable LineTable; SYMS_SpatialMap1D LineMap; SYMS_String8 CompileDir; }; struct FSymsSymbol { const ANSICHAR* Name; uint64 Address; uint64 Size; // Info that will be fill in later to to associate line info with a function FSymsUnit* LineUnit; uint64 LineSeqIndex; }; struct FSymsInstance { TArray Arenas; TArray Units; SYMS_SpatialMap1D UnitMap; uint64 DefaultBase; }; struct FSymsExported { FString Name; uint64 Address; }; // need to store our proper FullFile path (with the CompileDir) + the GlobalIndex for this file struct FullFileId { FString FullFilePath; uint64 GlobalIndex; }; bool LoadBinary(const TCHAR* BinaryPath, SYMS_Arena* Arena, SYMS_ParseBundle& Bundle, FAutoMappedFile& BinaryFile) { if (!BinaryFile.Load(BinaryPath)) { UE_LOG(LogSymsLibDump, Error, TEXT("Failed to load '%s' binary file"), BinaryPath); return false; } SYMS_FileAccel* Accel = syms_file_accel_from_data(Arena, BinaryFile.GetData()); SYMS_BinAccel* BinAccel = syms_bin_accel_from_file(Arena, BinaryFile.GetData(), Accel); if (!syms_accel_is_good(BinAccel)) { UE_LOG(LogSymsLibDump, Error, TEXT("Cannot parse '%s' binary file"), BinaryPath); return false; } Bundle.bin_data = BinaryFile.GetData(); Bundle.bin = BinAccel; return true; } bool LoadDebug(const TCHAR* BinaryPath, SYMS_Arena* Arena, SYMS_ParseBundle& Bundle, const FAutoMappedFile& BinaryFile, FAutoMappedFile& DebugFile) { if (syms_bin_is_dbg(Bundle.bin)) { // binary has debug info built-in (like dwarf file) Bundle.dbg = syms_dbg_accel_from_bin(Arena, BinaryFile.GetData(), Bundle.bin); Bundle.dbg_data = Bundle.bin_data; return true; } // we're loading extra file (pdb for exe) SYMS_ExtFileList List = syms_ext_file_list_from_bin(Arena, BinaryFile.GetData(), Bundle.bin); if (!List.first) { UE_LOG(LogSymsLibDump, Error, TEXT("Binary file '%s' built without debug info"), BinaryPath); return false; } // TODO make a fancy search path setup where we try to look for other place *besides* right next to the binary FString DebugFilePath = FPaths::GetPath(FString(BinaryPath)) / FString(UTF8_TO_TCHAR(List.first->ext_file.file_name.str)); if (!DebugFile.Load(*DebugFilePath)) { UE_LOG(LogSymsLibDump, Error, TEXT("Failed to load debug file '%s'"), *DebugFilePath); return false; } SYMS_FileAccel* Accel = syms_file_accel_from_data(Arena, DebugFile.GetData()); SYMS_BinAccel* BinAccel = syms_bin_accel_from_file(Arena, DebugFile.GetData(), Accel); SYMS_DbgAccel* DbgAccel = syms_dbg_accel_from_bin(Arena, DebugFile.GetData(), BinAccel); if (!syms_accel_is_good(DbgAccel)) { UE_LOG(LogSymsLibDump, Error, TEXT("Cannot parse '%s' debug file"), *DebugFilePath); return false; } Bundle.dbg = DbgAccel; Bundle.dbg_data = DebugFile.GetData(); return true; } // Most of this was taken from SymslibResolver.cpp bool ParseDebugInfo(SYMS_Group* Group, SYMS_ParseBundle& Bundle, FSymsInstance& OutInstance) { // init group with bundle syms_set_lane(0); syms_group_init(Group, &Bundle); SYMS_U64 UnitCount = syms_group_unit_count(Group); OutInstance.Units.SetNum(UnitCount); // per-thread arena storage (at least one) int32 WorkerThreadCount = FMath::Max(1, FTaskGraphInterface::Get().GetNumWorkerThreads() + 1); OutInstance.Arenas.SetNum(WorkerThreadCount); // how many symbols are loaded std::atomic SymbolCount = 0; // parse debug info in multiple threads { uint32 LaneSlot = FPlatformTLS::AllocTlsSlot(); std::atomic LaneCount = 0; syms_group_begin_multilane(Group, WorkerThreadCount); ParallelFor(UnitCount, [&OutInstance, Group, LaneSlot, &LaneCount, &SymbolCount](uint32 Index) { SYMS_Arena* Arena = nullptr; uint32 LaneValue = uint32(reinterpret_cast(FPlatformTLS::GetTlsValue(LaneSlot))); if (LaneValue == 0) { // first time we are on this thread LaneValue = ++LaneCount; FPlatformTLS::SetTlsValue(LaneSlot, reinterpret_cast(intptr_t(LaneValue))); // syms lane index is 0-based uint32 LaneIndex = LaneValue - 1; syms_set_lane(LaneIndex); Arena = OutInstance.Arenas[LaneIndex] = syms_arena_alloc(); } else { uint32 LaneIndex = LaneValue - 1; syms_set_lane(LaneIndex); Arena = OutInstance.Arenas[LaneIndex]; } SYMS_ArenaTemp Scratch = syms_get_scratch(0, 0); SYMS_UnitID UnitID = static_cast(Index) + 1; // syms unit id's are 1-based FSymsUnit* Unit = &OutInstance.Units[Index]; SYMS_SpatialMap1D* ProcSpatialMap = syms_group_proc_map_from_uid(Group, UnitID); Unit->ProcMap = syms_spatial_map_1d_copy(Arena, ProcSpatialMap); SYMS_String8Array* FileTable = syms_group_file_table_from_uid_with_fallbacks(Group, UnitID); Unit->FileTable = syms_string_array_copy(Arena, 0, FileTable); SYMS_LineParseOut* LineParse = syms_group_line_parse_from_uid(Group, UnitID); Unit->LineTable = syms_line_table_with_indexes_from_parse(Arena, LineParse); SYMS_SpatialMap1D* LineSpatialMap = syms_group_line_sequence_map_from_uid(Group, UnitID); Unit->LineMap = syms_spatial_map_1d_copy(Arena, LineSpatialMap); SYMS_UnitNames UnitNames = syms_group_unit_names_from_uid(Arena, Group, UnitID); Unit->CompileDir = UnitNames.compile_dir; SYMS_UnitAccel* UnitAccel = syms_group_unit_from_uid(Group, UnitID); SYMS_IDMap ProcIdMap = syms_id_map_alloc(Scratch.arena, 4093); SYMS_SymbolIDArray* ProcArray = syms_group_proc_sid_array_from_uid(Group, UnitID); SYMS_U64 ProcCount = ProcArray->count; FSymsSymbol* Symbols = syms_push_array(Arena, FSymsSymbol, ProcCount); for (SYMS_U64 ProcIndex = 0; ProcIndex < ProcCount; ProcIndex++) { SYMS_SymbolID SymbolID = ProcArray->ids[ProcIndex]; SYMS_U64RangeArray RangeArray = syms_proc_vranges_from_sid(Arena, Group->dbg_data, Group->dbg, UnitAccel, SymbolID); if (RangeArray.count > 0) { Symbols[ProcIndex].Address = RangeArray.ranges[0].min; Symbols[ProcIndex].Size = RangeArray.ranges[0].max - RangeArray.ranges[0].min; } // TODO need new changes from RAD tools SYMS_String8 Name {nullptr, 0}; // = syms_linkage_name_from_sid(Arena, Group->dbg_data, Group->dbg, UnitAccel, SymbolID); // If we fail to find a linkage name fallback to name from sid. Some platforms, like Windows wont have a linkage name if (Name.size == 0) { Name = syms_group_symbol_name_from_sid(Arena, Group, UnitAccel, SymbolID); } // If we have an empty name for some reason lets give it at least a default if (Name.str && *Name.str == '\0') { Symbols[ProcIndex].Name = UnknownName; } else { Symbols[ProcIndex].Name = reinterpret_cast(Name.str); } Symbols[ProcIndex].LineUnit = nullptr; Symbols[ProcIndex].LineSeqIndex = ~0; syms_id_map_insert(Scratch.arena, &ProcIdMap, SymbolID, &Symbols[ProcIndex]); } SYMS_SpatialMap1D* ProcMap = &Unit->ProcMap; for (SYMS_SpatialMap1DRange* Range = ProcMap->ranges, *EndRange = ProcMap->ranges + ProcMap->count; Range < EndRange; Range++) { void* SymbolPtr = syms_id_map_ptr_from_u64(&ProcIdMap, Range->val); Range->val = SYMS_U64(reinterpret_cast(SymbolPtr)); } syms_release_scratch(Scratch); SymbolCount += ProcCount; }); syms_group_end_multilane(Group); FPlatformTLS::FreeTlsSlot(LaneSlot); } SYMS_Arena* Arena = OutInstance.Arenas[0]; OutInstance.UnitMap = syms_spatial_map_1d_copy(Arena, syms_group_unit_map(Group)); OutInstance.DefaultBase = syms_group_default_vbase(Group); syms_group_release(Group); return true; } // Go through and gather all PUBLIC functions which some *may* not be part of the debug file // This allows us to gather a few more symbols that wont have an info but still give us some // function names. Such as _start TArray GatherExportedFunctions(const SYMS_ExportArray& ExportArray) { TArray ExportedFunctions; for (int ExporteIndex = 0; ExporteIndex < ExportArray.count; ExporteIndex++) { // Demangle allocates with malloc if it demangles. If we we must call free on that pointer ANSICHAR* DemangledName = Demangle(reinterpret_cast(ExportArray.exports[ExporteIndex].name.str)); if (DemangledName != nullptr) { ExportedFunctions.Add(FSymsExported{FString(ANSI_TO_TCHAR(DemangledName)), ExportArray.exports[ExporteIndex].address}); free(DemangledName); } else { ExportedFunctions.Add(FSymsExported{ FString(ANSI_TO_TCHAR(reinterpret_cast(ExportArray.exports[ExporteIndex].name.str))), ExportArray.exports[ExporteIndex].address }); } } ExportedFunctions.Sort([] (const FSymsExported& Left, const FSymsExported& Right) { return Left.Address < Right.Address; }); return ExportedFunctions; } // TODO actually figure out the rest of the info needed for a MODULE record void OutputModule(const FSymsInstance& Instance, FArchive* Ar) { // TODO // TUtf8StringBuilder<512> OutputLine; FAnsiStringView Module = ANSITEXTVIEW("MODULE\n"); Ar->Serialize((void*)Module.GetData(), Module.Len()); } // Generate a mapping that is used to go over all the exiting file tables *per* compilation units and create a global // mapping, where 1 file, has a single unique global ID. symslib only has them per unit. So this mapping // will store files, and then later the value will be setup as the *global index* once all files are added TMap GenerateStringToIdMap(const FSymsInstance& Instance) { TMap StringToIDMapping; for (const FSymsUnit& Unit : Instance.Units) { for (int FileTableIdx = 0; FileTableIdx < Unit.FileTable.count; FileTableIdx++) { if (Unit.FileTable.strings) { FString File(UTF8_TO_TCHAR(Unit.FileTable.strings[FileTableIdx].str)); uint64 FileHash = CityHash64(reinterpret_cast(Unit.FileTable.strings[FileTableIdx].str), Unit.FileTable.strings[FileTableIdx].size); // hash file (before compiledir), use that as a key, then store a struct with: // - full file path // - global index // If we are not an absolute path lets combine with the compile dir for hopefully a full absolute path if (FPaths::IsRelative(File)) { File = FString(UTF8_TO_TCHAR(Unit.CompileDir.str)) / File; } // Need to make sure when looking up later to replace \ with / File.ReplaceInline(TEXT("\\"), TEXT("/"), ESearchCase::CaseSensitive); // FindOrAdd to prevent overwriting existing ones, kind of nasty as we have multiple files with different compile dirs... // and honestly some are just wrong to use how we are using here. Need to think on this, or possibly just avoid using CompileDir StringToIDMapping.FindOrAdd(FileHash, FullFileId{File, 0}); } } } // Once we have the global string map setup we can then get a list of the keys // which will then be used to set the GlobalIndex *per* File TArray FileList; StringToIDMapping.GetKeys(FileList); for (uint64 Index = 0; Index < FileList.Num(); Index++) { StringToIDMapping[FileList[Index]].GlobalIndex = Index; } return StringToIDMapping; } // Using the StringToIDMapping, output our new fixed up global file table void OutputFileTable(const TMap& StringToIDMapping, FArchive* Ar) { TUtf8StringBuilder<512> OutputLine; for (const TPair& StringMap : StringToIDMapping) { OutputLine.Reset(); OutputLine << "FILE " << StringMap.Value.GlobalIndex << " " << StringMap.Value.FullFilePath << LINE_TERMINATOR_ANSI; Ar->Serialize(OutputLine.GetData(), OutputLine.Len()); } } // Each FUNC record needs to output its info right after its outputted. // So for each record, find the FUNC associated with it such that when we iterate over all the // FUNC records we can then output all the info void AssociateLineUnitsWithFuncs(FSymsInstance& Instance) { for (const FSymsUnit& Unit : Instance.Units) { for (SYMS_U64 SeqIdx = 0; SeqIdx < Unit.LineTable.sequence_count; SeqIdx++) { SYMS_U64 First = Unit.LineTable.sequence_index_array[SeqIdx]; SYMS_Line* Line = Unit.LineTable.line_array + First; SYMS_UnitID UnitID = syms_spatial_map_1d_value_from_point(&Instance.UnitMap, Line->voff); if (UnitID) { FSymsUnit* FoundUnit = &Instance.Units[UnitID - 1]; SYMS_U64 Value = syms_spatial_map_1d_value_from_point(&FoundUnit->ProcMap, Line->voff); if (Value) { FSymsSymbol* Syms = reinterpret_cast(Value); // Will be used later to iterate over all the line info for each FUNC that has this line info setup Syms->LineUnit = FoundUnit; Syms->LineSeqIndex = SeqIdx; } } } } } // Attempt to demangle the Syms->Name, if it can be use that name which should be a fully qualified name void OutputFunc(const FSymsSymbol* Syms, uint64 BaseOffset, FArchive* Ar) { TUtf8StringBuilder<512> OutputLine; const ANSICHAR* FunctionName = Syms->Name; ANSICHAR* DemangledName = Demangle(Syms->Name); bool bDemangled = false; if (DemangledName != nullptr) { bDemangled = true; FunctionName = DemangledName; } // FUNC output OutputLine.Appendf(UTF8TEXT("FUNC %llx %llx 0 %s" LINE_TERMINATOR_ANSI), Syms->Address - BaseOffset, Syms->Size, FunctionName); Ar->Serialize(OutputLine.GetData(), OutputLine.Len()); // Demangle allocates room with malloc if it demangles. If we we must call free on that pointer if (bDemangled) { free(DemangledName); } } // Output all FUNC records as well as output all records that have been fixed up per FUNC records // This gives us a complete FUNC + record output. void OutputFuncAndLine(const FSymsInstance& Instance, const TMap& StringToIDMapping, FArchive* Ar, TArray& OutFuncAddresses) { TUtf8StringBuilder<512> OutputLine; uint64 BaseOffset = Instance.DefaultBase; // Output all FUNCs and line info if the FUNC has them for (const FSymsUnit& Unit : Instance.Units) { for (int ProcIdx = 0; ProcIdx < Unit.ProcMap.count; ProcIdx++) { FSymsSymbol* Syms = reinterpret_cast(Unit.ProcMap.ranges[ProcIdx].val); if (Syms->Address) { // Collect these to make sure we only print the required PUBLIC symbols later OutFuncAddresses.Add(Syms->Address); OutputFunc(Syms, BaseOffset, Ar); if (Syms->LineUnit) { SYMS_U64 First = Syms->LineUnit->LineTable.sequence_index_array[Syms->LineSeqIndex]; SYMS_U64 OnePastLast = Syms->LineUnit->LineTable.sequence_index_array[Syms->LineSeqIndex + 1]; SYMS_Line* Line = Syms->LineUnit->LineTable.line_array + First; // we can compress line records if they share the same file id + line number uint64 CompressedStartAddress = 0; uint64 CompressedTotalSize = 0; for (SYMS_U64 LineIdx = First; LineIdx < OnePastLast - 1; LineIdx++) { Line = Syms->LineUnit->LineTable.line_array + LineIdx; SYMS_Line* LineNext = Syms->LineUnit->LineTable.line_array + LineIdx + 1; uint64 Size = LineNext->voff - Line->voff; uint64 Address = Line->voff - BaseOffset; // honestly not sure why the FileId is - 1 vs just being the correct index. Something to check with symslib at some point uint64 FileId = Line->src_coord.file_id - 1; uint32 LineNumber = Line->src_coord.line; uint64 UnitFileTableCount = Syms->LineUnit->FileTable.count; // we have the same line record, lets just compress these entries, and always add the last one! if (LineNext->src_coord.file_id - 1 == FileId && LineNext->src_coord.line == LineNumber && LineIdx + 1 < OnePastLast - 1) { if (CompressedStartAddress == 0) { CompressedStartAddress = Address; CompressedTotalSize = Size; } else { CompressedTotalSize += Size; } continue; } // If the next one is not a match, lets update our total size including this line record else { CompressedTotalSize += Size; } // If we have been bundling multiple line record together lets use the starting address + size of all if (CompressedStartAddress != 0) { Address = CompressedStartAddress; Size = CompressedTotalSize; } if (FileId < UnitFileTableCount) { FString FileToLookFor(ANSI_TO_TCHAR(reinterpret_cast(Syms->LineUnit->FileTable.strings[FileId].str))); uint64 FileHash = CityHash64(reinterpret_cast(Syms->LineUnit->FileTable.strings[FileId].str), Syms->LineUnit->FileTable.strings[FileId].size); FileId = StringToIDMapping[FileHash].GlobalIndex; } // output OutputLine.Reset(); OutputLine.Appendf(UTF8TEXT("%lx %lx %u %lu" LINE_TERMINATOR_ANSI), Address, Size, LineNumber, FileId); Ar->Serialize(OutputLine.GetData(), OutputLine.Len()); // reset our compressed accumulators after we output always CompressedStartAddress = 0; CompressedTotalSize = 0; } } } } } } // Output all PUBLIC symbols that can not been previously outputted through the FUNC records void OutputPublic(const TArray& ExportedFunctions, const TArray& FuncAddresses, uint64 BaseOffset, FArchive* Ar) { TUtf8StringBuilder<512> OutputLine; for (const FSymsExported& SymExported : ExportedFunctions) { // Only add this address *if* its not part of our FuncAddress list using a binary search. So it is assumed FuncAddresses is *sorted* if (Algo::BinarySearch(FuncAddresses, SymExported.Address) == INDEX_NONE) { OutputLine.Reset(); OutputLine.Appendf(UTF8TEXT("PUBLIC %llx 0 %s" LINE_TERMINATOR_ANSI), SymExported.Address - BaseOffset, TCHAR_TO_UTF8(*SymExported.Name)); Ar->Serialize(OutputLine.GetData(), OutputLine.Len()); } } } // The goal of this is to *produce* an exact same output file as dump_syms was doing. This is the format: // https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/symbol_files.md bool ProcessAndProducePortableSymbolFile(const FString& BinaryPath, const FString& OutputFile) { // temp memory used for loading SYMS_Group* Group = syms_group_alloc(); // contents of the binary & debug file SYMS_ParseBundle Bundle; // memory-mapped binary & debug file FAutoMappedFile BinaryFile; FAutoMappedFile DebugFile; if (!LoadBinary(*BinaryPath, Group->arena, Bundle, BinaryFile)) { UE_LOG(LogSymsLibDump, Warning, TEXT("Failed to load binary file '%s'"), *BinaryPath); return false; } if (!LoadDebug(*BinaryPath, Group->arena, Bundle, BinaryFile, DebugFile)) { UE_LOG(LogSymsLibDump, Warning, TEXT("Failed to load debug file from binary path '%s'"), *BinaryPath); return false; } // This array will be used for PUBLIC symbols which may not have be in the debug file, such as the _start symbol SYMS_ExportArray ExportArray = syms_exports_from_bin(Group->arena, BinaryFile.GetData(), Bundle.bin); TArray ExportedFunctions = GatherExportedFunctions(ExportArray); FSymsInstance Instance; if (!ParseDebugInfo(Group, Bundle, Instance)) { return false; } if (TUniquePtr Ar{IFileManager::Get().CreateFileWriter(*OutputFile)}) { // MODULE OutputModule(Instance, Ar.Get()); // FILE TABLE TMap StringToIDMapping = GenerateStringToIdMap(Instance); OutputFileTable(StringToIDMapping, Ar.Get()); // FUNC + (line) // need to be matched up with a FUNC so its much easier to output in one go AssociateLineUnitsWithFuncs(Instance); // Gather a list of our Function Address. This is assumed to be sorted as we binary search later on this TArray FuncAddresses; OutputFuncAndLine(Instance, StringToIDMapping, Ar.Get(), FuncAddresses); // PUBLIC OutputPublic(ExportedFunctions, FuncAddresses, Instance.DefaultBase, Ar.Get()); } else { UE_LOG(LogSymsLibDump, Error, TEXT("Failed to open '%s' for writing"), *OutputFile); } // Clean up Arenas, only do once done with the memory Algo::ForEach(Instance.Arenas, [] (SYMS_DefArena* Arena) { if (Arena != nullptr) { syms_arena_release(Arena); } }); Instance.Arenas.Empty(); return true; } int RealMain(const FString& CommandLine) { FPlatformMisc::SetCrashHandler(nullptr); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(*CommandLine); ON_SCOPE_EXIT { FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); }; FString BinaryFile; FString OutputFile; if (!FParse::Value(*CommandLine, TEXT("input "), BinaryFile)) { UE_LOG(LogSymsLibDump, Error, TEXT("Missing binary file, pass in `-input `")); return -1; } if (!FParse::Value(*CommandLine, TEXT("output "), OutputFile)) { UE_LOG(LogSymsLibDump, Error, TEXT("Missing output path, pass in `-output `")); return -1; } UE_LOG(LogSymsLibDump, Log, TEXT("Binary: '%s' Output: '%s'"), *BinaryFile, *OutputFile); if (!ProcessAndProducePortableSymbolFile(BinaryFile, OutputFile)) { return -1; } return 0; } } // anonymous namespace int main(int ArgC, char* ArgV[]) { FString CommandLine; for (int32 Option = 1; Option < ArgC; Option++) { CommandLine += TEXT(" "); FString Argument(ANSI_TO_TCHAR(ArgV[Option])); if (Argument.Contains(TEXT(" "))) { if (Argument.Contains(TEXT("="))) { FString ArgName; FString ArgValue; Argument.Split( TEXT("="), &ArgName, &ArgValue ); Argument = FString::Printf( TEXT("%s=\"%s\""), *ArgName, *ArgValue ); } else { Argument = FString::Printf(TEXT("\"%s\""), *Argument); } } CommandLine += Argument; } return RealMain(CommandLine); }