Skip to content
Open
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
21 changes: 19 additions & 2 deletions docs/design/datacontracts/GCInfo.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Contract GCInfo

This contract is for fetching information related to GCInfo associated with native code. Currently, this contract does not support x86 architecture.
This contract is for fetching information related to GCInfo associated with native code.

The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is not currently supported by this contract.
The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is partially supported by this contract.

## APIs of contract

Expand All @@ -25,10 +25,27 @@ uint GetCodeLength(IGCInfoHandle handle);
// Returns the stack base register number decoded from GCInfo
uint GetStackBaseRegister(IGCInfoHandle handle);

// Returns the size in bytes of the outgoing-argument scratch area in the current
// frame, decoded from `_fixedStackParameterScratchArea` (`SizeOfStackOutgoingAndScratchArea`,
// encoded via `SIZE_OF_STACK_AREA_ENCBASE`). Platform-specific and can be non-zero on
// AMD64/ARM64/ARM/RISCV64/LoongArch64. Not supported on x86 (the x86 GC info scheme has
// no separate scratch-area concept; the x86 GC walker uses per-offset transitions).
uint GetSizeOfStackParameterArea(IGCInfoHandle handle);

// Returns the size in bytes of stack-passed arguments that the callee pops on return,
// mirroring native `EECodeManager::GetStackParameterSize` (eetwain.cpp). Non-zero only
// on x86 (where managed code uses `__stdcall` calling convention with callee-popped args):
// returns 0 for varargs (caller-popped) and otherwise the argument size from the GC info
// header. Returns 0 on every other architecture.
uint GetCalleePoppedArgumentsSize(IGCInfoHandle handle);

// Returns the list of interruptible code offset ranges from the GCInfo
// (not implemented for x86 — x86 encodes per-offset transitions rather than explicit ranges).
IReadOnlyList<InterruptibleRange> GetInterruptibleRanges(IGCInfoHandle handle);

