【问题标题】:How do I determine detailed call-stack information in C++?如何确定 C++ 中的详细调用堆栈信息?
【发布时间】:2015-02-19 13:05:51
【问题描述】:

我想更好地理解调用堆栈,为了做到这一点,我想进一步研究它。

我将如何处理这些信息?我不知道!不过,学习新事物很有趣,这是我今天的新事物。

我不喜欢我的程序中的调用堆栈是一个我一无所知的神秘实体。它有多大?我当前的调用堆栈上有多少可用内存? 它在哪里?我不知道!而且,我想知道。

我目前处理调用堆栈的方法是意识到它,但在遇到堆栈溢出错误之前不会故意与它进行交互。 这还不够好!

所以在我的任务中,我想弄清楚如何做以下事情:

  1. 我想知道我正在操作的当前调用堆栈的总大小。

  2. 我想计算当前调用堆栈上可用的总内存。

  3. 我希望能够找到我的调用堆栈开始的地址。

声明可能如下所示,并针对各种平台填写:

class call_stack
{
    inline static void* base_address();
    inline static void* end_address();
    inline static std::size_t size();
    inline static std::size_t remaining();
};

当前桌面和移动平台上的堆栈信息是如何定义的,我如何访问它?如何在编译时编码,或在运行时确定它?

对此的研究让我想起了 Herb Sutter 关于原子的一次演讲。他说过类似“作为 C++ 程序员,我们喜欢按下红色的大按钮,上面写着不要触摸。”

我意识到有很多关于调用堆栈的问题,我相信我的问题既有意义又独特。

关于调用堆栈主题的其他问题不会像我在这里问的那样广泛地询问调用堆栈。

【问题讨论】:

  • 哪个操作系统?
  • @RichardHodges 当前桌面和移动设备 - Windows、OS X、Linux、iOS、Android。也许还有更多...
  • 您正在寻找的谷歌术语是“backtrace”和“stacktrace”。已经有很多代码示例将堆栈跟踪转储到 stackoverflow 中的 cout。
  • 这是一个非常广泛的 SO 问题。
  • @Richard Hodges,回溯只是 Michael 所问问题的一小部分。回溯仅涵盖堆栈中每个函数调用的返回地址。它不包含有关调用帧的其余内容或它们的大小的任何信息。

标签: c++ memory callstack


【解决方案1】:

由于堆栈未在标准中指定,因此您要求的任何内容都无法在标准 c++ 中完成。

您可以通过用汇编语言读取 cpu 寄存器来访问该信息。如何做到这一点取决于 cpu 架构、操作系统以及编译器使用的调用约定。查找所需信息的最佳位置是体系结构、操作系统等的手册。该平台还可以通过系统调用或虚拟文件系统提供信息。

例如,下面是常见 x86 架构的 wikipedia 页面的快速浏览

SP/ESP/RSP:栈顶地址的栈指针。

BP/EBP/RBP:栈基指针,用于保存当前栈帧的地址。

您可以展开堆栈并在第一个调用帧中找到堆栈的顶部。如何展开堆栈再次特定于使用的调用约定。用当前堆栈指针减去第一个堆栈帧的基数将为您提供堆栈的当前大小。另请记住,每个线程都有自己的调用堆栈,因此您必须从正确线程的堆栈底部减去。

但请记住:

虽然主寄存器(指令指针除外)在 32 位和 64 位版本的指令集中都是“通用”的,可以用于任何事情......

在假设寄存器的用途之前,您应该查看目标平台的手册。

获取剩余/总最大堆栈空间可能有点棘手。在 Linux 中,堆栈大小通常在运行时受到限制。可以通过/proc/ 文件系统或使用系统调用访问限制。在 Windows 中,最大堆栈大小可以在链接时设置,并且应该可以在可执行文件头中访问。

下面是一个在 Linux 上运行的示例程序。我从/proc/<pid>/stat 读取堆栈的开头。我还提供了一个展开示例,为此我使用了一个抽象出所有操作系统/体系结构特定汇编代码的库。堆栈一直展开到main 之前的初始化代码,并考虑了它使用的堆栈空间。

