WinDbg для .NET разработчика: "1. Начало"

WinDbg -- это, бесспорно, мощнейший отладчик под Windows. Он широко известен среди низкоуровневых системных разработчиков, но несправедливо пренебрегается, и, более того, не используется более высокоуровневыми прикладными разработчиками. Несмотря на то, что более известный отладчик, встроенный в Visual Studio Debugger, предоставляет простой и наглядный способ отладки приложений в ходе разработки, WinDbg имеет намного более мощные инструменты для отладки приложений в пользовательском окружении, когда единственное что у вас есть это дамп памяти уже умершей программы... или даже тогда, когда странные вещи творятся внутри вашей тестовой лаборатории или, еще того хуже, внутри вашего компьютера. В интернете можно найти огромное количество отличных книг и блог-постов, содержащих исчерпывающую информацию по использованию WinDbg. К несчастью, большинство из них имеет очень крутую кривую обучения (как у vi) и поскольку я так и не смог найти информацию в виде, позволяющем быстро начать использовать WinDbg, я решил собрать свой полученный опыт в виде коротких и практических историй. Первая часть "1. Начало" посвящена некоторым крайне простым вещам, которые Вам действительно необходимо сделать, и раскрывает наиболее частый сценарий применения WinDbg, а именно пост-мортем отладку.

Дисклеймер

Как и все в этом блоге, этот пост может содержать абсолютно жестокие и ужасные вещи, выполненные крайне нерационально и абсолютно некорректно, а часть из написанного может даже пытаться вынести ваш мозг. Если у Вас другое мнение или Вы знаете более логичный путь сделать что-либо из обсуждаемого в этом посте и у Вас останутся силы выразить это в виде комментариев или pull-request'ов к посту, я буду крайне признателен.

Установка

Несмотря на факт того, что некоторые web-сайты предоставляют сборки WinDbg минимального размера, я рекомендую не экономить на трафике и скачать WinDbg как часть WinSDK с официального центра разработчиков Microsoft. Для минимальной WEB установки потребуется всего ~150Mb и несколько минут Вашего времени, но это может предотвратить огромное количество проблем связанных с возможно некорректными неофициальными сборками.

/posts/2018/windbg_course/winsdk.png

Также я рекомендую установить следующие расширения WinDbg, которые позволяют намного облегчить процесс отладки .NET приложений:

  • sosex (SOS extended) -- превосходное расширение, ускоряющее огромное число часто используемых сценариев отладки и предоставляющее улучшенный вывод для некоторых комманд.
  • psscor4 -- расширение, предоставляющее расширенную версию функциональности SOS и немного более толерантная к некоторым ошибкам, которые могут быть в собранном дампе памяти.
  • soswow64 -- расширение, дающее возможность открывать дампы памяти под x86 архитектуру собранные через x64 версии Task Manager или ProcDump. Я очень надеюсь что это расширение никогда не пригодится Вам, поскольку все дампы всегда будут собраны правильными инструментами, но в случаях, когда это уже произошло, это расширение единственный способ провести анализ дампа памяти.

Вы можете скачать эти расширения, скопировать в любую директорию и загружать с помощью следующей команды .load C:\windbg\exts\x86\sosex.dll. Но я рекомендую скопировать их в следующие директории:

C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\winext // for x86
C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\winext // for x64

Тогда для загрузки расширений можно будет использовать сокращенный синтаксис .load sosex.

Эм... где мои очки?!

/posts/2018/windbg_course/windbg-init.png

WinDbg -- это инструмент с достаточно долгой историей без каких-либо изменений в его внешнем виде. Я нашел скриншот датированный 2001 годом, где WinDbg выглядел абсолютно идентично тому, как он выглядит сейчас после первого запуска. Шрифт по умолчанию абсолютно нечитаем на современных мониторах с высоким DPI (например, на моем 13 дюймовом FullHD мониторе) и высоко контрастная черно-белая тема может заставить слезится глаза, уже давно привыкшие современным темным темам типа Darcula. Изменить шрифт и его размер можно в меню View - Font.., а используемую цветовую схему в меню View - Options.... К сожалению, настройка цветовой схемы крайне нетривиальна, поскольку содержит более 60 трудно соотносимых с реальными элементами настроек, поэтому сейчас я рекомендую ограничиться только настройкой шрифта и взять одну из уже готовых цветовых схем, например, мой вариант Solarized Themes для WinDbg (она требует установленного шрифта Ubuntu Mono). Эти действия сделают внешний вид WinDbg намного ближе к современным текстовым редакторам и IDE.

