Как читать аварийные дампы в WinDbg + SOS — практика анализа после сбора

· Обновлено: · · WinDbg, SOS, Аварийный дамп, .NET, CSharp, Отладка, PDB, Расследование сбоев, Техническая консультация

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619964)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). Как читать аварийные дампы в WinDbg + SOS — практика анализа после сбора. KomuraSoft LLC. https://comcomponent.com/ru/blog/windbg-sos-crash-dump-analysis/

DOI (зарегистрированный архив)
10.5281/zenodo.21619964
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21619965

В предыдущей статье «Введение в сбор аварийных дампов Windows» мы разобрали, как «снять» дамп средствами WER LocalDumps, ProcDump и MiniDumpWriteDump. Сам по себе снятый дамп ничего не объясняет. Он становится материалом расследования только когда вы сами разбираете, какой поток упал и почему, или что именно продолжает удерживать память.

Эта статья продолжает тему сбора и сосредоточена только на том, как реально читать уже снятый дамп в WinDbg с расширением SOS. Речь пойдёт об установке и настройке символов, о загрузке расширения SOS, без которого .NET-приложение не прочитать, о том, «на что смотреть и как решать» по основным командам вроде !clrstack и !dumpheap -stat, о !analyze -v для нативного сбоя и о том, когда вместо WinDbg брать dotnet-dump analyze.

Что предполагается в этой статье

Пункт Содержание
Кому статья У вас уже есть аварийный дамп (.dmp), и вы собираетесь читать его содержимое как разработчик или сопровождение Windows / .NET-приложения
Что нужно знать Уметь читать трассировку стека на C#. Опыта WinDbg не требуется
Предыдущая статья Как снимать дамп разобрано в статье про сбор. Эта статья начинается с «дамп уже есть». Даже если ту статью не читали, по шагам раздела 2.4 можно самим сделать учебный дамп и идти дальше
Инструменты WinDbg (актуальная версия), расширение SOS, при необходимости dotnet-dump

Дальше без пояснений используются четыре сокращения.

Сокращение Полностью Что это значит в статье
WER Windows Error Reporting Штатный механизм сообщения об ошибках Windows. Через настройку LocalDumps дамп при сбое можно сохранять автоматически (шаги настройки — в статье про сбор)
PDB Program Database Файл символов, который создаётся при сборке. В нём таблица соответствия имён исходных файлов и номеров строк (глава 8)
CLR Common Language Runtime Сам исполняющий движок .NET. Загружается как clr.dll / coreclr.dll1
SOS Son of Strike Расширение отладчика, которым читают внутренности этого CLR. Загружаем в главе 32

1. Сначала вывод

  • Главный инструмент разбора дампов — WinDbg (актуальная версия, раньше называлась WinDbg Preview). Его ставят командой winget install Microsoft.WinDbg или из Microsoft Store; работает на x64 и ARM64 начиная с Windows 10 Anniversary Update (1607) и на Windows 11.3
  • Даже если символы (PDB) не загрузились, команды вроде !clrstack, !dumpheap -stat, !gcroot работают напрямую по метаданным CLR и данным кучи. Пропадают только имена исходных файлов и номера строк управляемого кода, а также имена символов для нативных кадров. Если же нужно дойти до строки исходника, это уже другой разговор: в _NT_SYMBOL_PATH обычно указывают и публичный сервер символов Microsoft, и каталог своих PDB.4 Как настроить символы — глава 2. Что видно с PDB и без, и как это держать в эксплуатации — глава 8.
  • В дампах приложений .NET (Framework / Core / 5+) сведения об управляемом коде появляются только после загрузки расширения SOS. Одной нативной командой k (вывод стека) код на C# не проследить.2
  • Типовых сценариев разбора три. Упал по исключению — !clrstack!pe, память продолжает расти — !dumpheap -stat!gcroot, нативный сбой — !analyze -v.
  • Если WinDbg не нужен, есть dotnet-dump analyze. Большинство команд SOS в нём работают как есть, но кадры нативного стека он не показывает. Для разбора только управляемого кода, без нативных DLL и COM, этот вариант проще поставить.5
  • Здесь описано «как читать», а не «как снимать». Способы получить дамп (WER / ProcDump / MiniDumpWriteDump) — в статье про сбор, а как спроектировать сопоставление логов и дампа в момент сбоя — в «Проектирование, при котором логи и дампы остаются при сбое».

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 19, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Ставим WinDbg и прописываем символы

2.1 Установка

Актуальный WinDbg ставится одним из следующих способов.3

winget install Microsoft.WinDbg

Через Microsoft Store ставится тот же движок: команды, расширения и рабочий процесс общие. После установки обновления приходят сами (в фоне — при установке из Store или напрямую; при установке через winget — командой winget upgrade Microsoft.WinDbg), поэтому разницей поведения между версиями почти не приходится заниматься.3

