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

13

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

我知道当使用execinfo.h中的函数(请参见问题76822)构建针对glibc时,可以使用此数据,并通过Microsoft的C ++实现中的StackWalk接口(请参见问题126450)。不过,我非常想避免使用任何不具备可移植性的东西。

我正在考虑以以下形式自己实现此功能:

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个回答

22

我认为这是一个非常糟糕的想法。

可移植性是一个非常值得追求的目标,但当结果是侵入性、降低性能和实现较差时,就不再是一个好的解决方案了。

我所用过的每个平台(Windows/Linux/PS2/iPhone等)都提供了一种在异常发生时遍历堆栈并将地址匹配到函数名称的方法。是的,它们都不是可移植的,但报告框架可以是可移植的,并且通常只需要花费一两天的时间编写特定于平台的版本的堆栈遍历代码。

这不仅比创建/维护跨平台解决方案所需的时间更少,而且结果更好:

  • 无需修改函数
  • 捕获标准或第三方库中的崩溃
  • 不需要在每个函数中使用 try/catch(速度慢且内存占用高)

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
我认为没有一种“平台无关”的方法来实现这个 - 毕竟,如果有的话,就不需要 StackWalk 或你提到的特殊 gcc 堆栈跟踪功能了。
虽然有点混乱,但我会实现一个类,提供一个一致的接口来访问堆栈跟踪,然后在实现中使用 #ifdefs 来使用适当的平台特定方法来实际组合堆栈跟踪。
这样,您对该类的使用是平台无关的,只需修改该类即可针对其他平台进行目标定位。

1
在调试器中:
为了获取异常抛出的堆栈跟踪,我只需在 std::exception 构造函数中设置断点。
因此,当异常被创建时,调试器会停止,然后您可以看到该点的堆栈跟踪。虽然不完美,但大多数情况下都有效。

1
他并没有要求调试器指令。 - Jonas Byström
1
这对调试没有帮助吗? - Martin York

1

堆栈管理是那些简单事情之一,很快就会变得复杂。最好留给专门的库来处理。你尝试过libunwind吗?它运行良好,据我所知它是可移植的,虽然我从未在Windows上尝试过。


0

这样做会更慢,但看起来应该能工作。

据我所知,实现快速、可移植的堆栈跟踪的问题在于堆栈实现既与操作系统有关,又与 CPU 有关,因此它隐含地是一个特定于平台的问题。另一种选择是使用 MS/glibc 函数,并使用 #ifdef 和适当的预处理器定义(如 _WIN32)在不同构建中实现特定于平台的解决方案。


0

由于堆栈使用高度依赖于平台和实现,没有办法直接做到完全可移植。但是,您可以构建一个可移植的接口到特定平台和编译器的实现,尽可能地本地化问题。在我看来,这将是您最好的方法。

追踪器实现将链接到任何可用的平台特定辅助库。仅当发生异常时它才会操作,即使这样,它也只有在从catch块中调用它时才会操作。它的最小API将简单地返回包含整个跟踪的字符串。

要求程序员在调用链中注入catch和rethrow处理在某些平台上具有显着的运行时成本,并且强加了很大的未来维护成本。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接