From aed65a2dab82cd8a90564b2696421116d7985eb0 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:59:10 +0900 Subject: [PATCH 01/14] chore: move asmarm64 reference to core library --- src/BenchmarkDotNet/BenchmarkDotNet.csproj | 1 + tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) mode change 100755 => 100644 tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj index d21c28cbc2..e023399587 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj +++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj old mode 100755 new mode 100644 index 29926b1652..11f7eb7ae4 --- a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj +++ b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj @@ -19,7 +19,6 @@ - @@ -38,7 +37,7 @@ - + From 9c93250b4f8dd1f18a04a712f5d60db6d82f4dd8 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:41:21 +0900 Subject: [PATCH 02/14] chore: refactor ValueAccumulator logics --- .../Arm64InstructionExtensions.cs | 360 ++++++++++++++++++ .../Arm64RegisterValueAccumulator.cs | 277 ++++++++++---- ...gisterValueAccumulatorTests.MovzThenLdr.cs | 23 ++ 3 files changed, 576 insertions(+), 84 deletions(-) create mode 100644 src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs new file mode 100644 index 0000000000..450eb7d646 --- /dev/null +++ b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs @@ -0,0 +1,360 @@ +using AsmArm64; +using Gee.External.Capstone.Arm64; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using AsmArm64Instruction = AsmArm64.Arm64Instruction; +using AsmArm64Operand = AsmArm64.Arm64Operand; +using Arm64InstructionId = AsmArm64.Arm64InstructionId; +using CapstoneArm64Instruction = Gee.External.Capstone.Arm64.Arm64Instruction; +using CapstoneArm64InstructionId = Gee.External.Capstone.Arm64.Arm64InstructionId; +using CapstoneArm64Operand = Gee.External.Capstone.Arm64.Arm64Operand; +using CapstoneArm64OperandType = Gee.External.Capstone.Arm64.Arm64OperandType; +using CapstoneArm64RegisterId = Gee.External.Capstone.Arm64.Arm64RegisterId; + +namespace BenchmarkDotNet.Disassemblers; + +internal static class Arm64InstructionExtensions +{ + #region Extension methods for Capstone + extension(CapstoneArm64Instruction instruction) + { + public CapstoneArm64RegisterId GetRegisterId(int index) + { + var operands = instruction.Details.Operands; + if ((uint)index >= (uint)operands.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + return operands[index].Register.Id; + } + + public Arm64ShiftOperation GetShiftOperation(int index) + { + var operands = instruction.Details.Operands; + if ((uint)index >= (uint)operands.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + var operand = operands[index]; + var shiftOperation = operand.ShiftOperation; + + switch (instruction.Id) + { + case CapstoneArm64InstructionId.ARM64_INS_ADD: + case CapstoneArm64InstructionId.ARM64_INS_ADRP: + case CapstoneArm64InstructionId.ARM64_INS_MOVK: + case CapstoneArm64InstructionId.ARM64_INS_MOVZ: + switch (shiftOperation) + { + case Arm64ShiftOperation.ARM64_SFT_LSL: + return shiftOperation; + + case Arm64ShiftOperation.Invalid: + if (operand.ShiftValue == 0) + return Arm64ShiftOperation.ARM64_SFT_LSL; // Capstone represents LSL #0 as ARM64_SFT_INVALID. + goto default; + + // Currently used instructions don't accept other shift operations. + default: + throw new NotSupportedException($"Unexpected shift operation({shiftOperation}) is specified on instruction({instruction.Id})"); + } + + // Currently other instructions are not supported. + default: + throw new UnreachableException($"Unexpected instruction({instruction.Id}) is specified"); + } + } + + public int GetShiftAmount(int index) + { + var operands = instruction.Details.Operands; + if ((uint)index >= (uint)operands.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + var operand = operands[index]; + + // Capstone represents LSL #0 as ARM64_SFT_INVALID. + if (operand.ShiftOperation == Arm64ShiftOperation.Invalid) + return 0; + + return operand.ShiftValue; + } + + public long GetRawImmediateValue(int index) + { + var operands = instruction.Details.Operands; + if ((uint)index >= (uint)operands.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + return operands[index].Immediate; + } + + public long GetShiftedImmediateValue(int index) + { + var operands = instruction.Details.Operands; + if ((uint)index >= (uint)operands.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + var operand = operands[index]; + + var shiftOperation = instruction.GetShiftOperation(index); + switch (shiftOperation) + { + case Arm64ShiftOperation.ARM64_SFT_LSL: + var shiftAmount = instruction.GetShiftAmount(index); + return operand.Immediate << shiftAmount; + default: + throw new NotSupportedException($"Unexpected shift operation({shiftOperation}) is specified on instruction({instruction.Id})"); + } + } + + public bool TryGetOperand( + int index, + CapstoneArm64OperandType type, + [NotNullWhen(true)] out CapstoneArm64Operand? result) + { + var operands = instruction.Details.Operands; + if ((uint)index >= (uint)operands.Length) + { + result = null; + return false; + } + + result = operands[index]; + if (result.Type == type) + return true; + + result = null; + return false; + } + + public bool BelongsToGroup(Arm64InstructionGroupId expectedGroup) + { + if (instruction.Bytes.Length < 4) + return false; + var rawInstruction = BinaryPrimitives.ReadUInt32LittleEndian(instruction.Bytes); + return AsmArm64Instruction.Decode(rawInstruction).BelongsToGroup(expectedGroup); + } + + public bool IsLdrFromTrackedRegister(CapstoneArm64RegisterId trackedRegister) + { + if (instruction.Id != CapstoneArm64InstructionId.ARM64_INS_LDR) + return false; + + if (!instruction.TryGetOperand(1, CapstoneArm64OperandType.Memory, out var memoryOperand)) + return false; + + // Check base register is the register that is currently tracked. + var memory = memoryOperand.Memory; + if (memory.Base.Id != trackedRegister) + return false; + + // Check displacement. + if (memory.Displacement != 0) + return false; + + // Check index register is specified. + if (memory.Index != null) + return false; + + // Ldr from tracked register (with no displacement/index register). + return true; + } + + public bool IsConditionalBranch() + { + switch (instruction.Id) + { + case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_CBZ: + case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_CBNZ: + case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_TBZ: + case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_TBNZ: + return true; + + case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_B: + switch (instruction.Details.ConditionCode) + { + // `B` and `B.AL` instructions is unconditional branch. + case Gee.External.Capstone.Arm64.Arm64ConditionCode.Invalid: + case Gee.External.Capstone.Arm64.Arm64ConditionCode.ARM64_CC_AL: + return false; + default: + return true; + } + + default: + return false; + } + } + + public bool IsUnconditionalControlFlow() + { + return instruction.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE) + || instruction.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_CALL) + || instruction.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP); + } + + } + #endregion + + #region Extension methods for AsmArm64 + extension(AsmArm64Instruction instruction) + { + public Arm64RegisterOperand GetRegisterOperand(int index) + => (Arm64RegisterOperand)instruction.GetOperand(index, Arm64OperandKind.Register); + + public Arm64ImmediateOperand GetImmediateOperand(int index) + => (Arm64ImmediateOperand)instruction.GetOperand(index, Arm64OperandKind.Immediate); + + private AsmArm64Operand GetOperand(int index, Arm64OperandKind expectedKind) + { + var operand = instruction.GetOperand(index); + if (operand.Kind != expectedKind) + throw new ArgumentException($"The operand at index {index} is not of the expected kind {expectedKind}. Actual kind: {operand.Kind}"); + + return operand; + } + + public bool BelongsToGroup(Arm64InstructionGroupId expectedGroup) + { + // AsmArm64 don't provide mapping for instruction groups. + // So it need to simulate Capstone's BelongsToGroup behavior. + // Capstone's group mappings are defined at https://github.com/capstone-engine/capstone/blob/6.0.0-Alpha10/arch/AArch64/AArch64GenCSMappingInsn.inc + switch (expectedGroup) + { + case Arm64InstructionGroupId.ARM64_GRP_JUMP: + switch (instruction.Id) + { + case Arm64InstructionId.B_only_branch_imm: + case Arm64InstructionId.B_only_condbranch: + case Arm64InstructionId.BC_only_condbranch: + case Arm64InstructionId.BR_64_branch_reg: + case Arm64InstructionId.BRAA_64p_branch_reg: + case Arm64InstructionId.BRAAZ_64_branch_reg: + case Arm64InstructionId.BRAB_64p_branch_reg: + case Arm64InstructionId.BRABZ_64_branch_reg: + case Arm64InstructionId.CBNZ_32_compbranch: + case Arm64InstructionId.CBNZ_64_compbranch: + case Arm64InstructionId.CBZ_32_compbranch: + case Arm64InstructionId.CBZ_64_compbranch: + case Arm64InstructionId.DRPS_64e_branch_reg: + case Arm64InstructionId.ERET_64e_branch_reg: + case Arm64InstructionId.ERETAA_64e_branch_reg: + case Arm64InstructionId.ERETAB_64e_branch_reg: + case Arm64InstructionId.RET_64r_branch_reg: + case Arm64InstructionId.RETAA_64e_branch_reg: + case Arm64InstructionId.RETAASPPCR_64m_branch_reg: + case Arm64InstructionId.RETAASPPC_only_miscbranch: + case Arm64InstructionId.RETAB_64e_branch_reg: + case Arm64InstructionId.RETABSPPCR_64m_branch_reg: + case Arm64InstructionId.RETABSPPC_only_miscbranch: + case Arm64InstructionId.TBNZ_only_testbranch: + case Arm64InstructionId.TBZ_only_testbranch: + return true; + + default: + return false; + } + + case Arm64InstructionGroupId.ARM64_GRP_CALL: + switch (instruction.Id) + { + case Arm64InstructionId.BL_only_branch_imm: + case Arm64InstructionId.BLR_64_branch_reg: // BLR: Branch with link + case Arm64InstructionId.BLRAA_64p_branch_reg: // BLRAA: Branch with link to register with PAC + case Arm64InstructionId.BLRAB_64p_branch_reg: // BLRAB: Branch with link to register with PAC + case Arm64InstructionId.BLRAAZ_64_branch_reg: // Z: Zero modifier + case Arm64InstructionId.BLRABZ_64_branch_reg: // Z: Zero modifier + case Arm64InstructionId.HVC_ex_exception: + case Arm64InstructionId.SMC_ex_exception: + case Arm64InstructionId.SVC_ex_exception: + return true; + default: + return false; + } + case Arm64InstructionGroupId.ARM64_GRP_RET: + switch (instruction.Id) + { + case Arm64InstructionId.DRPS_64e_branch_reg: + case Arm64InstructionId.ERET_64e_branch_reg: + case Arm64InstructionId.ERETAA_64e_branch_reg: + case Arm64InstructionId.ERETAB_64e_branch_reg: + case Arm64InstructionId.RET_64r_branch_reg: + case Arm64InstructionId.RETAA_64e_branch_reg: + case Arm64InstructionId.RETAASPPC_only_miscbranch: + case Arm64InstructionId.RETAASPPCR_64m_branch_reg: + case Arm64InstructionId.RETAB_64e_branch_reg: + case Arm64InstructionId.RETABSPPC_only_miscbranch: + case Arm64InstructionId.RETABSPPCR_64m_branch_reg: + return true; + default: + return false; + } + case Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE: + switch (instruction.Id) + { + case Arm64InstructionId.B_only_branch_imm: + case Arm64InstructionId.B_only_condbranch: + case Arm64InstructionId.BC_only_condbranch: + case Arm64InstructionId.BL_only_branch_imm: + case Arm64InstructionId.CBNZ_32_compbranch: + case Arm64InstructionId.CBNZ_64_compbranch: + case Arm64InstructionId.CBZ_32_compbranch: + case Arm64InstructionId.CBZ_64_compbranch: + case Arm64InstructionId.RETAASPPC_only_miscbranch: + case Arm64InstructionId.RETABSPPC_only_miscbranch: + case Arm64InstructionId.TBNZ_only_testbranch: + case Arm64InstructionId.TBZ_only_testbranch: + return true; + default: + return false; + } + + // Currently, other group ids are not supported. + case Arm64InstructionGroupId.Invalid: + case Arm64InstructionGroupId.ARM64_GRP_INT: + case Arm64InstructionGroupId.ARM64_GRP_PRIVILEGE: + case Arm64InstructionGroupId.ARM64_GRP_CRYPTO: + case Arm64InstructionGroupId.ARM64_GRP_FPARMV8: + case Arm64InstructionGroupId.ARM64_GRP_NEON: + case Arm64InstructionGroupId.ARM64_GRP_CRC: + default: + throw new NotSupportedException($"Group {expectedGroup} is not supported."); + } + } + + public IEnumerable GetWrittenRegisters() + { + foreach (var operand in instruction.Operands) + { + switch (operand.Kind) + { + case Arm64OperandKind.Register: + var registerOperand = (Arm64RegisterOperand)operand; + + if ((registerOperand.Flags & Arm64OperandFlags.Write) != 0) + yield return registerOperand.Value; + + continue; + + case Arm64OperandKind.RegisterGroup: + var registerGroupOperand = (Arm64RegisterGroupOperand)operand; + + if ((registerGroupOperand.Flags & Arm64OperandFlags.Write) != 0) + { + var registerGroupAny = registerGroupOperand.Value; + var baseRegister = registerGroupAny.BaseRegister; + for (int i = 0; i < registerGroupAny.Count; ++i) + { + yield return Arm64RegisterAny.Create(baseRegister.Kind, baseRegister.Index + i, baseRegister.VKind, baseRegister.ElementCount, baseRegister.ElementIndex); + } + } + continue; + + default: + continue; + } + } + } + } + #endregion +} diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs index 2fc4e23ce4..e61b072415 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs @@ -14,7 +14,7 @@ internal enum State } private State _state; - private long _value; + private long _value; // TODO: Change address value type to ulong (address with offset value) private int _expectedMovkShift; private Arm64RegisterId _registerId; private IClrRuntime _runtime; @@ -39,106 +39,215 @@ internal void Reset( 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; - } + if (TryHandleLookingForPattern(instruction)) + return; + + // TODO: Check register overwrite. + // goto default; 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; - } + if (TryHandleExpectingMovk(instruction)) + return; + + // If we didn't find a expecting MOVK instruction, we might be looking for a possible LDR _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; - } + if (TryHandleExpectingAdd(instruction)) + return; + + // TODO: Check register overwrite. + // goto default; 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; - } - } - } + if (TryHandleLdrFromTrackedRegister(instruction)) + return; + + if (TryHandleConditionalInstruction(instruction)) + return; + + if (TryHandleAnotherMovzInstruction(instruction)) + return; + + goto default; + + default: + ResetIfTrackedRegisterIsOverwritten(instruction); break; } } + // TODO: Include ExpectingAdd check. (ADRP instruction without ADD) public bool HasValue => _state == State.ExpectingMovk || _state == State.LookingForPossibleLdr; public long Value => _value; public Arm64RegisterId RegisterId => _registerId; + + private bool TryHandleLookingForPattern(Arm64Instruction instruction) + { + switch (instruction.Id) + { + case Arm64InstructionId.ARM64_INS_MOVZ: + StartMovzSequence(instruction); + return true; + case Arm64InstructionId.ARM64_INS_ADRP: + StartAdrpSequence(instruction); + return true; + default: + return false; + } + } + + private bool TryHandleExpectingMovk(Arm64Instruction instruction) + { + if (instruction.Id != Arm64InstructionId.ARM64_INS_MOVK) + return false; + + if (instruction.GetRegisterId(0) != _registerId) + return false; + + // TODO: Remove this condition because MOVK accept only LSL. + var details = instruction.Details; + if (instruction.GetShiftOperation(1) != Arm64ShiftOperation.ARM64_SFT_LSL) + return false; + + if (instruction.GetShiftAmount(1) != _expectedMovkShift) + return false; + + // TODO: Clear existing 16-bits values before setting MOVK immediate value. + _value |= instruction.GetShiftedImmediateValue(1); + _expectedMovkShift += 16; + return true; + } + + /// + /// Handle following expecting ADD instruction that source/target registers match value tracked register. + /// ADD Xd|SP, Xn|SP, #imm, {shift} + /// ADD Wd|SP, Wn|SP, #imm, {shift} + /// If condition is not matched. skip processing. and wait succeeding ADD instruction. + /// + private bool TryHandleExpectingAdd(Arm64Instruction instruction) + { + if (instruction.Id != Arm64InstructionId.ARM64_INS_ADD) + return false; + + if (instruction.GetRegisterId(0) != _registerId) + return false; + + if (instruction.GetRegisterId(1) != _registerId) + return false; + + if (!instruction.TryGetOperand(2, Arm64OperandType.Immediate, out var operand)) + return false; + + // TODO: Replace to use shifted immediate value. + _value += operand.Immediate; + _state = State.LookingForPossibleLdr; + return true; + } + + private void StartMovzSequence(Arm64Instruction instruction) + { + _registerId = instruction.GetRegisterId(0); + _value = instruction.GetRawImmediateValue(1); // TODO: Replace to shifted immediate value. + _expectedMovkShift = 16; + _state = State.ExpectingMovk; + } + + private void StartAdrpSequence(Arm64Instruction instruction) + { + _registerId = instruction.GetRegisterId(0); + _value = instruction.GetRawImmediateValue(1);// TODO: Replace to shifted immediate value. + _state = State.ExpectingAdd; + } + + private bool TryHandleLdrFromTrackedRegister(Arm64Instruction instruction) + { + if (!instruction.IsLdrFromTrackedRegister(_registerId)) + return false; + + // Simulate the LDR instruction. + var newValue = (long)_runtime.DataTarget.DataReader.ReadPointer((ulong)_value); + _value = newValue; + if (_value == 0) + { + // TODO: Call Reset() to clear internal state. + _state = State.LookingForPattern; + return true; + } + + // The LDR might have loaded the result in another register + _registerId = instruction.GetRegisterId(0); + return true; + } + + private bool TryHandleConditionalInstruction(Arm64Instruction instruction) + { + // TODO: Replace to AsmArm64 based IsConditionalBranch implementation + if (IsConditionalBranch(instruction)) + return true; + + // TODO: Replace to AsmArm64 based IsUnconditionalControlFlow implementation + if (IsUnconditionalControlFlow(instruction)) + { + // We've encountered an unconditional jump or call, the accumulated registers value is not valid anymore + Reset(); + _state = State.LookingForPattern; + return true; + } + + return false; + } + + /// + /// Handle MOVZ instruction that load value from tracked register to another register. + /// + private bool TryHandleAnotherMovzInstruction(Arm64Instruction instruction) + { + if (instruction.Id != Arm64InstructionId.ARM64_INS_MOVZ) + return false; + + // Another constant loading is starting, reprocess it as a new pattern. + _state = State.LookingForPattern; + return TryHandleLookingForPattern(instruction); + } + + // TODO: Remove this method and replace to AsmArm64 based implementation. + private static bool IsConditionalBranch(Arm64Instruction instruction) + { + return instruction.Id == Arm64InstructionId.ARM64_INS_CBZ + || instruction.Id == Arm64InstructionId.ARM64_INS_CBNZ + || (instruction.Id == Arm64InstructionId.ARM64_INS_B && instruction.Details.ConditionCode != Arm64ConditionCode.Invalid); + } + + // TODO: Remove this method and replace to AsmArm64 based implementation. + private static bool IsUnconditionalControlFlow(Arm64Instruction instruction) + { + var details = instruction.Details; + return details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE) + || details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_CALL) + || details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP); + } + + private void ResetIfTrackedRegisterIsOverwritten(Arm64Instruction instruction) + { + // 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 writtenRegister in instruction.Details.AllWrittenRegisters) + { + if (writtenRegister.Id != _registerId) + continue; + + // Some unexpected instruction overwriting the accumulated register + // TODO: Use Reset() to clear internal state. + _state = State.LookingForPattern; + } + } } diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs index 06eb3e68d6..30684ebdad 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs @@ -136,5 +136,28 @@ public void MovzThenLdrLiteral_ShouldNotHaveValue() // TODO: Current accumulator don't reset registerId. // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); } + + [Fact] + public void MovzThenLdr_ZeroAddress_ShouldNotHaveValue() + { + // Arrange + using var clrRuntime = new MockMemory() + .AddPointer(MovzOffset, 0) + .ToMockClrRuntime(); + var accumulator = CreateValueAccumulator(clrRuntime); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, MovzOffset), // movz x0, #0x1000 + Arm64TestInstructions.Ldr(X1, baseRegister: X0), // ldr x1, [x0] + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + + // Assert + accumulator.HasValue.Should().BeFalse(); // When LDR load zero. Accumulated state is expected to be resetted. + } } #endif From 7d40afcc62672fab3da6f5bedac8efa629f36964 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:00:31 +0900 Subject: [PATCH 03/14] chore: add Arm64InstructionExtensions decoding helpers --- .../Arm64InstructionExtensions.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs index 450eb7d646..f5e8804401 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs @@ -25,7 +25,11 @@ public CapstoneArm64RegisterId GetRegisterId(int index) if ((uint)index >= (uint)operands.Length) throw new ArgumentOutOfRangeException(nameof(index)); - return operands[index].Register.Id; + var operand = operands[index]; + if (operand.Type != CapstoneArm64OperandType.Register) + throw new ArgumentException($"Operand at index {index} is not a register (actual: {operand.Type})."); + + return operand.Register.Id; } public Arm64ShiftOperation GetShiftOperation(int index) @@ -48,10 +52,10 @@ public Arm64ShiftOperation GetShiftOperation(int index) case Arm64ShiftOperation.ARM64_SFT_LSL: return shiftOperation; + // Capstone represents `LSL #0` as ARM64_SFT_INVALID. + // So handle ARM64_SFT_INVALID as `LSL` for these instructions. (Other shift operation is not used currently) case Arm64ShiftOperation.Invalid: - if (operand.ShiftValue == 0) - return Arm64ShiftOperation.ARM64_SFT_LSL; // Capstone represents LSL #0 as ARM64_SFT_INVALID. - goto default; + return Arm64ShiftOperation.ARM64_SFT_LSL; // Currently used instructions don't accept other shift operations. default: @@ -88,7 +92,18 @@ public long GetRawImmediateValue(int index) return operands[index].Immediate; } + /// + /// Gets shifted immediate value as signed value (e.g. ADRP page offset can be negative). + /// public long GetShiftedImmediateValue(int index) + => unchecked((long)instruction.GetUnsignedShiftedImmediateValue(index)); + + /// + /// Gets shifted immediate value as unsigned value (e.g. MOVZ/MOVK/ADD address construction). + /// Shift is performed in ulong to avoid signed overflow + /// (e.g. movz x0, #0xFFFF, lsl #48 exceeds long.MaxValue) + /// + public ulong GetUnsignedShiftedImmediateValue(int index) { var operands = instruction.Details.Operands; if ((uint)index >= (uint)operands.Length) @@ -101,7 +116,7 @@ public long GetShiftedImmediateValue(int index) { case Arm64ShiftOperation.ARM64_SFT_LSL: var shiftAmount = instruction.GetShiftAmount(index); - return operand.Immediate << shiftAmount; + return unchecked((ulong)operand.Immediate << shiftAmount); default: throw new NotSupportedException($"Unexpected shift operation({shiftOperation}) is specified on instruction({instruction.Id})"); } From a9b97477fa42ecd5a0b1c1d2123c9eea147ae65c Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:03:22 +0900 Subject: [PATCH 04/14] chore: refactor Arm64RegisterValueAccumulator into TryHandle methods --- .../Arm64RegisterValueAccumulator.cs | 261 ++++++++++-------- 1 file changed, 140 insertions(+), 121 deletions(-) diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs index e61b072415..03208543a7 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs @@ -1,5 +1,6 @@ using Gee.External.Capstone.Arm64; using Microsoft.Diagnostics.Runtime.Interfaces; +using System.Diagnostics; namespace BenchmarkDotNet.Disassemblers; @@ -14,7 +15,7 @@ internal enum State } private State _state; - private long _value; // TODO: Change address value type to ulong (address with offset value) + private long _value; private int _expectedMovkShift; private Arm64RegisterId _registerId; private IClrRuntime _runtime; @@ -44,46 +45,41 @@ public void Feed(Arm64Instruction instruction) case State.LookingForPattern: if (TryHandleLookingForPattern(instruction)) return; - - // TODO: Check register overwrite. - // goto default; break; case State.ExpectingMovk: if (TryHandleExpectingMovk(instruction)) return; - // If we didn't find a expecting MOVK instruction, we might be looking for a possible LDR _state = State.LookingForPossibleLdr; goto case State.LookingForPossibleLdr; case State.ExpectingAdd: if (TryHandleExpectingAdd(instruction)) return; - - // TODO: Check register overwrite. - // goto default; break; case State.LookingForPossibleLdr: if (TryHandleLdrFromTrackedRegister(instruction)) return; - if (TryHandleConditionalInstruction(instruction)) - return; - - if (TryHandleAnotherMovzInstruction(instruction)) + if (TryHandleBranchOrRestart(instruction)) return; - goto default; - - default: - ResetIfTrackedRegisterIsOverwritten(instruction); + // 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 instruction.Details.AllWrittenRegisters) + { + // Some unexpected instruction overwriting the accumulated register + if (reg.Id == _registerId) + { + _state = State.LookingForPattern; + } + } break; } } - // TODO: Include ExpectingAdd check. (ADRP instruction without ADD) public bool HasValue => _state == State.ExpectingMovk || _state == State.LookingForPossibleLdr; public long Value => _value; @@ -95,36 +91,80 @@ private bool TryHandleLookingForPattern(Arm64Instruction instruction) switch (instruction.Id) { case Arm64InstructionId.ARM64_INS_MOVZ: - StartMovzSequence(instruction); - return true; + return TryStartMovzSequence(instruction); case Arm64InstructionId.ARM64_INS_ADRP: - StartAdrpSequence(instruction); - return true; + return TryStartAdrpSequence(instruction); default: return false; } } - private bool TryHandleExpectingMovk(Arm64Instruction instruction) + private bool TryStartMovzSequence(Arm64Instruction instruction) { - if (instruction.Id != Arm64InstructionId.ARM64_INS_MOVK) - return false; + try + { + if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _)) + return false; - if (instruction.GetRegisterId(0) != _registerId) - return false; + if (!instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) + return false; - // TODO: Remove this condition because MOVK accept only LSL. - var details = instruction.Details; - if (instruction.GetShiftOperation(1) != Arm64ShiftOperation.ARM64_SFT_LSL) + _registerId = instruction.GetRegisterId(0); + _value = instruction.GetRawImmediateValue(1); + _state = State.ExpectingMovk; + _expectedMovkShift = 16; + return true; + } + catch (Exception ex) when (IsDecodingException(ex)) + { return false; + } + } + + private bool TryStartAdrpSequence(Arm64Instruction instruction) + { + try + { + if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _)) + return false; - if (instruction.GetShiftAmount(1) != _expectedMovkShift) + if (!instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) + return false; + + _registerId = instruction.GetRegisterId(0); + _value = instruction.GetRawImmediateValue(1); + _state = State.ExpectingAdd; + return true; + } + catch (Exception ex) when (IsDecodingException(ex)) + { return false; + } + } + + private bool TryHandleExpectingMovk(Arm64Instruction instruction) + { + try + { + if (instruction.Id != Arm64InstructionId.ARM64_INS_MOVK) + return false; + + if (instruction.GetRegisterId(0) != _registerId) + return false; + + Arm64InstructionDetail details = instruction.Details; + if (details.Operands[1].ShiftOperation != Arm64ShiftOperation.ARM64_SFT_LSL + || details.Operands[1].ShiftValue != _expectedMovkShift) + return false; - // TODO: Clear existing 16-bits values before setting MOVK immediate value. - _value |= instruction.GetShiftedImmediateValue(1); - _expectedMovkShift += 16; - return true; + _value = _value | (instruction.GetRawImmediateValue(1) << details.Operands[1].ShiftValue); + _expectedMovkShift += 16; + return true; + } + catch (Exception ex) when (IsDecodingException(ex)) + { + return false; + } } /// @@ -135,71 +175,84 @@ private bool TryHandleExpectingMovk(Arm64Instruction instruction) /// private bool TryHandleExpectingAdd(Arm64Instruction instruction) { - if (instruction.Id != Arm64InstructionId.ARM64_INS_ADD) - return false; + try + { + if (instruction.Id != Arm64InstructionId.ARM64_INS_ADD) + return false; - if (instruction.GetRegisterId(0) != _registerId) - return false; + if (instruction.GetRegisterId(0) != _registerId) + return false; - if (instruction.GetRegisterId(1) != _registerId) - return false; + if (instruction.GetRegisterId(1) != _registerId) + return false; - if (!instruction.TryGetOperand(2, Arm64OperandType.Immediate, out var operand)) - return false; + if (!instruction.TryGetOperand(2, Arm64OperandType.Immediate, out _)) + return false; - // TODO: Replace to use shifted immediate value. - _value += operand.Immediate; - _state = State.LookingForPossibleLdr; - return true; + _value = _value | instruction.GetRawImmediateValue(2); + _state = State.LookingForPossibleLdr; + return true; + } + catch (Exception ex) when (IsDecodingException(ex)) + { + return false; + } } - private void StartMovzSequence(Arm64Instruction instruction) + private bool TryHandleLdrFromTrackedRegister(Arm64Instruction instruction) { - _registerId = instruction.GetRegisterId(0); - _value = instruction.GetRawImmediateValue(1); // TODO: Replace to shifted immediate value. - _expectedMovkShift = 16; - _state = State.ExpectingMovk; - } + try + { + if (!instruction.IsLdrFromTrackedRegister(_registerId)) + return false; - private void StartAdrpSequence(Arm64Instruction instruction) - { - _registerId = instruction.GetRegisterId(0); - _value = instruction.GetRawImmediateValue(1);// TODO: Replace to shifted immediate value. - _state = State.ExpectingAdd; + // 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 = instruction.GetRegisterId(0); + } + + return true; + } + catch (Exception ex) when (IsDecodingException(ex)) + { + return false; + } } - private bool TryHandleLdrFromTrackedRegister(Arm64Instruction instruction) + private bool TryHandleBranchOrRestart(Arm64Instruction instruction) { - if (!instruction.IsLdrFromTrackedRegister(_registerId)) - return false; + Arm64InstructionDetail details = instruction.Details; - // Simulate the LDR instruction. - var newValue = (long)_runtime.DataTarget.DataReader.ReadPointer((ulong)_value); - _value = newValue; - if (_value == 0) + if (instruction.Id == Arm64InstructionId.ARM64_INS_CBZ || + instruction.Id == Arm64InstructionId.ARM64_INS_CBNZ || + instruction.Id == Arm64InstructionId.ARM64_INS_B && details.ConditionCode != Arm64ConditionCode.Invalid) { - // TODO: Call Reset() to clear internal state. - _state = State.LookingForPattern; + // ignore conditional branches return true; } - // The LDR might have loaded the result in another register - _registerId = instruction.GetRegisterId(0); - return true; - } - - private bool TryHandleConditionalInstruction(Arm64Instruction instruction) - { - // TODO: Replace to AsmArm64 based IsConditionalBranch implementation - if (IsConditionalBranch(instruction)) + 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; return true; + } - // TODO: Replace to AsmArm64 based IsUnconditionalControlFlow implementation - if (IsUnconditionalControlFlow(instruction)) + if (instruction.Id == Arm64InstructionId.ARM64_INS_MOVZ) { - // We've encountered an unconditional jump or call, the accumulated registers value is not valid anymore - Reset(); + // Another constant loading is starting: restart matching from LookingForPattern. _state = State.LookingForPattern; + TryStartMovzSequence(instruction); return true; } @@ -207,47 +260,13 @@ private bool TryHandleConditionalInstruction(Arm64Instruction instruction) } /// - /// Handle MOVZ instruction that load value from tracked register to another register. + /// Only decoding failures (missing/mistyped operands, unexpected shift/instruction) + /// are treated as "not matched". Anything else (e.g. OOM) must propagate. /// - private bool TryHandleAnotherMovzInstruction(Arm64Instruction instruction) - { - if (instruction.Id != Arm64InstructionId.ARM64_INS_MOVZ) - return false; - - // Another constant loading is starting, reprocess it as a new pattern. - _state = State.LookingForPattern; - return TryHandleLookingForPattern(instruction); - } - - // TODO: Remove this method and replace to AsmArm64 based implementation. - private static bool IsConditionalBranch(Arm64Instruction instruction) - { - return instruction.Id == Arm64InstructionId.ARM64_INS_CBZ - || instruction.Id == Arm64InstructionId.ARM64_INS_CBNZ - || (instruction.Id == Arm64InstructionId.ARM64_INS_B && instruction.Details.ConditionCode != Arm64ConditionCode.Invalid); - } - - // TODO: Remove this method and replace to AsmArm64 based implementation. - private static bool IsUnconditionalControlFlow(Arm64Instruction instruction) - { - var details = instruction.Details; - return details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE) - || details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_CALL) - || details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP); - } - - private void ResetIfTrackedRegisterIsOverwritten(Arm64Instruction instruction) - { - // 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 writtenRegister in instruction.Details.AllWrittenRegisters) - { - if (writtenRegister.Id != _registerId) - continue; - - // Some unexpected instruction overwriting the accumulated register - // TODO: Use Reset() to clear internal state. - _state = State.LookingForPattern; - } - } + private static bool IsDecodingException(Exception ex) + => ex is ArgumentOutOfRangeException + or ArgumentException + or NotSupportedException + or InvalidOperationException + or UnreachableException; } From d58b4826caa1b35a6f73df917ebffea038b176d1 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:04:44 +0900 Subject: [PATCH 05/14] chore: change Arm64RegisterValueAccumulator address model to ulong with offset --- .../Arm64RegisterValueAccumulator.cs | 72 +++++++++++++++---- ...sassemblerTests.TryGetReferencedAddress.cs | 2 +- ...gisterValueAccumulatorTests.AdrpThenAdd.cs | 7 +- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs index 03208543a7..89408a7532 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs @@ -15,7 +15,8 @@ internal enum State } private State _state; - private long _value; + private ulong _addressValue; + private long _addressOffset; private int _expectedMovkShift; private Arm64RegisterId _registerId; private IClrRuntime _runtime; @@ -29,12 +30,13 @@ public void Init(IClrRuntime runtime) internal void Reset( State state = State.LookingForPattern, int expectedMovkShift = 0, - long value = 0, + ulong value = 0, Arm64RegisterId registerId = Arm64RegisterId.Invalid) { _state = state; _expectedMovkShift = expectedMovkShift; - _value = value; + _addressValue = value; + _addressOffset = 0; _registerId = registerId; } @@ -80,9 +82,39 @@ public void Feed(Arm64Instruction instruction) } } - public bool HasValue => _state == State.ExpectingMovk || _state == State.LookingForPossibleLdr; + public bool HasValue + { + get + { + // Value == 0 means invalid (negative combined clamps to 0, LDR resets on 0), + // So it is excluded to avoid a useless address lookup in the disassembler. + if (Value == 0) + return false; + + return _state == State.ExpectingMovk || _state == State.LookingForPossibleLdr; + } + } - public long Value => _value; + /// + /// Gets accumulated address value. + /// Address arithmetic wraps modulo 2^64, like hardware. + /// A negative combined value is invalid as a real address, so 0 is returned + /// (0 is already treated as invalid: LDR resets on 0, disassembler filters it). + /// + public ulong Value + { + get + { + if (_addressOffset < 0) + { + // Magnitude of the negative offset, safe even for long.MinValue. + ulong magnitude = unchecked((ulong)(-(_addressOffset + 1)) + 1); + if (_addressValue < magnitude) + return 0; + } + return unchecked(_addressValue + (ulong)_addressOffset); + } + } public Arm64RegisterId RegisterId => _registerId; @@ -110,7 +142,8 @@ private bool TryStartMovzSequence(Arm64Instruction instruction) return false; _registerId = instruction.GetRegisterId(0); - _value = instruction.GetRawImmediateValue(1); + _addressValue = (ulong)instruction.GetRawImmediateValue(1); + _addressOffset = 0; _state = State.ExpectingMovk; _expectedMovkShift = 16; return true; @@ -132,7 +165,8 @@ private bool TryStartAdrpSequence(Arm64Instruction instruction) return false; _registerId = instruction.GetRegisterId(0); - _value = instruction.GetRawImmediateValue(1); + _addressOffset = instruction.GetShiftedImmediateValue(1); + _addressValue = 0; _state = State.ExpectingAdd; return true; } @@ -157,7 +191,7 @@ private bool TryHandleExpectingMovk(Arm64Instruction instruction) || details.Operands[1].ShiftValue != _expectedMovkShift) return false; - _value = _value | (instruction.GetRawImmediateValue(1) << details.Operands[1].ShiftValue); + _addressValue = _addressValue | (ulong)instruction.GetRawImmediateValue(1) << details.Operands[1].ShiftValue; _expectedMovkShift += 16; return true; } @@ -189,7 +223,7 @@ private bool TryHandleExpectingAdd(Arm64Instruction instruction) if (!instruction.TryGetOperand(2, Arm64OperandType.Immediate, out _)) return false; - _value = _value | instruction.GetRawImmediateValue(2); + _addressValue = _addressValue | (ulong)instruction.GetRawImmediateValue(2); _state = State.LookingForPossibleLdr; return true; } @@ -207,9 +241,23 @@ private bool TryHandleLdrFromTrackedRegister(Arm64Instruction instruction) return false; // Simulate the LDR instruction. - long newValue = (long)_runtime.DataTarget.DataReader.ReadPointer((ulong)_value); - _value = newValue; - if (_value == 0) + // Use combined Value: after ADRP+ADD the effective address is _addressValue + _addressOffset. + // Use the Try-pattern so an unreadable address drops tracking instead of aborting disassembly. + if (Value == 0) + { + _state = State.LookingForPattern; + return true; + } + + if (!_runtime.DataTarget.DataReader.ReadPointer(Value, out ulong dereferenced)) + { + _state = State.LookingForPattern; + return true; + } + + _addressValue = dereferenced; + _addressOffset = 0; + if (_addressValue == 0) { _state = State.LookingForPattern; } diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs index 19e5553353..8ffa5cfe93 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs @@ -13,7 +13,7 @@ public partial class Arm64DisassemblerTests { private Arm64RegisterValueAccumulator CreateValueAccumulator( IClrRuntime clrRuntime, - long initialValue, + ulong initialValue, Arm64RegisterId registerId = Arm64RegisterId.ARM64_REG_X0, Arm64RegisterValueAccumulator.State state = Arm64RegisterValueAccumulator.State.ExpectingMovk) { diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs index 986320cad4..9bfeb01b38 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs @@ -32,7 +32,7 @@ public void AdrpThenAdd_ShouldCalculateAddress() } [Fact] - public void AdrpThenAdd_WithNegativeOffset_ShouldCalculateAddress() + public void AdrpThenAdd_WithNegativeOffset_ShouldClampToZero() { // Arrange var accumulator = CreateValueAccumulator(); @@ -48,8 +48,9 @@ public void AdrpThenAdd_WithNegativeOffset_ShouldCalculateAddress() accumulator.Feed(instructions[1]); // Assert - accumulator.HasValue.Should().BeTrue(); - accumulator.Value.Should().Be(-0x1000 + 0x100); + // A negative combined value is invalid as a real address, so it is clamped to 0. + accumulator.HasValue.Should().BeFalse(); + accumulator.Value.Should().Be(0UL); } [Fact] From fc4bfd7acbe39f48be971910a7f3b97fb3b4f2ba Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:05:59 +0900 Subject: [PATCH 06/14] chore: use lane-clear MOVK accumulation without shift order enforcement --- .../Arm64RegisterValueAccumulator.cs | 22 +++++++++---------- ...isterValueAccumulatorTests.MovzThenMovk.cs | 10 +++++---- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs index 89408a7532..5812c1614f 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs @@ -17,7 +17,6 @@ internal enum State private State _state; private ulong _addressValue; private long _addressOffset; - private int _expectedMovkShift; private Arm64RegisterId _registerId; private IClrRuntime _runtime; @@ -29,12 +28,10 @@ public void Init(IClrRuntime runtime) internal void Reset( State state = State.LookingForPattern, - int expectedMovkShift = 0, ulong value = 0, Arm64RegisterId registerId = Arm64RegisterId.Invalid) { _state = state; - _expectedMovkShift = expectedMovkShift; _addressValue = value; _addressOffset = 0; _registerId = registerId; @@ -142,10 +139,9 @@ private bool TryStartMovzSequence(Arm64Instruction instruction) return false; _registerId = instruction.GetRegisterId(0); - _addressValue = (ulong)instruction.GetRawImmediateValue(1); + _addressValue = instruction.GetUnsignedShiftedImmediateValue(1); _addressOffset = 0; _state = State.ExpectingMovk; - _expectedMovkShift = 16; return true; } catch (Exception ex) when (IsDecodingException(ex)) @@ -186,13 +182,16 @@ private bool TryHandleExpectingMovk(Arm64Instruction instruction) if (instruction.GetRegisterId(0) != _registerId) return false; - Arm64InstructionDetail details = instruction.Details; - if (details.Operands[1].ShiftOperation != Arm64ShiftOperation.ARM64_SFT_LSL - || details.Operands[1].ShiftValue != _expectedMovkShift) + // Note: MOVK instruction accept only LSL shift operation. + var shiftAmount = instruction.GetShiftAmount(1); + if (shiftAmount is not (0 or 16 or 32 or 48)) return false; - _addressValue = _addressValue | (ulong)instruction.GetRawImmediateValue(1) << details.Operands[1].ShiftValue; - _expectedMovkShift += 16; + var shiftedImmediateValue = instruction.GetUnsignedShiftedImmediateValue(1); + const ulong mask = 0xFFFF; + _addressValue &= ~(mask << shiftAmount); // Clear 16-bits of MOVK target + _addressValue |= shiftedImmediateValue; // Set shifted immediate value + return true; } catch (Exception ex) when (IsDecodingException(ex)) @@ -223,7 +222,8 @@ private bool TryHandleExpectingAdd(Arm64Instruction instruction) if (!instruction.TryGetOperand(2, Arm64OperandType.Immediate, out _)) return false; - _addressValue = _addressValue | (ulong)instruction.GetRawImmediateValue(2); + var shiftedImmediateValue = instruction.GetUnsignedShiftedImmediateValue(2); + _addressValue += shiftedImmediateValue; _state = State.LookingForPossibleLdr; return true; } diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs index d4fc160566..aeaf75f892 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs @@ -49,7 +49,7 @@ public void MovzThenMovk_ShouldHaveValue() accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); } - [Fact(Skip = "Current value accumulator expecting sequencial 0/16/32/48 shfit for MOVK.")] + [Fact] public void MovzThenMovk_WithSameShiftValue_ShouldHasAccumulatedValue() { // Arrange @@ -66,7 +66,7 @@ public void MovzThenMovk_WithSameShiftValue_ShouldHasAccumulatedValue() } [Fact] - public void MovzThenMovk_WithUnexpectedShift_ShouldResetValue() + public void MovzThenMovk_WithSkippedShift_ShouldAccumulateValue() { // Arrange var accumulator = CreateValueAccumulator(); @@ -78,10 +78,12 @@ public void MovzThenMovk_WithUnexpectedShift_ShouldResetValue() // Act accumulator.Feed(instructions[0]); - accumulator.Feed(instructions[1]); // Reset accumulated value, because _expectedMovkShift is not matched. + accumulator.Feed(instructions[1]); // Each MOVK clears and sets its own 16-bit lane, so order does not matter. // Assert - accumulator.HasValue.Should().BeFalse(); + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x5678_0000_1234UL); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); } [Fact] From a3ffec8c3e6ec0844eef1d41e190e908b6356e5e Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:07:47 +0900 Subject: [PATCH 07/14] chore: broaden Arm64RegisterValueAccumulator tracking continuation --- .../Arm64RegisterValueAccumulator.cs | 86 ++++++++++++------- ...gisterValueAccumulatorTests.AdrpThenAdd.cs | 7 +- ...ulatorTests.MovzThenUnConditionalBranch.cs | 11 +-- 3 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs index 5812c1614f..a55d8198ba 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs @@ -56,26 +56,32 @@ public void Feed(Arm64Instruction instruction) case State.ExpectingAdd: if (TryHandleExpectingAdd(instruction)) return; - break; + + goto default; case State.LookingForPossibleLdr: if (TryHandleLdrFromTrackedRegister(instruction)) return; - if (TryHandleBranchOrRestart(instruction)) + // Try handle MOVZ/ADRP for new constand load. + if (TryHandleLookingForPattern(instruction)) + return; + + goto default; + + default: + if (TryHandleConditionalInstruction(instruction)) return; - // 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 instruction.Details.AllWrittenRegisters) + // Reset accumulated state if instruction overwriting the accumulated register. + if (IsTrackedRegisterOverwritten(instruction)) { - // Some unexpected instruction overwriting the accumulated register - if (reg.Id == _registerId) - { - _state = State.LookingForPattern; - } + Reset(); + + // Reprocess current instruction. + TryHandleLookingForPattern(instruction); } - break; + return; } } @@ -88,7 +94,15 @@ public bool HasValue if (Value == 0) return false; - return _state == State.ExpectingMovk || _state == State.LookingForPossibleLdr; + switch (_state) + { + case State.ExpectingMovk: + case State.LookingForPossibleLdr: + case State.ExpectingAdd: + return true; + default: + return false; + } } } @@ -275,36 +289,46 @@ private bool TryHandleLdrFromTrackedRegister(Arm64Instruction instruction) } } - private bool TryHandleBranchOrRestart(Arm64Instruction instruction) + private bool TryHandleConditionalInstruction(Arm64Instruction instruction) { - Arm64InstructionDetail details = instruction.Details; + if (instruction.IsConditionalBranch()) + return true; - if (instruction.Id == Arm64InstructionId.ARM64_INS_CBZ || - instruction.Id == Arm64InstructionId.ARM64_INS_CBNZ || - instruction.Id == Arm64InstructionId.ARM64_INS_B && details.ConditionCode != Arm64ConditionCode.Invalid) + if (instruction.IsUnconditionalControlFlow()) { - // ignore conditional branches + Reset(); return true; } - if (details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE) || - details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_CALL) || - details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP)) + return false; + } + + private bool IsTrackedRegisterOverwritten(Arm64Instruction instruction) + { + try { - // We've encountered an unconditional jump or call, the accumulated registers value is not valid anymore - _state = State.LookingForPattern; - return true; - } + // Finally check if the current instruction modified the register that was accumulating the constant + // and reset the state machine in case it did. + var details = instruction.Details; + if (details is null) + return true; // Conservative: unknown writes invalidate tracking (avoid false HasValue). + + foreach (Arm64Register writtenRegister in details.AllWrittenRegisters) + { + if (writtenRegister.Id != _registerId) + continue; - if (instruction.Id == Arm64InstructionId.ARM64_INS_MOVZ) + return true; + } + + return false; + } + catch (Exception ex) when (IsDecodingException(ex) || ex is NullReferenceException) { - // Another constant loading is starting: restart matching from LookingForPattern. - _state = State.LookingForPattern; - TryStartMovzSequence(instruction); + // Conservative: on decoding failure assume overwritten so a stale value + // is not reported as a false-positive ReferencedAddress. return true; } - - return false; } /// diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs index 9bfeb01b38..10a5208350 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs @@ -96,7 +96,7 @@ public void AdrpThenAdd_WithShiftedImm_ShouldCalculateAddress() } [Fact] - public void AdrpThenAdd_DifferentDestinationRegister_ShouldResetValue() + public void AdrpThenAdd_DifferentDestinationRegister_ShouldKeepValue() { // Arrange var accumulator = CreateValueAccumulator(); @@ -112,7 +112,10 @@ public void AdrpThenAdd_DifferentDestinationRegister_ShouldResetValue() accumulator.Feed(instructions[1]); // Assert - accumulator.HasValue.Should().BeFalse(); // Register is not matched + // The ADD writes X1, so the tracked X0 value (page address) is still valid. + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x2000UL); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); } [Fact(Skip = "On current implementation, `State.ExpectingAdd` don't reset value when unexpected instruction passed.")] diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs index 06e0e47bbb..922deba92d 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs @@ -34,7 +34,7 @@ public void MovzThenUnconditionalB_ShouldResetValue() [Fact] - public void MovzThenTbz_ShouldResetValue() + public void MovzThenTbz_ShouldKeepValue() { // Arrange var accumulator = CreateValueAccumulator(); @@ -50,10 +50,11 @@ public void MovzThenTbz_ShouldResetValue() accumulator.Feed(instructions[1]); // Assert - accumulator.HasValue.Should().BeFalse(); - - // TODO: Current accumulator don't reset registerId. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + // TBZ is a conditional branch that does not overwrite the tracked register, + // so tracking continues (unlike unconditional control flow which resets). + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x1000UL); + accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); var details = instructions[1].Details; details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP).Should().BeTrue(); From 4ca01e2691f68391deee14d93c74a23218a949b8 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:11:49 +0900 Subject: [PATCH 08/14] chore: align Arm64RegisterValueAccumulator internals with final shape --- .../Arm64RegisterValueAccumulator.cs | 146 ++++++++++-------- ...ulatorTests.MovzThenUnConditionalBranch.cs | 6 +- 2 files changed, 81 insertions(+), 71 deletions(-) diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs index a55d8198ba..de92586af5 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs @@ -50,6 +50,9 @@ public void Feed(Arm64Instruction instruction) if (TryHandleExpectingMovk(instruction)) return; + // MOVK instructions are expected to be placed after MOVZ instruction. + // If other instruction found on ExpectingMovk state. + // Then state is transitioned to LookingForPossibleLdr. _state = State.LookingForPossibleLdr; goto case State.LookingForPossibleLdr; @@ -89,7 +92,7 @@ public bool HasValue { get { - // Value == 0 means invalid (negative combined clamps to 0, LDR resets on 0), + // Value == 0 means invalid(negative combined clamps to 0, LDR resets on 0), // So it is excluded to avoid a useless address lookup in the disassembler. if (Value == 0) return false; @@ -142,50 +145,6 @@ private bool TryHandleLookingForPattern(Arm64Instruction instruction) } } - private bool TryStartMovzSequence(Arm64Instruction instruction) - { - try - { - if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _)) - return false; - - if (!instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) - return false; - - _registerId = instruction.GetRegisterId(0); - _addressValue = instruction.GetUnsignedShiftedImmediateValue(1); - _addressOffset = 0; - _state = State.ExpectingMovk; - return true; - } - catch (Exception ex) when (IsDecodingException(ex)) - { - return false; - } - } - - private bool TryStartAdrpSequence(Arm64Instruction instruction) - { - try - { - if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _)) - return false; - - if (!instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) - return false; - - _registerId = instruction.GetRegisterId(0); - _addressOffset = instruction.GetShiftedImmediateValue(1); - _addressValue = 0; - _state = State.ExpectingAdd; - return true; - } - catch (Exception ex) when (IsDecodingException(ex)) - { - return false; - } - } - private bool TryHandleExpectingMovk(Arm64Instruction instruction) { try @@ -193,6 +152,10 @@ private bool TryHandleExpectingMovk(Arm64Instruction instruction) if (instruction.Id != Arm64InstructionId.ARM64_INS_MOVK) return false; + if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _) + || !instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) + return false; + if (instruction.GetRegisterId(0) != _registerId) return false; @@ -227,6 +190,10 @@ private bool TryHandleExpectingAdd(Arm64Instruction instruction) if (instruction.Id != Arm64InstructionId.ARM64_INS_ADD) return false; + if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _) + || !instruction.TryGetOperand(1, Arm64OperandType.Register, out _)) + return false; + if (instruction.GetRegisterId(0) != _registerId) return false; @@ -247,6 +214,59 @@ private bool TryHandleExpectingAdd(Arm64Instruction instruction) } } + /// + /// Only decoding failures (missing/mistyped operands, unexpected shift/instruction) + /// are treated as "not matched". Anything else (e.g. OOM) must propagate. + /// + private static bool IsDecodingException(Exception ex) + => ex is ArgumentOutOfRangeException + or ArgumentException + or NotSupportedException + or InvalidOperationException + or UnreachableException; + + private bool TryStartMovzSequence(Arm64Instruction instruction) + { + if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _)) + return false; + + if (!instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) + return false; + + try + { + _registerId = instruction.GetRegisterId(0); + _addressValue = instruction.GetUnsignedShiftedImmediateValue(1); + _addressOffset = 0; + _state = State.ExpectingMovk; + return true; + } + catch (Exception ex) when (IsDecodingException(ex)) + { + return false; + } + } + + private bool TryStartAdrpSequence(Arm64Instruction instruction) + { + if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _) + || !instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) + return false; + + try + { + _registerId = instruction.GetRegisterId(0); + _addressOffset = instruction.GetShiftedImmediateValue(1); + _addressValue = 0; + _state = State.ExpectingAdd; + return true; + } + catch (Exception ex) when (IsDecodingException(ex)) + { + return false; + } + } + private bool TryHandleLdrFromTrackedRegister(Arm64Instruction instruction) { try @@ -254,33 +274,36 @@ private bool TryHandleLdrFromTrackedRegister(Arm64Instruction instruction) if (!instruction.IsLdrFromTrackedRegister(_registerId)) return false; - // Simulate the LDR instruction. - // Use combined Value: after ADRP+ADD the effective address is _addressValue + _addressOffset. - // Use the Try-pattern so an unreadable address drops tracking instead of aborting disassembly. + if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _)) + return false; + + // Address 0 is never valid to dereference (e.g. `movz x0, #0` or clamped negative ADRP+ADD). if (Value == 0) { - _state = State.LookingForPattern; + Reset(); return true; } + // Simulate the LDR instruction. + // Use combined Value: after ADRP+ADD the effective address is _addressValue + _addressOffset. + // Use the Try-pattern so an unreadable address drops tracking instead of aborting disassembly. if (!_runtime.DataTarget.DataReader.ReadPointer(Value, out ulong dereferenced)) { - _state = State.LookingForPattern; + Reset(); return true; } _addressValue = dereferenced; _addressOffset = 0; + if (_addressValue == 0) { - _state = State.LookingForPattern; - } - else - { - // The LDR might have loaded the result in another register - _registerId = instruction.GetRegisterId(0); + Reset(); + return true; } + // The LDR might have loaded the result in another register + _registerId = instruction.GetRegisterId(0); return true; } catch (Exception ex) when (IsDecodingException(ex)) @@ -330,15 +353,4 @@ private bool IsTrackedRegisterOverwritten(Arm64Instruction instruction) return true; } } - - /// - /// Only decoding failures (missing/mistyped operands, unexpected shift/instruction) - /// are treated as "not matched". Anything else (e.g. OOM) must propagate. - /// - private static bool IsDecodingException(Exception ex) - => ex is ArgumentOutOfRangeException - or ArgumentException - or NotSupportedException - or InvalidOperationException - or UnreachableException; } diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs index 922deba92d..96def412b0 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs @@ -45,15 +45,13 @@ public void MovzThenTbz_ShouldKeepValue() }; PrintInstructions(instructions); - // Act + // Act accumulator.Feed(instructions[0]); accumulator.Feed(instructions[1]); // Assert - // TBZ is a conditional branch that does not overwrite the tracked register, - // so tracking continues (unlike unconditional control flow which resets). accumulator.HasValue.Should().BeTrue(); - accumulator.Value.Should().Be(0x1000UL); + accumulator.Value.Should().Be(0x1000UL); // TBZ is a conditional branch that does not overwrite the tracked register, accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); var details = instructions[1].Details; From 100d68e0abf5cf6d76321386820b0af63b8de1b5 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Sat, 17 Oct 2026 06:29:23 +0900 Subject: [PATCH 09/14] chore: add/update related unit tests --- ...sassemblerTests.TryGetReferencedAddress.cs | 24 ++++++++++++++++ ...gisterValueAccumulatorTests.AdrpThenAdd.cs | 4 +-- ...Arm64RegisterValueAccumulatorTests.Movz.cs | 21 +++++++++++++- ...gisterValueAccumulatorTests.MovzThenLdr.cs | 28 +++++++++++++++++-- ...ulatorTests.MovzThenUnConditionalBranch.cs | 4 +-- 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs index 8ffa5cfe93..006710b423 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs @@ -72,6 +72,30 @@ public void TryGetReferencedAddress_With_BLR() isReferencedAddressIndirect.Should().BeTrue(); } + [Fact] + public void TryGetReferencedAddress_WithAdrpThenBr_ResolvesPageAddress() + { + // Arrange + using var clrRuntime = CreateMockClrRuntime(); + var accumulator = new Arm64RegisterValueAccumulator(); + accumulator.Init(clrRuntime); + accumulator.Feed(Arm64TestInstructions.Adrp(X0, 0x2000)); // adrp x0, #0x2000 + var rawInstruction = Arm64InstructionFactory.BR(X0); + + // Act + var result = Arm64DisassemblerHelper.TryGetReferencedAddress( + rawInstruction.ToCapstoneArm64Instruction(), + accumulator, + pointerSize: 0, // This parameter is not used. + out ulong referencedAddress, + out bool isReferencedAddressIndirect); + + // Assert + result.Should().BeTrue(); + referencedAddress.Should().Be(0x2000); // no ADD between ADRP and BR, so the page address itself must resolve. + isReferencedAddressIndirect.Should().BeTrue(); + } + [Fact] public void TryGetReferencedAddress_With_BranchRelative() { diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs index 10a5208350..863074c770 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs @@ -74,7 +74,7 @@ public void AdrpThenAdd_ThenAdd_ShouldResetValue() accumulator.HasValue.Should().BeFalse(); } - [Fact(Skip = "Shifted value is not supported on current implementation.")] + [Fact] public void AdrpThenAdd_WithShiftedImm_ShouldCalculateAddress() { // Arrange @@ -118,7 +118,7 @@ public void AdrpThenAdd_DifferentDestinationRegister_ShouldKeepValue() accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); } - [Fact(Skip = "On current implementation, `State.ExpectingAdd` don't reset value when unexpected instruction passed.")] + [Fact] public void AdrpThenOther_ThenAdd_ShouldResetValue() { // Arrange diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs index eafb2c12bb..913c8e064c 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs @@ -9,7 +9,7 @@ namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; // MOVZ tests. public partial class Arm64RegisterValueAccumulatorTests { - [Fact(Skip = "On current implementation, shifted immediate value is not supported.")] + [Fact] public void Movz_WithShiftedImmediateValue_ShouldStartNewValue() { // Arrange @@ -32,5 +32,24 @@ public void Movz_WithShiftedImmediateValue_ShouldStartNewValue() accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); accumulator.Value.Should().Be(0x3333_0000_0000); } + + [Fact] + public void Movz_ZeroImmediate_ShouldNotHaveValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, 0x0), // movz x0, #0x0 + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + + // Assert: address 0 is never valid, so it is excluded from HasValue. + accumulator.HasValue.Should().BeFalse(); + accumulator.Value.Should().Be(0UL); + } } #endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs index 30684ebdad..d463a18cd0 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs @@ -1,10 +1,8 @@ // TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. #if NET -using Argon; -using AsmArm64; using AwesomeAssertions; using Gee.External.Capstone.Arm64; -using System.Net; +using System.Reflection; using static AsmArm64.Arm64RegisterX; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -159,5 +157,29 @@ public void MovzThenLdr_ZeroAddress_ShouldNotHaveValue() // Assert accumulator.HasValue.Should().BeFalse(); // When LDR load zero. Accumulated state is expected to be resetted. } + + [Fact] + public void MovzThenLdr_UnreadableAddress_ShouldNotHaveValue() + { + // Arrange + using var clrRuntime = new MockMemory() + .AddFalsePointer(MovzOffset) + .ToMockClrRuntime(); + var accumulator = CreateValueAccumulator(clrRuntime); + var instructions = new[] + { + Arm64TestInstructions.Movz(X0, MovzOffset), // movz x0, #0x1000 + Arm64TestInstructions.Ldr(X1, baseRegister: X0), // ldr x1, [x0] + }; + PrintInstructions(instructions); + + // Act + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); // No readable pointer is registered, so ReadPointer fails. + + // Assert + accumulator.HasValue.Should().BeFalse(); // Unreadable address drops tracking instead of aborting disassembly. + accumulator.Value.Should().Be(0UL); + } } #endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs index 96def412b0..3da52bbb4e 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs @@ -59,7 +59,7 @@ public void MovzThenTbz_ShouldKeepValue() details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE).Should().BeTrue(); } - [Fact(Skip = "Capstone 4.0.2 don't grouping RET instruction as JUMP group.")] + [Fact] public void MovzThenRet_ShouldResetValue() { // Arrange @@ -80,7 +80,7 @@ public void MovzThenRet_ShouldResetValue() // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); } - [Fact(Skip = "Capstone 4.0.2 don't grouping DRPS instruction as JUMP group.")] + [Fact] public void MovzThenDrps_ShouldResetValue() { // Arrange From 5f4f19a801c56e9480ecd12ca292d14d284b85d3 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Wed, 4 Nov 2026 03:13:12 +0900 Subject: [PATCH 10/14] chore: refactor Arm64InstructionExtensions to use AsmArm64 --- .../Arm64InstructionExtensions.cs | 422 +++++++++--------- .../Disassemblers/Arm64InstructionGroupId.cs | 21 + 2 files changed, 225 insertions(+), 218 deletions(-) create mode 100644 src/BenchmarkDotNet/Disassemblers/Arm64InstructionGroupId.cs diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs index f5e8804401..0e026edfc9 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs @@ -1,174 +1,141 @@ using AsmArm64; -using Gee.External.Capstone.Arm64; -using System.Buffers.Binary; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using AsmArm64Instruction = AsmArm64.Arm64Instruction; -using AsmArm64Operand = AsmArm64.Arm64Operand; -using Arm64InstructionId = AsmArm64.Arm64InstructionId; -using CapstoneArm64Instruction = Gee.External.Capstone.Arm64.Arm64Instruction; -using CapstoneArm64InstructionId = Gee.External.Capstone.Arm64.Arm64InstructionId; -using CapstoneArm64Operand = Gee.External.Capstone.Arm64.Arm64Operand; -using CapstoneArm64OperandType = Gee.External.Capstone.Arm64.Arm64OperandType; -using CapstoneArm64RegisterId = Gee.External.Capstone.Arm64.Arm64RegisterId; namespace BenchmarkDotNet.Disassemblers; internal static class Arm64InstructionExtensions { - #region Extension methods for Capstone - extension(CapstoneArm64Instruction instruction) + private static bool TryGetOperandCore(Arm64Instruction instruction, int index, Arm64OperandKind expectedKind, Func castFunc, out T result) + where T : struct, IArm64Operand { - public CapstoneArm64RegisterId GetRegisterId(int index) + if ((uint) index >= (uint) instruction.Operands.Count) { - var operands = instruction.Details.Operands; - if ((uint)index >= (uint)operands.Length) - throw new ArgumentOutOfRangeException(nameof(index)); - - var operand = operands[index]; - if (operand.Type != CapstoneArm64OperandType.Register) - throw new ArgumentException($"Operand at index {index} is not a register (actual: {operand.Type})."); - - return operand.Register.Id; + result = default; + return false; } - public Arm64ShiftOperation GetShiftOperation(int index) - { - var operands = instruction.Details.Operands; - if ((uint)index >= (uint)operands.Length) - throw new ArgumentOutOfRangeException(nameof(index)); - - var operand = operands[index]; - var shiftOperation = operand.ShiftOperation; - - switch (instruction.Id) - { - case CapstoneArm64InstructionId.ARM64_INS_ADD: - case CapstoneArm64InstructionId.ARM64_INS_ADRP: - case CapstoneArm64InstructionId.ARM64_INS_MOVK: - case CapstoneArm64InstructionId.ARM64_INS_MOVZ: - switch (shiftOperation) - { - case Arm64ShiftOperation.ARM64_SFT_LSL: - return shiftOperation; - - // Capstone represents `LSL #0` as ARM64_SFT_INVALID. - // So handle ARM64_SFT_INVALID as `LSL` for these instructions. (Other shift operation is not used currently) - case Arm64ShiftOperation.Invalid: - return Arm64ShiftOperation.ARM64_SFT_LSL; - - // Currently used instructions don't accept other shift operations. - default: - throw new NotSupportedException($"Unexpected shift operation({shiftOperation}) is specified on instruction({instruction.Id})"); - } + var operand = instruction.GetOperand(index); - // Currently other instructions are not supported. - default: - throw new UnreachableException($"Unexpected instruction({instruction.Id}) is specified"); - } + if (operand.Kind != expectedKind) + { + result = default; + return false; } - public int GetShiftAmount(int index) - { - var operands = instruction.Details.Operands; - if ((uint)index >= (uint)operands.Length) - throw new ArgumentOutOfRangeException(nameof(index)); + result = castFunc(operand); + return true; + } - var operand = operands[index]; + // Define TryGetOperand helper extension method per operand types. + // It's required because when using generics, it can't resolve cast (expicit operator). + extension(Arm64Instruction instruction) + { + public bool TryGetOperand(int index, out Arm64RegisterOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Register, x => (Arm64RegisterOperand) x, out result); - // Capstone represents LSL #0 as ARM64_SFT_INVALID. - if (operand.ShiftOperation == Arm64ShiftOperation.Invalid) - return 0; + public bool TryGetOperand(int index, out Arm64ImmediateOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Immediate, x => (Arm64ImmediateOperand) x, out result); - return operand.ShiftValue; - } + public bool TryGetOperand(int index, out Arm64LabelOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Label, x => (Arm64LabelOperand) x, out result); - public long GetRawImmediateValue(int index) - { - var operands = instruction.Details.Operands; - if ((uint)index >= (uint)operands.Length) - throw new ArgumentOutOfRangeException(nameof(index)); + public bool TryGetOperand(int index, out Arm64ShiftOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Shift, x => (Arm64ShiftOperand) x, out result); - return operands[index].Immediate; - } + public bool TryGetOperand(int index, out Arm64EnumOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Enum, x => (Arm64EnumOperand) x, out result); - /// - /// Gets shifted immediate value as signed value (e.g. ADRP page offset can be negative). - /// - public long GetShiftedImmediateValue(int index) - => unchecked((long)instruction.GetUnsignedShiftedImmediateValue(index)); + public bool TryGetOperand(int index, out Arm64MemoryOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Memory, x => (Arm64MemoryOperand) x, out result); + } + extension(Arm64Instruction instruction) + { /// - /// Gets shifted immediate value as unsigned value (e.g. MOVZ/MOVK/ADD address construction). - /// Shift is performed in ulong to avoid signed overflow - /// (e.g. movz x0, #0xFFFF, lsl #48 exceeds long.MaxValue) + /// Helper method to get immediate value as `ulong`. + /// arm64 immediate value is expected to be represented as signed long. + /// Note: + /// Some SIMD/SVE/SME instruction might have 64-bit immediate value, FP immediate value, or bitmask. + /// In this case, it needs to use separated API (Currently there is no code path that handles these immediate values). /// - public ulong GetUnsignedShiftedImmediateValue(int index) + public bool TryGetShiftedImmediateValueUnsigned(int index, out ulong value) { - var operands = instruction.Details.Operands; - if ((uint)index >= (uint)operands.Length) - throw new ArgumentOutOfRangeException(nameof(index)); - - var operand = operands[index]; - - var shiftOperation = instruction.GetShiftOperation(index); - switch (shiftOperation) + if (instruction.TryGetShiftedImmediateValue(index, out long signed)) { - case Arm64ShiftOperation.ARM64_SFT_LSL: - var shiftAmount = instruction.GetShiftAmount(index); - return unchecked((ulong)operand.Immediate << shiftAmount); - default: - throw new NotSupportedException($"Unexpected shift operation({shiftOperation}) is specified on instruction({instruction.Id})"); + value = (ulong) signed; + return true; } + + value = 0; + return false; } - public bool TryGetOperand( - int index, - CapstoneArm64OperandType type, - [NotNullWhen(true)] out CapstoneArm64Operand? result) + public bool TryGetShiftedImmediateValue(int index, out long value) { - var operands = instruction.Details.Operands; - if ((uint)index >= (uint)operands.Length) - { - result = null; + value = 0; + var operands = instruction.Operands; + if ((uint) index >= (uint) operands.Count) return false; - } - result = operands[index]; - if (result.Type == type) + if (!instruction.TryGetOperand(index, out Arm64ImmediateOperand immediateOperand)) + return false; + + var immediateValue = immediateOperand.Value; + + // Try to get shift modifier (AsmArm64 handle shift modifier as independent operand) + if (!instruction.TryGetOperand(index + 1, out Arm64ShiftOperand shiftOperand)) + { + value = immediateValue; return true; + } - result = null; - return false; + var shiftAmount = shiftOperand.Amount; + var shiftKind = shiftOperand.ShiftKind; + return TryShiftImmediateValue(immediateValue, shiftKind, shiftAmount, out value); } - public bool BelongsToGroup(Arm64InstructionGroupId expectedGroup) + /// + /// Gets the base address used to resolve a label operand. + /// ADRP encodes a page delta relative to the 4KB page containing the instruction, + /// while other label operands are relative to the instruction address. + /// (Matches AsmArm64's Arm64Disassembler.GetTargetAddress.) + /// + public ulong GetTargetAddressBase(ulong instructionAddress) { - if (instruction.Bytes.Length < 4) - return false; - var rawInstruction = BinaryPrimitives.ReadUInt32LittleEndian(instruction.Bytes); - return AsmArm64Instruction.Decode(rawInstruction).BelongsToGroup(expectedGroup); + if (instruction.Mnemonic == Arm64Mnemonic.ADRP) + return instructionAddress & ~0xFFFUL; + return instructionAddress; + } + + /// + /// Resolves the absolute target address of a label operand. + /// Address arithmetic wraps modulo 2^64, like hardware. + /// + public ulong GetTargetAddress(ulong instructionAddress, long labelOffset) + { + var targetAddress = instruction.GetTargetAddressBase(instructionAddress); + return unchecked(targetAddress + (ulong) labelOffset); } - public bool IsLdrFromTrackedRegister(CapstoneArm64RegisterId trackedRegister) + public bool IsLdrFromTrackedRegister(Arm64RegisterAny trackedRegister) { - if (instruction.Id != CapstoneArm64InstructionId.ARM64_INS_LDR) + if (instruction.Mnemonic != Arm64Mnemonic.LDR) return false; - if (!instruction.TryGetOperand(1, CapstoneArm64OperandType.Memory, out var memoryOperand)) + if (!instruction.TryGetOperand(1, out Arm64MemoryOperand memoryOperand)) return false; + var memoryAccessor = memoryOperand.Accessor; + // Check base register is the register that is currently tracked. - var memory = memoryOperand.Memory; - if (memory.Base.Id != trackedRegister) + if (memoryAccessor.BaseRegister != trackedRegister) return false; // Check displacement. - if (memory.Displacement != 0) + if (memoryAccessor.HasOptionalOffset && memoryAccessor.HasImmediate && memoryAccessor.Immediate != 0) return false; // Check index register is specified. - if (memory.Index != null) + if (memoryAccessor.HasIndexRegister) return false; // Ldr from tracked register (with no displacement/index register). @@ -177,20 +144,25 @@ public bool IsLdrFromTrackedRegister(CapstoneArm64RegisterId trackedRegister) public bool IsConditionalBranch() { - switch (instruction.Id) + switch (instruction.Mnemonic) { - case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_CBZ: - case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_CBNZ: - case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_TBZ: - case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_TBNZ: + case Arm64Mnemonic.CBZ: + case Arm64Mnemonic.CBNZ: + case Arm64Mnemonic.TBZ: + case Arm64Mnemonic.TBNZ: return true; - case Gee.External.Capstone.Arm64.Arm64InstructionId.ARM64_INS_B: - switch (instruction.Details.ConditionCode) + case Arm64Mnemonic.B: + + var conditionalKind = Arm64ConditionalKind.Undefined; + if (instruction.TryGetOperand(0, out Arm64EnumOperand enumOperand)) + conditionalKind = enumOperand.AsConditional; + + switch (conditionalKind) { // `B` and `B.AL` instructions is unconditional branch. - case Gee.External.Capstone.Arm64.Arm64ConditionCode.Invalid: - case Gee.External.Capstone.Arm64.Arm64ConditionCode.ARM64_CC_AL: + case Arm64ConditionalKind.Undefined: + case Arm64ConditionalKind.AL: return false; default: return true; @@ -208,117 +180,89 @@ public bool IsUnconditionalControlFlow() || instruction.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP); } - } - #endregion - - #region Extension methods for AsmArm64 - extension(AsmArm64Instruction instruction) - { - public Arm64RegisterOperand GetRegisterOperand(int index) - => (Arm64RegisterOperand)instruction.GetOperand(index, Arm64OperandKind.Register); - - public Arm64ImmediateOperand GetImmediateOperand(int index) - => (Arm64ImmediateOperand)instruction.GetOperand(index, Arm64OperandKind.Immediate); - - private AsmArm64Operand GetOperand(int index, Arm64OperandKind expectedKind) - { - var operand = instruction.GetOperand(index); - if (operand.Kind != expectedKind) - throw new ArgumentException($"The operand at index {index} is not of the expected kind {expectedKind}. Actual kind: {operand.Kind}"); - - return operand; - } - public bool BelongsToGroup(Arm64InstructionGroupId expectedGroup) { // AsmArm64 don't provide mapping for instruction groups. // So it need to simulate Capstone's BelongsToGroup behavior. - // Capstone's group mappings are defined at https://github.com/capstone-engine/capstone/blob/6.0.0-Alpha10/arch/AArch64/AArch64GenCSMappingInsn.inc + // Capstone's group mappings are defined at https://github.com/capstone-engine/capstone/blob/6.0.0-Alpha11/arch/AArch64/AArch64GenCSMappingInsn.inc switch (expectedGroup) { case Arm64InstructionGroupId.ARM64_GRP_JUMP: - switch (instruction.Id) + switch (instruction.Mnemonic) { - case Arm64InstructionId.B_only_branch_imm: - case Arm64InstructionId.B_only_condbranch: - case Arm64InstructionId.BC_only_condbranch: - case Arm64InstructionId.BR_64_branch_reg: - case Arm64InstructionId.BRAA_64p_branch_reg: - case Arm64InstructionId.BRAAZ_64_branch_reg: - case Arm64InstructionId.BRAB_64p_branch_reg: - case Arm64InstructionId.BRABZ_64_branch_reg: - case Arm64InstructionId.CBNZ_32_compbranch: - case Arm64InstructionId.CBNZ_64_compbranch: - case Arm64InstructionId.CBZ_32_compbranch: - case Arm64InstructionId.CBZ_64_compbranch: - case Arm64InstructionId.DRPS_64e_branch_reg: - case Arm64InstructionId.ERET_64e_branch_reg: - case Arm64InstructionId.ERETAA_64e_branch_reg: - case Arm64InstructionId.ERETAB_64e_branch_reg: - case Arm64InstructionId.RET_64r_branch_reg: - case Arm64InstructionId.RETAA_64e_branch_reg: - case Arm64InstructionId.RETAASPPCR_64m_branch_reg: - case Arm64InstructionId.RETAASPPC_only_miscbranch: - case Arm64InstructionId.RETAB_64e_branch_reg: - case Arm64InstructionId.RETABSPPCR_64m_branch_reg: - case Arm64InstructionId.RETABSPPC_only_miscbranch: - case Arm64InstructionId.TBNZ_only_testbranch: - case Arm64InstructionId.TBZ_only_testbranch: + case Arm64Mnemonic.B: + case Arm64Mnemonic.BC: + case Arm64Mnemonic.BR: + case Arm64Mnemonic.BRAA: + case Arm64Mnemonic.BRAAZ: + case Arm64Mnemonic.BRAB: + case Arm64Mnemonic.BRABZ: + case Arm64Mnemonic.CBNZ: + case Arm64Mnemonic.CBZ: + case Arm64Mnemonic.DRPS: + case Arm64Mnemonic.ERET: + case Arm64Mnemonic.ERETAA: + case Arm64Mnemonic.ERETAB: + case Arm64Mnemonic.RET: + case Arm64Mnemonic.RETAA: + case Arm64Mnemonic.RETAASPPC: + case Arm64Mnemonic.RETAASPPCR: + case Arm64Mnemonic.RETAB: + case Arm64Mnemonic.RETABSPPC: + case Arm64Mnemonic.RETABSPPCR: + case Arm64Mnemonic.TBZ: + case Arm64Mnemonic.TBNZ: return true; - default: return false; } case Arm64InstructionGroupId.ARM64_GRP_CALL: - switch (instruction.Id) + switch (instruction.Mnemonic) { - case Arm64InstructionId.BL_only_branch_imm: - case Arm64InstructionId.BLR_64_branch_reg: // BLR: Branch with link - case Arm64InstructionId.BLRAA_64p_branch_reg: // BLRAA: Branch with link to register with PAC - case Arm64InstructionId.BLRAB_64p_branch_reg: // BLRAB: Branch with link to register with PAC - case Arm64InstructionId.BLRAAZ_64_branch_reg: // Z: Zero modifier - case Arm64InstructionId.BLRABZ_64_branch_reg: // Z: Zero modifier - case Arm64InstructionId.HVC_ex_exception: - case Arm64InstructionId.SMC_ex_exception: - case Arm64InstructionId.SVC_ex_exception: + case Arm64Mnemonic.BL: + case Arm64Mnemonic.BLR: + case Arm64Mnemonic.BLRAA: + case Arm64Mnemonic.BLRAB: + case Arm64Mnemonic.BLRAAZ: + case Arm64Mnemonic.BLRABZ: + case Arm64Mnemonic.HVC: + case Arm64Mnemonic.SMC: + case Arm64Mnemonic.SVC: return true; default: return false; } case Arm64InstructionGroupId.ARM64_GRP_RET: - switch (instruction.Id) + switch (instruction.Mnemonic) { - case Arm64InstructionId.DRPS_64e_branch_reg: - case Arm64InstructionId.ERET_64e_branch_reg: - case Arm64InstructionId.ERETAA_64e_branch_reg: - case Arm64InstructionId.ERETAB_64e_branch_reg: - case Arm64InstructionId.RET_64r_branch_reg: - case Arm64InstructionId.RETAA_64e_branch_reg: - case Arm64InstructionId.RETAASPPC_only_miscbranch: - case Arm64InstructionId.RETAASPPCR_64m_branch_reg: - case Arm64InstructionId.RETAB_64e_branch_reg: - case Arm64InstructionId.RETABSPPC_only_miscbranch: - case Arm64InstructionId.RETABSPPCR_64m_branch_reg: + case Arm64Mnemonic.DRPS: + case Arm64Mnemonic.ERET: + case Arm64Mnemonic.ERETAA: + case Arm64Mnemonic.ERETAB: + case Arm64Mnemonic.RET: + case Arm64Mnemonic.RETAA: + case Arm64Mnemonic.RETAASPPC: + case Arm64Mnemonic.RETAASPPCR: + case Arm64Mnemonic.RETAB: + case Arm64Mnemonic.RETABSPPC: + case Arm64Mnemonic.RETABSPPCR: return true; default: return false; } case Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE: - switch (instruction.Id) + switch (instruction.Mnemonic) { - case Arm64InstructionId.B_only_branch_imm: - case Arm64InstructionId.B_only_condbranch: - case Arm64InstructionId.BC_only_condbranch: - case Arm64InstructionId.BL_only_branch_imm: - case Arm64InstructionId.CBNZ_32_compbranch: - case Arm64InstructionId.CBNZ_64_compbranch: - case Arm64InstructionId.CBZ_32_compbranch: - case Arm64InstructionId.CBZ_64_compbranch: - case Arm64InstructionId.RETAASPPC_only_miscbranch: - case Arm64InstructionId.RETABSPPC_only_miscbranch: - case Arm64InstructionId.TBNZ_only_testbranch: - case Arm64InstructionId.TBZ_only_testbranch: + case Arm64Mnemonic.B: + case Arm64Mnemonic.BC: + case Arm64Mnemonic.BL: + case Arm64Mnemonic.CBNZ: + case Arm64Mnemonic.CBZ: + case Arm64Mnemonic.RETAASPPC: + case Arm64Mnemonic.RETABSPPC: + case Arm64Mnemonic.TBNZ: + case Arm64Mnemonic.TBZ: return true; default: return false; @@ -344,7 +288,7 @@ public IEnumerable GetWrittenRegisters() switch (operand.Kind) { case Arm64OperandKind.Register: - var registerOperand = (Arm64RegisterOperand)operand; + var registerOperand = (Arm64RegisterOperand) operand; if ((registerOperand.Flags & Arm64OperandFlags.Write) != 0) yield return registerOperand.Value; @@ -352,7 +296,7 @@ public IEnumerable GetWrittenRegisters() continue; case Arm64OperandKind.RegisterGroup: - var registerGroupOperand = (Arm64RegisterGroupOperand)operand; + var registerGroupOperand = (Arm64RegisterGroupOperand) operand; if ((registerGroupOperand.Flags & Arm64OperandFlags.Write) != 0) { @@ -371,5 +315,47 @@ public IEnumerable GetWrittenRegisters() } } } - #endregion + + /// + /// Applies a shift to an immediate value. Returns false for unsupported/invalid shift kinds. + /// Use internal modifier for unit tests. + /// + internal static bool TryShiftImmediateValue( + long immediateValue, + Arm64ShiftKind shiftKind, + byte shiftAmount, + out long result) + { + result = 0; + switch (shiftKind) + { + case Arm64ShiftKind.LSL: + result = immediateValue << shiftAmount; + return true; + + case Arm64ShiftKind.LSR: + result = (long) ((ulong) immediateValue >> shiftAmount); + return true; + + case Arm64ShiftKind.ASR: + result = immediateValue >> shiftAmount; + return true; + + case Arm64ShiftKind.ROR: + { + var unsigned = (ulong) immediateValue; + var amount = shiftAmount & 63; + result = amount == 0 + ? (long) unsigned + : (long) ((unsigned >> amount) | (unsigned << (64 - amount))); + return true; + } + + // MSL (mask shift left) is used by bitmask immediates. + // Treat as unsupported to avoid producing an incorrect value. + case Arm64ShiftKind.MSL: + default: + return false; + } + } } diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionGroupId.cs b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionGroupId.cs new file mode 100644 index 0000000000..bc81c8bd1c --- /dev/null +++ b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionGroupId.cs @@ -0,0 +1,21 @@ +namespace BenchmarkDotNet.Disassemblers; + +// Based on Capstone's instruction grouping. +public enum Arm64InstructionGroupId +{ + /// + /// Indicates an invalid, or an uninitialized, instruction group. + /// + Invalid = 0, + + ARM64_GRP_JUMP, + ARM64_GRP_CALL, + ARM64_GRP_RET, + ARM64_GRP_INT, + ARM64_GRP_PRIVILEGE = 6, + ARM64_GRP_BRANCH_RELATIVE, + ARM64_GRP_CRYPTO = 128, + ARM64_GRP_FPARMV8, + ARM64_GRP_NEON, + ARM64_GRP_CRC +} From ffceaf6a2757de6125c181e2c165fa254ec7b882 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Wed, 4 Nov 2026 03:13:15 +0900 Subject: [PATCH 11/14] chore: refactor Arm64RegisterValueAccumulator to use AsmArm64 --- .../Arm64RegisterValueAccumulator.cs | 275 ++++++++---------- 1 file changed, 123 insertions(+), 152 deletions(-) diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs index de92586af5..c8d481958d 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs @@ -1,4 +1,4 @@ -using Gee.External.Capstone.Arm64; +using AsmArm64; using Microsoft.Diagnostics.Runtime.Interfaces; using System.Diagnostics; @@ -17,9 +17,11 @@ internal enum State private State _state; private ulong _addressValue; private long _addressOffset; - private Arm64RegisterId _registerId; + private Arm64RegisterAny _trackedRegister; private IClrRuntime _runtime; + private static readonly Arm64RegisterAny InvalidRegister = Arm64RegisterAny.Create(Arm64RegisterKind.Invalid, 0, Arm64RegisterVKind.Default, 0, 0); + public void Init(IClrRuntime runtime) { _runtime = runtime; @@ -29,20 +31,22 @@ public void Init(IClrRuntime runtime) internal void Reset( State state = State.LookingForPattern, ulong value = 0, - Arm64RegisterId registerId = Arm64RegisterId.Invalid) + Arm64RegisterAny register = default) { _state = state; _addressValue = value; _addressOffset = 0; - _registerId = registerId; + _trackedRegister = register == default + ? InvalidRegister + : register; } - public void Feed(Arm64Instruction instruction) + public void Feed(Arm64Instruction instruction, ulong instructionAddress = 0) { switch (_state) { case State.LookingForPattern: - if (TryHandleLookingForPattern(instruction)) + if (TryHandleLookingForPattern(instruction, instructionAddress)) return; break; @@ -67,7 +71,7 @@ public void Feed(Arm64Instruction instruction) return; // Try handle MOVZ/ADRP for new constand load. - if (TryHandleLookingForPattern(instruction)) + if (TryHandleLookingForPattern(instruction, instructionAddress)) return; goto default; @@ -82,7 +86,7 @@ public void Feed(Arm64Instruction instruction) Reset(); // Reprocess current instruction. - TryHandleLookingForPattern(instruction); + TryHandleLookingForPattern(instruction, instructionAddress); } return; } @@ -130,16 +134,26 @@ public ulong Value } } - public Arm64RegisterId RegisterId => _registerId; + public Arm64RegisterAny TrackedRegister => _trackedRegister; - private bool TryHandleLookingForPattern(Arm64Instruction instruction) + private bool TryHandleLookingForPattern(Arm64Instruction instruction, ulong instructionAddress) { - switch (instruction.Id) + switch (instruction.Mnemonic) { - case Arm64InstructionId.ARM64_INS_MOVZ: - return TryStartMovzSequence(instruction); - case Arm64InstructionId.ARM64_INS_ADRP: - return TryStartAdrpSequence(instruction); + // MOVZ is decoded as MOV instruction alias by default. + case Arm64Mnemonic.MOV: + var baseInstruction = instruction.AsBaseInstruction(); + if (baseInstruction.Mnemonic != Arm64Mnemonic.MOVZ) + goto default; + + return TryStartMovzSequence(baseInstruction); + + case Arm64Mnemonic.MOVZ: + //return TryStartMovzSequence(instruction); + throw new UnreachableException(); + + case Arm64Mnemonic.ADRP: + return TryStartAdrpSequence(instruction, instructionAddress); default: return false; } @@ -147,34 +161,37 @@ private bool TryHandleLookingForPattern(Arm64Instruction instruction) private bool TryHandleExpectingMovk(Arm64Instruction instruction) { - try - { - if (instruction.Id != Arm64InstructionId.ARM64_INS_MOVK) - return false; - - if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _) - || !instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) - return false; + if (instruction.Mnemonic != Arm64Mnemonic.MOVK) + return false; - if (instruction.GetRegisterId(0) != _registerId) - return false; + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand)) + return false; - // Note: MOVK instruction accept only LSL shift operation. - var shiftAmount = instruction.GetShiftAmount(1); - if (shiftAmount is not (0 or 16 or 32 or 48)) - return false; + if (registerOperand.Value != _trackedRegister) + return false; - var shiftedImmediateValue = instruction.GetUnsignedShiftedImmediateValue(1); - const ulong mask = 0xFFFF; - _addressValue &= ~(mask << shiftAmount); // Clear 16-bits of MOVK target - _addressValue |= shiftedImmediateValue; // Set shifted immediate value + if (!instruction.TryGetOperand(1, out Arm64ImmediateOperand _)) + return false; - return true; - } - catch (Exception ex) when (IsDecodingException(ex)) + var shiftAmount = 0; + if (instruction.TryGetOperand(2, out Arm64ShiftOperand shiftOperand)) { - return false; + if (shiftOperand.ShiftKind != Arm64ShiftKind.LSL) + return false; // MOVK only supports LSL shift kind. So this code path should never be hit. + shiftAmount = shiftOperand.Amount; } + + if (shiftAmount is not (0 or 16 or 32 or 48)) + return false; + + if (!instruction.TryGetShiftedImmediateValueUnsigned(1, out var shiftedImmediateValue)) + return false; + + const ulong mask = 0xFFFF; + _addressValue &= ~(mask << shiftAmount); // Clear 16-bits of MOVK target + _addressValue |= shiftedImmediateValue; // Set shifted immediate value + + return true; } /// @@ -185,131 +202,98 @@ private bool TryHandleExpectingMovk(Arm64Instruction instruction) /// private bool TryHandleExpectingAdd(Arm64Instruction instruction) { - try - { - if (instruction.Id != Arm64InstructionId.ARM64_INS_ADD) - return false; - - if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _) - || !instruction.TryGetOperand(1, Arm64OperandType.Register, out _)) - return false; + if (instruction.Mnemonic != Arm64Mnemonic.ADD) + return false; - if (instruction.GetRegisterId(0) != _registerId) - return false; + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand0) || registerOperand0.Value != _trackedRegister) + return false; - if (instruction.GetRegisterId(1) != _registerId) - return false; + if (!instruction.TryGetOperand(1, out Arm64RegisterOperand registerOperand1) || registerOperand1.Value != _trackedRegister) + return false; - if (!instruction.TryGetOperand(2, Arm64OperandType.Immediate, out _)) - return false; + if (!instruction.TryGetOperand(2, out Arm64ImmediateOperand _)) + return false; - var shiftedImmediateValue = instruction.GetUnsignedShiftedImmediateValue(2); - _addressValue += shiftedImmediateValue; - _state = State.LookingForPossibleLdr; - return true; - } - catch (Exception ex) when (IsDecodingException(ex)) - { + if (!instruction.TryGetShiftedImmediateValueUnsigned(2, out var shiftedImmediateValue)) return false; - } - } - /// - /// Only decoding failures (missing/mistyped operands, unexpected shift/instruction) - /// are treated as "not matched". Anything else (e.g. OOM) must propagate. - /// - private static bool IsDecodingException(Exception ex) - => ex is ArgumentOutOfRangeException - or ArgumentException - or NotSupportedException - or InvalidOperationException - or UnreachableException; + _addressValue += shiftedImmediateValue; + _state = State.LookingForPossibleLdr; + return true; + } private bool TryStartMovzSequence(Arm64Instruction instruction) { - if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _)) + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand)) return false; - if (!instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) + if (!instruction.TryGetOperand(1, out Arm64ImmediateOperand _)) return false; - try - { - _registerId = instruction.GetRegisterId(0); - _addressValue = instruction.GetUnsignedShiftedImmediateValue(1); - _addressOffset = 0; - _state = State.ExpectingMovk; - return true; - } - catch (Exception ex) when (IsDecodingException(ex)) - { + _trackedRegister = registerOperand.Value; + + if (!instruction.TryGetShiftedImmediateValueUnsigned(1, out var immediateValue)) return false; - } + + _addressValue = immediateValue; + _addressOffset = 0; + _state = State.ExpectingMovk; + return true; } - private bool TryStartAdrpSequence(Arm64Instruction instruction) + private bool TryStartAdrpSequence(Arm64Instruction instruction, ulong instructionAddress) { - if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _) - || !instruction.TryGetOperand(1, Arm64OperandType.Immediate, out _)) + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand) + || !instruction.TryGetOperand(1, out Arm64LabelOperand labelOperand)) return false; - try - { - _registerId = instruction.GetRegisterId(0); - _addressOffset = instruction.GetShiftedImmediateValue(1); - _addressValue = 0; - _state = State.ExpectingAdd; - return true; - } - catch (Exception ex) when (IsDecodingException(ex)) - { - return false; - } + _trackedRegister = registerOperand.Value; + + // ADRP label offset is a page delta relative to the page containing the instruction, + // so resolve it to an absolute address here. + _addressValue = instruction.GetTargetAddressBase(instructionAddress); + _addressOffset = labelOperand.Offset; + _state = State.ExpectingAdd; + return true; } private bool TryHandleLdrFromTrackedRegister(Arm64Instruction instruction) { - try - { - if (!instruction.IsLdrFromTrackedRegister(_registerId)) - return false; - - if (!instruction.TryGetOperand(0, Arm64OperandType.Register, out _)) - return false; - - // Address 0 is never valid to dereference (e.g. `movz x0, #0` or clamped negative ADRP+ADD). - if (Value == 0) - { - Reset(); - return true; - } - - // Simulate the LDR instruction. - // Use combined Value: after ADRP+ADD the effective address is _addressValue + _addressOffset. - // Use the Try-pattern so an unreadable address drops tracking instead of aborting disassembly. - if (!_runtime.DataTarget.DataReader.ReadPointer(Value, out ulong dereferenced)) - { - Reset(); - return true; - } + if (!instruction.IsLdrFromTrackedRegister(_trackedRegister)) + return false; - _addressValue = dereferenced; - _addressOffset = 0; + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand)) + return false; - if (_addressValue == 0) - { - Reset(); - return true; - } + // Address 0 is never valid to dereference (e.g. `movz x0, #0` or clamped negative ADRP+ADD). + if (Value == 0) + { + Reset(); + return true; + } - // The LDR might have loaded the result in another register - _registerId = instruction.GetRegisterId(0); + // Simulate the LDR instruction. + // Use combined Value: after ADRP+ADD the effective address is _addressValue + _addressOffset. + // Use the Try-pattern so an unreadable address drops tracking instead of aborting disassembly. + if (!_runtime.DataTarget.DataReader.ReadPointer(Value, out ulong dereferenced)) + { + Reset(); return true; } - catch (Exception ex) when (IsDecodingException(ex)) + + _addressValue = dereferenced; + _addressOffset = 0; + + if (_addressValue == 0) { - return false; + Reset(); + return true; } + + // The LDR might have loaded the result in another register + _trackedRegister = registerOperand.Value; + return true; + } private bool TryHandleConditionalInstruction(Arm64Instruction instruction) @@ -328,29 +312,16 @@ private bool TryHandleConditionalInstruction(Arm64Instruction instruction) private bool IsTrackedRegisterOverwritten(Arm64Instruction instruction) { - try + // Finally check if the current instruction modified the register that was accumulating the constant + // and reset the state machine in case it did. + foreach (var writtenRegister in instruction.GetWrittenRegisters()) { - // Finally check if the current instruction modified the register that was accumulating the constant - // and reset the state machine in case it did. - var details = instruction.Details; - if (details is null) - return true; // Conservative: unknown writes invalidate tracking (avoid false HasValue). - - foreach (Arm64Register writtenRegister in details.AllWrittenRegisters) - { - if (writtenRegister.Id != _registerId) - continue; - - return true; - } + if (writtenRegister != _trackedRegister) + continue; - return false; - } - catch (Exception ex) when (IsDecodingException(ex) || ex is NullReferenceException) - { - // Conservative: on decoding failure assume overwritten so a stale value - // is not reported as a false-positive ReferencedAddress. return true; } + + return false; } } From 28519b2e4c64560160c3c3cc037216bf254c74be Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Wed, 4 Nov 2026 03:13:30 +0900 Subject: [PATCH 12/14] chore: migrate Arm64 disassembler and model to AsmArm64 --- .../Disassemblers/Arm64Disassembler.cs | 145 +++++++++------- .../Arm64InstructionFormatter.cs | 156 +++++++++++++++--- .../Disassemblers/DataContracts.cs | 136 ++++----------- 3 files changed, 249 insertions(+), 188 deletions(-) diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs b/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs index b619389780..c7becb55bd 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs @@ -1,8 +1,8 @@ +using AsmArm64; using BenchmarkDotNet.Diagnosers; -using Gee.External.Capstone; -using Gee.External.Capstone.Arm64; using Microsoft.Diagnostics.Runtime; using Microsoft.Diagnostics.Runtime.Interfaces; +using System.Runtime.InteropServices; namespace BenchmarkDotNet.Disassemblers { @@ -10,47 +10,43 @@ internal class Arm64Disassembler : ClrMdDisassembler { protected override IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, IClrMethod currentMethod, DisassemblySyntax syntax) { - const Arm64DisassembleMode disassembleMode = Arm64DisassembleMode.Arm; - using (CapstoneArm64Disassembler disassembler = CapstoneFactory.CreateArm64Disassembler(disassembleMode)) - { - // Enables disassemble details, which are disabled by default, to provide more detailed information on - // disassembled binary code. - disassembler.EnableInstructionDetails = true; - disassembler.DisassembleSyntax = Map(syntax); + Arm64RegisterValueAccumulator accumulator = new(); + accumulator.Init(state.Runtime); + + ReadOnlySpan rawInstructions = MemoryMarshal.Cast(code); - Arm64RegisterValueAccumulator accumulator = new(); - accumulator.Init(state.Runtime); + var results = new List(); + + for (int i = 0; i < rawInstructions.Length; ++i) + { + var rawInstruction = rawInstructions[i]; + var instruction = Arm64Instruction.Decode(rawInstruction); + var instructionAddress = startAddress + (4 * (ulong) i); - Arm64Instruction[] instructions = disassembler.Disassemble(code, (long) startAddress); - foreach (Arm64Instruction instruction in instructions) + if (TryGetReferencedAddress(instruction, instructionAddress, accumulator, (uint) state.Runtime.DataTarget.DataReader.PointerSize, out var referencedAddress, out var isIndirect)) { - bool isIndirect = false; bool isPrestubMD = false; - - ulong address = 0; - if (TryGetReferencedAddress(instruction, accumulator, (uint) state.Runtime.DataTarget.DataReader.PointerSize, out address, out isIndirect)) + if (isIndirect && state.RuntimeVersion.Major >= 7) { - if (isIndirect && state.RuntimeVersion.Major >= 7) - { - FlushCachedDataIfNeeded(state.Runtime.DataTarget.DataReader, address, new byte[1]); - TryResolvePrecode(state.Runtime.DataTarget.DataReader, ref address, out isPrestubMD); - } - TryTranslateAddressToName(address, isPrestubMD, state, depth, currentMethod); + FlushCachedDataIfNeeded(state.Runtime.DataTarget.DataReader, referencedAddress, new byte[1]); + TryResolvePrecode(state.Runtime.DataTarget.DataReader, ref referencedAddress, out isPrestubMD); } + TryTranslateAddressToName(referencedAddress, isPrestubMD, state, depth, currentMethod); + } - accumulator.Feed(instruction); + accumulator.Feed(instruction, instructionAddress); - yield return new Arm64Asm() - { - InstructionPointer = (ulong) instruction.Address, - InstructionLength = instruction.Bytes.Length, - Instruction = instruction, - ReferencedAddress = (address > ushort.MaxValue) ? address : null, - IsReferencedAddressIndirect = isIndirect, - DisassembleSyntax = disassembler.DisassembleSyntax - }; - } + results.Add(new Arm64Asm() + { + InstructionPointer = instructionAddress, + RawInstruction = rawInstruction, + DisassembleSyntax = DisassembleSyntax.Gas, + ReferencedAddress = IsValidAddress(referencedAddress) ? referencedAddress : null, + IsReferencedAddressIndirect = isIndirect, + }); } + + return results; } // Counterpart of IntelDisassembler.TryResolvePrecode: recognise the AArch64 precode/stub @@ -65,7 +61,7 @@ protected override IEnumerable Decode(byte[] code, ulong startAddress, Stat // data-section layout is also stable. What can change between versions is the offset // between the code page and its data section, so we extract the LDR-literal displacements // straight from the bytes instead of consulting a runtime-version-specific page-size table. - private static bool TryResolvePrecode(IDataReader reader, ref ulong address, out bool isPrestubMD) + internal static bool TryResolvePrecode(IDataReader reader, ref ulong address, out bool isPrestubMD) { isPrestubMD = false; if (!TryReadStubHead(reader, address, out ulong parseBase, out uint instr0, out uint instr1, out uint instr2)) @@ -145,22 +141,31 @@ private static bool TryResolvePrecode(IDataReader reader, ref ulong address, out // Encoding: DMB ISHLD = 0xD50339BF. private const uint DmbIshLdInstr = 0xD50339BFu; - private static bool TryReadStubHead(IDataReader reader, ulong address, out ulong parseBase, out uint instr0, out uint instr1, out uint instr2) + internal static bool TryReadStubHead(IDataReader reader, ulong address, out ulong parseBase, out uint instr0, out uint instr1, out uint instr2) { parseBase = address; instr0 = instr1 = instr2 = 0; - byte[] buffer = new byte[16]; - // TODO: IDataReader.Read might return partial data. It need to call multiple times until buffer filled or readLength<=0. - int read = reader.Read(address, buffer); - if (read < 12) + // IDataReader.Read may return partial data. + // Call it repeatedly until the buffer is filled or no more data is available. + Span buffer = stackalloc byte[16]; + int totalBytesRead = 0; + while (totalBytesRead < buffer.Length) + { + int bytesRead = reader.Read(address + (ulong) totalBytesRead, buffer.Slice(totalBytesRead)); + if (bytesRead <= 0) + break; + totalBytesRead += bytesRead; + } + + if (totalBytesRead < 12) return false; int offset = 0; uint first = ReadInstr(buffer, 0); if (first == DmbIshLdInstr) { - if (read < 16) + if (totalBytesRead < 16) return false; offset = 4; parseBase = address + 4; @@ -172,7 +177,7 @@ private static bool TryReadStubHead(IDataReader reader, ulong address, out ulong return true; } - private static uint ReadInstr(byte[] buffer, int offset) + private static uint ReadInstr(ReadOnlySpan buffer, int offset) => (uint) buffer[offset] | ((uint) buffer[offset + 1] << 8) | ((uint) buffer[offset + 2] << 16) @@ -181,7 +186,7 @@ private static uint ReadInstr(byte[] buffer, int offset) // LDR (literal), 64-bit form. Encoding: bits[31:24]=0x58, bits[23:5]=imm19 (signed, // word-scaled offset relative to the LDR's own PC), bits[4:0]=Xt. Returns the destination // register and the byte-scaled offset from the LDR instruction's address to the loaded slot. - private static bool IsLdrLiteral64(uint instr, out int rt, out int offsetBytes) + internal static bool IsLdrLiteral64(uint instr, out int rt, out int offsetBytes) { rt = 0; offsetBytes = 0; @@ -196,41 +201,57 @@ private static bool IsLdrLiteral64(uint instr, out int rt, out int offsetBytes) return true; } - private static bool TryGetReferencedAddress(Arm64Instruction instruction, Arm64RegisterValueAccumulator accumulator, uint pointerSize, out ulong referencedAddress, out bool isReferencedAddressIndirect) + internal static bool TryGetReferencedAddress( + Arm64Instruction instruction, + ulong instructionAddress, + 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) + + switch (instruction.Mnemonic) { - // Branch via register where we have extracted the value of the register by parsing the disassembly - referencedAddress = (ulong) accumulator.Value; - isReferencedAddressIndirect = true; - return true; + // TODO: Add support for BRAA/BRAB/BRAAZ/BRABZ/BLRAA/BLRAB/BLRAAZ/BLRABZ instructions. Currently .NET JIT don't emit these instructions. + // See: https://github.com/dotnet/runtime/blob/v11.0.0-rc.3.26522.148/src/coreclr/jit/instrsarm64.h + case Arm64Mnemonic.BR: + case Arm64Mnemonic.BLR: + if (instruction.TryGetOperand(0, out Arm64RegisterOperand register) && register.Value == accumulator.TrackedRegister) + { + if (accumulator.HasValue) + { + // Branch via register where we have extracted the value of the register by parsing the disassembly. + // The accumulator holds an absolute address (MOVZ/MOVK constant or ADRP resolved to a page address). + referencedAddress = accumulator.Value; + isReferencedAddressIndirect = true; + return true; + } + } + break; } - else if (instruction.Details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE)) + + if (instruction.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE)) { - // One of the operands is the address - for (int i = 0; i < instruction.Details.Operands.Length; i++) + if (instruction.Flags.HasFlag(Arm64InstructionFlags.HasLabel)) { - if (instruction.Details.Operands[i].Type == Arm64OperandType.Immediate) + // One of the label operands is the address + for (int i = 0; i < instruction.Operands.Count; i++) { - referencedAddress = (ulong) instruction.Details.Operands[i].Immediate; + if (!instruction.TryGetOperand(i, out Arm64LabelOperand labelOperand)) + continue; + + referencedAddress = instruction.GetTargetAddress(instructionAddress, labelOperand.Offset); isReferencedAddressIndirect = false; return true; } } } + referencedAddress = 0; isReferencedAddressIndirect = false; return false; } - private static DisassembleSyntax Map(DisassemblySyntax syntax) - => syntax switch - { - DisassemblySyntax.Att => DisassembleSyntax.Att, - DisassemblySyntax.Intel => DisassembleSyntax.Intel, - _ => DisassembleSyntax.Masm - }; - // Recognise the AArch64 jump trampoline shape the CLR JIT emits when a call's real target // is out of rel26 range (±128 MB), plus the precode/stub shapes the runtime emits as the // stable entry point for tiered methods (so a direct `BL imm26` landing on the precode diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs index 2adcdc0966..818b7e09b9 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs @@ -1,52 +1,164 @@ -using Gee.External.Capstone.Arm64; +using AsmArm64; +using BenchmarkDotNet.Extensions; using Iced.Intel; +using System.Globalization; using System.Text; namespace BenchmarkDotNet.Disassemblers { internal static class Arm64InstructionFormatter { - // FormatterOptions is an Intel-specific concept that comes from the Iced library, but since our users can pass custom - // Iced Formatter to DisassemblyDiagnoserConfig and it provides all the settings we need, we just reuse it here. - internal static string Format(Arm64Asm asm, FormatterOptions formatterOptions, - bool printInstructionAddresses, uint pointerSize, IReadOnlyDictionary symbols) + internal static readonly Arm64InstructionFormattingOptions DefaultFormattingOptions = new() { - var instruction = asm.Instruction; - if (instruction == null) - return ""; + AliasMode = Arm64InstructionAliasMode.Preferred, + FormatProvider = CultureInfo.InvariantCulture, + ImmediateFormat = Arm64NumericFormat.Auto, + LabelOffsetFormat = Arm64NumericFormat.Auto, + MemoryOffsetFormat = Arm64NumericFormat.Auto, + UseUppercaseHex = true, + UseUppercaseText = false, + PrintDefaultOperands = false, + }; - StringBuilder output = new(); + internal static readonly Arm64InstructionFormattingOptions BaseInstructionAliasModeFormattingOptions = new() + { + AliasMode = Arm64InstructionAliasMode.BaseInstruction, + FormatProvider = CultureInfo.InvariantCulture, + ImmediateFormat = Arm64NumericFormat.Auto, + LabelOffsetFormat = Arm64NumericFormat.Auto, + MemoryOffsetFormat = Arm64NumericFormat.Auto, + UseUppercaseHex = true, + UseUppercaseText = false, + PrintDefaultOperands = false, + }; - if (printInstructionAddresses) + internal static string Format(Arm64Instruction instruction) + { + var asm = new Arm64Asm { - FormatInstructionPointer(instruction, formatterOptions, pointerSize, output); + RawInstruction = instruction.RawInstruction, + DisassembleSyntax = DisassembleSyntax.Gas, + }; + return Format(asm, new Iced.Intel.FormatterOptions { }, printInstructionAddresses: false, 8, new Dictionary()); + } + + internal static (string Mnemonic, string OperandsText) GetInstructionDetails( + Arm64Instruction instruction, + ulong instructionAddress, + IReadOnlyDictionary? symbols = null) + { + var formattingOptions = GetFormattingOptions(instruction); + var instructionText = FormatInstruction(instruction, formattingOptions, instructionAddress, symbols); + if (instructionText.IsBlank()) + return ("", ""); + + var spaceIndex = instructionText.IndexOf(' '); + return spaceIndex < 0 + ? (instructionText, "") + : (instructionText[..spaceIndex], instructionText[(spaceIndex + 1)..]); + } + + private static string FormatInstruction( + Arm64Instruction instruction, + Arm64InstructionFormattingOptions formattingOptions, + ulong instructionAddress, + IReadOnlyDictionary? symbols) + { + // If base address is not specified, keep the raw relative label offsets. + if (instructionAddress == 0) + return instruction.ToString(formattingOptions); + + var labelDidNotFit = false; + + // Symbol names can be long, so grow the buffer until every resolved label fits. + for (int bufferLength = 256; bufferLength <= 64 * 1024; bufferLength *= 2) + { + labelDidNotFit = false; + var buffer = new char[bufferLength]; + + if (instruction.TryFormat(formattingOptions, buffer, out int written, TryResolveLabel) && !labelDidNotFit) + return new string(buffer, 0, written); } - var padRight = instruction.Details.Operands.Length > 0 - ? Math.Max(formatterOptions.FirstOperandCharIndex, instruction.Mnemonic.Length + 1) - : 0; - output.Append(instruction.Mnemonic.PadRight(padRight)); + return instruction.ToString(formattingOptions); - if (asm.ReferencedAddress.HasValue && !asm.IsReferencedAddressIndirect && symbols.TryGetValue(asm.ReferencedAddress.Value, out var name)) + // Helper local function to resolve each label operand while formatting. + bool TryResolveLabel(long labelOffset, Span destination, out int charsWritten) { - string partToReplace = $"#0x{asm.ReferencedAddress.Value:x}"; - output.Append(instruction.Operand.Replace(partToReplace, name)); + var address = instruction.GetTargetAddress(instructionAddress, labelOffset); + var text = symbols is not null && symbols.TryGetValue(address, out var name) + ? name + : $"#0x{address:X}"; + + if (!text.AsSpan().TryCopyTo(destination)) + { + // Returning false makes AsmArm64 use the numeric fallback so that we can retry with a larger buffer. + labelDidNotFit = true; + charsWritten = 0; + return false; + } + + charsWritten = text.Length; + return true; } - else + } + + internal static Arm64InstructionFormattingOptions GetFormattingOptions(Arm64Instruction instruction) + { + // AsmArm64 automatically resolve instruction mnemonic to preferred alias. + // Too keep Gee.Extenal.Capstone disassembly results compatibility. + // Use `Arm64InstructionAliasMode.BaseInstruction` for MOVN and MOVZ instructions. + // Full list of instruction that use aliases: https://github.com/xoofx/AsmArm64/blob/main/src/AsmArm64/generated/Arm64InstructionAliasTable.gen.cs + + if (instruction.Mnemonic != Arm64Mnemonic.MOV) + return DefaultFormattingOptions; + + var baseMnemonic = instruction.AsBaseInstruction().Mnemonic; + switch (baseMnemonic) + { + case Arm64Mnemonic.MOVN: + case Arm64Mnemonic.MOVZ: + return BaseInstructionAliasModeFormattingOptions; + + default: + return DefaultFormattingOptions; + } + } + + // FormatterOptions is an Intel-specific concept that comes from the Iced library, but since our users can pass custom + // Iced Formatter to DisassemblyDiagnoserConfig and it provides all the settings we need, we just reuse it here. + internal static string Format( + Arm64Asm asm, + FormatterOptions formatterOptions, + bool printInstructionAddresses, + uint pointerSize, + IReadOnlyDictionary symbols) + { + StringBuilder output = new(256); + + if (printInstructionAddresses) { - output.Append(instruction.Operand); + FormatInstructionPointer(asm.InstructionPointer, formatterOptions, pointerSize, output); } + var (mnemonic, operandsText) = GetInstructionDetails(asm.Instruction, asm.InstructionPointer, symbols); + + var padRight = operandsText != "" + ? Math.Max(formatterOptions.FirstOperandCharIndex, mnemonic.Length + 1) + : 0; + output.Append(mnemonic.PadRight(padRight)); + output.Append(operandsText); + return output.ToString(); } - private static void FormatInstructionPointer(Arm64Instruction instruction, FormatterOptions formatterOptions, uint pointerSize, StringBuilder output) + private static void FormatInstructionPointer(ulong address, FormatterOptions formatterOptions, uint pointerSize, StringBuilder output) { string ipFormat = formatterOptions.LeadingZeroes ? pointerSize == 4 ? "X8" : "X16" : "X"; - output.Append(instruction.Address.ToString(ipFormat)); + output.Append(address.ToString(ipFormat)); output.Append(' '); } } diff --git a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs index 9915725abf..8b4282a4fd 100644 --- a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs +++ b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs @@ -1,9 +1,7 @@ -using Gee.External.Capstone; -using Gee.External.Capstone.Arm64; +using AsmArm64; using Iced.Intel; using Microsoft.Diagnostics.Runtime.Interfaces; -using System.ComponentModel; -using System.Text.Json; +using System.Diagnostics; using System.Text.Json.Serialization; @@ -13,6 +11,21 @@ namespace BenchmarkDotNet.Disassemblers; +// Based on Capstone definition.. +public enum DisassembleSyntax +{ + Default = Masm, + + // x64/x86 + Att = 1, + Intel = 2, + Masm = 3, + + // Following enum definitions are added for arm64 platform + Gas = 11, // (Default) GNU assembler inspired style + Llvm = 12, // LLVM inspired style +} + [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(Sharp), typeDiscriminator: nameof(Sharp))] [JsonDerivedType(typeof(IntelAsm), typeDiscriminator: nameof(IntelAsm))] @@ -57,117 +70,32 @@ public sealed class IntelAsm() : Asm public sealed class Arm64Asm : Asm { - [JsonInclude] - [EditorBrowsable(EditorBrowsableState.Never)] - internal Arm64AsmData Data { get; set; } - - [JsonIgnore] - public Arm64Instruction? Instruction - { - get => Data.Instruction; - set => Data = Data with { Instruction = value }; - } - - [JsonIgnore] - internal DisassembleSyntax DisassembleSyntax + public Arm64Asm() { - get => Data.DisassembleSyntax; - set => Data = Data with { DisassembleSyntax = value }; + InstructionLength = 4; } - public override string ToString() => Instruction?.ToString() ?? ""; + public required uint RawInstruction { get; init; } - // Wrapper class to hold Arm64 instruction and disassemble syntax. - [JsonConverter(typeof(Arm64AsmDataConverter))] - internal record struct Arm64AsmData - { - internal DisassembleSyntax DisassembleSyntax { get; set; } + public required DisassembleSyntax DisassembleSyntax { get; init; } - public Arm64Instruction? Instruction { get; set; } - } + private Arm64Instruction? _instruction; - // Custom JsonConverter for Arm64AsmData. - internal class Arm64AsmDataConverter : JsonConverter + [JsonIgnore] + public Arm64Instruction Instruction { - private const string Arm64AddressKey = "arm64Address"; - private const string Arm64BytesKey = "arm64Bytes"; - private const string Arm64SyntaxKey = "arm64Syntax"; - - public override Arm64AsmData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + get { - if (reader.TokenType != JsonTokenType.StartObject) - throw new JsonException(); - - long instructionAddress = default; - byte[] instructionBytes = []; - DisassembleSyntax syntax = DisassembleSyntax.Masm; - - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) - break; - - if (reader.TokenType != JsonTokenType.PropertyName) - throw new JsonException(); - - string propertyName = reader.GetString()!; - reader.Read(); - - switch (propertyName) - { - case Arm64AddressKey: - instructionAddress = reader.GetInt64(); - break; - - case Arm64BytesKey: - instructionBytes = reader.GetBytesFromBase64(); - break; - - case Arm64SyntaxKey: - var syntaxValue = reader.GetString()!; - syntax = syntaxValue switch - { - "Intel" => DisassembleSyntax.Intel, - "Att" => DisassembleSyntax.Att, - "Masm" => DisassembleSyntax.Masm, - _ => DisassembleSyntax.Masm, - }; - break; - - default: - throw new NotSupportedException($"Unknown property({propertyName}) found."); - } - } - - if (reader.TokenType != JsonTokenType.EndObject) - throw new JsonException("Invalid JSON"); - - using var disassembler = CapstoneFactory.CreateArm64Disassembler(Arm64DisassembleMode.Arm); - disassembler.EnableInstructionDetails = true; - disassembler.DisassembleSyntax = syntax; - var instruction = disassembler.Disassemble(instructionBytes, instructionAddress).SingleOrDefault(); - - return new Arm64AsmData - { - DisassembleSyntax = syntax, - Instruction = instruction, - }; - } - - public override void Write(Utf8JsonWriter writer, Arm64AsmData value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - writer.WriteString(Arm64SyntaxKey, value.DisassembleSyntax.ToString()); - var instruction = value.Instruction; - if (instruction != null) - { - writer.WriteNumber(Arm64AddressKey, instruction.Address); - writer.WriteBase64String(Arm64BytesKey, instruction.Bytes); - } - writer.WriteEndObject(); + _instruction ??= Arm64Instruction.Decode(RawInstruction); + return _instruction.Value; } } + public override string ToString() + { + var options = Arm64InstructionFormatter.GetFormattingOptions(Instruction); + return Instruction.ToString(options); + } } public sealed class MonoCode : SourceCode From 14c62d43441055af5af90f597ef309096ecdafd4 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Wed, 4 Nov 2026 03:13:38 +0900 Subject: [PATCH 13/14] chore: remove Capstone dependency --- src/BenchmarkDotNet/BenchmarkDotNet.csproj | 1 - src/BenchmarkDotNet/BenchmarkDotNet.targets | 34 ----------- .../Disassemblers/CapstoneFactory.cs | 58 ------------------- 3 files changed, 93 deletions(-) delete mode 100644 src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj index e023399587..3e6d079490 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj +++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj @@ -26,7 +26,6 @@ - diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.targets b/src/BenchmarkDotNet/BenchmarkDotNet.targets index 62c85d5b99..7703144453 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.targets +++ b/src/BenchmarkDotNet/BenchmarkDotNet.targets @@ -1,40 +1,6 @@ - - - $(RuntimeIdentifier) - - - - - - linux - win - osx - - $(BenchmarkDotNetTargetPlatform)-x64 - $(BenchmarkDotNetTargetPlatform)-x86 - $(BenchmarkDotNetTargetPlatform)-arm - $(BenchmarkDotNetTargetPlatform)-arm64 - - - - - - - - - - - true - - - - /// .NET Framework does not probe runtimes/{rid}/native, where BenchmarkDotNet.targets copies the native capstone. - /// Once it is loaded by its full path, the P/Invokes that name the module bind to the loaded library. - /// When the file is not there, the default search applies - a build for a specific RuntimeIdentifier puts it next to the application. - /// - private static class FrameworkNativeLibrary - { - internal static readonly Lazy Handle = new(Load); - - private static IntPtr Load() - { - if (!RuntimeInformation.IsFullFramework) - return IntPtr.Zero; - - string? rid = RuntimeInformation.GetCurrentPlatform() switch - { - Platform.X86 => "win-x86", - Platform.X64 => "win-x64", - Platform.Arm64 => "win-arm64", - _ => null - }; - var assembly = typeof(CapstoneDisassembler).Assembly; - string location = ShadowCopyHelper.TryGetOriginalLocation(assembly, out var originalLocation) ? originalLocation : assembly.Location; - if (rid is null || location.IsBlank()) - return IntPtr.Zero; - - string path = Path.Combine(Path.GetDirectoryName(location)!, "runtimes", rid, "native", "capstone.dll"); - return File.Exists(path) ? LoadLibraryW(path) : IntPtr.Zero; - } - - [DllImport("kernel32", ExactSpelling = true, CharSet = CharSet.Unicode)] - private static extern IntPtr LoadLibraryW(string lpLibFileName); - } -#endif -} From 2a103801f9fd636d19d4c27732e1d02869128275 Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Wed, 4 Nov 2026 03:13:50 +0900 Subject: [PATCH 14/14] chore: add/update related unit tests --- .../Arm64RegisterAnyAssertion.cs | 62 +++++ .../Arm64/Arm64DisassemblerTests.Decode.cs | 28 +-- .../Arm64DisassemblerTests.IsLdrLiteral64.cs | 14 +- ...sassemblerTests.TryFollowJumpTrampoline.cs | 7 +- ...sassemblerTests.TryGetReferencedAddress.cs | 225 ++++++++++++++---- .../Arm64DisassemblerTests.TryReadStubHead.cs | 12 +- ...rm64DisassemblerTests.TryResolvePreCode.cs | 24 +- ...ssemblerTests.TryTranslateAddressToName.cs | 3 - ...64InstructionExtensionsTests.ShiftKinds.cs | 79 ++++++ ...ructionExtensionsTests.ShiftedImmediate.cs | 68 ++++++ .../Arm64/Arm64InstructionExtensionsTests.cs | 12 + ...uctionFormatterTests.IntructionPatterns.cs | 60 +++++ ...structionFormatterTests.LabelResolution.cs | 77 ++++++ ...structionFormatterTests.MnemonicAliases.cs | 62 +++++ ...Arm64InstructionFormatterTests.Scenario.cs | 80 +++++++ .../Arm64/Arm64InstructionFormatterTests.cs | 163 ++----------- ...gisterValueAccumulatorTests.AdrpThenAdd.cs | 55 +++-- ...Arm64RegisterValueAccumulatorTests.Movz.cs | 21 +- ...umulatorTests.MovzThenBranchConditional.cs | 32 +-- ...gisterValueAccumulatorTests.MovzThenLdr.cs | 73 +++--- ...isterValueAccumulatorTests.MovzThenMovk.cs | 62 +++-- ...ulatorTests.MovzThenUnConditionalBranch.cs | 71 +++--- .../Arm64/BelongsToGroupTests.cs | 173 +++++--------- .../Arm64/Helpers/Arm64DisassemblerHelper.cs | 72 ------ .../Helpers/Arm64DisassemblerTestBase.cs | 18 +- .../Arm64/Helpers/Arm64TestInstructions.cs | 192 --------------- .../Arm64/Helpers/ExtensionMethods.cs | 31 +++ .../DisassemblerModelSerializationTests.cs | 45 ++-- 28 files changed, 1031 insertions(+), 790 deletions(-) create mode 100644 tests/BenchmarkDotNet.Tests/AwesomeAssertions/CustomAssertions/Arm64RegisterAnyAssertion.cs create mode 100644 tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.ShiftKinds.cs create mode 100644 tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.ShiftedImmediate.cs create mode 100644 tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.cs create mode 100644 tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.IntructionPatterns.cs create mode 100644 tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.LabelResolution.cs create mode 100644 tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.MnemonicAliases.cs create mode 100644 tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.Scenario.cs delete mode 100644 tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64TestInstructions.cs create mode 100644 tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/ExtensionMethods.cs diff --git a/tests/BenchmarkDotNet.Tests/AwesomeAssertions/CustomAssertions/Arm64RegisterAnyAssertion.cs b/tests/BenchmarkDotNet.Tests/AwesomeAssertions/CustomAssertions/Arm64RegisterAnyAssertion.cs new file mode 100644 index 0000000000..995694d3b9 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/AwesomeAssertions/CustomAssertions/Arm64RegisterAnyAssertion.cs @@ -0,0 +1,62 @@ +namespace BenchmarkDotNet.Tests; + +using AsmArm64; +using AwesomeAssertions; +using AwesomeAssertions.Execution; + +/// +/// Custom assertions for Arm64RegisterAny. +/// +[CustomAssertions] +public sealed class Arm64RegisterAnyAssertions +{ + private readonly Arm64RegisterAny subject; + private readonly AssertionChain assertionChain; + + public Arm64RegisterAnyAssertions( + Arm64RegisterAny subject, + AssertionChain assertionChain) + { + this.subject = subject; + this.assertionChain = assertionChain; + } + + [CustomAssertion] + public AndConstraint Be( + Arm64RegisterAny expected, + string because = "", + params object[] becauseArgs) + { + assertionChain + .BecauseOf(because, becauseArgs) + .ForCondition(EqualityComparer.Default.Equals(subject, expected)) + .FailWith("Expected register to be {0}, but found {1}.", expected.ToText(), subject.ToText()); + + return new AndConstraint(this); + } + + //[CustomAssertion] + //public AndConstraint NotBe( + // Arm64RegisterAny unexpected, + // string because = "", + // params object[] becauseArgs) + //{ + // assertionChain + // .BecauseOf(because, becauseArgs) + // .ForCondition(!EqualityComparer.Default.Equals( + // subject, unexpected)) + // .FailWith( + // "Did not expect {context:Arm64 register} to be {0}{reason}.", + // unexpected); + + // return new AndConstraint(this); + //} +} + +public static class Arm64RegisterAnyAssertionsExtensions +{ + public static Arm64RegisterAnyAssertions Should(this Arm64RegisterAny subject) + { + return new Arm64RegisterAnyAssertions(subject, AssertionChain.GetOrCreate()); + } +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.Decode.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.Decode.cs index 50b4f91134..1f46a42410 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.Decode.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.Decode.cs @@ -1,17 +1,13 @@ -// 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 +public partial class Arm64DisassemblerTests : Arm64DisassemblerTestBase { // Check Arm64Instruction equivalency by ToString output. private static EquivalencyOptions ConfigureCustomEquivalency(EquivalencyOptions options) @@ -33,7 +29,7 @@ public void Decode() byte[] bytes = rawInstructions.ToLittleEndianBytes(); - const ulong MdSlotAddress = 0x56781234 + 8 + 0x20000; + const ulong MdSlotAddress = 0x5678_1234 + 8 + 0x20000; using var clrRuntime = new MockMemory() .AddInstructions(0x5678_1234, @@ -43,8 +39,8 @@ public void Decode() Arm64InstructionFactory.LDR(X12, 0x20000), // ldr x12, #0x20000 Arm64InstructionFactory.BR(X10), // br x10 ]) - .AddPointer(MdSlotAddress, ExpectedResultAddress) // Called by TryResolvePrecode - .AddJitHelperFunctionName(ExpectedResultAddress, "") + .AddPointer(MdSlotAddress, ExpectedResultAddress) // Called by TryResolvePrecode + .AddJitHelperFunctionName(ExpectedResultAddress, "") // Called by TryTranslateAddressToName .AddMethodByInstructionPointer(ExpectedResultAddress, DummyTargetMethod) .ToMockClrRuntime(); var state = new State(clrRuntime, DummyTargetFrameworkVersion); @@ -63,9 +59,8 @@ public void Decode() results[0].Should().BeEquivalentTo(new Arm64Asm { InstructionPointer = baseAddress, - Instruction = rawInstructions[0].ToCapstoneArm64Instruction(baseAddress, 0), - InstructionLength = 4, - DisassembleSyntax = DisassembleSyntax.Masm, + RawInstruction = rawInstructions[0], + DisassembleSyntax = DisassembleSyntax.Gas, ReferencedAddress = null, IsReferencedAddressIndirect = false, }, ConfigureCustomEquivalency); @@ -74,9 +69,8 @@ public void Decode() results[1].Should().BeEquivalentTo(new Arm64Asm { InstructionPointer = baseAddress + 4, - Instruction = rawInstructions[1].ToCapstoneArm64Instruction(baseAddress, 1), - InstructionLength = 4, - DisassembleSyntax = DisassembleSyntax.Masm, + RawInstruction = rawInstructions[1], + DisassembleSyntax = DisassembleSyntax.Gas, ReferencedAddress = null, IsReferencedAddressIndirect = false, }, ConfigureCustomEquivalency); @@ -85,9 +79,8 @@ public void Decode() results[2].Should().BeEquivalentTo(new Arm64Asm { InstructionPointer = baseAddress + 8, - Instruction = rawInstructions[2].ToCapstoneArm64Instruction(baseAddress, 2), - InstructionLength = 4, - DisassembleSyntax = DisassembleSyntax.Masm, + RawInstruction = rawInstructions[2], + DisassembleSyntax = DisassembleSyntax.Gas, ReferencedAddress = ExpectedResultAddress, IsReferencedAddressIndirect = true, }, ConfigureCustomEquivalency); @@ -100,4 +93,3 @@ public void Decode() }); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.IsLdrLiteral64.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.IsLdrLiteral64.cs index f3dbae8698..1e11992a80 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.IsLdrLiteral64.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.IsLdrLiteral64.cs @@ -1,9 +1,8 @@ -// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. -#if NET using AsmArm64; using AwesomeAssertions; -using static AsmArm64.Arm64RegisterX; using static AsmArm64.Arm64RegisterW; +using static AsmArm64.Arm64RegisterX; +using Arm64Disassembler = BenchmarkDotNet.Disassemblers.Arm64Disassembler; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -17,7 +16,7 @@ public void IsLdrLiteral64() var rawInstruction = Arm64InstructionFactory.LDR(X5, label: expectedOffsetBytes); // Act - var result = Arm64DisassemblerHelper.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); + var result = Arm64Disassembler.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); // Assert result.Should().BeTrue(); @@ -44,7 +43,7 @@ public void IsLdrLiteral64_WithMinValue() var rawInstruction = Arm64InstructionFactory.LDR(X0, label: new Arm64LabelOffset(expectedOffsetBytes)); // Act - var result = Arm64DisassemblerHelper.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); + var result = Arm64Disassembler.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); // Assert result.Should().BeTrue(); @@ -71,7 +70,7 @@ public void IsLdrLiteral64_WithMaxValue() var rawInstruction = Arm64InstructionFactory.LDR(X0, label: new Arm64LabelOffset(expectedOffsetBytes)); // Act - var result = Arm64DisassemblerHelper.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); + var result = Arm64Disassembler.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); // Assert result.Should().BeTrue(); @@ -96,7 +95,7 @@ public void IsLdrLiteral64_With32bitRegister_ShouldBeFalse() var rawInstruction = Arm64InstructionFactory.LDR(W5, label: 0x100); // Act - var result = Arm64DisassemblerHelper.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); + var result = Arm64Disassembler.IsLdrLiteral64(rawInstruction, out var rt, out var offsetBytes); // Assert result.Should().BeFalse(); @@ -109,4 +108,3 @@ public void IsLdrLiteral64_With32bitRegister_ShouldBeFalse() registerOperand.Value.Should().Be((Arm64RegisterAny) W5); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryFollowJumpTrampoline.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryFollowJumpTrampoline.cs index ffb835c38b..7433d8ff07 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryFollowJumpTrampoline.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryFollowJumpTrampoline.cs @@ -1,14 +1,12 @@ -// 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; +using static AsmArm64.Arm64RegisterX; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; -public partial class Arm64DisassemblerTests +public partial class Arm64DisassemblerTests : Arm64DisassemblerTestBase { private const int DmbIshldOffset = 4; @@ -245,4 +243,3 @@ public void TryFollowJumpTrampoline_NonStubHead_ShouldReturnFalse() result.Should().BeFalse(); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs index 006710b423..f5ef9f61b7 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryGetReferencedAddress.cs @@ -1,11 +1,10 @@ -// 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 Gee.External.Capstone.Arm64; using Microsoft.Diagnostics.Runtime.Interfaces; using static AsmArm64.Arm64RegisterX; +using static AsmArm64.Arm64InstructionFactory; +using Arm64Disassembler = BenchmarkDotNet.Disassemblers.Arm64Disassembler; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -13,17 +12,20 @@ public partial class Arm64DisassemblerTests { private Arm64RegisterValueAccumulator CreateValueAccumulator( IClrRuntime clrRuntime, - ulong initialValue, - Arm64RegisterId registerId = Arm64RegisterId.ARM64_REG_X0, + ulong initialValue = 0, + Arm64RegisterAny trackedRegister = default, Arm64RegisterValueAccumulator.State state = Arm64RegisterValueAccumulator.State.ExpectingMovk) { var valueAccumulator = new Arm64RegisterValueAccumulator(); valueAccumulator.Init(clrRuntime); - valueAccumulator.Reset(state: state, registerId: registerId, value: initialValue); + valueAccumulator.Reset(state: state, value: initialValue, register: trackedRegister); - valueAccumulator.HasValue.Should().BeTrue(); - valueAccumulator.Value.Should().Be(initialValue); - valueAccumulator.RegisterId.Should().Be(registerId); + if (initialValue != 0) + { + valueAccumulator.HasValue.Should().BeTrue(); + valueAccumulator.Value.Should().Be(initialValue); + } + valueAccumulator.TrackedRegister.Should().Be(trackedRegister); return valueAccumulator; } @@ -33,16 +35,17 @@ public void TryGetReferencedAddress_With_BR() { // Arrange using var clrRuntime = CreateMockClrRuntime(); - var valueAccumulator = CreateValueAccumulator(clrRuntime, initialValue: 0x100); - var rawInstruction = Arm64InstructionFactory.BR(X0); + var valueAccumulator = CreateValueAccumulator(clrRuntime, trackedRegister: X0, initialValue: 0x100); + var instruction = BR(X0).ToArm64Instruction(); // Act - var result = Arm64DisassemblerHelper.TryGetReferencedAddress( - rawInstruction.ToCapstoneArm64Instruction(), + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, valueAccumulator, pointerSize: 0, // This parameter is not used. - out ulong referencedAddress, - out bool isReferencedAddressIndirect); + out var referencedAddress, + out var isReferencedAddressIndirect); // Assert result.Should().BeTrue(); @@ -55,16 +58,17 @@ public void TryGetReferencedAddress_With_BLR() { // Arrange using var clrRuntime = CreateMockClrRuntime(); - var valueAccumulator = CreateValueAccumulator(clrRuntime, initialValue: 0x100); - var rawInstruction = Arm64InstructionFactory.BLR(X0); + var valueAccumulator = CreateValueAccumulator(clrRuntime, trackedRegister: X0, initialValue: 0x100); + var instruction = BLR(X0).ToArm64Instruction(); // Act - var result = Arm64DisassemblerHelper.TryGetReferencedAddress( - rawInstruction.ToCapstoneArm64Instruction(), + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, valueAccumulator, pointerSize: 0, // This parameter is not used. - out ulong referencedAddress, - out bool isReferencedAddressIndirect); + out var referencedAddress, + out var isReferencedAddressIndirect); // Assert result.Should().BeTrue(); @@ -79,20 +83,72 @@ public void TryGetReferencedAddress_WithAdrpThenBr_ResolvesPageAddress() using var clrRuntime = CreateMockClrRuntime(); var accumulator = new Arm64RegisterValueAccumulator(); accumulator.Init(clrRuntime); - accumulator.Feed(Arm64TestInstructions.Adrp(X0, 0x2000)); // adrp x0, #0x2000 - var rawInstruction = Arm64InstructionFactory.BR(X0); + accumulator.Feed(ADRP(X0, 0x2000).ToArm64Instruction(), DummyBaseAddress); // adrp x0, #0x2000 + var instruction = BR(X0).ToArm64Instruction(); // Act - var result = Arm64DisassemblerHelper.TryGetReferencedAddress( - rawInstruction.ToCapstoneArm64Instruction(), + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, accumulator, pointerSize: 0, // This parameter is not used. - out ulong referencedAddress, - out bool isReferencedAddressIndirect); + out var referencedAddress, + out var isReferencedAddressIndirect); // Assert result.Should().BeTrue(); - referencedAddress.Should().Be(0x2000); // no ADD between ADRP and BR, so the page address itself must resolve. + referencedAddress.Should().Be(DummyBaseAddress + 0x2000); // There is no ADD between ADRP and BR, so the page address itself must resolve. + isReferencedAddressIndirect.Should().BeTrue(); + } + + [Fact] + public void TryGetReferencedAddress_WithAdrpAtUnalignedAddress_ResolvesPageAlignedAddress() + { + // Arrange + const ulong AdrpAddress = DummyBaseAddress + 0x1234; // Not page aligned. + using var clrRuntime = CreateMockClrRuntime(); + var accumulator = new Arm64RegisterValueAccumulator(); + accumulator.Init(clrRuntime); + accumulator.Feed(ADRP(X0, 0x2000).ToArm64Instruction(), AdrpAddress); // adrp x0, #0x2000 + var instruction = BR(X0).ToArm64Instruction(); + + // Act + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, + accumulator, + pointerSize: 0, // This parameter is not used. + out var referencedAddress, + out var isReferencedAddressIndirect); + + // Assert + result.Should().BeTrue(); + referencedAddress.Should().Be(DummyBaseAddress + 0x3000); // ADRP is relative to the page containing the instruction. + isReferencedAddressIndirect.Should().BeTrue(); + } + + [Fact] + public void TryGetReferencedAddress_WithAdrpNegativeOffset_ResolvesPageAddress() + { + // Arrange + using var clrRuntime = CreateMockClrRuntime(); + var accumulator = new Arm64RegisterValueAccumulator(); + accumulator.Init(clrRuntime); + accumulator.Feed(ADRP(X0, -0x1000).ToArm64Instruction(), DummyBaseAddress + 0x1234); // adrp x0, #-0x1000 + var instruction = BR(X0).ToArm64Instruction(); + + // Act + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, + accumulator, + pointerSize: 0, // This parameter is not used. + out var referencedAddress, + out var isReferencedAddressIndirect); + + // Assert + result.Should().BeTrue(); + referencedAddress.Should().Be(DummyBaseAddress); // (DummyBaseAddress + 0x1000) - 0x1000 isReferencedAddressIndirect.Should().BeTrue(); } @@ -101,20 +157,45 @@ public void TryGetReferencedAddress_With_BranchRelative() { // Arrange using var clrRuntime = CreateMockClrRuntime(); - var valueAccumulator = CreateValueAccumulator(clrRuntime, initialValue: 0x100); + var valueAccumulator = CreateValueAccumulator(clrRuntime, trackedRegister: X0); var rawInstruction = Arm64InstructionFactory.B(0x200); + var instruction = Arm64Instruction.Decode(rawInstruction); + + // Act + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, + valueAccumulator, + pointerSize: 0, // This parameter is not used. + out var referencedAddress, + out var isReferencedAddressIndirect); + + // Assert + result.Should().BeTrue(); + referencedAddress.Should().Be(DummyBaseAddress + 0x200); + isReferencedAddressIndirect.Should().BeFalse(); + } + + [Fact] + public void TryGetReferencedAddress_With_BranchRelative_Minus() + { + // Arrange + using var clrRuntime = CreateMockClrRuntime(); + var valueAccumulator = CreateValueAccumulator(clrRuntime, trackedRegister: X0); + var instruction = B(-0x200).ToArm64Instruction(); // Act - var result = Arm64DisassemblerHelper.TryGetReferencedAddress( - rawInstruction.ToCapstoneArm64Instruction(DummyBaseAddress), + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, valueAccumulator, pointerSize: 0, // This parameter is not used. - out ulong referencedAddress, - out bool isReferencedAddressIndirect); + out var referencedAddress, + out var isReferencedAddressIndirect); // Assert result.Should().BeTrue(); - referencedAddress.Should().Be(DummyBaseAddress + 0x200); // Verify absolute address is returned. + referencedAddress.Should().Be(DummyBaseAddress - 0x200); isReferencedAddressIndirect.Should().BeFalse(); } @@ -123,16 +204,17 @@ public void TryGetReferencedAddress_DontMatchCondition_ShouldReturnFalse() { // Arrange using var clrRuntime = CreateMockClrRuntime(); - var valueAccumulator = CreateValueAccumulator(clrRuntime, initialValue: 0x100); - var rawInstruction = Arm64InstructionFactory.RET(); // `ret` instruction is not BranchRelative group + var valueAccumulator = CreateValueAccumulator(clrRuntime, trackedRegister: X0, initialValue: 0x0); + var instruction = RET().ToArm64Instruction(); // `ret` instruction is not BranchRelative group // Act - var result = Arm64DisassemblerHelper.TryGetReferencedAddress( - rawInstruction.ToCapstoneArm64Instruction(), + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, valueAccumulator, pointerSize: 0, // This parameter is not used. - out ulong referencedAddress, - out bool isReferencedAddressIndirect); + out var referencedAddress, + out var isReferencedAddressIndirect); // Assert result.Should().BeFalse(); @@ -143,19 +225,68 @@ public void TryGetReferencedAddress_With_BR_DifferentRegister_ShouldReturnFalse( { // Arrange using var clrRuntime = CreateMockClrRuntime(); - var valueAccumulator = CreateValueAccumulator(clrRuntime, initialValue: 0x100); - var rawInstruction = Arm64InstructionFactory.BR(X1); + var valueAccumulator = CreateValueAccumulator(clrRuntime, trackedRegister: X0, initialValue: 0x100); + var instruction = BR(X1).ToArm64Instruction(); // Act - var result = Arm64DisassemblerHelper.TryGetReferencedAddress( - rawInstruction.ToCapstoneArm64Instruction(), + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, valueAccumulator, pointerSize: 0, // This parameter is not used. - out ulong referencedAddress, - out bool isReferencedAddressIndirect); + out var referencedAddress, + out var isReferencedAddressIndirect); // Assert result.Should().BeFalse(); } + + [Fact] + public void TryGetReferencedAddress_With_TBZ_ResolvesLabelOffset() + { + // Arrange + using var clrRuntime = CreateMockClrRuntime(); + var valueAccumulator = CreateValueAccumulator(clrRuntime, trackedRegister: X0); + var rawInstruction = Arm64InstructionFactory.TBZ(X0, 3, new Arm64LabelOffset(0x200)); // tbz x0, #3, #0x200 + var instruction = Arm64Instruction.Decode(rawInstruction); + + // Act + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, + valueAccumulator, + pointerSize: 0, // This parameter is not used. + out var referencedAddress, + out var isReferencedAddressIndirect); + + // Assert + result.Should().BeTrue(); + // The bit position imm value (3) is ignored. + referencedAddress.Should().Be(DummyBaseAddress + 0x200); + isReferencedAddressIndirect.Should().BeFalse(); + } + + [Fact] + public void TryGetReferencedAddress_With_TBNZ_ResolvesLabelOffset() + { + // Arrange + using var clrRuntime = CreateMockClrRuntime(); + var valueAccumulator = CreateValueAccumulator(clrRuntime, trackedRegister: X0); + var instruction = TBNZ(X0, 3, new Arm64LabelOffset(0x200)).ToArm64Instruction(); + + // Act + var result = Arm64Disassembler.TryGetReferencedAddress( + instruction, + DummyBaseAddress, + valueAccumulator, + pointerSize: 0, // This parameter is not used. + out var referencedAddress, + out var isReferencedAddressIndirect); + + // Assert + // The bit position (3) must not be mistaken for the branch target. + result.Should().BeTrue(); + referencedAddress.Should().Be(DummyBaseAddress + 0x200); + isReferencedAddressIndirect.Should().BeFalse(); + } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryReadStubHead.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryReadStubHead.cs index 02ab988298..35722d907d 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryReadStubHead.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryReadStubHead.cs @@ -1,7 +1,6 @@ -// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. -#if NET using AsmArm64; using AwesomeAssertions; +using Arm64Disassembler = BenchmarkDotNet.Disassemblers.Arm64Disassembler; using Arm64RegisterX = AsmArm64.Arm64RegisterX; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -28,7 +27,7 @@ public void TryReadStubHead() var dataReader = clrRuntime.DataTarget.DataReader; // Act - var result = Arm64DisassemblerHelper.TryReadStubHead( + var result = Arm64Disassembler.TryReadStubHead( dataReader, BaseAddress, out ulong parseBase, @@ -63,7 +62,7 @@ public void TryReadStubHead_PreDotNet10() var dataReader = clrRuntime.DataTarget.DataReader; // Act - var result = Arm64DisassemblerHelper.TryReadStubHead( + var result = Arm64Disassembler.TryReadStubHead( dataReader, BaseAddress, out ulong parseBase, @@ -99,7 +98,7 @@ public void TryReadStubHead_InsufficientInstructions_ShouldReturnFalse() var dataReader = clrRuntime.DataTarget.DataReader; // Act - var result = Arm64DisassemblerHelper.TryReadStubHead( + var result = Arm64Disassembler.TryReadStubHead( dataReader, BaseAddress, out ulong parseBase, @@ -130,7 +129,7 @@ public void TryReadStubHead_PreDotNet10_InsufficientInstructions_ShouldReturnFal var dataReader = clrRuntime.DataTarget.DataReader; // Act - var result = Arm64DisassemblerHelper.TryReadStubHead( + var result = Arm64Disassembler.TryReadStubHead( dataReader, BaseAddress, out ulong parseBase, @@ -142,4 +141,3 @@ public void TryReadStubHead_PreDotNet10_InsufficientInstructions_ShouldReturnFal result.Should().BeFalse(); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryResolvePreCode.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryResolvePreCode.cs index 22ff8b694c..1366da854f 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryResolvePreCode.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryResolvePreCode.cs @@ -1,9 +1,8 @@ -// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. -#if NET using AsmArm64; using AwesomeAssertions; using static AsmArm64.Arm64RegisterW; using static AsmArm64.Arm64RegisterX; +using Arm64Disassembler = BenchmarkDotNet.Disassemblers.Arm64Disassembler; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -34,7 +33,7 @@ public void TryResolvePrecode_StubPrecode() // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeTrue(); @@ -68,7 +67,7 @@ public void TryResolvePrecode_FixupPrecode() // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeTrue(); @@ -102,7 +101,7 @@ public void TryResolvePrecode_FixupPrecodeCode_Fixup() // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeTrue(); @@ -134,7 +133,7 @@ public void TryResolvePrecode_CallCountingStub() // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeTrue(); @@ -161,7 +160,7 @@ public void TryResolvePrecode_NoStubHead_ShouldBeFalse() // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeFalse(); @@ -188,7 +187,7 @@ public void TryResolvePrecode_NoMatch_ShouldBeFalse() // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeFalse(); @@ -220,7 +219,7 @@ public void TryResolvePrecode_StubPrecode_FailedToRead_ShouldBeFalse() // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeFalse(); @@ -252,7 +251,7 @@ public void TryResolvePrecode_FixupPrecode_FailedToRead_ShouldBeFalse() // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeFalse(); @@ -284,7 +283,7 @@ public void TryResolvePrecode_FixupPrecodeCode_Fixup_FailedToRead_ShouldBeFalse( // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeFalse(); @@ -314,10 +313,9 @@ public void TryResolvePrecode_CallCountingStub_FailedToRead_ShouldBeFalse() // Act ulong address = BaseAddress; - var result = Arm64DisassemblerHelper.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); + var result = Arm64Disassembler.TryResolvePrecode(dataReader, ref address, out var isPrestubMd); // Assert result.Should().BeFalse(); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryTranslateAddressToName.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryTranslateAddressToName.cs index c2c33f0c5d..81a77501ad 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryTranslateAddressToName.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64DisassemblerTests.TryTranslateAddressToName.cs @@ -1,5 +1,3 @@ -// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. -#if NET using AsmArm64; using AwesomeAssertions; using BenchmarkDotNet.Disassemblers; @@ -316,4 +314,3 @@ public void TryTranslateAddressToName_GetTypeByMethodTable() state.Todo.Should().BeEmpty(); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.ShiftKinds.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.ShiftKinds.cs new file mode 100644 index 0000000000..e7254ace5f --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.ShiftKinds.cs @@ -0,0 +1,79 @@ +using AsmArm64; +using AwesomeAssertions; +using static AsmArm64.Arm64InstructionFactory; +using static AsmArm64.Arm64RegisterX; +using BenchmarkDotNet.Disassemblers; + + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64InstructionExtensionsTests +{ + [Theory] + [InlineData(Arm64ShiftKind.LSL, 0, 0x1234L, 0x1234L)] + [InlineData(Arm64ShiftKind.LSL, 4, 0x1234L, 0x12340L)] + [InlineData(Arm64ShiftKind.LSL, 12, 0x123L, 0x123000L)] + public void TryShiftImmediate_Lsl_ShouldShiftLeft(Arm64ShiftKind kind, byte amount, long value, long expected) + { + // Act + var result = Arm64InstructionExtensions.TryShiftImmediateValue(value, kind, amount, out var shifted); + + // Assert + result.Should().BeTrue(); + shifted.Should().Be(expected); + } + + [Theory] + [InlineData(Arm64ShiftKind.LSR, 0, 0x1234L, 0x1234L)] + [InlineData(Arm64ShiftKind.LSR, 4, 0x1234L, 0x123L)] + [InlineData(Arm64ShiftKind.LSR, 4, -1L, 0x0FFFFFFFFFFFFFFFL)] + public void TryShiftImmediate_Lsr_ShouldShiftRightLogically(Arm64ShiftKind kind, byte amount, long value, long expected) + { + // Act + var result = Arm64InstructionExtensions.TryShiftImmediateValue(value, kind, amount, out var shifted); + + // Assert + result.Should().BeTrue(); + shifted.Should().Be(expected); + } + + [Theory] + [InlineData(Arm64ShiftKind.ASR, 0, 0x1234L, 0x1234L)] + [InlineData(Arm64ShiftKind.ASR, 4, 0x1234L, 0x123L)] + [InlineData(Arm64ShiftKind.ASR, 4, -16L, -1L)] + public void TryShiftImmediate_Asr_ShouldShiftRightArithmetically(Arm64ShiftKind kind, byte amount, long value, long expected) + { + // Act + var result = Arm64InstructionExtensions.TryShiftImmediateValue(value, kind, amount, out var shifted); + + // Assert + result.Should().BeTrue(); + shifted.Should().Be(expected); + } + + [Theory] + [InlineData(Arm64ShiftKind.ROR, 0, 0x1234L, 0x1234L)] + [InlineData(Arm64ShiftKind.ROR, 4, 0x1234L, 0x4000000000000123L)] + public void TryShiftImmediate_Ror_ShouldRotateRight(Arm64ShiftKind kind, byte amount, long value, long expected) + { + // Act + var result = Arm64InstructionExtensions.TryShiftImmediateValue(value, kind, amount, out var shifted); + + // Assert + result.Should().BeTrue(); + shifted.Should().Be(expected); + } + + [Theory] + [InlineData(Arm64ShiftKind.MSL)] + [InlineData(Arm64ShiftKind.Invalid)] + public void TryShiftImmediate_UnsupportedKind_ShouldReturnFalse(Arm64ShiftKind kind) + { + // Act + var result = Arm64InstructionExtensions.TryShiftImmediateValue(0x1234, kind, 4, out var shifted); + + // Assert + result.Should().BeFalse(); + shifted.Should().Be(0); + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.ShiftedImmediate.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.ShiftedImmediate.cs new file mode 100644 index 0000000000..0af94efdf7 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.ShiftedImmediate.cs @@ -0,0 +1,68 @@ +using AsmArm64; +using AwesomeAssertions; +using static AsmArm64.Arm64InstructionFactory; +using static AsmArm64.Arm64RegisterX; +using BenchmarkDotNet.Disassemblers; + + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64InstructionExtensionsTests +{ + + [Fact] + public void TryGetShiftedImmediateValue_WithNoShiftOperand_ShouldReturnImmediate() + { + // Arrange + var instruction = Arm64Instruction.Decode(ADD(X0, X0, 0x05)); // add x0, x0, #5 + + // Act + var result = instruction.TryGetShiftedImmediateValue(2, out var value); + + // Assert + result.Should().BeTrue(); + value.Should().Be(5); + } + + [Fact] + public void TryGetShiftedImmediateValue_WithLslShift_ShouldApplyShift() + { + // Arrange + var instruction = Arm64Instruction.Decode(ADD(X0, X0, 0x05, amount: 12)); // add x0, x0, #5, lsl #12 + + /// Act + var result = instruction.TryGetShiftedImmediateValue(2, out var value); + + // Assert + result.Should().BeTrue(); + value.Should().Be(5L << 12); + } + + [Fact] + public void TryGetShiftedImmediateValue_WithOutOfRangeIndex_ShouldReturnFalse() + { + // Arrange + var instruction = Arm64Instruction.Decode(ADD(X0, X0, 0x05)); // add x0, x0, #5 + + // Act + var result = instruction.TryGetShiftedImmediateValue(99, out var value); + + // Assert + result.Should().BeFalse(); + value.Should().Be(0); + } + + [Fact] + public void TryGetShiftedImmediateValue_WithNonImmediateOperand_ShouldReturnFalse() + { + // Arrange + var instruction = Arm64Instruction.Decode(ADD(X0, X0, 0x05)); // add x0, x0, #5 + + // Act + var result = instruction.TryGetShiftedImmediateValue(0, out var value); + + // Assert + result.Should().BeFalse(); + value.Should().Be(0); + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.cs new file mode 100644 index 0000000000..9e5e1a7f67 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionExtensionsTests.cs @@ -0,0 +1,12 @@ +using AsmArm64; +using AwesomeAssertions; +using static AsmArm64.Arm64InstructionFactory; +using static AsmArm64.Arm64RegisterX; +using BenchmarkDotNet.Disassemblers; + + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64InstructionExtensionsTests +{ +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.IntructionPatterns.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.IntructionPatterns.cs new file mode 100644 index 0000000000..428b4dea90 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.IntructionPatterns.cs @@ -0,0 +1,60 @@ +using AsmArm64; +using AwesomeAssertions; +using BenchmarkDotNet.Disassemblers; +using static AsmArm64.Arm64RegisterW; +using static AsmArm64.Arm64RegisterX; +using static AsmArm64.Arm64RegisterSP; +using static AsmArm64.Arm64InstructionFactory; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64InstructionFormatterTests : Arm64DisassemblerTestBase +{ + [Theory] + [MemberData(nameof(TestData.FormatOptionPatterns), MemberType = typeof(TestData))] + public void FormatInstruction(uint rawInstruction, string expected) + { + // Arrange + var asm = ToArm64Asm(rawInstruction, address: 0); + + // Act + var result = Format(asm); + + // Assert + result.Should().Be(expected); + } + + public static partial class TestData + { + public static TheoryData FormatOptionPatterns => new() + { + // Test cases to validate LabelOffsetFormat + { B(0x8), "b #8"}, // single digits representable value should be rendered with decimal format. + { B(0x100), "b #0x100"}, + { B(0xC), "b #0xC"}, + + // Test cases to validate ImmediateFormat + { MOVZ(W0, 0x8), "movz w0, #8"}, + { MOVZ(X0, 0x100), "movz x0, #0x100"}, + { MOVZ(X0, 0xFFF), "movz x0, #0xFFF"}, + { MOVZ(X0, 0x100, amount:16), "movz x0, #0x100, lsl #16"}, // Shift amount should be rendered as decimal. + + // Test cases to validate MemoryOffsetFormat + { LDR(X0, new Arm64ImmediateMemoryAccessor(SP, 0x8)), "ldr x0, [sp, #8]" }, + { LDR(X0, new Arm64ImmediateMemoryAccessor(SP, 0x100)), "ldr x0, [sp, #0x100]" }, + { LDR(X0, new Arm64ImmediateMemoryAccessor(SP, 0xf00)), "ldr x0, [sp, #0xF00]" }, + + // Test cases to validate minus label offset. + { B(-0x8), "b #-8"}, + { B(-0x100), "b #-0x100"}, + { ADRP(X0, -0x1000), "adrp x0, #-0x1000"}, + + // Test case to validate instruction aliases. + { CMP(X0,0x00), "cmp x0, #0"}, + { SUBS(XZR, X0, 0x00), "cmp x0, #0"}, // Resolved to CMP alias + + // Others + { NOP(), "nop"}, + }; + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.LabelResolution.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.LabelResolution.cs new file mode 100644 index 0000000000..3181b3b4d0 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.LabelResolution.cs @@ -0,0 +1,77 @@ +using AsmArm64; +using AwesomeAssertions; +using static AsmArm64.Arm64RegisterX; +using static AsmArm64.Arm64InstructionFactory; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64InstructionFormatterTests : Arm64DisassemblerTestBase +{ + [Theory] + [InlineData(0x2000, "adrp x0, #0xF00000003000")] + [InlineData(-0x1000, "adrp x0, #0xF00000000000")] + public void FormatInstruction_WithAdrp_ResolvesLabelRelativeToContainingPage(int labelOffset, string expected) + { + // Arrange + const ulong InstructionAddress = DummyBaseAddress + 0x1234; // Not page aligned. + var asm = ToArm64Asm(ADRP(X0, labelOffset), InstructionAddress); + + // Act + var result = Format(asm); + + // Assert + result.Should().Be(expected); + } + + [Fact] + public void FormatInstruction_WithLdrLiteral_ReplacesAddressWithLabel() + { + // Arrange + var asm = ToArm64Asm(LDR(X0, 0x100), DummyBaseAddress); // ldr x0, #0x100 + var symbols = new Dictionary + { + [DummyBaseAddress + 0x100] = "M00_L00", + }; + + // Act + var result = Format(asm, addressMappings: symbols); + + // Assert + result.Should().Be("ldr x0, M00_L00"); + } + + [Theory] + [InlineData(-4, "b #-4")] + [InlineData(-8, "b #-8")] + [InlineData(-12, "b #-0xC")] + [InlineData(-16, "b #-0x10")] + public void FormatInstruction_WithBranchNegativeSmallOffset_AtZeroAddress_KeepsRawOffset(int labelOffset, string expected) + { + // Arrange + var asm = ToArm64Asm(B(new Arm64LabelOffset(labelOffset)), 0); + + // Act + var result = Format(asm); + + // Assert + result.Should().Be(expected); + } + + [Fact] + public void FormatInstruction_WithLongSymbolName_ResolvesLabel() + { + // Arrange + var symbolName = new string('x', 512); + var asm = ToArm64Asm(LDR(X0, 0x100), DummyBaseAddress); // ldr x0, #0x100 + var symbols = new Dictionary + { + [DummyBaseAddress + 0x100] = symbolName, + }; + + // Act + var result = Format(asm, addressMappings: symbols); + + // Assert + result.Should().Be($"ldr x0, {symbolName}"); + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.MnemonicAliases.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.MnemonicAliases.cs new file mode 100644 index 0000000000..b607be3008 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.MnemonicAliases.cs @@ -0,0 +1,62 @@ +using AsmArm64; +using AwesomeAssertions; +using AwesomeAssertions.Execution; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Disassemblers; +using Iced.Intel; +using System.Security.Cryptography; +using static AsmArm64.Arm64InstructionFactory; +using static AsmArm64.Arm64RegisterD; +using static AsmArm64.Arm64RegisterSP; +using static AsmArm64.Arm64RegisterW; +using static AsmArm64.Arm64RegisterX; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64InstructionFormatterTests : Arm64DisassemblerTestBase +{ + [Theory] + [MemberData(nameof(TestData.MnemonicPatterns), MemberType = typeof(TestData))] + public void VerifyMnemonicAlias(string expected, bool useBaseInstruction, uint rawInstruction) + { + // Arrange + var asm = ToArm64Asm(rawInstruction, address: 0); + + // Act + var result = Arm64InstructionFormatter.GetInstructionDetails(asm.Instruction, asm.InstructionPointer).Mnemonic; + + // Assert + result.Should().Be(expected); + + var instruction = Arm64Instruction.Decode(rawInstruction); + if (useBaseInstruction) + { + result.Should().NotBe(instruction.Mnemonic.ToText()); + instruction = instruction.AsBaseInstruction(); + } + result.Should().Be(instruction.Mnemonic.ToText()); + } + + public static partial class TestData + { + public static TheoryData MnemonicPatterns => new() + { + { "mov", false, MOV(X0, 1) }, // `Arm64InstructionFactory.MOV` don't accept `0`. See: https://github.com/xoofx/AsmArm64/issues/16 + { "movi", false, MOVI(D0, 0) }, + { "movk", false, MOVK(X0, 0) }, + + // Folowing instructions are resolved to `mov` alias by default. + { "movn", true, MOVN(X0, 0) }, + { "movn", true, MOV_MOVN(X0, new Arm64InvertedShiftedImmediate64(0, 0)) }, + { "movz", true, MOVZ(X0, 0) }, + + // Folowing instructions are resolved to `subs` by default. + { "cmp", false, CMP(X0, X1) }, + { "cmn", false, CMN(X0, X1) }, + + // Other instructions are resolved with default setting(Arm64InstructionAliasMode.Preferred) + { "retaa", false, RETAA() }, + }; + } +} + diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.Scenario.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.Scenario.cs new file mode 100644 index 0000000000..d49b256bc3 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.Scenario.cs @@ -0,0 +1,80 @@ +using AsmArm64; +using AwesomeAssertions; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Disassemblers; +using static AsmArm64.Arm64RegisterW; +using static AsmArm64.Arm64RegisterX; +using static AsmArm64.Arm64RegisterSP; +using static AsmArm64.Arm64InstructionFactory; +using AwesomeAssertions.Execution; + +namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; + +public partial class Arm64InstructionFormatterTests : Arm64DisassemblerTestBase +{ + [Fact] + public void FormatInstruction_IntroDisassembly_SumLocal() + { + // Arrange + var rawInstructions = new[] + { + STP(X29, X30, new Arm64ImmediateMemoryAccessor(SP, -0x10).Pre), // stp x29, x30, [sp, #-0x10]! + MOV(X29, SP), // mov x29, sp + LDR(X0, new Arm64ImmediateMemoryAccessor(X0, 8)), // ldr x0, [x0, #8] + MOV(W1, WZR), // mov w1, wzr + LDR(W2, new Arm64ImmediateMemoryAccessor(X0, 8)), // ldr w2, [x0, #8] + CMP(W2, 0), // cmp w2, #0 + B(Arm64ConditionalKind.LE, 0x18), // b.le #0x18 + ADD(X0,X0, 0x10), // add x0, x0, #0x10 + // M00_L00: + LDR(W3, new Arm64BaseMemoryAccessor(X0), 4), // ldr w3, [x0], #4 + ADD(W1,W3,W1), // add w1, w3, w1 + SUB(W2,W2, 1), // sub w2, w2, #1 + CBNZ(W2, -0x0C), // cbnz w2, #-0xC + // M00_L01: + MOV(W0, W1), // mov w0, w1 + LDP(X29, X30, new Arm64BaseMemoryAccessor(SP), 0x10), // ldp x29, x30, [sp], #0x10 + RET(), + }; + PrintInstructions(rawInstructions); + + const ulong BaseAddress = 0xFF01097000A0; + var symbols = new Dictionary + { + [0xFF01097000C0] = "M00_L00", + [0xFF01097000D0] = "M00_L01", + }; + Arm64Asm[] asms = Decode(rawInstructions, BaseAddress, symbols); + + // Act + List results = new(); + foreach (var asm in asms) + { + var result = Arm64InstructionFormatter.Format(asm, IcedDefaultFormatterOptions, printInstructionAddresses: true, pointerSize: 8, symbols); + results.Add(result); + } + + // Assert + using (var scope = new AssertionScope()) + { + scope.FormattingOptions.StringPrintLength = 100; + results[00].Should().Be("FF01097000A0 stp x29, x30, [sp, #-0x10]!"); + results[01].Should().Be("FF01097000A4 mov x29, sp"); + results[02].Should().Be("FF01097000A8 ldr x0, [x0, #8]"); + results[03].Should().Be("FF01097000AC mov w1, wzr"); + results[04].Should().Be("FF01097000B0 ldr w2, [x0, #8]"); + results[05].Should().Be("FF01097000B4 cmp w2, #0"); + results[06].Should().Be("FF01097000B8 b.le M00_L01"); + results[07].Should().Be("FF01097000BC add x0, x0, #0x10"); + // M00_L00: + results[08].Should().Be("FF01097000C0 ldr w3, [x0], #4"); + results[09].Should().Be("FF01097000C4 add w1, w3, w1"); + results[10].Should().Be("FF01097000C8 sub w2, w2, #1"); + results[11].Should().Be("FF01097000CC cbnz w2, M00_L00"); + // M00_L01: + results[12].Should().Be("FF01097000D0 mov w0, w1"); + results[13].Should().Be("FF01097000D4 ldp x29, x30, [sp], #0x10"); + results[14].Should().Be("FF01097000D8 ret"); + } + } +} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.cs index e2e2c1dc74..d5a876d8c9 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64InstructionFormatterTests.cs @@ -1,122 +1,29 @@ -// TODO: Remove #if directive when migrated to xunit.v3 or migrated to AsmArm64 based implementation. -#if NET using AsmArm64; -using AwesomeAssertions; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Disassemblers; -using Iced.Intel; -using static AsmArm64.Arm64RegisterW; -using static AsmArm64.Arm64RegisterX; + namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; public partial class Arm64InstructionFormatterTests : Arm64DisassemblerTestBase { - private static readonly FormatterOptions DefaultFormatterOptions = new() + private static readonly Iced.Intel.FormatterOptions IcedDefaultFormatterOptions = new() { FirstOperandCharIndex = 10, // Use DisassemblyDiagnoserConfig default config value. }; - private static readonly Arm64InstructionFormattingOptions AsmArm64FormattingOptions = new() - { - AliasMode = Arm64InstructionAliasMode.BaseInstruction, - UseUppercaseText = false, - UseUppercaseHex = false, - ImmediateFormat = Arm64NumericFormat.Hexadecimal, - MemoryOffsetFormat = Arm64NumericFormat.Hexadecimal, - LabelOffsetFormat = Arm64NumericFormat.Auto, - }; + private static readonly Iced.Intel.FormatterOptions IcedEmptyFormatterOptions = new(); + + private static readonly Arm64InstructionFormattingOptions FormattingOptions = Arm64InstructionFormatter.DefaultFormattingOptions; public Arm64InstructionFormatterTests(ITestOutputHelper output) : base(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(new FormatterOptions()); - - // Assert - result.Should().Be(expected); - - // Verify AsmArm64 compatibility - var instruction = rawInstruction.ToAsmArm64Instruction(); - var text = instruction.ToString(AsmArm64FormattingOptions); - - text.Should().Be(expected); - } - - [Fact] - public void FormatInstruction_IntroDisassembly_SumLocal() - { - // Arrange - var rawInstructions = new[] - { - Arm64InstructionFactory.STP(X29, X30, new Arm64ImmediateMemoryAccessor(Arm64RegisterSP.SP, -0x10).Pre), - Arm64InstructionFactory.MOV(X29, Arm64RegisterSP.SP), - Arm64InstructionFactory.LDR(X0, new Arm64ImmediateMemoryAccessor(X0, 8)), - Arm64InstructionFactory.MOV(W1, WZR), - Arm64InstructionFactory.LDR(W2, new Arm64ImmediateMemoryAccessor(X0, 8)), - Arm64InstructionFactory.CMP(W2, 0), - Arm64InstructionFactory.B(Arm64ConditionalKind.LE, 24), - Arm64InstructionFactory.ADD(X0,X0, 0x10), - // M00_L00: - Arm64InstructionFactory.LDR(W3, new Arm64BaseMemoryAccessor(X0), 4), - Arm64InstructionFactory.ADD(W1,W3,W1), - Arm64InstructionFactory.SUB(W2,W2, 1), - Arm64InstructionFactory.CBNZ(W2, -12), - // M00_L01: - Arm64InstructionFactory.MOV(W0, W1), - Arm64InstructionFactory.LDP(X29, X30, new Arm64BaseMemoryAccessor(Arm64RegisterSP.SP), 0x10), - Arm64InstructionFactory.RET(), - }; - PrintInstructions(rawInstructions); - - const ulong BaseAddress = 0xFF01097000A0; - var symbols = new Dictionary - { - [0xFF01097000C0] = "M00_L00", - [0xFF01097000D0] = "M00_L01", - }; - Arm64Asm[] asms = GetArm64Asms(rawInstructions, BaseAddress, symbols); - - // Act - List results = new(); - foreach (var asm in asms) - { - var result = Arm64InstructionFormatter.Format(asm, DefaultFormatterOptions, printInstructionAddresses: true, pointerSize: 8, symbols); - results.Add(result); - } - - // Assert - results.Should().BeEquivalentTo( - [ - "FF01097000A0 stp x29, x30, [sp, #-0x10]!", - "FF01097000A4 mov x29, sp", - "FF01097000A8 ldr x0, [x0, #8]", - "FF01097000AC mov w1, wzr", - "FF01097000B0 ldr w2, [x0, #8]", - "FF01097000B4 cmp w2, #0", - "FF01097000B8 b.le M00_L01", - "FF01097000BC add x0, x0, #0x10", - // M00_L00: - "FF01097000C0 ldr w3, [x0], #4", - "FF01097000C4 add w1, w3, w1", - "FF01097000C8 sub w2, w2, #1", - "FF01097000CC cbnz w2, M00_L00", - // M00_L01: - "FF01097000D0 mov w0, w1", - "FF01097000D4 ldp x29, x30, [sp], #0x10", - "FF01097000D8 ret", - ], options => options.WithStrictOrdering()); - } - - private static Arm64Asm[] GetArm64Asms(uint[] rawInstructions, ulong baseAddress, Dictionary symbols) + private static Arm64Asm[] Decode( + uint[] rawInstructions, + ulong baseAddress, + Dictionary symbols) { using var clrRuntime = CreateMockClrRuntime(); var state = new State(clrRuntime, DummyTargetFrameworkVersion); @@ -130,38 +37,11 @@ private static Arm64Asm[] GetArm64Asms(uint[] rawInstructions, ulong baseAddress return helper.Decode(bytes, baseAddress, state, depth: 0, DummyCurrentMethod, DisassemblySyntax.Masm); } - 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"}, - { Arm64InstructionFactory.MOVZ(W0, 0x8), "movz w0, #0x8"}, - { Arm64InstructionFactory.MOVZ(X0, 0x100), "movz x0, #0x100"}, - { Arm64InstructionFactory.MOVZ(X0, 0xFFF), "movz x0, #0xfff"}, - { Arm64InstructionFactory.MOVZ(X0, 0x100, amount:16), "movz x0, #0x100, lsl #16"}, - - // TODO: `Gee.External.Capstone` seems not support negative label offset and wrong text is outputted. - // { Arm64InstructionFactory.B(-8), "b #-8"}, // Gee.External.Capstone don't support minus label offset. - - { Arm64InstructionFactory.NOP(), "nop"}, - }; - } -} - -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) + private static Arm64Asm ToArm64Asm( + uint rawInstruction, + ulong address, + ulong? referencedAddress = null, + bool isReferencedAddressIndirect = false) { if (referencedAddress <= ushort.MaxValue) referencedAddress = null; @@ -169,11 +49,20 @@ public static Arm64Asm ToArm64Asm(this uint rawInstruction, ulong address, ulong return new Arm64Asm { InstructionPointer = address, - InstructionLength = 4, + RawInstruction = rawInstruction, ReferencedAddress = referencedAddress, IsReferencedAddressIndirect = isReferencedAddressIndirect, - Instruction = rawInstruction.ToCapstoneArm64Instruction(address), + DisassembleSyntax = DisassembleSyntax.Gas, }; } + + private static string Format( + Arm64Asm asm, + Iced.Intel.FormatterOptions? formatterOptions = null, + bool printInstructionAddresses = false, + Dictionary? addressMappings = null) + { + formatterOptions ??= IcedEmptyFormatterOptions; + return Arm64InstructionFormatter.Format(asm, formatterOptions, printInstructionAddresses, pointerSize: 8, addressMappings ?? []); + } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs index 863074c770..1b403d688a 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.AdrpThenAdd.cs @@ -1,8 +1,6 @@ -// 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; +using static AsmArm64.Arm64InstructionFactory; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -14,11 +12,12 @@ public void AdrpThenAdd_ShouldCalculateAddress() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Adrp(X0, 0x2000), // adrp x0, #0x2000 - Arm64TestInstructions.Add(X0, X0, 0x123), // add x0, x0, #0x123 + ADRP(X0, 0x2000), // adrp x0, #0x2000 + ADD(X0, X0, 0x123), // add x0, x0, #0x123 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -28,7 +27,7 @@ public void AdrpThenAdd_ShouldCalculateAddress() // Assert accumulator.HasValue.Should().BeTrue(); accumulator.Value.Should().Be(0x2123L); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); } [Fact] @@ -36,11 +35,12 @@ public void AdrpThenAdd_WithNegativeOffset_ShouldClampToZero() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Adrp(X0, -0x1000), // adrp x0, #-0x1000 - Arm64TestInstructions.Add(X0, X0, 0x100), // add x0, x0, #0x100 + ADRP(X0, -0x1000), // adrp x0, #-0x1000 + ADD(X0, X0, 0x100), // add x0, x0, #0x100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Note: Capstone print minus offset as `0xFFFFFFFFFFFFF000` // Act @@ -57,12 +57,13 @@ public void AdrpThenAdd_WithNegativeOffset_ShouldClampToZero() public void AdrpThenAdd_ThenAdd_ShouldResetValue() { var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = 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 + ADRP(X0, 0x1000), // adrp x0, #0x1000 + ADD(X0, X0, 0x100), // add x0, x0, #0x100 + ADD(X0, X0, 0x200), // add x0, x0, #0x200 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -79,11 +80,12 @@ public void AdrpThenAdd_WithShiftedImm_ShouldCalculateAddress() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Adrp(X0, 0x1000000), // adrp x0, #0x1000000 - Arm64TestInstructions.Add(X0, X0, 0xFFF, 12), // add x0, x0, #0xFFF, lsl #12 + ADRP(X0, 0x1000000), // adrp x0, #0x1000000 + ADD(X0, X0, 0xFFF, amount:12), // add x0, x0, #0xFFF, lsl #12 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -100,11 +102,12 @@ public void AdrpThenAdd_DifferentDestinationRegister_ShouldKeepValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Adrp(X0, 0x2000), // adrp x0, #0x2000 - Arm64TestInstructions.Add(X1, X0, 0x100), // add x1, x0, #0x100 + ADRP(X0, 0x2000), // adrp x0, #0x2000 + ADD(X1, X0, 0x100), // add x1, x0, #0x100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -115,7 +118,7 @@ public void AdrpThenAdd_DifferentDestinationRegister_ShouldKeepValue() // The ADD writes X1, so the tracked X0 value (page address) is still valid. accumulator.HasValue.Should().BeTrue(); accumulator.Value.Should().Be(0x2000UL); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); } [Fact] @@ -123,12 +126,13 @@ public void AdrpThenOther_ThenAdd_ShouldResetValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Adrp(X0, 0x1000), // adrp x0, #0x1000 - Arm64TestInstructions.Movz(X0, 0x100), // movz x0, #0x100 - Arm64TestInstructions.Add(X0, X0, 0x100), // add x1, x0, #0x100 + ADRP(X0, 0x1000), // adrp x0, #0x1000 + MOVZ(X0, 0x100), // movz x0, #0x100 + ADD(X0, X0, 0x100), // add x1, x0, #0x100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -140,4 +144,3 @@ public void AdrpThenOther_ThenAdd_ShouldResetValue() 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 index 913c8e064c..840a252418 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs @@ -1,8 +1,6 @@ -// 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; +using static AsmArm64.Arm64InstructionFactory; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -14,12 +12,13 @@ public void Movz_WithShiftedImmediateValue_ShouldStartNewValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = 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 + MOVZ(X0, 0x1111, amount: 0), // movz x0, #0x1111 + MOVZ(X0, 0x2222, amount: 16), // movz x0, #0x2222, lsl #16 + MOVZ(X0, 0x3333, amount: 32), // movz x0, #0x3333, lsl #32 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -29,7 +28,7 @@ public void Movz_WithShiftedImmediateValue_ShouldStartNewValue() // Assert accumulator.HasValue.Should().BeTrue(); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); accumulator.Value.Should().Be(0x3333_0000_0000); } @@ -38,10 +37,11 @@ public void Movz_ZeroImmediate_ShouldNotHaveValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x0), // movz x0, #0x0 + MOVZ(X0, 0x0), // movz x0, #0x0 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -52,4 +52,3 @@ public void Movz_ZeroImmediate_ShouldNotHaveValue() accumulator.Value.Should().Be(0UL); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenBranchConditional.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenBranchConditional.cs index f10daf20c9..a3efd7169c 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenBranchConditional.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenBranchConditional.cs @@ -1,9 +1,7 @@ -// 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; +using static AsmArm64.Arm64InstructionFactory; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -15,11 +13,12 @@ public void MovzThenCbz_ShouldKeepValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 - Arm64TestInstructions.Cbz(X0, 0x0100), // cbz x0, #0x0100 + MOVZ(X0, 0x1000), // movz x0, #0x1000 + CBZ(X0, 0x0100), // cbz x0, #0x0100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -29,7 +28,7 @@ public void MovzThenCbz_ShouldKeepValue() // Assert accumulator.HasValue.Should().BeTrue(); accumulator.Value.Should().Be(0x1000); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); } [Fact] @@ -37,11 +36,12 @@ public void MovzThenCbnz_ShouldKeepValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 - Arm64TestInstructions.Cbnz(X0, 0x0100), // cbnz x0, #0x0100 + MOVZ(X0, 0x1000), // movz x0, #0x1000 + CBNZ(X0, 0x0100), // cbnz x0, #0x0100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -51,7 +51,7 @@ public void MovzThenCbnz_ShouldKeepValue() // Assert accumulator.HasValue.Should().BeTrue(); accumulator.Value.Should().Be(0x1000); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); } [Fact] @@ -59,11 +59,12 @@ public void MovzThenConditionalB_ShouldKeepValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 - Arm64TestInstructions.B(Arm64ConditionalKind.EQ, 0x100), // b.eq x0, #0x0100 + MOVZ(X0, 0x1000), // movz x0, #0x1000 + B(Arm64ConditionalKind.EQ, 0x100), // b.eq x0, #0x0100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -73,7 +74,6 @@ public void MovzThenConditionalB_ShouldKeepValue() // 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); + accumulator.TrackedRegister.Should().Be(X0); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs index d463a18cd0..74b832e740 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs @@ -1,8 +1,6 @@ -// 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 System.Reflection; +using static AsmArm64.Arm64InstructionFactory; using static AsmArm64.Arm64RegisterX; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -18,11 +16,12 @@ public void MovzThenLdr_SameRegister_ShouldHaveValue() .AddPointer(MovzOffset, ExpectedResultAddress) .ToMockClrRuntime(); var accumulator = CreateValueAccumulator(clrRuntime); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, MovzOffset), // movz x0, #0x1000 - Arm64TestInstructions.Ldr(X0, baseRegister: X0), // ldr x0, [x0] + MOVZ(X0, MovzOffset), // movz x0, #0x1000 + LDR(X0, new Arm64ImmediateMemoryAccessor(X0, 0)), // ldr x0, [x0] }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -31,7 +30,7 @@ public void MovzThenLdr_SameRegister_ShouldHaveValue() // Assert accumulator.HasValue.Should().BeTrue(); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); accumulator.Value.Should().Be(ExpectedResultAddress); // It should be value that is returned by ReadPointer. } @@ -43,11 +42,12 @@ public void MovzThenLdr_DifferentRegister_ShouldHaveValue() .AddPointer(MovzOffset, ExpectedResultAddress) .ToMockClrRuntime(); var accumulator = CreateValueAccumulator(clrRuntime); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, MovzOffset), // movz x0, #0x1000 - Arm64TestInstructions.Ldr(X1, baseRegister: X0, immediate: 0x0), // ldr x1, [x0] + MOVZ(X0, MovzOffset), // movz x0, #0x1000 + LDR(X1, new Arm64ImmediateMemoryAccessor(X0, 0)), // ldr x1, [x0] }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -56,7 +56,7 @@ public void MovzThenLdr_DifferentRegister_ShouldHaveValue() // Assert accumulator.HasValue.Should().BeTrue(); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X1); // Value is loaded to X1 register. + accumulator.TrackedRegister.Should().Be(X1); // Value is loaded to X1 register. accumulator.Value.Should().Be(ExpectedResultAddress); // It should be value that is returned by ReadPointer. } @@ -68,11 +68,12 @@ public void MovzThenLdr_WithDisplacement_ShouldNotHaveValue() .AddPointer(MovzOffset, ExpectedResultAddress) .ToMockClrRuntime(); var accumulator = CreateValueAccumulator(clrRuntime); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, MovzOffset), // movz x0, #0x1000 - Arm64TestInstructions.Ldr(X0, baseRegister: X0, immediate: 0x100), // ldr x0, [x0, #0x100] + MOVZ(X0, MovzOffset), // movz x0, #0x1000 + LDR(X0, new Arm64ImmediateMemoryAccessor(X0, 0x100)), // ldr x0, [x0, #0x100] }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -81,9 +82,7 @@ public void MovzThenLdr_WithDisplacement_ShouldNotHaveValue() // Assert accumulator.HasValue.Should().BeFalse(); - - // TODO: Current accumulator don't reset registerId. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + accumulator.TrackedRegister.Should().Be(default); } [Fact] @@ -94,11 +93,12 @@ public void MovzThenLdr_WithIndexRegister_ShouldNotHaveValue() .AddPointer(MovzOffset, ExpectedResultAddress) .ToMockClrRuntime(); var accumulator = CreateValueAccumulator(clrRuntime); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, MovzOffset), // movz x0, #0x1000 - Arm64TestInstructions.Ldr(X0, baseRegister: X0, indexRegister: X1), // ldr x0, [x0, x1] + MOVZ(X0, MovzOffset), // movz x0, #0x1000 + LDR(X0, new Arm64RegisterXExtendMemoryAccessor(X0, X1, new Arm64MemoryExtend())), // ldr x0, [x0, x1] }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -107,9 +107,7 @@ public void MovzThenLdr_WithIndexRegister_ShouldNotHaveValue() // Assert accumulator.HasValue.Should().BeFalse(); - - // TODO: Current accumulator don't reset registerId. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + accumulator.TrackedRegister.Should().Be(default); } [Fact] @@ -117,11 +115,12 @@ public void MovzThenLdrLiteral_ShouldNotHaveValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, MovzOffset), // movz x0, #0x1000 - Arm64TestInstructions.Ldr(X0, 0x100), // ldr x0, #0x100 + MOVZ(X0, MovzOffset), // movz x0, #0x1000 + LDR(X0, new Arm64ImmediateMemoryAccessor(X0, 0x100)), // ldr x0, #0x100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -130,9 +129,7 @@ public void MovzThenLdrLiteral_ShouldNotHaveValue() // Assert accumulator.HasValue.Should().BeFalse(); - - // TODO: Current accumulator don't reset registerId. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + accumulator.TrackedRegister.Should().Be(default); } [Fact] @@ -143,11 +140,12 @@ public void MovzThenLdr_ZeroAddress_ShouldNotHaveValue() .AddPointer(MovzOffset, 0) .ToMockClrRuntime(); var accumulator = CreateValueAccumulator(clrRuntime); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, MovzOffset), // movz x0, #0x1000 - Arm64TestInstructions.Ldr(X1, baseRegister: X0), // ldr x1, [x0] + MOVZ(X0, MovzOffset), // movz x0, #0x1000 + LDR(X1, new Arm64ImmediateMemoryAccessor(X0,0)), // ldr x1, [x0] }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -166,11 +164,12 @@ public void MovzThenLdr_UnreadableAddress_ShouldNotHaveValue() .AddFalsePointer(MovzOffset) .ToMockClrRuntime(); var accumulator = CreateValueAccumulator(clrRuntime); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, MovzOffset), // movz x0, #0x1000 - Arm64TestInstructions.Ldr(X1, baseRegister: X0), // ldr x1, [x0] + MOVZ(X0, MovzOffset), // movz x0, #0x1000 + LDR(X1, new Arm64ImmediateMemoryAccessor(X0,0)), // ldr x1, [x0] }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -182,4 +181,4 @@ public void MovzThenLdr_UnreadableAddress_ShouldNotHaveValue() accumulator.Value.Should().Be(0UL); } } -#endif + diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs index aeaf75f892..32def75206 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs @@ -1,8 +1,6 @@ -// 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; +using static AsmArm64.Arm64InstructionFactory; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -14,12 +12,13 @@ public void MovzThenMovz_ShouldStartNewValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = 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 + MOVZ(X0, 0x1111), // movz x0, #0x1111 + MOVZ(X1, 0x2222), // movz x1, #0x2222 + MOVK(X1, 0x3333, amount: 16), // movk x1, #0x3333, lsl #16 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -29,7 +28,7 @@ public void MovzThenMovz_ShouldStartNewValue() // Assert accumulator.HasValue.Should().BeTrue(); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X1); + accumulator.TrackedRegister.Should().Be(X1); accumulator.Value.Should().Be(0x3333_2222); // X1 register value is used. } @@ -38,15 +37,21 @@ public void MovzThenMovk_ShouldHaveValue() { // Arrange var accumulator = CreateValueAccumulator(); + var rawInstructions = new[] + { + MOVZ(X0, 0x1234), // movz x0, #0x1234 + MOVK(X0, 0x5678, amount: 16) // movk x0, #0x5678, lsl #16 + }; + var instructions = rawInstructions.ToArm64Instructions(); // Act - accumulator.Feed(Arm64TestInstructions.Movz(X0, 0x1234)); // movz x0, #0x1234 - accumulator.Feed(Arm64TestInstructions.Movk(X0, 0x5678, 16)); // movk x0, #0x5678, lsl #16 + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); // Assert accumulator.HasValue.Should().BeTrue(); accumulator.Value.Should().Be(0x5678_1234L); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); } [Fact] @@ -54,11 +59,18 @@ public void MovzThenMovk_WithSameShiftValue_ShouldHasAccumulatedValue() { // Arrange var accumulator = CreateValueAccumulator(); + var rawInstructions = new[] + { + MOVZ(X0, 0x1111), // movz x0, #0x1111 + MOVK(X0, 0x2222, amount: 16), // movk x0, #0x2222, lsl #16 + MOVK(X0, 0x0022, amount: 16) // movk x0, #0x0022, lsl #16 + }; + var instructions = rawInstructions.ToArm64Instructions(); // 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 + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); + accumulator.Feed(instructions[2]); // Assert accumulator.HasValue.Should().BeTrue(); @@ -70,11 +82,12 @@ public void MovzThenMovk_WithSkippedShift_ShouldAccumulateValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1234), // movz x0, #0x1234 - Arm64TestInstructions.Movk(X0, 0x5678, 32) // movk x0, #0x5678, lsl #32 + MOVZ(X0, 0x1234), // movz x0, #0x1234 + MOVK(X0, 0x5678, amount: 32) // movk x0, #0x5678, lsl #32 }; + var instructions = rawInstructions.ToArm64Instructions(); // Act accumulator.Feed(instructions[0]); @@ -83,7 +96,7 @@ public void MovzThenMovk_WithSkippedShift_ShouldAccumulateValue() // Assert accumulator.HasValue.Should().BeTrue(); accumulator.Value.Should().Be(0x5678_0000_1234UL); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); } [Fact] @@ -91,15 +104,20 @@ public void MovzThenMovk_DifferentRegister_ShouldHaveValue() { // Arrange var accumulator = CreateValueAccumulator(); + var rawInstructions = new[] + { + MOVZ(X0, 0x1234), // movz x0, #0x1234 + MOVK(X1, 0x5678, amount: 16), // movk x1, #0x5678, lsl #16 + }; + var instructions = rawInstructions.ToArm64Instructions(); // Act - accumulator.Feed(Arm64TestInstructions.Movz(X0, 0x1234)); // movz x0, #0x1234 - accumulator.Feed(Arm64TestInstructions.Movk(X1, 0x5678, 16)); // movk x1, #0x5678, lsl #16 + accumulator.Feed(instructions[0]); + accumulator.Feed(instructions[1]); // Assert accumulator.HasValue.Should().BeTrue(); accumulator.Value.Should().Be(0x1234); - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs index 3da52bbb4e..ed573f0625 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenUnConditionalBranch.cs @@ -1,8 +1,7 @@ -// 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 BenchmarkDotNet.Disassemblers; using static AsmArm64.Arm64RegisterX; +using static AsmArm64.Arm64InstructionFactory; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -14,11 +13,12 @@ public void MovzThenUnconditionalB_ShouldResetValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 - Arm64TestInstructions.B(0x100), // b #0x100 + MOVZ(X0, 0x1000), // movz x0, #0x1000 + B(0x100), // b #0x100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -27,9 +27,7 @@ public void MovzThenUnconditionalB_ShouldResetValue() // Assert accumulator.HasValue.Should().BeFalse(); - - // TODO: Current accumulator don't reset registerId. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + accumulator.TrackedRegister.Should().Be(default); } @@ -38,11 +36,12 @@ public void MovzThenTbz_ShouldKeepValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 - Arm64TestInstructions.Tbz(X0, 63, 0x100) // tbz x0, #63, #0x100 + MOVZ(X0, 0x1000), // movz x0, #0x1000 + TBZ(X0, 63, 0x100) // tbz x0, #63, #0x100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -52,11 +51,10 @@ public void MovzThenTbz_ShouldKeepValue() // Assert accumulator.HasValue.Should().BeTrue(); accumulator.Value.Should().Be(0x1000UL); // TBZ is a conditional branch that does not overwrite the tracked register, - accumulator.RegisterId.Should().Be(Arm64RegisterId.ARM64_REG_X0); + accumulator.TrackedRegister.Should().Be(X0); - var details = instructions[1].Details; - details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP).Should().BeTrue(); - details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE).Should().BeTrue(); + instructions[1].BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP).Should().BeTrue(); + instructions[1].BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE).Should().BeTrue(); } [Fact] @@ -64,11 +62,12 @@ public void MovzThenRet_ShouldResetValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 - Arm64TestInstructions.Ret(), // ret + MOVZ(X0, 0x1000), // movz x0, #0x1000 + RET(), // ret }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -85,11 +84,12 @@ public void MovzThenDrps_ShouldResetValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 - Arm64TestInstructions.Drps(), // drps + MOVZ(X0, 0x1000), // movz x0, #0x1000 + DRPS(), // drps }; + var instructions = rawInstructions.ToArm64Instructions(); // Act accumulator.Feed(instructions[0]); @@ -97,9 +97,7 @@ public void MovzThenDrps_ShouldResetValue() // Assert accumulator.HasValue.Should().BeFalse(); - - // TODO: Current implementation don't reset state. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + accumulator.TrackedRegister.Should().Be(default); } [Fact] @@ -107,11 +105,12 @@ public void MovzThenBl_ShouldResetValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 - Arm64TestInstructions.Bl(0x100), // bl 0x100 + MOVZ(X0, 0x1000), // movz x0, #0x1000 + BL(0x100), // bl 0x100 }; + var instructions = rawInstructions.ToArm64Instructions(); PrintInstructions(instructions); // Act @@ -120,9 +119,7 @@ public void MovzThenBl_ShouldResetValue() // Assert accumulator.HasValue.Should().BeFalse(); - - // TODO: Current implementation don't reset state. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + accumulator.TrackedRegister.Should().Be(default); } [Fact] @@ -130,11 +127,12 @@ public void MovzThenBlr_ShouldResetValue() { // Arrange var accumulator = CreateValueAccumulator(); - var instructions = new[] + var rawInstructions = new[] { - Arm64TestInstructions.Movz(X0, 0x1000), // movz x0, #0x1000 - Arm64TestInstructions.Blr(X0), // bl x0 + MOVZ(X0, 0x1000), // movz x0, #0x1000 + BLR(X0), // bl x0 }; + var instructions = rawInstructions.ToArm64Instructions(); // Act accumulator.Feed(instructions[0]); @@ -142,9 +140,6 @@ public void MovzThenBlr_ShouldResetValue() // Assert accumulator.HasValue.Should().BeFalse(); - - // TODO: Current implementation don't reset state. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + accumulator.TrackedRegister.Should().Be(default); } } -#endif diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/BelongsToGroupTests.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/BelongsToGroupTests.cs index 5a7e352145..ab9046875d 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/BelongsToGroupTests.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/BelongsToGroupTests.cs @@ -1,10 +1,10 @@ -// 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 BenchmarkDotNet.Disassemblers; using static AsmArm64.Arm64RegisterW; using static AsmArm64.Arm64RegisterX; +using static AsmArm64.Arm64InstructionFactory; +using static AsmArm64.Arm64ConditionalKind; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -18,33 +18,30 @@ public BelongsToGroupTests(ITestOutputHelper output) } [Theory] - [MemberData(nameof(TestData.Jump), MemberType = typeof(TestData))] - public void BelongToGroup_Jump(InstructionTestData testData) + [MemberData(nameof(TestData.JumpInstructions), MemberType = typeof(TestData))] + public void BelongToGroup_Jump(uint rawInstruction) { // Arrange var groupId = Arm64InstructionGroupId.ARM64_GRP_JUMP; - var instruction = testData.GetCapstoneInstruction(); + var instruction = Arm64Instruction.Decode(rawInstruction); // Act - var result = instruction.Details.BelongsToGroup(groupId); + var result = instruction.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) + [Theory] + [MemberData(nameof(TestData.CallInstructions), MemberType = typeof(TestData))] + public void BelongToGroup_Call(uint rawInstruction) { // Arrange var groupId = Arm64InstructionGroupId.ARM64_GRP_CALL; - var instruction = testData.GetCapstoneInstruction(); + var instruction = Arm64Instruction.Decode(rawInstruction); // Act - var result = instruction.Details.BelongsToGroup(groupId); + var result = instruction.BelongsToGroup(groupId); // Assert result.Should().BeTrue(); @@ -52,123 +49,83 @@ public void BelongToGroup_Call(InstructionTestData testData) [Theory] - [MemberData(nameof(TestData.BranchRelative), MemberType = typeof(TestData))] - public void BelongToGroup_BranchRelative(InstructionTestData testData) + [MemberData(nameof(TestData.BranchRelativeInstructions), MemberType = typeof(TestData))] + public void BelongToGroup_BranchRelative(uint rawInstruction) { // Arrange var groupId = Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE; - var instruction = testData.GetCapstoneInstruction(); + var instruction = Arm64Instruction.Decode(rawInstruction); // Act - var result = instruction.Details.BelongsToGroup(groupId); + var result = instruction.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 = + public static readonly TheoryData 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: + B(0x100), // B_only_branch_imm: + B(EQ, 0x100), // B_only_condbranch: + BC(EQ, 0x100), // BC_only_condbranch: // It's introduced at Armv9.2-A + BR(X0), // BR_64_branch_reg: + BRAA(X0, X1), // BRAA_64p_branch_reg: + BRAAZ(X0), // BRAAZ_64_branch_reg: + BRAB(X0, X1), // BRAB_64p_branch_reg: + BRABZ(X0), // BRABZ_64_branch_reg: + CBNZ(W0, 0x100), // CBNZ_32_compbranch: + CBNZ(X0, 0x100), // CBNZ_64_compbranch: + CBZ(W0, 0x100), // CBZ_32_compbranch: + CBZ(X0, 0x100), // CBZ_64_compbranch: + DRPS(), // DRPS_64e_branch_reg: + ERET(), // ERET_64e_branch_reg: + ERETAA(), // ERETAA_64e_branch_reg: + ERETAB(), // ERETAB_64e_branch_reg: + RET(X0), // RET_64r_branch_reg: + RETAA(), // RETAA_64e_branch_reg: + RETAASPPCR(X0), // RETAASPPCR_64m_branch_reg: + RETAASPPC(-0x100), // RETAASPPC_only_miscbranch: + RETAB(), // RETAB_64e_branch_reg: + RETABSPPCR(X0), // RETABSPPCR_64m_branch_reg: + RETABSPPC(-0x100), // RETABSPPC_only_miscbranch: + TBNZ(X0, imm: 0x10, 0x100), // TBNZ_only_testbranch: + TBZ(X0, imm: 0x10, 0x100), // TBZ_only_testbranch: ]; - private static readonly uint[] CallInstructions = + public static readonly TheoryData 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 + BL(0x100), // BL_only_branch_imm + BLR(X0), // BLR_64_branch_reg + BLRAA(X0, X1), // BLRAA_64p_branch_reg + BLRAB(X0, X1), // BLRAB_64p_branch_reg + BLRAAZ(X0), // BLRAAZ_64_branch_reg + BLRABZ(X0), // BLRABZ_64_branch_reg + HVC(0x100), // HVC_ex_exception + SMC(0x100), // HVC_ex_exception + SVC(0x100), // SVC_ex_exception ]; - private static readonly uint[] BranchRelativeInstructions = + public static readonly TheoryData 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 + B(0x100), // B_only_branch_imm + B(EQ, 0x100), // B_only_condbranch + BC(NE, 0x100), // BC_only_condbranch + BL(0x100), // BL_only_branch_imm + CBNZ(W0,0x100), // CBNZ_32_compbranch + CBNZ(X0,0x100), // CBNZ_64_compbranch + CBZ(W0,0x100), // CBZ_32_compbranch + CBZ(X0,0x100), // CBZ_64_compbranch + RETAASPPC(-0x100), // RETAASPPC_only_miscbranch + RETABSPPC(-0x100), // RETABSPPC_only_miscbranch + TBNZ(X0,imm:0x10, label: 0x20), // TBNZ_only_testbranch + 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 index 0cdb17bb1f..573d1467da 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerHelper.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerHelper.cs @@ -3,12 +3,6 @@ using Microsoft.Diagnostics.Runtime.Interfaces; using System.Diagnostics; -#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; @@ -39,70 +33,4 @@ internal class Arm64DisassemblerHelper : Arm64Disassembler { return base.TryFollowJumpTrampoline(state, address, out target); } - - // Following methods require UnsafeAccessor to invoke private methods. -#if NET8_0_OR_GREATER - - [DebuggerHidden] - public static bool TryResolvePrecode(IDataReader reader, ref ulong address, out bool isPrestubMD) - { - var disassembler = new Arm64Disassembler(); - return TryResolvePrecode(disassembler, reader, ref address, out isPrestubMD); - } - - [DebuggerHidden] - 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); - } - - [DebuggerHidden] - public static bool IsLdrLiteral64(uint instr, out int rt, out int offsetBytes) - { - var disassembler = new Arm64Disassembler(); - return IsLdrLiteral64(disassembler, instr, out rt, out offsetBytes); - } - - [DebuggerHidden] - 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 index 45e05fafd6..746e6b735a 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerTestBase.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64DisassemblerTestBase.cs @@ -1,6 +1,5 @@ -using Microsoft.Diagnostics.Runtime; -using Microsoft.Diagnostics.Runtime.Interfaces; -using System.Buffers.Binary; +using AsmArm64; +using BenchmarkDotNet.Disassemblers; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -21,22 +20,17 @@ public Arm64DisassemblerTestBase(ITestOutputHelper output) Output = output; } - protected void PrintInstructions(Gee.External.Capstone.Arm64.Arm64Instruction[] instructions) + protected void PrintInstructions(IEnumerable instructions) { foreach (var instruction in instructions) { - Output.WriteLine(instruction.ToString()); + var instructionText = Arm64InstructionFormatter.Format(instruction); + Output.WriteLine(instructionText); } } protected void PrintInstructions(uint[] rawInstructions) - { - foreach (var rawInstruction in rawInstructions) - { - var instruction = rawInstruction.ToCapstoneArm64Instruction(); - Output.WriteLine(instruction.ToString()); - } - } + => PrintInstructions(rawInstructions.Select(Arm64Instruction.Decode)); /// /// Create MockClrRuntime with MockDataReader that returns following data. diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64TestInstructions.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64TestInstructions.cs deleted file mode 100644 index 1371670c22..0000000000 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/Arm64TestInstructions.cs +++ /dev/null @@ -1,192 +0,0 @@ -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 int FillBuffer(this uint[] rawInstructions, Span buffer) - { - var bytes = rawInstructions.ToLittleEndianBytes(); - if (bytes.Length > buffer.Length) - throw new ArgumentException($"Buffer length must be greater than {bytes.Length}."); - - bytes.CopyTo(buffer); - return bytes.Length; - } - - 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 8"); - } -} diff --git a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/ExtensionMethods.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/ExtensionMethods.cs new file mode 100644 index 0000000000..f1d1bc2d48 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Helpers/ExtensionMethods.cs @@ -0,0 +1,31 @@ +using AsmArm64; +using System.Buffers.Binary; +using System.Runtime.InteropServices; + +namespace BenchmarkDotNet.Tests.Disassemblers; + +internal static class ExtensionMethods +{ + public static Arm64Instruction ToArm64Instruction(this uint rawInstruction) + => Arm64Instruction.Decode(rawInstruction); + + public static Arm64Instruction[] ToArm64Instructions(this uint[] rawInstructions) + => rawInstructions.Select(Arm64Instruction.Decode).ToArray(); + + 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 byte[] ToLittleEndianBytes(this uint rawInstruction) + => ToLittleEndianBytes([rawInstruction]); +} diff --git a/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs b/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs index 64f94e812c..eaa02594bd 100644 --- a/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs +++ b/tests/BenchmarkDotNet.Tests/Serialization/DisassemblerModelSerializationTests.cs @@ -1,14 +1,25 @@ +using AsmArm64; using AwesomeAssertions; +using AwesomeAssertions.Equivalency; using BenchmarkDotNet.Disassemblers; using BenchmarkDotNet.Serialization; -using Gee.External.Capstone; -using Gee.External.Capstone.Arm64; +using BenchmarkDotNet.Tests.Disassemblers; +using BenchmarkDotNet.Tests.Disassemblers.Arm64; using Iced.Intel; +using static AsmArm64.Arm64RegisterSP; +using static AsmArm64.Arm64RegisterW; +using static AsmArm64.Arm64RegisterX; namespace BenchmarkDotNet.Tests; -public class DisassemblerModelSerializationTests +public class DisassemblerModelSerializationTests(ITestOutputHelper output) : Arm64DisassemblerTestBase(output) { + // 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 ClrMdArgsSerializationTest() { @@ -108,25 +119,24 @@ public void IntelAsmSerializationTest() public void Arm64AsmSerializationTest() { // Arrange - byte[] instructionBytes = [0xE1, 0x0B, 0x40, 0xB9]; // ldr w1, [sp, #8] - var disassembleSyntax = DisassembleSyntax.Intel; + var rawInstruction = Arm64InstructionFactory.LDR(W1, new Arm64ImmediateMemoryAccessor(SP, 8)); // ldr w1, [sp, #8] + byte[] instructionBytes = rawInstruction.ToLittleEndianBytes(); + + using var clrRuntime = new MockMemory().ToMockClrRuntime(); + var state = new State(clrRuntime, DummyTargetFrameworkVersion); - // Create instruction instance by using disassembler. - using var disassembler = CapstoneFactory.CreateArm64Disassembler(Arm64DisassembleMode.Arm); - disassembler.EnableInstructionDetails = true; - disassembler.DisassembleSyntax = disassembleSyntax; + var helper = new Arm64DisassemblerHelper(); // Act - var instructions = disassembler.Disassemble(instructionBytes); + var instructions = helper.Decode(instructionBytes, 0, state, 0, DummyCurrentMethod, Diagnosers.DisassemblySyntax.Att); var instruction = instructions.Single(); var model = new Arm64Asm { - DisassembleSyntax = disassembleSyntax, - Instruction = instruction, - InstructionLength = instruction.Bytes.Length, - InstructionPointer = (ulong) instruction.Address, - ReferencedAddress = (instruction.Address > ushort.MaxValue) ? (ulong) instruction.Address : null, + InstructionPointer = instruction.InstructionPointer, + RawInstruction = instruction.RawInstruction, + DisassembleSyntax = instruction.DisassembleSyntax, + ReferencedAddress = (instruction.ReferencedAddress > ushort.MaxValue) ? instruction.ReferencedAddress : null, IsReferencedAddressIndirect = true, // Test with dummy value }; @@ -137,13 +147,12 @@ public void Arm64AsmSerializationTest() // Assert json.Should().NotBe("{}"); - // Compare properties (Except for`Instruction.Details.Operands` property that ) + // Compare properties result.Should().NotBeNull(); result.Instruction.Should().NotBeNull(); result.Instruction.ToString().Should().Be("ldr w1, [sp, #8]"); - result.Should() - .BeEquivalentTo(model, options => options.Excluding(x => x.Instruction!.Details.Operands)); + result.Should().BeEquivalentTo(model, ConfigureCustomEquivalency); } [Fact]