13

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

我知道在使用 execinfo.h 中的函数(参见问题 76822)和通过 Microsoft C++ 实现中的 StackWalk 接口(参见问题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 块是否需要对每个函数调用进行额外的设置和拆卸,或者在编译时以某种方式处理?还是有其他我没有考虑过的问题?

4

7 回答 7

22

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

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

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

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

  • 无需修改功能
  • 陷阱标准或第三方库中的崩溃
  • 无需在每个函数中都尝试/捕获(缓慢且占用大量内存)
于 2009-03-05T21:27:05.970 回答
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;
};

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

于 2009-03-05T21:55:40.927 回答
2

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

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

这样,您对类的使用就与平台无关,如果您想针对其他平台,只需修改该类。

于 2009-03-05T21:25:44.803 回答
1

在调试器中:

要获取异常抛出位置的堆栈跟踪,我只需在 std::exception 构造函数中设置断点。

因此,当创建异常时,调试器停止,然后您可以在该点看到堆栈跟踪。并不完美,但它在大多数情况下都有效。

于 2009-03-06T01:04:59.567 回答
1

堆栈管理是那些很快变得复杂的简单事情之一。最好把它留给专门的图书馆。你试过 libunwind 吗?效果很好,而且 AFAIK 它是可移植的,尽管我从未在 Windows 上尝试过。

于 2012-12-12T13:51:31.333 回答
0

这会更慢,但看起来应该可以工作。

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

于 2009-03-05T21:29:43.717 回答
0

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

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

要求编码器在调用链中注入 catch 和 rethrow 处理在某些平台上具有显着的运行时成本,并且会带来很大的未来维护成本。

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

于 2009-03-05T21:31:11.343 回答