Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c33a578
[cdac] x86: implement IGCInfoDecoder.EnumerateLiveSlots; unblock GCRo…
Jun 17, 2026
414f250
docs: move GCInfo.md x86 details to dedicated section
Jun 17, 2026
187b3eb
[cdac] x86 GetInterruptibleRanges: implement properly + correct doc
Jun 18, 2026
1b6868d
[cdac] Rename x86 GCInfo.cs to X86GCInfo.cs to match the class name
Jun 18, 2026
a000a57
[cdac] x86 VarPtr-tracked locals: pass through both interior and pinn…
Jun 18, 2026
9ba9ab9
[cdac] x86 GCTransition: store isThis/iptr in transition ctors
Jun 18, 2026
dc4acf3
[cdac] x86 GcArgTable: fix curOffs scope in GetTransitionsEbpFrame
Jun 18, 2026
e888634
[cdac] x86 EnumerateLiveSlots: stress-test correctness fixes
Jun 18, 2026
177e383
[cdac] x86 EnumerateLiveSlots: handle ParentOfFuncletStackFrame and A…
Jun 18, 2026
d8c9e5c
[cdac] x86 GCInfo: trim native code references and document Enumerate…
Jun 18, 2026
e2872db
[cdac] x86 GCInfo: keep file name as GCInfo.cs
Jun 18, 2026
68807ed
[cdac] GCInfo.md: x86 EnumerateLiveSlots and GetInterruptibleRanges a…
Jun 18, 2026
0518cce
[cdac] GCInfo.md: keep x86 specifics confined to the x86 specifics se…
Jun 18, 2026
3fd127c
[cdac] GCInfo.md: drop x86 Supported APIs table
Jun 18, 2026
8171ec3
[cdac] x86 GcArgTable: drop redundant curOffs scope comment
Jun 18, 2026
53de8f3
[cdac] DumpTests.targets: drop local-dev DebuggeeFilter property
Jun 18, 2026
d263b73
[cdac] x86 GCInfo: address PR review feedback
Jun 18, 2026
1763ba2
[cdac] x86 GcArgTable: emit negative stack-depth delta at partial-int…
Jun 18, 2026
d15cbd3
[cdac] x86 ApplyPointerTransition: respect IsPtr=false on non-pointer…
Jun 18, 2026
fbb6c03
[cdac] runtime-diagnostics pipeline: add windows_x86 to cDAC stress t…
Jun 19, 2026
99cee90
[cdac] x86 EnumerateLiveSlots: bias ESP-frame untracked/VarPtr slots …
Jun 19, 2026
326c469
[cdac] x86 ScanDynamicHelperFrame: swap ObjectArg / ObjectArg2 offsets
Jun 19, 2026
f67e53d
[cdac] x86 GCInfo: address PR review feedback round 2
Jun 19, 2026
2b47ee8
[cdac] x86 stress fixes uncovered by Helix CI
Jun 20, 2026
32c11d4
[cdac] x86 GCRefMap: use OffsetOfArgs for stack-arg positions
Jun 20, 2026
b772add
[cdac] Document known x86 stress flake (#129545/#129546 GC hole)
Jun 20, 2026
9dc6337
[cdac] x86 stress flake doc: correct attribution (not #129545/#129546)
Jun 20, 2026
fdc72cf
[cdac] x86 GCArgTable: case 0xFB code-delta is += not =
Jun 20, 2026
0fdc985
[cdac] x86 GCArgTable: don't emit spurious call for 'this-pointer' tag
Jun 20, 2026
37e9d47
[cdac] GCInfo.md: refresh x86 specifics for recent decoder fixes
Jun 21, 2026
fc6610a
[cdac] x86 stack-walker: compute cbStackPop in transition Frames
Jun 21, 2026
89f3cdc
[cdac] squash cdac-shared-argiterator: ICallingConvention contract + …
Jun 25, 2026
8bed0e2
[cdac] x86: rewire stack-walker to use shared CallingConvention/GCRefMap
Jun 25, 2026
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
41 changes: 41 additions & 0 deletions docs/design/datacontracts/CallingConvention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Contract CallingConvention

This contract walks a method's argument signature using the runtime's
calling-convention rules so consumers can locate each argument on the
caller's transition frame and reason about which slots hold GC references.

The actual ABI (which registers hold which arguments, what alignment and
padding rules apply, how structs are promoted to registers vs spilled, how
varargs are passed, etc.) is documented in the CLR ABI specs and is not
re-described here:

- [Common CLR ABI conventions](../coreclr/botr/clr-abi.md)

This contract's responsibility is to surface the *result* of that walk in
a form the cDAC can use, byte-for-byte compatible with what the runtime
itself produces.

## APIs of contract

``` csharp
// Encode the argument GCRefMap blob for `methodDesc` byte-for-byte
// compatible with the runtime's ComputeCallRefMap (frames.cpp).
// Returns false when this contract declines to encode the method
// (e.g. an unported ABI path); callers should map false to E_NOTIMPL.
// When false, the value of `blob` is unspecified.
bool TryComputeArgGCRefMapBlob(MethodDescHandle methodDesc, out byte[] blob);
```

## Version 1

The single API is implemented by walking the shared `ArgIterator`
(`src/coreclr/tools/Common/CallingConvention/ArgIterator.cs`) and feeding
the per-argument result into a GCRefMap encoder that mirrors
`GCRefMapBuilder` (`src/coreclr/inc/gcrefmap.h`).

`TryComputeArgGCRefMapBlob` returns `false` for any method whose
signature, ABI path, or generic context the encoder hasn't been taught
yet. The cdacstress harness (`src/coreclr/vm/cdacstress.cpp`,
`ARGITER` sub-check) uses byte-for-byte comparison of the returned blob
against the runtime's `ComputeCallRefMap` output as its correctness
oracle.
51 changes: 48 additions & 3 deletions docs/design/datacontracts/GCInfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,9 @@ uint GetSizeOfStackParameterArea(IGCInfoHandle handle);
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 Expand Up @@ -603,3 +600,51 @@ IReadOnlyList<LiveSlot> EnumerateLiveSlots(IGCInfoHandle handle,
// Collect each live slot into a list and return it.
}
```


## x86 specifics

x86 uses the legacy bit-packed `InfoHdr` byte-stream encoding (`src/coreclr/vm/gc_unwind_x86.inl`, `src/coreclr/inc/gcdecoder.cpp`) rather than the modern `GcInfoDecoder` shared by all other architectures. The cDAC decoder lives at `src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/` and is shared with the x86 stack walker.

A few API behaviors are worth calling out:

- `GetSizeOfStackParameterArea` always returns 0 -- x86 has no separate outgoing-argument scratch area; pushed args are reported directly through the per-offset transition stream.
- `GetInterruptibleRanges` reports one range covering the post-prolog body for fully-interruptible methods, or a single-byte range per call site for partially-interruptible methods. Consumed by `StackWalk_1.WalkStackReferences` for the catch-handler PC override (x86 uses the funclet EH model, see PR [#115957](https://github.com/dotnet/runtime/pull/115957)).
- `EnumerateLiveSlots` mirrors `EnumGcRefsX86`; see below.

### `EnumerateLiveSlots` behavior

Mirrors `EnumGcRefsX86` (gc_unwind_x86.inl), `scanArgRegTableI` (fully-interruptible) and `scanArgRegTable` (partially-interruptible).

Early-return gates (no slots reported):
- `IsParentOfFuncletStackFrame` — the funclet sharing this parent's locals will report them itself.
- Code offset is in the prolog or any epilog (only reachable via `IsExecutionAborted`; GC info does not describe these regions).
- `IsExecutionAborted` and the method is not fully interruptible.

Filter-funclet `SuppressUntrackedSlots` is honored within the untracked-locals step (the parent frame already reported them) but does not gate the rest of the walk.

Sources of live slots:
- **Untracked frame locals** — always live for the entire method body. Encoded as signed delta-from-previous offsets in the untracked table. On EBP frames the encoded value resolves directly to `EBP + stkOffs` (`FRAMEREG_REL`). On ESP frames the encoded value is `argBase`-relative where `argBase = ESP + pushedSize` (mirrors `EnumGcRefsX86`); the decoder rebases to a true `SP_REL` offset by adding the pushed-arg size computed at the queried instruction offset.
- **VarPtr-tracked locals** — live when the lifetime-check offset is within `[BeginOffset, EndOffset)`. EBP-frame offsets are stored as their negated form (mirrors `if (info.ebpFrame) stkOffs = -stkOffs`); ESP-frame offsets receive the same `pushedSize` bias as untracked locals. Non-active frames evaluate the lifetime at `instructionOffset - 1` because a variable can be dead at the return address (e.g. when the call is the last instruction of a try and the return is the catch-block jump target).
- **Live registers** — accumulated from the LIVE/DEAD transition stream up to the queried offset. Callee-saved registers (EBX/EBP/ESI/EDI) are reported when execution will continue; callee-trashed scratch (EAX/ECX/EDX) is reported only on the active leaf frame. On non-leaf frames register liveness is evaluated at the instruction *before* the call (`regOffset = instructionOffset - 1`) since liveness can change across calls.
- **Pushed pointer args** — for fully-interruptible code, accumulated from the PUSH/POP transition stream. Non-pointer pushes (`IsPtr = false`) still bump the stack depth (so subsequent pushed-ptr indices stay aligned) but do not contribute a slot. At emit time, once `finalDepth` is known, each tracked push is reported as a positive SP-relative offset: `addr = ESP_call + (finalDepth - 1 - pushIndex) * sizeof(DWORD)` (mirrors `pPendingArgFirst - i * sizeof(DWORD)` in `EnumGcRefsX86`). The translation must be deferred because subsequent pushes/pops change `finalDepth`. For partially-interruptible call sites, slots come from the matching `GcTransitionCall` instead: explicit per-pointer offsets in the huge (0xFB) encoding, or a uint32 `ArgMask` / `IArgs` bitmap walked low-to-high with `addr = ESP + i * sizeof(DWORD)` for the tiny / small / medium / large encodings.
- **`IsParentOfFuncletStackFrame`** suppresses all reporting from the parent: the funclet itself reports the shared locals via `EnumerateLiveSlots` on the funclet frame.

When `ReportFPBasedSlotsOnly` is set, the result list is filtered to drop register slots and any stack slot whose base is not the frame register (matching `GCInfoDecoder.ReportSlot`).

### Encoding correctness notes (x86)

A few subtleties of the legacy byte-stream encoding caught during stress validation, mirrored from native and worth remembering when modifying the decoder:

- **Huge call-site (`0xFB`) code-delta is cumulative.** The uint32 code delta is `curOffs += delta`, not `curOffs = delta`. Assigning loses all preceding offset accumulation and corrupts every subsequent call site.
- **Partial-EBP this-pointer tag (`val & 0x80 == 0 && val & 0x0F == 0`).** This encodes the callee-saved register holding `this` at the next call site; native (`gc_unwind_x86.inl` ~line 970) sets `thisPtrReg` only and does *not* record a call entry. Emitting a `GcTransitionCall` at the current offset would overwrite the real call site's `CallRegisters` when both fall at the same `curOffs`.
- **Partial-interrupt EBP-less call sites emit a negative stack-depth delta** (callee-popped args reverse the prior pushes); the transition stream is generated accordingly.
- **`0xC0..0xCF` partial-interruptible byte range** in the huge encoding is a call entry that names only callee-saved registers (no pointer args), distinct from `0xFD..0xFF`.

### Deferred edges

These do not affect the GC root scan / `WalkStackReferences` path validated by the cDAC stress suite, but are noted for future work:

- `info.thisPtrResult` reporting for synchronized methods on the `!willContinueExecution` path (the regular live-register report covers `willContinueExecution`, which is what stress exercises).
- VarPtr `0x2` legacy-encoder "this" bit (the modern x86 JIT uses `0x2` only for pinned, which we already pass through; the legacy "this" interpretation never appears in code from the current JIT).
- `IPtrMask` (`0xF0`) interior-pointer bitmaps for pushed args — accepted by the decoder as informational, but the bitmap is not yet applied to pushed-arg slots. Only relevant on the partial-interruptible ESP-frame path; in practice the current x86 JIT rarely emits these.
32 changes: 32 additions & 0 deletions docs/design/datacontracts/RuntimeTypeSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ partial interface IRuntimeTypeSystem : IContract
public virtual TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind);
// True if the MethodTable represents a type that contains managed references
public virtual bool ContainsGCPointers(TypeHandle typeHandle);
// True if the MethodTable represents a byref-like value type (Span<T>, ReadOnlySpan<T>, any ref struct).
public virtual bool IsByRefLike(TypeHandle typeHandle);
// True if the type requires 8-byte alignment on platforms that don't 8-byte align by default (FEATURE_64BIT_ALIGNMENT)
public virtual bool RequiresAlign8(TypeHandle typeHandle);
// True if the MethodTable represents a continuation type used by the async continuation feature
Expand Down Expand Up @@ -290,6 +292,10 @@ partial interface IRuntimeTypeSystem : IContract
// Return true if the method is a wrapper stub (unboxing or instantiating).
public virtual bool IsWrapperStub(MethodDescHandle methodDesc);

// Return true if the method is an unboxing stub (a wrapper around a
// value-type instance method that unboxes `this` before forwarding).
public virtual bool IsUnboxingStub(MethodDescHandle methodDesc);

}
```

Expand All @@ -302,6 +308,7 @@ bool IsFieldDescStatic(TargetPointer fieldDescPointer);
bool IsFieldDescRVA(TargetPointer fieldDescPointer);
uint GetFieldDescType(TargetPointer fieldDescPointer);
uint GetFieldDescOffset(TargetPointer fieldDescPointer, FieldDefinition? fieldDef);
TypeHandle GetFieldDescApproxTypeHandle(TargetPointer fieldDescPointer);
TargetPointer GetFieldDescStaticAddress(TargetPointer fieldDescPointer, bool unboxValueTypes = true);
TargetPointer GetFieldDescThreadStaticAddress(TargetPointer fieldDescPointer, TargetPointer thread, bool unboxValueTypes = true);
```
Expand Down Expand Up @@ -330,6 +337,8 @@ internal partial struct RuntimeTypeSystem_1
GenericsMask_SharedInst = 0x00000020, // shared instantiation, e.g. List<__Canon> or List<MyValueType<__Canon>>
GenericsMask_TypicalInstantiation = 0x00000030, // the type instantiated at its formal parameters, e.g. List<T>

IsByRefLike = 0x00001000, // value type that may contain managed pointers (e.g. Span<T>, ReadOnlySpan<T>)

StringArrayValues = GenericsMask_NonGeneric,
}

Expand Down Expand Up @@ -404,6 +413,7 @@ internal partial struct RuntimeTypeSystem_1
public bool IsTrackedReferenceWithFinalizer => GetFlag(WFLAGS_HIGH.IsTrackedReferenceWithFinalizer) != 0;
public bool IsGenericTypeDefinition => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_TypicalInstantiation);
public bool IsSharedByGenericInstantiations => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_SharedInst);
public bool IsByRefLike => TestFlagWithMask(WFLAGS_LOW.IsByRefLike, WFLAGS_LOW.IsByRefLike);
}

[Flags]
Expand Down Expand Up @@ -668,6 +678,8 @@ Contracts used:

public bool ContainsGCPointers(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[TypeHandle.Address].Flags.ContainsGCPointers;

public bool IsByRefLike(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _methodTables[typeHandle.Address].Flags.IsByRefLike;

public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8;

public bool IsCanonicalMethodTable(TypeHandle typeHandle)
Expand Down Expand Up @@ -1872,6 +1884,17 @@ Determining if a method is a wrapper stub (unboxing or instantiating):
}
```

Determining if a method is an unboxing stub. An unboxing stub is a wrapper
around a value-type instance method whose `this` is a boxed object: the
stub unboxes `this` and forwards to the real instance method. The bit is
stored in `MethodDescFlags3` and surfaces as the `IsUnboxingStub` flag on
`MethodDesc`:

```csharp
public bool IsUnboxingStub(MethodDescHandle methodDescHandle)
=> _methodDescs[methodDescHandle.Address].IsUnboxingStub;
```

Extracting a pointer to the `MethodDescVersioningState` data for a given method

```csharp
Expand Down Expand Up @@ -2232,6 +2255,15 @@ TargetPointer GetFieldDescThreadStaticAddress(TargetPointer fieldDescPointer, Ta
// Uses GetGCThreadStaticsBasePointer / GetNonGCThreadStaticsBasePointer.
// The unboxValueTypes parameter behaves the same as in GetFieldDescStaticAddress.
}

TypeHandle GetFieldDescApproxTypeHandle(TargetPointer fieldDescPointer)
{
// Resolve enclosing MT -> Module -> MetadataReader, decode the field's
// signature using the SignatureDecoder contract with a SignatureTypeProvider
// bound to the enclosing class as generic context, and return the resulting
// TypeHandle. Returns TypeHandle.Null if any link in the chain is unavailable
// (e.g. uncached constructed instantiation).
}
```

### Other APIs
Expand Down
1 change: 1 addition & 0 deletions eng/pipelines/runtime-diagnostics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ parameters:
type: object
default:
- windows_x64
- windows_x86
- linux_x64
- windows_arm64
- linux_arm64
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/inc/clrconfigvalues.h
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ CONFIG_STRING_INFO(INTERNAL_PrestubHalt, W("PrestubHalt"), "")
RETAIL_CONFIG_STRING_INFO(EXTERNAL_RestrictedGCStressExe, W("RestrictedGCStressExe"), "")
RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStressFailFast, W("CdacStressFailFast"), 0, "If nonzero, assert on cDAC/runtime GC ref mismatch during cDAC stress verification.")
RETAIL_CONFIG_STRING_INFO(INTERNAL_CdacStressLogFile, W("CdacStressLogFile"), "Log file path for cDAC stress verification results.")
RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStress, W("CdacStress"), 0, "Enable cDAC stress verification. Bit flags: 0x1=alloc points, 0x200=verbose per-ref diagnostics.")
RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStress, W("CdacStress"), 0, "Enable cDAC stress verification.")
CONFIG_DWORD_INFO(INTERNAL_ReturnSourceTypeForTesting, W("ReturnSourceTypeForTesting"), 0, "Allows returning the (internal only) source type of an IL to Native mapping for debugging purposes")
RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RSStressLog, W("RSStressLog"), 0, "Allows turning on logging for RS startup")
CONFIG_DWORD_INFO(INTERNAL_SBDumpOnNewIndex, W("SBDumpOnNewIndex"), 0, "Used for Syncblock debugging. It's been a while since any of those have been used.")
Expand Down
22 changes: 21 additions & 1 deletion src/coreclr/inc/dacprivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,32 @@ enum
DACSTACKPRIV_REQUEST_FRAME_DATA = 0xf0000000
};

