From 1b6a74f6ad26f6fbf21992d11a86d41e3446e97a Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 17 Sep 2026 05:14:22 -0400 Subject: [PATCH] Fix and enable windows+arm disassembly Co-Authored-By: Claude Opus 5 (1M context) --- src/BenchmarkDotNet/BenchmarkDotNet.targets | 10 ++++ .../Disassemblers/Arm64Disassembler.cs | 2 +- .../Disassemblers/CapstoneFactory.cs | 58 +++++++++++++++++++ .../Disassemblers/DataContracts.cs | 2 +- .../Disassemblers/DisassemblyDiagnoser.cs | 6 -- .../DisassemblyDiagnoserTests.cs | 19 +++--- .../DisassemblerModelSerializationTests.cs | 5 +- 7 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.targets b/src/BenchmarkDotNet/BenchmarkDotNet.targets index 110a9cde82..62c85d5b99 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.targets +++ b/src/BenchmarkDotNet/BenchmarkDotNet.targets @@ -25,6 +25,16 @@ Condition="'%(RuntimeTargetsCopyLocalItems.NuGetPackageId)' == 'Gee.External.Capstone' AND '%(RuntimeTargetsCopyLocalItems.RuntimeIdentifier)' != '$(BenchmarkDotNetTargetPlatform)'" /> + + + + true + + + Decode(byte[] code, ulong startAddress, State state, int depth, IClrMethod currentMethod, DisassemblySyntax syntax) { const Arm64DisassembleMode disassembleMode = Arm64DisassembleMode.Arm; - using (CapstoneArm64Disassembler disassembler = CapstoneDisassembler.CreateArm64Disassembler(disassembleMode)) + using (CapstoneArm64Disassembler disassembler = CapstoneFactory.CreateArm64Disassembler(disassembleMode)) { // Enables disassemble details, which are disabled by default, to provide more detailed information on // disassembled binary code. diff --git a/src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs b/src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs new file mode 100644 index 0000000000..f0d73557bc --- /dev/null +++ b/src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs @@ -0,0 +1,58 @@ +using Gee.External.Capstone; +using Gee.External.Capstone.Arm64; +#if !NET +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; +using System.Runtime.InteropServices; +using RuntimeInformation = BenchmarkDotNet.Portability.RuntimeInformation; +#endif + +namespace BenchmarkDotNet.Disassemblers; + +internal static class CapstoneFactory +{ + internal static CapstoneArm64Disassembler CreateArm64Disassembler(Arm64DisassembleMode disassembleMode) + { +#if !NET + _ = FrameworkNativeLibrary.Handle.Value; +#endif + return CapstoneDisassembler.CreateArm64Disassembler(disassembleMode); + } + +#if !NET + /// + /// .NET Framework does not probe runtimes/{rid}/native, where BenchmarkDotNet.targets copies the native capstone. + /// Once it is loaded by its full path, the P/Invokes that name the module bind to the loaded library. + /// When the file is not there, the default search applies - a build for a specific RuntimeIdentifier puts it next to the application. + /// + private static class FrameworkNativeLibrary + { + internal static readonly Lazy Handle = new(Load); + + private static IntPtr Load() + { + if (!RuntimeInformation.IsFullFramework) + return IntPtr.Zero; + + string? rid = RuntimeInformation.GetCurrentPlatform() switch + { + Platform.X86 => "win-x86", + Platform.X64 => "win-x64", + Platform.Arm64 => "win-arm64", + _ => null + }; + var assembly = typeof(CapstoneDisassembler).Assembly; + string location = ShadowCopyHelper.TryGetOriginalLocation(assembly, out var originalLocation) ? originalLocation : assembly.Location; + if (rid is null || location.IsBlank()) + return IntPtr.Zero; + + string path = Path.Combine(Path.GetDirectoryName(location)!, "runtimes", rid, "native", "capstone.dll"); + return File.Exists(path) ? LoadLibraryW(path) : IntPtr.Zero; + } + + [DllImport("kernel32", ExactSpelling = true, CharSet = CharSet.Unicode)] + private static extern IntPtr LoadLibraryW(string lpLibFileName); + } +#endif +} diff --git a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs index ec59b9bef8..cbc6b9406f 100644 --- a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs +++ b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs @@ -142,7 +142,7 @@ public override Arm64AsmData Read(ref Utf8JsonReader reader, Type typeToConvert, if (reader.TokenType != JsonTokenType.EndObject) throw new JsonException("Invalid JSON"); - using var disassembler = CapstoneDisassembler.CreateArm64Disassembler(Arm64DisassembleMode.Arm); + using var disassembler = CapstoneFactory.CreateArm64Disassembler(Arm64DisassembleMode.Arm); disassembler.EnableInstructionDetails = true; disassembler.DisassembleSyntax = syntax; var instruction = disassembler.Disassemble(instructionBytes, instructionAddress).SingleOrDefault(); diff --git a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs index 42959a5e2f..db140707a4 100644 --- a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs +++ b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs @@ -121,12 +121,6 @@ public async IAsyncEnumerable ValidateAsync(ValidationParameter yield break; } - if (currentPlatform == Platform.Arm64 && OsDetector.IsWindows()) - { - yield return new ValidationError(true, $"{nameof(DisassemblyDiagnoser)} does not support Arm on Windows"); - yield break; - } - if (Config.RunInHost && OsDetector.IsMacOS()) { yield return new ValidationError(true, "Disassembling in the host process is not supported on MacOS"); diff --git a/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs b/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs index 86dbebf5c1..1dfdf87de6 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Detectors; @@ -10,7 +10,6 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Portability; using BenchmarkDotNet.Tests.Loggers; -using BenchmarkDotNet.Tests.XUnit; using BenchmarkDotNet.Toolchains; using BenchmarkDotNet.Toolchains.Framework; using BenchmarkDotNet.Toolchains.InProcess.Emit; @@ -32,6 +31,12 @@ public static IEnumerable GetAllJits() if (RuntimeInformation.IsFullFramework) { + if (RuntimeInformation.GetCurrentPlatform() is Platform.Arm64) + { + // RyuJit for desktop .NET arm64. Supported only on net481, but we have to match the tfm in our test project. + yield return [Jit.RyuJit, Platform.Arm64, CsProjFrameworkToolchain.Net472]; + } + // Framework on arm emulates x86, so these platform targets should work on both. yield return [Jit.LegacyJit, Platform.X86, CsProjFrameworkToolchain.Net472]; // 32bit LegacyJit for desktop .NET yield return [Jit.LegacyJit, Platform.X64, CsProjFrameworkToolchain.Net472]; // 64bit LegacyJit for desktop .NET yield return [Jit.RyuJit, Platform.X64, CsProjFrameworkToolchain.Net472]; // RyuJit for desktop .NET @@ -87,7 +92,7 @@ public void Recursive() [MethodImpl(MethodImplOptions.NoInlining)] public void Benchmark(bool justAnOverload) { } // we need to test overloads (#562) } - [TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)] + [Theory] [MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)] [Trait(Constants.Category, Constants.BackwardCompatibilityCategory)] public void CanDisassembleAllMethodCalls(Jit jit, Platform platform, IToolchain toolchain) @@ -107,7 +112,7 @@ public void CanDisassembleAllMethodCalls(Jit jit, Platform platform, IToolchain AssertDisassemblyResult(result, $"{nameof(WithCalls.Recursive)}()"); } - [TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)] + [Theory] [MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)] [Trait(Constants.Category, Constants.BackwardCompatibilityCategory)] public void CanDisassembleAllMethodCallsUsingFilters(Jit jit, Platform platform, IToolchain toolchain) @@ -133,7 +138,7 @@ public void CanDisassembleAllMethodCallsUsingFilters(Jit jit, Platform platform, public T Create() => new T(); } - [TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)] + [Theory] [MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)] [Trait(Constants.Category, Constants.BackwardCompatibilityCategory)] public void CanDisassembleGenericTypes(Jit jit, Platform platform, IToolchain toolchain) @@ -154,7 +159,7 @@ public class WithInlineable [Benchmark] public void JustReturn() { } } - [TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)] + [Theory] [MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)] [Trait(Constants.Category, Constants.BackwardCompatibilityCategory)] public void CanDisassembleInlinableBenchmarks(Jit jit, Platform platform, IToolchain toolchain) @@ -188,7 +193,7 @@ public class WithGeneratedMemberNames [MethodImpl(MethodImplOptions.NoInlining)] public virtual void ForDisassemblyDiagnoser() { } } - [TheoryEnvSpecific("Not supported on Windows+Arm", EnvRequirement.NonWindowsArm)] + [Theory] [MemberData(nameof(GetAllJits), DisableDiscoveryEnumeration = true)] [Trait(Constants.Category, Constants.BackwardCompatibilityCategory)] public void CanDisassembleWhenBenchmarkDeclaresGeneratedMemberNames(Jit jit, Platform platform, IToolchain toolchain) diff --git a/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs b/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs index ce47c2d223..62738eea37 100644 --- a/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs +++ b/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs @@ -1,7 +1,6 @@ using AwesomeAssertions; using BenchmarkDotNet.Disassemblers; using BenchmarkDotNet.Serialization; -using BenchmarkDotNet.Tests.XUnit; using Gee.External.Capstone; using Gee.External.Capstone.Arm64; using Iced.Intel; @@ -105,7 +104,7 @@ public void IntelAsmSerializationTest() Assert.Equivalent(model, result, strict: true); } - [FactEnvSpecific("ARM64 disassembler is not supported on .NET Framework or Windows+Arm environment", EnvRequirement.NonFullFramework, EnvRequirement.NonWindowsArm)] + [Fact] public void Arm64AsmSerializationTest() { // Arrange @@ -113,7 +112,7 @@ public void Arm64AsmSerializationTest() var disassembleSyntax = DisassembleSyntax.Intel; // Create instruction instance by using disassembler. - using var disassembler = CapstoneDisassembler.CreateArm64Disassembler(Arm64DisassembleMode.Arm); + using var disassembler = CapstoneFactory.CreateArm64Disassembler(Arm64DisassembleMode.Arm); disassembler.EnableInstructionDetails = true; disassembler.DisassembleSyntax = disassembleSyntax;