From b1242935e3c1ed19882d87e3158b23afa29fb503 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 14:34:19 -0700 Subject: [PATCH 1/6] Add Webcil support to R2RDump R2RDump previously could not read Webcil files (the format used for managed assemblies in WebAssembly environments). This adds a WebcilImageReader that implements IBinaryImageReader for the Webcil format, enabling R2RDump to dump headers, methods, and section contents from Webcil-format R2R images. Changes: - New WebcilImageReader.cs implementing IBinaryImageReader - ReadyToRunReader detects Webcil format (after MachO, before PE) - DumpModel handles Webcil in reference assembly loading - Program.cs maps OperatingSystem.Unknown to TargetOS.Linux for Webcil - ReadyToRunMethod gracefully handles null PEReader (Webcil has no PE) - ILCompiler.Reflection.ReadyToRun.csproj includes shared Webcil.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ILCompiler.Reflection.ReadyToRun.csproj | 1 + .../ReadyToRunMethod.cs | 3 +- .../ReadyToRunReader.cs | 4 + .../WebcilImageReader.cs | 384 ++++++++++++++++++ src/coreclr/tools/r2rdump/DumpModel.cs | 7 + src/coreclr/tools/r2rdump/Program.cs | 1 + 6 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ILCompiler.Reflection.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ILCompiler.Reflection.ReadyToRun.csproj index 68bba4009b6e65..f3390f66b22be7 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ILCompiler.Reflection.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ILCompiler.Reflection.ReadyToRun.csproj @@ -28,6 +28,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs index 09487a29016796..0490fd8fe2a2cb 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs @@ -403,9 +403,8 @@ public ReadyToRunMethod( case HandleKind.MethodDefinition: { MethodDefinition methodDef = ComponentReader.MetadataReader.GetMethodDefinition((MethodDefinitionHandle)MethodHandle); - if (methodDef.RelativeVirtualAddress != 0) + if (methodDef.RelativeVirtualAddress != 0 && ComponentReader.ImageReader is not null) { - System.Diagnostics.Debug.Assert(ComponentReader.ImageReader != null, "Component should be a PE and have an associated PEReader"); MethodBodyBlock mbb = ComponentReader.ImageReader.GetMethodBody(methodDef.RelativeVirtualAddress); if (!mbb.LocalSignature.IsNil) { diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs index 13dd20c4e979be..2b1f78c3259063 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs @@ -501,6 +501,10 @@ private unsafe void Initialize(IAssemblyMetadata metadata) { CompositeReader = new MachO.MachOImageReader(image); } + else if (WebcilImageReader.IsWebcilImage(image)) + { + CompositeReader = new WebcilImageReader(image); + } else { CompositeReader = new PEImageReader(new PEReader(Unsafe.As>(ref image))); diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs new file mode 100644 index 00000000000000..72a8328b2b4055 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs @@ -0,0 +1,384 @@ +// 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.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Microsoft.NET.WebAssembly.Webcil; + +namespace ILCompiler.Reflection.ReadyToRun +{ + /// + /// Wrapper around Webcil files that implements IBinaryImageReader. + /// Webcil is a stripped-down PE format used for managed assemblies in WebAssembly environments. + /// + public class WebcilImageReader : IBinaryImageReader + { + private readonly byte[] _image; + private readonly WebcilHeader _header; + private readonly ImmutableArray _sections; + private readonly long _webcilOffset; + private readonly DirectoryEntry _corHeaderMetadataDirectory; + private readonly CorFlags _corFlags; + private readonly DirectoryEntry _managedNativeHeaderDirectory; + + public Machine Machine => Machine.I386; // Webcil doesn't encode machine type; wasm targets use a placeholder + public OperatingSystem OperatingSystem => OperatingSystem.Unknown; + public ulong ImageBase => 0; + + public WebcilImageReader(byte[] image) + { + _image = image; + _webcilOffset = 0; + + // Check for WASM wrapper + if (IsWasmModule(image)) + { + if (!TryFindWebcilInWasm(image, out _webcilOffset)) + { + throw new BadImageFormatException("WASM module does not contain a Webcil payload"); + } + } + + // Read the Webcil header + if (!TryReadHeader(image, _webcilOffset, out _header)) + { + throw new BadImageFormatException("Not a valid Webcil file"); + } + + // Read section headers + _sections = ReadSections(image, _webcilOffset, _header); + + // Read the COR header to get metadata and R2R header locations + ReadCorHeader(image, out _corFlags, out _corHeaderMetadataDirectory, out _managedNativeHeaderDirectory); + } + + /// + /// Detects whether a byte array starts with the Webcil magic bytes (or is a WASM module containing Webcil). + /// + public static bool IsWebcilImage(byte[] image) + { + if (image.Length < 4) + return false; + + uint magic = BitConverter.ToUInt32(image, 0); + if (magic == WebcilConstants.WEBCIL_MAGIC) + return true; + + // Check if it's a WASM module that might contain Webcil + if (IsWasmModule(image)) + { + return TryFindWebcilInWasm(image, out _); + } + + return false; + } + + /// + /// Detects whether the file at the specified path is a Webcil image. + /// + public static bool IsWebcilImage(string filename) + { + try + { + byte[] header = new byte[4]; + using var stream = File.OpenRead(filename); + if (stream.Read(header, 0, 4) != 4) + return false; + + uint magic = BitConverter.ToUInt32(header, 0); + if (magic == WebcilConstants.WEBCIL_MAGIC) + return true; + + // Check for WASM magic (\0asm) + if (header[0] == 0x00 && header[1] == 0x61 && header[2] == 0x73 && header[3] == 0x6D) + { + // Read the full file to check for Webcil inside WASM + stream.Seek(0, SeekOrigin.Begin); + byte[] fullImage = new byte[stream.Length]; + stream.Read(fullImage, 0, fullImage.Length); + return TryFindWebcilInWasm(fullImage, out _); + } + + return false; + } + catch + { + return false; + } + } + + public ImmutableArray GetEntireImage() + => Unsafe.As>(ref Unsafe.AsRef(in _image)); + + public int GetOffset(int rva) + { + foreach (var section in _sections) + { + if ((uint)rva >= section.VirtualAddress && (uint)rva < section.VirtualAddress + section.VirtualSize) + { + uint offset = (uint)rva - section.VirtualAddress; + if (offset >= section.SizeOfRawData) + { + throw new BadImageFormatException($"RVA 0x{rva:X} maps beyond section raw data"); + } + return (int)(section.PointerToRawData + offset + _webcilOffset); + } + } + throw new BadImageFormatException($"RVA 0x{rva:X} not found in any Webcil section"); + } + + public bool TryGetReadyToRunHeader(out int rva, out bool isComposite) + { + // Check the ManagedNativeHeaderDirectory (same as PE's CorHeader.ManagedNativeHeaderDirectory) + if ((_corFlags & CorFlags.ILLibrary) != 0 && _managedNativeHeaderDirectory.Size != 0) + { + rva = _managedNativeHeaderDirectory.RelativeVirtualAddress; + isComposite = false; + return true; + } + + rva = 0; + isComposite = false; + return false; + } + + public IAssemblyMetadata GetStandaloneAssemblyMetadata() + { + if (_corHeaderMetadataDirectory.Size == 0) + return null; + + int metadataOffset = GetOffset(_corHeaderMetadataDirectory.RelativeVirtualAddress); + var metadataBytes = new byte[_corHeaderMetadataDirectory.Size]; + Array.Copy(_image, metadataOffset, metadataBytes, 0, _corHeaderMetadataDirectory.Size); + + return new WebcilAssemblyMetadata(metadataBytes); + } + + public IAssemblyMetadata GetManifestAssemblyMetadata(MetadataReader manifestReader) + => new ManifestAssemblyMetadata(manifestReader); + + public void DumpImageInformation(TextWriter writer) + { + writer.WriteLine($"Format: Webcil v{_header.VersionMajor}.{_header.VersionMinor}"); + writer.WriteLine($"Sections: {_header.CoffSections}"); + writer.WriteLine($"CliHeaderRVA: 0x{_header.PeCliHeaderRva:X}"); + writer.WriteLine($"CliHeaderSize: {_header.PeCliHeaderSize}"); + writer.WriteLine($"DebugRVA: 0x{_header.PeDebugRva:X}"); + writer.WriteLine($"DebugSize: {_header.PeDebugSize}"); + + writer.WriteLine("Sections:"); + for (int i = 0; i < _sections.Length; i++) + { + var section = _sections[i]; + writer.WriteLine($" [{i}] VA=0x{section.VirtualAddress:X} VSize=0x{section.VirtualSize:X} RawSize=0x{section.SizeOfRawData:X} RawPtr=0x{section.PointerToRawData:X}"); + } + } + + public Dictionary GetSections() + { + Dictionary sectionMap = []; + for (int i = 0; i < _sections.Length; i++) + { + sectionMap.Add($".webcil{i}", (int)_sections[i].SizeOfRawData); + } + return sectionMap; + } + + private void ReadCorHeader(byte[] image, out CorFlags flags, out DirectoryEntry metadataDirectory, out DirectoryEntry managedNativeHeaderDirectory) + { + int corHeaderOffset = GetOffset((int)_header.PeCliHeaderRva); + + // CorHeader layout: + // int32 cb (byte count) + // uint16 MajorRuntimeVersion + // uint16 MinorRuntimeVersion + // DirectoryEntry MetaData (RVA + Size) + // uint32 Flags + // int32 EntryPointTokenOrRelativeVirtualAddress + // DirectoryEntry Resources (RVA + Size) + // DirectoryEntry StrongNameSignature (RVA + Size) + // DirectoryEntry CodeManagerTable (RVA + Size) + // DirectoryEntry VTableFixups (RVA + Size) + // DirectoryEntry ExportAddressTableJumps (RVA + Size) + // DirectoryEntry ManagedNativeHeader (RVA + Size) + + int offset = corHeaderOffset; + offset += 4; // cb + offset += 2; // MajorRuntimeVersion + offset += 2; // MinorRuntimeVersion + + int metadataRva = BitConverter.ToInt32(image, offset); offset += 4; + int metadataSize = BitConverter.ToInt32(image, offset); offset += 4; + metadataDirectory = new DirectoryEntry(metadataRva, metadataSize); + + flags = (CorFlags)BitConverter.ToUInt32(image, offset); offset += 4; + + offset += 4; // EntryPointTokenOrRelativeVirtualAddress + offset += 8; // Resources + offset += 8; // StrongNameSignature + offset += 8; // CodeManagerTable + offset += 8; // VTableFixups + offset += 8; // ExportAddressTableJumps + + int managedNativeRva = BitConverter.ToInt32(image, offset); offset += 4; + int managedNativeSize = BitConverter.ToInt32(image, offset); + managedNativeHeaderDirectory = new DirectoryEntry(managedNativeRva, managedNativeSize); + } + + private static bool TryReadHeader(byte[] image, long offset, out WebcilHeader header) + { + header = default; + + // V0 header is 28 bytes, V1 is 32 bytes + const int V0HeaderSize = 28; + const int V1HeaderSize = 32; + + if (offset + V0HeaderSize > image.Length) + return false; + + unsafe + { + fixed (byte* p = &image[(int)offset]) + { + WebcilHeader temp; + Buffer.MemoryCopy(p, &temp, sizeof(WebcilHeader), V0HeaderSize); + header = temp; + } + } + + if (!BitConverter.IsLittleEndian) + { + header.Id = BinaryPrimitives.ReverseEndianness(header.Id); + header.VersionMajor = BinaryPrimitives.ReverseEndianness(header.VersionMajor); + header.VersionMinor = BinaryPrimitives.ReverseEndianness(header.VersionMinor); + header.CoffSections = BinaryPrimitives.ReverseEndianness(header.CoffSections); + header.PeCliHeaderRva = BinaryPrimitives.ReverseEndianness(header.PeCliHeaderRva); + header.PeCliHeaderSize = BinaryPrimitives.ReverseEndianness(header.PeCliHeaderSize); + header.PeDebugRva = BinaryPrimitives.ReverseEndianness(header.PeDebugRva); + header.PeDebugSize = BinaryPrimitives.ReverseEndianness(header.PeDebugSize); + } + + if (header.Id != WebcilConstants.WEBCIL_MAGIC) + return false; + + if (header.VersionMajor != 0 && header.VersionMajor != 1) + return false; + + if (header.VersionMinor != WebcilConstants.WC_VERSION_MINOR) + return false; + + if (header.VersionMajor >= 1) + { + if (offset + V1HeaderSize > image.Length) + return false; + + header.TableBase = BitConverter.ToUInt32(image, (int)offset + V0HeaderSize); + if (!BitConverter.IsLittleEndian) + { + header.TableBase = BinaryPrimitives.ReverseEndianness(header.TableBase); + } + } + else + { + header.TableBase = uint.MaxValue; + } + + return true; + } + + private static unsafe ImmutableArray ReadSections(byte[] image, long webcilOffset, WebcilHeader header) + { + int sectionSize = sizeof(WebcilSectionHeader); + long sectionDirectoryOffset = webcilOffset + (header.VersionMajor >= 1 ? 32 : 28); + var sections = ImmutableArray.CreateBuilder(header.CoffSections); + + for (int i = 0; i < header.CoffSections; i++) + { + long sectionOffset = sectionDirectoryOffset + (i * sectionSize); + WebcilSectionHeader sectionHeader; + fixed (byte* p = &image[(int)sectionOffset]) + { + sectionHeader = *(WebcilSectionHeader*)p; + } + + if (!BitConverter.IsLittleEndian) + { + sectionHeader = new WebcilSectionHeader( + virtualSize: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualSize), + virtualAddress: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualAddress), + sizeOfRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.SizeOfRawData), + pointerToRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.PointerToRawData) + ); + } + + sections.Add(sectionHeader); + } + + return sections.MoveToImmutable(); + } + + private static bool IsWasmModule(byte[] image) + { + // WASM magic: \0asm + return image.Length >= 4 + && image[0] == 0x00 + && image[1] == 0x61 + && image[2] == 0x73 + && image[3] == 0x6D; + } + + private static bool TryFindWebcilInWasm(byte[] image, out long webcilOffset) + { + webcilOffset = 0; + + // Simple scan: look for the Webcil magic in the WASM module + // The Webcil payload is embedded as a custom section in the WASM module + for (int i = 8; i <= image.Length - 4; i++) + { + uint candidate = BitConverter.ToUInt32(image, i); + if (candidate == WebcilConstants.WEBCIL_MAGIC) + { + // Verify this is a valid Webcil header + if (TryReadHeader(image, i, out _)) + { + webcilOffset = i; + return true; + } + } + } + + return false; + } + } + + /// + /// Assembly metadata implementation for Webcil images that don't have a PEReader. + /// + internal sealed unsafe class WebcilAssemblyMetadata : IAssemblyMetadata + { + private readonly byte[] _metadataBytes; + private readonly GCHandle _pinnedBytes; + private readonly MetadataReader _metadataReader; + + public WebcilAssemblyMetadata(byte[] metadataBytes) + { + _metadataBytes = metadataBytes; + _pinnedBytes = GCHandle.Alloc(_metadataBytes, GCHandleType.Pinned); + _metadataReader = new MetadataReader( + (byte*)_pinnedBytes.AddrOfPinnedObject(), + _metadataBytes.Length); + } + + public PEReader ImageReader => null; + + public MetadataReader MetadataReader => _metadataReader; + } +} diff --git a/src/coreclr/tools/r2rdump/DumpModel.cs b/src/coreclr/tools/r2rdump/DumpModel.cs index fb920f2e5308be..509286020bfa07 100644 --- a/src/coreclr/tools/r2rdump/DumpModel.cs +++ b/src/coreclr/tools/r2rdump/DumpModel.cs @@ -95,6 +95,13 @@ static IAssemblyMetadata Open(string filename) { byte[] image = File.ReadAllBytes(filename); + if (WebcilImageReader.IsWebcilImage(image)) + { + var webcilReader = new WebcilImageReader(image); + return webcilReader.GetStandaloneAssemblyMetadata() + ?? throw new BadImageFormatException($"ECMA metadata not found in Webcil file '{filename}'"); + } + PEReader peReader = new PEReader(Unsafe.As>(ref image)); if (!peReader.HasMetadata) diff --git a/src/coreclr/tools/r2rdump/Program.cs b/src/coreclr/tools/r2rdump/Program.cs index 7dd5f49c5a8d85..b21bd1b77d7067 100644 --- a/src/coreclr/tools/r2rdump/Program.cs +++ b/src/coreclr/tools/r2rdump/Program.cs @@ -221,6 +221,7 @@ public void Dump(ReadyToRunReader r2r) OperatingSystem.Apple => TargetOS.OSX, OperatingSystem.FreeBSD => TargetOS.FreeBSD, OperatingSystem.NetBSD => TargetOS.FreeBSD, + OperatingSystem.Unknown => TargetOS.Linux, // Webcil/WASM images don't encode OS; use Linux as fallback _ => throw new NotImplementedException(r2r.OperatingSystem.ToString()), }; TargetDetails details = new(architecture, os, TargetAbi.NativeAot); From 5ed602b45a11b26be591b25a02f60c70d20ed14d Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 15:13:08 -0700 Subject: [PATCH 2/6] Add GetSectionData to IAssemblyMetadata and implement in all types Replace the PEReader ImageReader property with a GetSectionData(int rva) method that returns a BlobReader. This decouples the interface from PEReader, enabling non-PE formats (Webcil) to provide section data. Implementations: - StandaloneAssemblyMetadata: delegates to PEReader.GetSectionData - ManifestAssemblyMetadata: same with null-guard - WebcilAssemblyMetadata: resolves RVA via WebcilImageReader sections - SimpleAssemblyMetadata (tests): delegates to PEReader.GetSectionData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCasesRunner/R2RResultChecker.cs | 2 +- .../IAssemblyMetadata.cs | 3 +- .../ManifestAssemblyMetadata.cs | 7 +++- .../ReadyToRunMethod.cs | 14 ++++--- .../StandaloneAssemblyMetadata.cs | 2 +- .../WebcilImageReader.cs | 38 ++++++++++++------- 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index c2e5e651b32cd6..082bd903a3551f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -483,7 +483,7 @@ public SimpleAssemblyMetadata(string path) _peReader = new PEReader(new MemoryStream(imageBytes)); } - public PEReader ImageReader => _peReader; + public BlobReader GetSectionData(int relativeVirtualAddress) => _peReader.GetSectionData(relativeVirtualAddress).GetReader(); public MetadataReader MetadataReader => _peReader.GetMetadataReader(); } diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/IAssemblyMetadata.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/IAssemblyMetadata.cs index 5a49d9a06b82fb..a296d0b10387b8 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/IAssemblyMetadata.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/IAssemblyMetadata.cs @@ -11,8 +11,7 @@ namespace ILCompiler.Reflection.ReadyToRun /// public interface IAssemblyMetadata { - PEReader ImageReader { get; } - + BlobReader GetSectionData(int relativeVirtualAddress); MetadataReader MetadataReader { get; } } } diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ManifestAssemblyMetadata.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ManifestAssemblyMetadata.cs index e6c0032a9109cc..bd7cba6adcc638 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ManifestAssemblyMetadata.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ManifestAssemblyMetadata.cs @@ -36,7 +36,12 @@ public ManifestAssemblyMetadata(PEReader peReader, MetadataReader metadataReader _peReader = peReader; } - public PEReader ImageReader => _peReader; + public BlobReader GetSectionData(int relativeVirtualAddress) + { + if (_peReader is null) + return default; + return _peReader.GetSectionData(relativeVirtualAddress).GetReader(); + } public MetadataReader MetadataReader => _metadataReader; diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs index 0490fd8fe2a2cb..ffc9ce174c5061 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunMethod.cs @@ -403,13 +403,17 @@ public ReadyToRunMethod( case HandleKind.MethodDefinition: { MethodDefinition methodDef = ComponentReader.MetadataReader.GetMethodDefinition((MethodDefinitionHandle)MethodHandle); - if (methodDef.RelativeVirtualAddress != 0 && ComponentReader.ImageReader is not null) + if (methodDef.RelativeVirtualAddress != 0) { - MethodBodyBlock mbb = ComponentReader.ImageReader.GetMethodBody(methodDef.RelativeVirtualAddress); - if (!mbb.LocalSignature.IsNil) + BlobReader sectionData = ComponentReader.GetSectionData(methodDef.RelativeVirtualAddress); + if (sectionData.Length > 0) { - StandaloneSignature ss = ComponentReader.MetadataReader.GetStandaloneSignature(mbb.LocalSignature); - LocalSignature = ss.DecodeLocalSignature(typeProvider, genericContext); + MethodBodyBlock mbb = MethodBodyBlock.Create(sectionData); + if (!mbb.LocalSignature.IsNil) + { + StandaloneSignature ss = ComponentReader.MetadataReader.GetStandaloneSignature(mbb.LocalSignature); + LocalSignature = ss.DecodeLocalSignature(typeProvider, genericContext); + } } } Name = ComponentReader.MetadataReader.GetString(methodDef.Name); diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/StandaloneAssemblyMetadata.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/StandaloneAssemblyMetadata.cs index a5ccea15f3d869..23b28e28a8e814 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/StandaloneAssemblyMetadata.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/StandaloneAssemblyMetadata.cs @@ -28,7 +28,7 @@ public StandaloneAssemblyMetadata(PEReader peReader) _metadataReader = _peReader.GetMetadataReader(); } - public PEReader ImageReader => _peReader; + public BlobReader GetSectionData(int relativeVirtualAddress) => _peReader.GetSectionData(relativeVirtualAddress).GetReader(); public MetadataReader MetadataReader => _metadataReader; } diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs index 72a8328b2b4055..29ef96bde4a401 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs @@ -9,7 +9,6 @@ using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using Microsoft.NET.WebAssembly.Webcil; @@ -102,7 +101,7 @@ public static bool IsWebcilImage(string filename) { // Read the full file to check for Webcil inside WASM stream.Seek(0, SeekOrigin.Begin); - byte[] fullImage = new byte[stream.Length]; + byte[] fullImage = GC.AllocateArray((int)stream.Length, pinned: true); stream.Read(fullImage, 0, fullImage.Length); return TryFindWebcilInWasm(fullImage, out _); } @@ -118,6 +117,8 @@ public static bool IsWebcilImage(string filename) public ImmutableArray GetEntireImage() => Unsafe.As>(ref Unsafe.AsRef(in _image)); + internal byte[] GetImage() => _image; + public int GetOffset(int rva) { foreach (var section in _sections) @@ -156,10 +157,10 @@ public IAssemblyMetadata GetStandaloneAssemblyMetadata() return null; int metadataOffset = GetOffset(_corHeaderMetadataDirectory.RelativeVirtualAddress); - var metadataBytes = new byte[_corHeaderMetadataDirectory.Size]; + var metadataBytes = GC.AllocateArray(_corHeaderMetadataDirectory.Size, pinned: true); Array.Copy(_image, metadataOffset, metadataBytes, 0, _corHeaderMetadataDirectory.Size); - return new WebcilAssemblyMetadata(metadataBytes); + return new WebcilAssemblyMetadata(metadataBytes, this); } public IAssemblyMetadata GetManifestAssemblyMetadata(MetadataReader manifestReader) @@ -364,20 +365,31 @@ private static bool TryFindWebcilInWasm(byte[] image, out long webcilOffset) /// internal sealed unsafe class WebcilAssemblyMetadata : IAssemblyMetadata { - private readonly byte[] _metadataBytes; - private readonly GCHandle _pinnedBytes; private readonly MetadataReader _metadataReader; + private readonly WebcilImageReader _webcilReader; - public WebcilAssemblyMetadata(byte[] metadataBytes) + public WebcilAssemblyMetadata(byte[] metadataBytes, WebcilImageReader webcilReader) { - _metadataBytes = metadataBytes; - _pinnedBytes = GCHandle.Alloc(_metadataBytes, GCHandleType.Pinned); - _metadataReader = new MetadataReader( - (byte*)_pinnedBytes.AddrOfPinnedObject(), - _metadataBytes.Length); + _webcilReader = webcilReader; + fixed (byte* p = metadataBytes) + { + _metadataReader = new MetadataReader(p, metadataBytes.Length); + } } - public PEReader ImageReader => null; + public BlobReader GetSectionData(int relativeVirtualAddress) + { + if (_webcilReader is null) + return default; + + int offset = _webcilReader.GetOffset(relativeVirtualAddress); + byte[] image = _webcilReader.GetImage(); + int remaining = image.Length - offset; + fixed (byte* p = image) + { + return new BlobReader(p + offset, remaining); + } + } public MetadataReader MetadataReader => _metadataReader; } From 1fcf0cfbca76c9cdf78e83c5bc21ba6d023d1555 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 16:36:25 -0700 Subject: [PATCH 3/6] Add WASM bytecode disassembler for R2RDump Implement a full WASM instruction disassembler that decodes WebAssembly binary format into WAT-style text output. This enables the --disasm flag in R2RDump to work with Webcil/WASM R2R images. - Add WasmDisassembler.cs with complete opcode tables for all standard WASM instructions (control, parametric, variable, table, memory, numeric, conversion, sign-extension, reference types) plus 0xFC (bulk memory/saturating truncation), 0xFB (GC), and 0xFD (SIMD) prefixed opcodes - Add WebcilImageReader.GetWasmFunctionBody() to parse the WASM module's type, function, and code sections to extract function info including type signature and local declarations - Integrate into TextDumper.DumpWasmDisasm() to print parameters and locals with their local indices, result types, and disassembled instructions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WebcilImageReader.cs | 166 +++++ src/coreclr/tools/r2rdump/TextDumper.cs | 97 ++- src/coreclr/tools/r2rdump/WasmDisassembler.cs | 680 ++++++++++++++++++ 3 files changed, 939 insertions(+), 4 deletions(-) create mode 100644 src/coreclr/tools/r2rdump/WasmDisassembler.cs diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs index 29ef96bde4a401..501e3f5e06f35a 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs @@ -119,6 +119,172 @@ public ImmutableArray GetEntireImage() internal byte[] GetImage() => _image; + /// + /// Returns true if this Webcil image is wrapped inside a WASM module. + /// + public bool IsWasmWrapped => _webcilOffset > 0; + + /// + /// Represents a decoded WASM function body with its locals and type signature. + /// + public readonly struct WasmFunctionInfo + { + public byte[] Image { get; init; } + public int InstructionOffset { get; init; } + public int InstructionLength { get; init; } + /// Local variable declarations: (count, valtype byte) pairs. + public IReadOnlyList<(uint Count, byte ValType)> Locals { get; init; } + /// Parameter types from the function's type signature. + public IReadOnlyList ParamTypes { get; init; } + /// Result types from the function's type signature. + public IReadOnlyList ResultTypes { get; init; } + } + + /// + /// Gets the full function info for a WASM function by its index in the code section. + /// Returns null if the image is not WASM-wrapped or the function index is out of range. + /// + public WasmFunctionInfo? GetWasmFunctionBody(int functionIndex) + { + if (!IsWasmWrapped) + return null; + + // First pass: collect type section (section 1) and function section (section 3) + List<(byte[] ParamTypes, byte[] ResultTypes)> types = null; + List funcTypeIndices = null; + int codeOffset = -1; + + int offset = 8; // Skip WASM magic + version + while (offset < _image.Length) + { + byte sectionId = _image[offset++]; + uint sectionSize = ReadLebU32(_image, ref offset); + int sectionEnd = offset + (int)sectionSize; + + switch (sectionId) + { + case 1: // Type section + types = ParseTypeSection(_image, ref offset, sectionEnd); + break; + case 3: // Function section + funcTypeIndices = ParseFunctionSection(_image, ref offset, sectionEnd); + break; + case 10: // Code section + codeOffset = offset; + break; + } + + offset = sectionEnd; + } + + if (codeOffset < 0) + return null; + + // Parse the code section to find the target function body + offset = codeOffset; + uint funcCount = ReadLebU32(_image, ref offset); + for (uint i = 0; i < funcCount; i++) + { + uint bodySize = ReadLebU32(_image, ref offset); + int bodyEnd = offset + (int)bodySize; + + if (i == (uint)functionIndex) + { + // Read local declarations + var locals = new List<(uint Count, byte ValType)>(); + uint localDeclCount = ReadLebU32(_image, ref offset); + for (uint j = 0; j < localDeclCount; j++) + { + uint count = ReadLebU32(_image, ref offset); + byte valType = _image[offset++]; + locals.Add((count, valType)); + } + + int instrLength = bodyEnd - offset; + + // Resolve type signature + byte[] paramTypes = Array.Empty(); + byte[] resultTypes = Array.Empty(); + if (funcTypeIndices is not null && (uint)functionIndex < funcTypeIndices.Count) + { + uint typeIdx = funcTypeIndices[functionIndex]; + if (types is not null && typeIdx < types.Count) + { + paramTypes = types[(int)typeIdx].ParamTypes; + resultTypes = types[(int)typeIdx].ResultTypes; + } + } + + return new WasmFunctionInfo + { + Image = _image, + InstructionOffset = offset, + InstructionLength = instrLength, + Locals = locals, + ParamTypes = paramTypes, + ResultTypes = resultTypes + }; + } + + offset = bodyEnd; + } + + return null; + } + + private static List<(byte[] ParamTypes, byte[] ResultTypes)> ParseTypeSection(byte[] data, ref int offset, int end) + { + var types = new List<(byte[], byte[])>(); + uint count = ReadLebU32(data, ref offset); + for (uint i = 0; i < count && offset < end; i++) + { + byte form = data[offset++]; + // 0x60 = func type + if (form != 0x60) + { + // Skip unknown type forms + break; + } + uint paramCount = ReadLebU32(data, ref offset); + byte[] paramTypes = new byte[paramCount]; + for (uint j = 0; j < paramCount; j++) + paramTypes[j] = data[offset++]; + uint resultCount = ReadLebU32(data, ref offset); + byte[] resultTypes = new byte[resultCount]; + for (uint j = 0; j < resultCount; j++) + resultTypes[j] = data[offset++]; + types.Add((paramTypes, resultTypes)); + } + return types; + } + + private static List ParseFunctionSection(byte[] data, ref int offset, int end) + { + var indices = new List(); + uint count = ReadLebU32(data, ref offset); + for (uint i = 0; i < count && offset < end; i++) + { + indices.Add(ReadLebU32(data, ref offset)); + } + return indices; + } + + private static uint ReadLebU32(byte[] data, ref int offset) + { + uint result = 0; + int shift = 0; + byte b; + do + { + if (offset >= data.Length) + return result; + b = data[offset++]; + result |= (uint)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + return result; + } + public int GetOffset(int rva) { foreach (var section in _sections) diff --git a/src/coreclr/tools/r2rdump/TextDumper.cs b/src/coreclr/tools/r2rdump/TextDumper.cs index d9fd852366421f..13d30dd36f337a 100644 --- a/src/coreclr/tools/r2rdump/TextDumper.cs +++ b/src/coreclr/tools/r2rdump/TextDumper.cs @@ -230,21 +230,37 @@ public override void DumpRuntimeFunction(RuntimeFunction rtf) _writer.WriteLine(rtf.Method.SignatureString); rtf.WriteTo(_writer, _model); + bool isWasm = _r2r.CompositeReader is WebcilImageReader; + if (_model.Disasm) { - DumpDisasm(rtf, _r2r.GetOffset(rtf.StartAddress)); + if (isWasm) + { + DumpWasmDisasm(rtf); + } + else + { + DumpDisasm(rtf, _r2r.GetOffset(rtf.StartAddress)); + } } if (_model.Raw) { - _writer.WriteLine("Raw Bytes:"); - DumpBytes(rtf.StartAddress, (uint)rtf.Size); + if (isWasm) + { + _writer.WriteLine("Raw Bytes: (not available for WASM function indices)"); + } + else + { + _writer.WriteLine("Raw Bytes:"); + DumpBytes(rtf.StartAddress, (uint)rtf.Size); + } } if (_model.Unwind) { _writer.WriteLine("UnwindInfo:"); _writer.Write(rtf.UnwindInfo); - if (_model.Raw) + if (_model.Raw && !isWasm) { DumpBytes(rtf.UnwindRVA, (uint)rtf.UnwindInfo.Size); } @@ -252,6 +268,79 @@ public override void DumpRuntimeFunction(RuntimeFunction rtf) SkipLine(); } + private void DumpWasmDisasm(RuntimeFunction rtf) + { + var webcilReader = (WebcilImageReader)_r2r.CompositeReader; + var body = webcilReader.GetWasmFunctionBody(rtf.StartAddress); + if (body is null) + { + _writer.WriteLine($" ; WASM function index: {rtf.StartAddress} (function body not found)"); + return; + } + + var info = body.Value; + _writer.WriteLine($" ; WASM function index: {rtf.StartAddress}"); + + // Print parameters with their local indices + int localIdx = 0; + if (info.ParamTypes.Count > 0) + { + _writer.WriteLine(" ; Parameters:"); + foreach (byte paramType in info.ParamTypes) + { + _writer.WriteLine($" ; [{localIdx}] {WasmValTypeName(paramType)}"); + localIdx++; + } + } + + // Print locals with their local indices + if (info.Locals.Count > 0) + { + _writer.WriteLine(" ; Locals:"); + foreach (var (count, valType) in info.Locals) + { + string typeName = WasmValTypeName(valType); + for (uint k = 0; k < count; k++) + { + _writer.WriteLine($" ; [{localIdx}] {typeName}"); + localIdx++; + } + } + } + + // Print result types + if (info.ResultTypes.Count > 0) + { + string resultStr = string.Join(", ", FormatValTypes(info.ResultTypes)); + _writer.WriteLine($" ; Results: {resultStr}"); + } + + _writer.WriteLine(); + var disasm = new WasmDisassembler(info.Image, info.InstructionOffset, info.InstructionLength); + _writer.Write(disasm.Disassemble()); + } + + private static IEnumerable FormatValTypes(IReadOnlyList types) + { + foreach (byte t in types) + yield return WasmValTypeName(t); + } + + private static string WasmValTypeName(byte b) + { + return b switch + { + 0x7F => "i32", + 0x7E => "i64", + 0x7D => "f32", + 0x7C => "f64", + 0x7B => "v128", + 0x70 => "funcref", + 0x6F => "externref", + _ => $"0x{b:X2}" + }; + } + /// /// Dumps disassembly and register liveness /// diff --git a/src/coreclr/tools/r2rdump/WasmDisassembler.cs b/src/coreclr/tools/r2rdump/WasmDisassembler.cs new file mode 100644 index 00000000000000..9d11f5e14b8dbf --- /dev/null +++ b/src/coreclr/tools/r2rdump/WasmDisassembler.cs @@ -0,0 +1,680 @@ +// 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.Text; + +namespace R2RDump +{ + /// + /// Disassembler for WebAssembly bytecode. + /// Decodes WASM binary instructions into WAT (WebAssembly Text) format. + /// Based on the WebAssembly specification: https://webassembly.github.io/spec/core/ + /// + internal sealed class WasmDisassembler + { + private readonly byte[] _code; + private int _offset; + private readonly int _baseOffset; + private readonly int _endOffset; + + public WasmDisassembler(byte[] code, int offset, int length) + { + _code = code; + _baseOffset = offset; + _offset = offset; + _endOffset = offset + length; + } + + /// + /// Disassemble all instructions in the function body and return the textual representation. + /// + public string Disassemble() + { + var sb = new StringBuilder(); + int indent = 0; + + while (_offset < _endOffset) + { + int instrOffset = _offset - _baseOffset; + string instr = DecodeInstruction(ref indent); + + sb.Append($" {instrOffset:X4}: "); + if (indent > 0) + { + sb.Append(' ', indent * 2); + } + sb.AppendLine(instr); + } + + return sb.ToString(); + } + + private string DecodeInstruction(ref int indent) + { + byte opcode = ReadByte(); + + switch (opcode) + { + // Control instructions + case 0x00: return "unreachable"; + case 0x01: return "nop"; + case 0x02: + { + string bt = ReadBlockType(); + indent++; + return $"block{bt}"; + } + case 0x03: + { + string bt = ReadBlockType(); + indent++; + return $"loop{bt}"; + } + case 0x04: + { + string bt = ReadBlockType(); + indent++; + return $"if{bt}"; + } + case 0x05: + return "else"; + case 0x08: + return $"throw {ReadU32()}"; + case 0x0A: + return "throw_ref"; + case 0x0B: + if (indent > 0) indent--; + return "end"; + case 0x0C: return $"br {ReadU32()}"; + case 0x0D: return $"br_if {ReadU32()}"; + case 0x0E: + { + uint count = ReadU32(); + var sb = new StringBuilder("br_table"); + for (uint i = 0; i <= count; i++) + { + sb.Append($" {ReadU32()}"); + } + return sb.ToString(); + } + case 0x0F: return "return"; + case 0x10: return $"call {ReadU32()}"; + case 0x11: + { + uint typeIdx = ReadU32(); + uint tableIdx = ReadU32(); + return $"call_indirect {tableIdx} (type {typeIdx})"; + } + case 0x12: return $"return_call {ReadU32()}"; + case 0x13: + { + uint typeIdx = ReadU32(); + uint tableIdx = ReadU32(); + return $"return_call_indirect {tableIdx} (type {typeIdx})"; + } + case 0x14: return $"call_ref {ReadU32()}"; + case 0x15: return $"return_call_ref {ReadU32()}"; + + // Parametric instructions + case 0x1A: return "drop"; + case 0x1B: return "select"; + case 0x1C: + { + uint count = ReadU32(); + var sb = new StringBuilder("select"); + for (uint i = 0; i < count; i++) + { + sb.Append($" {ValTypeName(ReadByte())}"); + } + return sb.ToString(); + } + + // Variable instructions + case 0x20: return $"local.get {ReadU32()}"; + case 0x21: return $"local.set {ReadU32()}"; + case 0x22: return $"local.tee {ReadU32()}"; + case 0x23: return $"global.get {ReadU32()}"; + case 0x24: return $"global.set {ReadU32()}"; + + // Table instructions + case 0x25: return $"table.get {ReadU32()}"; + case 0x26: return $"table.set {ReadU32()}"; + + // Memory instructions + case 0x28: return $"i32.load {ReadMemArg()}"; + case 0x29: return $"i64.load {ReadMemArg()}"; + case 0x2A: return $"f32.load {ReadMemArg()}"; + case 0x2B: return $"f64.load {ReadMemArg()}"; + case 0x2C: return $"i32.load8_s {ReadMemArg()}"; + case 0x2D: return $"i32.load8_u {ReadMemArg()}"; + case 0x2E: return $"i32.load16_s {ReadMemArg()}"; + case 0x2F: return $"i32.load16_u {ReadMemArg()}"; + case 0x30: return $"i64.load8_s {ReadMemArg()}"; + case 0x31: return $"i64.load8_u {ReadMemArg()}"; + case 0x32: return $"i64.load16_s {ReadMemArg()}"; + case 0x33: return $"i64.load16_u {ReadMemArg()}"; + case 0x34: return $"i64.load32_s {ReadMemArg()}"; + case 0x35: return $"i64.load32_u {ReadMemArg()}"; + case 0x36: return $"i32.store {ReadMemArg()}"; + case 0x37: return $"i64.store {ReadMemArg()}"; + case 0x38: return $"f32.store {ReadMemArg()}"; + case 0x39: return $"f64.store {ReadMemArg()}"; + case 0x3A: return $"i32.store8 {ReadMemArg()}"; + case 0x3B: return $"i32.store16 {ReadMemArg()}"; + case 0x3C: return $"i64.store8 {ReadMemArg()}"; + case 0x3D: return $"i64.store16 {ReadMemArg()}"; + case 0x3E: return $"i64.store32 {ReadMemArg()}"; + case 0x3F: + { + uint memIdx = ReadU32(); + return $"memory.size {memIdx}"; + } + case 0x40: + { + uint memIdx = ReadU32(); + return $"memory.grow {memIdx}"; + } + + // Numeric instructions - constants + case 0x41: return $"i32.const {ReadI32()}"; + case 0x42: return $"i64.const {ReadI64()}"; + case 0x43: return $"f32.const {ReadF32()}"; + case 0x44: return $"f64.const {ReadF64()}"; + + // Numeric instructions - comparison (i32) + case 0x45: return "i32.eqz"; + case 0x46: return "i32.eq"; + case 0x47: return "i32.ne"; + case 0x48: return "i32.lt_s"; + case 0x49: return "i32.lt_u"; + case 0x4A: return "i32.gt_s"; + case 0x4B: return "i32.gt_u"; + case 0x4C: return "i32.le_s"; + case 0x4D: return "i32.le_u"; + case 0x4E: return "i32.ge_s"; + case 0x4F: return "i32.ge_u"; + + // Numeric instructions - comparison (i64) + case 0x50: return "i64.eqz"; + case 0x51: return "i64.eq"; + case 0x52: return "i64.ne"; + case 0x53: return "i64.lt_s"; + case 0x54: return "i64.lt_u"; + case 0x55: return "i64.gt_s"; + case 0x56: return "i64.gt_u"; + case 0x57: return "i64.le_s"; + case 0x58: return "i64.le_u"; + case 0x59: return "i64.ge_s"; + case 0x5A: return "i64.ge_u"; + + // Numeric instructions - comparison (f32) + case 0x5B: return "f32.eq"; + case 0x5C: return "f32.ne"; + case 0x5D: return "f32.lt"; + case 0x5E: return "f32.gt"; + case 0x5F: return "f32.le"; + case 0x60: return "f32.ge"; + + // Numeric instructions - comparison (f64) + case 0x61: return "f64.eq"; + case 0x62: return "f64.ne"; + case 0x63: return "f64.lt"; + case 0x64: return "f64.gt"; + case 0x65: return "f64.le"; + case 0x66: return "f64.ge"; + + // Numeric instructions - arithmetic (i32) + case 0x67: return "i32.clz"; + case 0x68: return "i32.ctz"; + case 0x69: return "i32.popcnt"; + case 0x6A: return "i32.add"; + case 0x6B: return "i32.sub"; + case 0x6C: return "i32.mul"; + case 0x6D: return "i32.div_s"; + case 0x6E: return "i32.div_u"; + case 0x6F: return "i32.rem_s"; + case 0x70: return "i32.rem_u"; + case 0x71: return "i32.and"; + case 0x72: return "i32.or"; + case 0x73: return "i32.xor"; + case 0x74: return "i32.shl"; + case 0x75: return "i32.shr_s"; + case 0x76: return "i32.shr_u"; + case 0x77: return "i32.rotl"; + case 0x78: return "i32.rotr"; + + // Numeric instructions - arithmetic (i64) + case 0x79: return "i64.clz"; + case 0x7A: return "i64.ctz"; + case 0x7B: return "i64.popcnt"; + case 0x7C: return "i64.add"; + case 0x7D: return "i64.sub"; + case 0x7E: return "i64.mul"; + case 0x7F: return "i64.div_s"; + case 0x80: return "i64.div_u"; + case 0x81: return "i64.rem_s"; + case 0x82: return "i64.rem_u"; + case 0x83: return "i64.and"; + case 0x84: return "i64.or"; + case 0x85: return "i64.xor"; + case 0x86: return "i64.shl"; + case 0x87: return "i64.shr_s"; + case 0x88: return "i64.shr_u"; + case 0x89: return "i64.rotl"; + case 0x8A: return "i64.rotr"; + + // Numeric instructions - arithmetic (f32) + case 0x8B: return "f32.abs"; + case 0x8C: return "f32.neg"; + case 0x8D: return "f32.ceil"; + case 0x8E: return "f32.floor"; + case 0x8F: return "f32.trunc"; + case 0x90: return "f32.nearest"; + case 0x91: return "f32.sqrt"; + case 0x92: return "f32.add"; + case 0x93: return "f32.sub"; + case 0x94: return "f32.mul"; + case 0x95: return "f32.div"; + case 0x96: return "f32.min"; + case 0x97: return "f32.max"; + case 0x98: return "f32.copysign"; + + // Numeric instructions - arithmetic (f64) + case 0x99: return "f64.abs"; + case 0x9A: return "f64.neg"; + case 0x9B: return "f64.ceil"; + case 0x9C: return "f64.floor"; + case 0x9D: return "f64.trunc"; + case 0x9E: return "f64.nearest"; + case 0x9F: return "f64.sqrt"; + case 0xA0: return "f64.add"; + case 0xA1: return "f64.sub"; + case 0xA2: return "f64.mul"; + case 0xA3: return "f64.div"; + case 0xA4: return "f64.min"; + case 0xA5: return "f64.max"; + case 0xA6: return "f64.copysign"; + + // Numeric instructions - conversions + case 0xA7: return "i32.wrap_i64"; + case 0xA8: return "i32.trunc_f32_s"; + case 0xA9: return "i32.trunc_f32_u"; + case 0xAA: return "i32.trunc_f64_s"; + case 0xAB: return "i32.trunc_f64_u"; + case 0xAC: return "i64.extend_i32_s"; + case 0xAD: return "i64.extend_i32_u"; + case 0xAE: return "i64.trunc_f32_s"; + case 0xAF: return "i64.trunc_f32_u"; + case 0xB0: return "i64.trunc_f64_s"; + case 0xB1: return "i64.trunc_f64_u"; + case 0xB2: return "f32.convert_i32_s"; + case 0xB3: return "f32.convert_i32_u"; + case 0xB4: return "f32.convert_i64_s"; + case 0xB5: return "f32.convert_i64_u"; + case 0xB6: return "f32.demote_f64"; + case 0xB7: return "f64.convert_i32_s"; + case 0xB8: return "f64.convert_i32_u"; + case 0xB9: return "f64.convert_i64_s"; + case 0xBA: return "f64.convert_i64_u"; + case 0xBB: return "f64.promote_f32"; + case 0xBC: return "i32.reinterpret_f32"; + case 0xBD: return "i64.reinterpret_f64"; + case 0xBE: return "f32.reinterpret_i32"; + case 0xBF: return "f64.reinterpret_i64"; + + // Sign extension instructions + case 0xC0: return "i32.extend8_s"; + case 0xC1: return "i32.extend16_s"; + case 0xC2: return "i64.extend8_s"; + case 0xC3: return "i64.extend16_s"; + case 0xC4: return "i64.extend32_s"; + + // Reference instructions + case 0xD0: return $"ref.null {ReadHeapType()}"; + case 0xD1: return "ref.is_null"; + case 0xD2: return $"ref.func {ReadU32()}"; + case 0xD3: return "ref.eq"; + case 0xD4: return "ref.as_non_null"; + case 0xD5: return $"br_on_null {ReadU32()}"; + case 0xD6: return $"br_on_non_null {ReadU32()}"; + + // GC instructions (0xFB prefix) + case 0xFB: return DecodeFBPrefixed(); + + // Saturating truncation and bulk memory (0xFC prefix) + case 0xFC: return DecodeFCPrefixed(); + + // SIMD (0xFD prefix) + case 0xFD: return DecodeFDPrefixed(); + + default: + return $""; + } + } + + private string DecodeFCPrefixed() + { + uint subOpcode = ReadU32(); + switch (subOpcode) + { + case 0: return "i32.trunc_sat_f32_s"; + case 1: return "i32.trunc_sat_f32_u"; + case 2: return "i32.trunc_sat_f64_s"; + case 3: return "i32.trunc_sat_f64_u"; + case 4: return "i64.trunc_sat_f32_s"; + case 5: return "i64.trunc_sat_f32_u"; + case 6: return "i64.trunc_sat_f64_s"; + case 7: return "i64.trunc_sat_f64_u"; + case 8: + { + uint dataIdx = ReadU32(); + uint memIdx = ReadU32(); + return $"memory.init {dataIdx} {memIdx}"; + } + case 9: return $"data.drop {ReadU32()}"; + case 10: + { + uint dstMem = ReadU32(); + uint srcMem = ReadU32(); + return $"memory.copy {dstMem} {srcMem}"; + } + case 11: return $"memory.fill {ReadU32()}"; + case 12: + { + uint elemIdx = ReadU32(); + uint tableIdx = ReadU32(); + return $"table.init {tableIdx} {elemIdx}"; + } + case 13: return $"elem.drop {ReadU32()}"; + case 14: + { + uint dstTable = ReadU32(); + uint srcTable = ReadU32(); + return $"table.copy {dstTable} {srcTable}"; + } + case 15: return $"table.grow {ReadU32()}"; + case 16: return $"table.size {ReadU32()}"; + case 17: return $"table.fill {ReadU32()}"; + default: + return $""; + } + } + + private string DecodeFBPrefixed() + { + uint subOpcode = ReadU32(); + switch (subOpcode) + { + case 0: return $"struct.new {ReadU32()}"; + case 1: return $"struct.new_default {ReadU32()}"; + case 2: + { + uint typeIdx = ReadU32(); + uint fieldIdx = ReadU32(); + return $"struct.get {typeIdx} {fieldIdx}"; + } + case 3: + { + uint typeIdx = ReadU32(); + uint fieldIdx = ReadU32(); + return $"struct.get_s {typeIdx} {fieldIdx}"; + } + case 4: + { + uint typeIdx = ReadU32(); + uint fieldIdx = ReadU32(); + return $"struct.get_u {typeIdx} {fieldIdx}"; + } + case 5: + { + uint typeIdx = ReadU32(); + uint fieldIdx = ReadU32(); + return $"struct.set {typeIdx} {fieldIdx}"; + } + case 6: return $"array.new {ReadU32()}"; + case 7: return $"array.new_default {ReadU32()}"; + case 8: + { + uint typeIdx = ReadU32(); + uint size = ReadU32(); + return $"array.new_fixed {typeIdx} {size}"; + } + case 9: + { + uint typeIdx = ReadU32(); + uint dataIdx = ReadU32(); + return $"array.new_data {typeIdx} {dataIdx}"; + } + case 10: + { + uint typeIdx = ReadU32(); + uint elemIdx = ReadU32(); + return $"array.new_elem {typeIdx} {elemIdx}"; + } + case 11: return $"array.get {ReadU32()}"; + case 12: return $"array.get_s {ReadU32()}"; + case 13: return $"array.get_u {ReadU32()}"; + case 14: return $"array.set {ReadU32()}"; + case 15: return "array.len"; + case 16: return $"array.fill {ReadU32()}"; + case 17: + { + uint dstType = ReadU32(); + uint srcType = ReadU32(); + return $"array.copy {dstType} {srcType}"; + } + case 18: + { + uint typeIdx = ReadU32(); + uint dataIdx = ReadU32(); + return $"array.init_data {typeIdx} {dataIdx}"; + } + case 19: + { + uint typeIdx = ReadU32(); + uint elemIdx = ReadU32(); + return $"array.init_elem {typeIdx} {elemIdx}"; + } + case 20: return $"ref.test (ref {ReadHeapType()})"; + case 21: return $"ref.test (ref null {ReadHeapType()})"; + case 22: return $"ref.cast (ref {ReadHeapType()})"; + case 23: return $"ref.cast (ref null {ReadHeapType()})"; + case 26: return "any.convert_extern"; + case 27: return "extern.convert_any"; + case 28: return $"ref.i31"; + case 29: return "i31.get_s"; + case 30: return "i31.get_u"; + default: + return $""; + } + } + + private string DecodeFDPrefixed() + { + uint subOpcode = ReadU32(); + // SIMD instructions - just show the sub-opcode for now + // Full SIMD decode would add hundreds of entries + if (subOpcode == 0) + { + // v128.load memarg + return $"v128.load {ReadMemArg()}"; + } + if (subOpcode == 11) + { + // v128.store memarg + return $"v128.store {ReadMemArg()}"; + } + if (subOpcode == 12) + { + // v128.const - 16 bytes immediate + var bytes = new byte[16]; + for (int i = 0; i < 16; i++) + bytes[i] = ReadByte(); + return $"v128.const 0x{BitConverter.ToString(bytes).Replace("-", "")}"; + } + return $""; + } + + private byte ReadByte() + { + if (_offset >= _endOffset) + return 0; + return _code[_offset++]; + } + + private uint ReadU32() + { + uint result = 0; + int shift = 0; + byte b; + do + { + b = ReadByte(); + result |= (uint)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + return result; + } + + private int ReadI32() + { + int result = 0; + int shift = 0; + byte b; + do + { + b = ReadByte(); + result |= (int)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + // Sign extend + if (shift < 32 && (b & 0x40) != 0) + result |= -(1 << shift); + return result; + } + + private long ReadI64() + { + long result = 0; + int shift = 0; + byte b; + do + { + b = ReadByte(); + result |= (long)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + // Sign extend + if (shift < 64 && (b & 0x40) != 0) + result |= -(1L << shift); + return result; + } + + private float ReadF32() + { + if (_offset + 4 > _endOffset) + { + _offset = _endOffset; + return 0; + } + float val = BitConverter.ToSingle(_code, _offset); + _offset += 4; + return val; + } + + private double ReadF64() + { + if (_offset + 8 > _endOffset) + { + _offset = _endOffset; + return 0; + } + double val = BitConverter.ToDouble(_code, _offset); + _offset += 8; + return val; + } + + private string ReadMemArg() + { + uint align = ReadU32(); + // Bit 6 indicates multi-memory (memory index follows) + uint memIdx = 0; + if ((align & 0x40) != 0) + { + align &= ~0x40u; + memIdx = ReadU32(); + } + uint offset = ReadU32(); + if (memIdx != 0) + return $"align={1u << (int)align} offset={offset} mem={memIdx}"; + if (offset != 0) + return $"align={1u << (int)align} offset={offset}"; + return $"align={1u << (int)align}"; + } + + private string ReadBlockType() + { + if (_offset >= _endOffset) + return ""; + byte b = _code[_offset]; + if (b == 0x40) + { + _offset++; + return ""; + } + // Value type encoding uses single bytes 0x7F-0x70 range + if (b >= 0x70 && b <= 0x7F) + { + _offset++; + return $" (result {ValTypeName(b)})"; + } + // Otherwise it's a type index as a signed LEB128 + int typeIdx = ReadI32(); + return $" (type {typeIdx})"; + } + + private string ReadHeapType() + { + byte b = _code[_offset]; + // Abstract heap types are encoded as single bytes + switch (b) + { + case 0x73: _offset++; return "nofunc"; + case 0x72: _offset++; return "noextern"; + case 0x71: _offset++; return "none"; + case 0x70: _offset++; return "func"; + case 0x6F: _offset++; return "extern"; + case 0x6E: _offset++; return "any"; + case 0x6D: _offset++; return "eq"; + case 0x6C: _offset++; return "i31"; + case 0x6B: _offset++; return "struct"; + case 0x6A: _offset++; return "array"; + default: + // Type index as signed LEB128 + return ReadI32().ToString(); + } + } + + private static string ValTypeName(byte b) + { + return b switch + { + 0x7F => "i32", + 0x7E => "i64", + 0x7D => "f32", + 0x7C => "f64", + 0x7B => "v128", + 0x70 => "funcref", + 0x6F => "externref", + 0x6E => "anyref", + 0x6D => "eqref", + 0x6C => "i31ref", + 0x6B => "structref", + 0x6A => "arrayref", + _ => $"" + }; + } + } +} From 0189c5b0af1e24d0648c09326758a512a3bf1508 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 5 May 2026 16:48:11 -0700 Subject: [PATCH 4/6] Fix GC collection of pinned metadata array in WebcilAssemblyMetadata WebcilAssemblyMetadata was not retaining a reference to the pinned metadata byte array passed to its constructor. After GetStandaloneAssemblyMetadata returned, the array could be collected by the GC despite being allocated on the Pinned Object Heap, since no live reference existed. This caused an AccessViolationException when MetadataReader accessed the freed memory on larger files like system.private.corelib.wasm. Fix: store the metadata byte array in a field to keep it rooted for the lifetime of the MetadataReader. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs index 501e3f5e06f35a..6174ba5c7da123 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/WebcilImageReader.cs @@ -533,10 +533,12 @@ internal sealed unsafe class WebcilAssemblyMetadata : IAssemblyMetadata { private readonly MetadataReader _metadataReader; private readonly WebcilImageReader _webcilReader; + private readonly byte[] _metadataBytes; public WebcilAssemblyMetadata(byte[] metadataBytes, WebcilImageReader webcilReader) { _webcilReader = webcilReader; + _metadataBytes = metadataBytes; fixed (byte* p = metadataBytes) { _metadataReader = new MetadataReader(p, metadataBytes.Length); From ee40342728e146c46700f337107d3bea929beae5 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Wed, 6 May 2026 13:28:10 -0700 Subject: [PATCH 5/6] Implement full SIMD instruction decoding in WasmDisassembler Replace the stub DecodeFDPrefixed() method with a complete implementation of all WebAssembly SIMD instructions (0xFD prefix, sub-opcodes 0-255) per the WebAssembly spec. This includes memory operations, lane load/store, shuffle, splat, extract/replace lane, comparisons, bitwise operations, arithmetic, and conversion instructions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/tools/r2rdump/WasmDisassembler.cs | 330 +++++++++++++++++- 1 file changed, 311 insertions(+), 19 deletions(-) diff --git a/src/coreclr/tools/r2rdump/WasmDisassembler.cs b/src/coreclr/tools/r2rdump/WasmDisassembler.cs index 9d11f5e14b8dbf..e604eb6fb4b419 100644 --- a/src/coreclr/tools/r2rdump/WasmDisassembler.cs +++ b/src/coreclr/tools/r2rdump/WasmDisassembler.cs @@ -494,27 +494,319 @@ private string DecodeFBPrefixed() private string DecodeFDPrefixed() { uint subOpcode = ReadU32(); - // SIMD instructions - just show the sub-opcode for now - // Full SIMD decode would add hundreds of entries - if (subOpcode == 0) - { - // v128.load memarg - return $"v128.load {ReadMemArg()}"; - } - if (subOpcode == 11) - { - // v128.store memarg - return $"v128.store {ReadMemArg()}"; - } - if (subOpcode == 12) + switch (subOpcode) { - // v128.const - 16 bytes immediate - var bytes = new byte[16]; - for (int i = 0; i < 16; i++) - bytes[i] = ReadByte(); - return $"v128.const 0x{BitConverter.ToString(bytes).Replace("-", "")}"; + // SIMD memory instructions + case 0: return $"v128.load {ReadMemArg()}"; + case 1: return $"v128.load8x8_s {ReadMemArg()}"; + case 2: return $"v128.load8x8_u {ReadMemArg()}"; + case 3: return $"v128.load16x4_s {ReadMemArg()}"; + case 4: return $"v128.load16x4_u {ReadMemArg()}"; + case 5: return $"v128.load32x2_s {ReadMemArg()}"; + case 6: return $"v128.load32x2_u {ReadMemArg()}"; + case 7: return $"v128.load8_splat {ReadMemArg()}"; + case 8: return $"v128.load16_splat {ReadMemArg()}"; + case 9: return $"v128.load32_splat {ReadMemArg()}"; + case 10: return $"v128.load64_splat {ReadMemArg()}"; + case 11: return $"v128.store {ReadMemArg()}"; + case 12: + { + var bytes = new byte[16]; + for (int i = 0; i < 16; i++) + bytes[i] = ReadByte(); + return $"v128.const 0x{BitConverter.ToString(bytes).Replace("-", "")}"; + } + + // i8x16.shuffle - 16 bytes of lane indices + case 13: + { + var lanes = new byte[16]; + for (int i = 0; i < 16; i++) + lanes[i] = ReadByte(); + return $"i8x16.shuffle {string.Join(" ", lanes)}"; + } + + // Swizzle and splat + case 14: return "i8x16.swizzle"; + case 15: return "i8x16.splat"; + case 16: return "i16x8.splat"; + case 17: return "i32x4.splat"; + case 18: return "i64x2.splat"; + case 19: return "f32x4.splat"; + case 20: return "f64x2.splat"; + + // Extract/replace lane instructions + case 21: return $"i8x16.extract_lane_s {ReadByte()}"; + case 22: return $"i8x16.extract_lane_u {ReadByte()}"; + case 23: return $"i8x16.replace_lane {ReadByte()}"; + case 24: return $"i16x8.extract_lane_s {ReadByte()}"; + case 25: return $"i16x8.extract_lane_u {ReadByte()}"; + case 26: return $"i16x8.replace_lane {ReadByte()}"; + case 27: return $"i32x4.extract_lane {ReadByte()}"; + case 28: return $"i32x4.replace_lane {ReadByte()}"; + case 29: return $"i64x2.extract_lane {ReadByte()}"; + case 30: return $"i64x2.replace_lane {ReadByte()}"; + case 31: return $"f32x4.extract_lane {ReadByte()}"; + case 32: return $"f32x4.replace_lane {ReadByte()}"; + case 33: return $"f64x2.extract_lane {ReadByte()}"; + case 34: return $"f64x2.replace_lane {ReadByte()}"; + + // i8x16 comparison + case 35: return "i8x16.eq"; + case 36: return "i8x16.ne"; + case 37: return "i8x16.lt_s"; + case 38: return "i8x16.lt_u"; + case 39: return "i8x16.gt_s"; + case 40: return "i8x16.gt_u"; + case 41: return "i8x16.le_s"; + case 42: return "i8x16.le_u"; + case 43: return "i8x16.ge_s"; + case 44: return "i8x16.ge_u"; + + // i16x8 comparison + case 45: return "i16x8.eq"; + case 46: return "i16x8.ne"; + case 47: return "i16x8.lt_s"; + case 48: return "i16x8.lt_u"; + case 49: return "i16x8.gt_s"; + case 50: return "i16x8.gt_u"; + case 51: return "i16x8.le_s"; + case 52: return "i16x8.le_u"; + case 53: return "i16x8.ge_s"; + case 54: return "i16x8.ge_u"; + + // i32x4 comparison + case 55: return "i32x4.eq"; + case 56: return "i32x4.ne"; + case 57: return "i32x4.lt_s"; + case 58: return "i32x4.lt_u"; + case 59: return "i32x4.gt_s"; + case 60: return "i32x4.gt_u"; + case 61: return "i32x4.le_s"; + case 62: return "i32x4.le_u"; + case 63: return "i32x4.ge_s"; + case 64: return "i32x4.ge_u"; + + // f32x4 comparison + case 65: return "f32x4.eq"; + case 66: return "f32x4.ne"; + case 67: return "f32x4.lt"; + case 68: return "f32x4.gt"; + case 69: return "f32x4.le"; + case 70: return "f32x4.ge"; + + // f64x2 comparison + case 71: return "f64x2.eq"; + case 72: return "f64x2.ne"; + case 73: return "f64x2.lt"; + case 74: return "f64x2.gt"; + case 75: return "f64x2.le"; + case 76: return "f64x2.ge"; + + // v128 bitwise operations + case 77: return "v128.not"; + case 78: return "v128.and"; + case 79: return "v128.andnot"; + case 80: return "v128.or"; + case 81: return "v128.xor"; + case 82: return "v128.bitselect"; + case 83: return "v128.any_true"; + + // Lane load/store with lane index + case 84: return $"v128.load8_lane {ReadMemArg()} {ReadByte()}"; + case 85: return $"v128.load16_lane {ReadMemArg()} {ReadByte()}"; + case 86: return $"v128.load32_lane {ReadMemArg()} {ReadByte()}"; + case 87: return $"v128.load64_lane {ReadMemArg()} {ReadByte()}"; + case 88: return $"v128.store8_lane {ReadMemArg()} {ReadByte()}"; + case 89: return $"v128.store16_lane {ReadMemArg()} {ReadByte()}"; + case 90: return $"v128.store32_lane {ReadMemArg()} {ReadByte()}"; + case 91: return $"v128.store64_lane {ReadMemArg()} {ReadByte()}"; + + // v128.load zero + case 92: return $"v128.load32_zero {ReadMemArg()}"; + case 93: return $"v128.load64_zero {ReadMemArg()}"; + + // Conversions + case 94: return "i32x4.trunc_sat_f32x4_s"; + case 95: return "i32x4.trunc_sat_f32x4_u"; + + // i8x16 operations + case 96: return "i8x16.abs"; + case 97: return "i8x16.neg"; + case 98: return "i8x16.popcnt"; + case 99: return "i8x16.all_true"; + case 100: return "i8x16.bitmask"; + case 101: return "i8x16.narrow_i16x8_s"; + case 102: return "i8x16.narrow_i16x8_u"; + + // f32x4 rounding + case 103: return "f32x4.ceil"; + case 104: return "f32x4.floor"; + case 105: return "f32x4.trunc"; + case 106: return "f32x4.nearest"; + + // i8x16 shift and arithmetic + case 107: return "i8x16.shl"; + case 108: return "i8x16.shr_s"; + case 109: return "i8x16.shr_u"; + case 110: return "i8x16.add"; + case 111: return "i8x16.add_sat_s"; + case 112: return "i8x16.add_sat_u"; + case 113: return "i8x16.sub"; + case 114: return "i8x16.sub_sat_s"; + case 115: return "i8x16.sub_sat_u"; + + // f64x2 rounding + case 116: return "f64x2.ceil"; + case 117: return "f64x2.floor"; + + // i8x16 min/max/avgr + case 118: return "i8x16.min_s"; + case 119: return "i8x16.min_u"; + case 120: return "i8x16.max_s"; + case 121: return "i8x16.max_u"; + case 123: return "i8x16.avgr_u"; + + // i16x8 pairwise addition + case 124: return "i16x8.extadd_pairwise_i8x16_s"; + case 125: return "i16x8.extadd_pairwise_i8x16_u"; + + // i32x4 pairwise addition + case 126: return "i32x4.extadd_pairwise_i16x8_s"; + case 127: return "i32x4.extadd_pairwise_i16x8_u"; + + // i16x8 operations + case 128: return "i16x8.abs"; + case 129: return "i16x8.neg"; + case 130: return "i16x8.q15mulr_sat_s"; + case 131: return "i16x8.all_true"; + case 132: return "i16x8.bitmask"; + case 133: return "i16x8.narrow_i32x4_s"; + case 134: return "i16x8.narrow_i32x4_u"; + case 135: return "i16x8.extend_low_i8x16_s"; + case 136: return "i16x8.extend_high_i8x16_s"; + case 137: return "i16x8.extend_low_i8x16_u"; + case 138: return "i16x8.extend_high_i8x16_u"; + case 139: return "i16x8.shl"; + case 140: return "i16x8.shr_s"; + case 141: return "i16x8.shr_u"; + case 142: return "i16x8.add"; + case 143: return "i16x8.add_sat_s"; + case 144: return "i16x8.add_sat_u"; + case 145: return "i16x8.sub"; + case 146: return "i16x8.sub_sat_s"; + case 147: return "i16x8.sub_sat_u"; + + // f64x2 rounding (continued) + case 148: return "f64x2.trunc"; + + // i16x8 multiply + case 149: return "i16x8.mul"; + case 150: return "i16x8.min_s"; + case 151: return "i16x8.min_u"; + case 152: return "i16x8.max_s"; + case 153: return "i16x8.max_u"; + + // f64x2 rounding (continued) + case 154: return "f64x2.nearest"; + + // i16x8 average and extended multiply + case 155: return "i16x8.avgr_u"; + case 156: return "i16x8.extmul_low_i8x16_s"; + case 157: return "i16x8.extmul_high_i8x16_s"; + case 158: return "i16x8.extmul_low_i8x16_u"; + case 159: return "i16x8.extmul_high_i8x16_u"; + + // i32x4 operations + case 160: return "i32x4.abs"; + case 161: return "i32x4.neg"; + case 163: return "i32x4.all_true"; + case 164: return "i32x4.bitmask"; + case 167: return "i32x4.extend_low_i16x8_s"; + case 168: return "i32x4.extend_high_i16x8_s"; + case 169: return "i32x4.extend_low_i16x8_u"; + case 170: return "i32x4.extend_high_i16x8_u"; + case 171: return "i32x4.shl"; + case 172: return "i32x4.shr_s"; + case 173: return "i32x4.shr_u"; + case 174: return "i32x4.add"; + case 177: return "i32x4.sub"; + case 181: return "i32x4.mul"; + case 182: return "i32x4.min_s"; + case 183: return "i32x4.min_u"; + case 184: return "i32x4.max_s"; + case 185: return "i32x4.max_u"; + case 186: return "i32x4.dot_i16x8_s"; + case 188: return "i32x4.extmul_low_i16x8_s"; + case 189: return "i32x4.extmul_high_i16x8_s"; + case 190: return "i32x4.extmul_low_i16x8_u"; + case 191: return "i32x4.extmul_high_i16x8_u"; + + // i64x2 operations + case 192: return "i64x2.abs"; + case 193: return "i64x2.neg"; + case 195: return "i64x2.all_true"; + case 196: return "i64x2.bitmask"; + case 199: return "i64x2.extend_low_i32x4_s"; + case 200: return "i64x2.extend_high_i32x4_s"; + case 201: return "i64x2.extend_low_i32x4_u"; + case 202: return "i64x2.extend_high_i32x4_u"; + case 203: return "i64x2.shl"; + case 204: return "i64x2.shr_s"; + case 205: return "i64x2.shr_u"; + case 206: return "i64x2.add"; + case 209: return "i64x2.sub"; + case 213: return "i64x2.mul"; + case 214: return "i64x2.eq"; + case 215: return "i64x2.ne"; + case 216: return "i64x2.lt_s"; + case 217: return "i64x2.gt_s"; + case 218: return "i64x2.le_s"; + case 219: return "i64x2.ge_s"; + case 220: return "i64x2.extmul_low_i32x4_s"; + case 221: return "i64x2.extmul_high_i32x4_s"; + case 222: return "i64x2.extmul_low_i32x4_u"; + case 223: return "i64x2.extmul_high_i32x4_u"; + + // f32x4 operations + case 224: return "f32x4.abs"; + case 225: return "f32x4.neg"; + case 227: return "f32x4.sqrt"; + case 228: return "f32x4.add"; + case 229: return "f32x4.sub"; + case 230: return "f32x4.mul"; + case 231: return "f32x4.div"; + case 232: return "f32x4.min"; + case 233: return "f32x4.max"; + case 234: return "f32x4.pmin"; + case 235: return "f32x4.pmax"; + + // f64x2 operations + case 236: return "f64x2.abs"; + case 237: return "f64x2.neg"; + case 239: return "f64x2.sqrt"; + case 240: return "f64x2.add"; + case 241: return "f64x2.sub"; + case 242: return "f64x2.mul"; + case 243: return "f64x2.div"; + case 244: return "f64x2.min"; + case 245: return "f64x2.max"; + case 246: return "f64x2.pmin"; + case 247: return "f64x2.pmax"; + + // Conversion instructions + case 248: return "i32x4.trunc_sat_f64x2_s_zero"; + case 249: return "i32x4.trunc_sat_f64x2_u_zero"; + case 250: return "f32x4.convert_i32x4_s"; + case 251: return "f32x4.convert_i32x4_u"; + case 252: return "f64x2.convert_low_i32x4_s"; + case 253: return "f64x2.convert_low_i32x4_u"; + case 254: return "f32x4.demote_f64x2_zero"; + case 255: return "f64x2.promote_low_f32x4"; + + default: + return $""; } - return $""; } private byte ReadByte() From 7bc3d288eee2131b580888afd9c9cb05d5ab835c Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Wed, 6 May 2026 13:36:16 -0700 Subject: [PATCH 6/6] Add try_table instruction decoding to WasmDisassembler Implement opcode 0x1F (try_table) per the WebAssembly exception handling spec. Decodes the block type and vector of catch clauses, supporting all four catch clause kinds: catch, catch_ref, catch_all, catch_all_ref. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/tools/r2rdump/WasmDisassembler.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/coreclr/tools/r2rdump/WasmDisassembler.cs b/src/coreclr/tools/r2rdump/WasmDisassembler.cs index e604eb6fb4b419..ed8876848809c5 100644 --- a/src/coreclr/tools/r2rdump/WasmDisassembler.cs +++ b/src/coreclr/tools/r2rdump/WasmDisassembler.cs @@ -88,6 +88,50 @@ private string DecodeInstruction(ref int indent) if (indent > 0) indent--; return "end"; case 0x0C: return $"br {ReadU32()}"; + case 0x1F: + { + string bt = ReadBlockType(); + uint catchCount = ReadU32(); + var sb = new StringBuilder($"try_table{bt}"); + for (uint i = 0; i < catchCount; i++) + { + byte catchKind = ReadByte(); + switch (catchKind) + { + case 0x00: + { + uint tagIdx = ReadU32(); + uint labelIdx = ReadU32(); + sb.Append($" (catch {tagIdx} {labelIdx})"); + break; + } + case 0x01: + { + uint tagIdx = ReadU32(); + uint labelIdx = ReadU32(); + sb.Append($" (catch_ref {tagIdx} {labelIdx})"); + break; + } + case 0x02: + { + uint labelIdx = ReadU32(); + sb.Append($" (catch_all {labelIdx})"); + break; + } + case 0x03: + { + uint labelIdx = ReadU32(); + sb.Append($" (catch_all_ref {labelIdx})"); + break; + } + default: + sb.Append($" ()"); + break; + } + } + indent++; + return sb.ToString(); + } case 0x0D: return $"br_if {ReadU32()}"; case 0x0E: {