diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs index 7426c0a59a..5d644e7a93 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -113,8 +113,6 @@ internal ImmutableConfig( internal bool HasPerfCollectProfiler() => diagnosers.OfType().Any(); - internal bool HasDisassemblyDiagnoser() => diagnosers.OfType().Any(); - public bool HasExtraIterationDiagnoser(BenchmarkCase benchmarkCase) => HasMemoryDiagnoser() || diagnosers.Any(d => d.GetRunMode(benchmarkCase) == RunMode.ExtraIteration); public IDiagnoser? GetCompositeDiagnoser(BenchmarkCase benchmarkCase, Func runModeComparer) diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs b/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs index fecbf8f9d0..3899e91b19 100644 --- a/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs @@ -138,63 +138,8 @@ public void Feed(Arm64Instruction instruction) internal class Arm64Disassembler : ClrMdDisassembler { - internal sealed class RuntimeSpecificData - { - // See dotnet/runtime src/coreclr/vm/arm64/thunktemplates.asm/.S for the stub code - // ldr x9, DATA_SLOT(CallCountingStub, RemainingCallCountCell) - // ldrh w10, [x9] - // subs w10, w10, #0x1 - internal readonly byte[] callCountingStubTemplate = [0x09, 0x00, 0x00, 0x58, 0x2a, 0x01, 0x40, 0x79, 0x4a, 0x05, 0x00, 0x71]; - // ldr x10, DATA_SLOT(StubPrecode, Target) - // ldr x12, DATA_SLOT(StubPrecode, MethodDesc) - // br x10 - internal readonly byte[] stubPrecodeTemplate = [0x4a, 0x00, 0x00, 0x58, 0xec, 0x00, 0x00, 0x58, 0x40, 0x01, 0x1f, 0xd6]; - // ldr x11, DATA_SLOT(FixupPrecode, Target) - // br x11 - // ldr x12, DATA_SLOT(FixupPrecode, MethodDesc) - internal readonly byte[] fixupPrecodeTemplate = [0x0b, 0x00, 0x00, 0x58, 0x60, 0x01, 0x1f, 0xd6, 0x0c, 0x00, 0x00, 0x58]; - internal readonly ulong stubPageSize; - - internal RuntimeSpecificData(State state) - { - stubPageSize = (ulong)Environment.SystemPageSize; - if (state.RuntimeVersion.Major >= 8) - { - // In .NET 8, the stub page size was changed to min 16kB - stubPageSize = Math.Max(stubPageSize, 16384); - } - - // The stubs code depends on the current OS memory page size, so we need to update the templates to reflect that - ulong pageSizeShifted = stubPageSize / 32; - // Calculate the ldr x9, #offset instruction with offset based on the page size - callCountingStubTemplate[1] = (byte)(pageSizeShifted & 0xff); - callCountingStubTemplate[2] = (byte)(pageSizeShifted >> 8); - - // Calculate the ldr x10, #offset instruction with offset based on the page size - stubPrecodeTemplate[1] = (byte)(pageSizeShifted & 0xff); - stubPrecodeTemplate[2] = (byte)(pageSizeShifted >> 8); - // Calculate the ldr x12, #offset instruction with offset based on the page size - stubPrecodeTemplate[5] = (byte)((pageSizeShifted - 1) & 0xff); - stubPrecodeTemplate[6] = (byte)((pageSizeShifted - 1) >> 8); - - // Calculate the ldr x11, #offset instruction with offset based on the page size - fixupPrecodeTemplate[1] = (byte)(pageSizeShifted & 0xff); - fixupPrecodeTemplate[2] = (byte)(pageSizeShifted >> 8); - // Calculate the ldr x12, #offset instruction with offset based on the page size - fixupPrecodeTemplate[9] = (byte)(pageSizeShifted & 0xff); - fixupPrecodeTemplate[10] = (byte)(pageSizeShifted >> 8); - } - } - - private static readonly Dictionary runtimeSpecificData = []; - protected override IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax) { - if (!runtimeSpecificData.TryGetValue(state.RuntimeVersion, out var data)) - { - runtimeSpecificData.Add(state.RuntimeVersion, data = new RuntimeSpecificData(state)); - } - const Arm64DisassembleMode disassembleMode = Arm64DisassembleMode.Arm; using (CapstoneArm64Disassembler disassembler = CapstoneDisassembler.CreateArm64Disassembler(disassembleMode)) { @@ -216,33 +161,8 @@ protected override IEnumerable Decode(byte[] code, ulong startAddress, Stat { if (isIndirect && state.RuntimeVersion.Major >= 7) { - // Check if the target is a known stub - // The stubs are allocated in interleaved code / data pages in memory. The data part of the stub - // is at an address one memory page higher than the code. - byte[] buffer = new byte[12]; - - FlushCachedDataIfNeeded(state.Runtime.DataTarget.DataReader, address, buffer); - - if (state.Runtime.DataTarget.DataReader.Read(address, buffer) == buffer.Length) - { - if (buffer.SequenceEqual(data.callCountingStubTemplate)) - { - const ulong TargetMethodAddressSlotOffset = 8; - address = state.Runtime.DataTarget.DataReader.ReadPointer(address + data.stubPageSize + TargetMethodAddressSlotOffset); - } - else if (buffer.SequenceEqual(data.stubPrecodeTemplate)) - { - const ulong MethodDescSlotOffset = 0; - address = state.Runtime.DataTarget.DataReader.ReadPointer(address + data.stubPageSize + MethodDescSlotOffset); - isPrestubMD = true; - } - else if (buffer.SequenceEqual(data.fixupPrecodeTemplate)) - { - const ulong MethodDescSlotOffset = 8; - address = state.Runtime.DataTarget.DataReader.ReadPointer(address + data.stubPageSize + MethodDescSlotOffset); - isPrestubMD = true; - } - } + FlushCachedDataIfNeeded(state.Runtime.DataTarget.DataReader, address, new byte[1]); + TryResolvePrecode(state.Runtime.DataTarget.DataReader, ref address, out isPrestubMD); } TryTranslateAddressToName(address, isPrestubMD, state, depth, currentMethod); } @@ -262,6 +182,120 @@ protected override IEnumerable Decode(byte[] code, ulong startAddress, Stat } } + // Counterpart of IntelDisassembler.TryResolvePrecode: recognise the AArch64 precode/stub + // shapes by matching the fixed opcode bits and reading slot displacements out of the + // encoded LDR-literal instructions. Resolves to the MethodDesc handle when one is present + // (so GetMethodByHandle can recover the live ClrMethod even if the call site is still + // pointing at PreStub), and to the TargetForMethod slot for call-counting stubs. + // + // See dotnet/runtime src/coreclr/vm/arm64/thunktemplates.asm/.S for the canonical stub + // shapes. The register numbers (x10/x12 for StubPrecode, x11/x12 for FixupPrecode, x9 for + // CallCountingStub) are part of the runtime's stub ABI and stay fixed across versions; the + // 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) + { + isPrestubMD = false; + byte[] buffer = new byte[12]; + if (reader.Read(address, buffer) != 12) + return false; + + uint instr0 = ReadInstr(buffer, 0); + uint instr1 = ReadInstr(buffer, 4); + uint instr2 = ReadInstr(buffer, 8); + + // StubPrecode: LDR x10, Target ; LDR x12, MethodDesc ; BR x10 + if (IsLdrLiteral64(instr0, out int rt0, out int _) && rt0 == 10 + && IsLdrLiteral64(instr1, out int rt1, out int off1) && rt1 == 12 + && instr2 == 0xD61F0140u) + { + ulong mdSlot = unchecked(address + 4 + (ulong)(long)off1); + if (reader.ReadPointer(mdSlot, out ulong md) && IsValidAddress(md)) + { + address = md; + isPrestubMD = true; + return true; + } + return false; + } + + // FixupPrecode: LDR x11, Target ; BR x11 ; LDR x12, MethodDesc + if (IsLdrLiteral64(instr0, out int rtA, out int _) && rtA == 11 + && instr1 == 0xD61F0160u + && IsLdrLiteral64(instr2, out int rtB, out int off2) && rtB == 12) + { + ulong mdSlot = unchecked(address + 8 + (ulong)(long)off2); + if (reader.ReadPointer(mdSlot, out ulong md) && IsValidAddress(md)) + { + address = md; + isPrestubMD = true; + return true; + } + return false; + } + + // FixupPrecodeCode_Fixup: LDR x12, MethodDesc ; LDR x11, PrecodeFixupThunk ; BR x11 + // This is the pre-backpatch shape — the call site has never been routed through the + // method's JIT'd entry point yet, so x11 still loads the fixup thunk instead of Target. + // Resolve via the MethodDesc slot loaded into x12 (instr0). + if (IsLdrLiteral64(instr0, out int rtF0, out int offF0) && rtF0 == 12 + && IsLdrLiteral64(instr1, out int rtF1, out int _) && rtF1 == 11 + && instr2 == 0xD61F0160u) + { + ulong mdSlot = unchecked(address + (ulong)(long)offF0); + if (reader.ReadPointer(mdSlot, out ulong md) && IsValidAddress(md)) + { + address = md; + isPrestubMD = true; + return true; + } + return false; + } + + // CallCountingStub: LDR x9, RemainingCallCount ; LDRH w10, [x9] ; SUBS w10, w10, #1 + // No MethodDesc to recover here; read TargetForMethod, which lives 8 bytes after + // RemainingCallCount in the data section. + if (IsLdrLiteral64(instr0, out int rtCount, out int offCount) && rtCount == 9 + && instr1 == 0x7940012Au + && instr2 == 0x7100054Au) + { + ulong countSlot = unchecked(address + (ulong)(long)offCount); + if (reader.ReadPointer(countSlot + 8, out ulong target) && IsValidAddress(target)) + { + address = target; + return true; + } + return false; + } + + return false; + } + + private static uint ReadInstr(byte[] buffer, int offset) + => (uint)buffer[offset] + | ((uint)buffer[offset + 1] << 8) + | ((uint)buffer[offset + 2] << 16) + | ((uint)buffer[offset + 3] << 24); + + // 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) + { + rt = 0; + offsetBytes = 0; + if ((instr & 0xFF000000u) != 0x58000000u) + return false; + rt = (int)(instr & 0x1Fu); + int imm19 = (int)((instr >> 5) & 0x7FFFFu); + // Sign-extend 19-bit imm to 32-bit. + if ((imm19 & 0x40000) != 0) + imm19 |= unchecked((int)0xFFF80000u); + offsetBytes = imm19 * 4; + return true; + } + private static bool TryGetReferencedAddress(Arm64Instruction instruction, RegisterValueAccumulator 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) @@ -296,5 +330,84 @@ private static DisassembleSyntax Map(DisassemblySyntax syntax) 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 + // still resolves to the underlying method): + // B imm26 (bits[31:26] = 0b000101) — target = address + sign_extended(imm26) * 4 + // CallCountingStub (opcode match) — reads TargetForMethod slot + // StubPrecode (opcode match) — reads Target slot (the LDR that BR consumes) + // FixupPrecode (opcode match) — reads Target slot (the LDR that BR consumes) + // Slot displacements are extracted from the LDR-literal instructions themselves, so the + // stub recognition doesn't depend on the runtime's code-to-data offset. Writes the resolved + // target into `target` and returns true if one matches. + protected override bool TryFollowJumpTrampoline(State state, ulong address, out ulong target) + { + target = 0; + IDataReader dataReader = state.Runtime.DataTarget.DataReader; + byte[] buffer = new byte[12]; + int read = dataReader.Read(address, buffer); + if (read < 4) + return false; + + uint instr0 = ReadInstr(buffer, 0); + + // B imm26 — bits[31:26] == 0b000101 (0x5) + if ((instr0 >> 26) == 0x5) + { + uint imm26 = instr0 & 0x03FFFFFFu; + // Sign-extend the 26-bit immediate to 32 bits, then multiply by 4 (instructions are 4-byte aligned). + int offset = (int)(imm26 & 0x02000000u) != 0 + ? unchecked((int)(imm26 | 0xFC000000u)) << 2 + : (int)imm26 << 2; + target = unchecked(address + (ulong)(long)offset); + return IsValidAddress(target); + } + + if (read < 12) + return false; + uint instr1 = ReadInstr(buffer, 4); + uint instr2 = ReadInstr(buffer, 8); + + // StubPrecode: LDR x10, Target ; LDR x12, MethodDesc ; BR x10. Follow the first LDR. + if (IsLdrLiteral64(instr0, out int rt0, out int off0) && rt0 == 10 + && IsLdrLiteral64(instr1, out int rt1, out int _) && rt1 == 12 + && instr2 == 0xD61F0140u) + { + ulong targetSlot = unchecked(address + (ulong)(long)off0); + if (dataReader.ReadPointer(targetSlot, out target) && IsValidAddress(target)) + return true; + target = 0; + return false; + } + + // FixupPrecode: LDR x11, Target ; BR x11 ; LDR x12, MethodDesc. Follow the first LDR. + if (IsLdrLiteral64(instr0, out int rtA, out int offA) && rtA == 11 + && instr1 == 0xD61F0160u + && IsLdrLiteral64(instr2, out int rtB, out int _) && rtB == 12) + { + ulong targetSlot = unchecked(address + (ulong)(long)offA); + if (dataReader.ReadPointer(targetSlot, out target) && IsValidAddress(target)) + return true; + target = 0; + return false; + } + + // CallCountingStub: LDR x9, RemainingCallCount ; LDRH w10, [x9] ; SUBS w10, w10, #1. + // TargetForMethod lives 8 bytes after RemainingCallCount in the data section. + if (IsLdrLiteral64(instr0, out int rtCount, out int offCount) && rtCount == 9 + && instr1 == 0x7940012Au + && instr2 == 0x7100054Au) + { + ulong countSlot = unchecked(address + (ulong)(long)offCount); + if (dataReader.ReadPointer(countSlot + 8, out target) && IsValidAddress(target)) + return true; + target = 0; + return false; + } + + return false; + } } } diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs index d600e7b86a..cc54ddea0c 100644 --- a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs @@ -30,7 +30,7 @@ private static ulong GetMinValidAddress() throw new NotSupportedException($"{System.Runtime.InteropServices.RuntimeInformation.OSDescription} is not supported"); } - private static bool IsValidAddress(ulong address) + protected static bool IsValidAddress(ulong address) // -1 (ulong.MaxValue) address is invalid, and will crash the runtime in older runtimes. https://github.com/dotnet/runtime/pull/90794 // 0 is NULL and therefore never valid. // Addresses less than the minimum virtual address are also invalid. @@ -38,6 +38,15 @@ private static bool IsValidAddress(ulong address) && address != 0 && address >= MinValidAddress; + // When ClrMD's GetMethodByInstructionPointer fails on a call target, the bytes at that + // address may be (a) a small JMP/B thunk the JIT inserted because the real callee was too + // far for a direct relative branch, or (b) a CoreCLR precode/stub (call-counting stub, + // stub precode, fixup precode) — the stable entry point for a tiered method. Architecture + // -specific subclasses decode their respective shapes and return the resolved target + // (the Target slot for precodes) so TryTranslateAddressToName can retry the lookup. + // Best-effort: return false for anything we don't recognise (matches prior behaviour). + protected abstract bool TryFollowJumpTrampoline(State state, ulong address, out ulong target); + private DataTarget Attach(int processId) { bool isSelf = processId == System.Diagnostics.Process.GetCurrentProcess().Id; @@ -266,6 +275,32 @@ protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD, method = runtime.GetMethodByInstructionPointer(newAddress); } + if (method is null) + { + // Chase trampolines/precodes iteratively: a near JMP/B thunk may target another + // near jump, and a stable-entry precode's Target slot may itself currently point at a + // tier-0 → tier-1 promotion stub. Bounded by maxHops so a pathological case + // (corrupted snapshot, self-pointing thunk) can't loop; visited-set short-circuits + // cycles. 8 hops is far more than CoreCLR is known to chain in practice. + ulong current = address; + HashSet? visited = null; + const int maxHops = 8; + for (int hop = 0; hop < maxHops; hop++) + { + if (!TryFollowJumpTrampoline(state, current, out ulong next)) + break; + if (next == current) + break; + visited ??= []; + if (!visited.Add(next)) + break; + method = runtime.GetMethodByInstructionPointer(next); + if (method is not null) + break; + current = next; + } + } + if (method is null) { var methodDescriptor = runtime.GetMethodByHandle(address); @@ -274,6 +309,11 @@ protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD, if (isAddressPrecodeMD) { state.AddressToNameMapping.Add(address, $"Precode of {methodDescriptor.Signature}"); + // The precode resolves to a method handle, but if the underlying method has + // already been JITted we still want to disassemble its body — otherwise a + // call routed through a stable-entry precode never enqueues its target. + if (methodDescriptor.NativeCode > 0 && !state.HandledMethods.Contains(methodDescriptor)) + state.Todo.Enqueue(new MethodInfo(methodDescriptor, depth + 1)); } else { diff --git a/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs index 048dd938bc..930eea4edb 100644 --- a/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs @@ -6,52 +6,8 @@ namespace BenchmarkDotNet.Disassemblers { internal class IntelDisassembler : ClrMdDisassembler { - internal sealed class RuntimeSpecificData - { - // See dotnet/runtime src/coreclr/vm/amd64/thunktemplates.asm/.S for the stub code - // mov rax,QWORD PTR [rip + DATA_SLOT(CallCountingStub, RemainingCallCountCell)] - // dec WORD PTR [rax] - // je LOCAL_LABEL(CountReachedZero) - // jmp QWORD PTR [rip + DATA_SLOT(CallCountingStub, TargetForMethod)] - // LOCAL_LABEL(CountReachedZero): - // jmp QWORD PTR [rip + DATA_SLOT(CallCountingStub, TargetForThresholdReached)] - internal readonly byte[] callCountingStubTemplate = [0x48, 0x8b, 0x05, 0xf9, 0x0f, 0x00, 0x00, 0x66, 0xff, 0x08]; - // mov r10, [rip + DATA_SLOT(StubPrecode, MethodDesc)] - // jmp [rip + DATA_SLOT(StubPrecode, Target)] - internal readonly byte[] stubPrecodeTemplate = [0x4c, 0x8b, 0x15, 0xf9, 0x0f, 0x00, 0x00, 0xff, 0x25, 0xfb, 0x0f, 0x00, 0x00]; - // jmp [rip + DATA_SLOT(FixupPrecode, Target)] - // mov r10, [rip + DATA_SLOT(FixupPrecode, MethodDesc)] - // jmp [rip + DATA_SLOT(FixupPrecode, PrecodeFixupThunk)] - internal readonly byte[] fixupPrecodeTemplate = [0xff, 0x25, 0xfa, 0x0f, 0x00, 0x00, 0x4c, 0x8b, 0x15, 0xfb, 0x0f, 0x00, 0x00, 0xff, 0x25, 0xfd, 0x0f, 0x00, 0x00]; - internal readonly ulong stubPageSize; - - internal RuntimeSpecificData(State state) - { - stubPageSize = (ulong)Environment.SystemPageSize; - if (state.RuntimeVersion.Major >= 8) - { - // In .NET 8, the stub page size was changed to 16kB - stubPageSize = 16384; - // Update the templates so that the offsets are correct - callCountingStubTemplate[4] = 0x3f; - stubPrecodeTemplate[4] = 0x3f; - stubPrecodeTemplate[10] = 0x3f; - fixupPrecodeTemplate[3] = 0x3f; - fixupPrecodeTemplate[10] = 0x3f; - fixupPrecodeTemplate[16] = 0x3f; - } - } - } - - private static readonly Dictionary runtimeSpecificData = []; - protected override IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax) { - if (!runtimeSpecificData.TryGetValue(state.RuntimeVersion, out var data)) - { - runtimeSpecificData.Add(state.RuntimeVersion, data = new RuntimeSpecificData(state)); - } - var reader = new ByteArrayCodeReader(code); var decoder = Decoder.Create(state.Runtime.DataTarget.DataReader.PointerSize * 8, reader); decoder.IP = startAddress; @@ -71,39 +27,8 @@ protected override IEnumerable Decode(byte[] code, ulong startAddress, Stat address = state.Runtime.DataTarget.DataReader.ReadPointer(address); if (state.RuntimeVersion.Major >= 7) { - // Check if the target is a known stub - // The stubs are allocated in interleaved code / data pages in memory. The data part of the stub - // is at an address one memory page higher than the code. - byte[] buffer = new byte[10]; - - FlushCachedDataIfNeeded(state.Runtime.DataTarget.DataReader, address, buffer); - - if (state.Runtime.DataTarget.DataReader.Read(address, buffer) == buffer.Length && buffer.SequenceEqual(data.callCountingStubTemplate)) - { - const ulong TargetMethodAddressSlotOffset = 8; - address = state.Runtime.DataTarget.DataReader.ReadPointer(address + data.stubPageSize + TargetMethodAddressSlotOffset); - } - else - { - buffer = new byte[13]; - if (state.Runtime.DataTarget.DataReader.Read(address, buffer) == buffer.Length && buffer.SequenceEqual(data.stubPrecodeTemplate)) - { - const ulong MethodDescSlotOffset = 0; - address = state.Runtime.DataTarget.DataReader.ReadPointer(address + data.stubPageSize + MethodDescSlotOffset); - isPrestubMD = true; - } - else - { - buffer = new byte[19]; - if (state.Runtime.DataTarget.DataReader.Read(address, buffer) == buffer.Length && buffer.SequenceEqual(data.fixupPrecodeTemplate)) - { - const ulong MethodDescSlotOffset = 8; - address = state.Runtime.DataTarget.DataReader.ReadPointer(address + data.stubPageSize + MethodDescSlotOffset); - isPrestubMD = true; - } - - } - } + FlushCachedDataIfNeeded(state.Runtime.DataTarget.DataReader, address, new byte[1]); + TryResolvePrecode(state.Runtime.DataTarget.DataReader, ref address, out isPrestubMD); } } TryTranslateAddressToName(address, isPrestubMD, state, depth, currentMethod); @@ -120,6 +45,93 @@ protected override IEnumerable Decode(byte[] code, ulong startAddress, Stat } } + // Resolve a precode/stub address (the body of one of the runtime's interleaved code-page + // thunks) to either the underlying MethodDesc handle (so GetMethodByHandle can find the + // method whose JITted body should be disassembled — even when the precode's own Target slot + // still points at PreStub/PrecodeFixupThunk because the call site has never been backpatched) + // or, for call-counting stubs that don't carry a MethodDesc, the TargetForMethod slot. + // Returns true when the bytes at `address` match a known precode shape and `address` was + // rewritten to the resolved slot value; `isPrestubMD` is set when the resolved value is a + // MethodDesc handle (used downstream to dispatch to GetMethodByHandle). + // + // See dotnet/runtime src/coreclr/vm/amd64/thunktemplates.asm/.S for the canonical stub + // shapes. The data-section layout (slot order within the data page) is stable, but the + // offset between the code page and its data section is part of the runtime's allocator + // policy and has changed in the past (currently 16 kB on x64). Reading the RIP-relative + // displacements out of the encoded instructions themselves avoids any dependency on that + // code-to-data gap, so the resolver doesn't need a runtime-version-specific table of stub + // page sizes. + private static bool TryResolvePrecode(IDataReader reader, ref ulong address, out bool isPrestubMD) + { + isPrestubMD = false; + byte[] buffer = new byte[19]; + int read = reader.Read(address, buffer); + if (read < 13) + return false; + + // FixupPrecode (19 bytes when read available): + // FF 25 [disp32] JMP qword [rip+disp] -> Target slot + // 4C 8B 15 [disp32] MOV r10, [rip+disp] -> MethodDesc slot + // FF 25 [disp32] JMP qword [rip+disp] -> PrecodeFixupThunk slot + // Resolve to MethodDesc so we can recover the ClrMethod even if the call site has + // not been backpatched and Target still points at PrecodeFixupThunk. + if (read >= 19 + && buffer[0] == 0xFF && buffer[1] == 0x25 + && buffer[6] == 0x4C && buffer[7] == 0x8B && buffer[8] == 0x15 + && buffer[13] == 0xFF && buffer[14] == 0x25) + { + int dispMD = BitConverter.ToInt32(buffer, 9); + ulong mdSlot = unchecked(address + 13 + (ulong)(long)dispMD); + if (reader.ReadPointer(mdSlot, out ulong md) && IsValidAddress(md)) + { + address = md; + isPrestubMD = true; + return true; + } + return false; + } + + // StubPrecode (13 bytes): + // 4C 8B 15 [disp32] MOV r10, [rip+disp] -> MethodDesc slot + // FF 25 [disp32] JMP qword [rip+disp] -> Target slot (usually PreStub) + if (buffer[0] == 0x4C && buffer[1] == 0x8B && buffer[2] == 0x15 + && buffer[7] == 0xFF && buffer[8] == 0x25) + { + int dispMD = BitConverter.ToInt32(buffer, 3); + ulong mdSlot = unchecked(address + 7 + (ulong)(long)dispMD); + if (reader.ReadPointer(mdSlot, out ulong md) && IsValidAddress(md)) + { + address = md; + isPrestubMD = true; + return true; + } + return false; + } + + // CallCountingStub (matches first ~10 bytes): + // 48 8B 05 [disp32] MOV rax, [rip+disp] -> RemainingCallCount slot + // 66 FF 08 DEC word ptr [rax] + // (later instructions JMP to TargetForMethod, which lives 8 bytes after + // RemainingCallCount in the data section) + // No MethodDesc slot here, so we have to read TargetForMethod and rely on + // GetMethodByInstructionPointer to identify the live tier-1 code. + if (read >= 10 + && buffer[0] == 0x48 && buffer[1] == 0x8B && buffer[2] == 0x05 + && buffer[7] == 0x66 && buffer[8] == 0xFF && buffer[9] == 0x08) + { + int dispCount = BitConverter.ToInt32(buffer, 3); + ulong countSlot = unchecked(address + 7 + (ulong)(long)dispCount); + if (reader.ReadPointer(countSlot + 8, out ulong target) && IsValidAddress(target)) + { + address = target; + return true; + } + return false; + } + + return false; + } + private static bool TryGetReferencedAddress(Instruction instruction, uint pointerSize, out ulong referencedAddress) { for (int i = 0; i < instruction.OpCount; i++) @@ -152,5 +164,94 @@ private static bool TryGetReferencedAddress(Instruction instruction, uint pointe referencedAddress = default; return false; } + + // Recognise the common x86/x64 JMP trampoline shapes the CLR JIT emits when a call's real + // target is out of rel32 range, and the precode/stub shapes the runtime emits as the stable + // entry point for tiered methods (so a direct `call rel32` landing on the precode still + // resolves to the underlying method): + // E9 rel32 — JMP near rel32 (5 bytes) + // EB rel8 — JMP short (2 bytes) + // FF 25 disp32 — JMP qword [rip+d] (6 bytes, RIP-relative) — also matches FixupPrecode + // 48 B8 imm64 ; FF E0 — MOV rax,imm64;JMP rax (12 bytes) + // CallCountingStub (opcode match) — reads TargetForMethod slot + // StubPrecode (opcode match) — reads Target slot + // Slot displacements are extracted from the encoded instructions themselves, so the stub + // recognition doesn't depend on the runtime's code-to-data offset. Writes the resolved + // target into `target` and returns true if one matches. + protected override bool TryFollowJumpTrampoline(State state, ulong address, out ulong target) + { + target = 0; + IDataReader dataReader = state.Runtime.DataTarget.DataReader; + byte[] buffer = new byte[13]; + int read = dataReader.Read(address, buffer); + if (read < 2) + return false; + + // E9 rel32 — JMP near rel32 (target = next-instr + sign_extended(rel32)) + if (read >= 5 && buffer[0] == 0xE9) + { + int rel = BitConverter.ToInt32(buffer, 1); + target = unchecked(address + 5 + (ulong)(long)rel); + return IsValidAddress(target); + } + + // EB rel8 — JMP short (target = next-instr + sign_extended(rel8)) + if (buffer[0] == 0xEB) + { + sbyte rel = (sbyte)buffer[1]; + target = unchecked(address + 2 + (ulong)(long)rel); + return IsValidAddress(target); + } + + // FF 25 disp32 — JMP qword ptr [rip+disp32]; the slot at rip+disp32 holds the actual + // target. This also matches FixupPrecode (Target slot lives at data offset 0). + if (read >= 6 && buffer[0] == 0xFF && buffer[1] == 0x25) + { + int disp = BitConverter.ToInt32(buffer, 2); + ulong slot = unchecked(address + 6 + (ulong)(long)disp); + if (dataReader.ReadPointer(slot, out ulong slotTarget) && IsValidAddress(slotTarget)) + { + target = slotTarget; + return true; + } + return false; + } + + // 48 B8 imm64 ; FF E0 — MOV rax, imm64; JMP rax + if (read >= 12 && buffer[0] == 0x48 && buffer[1] == 0xB8 && buffer[10] == 0xFF && buffer[11] == 0xE0) + { + target = BitConverter.ToUInt64(buffer, 2); + return IsValidAddress(target); + } + + // CallCountingStub: MOV rax, [rip+disp32] ; DEC word ptr [rax] ; ... (later JMP via + // TargetForMethod slot, which lives 8 bytes after RemainingCallCount in the data page). + if (read >= 10 + && buffer[0] == 0x48 && buffer[1] == 0x8B && buffer[2] == 0x05 + && buffer[7] == 0x66 && buffer[8] == 0xFF && buffer[9] == 0x08) + { + int disp = BitConverter.ToInt32(buffer, 3); + ulong countSlot = unchecked(address + 7 + (ulong)(long)disp); + if (dataReader.ReadPointer(countSlot + 8, out target) && IsValidAddress(target)) + return true; + target = 0; + return false; + } + + // StubPrecode: MOV r10, [rip+disp32] ; JMP [rip+disp32]. Follow the JMP slot. + if (read >= 13 + && buffer[0] == 0x4C && buffer[1] == 0x8B && buffer[2] == 0x15 + && buffer[7] == 0xFF && buffer[8] == 0x25) + { + int disp = BitConverter.ToInt32(buffer, 9); + ulong slot = unchecked(address + 13 + (ulong)(long)disp); + if (dataReader.ReadPointer(slot, out target) && IsValidAddress(target)) + return true; + target = 0; + return false; + } + + return false; + } } } diff --git a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs index ffe2724a09..acaca6f0ad 100644 --- a/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ProcessExtensions.cs @@ -134,9 +134,7 @@ internal static void SetEnvironmentVariables(this ProcessStartInfo start, Benchm // disable ReSharper's Dynamic Program Analysis (see https://github.com/dotnet/BenchmarkDotNet/issues/1871 for details) start.Environment["JETBRAINS_DPA_AGENT_ENABLE"] = "0"; - if (benchmarkCase.Job.ResolveValueAsNullable(RunMode.RunStrategyCharacteristic) != RunStrategy.ColdStart - // CallCountingDelayMs=0 breaks DisassemblyDiagnoser, so we only set it if the job doesn't need disassembly. https://github.com/dotnet/runtime/issues/117339 - && !benchmarkCase.Config.HasDisassemblyDiagnoser()) + if (benchmarkCase.Job.ResolveValueAsNullable(RunMode.RunStrategyCharacteristic) != RunStrategy.ColdStart) { SetClrEnvironmentVariables(start, JitInfo.EnvCallCountingDelayMs, "0"); }