2.2 Как открыть дамп

windbg -z C:\CrashDumps\MyApp\MyApp_20260702_101500.dmp

-z — параметр, которым при запуске задают файл дампа. Из GUI открывают пункт открытия дампа в меню File (в WinDbg Classic он называется Open crash dump, сочетание клавиш Ctrl+D).6

Снимков экрана в этой статье нет. Вместо них текстом указаны, куда вводить команды и как называются пункты меню. После открытия дампа появляется окно инструментов Command,7 и приглашение вроде 0:000> в самом низу — это поле ввода. Все команды с ! дальше вводят в это окно Command (в разборах Microsoft тоже пишут в виде 0:000> !analyze -v).8 0:000 в приглашении значит «выбран поток 0 процесса 0»; если в главе 4 переключить поток командой ~5s, приглашение станет 0:005>.

2.3 Настройка пути к символам

Где отладчик Windows ищет файлы символов (PDB), задают переменной окружения _NT_SYMBOL_PATH или командой .sympath внутри сессии.4 На практике базовый вариант — указать и публичный сервер символов Microsoft, и каталог своих PDB.

.symfix C:\Symbols\Microsoft
.sympath+ C:\Symbols\MyApp
.reload
  • .symfix — короткая команда, которая прописывает путь к публичному серверу символов Microsoft (https://msdl.microsoft.com/download/symbols) вместе с указанным локальным кэшем. Символы штатных DLL ОС качаются оттуда сами.9
  • .sympath+ дописывает к уже заданному пути каталог своих PDB. PDB своего кода нужно готовить самим — на сервере символов Microsoft их нет.
  • .reload перечитывает символы и позволяет проверить их состояние в списке модулей.

Чтобы задать путь постоянно через переменную окружения, используют такую форму. Она удобнее для автоматического разбора на CI или сборочных серверах.

set _NT_SYMBOL_PATH=srv*C:\Symbols\Microsoft*https://msdl.microsoft.com/download/symbols;C:\Symbols\MyApp

Читаются ли символы, проверяют командой lm (loaded modules): выводят список модулей и смотрят, стоит ли у нужного модуля статус pdb symbols. Если остался deferred, символы ещё не разрешены.

2.4 Учебный дамп своими руками

Разбирать сразу дамп боевого сбоя тяжело, поэтому полезно завести мини-приложение, которое падает намеренно, и на нём пройти шаги этой статьи. Если под рукой нет дампа, который можно читать, начните отсюда.

Создайте одно консольное приложение (.NET 8 / C# 12. В проекте, полученном командой dotnet new console -n CrashLab, целиком замените Program.cs на следующее).

// Program.cs
using System;
using System.Collections.Generic;

// (A) Для главы 5, !dumpheap -stat / !gcroot: массив, который растёт и не освобождается
var cache = new List<byte[]>();
for (int i = 0; i < 2000; i++)
{
    cache.Add(new byte[100_000]);
}
Console.WriteLine($"cached: {cache.Count} blocks");

// (B) Для главы 4, !clrstack / !pe: необработанное исключение, которое роняет процесс
string? name = null;
Console.WriteLine(name!.Length);   // здесь NullReferenceException

Дальше через ProcDump из Sysinternals запускают это приложение с настройкой «при необработанном исключении писать полный дамп». -ma — полный дамп, -e — «писать дамп, когда процесс встретил необработанное исключение», -x <каталог вывода> <исполняемый файл> — ProcDump сам запускает указанный файл и следит за ним.10

procdump.exe -accepteula -ma -e -x C:\CrashDumps CrashLab.exe

Если процесс уже запущен, передают имя процесса (или PID), например procdump.exe -accepteula -ma -e CrashLab.exe. Имя файла дампа по умолчанию — PROCESSNAME_YYMMDD_HHMMSS.dmp.10

Полученный .dmp открывают в WinDbg по шагам раздела 2.2 — и главы 4 и 5 можно отработать на одном файле. Если достаточно разбора только управляемого кода, такой же дамп снимает dotnet-dump collect --name CrashLab (глава 7). Но dotnet-dump collect снимает снимок в произвольный момент, поэтому момент падения ловят ProcDump или WER LocalDumps.

3. Загружаем расширение SOS

В дампе .NET-приложения одними нативными командами WinDbg не увидеть содержимое управляемой кучи, кадры стека C# и содержимое объектов исключений. Этот пробел закрывает расширение SOS (Son of Strike). Через команды SOS исследуют кучу, ищут её повреждение, выводят внутренние типы данных рантайма и смотрят состояние выполняющегося управляемого кода.2

3.1 Чем отличаются рантаймы

От того, перед вами .NET Framework или .NET (Core) / .NET 5+, зависят и сам рантайм, который нужно взять, и откуда берётся SOS.

Цель Модуль рантайма Команда загрузки
.NET Framework clr.dll .loadby sos clr
.NET Core / .NET 5+ coreclr.dll .loadby sos coreclr

.loadby ищет DLL расширения (sos.dll) в том же каталоге, что и указанный модуль (clr или coreclr), и загружает её оттуда. Плюс в том, что без полного пути подхватывается версия SOS, соответствующая окружению, где снимали дамп.1

Начиная с версии 10.0.18317.1001 WinDbg и cdb, обнаружив, что целевой процесс загрузил coreclr.dll (или libcoreclr.so на Linux/macOS), сами подтягивают расширение для .NET из Microsoft Extension Gallery.1 Команду .loadby выше набирают вручную, когда автозагрузка не сработала или отладчик старой версии.

3.2 Если SOS не находится

В окружениях, где автозагрузка не работает, SOS ставят локально инструментом dotnet-sos.

dotnet tool install --global dotnet-sos
dotnet-sos install

После установки его можно загрузить вручную прямо в WinDbg (на старых отладчиках это иногда единственный путь).11

.load %USERPROFILE%\.dotnet\sos\sos.dll

3.3 Проверяем, что загрузка прошла

!sos.help

Либо для целей на Core пробуют !Threads, для Framework — !sosstatus: если команда не падает с ошибкой и что-то возвращает, загрузка удалась. Если здесь приходит ошибка вроде Unable to find module, почти всегда виноваты путь к символам или несовпадение рантайма (другая разрядность или версия между машиной, где снимали дамп, и локальным рантаймом). Застрять именно здесь, ещё до команд следующей главы, на практике не редкость.

4. Читаем исключение и стек — !clrstack и !pe

Первый шаг для дампа, который упал из-за необработанного исключения.

!threads

Сначала через !Threads (в lldb это псевдоним clrthreads) смотрят список управляемых потоков и колонку Exception у каждого.12 Если у потока есть исключение, переключаются на него.

Вывод — таблица, по строке на поток: порядковый номер в отладчике, идентификатор потока CLR, идентификатор потока ОС, плюс колонка Domain с доменом приложения, колонка APT с режимом апартаментов COM и колонка Exception с последним исключением, которое бросил этот поток.12 Сначала смотрят только колонку Exception. Строка, где стоит имя типа вроде System.NullReferenceException, и есть объект разбора; порядковый номер слева в этой строке запоминают.

~5s
!clrstack

~<номер>s переключает на поток с запомненным порядковым номером. Следом !CLRStack показывает трассировку стека только для управляемого кода.13 Если нужны ещё аргументы и переменные, добавляют -a (сокращение, объединяющее -l и -p).

!clrstack -a

Вывод выглядит так (фрагмент примера clrstack с Microsoft Learn. Это сессия dotnet-dump, но !clrstack в WinDbg показывает то же).5

OS Thread Id: 0x573d (0)
    Child SP               IP Call Site
00007FFD28B42C58 00007fb22c1a8ed9 [HelperMethodFrame_PROTECTOBJ: 00007ffd28b42c58] System.RuntimeMethodHandle.InvokeMethod(System.Object, System.Object[], System.Signature, Boolean, Boolean)
00007FFD28B42E20 00007FB1B18D33ED SymbolTestApp.Program.Foo4(System.String) [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 54]
00007FFD28B42ED0 00007FB1B18D2FC4 SymbolTestApp.Program.Foo2(Int32, System.String) [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 29]
00007FFD28B42F00 00007FB1B18D2F5A SymbolTestApp.Program.Foo1(Int32, System.String) [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 24]
00007FFD28B42F30 00007FB1B18D168E SymbolTestApp.Program.Main(System.String[]) [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 19]
00007FFD28B43210 00007fb22aa9cedf [GCFrame: 00007ffd28b43210]

Какие строки смотреть:

  1. Читают только колонку Call Site, снизу вверх. Внизу вызывающий, вверху вызванный. В этом примере путь к месту падения читается как MainFoo1Foo2Foo4.
  2. В квадратных скобках [... .cs @ 54] — имя исходного файла и номер строки. Если они есть, символы разрешились. Что делать, если их нет, — глава 8.
  3. Строки [HelperMethodFrame_...] и [GCFrame: ...] пропускают. Это кадры внутри рантайма, они не соответствуют напрямую ошибке в своём коде.
  4. Колонки Child SP и IP в обычном разборе не нужны. Это указатель стека и указатель инструкций; их передают в команды вроде !dumpstackobjects, когда нужен адрес.
  • CLRStack перечисляет управляемые кадры напрямую из метаданных CLR, поэтому от наличия символов не зависит, появятся ли кадры. При нехватке PDB пропадают только имя исходного файла и номер строки из пункта 2 выше — сами кадры никогда не опускаются.13 Всё про символы собрано в главе 8: если номера строк не видны, смотрите туда.
  • Если не видно ни одного кадра своего кода, дело не в символах, а в одном из следующего: выбран не тот поток, у него нет исключения (ошибка выбора потока); падение произошло целиком на нативной стороне, и управляемых кадров там просто нет; либо тип дампа (например Mini) не содержит достаточно сведений о стеке на тот момент.

Дальше смотрят сам объект исключения.

!pe

!PrintException (кратко !pe) без адреса показывает последнее исключение, брошенное в текущем потоке. Видны имя типа, сообщение, внутренние исключения (их даёт -nested) и строка трассировки стека.14 Вывод выглядит так (тоже фрагмент примера с Microsoft Learn, с -lines, чтобы вышли сведения об исходниках).5

Exception object: 00007fb18c038590
Exception type:   System.Reflection.TargetInvocationException
Message:          Exception has been thrown by the target of an invocation.
InnerException:   System.Exception, Use !PrintException 00007FB18C038368 to see more.
StackTrace (generated):
SP               IP               Function
00007FFD28B42E20 00007FB1B18D33ED SymbolTestApp.dll!SymbolTestApp.Program.Foo4(System.String)+0x15d [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 54]
00007FFD28B42F30 00007FB1B18D168E SymbolTestApp.dll!SymbolTestApp.Program.Main(System.String[])+0x6e [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 19]

StackTraceString: <none>
HResult: 80131604

Смотрят верхние четыре строки. Exception type — какое это исключение, Message — пояснение, InnerException — «настоящая причина». Когда, как в этом примере, виден TargetInvocationException, тип на поверхности — лишь обёртка вызова через рефлексию, поэтому адрес из строки InnerException передают в !pe 00007FB18C038368 и открывают внутреннее исключение (ту же информацию даёт !pe -nested).14 HResult — значение HRESULT этого исключения, а всё после StackTrace (generated) — трассировка, которую несёт само исключение. Разница: !clrstack — «стек сейчас», а это — «стек в момент, когда исключение бросили». Если они расходятся, имеет смысл проверить, не поймали ли исключение и не бросили ли его заново в другом месте.

Чем меньше само имя типа говорит о причине — как у System.NullReferenceException, — тем нужнее сверка со значениями локальных переменных из !clrstack -a.

5. Идём по куче и утечкам — !dumpheap -stat и !gcroot

Эти команды — центр разборов в духе «память понемногу растёт и через часы или дни всё падает». Как отличить ожидание GC от настоящей утечки, подробно разобрано в «Как в .NET отличить ожидание GC от утечки памяти». Эта статья — продолжение той: читаем один дамп и доходим до того, кто удерживает объекты.

!dumpheap -stat

Параметр -stat показывает только статистическую сводку по управляемой куче. Типы идут примерно в порядке убывания числа экземпляров и суммарного размера, поэтому сначала находят «тип, который занимает больше всего места».15 Вывод выглядит так (реальный вывод из учебного разбора утечки памяти на Microsoft Learn).15

Statistics:
              MT    Count    TotalSize Class Name
00007f6c1eeefba8      576        59904 System.Reflection.RuntimeMethodInfo
00007f6c1dc021c8     1749        95696 System.SByte[]
00000000008c9db0     3847       116080      Free
00007f6c1e784a18      175       128640 System.Char[]
00007f6c1dbf5510      217       133504 System.Object[]
00007f6c1dc014c0      467       416464 System.Byte[]
00007f6c21625038        6      4063376 testwebapi.Controllers.Customer[]
00007f6c20a67498   200000      4800000 testwebapi.Controllers.Customer
00007f6c1dc00f90   206770     19494060 System.String
Total 428516 objects

Какие строки смотреть:

  1. Читать снизу. Строки идут по возрастанию TotalSize (в байтах), поэтому нижняя — тип, который занимает больше всего кучи. Здесь System.String занимает около 19 MB, Customer — около 4.8 MB.
  2. Колонки Count и TotalSize смотрят раздельно. Несколько огромных массивов (велик только размер) и сотни тысяч мелких объектов (велико только число) ведут к разным местам в коде.
  3. Строка Free — не утечка. Это уже собранные, но ещё не повторно использованные промежутки; по ней судят о фрагментации.
  4. Колонку MT (адрес MethodTable) запоминают. Если передать её в !dumpheap -mt <MT>, получите список адресов отдельных экземпляров этого типа.

На практике чаще встречаются два варианта.

  • Растёт сам прикладной класс (например, сотни тысяч MyApp.Models.Customer) — где-то осталась сильная ссылка, которая продолжает его удерживать
  • Аномально много только System.String или массивов — часто это внутренние данные прикладного класса, который стоит выше в списке; обычно быстрее сначала заподозрить сам прикладной класс, чем сразу разбирать отдельные экземпляры

Когда цель сужена, берут адреса отдельных экземпляров.

!dumpheap -type MyApp.Models.Customer

-type — частичное совпадение имени типа, -mt — адрес MethodTable, запомненный на предыдущем шаге. В обоих случаях вывод — список «по строке на экземпляр».15

         Address               MT     Size
00007f6ad09421f8 00007f6c20a67498       24
00007f6ad0942210 00007f6c20a67498       24

Здесь нужна только левая колонка Address. Это значение передают в следующую команду.

Дальше выясняют, почему этот объект так и не собрал GC.

!gcroot 000001a2b3c4d5e0

!GCRoot ищет по всей управляемой куче и таблице хендлов и перечисляет корни, через которые можно дойти до указанного объекта (переменные на стеке, статические поля, GC-хендлы и т. д.).16 Вывод выглядит так (тоже реальный вывод с Microsoft Learn).15

Thread 3f68:
    00007F6795BB58A0 00007F6C1D7D0745 testwebapi.Controllers.CustomerCache.GetAll()
        rbx:  (interior)
            ->  00007F6BDFFFF038 System.Object[]
            ->  00007F69D0033570 testwebapi.Controllers.Processor
            ->  00007F69D0033588 testwebapi.Controllers.CustomerCache
            ->  00007F69D00335A0 System.Collections.Generic.List`1[[testwebapi.Controllers.Customer]]
            ->  00007F6C000148A0 testwebapi.Controllers.Customer[]
            ->  00007F6AD0942258 testwebapi.Controllers.Customer

Found 1 root.

Какие строки смотреть:

  1. Первая строка-заголовок — тип корня. Thread 3f68: — переменная на стеке потока, HandleTable: — GC-хендл (рядом бывает вид (pinned handle) и другие виды). Если корень — статическое поле, здесь появляется имя типа, которому поле принадлежит.
  2. Цепочку -> читают сверху вниз. Это цепочка «кто кого удерживает», в конце — указанный объект.
  3. Виновник не в конце, а в коллекции или долгоживущем объекте посреди цепочки. В этом примере видно, что CustomerCache так и не отпустил List<Customer>. Пока это не исправить, сколько ни смотри на конечный Customer, проблема не уйдёт.
  4. В последней строке Found N root. проверяют число корней. Если корней несколько, объект не соберут, пока не разорвут все. Нельзя починить один и считать дело закрытым.

Если в выводе видно статическое поле, используемое как кэш, или подписку на обработчик события, это первый кандидат на то, что отписку забыли. Типичный пример такого кода — «положили в кэш и не освобождаем»:

public static class CustomerCache
{
    // Статический словарь без пути удаления записей — только растёт
    private static readonly Dictionary<int, Customer> _cache = new();

    public static void Add(Customer c) => _cache[c.Id] = c;
}

Если в выводе !gcroot появляется статический контейнер вроде CustomerCache, на стороне кода это повод подумать о сроке жизни записей, ограничении числа элементов или переходе на WeakReference.15

6. Автоматический разбор нативного сбоя — !analyze -v

Для нативного сбоя (нарушение доступа и тому подобное), где замешаны DLL на C++, COM или SDK вендора, начинают с этой команды.

!analyze -v

!analyze — команда расширения, которая сама разбирает сбой или исключение; -v включает подробный вывод.8 Вывод занимает десятки строк; если взять только основные поля из примера пользовательского режима на Microsoft Learn, получается следующее.8

FAULTING_IP:
ntdll!PropertyLengthAsVariant+73
77f97704 cc               int     3

EXCEPTION_RECORD:  ffffffff -- (.exr ffffffffffffffff)
ExceptionAddress: 77f97704 (ntdll!PropertyLengthAsVariant+0x00000073)
   ExceptionCode: 80000003 (Break instruction exception)
  ExceptionFlags: 00000000

BUGCHECK_STR:  80000003

PROCESS_NAME:  MyApp.exe

STACK_TEXT:
0006b9dc 01050963 00000000 0006ba04 000603fd ntdll!PropertyLengthAsVariant+0x73
0006b9f0 010509af 00000002 0006ba04 77e1a449 MyApp!FatalErrorBox+0x55 [D:\source_files\MyApp\util.c @ 541]
0006da04 01029f4e 01069850 0000034f 01069828 MyApp!ShowAssert+0x47 [D:\source_files\MyApp\util.c @ 579]
0006ff70 01062cbf 00000001 00683ed8 00682b88 MyApp!main+0x1e6 [D:\source_files\MyApp\MyApp.c @ 263]

FOLLOWUP_IP:
MyApp!FatalErrorBox+55
01050963 5e               pop     esi

SYMBOL_NAME:  MyApp!FatalErrorBox+55

MODULE_NAME:  MyApp

IMAGE_NAME:  MyApp.exe

В выводе в первую очередь смотрят три группы полей.

  • EXCEPTION_CODE / BUGCHECK_STR: какого рода аномалия (нарушение доступа, переполнение стека и т. п.). В примере выше это 80000003 (исключение инструкции останова), и в строке ExceptionCode внутри EXCEPTION_RECORD рядом дано пояснение. В дампе пользовательского режима в BUGCHECK_STR тоже попадает этот код исключения (имя поля пришло из ядра и путает, но bugcheck здесь — не синий экран).8
  • FAULTING_IP / FOLLOWUP_IP: адрес инструкции, на которой реально упали, и соответствующий модуль и имя функции. FAULTING_IP — «где упали», FOLLOWUP_IP — место, которое !analyze считает вероятной причиной; чаще они не совпадают. В примере падение в ntdll, а предполагаемая причина — свой MyApp!FatalErrorBox.
  • MODULE_NAME / IMAGE_NAME: свой это модуль или чужая DLL. Вместе с SYMBOL_NAME здесь указан модуль, которому принадлежит FOLLOWUP_IP.

В STACK_TEXT верхняя строка — самая внутренняя (место падения), ниже — вызывающие. Самый короткий способ чтения: сверху найти первую строку со своим именем модуля (в примере MyApp!) и посмотреть [имя файла @ номер строки].

Если падение не в своём модуле, а внутри DLL вендора, чтобы идти дальше, нужен PDB вендора (его обычно нет). На практике реалистичная точка остановки — подняться до места вызова (последних аргументов, которые передал свой код) и проверить, не были ли значения некорректными. В поле STACK_TEXT вывода !analyze -v смотрят, что именно вызвали из своего кода сразу перед падением.

!analyze можно применять и к дампам без исключения. Если подозревают зависание, выбирают нужный поток и выполняют следующую команду — она разберёт отношения блокировки между потоками.

!analyze -hang

7. Вариант без WinDbg — dotnet-dump analyze

Если нужно смотреть только управляемый код .NET Core / .NET 5+ (без нативных DLL и COM), есть более лёгкий, чем WinDbg, вариант — dotnet-dump.

dotnet tool install --global dotnet-dump
dotnet-dump analyze C:\CrashDumps\MyApp\MyApp_20260702_101500.dmp

Подкоманда analyze открывает интерактивную сессию с уже установленным SOS, где большинство показанных здесь команд — clrstack, dumpheap, gcroot и другие — можно вызывать напрямую, без префикса !.5

Ориентир для выбора такой:

Аспект WinDbg + SOS dotnet-dump analyze
Кадры нативного стека Видны Не видны (только управляемый код)5
Автоматический нативный разбор через !analyze -v Есть Нет
Дампы с Linux Можно разбирать в WinDbg на Windows (для x64-дампа — x64-версия, для Arm64-дампа — x64-версия, для x86-дампа — x86-версия)17 Поддерживается (нужен инструмент той же разрядности, что и платформа)17
Дампы с macOS Не поддерживается (поддержка Linux-дампов в WinDbg macOS не включает) Поддерживается (.NET 5 и новее)5
Насколько легко поставить Установщик или winget Одна команда dotnet global tool
Встраивание в кросс-платформенный CI Трудоёмко Проще

Практическая граница: если «могут быть задействованы COM, P/Invoke или нативные DLL» — WinDbg; если «утечка памяти в чисто управляемом коде, и нужно гонять это в CI и на разных платформах» — dotnet-dump analyze. Оба делят набор команд SOS, поэтому команды, выученные в одном, почти без изменений работают в другом. Учтите: дамп, снятый на macOS, WinDbg не берёт, поэтому остаются только dotnet-dump (или LLDB).

8. Если символы не читаются, разбор почти не начинается

Всё про символы (PDB) собрано в этой главе. В предыдущих главах сказано только «если PDB есть, появятся номера строк»; подробности — только здесь.

Сначала важное: часть шагов выше вполне работает и без корректно загруженных PDB. !clrstack, !dumpheap -stat и !gcroot читают метаданные CLR и данные кучи напрямую, поэтому кадры и сведения о типах видны и без PDB. Без PDB пропадают имена исходных файлов и номера строк управляемого кода и имена символов нативных кадров и нативных модулей (вместо имени функции виден только адрес).

Что нужно увидеть Без PDB С PDB
Управляемые кадры стека (имя метода, типы аргументов) Есть Есть
Имя исходного файла и номер строки управляемого кода (!clrstack) Нет Есть13
Тип, сообщение и внутренние исключения (!pe) Есть Есть
Имена типов, число и размер на куче (!dumpheap -stat) Есть Есть
Путь ссылок до корня GC (!gcroot) Есть Есть
Имена функций нативных кадров (k / STACK_TEXT в !analyze -v) Нет (только адреса) Есть

То есть в ситуации «под рукой только дамп и исполняемый файл, хочется хотя бы понять, что случилось» нормально начинать с !threads!clrstack, не упираясь заранее в поиск PDB. Если же нужно дойти до строки исходника и точно указать место причины, это уже другой разговор. В главе 2 мы добавили путь к своим PDB через .sympath+, но на практике одно «неизвестно, где PDB, соответствующий разосланным EXE/DLL» останавливает разбор до строки исходника чаще, чем можно ожидать по статье про сбор.

Что именно PDB содержит и чего в нём нет, Portable PDB и Source Link (в сборку встраивают метаданные системы контроля версий, и отладчик может сам взять исходники на момент соответствующего коммита) собраны в «Что такое PDB».18 Если разбор дампов входит в постоянную работу, хранить PDB каждой сборки и включить Source Link — подготовка не менее важная, чем сама настройка сбора дампов. Пренебрежёте этим — и в выводе !clrstack не будет ни одной строки исходника, останутся только адреса и имена типов.

9. Пример — разбор дампа при утечке хендлов

В статье «Расследование падения приложения промышленной камеры при долгой работе — часть про утечку хендлов» описан случай приложения управления промышленной камерой, которое внезапно падало после долгой работы: главным виновником оказалась не утечка памяти, а утечка хендлов. В таких разборах дамп полезен в следующей комбинации.

  1. !dumpheap -stat подтверждают, что управляемая куча в порядке (число и размер по типам не растут без конца)
  2. Если при этом число хендлов процесса всё равно растёт, утекают не управляемые объекты, а хендлы ОС (файлы, события, хендлы, которые SDK камеры выделяет внутри себя, и т. п.)
  3. Если ещё жив управляемый объект-обёртка с SafeHandle, через !gcroot идут по его корню GC и находят место, где ссылка осталась там, где её уже следовало отпустить

Иными словами, !dumpheap -stat работает как развилка: это рост управляемой кучи или нет. Как только ясно, что нет, центр разбора смещается к инструментам поиска аномалий на нативной границе, вроде Application Verifier. Как собрать такую инфраструктуру тестов нештатных сценариев, разобрано в «Инфраструктура тестов нештатных сценариев Windows на Application Verifier». Разбор дампа отвечает за «состояние прямо сейчас», Application Verifier — за «воспроизвести аномалию заранее»; оба вместе — обычная практика при сбоях после долгой работы.

10. Итог

Читать аварийный дамп осваивают дольше, чем снимать, но типовых сценариев немного.

  1. Ставят WinDbg и в путь к символам включают и публичный сервер символов Microsoft, и свои PDB (глава 2)
  2. Для .NET-приложения загружают расширение SOS (.loadby sos clr / .loadby sos coreclr либо автозагрузка. Глава 3)
  3. Упал по исключению — !clrstack!pe, растёт память — !dumpheap -stat!gcroot, нативный сбой — !analyze -v (главы 4–6)
  4. Если задача — только управляемый код, стоит рассмотреть более лёгкий dotnet-dump analyze (глава 7)

И основа всего этого — учёт PDB и символов. Если хранение PDB каждой сборки и включение Source Link решить вместе с настройкой сбора дампов, время разбора при реальном сбое меняется очень сильно. Если разобрать своими силами трудно или нет времени, пришлите дампы и логи комплектом — разберём у себя.

Похожие статьи

Смежные направления консультаций

Komura Software LLC занимается поиском причин сбоев на стыке дампов и логов, локализацией отказов, которые проявляются только после долгой работы, и консультациями по самому устройству хранения и разбора дампов и PDB.

Справочные ссылки

  1. Microsoft Learn, Debugging Managed Code Using the Windows Debugger. Рантайм .NET Framework — clr.dll, рантайм .NET Core/.NET 5+ — coreclr.dll; загрузка расширения из соседнего каталога через .loadby; автозагрузка в WinDbg 10.0.18317.1001 и новее.  2 3

  2. Microsoft Learn, SOS debugging extension. Расширение SOS собирает сведения об управляемой куче, ищет повреждение кучи и выводит внутренние типы данных рантайма; синтаксис в WinDbg — ![command] 2 3

  3. Microsoft Learn, Install the Windows debugger. Установка WinDbg через winget или Microsoft Store, поддерживаемые ОС (Windows 10 1607 и новее, Windows 11) и архитектуры (x64, ARM64), поведение автообновления.  2 3

  4. Microsoft Learn, Symbol path for Windows debuggers. Настройка пути к символам через переменную окружения _NT_SYMBOL_PATH и задание пути по умолчанию к публичному серверу символов командой .symfix 2

  5. Microsoft Learn, Dump collection and analysis utility (dotnet-dump). dotnet-dump analyze даёт интерактивную сессию, где команды SOS работают напрямую; это не нативный отладчик, поэтому кадры нативного стека не показывает; поддержка macOS — с .NET 5.  2 3 4 5 6

  6. Microsoft Learn, Open a Dump File with WinDbg. Как открыть дамп из меню File пунктом Open crash dump (Ctrl+D) и параметры командной строки -y (путь к символам), -i (путь к образам) и -z (файл дампа). 

  7. Microsoft Learn, Windows Debugger WinDbg Overview. Окна инструментов WinDbg (Command, Source code, Disassembly, Breakpoints и другие) и настройка подключения из меню File

  8. Microsoft Learn, Using the !analyze Extension и !analyze (WinDbg). Автоматический разбор сбоя и исключения через !analyze -v, смысл полей вроде FAULTING_IP и MODULE_NAME, команда !analyze -hang для зависаний.  2 3 4

  9. Microsoft Learn, Microsoft public symbol server. Синтаксис пути к символам вида srv*DownstreamStore*https://msdl.microsoft.com/download/symbols и настройка с локальным кэшем через .symfix

  10. Microsoft Learn, ProcDump - Sysinternals. Параметры -ma (полный дамп), -e (писать дамп при необработанном исключении), -x <Dump_Folder> <Image_File> (запустить цель и следить за ней) и имя файла дампа по умолчанию PROCESSNAME_YYMMDD_HHMMSS.dmp 2

  11. Microsoft Learn, SOS installer (dotnet-sos). Локальная установка расширения SOS через dotnet-sos install и команда ручной загрузки для старых версий отладчика. 

  12. Microsoft Learn, SOS debugging extension - Commands. Команда Threads (в lldb — псевдоним clrthreads) показывает идентификаторы каждого потока, домен, последнее брошенное исключение и прочее.  2

  13. Microsoft Learn, SOS debugging extension - Commands. Команда CLRStack показывает трассировку стека только для управляемого кода, параметр -a выводит и локальные переменные, и аргументы; символы (SYMOPT_LOAD_LINES) влияют только на имя исходного файла и номер строки, не на появление самих кадров.  2 3

  14. Microsoft Learn, SOS debugging extension - Commands. Команда PrintException (pe) без адреса показывает последнее исключение, брошенное в текущем потоке; -nested показывает и вложенные исключения.  2

  15. Microsoft Learn, Debug a memory leak in .NET и Dump collection and analysis utility (dotnet-dump) - Analyze memory leaks and allocations. Статистика числа экземпляров и суммарного размера по типам через dumpheap -stat и как от этой точки строить разбор.  2 3 4 5

  16. Microsoft Learn, SOS debugging extension - Commands. Команда GCRoot ищет по всей управляемой куче и таблице хендлов и перечисляет ссылки (корни) на указанный объект. 

  17. Microsoft Learn, Debug Linux dumps. Дампы Linux можно разбирать на Windows в WinDbg или dotnet-dump; нужна версия инструмента той же разрядности (x64/Arm64/x86), что и окружение, где снимали дамп.  2

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

С какой команды начинать разбор аварийного дампа .NET-приложения?
Есть три типовых сценария. Если процесс упал из-за необработанного исключения, через !threads находят поток с исключением, через !clrstack выводят стек управляемого кода и через !pe смотрят тип, сообщение и внутренние исключения объекта. Если память продолжает расти, через !dumpheap -stat находят тип с наибольшим объёмом, а через !gcroot выясняют корни, которые удерживают объект (статические поля, подписки на события). При нативном сбое начинают с автоматического разбора !analyze -v.
Как загрузить расширение SOS в WinDbg?
Для .NET Framework — .loadby sos clr, для .NET Core/.NET 5+ — .loadby sos coreclr. Начиная с версии 10.0.18317.1001 WinDbg сам загружает SOS, как только видит, что целевой процесс загрузил coreclr.dll. Если автозагрузка не срабатывает, SOS ставят локально инструментом dotnet-sos и загружают вручную командой .load. Что загрузка прошла, проверяют так: !sos.help или !Threads отрабатывают без ошибки и что-то возвращают.
Можно ли разбирать дамп без PDB (символов)?
В какой-то мере да. Команды !clrstack, !dumpheap -stat и !gcroot читают метаданные CLR и данные кучи напрямую, поэтому кадры и сведения о типах видны и без PDB. Пропадают только имена исходных файлов и номера строк управляемого кода, а также имена символов для нативных кадров. Если нужно дойти до конкретной строки исходника, в путь к символам включают и публичный сервер символов Microsoft, и каталог своих PDB. Для регулярной работы важно хранить PDB каждой сборки и включать Source Link.
Как выбирать между dotnet-dump analyze и WinDbg?
Если могут быть задействованы COM, P/Invoke или нативные DLL — WinDbg; если расследуют только управляемый код — ориентир на dotnet-dump analyze. dotnet-dump ставится одной командой dotnet global tool, и в нём сразу доступно большинство команд SOS вроде clrstack и dumpheap, но кадры нативного стека он не показывает и !analyze -v не поддерживает. Дампы, снятые на macOS, WinDbg разобрать не может, там остаются только dotnet-dump или LLDB. Оба инструмента делят набор команд SOS, поэтому выученные команды почти без изменений переносятся из одного в другой.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог