关于Visual Studio:在捕获到C ++之后发现异常的源头了吗?

关于Visual Studio:在捕获到C ++之后发现异常的源头了吗?

Finding out the source of an exception in C++ after it is caught?

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

调试大型C ++应用程序时,不幸的是C ++异常的用法非常广泛。 有时,我比真正想要的晚了一点。

伪代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FunctionB()
{
    ...
    throw e;
    ...
}

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

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

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

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

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

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


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

1
2
3
4
5
6
7
#define throwException(message) \
    {                           \
        std::ostringstream oss; \
        oss << __FILE __ <<"" << __LINE__ <<""  \
           << __FUNC__ <<"" << message; \
        throw std::exception(oss.str().c_str()); \
    }

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

然后使用抛出异常

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

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


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

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


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


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

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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,因为我想从堆栈跟踪中分离出异常的构造函数。


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

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


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


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

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


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

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

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


到目前为止,距提出此问题已有11年了,今天,我们可以仅使用标准C ++ 11(即跨平台)解决此问题,而无需调试器或繁琐的日志记录。
您可以跟踪导致异常的调用堆栈

使用std::nested_exceptionstd::throw_with_nested

这不会给您带来很多麻烦,但我认为下一件好事。
它在此处和此处的StackOverflow上进行了描述,如何通过简单地编写适当的异常处理程序(该异常处理程序将重新抛出嵌套的异常)来获得对代码内异常的追溯,而无需调试程序或繁琐的日志记录。

但是,这将要求您在要跟踪的函数中插入try / catch语句(即,没有此功能的函数将不会出现在跟踪中)。
您可以使用宏将其自动化,从而减少您必须编写/更改的代码量。

由于您可以使用任何派生的异常类执行此操作,因此可以向此类回溯中添加很多信息!
您还可以在GitHub上查看我的MWE,回溯显示如下:

1
2
3
4
Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file"nonexistent.txt"


我有自己的例外。您可以非常简单地处理它们-它们还包含文本。我使用以下格式:

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

我还有第二种例外格式:

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

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


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

阿尔特姆写道:

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

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

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

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

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


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


推荐阅读