From 1692fbeeefa23fb0289e0222b789e2caba27a971 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 19:00:12 -0400 Subject: [PATCH 1/4] [cdac] x86: implement IGCInfoDecoder.EnumerateLiveSlots; unblock GCRoots stackref tests Builds on the partial x86 IGCInfo support added in #129456 by porting the remaining decoder pieces required for GC-root scanning on x86, so that `IStackWalk.WalkStackReferences` returns live frame slots on x86 cDAC. The x86 GC info uses the legacy bit-packed `InfoHdr` byte-stream encoding (`src/coreclr/vm/gc_unwind_x86.inl`, `src/coreclr/inc/gcdecoder.cpp`) instead of the modern `GcInfoDecoder` shared by other architectures, so the implementation lives entirely on the existing `X86GCInfo` decoder under `Contracts/GCInfo/X86/`. Changes ------- * `X86GCInfo`: add `UntrackedSlots` lazy property + `DecodeUntrackedSlots()` -- delta-decoded signed varints with the double-align-frame rebase from `gc_unwind_x86.inl:3467`. * `X86GCInfo`: add `VarPtrLifetimes` lazy property + `DecodeVarPtrLifetimes()` -- triplets of (varOffs, begOffs delta, endOffs delta) for EBP-frame tracked locals. * Two new public record types `UntrackedSlot` and `VarPtrLifetime` capture the decoded entries. * `IsCodeOffsetInProlog` / `IsCodeOffsetInEpilog` helpers (offset-parameterised, so EnumerateLiveSlots can answer for any instruction offset without re-constructing X86GCInfo). * `RegMaskToRegisterNumber` helper maps the single-bit `RegMask` flags-enum values to the x86 ModRM register numbers used by `X86Context.TryReadRegister` and `LiveSlot.RegisterNumber`. * Implement `IGCInfoDecoder.EnumerateLiveSlots(uint offset, options)`: early-return empty in prolog/epilog (or aborted+non-interruptible), emit untracked locals (suppressed for filter funclets), emit VarPtr lifetimes covering `offset`, walk `Transitions` up to `offset` accumulating live registers + pushed pointer args, and emit a partially-interruptible `GcTransitionCall` exactly at `offset`. * Flip `IGCInfoDecoder.GetSizeOfStackParameterArea` from `NotSupportedException` to `return 0` for x86 -- x86 has no separate outgoing-argument scratch area; per-offset transitions report pushed args directly, so the GcScanner scratch-area filter is a no-op (correct). * Remove the `[SkipOnArch("x86", "GCInfo decoder does not support x86")]` markers on `GCRoots_WalkStackReferences_FindsRefs` and `GCRoots_RefsPointToValidObjects`. * `DumpTests.targets`: add optional `DebuggeeFilter=` to restrict `GenerateAllDumps` to a single debuggee. Useful for iterative local x86 work where some other debuggee's publish may fail. * `docs/design/datacontracts/GCInfo.md`: enumerate which `IGCInfoDecoder` APIs are wired up on x86. Out of scope (deferred) ----------------------- * `GetInterruptibleRanges` for x86 -- the only consumer is the catch-handler PC override in `StackWalk_1`; no x86-relevant scenarios today. * "this"-pointer special-case reporting for synchronized methods (VarPtr 0x2 bit currently masked out). * IPtrMask interior-pointer bitmaps for pushed args (uses the simpler per-push `Iptr` flag). * Funclet handling beyond the existing `IsParentOfFuncletStackFrame` caller-side early-skip. * Finer `IsActiveFrame` register filter precision. Validation ---------- * All 2525 cDAC unit tests pass. * The two unblocked `GCRoots_*` tests pass against a freshly generated x86 GCRoots dump. * Broader `DumpTests` x86 sweep: 34 pass / 46 fail / 830 skip -- net +2 vs. before this change (the two GCRoots tests), zero regressions. The 46 pre-existing failures are all unrelated to GCInfo (`ThreadDumpTests` / `ComWrappersDumpTests` / `RuntimeInfoDumpTests` / `WorkstationGCDumpTests` and similar). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GCInfo.md | 2 +- .../Contracts/GCInfo/X86/GCInfo.cs | 449 +++++++++++++++++- .../cdac/tests/DumpTests/DumpTests.targets | 7 +- .../DumpTests/StackReferenceDumpTests.cs | 2 - 4 files changed, 450 insertions(+), 10 deletions(-) diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index 70964f409a93d1..871356c2ab9756 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -2,7 +2,7 @@ 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 partially 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: x86 currently implements `GetCodeLength`, `GetStackBaseRegister`, `GetSizeOfStackParameterArea`, `GetCalleePoppedArgumentsSize`, and `EnumerateLiveSlots` (sufficient for SOS code-size lookups and for `WalkStackReferences` GC-root scanning). `GetInterruptibleRanges` is not yet implemented on x86 -- x86 does not encode explicit interruptible ranges; per-offset transitions are used instead, and the only consumer (catch-handler PC override in `StackWalk_1`) has no x86-relevant scenarios today. ## APIs of contract diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs index 9a8c44f7234f38..93e23ac9e47484 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs @@ -69,6 +69,20 @@ public record X86GCInfo : IGCInfoDecoder public uint PushedArgSize => _pushedArgSize.Value; private readonly Lazy _pushedArgSize; + /// + /// The untracked frame variable table, always-live GC frame slots. + /// Decoded lazily on first access. + /// + public ImmutableArray UntrackedSlots => _untrackedSlots.Value; + private readonly Lazy> _untrackedSlots; + + /// + /// The frame variable lifetime (VarPtr) table, per-offset-range tracked GC variables. + /// Decoded lazily on first access. Empty for non-EBP frames (only EBP frames track variables this way). + /// + public ImmutableArray VarPtrLifetimes => _varPtrLifetimes.Value; + private readonly Lazy> _varPtrLifetimes; + public X86GCInfo(Target target, TargetPointer gcInfoAddress, uint gcInfoVersion, uint relativeOffset = 0) { if (gcInfoVersion < MINIMUM_SUPPORTED_GCINFO_VERSION) @@ -151,6 +165,13 @@ public X86GCInfo(Target target, TargetPointer gcInfoAddress, uint gcInfoVersion, // Lazily calculate the pushed argument size. This forces the transitions to be decoded. _pushedArgSize = new(CalculatePushedArgSize); + + // Lazily decode the untracked-locals and VarPtr tables. These live between the + // NoGCRegion table and the argument table in the bitstream; see DecodeTransitions + // for the layout. Like the transitions, the underlying GC info bytes are typically + // only present in full (or selectively included) memory dumps. + _untrackedSlots = new(DecodeUntrackedSlots); + _varPtrLifetimes = new(DecodeVarPtrLifetimes); } private ImmutableDictionary> DecodeTransitions() @@ -243,6 +264,135 @@ private uint CalculatePushedArgSize() return (uint)(depth * _target.PointerSize); } + private ImmutableArray DecodeUntrackedSlots() + { + if (Header.UntrackedCount == 0) + return ImmutableArray.Empty; + + // The untracked-locals table follows the NoGCRegions table in the bitstream + // (see DecodeTransitions for the section layout). + TargetPointer offset = _gcInfoAddress + _infoHdrSize; + for (int i = 0; i < Header.NoGCRegionCount; i++) + { + _target.GCDecodeUnsigned(ref offset); + _target.GCDecodeUnsigned(ref offset); + } + + // Each entry is a signed varint, delta-encoded against the previous entry. + // Low 2 bits hold flags (byref=0x1, pinned=0x2); the remainder is the frame-relative + // stack offset. On EBP-frames the offset is EBP-relative; on ESP-frames it is + // ESP-relative. Double-aligned frames use a hybrid encoding: offsets that lie + // above the frame are EBP-relative even when the rest of the frame is ESP-based. + // Reference: gc_unwind_x86.inl:3467 (EnumGcRefsX86 untracked path) and + // ILCompiler.Reflection.ReadyToRun/x86/GcSlotTable.cs:127 (DecodeUntracked). + uint calleeSavedRegsCount = 0; + if (Header.DoubleAlign) + { + if (Header.EdiSaved) calleeSavedRegsCount++; + if (Header.EsiSaved) calleeSavedRegsCount++; + if (Header.EbxSaved) calleeSavedRegsCount++; + } + + ImmutableArray.Builder builder = ImmutableArray.CreateBuilder((int)Header.UntrackedCount); + int lastStkOffs = 0; + for (uint i = 0; i < Header.UntrackedCount; i++) + { + int delta = _target.GCDecodeSigned(ref offset); + int stkOffs = lastStkOffs - delta; + lastStkOffs = stkOffs; + + uint lowBits = OFFSET_MASK & (uint)stkOffs; + stkOffs = (int)((uint)stkOffs & ~OFFSET_MASK); + + bool isEbpRelative = Header.EbpFrame; + if (Header.DoubleAlign && + (uint)stkOffs >= _target.PointerSize * (Header.FrameSize + calleeSavedRegsCount)) + { + // Double-aligned frame: offsets above the frame proper are EBP-relative. + isEbpRelative = true; + stkOffs -= (int)(_target.PointerSize * (Header.FrameSize + calleeSavedRegsCount)); + } + + builder.Add(new UntrackedSlot(stkOffs, isEbpRelative, lowBits)); + } + + return builder.MoveToImmutable(); + } + + private ImmutableArray DecodeVarPtrLifetimes() + { + if (Header.VarPtrTableSize == 0) + return ImmutableArray.Empty; + + // The VarPtr table follows the untracked-locals table in the bitstream. + TargetPointer offset = _gcInfoAddress + _infoHdrSize; + for (int i = 0; i < Header.NoGCRegionCount; i++) + { + _target.GCDecodeUnsigned(ref offset); + _target.GCDecodeUnsigned(ref offset); + } + for (int i = 0; i < Header.UntrackedCount; i++) + { + _target.GCDecodeSigned(ref offset); + } + + // Each entry is three unsigned varints: (varOffs, begOffs, endOffs). + // varOffs is absolute; begOffs is delta-from-previous-begOffs; endOffs is delta-from-begOffs. + // Low 2 bits of varOffs hold flags (byref=0x1, this=0x2 -- NOT pinned for tracked locals). + // Reference: gc_unwind_x86.inl varPtrTable processing and + // ILCompiler.Reflection.ReadyToRun/x86/GcSlotTable.cs:168 (DecodeFrameVariableLifetimeTable). + ImmutableArray.Builder builder = ImmutableArray.CreateBuilder((int)Header.VarPtrTableSize); + uint curOffs = 0; + for (uint i = 0; i < Header.VarPtrTableSize; i++) + { + uint varOffsRaw = _target.GCDecodeUnsigned(ref offset); + uint begOffs = _target.GCDecodeUDelta(ref offset, curOffs); + uint endOffs = _target.GCDecodeUDelta(ref offset, begOffs); + + uint lowBits = varOffsRaw & OFFSET_MASK; + int stkOffs = (int)(varOffsRaw & ~OFFSET_MASK); + + curOffs = begOffs; + + builder.Add(new VarPtrLifetime(begOffs, endOffs, stkOffs, lowBits)); + } + + return builder.MoveToImmutable(); + } + + private const uint OFFSET_MASK = 0x3; + + /// + /// Returns true if falls within the method's prolog. + /// + private bool IsCodeOffsetInProlog(uint codeOffset) + => codeOffset < Header.PrologSize; + + /// + /// Returns true if falls within any epilog. + /// + private bool IsCodeOffsetInEpilog(uint codeOffset) + { + foreach (uint epilogStart in Header.Epilogs) + { + if (codeOffset > epilogStart && codeOffset < epilogStart + Header.EpilogSize) + return true; + } + return false; + } + + /// + /// Converts a single-bit value to the platform-agnostic + /// register number used by X86Context.TryReadRegister and by . + /// EAX=0, ECX=1, EDX=2, EBX=3, ESP=4, EBP=5, ESI=6, EDI=7 -- matches the x86 ModRM encoding. + /// + private static uint RegMaskToRegisterNumber(RegMask reg) + { + // RegMask is a flags enum where each register sits on its own bit + // (EAX=0x1, ECX=0x2, ..., EDI=0x80). Log2 yields the register number. + return (uint)System.Numerics.BitOperations.Log2((uint)reg); + } + uint IGCInfoDecoder.GetCodeLength() => MethodSize; uint IGCInfoDecoder.GetStackBaseRegister() @@ -255,10 +405,14 @@ uint IGCInfoDecoder.GetStackBaseRegister() } 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)."); + { + // x86 GC info does not encode a separate outgoing-argument scratch area; the + // per-offset transitions report pushed argument pointers directly at each offset. + // Returning 0 disables the GcScanner's scratch-area filter on x86, which is the + // correct behaviour: the live state at a given offset (call site or fully-interruptible + // point) already excludes any args that have been popped by the time we resume there. + return 0; + } uint IGCInfoDecoder.GetCalleePoppedArgumentsSize() { @@ -272,5 +426,290 @@ IReadOnlyList 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 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."); + { + // x86 stack base encoding for LiveSlot.SpBase: 1 = SP-relative, 2 = FRAMEREG (EBP/EBP-double-aligned) relative. + // (See IGCInfo.cs LiveSlot docs and GcScanner.EnumGcRefsForManagedFrame for how these get resolved.) + const uint SP_REL = 1; + const uint FRAMEREG_REL = 2; + + // In the prolog (before all locals are initialised) and in the epilog (after they've + // been torn down) the GC info doesn't accurately describe live slots. The runtime + // never suspends a thread inside the prolog/epilog under normal circumstances; the only + // path that reaches here is ExecutionAborted (thread abort or stack overflow). In that + // case we still drop reporting -- this matches native EnumGcRefsX86 in gc_unwind_x86.inl:3091 + // which returns true without enumerating when we're in the prolog/epilog. + if (IsCodeOffsetInProlog(instructionOffset) || IsCodeOffsetInEpilog(instructionOffset)) + return Array.Empty(); + + // For non-interruptible methods, an ExecutionAborted offset that isn't at a recorded + // safe point yields no reliable GC info; skip reporting as the native walker does + // (gc_unwind_x86.inl:3093). + if (options.IsExecutionAborted && !Header.Interruptible) + return Array.Empty(); + + List result = []; + + // (1) Untracked frame locals -- always live for the entire method body. + // Filter funclets suppress untracked reporting because the parent frame already + // reports them (matches native EnumGcRefsX86 isFilterFunclet path). + if (!options.SuppressUntrackedSlots) + { + foreach (UntrackedSlot us in UntrackedSlots) + { + // LowBits encoding matches LiveSlot.GcFlags exactly: 0x1 = interior, 0x2 = pinned. + uint spBase = us.IsEbpRelative ? FRAMEREG_REL : SP_REL; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: us.StackOffset, SpBase: spBase, GcFlags: us.LowBits)); + } + } + + // (2) VarPtr-tracked frame locals -- live when instructionOffset is within [Begin, End). + // Only EBP-frames produce entries here; the table is empty for ESP-frames. + { + uint spBase = Header.EbpFrame ? FRAMEREG_REL : SP_REL; + foreach (VarPtrLifetime vp in VarPtrLifetimes) + { + if (instructionOffset < vp.BeginOffset || instructionOffset >= vp.EndOffset) + continue; + + // VarPtr LowBits encoding differs from untracked: 0x1 = interior, 0x2 = "this" + // pointer (NOT pinned). The "this" pointer flag is consumed by special-case + // synchronized-method reporting in native code that this MVP cDAC decoder does + // not replicate. Map only the interior bit into LiveSlot.GcFlags. + uint gcFlags = vp.LowBits & 0x1u; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: vp.StackOffset, SpBase: spBase, GcFlags: gcFlags)); + } + } + + // (3) Live registers and pushed pointer args from the transition stream. + EnumerateTransitionLiveSlots(instructionOffset, options, result, SP_REL); + + return result; + } + + /// + /// Walks up to and including , + /// accumulating live register state and currently-pushed pointer arguments, and emits a + /// per live register / pushed pointer. + /// + /// + /// For fully-interruptible methods, every transition strictly before + /// contributes to the current state. For partially- + /// interruptible methods, the JIT only emits transitions at call sites (and at LIVE/DEAD + /// markers); the live state at the exact is whatever + /// the most-recent call-site transition described. + /// + private void EnumerateTransitionLiveSlots( + uint instructionOffset, + GcSlotEnumerationOptions options, + List result, + uint spRelBase) + { + // Set of registers currently holding live GC refs at the walked offset. + RegMask liveRegs = RegMask.NONE; + RegMask liveIptrRegs = RegMask.NONE; + + // Pushed pointer args on the stack, keyed by ESP-relative byte offset (positive). + // Value: GcFlags (0x1 = interior). Use a sorted dictionary for deterministic output. + SortedDictionary pushedPtrs = new(); + + // Stack depth in pointer-size units, used by GcTransitionPointer offsets which are + // expressed as "argOffs from top of pushed args". Mirrors the depth bookkeeping in + // CalculatePushedArgSize. + int depthSlots = 0; + + // For partially-interruptible methods, only the call-site state matters -- we collect + // the most-recent call site at-or-before instructionOffset and emit its registers/args. + GcTransitionCall? activeCallSite = null; + + foreach (int offset in Transitions.Keys.OrderBy(o => o)) + { + // Stop AFTER processing transitions at instructionOffset for fully-interruptible + // code. For partially-interruptible code, GcTransitionCall transitions can also + // contain register/arg state describing the call site. + if (offset > instructionOffset) + break; + + foreach (BaseGcTransition transition in Transitions[offset]) + { + switch (transition) + { + case GcTransitionRegister regT: + ApplyRegisterTransition(regT, ref liveRegs, ref liveIptrRegs, ref depthSlots, pushedPtrs); + break; + case GcTransitionPointer ptrT: + ApplyPointerTransition(ptrT, ref depthSlots, pushedPtrs); + break; + case StackDepthTransition stackT: + depthSlots += stackT.StackDepthChange; + if (depthSlots < 0) depthSlots = 0; + break; + case GcTransitionCall callT when offset == (int)instructionOffset: + // Partially-interruptible: this is the only kind of transition that + // directly describes the call-site live state. For fully-interruptible + // code, GcTransitionCall is informational only -- the surrounding + // PUSH/POP/LIVE/DEAD transitions already maintain the state. + activeCallSite = callT; + break; + case IPtrMask: + case CalleeSavedRegister: + case GcTransitionCall: + // CalleeSavedRegister is purely informational for the stack walker. + // IPtrMask is reserved for future interior-pointer-bitmap support; + // the current MVP decoder does not propagate it onto pushed slots. + // GcTransitionCall at offset != instructionOffset is also ignored. + break; + default: + throw new InvalidOperationException($"Unsupported x86 GC transition: {transition.GetType().Name}"); + } + } + } + + // Emit live registers from accumulated state. + // The IsActiveFrame option controls whether scratch (callee-trashed) registers are + // reported. On non-leaf frames, the live registers are by definition scratch from the + // caller's perspective, so they should not be reported. Native EnumGcRefsX86 handles + // this via the willContinueExecution / ExecutionAborted flags interacting with the + // CHK_AND_REPORT_REG macro family. + if (options.IsActiveFrame || activeCallSite is not null) + { + // Iterate single-bit register values via RM_ALL. + foreach (RegMask r in EnumerateSingleRegs()) + { + if ((liveRegs & r) == 0) continue; + uint gcFlags = (liveIptrRegs & r) != 0 ? 0x1u : 0u; + result.Add(new LiveSlot(IsRegister: true, RegisterNumber: RegMaskToRegisterNumber(r), SpOffset: 0, SpBase: 0, GcFlags: gcFlags)); + } + } + + // Emit pushed pointer args as SP-relative stack slots. + foreach (KeyValuePair pushed in pushedPtrs) + { + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: pushed.Key, SpBase: spRelBase, GcFlags: pushed.Value)); + } + + // Partially-interruptible call-site: emit its register set and pointer args directly. + if (activeCallSite is not null) + { + foreach (GcTransitionCall.CallRegister cr in activeCallSite.CallRegisters) + { + uint gcFlags = cr.IsByRef ? 0x1u : 0u; + result.Add(new LiveSlot(IsRegister: true, RegisterNumber: RegMaskToRegisterNumber(cr.Register), SpOffset: 0, SpBase: 0, GcFlags: gcFlags)); + } + foreach (GcTransitionCall.PtrArg pa in activeCallSite.PtrArgs) + { + uint gcFlags = pa.LowBit != 0 ? 0x1u : 0u; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: (int)pa.StackOffset, SpBase: spRelBase, GcFlags: gcFlags)); + } + } + } + + private static void ApplyRegisterTransition( + GcTransitionRegister regT, + ref RegMask liveRegs, + ref RegMask liveIptrRegs, + ref int depthSlots, + SortedDictionary pushedPtrs) + { + switch (regT.IsLive) + { + case Action.LIVE: + liveRegs |= regT.Register; + if (regT.Iptr) liveIptrRegs |= regT.Register; + else liveIptrRegs &= ~regT.Register; + break; + case Action.DEAD: + liveRegs &= ~regT.Register; + liveIptrRegs &= ~regT.Register; + break; + case Action.PUSH: + // The register's value is pushed onto the stack as a callee argument. + // Each push advances depth; record the slot at the new top-of-stack. + for (int i = 0; i < regT.PushCountOrPopSize; i++) + { + depthSlots++; + int sp = -depthSlots * 4; // x86 grows down; offset relative to call-site SP + pushedPtrs[sp] = regT.Iptr ? 0x1u : 0u; + } + break; + case Action.POP: + // Pop unrolls the most recently pushed slots. + for (int i = 0; i < regT.PushCountOrPopSize && depthSlots > 0; i++) + { + int sp = -depthSlots * 4; + pushedPtrs.Remove(sp); + depthSlots--; + } + break; + case Action.KILL: + // Used by EBP-frame partial-interrupt encoding to invalidate pushed args + // (kill all currently-tracked pushed pointers up to argOffset). + pushedPtrs.Clear(); + depthSlots = 0; + break; + } + } + + private static void ApplyPointerTransition( + GcTransitionPointer ptrT, + ref int depthSlots, + SortedDictionary pushedPtrs) + { + switch (ptrT.Act) + { + case Action.PUSH: + depthSlots++; + int spPush = -depthSlots * 4; + pushedPtrs[spPush] = ptrT.Iptr ? 0x1u : 0u; + break; + case Action.POP: + // ArgOffset slots are popped from the top of the pushed-args stack. + for (uint i = 0; i < ptrT.ArgOffset && depthSlots > 0; i++) + { + int sp = -depthSlots * 4; + pushedPtrs.Remove(sp); + depthSlots--; + } + break; + case Action.KILL: + pushedPtrs.Clear(); + depthSlots = 0; + break; + } + } + + private static IEnumerable EnumerateSingleRegs() + { + yield return RegMask.EAX; + yield return RegMask.ECX; + yield return RegMask.EDX; + yield return RegMask.EBX; + yield return RegMask.EBP; + yield return RegMask.ESI; + yield return RegMask.EDI; + // ESP is intentionally excluded -- it's never a live GC ref holder. + } } + +/// +/// An always-live GC frame slot (entry of the untracked-locals table). +/// The slot is live for the entire method body (post-prolog, pre-epilog). +/// +/// Frame-relative byte offset of the slot. +/// True if is EBP-relative; false if ESP-relative. +/// Raw flag bits from the encoded offset (0x1 = byref/interior, 0x2 = pinned). +public readonly record struct UntrackedSlot(int StackOffset, bool IsEbpRelative, uint LowBits); + +/// +/// A tracked GC frame variable with a per-offset lifetime range (entry of the +/// FrameVariableLifetime / VarPtr table). The slot is live while the executing +/// instruction offset lies in [BeginOffset, EndOffset). +/// VarPtr-tracked variables only exist on EBP-based frames. +/// +/// Inclusive code offset (relative to method start) at which the slot becomes live. +/// Exclusive code offset at which the slot becomes dead. +/// Frame-relative byte offset of the slot (EBP-relative on EBP frames, ESP-relative otherwise). +/// +/// Raw flag bits from the encoded offset (0x1 = byref/interior, 0x2 = "this" pointer -- note that for +/// tracked locals the 0x2 bit means "this", not "pinned" as it does for untracked slots). +/// +public readonly record struct VarPtrLifetime(uint BeginOffset, uint EndOffset, int StackOffset, uint LowBits); diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets index cac9cc85219315..36ecbb466094e4 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets +++ b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets @@ -72,9 +72,12 @@ $([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'testhost', '$(NetCoreAppCurrent)-$(HostOS)-$(_TestHostConfig)-$(_HostArch)')) - + - + +