/posts/2018/windbg_course/windbg-theme.png

Первое расследование

Для описанных далее действий можно использовать любой краш-дамп памяти любого .NET приложения. Но если у Вас его нет или Вы хотите немного облегчить себе жизнь, то можете взять специальное демо приложение, подготовленное для этого курса. Для сбора дампов памяти я предпочитаю использовать ProcDump от Window Sysinternals. Здесь надо упомянуть важный момент, поскольку внутренности CLR активно используют память из HEAP’a, "Mini" дампа памяти, собирающегося по умолчанию почти всеми программами, будет недостаточно для большинства случаев. Большинство команд просто будут сообщать о многочисленных ошибках при выполнении и не делать ничего полезного. Поэтому, нужно быть аккуратным при сборке дампов памяти и уточнять о необходимости "Full" дампа памяти. Если Вы решили использовать procdump и демо приложение, то необходимо запустить демо приложение, открыть командную строку из директории procdump'а, ввести procdump.exe -ma -e WinDbgCourse.exe и нажать на кнопку Basic Null Reference Crash в демо приложении. Через несколько секунд собранный дамп памяти с именем файла на подобии WinDbgCourse.exe_180527_142004 будет лежать в текущей директории. Затем нажмите на File - Open Crash Dump... (или просто нажмите на Ctrl+D) и загрузите дамп памяти в WinDbg (см. картинку выше). Поскольку собранный дамп памяти является краш-дампом WinDbg покажет нам подобное сообщение:

(2450.b88): Access violation - code c0000005 (first/second chance not available)

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

0:000> .loadby sos clr

0:000> !PrintException
c0000005 Exception in C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.PrintException debugger extension.
   PC: 0f99b8f3  VA: 00000000  R/W: 0  Parameter: 00000000

0:000> !PrintException -lines

Exception object: 027c934c
Exception type:   System.NullReferenceException
Message:          Object reference not set to an instance of an object.
InnerException:   <none>
StackTrace (generated):
*** WARNING: Unable to verify checksum for WinDbgCourse.exe
   SP       IP       Function
   001CEA98 02523133 WinDbgCourse!WinDbgCourse.BasicNullRefCrashCommand.Execute(System.Object)+0x73 [C:\Users\wwwzl\Dropbox\l-projects\windbg-course\BasicNullRefCrashCommand.cs @ 15]
   001CEAD0 56C6D2F1 PresentationFramework_ni!MS.Internal.Commands.CommandHelpers.CriticalExecuteCommandSource(System.Windows.Input.ICommandSource, Boolean)+0xb1
   001CEAF4 56D290C8 PresentationFramework_ni!System.Windows.Controls.Primitives.ButtonBase.OnClick()+0x54
   001CEB04 56BED59D PresentationFramework_ni!System.Windows.Controls.Button.OnClick()+0x55
...

Вывод уже содержит ссылку на исходный код, поэтому можно открыть его и понять, что проблема в попытке обращения к нулевому параметру parameter. Для того, чтобы подтвердить это, можно посмотреть на CLR стек:

0:000> !clrstack -a
OS Thread Id: 0xb88 (0)
Child SP       IP Call Site
001cea98 02523133 WinDbgCourse.BasicNullRefCrashCommand.Execute(System.Object) [C:\Users\wwwzl\Dropbox\l-projects\windbg-course\BasicNullRefCrashCommand.cs @ 15]
   PARAMETERS:
      this (0x001ceabc) = 0x0275cc48
      parameter (0x001ceab4) = 0x00000000
...

Ура! Мы нашли корень проблемы, поэтому можем подготовить патч и отправить обновленную версию приложения клиенту.

Погружаемся глубже

