Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/ILInspector.Decompiler.Tests/IlProjectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Text.RegularExpressions;

using ILInspector.Decompiler.Pipeline;

namespace ILInspector.Decompiler.Tests;

public class IlProjectionTests
{
static string Project(string method, IlProjectionDepth depth)
{
using var source = MetadataSource.Open(typeof(CfgSampleClass).Assembly.Location);
var result = IlProjection.Project(source, typeof(CfgSampleClass).FullName!, method, depth);
Assert.NotNull(result.Output);
// Normalize CRLF so line-anchored assertions are platform-agnostic (the
// projection renders with Environment.NewLine).
return result.Output!.ReplaceLineEndings("\n");
}

[Fact]
public void Raw_RendersOffsetsAndOpcodes()
{
var output = Project(nameof(CfgSampleClass.Add), IlProjectionDepth.Raw);
Assert.Contains("IL_0000:", output);
Assert.Contains("add", output);
Assert.Contains("ret", output);
}

[Fact]
public void Raw_ResolvesCallOperandToName()
{
// LengthOf is `s.Length` — the method token must render resolved (the
// importer's ResolveMethod), not as a raw `0x06......` token.
var output = Project(nameof(CfgSampleClass.LengthOf), IlProjectionDepth.Raw);
Assert.Contains("get_Length", output);
Assert.DoesNotContain("0x", output);
}

[Fact]
public void Structured_LabelsBranchBlocks()
{
// A conditional splits into multiple basic blocks, each leader labeled.
var output = Project(nameof(CfgSampleClass.Pick), IlProjectionDepth.Structured);
int labels = Regex.Matches(output, @"^IL_[0-9A-F]{4}:$", RegexOptions.Multiline).Count;
Assert.True(labels >= 2, $"expected >= 2 block labels, got {labels}");
}

[Fact]
public void Structured_MarksExceptionRegions()
{
var output = Project(nameof(CfgSampleClass.CatchLogs), IlProjectionDepth.Structured);
Assert.Contains("// .try", output);
Assert.Contains("// catch", output);
}
}
254 changes: 254 additions & 0 deletions src/ILInspector.Decompiler/Pipeline/Ir/IlProjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
using System.Buffers.Binary;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Text;

namespace ILInspector.Decompiler.Pipeline;

/// <summary>Depth of an <see cref="IlProjection"/> rendering.</summary>
public enum IlProjectionDepth
{
/// <summary>Flat instruction list with resolved operands.</summary>
Raw,

/// <summary>Block-structured output with labels, indentation, and exception-region markers.</summary>
Structured,
}

