Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1692fbe
[cdac] x86: implement IGCInfoDecoder.EnumerateLiveSlots; unblock GCRo…
Jun 17, 2026
687b4f6
docs: move GCInfo.md x86 details to dedicated section
Jun 17, 2026
4fdd75d
[cdac] x86 GetInterruptibleRanges: implement properly + correct doc
Jun 18, 2026
cb82b21
[cdac] Rename x86 GCInfo.cs to X86GCInfo.cs to match the class name
Jun 18, 2026
e93d40b
[cdac] x86 VarPtr-tracked locals: pass through both interior and pinn…
Jun 18, 2026
ed26cee
[cdac] x86 GCTransition: store isThis/iptr in transition ctors
Jun 18, 2026
5ec697e
[cdac] x86 GcArgTable: fix curOffs scope in GetTransitionsEbpFrame
Jun 18, 2026
53be78a
[cdac] x86 EnumerateLiveSlots: stress-test correctness fixes
Jun 18, 2026
a899035
[cdac] x86 EnumerateLiveSlots: handle ParentOfFuncletStackFrame and A…
Jun 18, 2026
be6863e
[cdac] x86 GCInfo: trim native code references and document Enumerate…
Jun 18, 2026
a11403b
[cdac] x86 GCInfo: keep file name as GCInfo.cs
Jun 18, 2026
b1efe0b
[cdac] GCInfo.md: x86 EnumerateLiveSlots and GetInterruptibleRanges a…
Jun 18, 2026
90fc47c
[cdac] GCInfo.md: keep x86 specifics confined to the x86 specifics se…
Jun 18, 2026
03af067
[cdac] GCInfo.md: drop x86 Supported APIs table
Jun 18, 2026
f895e3c
[cdac] x86 GcArgTable: drop redundant curOffs scope comment
Jun 18, 2026
9b4a17b
[cdac] DumpTests.targets: drop local-dev DebuggeeFilter property
Jun 18, 2026
eb136dc
[cdac] x86 GCInfo: address PR review feedback
Jun 18, 2026
49fd924
[cdac] x86 GcArgTable: emit negative stack-depth delta at partial-int…
Jun 18, 2026
9da24a4
[cdac] x86 ApplyPointerTransition: respect IsPtr=false on non-pointer…
Jun 18, 2026
a434af6
Merge branch 'main' of https://github.com/dotnet/runtime into cdac-x8…
Jun 19, 2026
1e9044e
[cdac] runtime-diagnostics pipeline: add windows_x86 to cDAC stress t…
Jun 19, 2026
4912495
[cdac] x86 EnumerateLiveSlots: bias ESP-frame untracked/VarPtr slots …
Jun 19, 2026
67213d8
[cdac] x86 ScanDynamicHelperFrame: swap ObjectArg / ObjectArg2 offsets
Jun 19, 2026
1326374
[cdac] x86 GCInfo: address PR review feedback round 2
Jun 19, 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
40 changes: 37 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,40 @@ 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.
- `IsExecutionAborted` and the method is not fully interruptible.
- Filter funclets (`SuppressUntrackedSlots`) skip the untracked table to avoid double-reporting with the parent.

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 is naturally signed and resolves to `EBP + stkOffs`.
- **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`). 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 and emitted as positive SP-relative offsets at emit time once final depth is known (`addr = ESP_call + (finalDepth - 1 - pushIndex) * 4`, mirroring `pPendingArgFirst - i * sizeof(DWORD)`). For partially-interruptible call sites, taken directly from the matching `GcTransitionCall`: explicit per-pointer offsets in the huge (0xFB) encoding, or a uint32 bitmap (`ArgMask` / `IArgs`) walked low-to-high with `addr = ESP + i * sizeof(DWORD)` for the tiny / small / medium / large encodings.

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`).

### 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 — only used in the partial-interruptible ESP-frame walker, not exercised by current x86 codegen because all post-funclet x86 methods are EBP frames.
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
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 Down Expand Up @@ -453,7 +453,7 @@ 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));
AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
break;

case 5:
Expand All @@ -468,7 +468,7 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
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));
AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
break;
case 6:
//
Expand All @@ -479,7 +479,7 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
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));
AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
break;
case 7:
switch (val & 0x0C)
Expand Down Expand Up @@ -509,7 +509,7 @@ private void GetTransitionsNoEbp(ref TargetPointer offset)
offset += 4;
callPndTab = true;
SaveCallTransition(ref offset, val, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, lastSkip, ref imask);
AddNewTransition(new StackDepthTransition((int)curOffs, (int)callArgCnt));
AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
break;
case 0x0C:
return;
Expand Down
Loading