Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b8ad036
[cdac] x86: implement IGCInfoDecoder.EnumerateLiveSlots; unblock GCRo…
Jun 17, 2026
c1ffa8b
docs: move GCInfo.md x86 details to dedicated section
Jun 17, 2026
2e2dd12
[cdac] x86 GetInterruptibleRanges: implement properly + correct doc
Jun 18, 2026
05005ef
[cdac] Rename x86 GCInfo.cs to X86GCInfo.cs to match the class name
Jun 18, 2026
ba00526
[cdac] x86 VarPtr-tracked locals: pass through both interior and pinn…
Jun 18, 2026
de2f3dd
[cdac] x86 GCTransition: store isThis/iptr in transition ctors
Jun 18, 2026
66b12e5
[cdac] x86 GcArgTable: fix curOffs scope in GetTransitionsEbpFrame
Jun 18, 2026
7c6ac7e
[cdac] x86 EnumerateLiveSlots: stress-test correctness fixes
Jun 18, 2026
97a1b79
[cdac] x86 EnumerateLiveSlots: handle ParentOfFuncletStackFrame and A…
Jun 18, 2026
7c52e7d
[cdac] x86 GCInfo: trim native code references and document Enumerate…
Jun 18, 2026
2475719
[cdac] x86 GCInfo: keep file name as GCInfo.cs
Jun 18, 2026
239adc5
[cdac] GCInfo.md: x86 EnumerateLiveSlots and GetInterruptibleRanges a…
Jun 18, 2026
4a7b2a5
[cdac] GCInfo.md: keep x86 specifics confined to the x86 specifics se…
Jun 18, 2026
e6e53c5
[cdac] GCInfo.md: drop x86 Supported APIs table
Jun 18, 2026
23dc850
[cdac] x86 GcArgTable: drop redundant curOffs scope comment
Jun 18, 2026
82baa5a
[cdac] DumpTests.targets: drop local-dev DebuggeeFilter property
Jun 18, 2026
4719a2e
[cdac] x86 GCInfo: address PR review feedback
Jun 18, 2026
396926e
[cdac] x86 GcArgTable: emit negative stack-depth delta at partial-int…
Jun 18, 2026
ae2c0c9
[cdac] x86 ApplyPointerTransition: respect IsPtr=false on non-pointer…
Jun 18, 2026
3b0f418
[cdac] x86 EnumerateLiveSlots: bias ESP-frame untracked/VarPtr slots …
Jun 19, 2026
412a9a5
[cdac] x86 ScanDynamicHelperFrame: swap ObjectArg / ObjectArg2 offsets
Jun 19, 2026
0a1ad25
[cdac] x86 GCInfo: address PR review feedback round 2
Jun 19, 2026
1bb9813
[cdac] x86 stress fixes uncovered by Helix CI
Jun 20, 2026
342c2f1
[cdac] x86 GCRefMap: use OffsetOfArgs for stack-arg positions
Jun 20, 2026
aecedf8
[cdac] Document known x86 stress flake (#129545/#129546 GC hole)
Jun 20, 2026
3cc1c58
[cdac] x86 stress flake doc: correct attribution (not #129545/#129546)
Jun 20, 2026
80f5724
[cdac] x86 GCArgTable: case 0xFB code-delta is += not =
Jun 20, 2026
a9eb6e7
[cdac] x86 GCArgTable: don't emit spurious call for 'this-pointer' tag
Jun 20, 2026
b81fca4
[cdac] GCInfo.md: refresh x86 specifics for recent decoder fixes
Jun 21, 2026
897647f
[cdac] Add PInvokeCalliFrame data class for x86 VASigCookie path
Jun 27, 2026
2c648d9
[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
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.
5 changes: 5 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,11 @@ CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame))
CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, Indirection, cdac_data<ExternalMethodFrame>::Indirection)
CDAC_TYPE_END(ExternalMethodFrame)

CDAC_TYPE_BEGIN(PInvokeCalliFrame)
CDAC_TYPE_SIZE(sizeof(PInvokeCalliFrame))
CDAC_TYPE_FIELD(PInvokeCalliFrame, T_POINTER, VASigCookiePtr, cdac_data<PInvokeCalliFrame>::VASigCookiePtr)
CDAC_TYPE_END(PInvokeCalliFrame)