// Returns all live GC slots at the given instruction offset
// (not implemented for x86 — see X86GCInfo for the underlying transition data; the cDAC
// adapter is future work).
IReadOnlyList<LiveSlot> EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, GcSlotEnumerationOptions options);
```

Expand Down
2 changes: 2 additions & 0 deletions docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,8 @@ The x86 platform has some major differences to other platforms. In general this
#### GCInfo Parsing
The GCInfo structure is encoded using a variety of formats to optimize for speed of decoding and size on disk. For information on decoding and parsing refer to [GC Information Encoding for x86](../coreclr/jit/jit-gc-info-x86.md).

The x86 GCInfo decoder lives under the [GCInfo contract](GCInfo.md) at `src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/` (`X86GCInfo` and the supporting `InfoHdr`, `GCArgTable`, `GCTransition`, `CallPattern`, `GCInfoTargetExtensions` types). It is shared between the GCInfo contract (which exposes the offset-independent queries `GetCodeLength` / `GetStackBaseRegister` / `GetSizeOfStackParameterArea` for SOS callers) and the x86 stack walker (which constructs `X86GCInfo` directly with a `relativeOffset` to access offset-bound state — `IsInProlog`, `IsInEpilog`, `PrologOffset`, `EpilogOffset`, `PushedArgSize` — that is not exposed through `IGCInfoDecoder`).

#### Unwinding Algorithm

The x86 architecture uses a custom unwinding algorithm defined in `gc_unwind_x86.inl`. The cDAC uses a copy of this algorithm ported to managed code in `X86Unwinder.cs`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public interface IGCInfo : IContract
uint GetCodeLength(IGCInfoHandle handle) => throw new NotImplementedException();
uint GetStackBaseRegister(IGCInfoHandle handle) => throw new NotImplementedException();
uint GetSizeOfStackParameterArea(IGCInfoHandle handle) => throw new NotImplementedException();
uint GetCalleePoppedArgumentsSize(IGCInfoHandle handle) => throw new NotImplementedException();
IReadOnlyList<InterruptibleRange> GetInterruptibleRanges(IGCInfoHandle handle) => throw new NotImplementedException();
IReadOnlyList<LiveSlot> EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, GcSlotEnumerationOptions options) => throw new NotImplementedException();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,9 +419,9 @@ uint IExecutionManager.GetStackParameterSize(CodeBlockHandle codeInfoHandle)
if (gcInfoAddress == TargetPointer.Null)
throw new InvalidOperationException($"GC info not available for {codeInfoHandle.Address}");

uint relOffset = (uint)eman.GetRelativeOffset(codeInfoHandle).Value;
StackWalkHelpers.X86.GCInfo gcInfo = new(_target, gcInfoAddress, gcInfoVersion, relOffset);
return gcInfo.Header.VarArgs ? 0u : gcInfo.Header.ArgCount * (uint)_target.PointerSize;
IGCInfo gcInfoContract = _target.Contracts.GCInfo;
IGCInfoHandle handle = gcInfoContract.DecodePlatformSpecificGCInfo(gcInfoAddress, gcInfoVersion);
return gcInfoContract.GetCalleePoppedArgumentsSize(handle);
}

TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers;

namespace Microsoft.Diagnostics.DataContractReader.Contracts;

/// <summary>
/// IGCInfo implementation for x86. x86 uses the legacy bit-packed InfoHdr GC info format,
/// which is fundamentally different from the GcInfoDecoder format used by every other
/// architecture, so it cannot share <see cref="GCInfo_1{TTraits}"/>.
/// The decoder lives at <see cref="GCInfoHelpers.X86.X86GCInfo"/> and is shared with the
/// x86 stack walker.
/// </summary>
internal sealed class GCInfoX86_1 : IGCInfo
{
private readonly Target _target;

internal GCInfoX86_1(Target target)
{
_target = target;
}

IGCInfoHandle IGCInfo.DecodePlatformSpecificGCInfo(TargetPointer gcInfoAddress, uint gcVersion)
=> new GCInfoHelpers.X86.X86GCInfo(_target, gcInfoAddress, gcVersion);

IGCInfoHandle IGCInfo.DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersion)
=> new GcInfoDecoder<InterpreterGCInfoTraits>(_target, gcInfoAddress, gcVersion);

uint IGCInfo.GetCodeLength(IGCInfoHandle gcInfoHandle)
=> AssertCorrectHandle(gcInfoHandle).GetCodeLength();

uint IGCInfo.GetStackBaseRegister(IGCInfoHandle gcInfoHandle)
=> AssertCorrectHandle(gcInfoHandle).GetStackBaseRegister();

uint IGCInfo.GetSizeOfStackParameterArea(IGCInfoHandle gcInfoHandle)
=> AssertCorrectHandle(gcInfoHandle).GetSizeOfStackParameterArea();

uint IGCInfo.GetCalleePoppedArgumentsSize(IGCInfoHandle gcInfoHandle)
=> AssertCorrectHandle(gcInfoHandle).GetCalleePoppedArgumentsSize();

IReadOnlyList<InterruptibleRange> IGCInfo.GetInterruptibleRanges(IGCInfoHandle gcInfoHandle)
=> AssertCorrectHandle(gcInfoHandle).GetInterruptibleRanges();

IReadOnlyList<LiveSlot> IGCInfo.EnumerateLiveSlots(IGCInfoHandle gcInfoHandle, uint instructionOffset, GcSlotEnumerationOptions options)
=> AssertCorrectHandle(gcInfoHandle).EnumerateLiveSlots(instructionOffset, options);

private static IGCInfoDecoder AssertCorrectHandle(IGCInfoHandle gcInfoHandle)
{
if (gcInfoHandle is not IGCInfoDecoder handle)
throw new System.ArgumentException("Invalid GC info handle", nameof(gcInfoHandle));

return handle;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ uint IGCInfo.GetSizeOfStackParameterArea(IGCInfoHandle gcInfoHandle)
return handle.GetSizeOfStackParameterArea();
}

uint IGCInfo.GetCalleePoppedArgumentsSize(IGCInfoHandle gcInfoHandle)
{
IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle);
return handle.GetCalleePoppedArgumentsSize();
}

IReadOnlyList<InterruptibleRange> IGCInfo.GetInterruptibleRanges(IGCInfoHandle gcInfoHandle)
{
IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ internal interface IGCInfoDecoder : IGCInfoHandle
uint GetCodeLength();
uint GetStackBaseRegister();
uint GetSizeOfStackParameterArea();

// Default 0; mirrors native EECodeManager::GetStackParameterSize, which only
// returns non-zero on x86 (where managed code uses __stdcall, callee-popped args).
uint GetCalleePoppedArgumentsSize() => 0;

IReadOnlyList<InterruptibleRange> GetInterruptibleRanges();
IReadOnlyList<LiveSlot> EnumerateLiveSlots(uint instructionOffset, GcSlotEnumerationOptions options);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86;
namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86;

internal static class CallPattern
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86;
namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86;

public class GCArgTable
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using System.Diagnostics;
using System.Linq;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86;
namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86;

[Flags]
public enum RegMask
Expand All @@ -27,7 +27,7 @@ public enum RegMask
RM_CALLEE_TRASHED = RM_ALL & ~RM_CALLEE_SAVED,
}

public record GCInfo
public record X86GCInfo : IGCInfoDecoder
{
private const uint MINIMUM_SUPPORTED_GCINFO_VERSION = 4;
private const uint MAXIMUM_SUPPORTED_GCINFO_VERSION = 5;
Expand Down Expand Up @@ -69,7 +69,7 @@ public record GCInfo
public uint PushedArgSize => _pushedArgSize.Value;
private readonly Lazy<uint> _pushedArgSize;

public GCInfo(Target target, TargetPointer gcInfoAddress, uint gcInfoVersion, uint relativeOffset)
public X86GCInfo(Target target, TargetPointer gcInfoAddress, uint gcInfoVersion, uint relativeOffset = 0)
{
if (gcInfoVersion < MINIMUM_SUPPORTED_GCINFO_VERSION)
{
Expand Down Expand Up @@ -242,4 +242,35 @@ private uint CalculatePushedArgSize()

return (uint)(depth * _target.PointerSize);
}

uint IGCInfoDecoder.GetCodeLength() => MethodSize;

uint IGCInfoDecoder.GetStackBaseRegister()
{
// x86 ModRM register encoding: ESP = 4, EBP = 5. EBP is the stack base for
// EBP-frames and double-aligned frames; otherwise stack base is ESP.
const uint REG_ESP = 4;
const uint REG_EBP = 5;
return (Header.EbpFrame || Header.DoubleAlign) ? REG_EBP : REG_ESP;
}

uint IGCInfoDecoder.GetSizeOfStackParameterArea()
=> throw new NotSupportedException(
"x86 GC info does not encode a separate outgoing-argument scratch area; the cDAC " +
"GC scanner does not consume scratch-area sizing on x86 (the legacy x86 GC walker " +
"reasons over per-offset transitions instead).");

uint IGCInfoDecoder.GetCalleePoppedArgumentsSize()
{
// Mirrors native ::GetStackParameterSize(hdrInfo) in gc_unwind_x86.inl: varargs are
// caller-popped (return 0); other methods report the argument size from the GC info
// header. Used by EECodeManager::GetStackParameterSize on x86.
return Header.VarArgs ? 0u : Header.ArgCount * (uint)_target.PointerSize;
}

IReadOnlyList<InterruptibleRange> IGCInfoDecoder.GetInterruptibleRanges()
=> throw new NotSupportedException("x86 GC info does not encode explicit interruptible ranges; per-offset transitions are used instead. Decoding for the cDAC IGCInfoDecoder consumers is not yet implemented.");

IReadOnlyList<LiveSlot> IGCInfoDecoder.EnumerateLiveSlots(uint instructionOffset, GcSlotEnumerationOptions options)
=> throw new NotSupportedException("x86 GC info live-slot enumeration through IGCInfoDecoder is not yet implemented; the underlying InfoHdr/Transitions data is decoded but the IGCInfoDecoder.EnumerateLiveSlots adapter is future work.");
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86;
namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86;

public static class GCInfoTargetExtensions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Collections.Generic;
using System.Text;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86;
namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86;

public enum Action
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Collections.Immutable;
using System.Diagnostics;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86;
namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86;

/// <summary>
/// based on <a href="https://github.com/dotnet/runtime/blob/main/src/coreclr/inc/gcinfotypes.h">src\inc\gcinfotypes.h</a> InfoHdrSmall
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Linq;
using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions;
using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86;
using static Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86Context;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86;
Expand Down Expand Up @@ -62,7 +63,7 @@ public bool Unwind(ref X86Context context)
TargetPointer funcletStart = eman.GetFuncletStartAddress(cbh);
bool isFunclet = eman.IsFunclet(cbh);

GCInfo gcInfo = new(_target, gcInfoAddress, gcInfoVersion, relOffset);
X86GCInfo gcInfo = new(_target, gcInfoAddress, gcInfoVersion, relOffset);

if (gcInfo.IsInEpilog)
{
Expand Down Expand Up @@ -96,7 +97,7 @@ public bool Unwind(ref X86Context context)
#endregion
#region Unwind Logic

private void UnwindEpilog(ref X86Context context, GCInfo gcInfo, TargetPointer epilogBase)
private void UnwindEpilog(ref X86Context context, X86GCInfo gcInfo, TargetPointer epilogBase)
{
Debug.Assert(gcInfo.IsInEpilog);
Debug.Assert(gcInfo.EpilogOffset > 0);
Expand All @@ -114,7 +115,7 @@ private void UnwindEpilog(ref X86Context context, GCInfo gcInfo, TargetPointer e
context.Esp += ESPIncrementOnReturn(gcInfo);
}

private void UnwindEbpDoubleAlignFrameEpilog(ref X86Context context, GCInfo gcInfo, TargetPointer epilogBase)
private void UnwindEbpDoubleAlignFrameEpilog(ref X86Context context, X86GCInfo gcInfo, TargetPointer epilogBase)
{
/* See how many instructions we have executed in the
epilog to determine which callee-saved registers
Expand Down Expand Up @@ -241,7 +242,7 @@ have already been popped */
context.Esp = esp;
}

private void UnwindEspFrameEpilog(ref X86Context context, GCInfo gcInfo, TargetPointer epilogBase)
private void UnwindEspFrameEpilog(ref X86Context context, X86GCInfo gcInfo, TargetPointer epilogBase)
{
Debug.Assert(gcInfo.IsInEpilog);
Debug.Assert(!gcInfo.Header.EbpFrame && !gcInfo.Header.DoubleAlign);
Expand Down Expand Up @@ -307,7 +308,7 @@ regsMask as well to exclude registers which have already been popped. */
context.Esp = esp;
}

private void UnwindEspFrame(ref X86Context context, GCInfo gcInfo, TargetPointer methodStart)
private void UnwindEspFrame(ref X86Context context, X86GCInfo gcInfo, TargetPointer methodStart)
{
Debug.Assert(!gcInfo.Header.EbpFrame && !gcInfo.Header.DoubleAlign);
Debug.Assert(!gcInfo.IsInEpilog);
Expand Down Expand Up @@ -347,7 +348,7 @@ private void UnwindEspFrame(ref X86Context context, GCInfo gcInfo, TargetPointer
context.Esp = esp + ESPIncrementOnReturn(gcInfo);
}

private void UnwindEspFrameProlog(ref X86Context context, GCInfo gcInfo, TargetPointer methodStart)
private void UnwindEspFrameProlog(ref X86Context context, X86GCInfo gcInfo, TargetPointer methodStart)
{
Debug.Assert(gcInfo.IsInProlog);
Debug.Assert(!gcInfo.Header.EbpFrame && !gcInfo.Header.DoubleAlign);
Expand Down Expand Up @@ -426,7 +427,7 @@ private void UnwindEspFrameProlog(ref X86Context context, GCInfo gcInfo, TargetP

private bool UnwindEbpDoubleAlignFrame(
ref X86Context context,
GCInfo gcInfo,
X86GCInfo gcInfo,
TargetPointer methodStart,
TargetPointer funcletStart,
bool isFunclet)
Expand Down Expand Up @@ -510,7 +511,7 @@ private bool UnwindEbpDoubleAlignFrame(
return true;
}

private void UnwindEbpDoubleAlignFrameProlog(ref X86Context context, GCInfo gcInfo, TargetPointer methodStart)
private void UnwindEbpDoubleAlignFrameProlog(ref X86Context context, X86GCInfo gcInfo, TargetPointer methodStart)
{
Debug.Assert(gcInfo.IsInProlog);
Debug.Assert(gcInfo.Header.EbpFrame || gcInfo.Header.DoubleAlign);
Expand Down Expand Up @@ -610,7 +611,7 @@ private static bool InstructionAlreadyExecuted(uint walkOffset, uint actualHaltO
return walkOffset < actualHaltOffset;
}

private uint ESPIncrementOnReturn(GCInfo gcInfo)
private uint ESPIncrementOnReturn(X86GCInfo gcInfo)
{

uint stackParameterSize = gcInfo.Header.VarArgs ? 0 // varargs are caller-popped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public static void Register(ContractRegistry registry)
return arch switch
{
RuntimeInfoArchitecture.X64 => new GCInfo_1<AMD64GCInfoTraits>(t),
RuntimeInfoArchitecture.X86 => new GCInfoX86_1(t),
RuntimeInfoArchitecture.Arm64 => new GCInfo_1<ARM64GCInfoTraits>(t),
RuntimeInfoArchitecture.Arm => new GCInfo_1<ARMGCInfoTraits>(t),
RuntimeInfoArchitecture.LoongArch64 => new GCInfo_1<LoongArch64GCInfoTraits>(t),
Expand Down
Loading
Loading