C++中捕获异常后找出其来源?
-
09-06-2019 - |
题
我正在 MS VC++ 中寻找答案。
在调试大型 C++ 应用程序时,不幸的是,该应用程序大量使用了 C++ 异常。有时我捕获异常的时间比我实际想要的要晚一些。
伪代码示例:
FunctionB()
{
...
throw e;
...
}
FunctionA()
{
...
FunctionB()
...
}
try
{
Function A()
}
catch(e)
{
(<--- breakpoint)
...
}
我可以在调试时使用断点捕获异常。但我无法追溯异常是否发生在 FunctionA()
或者 FunctionB()
, ,或其他一些功能。(假设广泛的异常使用和上述示例的巨大版本)。
解决我的问题的一种方法是确定并保存调用堆栈 在异常构造函数中 (IE。在被抓住之前)。但这需要我从这个异常基类派生所有异常。它还需要大量代码,并且可能会减慢我的程序速度。
有没有一种更简单、工作量更少的方法?无需更改我的大型代码库?
其他语言有更好的解决方案吗?
解决方案
如果您只对异常来自哪里感兴趣,您可以编写一个简单的宏,例如
#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!");
其他提示
您指向代码中的断点。由于您处于调试器中,因此可以在异常类的构造函数上设置断点,或者将 Visual Studio 调试器设置为在所有引发的异常上中断(“调试”->“异常”,单击“C++ 异常”,选择“引发”和“未捕获”选项)
John Robbins 写了一本很棒的书,解决了许多困难的调试问题。这本书叫 调试 Microsoft .NET 和 Microsoft Windows 的应用程序. 。尽管有这个标题,本书还是包含了大量有关调试本机 C++ 应用程序的信息。
在本书中,有一个很长的部分是关于如何获取抛出的异常的调用堆栈。如果我没记错的话,他的一些建议涉及使用 结构化异常处理 (SEH) 代替(或补充)C++ 异常。我真的极力推荐这本书。
在异常对象构造函数中放置一个断点。您将在引发异常之前获得断点。
以下是我使用 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
因为我喜欢从堆栈跟踪中切掉异常的构造函数。
没有标准方法可以做到这一点。
此外,通常必须在异常发生时记录调用堆栈 抛出;一旦它已经 捕捉 堆栈已经展开,因此您不再知道被抛出时发生了什么。
在 Win32/Win64 上的 VC++ 中,您 可能 通过记录编译器内部函数 _ReturnAddress() 的值并确保异常类构造函数为 __declspec(noinline) 来获得足够可用的结果。结合调试符号库,我认为您可能可以使用 SymGetLineFromAddr64 获取与返回地址相对应的函数名称(以及行号,如果您的 .pdb 包含它)。
在本机代码中,您可以通过安装一个来尝试遍历调用堆栈 向量异常处理程序. 。VC++ 在 SEH 异常之上实现了 C++ 异常,并且在任何基于帧的处理程序之前首先给出向量异常处理程序。然而要非常小心,向量异常处理引入的问题可能很难诊断。
还 迈克·斯托尔有一些警告 关于在具有托管代码的应用程序中使用它。最后,阅读 马特·皮特雷克的文章 并确保您在尝试此操作之前了解 SEH 和向量异常处理。(没有什么比跟踪您添加的代码中的关键问题来帮助跟踪关键问题更糟糕的了。)
我相信 MSDev 允许您在引发异常时设置断点。
或者将断点放在异常对象的构造函数上。
如果您从 IDE 进行调试,请转到“调试”->“异常”,单击“抛出 C++ 异常”。
其他语言?好吧,在 Java 中你可以调用 e.printStackTrace();没有比这更简单的了。
如果有人感兴趣,一位同事通过电子邮件回复了我这个问题:
阿尔乔姆写道:
MiniDumpWriteDump() 有一个标志可以更好地进行故障转储,从而允许查看完整的程序状态,以及所有全局变量等。至于调用堆栈,我怀疑它们会因为优化而变得更好......除非您关闭(也许某些)优化。
另外,我认为禁用内联函数和整个程序优化会有很大帮助。
事实上,转储类型有很多种,也许你可以选择一种足够小的但仍然有更多信息的http://msdn.microsoft.com/en-us/library/ms680519(VS.85).aspx
不过,这些类型对调用堆栈没有帮助,它们只会影响您能够看到的变量数量。
我注意到我们使用的 dbghelp.dll 版本 5.1 不支持其中一些转储类型。不过,我们可以将其更新到最新的 6.9 版本,我刚刚检查了 MS 调试工具的 EULA - 最新的 dbghelp.dll 仍然可以重新分发。
我使用我自己的例外。您可以非常简单地处理它们 - 它们也包含文本。我使用的格式:
throw Exception( "comms::serial::serial( )", "Something failed!" );
我还有第二种异常格式:
throw Exception( "comms::serial::serial( )", ::GetLastError( ) );
然后使用 FormatMessage 将其从 DWORD 值转换为实际消息。使用 where/what 格式将显示发生了什么以及在什么函数中发生。