diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj index d21c28cbc2..3e6d079490 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj +++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj @@ -23,9 +23,9 @@ + - 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 - - - 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/Arm64InstructionExtensions.cs b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs new file mode 100644 index 0000000000..0e026edfc9 --- /dev/null +++ b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionExtensions.cs @@ -0,0 +1,361 @@ +using AsmArm64; + +namespace BenchmarkDotNet.Disassemblers; + +internal static class Arm64InstructionExtensions +{ + private static bool TryGetOperandCore(Arm64Instruction instruction, int index, Arm64OperandKind expectedKind, Func castFunc, out T result) + where T : struct, IArm64Operand + { + if ((uint) index >= (uint) instruction.Operands.Count) + { + result = default; + return false; + } + + var operand = instruction.GetOperand(index); + + if (operand.Kind != expectedKind) + { + result = default; + return false; + } + + result = castFunc(operand); + return true; + } + + // 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); + + public bool TryGetOperand(int index, out Arm64ImmediateOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Immediate, x => (Arm64ImmediateOperand) x, out result); + + public bool TryGetOperand(int index, out Arm64LabelOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Label, x => (Arm64LabelOperand) x, out result); + + public bool TryGetOperand(int index, out Arm64ShiftOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Shift, x => (Arm64ShiftOperand) x, out result); + + public bool TryGetOperand(int index, out Arm64EnumOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Enum, x => (Arm64EnumOperand) x, out result); + + public bool TryGetOperand(int index, out Arm64MemoryOperand result) + => TryGetOperandCore(instruction, index, Arm64OperandKind.Memory, x => (Arm64MemoryOperand) x, out result); + } + + extension(Arm64Instruction instruction) + { + /// + /// 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 bool TryGetShiftedImmediateValueUnsigned(int index, out ulong value) + { + if (instruction.TryGetShiftedImmediateValue(index, out long signed)) + { + value = (ulong) signed; + return true; + } + + value = 0; + return false; + } + + public bool TryGetShiftedImmediateValue(int index, out long value) + { + value = 0; + var operands = instruction.Operands; + if ((uint) index >= (uint) operands.Count) + return false; + + 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; + } + + var shiftAmount = shiftOperand.Amount; + var shiftKind = shiftOperand.ShiftKind; + return TryShiftImmediateValue(immediateValue, shiftKind, shiftAmount, out value); + } + + /// + /// 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.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(Arm64RegisterAny trackedRegister) + { + if (instruction.Mnemonic != Arm64Mnemonic.LDR) + return false; + + if (!instruction.TryGetOperand(1, out Arm64MemoryOperand memoryOperand)) + return false; + + var memoryAccessor = memoryOperand.Accessor; + + // Check base register is the register that is currently tracked. + if (memoryAccessor.BaseRegister != trackedRegister) + return false; + + // Check displacement. + if (memoryAccessor.HasOptionalOffset && memoryAccessor.HasImmediate && memoryAccessor.Immediate != 0) + return false; + + // Check index register is specified. + if (memoryAccessor.HasIndexRegister) + return false; + + // Ldr from tracked register (with no displacement/index register). + return true; + } + + public bool IsConditionalBranch() + { + switch (instruction.Mnemonic) + { + case Arm64Mnemonic.CBZ: + case Arm64Mnemonic.CBNZ: + case Arm64Mnemonic.TBZ: + case Arm64Mnemonic.TBNZ: + return true; + + 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 Arm64ConditionalKind.Undefined: + case Arm64ConditionalKind.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); + } + + 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-Alpha11/arch/AArch64/AArch64GenCSMappingInsn.inc + switch (expectedGroup) + { + case Arm64InstructionGroupId.ARM64_GRP_JUMP: + switch (instruction.Mnemonic) + { + 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.Mnemonic) + { + 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.Mnemonic) + { + 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.Mnemonic) + { + 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; + } + + // 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; + } + } + } + } + + /// + /// 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/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/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 +} diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs index 2fc4e23ce4..c8d481958d 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64RegisterValueAccumulator.cs @@ -1,5 +1,6 @@ -using Gee.External.Capstone.Arm64; +using AsmArm64; using Microsoft.Diagnostics.Runtime.Interfaces; +using System.Diagnostics; namespace BenchmarkDotNet.Disassemblers; @@ -14,11 +15,13 @@ internal enum State } private State _state; - private long _value; - private int _expectedMovkShift; - private Arm64RegisterId _registerId; + private ulong _addressValue; + private long _addressOffset; + 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; @@ -27,118 +30,298 @@ public void Init(IClrRuntime runtime) internal void Reset( State state = State.LookingForPattern, - int expectedMovkShift = 0, - long value = 0, - Arm64RegisterId registerId = Arm64RegisterId.Invalid) + ulong value = 0, + Arm64RegisterAny register = default) { _state = state; - _expectedMovkShift = expectedMovkShift; - _value = value; - _registerId = registerId; + _addressValue = value; + _addressOffset = 0; + _trackedRegister = register == default + ? InvalidRegister + : register; } - public void Feed(Arm64Instruction instruction) + public void Feed(Arm64Instruction instruction, ulong instructionAddress = 0) { - 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, instructionAddress)) + return; 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; + + // 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; + case State.ExpectingAdd: - if (instruction.Id == Arm64InstructionId.ARM64_INS_ADD && - details.Operands[0].Register.Id == _registerId && - details.Operands[1].Register.Id == _registerId && - details.Operands[2].Type == Arm64OperandType.Immediate) - { - _value = _value | instruction.Details.Operands[2].Immediate; - _state = State.LookingForPossibleLdr; - } - break; + if (TryHandleExpectingAdd(instruction)) + return; + + goto default; + 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 + if (TryHandleLdrFromTrackedRegister(instruction)) + return; + + // Try handle MOVZ/ADRP for new constand load. + if (TryHandleLookingForPattern(instruction, instructionAddress)) + return; + + goto default; + + default: + if (TryHandleConditionalInstruction(instruction)) + return; + + // Reset accumulated state if instruction overwriting the accumulated register. + if (IsTrackedRegisterOverwritten(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 details.AllWrittenRegisters) - { - // Some unexpected instruction overwriting the accumulated register - if (reg.Id == _registerId) - { - _state = State.LookingForPattern; - } - } + Reset(); + + // Reprocess current instruction. + TryHandleLookingForPattern(instruction, instructionAddress); } - break; + return; + } + } + + 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; + + switch (_state) + { + case State.ExpectingMovk: + case State.LookingForPossibleLdr: + case State.ExpectingAdd: + return true; + default: + return false; + } + } + } + + /// + /// 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 Arm64RegisterAny TrackedRegister => _trackedRegister; + + private bool TryHandleLookingForPattern(Arm64Instruction instruction, ulong instructionAddress) + { + switch (instruction.Mnemonic) + { + // 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; + } + } + + private bool TryHandleExpectingMovk(Arm64Instruction instruction) + { + if (instruction.Mnemonic != Arm64Mnemonic.MOVK) + return false; + + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand)) + return false; + + if (registerOperand.Value != _trackedRegister) + return false; + + if (!instruction.TryGetOperand(1, out Arm64ImmediateOperand _)) + return false; + + var shiftAmount = 0; + if (instruction.TryGetOperand(2, out Arm64ShiftOperand shiftOperand)) + { + 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; + } + + /// + /// 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.Mnemonic != Arm64Mnemonic.ADD) + return false; + + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand0) || registerOperand0.Value != _trackedRegister) + return false; + + if (!instruction.TryGetOperand(1, out Arm64RegisterOperand registerOperand1) || registerOperand1.Value != _trackedRegister) + return false; + + if (!instruction.TryGetOperand(2, out Arm64ImmediateOperand _)) + return false; + + if (!instruction.TryGetShiftedImmediateValueUnsigned(2, out var shiftedImmediateValue)) + return false; + + _addressValue += shiftedImmediateValue; + _state = State.LookingForPossibleLdr; + return true; + } + + private bool TryStartMovzSequence(Arm64Instruction instruction) + { + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand)) + return false; + + if (!instruction.TryGetOperand(1, out Arm64ImmediateOperand _)) + return false; + + _trackedRegister = registerOperand.Value; + + if (!instruction.TryGetShiftedImmediateValueUnsigned(1, out var immediateValue)) + return false; + + _addressValue = immediateValue; + _addressOffset = 0; + _state = State.ExpectingMovk; + return true; } - public bool HasValue => _state == State.ExpectingMovk || _state == State.LookingForPossibleLdr; + private bool TryStartAdrpSequence(Arm64Instruction instruction, ulong instructionAddress) + { + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand) + || !instruction.TryGetOperand(1, out Arm64LabelOperand labelOperand)) + return false; + + _trackedRegister = registerOperand.Value; - public long Value => _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) + { + if (!instruction.IsLdrFromTrackedRegister(_trackedRegister)) + return false; - public Arm64RegisterId RegisterId => _registerId; + if (!instruction.TryGetOperand(0, out Arm64RegisterOperand registerOperand)) + 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; + } + + _addressValue = dereferenced; + _addressOffset = 0; + + if (_addressValue == 0) + { + Reset(); + return true; + } + + // The LDR might have loaded the result in another register + _trackedRegister = registerOperand.Value; + return true; + + } + + private bool TryHandleConditionalInstruction(Arm64Instruction instruction) + { + if (instruction.IsConditionalBranch()) + return true; + + if (instruction.IsUnconditionalControlFlow()) + { + Reset(); + return true; + } + + return false; + } + + private bool IsTrackedRegisterOverwritten(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 (var writtenRegister in instruction.GetWrittenRegisters()) + { + if (writtenRegister != _trackedRegister) + continue; + + return true; + } + + return false; + } } diff --git a/src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs b/src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs deleted file mode 100644 index f0d73557bc..0000000000 --- a/src/BenchmarkDotNet/Disassemblers/CapstoneFactory.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Gee.External.Capstone; -using Gee.External.Capstone.Arm64; -#if !NET -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Extensions; -using BenchmarkDotNet.Helpers; -using System.Runtime.InteropServices; -using RuntimeInformation = BenchmarkDotNet.Portability.RuntimeInformation; -#endif - -namespace BenchmarkDotNet.Disassemblers; - -internal static class CapstoneFactory -{ - internal static CapstoneArm64Disassembler CreateArm64Disassembler(Arm64DisassembleMode disassembleMode) - { -#if !NET - _ = FrameworkNativeLibrary.Handle.Value; -#endif - return CapstoneDisassembler.CreateArm64Disassembler(disassembleMode); - } - -#if !NET - /// - /// .NET Framework does not probe runtimes/{rid}/native, where BenchmarkDotNet.targets copies the native capstone. - /// Once it is loaded by its full path, the P/Invokes that name the module bind to the loaded library. - /// When the file is not there, the default search applies - a build for a specific RuntimeIdentifier puts it next to the application. - /// - private static class FrameworkNativeLibrary - { - internal static readonly Lazy Handle = new(Load); - - private static IntPtr Load() - { - if (!RuntimeInformation.IsFullFramework) - return IntPtr.Zero; - - string? rid = RuntimeInformation.GetCurrentPlatform() switch - { - Platform.X86 => "win-x86", - Platform.X64 => "win-x64", - Platform.Arm64 => "win-arm64", - _ => null - }; - var assembly = typeof(CapstoneDisassembler).Assembly; - string location = ShadowCopyHelper.TryGetOriginalLocation(assembly, out var originalLocation) ? originalLocation : assembly.Location; - if (rid is null || location.IsBlank()) - return IntPtr.Zero; - - string path = Path.Combine(Path.GetDirectoryName(location)!, "runtimes", rid, "native", "capstone.dll"); - return File.Exists(path) ? LoadLibraryW(path) : IntPtr.Zero; - } - - [DllImport("kernel32", ExactSpelling = true, CharSet = CharSet.Unicode)] - private static extern IntPtr LoadLibraryW(string lpLibFileName); - } -#endif -} diff --git a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs index 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 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/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 @@ - + 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 19e5553353..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, - long 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(); @@ -72,25 +76,126 @@ 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(ADRP(X0, 0x2000).ToArm64Instruction(), DummyBaseAddress); // 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 + 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(); + } + [Fact] 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 = 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(); + } + + [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 = 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(); } @@ -99,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(); @@ -119,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 986320cad4..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,19 +27,20 @@ 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] - public void AdrpThenAdd_WithNegativeOffset_ShouldCalculateAddress() + 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 @@ -48,20 +48,22 @@ 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] 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 @@ -73,16 +75,17 @@ 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 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 @@ -95,15 +98,16 @@ public void AdrpThenAdd_WithShiftedImm_ShouldCalculateAddress() } [Fact] - public void AdrpThenAdd_DifferentDestinationRegister_ShouldResetValue() + 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 @@ -111,20 +115,24 @@ 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.TrackedRegister.Should().Be(X0); } - [Fact(Skip = "On current implementation, `State.ExpectingAdd` don't reset value when unexpected instruction passed.")] + [Fact] 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 @@ -136,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 eafb2c12bb..840a252418 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.Movz.cs @@ -1,25 +1,24 @@ -// 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; // MOVZ tests. public partial class Arm64RegisterValueAccumulatorTests { - [Fact(Skip = "On current implementation, shifted immediate value is not supported.")] + [Fact] 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,8 +28,27 @@ 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); } + + [Fact] + public void Movz_ZeroImmediate_ShouldNotHaveValue() + { + // Arrange + var accumulator = CreateValueAccumulator(); + var rawInstructions = new[] + { + MOVZ(X0, 0x0), // movz x0, #0x0 + }; + var instructions = rawInstructions.ToArm64Instructions(); + 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.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 06eb3e68d6..74b832e740 100644 --- a/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs +++ b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenLdr.cs @@ -1,10 +1,6 @@ -// 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 static AsmArm64.Arm64InstructionFactory; using static AsmArm64.Arm64RegisterX; namespace BenchmarkDotNet.Tests.Disassemblers.Arm64; @@ -20,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 @@ -33,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. } @@ -45,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 @@ -58,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. } @@ -70,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 @@ -83,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] @@ -96,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 @@ -109,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] @@ -119,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 @@ -132,9 +129,56 @@ public void MovzThenLdrLiteral_ShouldNotHaveValue() // Assert accumulator.HasValue.Should().BeFalse(); + accumulator.TrackedRegister.Should().Be(default); + } + + [Fact] + public void MovzThenLdr_ZeroAddress_ShouldNotHaveValue() + { + // Arrange + using var clrRuntime = new MockMemory() + .AddPointer(MovzOffset, 0) + .ToMockClrRuntime(); + var accumulator = CreateValueAccumulator(clrRuntime); + var rawInstructions = new[] + { + MOVZ(X0, MovzOffset), // movz x0, #0x1000 + LDR(X1, new Arm64ImmediateMemoryAccessor(X0,0)), // ldr x1, [x0] + }; + var instructions = rawInstructions.ToArm64Instructions(); + PrintInstructions(instructions); - // TODO: Current accumulator don't reset registerId. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); + // 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. + } + + [Fact] + public void MovzThenLdr_UnreadableAddress_ShouldNotHaveValue() + { + // Arrange + using var clrRuntime = new MockMemory() + .AddFalsePointer(MovzOffset) + .ToMockClrRuntime(); + var accumulator = CreateValueAccumulator(clrRuntime); + var rawInstructions = new[] + { + MOVZ(X0, MovzOffset), // movz x0, #0x1000 + LDR(X1, new Arm64ImmediateMemoryAccessor(X0,0)), // ldr x1, [x0] + }; + var instructions = rawInstructions.ToArm64Instructions(); + 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.MovzThenMovk.cs b/tests/BenchmarkDotNet.Tests/Disassemblers/Arm64/Arm64RegisterValueAccumulatorTests.MovzThenMovk.cs index d4fc160566..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,27 +37,40 @@ 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(Skip = "Current value accumulator expecting sequencial 0/16/32/48 shfit for MOVK.")] + [Fact] 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(); @@ -66,22 +78,25 @@ public void MovzThenMovk_WithSameShiftValue_ShouldHasAccumulatedValue() } [Fact] - public void MovzThenMovk_WithUnexpectedShift_ShouldResetValue() + 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]); - 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.TrackedRegister.Should().Be(X0); } [Fact] @@ -89,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 06e0e47bbb..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,49 +27,47 @@ 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); } [Fact] - public void MovzThenTbz_ShouldResetValue() + 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 + // Act accumulator.Feed(instructions[0]); accumulator.Feed(instructions[1]); // Assert - accumulator.HasValue.Should().BeFalse(); + accumulator.HasValue.Should().BeTrue(); + accumulator.Value.Should().Be(0x1000UL); // TBZ is a conditional branch that does not overwrite the tracked register, + accumulator.TrackedRegister.Should().Be(X0); - // TODO: Current accumulator don't reset registerId. - // accumulator.RegisterId.Should().Be(Arm64RegisterId.Invalid); - - var details = instructions[1].Details; - details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP).Should().BeTrue(); - details.BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_BRANCH_RELATIVE).Should().BeTrue(); + instructions[1].BelongsToGroup(Arm64InstructionGroupId.ARM64_GRP_JUMP).Should().BeTrue(); + instructions[1].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 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 @@ -81,16 +79,17 @@ 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 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]); @@ -98,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] @@ -108,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 @@ -121,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] @@ -131,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]); @@ -143,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]