mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Merge pull request #4197 from icsharpcode/feature/dump-pdb
Add cross-platform PDB dumping to ilspycmd
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
+166
-7
@@ -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);
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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 && 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
@@ -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;
|
||||
}
|
||||
|
||||
Generated
+1
-10
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user