Существование этой части, говорит о том, что что-то пошло не так. Пропатченная версия до сих пор падает. И это не очень хорошая новость для нас, потому что мы знаем, что клиенты не любят обновления, которые ничего не исправляют. Поэтому постараемся быть аккуратнее в будущем. Во-первых, найдем IP (instruction pointer - указатель на текущую исполняемую инструкцию) из верхней строки трассировки стека (02523133) и попытаемся понять, что происходит в ассемблерном коде:

0:000> !U 02523133
Normal JIT generated code
WinDbgCourse.BasicNullRefCrashCommand.Execute(System.Object)
Begin 025230c0, size a4
...

C:\Users\wwwzl\Dropbox\l-projects\windbg-course\BasicNullRefCrashCommand.cs @ 15:
025230ed e846e9d953      call    PresentationFramework_ni+0x231a38 (562c1a38) (System.Windows.Application.get_Current(), mdToken: 0600028a)
025230f2 8945e8          mov     dword ptr [ebp-18h],eax
025230f5 8b4de8          mov     ecx,dword ptr [ebp-18h]
025230f8 3909            cmp     dword ptr [ecx],ecx
025230fa e879e9d953      call    PresentationFramework_ni+0x231a78 (562c1a78) (System.Windows.Application.get_MainWindow(), mdToken: 0600028c)
025230ff 8945e4          mov     dword ptr [ebp-1Ch],eax
02523102 e831e9d953      call    PresentationFramework_ni+0x231a38 (562c1a38) (System.Windows.Application.get_Current(), mdToken: 0600028a)
02523107 8945e0          mov     dword ptr [ebp-20h],eax
0252310a 8b4de0          mov     ecx,dword ptr [ebp-20h]
0252310d 3909            cmp     dword ptr [ecx],ecx
0252310f e864e9d953      call    PresentationFramework_ni+0x231a78 (562c1a78) (System.Windows.Application.get_MainWindow(), mdToken: 0600028c)
02523114 8945dc          mov     dword ptr [ebp-24h],eax
02523117 8b4ddc          mov     ecx,dword ptr [ebp-24h]
0252311a 3909            cmp     dword ptr [ecx],ecx
0252311c e82feedb53      call    PresentationFramework_ni+0x251f50 (562e1f50) (System.Windows.FrameworkElement.get_DataContext(), mdToken: 06000551)
02523121 8945d8          mov     dword ptr [ebp-28h],eax
02523124 8b55d8          mov     edx,dword ptr [ebp-28h]
02523127 b9a00bbf04      mov     ecx,4BF0BA0h (MT: WinDbgCourse.MainWindow)
0252312c e89f88bf0c      call    clr!JIT_IsInstanceOfClass (0f11b9d0)
02523131 8bc8            mov     ecx,eax
>>> 02523133 3909            cmp     dword ptr [ecx],ecx
02523135 e8e6e6db53      call    PresentationFramework_ni+0x251820 (562e1820) (System.Windows.Window.get_Title(), mdToken: 06000d20)
0252313a 8945d4          mov     dword ptr [ebp-2Ch],eax
0252313d 8b4dec          mov     ecx,dword ptr [ebp-14h]
02523140 8b01            mov     eax,dword ptr [ecx]
02523142 8b4028          mov     eax,dword ptr [eax+28h]
02523145 ff10            call    dword ptr [eax]
02523147 8945d0          mov     dword ptr [ebp-30h],eax
0252314a ff75d0          push    dword ptr [ebp-30h]
0252314d 8b4de4          mov     ecx,dword ptr [ebp-1Ch]
02523150 8b55d4          mov     edx,dword ptr [ebp-2Ch]
02523153 e884496054      call    PresentationFramework_ni+0xa97adc (56b27adc) (System.Windows.MessageBox.Show(System.Windows.Window, System.String, System.String), mdToken: 0600077b)
02523158 8945f0          mov     dword ptr [ebp-10h],eax
0252315b 90              nop
...

По имени функции ниже можно понять, что выполнение нашего кода даже не достигает инструкции, содержащей использование параметра parameter. Если мы внимательно сопоставим исходный код с ассемблерным листингом, то можно понять, что мы сделали ошибку и найти настоящий корень проблемы. И для того, чтобы сделать это чуть проще, мы можем также посмотреть на промежуточный IL код:

