如何在C ++中确定详细的调用堆栈信息?

时间:2014-12-21 00:19:49

标签: c++ memory callstack

我想更好地理解调用堆栈,并且为了做到这一点,我还想再使用它。

我将如何处理此信息?我不知道!虽然学习新东西很有趣,但这是我今天的新事物。

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

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

所以在我的任务中,我想弄清楚如何做到以下几点:

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

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

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

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

    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 ++程序员,我们喜欢按下那些不要碰的大红色按钮。”

    我意识到有很多关于call-stack的问题,我相信我的有意义和独特。

    关于call-stack主题的其他问题不会以与我在这里要求的方式相同的方式询问调用堆栈。

4 个答案:

答案 0 :(得分:1)

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

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

举个例子,现在快速浏览一下常见x86架构的wikipedia页面

  

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

     

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

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

但请记住:

  

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

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

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

以下是适用于Linux的示例程序。我从/proc/<pid>/stat读取了堆栈的开头。我还提供了展开的示例,为此我使用了一个库来抽象出所有特定于OS /体系结构的汇编代码。堆栈在main之前一直展开到初始化代码,并且会占用它所使用的堆栈空间。

我使用SP寄存器代替BP来获取fisrt调用帧中的堆栈底部,因为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

答案 1 :(得分:0)

答案 2 :(得分:0)

堆栈

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

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

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

CALL堆栈

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

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

参数的顺序最终取决于编译器(和语言)。编译器可以先将最左边的值或最后一个参数按下。

堆栈起始地址

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

堆栈大小

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

此处没有固定的共同限制。平台不同。通常,OS涉及分配堆栈的容量。在许多平台上,操作系统都不会检查您是否超出了容量。

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

与堆共享内存

常见的设置是让堆栈朝向堆增长。因此,进程的所有额外内存都被分配,以便例如,堆栈从额外内存开始向下增长,堆从底部向上分配。这允许使用少量动态内存的程序具有更多堆栈空间,并且允许使用很少堆栈空间的程序具有更多动态内存。最重要的问题是他们何时交叉。没有通知,只是未定义的行为。

访问堆栈信息

大多数情况下,我从不查看堆栈指针或寄存器的值。这通常是在具有受限内存容量的性能关键系统上(如嵌入式系统)。

通常由调试器检查堆栈以提供呼叫跟踪。

答案 3 :(得分:0)

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

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

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

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

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