#ifdef CDAC_STRESS
// Private requests for the cDAC stress harness.
enum
{
DACSTRESSPRIV_REQUEST_FLUSH_TARGET_STATE = 0xf2000000
DACSTRESSPRIV_REQUEST_FLUSH_TARGET_STATE = 0xf2000000,
DACSTRESSPRIV_REQUEST_COMPUTE_ARG_GCREFMAP = 0xf2000001
};

// In/out request descriptor for DACSTRESSPRIV_REQUEST_COMPUTE_ARG_GCREFMAP.
// outBuffer is unused; the caller-allocated blob destination + size are
// carried by this struct, and the handler writes cbFilled in place.
// S_OK blob fit; cbFilled bytes written to *BlobBuffer.
// HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) cbFilled = required size; *BlobBuffer untouched.
// E_NOTIMPL encoder declined this MD (bucketed as ARG_SKIP).
// E_FAIL encoder threw (bucketed as ARG_ERROR).
// E_INVALIDARG bad inBuffer.
struct DacStressArgGCRefMapRequest
{
CLRDATA_ADDRESS MethodDesc; // [in]
CLRDATA_ADDRESS BlobBuffer; // [in] caller-allocated destination (in-proc pointer)
ULONG32 BlobBufferLen; // [in] capacity at BlobBuffer
ULONG32 cbFilled; // [out] bytes actually written to *BlobBuffer
ULONG32 cbNeeded; // [out] total bytes the blob requires
};
#endif // CDAC_STRESS

enum DacpObjectType { OBJ_STRING=0,OBJ_FREE,OBJ_OBJECT,OBJ_ARRAY,OBJ_OTHER };
struct MSLAYOUT DacpObjectData
{
Expand Down
Loading
Loading