我在第一个调用帧中使用 SP 寄存器而不是 BP 来获取堆栈的底部,因为 BP 在某些体系结构中不存在,并且在我的平台上它在初始化帧中为零。这意味着底部偏离了第一个调用帧的大小,因此只是一个近似值。在coliru 上查看它,不幸的是,访问/proc/<pid>/stat 在那里被拒绝。

#include <iostream>
using namespace std;

#include <fstream>
#include <sstream>
#include <unistd.h>
// read bottom of stack from /proc/<pid>/stat
unsigned long bottom_of_stack() {
    unsigned long bottom = 0;
    ostringstream path;
    path << "/proc/" << getpid() << "/stat";
    ifstream stat(path.str());
    // possibly not the best way to parse /proc/pid/stat
    string ignore;
    if(stat.is_open()) {
        // startstack is the 28th field
        for(int i = 1; i < 28; i++)
            getline(stat, ignore, ' ');
        stat >> bottom;
    }
    return bottom;
}

#include <sys/resource.h>
rlim_t get_max_stack_size() {
    rlimit limits;
    getrlimit(RLIMIT_STACK, &limits);
    return limits.rlim_cur;
}

#define UNW_LOCAL_ONLY
#include <libunwind.h>

// using global variables for conciseness
unw_cursor_t frame_cursor;
unw_context_t unwind_context;

// approximate bottom of stack using SP register and unwinding
unw_word_t appr_bottom_of_stack() {
    unw_word_t bottom;
    unw_getcontext(&unwind_context);
    unw_init_local(&frame_cursor, &unwind_context);
    do {
        unw_get_reg(&frame_cursor, UNW_REG_SP, &bottom);
    } while(unw_step(&frame_cursor) > 0);
    return bottom;
}

// must not inline since that would change behaviour
unw_word_t __attribute__((noinline)) current_sp() {
    unw_word_t sp;
    unw_getcontext(&unwind_context);
    unw_init_local(&frame_cursor, &unwind_context);
    unw_step(&frame_cursor); // step to frame before this function
    unw_get_reg(&frame_cursor, UNW_REG_SP, &sp);
    return sp;
}

// a little helper for absolute delta of unsigned integers
#include <algorithm>
template<class UI>
UI abs_udelta(UI left, UI right) {
    return max(left,right) - min(left,right);
}

unsigned long global_bottom;
rlim_t global_max;

// a test function to grow the call stack
int recurse(int index) {
    if(index < 2 ) {
        auto stack_size = abs_udelta(current_sp(), global_bottom);
        cout << "Current stack size: " << stack_size
             << "\tStack left: " << global_max - stack_size << '\n';
        return index;
    }
    return recurse(index - 1) + recurse(index - 2); // do the fibonacci
}

int main() {
    global_max = get_max_stack_size();
    global_bottom = bottom_of_stack();
    auto appr_bottom = appr_bottom_of_stack();
    cout << "Maximum stack size: "
         << global_max << '\n';
    cout << "Approximate bottom of the stack by unwinding: "
         << (void*)appr_bottom << '\n';
    if(global_bottom > 0) {
        cout << "Bottom of the stack in /proc/<pid>/stat: "
             << (void*)global_bottom << '\n';
        cout << "Approximation error: "
             << abs_udelta(global_bottom, appr_bottom) << '\n';
    } else {
        global_bottom = appr_bottom;
        cout << "Could not parse /proc/<pid>/stat" << '\n';
    }
    // use the result so call won't get optimized out
    cout << "Result of recursion: " << recurse(6);
}

输出:

Maximum stack size: 8388608
Approximate bottom of the stack by unwinding: 0x7fff64570af8
Bottom of the stack in /proc/<pid>/stat: 0x7fff64570b00
Approximation error: 8
Current stack size: 640 Stack left: 8387968
Current stack size: 640 Stack left: 8387968
Current stack size: 576 Stack left: 8388032
Current stack size: 576 Stack left: 8388032
Current stack size: 576 Stack left: 8388032
Current stack size: 576 Stack left: 8388032
Current stack size: 576 Stack left: 8388032
Current stack size: 512 Stack left: 8388096
Current stack size: 576 Stack left: 8388032
Current stack size: 576 Stack left: 8388032
Current stack size: 512 Stack left: 8388096
Current stack size: 512 Stack left: 8388096
Current stack size: 512 Stack left: 8388096
Result of recursion: 8

