Search code examples
c++windowsdebuggingpdb-filesdiagnostics

DbgHelp: Wrong address of function parameter when it is a pass-by-value symbol on x64


I am using the Windows DbgHelp library to dump a callstack of my C++ application. The PDB is in the right place and I am successfully reporting the stack frames. I have an involved type traversal system that prints out all of the locals and this is mostly working. However, in some cases I am getting the wrong address and causing access violations. One case is when a function is passed an object by value:

struct Object { float x,y,z; }

void foo( Object objectByValue)
{
  // dump callstack
}

In this case, the address calculated for objectByValue is wrong. It is near the correct place on the stack, but not exactly. I am having some difficulties finding information on the right address. I am doing the following:

  • Set the correct context with SymSetContext(...)
  • Invoke SymEnumSymbols with a callback
  • Inside the callback, check if SYMFLAG_REGREL is set
  • Assign Address = SymInfo->Address + context.rbp or context.rsp depending on the SymInfo.Register value (CV_AMD64_RBP or CV_AMD64_RSP are only ever present )
  • Then I use that address to access the variable.

For variables on the stack this address is correct, as it is for most other cases. However, it is not in some, including this case.

I have included a working example below with the following output:

main-> Address of on stack: 000000000020B0D0 = { 1.000000, 2.000000,
3.000000 } 
foo-> Address of parameters: 000000000020D6F8 = { 1.000000, 2.000000, 3.000000 }
Print stack from bottom up:
Frame: foo Variable: objByValue offset=0xe0 address=0x20b090 size=8204 
Frame: main Variable: objOnStack offset=0x10 address=0x20b0d0 size=8204     

You can see in the example that the address calculate from the variable on the stack is correct, but for the pass by value it is wrong.

Does anyone have any insight on how I can correctly calculate this value?

#include "stdafx.h"
#include <windows.h>
#include <stdint.h>

#pragma comment(lib, "dbghelp.lib")

#pragma pack( push, before_imagehlp, 8 )
#include <imagehlp.h>
#pragma pack( pop, before_imagehlp )

// Normally it should be enough to use 'CONTEXT_FULL' (better would be 'CONTEXT_ALL')
#define USED_CONTEXT_FLAGS CONTEXT_FULL

#if defined(_M_AMD64)
const int ImageFileMachine = IMAGE_FILE_MACHINE_AMD64;
#else
const int ImageFileMachine = IMAGE_FILE_MACHINE_I386;
#endif
struct BaseAddresses
{
    uint64_t Rsp;
    uint64_t Rbp;
};

const int C_X64_REGISTER_RBP = 334; // Frame Base Pointer register
const int C_X64_REGISTER_RSP = 335; // Stack Pointer register (common in release builds with frame pointer removal)

BOOL EnumSymbolsCallback(PSYMBOL_INFO pSymInfo, ULONG SymbolSize, PVOID UserContext)
{
    BaseAddresses * pBaseAddresses = (BaseAddresses*)UserContext;
    ULONG64 base = 0;

    if ((pSymInfo->Flags & SYMFLAG_REGREL) != 0)
    {
        switch (pSymInfo->Register)
        {
        case C_X64_REGISTER_RBP:
            base = (ULONG64)pBaseAddresses->Rbp;
            break;
        case C_X64_REGISTER_RSP:
            base = (ULONG64)pBaseAddresses->Rsp;
            break;
        default:
            exit(0);
        }
    }

    ULONG64 address = base + pSymInfo->Address;

    printf("Variable: %s offset=0x%llx address=0x%llx size=%lu\n", pSymInfo->Name, pSymInfo->Address, address, pSymInfo->Size);

    return TRUE;
}

DWORD DumpStackTrace()
{
    HANDLE mProcess = GetCurrentProcess();
    HANDLE mThread = GetCurrentThread();

    if (!SymInitialize(mProcess, NULL, TRUE)) // load symbols, invasive
        return 0;

    CONTEXT c;

    memset(&c, 0, sizeof(CONTEXT));
    c.ContextFlags = USED_CONTEXT_FLAGS;
    RtlCaptureContext(&c);

    // SYMBOL_INFO & buffer storage
    char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
    PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer;

    STACKFRAME64        frame;
    memset(&frame, 0, sizeof(STACKFRAME64));

    DWORD64             displacement_from_symbol = 0;

    printf("Print stack from bottom up:\n");

    int framesToSkip = 1; // skip reporting this frame
    do 
    {
        // Get next stack frame
        if (!StackWalk64(ImageFileMachine, mProcess, mThread, &frame, &c, nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr))
        {
            break;
        }
        // Lookup symbol name using the address
        pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
        pSymbol->MaxNameLen = MAX_SYM_NAME;
        if (!SymFromAddr(mProcess, (ULONG64)frame.AddrPC.Offset, &displacement_from_symbol, pSymbol))
            return false;

        if (framesToSkip > 0)
        {
            framesToSkip--;
            continue;
        }

        printf("Frame: %s\n", pSymbol->Name);
        // Setup the context to get to the parameters
        IMAGEHLP_STACK_FRAME imSFrame = { 0 };
        imSFrame.InstructionOffset = frame.AddrPC.Offset;

        if (!SymSetContext(mProcess, &imSFrame, NULL))
            return false;

        BaseAddresses addresses;
        addresses.Rbp = c.Rbp;
        addresses.Rsp = c.Rsp;

        if (!SymEnumSymbols(mProcess, 0, 0, EnumSymbolsCallback, &addresses))                   
        {
            return false;
        }

        if (strcmp(pSymbol->Name, "main") == 0)
            break;


    } while (frame.AddrReturn.Offset != 0);

    SymCleanup(mProcess);

    return 0;
}

struct Structure
{
    float x, y, z;
};

void foo(Structure objByValue)
{
    printf("foo-> Address of parameters: %p = { %f, %f, %f }\n", &objByValue, objByValue.x, objByValue.y, objByValue.z);
    DumpStackTrace();
}

int main()
{
    Structure objOnStack = { 1, 2, 3 };

    printf("main-> Address of on stack: %p = { %f, %f, %f }\n", &objOnStack, objOnStack.x, objOnStack.y, objOnStack.z);

    foo(objOnStack);
        return 0;
}

Solution

  • After reading the documentation on X64 calling convention I discovered the following sentence:

    Any argument that doesn’t fit in 8 bytes, or isn't 1, 2, 4, or 8 bytes, must be passed by reference 1

    So that explains the funny address - what the debug symbols are giving me is the address of the memory storing the reference to the full data. So my code flow essentially says:

    if ( symbol is a parameter and the size > 8 ) address = *(uint64_t)address; // dereference

    And then passing that address through my type system resolves correctly.

    I thought other people might find this useful as it is not documented anywhere in the DbgHelp library, and while some people might understand the calling conventions, it didn't occur to me that the symbol data passed back wouldn't contain something helpful to indicate this.