Skip to content

Commit

Permalink
BrowserDebugProxy: unify debug metadata reading for PE and Webcil (#8…
Browse files Browse the repository at this point in the history
…1099)

* DebugStore: factor common PE and Webcil reading logic

* Move common logic to a MetadataDebugSummary class

Also switch from cascade of 'if's to a 'switch' when looking at debug entries

* Implement PDB checksum reader for WebcilReader

* Move WebcilReader reflection to a helper; add lazy initialization

Co-authored-by: Ankit Jain <[email protected]>
  • Loading branch information
lambdageek and radical authored Jan 26, 2023
1 parent 4a156cc commit d92e70f
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;

using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;

namespace Microsoft.NET.WebAssembly.Webcil;


public sealed partial class WebcilReader
{

// Helpers to call into System.Reflection.Metadata internals
internal static class Reflection
{
private static readonly Lazy<MethodInfo> s_readUtf8NullTerminated = new Lazy<MethodInfo>(() =>
{
var mi = typeof(BlobReader).GetMethod("ReadUtf8NullTerminated", BindingFlags.NonPublic | BindingFlags.Instance);
if (mi == null)
{
throw new InvalidOperationException("Could not find BlobReader.ReadUtf8NullTerminated");
}
return mi;
});

internal static string? ReadUtf8NullTerminated(BlobReader reader) => (string?)s_readUtf8NullTerminated.Value.Invoke(reader, null);

private static readonly Lazy<ConstructorInfo> s_codeViewDebugDirectoryDataCtor = new Lazy<ConstructorInfo>(() =>
{
var types = new Type[] { typeof(Guid), typeof(int), typeof(string) };
var mi = typeof(CodeViewDebugDirectoryData).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
if (mi == null)
{
throw new InvalidOperationException("Could not find CodeViewDebugDirectoryData constructor");
}
return mi;
});

internal static CodeViewDebugDirectoryData MakeCodeViewDebugDirectoryData(Guid guid, int age, string path) => (CodeViewDebugDirectoryData)s_codeViewDebugDirectoryDataCtor.Value.Invoke(new object[] { guid, age, path });

private static readonly Lazy<ConstructorInfo> s_pdbChecksumDebugDirectoryDataCtor = new Lazy<ConstructorInfo>(() =>
{
var types = new Type[] { typeof(string), typeof(ImmutableArray<byte>) };
var mi = typeof(PdbChecksumDebugDirectoryData).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
if (mi == null)
{
throw new InvalidOperationException("Could not find PdbChecksumDebugDirectoryData constructor");
}
return mi;
});
internal static PdbChecksumDebugDirectoryData MakePdbChecksumDebugDirectoryData(string algorithmName, ImmutableArray<byte> checksum) => (PdbChecksumDebugDirectoryData)s_pdbChecksumDebugDirectoryDataCtor.Value.Invoke(new object[] { algorithmName, checksum });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
namespace Microsoft.NET.WebAssembly.Webcil;


public sealed class WebcilReader : IDisposable
public sealed partial class WebcilReader : IDisposable
{
// WISH:
// This should be implemented in terms of System.Reflection.Internal.MemoryBlockProvider like the PEReader,
Expand Down Expand Up @@ -219,26 +219,11 @@ private static CodeViewDebugDirectoryData DecodeCodeViewDebugDirectoryData(BlobR
return MakeCodeViewDebugDirectoryData(guid, age, path);
}

private static string? ReadUtf8NullTerminated(BlobReader reader)
{
var mi = typeof(BlobReader).GetMethod("ReadUtf8NullTerminated", BindingFlags.NonPublic | BindingFlags.Instance);
if (mi == null)
{
throw new InvalidOperationException("Could not find BlobReader.ReadUtf8NullTerminated");
}
return (string?)mi.Invoke(reader, null);
}
private static string? ReadUtf8NullTerminated(BlobReader reader) => Reflection.ReadUtf8NullTerminated(reader);

private static CodeViewDebugDirectoryData MakeCodeViewDebugDirectoryData(Guid guid, int age, string path)
{
var types = new Type[] { typeof(Guid), typeof(int), typeof(string) };
var mi = typeof(CodeViewDebugDirectoryData).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
if (mi == null)
{
throw new InvalidOperationException("Could not find CodeViewDebugDirectoryData constructor");
}
return (CodeViewDebugDirectoryData)mi.Invoke(new object[] { guid, age, path });
}
private static CodeViewDebugDirectoryData MakeCodeViewDebugDirectoryData(Guid guid, int age, string path) => Reflection.MakeCodeViewDebugDirectoryData(guid, age, path);

private static PdbChecksumDebugDirectoryData MakePdbChecksumDebugDirectoryData(string algorithmName, ImmutableArray<byte> checksum) => Reflection.MakePdbChecksumDebugDirectoryData(algorithmName, checksum);

public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry)
{
Expand Down Expand Up @@ -297,6 +282,44 @@ private static MetadataReaderProvider DecodeEmbeddedPortablePdbDirectoryData(Blo

}

public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry)
{
if (entry.Type != DebugDirectoryEntryType.PdbChecksum)
{
throw new ArgumentException($"expected debug directory entry type {nameof(DebugDirectoryEntryType.PdbChecksum)}", nameof(entry));
}

var pos = entry.DataPointer;
var buffer = new byte[entry.DataSize];
if (_stream.Seek(pos, SeekOrigin.Begin) != pos)
{
throw new BadImageFormatException("Could not seek to CodeView debug directory data", nameof(_stream));
}
if (_stream.Read(buffer, 0, buffer.Length) != buffer.Length)
{
throw new BadImageFormatException("Could not read CodeView debug directory data", nameof(_stream));
}
unsafe
{
fixed (byte* p = buffer)
{
return DecodePdbChecksumDebugDirectoryData(new BlobReader(p, buffer.Length));
}
}
}

private static PdbChecksumDebugDirectoryData DecodePdbChecksumDebugDirectoryData(BlobReader reader)
{
var algorithmName = ReadUtf8NullTerminated(reader);
byte[]? checksum = reader.ReadBytes(reader.RemainingBytes);
if (string.IsNullOrEmpty(algorithmName) || checksum == null || checksum.Length == 0)
{
throw new BadImageFormatException("Invalid PdbChecksum data format");
}

return MakePdbChecksumDebugDirectoryData(algorithmName, ImmutableArray.Create(checksum));
}

private long TranslateRVA(uint rva)
{
if (_sections == null)
Expand Down
98 changes: 13 additions & 85 deletions src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -894,100 +894,27 @@ private AssemblyInfo(ILogger logger)
this.id = Interlocked.Increment(ref next_id);
this.logger = logger;
}

private static AssemblyInfo FromPEReader(MonoProxy monoProxy, SessionId sessionId, PEReader peReader, byte[] pdb, ILogger logger, CancellationToken token)
{
var entries = peReader.ReadDebugDirectory();
CodeViewDebugDirectoryData? codeViewData = null;
var isPortableCodeView = false;
List<PdbChecksum> pdbChecksums = new();
foreach (var entry in peReader.ReadDebugDirectory())
{
if (entry.Type == DebugDirectoryEntryType.CodeView)
{
codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry);
if (entry.IsPortableCodeView)
isPortableCodeView = true;
}
if (entry.Type == DebugDirectoryEntryType.PdbChecksum)
{
var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry);
pdbChecksums.Add(new PdbChecksum(checksum.AlgorithmName, checksum.Checksum.ToArray()));
}
}

var debugProvider = new PortableExecutableDebugMetadataProvider(peReader);

var asmMetadataReader = PEReaderExtensions.GetMetadataReader(peReader);
string name = ReadAssemblyName(asmMetadataReader);
var summary = MetadataDebugSummary.Create(monoProxy, sessionId, name, debugProvider, pdb, token);

MetadataReader pdbMetadataReader = null;
if (pdb != null)
{
var pdbStream = new MemoryStream(pdb);
try
{
// MetadataReaderProvider.FromPortablePdbStream takes ownership of the stream
pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader();
}
catch (BadImageFormatException)
{
monoProxy.SendLog(sessionId, $"Warning: Unable to read debug information of: {name} (use DebugType=Portable/Embedded)", token);
}
}
else
{
var embeddedPdbEntry = entries.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
if (embeddedPdbEntry.DataSize != 0)
{
pdbMetadataReader = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry).GetMetadataReader();
}
}

