使用Visual Studio确定堆栈空间

8
我正在使用Visual Studio 2005中的C编程。 我有一个多线程程序,但这里并不是特别重要。
如何确定(大约)我的线程使用了多少堆栈空间?
我计划使用的技术是将堆栈内存设置为一些预定值,例如0xDEADBEEF,运行程序很长时间,暂停程序并调查堆栈。
如何使用Visual Studio读写堆栈内存?
编辑:例如,参见{{link1:“如何确定最大堆栈使用情况。”}} 那个问题讨论了嵌入式系统,但在这里我试图确定常规PC上的答案。
4个回答

16

Windows并不会立即提交堆栈内存,而是保留了堆栈内存的地址空间,当访问时才逐页提交。阅读这篇文章以获取更多信息。

因此,堆栈地址空间由三个连续区域组成:

  • 已预留但未提交内存,可用于堆栈增长(但尚未被访问);
  • 警戒页面也尚未被访问过,它用于在访问时触发堆栈增长;
  • 已提交内存,即线程曾经访问过的堆栈内存。

这使我们能够构建一个函数来获取堆栈大小(以页面大小为粒度):

static size_t GetStackUsage()
{
    MEMORY_BASIC_INFORMATION mbi;
    VirtualQuery(&mbi, &mbi, sizeof(mbi));
    // now mbi.AllocationBase = reserved stack memory base address

    VirtualQuery(mbi.AllocationBase, &mbi, sizeof(mbi));
    // now (mbi.BaseAddress, mbi.RegionSize) describe reserved (uncommitted) portion of the stack
    // skip it

    VirtualQuery((char*)mbi.BaseAddress + mbi.RegionSize, &mbi, sizeof(mbi));
    // now (mbi.BaseAddress, mbi.RegionSize) describe the guard page
    // skip it

    VirtualQuery((char*)mbi.BaseAddress + mbi.RegionSize, &mbi, sizeof(mbi));
    // now (mbi.BaseAddress, mbi.RegionSize) describe the committed (i.e. accessed) portion of the stack

    return mbi.RegionSize;
}

需要考虑的一件事是:CreateThread允许通过dwStackSize参数(当未设置STACK_SIZE_PARAM_IS_A_RESERVATION标志时)指定初始堆栈提交大小。如果此参数非零,则只有当堆栈使用量大于dwStackSize值时,我们的函数才会返回正确的值。


栈不是向下增长吗?为什么你要将RegionSize加到基地址上而不是减去它呢? - Philip
2
@Philip - 栈确实是向下增长的(至少在x86上是这样)。我之所以补充是因为VirtualQuery返回内存分配区域的基地址 - 向下增长栈的最后一个(理论上)可用字节的地址。在一个向上增长栈的平台上,第一个VirtualQuery调用将会给出所需的结果。我想我可以用一张图片来说明它;等我有更多时间时,我可能会这样做。 - atzz
@atzz 我对这个解决方案有一点担忧(它非常有帮助)。我们如何知道在执行此函数或其调用的其中一个VirtualQuery时,我们不会遇到保护页,从而导致实际堆栈状态在我们下面发生变化?保护页不可能移动吗? - acm
@acm 如果你愿意接受一些关于VirtualQuery内部和编译器代码生成的合理假设,那么它是不可能的(栈增长应该在第一个VirtualQuery调用完成时结束)。虽然你可以调用这个函数两次(或n次),并取最后一个结果来确保。但这也不是100%可靠的;例如,另一个进程可以对我们进行WriteProcessMemory,那么我们就会出问题 :)。栈使用的概念只有在健康监测或调试时才有意义,所以这个函数应该是可以的。 - atzz

8
你可以利用Win32线程信息块中的信息。
当你想在一个线程中找出它使用了多少堆栈空间时,你可以这样做:
#include <windows.h>
#include <winnt.h>
#include <intrin.h>

inline NT_TIB* getTib()
{
    return (NT_TIB*)__readfsdword( 0x18 );
}
inline size_t get_allocated_stack_size()
{
    return (size_t)getTib()->StackBase - (size_t)getTib()->StackLimit;
}

void somewhere_in_your_thread()
{
    // ...
    size_t sp_value = 0;
    _asm { mov [sp_value], esp }
    size_t used_stack_size = (size_t)getTib()->StackBase - sp_value;

    printf("Number of bytes on stack used by this thread: %u\n", 
           used_stack_size);
    printf("Number of allocated bytes on stack for this thread : %u\n",
           get_allocated_stack_size());    
    // ...
}

1

栈并不像你期望的那样工作。栈是一个线性序列的页面,其中最后(顶部)一个页面标有页面保护位。当触摸此页面时,保护位被移除,页面可以使用。为了进一步增长,分配了一个新的保护页。

因此,你想要的答案是保护页的分配位置。但是,你提出的技术会触摸到问题页面,结果会使你试图测量的东西无效。

确定(堆栈)页面是否具有保护位的非侵入式方法是通过 VirtualQuery()


1
你的评论并不完全正确。实际上,触摸相关页面是可以的。这种技术是将所有相关内存用特定值写入,然后在长时间运行后,查看有多少内存不再具有该值。 - JXG
引用微软的话:“尝试从守卫页读取或写入数据会导致系统引发 STATUS_ACCESS_VIOLATION 异常并关闭守卫页状态。因此,守卫页就像一次性的访问警报。”。不,读取也不例外。 - MSalters
我觉得我们在相互误解。 - JXG
是的,您已经牢牢地想好了一个解决方案,并想知道如何实现它。我有一个替代方案,符合堆栈实际操作系统的实现方式,并且我知道如何做 - 只需检查警戒页面移动的量即可。 - MSalters
1
但是如果我理解你的意思正确,你的解决方案只有页面分辨率。你的回答很有帮助,但并没有给我如我所期望的那样具体的答案。 - JXG
3
实际上,这是正确的答案,因为分配给堆栈的页面只能被该堆栈和线程独占。因此,堆栈大小始终作为页面数表示。还可以查看MSVC编译器选项 - 例如,“初始堆栈空间”之类的选项以页面大小的倍数指定。 - MSalters

0
你可以使用 GetThreadContext() 函数来确定线程的当前堆栈指针。然后使用 VirtualQuery() 来查找此指针的堆栈基址。将这两个指针相减即可得到给定线程的堆栈大小。

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