DbgHelp:函数参数地址错误,当它是 x64 上的按值传递符号时

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

我正在使用 Windows DbgHelp 库转储我的 C++ 应用程序的调用堆栈。 PDB 位于正确的位置,我正在成功报告堆栈帧。我有一个复杂的类型遍历系统,它打印出所有的本地人,这是 mostly 工作。但是,在某些情况下,我得到了错误的地址并导致访问冲突。一种情况是当函数按值传递对象时:

struct Object { float x,y,z; }

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

在这种情况下,为objectByValue计算的地址是错误的。它是 near 在堆栈中的正确位置,但不完全是。我在查找有关正确地址的信息时遇到了一些困难。我正在执行以下操作:

对于堆栈上的变量,这个地址是正确的,对于大多数其他情况也是如此。但是,在某些情况下并非如此,包括这种情况。

我在下面包含了一个工作示例,输出如下:

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     

从例子中可以看出,从栈上的变量计算出的地址是正确的,但是传值是错误的。

有人知道我如何正确计算这个值吗?

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

在阅读了关于 X64 调用约定的文档后,我发现了以下句子:

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

这就解释了这个有趣的地址——调试符号给我的是存储完整数据引用的内存地址。所以我的代码流程基本上说:

if ( symbol 为参数且大小 > 8 ) 地址 = *(uint64_t)地址; // 解引用

然后通过我的类型系统传递该地址正确解析。

我认为其他人可能会觉得这很有用,因为它在 DbgHelp 库中的任何地方都没有记录,虽然有些人可能理解调用约定,但我没有想到传回的符号数据不会包含有助于表明这一点的内容。