如何获取基础堆栈指针的地址

18

我正在将一个应用程序从x86移植到x64。我使用的是Visual Studio 2009;大部分代码都是C++,一些部分是纯C。当编译为x64时,__asm关键字不受支持,并且我们的应用程序包含一些内联汇编器的部分。我没有编写这段代码,因此不知道它应该做什么:

int CallStackSize() {
    DWORD Frame;
    PDWORD pFrame;
    __asm
        {
            mov EAX, EBP
            mov Frame, EAX
        }
    pFrame = (PDWORD)Frame;
    /*... do stuff with pFrame here*/
}

EBP是指向当前函数堆栈的基指针。有没有一种方法可以在不使用内联汇编的情况下获取堆栈指针?我一直在查找微软提供的替代内联汇编的内部函数,但我找不到任何有用的东西。有什么想法吗?

Andreas问pFrame用于做什么。这是完整的函数:

int CallStackSize(DWORD frameEBP = 0)
{
    DWORD pc;
    int tmpint = 0;
    DWORD Frame;
    PDWORD pFrame, pPrevFrame;

    if(!frameEBP) // No frame supplied. Use current.
    {
        __asm
        {
            mov EAX, EBP
            mov Frame, EAX
        }
    }
    else Frame = frameEBP;

    pFrame = (PDWORD)Frame;
    do
    {
        pc = pFrame[1];
        pPrevFrame = pFrame;
        pFrame = (PDWORD)pFrame[0]; // precede to next higher frame on stack

        if ((DWORD)pFrame & 3) // Frame pointer must be aligned on a DWORD boundary. Bail if not so.
        break;

        if (pFrame <= pPrevFrame)
        break;

        // Can two DWORDs be read from the supposed frame address?
        if(IsBadWritePtr(pFrame, sizeof(PVOID)*2))
        break;

        tmpint++;
    } while (true);
    return tmpint;
}

变量pc没有被使用。这个函数似乎会一直向下遍历堆栈,直到失败为止。它假设无法读取应用程序堆栈之外的内容,因此当它失败时,就测量了调用堆栈的深度。这段代码不需要在所有编译器上都编译通过。只需要在VS2009上编译通过即可。该应用程序也不需要在所有计算机上运行。我们完全控制部署,因为我们自己安装/配置并将整个应用交付给客户。


pFrame是用来做什么的? - Andreas Brinck
6个回答

13

最正确的做法是重写这个函数,使其不需要访问实际的帧指针。这绝对是不好的行为。

但是,为了达到你想要的效果,你可以尝试以下操作:

int CallStackSize() {
    __int64 Frame = 0; /* MUST be the very first thing in the function */
    PDWORD pFrame;

    Frame++; /* make sure that Frame doesn't get optimized out */

    pFrame = (PDWORD)(&Frame);
    /*... do stuff with pFrame here*/
}

这段代码之所以有效,是因为在C语言中,通常函数的第一件事就是在分配本地变量之前保存基指针(ebp)的位置。通过创建一个本地变量(Frame),然后获取它的地址,我们实际上是获取了该函数堆栈帧的起始地址。
注意:某些优化可能会导致“Frame”变量被删除。虽然可能性不大,但请小心谨慎。
第二个注意点:您的原始代码以及这段代码操作的是“pFrame”指向的数据,而“pFrame”本身位于堆栈上。在这里,意外覆盖pFrame是有可能的,然后您将得到一个错误的指针,并可能出现一些奇怪的行为。当从x86移动到x64时,请特别注意此问题,因为pFrame现在是8字节而不是4字节,因此如果您旧的“使用pFrame进行操作”的代码在处理内存之前考虑了Frame和pFrame的大小,那么您需要考虑新的、更大的大小。

