Я думаю, что у меня есть кудрявое приложение ... У меня есть приложение WinForms, которое довольно часто вылетает каждый час или около того, когда выполняется как процесс x64. Я подозреваю, что это связано с повреждением стека, и хотел бы знать, не сталкивался ли кто-либо с подобной проблемой или у нее есть советы по диагностике и обнаружению проблемы.
У рассматриваемой программы нет видимого пользовательского интерфейса. Это просто окно сообщений, которое находится в фоновом режиме и действует как своего рода «промежуточное ПО» между нашими другими клиентскими программами и сервером.
Он умирает по-разному на разных машинах. Иногда это диалог «APPCRASH», который сообщает об ошибке в ntdll.dll. Иногда это «APPCRASH», который сообщает, что наш собственный dll виновник. Иногда это просто тихая смерть. Иногда наш необработанный обработчик исключений регистрирует ошибку, иногда - нет.
В тех случаях, когда включается отчет об ошибках Windows, я исследовал дампы памяти из нескольких различных сценариев сбоя и каждый раз обнаруживал одно и то же управляемое исключение в памяти. Это то же исключение, которое я вижу как необработанное исключение в тех случаях, когда мы регистрируем его до того, как оно умрет.
Мне также повезло (?), Что у меня произошел сбой приложения во время активной отладки в Visual Studio - и я увидел, что это же исключение сбивает программу.
Теперь вот кикер. Это исключение было брошено, поймано и проглочено в первые несколько секунд жизни программы. Я проверил это с помощью дополнительной регистрации трассировки, и через пару минут после запуска приложения я получил дампы памяти приложения и убедился, что исключение все еще находится где-то в куче. Я также запустил профилировщик памяти над приложением и использовал его для проверки того, что ни один другой объект .NET не имеет ссылки на него.
Рассматриваемый код выглядит примерно так (значительно упрощен, но сохраняет ключевые точки управления потоком)
public class AClass
{
public object FindAThing(string key)
{
object retVal = null;
Collection<Place> places= GetPlaces();
foreach (Place place in places)
{
try
{
retval = place.FindThing(key);
break;
}
catch {} // Guaranteed to only be a 'NotFound' exception
}
return retval;
}
}
public class Place
{
public object FindThing(string key)
{
bool found = InternalContains(key); // <snip> some complex if/else logic
if (code == success)
return InternalFetch(key);
throw new NotFoundException(/*UsefulInfo*/);
}
}
Трассировка стека, которую я вижу, как в журнале событий, так и при просмотре кучи с помощью windbg, выглядит примерно так:
Company.NotFoundException:
Place.FindThing()
AClass.FindAThing()
Теперь ... мне пахнет что-то вроде повреждения стека. Исключение выдается и перехватывается во время запуска приложения. Но указатель на него сохраняется в стеке в течение часа или более, как пуля в мозге, а затем внезапно пробивает критическую артерию, и приложение умирает в луже.
Дополнительные подсказки:
Код в «InternalFetch» использует некоторый маршал. [Alloc / Free] CoTask и пинвока код. Я запустил FxCop над ним для поиска проблем с переносимостью и ничего не нашел.
Это конкретное проявление проблемы касается только кода x64, встроенного в режим выпуска (с включенной оптимизацией кода). Код, который я перечислил для метода Place.Find, отражает оптимизированный код .NET. Неоптимизированный код возвращает найденный объект в качестве последнего оператора, а не «выбросить исключение».
Мы выполняем некоторые COM-вызовы во время запуска до запуска приведенного выше кода ... и в сценарии, где возникнет вышеуказанная проблема, самый первый COM-вызов завершится неудачно. (Исключение поймано и проглочено). Я закомментировал этот конкретный COM-вызов, и он не останавливает исключение, застрявшее в куче.
Проблема может также повлиять на 32-разрядные системы, но если это произойдет, то проблема не проявляется в одном месте. Мне прислали (обычные пользователи!) Снимок экрана в диалоговом окне «APP CRASH» на несколько пикселей, но я смог разглядеть только «StackHash_2264» в поле неисправного модуля.
EDIT:
Прорыв!
Я сузил проблему до конкретного звонка на SetTimer
.
PInvoke выглядит так:
[DllImport("user32")]
internal static extern IntPtr SetTimer(IntPtr hwnd, IntPtr nIDEvent, int uElapse, TimerProc CB);
internal delegate void TimerProc(IntPtr hWnd, uint nMsg, IntPtr nIDEvent, int dwTime);
Существует определенный класс, который запускает таймер в своем конструкторе.Любые таймеры, установленные до того, как этот объект построен, работают.Любые таймеры, установленные после того, как этот объект построен, работают.Любой таймер, установленный во время этого конструктора, вызывает сбой приложения, чаще всего.(У меня ноутбук зависает, может быть, в 95% случаев, но мой рабочий стол падает только в 10% случаев).
Независимо от того, установлен ли интервал в 1 час или 1 секунду, это не меняет дело.Приложение умирает при наступлении таймера - обычно, выбрасывая какое-то ранее обработанное исключение, как описано выше.Обратный вызов на самом деле не выполняется.Если я установлю тот же таймер на следующей строке управляемого кода после возврата конструктора - все будет хорошо и радостно.
У меня был подключен отладчик, когда собирался запустить плохой таймер, и это вызвалонарушение доступа в «DispatchMessage».Таймер обратного вызова никогда не вызывался.Я включил MDA, которые относятся к управляемым обратным вызовам, собираемым мусором, и он не запускается.Я проверил объекты с помощью sos и убедился, что обратный вызов все еще существует в памяти, и что адрес, на который он указывал, является правильной функцией обратного вызова.
Если я в этот момент запускаю '! Analysis -v', онобычно (но не всегда) сообщает что-то вроде 'ERROR_SXS_CORRUPT_ACTIVATION_STACK'
Замена вызова SetTimer на класс Microsoft 'System.Windows.Forms.Timer' также останавливает сбой.Я использовал Reflector в классе и вижу, что он все еще вызывает SetTimer, но не регистрирует процедуру.Вместо этого у него есть собственное окно, которое получает обратный вызов.Это определение pInvoke на самом деле выглядит неверно ... он использует 'int' для eventId, где в документации MSDN сказано, что это должен быть UIntPtr.
Наш собственный код первоначально также использовал int для nIDEvent, а не IntPtr - я изменил его в ходе этого исследования - но сбой продолжался как до, так и после изменения этого объявления.Так что единственное реальное отличие, которое я вижу, это то, что мы регистрируем обратный вызов, а класс Windows - нет.
Итак ... на этом этапе я могу «исправить» проблему, переместив один конкретный вызов SetTimer в немного другое место.Но я все еще не ближе к пониманию того, что такого особенного в запуске таймера внутри этого конструктора, который вызывает эту ошибку.И мне очень хотелось бы понять причину этой проблемы.