我正在努力解决堆栈溢出异常,该异常在重新引发不同的异常时发生。重新抛出的异常用于在递归函数调用自身超过一定次数后拆除调用堆栈。(为了防止发生堆栈溢出)
我设法编写了一个小程序,它重现了这个问题。它只发生在我使用 x64(发布和调试)编译程序时。
我用 MSVS2012 & MSVS2013 (StackSize=1MB - 默认) 测试了这个片段。对于 G++,在 about5000
调用之后会出现同样的问题。
代码:
#include <iostream>
using namespace std;
void recursiveFunction(int childCalls) {
cout << "Recursive call, left calls: " << childCalls << endl;
if (childCalls == 0) {
cout << "Throwing std::exception" << endl;
throw std::exception("Target depth reached");
}
try {
recursiveFunction(childCalls - 1);
} catch (std::exception&) {
cout << "Caught exception at level: " << childCalls << endl;
throw; //Simply rethrow exception
}
}
int main() {
//How many calls cause a stack overflow during unwinding with x64
const int calls = 120;
//How many recursive calls I can make before the call stack overflows due to recursive calls
//const int calls = 10600;
cout << "Initiating " << calls << " recursive calls" << endl;
recursiveFunction(calls);
}
该程序的输出是:
Initiating 120 recursive calls
Recursive call, left calls: 120
Recursive call, left calls: 119
Recursive call, left calls: 118
...
Recursive call, left calls: 2
Recursive call, left calls: 1
Recursive call, left calls: 0
Throwing std::exception
Caught exception at level: 1
Caught exception at level: 2
Caught exception at level: 3
...
Caught exception at level: 104
Caught exception at level: 105 <-- Stack overflow here!!!
我没想到的是堆栈溢出发生在调用堆栈大部分被清除并且只剩下 15 个调用帧需要清理的时候。似乎重新抛出异常会分配堆栈空间而不是释放它。
当我使用 WinDBG 调试程序并在堆栈溢出时打开堆栈帧 ( knf
) 时,我得到以下图片:
0:000> knf
# Memory Child-SP RetAddr Call Site
00 00000065`d5c37260 00007ffc`5ac10658 MSVCR110D!_chkstk+0x37
01 18 00000065`d5c37278 00007ffc`5ac105bf MSVCR110D!write_nolock+0x18
02 8 00000065`d5c37280 00007ffc`5ab23db1 MSVCR110D!write+0x21f
...
0a f0 00000065`d5c37710 00007ffc`5ac09150 Crashtest!`recursiveFunction'::`1'::catch$0+0x26
0b 40 00000065`d5c37750 00007ffc`5abf93f2 MSVCR110D!CallSettingFrame+0x20
0c 30 00000065`d5c37780 00007ffc`7dd3a193 MSVCR110D!_CxxCallCatchBlock+0x162
0d a0 00000065`d5c37820 00007ff7`f14714ca ntdll!RcConsolidateFrames+0x3
0e f7970 00000065`d5d2f190 00007ff7`f14714ca Crashtest!recursiveFunction+0xba
0f 60 00000065`d5d2f1f0 00007ff7`f14714ca Crashtest!recursiveFunction+0xba
...
注意:一个帧的大小位于下一行的第二列。该帧的大小
为ntdll!RcConsolidateFrames
( 0xf7970
1.014.128) 字节,因此占用了 1MB 总可用堆栈大小的 96%。
最让我烦恼的是,在调用堆栈用完并且另一个调用导致堆栈溢出之前,我可以(如截图中所述)递归调用该函数多达近 10.600 次。但是,如果我在超过 120 次调用后用异常中止递归,我再次得到堆栈溢出,这个异常是为了防止。
因此,用更大的堆栈编译程序只会将问题转移到稍高的常数上。
如前所述,此问题仅在 x64 编译时出现。如果用 Win32 编译,程序永远不会遇到堆栈溢出,一旦std::exception
被抛出。
一个程序在堆栈展开期间分配的堆栈空间比它释放的更多,这有什么意义?
我该如何解决这个问题?
由于这只是一个非常简化的情况,我不能简单地用throw
原始应用程序中的特殊返回值替换 。
编辑:微软只是删除了请求而没有提供任何帮助。我收到了以下答案,要求提供更多详细信息,但在我回复一天后该请求被删除。
感谢您报告此问题。虽然大堆栈的使用并不理想,但它是 EH 在 Windows 非 x86 平台上实现的重要结果。需要注意的一点是,RcConsolidateFrames 下的堆栈使用有点误导。该函数的重点是使展开器隐藏一堆中间帧,以便堆栈使用反映正在运行的 EH 机制的 105 个实例(每次执行的重新抛出一个实例),以及所有递归子调用。
您能否分享更多关于这种高堆栈使用阻塞的真实场景的信息?解决这个问题可能不是那么容易,但如果我们可以对重要的场景做出一些简化的假设,就可以改进这个问题。
谢谢,
Neeraj Singh VC++ 编译器后端开发人员
与此同时,虽然我希望,问题出在重新抛出机制上,它一遍又一遍地重新抛出分配在堆栈上的同一个异常对象。我以为其中之一
- 复制异常并抛出异常的副本
- 抛出异常,创建于
new
可以解决问题,但结果没有区别。