From d92e70f05b8de72758f3a990844e53c76b44d713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksey=20Kliger=20=28=CE=BBgeek=29?= Date: Wed, 25 Jan 2023 21:33:00 -0500 Subject: [PATCH] BrowserDebugProxy: unify debug metadata reading for PE and Webcil (#81099) * 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 --- .../src/Webcil/WebcilReader.Reflection.cs | 59 +++++++++++ .../src/Webcil/WebcilReader.cs | 63 ++++++++---- .../debugger/BrowserDebugProxy/DebugStore.cs | 98 +++---------------- .../IDebugMetadataProvider.cs | 24 +++++ .../BrowserDebugProxy/MetadataDebugSummary.cs | 89 +++++++++++++++++ ...PortableExecutableDebugMetadataProvider.cs | 27 +++++ .../WebcilDebugMetadataProvider.cs | 29 ++++++ 7 files changed, 284 insertions(+), 105 deletions(-) create mode 100644 src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.Reflection.cs create mode 100644 src/mono/wasm/debugger/BrowserDebugProxy/IDebugMetadataProvider.cs create mode 100644 src/mono/wasm/debugger/BrowserDebugProxy/MetadataDebugSummary.cs create mode 100644 src/mono/wasm/debugger/BrowserDebugProxy/PortableExecutableDebugMetadataProvider.cs create mode 100644 src/mono/wasm/debugger/BrowserDebugProxy/WebcilDebugMetadataProvider.cs diff --git a/src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.Reflection.cs b/src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.Reflection.cs new file mode 100644 index 0000000000000..56eae848a4aa5 --- /dev/null +++ b/src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.Reflection.cs @@ -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 s_readUtf8NullTerminated = new Lazy(() => + { + 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 s_codeViewDebugDirectoryDataCtor = new Lazy(() => + { + 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 s_pdbChecksumDebugDirectoryDataCtor = new Lazy(() => + { + var types = new Type[] { typeof(string), typeof(ImmutableArray) }; + 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 checksum) => (PdbChecksumDebugDirectoryData)s_pdbChecksumDebugDirectoryDataCtor.Value.Invoke(new object[] { algorithmName, checksum }); + } +} diff --git a/src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.cs b/src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.cs index 6782ebf4a5aae..39d58134ce6b4 100644 --- a/src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.cs +++ b/src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.cs @@ -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, @@ -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 checksum) => Reflection.MakePdbChecksumDebugDirectoryData(algorithmName, checksum); public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry) { @@ -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) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs index 06ddcb08de677..8596ca4bcf302 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs @@ -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 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 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; } @@ -997,10 +924,11 @@ 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; @@ -1008,12 +936,12 @@ private unsafe AssemblyInfo(IDisposable owningReader, string name, MetadataReade 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(); } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/IDebugMetadataProvider.cs b/src/mono/wasm/debugger/BrowserDebugProxy/IDebugMetadataProvider.cs new file mode 100644 index 0000000000000..413a85cf762c9 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/IDebugMetadataProvider.cs @@ -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; + +/// +/// An adapter on top of MetadataReader and WebcilReader for DebugStore compensating +/// for the lack of a common base class on those two types. +/// +public interface IDebugMetadataProvider +{ + public ImmutableArray ReadDebugDirectory(); + public CodeViewDebugDirectoryData ReadCodeViewDebugDirectoryData(DebugDirectoryEntry entry); + public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry); + + public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry); +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MetadataDebugSummary.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MetadataDebugSummary.cs new file mode 100644 index 0000000000000..714f0e9f1e4f6 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MetadataDebugSummary.cs @@ -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; + +/// +/// Information we can extract directly from the assembly image using metadata readers +/// +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 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); + } +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/PortableExecutableDebugMetadataProvider.cs b/src/mono/wasm/debugger/BrowserDebugProxy/PortableExecutableDebugMetadataProvider.cs new file mode 100644 index 0000000000000..94aebb35772d2 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/PortableExecutableDebugMetadataProvider.cs @@ -0,0 +1,27 @@ +// 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; + +public class PortableExecutableDebugMetadataProvider : IDebugMetadataProvider +{ + private readonly PEReader _peReader; + public PortableExecutableDebugMetadataProvider(PEReader peReader) + { + _peReader = peReader; + } + public ImmutableArray ReadDebugDirectory() => _peReader.ReadDebugDirectory(); + + public CodeViewDebugDirectoryData ReadCodeViewDebugDirectoryData(DebugDirectoryEntry entry) => _peReader.ReadCodeViewDebugDirectoryData(entry); + + public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry) => _peReader.ReadPdbChecksumDebugDirectoryData(entry); + + public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry) => _peReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry); +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/WebcilDebugMetadataProvider.cs b/src/mono/wasm/debugger/BrowserDebugProxy/WebcilDebugMetadataProvider.cs new file mode 100644 index 0000000000000..993bbb7895739 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/WebcilDebugMetadataProvider.cs @@ -0,0 +1,29 @@ +// 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; +using Microsoft.NET.WebAssembly.Webcil; + +namespace Microsoft.WebAssembly.Diagnostics; + +public class WebcilDebugMetadataProvider : IDebugMetadataProvider +{ + private readonly WebcilReader _webcilReader; + + public WebcilDebugMetadataProvider(WebcilReader webcilReader) + { + _webcilReader = webcilReader; + } + public ImmutableArray ReadDebugDirectory() => _webcilReader.ReadDebugDirectory(); + + public CodeViewDebugDirectoryData ReadCodeViewDebugDirectoryData(DebugDirectoryEntry entry) => _webcilReader.ReadCodeViewDebugDirectoryData(entry); + + public PdbChecksumDebugDirectoryData ReadPdbChecksumDebugDirectoryData(DebugDirectoryEntry entry) => _webcilReader.ReadPdbChecksumDebugDirectoryData(entry); + + public MetadataReaderProvider ReadEmbeddedPortablePdbDebugDirectoryData(DebugDirectoryEntry entry) => _webcilReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry); +}