var assemblyInfo = new AssemblyInfo(peReader, name, asmMetadataReader, codeViewData, pdbChecksums.ToArray(), isPortableCodeView, pdbMetadataReader, logger);
var assemblyInfo = new AssemblyInfo(peReader, name, asmMetadataReader, summary, logger);
return assemblyInfo;
}

private static AssemblyInfo FromWebcilReader(MonoProxy monoProxy, SessionId sessionId, WebcilReader wcReader, byte[] pdb, ILogger logger, CancellationToken token)
{
var entries = wcReader.ReadDebugDirectory();
CodeViewDebugDirectoryData? codeViewData = null;
var isPortableCodeView = false;
List<PdbChecksum> pdbChecksums = new();
foreach (var entry in entries)
{
var codeView = entries[0];
if (codeView.Type == DebugDirectoryEntryType.CodeView)
{
codeViewData = wcReader.ReadCodeViewDebugDirectoryData(codeView);
if (codeView.IsPortableCodeView)
isPortableCodeView = true;
}
}
var debugProvider = new WebcilDebugMetadataProvider(wcReader);
var asmMetadataReader = wcReader.GetMetadataReader();
string name = ReadAssemblyName(asmMetadataReader);

MetadataReader pdbMetadataReader = null;
if (pdb != null)
{
var pdbStream = new MemoryStream(pdb);
try
{
// MetadataReaderProvider.FromPortablePdbStream takes ownership of the stream
pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader();
}
catch (BadImageFormatException)
{
monoProxy.SendLog(sessionId, $"Warning: Unable to read debug information of: {name} (use DebugType=Portable/Embedded)", token);
}
}
else
{
var embeddedPdbEntry = entries.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
if (embeddedPdbEntry.DataSize != 0)
{
pdbMetadataReader = wcReader.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry).GetMetadataReader();
}
}
var summary = MetadataDebugSummary.Create(monoProxy, sessionId, name, debugProvider, pdb, token);

