异常上的可移植C ++堆栈跟踪

时间:2009-03-05 21:14:59

标签: c++ portability stack-trace

我正在写一个我想要移植的库。因此,它不应该依赖于glibc或Microsoft扩展或标准中没有的任何其他内容。我有一个很好的从std :: exception派生的类层次结构,我用它来处理逻辑和输入中的错误。知道在特定文件和行号处抛出特定类型的异常是有用的,但是知道执行的执行方式可能会更有价值,所以我一直在寻找获取堆栈跟踪的方法。

我知道这个数据在使用execinfo.h中的函数(参见question 76822)和Microsoft的C ++实现中的StackWalk接口(参见question 126450)构建glibc时可用,但是我非常想避免任何不便携的东西。

我正在考虑以这种形式自己实现这个功能:

class myException : public std::exception
{
public:
  ...
  void AddCall( std::string s )
  { m_vCallStack.push_back( s ); }
  std::string ToStr() const
  {
    std::string l_sRet = "";
    ...
    l_sRet += "Call stack:\n";
    for( int i = 0; i < m_vCallStack.size(); i++ )
      l_sRet += "  " + m_vCallStack[i] + "\n";
    ...
    return l_sRet;
  }
private:
  ...
  std::vector< std::string > m_vCallStack;
};

ret_type some_function( param_1, param_2, param_3 )
{
  try
  {
    ...
  }
  catch( myException e )
  {
    e.AddCall( "some_function( " + param_1 + ", " + param_2 + ", " + param_3 + " )" );
    throw e;
  }
}

int main( int argc, char * argv[] )
{
  try
  {
    ...
  }
  catch ( myException e )
  {
    std::cerr << "Caught exception: \n" << e.ToStr();
    return 1;
  }
  return 0;
}

这是一个糟糕的主意吗?这意味着在每个函数中添加try / catch块会有很多工作,但我可以忍受。当异常的原因是内存损坏或内存不足时,它将无法工作,但在那时你无论如何都要搞砸了。如果堆栈中的某些函数没有捕获异常,将自己添加到列表中并重新抛出,它可能会提供误导性信息,但我至少可以保证我的所有库函数都这样做。与“真正的”堆栈跟踪不同,我不会在调用函数中获取行号,但至少我会有一些东西。

我主要担心的是,即使没有实际抛出异常,这也会导致速度减慢。所有这些try / catch块是否需要对每个函数调用进行额外的设置和拆除,或者在编译时以某种方式处理?还是我还没有考虑过其他问题?

7 个答案:

答案 0 :(得分:21)

我认为这是一个非常糟糕的主意。

可移植性是一个非常有价值的目标,但是当它导致一个侵入性,性能损失和低劣实现的解决方案时。

我工作的每个平台(Windows / Linux / PS2 / iPhone /等)提供了一种在发生异常时将堆栈移动并将地址与函数名称匹配的方法。是的,这些都不是可移植的,但报告框架可以是,并且通常需要不到一天或两天的时间来编写特定于平台的堆栈行走代码版本。

这不仅比创建/维护跨平台解决方案的时间短,而且结果要好得多;

  • 无需修改功能
  • 陷阱在标准或第三方库中崩溃
  • 不需要在每个函数中使用try / catch(缓慢且内存密集)

答案 1 :(得分:6)

查询Nested Diagnostic Context一次。这是一个小提示:

class NDC {
public:
    static NDC* getContextForCurrentThread();
    int addEntry(char const* file, unsigned lineNo);
    void removeEntry(int key);
    void dump(std::ostream& os);
    void clear();
};

class Scope {
public:
    Scope(char const *file, unsigned lineNo) {
       NDC *ctx = NDC::getContextForCurrentThread();
       myKey = ctx->addEntry(file,lineNo);
    }
    ~Scope() {
       if (!std::uncaught_exception()) {
           NDC *ctx = NDC::getContextForCurrentThread();
           ctx->removeEntry(myKey);
       }
    }
private:
    int myKey;
};
#define DECLARE_NDC() Scope s__(__FILE__,__LINE__)

void f() {
    DECLARE_NDC(); // always declare the scope
    // only use try/catch when you want to handle an exception
    // and dump the stack
    try {
       // do stuff in here
    } catch (...) {
       NDC* ctx = NDC::getContextForCurrentThread();
       ctx->dump(std::cerr);
       ctx->clear();
    }
}

开销是在NDC的实施中。我正在玩一个懒惰的评估版本以及一个只保留固定数量的条目的版本。关键是如果你使用构造函数和析构函数来处理堆栈,那么你就不需要所有那些讨厌的try / catch块和任何地方的显式操作。

唯一的平台特定问题是getContextForCurrentThread()方法。您可以使用线程本地存储来使用特定于平台的实现来处理大多数情况下的作业。

如果您更注重性能并且生活在日志文件的世界中,那么更改范围以保存指向文件名和行号的指针并完全省略NDC:

class Scope {
public:
    Scope(char const* f, unsigned l): fileName(f), lineNo(l) {}
    ~Scope() {
        if (std::uncaught_exception()) {
            log_error("%s(%u): stack unwind due to exception\n",
                      fileName, lineNo);
        }
    }
private:
    char const* fileName;
    unsigned lineNo;
};

当抛出异常时,这将在日志文件中为您提供一个很好的堆栈跟踪。不需要任何真正的堆栈遍历,只是在抛出异常时只需要一条日志消息;)

答案 2 :(得分:2)

我认为没有一种“独立于平台”的方式 - 毕竟,如果有的话,就不需要StackWalk或你提到的特殊gcc堆栈跟踪功能。

这会有点混乱,但我实现这个的方法是创建一个类来提供访问堆栈跟踪的一致接口,然后在实现中使用#ifdefs使用适当的特定于平台的方法来实际上把堆栈跟踪放在一起。

这样你对类的使用是独立于平台的,如果你想要定位其他平台,那么只需修改那个类。

答案 3 :(得分:1)

在调试器中:

要获取抛出异常的堆栈跟踪,我只是stcik std :: exception构造函数中的断点。

因此,当创建异常时,调试器停止,然后您可以在该点看到堆栈跟踪。不完美,但大部分时间都有效。

答案 4 :(得分:1)

堆栈管理是很快变得复杂的简单事情之一。最好留给专业图书馆。你试过libunwind吗?虽然我从来没有在Windows上试过它,但它很好用且AFAIK可以移植。

答案 5 :(得分:0)

这会慢一点,但看起来应该可以。

根据我的理解,制作快速,可移植的堆栈跟踪的问题是堆栈实现是OS和CPU特定的,因此它隐含地是特定于平台的问题。另一种方法是使用MS / glibc函数并使用#ifdef和适当的预处理器定义(例如_WIN32)来实现不同构建中的特定于平台的解决方案。

答案 6 :(得分:0)

由于堆栈使用是高度平台和依赖于实现的,因此无法直接进行完全可移植的堆栈。但是,您可以构建一个平台和编译器特定实现的可移植接口,尽可能地将问题本地化。恕我直言,这将是你最好的方法。

然后,跟踪器实现将链接到可用的任何特定于平台的帮助程序库。然后它将仅在发生异常时运行,即使这样,只有在从catch块调用它时才会运行。它的最小API只会返回一个包含整个跟踪的字符串。

要求编码人员在调用链中注入catch和rethrow处理会在某些平台上产生大量的运行时成本,并且会产生很大的未来维护成本。

也就是说,如果您确实选择使用catch / throw机制,请不要忘记即使C ++仍然可以使用C预处理器,并且定义了宏__FILE____LINE__。您可以使用它们在跟踪信息中包含源文件名和行号。