12

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

在调试大型 C++ 应用程序时,不幸的是,它大量使用了 C++ 异常。有时我比我真正想要的晚一点才发现异常。

伪代码示例:

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

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

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

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

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

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

其他语言有没有更好的解决这个问题的方法?

4

14 回答 14

13

您指向了代码中的断点。由于您在调试器中,您可以在异常类的构造函数上设置断点,或设置 Visual Studio 调试器以中断所有抛出的异常(调试->异常单击 C++ 异常,选择已抛出和未捕获的选项)

于 2008-08-30T18:08:51.967 回答
12

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

#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!");
于 2008-09-12T23:15:34.693 回答
7

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

在这本书中,有一个很长的部分是关于如何获取抛出异常的调用堆栈的。如果我没记错的话,他的一些建议涉及使用结构化异常处理(SEH)而不是(或除了)C++ 异常。我真的不能高度推荐这本书。

于 2008-08-30T17:00:19.590 回答
5

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

于 2008-10-24T16:44:20.230 回答
4

捕获后无法找出异常的来源,除非您在抛出异常时包含该信息。当您捕捉到异常时,堆栈已经展开,并且无法重建堆栈的先前状态。

您建议在构造函数中包含堆栈跟踪是您最好的选择。是的,在构建过程中会花费时间,但您可能不应该经常抛出异常,以至于这是一个问题。让你的所有异常都继承自一个新的基础也可能超出你的需要。您可以简单地继承相关异常(谢谢,多重继承),并为这些异常单独捕获。

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

于 2008-08-30T16:37:53.433 回答
2

以下是我在 C++ 中使用 GCC 库的方法:

#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,因为我喜欢从堆栈跟踪中切掉异常的构造函数。

于 2008-08-30T18:14:57.087 回答
1

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

此外,调用堆栈通常必须在抛出异常时记录;一旦它被抓住,堆栈就会展开,所以你不再知道被抛出时发生了什么。

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

于 2008-08-30T16:25:36.177 回答
1

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

Mike Stall 也有一些关于在托管代码的应用程序中使用它的警告。最后,阅读Matt Pietrek 的文章并确保在尝试之前了解 SEH 和向量异常处理。(没有什么比追踪关键问题更糟糕的了,你添加的代码有助于追踪关键问题。)

于 2008-09-01T22:07:04.123 回答
1

如果您从 IDE 进行调试,请转到 Debug->Exceptions,单击 Thrown for C++ exceptions。

于 2008-09-05T20:40:04.970 回答
1

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

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

于 2008-10-24T17:16:19.293 回答
0

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

于 2008-08-30T16:22:23.627 回答
0

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

阿尔乔姆写道:

MiniDumpWriteDump() 有一个标志可以做更好的崩溃转储,这将允许查看完整的程序状态,所有全局变量等。至于调用堆栈,我怀疑它们会因为优化而变得更好......除非你转(也许一些)优化关闭。

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

事实上,有很多转储类型,也许你可以选择一个足够小的但仍然有更多信息 http://msdn.microsoft.com/en-us/library/ms680519(VS.85).aspx

但是,这些类型对调用堆栈没有帮助,它们只会影响您可以看到的变量数量。

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

于 2008-08-30T17:23:45.803 回答
0

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

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

我还有第二种异常格式:

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

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

于 2008-08-30T23:27:36.693 回答
0

到目前为止,这个问题被问到已经 11 年了,今天,我们可以只使用标准 C++11来解决这个问题,即跨平台并且不需要调试器或繁琐的日志记录。您可以跟踪导致异常的调用堆栈

使用std::nested_exceptionstd::throw_with_nested

这不会给你一个堆栈放松,但在我看来是下一个最好的事情。它在此处此处的 StackOverflow 上进行了描述,如何通过简单地编写一个将重新抛出嵌套异常的适当异常处理程序来获取代码中异常的回溯,而无需调试器或繁琐的日志记录。

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

由于您可以对任何派生的异常类执行此操作,因此您可以向此类回溯添加大量信息!你也可以看看我在 GitHub 上的 MWE,回溯看起来像这样:

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"
于 2019-12-24T15:39:36.487 回答