Переполнение стека при перебрасывании исключения из-за ntdll!RcConsolidateFrame (x64)

Я борюсь с исключением переполнения стека, которое происходит при перебрасывании другого исключения. Переброшенное исключение используется для разрушения стека вызовов после того, как рекурсивная функция вызывала себя более чем определенное количество раз. (Для предотвращения переполнения стека)

Мне удалось написать небольшую программу, которая воспроизводит эту проблему. Это происходит только тогда, когда я компилирую программу с x64 (Release & Debug).
Я протестировал фрагмент с MSVS2012 и MSVS2013 (StackSize=1 МБ - по умолчанию). С G++ та же проблема возникает после примерно 5000 звонки.

Код:

#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) байт и, таким образом, занимает 96% от общего доступного размера стека в 1 МБ.

Больше всего меня беспокоит тот факт, что я могу (как отмечалось во фрагменте) вызывать функцию рекурсивно почти до 10.600 раз, прежде чем стек вызовов будет израсходован, а другой вызов приведет к переполнению стека. Но если я прерву рекурсию с исключением после более чем 120 вызовов, я снова получу переполнение стека, которое должно исключить это исключение.
Таким образом, компиляция программы с большим стеком только сдвигает проблему к чуть более высокой константе. Как упоминалось ранее, эта проблема возникает только при компиляции x64. Если скомпилировано с Win32, программа никогда не запускается в переполнение стека, как только std::exception был брошен.

Какое значение имеет программа, которая выделяет больше стекового пространства при разматывании стека, чем освобождает?
Как я могу решить это?
Поскольку это только очень упрощенный случай, я не могу просто заменить throw по специальному возвращаемому значению в исходном приложении.

Microsoft Connect запрос (удален)

РЕДАКТИРОВАТЬ: Microsoft просто удалила запрос без предоставления какой-либо помощи. Я получил следующий ответ с просьбой предоставить более подробную информацию, но запрос был удален через день после моего ответа.

Спасибо за сообщение об этой проблеме. Хотя использование большого стека не является идеальным, это является существенным следствием того, как EH реализован на платформах Windows, отличных от x86. Стоит отметить, что использование стека в RcConsolidateFrames немного вводит в заблуждение. Смысл этой функции заключается в том, чтобы разматывать скрывать группу промежуточных кадров, чтобы использование стека отражало 105 экземпляров работающего механизма EH (по одному на каждый выполненный повторный бросок), а также все ваши рекурсивные дочерние вызовы.

Не могли бы вы поделиться более подробной информацией о реальном сценарии, который блокирует использование этого большого стека? Исправить это для общего броска от улова может быть не так просто, но можно улучшить его, если мы сделаем некоторые упрощающие предположения о важном сценарии.

Спасибо,
Neeraj Singh VC++ Compiler Backend Developer

Между тем, как я и надеялся, проблема заключается в механизме переброса, который перебрасывает один и тот же объект исключения, который размещается в стеке, снова и снова. Я думал, что один из

  • копирование исключения и создание копии исключения
  • бросить исключение, созданный с new

мог решить проблему, но не было никакой разницы в результате.

0 ответов

Другие вопросы по тегам