Visual Studio 调试器扩展 - 解析切换堆栈

问题描述 投票:0回答:1

这个问题引用了我之前的两个问题:

替换应用程序的调用堆栈

为 Visual-Studio 调试器提供符号

现在,快速总结一下背景:我使用自定义“可执行”格式(因此没有 PDB)生成自定义本机代码。我使用他们的 Concord 库实现了自定义 Visual Studio 调试器扩展,以允许我符号化此代码。

我的代码还支持基于堆栈的协程,这有点像纤程,但限制更多(并且使用自定义的、更简单的实现)。不过,这涉及在进入协程时切换 RSP。

这样做的问题是,现在调试器无法再看到原始的调用堆栈。

>   Scene_EFFEB5F7E23FA118!TestNewYield::MethodMultiplex Wait(0)    Unbekannt
    Scene_EFFEB5F7E23FA118!TestNewYield::EventInitTrigger TestNewYield::MethodMultiplex(0)  Unbekannt
    // here would be the c++ call-site

这是有道理的,我们现在是一个不同的堆栈,但对于调试来说,这很烦人。

出于所有意图和目的,我的协程将随时返回到原始调用站点(除非发生一些灾难性故障,这将导致应用程序崩溃)。因此,由于我已经在开发一个编辑器扩展,其中涉及操作调用堆栈,所以我想我只是将其扩展以支持创建合并的调用堆栈,仅用于显示。但事实证明这比预期要困难得多。我可以使用以下 (C#) 实现让它部分工作:

DkmStackWalkFrame[] IDkmCallStackFilter.FilterNextFrame(DkmStackContext stackContext, DkmStackWalkFrame input)
{
    if (input == null) // null input frame indicates the end of the call stack. This sample does nothing on end-of-stack.
        return null;

    if (input.InstructionAddress != null)
    {
        var info = input.Process.GetAcclimateRuntimeInfo();
        var rip = input.InstructionAddress.CPUInstructionPart.InstructionPointer;

        foreach (var module in info.EventModules)
        {
            if (module.TryLookupSymbol(rip, true, out var symbol))
            {
                if (symbol.HasValue)
                {
                    // resolve our non-native frame
                    var runtime = input.Process.GetAEEventRuntimeInstance();
                    var sourceLocation = new SourceLocation(symbol.Value.UnitName, symbol.Value.CommandName, 0);
                    var data = sourceLocation.Encode();
                    var instrAddr = DkmCustomInstructionAddress.Create(runtime, module.Instance, data, 0, data, input.InstructionAddress.CPUInstructionPart);
                    var frame = DkmStackWalkFrame.Create(
                            stackContext.Thread,
                            instrAddr,
                            input.FrameBase,
                            input.FrameSize,
                            DkmStackWalkFrameFlags.None,
                            null,
                            input.Registers,
                            input.Annotations);

                    // our current location is within a coroutine
                    if (symbol.Value.IsYieldEntryFrame)
                    {
                        // read the ExecutionState, storing global yielding information
                        var ptr = ReadExecutionStatePtr(input);
                        if (ptr != 0)
                        {
                            // create annotated frame to mark transit
                            var annotatedFrame = DkmStackWalkFrame.Create(
                                stackContext.Thread,
                                null,
                                input.FrameBase,
                                input.FrameSize,
                                DkmStackWalkFrameFlags.None,
                                "[Yield Return]",
                                null,
                                null
                            );

                            // lookup ExecutionState.oldRSP
                            var previousRsp = input.Process.ReadMemory<ulong>(ptr + 80, DkmReadMemoryFlags.None);
                            // lookup return-address, after all register have been popped
                            var previousInstruction = input.Process.ReadMemory<ulong>(previousRsp + 5 * 8, DkmReadMemoryFlags.None);

                            var instruction = input.Process.CreateNativeInstructionAddress(previousInstruction);

                            var previousFrame = DkmStackWalkFrame.Create(
                                stackContext.Thread,
                                instruction,
                                previousRsp + 48,
                                0, // TODO: what's our previous frame size?
                                DkmStackWalkFrameFlags.ReturnStackFrame,
                                null, 
                                input.Registers,
                                input.Annotations);

                            return new DkmStackWalkFrame[]
                            {
                                frame,
                                annotatedFrame,
                                previousFrame
                            };
                        }
                    }

                    return new DkmStackWalkFrame[]
                    {
                        frame
                    };
                }
                else
                    break;
            }
        }
    }
    
    return new DkmStackWalkFrame[] { input };
}

这会显示一帧,并且寄存器也不会正确。因此,我可能需要调用手动堆栈遍历,或实现支持此操作的不同接口。我需要的是一种让调试器启动完整堆栈遍历的方法,从提取的“oldRSP”值开始。我一整天尝试了无数的事情,但我似乎无法弄清楚。

我尝试过实现各种与堆栈遍历相关的接口,例如 IDkmStackProvider 和 IDKmSymbolStackWalker。大多数根本没有被调用,唯一运行的是

IDkmMonitorStackWalk
,但它只在未解析的外部 DLL(如 ntl)上调用,所以这可能是错误的。

对于手动堆栈遍历,我没有找到任何选项来指定完整的自定义返回地址以及自定义寄存器。

DkmStackWalkContext
具有需要
topFramePointer
的过载,但这似乎与当前的 RSP 相关。

DkmAsyncStackWalkContext
似乎是为了类似于我正在做的事情,但由于我没有真正的任务或不同的线程,所以我不确定如何开始。

我知道这是一个非常具体的事情,有一个非常复杂的 API。我只是希望有人知道如何处理这种类型的情况 - 重申一下,这是如何在 Visual Studio 调试器框架内触发堆栈遍历,从自定义堆栈指针地址(不属于线程的当前堆栈区域)。

c# visual-studio-debugging visual-studio-extensions
1个回答
0
投票

花了更多时间后,它开始工作了。解决办法比较简单。

您必须为 DkmStackWalkContext 设置“ThreadContext”字段。由于这只是一些字节数据(之前从未使用过低级线程代码),因此假设它是可定制的数据。它实际上只是处理器 ThreadContext 结构的 C# 表示。您应该创建一个自定义的线程上下文表示,将所有寄存器设置为您的目标值,并用它调用自定义 StackWalk。实际上,您可以使用 DkmThread.GetContext 读回当前处理器状态,但您必须自己操作数据,因为 CONTEXT 似乎没有像 C++ WinAPI 中那样定义的结构。我发现这篇有用的文章,其中有人创建了这样一个 C# 结构。这导致以下代码:

// helper for reading CONTEXT64 from the link
// (couldn't get it to be unsafe, so needed to use marshaling)
public static CONTEXT64 ReadThreadContext(this DkmThread thread)
{
    var bytes = new byte[1232];
    thread.GetContext(CONTEXT_CONTROL, bytes);
    var pData = GCHandle.Alloc(bytes, GCHandleType.Pinned);
    var result = (CONTEXT64)Marshal.PtrToStructure(pData.AddrOfPinnedObject(), typeof(CONTEXT64));
    pData.Free();
    return result;
}

//
// working variant of the routine shown in the OP (part of IDkmCallStackFilter.FilterNextFrame)
//

// create annotated frame to mark transit
var annotatedFrame = DkmStackWalkFrame.Create(
    stackContext.Thread,
    null,
    input.FrameBase,
    input.FrameSize,
    DkmStackWalkFrameFlags.None,
    "[Yield Return]",
    null,
    null
);

// lookup ExecutionState.oldRSP
var previousRsp = input.Process.ReadMemory<ulong>(ptr + 80, DkmReadMemoryFlags.None);
// lookup return-address, after all register have been popped
var previousInstruction = input.Process.ReadMemory<ulong>(previousRsp + 5 * 8, DkmReadMemoryFlags.None);

var targetRsp = previousRsp + 48;
var oldRbx = input.Process.ReadMemory<ulong>(previousRsp, DkmReadMemoryFlags.None);
var oldRsi = input.Process.ReadMemory<ulong>(previousRsp + 8, DkmReadMemoryFlags.None);
var oldRdi = input.Process.ReadMemory<ulong>(previousRsp + 8 *2, DkmReadMemoryFlags.None);
var oldRbp = input.Process.ReadMemory<ulong>(previousRsp + 8 *3, DkmReadMemoryFlags.None);
var oldR12 = input.Process.ReadMemory<ulong>(previousRsp + 8 * 4, DkmReadMemoryFlags.None);

DkmStackWalkFrame[] frames = new DkmStackWalkFrame[0]; ;

//! sometimes throws a single exception inside GetContext which goes away on subsequent calls; probably some bug due to debugging the extension
for (int i = 0; i < 10; i++)
{
    try
    {
        var threadContext = input.Thread.ReadThreadContext();
        threadContext.Rsp = targetRsp;
        threadContext.Rip = previousInstruction;
        threadContext.Rbx = oldRbx;
        threadContext.Rdi = oldRdi;
        threadContext.Rbp = oldRbp;
        threadContext.R12 = oldR12;

        var threadContextValue = new ReadOnlyCollection<byte>(threadContext.GetBytes());
        var context = DkmStackWalkContext.Create(input.Thread, threadContextValue, 0, null);
        frames = context.RuntimeWalkNextFrames(128, out var endOfStack2);

        break;
    }
    catch (Exception)
    {
        // attempt again
    }
}

return new DkmStackWalkFrame[]
{
    frame,
    annotatedFrame
}.Concat(frames).ToArray();

导致发出完整堆栈,包括正确保留的所有寄存器和局部变量:

Scene_EFFEB5F7E23FA118!TestNewYield::MethodMultiplex Wait(0)    Unbekannt
Scene_EFFEB5F7E23FA118!TestNewYield::EventInitTrigger TestNewYield::MethodMultiplex(0)  Unbekannt
[Yield Return]  
Acclimate Engine.dll!ae::event::callStaticTrigger<ae::event::EventInitTrigger>(const char * pCode, float dt) Zeile 161  C++
Acclimate Engine.dll!ae::event::callStaticTriggerMulti<ae::event::EventInitTrigger>(ae::sys::ArrayView<char const * const,unsigned int> vAddresses, float dt) Zeile 179 C++
© www.soinside.com 2019 - 2024. All rights reserved.