Visual Studio debugger extension - resolve switched stack

35 Views Asked by At

This question references two of my previous questions:

Replacing the call stack of an application

Provide symbols for the visual-studio debugger

Now, quick summary of the background: I generate custom native code, using a custom "executable" format (so no PDB). I implemented a custom Visual Studio debugger extension using their Concord library, to allow me to symbolize this code.

My code also supports stack-based coroutines, which are kind of like a fiber, but more restrictive (and using a custom, much simpler implementation). Still, this involves switching RSP upon entry of the coroutine.

The problem with this is that now the debugger can't see the original call-stack anymore.

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

Which makes sense, we are one a different stack now, but for debugging, this is annoying.

For all intents and purposes, my coroutine will return to the original call-site very time (unless some catastrophic failure occurs, which would make the application crash). So, since I'm already developing an editor-extension, that involves manipulating the call-stack, I though I'd just extend it to support creating a merged call-stack, just for display. But this turns out to be way more difficult than expected. I can get it to work, partially, using the following (C#) implementation:

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 };
}

This will display the one frame, and the registers won't be correct, too. So I probably need to either invoke a manual stack walk, or implement a different interface that supports this operation. What I need is a way to get the debugger to start a full stack walk, starting at the extracted value of "oldRSP". I've tried countless things for the entire day, but I can't seem to figure it out.

I've tried implementing diverse stack walk related interfaces, like IDkmStackProvider and IDKmSymbolStackWalker. Most are not being invoked at all, the only thing that got to kind of running was IDkmMonitorStackWalk, but it was only invoked on unresolved external DLLs like ntl, so it's probably the wrong thing.

As for manual stack walking, I don't find any option to specify a full, custom return address, alongside custom registers. DkmStackWalkContext has an overload that takes an topFramePointer, but this seems to be relative to the current RSP.

DkmAsyncStackWalkContext seems to be intended for something similar to what I'm doing, but since I don't really have a task, or a different thread, I'm unsure on how to even start with this.

I know this is a very specific thing, with a very complicated API. I'm just hoping somebody has any idea how to deal with this type of situation - which, to reiterate, is how to trigger a stack walk inside the Visual Studio debugger framework starting at a custom stack pointer address (that is not part of the current stack-region for the thread).

1

There are 1 best solutions below

0
Juliean On BEST ANSWER

Got it working, after much more time spent. Solution is comparatively simple.

You have to set the "ThreadContext" field for the DkmStackWalkContext. Since this was just some byte-data (and never worked with low-level thread code before), assumed it was customizable data. It's actually just a C#-representation of the processors ThreadContext-structure. You are supposed to create a customized thread-context representation, with all registers set to your target-values, and invoke a custom StackWalk with it. You can actually just read back the current processors state with DkmThread.GetContext, but you have to manipulate the data yourself, as there does not seem to be a defined struct for the CONTEXT, as you'd have in C++ WinAPI. I found this helpful post where somebody created such a C# struct. This leads to the following code:

// 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();

Resulting in the full stack being emitted, including all registers and locals correctly preserved:

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++