0:000> !ip2md 02523133

MethodDesc:   04bf1e2c
Method Name:  WinDbgCourse.BasicNullRefCrashCommand.Execute(System.Object)
Class:        04c3043c
MethodTable:  04bf1e48
mdToken:      06000006
Module:       00ab4014
IsJitted:     yes
CodeAddr:     025230c0
Transparency: Safe critical
Source file:  C:\Users\wwwzl\Dropbox\l-projects\windbg-course\BasicNullRefCrashCommand.cs @ 15

0:000> !dumpil 04bf1e2c
ilAddr = 000320dd
IL_0000: nop
IL_0001: call System.Windows.Application::get_Current
IL_0006: callvirt System.Windows.Application::get_MainWindow
IL_000b: call System.Windows.Application::get_Current
IL_0010: callvirt System.Windows.Application::get_MainWindow
IL_0015: callvirt System.Windows.FrameworkElement::get_DataContext
IL_001a: isinst WinDbgCourse.MainWindow
IL_001f: callvirt System.Windows.Window::get_Title
IL_0024: ldarg.1
IL_0025: callvirt System.Object::ToString
IL_002a: call System.Windows.MessageBox::Show
IL_002f: pop
IL_0030: ret

Параметр для команды !dumpil можно взять из строки MethodDesc: 04bf1e2c. Сейчас у нас достаточно информации для того, чтобы понять, что исходная строка содержит целых две проблемы: лишнее получение свойства из DataContext и нулевой параметр parameter. Плохие новости: это также просто только для очень простых примеров кода, таких как, например, в демо приложении. Хорошие новости: сейчас подходящее время для того, чтобы представить расширение SOSEX, которое может облегчить подобный анализ... и жизнь в принципе.

SOSEX

Для загрузки расширения SOSEX необходимости ввести .load sosex. К сожалению, все команды из расширений WinDbg используют одно пространство имен, начинающееся с префикса !, поэтому команда помощи SOS будет переписана командой помощи SOSEX:

0:000> .loadby sos clr
0:000> !help
-------------------------------------------------------------------------------
SOS is a debugger extension DLL designed to aid in the debugging of managed
programs. Functions are listed by category, then roughly in order of
importance. Shortcut names for popular functions are listed in parenthesis.
Type "!help <functionname>" for detailed info on that function.
...

0:000> .load sosex
This dump has no SOSEX heap index.
The heap index makes searching for references and roots much faster.
To create a heap index, run !bhi
0:000> !help
SOSEX - Copyright 2007-2015 by Steve Johnson - http://www.stevestechspot.com/
To report bugs or offer feedback about SOSEX, please email sjjohnson@pobox.com
...

Это корректно только для сокращенной формы для команды и всегда можно получить доступ к любой команде, если использовать её полную форму:

0:000> !sos.help
-------------------------------------------------------------------------------
SOS is a debugger extension DLL designed to aid in the debugging of managed
programs. Functions are listed by category, then roughly in order of
importance. Shortcut names for popular functions are listed in parenthesis.
Type "!help <functionname>" for detailed info on that function.
...

0:000> !sosex.help
SOSEX - Copyright 2007-2015 by Steve Johnson - http://www.stevestechspot.com/
To report bugs or offer feedback about SOSEX, please email sjjohnson@pobox.com
...

Эти команды особенно важны, поскольку они позволяют понять что делает любая из поддерживаемых команд и не читать глупые посты, типа этого:

0:000> !sos.help clrstack
-------------------------------------------------------------------------------
!CLRStack [-a] [-l] [-p] [-n]
!CLRStack [-a] [-l] [-p] [-i] [variable name] [frame]

CLRStack attempts to provide a true stack trace for managed code only. It is
handy for clean, simple traces when debugging straightforward managed
programs. The -p parameter will show arguments to the managed function. The
-l parameter can be used to show information on local variables in a frame.
SOS can't retrieve local names at this time, so the output for locals is in
the format <local address> = <value>. The -a (all) parameter is a short-cut
for -l and -p combined.
...