【讨论】:

  • 从 SP 中减去 BP 会得到当前堆栈帧的大小,但我认为 OP 对所有堆栈帧的总大小感兴趣。
  • @RussellReed 是的,你是对的。他需要展开堆栈以找到整个堆栈的大小。
【解决方案2】:

【讨论】:

  • 我很欣赏这些链接,但由于信息被隐藏在外部来源后面,我很犹豫是否支持。
  • 没关系。这是您需要在初始阶段独自旅行的旅程。请注意,如果您启用了优化,您的堆栈跟踪可能已被优化掉,因此您通常只会在运行调试构建时找到您期望的内容。
【解决方案3】:

堆栈

在现代平台中,堆栈结构被简化为使用指向下一项应该插入的位置的指针。大多数平台都使用寄存器来执行此操作。

+---+  
|   |  
+---+  
|   |  
+---+  
|   |  
+---+  
|   |  <-- Next available stack slot
+---+  

在堆栈指针的当前位置写入一项,然后堆栈指针递增。

调用堆栈

需要压入堆栈的最小值是调用后的返回地址或下一条指令的地址。这和你得到的一样普遍。压入堆栈的任何其他内容取决于编译器或操作系统设置的协议。

例如,编译器我选择将参数放入寄存器而不是将它们压入堆栈。

参数的顺序最终取决于编译器(和语言)。编译器可以先压入最左边的值,也可以先压入最后一个参数。

堆栈起始地址

堆栈起始地址通常由操作系统或嵌入式系统在固定位置确定。不能保证操作系统会在每次调用时将程序的堆栈放置在相同的位置。

堆栈大小

这里有两个术语:容量内容。堆栈大小可以指堆栈中的元素个数(内容)或堆栈可以容纳的容量。

这里没有固定的通用限制。平台不同。通常操作系统会参与分配堆栈的容量。在许多平台上,操作系统不会检查您是否超出了容量。

某些操作系统提供了一些机制,以便可执行文件可以调整堆栈的容量。大多数操作系统提供商通常提供平均数量。

与堆共享内存

一个常见的设置是让堆栈向堆增长。因此,分配进程的所有额外内存,例如,堆栈从额外内存的开头向下增长,而堆从底部向上分配。这允许使用很少动态内存的程序拥有更多的堆栈空间,而那些使用很少的堆栈空间的程序拥有更多的动态内存。最大的问题是他们什么时候交叉。没有通知,只是未定义的行为。

访问堆栈信息

大多数时候,我从不查看堆栈指针或寄存器的值。这通常用于内存容量受限的性能关键系统(如嵌入式系统)。

堆栈通常由 调试器 审查以提供调用跟踪。

【讨论】:

  • “需要压入堆栈的最小值是调用后下一条指令的返回地址或地址”:这不是真的(并且已经有 30 年没有了)。 ARM 处理器不需要这个。
【解决方案4】:

这是一种粗略估计当前调用堆栈大小的方法。

在 main() 中,将 argc 的地址保存到一个全局变量中。这有点接近你的堆栈开始的地方。然后,当您想检查当前大小时,将第一个参数的地址传递给当前函数,然后从保存的值中减去它。如果您在 main() 或当前函数的自动变量中有大量数据(我不确定是哪一个 - 它可能因平台而异),这将不太准确。

可能存在堆栈通过链接块动态增长的情况,在这种情况下,这种技术将不准确。

在多线程程序中,您必须分别跟踪每个线程堆栈的开始。与main类似,在线程启动时获取顶级线程函数的参数之一的地址。

我希望我曾想过将链接保存到我发现有人在做类似事情的页面。他们得到了一个自动变量的地址,而不是一个参数,这仍然会给出非常相似的结果。

【讨论】:

  • 我正在阅读的另一页指出 valgrind 的 massif 工具可以跟踪堆栈空间的使用情况。 Link
  • 这是一个相关的 Stack Overflow 问题:58614。它建议使用 pthread_attr_getstackaddr 函数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-06-28
  • 1970-01-01
  • 1970-01-01
  • 2015-02-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多