diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 065f950ab7b3c5..b10d545ec0081e 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1160,8 +1160,6 @@ CDAC_TYPE_BEGIN(TransitionBlock) CDAC_TYPE_SIZE(sizeof(TransitionBlock)) CDAC_TYPE_FIELD(TransitionBlock, TYPE(CodePointer), ReturnAddress, offsetof(TransitionBlock, m_ReturnAddress)) CDAC_TYPE_FIELD(TransitionBlock, TYPE(CalleeSavedRegisters), CalleeSavedRegisters, offsetof(TransitionBlock, m_calleeSavedRegisters)) -// Offset to where stack arguments begin (just past the end of the TransitionBlock) -CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, OffsetOfArgs, sizeof(TransitionBlock)) // Offset to argument registers and first GCRefMap slot (platform-specific) #if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, sizeof(TransitionBlock)) @@ -1204,6 +1202,11 @@ CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame)) CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, Indirection, cdac_data::Indirection) CDAC_TYPE_END(ExternalMethodFrame) +CDAC_TYPE_BEGIN(PInvokeCalliFrame) +CDAC_TYPE_SIZE(sizeof(PInvokeCalliFrame)) +CDAC_TYPE_FIELD(PInvokeCalliFrame, T_POINTER, VASigCookiePtr, cdac_data::VASigCookiePtr) +CDAC_TYPE_END(PInvokeCalliFrame) + CDAC_TYPE_BEGIN(DynamicHelperFrame) CDAC_TYPE_SIZE(sizeof(DynamicHelperFrame)) CDAC_TYPE_FIELD(DynamicHelperFrame, T_INT32, DynamicHelperFrameFlags, cdac_data::DynamicHelperFrameFlags) diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index 5e66053f0733d4..b25ea276fb629d 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1359,6 +1359,14 @@ class PInvokeCalliFrame : public FramedMethodFrame trace->InitForUnmanaged(GetPInvokeCalliTarget()); return TRUE; } + + friend struct ::cdac_data; +}; + +template <> +struct cdac_data +{ + static constexpr size_t VASigCookiePtr = offsetof(PInvokeCalliFrame, m_pVASigCookie); }; // Some context-related forwards. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs index d0284332d230d6..01f27de174c7a9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs @@ -197,6 +197,7 @@ private void GetTransitionsFullyInterruptible(ref TargetPointer offset) /// private void GetTransitionsEbpFrame(ref TargetPointer offset) { + uint curOffs = 0; while (true) { uint argMask = 0, byrefArgMask = 0; @@ -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(offset++); @@ -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; } } @@ -296,7 +291,12 @@ private void GetTransitionsEbpFrame(ref TargetPointer offset) val = _target.Read(offset++); regMask = val & 0x7; byrefRegMask = val >> 4; - curOffs = _target.Read(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(offset); offset += 4; argCnt = _target.Read(offset); offset += 4; @@ -345,7 +345,7 @@ argMask ... bitmask of pushed pointer arguments /// /// based on GCDump::DumpGCTable /// - 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; @@ -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)); } } @@ -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 (; ; ) { @@ -418,7 +412,6 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) // skip = _target.GCDecodeUnsigned(ref offset); curOffs += skip; - lastSkip = skip; } else { @@ -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) { @@ -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: @@ -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: // @@ -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) @@ -505,11 +496,11 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) offset += 4; callPndTabCnt = _target.Read(offset); offset += 4; - callPndTabSize = _target.Read(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; @@ -518,8 +509,6 @@ private void GetTransitionsNoEbp(ref TargetPointer offset) } break; } - Console.WriteLine($"CallArgCount: {callArgCnt}"); - Console.WriteLine($"CallPndTabCnt: {callPndTabSize}"); } } } 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..568469664ce059 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,26 @@ 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. + /// + internal 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. + /// + internal ImmutableArray VarPtrLifetimes => _varPtrLifetimes.Value; + private readonly Lazy> _varPtrLifetimes; + + // Transition offsets sorted ascending. Cached so EnumerateLiveSlots / + // CalculatePushedArgSizeAt / GetInterruptibleRanges don't re-sort on every call + // (EnumerateLiveSlots fires once per managed frame during stack walking). + private ImmutableArray SortedTransitionOffsets => _sortedTransitionOffsets.Value; + private readonly Lazy> _sortedTransitionOffsets; + public X86GCInfo(Target target, TargetPointer gcInfoAddress, uint gcInfoVersion, uint relativeOffset = 0) { if (gcInfoVersion < MINIMUM_SUPPORTED_GCINFO_VERSION) @@ -151,6 +171,14 @@ 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 + _untrackedSlots = new(DecodeUntrackedSlots); + _varPtrLifetimes = new(DecodeVarPtrLifetimes); + + // Sorted offsets walked by EnumerateLiveSlots / CalculatePushedArgSizeAt / + // GetInterruptibleRanges. Cached once instead of re-sorting per call. + _sortedTransitionOffsets = new(() => [.. Transitions.Keys.OrderBy(o => o)]); } private ImmutableDictionary> DecodeTransitions() @@ -196,12 +224,19 @@ private ImmutableDictionary> DecodeTransitions() return argTable.Transitions.ToImmutableDictionary(); } - private uint CalculatePushedArgSize() + private uint CalculatePushedArgSize() => CalculatePushedArgSizeAt(RelativeOffset); + + /// + /// Number of bytes pushed for outgoing arguments at , + /// derived by walking the transition stream. Equivalent to native EnumGcRefsX86's + /// `pushedSize` from `scanArgRegTableI` / `scanArgRegTable`. + /// + private uint CalculatePushedArgSizeAt(uint codeOffset) { int depth = 0; - foreach (int offset in Transitions.Keys.OrderBy(i => i)) + foreach (int offset in SortedTransitionOffsets) { - if (offset > RelativeOffset) + if (offset > codeOffset) break; // calculate only to current offset foreach (BaseGcTransition gcTransition in Transitions[offset]) { @@ -233,6 +268,9 @@ private uint CalculatePushedArgSize() break; case IPtrMask: case GcTransitionCall: + case CalleeSavedRegister: + // Callee-saved register tags (e.g. partial-interrupt ESP-frame + // "Reg is saved" markers) don't affect outgoing-argument depth. break; default: throw new InvalidOperationException("Unsupported gc transition type"); @@ -240,9 +278,147 @@ private uint CalculatePushedArgSize() } } + // Clamp to >= 0: StackDepthTransition can carry negative deltas (call-site arg pops in + // partial-interrupt ESP-frame encoding) and a transient under-flow shouldn't wrap to a + // huge uint. + if (depth < 0) depth = 0; 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 (EnumGcRefsX86 untracked path) and + // ILCompiler.Reflection.ReadyToRun/x86/GcSlotTable.cs (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 are flags matching LiveSlot.GcFlags (0x1 = byref/interior, 0x2 = pinned). + // Reference: gc_unwind_x86.inl varPtrTable processing and + // ILCompiler.Reflection.ReadyToRun/x86/GcSlotTable.cs (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); + + // EBP-frames encode VarPtr offsets as positive values that mean EBP-relative-negative + // (locals live below EBP). Native EnumGcRefsX86 (gc_unwind_x86.inl) negates here. + if (Header.EbpFrame) + stkOffs = -stkOffs; + + 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 +431,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() { @@ -269,8 +449,397 @@ uint IGCInfoDecoder.GetCalleePoppedArgumentsSize() } 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."); + { + // The x86 GC info `interruptible` header bit divides methods into two encodings: + // + // * Fully interruptible (`Header.Interruptible == true`): every offset in the + // method body (post-prolog, pre-epilog) is GC-safe. The C++ walker + // (`EnumGcRefsX86` in gc_unwind_x86.inl) explicitly returns without + // reporting refs when the queried offset falls inside the prolog or any + // epilog, so we exclude those regions here too. + // * Partially interruptible (`Header.Interruptible == false`): only call sites + // are GC-safe. Each call site appears as a `GcTransitionCall` at its code + // offset. We surface each as a single-byte range so the only consumer + // (the catch-handler PC override in `StackWalk_1.WalkStackReferences`) can + // pick the first call-site offset at or after the clause start. + if (Header.Interruptible) + { + // Body minus prolog minus all epilogs. Epilogs are stored as code offsets + // (start of each epilog); each spans `EpilogSize` bytes. + uint cursor = Header.PrologSize; + uint methodSize = MethodSize; + List ranges = []; + foreach (int epilogStart in Header.Epilogs.OrderBy(e => e)) + { + uint eStart = (uint)epilogStart; + uint eEnd = eStart + Header.EpilogSize; + // IsCodeOffsetInEpilog treats `epilogStart` itself as NOT in the epilog + // (strict `>`), so the epilogStart byte is interruptible. End the preceding + // range at eStart+1 (clamped) to include that one byte. + uint rangeEnd = Math.Min(eStart + 1, methodSize); + if (rangeEnd > cursor) + ranges.Add(new InterruptibleRange(cursor, rangeEnd)); + cursor = Math.Max(cursor, eEnd); + } + if (cursor < methodSize) + ranges.Add(new InterruptibleRange(cursor, methodSize)); + return ranges; + } + + // Partially interruptible: emit each call-site offset as a (offset, offset+1) range. + List callRanges = []; + foreach (int offset in SortedTransitionOffsets) + { + if ((uint)offset < Header.PrologSize) + continue; + + foreach (BaseGcTransition transition in Transitions[offset]) + { + if (transition is GcTransitionCall) + { + callRanges.Add(new InterruptibleRange((uint)offset, (uint)offset + 1)); + break; + } + } + } + return callRanges; + } 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."); + { + // LiveSlot.SpBase: 1 = SP-relative, 2 = FRAMEREG (EBP) relative. + // See IGCInfo.cs LiveSlot docs and GcScanner.EnumGcRefsForManagedFrame. + const uint SP_REL = 1; + const uint FRAMEREG_REL = 2; + + // The early-return gates below mirror EnumGcRefsX86 (gc_unwind_x86.inl). + + // Funclet (e.g. catch handler) sharing this parent's locals will report them itself. + if (options.IsParentOfFuncletStackFrame) + return Array.Empty(); + + // GC info doesn't describe live slots inside prolog/epilog. The runtime only reaches here + // in those regions on ExecutionAborted (thread abort, stack overflow); skip reporting. + if (IsCodeOffsetInProlog(instructionOffset) || IsCodeOffsetInEpilog(instructionOffset)) + return Array.Empty(); + + // Aborted execution at a non-safe-point in non-interruptible code yields no reliable info. + if (options.IsExecutionAborted && !Header.Interruptible) + return Array.Empty(); + + List result = []; + + // For ESP-based frames, untracked locals (and VarPtr locals when applicable) are + // argBase-relative where `argBase = ESP + pushedSize` (gc_unwind_x86.inl EnumGcRefsX86). + // Translate to a true SP-relative offset by adding the pushed size at the queried offset. + // EBP-frame offsets are FRAMEREG-relative and need no adjustment. + int espBias = Header.EbpFrame ? 0 : (int)CalculatePushedArgSizeAt(instructionOffset); + + // (1) Untracked frame locals -- always live for the entire method body. + // Filter funclets suppress untracked reporting because the parent frame already reports them + // (mirrors the isFilterFunclet path in EnumGcRefsX86). + 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; + int spOffset = us.IsEbpRelative ? us.StackOffset : us.StackOffset + espBias; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: spOffset, SpBase: spBase, GcFlags: us.LowBits)); + } + } + + // (2) VarPtr-tracked frame locals -- live when the lifetime-check offset is within [Begin, End). + // On non-active frames EnumGcRefsX86 evaluates lifetimes at curOffs-1: a variable can be dead + // at the return address (call was last instruction of a try, return jumps to a catch handler). + { + uint spBase = Header.EbpFrame ? FRAMEREG_REL : SP_REL; + uint varPtrOffset = (options.IsActiveFrame || instructionOffset == 0) + ? instructionOffset + : instructionOffset - 1; + foreach (VarPtrLifetime vp in VarPtrLifetimes) + { + if (varPtrOffset < vp.BeginOffset || varPtrOffset >= vp.EndOffset) + continue; + + // LowBits encoding matches LiveSlot.GcFlags exactly. + int spOffset = Header.EbpFrame ? vp.StackOffset : vp.StackOffset + espBias; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: spOffset, SpBase: spBase, GcFlags: vp.LowBits)); + } + } + + // (3) Live registers and pushed pointer args from the transition stream. + EnumerateTransitionLiveSlots(instructionOffset, options, result, SP_REL); + + // ReportFPBasedSlotsOnly: drop register slots and any stack slot that isn't + // frame-register-relative. Mirrors GCInfoDecoder.ReportSlot. + if (options.ReportFPBasedSlotsOnly) + result.RemoveAll(s => s.IsRegister || s.SpBase != FRAMEREG_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; the live + /// state at the queried offset is whatever the most-recent call-site transition described. + /// Mirrors the byte-stream walks in scanArgRegTableI / scanArgRegTable (gc_unwind_x86.inl). + /// + private void EnumerateTransitionLiveSlots( + uint instructionOffset, + GcSlotEnumerationOptions options, + List result, + uint spRelBase) + { + // Live register state at the walked offset. + RegMask liveRegs = RegMask.NONE; + RegMask liveIptrRegs = RegMask.NONE; + + // Pushed pointer args, keyed by push-index (depth at PUSH time, 0-indexed). Bit 0 is the + // first push (highest stack address). The SP-relative byte offset is computed at emit + // time once finalDepth is known: addr = ESP_call + (finalDepth - 1 - pushIndex) * 4 + // (mirrors `pPendingArgFirst - i*sizeof(DWORD)` in EnumGcRefsX86). The translation must + // be deferred because subsequent pushes/pops change finalDepth. + SortedDictionary pushedPtrs = new(); + + // Total pushed pointer-size slots (incl. non-ptr args). Mirrors `argCnt` in scanArgRegTableI. + int depthSlots = 0; + + // Set when a partially-interruptible call site falls at instructionOffset; its embedded + // CallRegisters/PtrArgs/ArgMask describe the live state at the call site. + GcTransitionCall? activeCallSite = null; + + // On non-leaf frames register liveness is evaluated at the instruction *before* the call + // (a register holding a GC ref before a call may be dead afterwards). Active leaf uses + // the exact instructionOffset since execution is paused there. Mirrors curOffsRegs in + // EnumGcRefsX86. + uint regOffset = (options.IsActiveFrame || instructionOffset == 0) + ? instructionOffset + : instructionOffset - 1; + + foreach (int offset in SortedTransitionOffsets) + { + // Walk through instructionOffset (inclusive) so the call-site GcTransitionCall is + // captured for the partially-interruptible path; the regOffset adjustment above + // handles the register-state-before-call case for non-leaf fully-interruptible frames. + if (offset > instructionOffset) + break; + + foreach (BaseGcTransition transition in Transitions[offset]) + { + switch (transition) + { + case GcTransitionRegister regT: + // scanArgRegTableI gates only register-liveness bytes (00RRR DDD / 01RRR DDD) + // by curOffsRegs; arg-stream bytes (push/pop/non-ptr-push/kill, encoded as + // GcTransitionRegister with RegMask.ESP) always update depth and pushed-ptrs + // up to curOffsArgs. + if (regT.IsLive == Action.LIVE || regT.IsLive == Action.DEAD) + { + if ((uint)offset > regOffset) + continue; + } + 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 call sites carry the only authoritative live + // state at the call instruction. For fully-interruptible code, + // GcTransitionCall is informational only -- the surrounding LIVE/DEAD/ + // PUSH/POP transitions already maintain the state. + activeCallSite = callT; + break; + case IPtrMask: + case CalleeSavedRegister: + case GcTransitionCall: + // CalleeSavedRegister is informational. IPtrMask is reserved for future + // interior-pointer-bitmap support. GcTransitionCall at offset != + // instructionOffset is also ignored. + break; + default: + throw new InvalidOperationException($"Unsupported x86 GC transition: {transition.GetType().Name}"); + } + } + } + + // Emit live registers. Callee-saved (EBX/EBP/ESI/EDI) are always reported when execution + // continues; callee-trashed (EAX/ECX/EDX) are valid only on the active leaf frame because + // any callee will have overwritten them. Mirrors CHK_AND_REPORT_REG in EnumGcRefsX86. + // (The !willContinueExecution case is short-circuited by the aborted+!interruptible gate.) + const RegMask CalleeTrashedScratch = RegMask.EAX | RegMask.ECX | RegMask.EDX; + foreach (RegMask r in EnumerateSingleRegs()) + { + if ((liveRegs & r) == 0) continue; + if (!options.IsActiveFrame && (r & CalleeTrashedScratch) != 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 positive SP-relative offsets. Bit 0 (first push) ends up at + // the highest offset; the last push at offset 0. + foreach (KeyValuePair pushed in pushedPtrs) + { + int spOffset = (depthSlots - 1 - pushed.Key) * (int)_target.PointerSize; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: spOffset, 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)); + } + if (activeCallSite.PtrArgs.Count > 0) + { + // Huge encoding (0xFB): explicit per-pointer stack offsets. + 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)); + } + } + else if (activeCallSite.ArgMask != 0) + { + // Tiny / small / medium / large encodings: argMask is a bitmap where bit i + // represents a live pointer at ESP + i*sizeof(DWORD). Mirrors the bitmap loop + // in scanArgRegTable (gc_unwind_x86.inl). + uint argMask = activeCallSite.ArgMask; + uint iargMask = activeCallSite.IArgs; + int i = 0; + while (argMask != 0) + { + if ((argMask & 1) != 0) + { + uint gcFlags = (iargMask & 1) != 0 ? 0x1u : 0u; + result.Add(new LiveSlot(IsRegister: false, RegisterNumber: 0, SpOffset: i * (int)_target.PointerSize, SpBase: spRelBase, GcFlags: gcFlags)); + } + argMask >>= 1; + iargMask >>= 1; + i++; + } + } + } + } + + 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: + // GcArgTable emits ESP push/pop as GcTransitionRegister with RegMask.ESP for + // non-ptr arg pushes (depth tracking only); real pointer pushes use other + // RegMasks. Mirror scanArgRegTableI: ESP-only pushes advance depth without + // recording a pointer. + bool isPtrPush = (regT.Register & ~RegMask.ESP) != 0; + for (int i = 0; i < regT.PushCountOrPopSize; i++) + { + if (isPtrPush) + pushedPtrs[depthSlots] = regT.Iptr ? 0x1u : 0u; + depthSlots++; + } + break; + case Action.POP: + for (int i = 0; i < regT.PushCountOrPopSize && depthSlots > 0; i++) + { + depthSlots--; + pushedPtrs.Remove(depthSlots); + } + break; + case Action.KILL: + // EBP-frame partial-interrupt 0xFD: invalidate all currently-tracked pushed args. + pushedPtrs.Clear(); + depthSlots = 0; + break; + } + } + + private static void ApplyPointerTransition( + GcTransitionPointer ptrT, + ref int depthSlots, + SortedDictionary pushedPtrs) + { + switch (ptrT.Act) + { + case Action.PUSH: + // Non-ptr arg pushes (GetTransitionsFullyInterruptible 0xB0..0xB7) advance depth + // only; pointer pushes also record into pushedPtrs. + if (ptrT.IsPtr) + pushedPtrs[depthSlots] = ptrT.Iptr ? 0x1u : 0u; + depthSlots++; + break; + case Action.POP: + for (uint i = 0; i < ptrT.ArgOffset && depthSlots > 0; i++) + { + depthSlots--; + pushedPtrs.Remove(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). +internal 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 = pinned). +internal readonly record struct VarPtrLifetime(uint BeginOffset, uint EndOffset, int StackOffset, uint LowBits); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs index d693702051410b..795ccdaf0a3448 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs @@ -79,6 +79,8 @@ public GcTransitionRegister(int codeOffset, RegMask reg, Action isLive, bool isT { Register = reg; IsLive = isLive; + IsThis = isThis; + Iptr = iptr; PushCountOrPopSize = pushCountOrPopSize; } @@ -130,6 +132,8 @@ public GcTransitionPointer(int codeOffset, uint argOffs, uint argCnt, Action act ArgOffset = argOffs; ArgCount = argCnt; Act = act; + IsThis = isThis; + Iptr = iptr; IsPtr = isPtr; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs index 8b06193322f99f..35616f75c8316f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs @@ -22,6 +22,42 @@ public void HandleHijackFrame(HijackFrame frame) UpdateFromRegisterDict(args.Registers); } + public override void HandleTransitionFrame(FramedMethodFrame framedMethodFrame) + { + // Set IP, SP and callee-saved registers from the transition block (shared logic). + base.HandleTransitionFrame(framedMethodFrame); + + // x86: the base implementation skips the callee-popped argument byte count + // (cbStackPop) that the runtime's TransitionFrame::UpdateRegDisplay_Impl adds + // to CallerSP. + FrameHelpers frameHelpers = new(_target); + FrameType frameType = frameHelpers.GetFrameType( + _target.ProcessedData.GetOrAdd(framedMethodFrame.Address).Identifier); + + if (frameType == FrameType.PInvokeCalliFrame) + { + PInvokeCalliFrame frame = _target.ProcessedData.GetOrAdd(framedMethodFrame.Address); + if (frame.VASigCookiePtr != TargetPointer.Null) + { + VASigCookie cookie = _target.ProcessedData.GetOrAdd(frame.VASigCookiePtr); + _context.Context.Esp += cookie.SizeOfArgs; + } + return; + } + + if (framedMethodFrame.MethodDescPtr == TargetPointer.Null) + return; + + MethodDescHandle md = _target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(framedMethodFrame.MethodDescPtr); + if (!_target.Contracts.CallingConvention.TryComputeArgGCRefMapBlob(md, out byte[] blob) || blob.Length == 0) + return; + + // ReadStackPop returns the count in pointer-size units (4 bytes on x86). + GCRefMapDecoder decoder = new(blob); + uint stackPopSlots = decoder.ReadStackPop(); + _context.Context.Esp += stackPopSlots * (uint)_target.PointerSize; + } + public override void HandleTailCallFrame(TailCallFrame frame) { _context.Context.Eip = (uint)frame.ReturnAddress; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs index 6815878ec65c86..6f295bd7923df6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; /// @@ -34,8 +36,16 @@ internal enum GCRefMapToken /// internal ref struct GCRefMapDecoder { - private readonly Target _target; + // The decoder can source its bytes either from target memory (the R2R image, + // for ExternalMethodFrame / StubDispatchFrame) or from a host-side byte[] + // (for blobs we synthesize via ICallingConvention.TryComputeArgGCRefMapBlob). + // Exactly one of (_target + _currentByte) or (_blob) is non-null per instance; + // see UsesBlob below for the invariant that lets GetBit pick the right path + // without runtime null checks. + private readonly Target? _target; private TargetPointer _currentByte; + private readonly byte[]? _blob; + private int _blobIndex; private int _pendingByte; private int _pos; @@ -43,6 +53,18 @@ public GCRefMapDecoder(Target target, TargetPointer blob) { _target = target; _currentByte = blob; + _blob = null; + _blobIndex = 0; + _pendingByte = 0x80; // Forces first byte read + _pos = 0; + } + + public GCRefMapDecoder(byte[] blob) + { + _target = null; + _currentByte = TargetPointer.Null; + _blob = blob; + _blobIndex = 0; _pendingByte = 0x80; // Forces first byte read _pos = 0; } @@ -51,13 +73,25 @@ public GCRefMapDecoder(Target target, TargetPointer blob) public readonly int CurrentPos => _pos; + [MemberNotNullWhen(true, nameof(_blob))] + [MemberNotNullWhen(false, nameof(_target))] + private readonly bool UsesBlob => _blob is not null; + private int GetBit() { int x = _pendingByte; if ((x & 0x80) != 0) { - x = _target.Read(_currentByte); - _currentByte = new TargetPointer(_currentByte.Value + 1); + if (UsesBlob) + { + x = _blobIndex < _blob.Length ? _blob[_blobIndex] : 0; + _blobIndex++; + } + else + { + x = _target.Read(_currentByte); + _currentByte = new TargetPointer(_currentByte.Value + 1); + } x |= (x & 0x80) << 7; } _pendingByte = x >> 1; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index c94e7b66757a9d..bc35f28cd13973 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -192,7 +192,14 @@ private void PromoteCallerStackUsingGCRefMap( { Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(transitionBlock); GCRefMapDecoder decoder = new(_target, gcRefMapBlob); + EnumerateGCRefMapTokens(ref decoder, tb, scanContext); + } + private void EnumerateGCRefMapTokens( + ref GCRefMapDecoder decoder, + Data.TransitionBlock tb, + GcScanContext scanContext) + { if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X86) decoder.ReadStackPop(); @@ -234,17 +241,15 @@ private void ScanDynamicHelperFrame( const int DynamicHelperFrameFlags_ObjectArg2 = 2; Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(transitionBlock); - TargetPointer argRegStart = tb.ArgumentRegisters; if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg) != 0) { - scanContext.GCReportCallback(argRegStart, GcScanFlags.None); + scanContext.GCReportCallback(ArgSlotAddress(tb, 0), GcScanFlags.None); } if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg2) != 0) { - TargetPointer argAddr = new(argRegStart.Value + (uint)_target.PointerSize); - scanContext.GCReportCallback(argAddr, GcScanFlags.None); + scanContext.GCReportCallback(ArgSlotAddress(tb, 1), GcScanFlags.None); } } @@ -330,22 +335,63 @@ private TargetPointer FindGCRefMap(TargetPointer indirection) /// Entry point for promoting caller stack GC references via method signature. /// Matches native TransitionFrame::PromoteCallerStack (frames.cpp:1494). /// - /// - /// Not yet ported. Every call records a deferred frame so the stress harness - /// buckets the resulting cDAC-vs-runtime diff at this frame as a known issue - /// rather than a real cDAC bug. Will be replaced with a real port once the - /// signature- and ArgIterator-based ref enumeration lands. - /// - private static void PromoteCallerStack(TargetPointer frameAddress, GcScanContext scanContext) + private void PromoteCallerStack(TargetPointer frameAddress, GcScanContext scanContext) { - scanContext.RecordDeferredFrame(frameAddress); + IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; + RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture(); + RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); + bool supportedByCallingConvention = + os is RuntimeInfoOperatingSystem.Windows + && arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64; + + if (!supportedByCallingConvention) + { + scanContext.RecordDeferredFrame(frameAddress); + return; + } + + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + if (fmf.MethodDescPtr == TargetPointer.Null) + { + scanContext.RecordDeferredFrame(frameAddress); + return; + } + + MethodDescHandle md = _target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(fmf.MethodDescPtr); + if (!_target.Contracts.CallingConvention.TryComputeArgGCRefMapBlob(md, out byte[] blob) || blob.Length == 0) + { + scanContext.RecordDeferredFrame(frameAddress); + return; + } + + Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(fmf.TransitionBlockPtr); + GCRefMapDecoder decoder = new(blob); + EnumerateGCRefMapTokens(ref decoder, tb, scanContext); } private TargetPointer AddressFromGCRefMapPos(Data.TransitionBlock tb, int pos) { + if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X86) + return ArgSlotAddress(tb, pos); return new TargetPointer(tb.FirstGCRefMapSlot.Value + (ulong)(pos * _target.PointerSize)); } + private TargetPointer ArgSlotAddress(Data.TransitionBlock tb, int argIndex) + { + if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X86) + { + const int x86NumArgRegs = 2; + if (argIndex < x86NumArgRegs) + { + int offset = (x86NumArgRegs - 1 - argIndex) * _target.PointerSize; + return new TargetPointer(tb.ArgumentRegisters.Value + (ulong)offset); + } + int stackOffset = (argIndex - x86NumArgRegs) * _target.PointerSize; + return new TargetPointer(tb.OffsetOfArgs.Value + (ulong)stackOffset); + } + return new TargetPointer(tb.ArgumentRegisters.Value + (ulong)(argIndex * _target.PointerSize)); + } + private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) { if (cached is null) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/PInvokeCalliFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/PInvokeCalliFrame.cs new file mode 100644 index 00000000000000..b0d7353c158de2 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/PInvokeCalliFrame.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +[CdacType(nameof(DataType.PInvokeCalliFrame))] +internal partial class PInvokeCalliFrame : IData +{ + [Field] public TargetPointer VASigCookiePtr { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs index 3daacea455fdbb..c59ed3959b762e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs @@ -22,4 +22,14 @@ internal partial class TransitionBlock : IData /// [FieldAddress] public TargetPointer FirstGCRefMapSlot { get; } + + /// + /// Address just past the end of the TransitionBlock, where caller-pushed + /// stack arguments begin. On x86 this is where GCRefMap positions + /// >= NUM_ARGUMENT_REGISTERS map to (see native OffsetFromGCRefMapPos). + /// Computed as address + sizeof(TransitionBlock), mirrors native + /// TransitionBlock::GetOffsetOfArgs(). + /// + [InstanceDataStart] + public TargetPointer OffsetOfArgs { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index 4abbf4ce732740..e7d2e874b36bc4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -178,6 +178,7 @@ public enum DataType ExternalMethodFrame, DynamicHelperFrame, InterpreterFrame, + PInvokeCalliFrame, ComCallWrapper, SimpleComCallWrapper, diff --git a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs index d2a844e9de8d6e..b50da0526c7d4c 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs @@ -56,7 +56,6 @@ public void WalkStackReferences_RefsHaveValidSourceInfo(TestConfiguration config [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] - [SkipOnArch("x86", "GCInfo decoder does not support x86")] public void GCRoots_WalkStackReferences_FindsRefs(TestConfiguration config) { InitializeDumpTest(config, "GCRoots", "full"); @@ -73,7 +72,6 @@ public void GCRoots_WalkStackReferences_FindsRefs(TestConfiguration config) [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] - [SkipOnArch("x86", "GCInfo decoder does not support x86")] public void GCRoots_RefsPointToValidObjects(TestConfiguration config) { InitializeDumpTest(config, "GCRoots", "full"); diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs index f67ad3e5329ec7..4cca2ddf310dac 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs @@ -158,11 +158,6 @@ private async Task RunStressAsync(string debuggeeName, Stress /// /// Asserts the GCREFS stress run produced a [GC_STATS] summary /// with at least one verification and no hard failures. - /// is intentionally - /// tolerated (the native harness emits [KNOWN_ISSUE] for acknowledged - /// divergences via s_knownIssueCount, separate from - /// s_failCount) but is logged so regressions in the known-issue - /// count are visible during triage. /// internal static void AssertAllPassed(CdacStressResults results, string debuggeeName) { @@ -187,6 +182,26 @@ internal static void AssertAllPassed(CdacStressResults results, string debuggeeN $"({results.KnownIssues} known issue(s) tolerated).\n" + $"Log: {results.LogFilePath}\n\n{analysis}"); } + + // On supported targets every Frame's caller-arg refs are enumerated via + // the GCRefMap blob synthesized by ICallingConvention -- there should be + // no deferred frames at all, so any KnownIssue count is a regression. + GetTargetPlatform(out OSPlatform os, out Architecture arch); + bool requiresZeroKnownIssues = + os == OSPlatform.Windows && arch is Architecture.X86 or Architecture.X64; + if (requiresZeroKnownIssues && results.KnownIssues > 0) + { + string analysis = results.AnalyzeFailures(maxFailures: 3); + Assert.Fail( + $"GCREFS stress test '{debuggeeName}' had {results.KnownIssues} known issue(s) " + + $"out of {results.TotalVerifications} verifications. " + + "Windows x86 / x64 do not accept any deferred frames in this PR's scope -- " + + "every transition Frame's caller-stack scan must succeed via the shared " + + "ICallingConvention.TryComputeArgGCRefMapBlob path. A non-zero count likely " + + "indicates the encoder declined a method it previously handled (regression " + + "in CallingConvention_1.ComputeArgGCRefMapBlobCore).\n" + + $"Log: {results.LogFilePath}\n\n{analysis}"); + } } /// diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs index 4c6676b5e8b8a9..57841006264962 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs @@ -58,12 +58,6 @@ public async Task GCRefStress_AllVerificationsPass(Debuggee debuggee) if (debuggee.SkipGCRefs) throw new SkipTestException($"{debuggee.Name} is excluded from GCREFS pending follow-up work."); - // The GCREFS sub-check has only been validated on architectures where - // the cDAC GC root enumeration is at parity with the runtime. x86 has - // not been brought up yet (a separate effort); skip there until it is. - if (arch == Architecture.X86) - throw new SkipTestException("GCREFS stress is not yet validated on x86 (ARGITER stress runs there instead)"); - CdacStressResults results = await RunGCRefStressAsync(debuggee.Name); AssertAllPassed(results, debuggee.Name); } diff --git a/src/native/managed/cdac/tests/StressTests/known-issues.md b/src/native/managed/cdac/tests/StressTests/known-issues.md index b3741fae9eb1a2..37e8890fba70b6 100644 --- a/src/native/managed/cdac/tests/StressTests/known-issues.md +++ b/src/native/managed/cdac/tests/StressTests/known-issues.md @@ -68,6 +68,64 @@ need to be handled — that case currently isn't reachable because more structured mechanism (e.g., ETW events or StressLog) for better tooling integration and reduced I/O overhead during stress runs. +## Known intermittent failure: x86 stress flake (cDAC EnumGcRefs misses callee-saved register refs during EH-rich startup) + +Pattern (~20% of full x86 suite runs trigger this on at least one +debuggee): + +``` +[FAIL] Thread=0x... IP=0x... cDAC=7 RT=45 frames=12 (match=5 mismatch=7 known_nie=0) + Frame #4 System.RuntimeType.SplitName(...) [MISMATCH] cDAC=0 RT=1 SP_cDAC=0x0 SP_RT=0x0 + [ONLY(RT)] Addr=0x0 Obj=0x... Flags=0x0 Reg=6 Off=0 + Frame #5 System.RuntimeType.GetNestedType(...) [MISMATCH] cDAC=0 RT=3 ... + [ONLY(RT)] Addr=0x0 Obj=0x... Flags=0x0 Reg=6 Off=0 + ... + ... continues up through System.Diagnostics.Tracing.EventSource frames +``` + +Signature: the RT-only refs are concentrated in callee-saved registers +(`Reg=6` ESI, `Reg=7` EDI) on frames whose stack trace runs through +`NativeRuntimeEventSource..cctor()` -> `EventSource.Initialize` -> +`RuntimeType.GetNestedType` -> `RuntimeType.SplitName`. The frames +otherwise unwind cleanly and surrounding frames match. + +x86-only -- x64 and arm64 stress runs are consistently clean. This is +not the previously-tracked x64 GC-stress crashes #129545/#129546 +(those are completely different crashes in `MethodTable::Validate` +during managed exception unwind). + +Investigation so far: +- `HasFrameBeenUnwoundByAnyActiveException` is NOT the cause: across + ~77k invocations during a reproduced flake it returned `false` every + time, matching the runtime's behavior. +- `EnumerateLiveSlots` returns 0 slots for the affected frames at the + cDAC-computed `relativeOffset` (small values like 0x2d / 0x1f / 0x14). + The runtime sees ESI/EDI live at those frames -- so either cDAC's IP + differs from the runtime's view of the frame, or our partially- + interruptible call-site matching has an off-by-one when the trigger + fires between (rather than exactly at) call sites. + +Most likely root cause is one of: +1. For partially-interruptible methods, our `activeCallSite` match + requires `transition.Offset == instructionOffset` exactly. If the + IP cDAC reads for a frame mid-EH-dispatch is not the call-site + return-address offset (e.g., it's the call-instruction offset or + somewhere mid-instruction), the match fails and no register refs + are emitted. +2. The runtime tracks callee-saved register values across unwinds via + REGDISPLAY's `pCallerContext`; cDAC re-unwinds via + `Context.Clone().Unwind()` each call. A divergent context state + would produce a divergent IP and thus a divergent live-mask. +3. Some x86-specific frame-type handling difference between cDAC's + `StackWalk_1` iteration and the runtime's `StackWalk` during the + EH-dispatch-of-managed-exceptions path. + +Resolving this requires a follow-up investigation that compares the +IPs cDAC and the runtime see for these specific frames during a +reproduced flake (e.g., by adding per-frame instruction-pointer logging +to both sides of the stress harness and diffing them). It is x86-only, +flaky, and not gated on this PR. + ## Log Format Each verification emits a single header line followed by, on `[FAIL]` or