【问题标题】:Can I find how much stack memory is currently used in a Mac thread?我可以找到 Mac 线程中当前使用了多少堆栈内存吗?
【发布时间】:2021-05-15 19:09:51
【问题描述】:

当我尝试 google 时,我发现的只是有关获取和设置堆栈限制的内容,例如 -[NSThread stackSize],但这不是我想要的。我想知道当前线程的堆栈上实际使用了多少内存,或者等效地有多少堆栈空间可用。

我希望在用户提交的崩溃报告中找出堆栈溢出。在我之前的经验中,堆栈溢出通常是由无限递归引起的,但这次不是。所以我想知道我的一些 C++ 函数是否真的使用了比它们应有的更多的堆栈空间。


一条评论建议我在线程开始时获取堆栈指针,然后比较它的值。我碰巧遇到了问题Print out value of stack pointer。它有几个答案:

  1. (接受的答案)获取局部变量的地址。
  2. 使用一点汇编语言来获取堆栈指针寄存器的值。
  3. 在 GCC 或 Clang 中使用函数 __builtin_frame_address(0)

我尝试了这些技术(Apple Clang,macOS 11.2)。方法 2 和 3 产生了相似的结果,但方法 1 产生了非常不同的结果。一方面,方法 1 给出的值会随着您深入调用链而增加,而其他方法给出的值会减少。这是怎么回事,有两种不同的堆栈吗?

【问题讨论】:

  • 未测试,但您可以pthread_attr_getstack() 并与堆栈指针的当前值进行比较吗?
  • 或者你可以在线程第一次启动时将堆栈指针的值保存在线程本地存储中,稍后再进行比较。
  • @NateEldredge,我尝试了pthread_attr_getstack,它返回 NULL 作为 stackaddr 参数,并且没有错误。这对我来说没有任何意义。
  • x86 和 arm64 上的堆栈增长了,不,没有两种。如果方法 #1 显示的数字上升,那么要么是您的测试代码有问题,要么是编译器优化正在影响它。你能展示你的测试用例吗?
  • @NateEldredge 你是对的,这是一个编译器设置:地址清理器 (ASAN) 带有选项“检测返回后使用堆栈”会导致方法 #1 中的奇怪值。

标签: c macos


【解决方案1】:

如果您尝试这样做,我想您想知道您使用了多少内存来猜测您可以创建某种类型的最佳线程数。

答案并不容易,因为您通常无法访问堆栈指针。但我会尝试为您设计一个不需要访问堆栈指针的解决方案,而它需要为每个线程使用一个全局变量。

这个想法是强制一个参数在堆栈中。即使您系统中的 ABI 使用寄存器来传递参数,如果您将参数(实际参数变量)的地址保存到某个局部变量中,然后调用一个函数,它就会接受一个参数(类型不没关系,因为您将使用它的地址来比较两者):

static char *initial_stack_pseudo_addr;

size_t save_initial_stack(char dumb)
{
    /* the & operator forces dumb to be implemented in the stack */
    initial_stack_pseudo_addr = &dumb;
}

size_t how_much_stack(int dumb)
{
    return initial_stack_pseudo_addr - &dumb;
}

所以当你启动线程时,你调用save_initial_stack(0);。当您想知道您消耗了多少堆栈时,可以执行以下操作:

    size_t stack_size = how_much_stack(0);
    printf("at this point I have %zi bytes of stack\n", stack_size);

基本上,您所做的就是计算调用save_initial_stack() 的本地参数的地址与您现在执行的调用的本地参数的地址之间有多少字节,以获取堆栈大小。这是近似值,但堆栈变化太快,无法准确判断。

下面的例子将说明这件事。在设置初始指针值后调用递归函数,然后在每次递归调用时计算并打印堆栈的当前大小(近似值),并进行新的递归调用。程序应该一直运行,直到进程发生堆栈溢出。

#include <stdio.h>

char *stack_at_start;

void save_stack_pointer(char dumb)
{
    stack_at_start = &dumb;
}

size_t get_stack_size(char dumb)
{
    return stack_at_start - &dumb;
}

void recursive()
{
    printf("Stack size: %zi\n", get_stack_size(0));
    recursive();
}

int main()
{
    save_stack_pointer(0);
    recursive();
}

【讨论】:

  • 不幸的是,这种方法很难测试,因为编译器可能会将尾递归优化到循环中,as gcc -O2 does with your example。那么你将无法判断它是否有效。
  • 是的,您会看到堆栈没有增长。很难认为编译器会优化它,当您添加一个在每一步都强制进入堆栈的新参数时......但谁知道优化可以做什么。如果你不是 printf 调用,那么优化器应该用一个简单的堆栈溢出信号代替所有...但是你想打印指针的值...你需要优化器来保存递归调用。
  • 样本确实是一个测试。测试堆栈是否增长。实际上,当编译器将尾递归函数转换为循环时,它实际上是在更改代码,并利用参数值始终是常量的优势。是否应该使用random()作为参数的值,无法保证参数的可预测性,可能没有做这样的优化。
猜你喜欢
  • 2018-05-02
  • 2012-10-05
  • 1970-01-01
  • 2012-03-24
  • 1970-01-01
  • 1970-01-01
  • 2011-02-28
  • 1970-01-01
  • 2016-09-19
相关资源
最近更新 更多