var assemblyInfo = new AssemblyInfo(wcReader, name, asmMetadataReader, codeViewData, pdbChecksums.ToArray(), isPortableCodeView, pdbMetadataReader, logger);
var assemblyInfo = new AssemblyInfo(wcReader, name, asmMetadataReader, summary, logger);
return assemblyInfo;
}

Expand All @@ -997,23 +924,24 @@ private static string ReadAssemblyName(MetadataReader asmMetadataReader)
return asmDef.GetAssemblyName().Name + ".dll";
}

private unsafe AssemblyInfo(IDisposable owningReader, string name, MetadataReader asmMetadataReader, CodeViewDebugDirectoryData? codeViewData, PdbChecksum[] pdbChecksums, bool isPortableCodeView, MetadataReader pdbMetadataReader, ILogger logger)
private unsafe AssemblyInfo(IDisposable owningReader, string name, MetadataReader asmMetadataReader, MetadataDebugSummary summary, ILogger logger)
: this(logger)
{
peReaderOrWebcilReader = owningReader;
var codeViewData = summary.CodeViewData;
if (codeViewData != null)
{
PdbAge = codeViewData.Value.Age;
PdbGuid = codeViewData.Value.Guid;
PdbName = codeViewData.Value.Path;
CodeViewInformationAvailable = true;
}
IsPortableCodeView = isPortableCodeView;
PdbChecksums = pdbChecksums;
IsPortableCodeView = summary.IsPortableCodeView;
PdbChecksums = summary.PdbChecksums;
this.asmMetadataReader = asmMetadataReader;
Name = name;
logger.LogTrace($"Info: loading AssemblyInfo with name {Name}");
this.pdbMetadataReader = pdbMetadataReader;
this.pdbMetadataReader = summary.PdbMetadataReader;
Populate();
}

