在捕获到C ++后找出异常的源头了吗?

时间:2020-03-05 18:45:28  来源:igfitidea点击:

我正在寻找MS VC ++中的答案。

调试大型C ++应用程序时,不幸的是,该应用程序对C ++异常的使用非常广泛。有时,我比实际想要的晚了一点。

伪代码示例:

FunctionB()
{
    ...
    throw e;
    ...
}

FunctionA()
{
    ...
    FunctionB()
    ...
}

try
{
    Function A()
}
catch(e)
{
    (<--- breakpoint)
    ...
}

调试时,我可以使用断点捕获异常。但是,如果在FunctionA()或者FunctionB()或者其他函数中发生了异常,我将无法追溯。 (假设广泛使用例外情况以及上述示例的巨大版本)。

解决我的问题的一种方法是确定调用堆栈并将其保存在异常构造函数中(即在捕获之前)。但这将要求我从该基本异常类派生所有异常。它还需要大量代码,并且可能会使我的程序变慢。

有没有更简单的方法需要更少的工作?无需更改大型代码库?

是否有其他语言可以更好地解决此问题?

解决方案

回答

其他语言?好吧,在Java中,我们可以调用e.printStackTrace();没有比这更简单的了。

回答

没有标准的方法可以做到这一点。

此外,通常必须在引发异常时记录调用堆栈。一旦被捕获,堆栈就会展开,因此我们不再知道被抛出时发生了什么。

在Win32 / Win64上的VC ++中,通过记录编译器固有的_ReturnAddress()的值并确保异常类构造函数为__declspec(noinline),可能会获得足够的可用结果。结合调试符号库,我认为我们可能可以使用SymGetLineFromAddr64获得与返回地址相对应的函数名称(和行号,如果.pdb包含它)。

回答

在捕获到异常之后,无法找到异常的来源,除非我们在引发异常时将其包括在内。到捕获异常时,堆栈已经解卷,无法重建堆栈的先前状态。

我们最好将堆栈跟踪包括在构造函数中的建议。是的,在构建过程中会花费一些时间,但是我们可能不应该经常抛出异常,这是一个令人担忧的问题。使所有异常都从新的基础继承可能还比我们需要的更多。我们可以简单地继承相关的异常(感谢我们,多重继承),并为这些异常单独捕获。

我们可以使用StackTrace64函数构建跟踪(我相信还有其他方法)。查看本文以获得示例代码。

回答

约翰·罗宾斯(John Robbins)写了一本很棒的书,它解决了许多困难的调试问题。这本书称为Microsoft .NET和Microsoft Windows的调试应用程序。尽管有标题,该书还是包含大量有关调试本机C ++应用程序的信息。

本书中有很长的一节,内容涉及如何获取引发的异常的调用堆栈。如果我没记错的话,他的一些建议涉及使用结构化异常处理(SEH)代替C ++异常(或者除此以外)。我真的不能高度推荐这本书。

回答

如果有人感兴趣,一位同事通过电子邮件向我答复了这个问题:

阿尔特姆写道:

MiniDumpWriteDump()有一个标记可以执行更好的崩溃转储,从而可以查看完整的程序状态以及所有全局变量等。至于调用堆栈,我怀疑它们会因为优化而变得更好...除非我们打开(也许有一些优化)。

另外,我认为禁用内联函数和整个程序优化将有很大帮助。

实际上,转储类型很多,也许我们可​​以选择一种较小的类型,但仍有更多信息
http://msdn.microsoft.com/zh-CN/library/ms680519(VS.85).aspx

这些类型对调用堆栈无济于事,它们只会影响我们将看到的变量数量。

我注意到我们使用的dbghelp.dll版本5.1不支持某些转储类型。我们可以将其更新到最新的6.9版本,但是我刚刚检查了EULA中的MS调试工具-最新的dbghelp.dll仍然可以重新分发。

回答

我们指出了代码中的断点。由于我们在调试器中,因此可以在异常类的构造函数上设置一个断点,或者将Visual Studio调试器设置为在所有引发的异常上均断开(Debug-> Exceptions单击C ++异常,选择throw和unaught选项)

回答

这是我使用GCC库在C ++中执行的操作:

#include <execinfo.h> // Backtrace
#include <cxxabi.h> // Demangling

vector<Str> backtrace(size_t numskip) {
    vector<Str> result;
    std::vector<void*> bt(100);
    bt.resize(backtrace(&(*bt.begin()), bt.size()));
    char **btsyms = backtrace_symbols(&(*bt.begin()), bt.size());
    if (btsyms) {
        for (size_t i = numskip; i < bt.size(); i++) {
            Aiss in(btsyms[i]);
            int idx = 0; Astr nt, addr, mangled;
            in >> idx >> nt >> addr >> mangled;
            if (mangled == "start") break;
            int status = 0;
            char *demangled = abi::__cxa_demangle(mangled.c_str(), 0, 0, &status);

            Str frame = (status==0) ? Str(demangled, demangled+strlen(demangled)) : 
                                      Str(mangled.begin(), mangled.end());
            result.push_back(frame);

            free(demangled);
        }
        free(btsyms);
    }
    return result;
}

异常的构造函数可以简单地调用此函数并存储堆栈跟踪。它采用参数numskip是因为我想从堆栈跟踪中分离出异常的构造函数。

回答

我有自己的例外。我们可以很简单地处理它们,它们也包含文本。我使用以下格式:

throw Exception( "comms::serial::serial( )", "Something failed!" );

我还有第二种例外格式:

throw Exception( "comms::serial::serial( )", ::GetLastError( ) );

然后使用FormatMessage将其从DWORD值转换为实际消息。使用where / what格式将向我们显示发生了什么以及在什么功能中发生。

回答

在本机代码中,我们可以通过安装Vectored Exception处理程序来了解如何遍历调用栈。 VC ++在SEH异常的基础上实现了C ++异常,并且在任何基于帧的处理程序之前都首先提供了矢量异常处理程序。但是要非常小心,矢量异常处理引入的问题可能很难诊断。

另外,迈克·斯托尔(Mike Stall)对于在具有托管代码的应用程序中使用它也有一些警告。最后,请阅读Matt Mattretrek的文章,并在尝试之前确保我们了解SEH和向量化异常处理。 (没有什么比跟踪关键问题到添加的帮助跟踪关键问题还糟的多了。)

回答

如果从IDE进行调试,请转到"调试"->"异常",单击"引发"以获取C ++异常。

回答

如果我们只对异常的来源感兴趣,则可以编写一个简单的宏,例如

#define throwException(message) \
    {                           \
        std::ostringstream oss; \
        oss << __FILE __ << " " << __LINE__ << " "  \
           << __FUNC__ << " " << message; \
        throw std::exception(oss.str().c_str()); \
    }

它将在异常文本中添加文件名,行号和函数名(如果编译器提供了相应的宏)。

然后使用抛出异常

throwException("An unknown enum value has been passed!");

回答

在异常对象构造函数中放置一个断点。在引发异常之前,我们将获得断点。

回答

我相信MSDev允许我们在引发异常时设置断点。

或者,将断点放在异常对象的构造函数上。