diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs b/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs index d8c1486793..3293fd2a78 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs @@ -2,143 +2,13 @@ using Gee.External.Capstone; using Gee.External.Capstone.Arm64; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace BenchmarkDotNet.Disassemblers { - internal struct RegisterValueAccumulator - { - private enum State - { - LookingForPattern, - ExpectingMovk, - ExpectingAdd, - LookingForPossibleLdr - } - - private State _state; - private long _value; - private int _expectedMovkShift; - private Arm64RegisterId _registerId; - private ClrRuntime _runtime; - - public void Init(ClrRuntime runtime) - { - _state = State.LookingForPattern; - _expectedMovkShift = 0; - _value = 0; - _registerId = Arm64RegisterId.Invalid; - _runtime = runtime; - } - - public void Feed(Arm64Instruction instruction) - { - Arm64InstructionDetail details = instruction.Details; - - switch (_state) - { - case State.LookingForPattern: - if (instruction.Id == Arm64InstructionId.ARM64_INS_MOVZ) - { - _registerId = details.Operands[0].Register.Id; - _value = details.Operands[1].Immediate; - _state = State.ExpectingMovk; - _expectedMovkShift = 16; - } - else if (instruction.Id == Arm64InstructionId.ARM64_INS_ADRP) - { - _registerId = details.Operands[0].Register.Id; - _value = details.Operands[1].Immediate; - _state = State.ExpectingAdd; - } - break; - case State.ExpectingMovk: - if (instruction.Id == Arm64InstructionId.ARM64_INS_MOVK && - details.Operands[0].Register.Id == _registerId && - details.Operands[1].ShiftOperation == Arm64ShiftOperation.ARM64_SFT_LSL && - details.Operands[1].ShiftValue == _expectedMovkShift) - { - _value = _value | (instruction.Details.Operands[1].Immediate << details.Operands[1].ShiftValue); - _expectedMovkShift += 16; - break; - } - _state = State.LookingForPossibleLdr; - goto case State.LookingForPossibleLdr; - case State.ExpectingAdd: - if (instruction.Id == Arm64InstructionId.ARM64_INS_ADD && - details.Operands[0].Register.Id == _registerId && - details.Operands[1].Register.Id == _registerId && - details.Operands[2].Type == Arm64OperandType.Immediate) - { - _value = _value | instruction.Details.Operands[2].Immediate; - _state = State.LookingForPossibleLdr; - } - break; - case State.LookingForPossibleLdr: - if (instruction.Id == Arm64InstructionId.ARM64_INS_LDR && - details.Operands[1].Type == Arm64OperandType.Memory && - details.Operands[1].Memory.Base.Id == _registerId && // The source address is in the register we are tracking - details.Operands[1].Memory.Displacement == 0 && // There is no displacement - details.Operands[1].Memory.Index == null) // And there is no extra index register - { - // Simulate the LDR instruction. - long newValue = (long)_runtime.DataTarget.DataReader.ReadPointer((ulong)_value); - _value = newValue; - if (_value == 0) - { - _state = State.LookingForPattern; - } - else - { - // The LDR might have loaded the result in another register - _registerId = details.Operands[0].Register.Id; - } - } - else if (instruction.Id == Arm64InstructionId.ARM64_INS_CBZ || - instruction.Id == Arm64InstructionId.ARM64_INS_CBNZ || - instruction.Id == Arm64InstructionId.ARM64_INS_B && details.ConditionCode != Arm64ConditionCode.Invalid) - { - // ignore conditional branches - } - else if (details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE) || - details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_CALL) || - details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP)) - { - // We've encountered an unconditional jump or call, the accumulated registers value is not valid anymore - _state = State.LookingForPattern; - } - else if (instruction.Id == Arm64InstructionId.ARM64_INS_MOVZ) - { - // Another constant loading is starting - _state = State.LookingForPattern; - goto case State.LookingForPattern; - } - else - { - // Finally check if the current instruction modified the register that was accumulating the constant - // and reset the state machine in case it did. - foreach (Arm64Register reg in details.AllWrittenRegisters) - { - // Some unexpected instruction overwriting the accumulated register - if (reg.Id == _registerId) - { - _state = State.LookingForPattern; - } - } - } - break; - } - } - - public bool HasValue => _state == State.ExpectingMovk || _state == State.LookingForPossibleLdr; - - public long Value { get { return _value; } } - - public Arm64RegisterId RegisterId { get { return _registerId; } } - } - internal class Arm64Disassembler : ClrMdDisassembler { - protected override IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax) + protected override IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, IClrMethod currentMethod, DisassemblySyntax syntax) { const Arm64DisassembleMode disassembleMode = Arm64DisassembleMode.Arm; using (CapstoneArm64Disassembler disassembler = CapstoneDisassembler.CreateArm64Disassembler(disassembleMode)) @@ -147,7 +17,8 @@ protected override IEnumerable Decode(byte[] code, ulong startAddress, Stat // disassembled binary code. disassembler.EnableInstructionDetails = true; disassembler.DisassembleSyntax = Map(syntax); - RegisterValueAccumulator accumulator = new RegisterValueAccumulator(); + + Arm64RegisterValueAccumulator accumulator = new(); accumulator.Init(state.Runtime); Arm64Instruction[] instructions = disassembler.Disassemble(code, (long)startAddress); @@ -324,7 +195,7 @@ private static bool IsLdrLiteral64(uint instr, out int rt, out int offsetBytes) return true; } - private static bool TryGetReferencedAddress(Arm64Instruction instruction, RegisterValueAccumulator accumulator, uint pointerSize, out ulong referencedAddress, out bool isReferencedAddressIndirect) + private static bool TryGetReferencedAddress(Arm64Instruction instruction, Arm64RegisterValueAccumulator accumulator, uint pointerSize, out ulong referencedAddress, out bool isReferencedAddressIndirect) { if ((instruction.Id == Arm64InstructionId.ARM64_INS_BR || instruction.Id == Arm64InstructionId.ARM64_INS_BLR) && instruction.Details.Operands[0].Register.Id == accumulator.RegisterId && accumulator.HasValue) { diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs index bc8203f023..166abf7b37 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs @@ -22,7 +22,8 @@ internal static string Format(Arm64Asm asm, FormatterOptions formatterOptions, FormatInstructionPointer(instruction, formatterOptions, pointerSize, output); } - output.Append(instruction.Mnemonic.ToString().PadRight(formatterOptions.FirstOperandCharIndex)); + var padRight = Math.Max(formatterOptions.FirstOperandCharIndex, instruction.Mnemonic.Length + 1); + output.Append(instruction.Mnemonic.PadRight(padRight)); if (asm.ReferencedAddress.HasValue && !asm.IsReferencedAddressIndirect && symbols.TryGetValue(asm.ReferencedAddress.Value, out var name)) { diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs new file mode 100644 index 0000000000..b3c601e72e --- /dev/null +++ b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs @@ -0,0 +1,135 @@ +using Gee.External.Capstone.Arm64; +using Microsoft.Diagnostics.Runtime.Interfaces; + +namespace BenchmarkDotNet.Disassemblers; + +internal struct Arm64RegisterValueAccumulator +{ + private enum State + { + LookingForPattern, + ExpectingMovk, + ExpectingAdd, + LookingForPossibleLdr + } + + private State _state; + private long _value; + private int _expectedMovkShift; + private Arm64RegisterId _registerId; + private IClrRuntime _runtime; + + public void Init(IClrRuntime runtime) + { + _state = State.LookingForPattern; + _expectedMovkShift = 0; + _value = 0; + _registerId = Arm64RegisterId.Invalid; + _runtime = runtime; + } + + public void Feed(Arm64Instruction instruction) + { + Arm64InstructionDetail details = instruction.Details; + + switch (_state) + { + case State.LookingForPattern: + if (instruction.Id == Arm64InstructionId.ARM64_INS_MOVZ) + { + _registerId = details.Operands[0].Register.Id; + _value = details.Operands[1].Immediate; + _state = State.ExpectingMovk; + _expectedMovkShift = 16; + } + else if (instruction.Id == Arm64InstructionId.ARM64_INS_ADRP) + { + _registerId = details.Operands[0].Register.Id; + _value = details.Operands[1].Immediate; + _state = State.ExpectingAdd; + } + break; + case State.ExpectingMovk: + if (instruction.Id == Arm64InstructionId.ARM64_INS_MOVK && + details.Operands[0].Register.Id == _registerId && + details.Operands[1].ShiftOperation == Arm64ShiftOperation.ARM64_SFT_LSL && + details.Operands[1].ShiftValue == _expectedMovkShift) + { + _value = _value | (instruction.Details.Operands[1].Immediate << details.Operands[1].ShiftValue); + _expectedMovkShift += 16; + break; + } + _state = State.LookingForPossibleLdr; + goto case State.LookingForPossibleLdr; + case State.ExpectingAdd: + if (instruction.Id == Arm64InstructionId.ARM64_INS_ADD && + details.Operands[0].Register.Id == _registerId && + details.Operands[1].Register.Id == _registerId && + details.Operands[2].Type == Arm64OperandType.Immediate) + { + _value = _value | instruction.Details.Operands[2].Immediate; + _state = State.LookingForPossibleLdr; + } + break; + case State.LookingForPossibleLdr: + if (instruction.Id == Arm64InstructionId.ARM64_INS_LDR && + details.Operands[1].Type == Arm64OperandType.Memory && + details.Operands[1].Memory.Base.Id == _registerId && // The source address is in the register we are tracking + details.Operands[1].Memory.Displacement == 0 && // There is no displacement + details.Operands[1].Memory.Index == null) // And there is no extra index register + { + // Simulate the LDR instruction. + long newValue = (long)_runtime.DataTarget.DataReader.ReadPointer((ulong)_value); + _value = newValue; + if (_value == 0) + { + _state = State.LookingForPattern; + } + else + { + // The LDR might have loaded the result in another register + _registerId = details.Operands[0].Register.Id; + } + } + else if (instruction.Id == Arm64InstructionId.ARM64_INS_CBZ || + instruction.Id == Arm64InstructionId.ARM64_INS_CBNZ || + instruction.Id == Arm64InstructionId.ARM64_INS_B && details.ConditionCode != Arm64ConditionCode.Invalid) + { + // ignore conditional branches + } + else if (details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE) || + details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_CALL) || + details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP)) + { + // We've encountered an unconditional jump or call, the accumulated registers value is not valid anymore + _state = State.LookingForPattern; + } + else if (instruction.Id == Arm64InstructionId.ARM64_INS_MOVZ) + { + // Another constant loading is starting + _state = State.LookingForPattern; + goto case State.LookingForPattern; + } + else + { + // Finally check if the current instruction modified the register that was accumulating the constant + // and reset the state machine in case it did. + foreach (Arm64Register reg in details.AllWrittenRegisters) + { + // Some unexpected instruction overwriting the accumulated register + if (reg.Id == _registerId) + { + _state = State.LookingForPattern; + } + } + } + break; + } + } + + public bool HasValue => _state == State.ExpectingMovk || _state == State.LookingForPossibleLdr; + + public long Value { get { return _value; } } + + public Arm64RegisterId RegisterId { get { return _registerId; } } +} diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs index cc54ddea0c..f6e06d7b00 100644 --- a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs @@ -4,6 +4,7 @@ using BenchmarkDotNet.Filters; using BenchmarkDotNet.Portability; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using System.Text.RegularExpressions; namespace BenchmarkDotNet.Disassemblers @@ -157,7 +158,7 @@ private DisassembledMethod[] Disassemble(ClrMdArgs args, State state) return result.ToArray(); } - private static bool CanBeDisassembled(ClrMethod method) => method.ILOffsetMap.Length > 0 && method.NativeCode > 0; + private static bool CanBeDisassembled(IClrMethod method) => method.ILOffsetMap.Length > 0 && method.NativeCode > 0; private DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, ClrMdArgs args, DisassemblySyntax syntax, SourceCodeProvider sourceCodeProvider) { @@ -206,7 +207,7 @@ private DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, }; } - private IEnumerable Decode(ILToNativeMap map, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax) + private IEnumerable Decode(ILToNativeMap map, State state, int depth, IClrMethod currentMethod, DisassemblySyntax syntax) { ulong startAddress = map.StartAddress; uint size = (uint)(map.EndAddress - map.StartAddress); @@ -227,9 +228,9 @@ private IEnumerable Decode(ILToNativeMap map, State state, int depth, ClrMe return Decode(code, startAddress, state, depth, currentMethod, syntax); } - protected abstract IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax); + protected abstract IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, IClrMethod currentMethod, DisassemblySyntax syntax); - private static ILToNativeMap[] GetCompleteNativeMap(ClrMethod method, ClrRuntime runtime) + private static ILToNativeMap[] GetCompleteNativeMap(IClrMethod method, IClrRuntime runtime) { // it's better to use one single map rather than few small ones // it's simply easier to get next instruction when decoding ;) @@ -251,10 +252,10 @@ private static ILToNativeMap[] GetCompleteNativeMap(ClrMethod method, ClrRuntime .ToArray(); } - private static DisassembledMethod CreateEmpty(ClrMethod method, string reason) + private static DisassembledMethod CreateEmpty(IClrMethod method, string reason) => DisassembledMethod.Empty(method.Signature ?? "", method.NativeCode, reason); - protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD, State state, int depth, ClrMethod currentMethod) + protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD, State state, int depth, IClrMethod currentMethod) { if (!IsValidAddress(address) || state.AddressToNameMapping.ContainsKey(address)) return; diff --git a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs index c7b5ddf428..2c83b46bf2 100644 --- a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs +++ b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs @@ -1,7 +1,7 @@ using Gee.External.Capstone; using Gee.External.Capstone.Arm64; using Iced.Intel; -using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using System.ComponentModel; using System.Text.Json; using System.Text.Json.Serialization; @@ -218,18 +218,18 @@ public static class DisassemblerConstants internal sealed class State { - internal State(ClrRuntime runtime, string targetFrameworkMoniker) + internal State(IClrRuntime runtime, string targetFrameworkMoniker) { Runtime = runtime; Todo = new Queue(); - HandledMethods = new HashSet(new ClrMethodComparer()); + HandledMethods = new HashSet(new IClrMethodComparer()); AddressToNameMapping = []; RuntimeVersion = ParseVersion(targetFrameworkMoniker); } - internal ClrRuntime Runtime { get; } + internal IClrRuntime Runtime { get; } internal Queue Todo { get; } - internal HashSet HandledMethods { get; } + internal HashSet HandledMethods { get; } internal Dictionary AddressToNameMapping { get; } internal Version RuntimeVersion { get; } @@ -258,9 +258,9 @@ internal static Version ParseVersion(string targetFrameworkMoniker) return Version.Parse(versionToParse); } - private sealed class ClrMethodComparer : IEqualityComparer + private sealed class IClrMethodComparer : IEqualityComparer { - public bool Equals(ClrMethod? x, ClrMethod? y) + public bool Equals(IClrMethod? x, IClrMethod? y) { if (ReferenceEquals(x, y)) return true; @@ -271,16 +271,16 @@ public bool Equals(ClrMethod? x, ClrMethod? y) return x.NativeCode == y.NativeCode; } - public int GetHashCode(ClrMethod obj) => (int)obj.NativeCode; + public int GetHashCode(IClrMethod obj) => (int)obj.NativeCode; } } internal readonly struct MethodInfo // I am not using ValueTuple here (would be perfect) to keep the number of dependencies as low as possible { - internal ClrMethod Method { get; } + internal IClrMethod Method { get; } internal int Depth { get; } - internal MethodInfo(ClrMethod method, int depth) + internal MethodInfo(IClrMethod method, int depth) { Method = method; Depth = depth; diff --git a/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs index 94c874e57e..ecf513b9cf 100644 --- a/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs @@ -1,12 +1,13 @@ using BenchmarkDotNet.Diagnosers; using Iced.Intel; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; namespace BenchmarkDotNet.Disassemblers { internal class IntelDisassembler : ClrMdDisassembler { - protected override IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax) + protected override IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, IClrMethod currentMethod, DisassemblySyntax syntax) { var reader = new ByteArrayCodeReader(code); var decoder = Decoder.Create(state.Runtime.DataTarget.DataReader.PointerSize * 8, reader); diff --git a/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs b/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs index 18b9d17111..a2e568840a 100644 --- a/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs +++ b/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Extensions; using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; using Microsoft.Diagnostics.Symbols; using System.Diagnostics; @@ -17,7 +18,7 @@ public void Dispose() symbolReader.Dispose(); } - internal IEnumerable GetSource(ClrMethod method, ILToNativeMap map) + internal IEnumerable GetSource(IClrMethod method, ILToNativeMap map) { var sourceLocation = GetSourceLocation(method, map.ILOffset); if (sourceLocation is not { LineNumber: > 0 }) @@ -105,7 +106,7 @@ private static string GetSmartPointer(string sourceLine, int? start, int? end) return new string(prefix); } - internal SourceLocation? GetSourceLocation(ClrMethod method, int ilOffset) + internal SourceLocation? GetSourceLocation(IClrMethod method, int ilOffset) { var reader = GetReaderForMethod(method); if (reader == null) @@ -114,9 +115,9 @@ private static string GetSmartPointer(string sourceLine, int? start, int? end) return reader.SourceLocationForManagedCode((uint)method.MetadataToken, ilOffset); } - private ManagedSymbolModule? GetReaderForMethod(ClrMethod? method) + private ManagedSymbolModule? GetReaderForMethod(IClrMethod? method) { - ClrModule? module = method?.Type?.Module; + IClrModule? module = method?.Type?.Module; PdbInfo? info = module?.Pdb; ManagedSymbolModule? reader = null; diff --git a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj index 0a9cb76c9b..c648b64248 100755 --- a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj +++ b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.Decode.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.Decode.cs new file mode 100644 index 0000000000..60474e7e80 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.Decode.cs @@ -0,0 +1,104 @@ +// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. +#if NET +using AsmArm64; +using AwesomeAssertions; +using AwesomeAssertions.Equivalency; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Disassemblers; +using Gee.External.Capstone; +using static AsmArm64.Arm64RegisterX; +using Arm64Instruction = Gee.External.Capstone.Arm64.Arm64Instruction; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64DisassemblerTests +{ + // Check Arm64Instruction equivalency by ToString output. + private static EquivalencyOptions ConfigureCustomEquivalency(EquivalencyOptions options) + => options + .Using(ctx => ctx.Subject.ToString().Should().Be(ctx.Expectation.ToString())) + .When(info => info.Path == "Instruction"); + + [Fact] + public void Decode() + { + // Arrange + var rawInstructions = new[] + { + Arm64InstructionFactory.MOVZ(X1, 0x1234), // movz x0, #0x1234 + Arm64InstructionFactory.MOVK(X1, 0x5678, amount: 16), // movk x0, #0x5678, lsl #16 + Arm64InstructionFactory.BR(X1), // br x1 + }; + PrintInstructions(rawInstructions); + + byte[] bytes = rawInstructions.ToLittleEndianBytes(); + + var clrRuntime = CreateMockClrRuntime( + [ + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(X10, 0x10000), // ldr x11, #0x10000 + Arm64InstructionFactory.LDR(X12, 0x20000), // ldr x12, #0x20000 + Arm64InstructionFactory.BR(X10), // br x11 + ], + address => + { + return Address2; // Called by TryResolvePrecode + } + ); + clrRuntime.GetJitHelperFunctionNameFunc = _ => null; + clrRuntime.GetMethodByInstructionPointerFunc = _ => DummyTargetMethod; + + var state = new State(clrRuntime, DummyTargetFramework); + + ulong baseAddress = DummyBaseAddress; + DisassemblySyntax syntax = DisassemblySyntax.Masm; + + // Act + var helper = new Arm64DisassemblerHelper(); + var results = helper.Decode(bytes, baseAddress, state, depth: 0, DummyCurrentMethod, syntax); + + // Assert + results.Length.Should().Be(rawInstructions.Length); + + // movz x1, #0x1000 + results[0].Should().BeEquivalentTo(new Arm64Asm + { + InstructionPointer = baseAddress, + Instruction = rawInstructions[0].ToCapstoneArm64Instruction(baseAddress, 0), + InstructionLength = 4, + DisassembleSyntax = DisassembleSyntax.Masm, + ReferencedAddress = null, + IsReferencedAddressIndirect = false, + }, ConfigureCustomEquivalency); + + // movk x1, #0x1 + results[1].Should().BeEquivalentTo(new Arm64Asm + { + InstructionPointer = baseAddress + 4, + Instruction = rawInstructions[1].ToCapstoneArm64Instruction(baseAddress, 1), + InstructionLength = 4, + DisassembleSyntax = DisassembleSyntax.Masm, + ReferencedAddress = null, + IsReferencedAddressIndirect = false, + }, ConfigureCustomEquivalency); + + // br x1 + results[2].Should().BeEquivalentTo(new Arm64Asm + { + InstructionPointer = baseAddress + 8, + Instruction = rawInstructions[2].ToCapstoneArm64Instruction(baseAddress, 2), + InstructionLength = 4, + DisassembleSyntax = DisassembleSyntax.Masm, + ReferencedAddress = Address2, + IsReferencedAddressIndirect = true, + }, ConfigureCustomEquivalency); + + state.HandledMethods.Should().BeEmpty(); // HandledMethods is added when disassembled. It's not set on decode timing. + + state.AddressToNameMapping.Should().BeEquivalentTo(new Dictionary() + { + [Address2] = DummyTargetMethod.MethodName, + }); + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.IsLdrLiteral64.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.IsLdrLiteral64.cs new file mode 100644 index 0000000000..0b5fbc97f9 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.IsLdrLiteral64.cs @@ -0,0 +1,111 @@ +#if NET8_0_OR_GREATER +using AsmArm64; +using AwesomeAssertions; +using static AsmArm64.Arm64RegisterX; +using static AsmArm64.Arm64RegisterW; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64DisassemblerTests +{ + [Fact] + public void IsLdrLiteral64() + { + // Arrange + const int expectedOffsetBytes = 0x100; + var rawInstruction = Arm64InstructionFactory.LDR(X5, label: expectedOffsetBytes); + + // Act + var result = Arm64DisassemblerHelper.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); + + // Assert + result.Should().BeTrue(); + + rt.Should().Be(5); + offsetBytes.Should().Be(expectedOffsetBytes); + + // Additional assertions + var instruction = Arm64Instruction.Decode(rawInstruction); + instruction.Id.Should().Be(Arm64InstructionId.LDR_64_loadlit); + + var registerOperand = (Arm64RegisterOperand)instruction.GetOperand(0); + registerOperand.Value.Should().Be((Arm64RegisterAny)X5); + + var labelOperand = (Arm64LabelOperand)instruction.GetOperand(1); + labelOperand.Offset.Should().Be(expectedOffsetBytes); + } + + [Fact] + public void IsLdrLiteral64_WithMinValue() + { + // Arrange + const int expectedOffsetBytes = -1_048_576; + var rawInstruction = Arm64InstructionFactory.LDR(X0, label: new Arm64LabelOffset(expectedOffsetBytes)); + + // Act + var result = Arm64DisassemblerHelper.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); + + // Assert + result.Should().BeTrue(); + rt.Should().Be(0); + offsetBytes.Should().Be(expectedOffsetBytes); + + // Additional assertions + var instruction = Arm64Instruction.Decode(rawInstruction); + instruction.Id.Should().Be(Arm64InstructionId.LDR_64_loadlit); + + var registerOperand = (Arm64RegisterOperand)instruction.GetOperand(0); + registerOperand.Value.Should().Be((Arm64RegisterAny)X0); + + + var labelOperand = (Arm64LabelOperand)instruction.GetOperand(1); + labelOperand.Offset.Should().Be(expectedOffsetBytes); + } + + [Fact] + public void IsLdrLiteral64_WithMaxValue() + { + // Arrange + const int expectedOffsetBytes = 1_048_572; + var rawInstruction = Arm64InstructionFactory.LDR(X0, label: new Arm64LabelOffset(expectedOffsetBytes)); + + // Act + var result = Arm64DisassemblerHelper.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); + + // Assert + result.Should().BeTrue(); + rt.Should().Be(0); + offsetBytes.Should().Be(expectedOffsetBytes); + + // Additional assertions + var instruction = Arm64Instruction.Decode(rawInstruction); + instruction.Id.Should().Be(Arm64InstructionId.LDR_64_loadlit); + + var registerOperand = (Arm64RegisterOperand)instruction.GetOperand(0); + registerOperand.Value.Should().Be((Arm64RegisterAny)X0); + + var labelOperand = (Arm64LabelOperand)instruction.GetOperand(1); + labelOperand.Offset.Should().Be(expectedOffsetBytes); + } + + [Fact] + public void IsLdrLiteral64_With32bitRegister_ShouldBeFalse() + { + // Arrange + var rawInstruction = Arm64InstructionFactory.LDR(W5, label: 0x100); + + // Act + var result = Arm64DisassemblerHelper.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); + + // Assert + result.Should().BeFalse(); + + // Additional assertions + var instruction = Arm64Instruction.Decode(rawInstruction); + instruction.Id.Should().Be(Arm64InstructionId.LDR_32_loadlit); + + var registerOperand = (Arm64RegisterOperand)instruction.GetOperand(0); + registerOperand.Value.Should().Be((Arm64RegisterAny)W5); + } +} +#endif \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryFollowJumpTrampoline.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryFollowJumpTrampoline.cs new file mode 100644 index 0000000000..ac1ebe065a --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryFollowJumpTrampoline.cs @@ -0,0 +1,203 @@ +// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. +#if NET +using AsmArm64; +using AwesomeAssertions; +using BenchmarkDotNet.Disassemblers; +using static AsmArm64.Arm64RegisterX; +using static AsmArm64.Arm64RegisterW; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64DisassemblerTests +{ + [Fact] + public void TryFollowJumpTrampoline_B() + { + // Arrange + var rawInstructions = new[] + { + Arm64InstructionFactory.B(0x10000), // b 0x10000 + }; + PrintInstructions(rawInstructions); + + var clrRuntime = CreateMockClrRuntime(rawInstructions); + var state = new State(clrRuntime, DummyTargetFramework); + ulong baseAddress = DummyBaseAddress; + + // Act + var helper = new Arm64DisassemblerHelper(); + var result = helper.TryFollowJumpTrampoline(state, baseAddress, out var target); + + // Assert + result.Should().BeTrue(); + target.Should().Be(baseAddress + 0x10000); + } + + [Fact] + public void TryFollowJumpTrampoline_B_MinusOffset() + { + // Arrange + var rawInstructions = new[] + { + Arm64InstructionFactory.B(-0x10000), // b 0x10000 + }; + + PrintInstructions(rawInstructions); + + var clrRuntime = CreateMockClrRuntime(rawInstructions); + var state = new State(clrRuntime, DummyTargetFramework); + ulong baseAddress = DummyBaseAddress; + + // Act + var helper = new Arm64DisassemblerHelper(); + var result = helper.TryFollowJumpTrampoline(state, baseAddress, out var target); + + // Assert + result.Should().BeTrue(); + target.Should().Be(baseAddress - 0x10000); + } + + [Fact] + public void TryFollowJumpTrampoline_StubPrecode() + { + // Arrange + var rawInstructions = new[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(X10, 0x10000), // ldr x11, #0x10000 + Arm64InstructionFactory.LDR(X12, 0x20000), // ldr x12, #0x20000 + Arm64InstructionFactory.BR(X10), // br x11 + }; + PrintInstructions(rawInstructions); + + var clrRuntime = CreateMockClrRuntime(rawInstructions, address => + { + return ExpectedResultAddress; + }); + var state = new State(clrRuntime, DummyTargetFramework); + ulong baseAddress = DummyBaseAddress; + + // Act + var helper = new Arm64DisassemblerHelper(); + var result = helper.TryFollowJumpTrampoline(state, baseAddress, out var target); + + // Assert + result.Should().BeTrue(); + target.Should().Be(ExpectedResultAddress); + } + + [Fact] + public void TryFollowJumpTrampoline_FixupPrecode() + { + // Arrange + var rawInstructions = new uint[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(X11, 0x10000), // ldr x11, #0x10000 + Arm64InstructionFactory.BR(X11), // br x11 + Arm64InstructionFactory.LDR(X12, 0x20000), // ldr x12, #0x20000 + }; + PrintInstructions(rawInstructions); + + var clrRuntime = CreateMockClrRuntime(rawInstructions, address => + { + return ExpectedResultAddress; + }); + var state = new State(clrRuntime, DummyTargetFramework); + ulong baseAddress = DummyBaseAddress; + + // Act + var helper = new Arm64DisassemblerHelper(); + var result = helper.TryFollowJumpTrampoline(state, baseAddress, out var target); + + // Assert + result.Should().BeTrue(); + target.Should().Be(ExpectedResultAddress); + } + + [Fact] + public void TryFollowJumpTrampoline_CallCountingStub() + { + // Arrange + var rawInstructions = new uint[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(X9, 0x10000), // ldr x11, #0x10000 + Arm64InstructionFactory.LDRH(W10, new Arm64ImmediateMemoryAccessor(X9, 0)), // ldrh w10, [x9] + Arm64InstructionFactory.SUBS(W10, W10, 1), // subs w10, w10, #1 + }; + PrintInstructions(rawInstructions); + + var clrRuntime = CreateMockClrRuntime(rawInstructions, address => + { + return ExpectedResultAddress; + }); + var state = new State(clrRuntime, DummyTargetFramework); + ulong baseAddress = DummyBaseAddress; + + // Act + var helper = new Arm64DisassemblerHelper(); + var result = helper.TryFollowJumpTrampoline(state, baseAddress, out var target); + + // Assert + result.Should().BeTrue(); + target.Should().Be(ExpectedResultAddress); + } + + // TryFollowJumTrampoline seems not support FixupPrecode with pre-backpatch form. + [Fact] + public void TryFollowJumpTrampoline_FixupPrecodeCode_Fixup_ShouldReturnFalse() + { + // Arrange + var rawInstructions = new uint[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(X12, 0x10000), // ldr x12, #0x10000 + Arm64InstructionFactory.LDR(X11, 0x20000), // ldr x11, #0x20000 + Arm64InstructionFactory.BR(X11), // br x11 + }; + PrintInstructions(rawInstructions); + + var clrRuntime = CreateMockClrRuntime(rawInstructions, address => + { + return ExpectedResultAddress; + }); + var state = new State(clrRuntime, DummyTargetFramework); + ulong baseAddress = DummyBaseAddress; + + // Act + var helper = new Arm64DisassemblerHelper(); + var result = helper.TryFollowJumpTrampoline(state, baseAddress, out var target); + + // Assert + result.Should().BeFalse(); + } + + + [Fact] + public void TryFollowJumpTrampoline_NonStubHead_ShouldReturnFalse() + { + // Arrange + var rawInstructions = new[] + { + // Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.BL(0x10000), // bl 0x10000 + }; + PrintInstructions(rawInstructions); + + var clrRuntime = CreateMockClrRuntime(rawInstructions, address => + { + return ExpectedResultAddress; + }); + var state = new State(clrRuntime, DummyTargetFramework); + ulong baseAddress = DummyBaseAddress; + + // Act + var helper = new Arm64DisassemblerHelper(); + var result = helper.TryFollowJumpTrampoline(state, baseAddress, out var target); + + // Assert + result.Should().BeFalse(); + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs new file mode 100644 index 0000000000..56f7a6be19 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs @@ -0,0 +1,104 @@ +#if NET8_0_OR_GREATER +using AsmArm64; +using AwesomeAssertions; +using BenchmarkDotNet.Disassemblers; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64DisassemblerTests +{ + private Arm64RegisterValueAccumulator CreateValueAccumulator(ushort initialValue) + { + var valueAccumulator = new Arm64RegisterValueAccumulator(); + valueAccumulator.Feed(Arm64TestInstructions.Movz(X0, initialValue)); + + valueAccumulator.HasValue.Should().BeTrue(); + valueAccumulator.Value.Should().Be(initialValue); + + return valueAccumulator; + } + + [Fact] + public void TryGetReferencedAddress_With_BL() + { + // Arrange + var rawInstruction = Arm64InstructionFactory.BR(X0); + var valueAccumulator = CreateValueAccumulator(0x100); + + // Act + var result = Arm64DisassemblerHelper.TryGetReferencedAddress( + rawInstruction.ToCapstoneArm64Instruction(), + valueAccumulator, + pointerSize: 0, // Thiss parameter is not used. + out ulong referencedAddress, + out bool isReferencedAddressIndirect); + + // Assert + result.Should().BeTrue(); + referencedAddress.Should().Be(0x100); + isReferencedAddressIndirect.Should().BeTrue(); + } + + [Fact] + public void TryGetReferencedAddress_With_BLR() + { + // Arrange + var rawInstruction = Arm64InstructionFactory.BLR(X0); + var valueAccumulator = CreateValueAccumulator(0x100); + + // Act + var result = Arm64DisassemblerHelper.TryGetReferencedAddress( + rawInstruction.ToCapstoneArm64Instruction(), + valueAccumulator, + pointerSize: 0, // Thiss parameter is not used. + out ulong referencedAddress, + out bool isReferencedAddressIndirect); + + // Assert + result.Should().BeTrue(); + referencedAddress.Should().Be(0x100); + isReferencedAddressIndirect.Should().BeTrue(); + } + + [Fact] + public void TryGetReferencedAddress_With_BranchRelative() + { + // Arrange + var rawInstruction = Arm64InstructionFactory.B(0x100); + var valueAccumulator = CreateValueAccumulator(0x100); + + // Act + var result = Arm64DisassemblerHelper.TryGetReferencedAddress( + rawInstruction.ToCapstoneArm64Instruction(), + valueAccumulator, + pointerSize: 0, // Thiss parameter is not used. + out ulong referencedAddress, + out bool isReferencedAddressIndirect); + + // Assert + result.Should().BeTrue(); + referencedAddress.Should().Be(0x100); + isReferencedAddressIndirect.Should().BeFalse(); + } + + [Fact] + public void TryGetReferencedAddress_DontMatchCondition_ShouldReturnFalse() + { + // Arrange + var rawInstruction = Arm64InstructionFactory.RET(); // `ret` instruction is not BranchRelative group + var valueAccumulator = CreateValueAccumulator(0x100); + + // Act + var result = Arm64DisassemblerHelper.TryGetReferencedAddress( + rawInstruction.ToCapstoneArm64Instruction(), + valueAccumulator, + pointerSize: 0, // Thiss parameter is not used. + out ulong referencedAddress, + out bool isReferencedAddressIndirect); + + // Assert + result.Should().BeFalse(); + } +} +#endif \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryReadStubHead.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryReadStubHead.cs new file mode 100644 index 0000000000..72e2e9bd85 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryReadStubHead.cs @@ -0,0 +1,133 @@ +#if NET8_0_OR_GREATER +using AsmArm64; +using AwesomeAssertions; +using Arm64RegisterX = AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64DisassemblerTests +{ + [Fact] + public void TryReadStubHead() + { + // Arrange + var rawInstructions = new[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(Arm64RegisterX.X10, 0x100), // ldr x10 0x100 + Arm64InstructionFactory.LDR(Arm64RegisterX.X12, 0x200), // ldr x12 0x200 + Arm64InstructionFactory.BR(Arm64RegisterX.X10), // br x10 + }; + PrintInstructions(rawInstructions); + + var dataReader = CreateMockDataReader(rawInstructions); + + ulong address = 0x100000; + + // Act + var result = Arm64DisassemblerHelper.TryReadStubHead( + dataReader, + address, + out ulong parseBase, + out uint instr0, + out uint instr1, + out uint instr2); + + // Assert + result.Should().BeTrue(); + parseBase.Should().Be(address + 4); // Skip `dmb ishld` instruction + instr0.Should().Be(rawInstructions[1]); // ldr x10 0x100 + instr1.Should().Be(rawInstructions[2]); // ldr x12 0x200 + instr2.Should().Be(rawInstructions[3]); // br x10 + } + + [Fact] + public void TryReadStubHead_PreDotNet10() + { + // Arrange + var rawInstructions = new[] + { + Arm64InstructionFactory.LDR(Arm64RegisterX.X10, 0x100), // ldr x10 0x100 + Arm64InstructionFactory.LDR(Arm64RegisterX.X12, 0x200), // ldr x12 0x200 + Arm64InstructionFactory.BR(Arm64RegisterX.X10), // br x10 + }; + PrintInstructions(rawInstructions); + + var dataReader = CreateMockDataReader(rawInstructions); + ulong address = 0x10000; + + // Act + var result = Arm64DisassemblerHelper.TryReadStubHead( + dataReader, + address, + out ulong parseBase, + out uint instr0, + out uint instr1, + out uint instr2); + + // Assert + result.Should().BeTrue(); + parseBase.Should().Be(address); + instr0.Should().Be(rawInstructions[0]); // ldr x10 0x100 + instr1.Should().Be(rawInstructions[1]); // ldr x12 0x200 + instr2.Should().Be(rawInstructions[2]); // br x10 + } + + [Fact] + public void TryReadStubHead_InsufficientInstructions_ShouldReturnFalse() + { + // Arrange + var rawInstructions = new[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(Arm64RegisterX.X10, 0x100), // ldr x10 0x100 + Arm64InstructionFactory.LDR(Arm64RegisterX.X12, 0x200), // ldr x12 0x200 + // Arm64InstructionFactory.BR(Arm64RegisterX.X10), // br x10 + }; + PrintInstructions(rawInstructions); + + var dataReader = CreateMockDataReader(rawInstructions); + ulong address = 0; + + // Act + var result = Arm64DisassemblerHelper.TryReadStubHead( + dataReader, + address, + out ulong parseBase, + out uint instr0, + out uint instr1, + out uint instr2); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void TryReadStubHead_PreDotNet10_InsufficientInstructions_ShouldReturnFalse() + { + // Arrange + var rawInstructions = new[] + { + Arm64InstructionFactory.LDR(Arm64RegisterX.X10, 0x100), // ldr x10 0x100 + Arm64InstructionFactory.LDR(Arm64RegisterX.X12, 0x200), // ldr x12 0x200 + // Arm64InstructionFactory.BR(Arm64RegisterX.X10), // br x10 + }; + PrintInstructions(rawInstructions); + + var dataReader = CreateMockDataReader(rawInstructions); + ulong address = 0; + + // Act + var result = Arm64DisassemblerHelper.TryReadStubHead( + dataReader, + address, + out ulong parseBase, + out uint instr0, + out uint instr1, + out uint instr2); + + // Assert + result.Should().BeFalse(); + } +} +#endif \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryResolvePreCode.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryResolvePreCode.cs new file mode 100644 index 0000000000..3298a2f0f8 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryResolvePreCode.cs @@ -0,0 +1,151 @@ +#if NET8_0_OR_GREATER +using AsmArm64; +using AwesomeAssertions; +using static AsmArm64.Arm64RegisterX; +using static AsmArm64.Arm64RegisterW; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64DisassemblerTests +{ + [Fact] + public void TryResolvePrecode_StubPrecode() + { + // Arrange + const int JumpAddress = 0x10000; + const int MdOffset = 0x20000; // This value is not used for test. + var rawInstructions = new[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(X10, JumpAddress), // ldr x11, #0x10000 + Arm64InstructionFactory.LDR(X12, MdOffset), // ldr x12, #0x20000 + Arm64InstructionFactory.BR(X10), // br x11 + }; + PrintInstructions(rawInstructions); + + var dataReader = CreateMockDataReader(rawInstructions, getPointer: (ulong address) => + { + // Validate address value + var parseBase = DummyBaseAddress + 4; // Skip `dmb ishld` instruction + ulong mdSlot = parseBase + 4 + (ulong)MdOffset; + address.Should().Be(mdSlot); + + // Return resolved address + return ExpectedResultAddress; + }); + + ulong address = DummyBaseAddress; + + // Act + var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + + // Assert + result.Should().BeTrue(); + address.Should().Be(ExpectedResultAddress); + isPrestubMd.Should().BeTrue(); + } + + [Fact] + public void TryResolvePrecode_FixupPrecode() + { + // Arrange + var rawInstructions = new uint[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(X11, 0x10000), // ldr x11, #0x10000 + Arm64InstructionFactory.BR(X11), // br x11 + Arm64InstructionFactory.LDR(X12, 0x20000), // ldr x12, #0x20000 + }; + PrintInstructions(rawInstructions); + + var dataReader = CreateMockDataReader(rawInstructions, getPointer: (ulong address) => + { + const int mdLdrOffset = 12; + address.Should().Be(DummyBaseAddress + mdLdrOffset + 0x20000); + return ExpectedResultAddress; + }); + + ulong address = DummyBaseAddress; + + // Act + var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + + // Assert + result.Should().BeTrue(); + address.Should().Be(ExpectedResultAddress); + isPrestubMd.Should().BeTrue(); + } + + [Fact] + public void TryResolvePrecode_FixupPrecodeCode_Fixup() + { + // Arrange + const int MdOffset = 0x10000; + const int JumpAddress = 0x20000; // This value is not used. + + var rawInstructions = new uint[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(X12, MdOffset), // ldr x12, #0x10000 + Arm64InstructionFactory.LDR(X11, JumpAddress), // ldr x11, #0x20000 + Arm64InstructionFactory.BR(X11), // br x11 + }; + PrintInstructions(rawInstructions); + + var dataReader = CreateMockDataReader(rawInstructions, getPointer: (ulong address) => + { + // Validate address value + ulong parseBase = DummyBaseAddress + 4; // Skip `dmb ishld` instruction + ulong mdAddress = unchecked(parseBase + MdOffset); + address.Should().Be(mdAddress); + + // Return resolved address + return ExpectedResultAddress; + }); + + // Act + ulong address = DummyBaseAddress; + var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + + // Assert + result.Should().BeTrue(); + address.Should().Be(ExpectedResultAddress); + isPrestubMd.Should().BeTrue(); + } + + [Fact] + public void TryResolvePrecode_CallCountingStub() + { + // Arrange + const int RemainingCallCount = 0x10000; + var rawInstructions = new uint[] + { + Arm64InstructionFactory.DMB(Arm64BarrierOperationLimitKind.ISHLD), // dmb ishld + Arm64InstructionFactory.LDR(X9, RemainingCallCount), // ldr x9, #0x10000 + Arm64InstructionFactory.LDRH(W10, new Arm64ImmediateMemoryAccessor(X9, 0)), // ldrh w10, [x9] + Arm64InstructionFactory.SUBS(W10, W10, 1), // subs w10, w10, #1 + }; + PrintInstructions(rawInstructions); + + var dataReader = CreateMockDataReader(rawInstructions, getPointer: (ulong address) => + { + // Validate address value + ulong parseBase = DummyBaseAddress + 4; // Skip `dmb ishld` instruction + ulong countSlot = unchecked(parseBase + RemainingCallCount); + address.Should().Be(countSlot + 8); + + // Return resolved address + return ExpectedResultAddress; + }); + + // Act + ulong address = DummyBaseAddress; + var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + + // Assert + result.Should().BeTrue(); + address.Should().Be(ExpectedResultAddress); + isPrestubMd.Should().BeFalse(); + } +} +#endif \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryTranslateAddressToName.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryTranslateAddressToName.cs new file mode 100644 index 0000000000..669b65bfdd --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryTranslateAddressToName.cs @@ -0,0 +1,411 @@ +using AsmArm64; +using AwesomeAssertions; +using BenchmarkDotNet.Disassemblers; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64DisassemblerTests +{ + private static readonly MockClrMethod DummyMethodNotUsed = default!; + private static readonly MockClrMethod DummyCurrentMethod = new("DummyCurrentMethod", 0x10000, "DummyCurrentMethodSignature", new MockClrType("DummyCurrentMethodType")); + private static readonly MockClrMethod DummyTargetMethod = new("DummyTargetMethod", 0x20000, "DummyTargetMethodSignature", new MockClrType("DummyTargetMethodType")); + + /// + /// When GetJitHelperFunctionName returns non-empty name. + /// It's added to AddressToNameMapping. and no further processing is done. + /// + [Fact] + public void TryTranslateAddressToName_GetJitHelperFunctionName_ReturnsNonEmptyValue() + { + var clrRuntime = CreateMockClrRuntime(); + clrRuntime.GetJitHelperFunctionNameFunc = address => + { + return address switch + { + Address1 => DummyTargetMethod.Name, + _ => throw new ArgumentOutOfRangeException($"Unexpected address specified: 0x{address:X2}"), + }; + }; + + const ulong address = Address1; + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(address, isAddressPrecodeMD: false, state, depth: 0, DummyMethodNotUsed); + + // Assert + state.AddressToNameMapping.Should().BeEquivalentTo(new Dictionary + { + [Address1] = DummyTargetMethod.Name!, + }); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEmpty(); + } + + /// + /// TryTranslateAddressToName try to resolve indirect address. + /// When following conditions are met. + /// 1. Failed to get method from specified address. + /// 2. Specified address is aligned to PointerSize(8). + /// + [Fact] + public void TryTranslateAddressToName_ResolveIndirectAddress() + { + var clrRuntime = CreateMockClrRuntime([], address => + { + return address switch + { + Address1 => Address2, + _ => throw new ArgumentOutOfRangeException($"Unexpected address specified: 0x{address:X2}"), + }; + }); + clrRuntime.GetJitHelperFunctionNameFunc = address => null; + clrRuntime.GetMethodByInstructionPointerFunc = address => + { + return address switch + { + Address1 => null, + Address2 => DummyTargetMethod, + _ => throw new ArgumentOutOfRangeException($"Unexpected address specified: 0x{address:X2}"), + }; + }; + + const ulong address = Address1; + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(address, isAddressPrecodeMD: false, state, depth: 0, DummyCurrentMethod); + + // Assert + state.AddressToNameMapping.Should().BeEquivalentTo(new Dictionary + { + [Address1] = DummyTargetMethod.MethodName, + }); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEquivalentTo( + [ + new MethodInfo(DummyTargetMethod, depth:1), + ]); + } + + /// + /// When address is not aligned to PointerSize(8). + /// Skip to resolve indirect address. + /// Instead, this test verify GetMethodByHandle/GetTypeByMethodTable code path. + /// + [Fact] + public void TryTranslateAddressToName_NoAlignedAddress() + { + const ulong NonAlignedAddress = 0x10004; + var clrRuntime = CreateMockClrRuntime(); + clrRuntime.GetJitHelperFunctionNameFunc = address => null; + clrRuntime.GetMethodByInstructionPointerFunc = address => + { + return address switch + { + NonAlignedAddress => null, + _ => throw new ArgumentOutOfRangeException($"Address: 0x{address:X2}"), + }; + }; + clrRuntime.GetMethodByHandleFunc = handle => null; + clrRuntime.GetTypeByMethodTableFunc = address => null; + + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(NonAlignedAddress, isAddressPrecodeMD: false, state, depth: 0, DummyMethodNotUsed); + + // Assert + state.AddressToNameMapping.Should().BeEmpty(); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEmpty(); + } + + /// + /// Test GetMethodByInstructionPointer behavior. + /// When resolved method has same address/signature as current method, no further processing is done. + /// + [Fact] + public void TryTranslateAddressToName_GetMethodByInstructionPointer_ReturnsSameMethod() + { + var clrRuntime = CreateMockClrRuntime([]); + clrRuntime.GetJitHelperFunctionNameFunc = _ => null; + clrRuntime.GetMethodByInstructionPointerFunc = address => + { + return address switch + { + Address1 => DummyCurrentMethod, // Return same method. + _ => throw new ArgumentOutOfRangeException($"Address: 0x{address:X2}"), + }; + }; + + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(Address1, isAddressPrecodeMD: false, state, depth: 0, DummyCurrentMethod); + + // Assert + state.AddressToNameMapping.Should().BeEmpty(); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEmpty(); + } + + /// + /// Test GetMethodByInstructionPointer behavior. + /// When resolved method has different address/signature as current method. + /// It's added AddressToNameMapping/Todo. + /// + [Fact] + public void TryTranslateAddressToName_GetMethodByInstructionPointer_ReturnsDifferentMethod() + { + var clrRuntime = CreateMockClrRuntime(); + clrRuntime.GetJitHelperFunctionNameFunc = _ => null; + clrRuntime.GetMethodByInstructionPointerFunc = address => + { + return address switch + { + Address1 => DummyTargetMethod, + _ => throw new ArgumentOutOfRangeException($"Address: 0x{address:X2}"), + }; + }; + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(Address1, isAddressPrecodeMD: false, state, 0, DummyCurrentMethod); + + // Assert + state.AddressToNameMapping.Should().BeEquivalentTo(new Dictionary + { + [Address1] = DummyTargetMethod.MethodName, + }); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEquivalentTo( + [ + new MethodInfo(DummyTargetMethod, depth:1), + ]); + } + + /// + /// When GetMethodByInstructionPointer returns null. + /// Then it try to resolve method with TryFollowJumpTrampoline. + /// + [Fact] + public void TryTranslateAddressToName_TryFollowJumpTrampoline_B() + { + var clrRuntime = CreateMockClrRuntime( + [ + Arm64InstructionFactory.B(0x10000), // b 0x10000 + ], + address => + { + return Address1; + }); + + clrRuntime.GetJitHelperFunctionNameFunc = _ => null; + clrRuntime.GetMethodByInstructionPointerFunc = address => + { + return address switch + { + Address1 => null, + Address1 + 0x10000 => DummyTargetMethod, + _ => throw new ArgumentOutOfRangeException($"Address: 0x{address:X2}") + }; + }; + + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(Address1, isAddressPrecodeMD: false, state, 0, DummyCurrentMethod); + + // Assert + state.AddressToNameMapping.Should().BeEquivalentTo(new Dictionary + { + [Address1] = DummyTargetMethod.MethodName, + }); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEquivalentTo( + [ + new MethodInfo(DummyTargetMethod, depth:1), + ]); + } + + /// + /// When GetMethodByInstructionPointer returns null. + /// Then it try to resolve method with TryFollowJumpTrampoline up to 8 hops. + /// + [Fact] + public void TryTranslateAddressToName_TryFollowJumpTrampoline_MultiHop() + { + var clrRuntime = CreateMockClrRuntime( + [ + Arm64InstructionFactory.B(0x1000), // b 0x1000 + ], + address => + { + return Address2; + }); + + clrRuntime.GetJitHelperFunctionNameFunc = _ => null; + clrRuntime.GetMethodByInstructionPointerFunc = address => + { + switch (address) + { + case Address1: + case Address2: + return null; // Return null to test JumpTrampoline + + case Address1 + 0x1000: + case Address1 + 0x2000: + case Address1 + 0x3000: + case Address1 + 0x4000: + case Address1 + 0x5000: + case Address1 + 0x6000: + case Address1 + 0x7000: + return null; + + case Address1 + 0x8000: + return DummyTargetMethod; // Return method when Hop:8 + + default: + throw new ArgumentOutOfRangeException($"Address: 0x{address:X2}"); + } + }; + + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(Address1, isAddressPrecodeMD: false, state, 0, DummyCurrentMethod); + + // Assert + state.AddressToNameMapping.Should().BeEquivalentTo(new Dictionary + { + [Address1] = DummyTargetMethod.MethodName, + }); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEquivalentTo( + [ + new MethodInfo(DummyTargetMethod, depth:1), + ]); + } + + /// + /// When TryFollowJumpTrampoline failed to resolve method descriptor, + /// Try to get method discriptor via GetMethodByHandleFunc. + /// + [Fact] + public void TryTranslateAddressToName_GetMethodByHandle_WithPreCode() + { + var clrRuntime = CreateMockClrRuntime([], _ => 0); + clrRuntime.GetJitHelperFunctionNameFunc = _ => null; + clrRuntime.GetMethodByInstructionPointerFunc = address => + { + return address switch + { + Address1 => null, // Return null to test GetMethodByHandle; + _ => throw new ArgumentOutOfRangeException($"Address: 0x{address:X2}"), + }; + }; + clrRuntime.GetMethodByHandleFunc = handle => + { + return DummyTargetMethod; + }; + + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(Address1, isAddressPrecodeMD: true, state, 0, DummyCurrentMethod); + + // Assert + state.AddressToNameMapping.Should().BeEquivalentTo(new Dictionary + { + [Address1] = $"Precode of {DummyTargetMethod.Signature}", + }); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEquivalentTo( + [ + new MethodInfo(DummyTargetMethod, depth:1), + ]); + } + + /// + /// When TryFollowJumpTrampoline failed to resolve method descriptor, + /// Try to get method discriptor via GetMethodByHandleFunc. + /// + [Fact] + public void TryTranslateAddressToName_GetMethodByHandle_WithoutPreCode() + { + var clrRuntime = CreateMockClrRuntime([], _ => 0); + clrRuntime.GetJitHelperFunctionNameFunc = _ => ""; + clrRuntime.GetMethodByInstructionPointerFunc = address => + { + return address switch + { + Address1 => null, // Return null to test GetMethodByHandle; + _ => throw new ArgumentOutOfRangeException($"Address: 0x{address:X2}"), + }; + }; + clrRuntime.GetMethodByHandleFunc = address => + { + return address switch + { + Address1 => DummyTargetMethod, + _ => throw new ArgumentOutOfRangeException($"Address: 0x{address:X2}"), + }; + }; + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(Address1, isAddressPrecodeMD: false, state, 0, DummyCurrentMethod); + + // Assert + state.AddressToNameMapping.Should().BeEquivalentTo(new Dictionary + { + [Address1] = $"MD_{DummyTargetMethod.Signature}", + }); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEmpty(); // It's not added when isAddressPrecodeMD:false + } + + /// + /// When GetMethodByHandleFunc failed to resolve method descriptor, + /// Try to get method discriptor via GetTypeByMethodTable. + /// + [Fact] + public void TryTranslateAddressToName_GetTypeByMethodTable() + { + var clrRuntime = CreateMockClrRuntime([], _ => 0); + clrRuntime.GetJitHelperFunctionNameFunc = _ => null; + clrRuntime.GetMethodByInstructionPointerFunc = address => + { + return address switch + { + Address1 => null, + _ => throw new ArgumentOutOfRangeException($"Address: 0x{address:X2}"), + }; + }; + clrRuntime.GetMethodByHandleFunc = handle => null; // Returns null to test GetTypeByMethodTable + clrRuntime.GetTypeByMethodTableFunc = address => new MockClrType("DummyType"); + State state = new State(clrRuntime, DummyTargetFramework); + + // Act + var helper = new Arm64DisassemblerHelper(); + helper.TryTranslateAddressToName(Address1, isAddressPrecodeMD: false, state, 0, DummyCurrentMethod); + + // Assert + state.AddressToNameMapping.Should().BeEquivalentTo(new Dictionary + { + [Address1] = "MT_DummyType", + }); + state.HandledMethods.Should().BeEmpty(); + state.Todo.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.cs new file mode 100644 index 0000000000..b54a9bf9ff --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.cs @@ -0,0 +1,20 @@ +using BenchmarkDotNet.Disassemblers; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64DisassemblerTests : Arm64DisassemblerTestBase +{ + // On macos(arm64), available virtual address space is 48-bit (or 52-bit) and minimum address must be 4GB (0x1_0000_0000) + internal const ulong DummyBaseAddress = 0x0000_F000_0000_0000UL; + internal const string DummyTargetFramework = "net10.0"; + + // 4GB as base address. it's minimum valid address on macos(arm64) + internal const ulong Address1 = 0x1_0000_0000; + internal const ulong Address2 = 0x2_0000_0000; + internal const ulong ExpectedResultAddress = 0x3_0000_0000; + + public Arm64DisassemblerTests(ITestOutputHelper output) + : base(output) + { + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.cs new file mode 100644 index 0000000000..ae213014f0 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.cs @@ -0,0 +1,147 @@ +// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. +#if NET +using AsmArm64; +using AwesomeAssertions; +using BenchmarkDotNet.Disassemblers; +using Iced.Intel; +using System.Text.RegularExpressions; +using static AsmArm64.Arm64RegisterW; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64InstructionFormatterTests +{ + private readonly ITestOutputHelper Output; + + private static readonly FormatterOptions FormatterOptions = new() + { + // FirstOperandCharIndex = 10, // Use DisassemblyDiagnoserConfig default config value. + }; + + public Arm64InstructionFormatterTests(ITestOutputHelper output) + { + Output = output; + } + + [Theory] + [MemberData(nameof(TestData.TestData01), MemberType = typeof(TestData))] + public void FormatInstruction(uint rawInstruction, string expected) + { + // Arrange + var asm = rawInstruction.ToArm64Asm(address: 0); + + // Act + var result = asm.Format(FormatterOptions); + + // Assert + result.Should().Be(expected); + + // Verify AsmArm64 compatibility + var instruction = rawInstruction.ToAsmArm64Instruction(); + var text = instruction.ToString("X", null); + text = ReplaceNoCompatibleText(instruction, text); + + text.Should().Be(expected); + + } + + [Fact] + public void FormatInstruction_B_WithReferencedAddress() + { + // Arrange + var rawInstruction = Arm64InstructionFactory.B(0x100); + var asm = rawInstruction.ToArm64Asm(0, 0x10_000); + + var options = new FormatterOptions() + { + FirstOperandCharIndex = 6, + }; + + var symbols = new Dictionary + { + }; + + // Act + var result = Arm64InstructionFormatter.Format(asm, options, printInstructionAddresses: true, pointerSize: 8, symbols); + + // TODO: + // Assert + result.Should().Be("0 b #0x100"); + } + + // Temporary workaround code to pass AsmArm64/Capstone compatibility test. + // See: https://github.com/xoofx/AsmArm64/issues/15 + private static string ReplaceNoCompatibleText(Arm64Instruction instruction, string text) + { + // Arm64LabelOffset seems not support `X` format, so we need to manually replace label from decimal format to hex format + if (instruction.Flags.HasFlag(Arm64InstructionFlags.HasLabel)) + { + text = Regex.Replace(text, @"#(-?\d+)\b", m => + { + var value = int.Parse(m.Groups[1].Value); + return Math.Abs(value) < 10 + ? $"#{m.Groups[1].Value}" + : $"#0x{value:X}"; + }); + } + + // AsmArm64 use aliases for mnemonic for some instructions, so we need to replace them with the original instruction name + switch (instruction.Id) + { + case Arm64InstructionId.MOV_movz_32_movewide: + case Arm64InstructionId.MOV_movz_64_movewide: + text = Regex.Replace(text, "^mov", "movz"); + break; + default: + break; + } + + return text; + } + + public static class TestData + { + public static TheoryData TestData01 => new() + { + { Arm64InstructionFactory.B(0x8), "b #8"}, // Label value that lower than 0x10 use decimal format. + { Arm64InstructionFactory.B(0x100), "b #0x100"}, + // TODO: MOVZ instruction is printed as `mov` in AsmArm64. + { Arm64InstructionFactory.MOVZ(W0, 0x100), "movz w0, #0x100"}, + { Arm64InstructionFactory.MOVZ(X0, 0x100), "movz x0, #0x100"}, + // TODO: MOVZ with shift amount representation is different in AsmArm64. + // { Arm64InstructionFactory.MOVZ(X0, 0x100, amount:16), "movz x0, #0x100, lsl #16"} + // TODO: Minus label offset representation is different between AsmArm64 and Gee.External.Capstone. + // { Arm64InstructionFactory.B(-8), "b #-8"}, // Gee.External.Capstone don't support minus label offset. + }; + } +} + +file static class ExtensionMethods +{ + public static string Format( + this Arm64Asm asm, + FormatterOptions options, + bool printInstructionAddresses = false, + Dictionary? addressMappings = null) + { + uint pointerSize = 8; + return Arm64InstructionFormatter.Format(asm, options, printInstructionAddresses, pointerSize, addressMappings ?? []); + } + + public static Arm64Asm ToArm64Asm(this uint rawInstruction, ulong address, ulong? referencedAddress = null, bool isReferencedAddressIndirect = false) + { + if (referencedAddress <= ushort.MaxValue) + referencedAddress = null; + + return new Arm64Asm + { + InstructionPointer = address, + InstructionLength = 4, + ReferencedAddress = referencedAddress, + IsReferencedAddressIndirect = isReferencedAddressIndirect, + Instruction = rawInstruction.ToCapstoneArm64Instruction(address), + }; + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs new file mode 100644 index 0000000000..986320cad4 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs @@ -0,0 +1,139 @@ +// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. +#if NET +using AwesomeAssertions; +using Gee.External.Capstone.Arm64; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +// ADPR->ADD tests. +public partial class Arm64RegisterValueAccumulatorTests +{ + [Fact] + public void AdrpThenAdd_ShouldCalculateAddress() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Adrp(X0, 0x2000), // adrp x0, #0x2000 + Arm64TestInstructions.Add(X0, X0, 0x123), // add x0, x0, #0x123 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x2123L); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + } + + [Fact] + public void AdrpThenAdd_WithNegativeOffset_ShouldCalculateAddress() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Adrp(X0, -0x1000), // adrp x0, #-0x1000 + Arm64TestInstructions.Add(X0, X0, 0x100), // add x0, x0, #0x100 + }; + PrintInstructions(instructions); // Note: Capstone print minus offset as `0xFFFFFFFFFFFFF000` + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(-0x1000 + 0x100); + } + + [Fact] + public void AdrpThenAdd_ThenAdd_ShouldResetValue() + { + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Adrp(X0, 0x1000), // adrp x0, #0x1000 + Arm64TestInstructions.Add(X0, X0, 0x100), // add x0, x0, #0x100 + Arm64TestInstructions.Add(X0, X0, 0x200), // add x0, x0, #0x200 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + accumulator.Feed(instructions[2]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + } + + [Fact(Skip = "Shifted value is not supported on current implementation.")] + public void AdrpThenAdd_WithShiftedImm_ShouldCalculateAddress() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Adrp(X0, 0x1000000), // adrp x0, #0x1000000 + Arm64TestInstructions.Add(X0, X0, 0xFFF, 12), // add x0, x0, #0xFFF, lsl #12 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x1FFF000); + } + + [Fact] + public void AdrpThenAdd_DifferentDestinationRegister_ShouldResetValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Adrp(X0, 0x2000), // adrp x0, #0x2000 + Arm64TestInstructions.Add(X1, X0, 0x100), // add x1, x0, #0x100 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); // Register is not matched + } + + [Fact(Skip = "On current implementation, `State.ExpectingAdd` don't reset value when unexpected instruction passed.")] + public void AdrpThenOther_ThenAdd_ShouldResetValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Adrp(X0, 0x1000), // adrp x0, #0x1000 + Arm64TestInstructions.Movz(X0, 0x100), // movz x0, #0x100 + Arm64TestInstructions.Add(X0, X0, 0x100), // add x1, x0, #0x100 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + accumulator.Feed(instructions[2]); + + // Assert + accumulator.HasValue.Should().BeFalse(); // Register is not matched + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs new file mode 100644 index 0000000000..52035cfaea --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs @@ -0,0 +1,33 @@ +using AwesomeAssertions; +using Gee.External.Capstone.Arm64; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +// MOVZ tests. +public partial class Arm64RegisterValueAccumulatorTests +{ + [Fact(Skip = "On current implementation, shifted immediate value is not supported.")] + public void Movz_WithShiftedImmediateValue_ShouldStartNewValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1111, 0), // movz x0, #0x1111 + Arm64TestInstructions.Movz(X0, 0x2222, 16), // movz x0, #0x2222, lsl #16 + Arm64TestInstructions.Movz(X0, 0x3333, 32), // movz x0, #0x3333, lsl #32 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + accumulator.Feed(instructions[2]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.Value.Should().Be(0x3333_2222_1111); + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenBranchConditional.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenBranchConditional.cs new file mode 100644 index 0000000000..f10daf20c9 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenBranchConditional.cs @@ -0,0 +1,79 @@ +// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. +#if NET +using AsmArm64; +using AwesomeAssertions; +using Gee.External.Capstone.Arm64; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +// MOVZ with conditional branch instruction tests +public partial class Arm64RegisterValueAccumulatorTests +{ + [Fact] + public void MovzThenCbz_ShouldKeepValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Cbz(X0, 0x0100), // cbz x0, #0x0100 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x1000); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + } + + [Fact] + public void MovzThenCbnz_ShouldKeepValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Cbnz(X0, 0x0100), // cbnz x0, #0x0100 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x1000); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + } + + [Fact] + public void MovzThenConditionalB_ShouldKeepValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.B(Arm64ConditionalKind.EQ, 0x100), // b.eq x0, #0x0100 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x1000); // branch instruction's immediate value is not affects accumulated value. + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs new file mode 100644 index 0000000000..52a9e7cefc --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs @@ -0,0 +1,134 @@ +// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. +#if NET +using AsmArm64; +using AwesomeAssertions; +using Gee.External.Capstone.Arm64; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +// MOVZ->LDR tests +public partial class Arm64RegisterValueAccumulatorTests +{ + [Fact] + public void MovzThenLdr_SameRegister_ShouldHaveValue() + { + // Arrange + var expectedAddress = 0x1234UL; + using var clrRuntime = CreateMockClrRuntime(expectedAddress); + var accumulator = CreateValueAccumulator(clrRuntime); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Ldr(X0, baseRegister: X0), // ldr x0, [x0] + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.Value.Should().Be((long)expectedAddress); // It should be value that is returned by ReadPointer. + } + + [Fact] + public void MovzThenLdr_DifferentRegister_ShouldHaveValue() + { + // Arrange + var expectedAddress = 0x1234UL; + using var clrRuntime = CreateMockClrRuntime(expectedAddress); + var accumulator = CreateValueAccumulator(clrRuntime); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Ldr(X1, baseRegister: X0, immediate: 0x0), // ldr x1, [x0] + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X1); // Value is loaded to X1 register. + accumulator.Value.Should().Be((long)expectedAddress); // It should be value that is returned by ReadPointer. + } + + [Fact] + public void MovzThenLdr_WithDisplacement_ShouldNotHaveValue() + { + // Arrange + var expectedAddress = 0x1234UL; + using var clrRuntime = CreateMockClrRuntime(expectedAddress); + var accumulator = CreateValueAccumulator(clrRuntime); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Ldr(X0, baseRegister: X0, immediate: 0x100), // ldr x0, [x0, #0x100] + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + + // TODO: Current accumulator don't reset registerId. + // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + } + + [Fact] + public void MovzThenLdr_WithIndexRegister_ShouldNotHaveValue() + { + // Arrange + var expectedAddress = 0x1234UL; + using var clrRuntime = CreateMockClrRuntime(expectedAddress); + var accumulator = CreateValueAccumulator(clrRuntime); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Ldr(X0, baseRegister: X0, indexRegister: X1), // ldr x0, [x0, x1] + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + + // TODO: Current accumulator don't reset registerId. + // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + } + + [Fact] + public void MovzThenLdrLiteral_ShouldNotHaveValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Ldr(X0, 0x100), // ldr x0, #0x100 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + + // TODO: Current accumulator don't reset registerId. + // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs new file mode 100644 index 0000000000..86b2d83dde --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs @@ -0,0 +1,87 @@ +// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. +#if NET +using AwesomeAssertions; +using Gee.External.Capstone.Arm64; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +// MOVZ->MOVK tests. +public partial class Arm64RegisterValueAccumulatorTests +{ + [Fact] + public void MovzThenMovz_ShouldStartNewValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1111), // movz x0, #0x1111 + Arm64TestInstructions.Movz(X1, 0x2222), // movz x1, #0x2222 + Arm64TestInstructions.Movk(X1, 0x3333, 16), // movk x1, #0x3333, lsl #16 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); // Reset accumulated value. + accumulator.Feed(instructions[2]); + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X1); + accumulator.Value.Should().Be(0x3333_2222); // X1 register value is used. + } + + [Fact] + public void MovzThenMovk_ShouldHaveValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + + // Act + accumulator.Feed(Arm64TestInstructions.Movz(X0, 0x1234)); // movz x0, #0x1234 + accumulator.Feed(Arm64TestInstructions.Movk(X0, 0x5678, 16)); // movk x0, #0x5678, lsl #16 + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x5678_1234L); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + } + + [Fact(Skip = "Current value accumulator expecting sequencial 0/16/32/48 shfit for MOVK.")] + public void MovzThenMovk_WithSameShiftValue_ShouldHasAccumulatedValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + + // Act + accumulator.Feed(Arm64TestInstructions.Movz(X0, 0x1111)); // movz x0, #0x1111 + accumulator.Feed(Arm64TestInstructions.Movk(X0, 0x2222, 16)); // movk x0, #0x2222, lsl #16 + accumulator.Feed(Arm64TestInstructions.Movk(X0, 0x0022, 16)); // movk x0, #0x0022, lsl #16 + + // Assert + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x0022_1111L); // Shifted value should overwrite bits. + } + + [Fact] + public void MovzThenMovk_WithUnexpectedShift_ShouldResetValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1234), // movz x0, #0x1234 + Arm64TestInstructions.Movk(X0, 0x5678, 32) // movk x0, #0x5678, lsl #32 + }; + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); // Reset accumulated value, because _expectedMovkShift is not matched. + + // Assert + accumulator.HasValue.Should().BeFalse(); + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs new file mode 100644 index 0000000000..467a7d17f8 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs @@ -0,0 +1,151 @@ +// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. +#if NET +using AwesomeAssertions; +using Gee.External.Capstone.Arm64; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +// MOVZ with unconditional branch tests. +public partial class Arm64RegisterValueAccumulatorTests +{ + [Fact] + public void MovzThenUnconditionalB_ShouldResetValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.B(0x100), // b #0x100 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + + // TODO: Current accumulator don't reset registerId. + // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + } + + + [Fact] + public void MovzThenTbz_ShouldResetValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Tbz(X0, 63, 0x100) // tbz x0, #63, #0x100 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + + // TODO: Current accumulator don't reset registerId. + // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + + var details = instructions[1].Details; + details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP).Should().BeTrue(); + details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE).Should().BeTrue(); + } + + [Fact(Skip = "Capstone 4.0.2 don't grouping RET instruction as JUMP group.")] + public void MovzThenRet_ShouldResetValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Ret(), // ret + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + } + + [Fact(Skip = "Capstone 4.0.2 don't grouping DRPS instruction as JUMP group.")] + public void MovzThenDrps_ShouldResetValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Drps(), // drps + }; + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + + // TODO: Current implementation don't reset state. + // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + } + + [Fact] + public void MovzThenBl_ShouldResetValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Bl(0x100), // bl 0x100 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + + // TODO: Current implementation don't reset state. + // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + } + + [Fact] + public void MovzThenBlr_ShouldResetValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 + Arm64TestInstructions.Blr(X0), // bl x0 + }; + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); + + // TODO: Current implementation don't reset state. + // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.cs new file mode 100644 index 0000000000..5e29e08b16 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.cs @@ -0,0 +1,32 @@ +using AwesomeAssertions; +using BenchmarkDotNet.Disassemblers; +using Microsoft.Diagnostics.Runtime.Interfaces; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64RegisterValueAccumulatorTests : Arm64DisassemblerTestBase +{ + private static readonly IClrRuntime DummyClrRuntime = CreateMockClrRuntime(0); + + public Arm64RegisterValueAccumulatorTests(ITestOutputHelper output) : base(output) + { + } + + private static Arm64RegisterValueAccumulator CreateValueAccumulator(IClrRuntime? clrRuntime = null) + { + var accumulator = new Arm64RegisterValueAccumulator(); + accumulator.Init(clrRuntime ?? DummyClrRuntime); + return accumulator; + } + + [Fact] + public void InitialState_ShouldNotHaveValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + + // Assert + accumulator.HasValue.Should().BeFalse(); + accumulator.Value.Should().Be(0); + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/BelongsToGroupTests.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/BelongsToGroupTests.cs new file mode 100644 index 0000000000..5a7e352145 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/BelongsToGroupTests.cs @@ -0,0 +1,174 @@ +// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. +#if NET +using AsmArm64; +using AwesomeAssertions; +using Gee.External.Capstone.Arm64; +using static AsmArm64.Arm64RegisterW; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class BelongsToGroupTests +{ + private readonly ITestOutputHelper Output; + + public BelongsToGroupTests(ITestOutputHelper output) + { + Output = output; + } + + [Theory] + [MemberData(nameof(TestData.Jump), MemberType = typeof(TestData))] + public void BelongToGroup_Jump(InstructionTestData testData) + { + // Arrange + var groupId = Arm64InstructionGroupId.ARM64_GRP_JUMP; + var instruction = testData.GetCapstoneInstruction(); + + // Act + var result = instruction.Details.BelongsToGroup(groupId); + + // Assert + result.Should().BeTrue(); + } + + // It looks like underlying capstone version (v4.0.2) don't contains following PR changes. + // So It returns always false. + // https://github.com/capstone-engine/capstone/pull/1610 + [Theory(Skip = "Incomaptible with capstone v4.0.2")] + [MemberData(nameof(TestData.Call), MemberType = typeof(TestData))] + public void BelongToGroup_Call(InstructionTestData testData) + { + // Arrange + var groupId = Arm64InstructionGroupId.ARM64_GRP_CALL; + var instruction = testData.GetCapstoneInstruction(); + + // Act + var result = instruction.Details.BelongsToGroup(groupId); + + // Assert + result.Should().BeTrue(); + } + + + [Theory] + [MemberData(nameof(TestData.BranchRelative), MemberType = typeof(TestData))] + public void BelongToGroup_BranchRelative(InstructionTestData testData) + { + // Arrange + var groupId = Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE; + var instruction = testData.GetCapstoneInstruction(); + + // Act + var result = instruction.Details.BelongsToGroup(groupId); + + // Assert + result.Should().BeTrue(); + } + + + public record struct InstructionTestData( + Arm64Mnemonic Nemonic, + AsmArm64.Arm64InstructionId InstructionId, + string Text, + uint RawInstruction + ) + { + public AsmArm64.Arm64Instruction GetInstruction() => AsmArm64.Arm64Instruction.Decode(RawInstruction); + + public Gee.External.Capstone.Arm64.Arm64Instruction GetCapstoneInstruction() => RawInstruction.ToCapstoneArm64Instruction(); + }; + + + public static class TestData + { + public static TheoryData Jump + => new(JumpInstructions.Select(ToInstructionTestData)); + + public static TheoryData Call + => new(CallInstructions.Select(ToInstructionTestData)); + + public static TheoryData BranchRelative + => new(BranchRelativeInstructions.Select(ToInstructionTestData)); + + // Following instructions is based on + // Capstone's group mapping at https://github.com/capstone-engine/capstone/blob/6.0.0-Alpha10/arch/AArch64/AArch64GenCSMappingInsn.inc + // + // Gee.External.Capstone internally use v4.0.2 mappings. + // https://github.com/capstone-engine/capstone/blob/4.0.2/arch/AArch64/AArch64MappingInsn.inc + + // TODO: Some instructions are commented out. Because it's not supported by current Capstone version and failed to decode. + + private static readonly uint[] JumpInstructions = + [ + Arm64InstructionFactory.B(0x100), // B_only_branch_imm: + Arm64InstructionFactory.B(Arm64ConditionalKind.EQ, 0x100), // B_only_condbranch: + // Arm64InstructionFactory.BC(Arm64ConditionalKind.EQ, 0x100), // BC_only_condbranch: // It's introduced at Armv9.2-A + Arm64InstructionFactory.BR(X0), // BR_64_branch_reg: + // Arm64InstructionFactory.BRAA(X0, X1), // BRAA_64p_branch_reg: + // Arm64InstructionFactory.BRAAZ(X0), // BRAAZ_64_branch_reg: + // Arm64InstructionFactory.BRAB(X0, X1), // BRAB_64p_branch_reg: + // Arm64InstructionFactory.BRABZ(X0), // BRABZ_64_branch_reg: + Arm64InstructionFactory.CBNZ(W0, 0x100), // CBNZ_32_compbranch: + Arm64InstructionFactory.CBNZ(X0, 0x100), // CBNZ_64_compbranch: + Arm64InstructionFactory.CBZ(W0, 0x100), // CBZ_32_compbranch: + Arm64InstructionFactory.CBZ(X0, 0x100), // CBZ_64_compbranch: + // Arm64InstructionFactory.DRPS(), // DRPS_64e_branch_reg: + // Arm64InstructionFactory.ERET(), // ERET_64e_branch_reg: + // Arm64InstructionFactory.ERETAA(), // ERETAA_64e_branch_reg: + // Arm64InstructionFactory.ERETAB(), // ERETAB_64e_branch_reg: + // Arm64InstructionFactory.RET(X0), // RET_64r_branch_reg: + // Arm64InstructionFactory.RETAA(), // RETAA_64e_branch_reg: + // Arm64InstructionFactory.RETAASPPCR(X0), // RETAASPPCR_64m_branch_reg: + // Arm64InstructionFactory.RETAASPPC(-0x100), // RETAASPPC_only_miscbranch: + // Arm64InstructionFactory.RETAB(), // RETAB_64e_branch_reg: + // Arm64InstructionFactory.RETABSPPCR(X0), // RETABSPPCR_64m_branch_reg: + // Arm64InstructionFactory.RETABSPPC(-0x100), // RETABSPPC_only_miscbranch: + Arm64InstructionFactory.TBNZ(X0, imm: 0x10, 0x100), // TBNZ_only_testbranch: + Arm64InstructionFactory.TBZ(X0, imm: 0x10, 0x100), // TBZ_only_testbranch: + ]; + + private static readonly uint[] CallInstructions = + [ + Arm64InstructionFactory.BL(0x100), // BL_only_branch_imm + Arm64InstructionFactory.BLR(X0), // BLR_64_branch_reg + // Arm64InstructionFactory.BLRAA(X0, X1), // BLRAA_64p_branch_reg + // Arm64InstructionFactory.BLRAB(X0, X1), // BLRAB_64p_branch_reg + // Arm64InstructionFactory.BLRAAZ(X0), // BLRAAZ_64_branch_reg + // Arm64InstructionFactory.BLRABZ(X0), // BLRABZ_64_branch_reg + Arm64InstructionFactory.HVC(0x100), // HVC_ex_exception + Arm64InstructionFactory.SMC(0x100), // HVC_ex_exception + Arm64InstructionFactory.SVC(0x100), // SVC_ex_exception + ]; + + private static readonly uint[] BranchRelativeInstructions = + [ + Arm64InstructionFactory.B(0x100), // B_only_branch_imm + Arm64InstructionFactory.B(Arm64ConditionalKind.EQ, 0x100), // B_only_condbranch + // Arm64InstructionFactory.BC(Arm64ConditionalKind.NE, 0x100), // BC_only_condbranch + Arm64InstructionFactory.BL(0x100), // BL_only_branch_imm + Arm64InstructionFactory.CBNZ(W0,0x100), // CBNZ_32_compbranch + Arm64InstructionFactory.CBNZ(X0,0x100), // CBNZ_64_compbranch + Arm64InstructionFactory.CBZ(W0,0x100), // CBZ_32_compbranch + Arm64InstructionFactory.CBZ(X0,0x100), // CBZ_64_compbranch + // Arm64InstructionFactory.RETAASPPC(0x100), // RETAASPPC_only_miscbranch + // Arm64InstructionFactory.RETABSPPC(0x100), // RETAASPPC_only_miscbranch + Arm64InstructionFactory.TBNZ(X0,imm:0x10, label: 0x20), // TBNZ_only_testbranch + Arm64InstructionFactory.TBZ(X0,imm:0x10, label: 0x20), // TBZ_only_testbranch + ]; + + private static InstructionTestData ToInstructionTestData(uint rawInstruction) + { + AsmArm64.Arm64Instruction instruction = AsmArm64.Arm64Instruction.Decode(rawInstruction); + + return new InstructionTestData + { + InstructionId = instruction.Id, + Nemonic = instruction.Mnemonic, + Text = instruction.ToString(), + RawInstruction = rawInstruction, + }; + } + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerHelper.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerHelper.cs new file mode 100644 index 0000000000..58aa27f65c --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerHelper.cs @@ -0,0 +1,100 @@ +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Disassemblers; +using Microsoft.Diagnostics.Runtime.Interfaces; + +#if NET8_0_OR_GREATER +using Microsoft.Diagnostics.Runtime; +using System.Runtime.CompilerServices; +using Arm64Instruction = Gee.External.Capstone.Arm64.Arm64Instruction; +#endif + +using Arm64Disassembler = BenchmarkDotNet.Disassemblers.Arm64Disassembler; + + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +internal class Arm64DisassemblerHelper : Arm64Disassembler +{ + public new Arm64Asm[] Decode( + byte[] code, + ulong startAddress, + State state, + int depth, + IClrMethod currentMethod, + DisassemblySyntax syntax) + { + var results = base.Decode(code, startAddress, state, depth, currentMethod, syntax); + return results.Cast().ToArray(); + } + + public new void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD, State state, int depth, IClrMethod currentMethod) + { + base.TryTranslateAddressToName(address, isAddressPrecodeMD, state, depth, currentMethod); + } + + public new bool TryFollowJumpTrampoline(State state, ulong address, out ulong target) + { + return base.TryFollowJumpTrampoline(state, address, out target); + } + + // Following methods require UnsafeAccessor to invoke private methods. +#if NET8_0_OR_GREATER + public static bool TryResolvePrecode(IDataReader reader, ref ulong address, out bool isPrestubMD) + { + var disassembler = new Arm64Disassembler(); + return TryResolvePrecode(disassembler, reader, ref address, out isPrestubMD); + } + + public static bool TryReadStubHead(IDataReader reader, ulong address, out ulong parseBase, out uint instr0, out uint instr1, out uint instr2) + { + var disassembler = new Arm64Disassembler(); + return TryReadStubHead(disassembler, reader, address, out parseBase, out instr0, out instr1, out instr2); + } + + public static bool IsLdrLiteral64(uint instr, out int rt, out int offsetBytes) + { + var disassembler = new Arm64Disassembler(); + return IsLdrLiteral64(disassembler, instr, out rt, out offsetBytes); + } + + public static bool TryGetReferencedAddress(Arm64Instruction instruction, Arm64RegisterValueAccumulator accumulator, uint pointerSize, out ulong referencedAddress, out bool isReferencedAddressIndirect) + { + var disassembler = new Arm64Disassembler(); + return TryGetReferencedAddress(disassembler, instruction, accumulator, pointerSize, out referencedAddress, out isReferencedAddressIndirect); + } + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = nameof(TryResolvePrecode))] + private static extern bool TryResolvePrecode( + Arm64Disassembler disassembler, + IDataReader reader, + ref ulong address, + out bool isPrestubMD); + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = nameof(TryReadStubHead))] + private static extern bool TryReadStubHead( + Arm64Disassembler disassembler, + IDataReader reader, + ulong address, + out ulong parseBase, + out uint instr0, + out uint instr1, + out uint instr2); + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = nameof(IsLdrLiteral64))] + private static extern bool IsLdrLiteral64( + Arm64Disassembler disassembler, + uint instr, + out int rt, + out int offsetBytes); + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = nameof(TryGetReferencedAddress))] + private static extern bool TryGetReferencedAddress( + Arm64Disassembler + disassembler, + Arm64Instruction instruction, + Arm64RegisterValueAccumulator accumulator, + uint pointerSize, + out ulong referencedAddress, + out bool isReferencedAddressIndirect); +#endif +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerTestBase.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerTestBase.cs new file mode 100644 index 0000000000..afda72636c --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerTestBase.cs @@ -0,0 +1,80 @@ +using Microsoft.Diagnostics.Runtime; +using System.Buffers.Binary; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public abstract class Arm64DisassemblerTestBase +{ + protected readonly ITestOutputHelper Output; + + public Arm64DisassemblerTestBase(ITestOutputHelper output) + { + Output = output; + } + + protected void PrintInstructions(Gee.External.Capstone.Arm64.Arm64Instruction[] instructions) + { + foreach (var instruction in instructions) + { + Output.WriteLine(instruction.ToString()); + } + } + + protected void PrintInstructions(uint[] rawInstructions) + { + foreach (var rawinstruction in rawInstructions) + { + var instruction = rawinstruction.ToCapstoneArm64Instruction(); + Output.WriteLine(instruction.ToString()); + } + } + + protected static MockClrRuntime CreateMockClrRuntime(ulong dummyValue) + { + return new MockClrRuntime(new MockDataTarget(new MockDataReader(dummyValue))); + } + + protected static MockClrRuntime CreateMockClrRuntime() + => CreateMockClrRuntime([]); + + protected static MockClrRuntime CreateMockClrRuntime(uint[] rawInstructions) + { + Func dummyFunc = _ => throw new InvalidOperationException("This func is not expected to be called."); + return CreateMockClrRuntime(rawInstructions, dummyFunc); + } + + protected static MockClrRuntime CreateMockClrRuntime(uint[] rawInstructions, Func getPointer) + { + var dataReader = CreateMockDataReader(rawInstructions, getPointer); + return new MockClrRuntime(new MockDataTarget(dataReader)); + } + + protected static IDataReader CreateMockDataReader(uint[] rawInstructions) + { + Func dummyFunc = _ => throw new InvalidOperationException("This func is not expected to be called."); + return CreateMockDataReader(rawInstructions, dummyFunc); + } + + protected static IDataReader CreateMockDataReader(uint[] rawInstructions, Func getPointer) + { + return new MockDataReader( + read: (address, buffer) => + { + var instructionCountToWrite = Math.Min(rawInstructions.Length, buffer.Length / 4); + + for (int i = 0; i < instructionCountToWrite; i++) + { + uint rawInstruction = rawInstructions[i]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(i * 4), rawInstruction); + } + + return instructionCountToWrite * 4; + }, + tryReadPointer: new TryReadPointerDelegate((address, out value) => + { + value = getPointer(address); + return true; + }) + ); + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64TestInstructions.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64TestInstructions.cs new file mode 100644 index 0000000000..5b1ec13c20 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64TestInstructions.cs @@ -0,0 +1,182 @@ +using AsmArm64; +using Gee.External.Capstone; +using Gee.External.Capstone.Arm64; +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using Arm64Instruction = Gee.External.Capstone.Arm64.Arm64Instruction; + +namespace BenchmarkDotNet.Tests.Disassemblers; + +internal static class Arm64TestInstructions +{ + public static Arm64Instruction Movz(Arm64RegisterX register, ushort value, int shift = 0) + => Arm64InstructionFactory.MOVZ(register, value, amount: shift).ToCapstoneArm64Instruction(); + + public static Arm64Instruction Movk(Arm64RegisterX register, ushort value, int shift = 0) + => Arm64InstructionFactory.MOVK(register, value, amount: shift).ToCapstoneArm64Instruction(); + + public static Arm64Instruction Add(Arm64RegisterX destination, Arm64RegisterX source, ushort immediate) + => Arm64InstructionFactory.ADD(destination, source, immediate).ToCapstoneArm64Instruction(); + + public static Arm64Instruction Add(Arm64RegisterX destination, Arm64RegisterX source, ushort immediate, byte shiftAmount) + => Arm64InstructionFactory.ADD(destination, source, immediate, amount: shiftAmount).ToCapstoneArm64Instruction(); + + public static Arm64Instruction Adrp(Arm64RegisterX register, Arm64LabelOffset offset) + { + if (offset.Value % 4096 != 0) + throw new ArgumentException("ADRP label offset must be a multiple of 4096", nameof(offset)); + + return Arm64InstructionFactory.ADRP(register, offset).ToCapstoneArm64Instruction(); + } + + public static Arm64Instruction Ldr(Arm64RegisterX destination, Arm64RegisterX baseRegister, short immediate = 0) + { + var memoryAccessor = new Arm64ImmediateMemoryAccessor(baseRegister, immediate); // Use unsigned offset + return Arm64InstructionFactory.LDR(destination, memoryAccessor).ToCapstoneArm64Instruction(); // ldr Xt, [Xn] + } + + public static Arm64Instruction Ldr(Arm64RegisterX destination, Arm64RegisterX baseRegister, Arm64RegisterX indexRegister) + { + var dummyMemoryExtend = new Arm64MemoryExtend(); // Use dummy IArm64MemoryExtend. Because it's not used. + var memoryAccessor = new Arm64RegisterXExtendMemoryAccessor(baseRegister, indexRegister, dummyMemoryExtend); + return Arm64InstructionFactory.LDR(destination, memoryAccessor).ToCapstoneArm64Instruction(); + } + + // LDR with PC(Program Counter) relative offset. + public static Arm64Instruction Ldr(Arm64RegisterX destination, Arm64LabelOffset label) + { + label.ValidateMultipleOf8(); + return Arm64InstructionFactory.LDR(destination, label).ToCapstoneArm64Instruction(); // ldr Xd, [Xn, #label] + } + + public static Arm64Instruction Cbz(Arm64RegisterX register, Arm64LabelOffset label) + { + label.ValidateMultipleOf4(); + return Arm64InstructionFactory.CBZ(register, label).ToCapstoneArm64Instruction(); // cbz Xn, label + } + + public static Arm64Instruction Cbnz(Arm64RegisterX register, Arm64LabelOffset label) + { + label.ValidateMultipleOf4(); + return Arm64InstructionFactory.CBNZ(register, label).ToCapstoneArm64Instruction(); // cbnz Xn, label + } + + // Unconditional branch + public static Arm64Instruction B(Arm64LabelOffset label) + { + label.ValidateMultipleOf4(); + return Arm64InstructionFactory.B(label).ToCapstoneArm64Instruction(); + } + + // Conditional branch + // B.EQ: Equal (Z == 1). + // B.NE: Not equal (Z == 0). + // B.HS: Carry set/unsigned higher or same (C == 1). + // B.LO: Carry clear/unsigned lower (C == 0). + // B.MI: Minus/negative (N == 1). + // B.PL: Plus/positive or zero (N == 0). + // B.VS: Overflow (V == 1). + // B.VC: No overflow (V == 0). + // B.HI: Unsigned higher (C == 1 and Z == 0). + // B.LS: Unsigned lower or same (C == 0 or Z == 1). + // B.GE: Signed greater than or equal (N == V). + // B.LT: Signed less than (N != V). + // B.GT: Signed greater than (Z == 0 and N == V). + // B.LE: Signed less than or equal (Z == 1 or N != V). + // B.AL: Always (unconditional execution). + // B.NV: (this condition is deprecated or not used). + public static Arm64Instruction B(Arm64ConditionalKind conditionalKind, Arm64LabelOffset label) + { + label.ValidateMultipleOf4(); + return Arm64InstructionFactory.B(conditionalKind, label).ToCapstoneArm64Instruction(); // B.EQ label + } + + // RET with X30(Link Register) + public static Arm64Instruction Ret() + => Arm64InstructionFactory.RET().ToCapstoneArm64Instruction(); + + public static Arm64Instruction Ret(Arm64RegisterX register) + => Arm64InstructionFactory.RET(register).ToCapstoneArm64Instruction(); + + public static Arm64Instruction Nop() + => Arm64InstructionFactory.NOP().ToCapstoneArm64Instruction(); + + public static Arm64Instruction Tbz(Arm64RegisterX register, byte imm, Arm64LabelOffset label) + { + label.ValidateMultipleOf4(); + return Arm64InstructionFactory.TBZ(register, imm, label).ToCapstoneArm64Instruction(); + } + + public static Arm64Instruction Bl(Arm64LabelOffset label) + { + label.ValidateMultipleOf4(); + return Arm64InstructionFactory.BL(label).ToCapstoneArm64Instruction(); + } + + public static Arm64Instruction Blr(Arm64RegisterX register) + { + return Arm64InstructionFactory.BLR(register).ToCapstoneArm64Instruction(); + } + + public static Arm64Instruction Drps() + => Arm64InstructionFactory.DRPS().ToCapstoneArm64Instruction(); + + public static Arm64Instruction RetAA() + => Arm64InstructionFactory.RETAA().ToCapstoneArm64Instruction(); +} + +internal static class ExtensionMethods +{ + public static AsmArm64.Arm64Instruction ToAsmArm64Instruction(this uint rawInstruction) + => AsmArm64.Arm64Instruction.Decode(rawInstruction); + + public static Arm64Instruction ToCapstoneArm64Instruction(this uint rawInstruction, ulong baseAddress, uint i) + { + ulong offset = (ulong)i * 4; + return rawInstruction.ToCapstoneArm64Instruction(baseAddress + offset); + } + + public static Arm64Instruction ToCapstoneArm64Instruction(this uint rawInstruction, ulong baseAddress = 0) + { + Span bytes = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(bytes, rawInstruction); + + using var disassembler = CapstoneDisassembler.CreateArm64Disassembler(Arm64DisassembleMode.Arm); + disassembler.EnableInstructionDetails = true; + + var instructions = disassembler.Disassemble(bytes.ToArray(), (long)baseAddress); + if (instructions.Length == 0) + { + var instruction = AsmArm64.Arm64Instruction.Decode(rawInstruction); + throw new Exception($"CapstoneDisassembler failed to deserialize instruction: {instruction.ToString()} (rawInstruction: 0x{rawInstruction:X2})"); + } + + return instructions.First(); + } + + public static byte[] ToLittleEndianBytes(this uint[] rawInstructions) + { + if (BitConverter.IsLittleEndian) + return MemoryMarshal.AsBytes(rawInstructions).ToArray(); + + byte[] bytes = new byte[rawInstructions.Length * 4]; + + for (int i = 0; i < rawInstructions.Length; i++) + { + BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(i * 4, 4), rawInstructions[i]); + } + return bytes; + } + + public static void ValidateMultipleOf4(this Arm64LabelOffset label) + { + if (label.Value % 4 != 0) + throw new ArgumentException("Arm64LabelOffset value must be multiple of 4"); + } + + public static void ValidateMultipleOf8(this Arm64LabelOffset label) + { + if (label.Value % 8 != 0) + throw new ArgumentException("Arm64LabelOffset value must be multiple of 4"); + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockClrMethod.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockClrMethod.cs new file mode 100644 index 0000000000..03a2cb0be1 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockClrMethod.cs @@ -0,0 +1,55 @@ +using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; +using System.Collections.Immutable; +using System.Reflection; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +internal class MockClrMethod : IClrMethod +{ + public MockClrMethod(string? name, ulong nativeCode, string? signature, MockClrType type) + { + Name = name; + NativeCode = nativeCode; + Signature = signature; + Type = type; + } + + public string MethodName => + Signature!.Contains(".") + ? Signature + : $"{Type.Name}.{Signature}"; + + public string? Name { get; } + + public ulong NativeCode { get; } + + public string? Signature { get; } + + public IClrType Type { get; } = default!; + + #region Not implemented + public MethodAttributes Attributes => throw new NotImplementedException(); + + public MethodCompilationType CompilationType => throw new NotImplementedException(); + + public HotColdRegions HotColdInfo => throw new NotImplementedException(); + + public ImmutableArray ILOffsetMap => throw new NotImplementedException(); + + public bool IsClassConstructor => throw new NotImplementedException(); + + public bool IsConstructor => throw new NotImplementedException(); + + public int MetadataToken => throw new NotImplementedException(); + + public ulong MethodDesc => throw new NotImplementedException(); + + public ILInfo? GetILInfo() + => throw new NotImplementedException(); + + + public int GetILOffset(ulong addr) + => throw new NotImplementedException(); + #endregion +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockClrRuntime.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockClrRuntime.cs new file mode 100644 index 0000000000..0548ddbf05 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockClrRuntime.cs @@ -0,0 +1,101 @@ +using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; +using System.Collections.Immutable; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public class MockClrRuntime : IClrRuntime +{ + public MockClrRuntime(IDataTarget dataTarget) + { + DataTarget = dataTarget; + } + + public IDataTarget DataTarget { get; } + + public void FlushCachedData() { } + + public void Dispose() + { + DataTarget.Dispose(); + } + + #region APIs that are used by TryTranslateAddressToName + + internal Func GetJitHelperFunctionNameFunc = + _ => throw new InvalidOperationException("GetJitHelperFunctionNameFunc is not set."); + + internal Func GetMethodByHandleFunc = + _ => throw new InvalidOperationException("GetMethodByHandleFunc is not set."); + + + internal Func GetMethodByInstructionPointerFunc = + _ => throw new InvalidOperationException("GetMethodByInstructionPointerFunc is not set."); + + + internal Func GetTypeByMethodTableFunc = + _ => throw new InvalidOperationException("GetTypeByMethodTableFunc is not set."); + + public string? GetJitHelperFunctionName(ulong address) + => GetJitHelperFunctionNameFunc(address); + + public IClrMethod? GetMethodByHandle(ulong methodHandle) + => GetMethodByHandleFunc(methodHandle); + + public IClrMethod? GetMethodByInstructionPointer(ulong ip) + => GetMethodByInstructionPointerFunc(ip); + + public IClrType? GetTypeByMethodTable(ulong methodTable) + => GetTypeByMethodTableFunc(methodTable); + #endregion + + #region Methods/Properties that is not used + public ImmutableArray AppDomains + => throw new NotImplementedException(); + + public IClrModule BaseClassLibrary + => throw new NotImplementedException(); + + public IClrInfo ClrInfo + => throw new NotImplementedException(); + + public IClrHeap Heap + => throw new NotImplementedException(); + + public bool IsThreadSafe + => throw new NotImplementedException(); + + public IClrAppDomain? SharedDomain + => throw new NotImplementedException(); + + public IClrAppDomain? SystemDomain + => throw new NotImplementedException(); + + public ImmutableArray Threads + => throw new NotImplementedException(); + + public IClrThreadPool? ThreadPool + => throw new NotImplementedException(); + + public uint? TlsSlotIndex + => throw new NotImplementedException(); + + public IEnumerable EnumerateClrNativeHeaps() + => throw new NotImplementedException(); + + public IEnumerable EnumerateHandles() + => throw new NotImplementedException(); + + public IEnumerable EnumerateJitManagers() + => throw new NotImplementedException(); + + public IEnumerable EnumerateModules() + => throw new NotImplementedException(); + + public IEnumerable EnumerateRcwCleanupData() + => throw new NotImplementedException(); + + public IEnumerable EnumerateSyncBlockCleanupData() + => throw new NotImplementedException(); + #endregion +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockClrType.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockClrType.cs new file mode 100644 index 0000000000..346a999c41 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockClrType.cs @@ -0,0 +1,131 @@ +using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; +using System.Collections.Immutable; +using System.Reflection; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +internal class MockClrType : IClrType +{ + public MockClrType(string? name) + { + Name = name; + } + + public string? Name { get; } + + + #region NotImplemented + public ulong AssemblyLoadContextAddress + => throw new NotImplementedException(); + + public IClrType? BaseType + => throw new NotImplementedException(); + + public int ComponentSize + => throw new NotImplementedException(); + + public IClrType? ComponentType + => throw new NotImplementedException(); + + public bool ContainsPointers + => throw new NotImplementedException(); + + public ClrElementType ElementType + => throw new NotImplementedException(); + + public ImmutableArray Fields + => throw new NotImplementedException(); + + public GCDesc GCDesc + => throw new NotImplementedException(); + + public IClrHeap Heap + => throw new NotImplementedException(); + + public bool IsArray + => throw new NotImplementedException(); + + public bool IsCollectible + => throw new NotImplementedException(); + + public bool IsEnum + => throw new NotImplementedException(); + + public bool IsException + => throw new NotImplementedException(); + + public bool IsFinalizable + => throw new NotImplementedException(); + + public bool IsFree + => throw new NotImplementedException(); + + public bool IsObjectReference + => throw new NotImplementedException(); + + public bool IsPointer + => throw new NotImplementedException(); + + public bool IsPrimitive + => throw new NotImplementedException(); + + public bool IsShared + => throw new NotImplementedException(); + + public bool IsString + => throw new NotImplementedException(); + + public bool IsValueType + => throw new NotImplementedException(); + + public ulong LoaderAllocatorHandle + => throw new NotImplementedException(); + + public int MetadataToken + => throw new NotImplementedException(); + + public ImmutableArray Methods + => throw new NotImplementedException(); + + public ulong MethodTable + => throw new NotImplementedException(); + + public IClrModule Module + => throw new NotImplementedException(); + + public ImmutableArray StaticFields + => throw new NotImplementedException(); + + public int StaticSize => throw new NotImplementedException(); + + public TypeAttributes TypeAttributes => throw new NotImplementedException(); + + public IClrEnum AsEnum() + => throw new NotImplementedException(); + + public IEnumerable EnumerateGenericParameters() + => throw new NotImplementedException(); + + public IEnumerable EnumerateInterfaces() + => throw new NotImplementedException(); + + public bool Equals(IClrType? other) + => throw new NotImplementedException(); + + public ulong GetArrayElementAddress(ulong objRef, int index) + => throw new NotImplementedException(); + + public IClrInstanceField? GetFieldByName(string name) + => throw new NotImplementedException(); + + public IClrStaticField? GetStaticFieldByName(string name) + => throw new NotImplementedException(); + + public bool IsFinalizeSuppressed(ulong obj) + => throw new NotImplementedException(); + + public T[]? ReadArrayElements(ulong objRef, int start, int count) where T : unmanaged + => throw new NotImplementedException(); + #endregion +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockDataReader.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockDataReader.cs new file mode 100644 index 0000000000..5fe9ba32a6 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockDataReader.cs @@ -0,0 +1,91 @@ +using Microsoft.Diagnostics.Runtime; +using System.Runtime.InteropServices; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public delegate int ReadBytesDelegate(ulong address, Span buffer); +public delegate bool TryReadPointerDelegate(ulong address, out ulong value); + +internal class MockDataReader : IDataReader +{ + private readonly ReadBytesDelegate _read = (_, _) => throw new InvalidOperationException($"{nameof(_read)} field is not set."); + private readonly TryReadPointerDelegate _tryReadPointer = (_, out _) => throw new InvalidOperationException($"{nameof(_tryReadPointer)} field is not set."); + + + public MockDataReader(ulong dummyValue = 0) + { + _tryReadPointer = (ulong address, out ulong value) => + { + value = dummyValue; + return true; + }; + } + + public MockDataReader(TryReadPointerDelegate tryReadPointer) + { + _tryReadPointer = tryReadPointer; + } + + public MockDataReader(ReadBytesDelegate read) + { + _read = read; + } + + public MockDataReader(ReadBytesDelegate read, TryReadPointerDelegate tryReadPointer) + { + _read = read; + _tryReadPointer = tryReadPointer; + } + + public void FlushCachedData() + { + } + + public int PointerSize + => 8; + + public ulong ReadPointer(ulong address) + { + if (_tryReadPointer(address, out var value)) + return value; + + return 0; + } + + // It's used by TryResolvePrecode + public int Read(ulong address, Span buffer) + => _read(address, buffer); + + // It's used by TryResolvePrecode + public bool ReadPointer(ulong address, out ulong value) + => _tryReadPointer(address, out value); + + #region Methods/Properties that is not used + public string DisplayName + => throw new NotImplementedException(); + + public bool IsThreadSafe + => throw new NotImplementedException(); + + public OSPlatform TargetPlatform + => throw new NotImplementedException(); + + public Architecture Architecture + => throw new NotImplementedException(); + + public int ProcessId + => throw new NotImplementedException(); + + public IEnumerable EnumerateModules() + => throw new NotImplementedException(); + + public bool GetThreadContext(uint threadID, uint contextFlags, Span context) + => throw new NotImplementedException(); + + public bool Read(ulong address, out T value) where T : unmanaged + => throw new NotImplementedException(); + + public T Read(ulong address) where T : unmanaged + => throw new NotImplementedException(); + #endregion +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockDataTarget.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockDataTarget.cs new file mode 100644 index 0000000000..4e86eb4472 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/MockDataTarget.cs @@ -0,0 +1,35 @@ +using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Interfaces; +using System.Collections.Immutable; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +internal class MockDataTarget : IDataTarget +{ + public IDataReader DataReader { get; } + + public MockDataTarget(IDataReader dataReader) + { + DataReader = dataReader; + } + + public void Dispose() + { + DataReader.FlushCachedData(); + } + + #region Methods/Properties that is not used + public CacheOptions CacheOptions + => throw new NotImplementedException(); + + public ImmutableArray ClrVersions + => throw new NotImplementedException(); + + + public IFileLocator? FileLocator + => throw new NotImplementedException(); + + public IEnumerable EnumerateModules() + => throw new NotImplementedException(); + #endregion +}