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

我知道使用 execinfo.h 中的函数针对 glibc 构建时可以使用此数据(请参阅 问题 76822)并通过 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 块是否都需要在每次函数调用时进行额外的设置和拆卸,或者在编译时以某种方式进行处理?还是还有其他我没有考虑到的问题?

有帮助吗?

解决方案

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

可移植性是一个非常有价值的目标,但当它导致解决方案具有侵入性、降低性能和实施质量较差时,就不是一个目标了。

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

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

  • 无需修改功能
  • 标准或第三方库中的陷阱崩溃
  • 无需在每个函数中都进行 try/catch(速度慢且占用内存)

其他提示

查找嵌套诊断上下文一次。这是一个小提示:

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的实施中。我正在玩一个懒惰的评估版本以及一个只保留固定数量的条目的版本。关键点在于,如果使用构造函数和析构函数来处理堆栈,那么就不需要所有那些讨厌的尝试 / catch 块和在任何地方进行显式操作。 / p>

唯一特定于平台的问题是 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;
};

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

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

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

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

在调试器中:

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

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

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

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

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

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

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top