Если Вы вдруг, не смотря ни на что, решили продолжить чтение этого поста, можно перейти к следующей очень полезной команде (я специально не привожу описание параметров команды и надеюсь, что это достаточная мотивация выполнить !sosex.help muf и прочитать все там -_-):

0:000> !muf 02523133
...
{
   IL_0000: nop
        025230ec 90              nop
MessageBox.Show(Application.Current.MainWindow, (Application.Current.MainWindow.DataContext as MainWindow).Title, parameter.ToString());
   IL_0001: call System.Windows.Application::get_Current
        025230ed e846e9d953      call    PresentationFramework_ni+0x231a38 (562c1a38)  [System.Windows.Application.get_Current()]
        025230f2 8945e8          mov     dword ptr [ebp-18h],eax
   IL_0006: callvirt System.Windows.Application::get_MainWindow
        025230f5 8b4de8          mov     ecx,dword ptr [ebp-18h]
        025230f8 3909            cmp     dword ptr [ecx],ecx
   IL_0006: callvirt System.Windows.Application::get_MainWindow
        025230fa e879e9d953      call    PresentationFramework_ni+0x231a78 (562c1a78)  [System.Windows.Application.get_MainWindow()]
        025230ff 8945e4          mov     dword ptr [ebp-1Ch],eax
   IL_000b: call System.Windows.Application::get_Current
   IL_000b: call System.Windows.Application::get_Current
        02523102 e831e9d953      call    PresentationFramework_ni+0x231a38 (562c1a38)  [System.Windows.Application.get_Current()]
        02523107 8945e0          mov     dword ptr [ebp-20h],eax
   IL_0010: callvirt System.Windows.Application::get_MainWindow
        0252310a 8b4de0          mov     ecx,dword ptr [ebp-20h]
        0252310d 3909            cmp     dword ptr [ecx],ecx
   IL_0010: callvirt System.Windows.Application::get_MainWindow
        0252310f e864e9d953      call    PresentationFramework_ni+0x231a78 (562c1a78)  [System.Windows.Application.get_MainWindow()]
        02523114 8945dc          mov     dword ptr [ebp-24h],eax
   IL_0015: callvirt System.Windows.FrameworkElement::get_DataContext
        02523117 8b4ddc          mov     ecx,dword ptr [ebp-24h]
        0252311a 3909            cmp     dword ptr [ecx],ecx
   IL_0015: callvirt System.Windows.FrameworkElement::get_DataContext
        0252311c e82feedb53      call    PresentationFramework_ni+0x251f50 (562e1f50)  [System.Windows.FrameworkElement.get_DataContext()]
        02523121 8945d8          mov     dword ptr [ebp-28h],eax
   IL_001a: isinst WinDbgCourse.MainWindow
        02523124 8b55d8          mov     edx,dword ptr [ebp-28h]
        02523127 b9a00bbf04      mov     ecx,4BF0BA0h
        0252312c e89f88bf0c      call    clr!JIT_IsInstanceOfClass (0f11b9d0)
        02523131 8bc8            mov     ecx,eax
>>>>>>>>02523133 3909            cmp     dword ptr [ecx],ecx
   IL_001f: callvirt System.Windows.Window::get_Title
        02523135 e8e6e6db53      call    PresentationFramework_ni+0x251820 (562e1820)  [System.Windows.Window.get_Title()]
        0252313a 8945d4          mov     dword ptr [ebp-2Ch],eax
...

Великолепно! SOSEX предоставляет нам намного лучший результат с использованием всего одной команды, намного более лучший чем мы можем получить используя три команды SOS.

Заключение

Это только первая часть курса, которая посвящена очень базовому сценарию анализа краш-дампа. И несмотря на простоту и банальность этого сценария, широко известный отладчик из Visual Studio Debugger был бы абсолютно бесполезен в этом расследовании. Список возможных сценариев, которые могут быть проанализированы с применением WinDbg ужасен: краш-дампы с некорректной обработкой исключений, подвисания приложений, дедлоки, утечки памяти и ресурсов и многое другое. Я надеюсь, что эта часть показала, что WinDbg ненамного сложнее отладчика из Visual Studio, и Вы включите WinDbg в стандартный набор ваших инструментов.

Комментарии

Comments powered by Disqus