Skip to content

Commit

Permalink
Add a plugin for outputting Windows PDB files (#382)
Browse files Browse the repository at this point in the history
  • Loading branch information
ds5678 authored Nov 28, 2024
1 parent 73abd20 commit c3eab50
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 43 deletions.
88 changes: 50 additions & 38 deletions Cpp2IL.Core/Il2CppApiFunctions/BaseKeyFunctionAddresses.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -49,13 +50,16 @@ public abstract class BaseKeyFunctionAddresses

public ulong AddrPInvokeLookup; //TODO Re-find this and fix name

public IEnumerable<KeyValuePair<string, ulong>> Pairs => resolvedAddressMap;

private ApplicationAnalysisContext _appContext = null!; //Always initialized before used

private readonly HashSet<ulong> resolvedAddresses = [];
private readonly Dictionary<string, ulong> resolvedAddressMap = [];
private readonly HashSet<ulong> 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)
Expand Down Expand Up @@ -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;
}
}
}
7 changes: 6 additions & 1 deletion Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using LibCpp2IL;
using LibCpp2IL.Metadata;
using StableNameDotNet.Providers;
using System.Linq;

namespace Cpp2IL.Core.Model.Contexts;

Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Cpp2IL.Core/Model/Contexts/ParameterAnalysisContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class ParameterAnalysisContext : HasCustomAttributesAndName, IParameterIn
/// <summary>
/// The human-readable display value of the parameter type.
/// </summary>
public string ReadableTypeName => LibCpp2ILUtils.GetTypeReflectionData(ParameterType).ToString();
public string ReadableTypeName => ParameterTypeContext.FullName;

/// <summary>
/// The human-readable display value of the parameter, as it would appear in a c# method declaration.
Expand Down
18 changes: 18 additions & 0 deletions Cpp2IL.Plugin.Pdb/Cpp2IL.Plugin.Pdb.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Cpp2IL.Core\Cpp2IL.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="AssetRipper.Bindings.MsPdbCore" Version="1.0.1" />
</ItemGroup>

</Project>
137 changes: 137 additions & 0 deletions Cpp2IL.Plugin.Pdb/PdbOutputFormat.cs
Original file line number Diff line number Diff line change
@@ -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<string, ulong> 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<byte> haystack, ReadOnlySpan<byte> needle)
{
for (var i = 0; i <= haystack.Length - needle.Length; i++)
{
if (haystack.Slice(i, needle.Length).SequenceEqual(needle))
{
return i;
}
}

return -1;
}
}
19 changes: 19 additions & 0 deletions Cpp2IL.Plugin.Pdb/PdbOutputPlugin.cs
Original file line number Diff line number Diff line change
@@ -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<PdbOutputFormat>();
}
}
10 changes: 8 additions & 2 deletions Cpp2IL.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions LibCpp2IL/Elf/ElfFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,11 @@ public override bool TryGetExportedFunctionName(ulong addr, [NotNullWhen(true)]
}
}

public override IEnumerable<KeyValuePair<string, ulong>> GetExportedFunctions()
{
return _exportNameTable.Select(kv => new KeyValuePair<string, ulong>(kv.Key, kv.Value.VirtualAddress));
}

public override ulong GetVirtualAddressOfPrimaryExecutableSection() => _elfSectionHeaderEntries.FirstOrDefault(s => s.Name == ".text")?.VirtualAddress ?? 0;

public override byte[] GetEntirePrimaryExecutableSection()
Expand Down
2 changes: 2 additions & 0 deletions LibCpp2IL/Il2CppBinary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,8 @@ public virtual bool TryGetExportedFunctionName(ulong addr, [NotNullWhen(true)] o
return false;
}

public virtual IEnumerable<KeyValuePair<string, ulong>> GetExportedFunctions() => [];

public abstract byte[] GetEntirePrimaryExecutableSection();

public abstract ulong GetVirtualAddressOfPrimaryExecutableSection();
Expand Down
5 changes: 5 additions & 0 deletions LibCpp2IL/MachO/MachOFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ public override bool TryGetExportedFunctionName(ulong addr, [NotNullWhen(true)]
return _exportNamesDict.TryGetValue((long)addr, out name);
}

public override IEnumerable<KeyValuePair<string, ulong>> GetExportedFunctions()
{
return _exportAddressesDict.Select(pair => new KeyValuePair<string, ulong>(pair.Key, (ulong)pair.Value));
}

private MachOSection GetTextSection64()
{
var textSection = Sections64.FirstOrDefault(s => s.SectionName == "__text");
Expand Down
Loading

0 comments on commit c3eab50

Please sign in to comment.