Диагностика / отладка потенциального повреждения стека .NET-приложения - PullRequest
4 голосов
/ 15 июня 2011

Я думаю, что у меня есть кудрявое приложение ... У меня есть приложение 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 в немного другое место.Но я все еще не ближе к пониманию того, что такого особенного в запуске таймера внутри этого конструктора, который вызывает эту ошибку.И мне очень хотелось бы понять причину этой проблемы.

Ответы [ 3 ]

3 голосов
/ 15 июня 2011

Если кратко подумать, это звучит как проблема взаимодействия x64 (то есть вызов собственных функций x32 из управляемого кода x64 чреват опасностью). Проблема исчезнет, ​​если вы заставите ваше приложение скомпилировать как платформу x32 из свойств проекта?

Вы можете прочитать предложения по принудительной компиляции x32 во время разработки x32 / x64 на Dotnetrocks. Ричард Кэмпбелл предлагает, чтобы Visual Studio по умолчанию работала на платформе x32, а не на AnyCPU. http://www.dotnetrocks.com/default.aspx?showNum=341 ( стенограмма ).

Что касается расширенной отладки, у меня не было возможности отладить код взаимодействия x64, но я слышал, что эта книга - отличный ресурс: Расширенная отладка .NET .

Наконец, одна вещь, которую вы можете попробовать, - это заставить Visual Studio прекратить работу при возникновении исключения .

2 голосов
/ 15 июня 2011

Используйте что-то вроде DebugDiag для x64 или Windbg, чтобы записать дамп на Kernel32!TerminateProcess и исключение второго шанса на .NET, которое должно дать вам фактический .excr контекстный фрейм возникшего исключения.

Это должно помочь вам определить стек вызовов для завершения процесса.

IMO это может быть в основном из-за вызовов PInvoke.Вы можете использовать Managed Debugging Assistants для устранения этих проблем.

Если MDA используется вместе с Windbg, он выдаст сообщения, которые будут полезны при отладке

enter image description here

Также я обнаружил, что инструменты от команды http://clrinterop.codeplex.com/ чрезвычайно удобны при взаимодействии

РЕДАКТИРОВАТЬ

Это должно датьответ, почему он не работает в 64-битном Проблема с методом обратного вызова в Windows API SetTimer, вызываемого из кода C # .

1 голос
/ 15 июня 2011

Это похоже на проблему коррупции. Я хотел бы пройти через все ваши вызовы взаимодействия и убедиться, что все параметры функций DllImport являются правильными типами. Например, использование int вместо IntPtr будет работать в 32-битном коде, но может привести к сбою 64-битного.

Я бы использовал сайт вроде PInvoke.net для проверки всех подписей.

...