/// <summary>
/// Renders ground-truth IL views from the replacement pipeline's materialized
/// method data (off a <see cref="MetadataSource"/>) — the new-pipeline backing
/// for the annotated-IL view, replacing the old
/// <c>MethodBodyContext</c> → <c>MethodAnalysis</c> → <c>AnnotatedILEmitter</c>
/// contract. Operands are resolved through the importer's own token resolvers,
/// and block structure reuses the importer's <c>FindLeaders</c>, so the views
/// share one analysis with the IR import rather than a parallel one.
///
/// The per-instruction stack-typed depth (the old <c>Typed</c> view) is added
/// separately: it sources stack types from the importer's typed operand stack
/// and so rides on a per-instruction trace, not on this token-and-structure
/// projection.
///
/// Exception-safe by construction: any malformed-metadata read or resolver
/// failure surfaces as a diagnosed <see cref="DecompilerResult"/>, never a throw.
/// </summary>
public static class IlProjection
{
public static DecompilerResult Project(
MetadataSource source, string typeFullName, string methodName,
IlProjectionDepth depth, int overloadIndex = 0, bool publicOnly = false)
=> DecompilerResult.Run(
() => Render(source, typeFullName, methodName, depth, overloadIndex, publicOnly),
emptyOutputIsFailure: true);

static string Render(MetadataSource source, string type, string method, IlProjectionDepth depth, int overloadIndex, bool publicOnly)
{
var (typeDef, methodDef, methodHandle) = Locate(source.Reader, type, method, overloadIndex, publicOnly);
var imported = MethodImporter.Import(source, (TypeDefinitionHandle)methodDef.GetDeclaringType(), methodHandle);
var scope = IrImporter.CallerScope(source.Reader, typeDef, methodDef);
var instructions = Decode(source.Reader, scope, imported.Body.IL.AsSpan());
return depth == IlProjectionDepth.Structured
? RenderStructured(instructions, imported.Body)
: RenderRaw(instructions);
}

static (TypeDefinition Type, MethodDefinition Method, MethodDefinitionHandle Handle) Locate(
MetadataReader reader, string typeFullName, string methodName, int overloadIndex, bool publicOnly)
{
foreach (var typeHandle in reader.TypeDefinitions)
{
var typeDef = reader.GetTypeDefinition(typeHandle);
string ns = reader.GetString(typeDef.Namespace);
string name = reader.GetString(typeDef.Name);
if ((ns.Length == 0 ? name : $"{ns}.{name}") != typeFullName)
continue;
int seen = 0;
foreach (var methodHandle in typeDef.GetMethods())
{
var method = reader.GetMethodDefinition(methodHandle);
if (reader.GetString(method.Name) != methodName)
continue;
if (publicOnly && (method.Attributes & System.Reflection.MethodAttributes.Public) == 0)
continue;
if (seen++ != overloadIndex)
continue;
if (method.RelativeVirtualAddress == 0)
throw new InvalidOperationException($"{typeFullName}::{methodName} has no IL body");
return (typeDef, method, methodHandle);
}
}
throw new InvalidOperationException($"{typeFullName}::{methodName} not found");
}

readonly record struct Instr(int Offset, string Name, string Operand);

static List<Instr> Decode(MetadataReader reader, GenericScope scope, ReadOnlySpan<byte> il)
{
var result = new List<Instr>();
int pos = 0;
while (pos < il.Length)
{
int offset = pos;
int b = il[pos++];
var op = b == 0xFE ? (ILOpCode)(0xFE00 | il[pos++]) : (ILOpCode)b;
string name = op.ToString().ToLowerInvariant().Replace('_', '.');
result.Add(new Instr(offset, name, ReadOperand(reader, scope, il, op, ref pos)));
}
return result;
}

static string ReadOperand(MetadataReader reader, GenericScope scope, ReadOnlySpan<byte> il, ILOpCode op, ref int pos)
{
if (op == ILOpCode.Switch)
{
int count = BinaryPrimitives.ReadInt32LittleEndian(il[pos..]); pos += 4;
int origin = pos + count * 4;
var targets = new string[count];
for (int i = 0; i < count; i++)
{
targets[i] = $"IL_{origin + BinaryPrimitives.ReadInt32LittleEndian(il[pos..]):X4}"; pos += 4;
}
return $"({string.Join(", ", targets)})";
}

int length = OperandLength(op);
if (length == 0)
return "";
var bytes = il.Slice(pos, length);
pos += length;
if (IsBranch(op))
return $"IL_{pos + (length == 1 ? (sbyte)bytes[0] : BinaryPrimitives.ReadInt32LittleEndian(bytes)):X4}";
return op switch
{
ILOpCode.Ldc_i4 => BinaryPrimitives.ReadInt32LittleEndian(bytes).ToString(),
ILOpCode.Ldc_i4_s => ((sbyte)bytes[0]).ToString(),
ILOpCode.Ldc_i8 => BinaryPrimitives.ReadInt64LittleEndian(bytes).ToString(),
ILOpCode.Ldc_r4 => BinaryPrimitives.ReadSingleLittleEndian(bytes).ToString(),
ILOpCode.Ldc_r8 => BinaryPrimitives.ReadDoubleLittleEndian(bytes).ToString(),
_ when IsMetadataToken(op) => ResolveToken(reader, scope, op, BinaryPrimitives.ReadInt32LittleEndian(bytes)),
_ when length == 1 => bytes[0].ToString(), // short var/arg index
_ when length == 2 => BinaryPrimitives.ReadUInt16LittleEndian(bytes).ToString(), // var/arg index
_ => $"0x{BinaryPrimitives.ReadUInt32LittleEndian(bytes):X8}",
};
}

/// <summary>Resolves a metadata-token operand to its display form, falling back to raw token hex if resolution fails.</summary>
static string ResolveToken(MetadataReader reader, GenericScope scope, ILOpCode op, int token)
{
try
{
if (op == ILOpCode.Ldstr)
return $"\"{reader.GetUserString(MetadataTokens.UserStringHandle(token))}\"";
var handle = MetadataTokens.EntityHandle(token);
return op switch
{
ILOpCode.Ldfld or ILOpCode.Ldflda or ILOpCode.Stfld
or ILOpCode.Ldsfld or ILOpCode.Ldsflda or ILOpCode.Stsfld => Field(reader, scope, handle),
ILOpCode.Call or ILOpCode.Callvirt or ILOpCode.Newobj
or ILOpCode.Ldftn or ILOpCode.Ldvirtftn or ILOpCode.Jmp => Method(reader, scope, handle),
ILOpCode.Castclass or ILOpCode.Isinst or ILOpCode.Box or ILOpCode.Unbox or ILOpCode.Unbox_any
or ILOpCode.Newarr or ILOpCode.Ldobj or ILOpCode.Stobj or ILOpCode.Cpobj or ILOpCode.Initobj
or ILOpCode.Constrained or ILOpCode.Sizeof or ILOpCode.Mkrefany or ILOpCode.Refanyval
or ILOpCode.Ldelem or ILOpCode.Ldelema or ILOpCode.Stelem
=> IrImporter.ResolveTypeToken(reader, handle, scope).ToDisplayString(),
ILOpCode.Ldtoken => AnyToken(reader, scope, handle),
_ => $"0x{token:X8}",
};
}
catch
{
return $"0x{token:X8}"; // resolution is best-effort; the view stays ground truth on the structure
}
}

static string AnyToken(MetadataReader reader, GenericScope scope, EntityHandle handle) => handle.Kind switch
{
HandleKind.FieldDefinition => Field(reader, scope, handle),
HandleKind.MethodDefinition or HandleKind.MethodSpecification => Method(reader, scope, handle),
HandleKind.MemberReference when reader.GetMemberReference((MemberReferenceHandle)handle).GetKind() == MemberReferenceKind.Field
=> Field(reader, scope, handle),
HandleKind.MemberReference => Method(reader, scope, handle),
_ => IrImporter.ResolveTypeToken(reader, handle, scope).ToDisplayString(),
};

static string Method(MetadataReader reader, GenericScope scope, EntityHandle handle)
{
var m = IrImporter.ResolveMethod(reader, handle, scope);
return $"{m.DeclaringType.ToDisplayString()}::{m.Name}({string.Join(", ", m.ParameterTypes.Select(p => p.ToDisplayString()))})";
}

static string Field(MetadataReader reader, GenericScope scope, EntityHandle handle)
{
var f = IrImporter.ResolveField(reader, handle, scope);
return $"{f.Type.ToDisplayString()} {f.DeclaringType.ToDisplayString()}::{f.Name}";
}

static bool IsMetadataToken(ILOpCode op) => op is
ILOpCode.Call or ILOpCode.Callvirt or ILOpCode.Newobj or ILOpCode.Ldftn or ILOpCode.Ldvirtftn or ILOpCode.Jmp
or ILOpCode.Ldfld or ILOpCode.Ldflda or ILOpCode.Stfld or ILOpCode.Ldsfld or ILOpCode.Ldsflda or ILOpCode.Stsfld
or ILOpCode.Castclass or ILOpCode.Isinst or ILOpCode.Box or ILOpCode.Unbox or ILOpCode.Unbox_any
or ILOpCode.Newarr or ILOpCode.Ldobj or ILOpCode.Stobj or ILOpCode.Cpobj or ILOpCode.Initobj
or ILOpCode.Constrained or ILOpCode.Sizeof or ILOpCode.Mkrefany or ILOpCode.Refanyval
or ILOpCode.Ldelem or ILOpCode.Ldelema or ILOpCode.Stelem or ILOpCode.Ldstr or ILOpCode.Ldtoken;

static bool IsBranch(ILOpCode op) => op is
ILOpCode.Br or ILOpCode.Br_s or ILOpCode.Brfalse or ILOpCode.Brfalse_s or ILOpCode.Brtrue or ILOpCode.Brtrue_s
or ILOpCode.Beq or ILOpCode.Beq_s or ILOpCode.Bge or ILOpCode.Bge_s or ILOpCode.Bgt or ILOpCode.Bgt_s
or ILOpCode.Ble or ILOpCode.Ble_s or ILOpCode.Blt or ILOpCode.Blt_s or ILOpCode.Bne_un or ILOpCode.Bne_un_s
or ILOpCode.Bge_un or ILOpCode.Bge_un_s or ILOpCode.Bgt_un or ILOpCode.Bgt_un_s or ILOpCode.Ble_un or ILOpCode.Ble_un_s
or ILOpCode.Blt_un or ILOpCode.Blt_un_s or ILOpCode.Leave or ILOpCode.Leave_s;

static int OperandLength(ILOpCode op) => op switch
{
ILOpCode.Ldc_i8 or ILOpCode.Ldc_r8 => 8,
ILOpCode.Ldarg or ILOpCode.Ldarga or ILOpCode.Starg or ILOpCode.Ldloc or ILOpCode.Ldloca or ILOpCode.Stloc => 2,
ILOpCode.Br_s or ILOpCode.Brfalse_s or ILOpCode.Brtrue_s or ILOpCode.Beq_s or ILOpCode.Bge_s or ILOpCode.Bgt_s
or ILOpCode.Ble_s or ILOpCode.Blt_s or ILOpCode.Bne_un_s or ILOpCode.Bge_un_s or ILOpCode.Bgt_un_s
or ILOpCode.Ble_un_s or ILOpCode.Blt_un_s or ILOpCode.Leave_s
or ILOpCode.Ldc_i4_s or ILOpCode.Ldarg_s or ILOpCode.Ldarga_s or ILOpCode.Starg_s or ILOpCode.Ldloc_s
or ILOpCode.Ldloca_s or ILOpCode.Stloc_s or ILOpCode.Unaligned => 1,
ILOpCode.Br or ILOpCode.Brfalse or ILOpCode.Brtrue or ILOpCode.Beq or ILOpCode.Bge or ILOpCode.Bgt
or ILOpCode.Ble or ILOpCode.Blt or ILOpCode.Bne_un or ILOpCode.Bge_un or ILOpCode.Bgt_un
or ILOpCode.Ble_un or ILOpCode.Blt_un or ILOpCode.Leave
or ILOpCode.Ldc_i4 or ILOpCode.Ldc_r4 or ILOpCode.Jmp
or ILOpCode.Call or ILOpCode.Calli or ILOpCode.Callvirt or ILOpCode.Newobj or ILOpCode.Ldftn
or ILOpCode.Ldvirtftn or ILOpCode.Ldfld or ILOpCode.Ldflda or ILOpCode.Stfld or ILOpCode.Ldsfld
or ILOpCode.Ldsflda or ILOpCode.Stsfld or ILOpCode.Castclass or ILOpCode.Isinst or ILOpCode.Box
or ILOpCode.Unbox or ILOpCode.Unbox_any or ILOpCode.Newarr or ILOpCode.Ldelem or ILOpCode.Ldelema
or ILOpCode.Stelem or ILOpCode.Ldobj or ILOpCode.Stobj or ILOpCode.Cpobj or ILOpCode.Initobj
or ILOpCode.Constrained or ILOpCode.Sizeof or ILOpCode.Ldtoken or ILOpCode.Ldstr
or ILOpCode.Mkrefany or ILOpCode.Refanyval => 4,
_ => 0,
};

static string RenderRaw(List<Instr> instructions)
{
var sb = new StringBuilder();
foreach (var i in instructions)
sb.AppendLine(Format(i));
return sb.ToString();
}

static string RenderStructured(List<Instr> instructions, MethodBody body)
{
var leaders = IrImporter.FindLeaders([.. body.IL], body.Handlers);
var tryStarts = body.Handlers.Select(h => h.TryOffset).ToHashSet();
var handlerStarts = body.Handlers
.GroupBy(h => h.HandlerOffset)
.ToDictionary(g => g.Key, g => g.First().Kind);
var sb = new StringBuilder();
foreach (var i in instructions)
{
if (tryStarts.Contains(i.Offset))
sb.AppendLine(" // .try");
if (handlerStarts.TryGetValue(i.Offset, out var kind))
sb.AppendLine($" // {kind.ToString().ToLowerInvariant()}");
if (leaders.Contains(i.Offset))
sb.AppendLine($"IL_{i.Offset:X4}:");
sb.AppendLine(" " + Format(i));
}
return sb.ToString();
}

static string Format(Instr i) => $"IL_{i.Offset:X4}: {i.Name}{(i.Operand.Length > 0 ? " " + i.Operand : "")}";
}
6 changes: 3 additions & 3 deletions src/ILInspector.Decompiler/Pipeline/Ir/IrImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ static IrFunction CrashFunction(string methodName, string typeName, Exception ex
return function;
}