CDAC_TYPE_BEGIN(DynamicHelperFrame)
CDAC_TYPE_SIZE(sizeof(DynamicHelperFrame))
CDAC_TYPE_FIELD(DynamicHelperFrame, T_INT32, DynamicHelperFrameFlags, cdac_data<DynamicHelperFrame>::DynamicHelperFrameFlags)
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/vm/frames.h
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,14 @@ class PInvokeCalliFrame : public FramedMethodFrame
trace->InitForUnmanaged(GetPInvokeCalliTarget());
return TRUE;
}

friend struct ::cdac_data<PInvokeCalliFrame>;
};

template <>
struct cdac_data<PInvokeCalliFrame>
{
static constexpr size_t VASigCookiePtr = offsetof(PInvokeCalliFrame, m_pVASigCookie);
};

// Some context-related forwards.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ private void GetTransitionsFullyInterruptible(ref TargetPointer offset)
/// </summary>
private void GetTransitionsEbpFrame(ref TargetPointer offset)
{
uint curOffs = 0;
while (true)
{
uint argMask = 0, byrefArgMask = 0;
Expand All @@ -207,7 +208,6 @@ private void GetTransitionsEbpFrame(ref TargetPointer offset)
uint argTabSize;

uint val, nxt;
uint curOffs = 0;

// Get the next byte and check for a 'special' entry
uint encType = _target.Read<byte>(offset++);
Expand All @@ -231,19 +231,14 @@ private void GetTransitionsEbpFrame(ref TargetPointer offset)
}
else
{
RegMask reg;
if ((val & 0x10) != 0)
reg = RegMask.EDI;
else if ((val & 0x20) != 0)
reg = RegMask.ESI;
else if ((val & 0x40) != 0)
reg = RegMask.EBX;
else
throw new BadImageFormatException("Invalid register");
transition = new GcTransitionCall((int)curOffs);
transition.CallRegisters.Add(new GcTransitionCall.CallRegister(reg, false));
AddNewTransition(transition);

// "This pointer liveness encoding" (val & 0x80 == 0 && val & 0x0F == 0):
// metadata for which callee-saved register holds the 'this' pointer
// at the next call site. Native (gc_unwind_x86.inl ~line 970) does NOT
// record a call entry here -- it only sets thisPtrReg. Adding a spurious
// GcTransitionCall at the current curOffs would overwrite the real
// call site's CallRegisters during EnumerateLiveSlots (since the
// partial-EBP decoder may emit the this-ptr tag at the same curOffs
// as a real call site), so we just consume the byte and continue.
continue;
}
}
Expand Down Expand Up @@ -296,7 +291,12 @@ private void GetTransitionsEbpFrame(ref TargetPointer offset)
val = _target.Read<byte>(offset++);
regMask = val & 0x7;
byrefRegMask = val >> 4;
curOffs = _target.Read<uint>(offset);
// Code delta is 32-bit and is added to curOffs (mirrors `scanOffs +=` in
// native gc_unwind_x86.inl scanArgRegTable case 0xFB). The pre-PR cDAC port
// assigned `curOffs = ...` here, which silently truncated method-relative
// offsets for the first 0xFB call site and corrupted all subsequent calls
// in long methods (e.g. EventSource cctors).
curOffs += _target.Read<uint>(offset);
offset += 4;
argCnt = _target.Read<uint>(offset);
offset += 4;
Expand Down Expand Up @@ -345,7 +345,7 @@ argMask ... bitmask of pushed pointer arguments
/// <summary>
/// based on <a href="https://github.com/dotnet/runtime/blob/main/src/coreclr/gcdump/i386/gcdumpx86.cpp">GCDump::DumpGCTable</a>
/// </summary>
private void SaveCallTransition(ref TargetPointer offset, uint val, uint curOffs, uint callRegMask, bool callPndTab, uint callPndTabCnt, uint callPndMask, uint lastSkip, ref uint imask)
private void SaveCallTransition(ref TargetPointer offset, uint curOffs, uint callRegMask, bool callPndTab, uint callPndTabCnt, uint callPndMask, ref uint imask)
{
uint iregMask, iargMask;
iregMask = imask & 0xF;
Expand All @@ -359,11 +359,6 @@ private void SaveCallTransition(ref TargetPointer offset, uint val, uint curOffs
for (int i = 0; i < callPndTabCnt; i++)
{
uint pndOffs = _target.GCDecodeUnsigned(ref offset);

uint stkOffs = val & ~byref_OFFSET_FLAG;
uint lowBit = val & byref_OFFSET_FLAG;
Console.WriteLine($"stkOffs: {stkOffs}, lowBit: {lowBit}");

transition.PtrArgs.Add(new GcTransitionCall.PtrArg(pndOffs, 0));
}
}
Expand All @@ -375,15 +370,14 @@ private void SaveCallTransition(ref TargetPointer offset, uint val, uint curOffs
transition.IArgs = iargMask;
}

Console.WriteLine($"lastSkip: {lastSkip}");
imask /* = lastSkip */ = 0;
}

