Ошибки Net Framework

Метки:  , , , , , , , , , , , , ,

Данная статья создавалась как попытка продолжить изучение не достаточно хорошо знакомой для меня темы восстановления работоспособности [приложений] .Net Framework в системах Windows, а так же поиска проблем ошибок в Net Framework приложениях. Очевидно, что все тут перечисленное представляет собой достаточно упрощенный подход к изучению структур .Net-приложений, тем не менее, в некоторых случаях достаточный для выявления причин неисправностей.

.NET Framework - программная платформа, основой которой является общеязыковая среда исполнения (Common Language Runtime, CLR) байт-кода "промежуточного языка высокоуровневого ассемблера". Из определения "общеязыковая" следует, что она предназначается для выполнения кода модулей, написанных на множестве языков программирования. Получила дальнейшее развитие в виде .NET Core.

Основная концепция создания .Net Framework заключалась в обеспечении свободы разработки, обусловленной возможностью создавать приложения с использованием множества языков программирования, способных исполняться на широком спектре устройств, работающих под разнообразными операционных системах [мультиязычность и кроссплатформенность]. Программа для платформы .NET Framework, в начале исполнения переводится компилятором в единый для .NET промежуточный байт-код "высокоуровневого ассемблера" виртуальной машины .NET (Common Intermediate Language, CIL -- ранее известного как Microsoft Intermediate Language, MSIL), называемый в контексте .NET сборкой (assembly). Далее получившийся код либо исполняется виртуальной машиной Common Language Runtime (общеязыковая среда выполнения, CLR), либо транслируется утилитой NGen.exe в исполняемый код для определенного целевого процессора. И на финальном этапе, встроенный в виртуальную машину CLR компилятор "на лету" (в режиме реального времени) преобразует промежуточный байт-код в машинные коды целевого процессора [для непосредственного исполнения кода ядром].

В некотором смысле .Net Framework это система в системе. Она обеспечивает множество собственных механизмов: управление виртуальной памятью, система безопасности, загрузчик модулей, механизм исключений (обработки ошибок),модель многопоточности, изолированная среда выполнения приложений и многое другое. Тем самым .Net Framework как бы создает еще один уровень абстракции.

Поскольку мы упомянули некий "высокоуровневый ассемблер" виртуальной машины .NET, становится очевидным что в .Net Framework мы имеем дело с так называемым "управляемым" кодом.

Управляемый код (managed code) — код на языке "высокоуровневого ассемблера", исполняемый под управлением (внутри) виртуальной машины .NET CLR.

В приложениях с управляемым кодом, бинарный код, получающийся на выходе компилятора, получается в формате промежуточного языка (Microsoft Intermediate Language, MSIL), который является платформонезависимым. Когда управляемый код исполняется, среда выполнения преобразует его в обычный машинный код конкретной процессорной архитектуры (х86, х64 или IA64).

CLR компилирует MSIL в команды процессора [целевой архитектуры].

Процесс генерации машинного кода из кода MSIL называется компиляцией на лету (just-in-time (JIT) compiling). После того, как JIT-компилятор (jitter) скомпилировал MSIL для определенного метода, машинный код этого метода остается в памяти. Если когда-либо еще данный метод будет вызван, машинный код просто выполняется (поскольку он уже размещен в памяти) и JIT-компилятор может вообще в этом случае не вовлекаться в процесс.

Исключения в .NET

Поскольку популярность .Net платформы с каждым годом набирает обороты, в валовом отношении растет и количество ошибок .Net Framework, возникающих в коде. С другой стороны, неоспоримым плюсом является то, что по сравнению с неуправляемыми (классическими) приложениями, управляемые приложения меньше подвержены появлению в них [определенных видов] ошибок. Тем не менее, как и любой другой тип приложений, .NET-приложения в процессе своего функционирования сталкиваются с ошибками времени выполнения кода, иными словами - с некоторыми видами исключений.

Модель исключений в .NET работает поверх системного механизма обработки структурных исключений Windows (SEH), что обеспечивает объектно-ориентированную модель исключений, позволяющую предоставлять довольно детальную информацию об исключениях.