static GenericScope CallerScope(MetadataReader reader, TypeDefinition typeDef, MethodDefinition method)
internal static GenericScope CallerScope(MetadataReader reader, TypeDefinition typeDef, MethodDefinition method)
=> new(GenericParameterNames(reader, typeDef.GetGenericParameters()),
GenericParameterNames(reader, method.GetGenericParameters()));

Expand Down Expand Up @@ -256,7 +256,7 @@ void Consider(TypeRef? type)
}

/// <summary>Block leaders: entry, branch and leave targets, instructions following a terminator, and every exception-region boundary.</summary>
static SortedSet<int> FindLeaders(ReadOnlySpan<byte> il, System.Collections.Immutable.ImmutableArray<HandlerRegion> handlers)
internal static SortedSet<int> FindLeaders(ReadOnlySpan<byte> il, System.Collections.Immutable.ImmutableArray<HandlerRegion> handlers)
{
var leaders = new SortedSet<int> { 0 };
foreach (var region in handlers)
Expand Down Expand Up @@ -1359,7 +1359,7 @@ internal static FieldRef ResolveField(MetadataReader reader, EntityHandle handle
}
}

static TypeRef ResolveTypeToken(MetadataReader reader, EntityHandle handle, GenericScope callerScope) => handle.Kind switch
internal static TypeRef ResolveTypeToken(MetadataReader reader, EntityHandle handle, GenericScope callerScope) => handle.Kind switch
{
HandleKind.TypeDefinition => TypeRefDecoder.Instance.GetTypeFromDefinition(reader, (TypeDefinitionHandle)handle, 0),
HandleKind.TypeReference => TypeRefDecoder.Instance.GetTypeFromReference(reader, (TypeReferenceHandle)handle, 0),
Expand Down
Loading