private void GetTransitionsNoEbp(ref TargetPointer offset)
{
uint curOffs = 0;
uint lastSkip = 0;
uint imask = 0;
uint lastSkip;

for (; ; )
{
Expand Down Expand Up @@ -418,7 +412,6 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
//
skip = _target.GCDecodeUnsigned(ref offset);
curOffs += skip;
lastSkip = skip;
}
else
{
Expand All @@ -431,18 +424,16 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
{
AddNewTransition(new GcTransitionRegister((int)curOffs, RegMask.ESP, Action.POP, false, false, (int)popSize));
}
else
lastSkip = skip;
}
}
}
else
{
uint callArgCnt = 0;
uint callArgCnt;
uint callRegMask;
bool callPndTab = false;
uint callPndMask = 0;
uint callPndTabCnt = 0, callPndTabSize = 0;
uint callPndTabCnt = 0;

switch ((val & 0x70) >> 4)
{
Expand All @@ -452,8 +443,8 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
//
CallPattern.DecodeCallPattern((val & 0x7f), out callArgCnt, out callRegMask, out callPndMask, out lastSkip);
curOffs += lastSkip;
SaveCallTransition(ref offset, val, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, lastSkip, ref imask);
AddNewTransition(new StackDepthTransition((int)curOffs, (int)callArgCnt));
SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask);
AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
break;

case 5:
Expand All @@ -467,8 +458,8 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
callArgCnt = (val >> 3) & 0x7;
lastSkip = CallPattern.CallCommonDelta[(int)(val >> 6)];
curOffs += lastSkip;
SaveCallTransition(ref offset, val, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, lastSkip, ref imask);
AddNewTransition(new StackDepthTransition((int)curOffs, (int)callArgCnt));
SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask);
AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
break;
case 6:
//
Expand All @@ -478,8 +469,8 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
callRegMask = val & 0xf; // EBP,EBX,ESI,EDI
callArgCnt = _target.GCDecodeUnsigned(ref offset);
callPndMask = _target.GCDecodeUnsigned(ref offset);
SaveCallTransition(ref offset, val, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, lastSkip, ref imask);
AddNewTransition(new StackDepthTransition((int)curOffs, (int)callArgCnt));
SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask);
AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
break;
case 7:
switch (val & 0x0C)
Expand All @@ -505,11 +496,11 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
offset += 4;
callPndTabCnt = _target.Read<uint>(offset);
offset += 4;
callPndTabSize = _target.Read<uint>(offset);
// Skip callPndTabSize - present in encoding but unused by the decoder.
offset += 4;
callPndTab = true;
SaveCallTransition(ref offset, val, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, lastSkip, ref imask);
AddNewTransition(new StackDepthTransition((int)curOffs, (int)callArgCnt));
SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask);
AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
break;
case 0x0C:
return;
Expand All @@ -518,8 +509,6 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
}
break;
}
Console.WriteLine($"CallArgCount: {callArgCnt}");
Console.WriteLine($"CallPndTabCnt: {callPndTabSize}");
}
}
}
Expand Down
Loading