2

我正在用 C++ 为我正在使用的一些代码编写这个错误处理程序。我希望能够对堆栈上的任何内容进行某种引用,而无需将其明确传递给我。具体来说,假设我想按顺序打印调用堆栈上的函数名称。这在像 JVM 这样的托管运行时环境中是微不足道的,对于“简单”的编译代码可能不是那么微不足道。我可以这样做吗?

笔记:

  • 为简单起见,假设我用调试信息编译我的代码并且没有优化。
  • 我想写一些独立于平台或多平台的东西。更喜欢前者。
  • 如果您认为我正在尝试重新发明轮子,只需链接到相关轮子的来源,我就会去那里看看。

更新:

我无法相信你需要向后弯腰才能做到这一点......几乎让我渴望另一种不会被提及的语言。

4

3 回答 3

3

有一种方法可以在 C++ 中获得回溯,尽管它不可​​移植。我不能代表 Windows,但在类 Unix 系统上,有一个回溯 API,主要由以下函数组成:

  • int backtrace(void** array, int size);
  • char** backtrace_symbols(void* const* array, int size);
  • void backtrace_symbols_fd(void* const* array, int size, int fd);

您可以在 GNU 网站上找到最新的文档和示例。还有其他来源,例如OS X 的手册页等。

请记住,使用此 API 获取回溯存在一些问题。首先,没有文件名和行号。其次,在某些情况下,您甚至无法获得回溯,例如完全省略帧指针(x86_64 平台的最新 GCC 编译器的默认行为)。或者二进制文件可能没有任何调试符号。在某些系统上,您还必须-rdynamic在编译二进制文件时指定标志(它具有其他可能不受欢迎的效果)。

于 2013-08-07T15:13:27.253 回答
1

不幸的是,标准 C++ 没有内置的方法来做到这一点。您可以构建一个类系统来帮助您构建堆栈跟踪器实用程序,但您需要在要跟踪的每个方法中放置一个特殊的宏。

我已经看到它使用下面概述的策略完成(甚至实现了部分):

  • 定义您自己的类来存储有关堆栈帧的信息。每个节点至少应该包含被调用函数的名称,文件名/行号信息紧随其后。
  • 栈帧节点存储在链表中,存在则复用,不存在则创建
  • 通过实例化一个特殊对象来创建堆栈帧并将其添加到列表中。Object 的构造函数将帧节点添加到列表中;对象的析构函数从列表中删除节点。
  • 相同的构造函数/析构函数对负责在线程本地存储中创建帧列表,并删除它创建的列表
  • 特殊对象的构造由宏处理。该宏使用特殊的预处理器标记将函数标识和位置信息传递给帧创建者对象。

这是这种方法的一个相当简单的概念验证实现:

#include <iostream>
#include <list>

using namespace std;

struct stack_frame {
    const char *funName;
    const char *fileName;
    int line;
    stack_frame(const char* func, const char* file, int ln)
    : funName(func), fileName(file), line(ln) {}
};

thread_local list<stack_frame> *frames = 0;

struct entry_exit {
    bool delFrames;
    entry_exit(const char* func, const char* file, int ln) {
        if (!frames) {
            frames = new list<stack_frame>();
            delFrames = true;
        } else {
            delFrames = false;
        }
        frames->push_back(stack_frame(func, file, ln));
    }
    ~entry_exit() {
        frames ->pop_back();
        if (delFrames) {
            delete frames;
            frames = 0;
        }
    }
};

void show_stack() {
    for (list<stack_frame>::const_iterator i = frames->begin() ; i != frames->end() ; ++i) {
        cerr << i->funName << " - " << i->fileName << " (" << i->line << ")" << endl;
    }
}

#define FUNCTION_ENTRY entry_exit _entry_exit_(__func__, __FILE__, __LINE__);

void foo() {
    FUNCTION_ENTRY;
    show_stack();
}
void bar() {
    FUNCTION_ENTRY;
    foo();
}
void baz() {
    FUNCTION_ENTRY;
    bar();
}

int main() {
        baz();
        return 0;
}

上面的代码用 C++11 编译并打印:

baz - prog.cpp (52)
bar - prog.cpp (48)
foo - prog.cpp (44)

没有该宏的函数将在堆栈上不可见。性能关键函数不应该有这样的宏。

这是关于 ideone 的演示

于 2013-08-07T15:20:10.367 回答
0

这不简单。确切的解决方案在很大程度上取决于操作系统和执行环境。

打印堆栈通常并不难,但查找符号可能非常棘手,因为这通常意味着读取调试符号。

另一种方法是使用侵入式方法并向每个函数添加一些“我在哪里”类型的代码(大概是“仅用于调试构建”):

#ifdef DEBUG
struct StackEntry
{
   const char *file;
   const char *func;
   int         line;
   StackEntry(const char *f, const char *fn, int ln) : file(f), func(fn), line(ln) {}
};

std::stack<StackEntry> call_stack;

class FuncEntry
{
   public:
    FuncEntry(const char *file, const char *func, int line)
    {
       StackEntry se(file, func, line);
       call_stack.push_back(se);
    }
    ~FuncEntry()
    {
        call_stack.pop_back();
    }

    void DumpStack()
    {
         for(sp : call_stack)
         {
             cout << sp->file << ":" << sp->line << ": " << sp->func << "\n";
         }
    }
 };


 #define FUNC() FuncEntry(__FILE__, __func__, __LINE__); 
 #else
 #define FUNC()
 #endif


 void somefunction()
 {
     FUNC();
     ... more code here. 
 }

我过去使用过这种技术,但我只是输入了这段代码,它可能无法编译,但我认为它已经足够清晰了。一个主要的好处是您不必将它放在每个功能中 - 只是“重要的”。[您甚至可以FUNC根据不同的调试级别启用或禁用不同类型的宏]。

于 2013-08-07T15:21:32.143 回答