8
如果您取变量的地址,我认为该变量无法被删除。但是,编译器可以随意重新排列变量。从技术上讲,语言层面上没有保证“Frame”甚至位于堆栈上(但实际上我认为这样做应该没问题)。 - Jason Orendorff
4
你如果将其设为volatile会发生什么? - Josh Lee
2
这在应用程序代码中应该极为罕见,但在系统代码中却很常见。保守的垃圾收集器可以利用它。Mozilla的JavaScript引擎使用它有一个不同的原因:避免C堆栈溢出。http://mxr.mozilla.org/mozilla-central/ident?i=JS_CHECK_STACK_SIZE - Jason Orendorff
1
@JoshLee:是的,volatile int Frame = 0; 可以避免编译器将 0 存储到 Frame 中进行优化。如果启用了优化,Frame++ 将毫无帮助。但是在使用 MSVC 或 GCC 时,我们实际上并不需要这样做。使用地址就足以让编译器获取可以存储本地变量的堆栈帧中的空间地址。x64 MSVC 首先使用影子空间(返回地址上方)。针对 Linux 的 x86-64 GCC 使用 RSP 下方的红区。32 位 MSVC 保留实际空间。 - Peter Cordes
https://godbolt.org/z/PYJugh 展示了一个函数的汇编输出,该函数执行 return top - pFrame 操作,其中函数接受一个 uintptr_t top 参数,因此优化器不知道这个减法操作的含义。 - Peter Cordes
显示剩余2条评论

8
您可以使用_AddressOfReturnAddress()内嵌函数来确定当前帧指针中的一个位置,假设它还没有被完全优化。我假设编译器会防止该函数优化掉帧指针,如果您明确地引用它。或者,如果您只使用一个线程,您可以使用IMAGE_NT_HEADER.OptionalHeader.SizeOfStackReserveIMAGE_NT_HEADER.OptionalHeader.SizeOfStackCommit来确定主线程的堆栈大小。请参阅此文以了解如何访问当前映像的IMAGE_NT_HEADER
我还建议不要使用IsBadWritePtr来确定堆栈的末尾。至少你可能会导致堆栈增长直到达到保留区域,因为这将触发守卫页。如果您真的想要找到堆栈的当前大小,请使用VirtualQuery和您正在检查的地址。
如果最初的用途是遍历堆栈,您可以使用StackWalk64来实现。

它并不是查看剩余空间有多少,而是沿着前面的堆栈帧链向上遍历。基本上是执行回溯操作。 - caf
如果是这种情况,那么有一个API可以解决:StackWalk64。 - MSN
我建议你将其作为一个新答案添加,因为它听起来可能正是OP所需要的。 - caf

3
没有保证RBP(x64的EBP等效)实际上是指向调用堆栈中当前帧的指针。我猜想微软认为,尽管有几个新的通用寄存器,他们仍需要释放另一个寄存器,因此RBP只在调用alloca()的函数以及某些其他情况下作为帧指针使用。因此,即使支持内联汇编,这也不是最佳选择。
如果您只想要回溯,您需要在dbghelp.dll中使用StackWalk64。它在XP附带的dbghelp.dll中,并且在XP之前没有64位支持,因此您不需要将dll与应用程序一起发布。
对于32位版本,只需使用您当前的方法。您自己的方法可能比dbghelp的导入库小得多,更不用说内存中的实际dll了,因此这是一种明显的优化(个人经验:我已经实现了x86的Glibc风格的backtrace和backtrace_symbols,其大小不到dbghelp导入库的十分之一)。
此外,如果您将其用于进程内调试或发布后崩溃报告生成,则强烈建议仅使用提供给异常处理程序的CONTEXT结构进行工作。
也许有一天我会认真考虑x64,并找到绕过使用StackWalk64的廉价方法,可以分享,但由于我仍然将所有项目都定位为x86,因此我没有费心。

3

1

如果您需要精确的"基指针",那么嵌入式汇编是唯一的选择。

令人惊讶的是,可以编写代码来管理堆栈而只需较少的平台特定代码,但要完全避免使用汇编可能比较困难(这取决于您要做什么)。

如果您只是想避免堆栈溢出,可以获取任何本地变量的地址。


看起来你需要使用专用汇编器(ml64)来创建一个例程,该例程检查堆栈指针,发现前一个堆栈帧的地址(以便在调用汇编器例程时考虑堆栈指针的移动),并将其返回(我不知道你会如何实现这一点)。然后将其链接到你的C程序中。 - PP.

1
.code

PUBLIC getStackFrameADDR _getStackFrameADDR
getStackFrameADDR:
    mov RAX, RBP
    ret 0

END

这样的东西可能适合你。

使用 ml64 或 jwasm 进行编译,并在代码中使用以下方式调用它 extern "C" void getstackFrameADDR(void);


2
x86-64代码通常默认不带有帧指针。GCC和MSVC都可以在没有帧指针的情况下编译。 - Ted Mielczarek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接