From c3eab50c40a2e0ce50e6897cba2766de94ff204f Mon Sep 17 00:00:00 2001 From: Jeremy Pritts <49847914+ds5678@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:54:02 -0800 Subject: [PATCH] Add a plugin for outputting Windows PDB files (#382) --- .../BaseKeyFunctionAddresses.cs | 88 ++++++----- .../Model/Contexts/MethodAnalysisContext.cs | 7 +- .../Contexts/ParameterAnalysisContext.cs | 2 +- Cpp2IL.Plugin.Pdb/Cpp2IL.Plugin.Pdb.csproj | 18 +++ Cpp2IL.Plugin.Pdb/PdbOutputFormat.cs | 137 ++++++++++++++++++ Cpp2IL.Plugin.Pdb/PdbOutputPlugin.cs | 19 +++ Cpp2IL.sln | 10 +- LibCpp2IL/Elf/ElfFile.cs | 5 + LibCpp2IL/Il2CppBinary.cs | 2 + LibCpp2IL/MachO/MachOFile.cs | 5 + LibCpp2IL/PE/PE.cs | 17 ++- 11 files changed, 267 insertions(+), 43 deletions(-) create mode 100644 Cpp2IL.Plugin.Pdb/Cpp2IL.Plugin.Pdb.csproj create mode 100644 Cpp2IL.Plugin.Pdb/PdbOutputFormat.cs create mode 100644 Cpp2IL.Plugin.Pdb/PdbOutputPlugin.cs diff --git a/Cpp2IL.Core/Il2CppApiFunctions/BaseKeyFunctionAddresses.cs b/Cpp2IL.Core/Il2CppApiFunctions/BaseKeyFunctionAddresses.cs index 303dab87..5fac4f97 100644 --- a/Cpp2IL.Core/Il2CppApiFunctions/BaseKeyFunctionAddresses.cs +++ b/Cpp2IL.Core/Il2CppApiFunctions/BaseKeyFunctionAddresses.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using Cpp2IL.Core.Logging; using Cpp2IL.Core.Model.Contexts; using Cpp2IL.Core.Utils; @@ -49,13 +50,16 @@ public abstract class BaseKeyFunctionAddresses public ulong AddrPInvokeLookup; //TODO Re-find this and fix name + public IEnumerable> Pairs => resolvedAddressMap; + private ApplicationAnalysisContext _appContext = null!; //Always initialized before used - private readonly HashSet resolvedAddresses = []; + private readonly Dictionary resolvedAddressMap = []; + private readonly HashSet resolvedAddressSet = []; public bool IsKeyFunctionAddress(ulong address) { - return address != 0 && resolvedAddresses.Contains(address); + return address != 0 && resolvedAddressSet.Contains(address); } private void FindExport(string name, out ulong ptr) @@ -294,41 +298,49 @@ protected virtual void Init(ApplicationAnalysisContext context) private void InitializeResolvedAddresses() { - resolvedAddresses.Clear(); - resolvedAddresses.Add(il2cpp_codegen_initialize_method); - resolvedAddresses.Add(il2cpp_codegen_initialize_runtime_metadata); - resolvedAddresses.Add(il2cpp_vm_metadatacache_initializemethodmetadata); - resolvedAddresses.Add(il2cpp_runtime_class_init_export); - resolvedAddresses.Add(il2cpp_runtime_class_init_actual); - resolvedAddresses.Add(il2cpp_object_new); - resolvedAddresses.Add(il2cpp_vm_object_new); - resolvedAddresses.Add(il2cpp_codegen_object_new); - resolvedAddresses.Add(il2cpp_array_new_specific); - resolvedAddresses.Add(il2cpp_vm_array_new_specific); - resolvedAddresses.Add(SzArrayNew); - resolvedAddresses.Add(il2cpp_type_get_object); - resolvedAddresses.Add(il2cpp_vm_reflection_get_type_object); - resolvedAddresses.Add(il2cpp_resolve_icall); - resolvedAddresses.Add(InternalCalls_Resolve); - - resolvedAddresses.Add(il2cpp_string_new); - resolvedAddresses.Add(il2cpp_vm_string_new); - resolvedAddresses.Add(il2cpp_string_new_wrapper); - resolvedAddresses.Add(il2cpp_vm_string_newWrapper); - resolvedAddresses.Add(il2cpp_codegen_string_new_wrapper); - - resolvedAddresses.Add(il2cpp_value_box); - resolvedAddresses.Add(il2cpp_vm_object_box); - - resolvedAddresses.Add(il2cpp_object_unbox); - resolvedAddresses.Add(il2cpp_vm_object_unbox); - - resolvedAddresses.Add(il2cpp_raise_exception); - resolvedAddresses.Add(il2cpp_vm_exception_raise); - resolvedAddresses.Add(il2cpp_codegen_raise_exception); - - resolvedAddresses.Add(il2cpp_vm_object_is_inst); - - resolvedAddresses.Add(AddrPInvokeLookup); + resolvedAddressMap.Clear(); + resolvedAddressSet.Clear(); + + AddResolved(il2cpp_codegen_initialize_method); + AddResolved(il2cpp_codegen_initialize_runtime_metadata); + AddResolved(il2cpp_vm_metadatacache_initializemethodmetadata); + AddResolved(il2cpp_runtime_class_init_export); + AddResolved(il2cpp_runtime_class_init_actual); + AddResolved(il2cpp_object_new); + AddResolved(il2cpp_vm_object_new); + AddResolved(il2cpp_codegen_object_new); + AddResolved(il2cpp_array_new_specific); + AddResolved(il2cpp_vm_array_new_specific); + AddResolved(SzArrayNew); + AddResolved(il2cpp_type_get_object); + AddResolved(il2cpp_vm_reflection_get_type_object); + AddResolved(il2cpp_resolve_icall); + AddResolved(InternalCalls_Resolve); + + AddResolved(il2cpp_string_new); + AddResolved(il2cpp_vm_string_new); + AddResolved(il2cpp_string_new_wrapper); + AddResolved(il2cpp_vm_string_newWrapper); + AddResolved(il2cpp_codegen_string_new_wrapper); + + AddResolved(il2cpp_value_box); + AddResolved(il2cpp_vm_object_box); + + AddResolved(il2cpp_object_unbox); + AddResolved(il2cpp_vm_object_unbox); + + AddResolved(il2cpp_raise_exception); + AddResolved(il2cpp_vm_exception_raise); + AddResolved(il2cpp_codegen_raise_exception); + + AddResolved(il2cpp_vm_object_is_inst); + + AddResolved(AddrPInvokeLookup); + + void AddResolved(ulong address, [CallerArgumentExpression(nameof(address))] string name = "") + { + resolvedAddressSet.Add(address); + resolvedAddressMap[name] = address; + } } } diff --git a/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs index 06cd2701..c99c948a 100644 --- a/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs @@ -10,6 +10,7 @@ using LibCpp2IL; using LibCpp2IL.Metadata; using StableNameDotNet.Providers; +using System.Linq; namespace Cpp2IL.Core.Model.Contexts; @@ -67,6 +68,10 @@ public class MethodAnalysisContext : HasCustomAttributesAndName, IMethodInfoProv public override string DefaultName => Definition?.Name ?? throw new("Subclasses of MethodAnalysisContext should override DefaultName"); + public string FullName => DeclaringType == null ? Name : $"{DeclaringType.FullName}::{Name}"; + + public string FullNameWithSignature => $"{ReturnTypeContext.FullName} {FullName}({string.Join(", ", Parameters.Select(p => p.HumanReadableSignature))})"; + public virtual MethodAttributes Attributes => Definition?.Attributes ?? throw new("Subclasses of MethodAnalysisContext should override Attributes"); public TypeAnalysisContext? InjectedReturnType { get; set; } @@ -170,7 +175,7 @@ public void ReleaseAnalysisData() ControlFlowGraph = null; } - public override string ToString() => $"Method: {Definition?.DeclaringType!.Name}::{Definition?.Name ?? "No definition"}"; + public override string ToString() => $"Method: {FullName}"; #region StableNameDot implementation diff --git a/Cpp2IL.Core/Model/Contexts/ParameterAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/ParameterAnalysisContext.cs index 3dcffd06..fd5a3498 100644 --- a/Cpp2IL.Core/Model/Contexts/ParameterAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/ParameterAnalysisContext.cs @@ -37,7 +37,7 @@ public class ParameterAnalysisContext : HasCustomAttributesAndName, IParameterIn /// /// The human-readable display value of the parameter type. /// - public string ReadableTypeName => LibCpp2ILUtils.GetTypeReflectionData(ParameterType).ToString(); + public string ReadableTypeName => ParameterTypeContext.FullName; /// /// The human-readable display value of the parameter, as it would appear in a c# method declaration. diff --git a/Cpp2IL.Plugin.Pdb/Cpp2IL.Plugin.Pdb.csproj b/Cpp2IL.Plugin.Pdb/Cpp2IL.Plugin.Pdb.csproj new file mode 100644 index 00000000..0ce203e7 --- /dev/null +++ b/Cpp2IL.Plugin.Pdb/Cpp2IL.Plugin.Pdb.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + diff --git a/Cpp2IL.Plugin.Pdb/PdbOutputFormat.cs b/Cpp2IL.Plugin.Pdb/PdbOutputFormat.cs new file mode 100644 index 00000000..9928517e --- /dev/null +++ b/Cpp2IL.Plugin.Pdb/PdbOutputFormat.cs @@ -0,0 +1,137 @@ +using System.Reflection.PortableExecutable; +using AssetRipper.Bindings.MsPdbCore; +using Cpp2IL.Core.Api; +using Cpp2IL.Core.Model.Contexts; + +namespace Cpp2IL.Plugin.Pdb; + +internal unsafe class PdbOutputFormat : Cpp2IlOutputFormat +{ + public override string OutputFormatId => "pdb_windows"; + + public override string OutputFormatName => "Windows PDB generation"; + + public override void DoOutput(ApplicationAnalysisContext context, string outputRoot) + { + if (!Directory.Exists(outputRoot)) + Directory.CreateDirectory(outputRoot); + + using var peReader = new PEReader(new MemoryStream(context.Binary.GetRawBinaryContent())); + + var pdbFilePath = Path.Combine(outputRoot, "GameAssembly.pdb"); + MsPdbCore.PDBOpen2W(pdbFilePath, "w", out var err, out var openError, out var pdb); + + MsPdbCore.PDBOpenDBI(pdb, "w", "", out var dbi); + + MsPdbCore.DBIOpenModW(dbi, "__Globals", "__Globals", out var mod); + + ushort secNum = 1; + ushort i2cs = 1; + foreach (var sectionHeader in peReader.PEHeaders.SectionHeaders) + { + if (sectionHeader.Name == "il2cpp") + i2cs = secNum; + MsPdbCore.DBIAddSec(dbi, secNum++, 0 /* TODO? */, sectionHeader.VirtualAddress, sectionHeader.VirtualSize); + } + + Dictionary keyFunctions = []; + + foreach ((var name, var address) in context.GetOrCreateKeyFunctionAddresses().Pairs) + { + keyFunctions[name] = address; + } + + foreach ((var name, var address) in context.Binary.GetExportedFunctions()) + { + keyFunctions[name] = address; + } + + foreach ((var name, var address) in keyFunctions) + { + if (address == 0) + continue; + + GetSectionInformation(peReader, (long)context.Binary.GetRva(address), out var targetSection, out var offset); + MsPdbCore.ModAddPublic2(mod, name, targetSection, offset, CV_PUBSYMFLAGS_e.Function); + } + + foreach ((var virtualAddress, var list) in context.MethodsByAddress) + { + if (virtualAddress <= 0) + continue; + + GetSectionInformation(peReader, (long)context.Binary.GetRva(virtualAddress), out var targetSection, out var offset); + + foreach (var method in list) + { + if (method is NativeMethodAnalysisContext nativeMethod) + { + continue; // Skip native methods + } + MsPdbCore.ModAddPublic2(mod, method.FullName, targetSection, offset, CV_PUBSYMFLAGS_e.Function); + } + } + + MsPdbCore.ModClose(mod); + MsPdbCore.DBIClose(dbi); + + MsPdbCore.PDBCommit(pdb); + + MsPdbCore.PDBQuerySignature2(pdb, out var wrongGuid); + + MsPdbCore.PDBClose(pdb); + + // Hack: manually replace guid and age in generated .pdb, because there's no API on mspdbcore to set them manually + var targetDebugInfo = peReader.ReadCodeViewDebugDirectoryData(peReader.ReadDebugDirectory() + .Single(it => it.Type == DebugDirectoryEntryType.CodeView)); + + var wrongGuidBytes = wrongGuid.ToByteArray(); + var allPdbBytes = File.ReadAllBytes(pdbFilePath); + + var patchTarget = IndexOfBytes(allPdbBytes, wrongGuidBytes); + targetDebugInfo.Guid.TryWriteBytes(allPdbBytes.AsSpan(patchTarget)); + + Console.WriteLine(targetDebugInfo.Guid); + Console.WriteLine(targetDebugInfo.Age); + + BitConverter.TryWriteBytes(allPdbBytes.AsSpan(patchTarget - 4), targetDebugInfo.Age); + File.WriteAllBytes(pdbFilePath, allPdbBytes); + } + + private static void GetSectionInformation(PEReader peReader, long virtualAddress, out ushort targetSection, out int offset) + { + targetSection = 0; + long tsva = 0; + ushort sc = 1; + foreach (var sectionHeader in peReader.PEHeaders.SectionHeaders) + { + if (virtualAddress > sectionHeader.VirtualAddress) + { + targetSection = sc; + tsva = sectionHeader.VirtualAddress; + } + else + break; + + sc++; + } + + if (targetSection == 0) + throw new ApplicationException("Bad segment"); + + offset = (int)(virtualAddress - tsva); + } + + private static int IndexOfBytes(ReadOnlySpan haystack, ReadOnlySpan needle) + { + for (var i = 0; i <= haystack.Length - needle.Length; i++) + { + if (haystack.Slice(i, needle.Length).SequenceEqual(needle)) + { + return i; + } + } + + return -1; + } +} diff --git a/Cpp2IL.Plugin.Pdb/PdbOutputPlugin.cs b/Cpp2IL.Plugin.Pdb/PdbOutputPlugin.cs new file mode 100644 index 00000000..d88888b2 --- /dev/null +++ b/Cpp2IL.Plugin.Pdb/PdbOutputPlugin.cs @@ -0,0 +1,19 @@ +using Cpp2IL.Core.Api; +using Cpp2IL.Core.Attributes; +using Cpp2IL.Plugin.Pdb; + +[assembly: RegisterCpp2IlPlugin(typeof(PdbOutputPlugin))] + +namespace Cpp2IL.Plugin.Pdb; + +public class PdbOutputPlugin : Cpp2IlPlugin +{ + public override string Name => "PDB Output Plugin"; + + public override string Description => "Adds an output format which generates debug symbols"; + + public override void OnLoad() + { + OutputFormatRegistry.Register(); + } +} diff --git a/Cpp2IL.sln b/Cpp2IL.sln index 10af1768..c45c3e39 100644 --- a/Cpp2IL.sln +++ b/Cpp2IL.sln @@ -11,15 +11,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitignore = .gitignore + do-release.ps1 = do-release.ps1 .github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml .github\FUNDING.yml = .github\FUNDING.yml + global.json = global.json LICENSE = LICENSE nuget.config = nuget.config README.md = README.md LibCpp2Il\README.md = LibCpp2Il\README.md Cpp2IL.Core\README_CORE.md = Cpp2IL.Core\README_CORE.md - global.json = global.json - do-release.ps1 = do-release.ps1 EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibCpp2ILTests", "LibCpp2ILTests\LibCpp2ILTests.csproj", "{EB3CFC80-2125-48D2-AA2F-548F5AA58342}" @@ -45,6 +45,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cpp2IL.Plugin.StrippedCodeR EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cpp2IL.Plugin.ControlFlowGraph", "Cpp2IL.Plugin.ControlFlowGraph\Cpp2IL.Plugin.ControlFlowGraph.csproj", "{29683659-D4B0-444B-8D35-F35309535EE6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cpp2IL.Plugin.Pdb", "Cpp2IL.Plugin.Pdb\Cpp2IL.Plugin.Pdb.csproj", "{ED407C97-534B-476C-8CCC-BF9D2FB5458A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +97,10 @@ Global {29683659-D4B0-444B-8D35-F35309535EE6}.Debug|Any CPU.Build.0 = Debug|Any CPU {29683659-D4B0-444B-8D35-F35309535EE6}.Release|Any CPU.ActiveCfg = Release|Any CPU {29683659-D4B0-444B-8D35-F35309535EE6}.Release|Any CPU.Build.0 = Release|Any CPU + {ED407C97-534B-476C-8CCC-BF9D2FB5458A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED407C97-534B-476C-8CCC-BF9D2FB5458A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED407C97-534B-476C-8CCC-BF9D2FB5458A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED407C97-534B-476C-8CCC-BF9D2FB5458A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LibCpp2IL/Elf/ElfFile.cs b/LibCpp2IL/Elf/ElfFile.cs index b27e1623..513ef057 100644 --- a/LibCpp2IL/Elf/ElfFile.cs +++ b/LibCpp2IL/Elf/ElfFile.cs @@ -739,6 +739,11 @@ public override bool TryGetExportedFunctionName(ulong addr, [NotNullWhen(true)] } } + public override IEnumerable> GetExportedFunctions() + { + return _exportNameTable.Select(kv => new KeyValuePair(kv.Key, kv.Value.VirtualAddress)); + } + public override ulong GetVirtualAddressOfPrimaryExecutableSection() => _elfSectionHeaderEntries.FirstOrDefault(s => s.Name == ".text")?.VirtualAddress ?? 0; public override byte[] GetEntirePrimaryExecutableSection() diff --git a/LibCpp2IL/Il2CppBinary.cs b/LibCpp2IL/Il2CppBinary.cs index 6311c0c7..f48a4472 100644 --- a/LibCpp2IL/Il2CppBinary.cs +++ b/LibCpp2IL/Il2CppBinary.cs @@ -466,6 +466,8 @@ public virtual bool TryGetExportedFunctionName(ulong addr, [NotNullWhen(true)] o return false; } + public virtual IEnumerable> GetExportedFunctions() => []; + public abstract byte[] GetEntirePrimaryExecutableSection(); public abstract ulong GetVirtualAddressOfPrimaryExecutableSection(); diff --git a/LibCpp2IL/MachO/MachOFile.cs b/LibCpp2IL/MachO/MachOFile.cs index ba0804f2..2a691017 100644 --- a/LibCpp2IL/MachO/MachOFile.cs +++ b/LibCpp2IL/MachO/MachOFile.cs @@ -147,6 +147,11 @@ public override bool TryGetExportedFunctionName(ulong addr, [NotNullWhen(true)] return _exportNamesDict.TryGetValue((long)addr, out name); } + public override IEnumerable> GetExportedFunctions() + { + return _exportAddressesDict.Select(pair => new KeyValuePair(pair.Key, (ulong)pair.Value)); + } + private MachOSection GetTextSection64() { var textSection = Sections64.FirstOrDefault(s => s.SectionName == "__text"); diff --git a/LibCpp2IL/PE/PE.cs b/LibCpp2IL/PE/PE.cs index 3ac24d0b..f0bfb960 100644 --- a/LibCpp2IL/PE/PE.cs +++ b/LibCpp2IL/PE/PE.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Text; using LibCpp2IL.Logging; namespace LibCpp2IL.PE; @@ -215,6 +215,21 @@ public override bool TryGetExportedFunctionName(ulong addr, [NotNullWhen(true)] } } + public override IEnumerable> GetExportedFunctions() + { + if (peExportedFunctionPointers == null) + LoadPeExportTable(); + + for (var i = 0; i < peExportedFunctionPointers.Length; i++) + { + var functionPointer = peExportedFunctionPointers[i]; + var namePointer = peExportedFunctionNamePtrs[i]; + + var name = ReadStringToNull(MapVirtualAddressToRaw(namePointer + peImageBase)); + yield return new(name, functionPointer + peImageBase); + } + } + public override ulong GetRva(ulong pointer) { return pointer - peImageBase;