В момент возникновения исключения, CLR начинает поиск блока Catch (тип которого соответствует типу исключения) в стеке вызовов. В случае, когда ни один из блоков Catch не отвечает типу исключения (обработчик не найден), исключение считается необработанным (unhandled exception). Необработанное исключение указывает на ситуацию, не предусмотренную разработчиком приложения, и обычно считается признаком серьезной ошибки. Системный механизм WER производит запись в Журнале событий (раздел Приложение) и на экран выдается информационное окно (внешний вид может варьироваться в зависимости от версии):

clr20r3

Фактически на этом [моменте] работа сбойного приложения прекращается. Самая информативная для технического инженера часть - это сигнатуры проблемы, которая в Журнале событий (раздел Приложение) и в поле Подробности проблемы информационного окна дает нам некоторое представления о деталях сбоя:

В случае, описанном выше, имеет место падение оснастки Просмотр Событий (eventvwr.exe), которая работает через консоль управления (mmc.exe). Далее приведем описание полей сигнатуры:

Сигнатура Описание
сигнатура_01 Имя процесса программы (исполняемого образа/файла), в контексте выполнения которого возникло исключение ( <= 32 знака).
Сигнатура_02 Версия сборки [исполняемого образа].
Сигнатура_03 Штамп времени [исполняемого образа].
Сигнатура_04 Имя библиотеки (из состава приложения), сборки или иной файл из состава .Net Framework, при исполнении функции [из] которой возникла ошибка ( <= 64 знака).
Сигнатура_05 Версия (аварийной) библиотеки/сборки приложения/платформы .Net Framework, в которой произошло исключение.
Сигнатура_06 Штамп времени проблемной (аварийной) сборки.
Сигнатура_07 Определение (маркер) метода (в таблице MethodDef) и типа, в котором произошло исключение (с "обрезанным" старшим байтом 0x06, типом маркера). Идентифицируют запись в соответствующей таблице метаданных.
Сигнатура_08 Смещение инструкции (команды) в рамках метода в коде на промежуточном языке (IL), при выполнении которой произошло исключение. Взяв величину смещения, при помощи любого .Net-рефлектора можно найти некорректный код.
Сигнатура_09 Тип вброшенного исключения (название класса или пространства имен .Net).

Для формирования полной картины сбоя, потребуется взять во внимание совокупность всех сигнатур проблемы, в комплексе могущих дать понимание о природе сбоя (особое внимание обратить на имя класса-источника). Ну и для полноты картины можно привести открытые свойства типа System.Exception, которые можно найти в отчете отладчика/аварийном дампе памяти после прекращения работы приложения:

Свойство Тип Описание
message String Содержит осмысленный (иногда) текст, описывающий причину исключения. Сообщение содержит технические подробности, которые могут оказаться полезными для технических специалистов/разработчиков.
data IDictionary Ссылка на список пар в формате параметр-значение. Непосредственно перед вбросом исключения, код добавляет запись в этот набор.
stacktrace String Имена/сигнатуры методов, вызов которых привел к возникновению исключения. Помогает обнаружить объект, являющийся источником исключения.
source String Имя сборки, вбросившей исключение.
innerexception Exception Указатель на "предыдущее" исключение (в случае, если текущее исключение было вброшено в ходе обработки другого). Часто содержит значение null. Тип Exception содержит также открытый метод GetBaseException, анализирующий список внутренних исключений и возвращающий самое первое.
helpURL String URL документации с информацией об исключении. Обычно содержит null, поскольку с точки зрения безопасности сведения о необработанных исключениях не должны быть доступны.
TargetSite MethodBase Имя метода, ставшего источником исключения.

Возникает резонный вопрос: есть ли возможность, исключительно на основании представленной выше информации, сделать однозначный вывод о причине исключительной ситуации? Я думаю, исключать подобную возможность не стоит, поскольку все зависит от глубины теоретических и практических знаний, и если у Вас уже большая наработка данных об исключениях, то я думаю сможете сделать довольно точное предположение относительно причины сбоя. Для всех остальных разработан специализированный инструментарий.

Метод 1: отладка дампа .Net-приложения

Далеко не всегда у нас имеется возможность "живой отладки" сбойного .Net-приложения в реальном времени, непосредственно на системе пользователя, значительно чаще приходится довольствоваться созданным аварийным дампом. Стоит напомнить, что в случае приложений, написанных для платформы .Net Framework, мы имеем дело с управляемым кодом, в противоположность типовому неуправляемому коду классических приложений. Но так просто до управляемого кода с помощью отладчика не добраться.

Логично предположить, что если .NET представляет собой еще один уровень абстракции, то по сравнению с классическими (native) приложениями, отлаживать .Net-приложения классическими методами становится сложнее!!

Когда .Net-приложение "падает" (завершается аварийно) или "подвисает", специалист тут же получает большую проблему, поскольку очень сложно продраться сквозь нативный ассемблерный код, исследовать стеки вызовов, найти исходные тексты и строки структур .NET-приложения. Чтобы помочь исследователям увидеть при анализе дампа или живой отладке разнообразные структуры .Net-приложения, легче баражировать через комбинации управляемого и неуправляемого кода, специалисты Microsoft разработали расширение отладчика SOS, весь функционал которого сосредоточен в библиотеке sos.dll (являющейся частью .NET Framework).

SOS (Son of STRIKE) - расширение отладчика, которое можно использовать для отладки .NET-приложений с использованием серии отладчиков из комплекта Debugging Tools for Windows. Расширение предоставляет воистину великолепный набор команд, который позволяет разработчикам глубоко внедряться в CLR: просматривать очередь финализаторов, управляемые кучи, управляемые потоки, расставлять точки останова в управляемом коде, разбирать исключения и многое другое. Когда .NET Framework был в версии 1.0, разработчики Microsoft использовали расширение под названием STRIKE для решения определенных проблем в коде .NET, позднее, когда .NET Framework "повзрослел", расширение отладчика стало именоваться как Son of STRIKE (SOS).

Проблемы в .Net-приложениях [с управляемым кодом] могут диагностироваться из без расширения SOS, он этот подход требует превосходного знания разнообразных внутренних структур .Net. С расширением SOS все существенно упрощается, позволяя исследователям/разработчикам сфокусироваться на поиске источника проблемы. Поэтому расширение SOS предоставляет все необходимое для отладки приложений, в которых скомбинированы управляемый и неуправляемый код.

Создание дампа приложения

  1. В данном сценарии подразумевается, что мы уже имеем на руках дамп сбойного приложения (полученный от пользователя). Если дамп приложения необходимо создать, то можно посоветовать использовать один из следующих способов: с использованием CDB, при помощи ProcDump, с использованием встроенного механизма WER. Способ с ProcDump один из самых простых, поэтому скачиваем ProcDump.
  2. Запускаем сбойное приложение через ProcDump с использованием команды:

    procdump.exe -accepteula -e -w <имя_сбойного_приложения.exe> c:\temp\

    где параметр c:\temp - любая временная директория по вашему выбору.

  3. Дожидаемся когда в приложении возникнет исключение;
  4. После падения приложения в каталоге C:\TEMP получаем дамп приложения (файл с расширением .dmp);

Изучение дампа приложения

Наиболее часто встречающийся сценарий подразумевает, что вы используете отладчик для изучения дампа [приложения с управляемым кодом], созданного на другом компьютере. Общий алгоритм отладки, в этом случае, следующий:

  1. Устанавливаем пакет Debugging Tools for Windows.
  2. Запускаем отладчик WinDbg. По предварительной настройке отладчика можете почитать эту статью.
  3. Открываем дамп приложения через меню File - Open Crash Dump.... Мы работаем с дампом процесса, поэтому отладчик должен автоматически загрузить версию DAC (компонент доступа к данным, используемый SOS для связи с CLR), соответствующую версии CLR, используемую .Net-приложением на другой станции (на которой создавался дамп). Разрядность (32/64-бит) так же имеет значение. DAC в данном случае это библиотека mscordacwks.dll, которая входит в состав пакета .NET Framework.
  4. Если у Вас на компьютере нет подходящей версии .Net Framework, то можно загрузить требуемый DAC с публичного сервера символов Microsoft. Для этого, на всякий случай, если предварительная настройка на получение символов у вас не проведена, мы может настроиться вручную и выполнить серию команд:

    .sympath+ srv\*
    !sym noisy

  5. Важно отметить, что при отладке с использованием расширения SOS необходимо использовать правильную версию SOS. Для загрузки требуемой отлаживаемому приложению версии DAC, присоединяем отладчик к .Net-приложению, после чего расширение SOS (sos.dll) загружается автоматически. Для этого выполним следующую команду:

    .cordll -ve -u -l

    если по каким-либо причинам этого не произошло, можно выполнить ручную загрузку SOS:

  6. Проверяем работоспособность расширения путем выполнения команды !sos.help:

    Выполняем команду !sos.pe:

    В принципе, мы могли сразу получить развернутый вывод по всем имеющимся вложенным объектам исключений командой !sos.pe -nested. Но можем использовать и другую стратегию: двигаться вниз по цепочке вложенных исключений. Выведем информацию по вложенному исключению командой (или щелчком по ссылке) !PrintException 0000000004ff7188 или !sos.pe 0000000004ff7188 (где аргумент команды - адрес объекта исключения):

    тут у нас видно сразу и свойство Message, содержащее осмысленное описание ошибки и на вершине стека непосредственно функцию OpenSubKey, вызвавшую исключение. Теперь можно посмотреть CLR-стек для активного потока процесса и найти там интересующую нас функцию OpenSubKey (вывод сокращен до данных интересующей нас функции):

    в данном случае почему то не видно параметров и переменных, возможно это объясняется тем, что вызываемый метод является функцией WinAPI и входные параметры в неё передаются как-то иначе. В случае же наличия параметров, можно выполнить команду !sos.do XxXXXXXXXX - с указанием адреса, который будет виден в выводе команды clrstack для данного метода. Таким образом мы надеемся получить подробную информацию об объекте, попытка доступа к которому завершилась возбуждением исключения.
    Но поскольку в нашем случае параметров не видно, попробуем подойти к вопросу с другой стороны. Запрашиваем список всех объектов, находящихся в настоящее время в стеке текущего потока (вывод сокращен):

    Так как команда !dumpstackobjects проходит по стеку вверх, в выводе её мы можем заметить повторение некоторых элементов по несколько раз, поскольку они передаются в качестве параметров ко многим функциям. В стеке можно наблюдать несколько объектов System.Security.SecurityException, но если вы обратите внимание на значение, то заметите, что все они ссылаются на один и тот же экземпляр объекта 0000000004ff7188. Давайте посмотрим на содержимое данного объекта:

    для нас важным является свойство Message. Давайте выведем значение поля _message, поскольку именно в нем Message хранит текстовую строку. Шестнадцатеричное число в столбце Value является экземпляром объекта:

    ну да, очень похоже на то, что мы видели выше в выводе команды !PrintException. Но это всего-лишь сообщение об ошибке, а как нам добраться до конкретного ключа, попытка доступа к которому у нас закончилась неудачей? Попробуем посмотреть объект с именем Microsoft.Win32.RegistryKey:

    А затем значение поля keyName:

    опять же, добрались до куста HKEY_CURRENT_USER, можно конечно попробовать поиграться с разрешениями на целый куст, но это не очень хорошая затея и комфортнее было бы определить полный путь. Как нам это сделать? Вспомним, что выделенная стеку область памяти используется [в том числе] для передачи параметров в методы и хранения
    определенных в пределах методов переменных. Поэтому непосредственно до вызова самого метода (снизу вверх) в стеке должны храниться параметры. Давайте вернемся к списку объектов (виденному нами выше) в стеке, среди прочего у нас есть объекты System.String:

    Последний в списке (0000000004ff7120) нами уже проверен выше. Два остальных объекта у нас идентичны, поскольку показывают один и тот же адрес, поэтому стоит просмотреть содержимое:

    И что же мы тут видим? Объект содержит строку, являющуюся полным путем к искомому разделу реестра. В итоге, мы смогли воспроизвести полный путь раздела, при попытке доступа к которому возникло исключение: это HKCU\Software\Microsoft\Internet Explorer\Main. Смотрим через regedit.exe на разрешения к данному ключу, и выясняется, что для текущего пользователя кто-то их убрал (очередное криво-установившееся обновление?). Не важно, посмотрим разрешения на аналогичной рабочей операционной системе, они там присутствуют, значит надо выставлять по аналогии.

Метод 2: определение источника при помощи ProcMon

Отладчик является довольно мощным средством для поиска причин ошибок Net Framework. Но встречаются случаи, когда мы (по абсолютно разным причинам) не можем найти нужных структур в файле дампа приложения, либо у нас не хватает уровня знаний для того, чтобы до этих данных дотянуться. В подобной ситуации хорошо бы иметь под рукой еще несколько методов анализа боя. И в этом разделе мы рассмотрим другой метод поиска причин ошибок Net Framework, который предполагает работу уже с объектами файловой системы, сбор возникающих во время работы сбойного приложения событий при помощи средства под названием Procmon.
Общий алгоритм решения:

  • Скачиваем утилиту Procmon. Более подробно о данной программе можно почитать в этой статье.
  • Запускаем Procmon из-под учетной записи с правами локального Администратора (с повышением привилегий). Стартует процесс сбора системных событий.
  • В параллель запускаем вызывающее ошибку приложение. Дожидаемся возникновения ошибки.
  • Переключаемся в окно Process Monitor, нажатием на значок лупы прекращаем запись событий (дабы не раздувать список событий и не увеличивать понапрасну нагрузку на систему).
  • В получившемся списке собранных событий ставим курсор (маркируем) на самое первое событие, открываем окно поиска комбинацией клавиш Ctrl+F и сперва ищем словосочетание access denied.
  • Каждое найденное таким образом событие сверяем по столбцу Process Name, дабы имя соответствовало нашей проблемной (падающей, сбойной) программе (для случая выше это eventvwr.exe). Выглядеть это будет подобным образом:

    procmon access denied

    Как мы видим, проблема заключается в отсутствии доступа к разделу реестра HKCU\Software\Microsoft\Internet Explorer\Main

  • Запускаем Regedit.exe. Разворачиваем путь до проблемного раздела реестра, в меню открываем Разрешения объекта. Для начала меняем владельца объекта на собственную учетную запись, затем переоткрываем разрешения и устанавливаем необходимые разрешения безопасности для нашей учетной записи (из-под которой планируется запускать наше сбойное приложение).

Метод 3: дизассемблер IL (IL DASM)

Все исполняемые файлы, содержащие в себе управляемый и неуправляемый код, размещаются в файловой системе в виде типовых исполняемых .exe-файлов в формате PE. Дизассемблер, входящий в состав пакета Windows SDK, предназначен для декомпиляции (рефлексии) исполняемых файлов, содержащих в своем составе код на языке IL.

  • Скачиваем и устанавливаем Архив Windows SDK под нужную нам систему;
  • Запускаем ildasm.exe. Обычно располагается по пути: C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7.2 Tools"\ - *соответственно не забудьте сделать корректировку версий в составе пути, у вас они могут отличаться.
  • Открываем файл, фигурирующий у нас в параметре Сигнатура 04 подробностей исключения.
  • Открываем окно с метаданными указанной сборки: пункт меню Вид - Метаданные - Показать!.
  • В открывшемся окне метаданных (MetaInfo) выполняем поиск по определению метода, которое содержится в параметре исключения Сигнатура 07 деталей сбоя, при этом добавляя префикс 0x06 (таблица определений методов). В итоге, для примера выше, получается значение 06002d59.
  • В найденной записи таблиц метаданных смотрим соответствующее имя метода в поле MethodName: <имя_метода> (06002d59).

Таким образом мы узнаем метод, в котором возникло (необработанное) исключение, и имея под рукой его исходный код, уже гораздо проще найти место возникновения ошибки. Преимущество дизассемблера IL Dasm состоит в том, что он самостоятельно анализирует таблицы метаданных и группирует всю необходимую информацию в удобочитаемом виде, поэтому специалисту не приходится заниматься самостоятельным синтаксическим разбором "сырых" табличных данных.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *