diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index adb057fba3011a..3c8e48f22517cb 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -6767,11 +6767,11 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::IsValidObject(CORDB_ADDRESS obj, return hr; } -HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, BOOL walkFQ, UINT32 handleWalkMask) +HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, UINT32 handleWalkMask) { DD_ENTER_MAY_THROW; - DacRefWalker *walker = new (nothrow) DacRefWalker(this, walkStacks, walkFQ, handleWalkMask, TRUE); + DacRefWalker *walker = new (nothrow) DacRefWalker(this, walkStacks, handleWalkMask, TRUE); if (walker == NULL) return E_OUTOFMEMORY; @@ -7410,9 +7410,9 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetGenericArgTokenIndex(VMPTR_Met return S_OK; } -DacRefWalker::DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, BOOL walkFQ, UINT32 handleMask, BOOL resolvePointers) - : mDac(dac), mWalkStacks(walkStacks), mWalkFQ(walkFQ), mHandleMask(handleMask), mStackWalker(NULL), - mResolvePointers(resolvePointers), mHandleWalker(NULL), mFQStart(PTR_NULL), mFQEnd(PTR_NULL), mFQCurr(PTR_NULL) +DacRefWalker::DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers) + : mDac(dac), mWalkStacks(walkStacks), mHandleMask(handleMask), mStackWalker(NULL), + mResolvePointers(resolvePointers), mHandleWalker(NULL) { } @@ -7507,21 +7507,6 @@ HRESULT DacRefWalker::Next(ULONG celt, DacGcReference roots[], ULONG *pceltFetch } } - if (total < celt) - { - while (total < celt && mFQCurr < mFQEnd) - { - DacGcReference &ref = roots[total++]; - - ref.vmDomain = VMPTR_AppDomain::NullPtr(); - ref.objHnd.SetDacTargetPtr(mFQCurr.GetAddr()); - ref.dwType = (DWORD)CorReferenceFinalizer; - ref.i64ExtraData = 0; - - mFQCurr++; - } - } - while (total < celt && mStackWalker) { ULONG fetched = 0; diff --git a/src/coreclr/debug/daccess/dacdbiimpl.h b/src/coreclr/debug/daccess/dacdbiimpl.h index 1e4f450c4114ca..a1e947dc357a95 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.h +++ b/src/coreclr/debug/daccess/dacdbiimpl.h @@ -123,7 +123,7 @@ class DacDbiInterfaceImpl : HRESULT STDMETHODCALLTYPE IsValidObject(CORDB_ADDRESS obj, OUT BOOL * pResult); - HRESULT STDMETHODCALLTYPE CreateRefWalk(RefWalkHandle * pHandle, BOOL walkStacks, BOOL walkFQ, UINT32 handleWalkMask); + HRESULT STDMETHODCALLTYPE CreateRefWalk(RefWalkHandle * pHandle, BOOL walkStacks, UINT32 handleWalkMask); HRESULT STDMETHODCALLTYPE DeleteRefWalk(RefWalkHandle handle); HRESULT STDMETHODCALLTYPE WalkRefs(RefWalkHandle handle, ULONG count, OUT DacGcReference * objects, OUT ULONG *pFetched); @@ -958,7 +958,7 @@ class DDHolder class DacRefWalker { public: - DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, BOOL walkFQ, UINT32 handleMask, BOOL resolvePointers); + DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers); ~DacRefWalker(); HRESULT Init(); @@ -971,7 +971,7 @@ class DacRefWalker private: ClrDataAccess *mDac; - BOOL mWalkStacks, mWalkFQ; + BOOL mWalkStacks; UINT32 mHandleMask; // Stacks @@ -980,11 +980,6 @@ class DacRefWalker // Handles DacHandleWalker *mHandleWalker; - - // FQ - PTR_PTR_Object mFQStart; - PTR_PTR_Object mFQEnd; - PTR_PTR_Object mFQCurr; }; #endif // _DACDBI_IMPL_H_ diff --git a/src/coreclr/debug/di/process.cpp b/src/coreclr/debug/di/process.cpp index 0e889736c64c62..3952676ed852f4 100644 --- a/src/coreclr/debug/di/process.cpp +++ b/src/coreclr/debug/di/process.cpp @@ -2554,13 +2554,13 @@ HRESULT CordbProcess::GetTypeForObject(CORDB_ADDRESS addr, CordbType **ppType, C // CordbRefEnum // ****************************************** CordbRefEnum::CordbRefEnum(CordbProcess *proc, BOOL walkWeakRefs) - : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacksFQ(TRUE), + : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacks(TRUE), mHandleMask((UINT32)(walkWeakRefs ? CorHandleAll : CorHandleStrongOnly)) { } CordbRefEnum::CordbRefEnum(CordbProcess *proc, CorGCReferenceType types) - : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacksFQ(FALSE), + : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacks(FALSE), mHandleMask((UINT32)types) { } @@ -2659,7 +2659,7 @@ HRESULT CordbRefEnum::Next(ULONG celt, COR_GC_REFERENCE refs[], ULONG *pceltFetc EX_TRY { if (!mRefHandle) - hr = process->GetDAC()->CreateRefWalk(&mRefHandle, mEnumStacksFQ, mEnumStacksFQ, mHandleMask); + hr = process->GetDAC()->CreateRefWalk(&mRefHandle, mEnumStacks, mHandleMask); if (SUCCEEDED(hr)) { diff --git a/src/coreclr/debug/di/rspriv.h b/src/coreclr/debug/di/rspriv.h index 84c9b8ce1ced51..0fa7b000d37e6c 100644 --- a/src/coreclr/debug/di/rspriv.h +++ b/src/coreclr/debug/di/rspriv.h @@ -10613,7 +10613,7 @@ class CordbRefEnum : public CordbBase, public ICorDebugGCReferenceEnum private: RefWalkHandle mRefHandle; - BOOL mEnumStacksFQ; + BOOL mEnumStacks; UINT32 mHandleMask; }; diff --git a/src/coreclr/debug/inc/dacdbiinterface.h b/src/coreclr/debug/inc/dacdbiinterface.h index d73f0247fe3d9d..094c4ffacb18c0 100644 --- a/src/coreclr/debug/inc/dacdbiinterface.h +++ b/src/coreclr/debug/inc/dacdbiinterface.h @@ -1980,13 +1980,12 @@ IDacDbiInterface : public IUnknown // Parameters: // pHandle - out - the reference walk handle to create // walkStacks - in - whether or not to report stack references - // walkFQ - in - whether or not to report references from the finalizer queue // handleWalkMask - in - the types of handles report (see CorGCReferenceType, cordebug.idl) // Returns: // An HRESULT indicating whether it succeeded or failed. // Exceptions: // Returns an HRESULT indicating success or failure. - virtual HRESULT STDMETHODCALLTYPE CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, BOOL walkFQ, UINT32 handleWalkMask) = 0; + virtual HRESULT STDMETHODCALLTYPE CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, UINT32 handleWalkMask) = 0; // Deletes a reference walk. // Parameters: diff --git a/src/coreclr/inc/dacdbi.idl b/src/coreclr/inc/dacdbi.idl index bbcd41c6ed56c0..3dd2f4da9bd052 100644 --- a/src/coreclr/inc/dacdbi.idl +++ b/src/coreclr/inc/dacdbi.idl @@ -384,7 +384,7 @@ interface IDacDbiInterface : IUnknown HRESULT IsValidObject([in] CORDB_ADDRESS obj, [out] BOOL * pResult); // Reference Walking - HRESULT CreateRefWalk([out] RefWalkHandle * pHandle, [in] BOOL walkStacks, [in] BOOL walkFQ, [in] UINT32 handleWalkMask); + HRESULT CreateRefWalk([out] RefWalkHandle * pHandle, [in] BOOL walkStacks, [in] UINT32 handleWalkMask); HRESULT DeleteRefWalk([in] RefWalkHandle handle); HRESULT WalkRefs([in] RefWalkHandle handle, [in] ULONG count, [out] struct DacGcReference * refs, [out] ULONG * pFetched); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IGCExtensions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IGCExtensions.cs new file mode 100644 index 00000000000000..d13f89ca7f105a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IGCExtensions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; + +public static class IGCExtensions +{ + public static IEnumerable<(GCHeapSegmentInfo Segment, GCHeapData Heap)> EnumerateAllSegments(this IGC gc) + { + string[] gcIdentifiers = gc.GetGCIdentifiers(); + bool isWorkstation = gcIdentifiers.Contains(GCIdentifiers.Workstation); + foreach (GCHeapData heap in EnumerateHeaps(gc, isWorkstation)) + { + foreach (GCHeapSegmentInfo seg in gc.EnumerateHeapSegments(heap)) + { + yield return (seg, heap); + } + } + } + + private static IEnumerable EnumerateHeaps(IGC gc, bool isWorkstation) + { + if (isWorkstation) + { + yield return gc.GetHeapData(); + } + else + { + foreach (TargetPointer heapAddress in gc.GetGCHeaps()) + yield return gc.GetHeapData(heapAddress); + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs index e1c0da91b5f8c5..caa8ebe0ac719f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs @@ -48,6 +48,7 @@ public enum StackSourceType public class StackReferenceData { public bool HasRegisterInformation { get; init; } + public bool IsInteriorPointer { get; init; } public int Register { get; init; } public int Offset { get; init; } public TargetPointer Address { get; init; } @@ -91,7 +92,7 @@ public interface IStackWalk : IContract static string IContract.Name => nameof(StackWalk); IEnumerable CreateStackWalk(ThreadData threadData) => throw new NotImplementedException(); IEnumerable CreateStackWalk(ThreadData threadData, byte[] contextBuffer, bool isFirst = true) => throw new NotImplementedException(); - IReadOnlyList WalkStackReferences(ThreadData threadData) => throw new NotImplementedException(); + IReadOnlyList WalkStackReferences(ThreadData threadData, bool resolveInteriorPointers) => throw new NotImplementedException(); byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle, StackwalkFlag flags = StackwalkFlag.Default) => throw new NotImplementedException(); TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); string GetFrameName(TargetPointer frameIdentifier) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index abc141dd5cfdc5..e26549b29caee9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -10,6 +11,14 @@ internal class GcScanContext { private readonly Target _target; + private readonly IGC _gc; + private readonly IRuntimeTypeSystem _rts; + + private readonly LinearReadCache _cache; + private readonly uint _numComponentsOffsetArray; + private readonly uint _numComponentsOffsetString; + private readonly ulong _methodTableOffset; + private readonly byte _objectToMethodTableUnmask; public bool ResolveInteriorPointers { get; } public List StackRefs { get; } = []; public TargetPointer StackPointer { get; private set; } @@ -25,6 +34,13 @@ public GcScanContext(Target target, bool resolveInteriorPointers) { _target = target; ResolveInteriorPointers = resolveInteriorPointers; + _gc = target.Contracts.GC; + _rts = target.Contracts.RuntimeTypeSystem; + _cache = new LinearReadCache(target); + _numComponentsOffsetArray = (uint)target.GetTypeInfo(DataType.Array).Fields[Constants.FieldNames.Array.NumComponents].Offset; + _numComponentsOffsetString = (uint)target.GetTypeInfo(DataType.String).Fields["m_StringLength"].Offset; + _methodTableOffset = (ulong)target.GetTypeInfo(DataType.Object).Fields["m_pMethTab"].Offset; + _objectToMethodTableUnmask = target.ReadGlobal(Constants.Globals.ObjectToMethodTableUnmask); } public void UpdateScanContext(TargetPointer sp, TargetPointer ip, TargetPointer frame, StackRefData.SourceTypes? sourceTypeOverride = null) @@ -59,6 +75,7 @@ public void RecordDeferredFrame(TargetPointer frameAddress) StackRefs.Add(new StackRefData { HasRegisterInformation = false, + IsInteriorPointer = false, Register = 0, Offset = 0, Address = 0, @@ -88,14 +105,16 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL if (flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR) && ResolveInteriorPointers) { - // TODO(stackref): handle interior pointers - // https://github.com/dotnet/runtime/issues/125728 - throw new NotImplementedException(); + TargetPointer interiorObj = GetInteriorPointer(obj); + if (interiorObj == TargetPointer.Null) + return; + obj = interiorObj; } StackRefData data = new() { HasRegisterInformation = true, + IsInteriorPointer = flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR), Register = loc.Reg, Offset = loc.RegOffset, Address = addr, @@ -109,21 +128,90 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL StackRefs.Add(data); } - public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) + private TargetPointer GetInteriorPointer(TargetPointer obj) { - if (flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR) && ResolveInteriorPointers) + TargetPointer outerObj = TargetPointer.Null; + foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { - // TODO(stackref): handle interior pointers - // https://github.com/dotnet/runtime/issues/125728 - throw new NotImplementedException(); + if (obj.Value < seg.Start.Value || obj.Value >= seg.End.Value) + continue; + + TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); + ulong size = 0; + while (currentObj.Value <= obj.Value) + { + // Replicate IObject.GetMethodTableAddress in fast path with linear read cache + if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) + { + return TargetPointer.Null; + } + mt = mt.Value & (ulong)~_objectToMethodTableUnmask; + + // Replicate IObject.GetSize in fast path with linear read cache + if (!TryGetObjectSize(currentObj, mt, out size) || size == 0) + { + return TargetPointer.Null; + } + + size = _gc.AlignObjectSize(size, seg.Generation); + if (currentObj.Value + size > seg.End.Value || size == 0) + { + return TargetPointer.Null; + } + outerObj = currentObj; + currentObj = _gc.GetPotentialNextObjectAddress(currentObj, size, seg); + } + return outerObj + size > obj ? outerObj : TargetPointer.Null; } + return outerObj; + } + private bool TryGetObjectSize(TargetPointer objAddr, TargetPointer mt, out ulong size) + { + size = 0; + try + { + TypeHandle handle = _rts.GetTypeHandle(mt); + ulong baseSize = _rts.GetBaseSize(handle); + uint componentSize = _rts.GetComponentSize(handle); + uint numComponentsOffset = 0; + if (componentSize != 0) + { + if (_rts.IsArray(handle, out _) || _rts.IsFreeObjectMethodTable(handle)) + numComponentsOffset = _numComponentsOffsetArray; + else if (_rts.IsString(handle)) + numComponentsOffset = _numComponentsOffsetString; + else + return false; // unrecognized component type + if (!_cache.TryReadUInt32(objAddr.Value + numComponentsOffset, out uint numComponents)) + return false; + baseSize += (ulong)componentSize * numComponents; + } + size = baseSize; + return true; + } + catch + { + // The MT may be corrupt — surface as a read failure. + return false; + } + } + + public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) + { // Read the object pointer from the stack slot. TargetPointer obj = _target.ReadPointer(ppObj); + if (flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR) && ResolveInteriorPointers) + { + TargetPointer interiorObj = GetInteriorPointer(obj); + if (interiorObj != TargetPointer.Null) + obj = interiorObj; + } StackRefData data = new() { HasRegisterInformation = false, + IsInteriorPointer = flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR), Register = 0, Offset = 0, Address = ppObj, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs index c8cf0e64a95ced..692e649ba3ebcf 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs @@ -13,6 +13,7 @@ public enum SourceTypes } public bool HasRegisterInformation { get; set; } + public bool IsInteriorPointer { get; set; } public int Register { get; set; } public int Offset { get; set; } public TargetPointer Address { get; set; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index d3a0d4d8a9018c..b6e0db3b1e5bbc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -246,7 +246,7 @@ private IEnumerable RunStackWalk( } } - IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData) + IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData, bool resolveInteriorPointers) { // Initialize the walk data directly IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); @@ -273,7 +273,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre if (walkData.State == StackWalkState.Frameless && CheckForSkippedFrames(walkData)) walkData.State = StackWalkState.SkippedFrame; - GcScanContext scanContext = new(_target, resolveInteriorPointers: false); + GcScanContext scanContext = new(_target, resolveInteriorPointers); // Filter drives Next() directly, matching native Filter()+NextRaw() integration. // This prevents funclet-to-parent transitions from re-visiting already-walked frames. @@ -361,6 +361,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre return scanContext.StackRefs.Select(r => new StackReferenceData { HasRegisterInformation = r.HasRegisterInformation, + IsInteriorPointer = r.IsInteriorPointer, Register = r.Register, Offset = r.Offset, Address = r.Address, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs new file mode 100644 index 00000000000000..69a67b3872d53f --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; + +namespace Microsoft.Diagnostics.DataContractReader; +// Linear page cache used by the per-object heap walk. +public sealed class LinearReadCache +{ + // Typical page size + private const uint PageSize = 0x1000; + private readonly Target _target; + private readonly byte[] _page = new byte[PageSize]; + private ulong _currPageStart; + private uint _currPageSize; + + public LinearReadCache(Target target) + { + _target = target; + } + + public bool TryReadPointer(ulong addr, out TargetPointer value) + { + Span buffer = stackalloc byte[sizeof(ulong)]; + buffer = buffer.Slice(0, _target.PointerSize); + if (!TryRead(addr, buffer)) + { + value = TargetPointer.Null; + return false; + } + value = _target.ReadPointerFromSpan(buffer); + return true; + } + + public bool TryReadUInt32(ulong addr, out uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + if (!TryRead(addr, buffer)) + { + value = 0; + return false; + } + value = _target.IsLittleEndian + ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) + : BinaryPrimitives.ReadUInt32BigEndian(buffer); + return true; + } + + private bool TryRead(ulong addr, Span dest) + { + // If the request misses the currently-cached page, try to load the page + // containing it. If that fails (e.g. the page is unmapped), or the request + // straddles the end of the cached page, fall back to a direct read. + if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) + { + if (!MoveToPage(addr)) + return DirectRead(addr, dest); + } + + ulong offset = addr - _currPageStart; + if (offset + (ulong)dest.Length > _currPageSize) + return DirectRead(addr, dest); + + _page.AsSpan((int)offset, dest.Length).CopyTo(dest); + return true; + } + + private bool MoveToPage(ulong addr) + { + ulong pageStart = addr - (addr % PageSize); + try + { + _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); + _currPageStart = pageStart; + _currPageSize = PageSize; + return true; + } + catch + { + _currPageStart = 0; + _currPageSize = 0; + return false; + } + } + + private bool DirectRead(ulong addr, Span dest) + { + try + { + _target.ReadBuffer(addr, dest); + return true; + } + catch + { + return false; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 0a34ff6f9ed546..0bea797176476c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -4135,14 +4135,154 @@ public int IsValidObject(ulong obj, Interop.BOOL* pResult) return hr; } - public int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, Interop.BOOL walkFQ, uint handleWalkMask) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.CreateRefWalk(pHandle, walkStacks, walkFQ, handleWalkMask) : HResults.E_NOTIMPL; + public int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, CorGCReferenceType handleWalkMask) + { + int hr = HResults.S_OK; + RefWalk? walk = null; + try + { + if (pHandle is null) + throw new NullReferenceException(nameof(pHandle)); + + *pHandle = 0; + walk = new RefWalk(_target, walkStacks != Interop.BOOL.FALSE, handleWalkMask); + *pHandle = (nuint)((IEnum)walk).GetHandle(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + nuint legacyHandle = 0; + int hrLocal = _legacy.CreateRefWalk(&legacyHandle, walkStacks, handleWalkMask); + Debug.ValidateHResult(hr, hrLocal); + if (hrLocal == HResults.S_OK && walk is not null) + walk.LegacyHandle = legacyHandle; + else if (hrLocal == HResults.S_OK) + _legacy.DeleteRefWalk(legacyHandle); + } +#endif + return hr; + } public int DeleteRefWalk(nuint handle) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.DeleteRefWalk(handle) : HResults.E_NOTIMPL; + { + if (handle == 0) + return HResults.S_OK; + + int hr = HResults.S_OK; + nuint legacyHandle = 0; + try + { + GCHandle gcHandle = GCHandle.FromIntPtr((nint)handle); + if (gcHandle.Target is not RefWalk walk) + throw new ArgumentException("Handle does not reference a valid RefWalk instance.", nameof(handle)); + legacyHandle = walk.LegacyHandle; + ((IEnum)walk).Dispose(); + gcHandle.Free(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null && legacyHandle != 0) + { + int hrLocal = _legacy.DeleteRefWalk(legacyHandle); + Debug.ValidateHResult(hr, hrLocal); + } +#endif + return hr; + } + + // Should be called repeatedly until it returns S_FALSE. + public int WalkRefs(nuint handle, uint count, [In, MarshalUsing(CountElementName = "count"), Out] DacGcReference[] refs, uint* pFetched) + { + RefWalk walk; + try + { + if (pFetched is null) + throw new NullReferenceException(nameof(pFetched)); + if (handle == 0) + throw new ArgumentException("Handle is invalid.", nameof(handle)); + GCHandle gcHandle = GCHandle.FromIntPtr((nint)handle); + if (gcHandle.Target is not RefWalk rw) + throw new ArgumentException("Handle does not reference a valid RefWalk instance.", nameof(handle)); + walk = rw; + *pFetched = 0; + } + catch (System.Exception ex) + { + return ex.HResult; + } + + int hr = HResults.S_OK; + uint i = 0; + try + { + while (i < count && walk.Enumerator.MoveNext()) + refs[i++] = walk.Enumerator.Current; + + // A clean batch reports S_FALSE iff we couldn't fill the caller's request. + if (i < count) + hr = HResults.S_FALSE; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + *pFetched = i; + +#if DEBUG + if (_legacy is not null && walk.LegacyHandle != 0 && count > 0) + { + // Parity check covers the handle prefix only. + DacGcReference[] legacyRefs = new DacGcReference[(int)count]; + uint legacyFetched = 0; + int hrLocal = _legacy.WalkRefs(walk.LegacyHandle, count, legacyRefs, &legacyFetched); + // The number of reported stack refs is not guaranteed to match between cDAC and legacy DAC. + // If this is the case, the cDAC may report S_FALSE while the legacy DAC reports S_OK, or vice versa. + // Allow divergent success codes, but still validate the rest of the results. + Debug.ValidateHResult(hr, hrLocal, HResultValidationMode.AllowDivergentSuccess); + uint cdacHandlePrefix = CountHandlePrefix(refs, i); + uint legacyHandlePrefix = CountHandlePrefix(legacyRefs, legacyFetched); + Debug.Assert( + cdacHandlePrefix == legacyHandlePrefix, + $"cDAC handle-prefix count {cdacHandlePrefix}, legacy {legacyHandlePrefix}"); + + uint compare = Math.Min(cdacHandlePrefix, legacyHandlePrefix); + for (uint j = 0; j < compare; j++) + { + Debug.Assert(refs[j].dwType == legacyRefs[j].dwType, + $"refs[{j}].dwType cDAC={refs[j].dwType:X}, legacy={legacyRefs[j].dwType:X}"); + Debug.Assert(refs[j].vmDomain == legacyRefs[j].vmDomain, + $"refs[{j}].vmDomain cDAC=0x{refs[j].vmDomain:X}, legacy=0x{legacyRefs[j].vmDomain:X}"); + Debug.Assert(refs[j].objHnd == legacyRefs[j].objHnd, + $"refs[{j}].objHnd cDAC=0x{refs[j].objHnd:X}, legacy=0x{legacyRefs[j].objHnd:X}"); + Debug.Assert(refs[j].i64ExtraData == legacyRefs[j].i64ExtraData, + $"refs[{j}].i64ExtraData cDAC=0x{refs[j].i64ExtraData:X}, legacy=0x{legacyRefs[j].i64ExtraData:X}"); + } + } - public int WalkRefs(nuint handle, uint count, nint refs, uint* pFetched) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.WalkRefs(handle, count, refs, pFetched) : HResults.E_NOTIMPL; + static uint CountHandlePrefix(DacGcReference[] buffer, uint length) + { + for (uint j = 0; j < length; j++) + { + CorGCReferenceType dwType = buffer[j].dwType; + if (dwType == CorGCReferenceType.CorReferenceStack) + { + return j; + } + } + return length; + } +#endif + + return hr; + } public int GetTypeID(ulong obj, COR_TYPEID* pType) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index 3c039ee0148fb4..4910f02297cc15 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -3,9 +3,8 @@ using System; using System.Collections.Generic; -using System.Buffers.Binary; -using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; namespace Microsoft.Diagnostics.DataContractReader.Legacy; @@ -41,7 +40,8 @@ public HeapWalk(Target target) private IEnumerable Walk() { bool pendingFailure = false; - foreach ((GCHeapSegmentInfo seg, GCHeapData _) in EnumerateAllSegments()) + + foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { if (seg.Start.Value >= seg.End.Value) continue; @@ -49,6 +49,7 @@ private IEnumerable Walk() TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); while (currentObj.Value < seg.End.Value) { + // Replicate IObject.GetMethodTableAddress in fast path with linear read cache if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) { pendingFailure = true; @@ -56,6 +57,7 @@ private IEnumerable Walk() } mt = new TargetPointer(mt.Value & _methodTableMask); + // Replicate IObject.GetSize in fast path with linear read cache if (!TryGetObjectSize(currentObj, mt, out ulong size) || size == 0) { pendingFailure = true; @@ -63,7 +65,7 @@ private IEnumerable Walk() } size = _gc.AlignObjectSize(size, seg.Generation); - if (currentObj.Value + size > seg.End.Value) + if (currentObj.Value + size > seg.End.Value || size == 0) { pendingFailure = true; break; @@ -106,19 +108,6 @@ private IEnumerable Walk() yield return default; } - private IEnumerable<(GCHeapSegmentInfo Segment, GCHeapData Heap)> EnumerateAllSegments() - { - string[] gcIdentifiers = _gc.GetGCIdentifiers(); - bool isWorkstation = gcIdentifiers.Contains(GCIdentifiers.Workstation); - foreach (GCHeapData heap in EnumerateHeaps(_gc, isWorkstation)) - { - foreach (GCHeapSegmentInfo seg in _gc.EnumerateHeapSegments(heap)) - { - yield return (seg, heap); - } - } - } - private bool TryGetObjectSize(TargetPointer objAddr, TargetPointer mt, out ulong size) { size = 0; @@ -149,111 +138,4 @@ private bool TryGetObjectSize(TargetPointer objAddr, TargetPointer mt, out ulong return false; } } - - private static IEnumerable EnumerateHeaps(IGC gc, bool isWorkstation) - { - if (isWorkstation) - { - yield return gc.GetHeapData(); - } - else - { - foreach (TargetPointer heapAddress in gc.GetGCHeaps()) - yield return gc.GetHeapData(heapAddress); - } - } - - // Linear page cache used by the per-object heap walk. - private sealed class LinearReadCache - { - // Typical page size - private const uint PageSize = 0x1000; - - private readonly Target _target; - private readonly byte[] _page = new byte[PageSize]; - private ulong _currPageStart; - private uint _currPageSize; - - public LinearReadCache(Target target) - { - _target = target; - } - - public bool TryReadPointer(ulong addr, out TargetPointer value) - { - Span buffer = stackalloc byte[sizeof(ulong)]; - buffer = buffer.Slice(0, _target.PointerSize); - if (!TryRead(addr, buffer)) - { - value = TargetPointer.Null; - return false; - } - value = _target.ReadPointerFromSpan(buffer); - return true; - } - - public bool TryReadUInt32(ulong addr, out uint value) - { - Span buffer = stackalloc byte[sizeof(uint)]; - if (!TryRead(addr, buffer)) - { - value = 0; - return false; - } - value = _target.IsLittleEndian - ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) - : BinaryPrimitives.ReadUInt32BigEndian(buffer); - return true; - } - - private bool TryRead(ulong addr, Span dest) - { - // If the request misses the currently-cached page, try to load the page - // containing it. If that fails (e.g. the page is unmapped), or the request - // straddles the end of the cached page, fall back to a direct read. - if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) - { - if (!MoveToPage(addr)) - return DirectRead(addr, dest); - } - - ulong offset = addr - _currPageStart; - if (offset + (ulong)dest.Length > _currPageSize) - return DirectRead(addr, dest); - - _page.AsSpan((int)offset, dest.Length).CopyTo(dest); - return true; - } - - private bool MoveToPage(ulong addr) - { - ulong pageStart = addr - (addr % PageSize); - try - { - _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); - _currPageStart = pageStart; - _currPageSize = PageSize; - return true; - } - catch - { - _currPageStart = 0; - _currPageSize = 0; - return false; - } - } - - private bool DirectRead(ulong addr, Span dest) - { - try - { - _target.ReadBuffer(addr, dest); - return true; - } - catch - { - return false; - } - } - } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs new file mode 100644 index 00000000000000..dfdf3ff5a47bb8 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +/// +/// cDAC port of the native DacRefWalker. +/// +internal sealed class RefWalk : IEnum +{ + private const uint CDAC_DEFERRED_FRAME = 0x40000000; + private readonly Target _target; + private readonly IGC _gc; + private readonly bool _walkStacks; + private readonly CorGCReferenceType _handleWalkMask; + private readonly TargetPointer _appDomain; + + public IEnumerator Enumerator { get; } + public nuint LegacyHandle { get; set; } = 0; + + public RefWalk(Target target, bool walkStacks, CorGCReferenceType handleWalkMask) + { + _target = target; + _gc = target.Contracts.GC; + _walkStacks = walkStacks; + _handleWalkMask = handleWalkMask; + _appDomain = target.Contracts.Loader.GetAppDomain(); + Enumerator = Walk().GetEnumerator(); + } + + private IEnumerable Walk() + { + if (_handleWalkMask != 0) + { + foreach (DacGcReference reference in WalkHandles()) + yield return reference; + } + + if (_walkStacks) + { + foreach (DacGcReference reference in WalkStacks()) + yield return reference; + } + } + + private IEnumerable WalkHandles() + { + HandleType[] requestedTypes = GetRequestedHandleTypes(); + if (requestedTypes.Length == 0) + yield break; + + foreach (HandleData handle in _gc.GetHandles(requestedTypes)) + { + if (!TryMapHandle(handle, out CorGCReferenceType dwType, out ulong extraData)) + continue; + yield return new DacGcReference + { + vmDomain = _appDomain.Value, + objHnd = handle.Handle.Value, + dwType = dwType, + i64ExtraData = extraData, + }; + } + } + + private HandleType[] GetRequestedHandleTypes() + { + // Mirror native DacRefWalker::GetHandleWalkerMask: translate the CorGCReferenceType bits + // in the mask into the handle types consumed by IGC.GetHandles. + List types = new(); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrong)) + types.Add(HandleType.Strong); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrongPinning)) + types.Add(HandleType.Pinned); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleWeakShort)) + types.Add(HandleType.WeakShort); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleWeakLong)) + types.Add(HandleType.WeakLong); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleWeakRefCount) || _handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrongRefCount)) + types.Add(HandleType.RefCounted); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrongDependent)) + types.Add(HandleType.Dependent); + + if (types.Count == 0) + return []; + + // Only request types the target actually supports + HashSet supported = new(_gc.GetSupportedHandleTypes()); + types.RemoveAll(t => !supported.Contains(t)); + return types.ToArray(); + } + + private bool TryMapHandle(HandleData handle, out CorGCReferenceType dwType, out ulong extraData) + { + extraData = 0; + switch (_gc.GetHandleTypes([handle.Type])[0]) + { + case HandleType.Strong: + dwType = CorGCReferenceType.CorHandleStrong; + return true; + case HandleType.Pinned: + dwType = CorGCReferenceType.CorHandleStrongPinning; + return true; + case HandleType.WeakShort: + dwType = CorGCReferenceType.CorHandleWeakShort; + return true; + case HandleType.WeakLong: + dwType = CorGCReferenceType.CorHandleWeakLong; + return true; + case HandleType.RefCounted: + extraData = handle.RefCount; + dwType = handle.RefCount != 0 + ? CorGCReferenceType.CorHandleStrongRefCount + : CorGCReferenceType.CorHandleWeakRefCount; + return true; + case HandleType.Dependent: + dwType = CorGCReferenceType.CorHandleStrongDependent; + extraData = handle.Secondary.Value; + return true; + default: + dwType = 0; + return false; + } + } + + private IEnumerable WalkStacks() + { + IThread threadContract = _target.Contracts.Thread; + IStackWalk stackWalkContract = _target.Contracts.StackWalk; + + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadAddr = threadStore.FirstThread; + while (threadAddr != TargetPointer.Null) + { + ThreadData threadData = threadContract.GetThreadData(threadAddr); + + foreach (StackReferenceData stackRef in stackWalkContract.WalkStackReferences(threadData, true)) + { + // Skip cDAC-private deferred-frame markers; they are not real GC references. + if ((stackRef.Flags & CDAC_DEFERRED_FRAME) != 0) + continue; + + DacGcReference reference = new() + { + vmDomain = _appDomain.Value, + dwType = CorGCReferenceType.CorReferenceStack, + i64ExtraData = 0, + }; + + // Interior pointers, Frame refs, and enregistered vars are reported as a direct object pointer with the low bit set; + // everything else is reported by the address of the stack slot holding the object. + if (stackRef.IsInteriorPointer || stackRef.Address == TargetPointer.Null) + reference.pObject = stackRef.Object.Value | 1; + else + reference.objHnd = stackRef.Address.Value; + + yield return reference; + } + + threadAddr = threadData.NextThread; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index ceb5d8eb752013..339ed61c7a59dd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -128,6 +128,16 @@ public struct COR_HEAPOBJECT public COR_TYPEID type; } +[StructLayout(LayoutKind.Explicit)] +public struct DacGcReference +{ + [FieldOffset(0)] public ulong vmDomain; + [FieldOffset(8)] public ulong pObject; + [FieldOffset(8)] public ulong objHnd; + [FieldOffset(16)] public CorGCReferenceType dwType; + [FieldOffset(24)] public ulong i64ExtraData; +} + [StructLayout(LayoutKind.Sequential)] public struct COR_SEGMENT { @@ -369,6 +379,19 @@ public enum IlNum : int TYPECTXT_ILNUM = -3, } +[Flags] +public enum CorGCReferenceType : uint +{ + CorHandleStrong = 1 << 0, + CorHandleStrongPinning = 1 << 1, + CorHandleWeakShort = 1 << 2, + CorHandleWeakLong = 1 << 3, + CorHandleWeakRefCount = 1 << 4, + CorHandleStrongRefCount = 1 << 5, + CorHandleStrongDependent = 1 << 6, + CorReferenceStack = 0x80000001, +} + public enum CorDebugSetContextFlags { SET_CONTEXT_FLAG_ACTIVE_FRAME = 0x1, @@ -693,13 +716,13 @@ int EnumerateTypeHandleParams(ulong vmTypeHandle, int IsValidObject(ulong obj, Interop.BOOL* pResult); [PreserveSig] - int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, Interop.BOOL walkFQ, uint handleWalkMask); + int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, CorGCReferenceType handleWalkMask); [PreserveSig] int DeleteRefWalk(nuint handle); [PreserveSig] - int WalkRefs(nuint handle, uint count, nint refs, uint* pFetched); + int WalkRefs(nuint handle, uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] DacGcReference[] refs, uint* pFetched); [PreserveSig] int GetTypeID(ulong obj, COR_TYPEID* pType); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/DebugExtensions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/DebugExtensions.cs index 3b1600e5863b84..1ea57467a4bdeb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/DebugExtensions.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/DebugExtensions.cs @@ -22,6 +22,11 @@ internal enum HResultValidationMode /// AllowDivergentFailures, + /// + /// Allows divergent success HRESULTs, but failing HRESULTs must match exactly. + /// + AllowDivergentSuccess, + /// /// Like , but also allows the cDAC to succeed when the native DAC fails. /// The native DAC's MetaSig constructor traverses MethodDesc -> Module -> MDImport -> signature blob via @@ -95,6 +100,7 @@ internal static void ValidateHResult( { HResultValidationMode.Exact => cdacHr == dacHr, HResultValidationMode.AllowDivergentFailures => cdacHr == dacHr || (cdacHr < 0 && dacHr < 0), + HResultValidationMode.AllowDivergentSuccess => cdacHr == dacHr || (cdacHr >= 0 && dacHr >= 0), HResultValidationMode.AllowCdacSuccess => cdacHr == dacHr || (cdacHr < 0 && dacHr < 0) || (cdacHr >= 0 && dacHr < 0), _ => cdacHr == dacHr, }; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 58c1f420ef70b0..5306017a19e671 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -4057,7 +4057,7 @@ int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef refs = stackWalkContract.WalkStackReferences(matchingThread.Value); + IReadOnlyList refs = stackWalkContract.WalkStackReferences(matchingThread.Value, false); SOSStackRefData[] sosRefs = new SOSStackRefData[refs.Count]; for (int i = 0; i < refs.Count; i++) diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs new file mode 100644 index 00000000000000..c05efc91fe18f5 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for the ref-walking APIs +/// ( / / +/// ), cross-validated against the GC handle table +/// and the stack-reference walk in the GCRoots debuggee. +/// +public class DacDbiRefWalkDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "GCRoots"; + + private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); + + /// + /// Drives to completion and returns every reference reported. + /// + private static unsafe List WalkAllRefs(DacDbiImpl dbi, bool walkStacks, CorGCReferenceType handleWalkMask, uint batchSize = 32) + { + List refs = new(); + + nuint handle = 0; + int hr = dbi.CreateRefWalk(&handle, walkStacks ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, handleWalkMask); + Assert.Equal(System.HResults.S_OK, hr); + Assert.True(handle != 0, "CreateRefWalk produced a null handle"); + + try + { + DacGcReference[] buffer = new DacGcReference[batchSize]; + while (true) + { + uint fetched = 0; + int walkHr = dbi.WalkRefs(handle, (uint)batchSize, buffer, &fetched); + + Assert.True( + walkHr == System.HResults.S_OK || walkHr == System.HResults.S_FALSE, + $"WalkRefs returned 0x{walkHr:x}"); + + for (int i = 0; i < (int)fetched; i++) + refs.Add(buffer[i]); + + if (walkHr == System.HResults.S_FALSE) + break; + } + } + finally + { + int delHr = dbi.DeleteRefWalk(handle); + Assert.Equal(System.HResults.S_OK, delHr); + } + + return refs; + } + + /// + /// Cross product of with each handle type the + /// GCRoots debuggee allocates, along with its walk mask and the + /// the ref-walk is expected to report for that handle. + /// + public static IEnumerable HandleTypeConfigurations() + { + (HandleType HandleType, CorGCReferenceType Mask, CorGCReferenceType ExpectedType)[] cases = + [ + (HandleType.Strong, CorGCReferenceType.CorHandleStrong, CorGCReferenceType.CorHandleStrong), + (HandleType.Pinned, CorGCReferenceType.CorHandleStrongPinning, CorGCReferenceType.CorHandleStrongPinning), + (HandleType.WeakShort, CorGCReferenceType.CorHandleWeakShort, CorGCReferenceType.CorHandleWeakShort), + (HandleType.WeakLong, CorGCReferenceType.CorHandleWeakLong, CorGCReferenceType.CorHandleWeakLong), + (HandleType.Dependent, CorGCReferenceType.CorHandleStrongDependent, CorGCReferenceType.CorHandleStrongDependent), + ]; + + foreach (object[] config in TestConfigurations) + { + foreach ((HandleType handleType, CorGCReferenceType mask, CorGCReferenceType expectedType) in cases) + yield return [config[0], handleType, mask, expectedType]; + } + } + + [ConditionalTheory] + [MemberData(nameof(HandleTypeConfigurations))] + public unsafe void WalkRefs_Handles_MatchHandleTable(TestConfiguration config, HandleType handleType, CorGCReferenceType mask, CorGCReferenceType expectedType) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IGC gc = Target.Contracts.GC; + + List refs = WalkAllRefs(dbi, walkStacks: false, handleWalkMask: mask); + + // Every reference must be a handle of the requested type reported by its (low-bit-clear) handle address. + HashSet walkedHandles = new(); + foreach (DacGcReference r in refs) + { + Assert.Equal(expectedType, r.dwType); + Assert.Equal(0ul, r.pObject & 1); + walkedHandles.Add(r.pObject); + } + + HashSet expectedHandles = gc.GetHandles([handleType]).Select(h => h.Handle.Value).ToHashSet(); + Assert.True(expectedHandles.Count > 0, $"Expected at least one {handleType} handle in GCRoots."); + Assert.Equal(expectedHandles, walkedHandles); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void WalkRefs_StacksOnly_MatchStackReferenceWalk(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IThread threadContract = Target.Contracts.Thread; + IStackWalk stackWalk = Target.Contracts.StackWalk; + + List refs = WalkAllRefs(dbi, walkStacks: true, handleWalkMask: (CorGCReferenceType)0); + + // Compute the expected count of stack references directly (excluding cDAC-private + // deferred-frame markers, which WalkRefs filters out). + // Mirrors StackWalkHelpers.GcScanFlags.CDAC_DEFERRED_FRAME (internal to the Contracts assembly). + const uint CdacDeferredFrame = 0x40000000; + int expected = 0; + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadAddr = threadStore.FirstThread; + while (threadAddr != TargetPointer.Null) + { + ThreadData td = threadContract.GetThreadData(threadAddr); + expected += stackWalk.WalkStackReferences(td, true).Count(r => (r.Flags & CdacDeferredFrame) == 0); + threadAddr = td.NextThread; + } + + foreach (DacGcReference r in refs) + Assert.Equal(CorGCReferenceType.CorReferenceStack, r.dwType); + + Assert.Equal(expected, refs.Count); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs index 0a849fc577e514..d2a844e9de8d6e 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs @@ -29,7 +29,7 @@ public void WalkStackReferences_ReturnsWithoutThrowing(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.NotNull(refs); } @@ -43,7 +43,7 @@ public void WalkStackReferences_RefsHaveValidSourceInfo(TestConfiguration config ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); foreach (StackReferenceData r in refs) { Assert.True(r.Source != TargetPointer.Null, "Stack reference should have a non-null Source (IP or Frame address)"); @@ -64,7 +64,7 @@ public void GCRoots_WalkStackReferences_FindsRefs(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.NotNull(refs); Assert.True(refs.Count > 0, "Expected GCRoots Main thread to have at least one stack reference (objects kept alive via GC.KeepAlive)"); @@ -81,7 +81,7 @@ public void GCRoots_RefsPointToValidObjects(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); int validObjectCount = 0; foreach (StackReferenceData r in refs) @@ -143,7 +143,7 @@ public void NestedException_InFlightExceptionsReportedAsRoots(TestConfiguration // WalkStackReferences must surface every in-flight exception object as a stack reference, // reported with the Other source type (the ExInfo node is not a capital-F Frame). HashSet reported = new(); - foreach (StackReferenceData r in stackWalk.WalkStackReferences(crashingThread)) + foreach (StackReferenceData r in stackWalk.WalkStackReferences(crashingThread, false)) { if (r.Object == TargetPointer.Null) continue; @@ -174,7 +174,7 @@ public void GCProtect_GCFrameRootsAreReported(TestConfiguration config) // holding a GCPROTECT frame over the requesting Assembly reference. WalkStackReferences reports // each GCFrame-protected object with the GCFrame node address as its Source; the test walks the // thread's GCFrame chain and asserts a reported root's Source matches a node in that chain. - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); // Enumerate the thread's GCFrame chain node addresses; each reported GCFrame root carries the // GCFrame node address as its Source (UpdateScanContext(frame: pGCFrame)). @@ -232,7 +232,7 @@ public void StackRefs_FindsMarkerString(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodWithStackRefs"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.True(refs.Count > 0, "Expected at least one stack reference from MethodWithStackRefs"); bool foundMarker = false; @@ -272,7 +272,7 @@ public void StackRefs_FindsArrayReference(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodWithStackRefs"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.True(refs.Count > 0, "Expected at least one stack reference from MethodWithStackRefs"); // Look for the int[] { 1, 2, 3, 4, 5 } array using the Object contract. @@ -322,7 +322,7 @@ public void PInvoke_WalkStackReferences_ReturnsWithoutThrowing(TestConfiguration ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.NotNull(refs); } }