Expand Down
24 changes: 24 additions & 0 deletions src/mono/wasm/debugger/BrowserDebugProxy/IDebugMetadataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Collections.Immutable;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;

namespace Microsoft.WebAssembly.Diagnostics;

/// <summary>
/// An adapter on top of MetadataReader and WebcilReader for DebugStore compensating
/// for the lack of a common base class on those two types.
/// </summary>
public interface IDebugMetadataProvider
{
public ImmutableArray<DebugDirectoryEntry> ReadDebugDirectory();
public CodeViewDebugDirectoryData ReadCodeViewDebugDirectoryData(DebugDirectoryEntry entry);
public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry);

public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry);
}
89 changes: 89 additions & 0 deletions src/mono/wasm/debugger/BrowserDebugProxy/MetadataDebugSummary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Threading;
using Microsoft.FileFormats.PE;

namespace Microsoft.WebAssembly.Diagnostics;

/// <summary>
/// Information we can extract directly from the assembly image using metadata readers
/// </summary>
internal sealed class MetadataDebugSummary
{
internal MetadataReader? PdbMetadataReader { get; private init; }
internal bool IsPortableCodeView { get; private init; }
internal PdbChecksum[] PdbChecksums { get; private init; }

internal CodeViewDebugDirectoryData? CodeViewData { get; private init; }

private MetadataDebugSummary(MetadataReader? pdbMetadataReader, bool isPortableCodeView, PdbChecksum[] pdbChecksums, CodeViewDebugDirectoryData? codeViewData)
{
PdbMetadataReader = pdbMetadataReader;
IsPortableCodeView = isPortableCodeView;
PdbChecksums = pdbChecksums;
CodeViewData = codeViewData;
}

internal static MetadataDebugSummary Create(MonoProxy monoProxy, SessionId sessionId, string name, IDebugMetadataProvider provider, byte[]? pdb, CancellationToken token)
{
var entries = provider.ReadDebugDirectory();
CodeViewDebugDirectoryData? codeViewData = null;
bool isPortableCodeView = false;
List<PdbChecksum> pdbChecksums = new();
DebugDirectoryEntry? embeddedPdbEntry = null;
foreach (var entry in entries)
{
switch (entry.Type)
{
case DebugDirectoryEntryType.CodeView:
codeViewData = provider.ReadCodeViewDebugDirectoryData(entry);
if (entry.IsPortableCodeView)
isPortableCodeView = true;
break;
case DebugDirectoryEntryType.PdbChecksum:
var checksum = provider.ReadPdbChecksumDebugDirectoryData(entry);
pdbChecksums.Add(new PdbChecksum(checksum.AlgorithmName, checksum.Checksum.ToArray()));
break;
case DebugDirectoryEntryType.EmbeddedPortablePdb:
embeddedPdbEntry = entry;
break;
default:
break;
}
}

MetadataReader? pdbMetadataReader = null;
if (pdb != null)
{
var pdbStream = new MemoryStream(pdb);
try
{
// MetadataReaderProvider.FromPortablePdbStream takes ownership of the stream
pdbMetadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader();
}
catch (BadImageFormatException)
{
monoProxy.SendLog(sessionId, $"Warning: Unable to read debug information of: {name} (use DebugType=Portable/Embedded)", token);
}
}
else
{
if (embeddedPdbEntry != null && embeddedPdbEntry.Value.DataSize != 0)
{
pdbMetadataReader = provider.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry.Value).GetMetadataReader();
}
}

return new MetadataDebugSummary(pdbMetadataReader, isPortableCodeView, pdbChecksums.ToArray(), codeViewData);
}
}
Loading

0 comments on commit d92e70f

Please sign in to comment.