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);
+ }
+ }
+}