From 7b0f4465fc70362f6505551c847f44a4d3e1ed27 Mon Sep 17 00:00:00 2001
From: Adeel <3840695+am11@users.noreply.github.com>
Date: Fri, 20 Feb 2026 11:50:45 +0200
Subject: [PATCH 1/5] Add managed ildasm initial project structure
- Add src/tools/ildasm with CLI, library, and test projects
- Implement all 218 IL opcodes in InstructionDisassembler
- Add Options class and wire CLI arguments to library
- Output basic assembly/module/type/method structure
- Add TODOs for remaining work (token resolution, etc.)
---
eng/Subsets.props | 3 +
eng/pipelines/coreclr/ilasm.yml | 1 +
src/tests/Common/Directory.Build.targets | 7 +-
src/tools/ildasm/ildasm.slnx | 5 +
.../ildasm/src/ILDisassembler/Disassembler.cs | 154 +++++++
.../src/ILDisassembler/ILDisassembler.csproj | 16 +
.../ildasm/src/ILDisassembler/ILWriter.cs | 55 +++
.../ILDisassembler/InstructionDisassembler.cs | 407 ++++++++++++++++++
.../ildasm/src/ILDisassembler/Options.cs | 125 ++++++
.../src/ILDisassembler/TypeDisassembler.cs | 343 +++++++++++++++
.../ildasm/src/ildasm/IldasmRootCommand.cs | 148 +++++++
src/tools/ildasm/src/ildasm/Program.cs | 125 ++++++
src/tools/ildasm/src/ildasm/ildasm.csproj | 28 ++
.../ILDisassembler.Tests/DisassemblerTests.cs | 70 +++
.../ILDisassembler.Tests.csproj | 16 +
.../InstructionDisassemblerTests.cs | 35 ++
16 files changed, 1534 insertions(+), 4 deletions(-)
create mode 100644 src/tools/ildasm/ildasm.slnx
create mode 100644 src/tools/ildasm/src/ILDisassembler/Disassembler.cs
create mode 100644 src/tools/ildasm/src/ILDisassembler/ILDisassembler.csproj
create mode 100644 src/tools/ildasm/src/ILDisassembler/ILWriter.cs
create mode 100644 src/tools/ildasm/src/ILDisassembler/InstructionDisassembler.cs
create mode 100644 src/tools/ildasm/src/ILDisassembler/Options.cs
create mode 100644 src/tools/ildasm/src/ILDisassembler/TypeDisassembler.cs
create mode 100644 src/tools/ildasm/src/ildasm/IldasmRootCommand.cs
create mode 100644 src/tools/ildasm/src/ildasm/Program.cs
create mode 100644 src/tools/ildasm/src/ildasm/ildasm.csproj
create mode 100644 src/tools/ildasm/tests/ILDisassembler.Tests/DisassemblerTests.cs
create mode 100644 src/tools/ildasm/tests/ILDisassembler.Tests/ILDisassembler.Tests.csproj
create mode 100644 src/tools/ildasm/tests/ILDisassembler.Tests/InstructionDisassemblerTests.cs
diff --git a/eng/Subsets.props b/eng/Subsets.props
index cbc4aa41982afd..3956c80928cbc2 100644
--- a/eng/Subsets.props
+++ b/eng/Subsets.props
@@ -485,6 +485,7 @@
+
@@ -551,6 +552,8 @@
+
+
diff --git a/eng/pipelines/coreclr/ilasm.yml b/eng/pipelines/coreclr/ilasm.yml
index b7eb5d82c1f1b2..bdc2f21ae981cb 100644
--- a/eng/pipelines/coreclr/ilasm.yml
+++ b/eng/pipelines/coreclr/ilasm.yml
@@ -9,6 +9,7 @@ pr:
- src/coreclr/ilasm/*
- src/coreclr/ildasm/*
- src/tools/ilasm/*
+ - src/tools/ildasm/*
schedules:
- cron: "0 19 * * 6"
diff --git a/src/tests/Common/Directory.Build.targets b/src/tests/Common/Directory.Build.targets
index 8190dabe61532c..6ce845311b8c94 100644
--- a/src/tests/Common/Directory.Build.targets
+++ b/src/tests/Common/Directory.Build.targets
@@ -124,10 +124,9 @@
-
-
- True
-
+
+
+
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..272eadf0999d53
--- /dev/null
+++ b/src/tools/ildasm/src/ILDisassembler/ILDisassembler.csproj
@@ -0,0 +1,16 @@
+
+
+
+ $(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..3d47bde19c5891
--- /dev/null
+++ b/src/tools/ildasm/src/ILDisassembler/Options.cs
@@ -0,0 +1,125 @@
+// 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 (MDHEADER, HEX, CSV, UNREX, SCHEMA, RAW, HEAPS, VALIDATE).
+ ///
+ public string? Metadata { 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..99ba440adaf1fe
--- /dev/null
+++ b/src/tools/ildasm/src/ildasm/IldasmRootCommand.cs
@@ -0,0 +1,148 @@
+// 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
+{
+ // Input file
+ public Argument InputFilePath { get; } =
+ new("input-file-path") { Description = "Input PE file (.dll or .exe) to disassemble" };
+
+ // Output redirection options
+ public Option OutputFilePath { get; } =
+ new("-o", "--output") { Description = "Direct output to file rather than to console" };
+
+ public Option Html { get; } =
+ new("--html", "--htm") { 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 options
+ public Option ShowBytes { get; } =
+ new("--bytes", "--byt") { 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", "--tok") { Description = "Show metadata tokens of classes and members" };
+
+ public Option ShowSource { get; } =
+ new("--source", "--src") { Description = "Show original source lines as comments (requires PDB)" };
+
+ public Option ShowLineNumbers { get; } =
+ new("--linenum", "--lin") { Description = "Include references to original source lines" };
+
+ public Option Visibility { get; } =
+ new("--visibility", "--vis") { Description = "Only disassemble items with specified visibility (PUB|PRI|FAM|ASM|FAA|FOA|PSC)" };
+
+ public Option PublicOnly { get; } =
+ new("--pubonly", "--pub") { Description = "Only disassemble the public items (same as --visibility=PUB)" };
+
+ public Option QuoteAllNames { get; } =
+ new("--quoteallnames", "--quo") { 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", "--cav") { Description = "Output CA blobs in verbal form (default: binary form)" };
+
+ public Option R2RNativeMetadata { get; } =
+ new("--r2rnativemetadata", "--r2r") { Description = "Output the metadata from the R2R Native manifest" };
+
+ // EXE/DLL file options
+ public Option Utf8 { get; } =
+ new("--utf8") { Description = "Use UTF-8 encoding for output (default)" };
+
+ public Option Unicode { get; } =
+ new("--unicode", "--uni") { Description = "Use UNICODE encoding for output" };
+
+ public Option NoIL { get; } =
+ new("--noil") { Description = "Suppress IL assembler code output" };
+
+ public Option ForwardDeclarations { get; } =
+ new("--forward", "--for") { Description = "Use forward class declaration" };
+
+ public Option TypeList { get; } =
+ new("--typelist", "--typ") { Description = "Output full list of types (to preserve type ordering in round-trip)" };
+
+ public Option Headers { get; } =
+ new("--headers", "--hea") { Description = "Include file headers information in the output" };
+
+ public Option Item { get; } =
+ new("--item", "--ite") { Description = "Disassemble the specified item only ([::method[(sig)]])" };
+
+ public Option Stats { get; } =
+ new("--stats", "--sta") { Description = "Include statistics on the image" };
+
+ public Option ClassList { get; } =
+ new("--classlist", "--cla") { 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", "--met") { Description = "Show MetaData (MDHEADER|HEX|CSV|UNREX|SCHEMA|RAW|HEAPS|VALIDATE)" };
+
+ // Common options
+ public Option NoLogo { get; } =
+ new("--nologo", "--nol") { Description = "Don't display the logo" };
+
+ public Option Quiet { get; } =
+ new("--quiet", "-q") { Description = "Don't report disassembly progress" };
+
+ public ParseResult Result { get; private set; } = null!;
+
+ internal const string ProductName = ".NET IL Disassembler";
+
+ public IldasmRootCommand() : base(ProductName)
+ {
+ Arguments.Add(InputFilePath);
+
+ // Output redirection
+ Options.Add(OutputFilePath);
+ Options.Add(Html);
+ Options.Add(Rtf);
+
+ // File/console output options
+ 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);
+
+ // EXE/DLL options
+ 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);
+
+ // Common options
+ Options.Add(NoLogo);
+ Options.Add(Quiet);
+
+ this.SetAction(result =>
+ {
+ Result = result;
+ return new Program(this).Run();
+ });
+ }
+}
diff --git a/src/tools/ildasm/src/ildasm/Program.cs b/src/tools/ildasm/src/ildasm/Program.cs
new file mode 100644
index 00000000000000..ad41fca10dcae3
--- /dev/null
+++ b/src/tools/ildasm/src/ildasm/Program.cs
@@ -0,0 +1,125 @@
+// 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.CommandLine.Parsing;
+using System.IO;
+
+namespace ILDisassembler;
+
+internal sealed class Program
+{
+ private readonly IldasmRootCommand _command;
+
+ public Program(IldasmRootCommand command)
+ {
+ _command = command;
+ }
+
+ 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);
+
+ // Build options from command line
+ 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),
+ Html = Get(_command.Html),
+ Rtf = Get(_command.Rtf),
+ Utf8 = Get(_command.Utf8),
+ Unicode = Get(_command.Unicode),
+ };
+
+ // Handle --all shortcut
+ 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) => _command.Result.GetValue(argument)!;
+
+ private T Get(Option option) => _command.Result.GetValue(option)!;
+
+ private static int Main(string[] args) =>
+ new IldasmRootCommand()
+ .Parse(args)
+ .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..c53d52777fb3d3
--- /dev/null
+++ b/src/tools/ildasm/src/ildasm/ildasm.csproj
@@ -0,0 +1,28 @@
+
+
+
+ Exe
+ $(NetCoreAppToolCurrent)
+ $(PortableTargetRid)
+ $(CoreclrRuntimeBinDir)managed-ildasm
+ false
+ false
+ true
+ true
+ true
+ true
+ true
+
+
+ false
+ enable
+
+
+
+
+
+
+
+
+
+
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..ecc9b568dbd9e4
--- /dev/null
+++ b/src/tools/ildasm/tests/ILDisassembler.Tests/ILDisassembler.Tests.csproj
@@ -0,0 +1,16 @@
+
+
+
+ $(NetCoreAppToolCurrent)
+ enable
+ enable
+ true
+ false
+
+
+
+
+
+
+
+
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);
+ }
+ }
+}
From 3fe7c8269551198605af063357ff7dcb8594277f Mon Sep 17 00:00:00 2001
From: am11 <3840695+am11@users.noreply.github.com>
Date: Sat, 31 Oct 2026 18:37:59 +0200
Subject: [PATCH 2/5] Fix NativeAOT tool package paths for extensionless
executables
---
eng/NativeAotToolPackage.targets | 7 ++++++-
eng/pipelines/coreclr/ilasm.yml | 1 +
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/eng/NativeAotToolPackage.targets b/eng/NativeAotToolPackage.targets
index b77e015041ba6c..4875ea21036231 100644
--- a/eng/NativeAotToolPackage.targets
+++ b/eng/NativeAotToolPackage.targets
@@ -29,9 +29,14 @@
<_NativeAotToolPackageFile Include="@(PublishItemsOutputGroupOutputs->'%(OutputPath)')">
runtimes/$(PackageTargetRuntime)/native/%(PublishItemsOutputGroupOutputs.TargetPath)
+
+ runtimes/$(PackageTargetRuntime)/native/$([System.IO.Path]::GetDirectoryName('%(PublishItemsOutputGroupOutputs.TargetPath)'))
true
-
+
<_TargetPathsToSymbols Include="@(_NativeAotToolPackageFile)"
Condition="'%(_NativeAotToolPackageFile.IsSymbolFile)' == 'true'"
FinalOutputPath="%(_NativeAotToolPackageFile.Identity)"
diff --git a/eng/pipelines/coreclr/ilasm.yml b/eng/pipelines/coreclr/ilasm.yml
index 2f744d487a05ee..a9780531f8b072 100644
--- a/eng/pipelines/coreclr/ilasm.yml
+++ b/eng/pipelines/coreclr/ilasm.yml
@@ -15,6 +15,7 @@ pr:
- src/coreclr/.nuget/Microsoft.NETCore.ILAsm/*
- src/coreclr/.nuget/package-rids.props
- src/tasks/installer.tasks/*
+ - eng/NativeAotToolPackage.targets
- eng/Subsets.props
schedules:
From 8a2674ac146030f95db27c2a759dd9418a41b92b Mon Sep 17 00:00:00 2001
From: am11 <3840695+am11@users.noreply.github.com>
Date: Sun, 1 Nov 2026 00:15:35 +0200
Subject: [PATCH 3/5] Replace tools.ilasm subset with tools.iltools
---
eng/Subsets.props | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/eng/Subsets.props b/eng/Subsets.props
index 6495423f404394..caaa2571e53441 100644
--- a/eng/Subsets.props
+++ b/eng/Subsets.props
@@ -190,8 +190,6 @@
<_subset>$(_subset.Replace('+host+', '+$(DefaultHostSubsets)+'))
<_subset>$(_subset.Replace('+packs+', '+$(DefaultPacksSubsets)+'))
<_subset>$(_subset.Replace('+bootstrap+', '+bootstrap+$(BootstrapSubsets)+'))
- <_subset Condition="'$(SdkToolsSupported)' == 'true'">$(_subset.Replace('+clr.tools+', '+clr.tools+tools.ilasm+'))
- <_subset>$(_subset.Replace('+tools.iltools+', '+tools.iltools+tools.ilasm+'))
<_subset Condition="'$(TargetOS)' == 'browser'">$(_subset.Replace('+clr.runtime+', '+provision.emsdk+clr.corelib+clr.runtime+'))
@@ -268,8 +266,7 @@
-
-
+
@@ -602,7 +599,11 @@
Test="true" Category="tools"/>
-
+
+ <_BuildManagedILAsm Condition="$(_subset.Contains('+tools.iltools+')) or ('$(SdkToolsSupported)' == 'true' and $(_subset.Contains('+clr.tools+')))">true
+
+
+
@@ -613,7 +614,7 @@
Category="tools" />
-
+
From 91718469ccd51bb96de39b1af30eade2dd13a4e9 Mon Sep 17 00:00:00 2001
From: am11 <3840695+am11@users.noreply.github.com>
Date: Sun, 1 Nov 2026 00:15:53 +0200
Subject: [PATCH 4/5] Update build skill for tools.iltools
---
.github/skills/build-and-test/components/host-and-tools.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/skills/build-and-test/components/host-and-tools.md b/.github/skills/build-and-test/components/host-and-tools.md
index 7ade65942835d9..d1d6017ab3483b 100644
--- a/.github/skills/build-and-test/components/host-and-tools.md
+++ b/.github/skills/build-and-test/components/host-and-tools.md
@@ -11,9 +11,9 @@ Covers `src/native/corehost/`, `src/installer/`, `src/tools`, `src/native/manage
## Tools
-**Build:** `./build.sh tools+tools.ilasm`
+**Build:** `./build.sh tools+tools.iltools`
-**Test:** `./build.sh tools+tools.ilasm+tools.illinktests+tools.cdactests -test`
+**Test:** `./build.sh tools+tools.iltools+tools.illinktests+tools.cdactests -test`
## Build Tasks
From b7f7af4ed19f85d3f98c0b2a67b5256b3b30b064 Mon Sep 17 00:00:00 2001
From: am11 <3840695+am11@users.noreply.github.com>
Date: Wed, 4 Nov 2026 23:28:00 +0200
Subject: [PATCH 5/5] Use separate tools.ildasm subset; address review feedback
---
.../build-and-test/components/host-and-tools.md | 4 ++--
eng/Subsets.props | 14 ++++++--------
eng/pipelines/coreclr/ilasm.yml | 2 +-
.../src/ILDisassembler/ILDisassembler.csproj | 5 -----
4 files changed, 9 insertions(+), 16 deletions(-)
diff --git a/.github/skills/build-and-test/components/host-and-tools.md b/.github/skills/build-and-test/components/host-and-tools.md
index d1d6017ab3483b..7ade65942835d9 100644
--- a/.github/skills/build-and-test/components/host-and-tools.md
+++ b/.github/skills/build-and-test/components/host-and-tools.md
@@ -11,9 +11,9 @@ Covers `src/native/corehost/`, `src/installer/`, `src/tools`, `src/native/manage
## Tools
-**Build:** `./build.sh tools+tools.iltools`
+**Build:** `./build.sh tools+tools.ilasm`
-**Test:** `./build.sh tools+tools.iltools+tools.illinktests+tools.cdactests -test`
+**Test:** `./build.sh tools+tools.ilasm+tools.illinktests+tools.cdactests -test`
## Build Tasks
diff --git a/eng/Subsets.props b/eng/Subsets.props
index caaa2571e53441..4adcc757513691 100644
--- a/eng/Subsets.props
+++ b/eng/Subsets.props
@@ -190,6 +190,7 @@
<_subset>$(_subset.Replace('+host+', '+$(DefaultHostSubsets)+'))
<_subset>$(_subset.Replace('+packs+', '+$(DefaultPacksSubsets)+'))
<_subset>$(_subset.Replace('+bootstrap+', '+bootstrap+$(BootstrapSubsets)+'))
+ <_subset Condition="'$(SdkToolsSupported)' == 'true'">$(_subset.Replace('+clr.tools+', '+clr.tools+tools.ilasm+'))
<_subset Condition="'$(TargetOS)' == 'browser'">$(_subset.Replace('+clr.runtime+', '+provision.emsdk+clr.corelib+clr.runtime+'))
@@ -266,7 +267,8 @@
-
+
+
@@ -599,11 +601,7 @@
Test="true" Category="tools"/>
-
- <_BuildManagedILAsm Condition="$(_subset.Contains('+tools.iltools+')) or ('$(SdkToolsSupported)' == 'true' and $(_subset.Contains('+clr.tools+')))">true
-
-
-
+
@@ -614,7 +612,7 @@
Category="tools" />
-
+
@@ -626,7 +624,7 @@
-
+
diff --git a/eng/pipelines/coreclr/ilasm.yml b/eng/pipelines/coreclr/ilasm.yml
index 43140fd073e4b8..0bb786934cfd28 100644
--- a/eng/pipelines/coreclr/ilasm.yml
+++ b/eng/pipelines/coreclr/ilasm.yml
@@ -59,7 +59,7 @@ extends:
- windows_x64
jobParameters:
nameSuffix: ILAsm
- buildArgs: -s tools.iltools -c $(_BuildConfig) -pack -test
+ buildArgs: -s tools.ilasm+tools.ildasm -c $(_BuildConfig) -pack -test
timeoutInMinutes: 120
enablePublishTestResults: true
testResultsFormat: 'xunit'
diff --git a/src/tools/ildasm/src/ILDisassembler/ILDisassembler.csproj b/src/tools/ildasm/src/ILDisassembler/ILDisassembler.csproj
index 446166ad300aa0..57d9dba5ab460e 100644
--- a/src/tools/ildasm/src/ILDisassembler/ILDisassembler.csproj
+++ b/src/tools/ildasm/src/ILDisassembler/ILDisassembler.csproj
@@ -8,9 +8,4 @@
false
-
-