diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 1981508c041dc3..3e0cc310ea3e69 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -9,6 +9,8 @@ public interface IStackDataFrameHandle { // Describes what the current Context/FrameIter of this handle represents. StackWalkState State { get; } + bool IsActiveFrame { get; } + bool IsExceptionFrame { get; } } public enum StackWalkState 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 05de2d40403c66..54b417a7eef511 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 @@ -9,6 +9,8 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; public interface IStackDataFrameHandle { StackWalkState State { get; } + bool IsActiveFrame { get; } + bool IsExceptionFrame { get; } } public enum StackWalkState 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 7f07aef29f38c6..3726d039afbb11 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 @@ -35,7 +35,8 @@ private record StackDataFrameHandle( TargetPointer FrameAddress, ThreadData ThreadData, bool IsResumableFrame = false, - bool IsActiveFrame = false) : IStackDataFrameHandle + bool IsActiveFrame = false, + bool IsExceptionFrame = false) : IStackDataFrameHandle { } private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData) @@ -113,7 +114,9 @@ public StackDataFrameHandle ToDataFrame() { bool isResumable = IsCurrentFrameResumable(); bool isActiveFrame = IsFirst && State == StackWalkState.Frameless; - return new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData, isResumable, isActiveFrame); + bool isExceptionFrame = State is StackWalkState.Frame or StackWalkState.SkippedFrame + && FrameIter.GetCurrentFrameType() is FrameType.FaultingExceptionFrame or FrameType.SoftwareExceptionFrame; + return new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData, isResumable, isActiveFrame, isExceptionFrame); } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs index d9e121766b0288..68127810f9ecd7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs @@ -14,13 +14,27 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; [GeneratedComClass] public sealed unsafe partial class ClrDataStackWalk : IXCLRDataStackWalk { + private const uint SimpleFrameUnrecognized = 0x1; + private const uint SimpleFrameManagedMethod = 0x2; + private const uint SimpleFrameRuntimeUnmanagedCode = 0x8; + private const uint DetailedFrameUnrecognized = 0; + private const uint DetailedFrameExceptionFilter = 3; + private const uint StackSetCurrentContext = 0x1; + private const uint RequestSetFirstFrame = 0xe1000000; + private const uint RequestFrameData = 0xf0000000; + private readonly TargetPointer _threadAddr; private readonly uint _flags; private readonly Target _target; private readonly IXCLRDataStackWalk? _legacyImpl; private bool _currentFrameIsValid; - private readonly IEnumerator _dataFrames; + private bool _currentFrameIsActive; + private bool _currentFrameIsException; + private IEnumerator _dataFrames; + private byte[] _currentContext = []; + private ulong _currentStackPointer; + private ulong? _stackPrevious; public ClrDataStackWalk(TargetPointer threadAddr, uint flags, Target target, IXCLRDataStackWalk? legacyImpl) { @@ -31,58 +45,121 @@ public ClrDataStackWalk(TargetPointer threadAddr, uint flags, Target target, IXC ThreadData threadData = _target.Contracts.Thread.GetThreadData(_threadAddr); _dataFrames = _target.Contracts.StackWalk.CreateStackWalk(threadData).GetEnumerator(); + InitializeCurrentFrame(); + } - // IEnumerator begins before the first element. - // Call MoveNext() to set _dataFrames.Current to the first element. - _currentFrameIsValid = MoveNextLegacyVisible(); + private void InitializeCurrentFrame() + { + _currentFrameIsValid = false; + _currentFrameIsActive = false; + _currentFrameIsException = false; + _currentContext = []; + _currentStackPointer = 0; + _stackPrevious = null; + + if (_dataFrames.MoveNext()) + { + UpdateCurrentFrame(); + _stackPrevious = _currentStackPointer; + _currentFrameIsValid = MoveToVisibleFrame(); + } } - /// - /// Advance the enumerator to the next frame that the legacy SOSDAC stack walker - /// would have surfaced. - /// - private bool MoveNextLegacyVisible() + private bool MoveToVisibleFrame() { - while (_dataFrames.MoveNext()) + while (true) { - if (IsLegacyVisible(_dataFrames.Current)) + if (IsVisible(_dataFrames.Current)) { + _currentFrameIsActive = _dataFrames.Current.IsActiveFrame; + _currentFrameIsException = _dataFrames.Current.IsExceptionFrame; return true; } + + if (!_dataFrames.MoveNext()) + { + _currentFrameIsActive = false; + _currentFrameIsException = false; + return false; + } + + UpdateCurrentFrame(); } - return false; } - internal static bool IsLegacyVisible(IStackDataFrameHandle frame) - => frame.State is StackWalkState.Frameless - or StackWalkState.Frame - or StackWalkState.SkippedFrame; + private bool MoveNextVisibleFrame() + { + if (!_currentFrameIsValid) + return false; + + _stackPrevious = _currentStackPointer; + if (!_dataFrames.MoveNext()) + { + _currentFrameIsActive = false; + _currentFrameIsException = false; + return false; + } + + UpdateCurrentFrame(); + _stackPrevious = _currentStackPointer; + return MoveToVisibleFrame(); + } + + private bool IsVisible(IStackDataFrameHandle frame) + => frame.State switch + { + StackWalkState.Frameless => (_flags & SimpleFrameManagedMethod) != 0, + StackWalkState.Frame or StackWalkState.SkippedFrame => (_flags & SimpleFrameRuntimeUnmanagedCode) != 0, + _ => false, + }; + + private void UpdateCurrentFrame() + { + _currentContext = _target.Contracts.StackWalk.GetRawContext(_dataFrames.Current); + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); + context.FillFromBuffer(_currentContext); + _currentStackPointer = context.StackPointer.Value; + } + + private void ResetStackWalk(byte[] context, bool isFirst) + { + ThreadData threadData = _target.Contracts.Thread.GetThreadData(_threadAddr); + IEnumerator dataFrames = + _target.Contracts.StackWalk.CreateStackWalk(threadData, context, isFirst).GetEnumerator(); + + _dataFrames.Dispose(); + _dataFrames = dataFrames; + InitializeCurrentFrame(); + } int IXCLRDataStackWalk.GetContext(uint contextFlags, uint contextBufSize, uint* contextSize, [MarshalUsing(CountElementName = "contextBufSize"), Out] byte[] contextBuf) { int hr = HResults.S_OK; - - if (_currentFrameIsValid) + try { - IStackWalk sw = _target.Contracts.StackWalk; - IStackDataFrameHandle dataFrame = _dataFrames.Current; - byte[] context = sw.GetRawContext(dataFrame); - if (context.Length > contextBufSize) - hr = HResults.E_INVALIDARG; + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); + uint requiredContextSize = GetContextSizeForFlags(context, contextFlags); if (contextSize is not null) + *contextSize = requiredContextSize; + + if (contextBuf is null || contextBufSize < requiredContextSize || (uint)contextBuf.Length < contextBufSize) + throw new ArgumentException(); + + if (!_currentFrameIsValid) { - *contextSize = (uint)context.Length; + hr = HResults.S_FALSE; + } + else + { + Array.Copy(_currentContext, 0, contextBuf, 0, Math.Min(_currentContext.Length, (int)contextBufSize)); } - - context.CopyTo(contextBuf); } - else + catch (System.Exception ex) { - hr = HResults.S_FALSE; + hr = ex.HResult; } - #if DEBUG if (_legacyImpl is not null) { @@ -134,15 +211,99 @@ int IXCLRDataStackWalk.GetFrame(DacComNullableByRef frame) return hr; } int IXCLRDataStackWalk.GetFrameType(uint* simpleType, uint* detailedType) - => LegacyFallbackHelper.CanFallback() && _legacyImpl is not null ? _legacyImpl.GetFrameType(simpleType, detailedType) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + if (!_currentFrameIsValid) + { + hr = HResults.S_FALSE; + } + else + { + if (simpleType is not null) + { + *simpleType = _dataFrames.Current.State switch + { + StackWalkState.Frameless => SimpleFrameManagedMethod, + StackWalkState.Frame or StackWalkState.SkippedFrame => SimpleFrameRuntimeUnmanagedCode, + _ => SimpleFrameUnrecognized, + }; + } + + if (detailedType is not null) + *detailedType = _currentFrameIsException ? DetailedFrameExceptionFilter : DetailedFrameUnrecognized; + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl is not null) + { + uint simpleTypeLocal = 0; + uint detailedTypeLocal = 0; + int hrLocal = _legacyImpl.GetFrameType( + simpleType is null ? null : &simpleTypeLocal, + detailedType is null ? null : &detailedTypeLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + if (simpleType is not null) + Debug.Assert(*simpleType == simpleTypeLocal); + if (detailedType is not null) + Debug.Assert(*detailedType == detailedTypeLocal); + } + } +#endif + + return hr; + } + int IXCLRDataStackWalk.GetStackSizeSkipped(ulong* stackSizeSkipped) - => LegacyFallbackHelper.CanFallback() && _legacyImpl is not null ? _legacyImpl.GetStackSizeSkipped(stackSizeSkipped) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + if (_stackPrevious is null) + { + hr = HResults.S_FALSE; + } + else + { + if (stackSizeSkipped is null) + throw new ArgumentException(); + + *stackSizeSkipped = unchecked(_currentStackPointer - _stackPrevious.Value); + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl is not null) + { + ulong stackSizeSkippedLocal = 0; + int hrLocal = _legacyImpl.GetStackSizeSkipped(&stackSizeSkippedLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*stackSizeSkipped == stackSizeSkippedLocal); + } +#endif + + return hr; + } + int IXCLRDataStackWalk.Next() { int hr; try { - _currentFrameIsValid = MoveNextLegacyVisible(); + _currentFrameIsValid = MoveNextVisibleFrame(); hr = _currentFrameIsValid ? HResults.S_OK : HResults.S_FALSE; } catch (System.Exception ex) @@ -150,10 +311,6 @@ int IXCLRDataStackWalk.Next() hr = ex.HResult; } - // Advance the legacy stack walk to keep it in sync with the cDAC walk. - // GetFrame() passes the legacy frame to ClrDataFrame, which delegates - // GetArgumentByIndex/GetLocalVariableByIndex to it. If we don't advance - // the legacy walk here, those calls operate on the wrong frame. if (_legacyImpl is not null) { int hrLocal = _legacyImpl.Next(); @@ -166,35 +323,36 @@ int IXCLRDataStackWalk.Next() } int IXCLRDataStackWalk.Request(uint reqCode, uint inBufferSize, byte* inBuffer, uint outBufferSize, byte* outBuffer) { - const uint DACSTACKPRIV_REQUEST_FRAME_DATA = 0xf0000000; - int hr = HResults.S_OK; try { - if (inBufferSize != 0 || inBuffer != null) - throw new ArgumentException("Invalid input buffer parameters"); switch (reqCode) { case (uint)CLRDataGeneralRequest.CLRDATA_REQUEST_REVISION: - if (outBufferSize != sizeof(uint)) - throw new ArgumentException("Invalid buffer parameters for CLRDATA_REQUEST_REVISION"); + if (inBufferSize != 0 || inBuffer is not null || outBufferSize != sizeof(uint)) + throw new ArgumentException(); *(uint*)outBuffer = 1; - hr = HResults.S_OK; break; - case DACSTACKPRIV_REQUEST_FRAME_DATA: - if (outBufferSize != sizeof(ulong)) - throw new ArgumentException("Invalid buffer parameters for DACSTACKPRIV_REQUEST_FRAME_DATA"); + case RequestSetFirstFrame: + if (inBufferSize != sizeof(uint) || outBufferSize != 0) + throw new ArgumentException(); + _currentFrameIsActive = *(uint*)inBuffer != 0 + && _currentFrameIsValid + && _dataFrames.Current.State == StackWalkState.Frameless; + break; + case RequestFrameData: + if (inBufferSize != 0 || inBuffer is not null || outBufferSize != sizeof(ulong)) + throw new ArgumentException(); if (!_currentFrameIsValid) - throw new ArgumentException("Invalid frame"); + throw new ArgumentException(); IStackWalk sw = _target.Contracts.StackWalk; IStackDataFrameHandle frameData = _dataFrames.Current; TargetPointer frameAddr = sw.GetFrameAddress(frameData); *(ulong*)outBuffer = frameAddr.ToClrDataAddress(_target); - hr = HResults.S_OK; break; default: - throw new NotImplementedException(); + throw new ArgumentException(); } } catch (System.Exception ex) @@ -203,28 +361,106 @@ int IXCLRDataStackWalk.Request(uint reqCode, uint inBufferSize, byte* inBuffer, } #if DEBUG + int hrLocal = HResults.S_OK; + byte[]? localOutBuffer = null; if (_legacyImpl is not null) { - int hrLocal; - byte[] localOutBuffer = new byte[outBufferSize]; + localOutBuffer = new byte[outBufferSize]; fixed (byte* localOutBufferPtr = localOutBuffer) { hrLocal = _legacyImpl.Request(reqCode, inBufferSize, inBuffer, outBufferSize, localOutBufferPtr); } + } + + if (_legacyImpl is not null) + { Debug.ValidateHResult(hr, hrLocal); if (hr == HResults.S_OK) { for (int i = 0; i < outBufferSize; i++) { - Debug.Assert(localOutBuffer[i] == outBuffer[i], $"cDAC: {outBuffer[i]:x}, DAC: {localOutBuffer[i]:x}"); + Debug.Assert(localOutBuffer![i] == outBuffer[i], $"cDAC: {outBuffer[i]:x}, DAC: {localOutBuffer[i]:x}"); } } } +#else + if (reqCode == RequestSetFirstFrame) + _legacyImpl?.Request(reqCode, inBufferSize, inBuffer, outBufferSize, outBuffer); #endif return hr; } + int IXCLRDataStackWalk.SetContext(uint contextSize, [In, MarshalUsing(CountElementName = "contextSize")] byte[] context) - => LegacyFallbackHelper.CanFallback() && _legacyImpl is not null ? _legacyImpl.SetContext(contextSize, context) : HResults.E_NOTIMPL; + { + int hr = SetContext(contextSize, context, _currentFrameIsActive); + if (_legacyImpl is not null) + { + int hrLocal = _legacyImpl.SetContext(contextSize, context); +#if DEBUG + Debug.ValidateHResult(hr, hrLocal); +#endif + } + + return hr; + } + int IXCLRDataStackWalk.SetContext2(uint flags, uint contextSize, [In, MarshalUsing(CountElementName = "contextSize")] byte[] context) - => LegacyFallbackHelper.CanFallback() && _legacyImpl is not null ? _legacyImpl.SetContext2(flags, contextSize, context) : HResults.E_NOTIMPL; + { + int hr; + if ((flags & ~StackSetCurrentContext) != 0) + { + hr = HResults.E_INVALIDARG; + } + else + { + hr = SetContext(contextSize, context, (flags & StackSetCurrentContext) != 0); + } + + if (_legacyImpl is not null) + { + int hrLocal = _legacyImpl.SetContext2(flags, contextSize, context); +#if DEBUG + Debug.ValidateHResult(hr, hrLocal); +#endif + } + + return hr; + } + + private int SetContext(uint contextSize, byte[] context, bool isFirst) + { + int hr = HResults.S_OK; + try + { + IPlatformAgnosticContext platformContext = IPlatformAgnosticContext.GetContextForPlatform(_target); + uint contextFlagsOffset = _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X64 ? 0x30u : 0; + if (context is null + || contextSize < contextFlagsOffset + sizeof(uint) + || contextSize > (uint)context.Length) + throw new ArgumentException(); + + platformContext.FillFromBuffer(context.AsSpan(0, (int)contextSize)); + if (contextSize < GetContextSizeForFlags(platformContext, platformContext.RawContextFlags)) + throw new ArgumentException(); + + ResetStackWalk(context.AsSpan(0, (int)contextSize).ToArray(), isFirst); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; + } + + private uint GetContextSizeForFlags(IPlatformAgnosticContext context, uint contextFlags) + { + const uint X86ContextExtendedRegisters = 0x00010020; + const uint X86ExtendedRegistersOffset = 0xcc; + + return _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86 + && (contextFlags & X86ContextExtendedRegisters) != X86ContextExtendedRegisters + ? X86ExtendedRegistersOffset + : context.Size; + } } diff --git a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs index c8945685dbac78..6093e12d5cd429 100644 --- a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs @@ -5,9 +5,12 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Microsoft.Diagnostics.DataContractReader.Legacy; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Moq; using Xunit; +using HResults = System.HResults; namespace Microsoft.Diagnostics.DataContractReader.Tests; @@ -289,4 +292,209 @@ public void GetDebuggerEvalData_ReturnsTokenAndAssemblyFromDebuggerEval(MockTarg Assert.Equal(expectedToken, data.MethodToken); Assert.Equal(expectedAssembly, data.AssemblyPtr.Value); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ClrDataStackWalk_RuntimeFrameOperations(MockTarget.Architecture arch) + { + TestStackDataFrame initial = new(StackWalkState.InitialNativeContext); + TestStackDataFrame exception = new(StackWalkState.Frame, isExceptionFrame: true); + TestStackDataFrame managed = new(StackWalkState.Frameless, isActiveFrame: true); + (IXCLRDataStackWalk stackWalk, Mock contract, TestPlaceholderTarget target) = + CreateClrDataStackWalk( + arch, + flags: 0x8, + [(initial, 0x1000), (exception, 0x1100), (managed, 0x1200)]); + + uint simpleType; + uint detailedType; + Assert.Equal(HResults.S_OK, stackWalk.GetFrameType(&simpleType, &detailedType)); + Assert.Equal(0x8u, simpleType); + Assert.Equal(3u, detailedType); + + ulong stackSizeSkipped; + Assert.Equal(HResults.S_OK, stackWalk.GetStackSizeSkipped(&stackSizeSkipped)); + Assert.Equal(0x100ul, stackSizeSkipped); + + IPlatformAgnosticContext expectedContext = IPlatformAgnosticContext.GetContextForPlatform(target); + byte[] contextBuffer = new byte[expectedContext.Size]; + uint contextSize; + Assert.Equal(HResults.S_OK, stackWalk.GetContext(expectedContext.AllContextFlags, (uint)contextBuffer.Length, &contextSize, contextBuffer)); + Assert.Equal(expectedContext.Size, contextSize); + expectedContext.FillFromBuffer(contextBuffer); + Assert.Equal(0x1100ul, expectedContext.StackPointer.Value); + + byte[] smallBuffer = new byte[contextBuffer.Length - 1]; + Assert.Equal(HResults.E_INVALIDARG, stackWalk.GetContext(expectedContext.AllContextFlags, (uint)smallBuffer.Length, &contextSize, smallBuffer)); + Assert.Equal(expectedContext.Size, contextSize); + + if (!arch.Is64Bit) + { + byte[] contextWithoutExtendedRegisters = new byte[0xcc]; + Assert.Equal(HResults.S_OK, stackWalk.GetContext(expectedContext.FullContextFlags, (uint)contextWithoutExtendedRegisters.Length, &contextSize, contextWithoutExtendedRegisters)); + Assert.Equal((uint)contextWithoutExtendedRegisters.Length, contextSize); + } + + TargetPointer frameAddress = new(0x1234); + contract.Setup(s => s.GetFrameAddress(exception)).Returns(frameAddress); + ulong frameData; + Assert.Equal(HResults.S_OK, stackWalk.Request(0xf0000000, 0, null, sizeof(ulong), (byte*)&frameData)); + Assert.Equal(frameAddress.ToClrDataAddress(target).Value, frameData); + + DacComNullableByRef frame = new(isNullRef: false); + Assert.Equal(HResults.S_OK, stackWalk.GetFrame(frame)); + Assert.IsType(frame.Interface); + + Assert.Equal(HResults.S_FALSE, stackWalk.Next()); + Assert.Equal(HResults.S_FALSE, stackWalk.GetFrameType(&simpleType, &detailedType)); + Assert.Equal(HResults.E_INVALIDARG, stackWalk.GetFrame(frame)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ClrDataStackWalk_ManagedFilteringTracksSkippedStack(MockTarget.Architecture arch) + { + TestStackDataFrame initial = new(StackWalkState.InitialNativeContext); + TestStackDataFrame firstRuntime = new(StackWalkState.Frame); + TestStackDataFrame firstManaged = new(StackWalkState.Frameless); + TestStackDataFrame secondRuntime = new(StackWalkState.SkippedFrame); + TestStackDataFrame secondManaged = new(StackWalkState.Frameless); + (IXCLRDataStackWalk stackWalk, _, _) = CreateClrDataStackWalk( + arch, + flags: 0x2, + [ + (initial, 0x1000), + (firstRuntime, 0x1100), + (firstManaged, 0x1300), + (secondRuntime, 0x1400), + (secondManaged, 0x1700), + ]); + + uint simpleType; + uint detailedType; + Assert.Equal(HResults.S_OK, stackWalk.GetFrameType(&simpleType, &detailedType)); + Assert.Equal(0x2u, simpleType); + Assert.Equal(0u, detailedType); + + ulong stackSizeSkipped; + Assert.Equal(HResults.S_OK, stackWalk.GetStackSizeSkipped(&stackSizeSkipped)); + Assert.Equal(0x300ul, stackSizeSkipped); + + Assert.Equal(HResults.S_OK, stackWalk.Next()); + Assert.Equal(HResults.S_OK, stackWalk.GetStackSizeSkipped(&stackSizeSkipped)); + Assert.Equal(0x300ul, stackSizeSkipped); + Assert.Equal(HResults.S_FALSE, stackWalk.Next()); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ClrDataStackWalk_SetContextAndRequests(MockTarget.Architecture arch) + { + TestStackDataFrame initial = new(StackWalkState.Frameless); + TestStackDataFrame reset = new(StackWalkState.Frameless); + List isFirstValues = []; + (IXCLRDataStackWalk stackWalk, _, TestPlaceholderTarget target) = + CreateClrDataStackWalk( + arch, + flags: 0x2, + [(initial, 0x1000)], + [(reset, 0x2000)], + isFirstValues); + + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.RawContextFlags = context.FullContextFlags; + context.StackPointer = new TargetPointer(0x2000); + byte[] contextBuffer = context.GetBytes(); + + Assert.Equal(HResults.S_OK, stackWalk.SetContext((uint)contextBuffer.Length, contextBuffer)); + Assert.Equal([false], isFirstValues); + + uint isFirst = 1; + Assert.Equal(HResults.S_OK, stackWalk.Request(0xe1000000, sizeof(uint), (byte*)&isFirst, 0, null)); + Assert.Equal(HResults.S_OK, stackWalk.SetContext((uint)contextBuffer.Length, contextBuffer)); + Assert.Equal([false, true], isFirstValues); + + Assert.Equal(HResults.S_OK, stackWalk.SetContext2(0, (uint)contextBuffer.Length, contextBuffer)); + Assert.Equal(HResults.S_OK, stackWalk.SetContext2(1, (uint)contextBuffer.Length, contextBuffer)); + Assert.Equal([false, true, false, true], isFirstValues); + + Assert.Equal(HResults.E_INVALIDARG, stackWalk.SetContext2(2, (uint)contextBuffer.Length, contextBuffer)); + Assert.Equal(HResults.E_INVALIDARG, stackWalk.SetContext(1, contextBuffer)); + + uint revision; + Assert.Equal(HResults.S_OK, stackWalk.Request((uint)CLRDataGeneralRequest.CLRDATA_REQUEST_REVISION, 0, null, sizeof(uint), (byte*)&revision)); + Assert.Equal(1u, revision); + Assert.Equal(HResults.E_INVALIDARG, stackWalk.Request(0, 0, null, 0, null)); + } + + private static ( + IXCLRDataStackWalk StackWalk, + Mock Contract, + TestPlaceholderTarget Target) + CreateClrDataStackWalk( + MockTarget.Architecture arch, + uint flags, + IReadOnlyList<(TestStackDataFrame Frame, ulong StackPointer)> initialFrames, + IReadOnlyList<(TestStackDataFrame Frame, ulong StackPointer)>? resetFrames = null, + List? isFirstValues = null) + { + TargetPointer threadAddress = new(0x5000); + Mock thread = new(MockBehavior.Strict); + thread.Setup(t => t.GetThreadData(threadAddress)).Returns(default(ThreadData)); + + Mock stackWalk = new(MockBehavior.Strict); + stackWalk.Setup(s => s.CreateStackWalk(It.IsAny())) + .Returns(initialFrames.Select(f => (IStackDataFrameHandle)f.Frame)); + + Mock runtimeInfo = new(MockBehavior.Strict); + runtimeInfo.Setup(r => r.GetTargetArchitecture()) + .Returns(arch.Is64Bit ? RuntimeInfoArchitecture.X64 : RuntimeInfoArchitecture.X86); + + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(arch) + .AddMockContract(thread.Object) + .AddMockContract(stackWalk.Object) + .AddMockContract(runtimeInfo.Object) + .Build(); + + Dictionary contexts = []; + AddContexts(initialFrames); + if (resetFrames is not null) + { + AddContexts(resetFrames); + stackWalk.Setup(s => s.CreateStackWalk(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((ThreadData _, byte[] _, bool isFirst) => isFirstValues?.Add(isFirst)) + .Returns(resetFrames.Select(f => (IStackDataFrameHandle)f.Frame)); + } + + stackWalk.Setup(s => s.GetRawContext(It.IsAny(), StackwalkFlag.Default)) + .Returns((IStackDataFrameHandle frame, StackwalkFlag _) => contexts[frame]); + + return (new ClrDataStackWalk(threadAddress, flags, target, legacyImpl: null), stackWalk, target); + + void AddContexts(IReadOnlyList<(TestStackDataFrame Frame, ulong StackPointer)> frames) + { + foreach ((TestStackDataFrame frame, ulong stackPointer) in frames) + { + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + context.RawContextFlags = context.FullContextFlags; + context.StackPointer = new TargetPointer(stackPointer); + context.InstructionPointer = new TargetCodePointer(0x7000); + contexts.Add(frame, context.GetBytes()); + } + } + } + + private sealed class TestStackDataFrame : IStackDataFrameHandle + { + public TestStackDataFrame(StackWalkState state, bool isActiveFrame = false, bool isExceptionFrame = false) + { + State = state; + IsActiveFrame = isActiveFrame; + IsExceptionFrame = isExceptionFrame; + } + + public StackWalkState State { get; } + public bool IsActiveFrame { get; } + public bool IsExceptionFrame { get; } + } }