Merge pull request #4197 from icsharpcode/feature/dump-pdb

Add cross-platform PDB dumping to ilspycmd
This commit is contained in:
Siegfried Pammer
2026-10-05 07:32:09 +02:00
committed by GitHub
26 changed files with 1651 additions and 11228 deletions
+27 -1
View File
@@ -18,7 +18,8 @@
using System;
using System.Collections.Generic;
using System.Text;
#nullable enable
namespace ICSharpCode.Decompiler.DebugInfo
{
@@ -45,5 +46,30 @@ namespace ICSharpCode.Decompiler.DebugInfo
public static readonly Guid HashAlgorithmSHA1 = new Guid("ff1816ec-aa5e-4d10-87f7-6f4963833460");
public static readonly Guid HashAlgorithmSHA256 = new Guid("8829d00f-11b8-4213-878b-770e8597ac16");
static readonly Dictionary<Guid, string> customDebugInformationKindNames = new() {
{ StateMachineHoistedLocalScopes, "State Machine Hoisted Local Scopes" },
{ DynamicLocalVariables, "Dynamic Local Variables" },
{ DefaultNamespaces, "Default Namespaces" },
{ EditAndContinueLocalSlotMap, "Edit And Continue Local Slot Map" },
{ EditAndContinueLambdaAndClosureMap, "Edit And Continue Lambda And Closure Map" },
{ EncStateMachineStateMap, "Edit And Continue State Machine State Map" },
{ EmbeddedSource, "Embedded Source" },
{ SourceLink, "Source Link" },
{ MethodSteppingInformation, "Method Stepping Information" },
{ CompilationOptions, "Compilation Options" },
{ CompilationMetadataReferences, "Compilation Metadata References" },
{ TupleElementNames, "Tuple Element Names" },
{ TypeDefinitionDocuments, "Type Definition Documents" },
};
/// <summary>
/// The name of a CustomDebugInformation kind, or null if <paramref name="kind"/> is none
/// of the kinds Roslyn documents.
/// </summary>
public static string? GetCustomDebugInformationKindName(Guid kind)
{
return customDebugInformationKindNames.TryGetValue(kind, out var name) ? name : null;
}
}
}
@@ -17,7 +17,6 @@
// DEALINGS IN THE SOFTWARE.
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -348,382 +347,6 @@ namespace ICSharpCode.Decompiler.Metadata
}
}
public static IEnumerable<(Handle Handle, MethodSemanticsAttributes Semantics, MethodDefinitionHandle Method, EntityHandle Association)> GetMethodSemantics(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.MethodSemantics);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.MethodSemantics);
int methodSize = SimpleIndexSize(metadata, TableIndex.MethodDef);
// HasSemantics coded index: 1 tag bit over Event (tag 0) and Property (tag 1).
int assocSize = CodedIndexSize(metadata, 1, TableIndex.Event, TableIndex.Property);
CheckRowSize(metadata, TableIndex.MethodSemantics, 2 + methodSize + assocSize);
for (int rid = 1; rid <= rowCount; rid++)
{
var semantics = (MethodSemanticsAttributes)reader.ReadUInt16();
int methodRow = methodSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int assocTag = assocSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
EntityHandle propOrEvent;
if ((assocTag & 0x1) == 1)
{
propOrEvent = MetadataTokens.PropertyDefinitionHandle(assocTag >> 1);
}
else
{
propOrEvent = MetadataTokens.EventDefinitionHandle(assocTag >> 1);
}
yield return (MetadataTokens.Handle(((int)TableIndex.MethodSemantics << 24) | rid), semantics, MetadataTokens.MethodDefinitionHandle(methodRow), propOrEvent);
}
}
/// <summary>
/// Size in bytes of a column indexing <paramref name="table"/> directly
/// (ECMA-335 II.24.2.6: 2 bytes while the table has fewer than 2^16 rows).
/// </summary>
static int SimpleIndexSize(MetadataReader metadata, TableIndex table)
{
return metadata.GetTableRowCount(table) < (1 << 16) ? 2 : 4;
}
/// <summary>
/// Size in bytes of a coded-index column over <paramref name="tables"/> with
/// <paramref name="tagBits"/> tag bits (ECMA-335 II.24.2.6: 2 bytes while every
/// indexed table has fewer than 2^(16 - tagBits) rows).
/// </summary>
static int CodedIndexSize(MetadataReader metadata, int tagBits, params TableIndex[] tables)
{
int smallLimit = 1 << (16 - tagBits);
foreach (var table in tables)
{
if (metadata.GetTableRowCount(table) >= smallLimit)
return 4;
}
return 2;
}
/// <summary>
/// Size in bytes of the FieldList/MethodList/ParamList member-range columns. SRM's rule
/// (MetadataReader.InitializeTableReaders): the column is wide when the *Ptr table itself
/// is large; otherwise the member table's row count governs the width, whether or not the
/// indirection is present.
/// </summary>
static int ListColumnSize(MetadataReader metadata, TableIndex ptrTable, TableIndex memberTable)
{
return SimpleIndexSize(metadata, ptrTable) > 2 ? 4 : SimpleIndexSize(metadata, memberTable);
}
/// <summary>
/// Size in bytes of a #Strings heap index column. The width is declared by the HeapSizes
/// flags in the tables-stream header, not derived from the heap's size (a producer may set
/// the large-heap flag over a small heap). SRM does not expose the flags, but the ModuleRef
/// row consists of a single String column, so its computed row size is exactly this width.
/// </summary>
static int StringIndexSize(MetadataReader metadata)
{
return metadata.GetTableRowSize(TableIndex.ModuleRef);
}
/// <summary>
/// Size in bytes of a #Blob heap index column (see <see cref="StringIndexSize"/>; the
/// TypeSpec row consists of a single Blob column).
/// </summary>
static int BlobIndexSize(MetadataReader metadata)
{
return metadata.GetTableRowSize(TableIndex.TypeSpec);
}
/// <summary>
/// Verifies that the computed column widths sum to the row size SRM derived from the
/// tables-stream header. A width bug reads plausible-looking garbage rather than failing,
/// and some width rules are not reproducible through public API at all (EnC minimal-delta
/// metadata forces every table and coded index to 4 bytes), so fail loudly on mismatch.
/// </summary>
static void CheckRowSize(MetadataReader metadata, TableIndex table, int computedRowSize)
{
int actualRowSize = metadata.GetTableRowSize(table);
if (computedRowSize != actualRowSize)
throw new BadImageFormatException($"Unexpected {table} row size: computed {computedRowSize}, actual {actualRowSize}.");
}
public static IEnumerable<(int PackingSize, uint ClassSize, TypeDefinitionHandle Parent)> GetClassLayouts(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.ClassLayout);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.ClassLayout);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
CheckRowSize(metadata, TableIndex.ClassLayout, 2 + 4 + typeDefSize);
for (int rid = 1; rid <= rowCount; rid++)
{
ushort packingSize = reader.ReadUInt16();
uint classSize = reader.ReadUInt32();
int parentRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (packingSize, classSize, MetadataTokens.TypeDefinitionHandle(parentRow));
}
}
public static IEnumerable<(int Offset, FieldDefinitionHandle Field)> GetFieldLayoutRows(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.FieldLayout);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.FieldLayout);
int fieldSize = SimpleIndexSize(metadata, TableIndex.Field);
CheckRowSize(metadata, TableIndex.FieldLayout, 4 + fieldSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int offset = reader.ReadInt32();
int fieldRow = fieldSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (offset, MetadataTokens.FieldDefinitionHandle(fieldRow));
}
}
public static IEnumerable<(TypeDefinitionHandle Parent, EventDefinitionHandle EventList)> GetEventMaps(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.EventMap);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.EventMap);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
int eventListSize = ListColumnSize(metadata, TableIndex.EventPtr, TableIndex.Event);
CheckRowSize(metadata, TableIndex.EventMap, typeDefSize + eventListSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int parentRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int eventListRow = eventListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (MetadataTokens.TypeDefinitionHandle(parentRow), MetadataTokens.EventDefinitionHandle(eventListRow));
}
}
public static IEnumerable<(TypeDefinitionHandle Parent, PropertyDefinitionHandle PropertyList)> GetPropertyMaps(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.PropertyMap);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.PropertyMap);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
int propertyListSize = ListColumnSize(metadata, TableIndex.PropertyPtr, TableIndex.Property);
CheckRowSize(metadata, TableIndex.PropertyMap, typeDefSize + propertyListSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int parentRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int propertyListRow = propertyListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (MetadataTokens.TypeDefinitionHandle(parentRow), MetadataTokens.PropertyDefinitionHandle(propertyListRow));
}
}
public static IEnumerable<(TypeDefinitionHandle NestedClass, TypeDefinitionHandle EnclosingClass)> GetNestedClasses(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.NestedClass);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.NestedClass);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
CheckRowSize(metadata, TableIndex.NestedClass, 2 * typeDefSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int nestedRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int enclosingRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (MetadataTokens.TypeDefinitionHandle(nestedRow), MetadataTokens.TypeDefinitionHandle(enclosingRow));
}
}
public static IEnumerable<(int RelativeVirtualAddress, FieldDefinitionHandle Field)> GetFieldRvas(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.FieldRva);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.FieldRva);
int fieldSize = SimpleIndexSize(metadata, TableIndex.Field);
CheckRowSize(metadata, TableIndex.FieldRva, 4 + fieldSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int rva = reader.ReadInt32();
int fieldRow = fieldSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (rva, MetadataTokens.FieldDefinitionHandle(fieldRow));
}
}
public static IEnumerable<(EntityHandle Parent, BlobHandle NativeType)> GetFieldMarshals(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.FieldMarshal);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.FieldMarshal);
// HasFieldMarshal coded index: 1 tag bit over Field (tag 0) and Param (tag 1).
int parentSize = CodedIndexSize(metadata, 1, TableIndex.Field, TableIndex.Param);
int blobSize = BlobIndexSize(metadata);
CheckRowSize(metadata, TableIndex.FieldMarshal, parentSize + blobSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int parentTag = parentSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int blobOffset = blobSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
EntityHandle parent = (parentTag & 0x1) == 1
? MetadataTokens.ParameterHandle(parentTag >> 1)
: MetadataTokens.FieldDefinitionHandle(parentTag >> 1);
yield return (parent, MetadataTokens.BlobHandle(blobOffset));
}
}
public static IEnumerable<(System.Reflection.MethodImportAttributes MappingFlags, EntityHandle MemberForwarded, StringHandle ImportName, ModuleReferenceHandle ImportScope)> GetImplMaps(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.ImplMap);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.ImplMap);
// MemberForwarded coded index: 1 tag bit over Field (tag 0) and MethodDef (tag 1).
int memberForwardedSize = CodedIndexSize(metadata, 1, TableIndex.Field, TableIndex.MethodDef);
int stringSize = StringIndexSize(metadata);
int moduleRefSize = SimpleIndexSize(metadata, TableIndex.ModuleRef);
CheckRowSize(metadata, TableIndex.ImplMap, 2 + memberForwardedSize + stringSize + moduleRefSize);
for (int rid = 1; rid <= rowCount; rid++)
{
var mappingFlags = (System.Reflection.MethodImportAttributes)reader.ReadUInt16();
int memberForwardedTag = memberForwardedSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int importNameOffset = stringSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int importScopeRow = moduleRefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
EntityHandle memberForwarded = (memberForwardedTag & 0x1) == 1
? MetadataTokens.MethodDefinitionHandle(memberForwardedTag >> 1)
: MetadataTokens.FieldDefinitionHandle(memberForwardedTag >> 1);
yield return (mappingFlags, memberForwarded, MetadataTokens.StringHandle(importNameOffset), MetadataTokens.ModuleReferenceHandle(importScopeRow));
}
}
public static IEnumerable<(TypeDefinitionHandle Class, EntityHandle Interface)> GetInterfaceImplRows(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.InterfaceImpl);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.InterfaceImpl);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
// TypeDefOrRef coded index: 2 tag bits over TypeDef (0), TypeRef (1) and TypeSpec (2).
int interfaceSize = CodedIndexSize(metadata, 2, TableIndex.TypeDef, TableIndex.TypeRef, TableIndex.TypeSpec);
CheckRowSize(metadata, TableIndex.InterfaceImpl, typeDefSize + interfaceSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int classRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int interfaceTag = interfaceSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
EntityHandle iface = (interfaceTag & 0x3) switch {
0 => MetadataTokens.TypeDefinitionHandle(interfaceTag >> 2),
1 => MetadataTokens.TypeReferenceHandle(interfaceTag >> 2),
_ => MetadataTokens.TypeSpecificationHandle(interfaceTag >> 2),
};
yield return (MetadataTokens.TypeDefinitionHandle(classRow), iface);
}
}
/// <summary>
/// Enumerates one of the five *Ptr indirection tables (EventPtr, FieldPtr, MethodPtr,
/// ParamPtr, PropertyPtr), yielding the referenced entity of each row.
/// </summary>
public static IEnumerable<EntityHandle> GetPtrRows(this MetadataReader metadata, TableIndex ptrTable)
{
TableIndex referencedTable = ptrTable switch {
TableIndex.EventPtr => TableIndex.Event,
TableIndex.FieldPtr => TableIndex.Field,
TableIndex.MethodPtr => TableIndex.MethodDef,
TableIndex.ParamPtr => TableIndex.Param,
TableIndex.PropertyPtr => TableIndex.Property,
_ => throw new ArgumentOutOfRangeException(nameof(ptrTable)),
};
int rowCount = metadata.GetTableRowCount(ptrTable);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(ptrTable);
int handleSize = SimpleIndexSize(metadata, referencedTable);
CheckRowSize(metadata, ptrTable, handleSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int row = handleSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return MetadataTokens.EntityHandle(((int)referencedTable << 24) | row);
}
}
/// <summary>
/// Reads the FieldList/MethodList columns of the TypeDef table, which SRM does not expose:
/// the stored value is the running list position (the next type's first member row, or one
/// past the member table's end), never 0. The columns sit at the end of the row, so they are
/// read relative to the row end to avoid re-deriving the widths of the preceding columns.
/// </summary>
public static IEnumerable<(TypeDefinitionHandle Type, int FieldList, int MethodList)> GetTypeDefListColumns(this MetadataReader metadata)
{
int fieldListSize = ListColumnSize(metadata, TableIndex.FieldPtr, TableIndex.Field);
int methodListSize = ListColumnSize(metadata, TableIndex.MethodPtr, TableIndex.MethodDef);
// Flags + Name + Namespace + Extends (TypeDefOrRef coded index) precede the list columns.
CheckRowSize(metadata, TableIndex.TypeDef,
4 + 2 * StringIndexSize(metadata)
+ CodedIndexSize(metadata, 2, TableIndex.TypeDef, TableIndex.TypeRef, TableIndex.TypeSpec)
+ fieldListSize + methodListSize);
int rowSize = metadata.GetTableRowSize(TableIndex.TypeDef);
int tableOffset = metadata.GetTableMetadataOffset(TableIndex.TypeDef);
var reader = metadata.AsBlobReader();
foreach (var handle in metadata.TypeDefinitions)
{
reader.Offset = tableOffset + rowSize * MetadataTokens.GetRowNumber(handle) - fieldListSize - methodListSize;
int fieldList = fieldListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int methodList = methodListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (handle, fieldList, methodList);
}
}
/// <summary>
/// Reads the ParamList column of the MethodDef table (see <see cref="GetTypeDefListColumns"/>
/// for why SRM cannot provide it).
/// </summary>
public static IEnumerable<(MethodDefinitionHandle Method, int ParamList)> GetMethodDefParamLists(this MetadataReader metadata)
{
int paramListSize = ListColumnSize(metadata, TableIndex.ParamPtr, TableIndex.Param);
// RVA + ImplFlags + Flags + Name + Signature precede the ParamList column.
CheckRowSize(metadata, TableIndex.MethodDef,
4 + 2 + 2 + StringIndexSize(metadata) + BlobIndexSize(metadata) + paramListSize);
int rowSize = metadata.GetTableRowSize(TableIndex.MethodDef);
int tableOffset = metadata.GetTableMetadataOffset(TableIndex.MethodDef);
var reader = metadata.AsBlobReader();
foreach (var handle in metadata.MethodDefinitions)
{
reader.Offset = tableOffset + rowSize * MetadataTokens.GetRowNumber(handle) - paramListSize;
int paramList = paramListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (handle, paramList);
}
}
public static IEnumerable<EntityHandle> GetFieldLayouts(this MetadataReader metadata)
{
var rowCount = metadata.GetTableRowCount(TableIndex.FieldLayout);
for (int row = 1; row <= rowCount; row++)
{
yield return MetadataTokens.EntityHandle(TableIndex.FieldLayout, row);
}
}
public static (int Offset, FieldDefinitionHandle FieldDef) GetFieldLayout(this MetadataReader metadata, EntityHandle fieldLayoutHandle)
{
var startPointer = metadata.AsReadOnlySpan();
int offset = metadata.GetTableMetadataOffset(TableIndex.FieldLayout);
int rowSize = metadata.GetTableRowSize(TableIndex.FieldLayout);
int rowCount = metadata.GetTableRowCount(TableIndex.FieldLayout);
int fieldRowNo = metadata.GetRowNumber(fieldLayoutHandle);
bool small = metadata.GetTableRowCount(TableIndex.Field) <= ushort.MaxValue;
for (int row = rowCount - 1; row >= 0; row--)
{
ReadOnlySpan<byte> ptr = startPointer.Slice(offset + rowSize * row);
var rowNoSpan = ptr.Slice(4);
uint rowNo = small ? BinaryPrimitives.ReadUInt16LittleEndian(rowNoSpan) : BinaryPrimitives.ReadUInt32LittleEndian(rowNoSpan);
if (fieldRowNo == rowNo)
{
return (BinaryPrimitives.ReadInt32LittleEndian(ptr), MetadataTokens.FieldDefinitionHandle(fieldRowNo));
}
}
return (0, default);
}
public static ReadOnlySpan<byte> AsReadOnlySpan(this MetadataReader metadataReader)
{
unsafe
{
return new(metadataReader.MetadataPointer, metadataReader.MetadataLength);
}
}
public static BlobReader AsBlobReader(this MetadataReader metadataReader)
{
unsafe
{
return new(metadataReader.MetadataPointer, metadataReader.MetadataLength);
}
}
public static uint ReadULEB128(this BinaryReader reader)
{
uint val = 0;
@@ -0,0 +1,429 @@
// Copyright (c) 2018 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
namespace ICSharpCode.Decompiler.Metadata
{
public static class RawMetadataExtensions
{
/// <summary>
/// Enumerates the StateMachineMethod table of a Portable PDB, yielding the MoveNext
/// method and the kickoff method of each row. SRM exposes the table only through
/// <c>MethodDebugInformation.GetStateMachineKickoffMethod</c>, which cannot enumerate
/// the rows, so the columns are read directly.
/// </summary>
public static IEnumerable<(MethodDefinitionHandle MoveNextMethod, MethodDefinitionHandle KickoffMethod)> GetStateMachineMethods(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.StateMachineMethod);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.StateMachineMethod);
int methodSize = metadata.GetTableRowSize(TableIndex.StateMachineMethod) / 2;
CheckRowSize(metadata, TableIndex.StateMachineMethod, 2 * methodSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int moveNextRow = methodSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int kickoffRow = methodSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (MetadataTokens.MethodDefinitionHandle(moveNextRow),
MetadataTokens.MethodDefinitionHandle(kickoffRow));
}
}
public static IEnumerable<(Handle Handle, MethodSemanticsAttributes Semantics, MethodDefinitionHandle Method, EntityHandle Association)> GetMethodSemantics(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.MethodSemantics);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.MethodSemantics);
int methodSize = SimpleIndexSize(metadata, TableIndex.MethodDef);
// HasSemantics coded index: 1 tag bit over Event (tag 0) and Property (tag 1).
int assocSize = CodedIndexSize(metadata, 1, TableIndex.Event, TableIndex.Property);
CheckRowSize(metadata, TableIndex.MethodSemantics, 2 + methodSize + assocSize);
for (int rid = 1; rid <= rowCount; rid++)
{
var semantics = (MethodSemanticsAttributes)reader.ReadUInt16();
int methodRow = methodSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int assocTag = assocSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
EntityHandle propOrEvent;
if ((assocTag & 0x1) == 1)
{
propOrEvent = MetadataTokens.PropertyDefinitionHandle(assocTag >> 1);
}
else
{
propOrEvent = MetadataTokens.EventDefinitionHandle(assocTag >> 1);
}
yield return (MetadataTokens.Handle(((int)TableIndex.MethodSemantics << 24) | rid), semantics, MetadataTokens.MethodDefinitionHandle(methodRow), propOrEvent);
}
}
/// <summary>
/// Size in bytes of a column indexing <paramref name="table"/> directly
/// (ECMA-335 II.24.2.6: 2 bytes while the table has fewer than 2^16 rows).
/// </summary>
static int SimpleIndexSize(MetadataReader metadata, TableIndex table)
{
return metadata.GetTableRowCount(table) < (1 << 16) ? 2 : 4;
}
/// <summary>
/// Size in bytes of a coded-index column over <paramref name="tables"/> with
/// <paramref name="tagBits"/> tag bits (ECMA-335 II.24.2.6: 2 bytes while every
/// indexed table has fewer than 2^(16 - tagBits) rows).
/// </summary>
static int CodedIndexSize(MetadataReader metadata, int tagBits, params TableIndex[] tables)
{
int smallLimit = 1 << (16 - tagBits);
foreach (var table in tables)
{
if (metadata.GetTableRowCount(table) >= smallLimit)
return 4;
}
return 2;
}
/// <summary>
/// Size in bytes of the FieldList/MethodList/ParamList member-range columns. SRM's rule
/// (MetadataReader.InitializeTableReaders): the column is wide when the *Ptr table itself
/// is large; otherwise the member table's row count governs the width, whether or not the
/// indirection is present.
/// </summary>
static int ListColumnSize(MetadataReader metadata, TableIndex ptrTable, TableIndex memberTable)
{
return SimpleIndexSize(metadata, ptrTable) > 2 ? 4 : SimpleIndexSize(metadata, memberTable);
}
/// <summary>
/// Size in bytes of a #Strings heap index column. The width is declared by the HeapSizes
/// flags in the tables-stream header, not derived from the heap's size (a producer may set
/// the large-heap flag over a small heap). SRM does not expose the flags, but the ModuleRef
/// row consists of a single String column, so its computed row size is exactly this width.
/// </summary>
static int StringIndexSize(MetadataReader metadata)
{
return metadata.GetTableRowSize(TableIndex.ModuleRef);
}
/// <summary>
/// Size in bytes of a #Blob heap index column (see <see cref="StringIndexSize"/>; the
/// TypeSpec row consists of a single Blob column).
/// </summary>
static int BlobIndexSize(MetadataReader metadata)
{
return metadata.GetTableRowSize(TableIndex.TypeSpec);
}
/// <summary>
/// Verifies that the computed column widths sum to the row size SRM derived from the
/// tables-stream header. A width bug reads plausible-looking garbage rather than failing,
/// and some width rules are not reproducible through public API at all (EnC minimal-delta
/// metadata forces every table and coded index to 4 bytes), so fail loudly on mismatch.
/// </summary>
static void CheckRowSize(MetadataReader metadata, TableIndex table, int computedRowSize)
{
int actualRowSize = metadata.GetTableRowSize(table);
if (computedRowSize != actualRowSize)
throw new BadImageFormatException($"Unexpected {table} row size: computed {computedRowSize}, actual {actualRowSize}.");
}
public static IEnumerable<(int PackingSize, uint ClassSize, TypeDefinitionHandle Parent)> GetClassLayouts(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.ClassLayout);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.ClassLayout);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
CheckRowSize(metadata, TableIndex.ClassLayout, 2 + 4 + typeDefSize);
for (int rid = 1; rid <= rowCount; rid++)
{
ushort packingSize = reader.ReadUInt16();
uint classSize = reader.ReadUInt32();
int parentRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (packingSize, classSize, MetadataTokens.TypeDefinitionHandle(parentRow));
}
}
public static IEnumerable<(int Offset, FieldDefinitionHandle Field)> GetFieldLayoutRows(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.FieldLayout);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.FieldLayout);
int fieldSize = SimpleIndexSize(metadata, TableIndex.Field);
CheckRowSize(metadata, TableIndex.FieldLayout, 4 + fieldSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int offset = reader.ReadInt32();
int fieldRow = fieldSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (offset, MetadataTokens.FieldDefinitionHandle(fieldRow));
}
}
public static IEnumerable<(TypeDefinitionHandle Parent, EventDefinitionHandle EventList)> GetEventMaps(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.EventMap);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.EventMap);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
int eventListSize = ListColumnSize(metadata, TableIndex.EventPtr, TableIndex.Event);
CheckRowSize(metadata, TableIndex.EventMap, typeDefSize + eventListSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int parentRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int eventListRow = eventListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (MetadataTokens.TypeDefinitionHandle(parentRow), MetadataTokens.EventDefinitionHandle(eventListRow));
}
}
public static IEnumerable<(TypeDefinitionHandle Parent, PropertyDefinitionHandle PropertyList)> GetPropertyMaps(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.PropertyMap);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.PropertyMap);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
int propertyListSize = ListColumnSize(metadata, TableIndex.PropertyPtr, TableIndex.Property);
CheckRowSize(metadata, TableIndex.PropertyMap, typeDefSize + propertyListSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int parentRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int propertyListRow = propertyListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (MetadataTokens.TypeDefinitionHandle(parentRow), MetadataTokens.PropertyDefinitionHandle(propertyListRow));
}
}
public static IEnumerable<(TypeDefinitionHandle NestedClass, TypeDefinitionHandle EnclosingClass)> GetNestedClasses(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.NestedClass);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.NestedClass);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
CheckRowSize(metadata, TableIndex.NestedClass, 2 * typeDefSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int nestedRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int enclosingRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (MetadataTokens.TypeDefinitionHandle(nestedRow), MetadataTokens.TypeDefinitionHandle(enclosingRow));
}
}
public static IEnumerable<(int RelativeVirtualAddress, FieldDefinitionHandle Field)> GetFieldRvas(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.FieldRva);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.FieldRva);
int fieldSize = SimpleIndexSize(metadata, TableIndex.Field);
CheckRowSize(metadata, TableIndex.FieldRva, 4 + fieldSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int rva = reader.ReadInt32();
int fieldRow = fieldSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (rva, MetadataTokens.FieldDefinitionHandle(fieldRow));
}
}
public static IEnumerable<(EntityHandle Parent, BlobHandle NativeType)> GetFieldMarshals(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.FieldMarshal);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.FieldMarshal);
// HasFieldMarshal coded index: 1 tag bit over Field (tag 0) and Param (tag 1).
int parentSize = CodedIndexSize(metadata, 1, TableIndex.Field, TableIndex.Param);
int blobSize = BlobIndexSize(metadata);
CheckRowSize(metadata, TableIndex.FieldMarshal, parentSize + blobSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int parentTag = parentSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int blobOffset = blobSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
EntityHandle parent = (parentTag & 0x1) == 1
? MetadataTokens.ParameterHandle(parentTag >> 1)
: MetadataTokens.FieldDefinitionHandle(parentTag >> 1);
yield return (parent, MetadataTokens.BlobHandle(blobOffset));
}
}
public static IEnumerable<(MethodImportAttributes MappingFlags, EntityHandle MemberForwarded, StringHandle ImportName, ModuleReferenceHandle ImportScope)> GetImplMaps(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.ImplMap);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.ImplMap);
// MemberForwarded coded index: 1 tag bit over Field (tag 0) and MethodDef (tag 1).
int memberForwardedSize = CodedIndexSize(metadata, 1, TableIndex.Field, TableIndex.MethodDef);
int stringSize = StringIndexSize(metadata);
int moduleRefSize = SimpleIndexSize(metadata, TableIndex.ModuleRef);
CheckRowSize(metadata, TableIndex.ImplMap, 2 + memberForwardedSize + stringSize + moduleRefSize);
for (int rid = 1; rid <= rowCount; rid++)
{
var mappingFlags = (MethodImportAttributes)reader.ReadUInt16();
int memberForwardedTag = memberForwardedSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int importNameOffset = stringSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int importScopeRow = moduleRefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
EntityHandle memberForwarded = (memberForwardedTag & 0x1) == 1
? MetadataTokens.MethodDefinitionHandle(memberForwardedTag >> 1)
: MetadataTokens.FieldDefinitionHandle(memberForwardedTag >> 1);
yield return (mappingFlags, memberForwarded, MetadataTokens.StringHandle(importNameOffset), MetadataTokens.ModuleReferenceHandle(importScopeRow));
}
}
public static IEnumerable<(TypeDefinitionHandle Class, EntityHandle Interface)> GetInterfaceImplRows(this MetadataReader metadata)
{
int rowCount = metadata.GetTableRowCount(TableIndex.InterfaceImpl);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.InterfaceImpl);
int typeDefSize = SimpleIndexSize(metadata, TableIndex.TypeDef);
// TypeDefOrRef coded index: 2 tag bits over TypeDef (0), TypeRef (1) and TypeSpec (2).
int interfaceSize = CodedIndexSize(metadata, 2, TableIndex.TypeDef, TableIndex.TypeRef, TableIndex.TypeSpec);
CheckRowSize(metadata, TableIndex.InterfaceImpl, typeDefSize + interfaceSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int classRow = typeDefSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int interfaceTag = interfaceSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
EntityHandle iface = (interfaceTag & 0x3) switch {
0 => MetadataTokens.TypeDefinitionHandle(interfaceTag >> 2),
1 => MetadataTokens.TypeReferenceHandle(interfaceTag >> 2),
_ => MetadataTokens.TypeSpecificationHandle(interfaceTag >> 2),
};
yield return (MetadataTokens.TypeDefinitionHandle(classRow), iface);
}
}
/// <summary>
/// Enumerates one of the five *Ptr indirection tables (EventPtr, FieldPtr, MethodPtr,
/// ParamPtr, PropertyPtr), yielding the referenced entity of each row.
/// </summary>
public static IEnumerable<EntityHandle> GetPtrRows(this MetadataReader metadata, TableIndex ptrTable)
{
TableIndex referencedTable = ptrTable switch {
TableIndex.EventPtr => TableIndex.Event,
TableIndex.FieldPtr => TableIndex.Field,
TableIndex.MethodPtr => TableIndex.MethodDef,
TableIndex.ParamPtr => TableIndex.Param,
TableIndex.PropertyPtr => TableIndex.Property,
_ => throw new ArgumentOutOfRangeException(nameof(ptrTable)),
};
int rowCount = metadata.GetTableRowCount(ptrTable);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(ptrTable);
int handleSize = SimpleIndexSize(metadata, referencedTable);
CheckRowSize(metadata, ptrTable, handleSize);
for (int rid = 1; rid <= rowCount; rid++)
{
int row = handleSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return MetadataTokens.EntityHandle(((int)referencedTable << 24) | row);
}
}
/// <summary>
/// Reads the FieldList/MethodList columns of the TypeDef table, which SRM does not expose:
/// the stored value is the running list position (the next type's first member row, or one
/// past the member table's end), never 0. The columns sit at the end of the row, so they are
/// read relative to the row end to avoid re-deriving the widths of the preceding columns.
/// </summary>
public static IEnumerable<(TypeDefinitionHandle Type, int FieldList, int MethodList)> GetTypeDefListColumns(this MetadataReader metadata)
{
int fieldListSize = ListColumnSize(metadata, TableIndex.FieldPtr, TableIndex.Field);
int methodListSize = ListColumnSize(metadata, TableIndex.MethodPtr, TableIndex.MethodDef);
// Flags + Name + Namespace + Extends (TypeDefOrRef coded index) precede the list columns.
CheckRowSize(metadata, TableIndex.TypeDef,
4 + 2 * StringIndexSize(metadata)
+ CodedIndexSize(metadata, 2, TableIndex.TypeDef, TableIndex.TypeRef, TableIndex.TypeSpec)
+ fieldListSize + methodListSize);
int rowSize = metadata.GetTableRowSize(TableIndex.TypeDef);
int tableOffset = metadata.GetTableMetadataOffset(TableIndex.TypeDef);
var reader = metadata.AsBlobReader();
foreach (var handle in metadata.TypeDefinitions)
{
reader.Offset = tableOffset + rowSize * MetadataTokens.GetRowNumber(handle) - fieldListSize - methodListSize;
int fieldList = fieldListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int methodList = methodListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (handle, fieldList, methodList);
}
}
/// <summary>
/// Reads the ParamList column of the MethodDef table (see <see cref="GetTypeDefListColumns"/>
/// for why SRM cannot provide it).
/// </summary>
public static IEnumerable<(MethodDefinitionHandle Method, int ParamList)> GetMethodDefParamLists(this MetadataReader metadata)
{
int paramListSize = ListColumnSize(metadata, TableIndex.ParamPtr, TableIndex.Param);
// RVA + ImplFlags + Flags + Name + Signature precede the ParamList column.
CheckRowSize(metadata, TableIndex.MethodDef,
4 + 2 + 2 + StringIndexSize(metadata) + BlobIndexSize(metadata) + paramListSize);
int rowSize = metadata.GetTableRowSize(TableIndex.MethodDef);
int tableOffset = metadata.GetTableMetadataOffset(TableIndex.MethodDef);
var reader = metadata.AsBlobReader();
foreach (var handle in metadata.MethodDefinitions)
{
reader.Offset = tableOffset + rowSize * MetadataTokens.GetRowNumber(handle) - paramListSize;
int paramList = paramListSize == 2 ? reader.ReadUInt16() : reader.ReadInt32();
yield return (handle, paramList);
}
}
public static IEnumerable<EntityHandle> GetFieldLayouts(this MetadataReader metadata)
{
var rowCount = metadata.GetTableRowCount(TableIndex.FieldLayout);
for (int row = 1; row <= rowCount; row++)
{
yield return MetadataTokens.EntityHandle(TableIndex.FieldLayout, row);
}
}
public static (int Offset, FieldDefinitionHandle FieldDef) GetFieldLayout(this MetadataReader metadata, EntityHandle fieldLayoutHandle)
{
var startPointer = metadata.AsReadOnlySpan();
int offset = metadata.GetTableMetadataOffset(TableIndex.FieldLayout);
int rowSize = metadata.GetTableRowSize(TableIndex.FieldLayout);
int rowCount = metadata.GetTableRowCount(TableIndex.FieldLayout);
int fieldRowNo = metadata.GetRowNumber(fieldLayoutHandle);
bool small = metadata.GetTableRowCount(TableIndex.Field) <= ushort.MaxValue;
for (int row = rowCount - 1; row >= 0; row--)
{
ReadOnlySpan<byte> ptr = startPointer.Slice(offset + rowSize * row);
var rowNoSpan = ptr.Slice(4);
uint rowNo = small ? BinaryPrimitives.ReadUInt16LittleEndian(rowNoSpan) : BinaryPrimitives.ReadUInt32LittleEndian(rowNoSpan);
if (fieldRowNo == rowNo)
{
return (BinaryPrimitives.ReadInt32LittleEndian(ptr), MetadataTokens.FieldDefinitionHandle(fieldRowNo));
}
}
return (0, default);
}
public static ReadOnlySpan<byte> AsReadOnlySpan(this MetadataReader metadataReader)
{
unsafe
{
return new(metadataReader.MetadataPointer, metadataReader.MetadataLength);
}
}
public static BlobReader AsBlobReader(this MetadataReader metadataReader)
{
unsafe
{
return new(metadataReader.MetadataPointer, metadataReader.MetadataLength);
}
}
}
}
+22
View File
@@ -35,6 +35,9 @@ namespace ICSharpCode.Decompiler
{
public static partial class SRMExtensions
{
internal const GenericParameterAttributes AllowByRefLike = (GenericParameterAttributes)0x0020;
internal const MethodImplAttributes MethodImplAsync = (MethodImplAttributes)0x2000;
public static bool HasFlag(this TypeDefinition typeDefinition, TypeAttributes attribute)
=> (typeDefinition.Attributes & attribute) == attribute;
public static bool HasFlag(this MethodDefinition methodDefinition, MethodAttributes attribute)
@@ -161,6 +164,25 @@ namespace ICSharpCode.Decompiler
return accessors.Raiser;
}
public static ImmutableArray<MethodImplementationHandle> GetMethodImplementations(
this MethodDefinitionHandle handle, MetadataReader reader)
{
var resultBuilder = ImmutableArray.CreateBuilder<MethodImplementationHandle>();
var typeDefinition = reader.GetTypeDefinition(reader.GetMethodDefinition(handle)
.GetDeclaringType());
foreach (var methodImplementationHandle in typeDefinition.GetMethodImplementations())
{
var methodImplementation = reader.GetMethodImplementation(methodImplementationHandle);
if (methodImplementation.MethodBody == handle)
{
resultBuilder.Add(methodImplementationHandle);
}
}
return resultBuilder.ToImmutable();
}
public static EntityHandle GetGenericType(this in TypeSpecification ts, MetadataReader metadata)
{
if (ts.Signature.IsNil)
-57
View File
@@ -1,57 +0,0 @@
// Copyright (c) 2018 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading.Tasks;
using ICSharpCode.Decompiler.Metadata;
namespace ICSharpCode.Decompiler
{
public static partial class SRMExtensions
{
internal const GenericParameterAttributes AllowByRefLike = (GenericParameterAttributes)0x0020;
internal const MethodImplAttributes MethodImplAsync = (MethodImplAttributes)0x2000;
public static ImmutableArray<MethodImplementationHandle> GetMethodImplementations(
this MethodDefinitionHandle handle, MetadataReader reader)
{
var resultBuilder = ImmutableArray.CreateBuilder<MethodImplementationHandle>();
var typeDefinition = reader.GetTypeDefinition(reader.GetMethodDefinition(handle)
.GetDeclaringType());
foreach (var methodImplementationHandle in typeDefinition.GetMethodImplementations())
{
var methodImplementation = reader.GetMethodImplementation(methodImplementationHandle);
if (methodImplementation.MethodBody == handle)
{
resultBuilder.Add(methodImplementationHandle);
}
}
return resultBuilder.ToImmutable();
}
}
}
@@ -197,6 +197,19 @@ namespace ICSharpCode.ILSpyCmd.Tests
Assert.That(result.Output, Does.Contain("MemberOptionSample"));
}
[Test]
public async Task NamedEntryWorksInDumpPdbMode()
{
string pdbPath = Path.ChangeExtension(testAssemblyPath, ".pdb");
var result = await RunAsync(bundlePath, "--disable-updatecheck", "--bundle-entry", "Sample.dll",
"--dump-pdb", "-usepdb:" + pdbPath);
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("Documents"));
Assert.That(result.Output, Does.Contain("BundleEntryOptionTests.cs"));
}
[Test]
public async Task UnknownEntryNameListsValidEntries()
{
@@ -0,0 +1,127 @@
// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using NUnit.Framework;
using static ICSharpCode.ILSpyCmd.Tests.CliTestRunner;
namespace ICSharpCode.ILSpyCmd.Tests
{
[TestFixture]
public class DumpPdbOptionTests
{
static readonly string testAssemblyPath = typeof(DumpPdbOptionTests).Assembly.Location;
[Test]
public async Task DumpPdbListsDocumentsAndSequencePoints()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("Documents"));
Assert.That(result.Output, Does.Contain("DumpPdbOptionTests.cs"));
// the method this assertion lives in must show up with its sequence points
Assert.That(result.Output, Does.Contain(nameof(DumpPdbListsDocumentsAndSequencePoints)));
Assert.That(result.Output, Does.Contain("IL_"));
}
[Test]
public async Task DumpPdbShowsLocalVariableNames()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
// a local of this test class, only knowable from the PDB
Assert.That(result.Output, Does.Match(@"\b(scopeProbe|result)\b"));
}
[Test]
public async Task DumpPdbJsonIsParseableAndHasMethods()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb", "--json");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
using var doc = JsonDocument.Parse(result.Output);
var root = doc.RootElement;
Assert.That(root.GetProperty("documents").GetArrayLength(), Is.GreaterThan(0));
var methods = root.GetProperty("methods");
Assert.That(methods.GetArrayLength(), Is.GreaterThan(0));
var method = methods.EnumerateArray()
.First(m => m.GetProperty("name").GetString() == nameof(ScopeProbe));
Assert.That(method.GetProperty("sequencePoints").GetArrayLength(), Is.GreaterThan(0));
}
[Test]
public async Task DumpPdbListsMethodCustomDebugInformation()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("CDI:"));
}
[Test]
public async Task DumpPdbJsonListsMethodCustomDebugInformation()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb", "--json");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
using var doc = JsonDocument.Parse(result.Output);
var methods = doc.RootElement.GetProperty("methods").EnumerateArray();
Assert.That(methods.Any(m => m.GetProperty("customDebugInformation").GetArrayLength() > 0), Is.True);
}
[Test]
public async Task DumpPdbReportsMissingSymbols()
{
// a reference assembly ships without any PDB next to it
string noPdbAssembly = typeof(object).Assembly.Location;
var result = await RunAsync(noPdbAssembly, "--disable-updatecheck", "--dump-pdb");
Assert.That(result.ExitCode, Is.Not.EqualTo(0));
Assert.That(result.Error, Does.Contain("No debug symbols"));
}
[Test]
public async Task DumpPdbAcceptsExplicitPdbPath()
{
string pdbPath = Path.ChangeExtension(testAssemblyPath, ".pdb");
Assert.That(File.Exists(pdbPath), Is.True, pdbPath);
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-pdb", "-usepdb:" + pdbPath);
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("DumpPdbOptionTests.cs"));
}
/// <summary>
/// Gives <see cref="DumpPdbShowsLocalVariableNames"/> a named local to find in the dump.
/// </summary>
public static int ScopeProbe()
{
int scopeProbe = 42;
return scopeProbe;
}
}
}
@@ -183,6 +183,91 @@ namespace ICSharpCode.ILSpyCmd.Tests
}
}
[Test]
public async Task DocumentTableListsSourceFiles()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-table", "Document");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("DumpTableOptionTests.cs"));
Assert.That(result.Output, Does.Contain("HashAlgorithm"));
}
[Test]
public async Task LocalVariableTableContainsNamedLocal()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-table", "LocalVariable");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Match(@"\b(scopeProbe|result)\b"));
}
[Test]
public async Task MethodDebugInformationTableHasRows()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-table", "MethodDebugInformation");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("SequencePoints"));
Assert.That(result.Output, Does.Contain("LocalSignature"));
Assert.That(result.Output, Does.Not.Contain("0 rows"));
}
[Test]
public async Task CustomDebugInformationTableNamesKnownKinds()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "--dump-table", "CustomDebugInformation");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("KindName"));
// every Roslyn-built assembly carries the compilation-options record
Assert.That(result.Output, Does.Contain("Compilation Options"));
}
[Test]
public async Task DebugTablesCanBeDumpedFromStandalonePdb()
{
string pdbPath = Path.ChangeExtension(testAssemblyPath, ".pdb");
Assert.That(File.Exists(pdbPath), Is.True, pdbPath);
var result = await RunAsync(pdbPath, "--disable-updatecheck", "--dump-table", "Document");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("DumpTableOptionTests.cs"));
}
[Test]
public async Task CorTableOfAnAssemblyIsEmptyInItsPdb()
{
string pdbPath = Path.ChangeExtension(testAssemblyPath, ".pdb");
var result = await RunAsync(pdbPath, "--disable-updatecheck", "--dump-table", "TypeDef");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(result.Output, Does.Contain("0 rows"));
}
[Test]
public async Task DebugTableFromAssemblyWithoutPortablePdbReportsMissingSymbols()
{
string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(tempDir);
try
{
string assemblyPath = Path.Combine(tempDir, Path.GetFileName(testAssemblyPath));
File.Copy(testAssemblyPath, assemblyPath);
var result = await RunAsync(assemblyPath, "--disable-updatecheck", "--dump-table", "Document");
Assert.That(result.ExitCode, Is.EqualTo(ProgramExitCodes.EX_NOINPUT));
Assert.That(result.Error, Does.Contain("No Portable PDB debug metadata"));
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Test]
public async Task TableNameIsCaseInsensitive()
{
+62 -10
View File
@@ -42,6 +42,7 @@ using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Solution;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.MermaidDiagrammer;
using ICSharpCode.ILSpyX.Metadata;
using ICSharpCode.ILSpyX.PdbProvider;
using McMaster.Extensions.CommandLineUtils;
@@ -151,7 +152,10 @@ Examples:
[Option("--dump-table <table>", "Dump a metadata table: prints RID, token, names, heap offsets and coded indexes of every row. <table> is the ECMA-335 table name (e.g. TypeDef, Property, MethodSemantics; case-insensitive) or table number (decimal or 0x-prefixed hex, e.g. 0x17).", CommandOptionType.SingleValue)]
public string DumpTableName { get; }
[Option("--json", "Output as JSON. Currently only supported together with --dump-table.", CommandOptionType.NoValue)]
[Option("--dump-pdb", "Dump the debug information of the assembly's PDB: documents, per-method sequence points, the scope tree with its variables, constants and imports, and custom debug information. Reads both Portable and Windows PDBs. Use -usepdb:<file> to point at a PDB that is not next to the assembly.", CommandOptionType.NoValue)]
public bool DumpPdbFlag { get; }
[Option("--json", "Output as JSON. Only supported together with --dump-table or --dump-pdb.", CommandOptionType.NoValue)]
public bool JsonOutputFlag { get; }
public string DecompilerVersion => "ilspycmd: " + typeof(ILSpyCmdProgram).Assembly.GetName().Version.ToString() +
@@ -260,9 +264,9 @@ Examples:
Directory.CreateDirectory(outputDirectory);
}
if (JsonOutputFlag && DumpTableName == null)
if (JsonOutputFlag && DumpTableName == null && !DumpPdbFlag)
{
app.Error.WriteLine("The --json option is currently only supported together with --dump-table.");
app.Error.WriteLine("The --json option is only supported together with --dump-table or --dump-pdb.");
return ProgramExitCodes.EX_USAGE;
}
@@ -425,19 +429,40 @@ Examples:
return ProgramExitCodes.EX_USAGE;
}
using var tableModule = LoadInputModule(fileName);
// a standalone Portable PDB is a valid input here: it carries the debug tables
using var tableModule = InputFileLoader.LoadMetadata(fileName, BundleEntryName);
try
{
if (outputDirectory != null)
{
// per-file writer, disposed here: the shared 'output' is only closed once
// at the end of the run, which would lose the buffered tail of every file
// but the last when dumping multiple assemblies
string outputName = Path.GetFileNameWithoutExtension(fileName);
using var tableOutput = File.CreateText(Path.Combine(outputDirectory, outputName) + $".{table}.{(JsonOutputFlag ? "json" : "txt")}");
return MetadataTableDumper.DumpTable(tableModule, tableOutput, table, JsonOutputFlag);
}
return MetadataTableDumper.DumpTable(tableModule, output, table, JsonOutputFlag);
}
catch (MetadataTableDumper.NoPortableDebugMetadataException ex)
{
app.Error.WriteLine(ex.Message);
return ProgramExitCodes.EX_NOINPUT;
}
}
else if (DumpPdbFlag)
{
if (outputDirectory != null)
{
// per-file writer, disposed here: the shared 'output' is only closed once
// at the end of the run, which would lose the buffered tail of every file
// but the last when dumping multiple assemblies
// per-file writer, disposed here, for the same reason as in --dump-table
string outputName = Path.GetFileNameWithoutExtension(fileName);
using var tableOutput = File.CreateText(Path.Combine(outputDirectory, outputName) + $".{table}.{(JsonOutputFlag ? "json" : "txt")}");
return MetadataTableDumper.DumpTable(tableModule, tableOutput, table, JsonOutputFlag);
using var pdbOutput = File.CreateText(Path.Combine(outputDirectory, outputName) + $".pdb.{(JsonOutputFlag ? "json" : "txt")}");
return DumpPdb(fileName, pdbOutput, app);
}
return MetadataTableDumper.DumpTable(tableModule, output, table, JsonOutputFlag);
return DumpPdb(fileName, output, app);
}
else
{
@@ -1264,6 +1289,33 @@ Examples:
return 0;
}
int DumpPdb(string assemblyFileName, TextWriter output, CommandLineApplication app)
{
try
{
string pdbFileName = InputPDBFile.IsSet ? InputPDBFile.Value : null;
if (InputFileLoader.TryLoadPackageEntry(assemblyFileName, BundleEntryName, applyWinRTProjections: false, out var module))
{
using (module)
{
var image = module.Reader.GetEntireImage().GetContent().ToArray();
using var assemblyStream = new MemoryStream(image, writable: false);
PdbDumper.Dump(assemblyStream, module.FileName, pdbFileName, output, JsonOutputFlag);
}
}
else
{
PdbDumper.Dump(assemblyFileName, pdbFileName, output, JsonOutputFlag);
}
}
catch (PdbDumper.NoDebugSymbolsException ex)
{
app.Error.WriteLine(ex.Message);
return ProgramExitCodes.EX_NOINPUT;
}
return 0;
}
IDebugInfoProvider TryLoadPDB(PEFile module)
{
if (InputPDBFile.IsSet)
+34 -2
View File
@@ -63,6 +63,21 @@ namespace ICSharpCode.ILSpyCmd
/// The file is a package and <paramref name="entryName"/> is null or unknown.
/// </exception>
public static PEFile Load(string fileName, string entryName, bool applyWinRTProjections = true)
{
var file = LoadMetadata(fileName, entryName, applyWinRTProjections);
if (file is PEFile module)
{
return module;
}
file.Dispose();
throw new BadImageFormatException($"'{fileName}' contains metadata, but no PE image.");
}
/// <summary>
/// Like <see cref="Load"/>, but also accepts a file that carries metadata without a PE
/// image around it: a standalone Portable PDB, or a raw metadata blob.
/// </summary>
public static MetadataFile LoadMetadata(string fileName, string entryName, bool applyWinRTProjections = true)
{
var context = new FileLoadContext(applyWinRTProjections, null);
var result = LoadFile(fileName, context);
@@ -70,15 +85,32 @@ namespace ICSharpCode.ILSpyCmd
{
return LoadPackageEntry(package, entryName, context);
}
if (result?.MetadataFile is PEFile module)
if (result?.MetadataFile is { } file)
{
return module;
return file;
}
// Not a file any loader recognized: let PEFile report what is wrong with it, which is
// the error the tool has always produced for such input.
return new PEFile(fileName, metadataOptions: MetadataOptions(context));
}
/// <summary>
/// Loads the selected package entry when <paramref name="fileName"/> is a package, and
/// returns false for ordinary files so callers can keep path-based behavior.
/// </summary>
public static bool TryLoadPackageEntry(string fileName, string entryName, bool applyWinRTProjections, out PEFile module)
{
var context = new FileLoadContext(applyWinRTProjections, null);
var result = LoadFile(fileName, context);
if (result?.Package is { } package)
{
module = LoadPackageEntry(package, entryName, context);
return true;
}
module = null;
return false;
}
static LoadResult LoadFile(string fileName, FileLoadContext context)
{
using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
+5 -1
View File
@@ -45,7 +45,11 @@ Options:
indexes of every row. <table> is the ECMA-335 table name (e.g. TypeDef,
Property, MethodSemantics; case-insensitive) or table number (decimal or
0x-prefixed hex, e.g. 0x17).
--json Output as JSON. Currently only supported together with --dump-table.
--dump-pdb Dump the debug information of the assembly's PDB: documents, per-method
sequence points, the scope tree with its variables, constants and imports, and
custom debug information. Reads both Portable and Windows PDBs. Use
-usepdb:<file> to point at a PDB that is not next to the assembly.
--json Output as JSON. Only supported together with --dump-table or --dump-pdb.
-lv|--languageversion <version> C# Language version: CSharp1, CSharp2, CSharp3, CSharp4, CSharp5, CSharp6,
CSharp7, CSharp7_1, CSharp7_2, CSharp7_3, CSharp8_0, CSharp9_0, CSharp10_0,
CSharp11_0, CSharp12_0, CSharp13_0, Preview or Latest
@@ -25,9 +25,11 @@ using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Text.Json;
using ICSharpCode.Decompiler.DebugInfo;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpyX.PdbProvider;
namespace ICSharpCode.ILSpyCmd
namespace ICSharpCode.ILSpyX.Metadata
{
/// <summary>
/// Implements the --dump-table mode: prints every row of a metadata table (RID, token,
@@ -35,9 +37,19 @@ namespace ICSharpCode.ILSpyCmd
/// The columns of every table are spelled out explicitly (in ECMA-335 declaration order)
/// so the output stays deterministic across runtime versions.
/// </summary>
internal static class MetadataTableDumper
public static class MetadataTableDumper
{
// The Cor tables the GUI's metadata view supports; EnC and Portable-PDB tables are out of scope.
public sealed class NoPortableDebugMetadataException : Exception
{
public NoPortableDebugMetadataException(string message)
: base(message)
{
}
}
// The Cor tables plus the eight Portable-PDB debug tables, matching what the GUI's
// metadata view shows. EnC tables are out of scope. A table that does not exist in the
// file being dumped simply has no rows.
static readonly TableIndex[] supportedTables = {
TableIndex.Module, TableIndex.TypeRef, TableIndex.TypeDef, TableIndex.FieldPtr,
TableIndex.Field, TableIndex.MethodPtr, TableIndex.MethodDef, TableIndex.ParamPtr,
@@ -50,6 +62,9 @@ namespace ICSharpCode.ILSpyCmd
TableIndex.FieldRva, TableIndex.Assembly, TableIndex.AssemblyRef, TableIndex.File,
TableIndex.ExportedType, TableIndex.ManifestResource, TableIndex.NestedClass,
TableIndex.GenericParam, TableIndex.MethodSpec, TableIndex.GenericParamConstraint,
TableIndex.Document, TableIndex.MethodDebugInformation, TableIndex.LocalScope,
TableIndex.LocalVariable, TableIndex.LocalConstant, TableIndex.ImportScope,
TableIndex.StateMachineMethod, TableIndex.CustomDebugInformation,
};
public static string SupportedTableNames => string.Join(", ",
@@ -71,13 +86,16 @@ namespace ICSharpCode.ILSpyCmd
&& supportedTables.Contains(table);
}
public static int DumpTable(PEFile module, TextWriter output, TableIndex table, bool asJson)
public static int DumpTable(MetadataFile module, TextWriter output, TableIndex table, bool asJson)
{
var metadata = module.Metadata;
var rows = LoadRows(metadata, table);
// the rows of a debug table live in the PDB, which for a PE file is either embedded
// in it or sits next to it; the provider owns the reader, so it outlives the dump
using var debugInfo = LoadDebugMetadata(module, table);
var source = debugInfo?.ToMetadataFile() ?? module;
var rows = LoadRows(source.Metadata, table);
if (asJson)
{
WriteJson(output, module.FileName, table, rows);
WriteJson(output, source.FileName, table, rows);
}
else
{
@@ -86,6 +104,39 @@ namespace ICSharpCode.ILSpyCmd
return 0;
}
/// <summary>
/// True for the eight Portable-PDB debug tables, whose rows are stored in debug metadata
/// rather than in the assembly.
/// </summary>
public static bool IsDebugTable(TableIndex table)
{
return table is TableIndex.Document or TableIndex.MethodDebugInformation
or TableIndex.LocalScope or TableIndex.LocalVariable or TableIndex.LocalConstant
or TableIndex.ImportScope or TableIndex.StateMachineMethod
or TableIndex.CustomDebugInformation;
}
/// <summary>
/// Finds the debug metadata belonging to <paramref name="module"/> when a debug table is
/// asked for and the module itself carries no such rows. Null when the module already is
/// (or has) the right metadata, or when the input already is standalone debug metadata.
/// </summary>
/// <exception cref="NoPortableDebugMetadataException">No Portable PDB could be found for a PE file.</exception>
static PortableDebugInfoProvider? LoadDebugMetadata(MetadataFile module, TableIndex table)
{
if (!IsDebugTable(table) || module.Metadata.GetTableRowCount(table) > 0)
return null;
if (module is not PEFile peFile)
return null;
var debugInfo = DebugInfoUtils.LoadSymbols(peFile);
if (debugInfo is PortableDebugInfoProvider portableDebugInfo)
return portableDebugInfo;
if (debugInfo is IDisposable disposableDebugInfo)
disposableDebugInfo.Dispose();
throw new NoPortableDebugMetadataException($"No Portable PDB debug metadata found for '{module.FileName}'.");
}
/// <summary>
/// A row is an ordered list of (column, value) pairs; values are either int (RID)
/// or already-formatted strings, so the console and JSON writers stay in sync.
@@ -389,6 +440,87 @@ namespace ICSharpCode.ILSpyCmd
("Constraint", constraint.Type)));
}
break;
case TableIndex.Document:
foreach (var h in metadata.Documents)
{
var document = metadata.GetDocument(h);
rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h,
("Name", document.Name),
("HashAlgorithm", document.HashAlgorithm),
("Hash", document.Hash),
("Language", document.Language)));
}
break;
case TableIndex.MethodDebugInformation:
foreach (var h in metadata.MethodDebugInformation)
{
var debugInfo = metadata.GetMethodDebugInformation(h);
rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h,
("Document", debugInfo.Document),
("SequencePoints", debugInfo.SequencePointsBlob),
("LocalSignature", (EntityHandle)debugInfo.LocalSignature)));
}
break;
case TableIndex.LocalScope:
foreach (var h in metadata.LocalScopes)
{
var scope = metadata.GetLocalScope(h);
rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h,
("Method", scope.Method),
("ImportScope", scope.ImportScope),
("VariableList", scope.GetLocalVariables().FirstOrDefault()),
("ConstantList", scope.GetLocalConstants().FirstOrDefault()),
("StartOffset", scope.StartOffset),
("Length", scope.Length)));
}
break;
case TableIndex.LocalVariable:
foreach (var h in metadata.LocalVariables)
{
var localVariable = metadata.GetLocalVariable(h);
rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h,
("Attributes", localVariable.Attributes),
("Index", localVariable.Index),
("Name", localVariable.Name)));
}
break;
case TableIndex.LocalConstant:
foreach (var h in metadata.LocalConstants)
{
var localConstant = metadata.GetLocalConstant(h);
rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h,
("Name", localConstant.Name),
("Signature", localConstant.Signature)));
}
break;
case TableIndex.ImportScope:
foreach (var h in metadata.ImportScopes)
{
var scope = metadata.GetImportScope(h);
rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h,
("Parent", scope.Parent),
("Imports", scope.ImportsBlob)));
}
break;
case TableIndex.StateMachineMethod:
foreach (var (moveNext, kickoff) in metadata.GetStateMachineMethods())
{
rows.Add(Row(metadata, ++rid, table,
("MoveNextMethod", moveNext),
("KickoffMethod", kickoff)));
}
break;
case TableIndex.CustomDebugInformation:
foreach (var h in metadata.CustomDebugInformation)
{
var debugInfo = metadata.GetCustomDebugInformation(h);
rows.Add(Row(metadata, MetadataTokens.GetRowNumber(h), h,
("Parent", debugInfo.Parent),
("Kind", debugInfo.Kind),
("KindName", GetCustomDebugInformationKindName(metadata, debugInfo.Kind)),
("Value", debugInfo.Value)));
}
break;
case TableIndex.EventPtr:
case TableIndex.FieldPtr:
case TableIndex.MethodPtr:
@@ -463,6 +595,26 @@ namespace ICSharpCode.ILSpyCmd
return FormatValue(metadata, (EntityHandle)pdh);
case ModuleReferenceHandle mrh:
return FormatValue(metadata, (EntityHandle)mrh);
case StandaloneSignatureHandle ssh:
return FormatValue(metadata, (EntityHandle)ssh);
case DocumentHandle dh:
return FormatValue(metadata, (EntityHandle)dh);
case MethodDebugInformationHandle mdih:
return FormatValue(metadata, (EntityHandle)mdih);
case LocalScopeHandle lsh:
return FormatValue(metadata, (EntityHandle)lsh);
case LocalVariableHandle lvh:
return FormatValue(metadata, (EntityHandle)lvh);
case LocalConstantHandle lch:
return FormatValue(metadata, (EntityHandle)lch);
case ImportScopeHandle ish:
return FormatValue(metadata, (EntityHandle)ish);
case CustomDebugInformationHandle cdih:
return FormatValue(metadata, (EntityHandle)cdih);
// the document name is a blob in a dedicated encoding; SRM decodes it to the
// source path, which is what the row is about
case DocumentNameBlobHandle dnbh:
return dnbh.IsNil ? "" : metadata.GetString(dnbh);
case Enum e:
return e.ToString();
case bool b:
@@ -478,6 +630,13 @@ namespace ICSharpCode.ILSpyCmd
}
}
static string GetCustomDebugInformationKindName(MetadataReader metadata, GuidHandle kind)
{
if (kind.IsNil)
return "";
return KnownGuids.GetCustomDebugInformationKindName(metadata.GetGuid(kind)) ?? "Unknown";
}
static string FormatHex(int value)
{
return "0x" + value.ToString("X8", CultureInfo.InvariantCulture);
+491
View File
@@ -0,0 +1,491 @@
// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using ICSharpCode.ILSpyX.PdbProvider;
using Mono.Cecil;
using Mono.Cecil.Cil;
using KnownGuids = ICSharpCode.Decompiler.DebugInfo.KnownGuids;
namespace ICSharpCode.ILSpyX.Metadata
{
/// <summary>
/// Prints the debug information of an assembly's PDB: documents, per-method sequence points,
/// the scope tree with its variables, constants and imports, and custom debug information.
/// </summary>
/// <remarks>
/// Reading goes through Mono.Cecil, whose symbol readers cover both the Portable PDB and the
/// Windows (native MSF) format in managed code, so the dump works the same on every OS. The
/// raw table view of a Portable PDB is a different thing and belongs to
/// <see cref="MetadataTableDumper"/>; a Windows PDB has no metadata tables at all.
/// </remarks>
public static class PdbDumper
{
/// <summary>
/// Thrown when the assembly has no PDB next to it, or the given PDB cannot be read.
/// </summary>
public sealed class NoDebugSymbolsException : Exception
{
public NoDebugSymbolsException(string message, Exception? innerException = null)
: base(message, innerException)
{
}
}
/// <summary>
/// Writes the debug information of <paramref name="assemblyFileName"/> to <paramref name="output"/>.
/// </summary>
/// <param name="pdbFileName">
/// The PDB to read. When null, the symbol file next to the assembly (or embedded in it) is used.
/// </param>
/// <exception cref="NoDebugSymbolsException">No readable debug symbols were found.</exception>
public static void Dump(string assemblyFileName, string? pdbFileName, TextWriter output, bool asJson)
{
using var module = ReadModuleWithSymbols(assemblyFileName, pdbFileName);
Dump(module, assemblyFileName, output, asJson);
}
/// <summary>
/// Writes the debug information of the assembly image in <paramref name="assemblyStream"/> to <paramref name="output"/>.
/// </summary>
/// <param name="pdbFileName">
/// The PDB to read. When null, only symbols embedded in the assembly image can be used.
/// </param>
/// <exception cref="NoDebugSymbolsException">No readable debug symbols were found.</exception>
public static void Dump(Stream assemblyStream, string assemblyName, string? pdbFileName, TextWriter output, bool asJson)
{
using var module = ReadModuleWithSymbols(assemblyStream, assemblyName, pdbFileName);
Dump(module, assemblyName, output, asJson);
}
static void Dump(ModuleDefinition module, string assemblyName, TextWriter output, bool asJson)
{
var methods = EnumerateTypes(module.Types)
.SelectMany(t => t.Methods)
.Where(HasContent)
.ToList();
var documents = CollectDocuments(methods);
if (asJson)
WriteJson(output, module, assemblyName, documents, methods);
else
WriteText(output, module, assemblyName, documents, methods);
}
static ModuleDefinition ReadModuleWithSymbols(string assemblyFileName, string? pdbFileName)
{
var parameters = new ReaderParameters { ReadSymbols = true, InMemory = true };
ApplyExplicitSymbolFile(parameters, pdbFileName);
ModuleDefinition module;
try
{
module = ModuleDefinition.ReadModule(assemblyFileName, parameters);
}
catch (Exception ex) when (ex is FileNotFoundException or InvalidOperationException or BadImageFormatException)
{
throw new NoDebugSymbolsException(
$"No debug symbols could be read for '{assemblyFileName}'.", ex);
}
if (module.SymbolReader == null)
{
module.Dispose();
throw new NoDebugSymbolsException($"No debug symbols found for '{assemblyFileName}'.");
}
return module;
}
static ModuleDefinition ReadModuleWithSymbols(Stream assemblyStream, string assemblyName, string? pdbFileName)
{
var parameters = new ReaderParameters { ReadSymbols = true, InMemory = true };
ApplyExplicitSymbolFile(parameters, pdbFileName);
ModuleDefinition module;
try
{
assemblyStream.Position = 0;
module = ModuleDefinition.ReadModule(assemblyStream, parameters);
}
catch (Exception ex) when (ex is FileNotFoundException or InvalidOperationException or BadImageFormatException)
{
throw new NoDebugSymbolsException(
$"No debug symbols could be read for '{assemblyName}'.", ex);
}
if (module.SymbolReader == null)
{
module.Dispose();
throw new NoDebugSymbolsException($"No debug symbols found for '{assemblyName}'.");
}
return module;
}
static void ApplyExplicitSymbolFile(ReaderParameters parameters, string? pdbFileName)
{
if (pdbFileName == null)
return;
// SymbolStream is only honoured together with an explicit provider, and the
// provider must match the format of the file the user named
parameters.SymbolStream = File.OpenRead(pdbFileName);
parameters.SymbolReaderProvider = IsWindowsPdb(pdbFileName)
? new Mono.Cecil.Pdb.NativePdbReaderProvider()
: new Mono.Cecil.Cil.PortablePdbReaderProvider();
}
static bool IsWindowsPdb(string pdbFileName)
{
using var stream = File.OpenRead(pdbFileName);
return DebugInfoUtils.IsWindowsPdb(stream);
}
static IEnumerable<TypeDefinition> EnumerateTypes(IEnumerable<TypeDefinition> types)
{
foreach (var type in types)
{
yield return type;
foreach (var nested in EnumerateTypes(type.NestedTypes))
yield return nested;
}
}
static bool HasContent(MethodDefinition method)
{
var debugInfo = method.DebugInformation;
return debugInfo.HasSequencePoints
|| debugInfo.Scope != null
|| debugInfo.StateMachineKickOffMethod != null
|| debugInfo.HasCustomDebugInformations
|| method.HasCustomDebugInformations;
}
static IEnumerable<CustomDebugInformation> EnumerateCustomDebugInformation(MethodDefinition method)
{
return method.DebugInformation.CustomDebugInformations.Concat(method.CustomDebugInformations);
}
/// <summary>
/// Collects the documents the methods refer to. Neither symbol reader exposes the
/// document list itself, so the sequence points are the only way to reach them.
/// </summary>
static List<Document> CollectDocuments(List<MethodDefinition> methods)
{
var documents = new Dictionary<string, Document>(StringComparer.Ordinal);
foreach (var method in methods)
{
if (!method.DebugInformation.HasSequencePoints)
continue;
foreach (var point in method.DebugInformation.SequencePoints)
{
if (point.Document != null)
documents[point.Document.Url] = point.Document;
}
}
return documents.Values.OrderBy(d => d.Url, StringComparer.Ordinal).ToList();
}
static string FormatToken(MetadataToken token) => "0x" + token.ToUInt32().ToString("X8", CultureInfo.InvariantCulture);
static string FormatOffset(int offset) => "IL_" + offset.ToString("X4", CultureInfo.InvariantCulture);
/// <summary>
/// Reading <see cref="InstructionOffset.Offset"/> of an end-of-method offset throws, so
/// that case gets its own text.
/// </summary>
static string FormatOffset(InstructionOffset offset)
{
return offset.IsEndOfMethod ? "end" : FormatOffset(offset.Offset);
}
static string FormatCatchHandlerOffset(InstructionOffset offset)
{
if (offset.IsEndOfMethod)
return "end";
return offset.Offset < 0 ? "none" : FormatOffset(offset.Offset);
}
static string FormatSpan(SequencePoint point)
{
return point.IsHidden
? "hidden"
: $"({point.StartLine},{point.StartColumn})-({point.EndLine},{point.EndColumn})";
}
static string FormatHash(byte[]? hash)
{
return hash == null ? "" : string.Concat(hash.Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
}
static string FormatConstantValue(object? value)
{
return value switch {
null => "null",
string s => "\"" + s + "\"",
IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString() ?? "",
};
}
static string FormatImport(ImportTarget target)
{
var text = new StringBuilder(target.Kind.ToString());
if (target.Alias != null)
text.Append(" ").Append(target.Alias).Append(" =");
if (target.Namespace != null)
text.Append(" ").Append(target.Namespace);
if (target.Type != null)
text.Append(" ").Append(target.Type.FullName);
if (target.AssemblyReference != null)
text.Append(" [").Append(target.AssemblyReference.Name).Append("]");
return text.ToString();
}
static string DescribeCustomDebugInformation(CustomDebugInformation info)
{
return info switch {
// Cecil hands every kind it does not model back as raw bytes; name the ones
// Roslyn documents so the dump is readable
BinaryCustomDebugInformation binary =>
$"{KnownGuids.GetCustomDebugInformationKindName(info.Identifier) ?? info.Identifier.ToString()} ({binary.Data?.Length ?? 0} bytes)",
StateMachineScopeDebugInformation scopes =>
$"{info.Kind} " + string.Join(", ", scopes.Scopes.Select(s => $"{FormatOffset(s.Start)}..{FormatOffset(s.End)}")),
AsyncMethodBodyDebugInformation async =>
$"{info.Kind} catch handler {FormatCatchHandlerOffset(async.CatchHandler)}, {async.Resumes.Count} resume point(s)",
EmbeddedSourceDebugInformation embedded => $"{info.Kind} ({embedded.Content?.Length ?? 0} bytes, compressed={embedded.Compress})",
SourceLinkDebugInformation sourceLink => $"{info.Kind} {sourceLink.Content}",
_ => info.Kind.ToString(),
};
}
static void WriteText(TextWriter output, ModuleDefinition module, string assemblyFileName,
List<Document> documents, List<MethodDefinition> methods)
{
output.WriteLine($"Assembly: {assemblyFileName}");
output.WriteLine($"Symbols: {module.SymbolReader!.GetType().Name}");
output.WriteLine();
if (module.HasCustomDebugInformations)
{
// SourceLink and the compilation options are recorded against the module, not
// against any single method
output.WriteLine("Module custom debug information");
foreach (var info in module.CustomDebugInformations)
{
output.WriteLine($" {DescribeCustomDebugInformation(info)}");
}
output.WriteLine();
}
output.WriteLine($"Documents ({documents.Count})");
foreach (var document in documents)
{
output.WriteLine($" {document.Url}");
output.WriteLine($" language={document.Language} hashAlgorithm={document.HashAlgorithm} hash={FormatHash(document.Hash)}");
if (document.EmbeddedSource is { Length: > 0 } embedded)
output.WriteLine($" embeddedSource={embedded.Length} bytes");
}
output.WriteLine();
output.WriteLine($"Methods ({methods.Count})");
foreach (var method in methods)
{
var debugInfo = method.DebugInformation;
output.WriteLine($" {FormatToken(method.MetadataToken)} {method.FullName}");
if (debugInfo.StateMachineKickOffMethod != null)
output.WriteLine($" kickoff: {FormatToken(debugInfo.StateMachineKickOffMethod.MetadataToken)} {debugInfo.StateMachineKickOffMethod.FullName}");
if (debugInfo.HasSequencePoints)
{
output.WriteLine(" Sequence points:");
foreach (var point in debugInfo.SequencePoints)
{
output.WriteLine($" {FormatOffset(point.Offset)} {FormatSpan(point)} {point.Document?.Url}");
}
}
if (debugInfo.Scope != null)
WriteScopeText(output, debugInfo.Scope, " ");
foreach (var info in EnumerateCustomDebugInformation(method))
{
output.WriteLine($" CDI: {DescribeCustomDebugInformation(info)}");
}
}
}
static void WriteScopeText(TextWriter output, ScopeDebugInformation scope, string indent)
{
output.WriteLine($"{indent}Scope {FormatOffset(scope.Start)}..{FormatOffset(scope.End)}");
foreach (var variable in scope.Variables)
{
output.WriteLine($"{indent} [{variable.Index}] {variable.Name}{(variable.IsDebuggerHidden ? " (hidden)" : "")}");
}
foreach (var constant in scope.Constants)
{
output.WriteLine($"{indent} const {constant.Name} : {constant.ConstantType?.FullName} = {FormatConstantValue(constant.Value)}");
}
for (var import = scope.Import; import != null; import = import.Parent)
{
foreach (var target in import.Targets)
{
output.WriteLine($"{indent} import {FormatImport(target)}");
}
}
foreach (var nested in scope.Scopes)
{
WriteScopeText(output, nested, indent + " ");
}
}
static void WriteJson(TextWriter output, ModuleDefinition module, string assemblyFileName,
List<Document> documents, List<MethodDefinition> methods)
{
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
{
writer.WriteStartObject();
writer.WriteString("assembly", assemblyFileName);
writer.WriteString("symbolReader", module.SymbolReader!.GetType().Name);
writer.WriteStartArray("moduleCustomDebugInformation");
foreach (var info in module.CustomDebugInformations)
{
writer.WriteStartObject();
writer.WriteString("kind", info.Kind.ToString());
writer.WriteString("identifier", info.Identifier.ToString());
writer.WriteString("description", DescribeCustomDebugInformation(info));
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteStartArray("documents");
foreach (var document in documents)
{
writer.WriteStartObject();
writer.WriteString("url", document.Url);
writer.WriteString("language", document.Language.ToString());
writer.WriteString("hashAlgorithm", document.HashAlgorithm.ToString());
writer.WriteString("hash", FormatHash(document.Hash));
writer.WriteNumber("embeddedSourceLength", document.EmbeddedSource?.Length ?? 0);
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteStartArray("methods");
foreach (var method in methods)
{
var debugInfo = method.DebugInformation;
writer.WriteStartObject();
writer.WriteString("token", FormatToken(method.MetadataToken));
writer.WriteString("name", method.Name);
writer.WriteString("fullName", method.FullName);
if (debugInfo.StateMachineKickOffMethod != null)
writer.WriteString("kickoffMethod", debugInfo.StateMachineKickOffMethod.FullName);
writer.WriteStartArray("sequencePoints");
foreach (var point in debugInfo.SequencePoints)
{
writer.WriteStartObject();
writer.WriteNumber("offset", point.Offset);
writer.WriteBoolean("hidden", point.IsHidden);
writer.WriteNumber("startLine", point.StartLine);
writer.WriteNumber("startColumn", point.StartColumn);
writer.WriteNumber("endLine", point.EndLine);
writer.WriteNumber("endColumn", point.EndColumn);
writer.WriteString("document", point.Document?.Url);
writer.WriteEndObject();
}
writer.WriteEndArray();
if (debugInfo.Scope != null)
{
writer.WritePropertyName("scope");
WriteScopeJson(writer, debugInfo.Scope);
}
writer.WriteStartArray("customDebugInformation");
foreach (var info in EnumerateCustomDebugInformation(method))
{
writer.WriteStartObject();
writer.WriteString("kind", info.Kind.ToString());
writer.WriteString("identifier", info.Identifier.ToString());
writer.WriteString("description", DescribeCustomDebugInformation(info));
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteEndObject();
}
output.WriteLine(Encoding.UTF8.GetString(stream.ToArray()));
}
static void WriteScopeJson(Utf8JsonWriter writer, ScopeDebugInformation scope)
{
writer.WriteStartObject();
writer.WriteString("start", FormatOffset(scope.Start));
writer.WriteString("end", FormatOffset(scope.End));
writer.WriteStartArray("variables");
foreach (var variable in scope.Variables)
{
writer.WriteStartObject();
writer.WriteNumber("index", variable.Index);
writer.WriteString("name", variable.Name);
writer.WriteBoolean("debuggerHidden", variable.IsDebuggerHidden);
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteStartArray("constants");
foreach (var constant in scope.Constants)
{
writer.WriteStartObject();
writer.WriteString("name", constant.Name);
writer.WriteString("type", constant.ConstantType?.FullName);
writer.WriteString("value", FormatConstantValue(constant.Value));
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteStartArray("imports");
for (var import = scope.Import; import != null; import = import.Parent)
{
foreach (var target in import.Targets)
{
writer.WriteStringValue(FormatImport(target));
}
}
writer.WriteEndArray();
writer.WriteStartArray("scopes");
foreach (var nested in scope.Scopes)
{
WriteScopeJson(writer, nested);
}
writer.WriteEndArray();
writer.WriteEndObject();
}
}
}
@@ -87,6 +87,17 @@ namespace ICSharpCode.ILSpyX.PdbProvider
const string LegacyPDBPrefix = "Microsoft C/C++ MSF 7.00";
static readonly byte[] buffer = new byte[LegacyPDBPrefix.Length];
/// <summary>
/// True for a Windows (native MSF) PDB, false for a Portable PDB. Reads from the current
/// position and leaves the stream positioned after the signature.
/// </summary>
public static bool IsWindowsPdb(Stream stream)
{
var signature = new byte[LegacyPDBPrefix.Length];
return stream.Read(signature, 0, signature.Length) == signature.Length
&& System.Text.Encoding.ASCII.GetString(signature) == LegacyPDBPrefix;
}
static bool TryOpenPortablePdb(PEFile module,
[NotNullWhen(true)] out MetadataReaderProvider? provider,
[NotNullWhen(true)] out string? pdbFileName)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,174 @@
// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using AwesomeAssertions;
using ICSharpCode.ILSpyX.Metadata;
using ICSharpCode.ILSpyX.PdbProvider;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Windows.Pdb;
/// <summary>
/// Covers PdbDumper against a Windows (native MSF) PDB. Mono.Cecil reads that format in managed
/// code anywhere, but its writer is COM-only, so no such PDB can be produced off Windows: the
/// fixture is compiled here by the in-box C# compiler, which still emits the native format.
/// The Portable PDB path is covered cross-platform by the ilspycmd tests.
/// </summary>
[TestFixture]
public class NativePdbDumperTests
{
const string Source = """
using System;
public class Sample
{
public int Add(int a, int b)
{
int sum = a + b;
return sum;
}
}
""";
static string directory = null!;
static string assemblyPath = null!;
static string pdbPath = null!;
[OneTimeSetUp]
public void CompileFixture()
{
string csc = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Windows),
@"Microsoft.NET\Framework64\v4.0.30319\csc.exe");
if (!File.Exists(csc))
Assert.Ignore($"The in-box C# compiler is not present at '{csc}'.");
directory = Path.Combine(Path.GetTempPath(), "ilspy-nativepdb-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
string sourcePath = Path.Combine(directory, "Sample.cs");
File.WriteAllText(sourcePath, Source);
assemblyPath = Path.Combine(directory, "Sample.dll");
pdbPath = Path.ChangeExtension(assemblyPath, ".pdb");
// /debug:full is the native format; Roslyn would have to be told /debug:portable
var psi = new ProcessStartInfo(csc,
$"/nologo /target:library /debug:full /out:\"{assemblyPath}\" \"{sourcePath}\"") {
RedirectStandardOutput = true,
RedirectStandardError = true,
};
using var process = Process.Start(psi)!;
string output = process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd();
process.WaitForExit();
Assert.That(process.ExitCode, Is.Zero, $"csc failed: {output}");
Assert.That(File.Exists(pdbPath), Is.True, "csc did not produce a PDB.");
DebugInfoUtils.IsWindowsPdb(File.OpenRead(pdbPath)).Should()
.BeTrue("the fixture only means anything if the PDB is the native format");
}
[OneTimeTearDown]
public void DeleteFixture()
{
if (directory != null && Directory.Exists(directory))
Directory.Delete(directory, recursive: true);
}
static string DumpToString(string? explicitPdb, bool asJson)
{
var writer = new StringWriter();
PdbDumper.Dump(assemblyPath, explicitPdb, writer, asJson);
return writer.ToString();
}
[Test]
public void ReadsItWithTheNativeReader()
{
DumpToString(explicitPdb: null, asJson: false)
.Should().Contain("Symbols: NativePdbReader",
"the point of this fixture is the native format; the Portable reader would mean "
+ "the compiler emitted a Portable PDB and the suite covers nothing new");
}
[Test]
public void ListsTheDocumentAndTheMethod()
{
string dump = DumpToString(explicitPdb: null, asJson: false);
dump.Should().MatchRegex(@"Documents \(1\)", "the one source file is listed");
dump.Should().Contain("Sample.cs");
dump.Should().Contain("System.Int32 Sample::Add(System.Int32,System.Int32)",
"methods are listed by their full signature");
}
[Test]
public void ReadsAPdbNamedExplicitly()
{
DumpToString(explicitPdb: pdbPath, asJson: false)
.Should().Be(DumpToString(explicitPdb: null, asJson: false),
"naming the file next to the assembly is the same as finding it");
}
[Test]
public void DecodesSequencePoints()
{
string dump = DumpToString(explicitPdb: null, asJson: false);
// 'int sum = a + b;' is line 7 of Source, and a sequence point maps an IL offset to it.
dump.Should().MatchRegex(@"IL_[0-9a-fA-F]{4}\s+\(7,\d+\)-\(7,\d+\)",
"a sequence point carries the source span, not just the offset");
}
[Test]
public void DecodesTheScopeTree()
{
string dump = DumpToString(explicitPdb: null, asJson: false);
dump.Should().MatchRegex(@"Scope IL_[0-9a-fA-F]{4}\.\.",
"the scope tree is decoded, not skipped");
dump.Should().MatchRegex(@"\[\d+\] sum",
"a native PDB carries the local's name and slot");
}
[Test]
public void ProducesParseableJson()
{
string json = DumpToString(explicitPdb: null, asJson: true);
Action parse = () => JsonDocument.Parse(json);
parse.Should().NotThrow("--json output has to be machine-readable");
json.Should().Contain("Sample.cs");
}
[Test]
public void ReportsAnAssemblyWithoutSymbols()
{
string stripped = Path.Combine(directory, "NoSymbols.dll");
File.Copy(assemblyPath, stripped);
Action dump = () => PdbDumper.Dump(stripped, null, TextWriter.Null, asJson: false);
dump.Should().Throw<PdbDumper.NoDebugSymbolsException>(
"an assembly whose PDB is not beside it has nothing to dump");
}
}
-3
View File
@@ -1620,9 +1620,6 @@
"ICSharpCode.Decompiler": "[8.0.0-noversion, )",
"ICSharpCode.ILSpyX": "[8.0.0-noversion, )",
"McMaster.Extensions.CommandLineUtils": "[5.1.0, )",
"Microsoft.DiaSymReader": "[1.4.0, )",
"Microsoft.DiaSymReader.Converter.Xml": "[1.1.0-beta2-22171-02, )",
"Microsoft.DiaSymReader.Native": "[17.0.0-beta1.21524.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.12, )",
"NuGet.Frameworks": "[7.9.0, )",
"NuGet.Protocol": "[7.9.0, )",
File diff suppressed because it is too large Load Diff
-3
View File
@@ -215,9 +215,6 @@ namespace ICSharpCode.ILSpy.Commands
// DEBUG-only DisassembleAllCommand also moved to DecompileAllCommand.cs to keep all
// three parallel-decompile/disassemble stress-test commands in one place.
// DEBUG-only Pdb2XmlCommand moved to its own file with `#if DEBUG && WINDOWS` gating.
// On non-Windows or non-Debug builds the entry simply isn't compiled.
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._RemoveAssembliesWithLoadErrors), MenuCategory = "AssemblyList", MenuOrder = 11)]
[Shared]
[method: ImportingConstructor]
-111
View File
@@ -1,111 +0,0 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#if DEBUG && WINDOWS
using System.Collections.Generic;
using System.Composition;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpy.AssemblyTree;
using ICSharpCode.ILSpy.Docking;
using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.TreeNodes;
using Microsoft.DiaSymReader.Tools;
namespace ICSharpCode.ILSpy.Commands
{
/// <summary>
/// DEBUG-only main-menu entry: Dump every selected assembly's PDB as XML in the active
/// decompiler tab via <see cref="PdbToXmlConverter"/>. Windows-only because
/// <c>Microsoft.DiaSymReader</c> uses native COM interop. WPF gates this command
/// identically (<c>#if DEBUG &amp;&amp; WINDOWS</c>).
/// </summary>
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDumpPDBAsXML), MenuCategory = "Debug", MenuOrder = 33)]
[Shared]
sealed class Pdb2XmlCommand : SimpleCommand
{
readonly AssemblyTreeModel assemblyTreeModel;
readonly DockWorkspace dockWorkspace;
[ImportingConstructor]
public Pdb2XmlCommand(AssemblyTreeModel assemblyTreeModel, DockWorkspace dockWorkspace)
{
this.assemblyTreeModel = assemblyTreeModel;
this.dockWorkspace = dockWorkspace;
}
public override bool CanExecute(object? parameter)
{
var selected = assemblyTreeModel.SelectedItems;
return selected?.Count > 0
&& selected.All(n => n is AssemblyTreeNode asm && !asm.LoadedAssembly.HasLoadError);
}
public override void Execute(object? parameter)
{
var nodes = assemblyTreeModel.SelectedItems.OfType<AssemblyTreeNode>().ToArray();
if (nodes.Length == 0)
return;
ExecuteAsync(nodes).HandleExceptions();
}
async Task ExecuteAsync(AssemblyTreeNode[] nodes)
{
var options = PdbToXmlOptions.IncludeEmbeddedSources
| PdbToXmlOptions.IncludeMethodSpans
| PdbToXmlOptions.IncludeTokens;
// Run in a dedicated frozen tab so navigation cannot cancel this long run.
await dockWorkspace.RunInNewTabAsync("Dumping PDB as XML…", token => Task.Run(() => {
var output = new AvaloniaEditTextOutput { Title = "PDB as XML", SyntaxExtensionOverride = ".xml" };
var writer = new TextOutputWriter(output);
foreach (var node in nodes)
{
var pdbFileName = Path.ChangeExtension(node.LoadedAssembly.FileName, ".pdb");
if (!File.Exists(pdbFileName))
continue;
using var pdbStream = File.OpenRead(pdbFileName);
using var peStream = File.OpenRead(node.LoadedAssembly.FileName);
PdbToXmlConverter.ToXml(writer, pdbStream, peStream, options);
}
return output;
}, token));
}
}
/// <summary>
/// Adapter that lets <see cref="PdbToXmlConverter"/>'s TextWriter API write into our
/// <see cref="AvaloniaEditTextOutput"/> sink.
/// </summary>
internal sealed class TextOutputWriter(ICSharpCode.Decompiler.ITextOutput output) : System.IO.TextWriter
{
public override System.Text.Encoding Encoding => System.Text.Encoding.UTF8;
public override void Write(char value) => output.Write(value);
public override void Write(string? value)
{
if (value != null)
output.Write(value);
}
public override void WriteLine() => output.WriteLine();
}
}
#endif
+1 -13
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
@@ -131,18 +131,6 @@
<ProjectReference Include="..\ICSharpCode.BamlDecompiler\ICSharpCode.BamlDecompiler.csproj" />
</ItemGroup>
<!-- DEBUG-only Pdb2Xml command requires Microsoft.DiaSymReader, which uses native COM
interop and is Windows-only AT RUNTIME. The packages are referenced on every build host
(Debug only) so that restore produces an identical packages.lock.json regardless of OS;
a Windows-host-gated reference makes the lock diverge between Windows and Linux restores.
The consuming command file is still gated by DEBUG and WINDOWS, so on non-Windows the
packages are restored but unused. -->
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<PackageReference Include="Microsoft.DiaSymReader.Converter.Xml" />
<PackageReference Include="Microsoft.DiaSymReader" />
<PackageReference Include="Microsoft.DiaSymReader.Native" />
</ItemGroup>
<!-- The Debug Steps pane is a DEBUG-only diagnostic. The code-behind and viewmodel
are wrapped in `#if DEBUG`, so the C# type vanishes under Release; but Avalonia
compiles every .axaml file regardless of preprocessor state, and the XamlIl
@@ -40,16 +40,10 @@ namespace ICSharpCode.ILSpy.Metadata.DebugTables
protected override IReadOnlyList<StateMachineMethodEntry> LoadTable()
{
var list = new List<StateMachineMethodEntry>();
var metadata = metadataFile.Metadata;
var length = metadata.GetTableRowCount(TableIndex.StateMachineMethod);
var reader = metadata.AsBlobReader();
reader.Offset = metadata.GetTableMetadataOffset(TableIndex.StateMachineMethod);
int methodDefSize = metadata.GetTableRowCount(TableIndex.MethodDef) < ushort.MaxValue ? 2 : 4;
for (int rid = 1; rid <= length; rid++)
int rid = 0;
foreach (var (moveNext, kickoff) in metadataFile.Metadata.GetStateMachineMethods())
{
var moveNext = MetadataTokens.MethodDefinitionHandle(methodDefSize == 2 ? reader.ReadInt16() : reader.ReadInt32());
var kickoff = MetadataTokens.MethodDefinitionHandle(methodDefSize == 2 ? reader.ReadInt16() : reader.ReadInt32());
list.Add(new StateMachineMethodEntry(metadataFile, rid, moveNext, kickoff));
list.Add(new StateMachineMethodEntry(metadataFile, ++rid, moveNext, kickoff));
}
return list;
}
+1 -10
View File
@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
@@ -801,15 +801,6 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to DEBUG -- Dump PDB as XML.
/// </summary>
public static string DEBUGDumpPDBAsXML {
get {
return ResourceManager.GetString("DEBUGDumpPDBAsXML", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Debug Steps.
/// </summary>
-3
View File
@@ -285,9 +285,6 @@ Are you sure you want to continue?</value>
<data name="DEBUGDisassemble" xml:space="preserve">
<value>DEBUG -- Disassemble All</value>
</data>
<data name="DEBUGDumpPDBAsXML" xml:space="preserve">
<value>DEBUG -- Dump PDB as XML</value>
</data>
<data name="DebugSteps" xml:space="preserve">
<value>Debug Steps</value>
</data>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff