Ошибки Net Framework

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

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

.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-приложения в процессе своего функционирования сталкиваются с ошибками времени выполнения кода, иными словами - исключениями. В момент возникновения исключения, CLR начинает поиск блока Catch (тип которого соответствует типу исключения) в стеке вызовов. В случае, когда ни один из блоков Catch не отвечает типу исключения (обработчик не найден), исключение считается необработанным (unhandled exception). Необработанное исключение указывает на ситуацию, не предусмотренную разработчиком приложения, и обычно считается признаком серьезной ошибки. Производится запись в Журнале событий (раздел Приложение) и на экран выдается информационное окно (внешний вид может варьироваться в зависимости от версии):

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).

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

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

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

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

Проблемы в .Net-приложениях [с управляемым кодом] могут диагностироваться из без расширения SOS, он этот подход требует превосходного знания команд отладчика и разнообразных внутренних структур. С расширением 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 (компонент доступа к данным), соответствующую версии CLR, используемую .Net-приложением на другой станции (на которой создавался дамп). Разрядность (32/64-бит) так же имеет значение. DAC в данном случае это библиотека mscordacwks.dll, которая входит в состав пакета .NET Framework.
  4. Если у Вас на компьютере нет подходящей версии .Net Framework, то можно загрузить требуемый DAC с публичного сервера символов Microsoft. Для этого, на всякий случай, если предварительная настройка на получение символов у вас не проведена, мы может настроиться вручную и выполнить серию команд:

    .sympath+ srv\*
    !sym noisy

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

    .cordll -ve -u -l

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

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

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

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

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

    в данном случае почему то не видно параметров и переменных, возможно это объясняется тем, что вызываемый метод является функцией WinAPI и входные параметры в неё передаются как-то иначе. В случае же наличия параметров, можно выполнить команду !sos.do 0x00000000 - с указанием адреса, который будет виден в выводе команды 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 не будет опубликован. Обязательные поля помечены *