Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/BenchmarkDotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
<None Include="..\..\README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AsmArm64" Version="2.3.0" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="deniszykov.WebSocketListener" Version="4.2.19" />
<PackageReference Include="Gee.External.Capstone" Version="2.3.0" />
<PackageReference Include="Iced" Version="1.21.0" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="4.2.751201" />
<PackageReference Include="Perfolizer" Version="[0.7.5]" />
Expand Down
34 changes: 0 additions & 34 deletions src/BenchmarkDotNet/BenchmarkDotNet.targets
Original file line number Diff line number Diff line change
@@ -1,40 +1,6 @@
<Project>

<PropertyGroup>
<!-- If the RuntimeIdentifier is explicitly specified. Use the specified target platform. -->
<BenchmarkDotNetTargetPlatform>$(RuntimeIdentifier)</BenchmarkDotNetTargetPlatform>
</PropertyGroup>

<PropertyGroup Condition="'$(BenchmarkDotNetTargetPlatform)' == ''">
<!-- If RuntimeIdentifier is not specified Use current build platform -->
<!-- List of runtimes supported by Gee.External.Capstone: https://github.com/9ee1/Capstone.NET/tree/master/Gee.External.Capstone/runtimes -->
<BenchmarkDotNetTargetPlatform Condition="$([MSBuild]::IsOSPlatform('Linux'))">linux</BenchmarkDotNetTargetPlatform>
<BenchmarkDotNetTargetPlatform Condition="$([MSBuild]::IsOSPlatform('Windows'))">win</BenchmarkDotNetTargetPlatform>
<BenchmarkDotNetTargetPlatform Condition="$([MSBuild]::IsOSPlatform('OSX'))">osx</BenchmarkDotNetTargetPlatform>

<BenchmarkDotNetTargetPlatform Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">$(BenchmarkDotNetTargetPlatform)-x64</BenchmarkDotNetTargetPlatform>
<BenchmarkDotNetTargetPlatform Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X86'">$(BenchmarkDotNetTargetPlatform)-x86</BenchmarkDotNetTargetPlatform>
<BenchmarkDotNetTargetPlatform Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm'">$(BenchmarkDotNetTargetPlatform)-arm</BenchmarkDotNetTargetPlatform>
<BenchmarkDotNetTargetPlatform Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">$(BenchmarkDotNetTargetPlatform)-arm64</BenchmarkDotNetTargetPlatform>
</PropertyGroup>

<Target Name="BdnFilterPackageAssets" AfterTargets="ResolvePackageAssets">
<!-- Remove `runtimes/{RuntimeIdentifier}` files that is not matched to target platform. -->
<ItemGroup Condition="'$(BenchmarkDotNetTargetPlatform)' != '' AND $(BenchmarkDotNetTargetPlatform) != 'all'">
<RuntimeTargetsCopyLocalItems Remove="@(RuntimeTargetsCopyLocalItems)"
Condition="'%(RuntimeTargetsCopyLocalItems.NuGetPackageId)' == 'Gee.External.Capstone' AND '%(RuntimeTargetsCopyLocalItems.RuntimeIdentifier)' != '$(BenchmarkDotNetTargetPlatform)'" />
</ItemGroup>

<!--
The SDK copies `runtimes/{RuntimeIdentifier}` assets only for .NETCoreApp.
Copy the native capstone for .NET Framework too, where the Arm64 disassembler loads it from that folder itself.
-->
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<RuntimeTargetsCopyLocalItems Condition="'%(RuntimeTargetsCopyLocalItems.NuGetPackageId)' == 'Gee.External.Capstone'">
<CopyLocal>true</CopyLocal>
</RuntimeTargetsCopyLocalItems>
</ItemGroup>

<!-- Remove unnecessary satellite assemblies. -->
<ItemGroup>
<ResourceCopyLocalItems
Expand Down
145 changes: 83 additions & 62 deletions src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,52 @@
using AsmArm64;
using BenchmarkDotNet.Diagnosers;
using Gee.External.Capstone;
using Gee.External.Capstone.Arm64;
using Microsoft.Diagnostics.Runtime;
using Microsoft.Diagnostics.Runtime.Interfaces;
using System.Runtime.InteropServices;

namespace BenchmarkDotNet.Disassemblers
{
internal class Arm64Disassembler : ClrMdDisassembler
{
protected override IEnumerable<Asm> 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<uint> rawInstructions = MemoryMarshal.Cast<byte, uint>(code);

Arm64RegisterValueAccumulator accumulator = new();
accumulator.Init(state.Runtime);
var results = new List<Arm64Asm>();

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
Expand All @@ -65,7 +61,7 @@ protected override IEnumerable<Asm> 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))
Expand Down Expand Up @@ -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<byte> 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;
Expand All @@ -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<byte> buffer, int offset)
=> (uint) buffer[offset]
| ((uint) buffer[offset + 1] << 8)
| ((uint) buffer[offset + 2] << 16)
Expand All @@ -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;
Expand All @@ -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
Expand Down
Loading
Loading