C++ 异常与调用堆栈





5.00/5 (5投票s)
C++23 异常和 C++ Windows 异常与调用栈。
目录
示例代码托管在 Github 上。
我开发了两个 C++ 异常类,它们可以极大地帮助排查客户问题,这些问题在开发人员的系统上无法重现,并且调用了一个公共函数,但由于调用者众多而不知道是谁调用的。必须向客户提供一个可执行文件和一个与该可执行文件一起构建的 pdb 文件,才能获得正确的调用堆栈。pdb 文件包含调试符号。你不能提供稍后从相同代码构建的 pdb 文件。当构建可执行文件时,每个函数都会被赋予一个地址偏移量。pdb 是基于这些偏移量构建的。当再次构建二进制文件时,偏移量会发生变化。简而言之,pdb 的函数偏移量必须与可执行文件匹配。
带有调用堆栈的 C++23 异常类
今年通过的标准 C++23 引入了一个 stacktrace
类,用于检索调用堆栈跟踪。stacktrace
可以在异常期间的故障排除中提供帮助。因此,我从 std::runtime_error
派生了 Cpp23ExceptionWithCallstack
,并添加了 stacktrace
作为成员以获取跟踪信息。我对 stacktrace
的使用很大程度上依赖于 Stephan T. Lavavej 的 Godbolt 示例。
以下是如何使用 Cpp23ExceptionWithCallstack
的示例。
#include <iostream>
#include "Cpp23ExceptionWithCallstack.h"
using namespace std;
int inner(const int n)
{
if (n < 0)
{
throw Cpp23ExceptionWithCallstack{"Error"};
}
return n * n;
}
int outer(const int n)
{
return inner(n);
}
int main()
{
try
{
cout << outer(-5) << "\n";
}
catch (const Cpp23ExceptionWithCallstack& except)
{
cout << except.GetCallstack() << std::endl;
}
return 0;
}
调用堆栈输出如下。请确保将可执行文件的 pdb 文件放在同一文件夹中,以在输出中获取函数名称、文件名和行号。
Cpp23CallStack!inner+0x3A, D:\GitHub\cpp_show_callstack\Cpp23CallStack\Cpp23CallStack.cpp(11) Cpp23CallStack!outer+0x29, D:\GitHub\cpp_show_callstack\Cpp23CallStack\Cpp23CallStack.cpp(18) Cpp23CallStack!main+0x37, D:\GitHub\cpp_show_callstack\Cpp23CallStack\Cpp23CallStack.cpp(22) Cpp23CallStack!invoke_main+0x39, D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(79) Cpp23CallStack!__scrt_common_main_seh+0x12E, D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288) Cpp23CallStack!__scrt_common_main+0xE, D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(331) Cpp23CallStack!mainCRTStartup+0xE, D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp(17) KERNEL32!BaseThreadInitThunk+0x1D, unknown(0) ntdll!RtlUserThreadStart+0x28, unknown(0)
带有调用堆栈的 C++ 异常类(仅限 Windows)
如果你没有使用 C++23 怎么办?别担心。我还有另一个 C++ 异常类,CppExceptionWithCallstack
,它基于 StackWalker,由 Jochen Kalmbach 创建,它依赖于 Windows 调试 API 来完成其工作。最初,我在我的 StackWalker
派生类中遇到一个奇怪的问题,即第一次运行时打印了完整的调用堆栈,而在后续运行时仅打印了第一个函数。我调试了这个问题,发现其操作没有问题:它报告在第一个函数之后没有函数调用者。我在 StackWalker GitHub 上提交了此 issue。从那时起,我直接使用 StackWalker
,而不是从它派生,我没有遇到这个问题。如果你遇到相同的问题,请告诉我。
以下是如何使用 CppExceptionWithCallstack
的示例。
#include <iostream>
#include "CppExceptionWithCallstack.h"
using namespace std;
int inner(const int n)
{
if (n < 0)
{
throw CppExceptionWithCallstack{"Error"};
}
return n * n;
}
int outer(const int n)
{
return inner(n);
}
int main()
{
try
{
cout << outer(-5) << "\n";
}
catch (const CppExceptionWithCallstack& except)
{
cout << except.GetCallstack() << std::endl;
}
return 0;
}
这是调用堆栈输出。请注意,如上所述,你必须将可执行文件的 pdb 文件放在程序的文件夹中,才能在输出中获取函数名称、文件名和行号。
D:\GitHub\cpp_show_callstack\WinCallStack\StackWalker.cpp (1140): StackWalker::ShowCallstack D:\GitHub\cpp_show_callstack\WinCallStack\CppExceptionWithCallstack.h (19): CppExceptionWithCallstack::CppExceptionWithCallstack D:\GitHub\cpp_show_callstack\WinCallStack\WinCallStack.cpp (10): inner D:\GitHub\cpp_show_callstack\WinCallStack\WinCallStack.cpp (18): outer D:\GitHub\cpp_show_callstack\WinCallStack\WinCallStack.cpp (24): main D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl (79): invoke_main D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl (288): __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl (331): __scrt_common_main D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp (17): mainCRTStartup 00007FFF7E7226AD (KERNEL32): (filename not available): BaseThreadInitThunk 00007FFF7F14AA68 (ntdll): (filename not available): RtlUserThreadStart
历史
- 2023年8月14日:首次发布