diff --git a/eng/Subsets.props b/eng/Subsets.props index e61c5d40cf7a4a..4adcc757513691 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -268,6 +268,7 @@ + @@ -622,6 +623,18 @@ Category="clr" /> + + + + + + + + + diff --git a/eng/pipelines/coreclr/ilasm.yml b/eng/pipelines/coreclr/ilasm.yml index a9780531f8b072..0bb786934cfd28 100644 --- a/eng/pipelines/coreclr/ilasm.yml +++ b/eng/pipelines/coreclr/ilasm.yml @@ -12,7 +12,9 @@ pr: - src/coreclr/ilasm/* - src/coreclr/ildasm/* - src/tools/ilasm/* + - src/tools/ildasm/* - src/coreclr/.nuget/Microsoft.NETCore.ILAsm/* + - src/coreclr/.nuget/Microsoft.NETCore.ILDAsm/* - src/coreclr/.nuget/package-rids.props - src/tasks/installer.tasks/* - eng/NativeAotToolPackage.targets @@ -57,7 +59,7 @@ extends: - windows_x64 jobParameters: nameSuffix: ILAsm - buildArgs: -s tools.ilasm -c $(_BuildConfig) -pack -test + buildArgs: -s tools.ilasm+tools.ildasm -c $(_BuildConfig) -pack -test timeoutInMinutes: 120 enablePublishTestResults: true testResultsFormat: 'xunit' diff --git a/src/coreclr/.nuget/Microsoft.NETCore.ILDAsm/Microsoft.NETCore.ILDAsm.Package.csproj b/src/coreclr/.nuget/Microsoft.NETCore.ILDAsm/Microsoft.NETCore.ILDAsm.Package.csproj new file mode 100644 index 00000000000000..3fe629460f7b50 --- /dev/null +++ b/src/coreclr/.nuget/Microsoft.NETCore.ILDAsm/Microsoft.NETCore.ILDAsm.Package.csproj @@ -0,0 +1,41 @@ + + + false + + + + + true + false + + + + true + Microsoft.NETCore.ILDAsm + The .NET IL Disassembler. + false + true + $(BeforePack);GenerateILDAsmRuntimeDependencies + + + + + + + + + + + + + + + + + diff --git a/src/coreclr/.nuget/Microsoft.NETCore.ILDAsm/PACKAGE.md b/src/coreclr/.nuget/Microsoft.NETCore.ILDAsm/PACKAGE.md new file mode 100644 index 00000000000000..95361c2b401d7c --- /dev/null +++ b/src/coreclr/.nuget/Microsoft.NETCore.ILDAsm/PACKAGE.md @@ -0,0 +1 @@ +The .NET IL Disassembler. diff --git a/src/tools/ildasm/ildasm.slnx b/src/tools/ildasm/ildasm.slnx new file mode 100644 index 00000000000000..2a858c1f0939e0 --- /dev/null +++ b/src/tools/ildasm/ildasm.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/src/tools/ildasm/src/ILDisassembler/Disassembler.cs b/src/tools/ildasm/src/ILDisassembler/Disassembler.cs new file mode 100644 index 00000000000000..60cced54fb5dc5 --- /dev/null +++ b/src/tools/ildasm/src/ILDisassembler/Disassembler.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace ILDisassembler; + +/// +/// Main entry point for IL disassembly. +/// Reads a PE file and outputs IL assembly syntax. +/// +public sealed class Disassembler : IDisposable +{ + private readonly PEReader _peReader; + private readonly MetadataReader _metadataReader; + private readonly string _filePath; + private readonly Options _options; + + public Disassembler(string filePath, Options? options = null) + { + _filePath = filePath; + // TODO: Wire up options to control disassembly behavior (ShowBytes, ShowTokens, etc.) + _options = options ?? new Options(); + var stream = File.OpenRead(filePath); + // PEReader takes ownership of the stream and will dispose it when PEReader is disposed + _peReader = new PEReader(stream); + + if (!_peReader.HasMetadata) + { + throw new InvalidOperationException("PE file does not contain metadata"); + } + + _metadataReader = _peReader.GetMetadataReader(); + } + + public Disassembler(Stream stream, Options? options = null) + { + _filePath = ""; + _options = options ?? new Options(); + _peReader = new PEReader(stream); + + if (!_peReader.HasMetadata) + { + throw new InvalidOperationException("PE file does not contain metadata"); + } + + _metadataReader = _peReader.GetMetadataReader(); + } + + /// + /// Disassembles the PE file to the specified output. + /// + public void Disassemble(TextWriter output) + { + var writer = new ILWriter(output); + + // Write header comment + writer.WriteComment($".NET IL Disassembler. Version {typeof(Disassembler).Assembly.GetName().Version}"); + writer.WriteLine(); + writer.WriteComment($"Metadata version: {_metadataReader.MetadataVersion}"); + + // TODO: Support --headers option to output PE headers (.imagebase, .file alignment, .stackreserve, .corflags, .subsystem) + // TODO: Support --html option to wrap output in HTML tags + // TODO: Support --rtf option to wrap output in RTF format + + // Disassemble in order: + // 1. Assembly extern references + WriteAssemblyRefs(writer); + + // 2. Assembly definition + WriteAssemblyDef(writer); + + // 3. Module definition + WriteModuleDef(writer); + + // TODO: Output manifest resources (.mresource) + // TODO: Output file references (.file) + // TODO: Output exported types (.class extern) + + // 4. Type definitions (classes, interfaces, etc.) + WriteTypeDefs(writer); + } + + private void WriteAssemblyRefs(ILWriter writer) + { + foreach (var handle in _metadataReader.AssemblyReferences) + { + var assemblyRef = _metadataReader.GetAssemblyReference(handle); + var name = _metadataReader.GetString(assemblyRef.Name); + var version = assemblyRef.Version; + + writer.WriteLine($".assembly extern {name}"); + writer.WriteLine("{"); + // TODO: Output .publickeytoken if present + // TODO: Output .culture if present + // TODO: Output .hash if present + writer.WriteLine($" .ver {version.Major}:{version.Minor}:{version.Build}:{version.Revision}"); + writer.WriteLine("}"); + } + } + + private void WriteAssemblyDef(ILWriter writer) + { + if (!_metadataReader.IsAssembly) + { + return; + } + + var assemblyDef = _metadataReader.GetAssemblyDefinition(); + var name = _metadataReader.GetString(assemblyDef.Name); + var version = assemblyDef.Version; + + writer.WriteLine($".assembly {name}"); + writer.WriteLine("{"); + // TODO: Output .publickey if present + // TODO: Output .culture if present + // TODO: Output .hash algorithm if present + // TODO: Output custom attributes on assembly + writer.WriteLine($" .ver {version.Major}:{version.Minor}:{version.Build}:{version.Revision}"); + writer.WriteLine("}"); + } + + private void WriteModuleDef(ILWriter writer) + { + var moduleDef = _metadataReader.GetModuleDefinition(); + var name = _metadataReader.GetString(moduleDef.Name); + + writer.WriteLine($".module {name}"); + writer.WriteComment($"MVID: {{{_metadataReader.GetGuid(moduleDef.Mvid)}}}"); + // TODO: Output custom attributes on module + } + + private void WriteTypeDefs(ILWriter writer) + { + // TODO: Support --typelist option to output full type list for round-trip + // TODO: Support --classlist option to output class list + // TODO: Support --visibility/--pubonly options to filter types + // TODO: Support --item option to disassemble specific item only + foreach (var handle in _metadataReader.TypeDefinitions) + { + var typeDef = _metadataReader.GetTypeDefinition(handle); + var typeWriter = new TypeDisassembler(_metadataReader, _peReader, typeDef); + typeWriter.Write(writer); + } + } + + public void Dispose() + { + _peReader.Dispose(); + } +} diff --git a/src/tools/ildasm/src/ILDisassembler/ILDisassembler.csproj b/src/tools/ildasm/src/ILDisassembler/ILDisassembler.csproj new file mode 100644 index 00000000000000..57d9dba5ab460e --- /dev/null +++ b/src/tools/ildasm/src/ILDisassembler/ILDisassembler.csproj @@ -0,0 +1,11 @@ + + + + $(NetCoreAppToolCurrent) + true + enable + true + false + + + diff --git a/src/tools/ildasm/src/ILDisassembler/ILWriter.cs b/src/tools/ildasm/src/ILDisassembler/ILWriter.cs new file mode 100644 index 00000000000000..0fbb1f13536eb3 --- /dev/null +++ b/src/tools/ildasm/src/ILDisassembler/ILWriter.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace ILDisassembler; + +/// +/// Helper class for writing IL assembly output with proper formatting. +/// +internal sealed class ILWriter +{ + private readonly TextWriter _output; + private int _indentLevel; + private const string IndentString = " "; + + public ILWriter(TextWriter output) + { + _output = output; + } + + public void Indent() => _indentLevel++; + + public void Dedent() => _indentLevel = _indentLevel > 0 ? _indentLevel - 1 : 0; + + public void WriteLine() + { + _output.WriteLine(); + } + + public void WriteLine(string line) + { + WriteIndent(); + _output.WriteLine(line); + } + + public void Write(string text) + { + _output.Write(text); + } + + public void WriteComment(string comment) + { + WriteIndent(); + _output.WriteLine($"// {comment}"); + } + + public void WriteIndent() + { + for (int i = 0; i < _indentLevel; i++) + { + _output.Write(IndentString); + } + } +} diff --git a/src/tools/ildasm/src/ILDisassembler/InstructionDisassembler.cs b/src/tools/ildasm/src/ILDisassembler/InstructionDisassembler.cs new file mode 100644 index 00000000000000..5b1057a365a28e --- /dev/null +++ b/src/tools/ildasm/src/ILDisassembler/InstructionDisassembler.cs @@ -0,0 +1,407 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Text; + +namespace ILDisassembler; + +/// +/// Disassembles IL instructions from a method body. +/// This is the core "switch on opcode" logic. +/// +public sealed class InstructionDisassembler +{ + private readonly MetadataReader _reader; + + public InstructionDisassembler(MetadataReader reader) + { + _reader = reader; + } + + /// + /// Disassembles a single instruction and advances the reader. + /// + public string DisassembleInstruction(ref BlobReader reader) + { + int opcode = reader.ReadByte(); + + // Two-byte opcodes start with 0xFE + if (opcode == 0xFE) + { + opcode = 0xFE00 + reader.ReadByte(); + } + + var ilOpcode = (ILOpCode)opcode; + + return ilOpcode switch + { + // ===== No operand instructions ===== + ILOpCode.Nop => "nop", + ILOpCode.Break => "break", + ILOpCode.Ret => "ret", + ILOpCode.Ldnull => "ldnull", + ILOpCode.Dup => "dup", + ILOpCode.Pop => "pop", + ILOpCode.Throw => "throw", + ILOpCode.Rethrow => "rethrow", + ILOpCode.Ldlen => "ldlen", + ILOpCode.Ckfinite => "ckfinite", + ILOpCode.Localloc => "localloc", + ILOpCode.Endfilter => "endfilter", + ILOpCode.Endfinally => "endfinally", + ILOpCode.Arglist => "arglist", + ILOpCode.Cpblk => "cpblk", + ILOpCode.Initblk => "initblk", + ILOpCode.Refanytype => "refanytype", + + // ===== Arithmetic ===== + ILOpCode.Add => "add", + ILOpCode.Add_ovf => "add.ovf", + ILOpCode.Add_ovf_un => "add.ovf.un", + ILOpCode.Sub => "sub", + ILOpCode.Sub_ovf => "sub.ovf", + ILOpCode.Sub_ovf_un => "sub.ovf.un", + ILOpCode.Mul => "mul", + ILOpCode.Mul_ovf => "mul.ovf", + ILOpCode.Mul_ovf_un => "mul.ovf.un", + ILOpCode.Div => "div", + ILOpCode.Div_un => "div.un", + ILOpCode.Rem => "rem", + ILOpCode.Rem_un => "rem.un", + ILOpCode.Neg => "neg", + + // ===== Bitwise ===== + ILOpCode.And => "and", + ILOpCode.Or => "or", + ILOpCode.Xor => "xor", + ILOpCode.Not => "not", + ILOpCode.Shl => "shl", + ILOpCode.Shr => "shr", + ILOpCode.Shr_un => "shr.un", + + // ===== Comparison ===== + ILOpCode.Ceq => "ceq", + ILOpCode.Cgt => "cgt", + ILOpCode.Cgt_un => "cgt.un", + ILOpCode.Clt => "clt", + ILOpCode.Clt_un => "clt.un", + + // ===== Load constants ===== + ILOpCode.Ldc_i4_m1 => "ldc.i4.m1", + ILOpCode.Ldc_i4_0 => "ldc.i4.0", + ILOpCode.Ldc_i4_1 => "ldc.i4.1", + ILOpCode.Ldc_i4_2 => "ldc.i4.2", + ILOpCode.Ldc_i4_3 => "ldc.i4.3", + ILOpCode.Ldc_i4_4 => "ldc.i4.4", + ILOpCode.Ldc_i4_5 => "ldc.i4.5", + ILOpCode.Ldc_i4_6 => "ldc.i4.6", + ILOpCode.Ldc_i4_7 => "ldc.i4.7", + ILOpCode.Ldc_i4_8 => "ldc.i4.8", + ILOpCode.Ldc_i4_s => $"ldc.i4.s {reader.ReadSByte()}", + ILOpCode.Ldc_i4 => $"ldc.i4 0x{reader.ReadInt32():x}", + ILOpCode.Ldc_i8 => $"ldc.i8 0x{reader.ReadInt64():x}", + ILOpCode.Ldc_r4 => $"ldc.r4 {reader.ReadSingle():G9}", + ILOpCode.Ldc_r8 => $"ldc.r8 {reader.ReadDouble():G17}", + + // ===== Load/store locals (short form) ===== + ILOpCode.Ldloc_0 => "ldloc.0", + ILOpCode.Ldloc_1 => "ldloc.1", + ILOpCode.Ldloc_2 => "ldloc.2", + ILOpCode.Ldloc_3 => "ldloc.3", + ILOpCode.Stloc_0 => "stloc.0", + ILOpCode.Stloc_1 => "stloc.1", + ILOpCode.Stloc_2 => "stloc.2", + ILOpCode.Stloc_3 => "stloc.3", + ILOpCode.Ldloc_s => $"ldloc.s {reader.ReadByte()}", + ILOpCode.Stloc_s => $"stloc.s {reader.ReadByte()}", + ILOpCode.Ldloca_s => $"ldloca.s {reader.ReadByte()}", + + // ===== Load/store locals (long form) ===== + ILOpCode.Ldloc => $"ldloc {reader.ReadUInt16()}", + ILOpCode.Stloc => $"stloc {reader.ReadUInt16()}", + ILOpCode.Ldloca => $"ldloca {reader.ReadUInt16()}", + + // ===== Load/store args (short form) ===== + ILOpCode.Ldarg_0 => "ldarg.0", + ILOpCode.Ldarg_1 => "ldarg.1", + ILOpCode.Ldarg_2 => "ldarg.2", + ILOpCode.Ldarg_3 => "ldarg.3", + ILOpCode.Ldarg_s => $"ldarg.s {reader.ReadByte()}", + ILOpCode.Starg_s => $"starg.s {reader.ReadByte()}", + ILOpCode.Ldarga_s => $"ldarga.s {reader.ReadByte()}", + + // ===== Load/store args (long form) ===== + ILOpCode.Ldarg => $"ldarg {reader.ReadUInt16()}", + ILOpCode.Starg => $"starg {reader.ReadUInt16()}", + ILOpCode.Ldarga => $"ldarga {reader.ReadUInt16()}", + + // ===== Indirect load ===== + ILOpCode.Ldind_i1 => "ldind.i1", + ILOpCode.Ldind_u1 => "ldind.u1", + ILOpCode.Ldind_i2 => "ldind.i2", + ILOpCode.Ldind_u2 => "ldind.u2", + ILOpCode.Ldind_i4 => "ldind.i4", + ILOpCode.Ldind_u4 => "ldind.u4", + ILOpCode.Ldind_i8 => "ldind.i8", + ILOpCode.Ldind_i => "ldind.i", + ILOpCode.Ldind_r4 => "ldind.r4", + ILOpCode.Ldind_r8 => "ldind.r8", + ILOpCode.Ldind_ref => "ldind.ref", + + // ===== Indirect store ===== + ILOpCode.Stind_ref => "stind.ref", + ILOpCode.Stind_i1 => "stind.i1", + ILOpCode.Stind_i2 => "stind.i2", + ILOpCode.Stind_i4 => "stind.i4", + ILOpCode.Stind_i8 => "stind.i8", + ILOpCode.Stind_r4 => "stind.r4", + ILOpCode.Stind_r8 => "stind.r8", + ILOpCode.Stind_i => "stind.i", + + // ===== Conversions ===== + ILOpCode.Conv_i1 => "conv.i1", + ILOpCode.Conv_i2 => "conv.i2", + ILOpCode.Conv_i4 => "conv.i4", + ILOpCode.Conv_i8 => "conv.i8", + ILOpCode.Conv_r4 => "conv.r4", + ILOpCode.Conv_r8 => "conv.r8", + ILOpCode.Conv_u1 => "conv.u1", + ILOpCode.Conv_u2 => "conv.u2", + ILOpCode.Conv_u4 => "conv.u4", + ILOpCode.Conv_u8 => "conv.u8", + ILOpCode.Conv_i => "conv.i", + ILOpCode.Conv_u => "conv.u", + ILOpCode.Conv_r_un => "conv.r.un", + ILOpCode.Conv_ovf_i1 => "conv.ovf.i1", + ILOpCode.Conv_ovf_i2 => "conv.ovf.i2", + ILOpCode.Conv_ovf_i4 => "conv.ovf.i4", + ILOpCode.Conv_ovf_i8 => "conv.ovf.i8", + ILOpCode.Conv_ovf_u1 => "conv.ovf.u1", + ILOpCode.Conv_ovf_u2 => "conv.ovf.u2", + ILOpCode.Conv_ovf_u4 => "conv.ovf.u4", + ILOpCode.Conv_ovf_u8 => "conv.ovf.u8", + ILOpCode.Conv_ovf_i => "conv.ovf.i", + ILOpCode.Conv_ovf_u => "conv.ovf.u", + ILOpCode.Conv_ovf_i1_un => "conv.ovf.i1.un", + ILOpCode.Conv_ovf_i2_un => "conv.ovf.i2.un", + ILOpCode.Conv_ovf_i4_un => "conv.ovf.i4.un", + ILOpCode.Conv_ovf_i8_un => "conv.ovf.i8.un", + ILOpCode.Conv_ovf_u1_un => "conv.ovf.u1.un", + ILOpCode.Conv_ovf_u2_un => "conv.ovf.u2.un", + ILOpCode.Conv_ovf_u4_un => "conv.ovf.u4.un", + ILOpCode.Conv_ovf_u8_un => "conv.ovf.u8.un", + ILOpCode.Conv_ovf_i_un => "conv.ovf.i.un", + ILOpCode.Conv_ovf_u_un => "conv.ovf.u.un", + + // ===== Array element load ===== + ILOpCode.Ldelem_i1 => "ldelem.i1", + ILOpCode.Ldelem_u1 => "ldelem.u1", + ILOpCode.Ldelem_i2 => "ldelem.i2", + ILOpCode.Ldelem_u2 => "ldelem.u2", + ILOpCode.Ldelem_i4 => "ldelem.i4", + ILOpCode.Ldelem_u4 => "ldelem.u4", + ILOpCode.Ldelem_i8 => "ldelem.i8", + ILOpCode.Ldelem_i => "ldelem.i", + ILOpCode.Ldelem_r4 => "ldelem.r4", + ILOpCode.Ldelem_r8 => "ldelem.r8", + ILOpCode.Ldelem_ref => "ldelem.ref", + ILOpCode.Ldelem => $"ldelem {FormatToken(reader.ReadInt32())}", + ILOpCode.Ldelema => $"ldelema {FormatToken(reader.ReadInt32())}", + + // ===== Array element store ===== + ILOpCode.Stelem_i => "stelem.i", + ILOpCode.Stelem_i1 => "stelem.i1", + ILOpCode.Stelem_i2 => "stelem.i2", + ILOpCode.Stelem_i4 => "stelem.i4", + ILOpCode.Stelem_i8 => "stelem.i8", + ILOpCode.Stelem_r4 => "stelem.r4", + ILOpCode.Stelem_r8 => "stelem.r8", + ILOpCode.Stelem_ref => "stelem.ref", + ILOpCode.Stelem => $"stelem {FormatToken(reader.ReadInt32())}", + + // ===== Branches (short form) ===== + ILOpCode.Br_s => FormatBranchShort("br.s", ref reader), + ILOpCode.Brfalse_s => FormatBranchShort("brfalse.s", ref reader), + ILOpCode.Brtrue_s => FormatBranchShort("brtrue.s", ref reader), + ILOpCode.Beq_s => FormatBranchShort("beq.s", ref reader), + ILOpCode.Bne_un_s => FormatBranchShort("bne.un.s", ref reader), + ILOpCode.Bge_s => FormatBranchShort("bge.s", ref reader), + ILOpCode.Bge_un_s => FormatBranchShort("bge.un.s", ref reader), + ILOpCode.Bgt_s => FormatBranchShort("bgt.s", ref reader), + ILOpCode.Bgt_un_s => FormatBranchShort("bgt.un.s", ref reader), + ILOpCode.Ble_s => FormatBranchShort("ble.s", ref reader), + ILOpCode.Ble_un_s => FormatBranchShort("ble.un.s", ref reader), + ILOpCode.Blt_s => FormatBranchShort("blt.s", ref reader), + ILOpCode.Blt_un_s => FormatBranchShort("blt.un.s", ref reader), + ILOpCode.Leave_s => FormatBranchShort("leave.s", ref reader), + + // ===== Branches (long form) ===== + ILOpCode.Br => FormatBranchLong("br", ref reader), + ILOpCode.Brfalse => FormatBranchLong("brfalse", ref reader), + ILOpCode.Brtrue => FormatBranchLong("brtrue", ref reader), + ILOpCode.Beq => FormatBranchLong("beq", ref reader), + ILOpCode.Bne_un => FormatBranchLong("bne.un", ref reader), + ILOpCode.Bge => FormatBranchLong("bge", ref reader), + ILOpCode.Bge_un => FormatBranchLong("bge.un", ref reader), + ILOpCode.Bgt => FormatBranchLong("bgt", ref reader), + ILOpCode.Bgt_un => FormatBranchLong("bgt.un", ref reader), + ILOpCode.Ble => FormatBranchLong("ble", ref reader), + ILOpCode.Ble_un => FormatBranchLong("ble.un", ref reader), + ILOpCode.Blt => FormatBranchLong("blt", ref reader), + ILOpCode.Blt_un => FormatBranchLong("blt.un", ref reader), + ILOpCode.Leave => FormatBranchLong("leave", ref reader), + + // ===== Switch ===== + ILOpCode.Switch => FormatSwitch(ref reader), + + // ===== Method calls ===== + ILOpCode.Call => $"call {FormatToken(reader.ReadInt32())}", + ILOpCode.Callvirt => $"callvirt {FormatToken(reader.ReadInt32())}", + ILOpCode.Calli => $"calli {FormatToken(reader.ReadInt32())}", // TODO: signature formatting + ILOpCode.Jmp => $"jmp {FormatToken(reader.ReadInt32())}", + ILOpCode.Newobj => $"newobj {FormatToken(reader.ReadInt32())}", + ILOpCode.Ldftn => $"ldftn {FormatToken(reader.ReadInt32())}", + ILOpCode.Ldvirtftn => $"ldvirtftn {FormatToken(reader.ReadInt32())}", + + // ===== Field access ===== + ILOpCode.Ldfld => $"ldfld {FormatToken(reader.ReadInt32())}", + ILOpCode.Ldflda => $"ldflda {FormatToken(reader.ReadInt32())}", + ILOpCode.Stfld => $"stfld {FormatToken(reader.ReadInt32())}", + ILOpCode.Ldsfld => $"ldsfld {FormatToken(reader.ReadInt32())}", + ILOpCode.Ldsflda => $"ldsflda {FormatToken(reader.ReadInt32())}", + ILOpCode.Stsfld => $"stsfld {FormatToken(reader.ReadInt32())}", + + // ===== Type operations ===== + ILOpCode.Ldstr => $"ldstr {FormatString(reader.ReadInt32())}", + ILOpCode.Newarr => $"newarr {FormatToken(reader.ReadInt32())}", + ILOpCode.Castclass => $"castclass {FormatToken(reader.ReadInt32())}", + ILOpCode.Isinst => $"isinst {FormatToken(reader.ReadInt32())}", + ILOpCode.Box => $"box {FormatToken(reader.ReadInt32())}", + ILOpCode.Unbox => $"unbox {FormatToken(reader.ReadInt32())}", + ILOpCode.Unbox_any => $"unbox.any {FormatToken(reader.ReadInt32())}", + ILOpCode.Ldtoken => $"ldtoken {FormatToken(reader.ReadInt32())}", + ILOpCode.Ldobj => $"ldobj {FormatToken(reader.ReadInt32())}", + ILOpCode.Stobj => $"stobj {FormatToken(reader.ReadInt32())}", + ILOpCode.Cpobj => $"cpobj {FormatToken(reader.ReadInt32())}", + ILOpCode.Initobj => $"initobj {FormatToken(reader.ReadInt32())}", + ILOpCode.Sizeof => $"sizeof {FormatToken(reader.ReadInt32())}", + ILOpCode.Mkrefany => $"mkrefany {FormatToken(reader.ReadInt32())}", + ILOpCode.Refanyval => $"refanyval {FormatToken(reader.ReadInt32())}", + + // ===== Prefix instructions ===== + ILOpCode.Constrained => $"constrained. {FormatToken(reader.ReadInt32())}", + ILOpCode.Unaligned => $"unaligned. {reader.ReadByte()}", + ILOpCode.Volatile => "volatile.", + ILOpCode.Tail => "tail.", + ILOpCode.Readonly => "readonly.", + + // ===== Default - should not happen if all opcodes are handled ===== + _ => $"/* unknown opcode 0x{opcode:X2} */" + }; + } + + private static string FormatBranchShort(string mnemonic, ref BlobReader reader) + { + // Per ECMA-335 §III.1.7.1, branch offsets are relative to the beginning of the next instruction + sbyte delta = reader.ReadSByte(); + int target = reader.Offset + delta; + return $"{mnemonic} IL_{target:X4}"; + } + + private static string FormatBranchLong(string mnemonic, ref BlobReader reader) + { + // Per ECMA-335 §III.1.7.1, branch offsets are relative to the beginning of the next instruction + int delta = reader.ReadInt32(); + int target = reader.Offset + delta; + return $"{mnemonic} IL_{target:X4}"; + } + + private static string FormatSwitch(ref BlobReader reader) + { + // Per ECMA-335 §III.1.7.1, switch offsets are relative to the end of the switch instruction + uint count = reader.ReadUInt32(); + + // Read all the offsets first, then calculate targets based on position after the switch instruction + int[] deltas = new int[count]; + for (uint i = 0; i < count; i++) + { + deltas[i] = reader.ReadInt32(); + } + + // reader.Offset is now at the end of the switch instruction (the "next instruction") + int afterSwitch = reader.Offset; + + var sb = new StringBuilder(); + sb.Append("switch ("); + + for (uint i = 0; i < count; i++) + { + if (i > 0) + { + sb.Append(", "); + } + int target = afterSwitch + deltas[i]; + sb.Append($"IL_{target:X4}"); + } + + sb.Append(')'); + return sb.ToString(); + } + + private static string FormatToken(int token) + { + // TODO: Resolve TypeDef tokens to Namespace.TypeName + // TODO: Resolve TypeRef tokens to [AssemblyName]Namespace.TypeName + // TODO: Resolve TypeSpec tokens by decoding generic instantiation signatures + // TODO: Resolve MethodDef tokens to ReturnType Namespace.Type::MethodName(ParamTypes) + // TODO: Resolve MemberRef tokens to Type::MemberName with signature + // TODO: Resolve MethodSpec tokens by decoding generic method instantiation + // TODO: Resolve FieldDef tokens to FieldType Namespace.Type::FieldName + // TODO: Resolve StandaloneSignature tokens for calli instruction + // TODO: Use SignatureDecoder to decode type signatures + // TODO: Handle primitive types, arrays, pointers, byrefs + // TODO: Handle generic type arguments (T, !0, !!0) + // TODO: Handle custom modifiers (modreq/modopt) + // TODO: Support --tokens option to append raw token values + var handle = MetadataTokens.EntityHandle(token); + return $"/* {handle.Kind} 0x{token:X8} */"; + } + + private string FormatString(int token) + { + var handle = MetadataTokens.UserStringHandle(token); + var str = _reader.GetUserString(handle); + // Escape special characters + return $"\"{EscapeString(str)}\""; + } + + private static string EscapeString(string str) + { + var sb = new StringBuilder(str.Length); + foreach (char c in str) + { + switch (c) + { + case '\\': sb.Append("\\\\"); break; + case '"': sb.Append("\\\""); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + case '\0': sb.Append("\\0"); break; + default: + if (c < 32 || c > 126) + { + sb.Append($"\\u{(int)c:X4}"); + } + else + { + sb.Append(c); + } + break; + } + } + return sb.ToString(); + } +} diff --git a/src/tools/ildasm/src/ILDisassembler/Options.cs b/src/tools/ildasm/src/ILDisassembler/Options.cs new file mode 100644 index 00000000000000..0a43c1afd91ab4 --- /dev/null +++ b/src/tools/ildasm/src/ILDisassembler/Options.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace ILDisassembler; + +/// +/// Options for controlling IL disassembly output. +/// +public sealed class Options +{ + /// + /// Show actual bytes (in hex) as instruction comments. + /// + public bool ShowBytes { get; set; } + + /// + /// Show exception handling clauses in raw form. + /// + public bool RawExceptionHandling { get; set; } + + /// + /// Show metadata tokens of classes and members. + /// + public bool ShowTokens { get; set; } + + /// + /// Show original source lines as comments (requires PDB). + /// + public bool ShowSource { get; set; } + + /// + /// Include references to original source lines. + /// + public bool ShowLineNumbers { get; set; } + + /// + /// Visibility filter (PUB, PRI, FAM, ASM, FAA, FOA, PSC). + /// + public string? Visibility { get; set; } + + /// + /// Only disassemble public items. + /// + public bool PublicOnly { get; set; } + + /// + /// Include all names in single quotes. + /// + public bool QuoteAllNames { get; set; } + + /// + /// Suppress output of custom attributes. + /// + public bool NoCustomAttributes { get; set; } + + /// + /// Output custom attribute blobs in verbal form (default: binary). + /// + public bool CustomAttributesVerbal { get; set; } + + /// + /// Output the metadata from the R2R Native manifest. + /// + public bool R2RNativeMetadata { get; set; } + + /// + /// Suppress IL assembler code output. + /// + public bool NoIL { get; set; } + + /// + /// Use forward class declaration. + /// + public bool ForwardDeclarations { get; set; } + + /// + /// Output full list of types (to preserve type ordering in round-trip). + /// + public bool TypeList { get; set; } + + /// + /// Include file headers information in the output. + /// + public bool Headers { get; set; } + + /// + /// Disassemble the specified item only (class[::method[(sig)]]). + /// + public string? Item { get; set; } + + /// + /// Include statistics on the image. + /// + public bool Stats { get; set; } + + /// + /// Include list of classes defined in the module. + /// + public bool ClassList { get; set; } + + /// + /// Show MetaData. + /// + public bool Metadata { get; set; } + + /// + /// What to show with (MDHEADER, HEX, CSV, UNREX, SCHEMA, RAW, HEAPS, VALIDATE), + /// recognized by their first three characters. + /// + public string[] MetadataFilters { get; set; } = []; + + /// + /// Output in HTML format. + /// + public bool Html { get; set; } + + /// + /// Output in RTF format. + /// + public bool Rtf { get; set; } + + /// + /// Use UTF-8 encoding for output (default). + /// + public bool Utf8 { get; set; } = true; + + /// + /// Use Unicode encoding for output. + /// + public bool Unicode { get; set; } +} diff --git a/src/tools/ildasm/src/ILDisassembler/TypeDisassembler.cs b/src/tools/ildasm/src/ILDisassembler/TypeDisassembler.cs new file mode 100644 index 00000000000000..60ced53cf23cd9 --- /dev/null +++ b/src/tools/ildasm/src/ILDisassembler/TypeDisassembler.cs @@ -0,0 +1,343 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace ILDisassembler; + +/// +/// Disassembles a single type definition. +/// +internal sealed class TypeDisassembler +{ + private readonly MetadataReader _reader; + private readonly PEReader _peReader; + private readonly TypeDefinition _typeDef; + + public TypeDisassembler(MetadataReader reader, PEReader peReader, TypeDefinition typeDef) + { + _reader = reader; + _peReader = peReader; + _typeDef = typeDef; + } + + public void Write(ILWriter writer) + { + var name = _reader.GetString(_typeDef.Name); + var ns = _reader.GetString(_typeDef.Namespace); + + // Skip type (global functions container) + // TODO: Handle global functions in type + if (string.IsNullOrEmpty(name) || name == "") + { + return; + } + + writer.WriteLine(); + + // Build full type name + // TODO: Handle nested types properly (currently flattened) + string fullName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + + // Determine type kind and attributes + var attributes = _typeDef.Attributes; + string typeKeyword = GetTypeKeyword(attributes); + string visibility = GetVisibility(attributes); + string modifiers = GetModifiers(attributes); + + // TODO: Output generic type parameters and constraints + // Write class header + writer.WriteLine($".class {visibility}{modifiers}{typeKeyword} {fullName}"); + + // Write extends clause + if (!_typeDef.BaseType.IsNil) + { + string baseTypeName = GetTypeName(_typeDef.BaseType); + writer.WriteLine($" extends {baseTypeName}"); + } + + // TODO: Write implements clause for interfaces + // TODO: Output .pack and .size for explicit layout + // TODO: Output security declarations (.permissionset) + // TODO: Output custom attributes on type + + writer.WriteLine("{"); + writer.Indent(); + + // Write fields + foreach (var fieldHandle in _typeDef.GetFields()) + { + WriteField(writer, fieldHandle); + } + + // TODO: Output .property declarations with .get/.set accessors + // TODO: Output .event declarations with .addon/.removeon/.fire accessors + + // Write methods + foreach (var methodHandle in _typeDef.GetMethods()) + { + WriteMethod(writer, methodHandle); + } + + writer.Dedent(); + writer.WriteLine($"}} // end of class {fullName}"); + } + + private void WriteField(ILWriter writer, FieldDefinitionHandle handle) + { + var field = _reader.GetFieldDefinition(handle); + var name = _reader.GetString(field.Name); + var attributes = field.Attributes; + + string visibility = GetFieldVisibility(attributes); + string modifiers = GetFieldModifiers(attributes); + + // TODO: Decode field type from signature blob using SignatureDecoder + // TODO: Handle generic field types + // TODO: Output field initial values (.data) + // TODO: Output field marshaling info + // TODO: Output field RVA for mapped fields + // TODO: Output custom attributes on field + writer.WriteLine($".field {visibility}{modifiers}{name}"); + } + + private void WriteMethod(ILWriter writer, MethodDefinitionHandle handle) + { + var method = _reader.GetMethodDefinition(handle); + var name = _reader.GetString(method.Name); + var attributes = method.Attributes; + + string visibility = GetMethodVisibility(attributes); + string modifiers = GetMethodModifiers(attributes); + + // TODO: Decode return type and parameter types from signature + // TODO: Output calling conventions (instance, vararg, etc.) + // TODO: Output generic method parameters + // TODO: Output parameter names and attributes (in, out, optional) + // TODO: Output default parameter values + // TODO: Output P/Invoke info (.pinvokeimpl) + // TODO: Output custom attributes on method and parameters + writer.WriteLine(); + writer.WriteLine($".method {visibility}{modifiers}{name}() cil managed"); + writer.WriteLine("{"); + writer.Indent(); + + // Write IL body if present + if (method.RelativeVirtualAddress != 0) + { + WriteMethodBody(writer, method); + } + + writer.Dedent(); + writer.WriteLine($"}} // end of method {name}"); + } + + private void WriteMethodBody(ILWriter writer, MethodDefinition method) + { + var body = _peReader.GetMethodBody(method.RelativeVirtualAddress); + + writer.WriteLine($"// Code size: {body.Size}"); + writer.WriteLine($".maxstack {body.MaxStack}"); + + // TODO: Decode and output local variable declarations with types + // TODO: Output exception handling regions (.try/.catch/.finally/.fault/.filter) + // TODO: Support --bytes option to show IL bytes as hex comments + // TODO: Support --source/--linenum options using PDB for source lines + + // Disassemble IL + var ilReader = body.GetILReader(); + var ilDisasm = new InstructionDisassembler(_reader); + + while (ilReader.Offset < ilReader.Length) + { + int offset = ilReader.Offset; + string instruction = ilDisasm.DisassembleInstruction(ref ilReader); + writer.WriteLine($"IL_{offset:X4}: {instruction}"); + } + } + + private static string GetTypeKeyword(TypeAttributes attributes) + { + if ((attributes & TypeAttributes.Interface) != 0) + { + return "interface "; + } + + // Check for value type by looking at semantics + // Note: Proper check requires looking at base type + return ""; + } + + private static string GetVisibility(TypeAttributes attributes) + { + return (attributes & TypeAttributes.VisibilityMask) switch + { + TypeAttributes.Public => "public ", + TypeAttributes.NotPublic => "private ", + TypeAttributes.NestedPublic => "nested public ", + TypeAttributes.NestedPrivate => "nested private ", + TypeAttributes.NestedFamily => "nested family ", + TypeAttributes.NestedAssembly => "nested assembly ", + TypeAttributes.NestedFamANDAssem => "nested famandassem ", + TypeAttributes.NestedFamORAssem => "nested famorassem ", + _ => "" + }; + } + + private static string GetModifiers(TypeAttributes attributes) + { + string result = ""; + + if ((attributes & TypeAttributes.Abstract) != 0) + { + result += "abstract "; + } + + if ((attributes & TypeAttributes.Sealed) != 0) + { + result += "sealed "; + } + + // Layout attributes + result += (attributes & TypeAttributes.LayoutMask) switch + { + TypeAttributes.AutoLayout => "auto ", + TypeAttributes.SequentialLayout => "sequential ", + TypeAttributes.ExplicitLayout => "explicit ", + _ => "" + }; + + // String format attributes + result += (attributes & TypeAttributes.StringFormatMask) switch + { + TypeAttributes.AnsiClass => "ansi ", + TypeAttributes.UnicodeClass => "unicode ", + TypeAttributes.AutoClass => "autochar ", + _ => "" + }; + + return result; + } + + private static string GetFieldVisibility(FieldAttributes attributes) + { + return (attributes & FieldAttributes.FieldAccessMask) switch + { + FieldAttributes.Public => "public ", + FieldAttributes.Private => "private ", + FieldAttributes.Family => "family ", + FieldAttributes.Assembly => "assembly ", + FieldAttributes.FamANDAssem => "famandassem ", + FieldAttributes.FamORAssem => "famorassem ", + _ => "" + }; + } + + private static string GetFieldModifiers(FieldAttributes attributes) + { + string result = ""; + + if ((attributes & FieldAttributes.Static) != 0) + { + result += "static "; + } + + if ((attributes & FieldAttributes.InitOnly) != 0) + { + result += "initonly "; + } + + if ((attributes & FieldAttributes.Literal) != 0) + { + result += "literal "; + } + + return result; + } + + private static string GetMethodVisibility(MethodAttributes attributes) + { + return (attributes & MethodAttributes.MemberAccessMask) switch + { + MethodAttributes.Public => "public ", + MethodAttributes.Private => "private ", + MethodAttributes.Family => "family ", + MethodAttributes.Assembly => "assembly ", + MethodAttributes.FamANDAssem => "famandassem ", + MethodAttributes.FamORAssem => "famorassem ", + _ => "" + }; + } + + private static string GetMethodModifiers(MethodAttributes attributes) + { + string result = ""; + + if ((attributes & MethodAttributes.HideBySig) != 0) + { + result += "hidebysig "; + } + + if ((attributes & MethodAttributes.Static) != 0) + { + result += "static "; + } + + if ((attributes & MethodAttributes.Virtual) != 0) + { + result += "virtual "; + } + + if ((attributes & MethodAttributes.Final) != 0) + { + result += "final "; + } + + if ((attributes & MethodAttributes.NewSlot) != 0) + { + result += "newslot "; + } + + if ((attributes & MethodAttributes.Abstract) != 0) + { + result += "abstract "; + } + + return result; + } + + private string GetTypeName(EntityHandle handle) + { + if (handle.Kind == HandleKind.TypeReference) + { + var typeRef = _reader.GetTypeReference((TypeReferenceHandle)handle); + var name = _reader.GetString(typeRef.Name); + var ns = _reader.GetString(typeRef.Namespace); + + // Get assembly ref for scope + string scope = ""; + if (typeRef.ResolutionScope.Kind == HandleKind.AssemblyReference) + { + var asmRef = _reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); + scope = $"[{_reader.GetString(asmRef.Name)}]"; + } + + return string.IsNullOrEmpty(ns) ? $"{scope}{name}" : $"{scope}{ns}.{name}"; + } + + if (handle.Kind == HandleKind.TypeDefinition) + { + var typeDef = _reader.GetTypeDefinition((TypeDefinitionHandle)handle); + var name = _reader.GetString(typeDef.Name); + var ns = _reader.GetString(typeDef.Namespace); + + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + + return handle.ToString()!; + } +} diff --git a/src/tools/ildasm/src/ildasm/IldasmRootCommand.cs b/src/tools/ildasm/src/ildasm/IldasmRootCommand.cs new file mode 100644 index 00000000000000..9963fdf07ae517 --- /dev/null +++ b/src/tools/ildasm/src/ildasm/IldasmRootCommand.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; + +namespace ILDisassembler; + +internal sealed class IldasmRootCommand : RootCommand +{ + public Argument InputFilePath { get; } = + new("input-file-path") { Description = "Input PE file (.dll or .exe) to disassemble" }; + + // Output redirection + public Option OutputFilePath { get; } = + new("--output", "-o") { Description = "Direct output to file rather than to console" }; + + public Option Html { get; } = + new("--html") { Description = "Output in HTML format (valid with --output option only)" }; + + public Option Rtf { get; } = + new("--rtf") { Description = "Output in rich text format (valid with --output option only)" }; + + // File/console output + public Option ShowBytes { get; } = + new("--bytes") { Description = "Show actual bytes (in hex) as instruction comments" }; + + public Option RawExceptionHandling { get; } = + new("--raweh") { Description = "Show exception handling clauses in raw form" }; + + public Option ShowTokens { get; } = + new("--tokens") { Description = "Show metadata tokens of classes and members" }; + + public Option ShowSource { get; } = + new("--source") { Description = "Show original source lines as comments (requires PDB)" }; + + public Option ShowLineNumbers { get; } = + new("--linenum") { Description = "Include references to original source lines" }; + + public Option Visibility { get; } = + new("--visibility") { Description = "Only disassemble the items with specified visibility: [+...] ( = PUB|PRI|FAM|ASM|FAA|FOA|PSC)" }; + + public Option PublicOnly { get; } = + new("--pubonly") { Description = "Only disassemble the public items (same as --visibility PUB)" }; + + public Option QuoteAllNames { get; } = + new("--quoteallnames") { Description = "Include all names into single quotes" }; + + public Option NoCustomAttributes { get; } = + new("--noca") { Description = "Suppress output of custom attributes" }; + + public Option CustomAttributesVerbal { get; } = + new("--caverbal") { Description = "Output CA blobs in verbal form (default: binary form)" }; + + public Option R2RNativeMetadata { get; } = + new("--r2rnativemetadata") { Description = "Output the metadata from the R2R Native manifest" }; + + // EXE and DLL files + public Option Utf8 { get; } = + new("--utf8") { Description = "Use UTF-8 encoding for output (default)" }; + + public Option Unicode { get; } = + new("--unicode") { Description = "Use UNICODE encoding for output" }; + + public Option NoIL { get; } = + new("--noil") { Description = "Suppress IL assembler code output" }; + + public Option ForwardDeclarations { get; } = + new("--forward") { Description = "Use forward class declaration" }; + + public Option TypeList { get; } = + new("--typelist") { Description = "Output full list of types (to preserve type ordering in round-trip)" }; + + public Option Headers { get; } = + new("--headers") { Description = "Include file headers information in the output" }; + + public Option Item { get; } = + new("--item") { Description = "Disassemble the specified item only: [::[()]]" }; + + public Option Stats { get; } = + new("--stats") { Description = "Include statistics on the image" }; + + public Option ClassList { get; } = + new("--classlist") { Description = "Include list of classes defined in the module" }; + + public Option All { get; } = + new("--all") { Description = "Combination of --headers, --bytes, --stats, --classlist, --tokens" }; + + public Option Metadata { get; } = + new("--metadata") { Description = "Show MetaData" }; + + public Option MetadataFilters { get; } = + new("--metadata-filter") { Description = "Show MetaData, selecting what is shown (implies --metadata): MDHEADER|HEX|CSV|UNREX|SCHEMA|RAW|HEAPS|VALIDATE" }; + + // Common + public Option NoLogo { get; } = + new("--nologo") { Description = "Don't print the logo" }; + + public Option Quiet { get; } = + new("--quiet", "-q") { Description = "Don't report disassembly progress" }; + + public Option WaitForDebugger { get; } = + new("--waitfordebugger") { Description = "Pause to give opportunity to attach debugger" }; + + internal const string ProductName = ".NET IL Disassembler"; + + public IldasmRootCommand() : base(ProductName) + { + Arguments.Add(InputFilePath); + Options.Add(OutputFilePath); + Options.Add(Html); + Options.Add(Rtf); + Options.Add(ShowBytes); + Options.Add(RawExceptionHandling); + Options.Add(ShowTokens); + Options.Add(ShowSource); + Options.Add(ShowLineNumbers); + Options.Add(Visibility); + Options.Add(PublicOnly); + Options.Add(QuoteAllNames); + Options.Add(NoCustomAttributes); + Options.Add(CustomAttributesVerbal); + Options.Add(R2RNativeMetadata); + Options.Add(Utf8); + Options.Add(Unicode); + Options.Add(NoIL); + Options.Add(ForwardDeclarations); + Options.Add(TypeList); + Options.Add(Headers); + Options.Add(Item); + Options.Add(Stats); + Options.Add(ClassList); + Options.Add(All); + Options.Add(Metadata); + Options.Add(MetadataFilters); + Options.Add(NoLogo); + Options.Add(Quiet); + Options.Add(WaitForDebugger); + } +} diff --git a/src/tools/ildasm/src/ildasm/NativeCommandLine.cs b/src/tools/ildasm/src/ildasm/NativeCommandLine.cs new file mode 100644 index 00000000000000..bb3b2fb0576dd2 --- /dev/null +++ b/src/tools/ildasm/src/ildasm/NativeCommandLine.cs @@ -0,0 +1,277 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.CommandLine; + +namespace ILDisassembler; + +internal static class NativeCommandLine +{ + private static readonly Dictionary s_options = new(StringComparer.OrdinalIgnoreCase) + { + ["ALL"] = "--all", + ["BYT"] = "--bytes", + ["CAV"] = "--caverbal", + ["CLA"] = "--classlist", + ["FOR"] = "--forward", + ["HEA"] = "--headers", + ["HTM"] = "--html", + ["ITE"] = "--item", + ["LIN"] = "--linenum", + ["MET"] = "--metadata", + ["NOC"] = "--noca", + ["NOI"] = "--noil", + ["NOL"] = "--nologo", + ["NOT"] = "--raweh", + ["OUT"] = "--output", + ["PUB"] = "--pubonly", + ["QUI"] = "--quiet", + ["QUO"] = "--quoteallnames", + ["R2R"] = "--r2rnativemetadata", + ["RAW"] = "--raweh", + ["RTF"] = "--rtf", + ["SOU"] = "--source", + ["STA"] = "--stats", + ["TOK"] = "--tokens", + ["TYP"] = "--typelist", + ["UNI"] = "--unicode", + ["UTF"] = "--utf8", + ["VIS"] = "--visibility", + }; + + // ADV only unlocked STATS, CLASSLIST and METADATA in release builds; PRE (PRETTY) is obsolete. + private static readonly HashSet s_ignoredOptions = new(StringComparer.OrdinalIgnoreCase) + { + "ADV", + "PRE", + }; + + private static readonly HashSet s_unsupportedOptions = new(StringComparer.OrdinalIgnoreCase) + { + "DEC", + "OBJ", + "PRO", + "REF", + "SUM", + }; + + internal static string[] Normalize(string[] args) => + Normalize(args, new IldasmRootCommand(), allowSlashOptions: OperatingSystem.IsWindows()); + + internal static string[] Normalize(string[] args, bool allowSlashOptions) + => Normalize(args, new IldasmRootCommand(), allowSlashOptions); + + internal static string[] Normalize(string[] args, IldasmRootCommand command) + => Normalize(args, command, allowSlashOptions: OperatingSystem.IsWindows()); + + private static string[] Normalize(string[] args, IldasmRootCommand command, bool allowSlashOptions) + { + HashSet optionsWithValues = new(StringComparer.Ordinal); + Dictionary modernShortOptions = new(StringComparer.Ordinal); + foreach (Option option in command.Options) + { + AddModernOption(option.Name, option); + foreach (string alias in option.Aliases) + { + AddModernOption(alias, option); + } + } + + List result = new(args.Length); + bool preserveRemainingArguments = false; + bool preserveNextArgument = false; + + foreach (string arg in args) + { + if (preserveRemainingArguments || preserveNextArgument) + { + result.Add(arg); + preserveNextArgument = false; + continue; + } + + if (arg == "--") + { + result.Add(arg); + preserveRemainingArguments = true; + continue; + } + + if (optionsWithValues.Contains(arg)) + { + result.Add(arg); + preserveNextArgument = true; + continue; + } + + if (arg is "-?" || (allowSlashOptions && arg == "/?")) + { + result.Add("--help"); + continue; + } + + if (!TrySplitNativeOption(arg, allowSlashOptions, out string optionName, out string? value)) + { + result.Add(arg); + continue; + } + + if (optionName.Length < 3) + { + if (!IsValidModernShortOption(arg[0], optionName, value, modernShortOptions)) + { + throw new ArgumentException($"Invalid native option '{arg}'."); + } + + result.Add(arg); + continue; + } + + string prefix = optionName[..3]; + if (s_unsupportedOptions.Contains(prefix)) + { + throw new ArgumentException( + $"Unsupported native option '{arg}'. The managed ildasm implementation does not support -{prefix.ToUpperInvariant()}."); + } + + if (s_ignoredOptions.Contains(prefix)) + { + continue; + } + + if (!s_options.TryGetValue(prefix, out string? normalizedOption)) + { + if (IsValidModernShortOption(arg[0], optionName, value, modernShortOptions)) + { + result.Add(arg); + continue; + } + + throw new ArgumentException($"Invalid native option '{arg}'."); + } + + if (normalizedOption == "--metadata") + { + result.Add("--metadata"); + if (value is not null) + { + string filter = value.TrimStart(); + if (filter.Length == 0) + { + throw new ArgumentException($"Invalid native option '{arg}'."); + } + + result.Add("--metadata-filter"); + result.Add(filter); + } + + continue; + } + + if (!optionsWithValues.Contains(normalizedOption)) + { + result.Add(normalizedOption); + continue; + } + + string normalizedValue = value?.TrimStart() ?? string.Empty; + if (normalizedValue.Length == 0) + { + throw new ArgumentException($"Invalid native option '{arg}'."); + } + + // The native tool writes to the console for -OUT=CON. + if (normalizedOption == "--output" && normalizedValue.Equals("CON", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + result.Add(normalizedOption); + result.Add(normalizedValue); + } + + return result.ToArray(); + + void AddModernOption(string name, Option option) + { + if (!name.StartsWith('-', StringComparison.Ordinal)) + { + return; + } + + if (option.Arity.MinimumNumberOfValues > 0) + { + optionsWithValues.Add(name); + } + + AddModernShortOption(name, option); + } + + void AddModernShortOption(string name, Option option) + { + if (name is ['-', _]) + { + modernShortOptions.Add(name, option); + } + } + } + + private static bool TrySplitNativeOption( + string arg, + bool allowSlash, + out string optionName, + out string? value) + { + optionName = string.Empty; + value = null; + + if (arg.Length < 2 || + arg.StartsWith("--", StringComparison.Ordinal) || + (arg[0] != '-' && (!allowSlash || arg[0] != '/'))) + { + return false; + } + + ReadOnlySpan option = arg.AsSpan(1); + int separatorIndex = option.IndexOfAny('=', ':'); + if (separatorIndex >= 0) + { + optionName = option[..separatorIndex].ToString(); + value = option[(separatorIndex + 1)..].ToString(); + } + else + { + optionName = option.ToString(); + } + + return true; + } + + private static bool IsValidModernShortOption( + char prefix, + string optionName, + string? value, + Dictionary modernShortOptions) + { + if (prefix != '-' || optionName.Length != 1) + { + return false; + } + + if (!modernShortOptions.TryGetValue($"-{optionName}", out Option? option)) + { + return false; + } + + if (value is null) + { + return true; + } + + return option.ValueType == typeof(bool) + ? bool.TryParse(value, out _) + : option.Arity.MaximumNumberOfValues > 0; + } +} diff --git a/src/tools/ildasm/src/ildasm/PACKAGE.md b/src/tools/ildasm/src/ildasm/PACKAGE.md new file mode 100644 index 00000000000000..fd0a3c3b3ee78d --- /dev/null +++ b/src/tools/ildasm/src/ildasm/PACKAGE.md @@ -0,0 +1,2 @@ +Internal implementation package not meant for direct consumption. Please do not reference directly. +The .NET IL Disassembler. diff --git a/src/tools/ildasm/src/ildasm/Program.cs b/src/tools/ildasm/src/ildasm/Program.cs new file mode 100644 index 00000000000000..1be440226952db --- /dev/null +++ b/src/tools/ildasm/src/ildasm/Program.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.CommandLine; +using System.IO; + +namespace ILDisassembler; + +internal sealed class Program +{ + private readonly IldasmRootCommand _command; + private readonly ParseResult _result; + + public Program(IldasmRootCommand command, ParseResult result) + { + _command = command; + _result = result; + } + + public int Run() + { + bool quiet = Get(_command.Quiet); + + if (!Get(_command.NoLogo) && !quiet) + { + Console.WriteLine(IldasmRootCommand.ProductName); + Console.WriteLine(); + } + + string? inputFile = Get(_command.InputFilePath); + if (string.IsNullOrEmpty(inputFile)) + { + Console.Error.WriteLine("Error: No input file specified"); + return 1; + } + + if (!File.Exists(inputFile)) + { + Console.Error.WriteLine($"Error: Input file not found: {inputFile}"); + return 1; + } + + string? outputFile = Get(_command.OutputFilePath); + string[] metadataFilters = Get(_command.MetadataFilters) ?? []; + + var options = new Options + { + ShowBytes = Get(_command.ShowBytes), + RawExceptionHandling = Get(_command.RawExceptionHandling), + ShowTokens = Get(_command.ShowTokens), + ShowSource = Get(_command.ShowSource), + ShowLineNumbers = Get(_command.ShowLineNumbers), + Visibility = Get(_command.Visibility), + PublicOnly = Get(_command.PublicOnly), + QuoteAllNames = Get(_command.QuoteAllNames), + NoCustomAttributes = Get(_command.NoCustomAttributes), + CustomAttributesVerbal = Get(_command.CustomAttributesVerbal), + R2RNativeMetadata = Get(_command.R2RNativeMetadata), + NoIL = Get(_command.NoIL), + ForwardDeclarations = Get(_command.ForwardDeclarations), + TypeList = Get(_command.TypeList), + Headers = Get(_command.Headers), + Item = Get(_command.Item), + Stats = Get(_command.Stats), + ClassList = Get(_command.ClassList), + Metadata = Get(_command.Metadata) || metadataFilters.Length > 0, + MetadataFilters = metadataFilters, + Html = Get(_command.Html), + Rtf = Get(_command.Rtf), + Utf8 = Get(_command.Utf8), + Unicode = Get(_command.Unicode), + }; + + if (Get(_command.All)) + { + options.Headers = true; + options.ShowBytes = true; + options.Stats = true; + options.ClassList = true; + options.ShowTokens = true; + } + + try + { + using var disassembler = new Disassembler(inputFile, options); + + TextWriter output = outputFile is not null + ? new StreamWriter(outputFile) + : Console.Out; + + try + { + disassembler.Disassemble(output); + + if (!quiet) + { + Console.WriteLine("// Disassembly complete"); + } + } + finally + { + if (outputFile is not null) + { + output.Dispose(); + } + } + + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + + private T Get(Argument argument) => _result.GetValue(argument)!; + + private T Get(Option option) => _result.GetValue(option)!; + + private static int Main(string[] args) + { + IldasmRootCommand command = new(); + string[] normalizedArgs; + try + { + normalizedArgs = NativeCommandLine.Normalize(args, command); + } + catch (ArgumentException e) + { + Console.Error.WriteLine($"Error: {e.Message}"); + return 1; + } + + command.SetAction(result => + { + if (result.GetValue(command.WaitForDebugger)) + { + Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue"); + Console.ReadLine(); + } + + try + { + return new Program(command, result).Run(); + } + catch (Exception e) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine("Error: " + e.Message); + Console.ResetColor(); + return 1; + } + }); + + return command + .Parse(normalizedArgs) + .Invoke(); + } +} diff --git a/src/tools/ildasm/src/ildasm/ildasm.csproj b/src/tools/ildasm/src/ildasm/ildasm.csproj new file mode 100644 index 00000000000000..a05e67b96dc263 --- /dev/null +++ b/src/tools/ildasm/src/ildasm/ildasm.csproj @@ -0,0 +1,17 @@ + + + + $(PortableTargetRid) + $(CoreclrRuntimeBinDir)managed-ildasm + true + true + true + com.microsoft.dotnet.ildasm + ildasm + true + + + + + + diff --git a/src/tools/ildasm/src/ildasm/ildasm.props b/src/tools/ildasm/src/ildasm/ildasm.props new file mode 100644 index 00000000000000..bd915f83e0ea71 --- /dev/null +++ b/src/tools/ildasm/src/ildasm/ildasm.props @@ -0,0 +1,26 @@ + + + + + ildasm + Exe + $(NetCoreAppToolCurrent) + false + false + true + enable + true + runtime.$(PackageTargetRuntime).Microsoft.NETCore.ILDAsm + $(PackageId) + The .NET IL Disassembler. + https://dot.net/ + http://go.microsoft.com/fwlink/?LinkID=288859 + false + $(NoWarn);NU5128 + + + + + + + diff --git a/src/tools/ildasm/src/ildasm/ildasm_inbuild.csproj b/src/tools/ildasm/src/ildasm/ildasm_inbuild.csproj new file mode 100644 index 00000000000000..3e60ef196c7562 --- /dev/null +++ b/src/tools/ildasm/src/ildasm/ildasm_inbuild.csproj @@ -0,0 +1,16 @@ + + + $(NETCoreSdkRuntimeIdentifier) + $(CoreclrRuntimeBinDir)$(BuildArchitecture)\managed-ildasm\ + true + true + true + + + + true + true + + + + diff --git a/src/tools/ildasm/tests/ILDisassembler.Tests/CommandLineTests.cs b/src/tools/ildasm/tests/ILDisassembler.Tests/CommandLineTests.cs new file mode 100644 index 00000000000000..86a0589ccabffa --- /dev/null +++ b/src/tools/ildasm/tests/ILDisassembler.Tests/CommandLineTests.cs @@ -0,0 +1,298 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.CommandLine; +using Xunit; + +namespace ILDisassembler.Tests; + +public class CommandLineTests +{ + public static TheoryData NativeBooleanOptions { get; } = new() + { + { "ALL", "--all" }, + { "BYT", "--bytes" }, + { "CAV", "--caverbal" }, + { "CLA", "--classlist" }, + { "FOR", "--forward" }, + { "HEA", "--headers" }, + { "HTM", "--html" }, + { "LIN", "--linenum" }, + { "NOC", "--noca" }, + { "NOI", "--noil" }, + { "NOL", "--nologo" }, + { "NOT", "--raweh" }, + { "PUB", "--pubonly" }, + { "QUI", "--quiet" }, + { "QUO", "--quoteallnames" }, + { "R2R", "--r2rnativemetadata" }, + { "RAW", "--raweh" }, + { "RTF", "--rtf" }, + { "SOU", "--source" }, + { "STA", "--stats" }, + { "TOK", "--tokens" }, + { "TYP", "--typelist" }, + { "UNI", "--unicode" }, + { "UTF", "--utf8" }, + }; + + public static TheoryData NativeValueOptions { get; } = new() + { + { "ITE", "--item" }, + { "OUT", "--output" }, + { "VIS", "--visibility" }, + }; + + public static TheoryData ModernValueOptions + { + get + { + TheoryData options = new(); + foreach (Option option in new IldasmRootCommand().Options) + { + if (option.Arity.MinimumNumberOfValues == 0) + { + continue; + } + + if (option.Name.StartsWith("-", StringComparison.Ordinal)) + { + options.Add(option.Name); + } + + foreach (string alias in option.Aliases) + { + if (alias.StartsWith("-", StringComparison.Ordinal)) + { + options.Add(alias); + } + } + } + + return options; + } + } + + [Theory] + [MemberData(nameof(NativeBooleanOptions))] + public void NativeBooleanOption_IsCaseInsensitiveAndUsesThreeCharacterPrefix( + string prefix, + string expectedOption) + { + Assert.Equal( + [expectedOption], + NativeCommandLine.Normalize([$"-{prefix.ToLowerInvariant()}Suffix"], allowSlashOptions: false)); + Assert.Equal( + [expectedOption], + NativeCommandLine.Normalize([$"/{prefix.ToLowerInvariant()}Suffix"], allowSlashOptions: true)); + } + + [Theory] + [MemberData(nameof(NativeBooleanOptions))] + public void NativeBooleanOption_IgnoresAttachedValue(string prefix, string expectedOption) + { + Assert.Equal([expectedOption], NativeCommandLine.Normalize([$"-{prefix}:false"], allowSlashOptions: false)); + Assert.Equal([expectedOption], NativeCommandLine.Normalize([$"-{prefix}="], allowSlashOptions: false)); + Assert.Equal([expectedOption], NativeCommandLine.Normalize([$"/{prefix}:ignored"], allowSlashOptions: true)); + } + + [Theory] + [MemberData(nameof(NativeValueOptions))] + public void NativeValueOption_IsCaseInsensitiveAndUsesThreeCharacterPrefix( + string prefix, + string expectedOption) + { + string nativeOption = $"-{prefix.ToLowerInvariant()}Suffix"; + + Assert.Equal( + [expectedOption, "value:part"], + NativeCommandLine.Normalize([$"{nativeOption}=value:part"], allowSlashOptions: false)); + Assert.Equal( + [expectedOption, "value=part"], + NativeCommandLine.Normalize([$"{nativeOption}:value=part"], allowSlashOptions: false)); + Assert.Equal( + [expectedOption, "value"], + NativeCommandLine.Normalize([$"/{nativeOption[1..]}=value"], allowSlashOptions: true)); + } + + [Theory] + [MemberData(nameof(NativeValueOptions))] + public void NativeValueOption_RequiresNonEmptyValue(string prefix, string _) + { + Assert.Throws( + () => NativeCommandLine.Normalize([$"-{prefix}Suffix"], allowSlashOptions: false)); + Assert.Throws( + () => NativeCommandLine.Normalize([$"-{prefix}:"], allowSlashOptions: false)); + Assert.Throws( + () => NativeCommandLine.Normalize([$"-{prefix}= "], allowSlashOptions: false)); + } + + [Theory] + [InlineData("-out: test.il", "--output", "test.il")] + [InlineData("-OUT:C:\\Temp\\test.il", "--output", "C:\\Temp\\test.il")] + [InlineData("-item=Namespace.Type::Method(int32)", "--item", "Namespace.Type::Method(int32)")] + [InlineData("-vis=PUB+FAM", "--visibility", "PUB+FAM")] + public void NativeValueOption_PreservesValue(string argument, string expectedOption, string expectedValue) + { + Assert.Equal( + [expectedOption, expectedValue], + NativeCommandLine.Normalize([argument], allowSlashOptions: false)); + } + + [Theory] + [InlineData("-out=con")] + [InlineData("-OUTPUT:CON")] + public void NativeOutputToConsole_IsDropped(string argument) + { + Assert.Empty(NativeCommandLine.Normalize([argument], allowSlashOptions: false)); + } + + [Theory] + [InlineData("-metadata", new[] { "--metadata" })] + [InlineData("-MET=RAW", new[] { "--metadata", "--metadata-filter", "RAW" })] + [InlineData("-metadata: heaps", new[] { "--metadata", "--metadata-filter", "heaps" })] + public void NativeMetadataOption_HasOptionalFilter(string argument, string[] expected) + { + Assert.Equal(expected, NativeCommandLine.Normalize([argument], allowSlashOptions: false)); + } + + [Theory] + [InlineData("-met:")] + [InlineData("-metadata= ")] + public void NativeMetadataOption_RejectsEmptyFilter(string argument) + { + Assert.Throws(() => NativeCommandLine.Normalize([argument], allowSlashOptions: false)); + } + + [Fact] + public void MetadataFilters_DoNotConsumeInputFile() + { + IldasmRootCommand command = new(); + ParseResult result = command.Parse( + NativeCommandLine.Normalize(["-MET=RAW", "-MET=HEX", "input.dll"], allowSlashOptions: false)); + + Assert.Empty(result.Errors); + Assert.True(result.GetValue(command.Metadata)); + Assert.Equal(["RAW", "HEX"], result.GetValue(command.MetadataFilters)!); + Assert.Equal("input.dll", result.GetValue(command.InputFilePath)); + } + + [Theory] + [InlineData("-adv")] + [InlineData("/ADVANCED")] + [InlineData("-pretty")] + public void NativeNoOpOption_IsAccepted(string argument) + { + Assert.Empty(NativeCommandLine.Normalize([argument], allowSlashOptions: true)); + } + + [Theory] + [InlineData("-decompile")] + [InlineData("-OBJECTFILE=a.obj")] + [InlineData("/project")] + [InlineData("-refs")] + [InlineData("-summary")] + public void UnsupportedNativeOption_Throws(string argument) + { + Assert.Throws(() => NativeCommandLine.Normalize([argument], allowSlashOptions: true)); + } + + [Theory] + [InlineData("-OU", false)] + [InlineData("-unknown", false)] + [InlineData("-x", false)] + [InlineData("/unknown", true)] + public void UnrecognizedNativeOption_Throws(string argument, bool allowSlashOptions) + { + Assert.Throws(() => NativeCommandLine.Normalize([argument], allowSlashOptions)); + } + + [Theory] + [InlineData("/OuT:test.il", "--output", "test.il")] + [InlineData("/tOkEnS", "--tokens", null)] + [InlineData("/?", "--help", null)] + public void SlashOption_IsNormalizedWhenEnabled(string argument, string expectedOption, string? expectedValue) + { + string[] expected = expectedValue is null ? [expectedOption] : [expectedOption, expectedValue]; + + Assert.Equal(expected, NativeCommandLine.Normalize([argument], allowSlashOptions: true)); + } + + [Fact] + public void SlashOption_IsPreservedWhenDisabled() + { + Assert.Equal(["/tmp/input.dll"], NativeCommandLine.Normalize(["/tmp/input.dll"], allowSlashOptions: false)); + Assert.Equal(["/unknown"], NativeCommandLine.Normalize(["/unknown"], allowSlashOptions: false)); + } + + [Fact] + public void SlashOption_DefaultMatchesPlatform() + { + string[] expected = OperatingSystem.IsWindows() ? ["--tokens"] : ["/tok"]; + + Assert.Equal(expected, NativeCommandLine.Normalize(["/tok"])); + } + + [Fact] + public void ModernAndShortOptions_ArePreserved() + { + string[] args = ["--output=test.il", "-o:other.il", "-q", "-h", "--metadata-filter", "RAW", "input.dll"]; + Assert.Equal(args, NativeCommandLine.Normalize(args, allowSlashOptions: false)); + } + + [Theory] + [MemberData(nameof(ModernValueOptions))] + public void ModernValueOption_PreservesNextArgument(string option) + { + Assert.Equal( + [option, "-TOK", "input.dll"], + NativeCommandLine.Normalize([option, "-TOK", "input.dll"], allowSlashOptions: false)); + } + + [Theory] + [InlineData("-ooutput.il")] + [InlineData("-qvalue")] + [InlineData("-q:value")] + [InlineData("-h:value")] + public void ShortOption_RejectsInvalidAttachedValue(string argument) + { + Assert.Throws(() => NativeCommandLine.Normalize([argument], allowSlashOptions: false)); + } + + [Theory] + [InlineData("input.dll")] + [InlineData("--unknown")] + [InlineData("--")] + [InlineData("-")] + public void NonNativeArgument_IsPreserved(string argument) + { + Assert.Equal([argument], NativeCommandLine.Normalize([argument], allowSlashOptions: false)); + } + + [Fact] + public void ArgumentsAfterDoubleDash_ArePreserved() + { + Assert.Equal( + ["--tokens", "--", "-BYT", "/OUT=x.il"], + NativeCommandLine.Normalize(["-TOK", "--", "-BYT", "/OUT=x.il"], allowSlashOptions: true)); + } + + [Fact] + public void NativeCommandLine_ParsesWithoutErrors() + { + IldasmRootCommand command = new(); + ParseResult result = command.Parse( + NativeCommandLine.Normalize( + ["-tok", "-byt", "input.dll", "-out=input.il", "-vis=PUB+FAM", "-ITEM=N.T::M"], + allowSlashOptions: false)); + + Assert.Empty(result.Errors); + Assert.True(result.GetValue(command.ShowTokens)); + Assert.True(result.GetValue(command.ShowBytes)); + Assert.Equal("input.dll", result.GetValue(command.InputFilePath)); + Assert.Equal("input.il", result.GetValue(command.OutputFilePath)); + Assert.Equal("PUB+FAM", result.GetValue(command.Visibility)); + Assert.Equal("N.T::M", result.GetValue(command.Item)); + } +} diff --git a/src/tools/ildasm/tests/ILDisassembler.Tests/DisassemblerTests.cs b/src/tools/ildasm/tests/ILDisassembler.Tests/DisassemblerTests.cs new file mode 100644 index 00000000000000..f8d2a8f9bf60ca --- /dev/null +++ b/src/tools/ildasm/tests/ILDisassembler.Tests/DisassemblerTests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Text; +using Xunit; + +namespace ILDisassembler.Tests; + +public class DisassemblerTests +{ + [Fact] + public void CanDisassembleSelf() + { + // Disassemble this test assembly itself + var assemblyPath = typeof(DisassemblerTests).Assembly.Location; + + using var disassembler = new Disassembler(assemblyPath); + + var output = new StringBuilder(); + using var writer = new StringWriter(output); + + disassembler.Disassemble(writer); + + var result = output.ToString(); + + // Basic sanity checks + Assert.Contains(".assembly", result); + Assert.Contains(".module", result); + Assert.Contains(".class", result); + Assert.Contains("ILDisassembler.Tests", result); + } + + [Fact] + public void DisassemblyContainsAssemblyReferences() + { + var assemblyPath = typeof(DisassemblerTests).Assembly.Location; + + using var disassembler = new Disassembler(assemblyPath); + + var output = new StringBuilder(); + using var writer = new StringWriter(output); + + disassembler.Disassemble(writer); + + var result = output.ToString(); + + // Should reference System.Runtime + Assert.Contains(".assembly extern", result); + } + + [Fact] + public void DisassemblyContainsMethods() + { + var assemblyPath = typeof(DisassemblerTests).Assembly.Location; + + using var disassembler = new Disassembler(assemblyPath); + + var output = new StringBuilder(); + using var writer = new StringWriter(output); + + disassembler.Disassemble(writer); + + var result = output.ToString(); + + // Should have methods + Assert.Contains(".method", result); + Assert.Contains("cil managed", result); + } +} diff --git a/src/tools/ildasm/tests/ILDisassembler.Tests/ILDisassembler.Tests.csproj b/src/tools/ildasm/tests/ILDisassembler.Tests/ILDisassembler.Tests.csproj new file mode 100644 index 00000000000000..b4dd9715e13b22 --- /dev/null +++ b/src/tools/ildasm/tests/ILDisassembler.Tests/ILDisassembler.Tests.csproj @@ -0,0 +1,33 @@ + + + + $(NetCoreAppToolCurrent) + enable + enable + true + false + + false + + + + + + + + + + + + + Program.cs + + + IldasmRootCommand.cs + + + NativeCommandLine.cs + + + + diff --git a/src/tools/ildasm/tests/ILDisassembler.Tests/InstructionDisassemblerTests.cs b/src/tools/ildasm/tests/ILDisassembler.Tests/InstructionDisassemblerTests.cs new file mode 100644 index 00000000000000..9c6883d56ef50a --- /dev/null +++ b/src/tools/ildasm/tests/ILDisassembler.Tests/InstructionDisassemblerTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection.Metadata; +using Xunit; + +namespace ILDisassembler.Tests; + +public class InstructionDisassemblerTests +{ + [Theory] + [InlineData(new byte[] { 0x00 }, "nop")] // nop + [InlineData(new byte[] { 0x2A }, "ret")] // ret + [InlineData(new byte[] { 0x14 }, "ldnull")] // ldnull + [InlineData(new byte[] { 0x16 }, "ldc.i4.0")] // ldc.i4.0 + [InlineData(new byte[] { 0x17 }, "ldc.i4.1")] // ldc.i4.1 + [InlineData(new byte[] { 0x1F, 0x2A }, "ldc.i4.s 42")] // ldc.i4.s 42 + [InlineData(new byte[] { 0x06 }, "ldloc.0")] // ldloc.0 + [InlineData(new byte[] { 0x0A }, "stloc.0")] // stloc.0 + [InlineData(new byte[] { 0x02 }, "ldarg.0")] // ldarg.0 + [InlineData(new byte[] { 0x58 }, "add")] // add + [InlineData(new byte[] { 0x59 }, "sub")] // sub + public unsafe void DisassemblesBasicInstructions(byte[] ilBytes, string expected) + { + // Create a mock MetadataReader - we don't need it for these basic instructions + var disasm = new InstructionDisassembler(null!); + + fixed (byte* ptr = ilBytes) + { + var reader = new BlobReader(ptr, ilBytes.Length); + var result = disasm.DisassembleInstruction(ref reader); + Assert.Equal(expected, result); + } + } +}