WinDbg + SOS para leer volcados de memoria — introducción práctica al análisis tras la recolección

· Actualizado el: · · WinDbg, SOS, Volcado de memoria, .NET, CSharp, Depuración, PDB, Investigación de fallos, Consultoría técnica

En el artículo anterior, «Introducción a la recolección de volcados de memoria de Windows», repasamos cómo «obtener» un volcado usando WER LocalDumps, ProcDump y MiniDumpWriteDump. Sin embargo, un volcado por sí solo no dice nada. Solo se convierte en material de investigación cuando uno se pone manos a la obra y descifra «qué hilo» y «por qué» falló, o «qué» está reteniendo la memoria.

Este artículo continúa el de recolección y se centra exclusivamente en el procedimiento para leer realmente el volcado obtenido con WinDbg y la extensión SOS. Cubrimos la instalación y la configuración de símbolos, la carga de la extensión SOS —imprescindible en aplicaciones .NET—, qué mirar y cómo interpretarlo con comandos representativos como !clrstack y !dumpheap -stat, !analyze -v para fallos nativos, y cuándo conviene usar dotnet-dump analyze en lugar de WinDbg.

Requisitos previos de este artículo

Elemento Contenido
Público objetivo Personas a cargo del desarrollo o el mantenimiento de aplicaciones Windows/.NET que ya tienen un volcado de memoria (.dmp) y necesitan leer su contenido
Conocimientos previos Saber leer una traza de pila de C#. No se presupone experiencia con WinDbg
Artículo previo Cómo obtener el volcado se trata en el artículo de recolección. Este artículo parte de que «ya se dispone de un volcado». Aunque no lo haya leído, puede seguir el artículo si crea su propio volcado de práctica con el procedimiento de la sección 2.4
Herramientas usadas WinDbg (versión actual), la extensión SOS y, según convenga, dotnet-dump

En adelante se usan sin aclaración las siguientes cuatro abreviaturas.

Abreviatura Forma completa Significado en este artículo
WER Windows Error Reporting Mecanismo estándar de Windows para informar errores. Con la configuración de LocalDumps se pueden guardar automáticamente volcados al producirse un fallo (el procedimiento de configuración está en el artículo de recolección)
PDB Program Database Archivo de símbolos generado durante la compilación. Contiene la correspondencia entre el código y el nombre de archivo fuente y el número de línea (capítulo 8)
CLR Common Language Runtime El motor de ejecución de .NET. Se carga como clr.dll o coreclr.dll1
SOS Son of Strike Extensión del depurador para leer el interior de ese CLR. Se carga en el capítulo 32

1. Antes que nada, la conclusión

  • El protagonista del análisis de volcados es WinDbg (la versión actual, antes llamada WinDbg Preview). Se obtiene con winget install Microsoft.WinDbg o desde Microsoft Store, y funciona en x64 y ARM64 en Windows 10 Anniversary Update (1607) o posterior, y en Windows 11.3
  • Aunque no se hayan cargado los símbolos (PDB), comandos como !clrstack, !dumpheap -stat y !gcroot funcionan igualmente porque leen los metadatos y los datos del montículo del CLR. Lo que se pierde es el nombre del archivo fuente y el número de línea del código administrado, y el nombre de símbolo de los marcos nativos. Dicho esto, si quiere llegar hasta la línea de código fuente, la práctica habitual es incluir en _NT_SYMBOL_PATH tanto el servidor público de símbolos de Microsoft como la ubicación de sus propios PDB.4 El procedimiento de configuración de símbolos está en el capítulo 2, y qué deja de verse con o sin PDB, junto con su gestión, está reunido en el capítulo 8.
  • En los volcados de aplicaciones .NET (Framework, Core o 5+), la información administrada solo se hace visible una vez cargada la extensión SOS. Con el comando nativo k (mostrar la pila) por sí solo no se puede seguir el código C#.2
  • Hay tres patrones de investigación representativos. Si el fallo fue por una excepción, empiece con !clrstack!pe; si la memoria crece continuamente, con !dumpheap -stat!gcroot; y si es un fallo nativo, con !analyze -v.
  • Como alternativa sin WinDbg está dotnet-dump analyze. Permite usar tal cual la mayoría de los comandos de SOS, pero no puede manejar marcos de pila nativos. Si la investigación es exclusivamente de código administrado, sin DLL nativas ni COM de por medio, esta opción resulta más ligera de instalar.5
  • El procedimiento descrito aquí es «cómo leer», no «cómo obtener». Para los métodos de captura del volcado (WER, ProcDump, MiniDumpWriteDump), consulte el artículo de recolección; para el diseño que cruza el volcado con los registros en el momento del fallo, consulte «Diseño para conservar registros y volcados en el momento del fallo».

2. Instalar WinDbg y configurar los símbolos

2.1 Instalación

La versión actual de WinDbg se puede obtener de cualquiera de estas formas.3

winget install Microsoft.WinDbg

A través de Microsoft Store se instala el mismo motor, y los comandos, las extensiones y el flujo de trabajo son iguales. Después de instalarlo se actualiza automáticamente (en segundo plano si se instaló desde Store o directamente, y con winget upgrade Microsoft.WinDbg si se instaló con winget), por lo que las diferencias de comportamiento entre versiones no suelen ser un problema.3

2.2 Abrir el volcado

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

-z es la opción para iniciar indicando el archivo de volcado. Para abrirlo desde la GUI, use la opción de abrir un volcado del menú File (en WinDbg Classic aparece como Open crash dump, con el atajo Ctrl+D).6

Este artículo no incluye capturas de pantalla. En su lugar, se describe con palabras dónde escribir cada comando y cómo se llama cada menú. Al abrir el volcado se abre la ventana de herramienta Command,7 y en su parte inferior aparece un indicador como 0:000>, que es el campo de entrada. Todos los comandos con ! que aparecen a continuación se escriben en esta ventana Command (los ejemplos de análisis de Microsoft también los escriben en la forma 0:000> !analyze -v).8 El 0:000 del indicador significa «seleccionado el hilo 0 del proceso 0»; al cambiar de hilo con ~5s en el capítulo 4, pasa a 0:005>.

2.3 Configurar la ruta de símbolos

El lugar donde el depurador de Windows busca los archivos de símbolos (PDB) se especifica con la variable de entorno _NT_SYMBOL_PATH o con el comando .sympath dentro de la sesión.4 En la práctica, la configuración básica consiste en incluir tanto el servidor público de símbolos de Microsoft como la ubicación de los PDB propios.

.symfix C:\Symbols\Microsoft
.sympath+ C:\Symbols\MyApp
.reload
  • .symfix es un atajo que configura la ruta al servidor público de símbolos de Microsoft (https://msdl.microsoft.com/download/symbols) junto con una caché local especificada. Los símbolos de las DLL estándar del sistema operativo se descargan automáticamente desde ahí.9
  • .sympath+ añade a la ruta existente la ubicación de los PDB propios. Los PDB del código propio hay que prepararlos uno mismo, ya que no están en el servidor de símbolos de Microsoft.
  • .reload vuelve a cargar los módulos y permite comprobar el estado de los símbolos en la lista de módulos.

Para configurarlo de forma permanente con una variable de entorno, se usa la siguiente forma. Es la más adecuada para el análisis automático en CI o en servidores de compilación.

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

Para comprobar si los símbolos se han leído correctamente, muestre la lista de módulos con el comando lm (loaded modules) y verifique si el módulo en cuestión aparece como pdb symbols. Si permanece en deferred, significa que los símbolos aún no se han resuelto.

2.4 Crear su propio volcado de práctica

Practicar directamente con el volcado de un incidente en producción es incómodo, así que conviene preparar una miniaplicación que falla a propósito para poder probar todo el procedimiento de este artículo, lo que acelera la comprensión. Si no dispone de ningún volcado para leer, empiece por aquí.

Cree una aplicación de consola (.NET 8 / C# 12; reemplace por completo el Program.cs del proyecto creado con dotnet new console -n CrashLab por lo siguiente).

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

// (A) Para !dumpheap -stat / !gcroot del capítulo 5: un arreglo que crece sin liberarse
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) Para !clrstack / !pe del capítulo 4: hace fallar el proceso con una excepción no controlada
string? name = null;
Console.WriteLine(name!.Length);   // aquí se produce NullReferenceException

A continuación, inicie esta aplicación con Sysinternals ProcDump configurado para «escribir un volcado completo cuando se produzca una excepción no controlada». -ma genera un volcado completo, -e es la opción para «escribir un volcado cuando el proceso encuentre una excepción no controlada», y -x <carpeta_de_salida> <ejecutable> hace que el propio ProcDump inicie y supervise el ejecutable indicado.10

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

Si el objetivo es un proceso que ya está en ejecución, pase el nombre del proceso (o el PID) como en procdump.exe -accepteula -ma -e CrashLab.exe. El nombre de archivo predeterminado del volcado generado es PROCESSNAME_YYMMDD_HHMMSS.dmp.10

Si carga el .dmp resultante en WinDbg siguiendo el procedimiento de la sección 2.2, podrá practicar tanto el capítulo 4 como el capítulo 5 con este único archivo. Si basta con una investigación exclusivamente administrada, dotnet-dump collect --name CrashLab también permite obtener un volcado equivalente (capítulo 7). No obstante, como dotnet-dump collect es una herramienta que «captura en el momento que se elija», si lo que quiere es atrapar el instante exacto del fallo, use ProcDump o WER LocalDumps.

3. Cargar la extensión SOS

En los volcados de aplicaciones .NET, los comandos nativos de WinDbg por sí solos no permiten ver «el contenido del montículo administrado», «los marcos de pila de C#» ni «el contenido de los objetos de excepción». Quien cubre este vacío es la extensión SOS (Son of Strike). A través de sus comandos se investiga el montículo, se detecta su corrupción, se muestran los tipos de datos internos del runtime y se examina el estado del código administrado en ejecución.2

3.1 Diferencias según el runtime

Según la aplicación de destino sea .NET Framework o .NET (Core)/.NET 5+, cambia el runtime que hay que cargar y el origen de SOS.

Destino Runtime principal Comando de carga
.NET Framework clr.dll .loadby sos clr
.NET Core / .NET 5+ coreclr.dll .loadby sos coreclr

.loadby es un comando que busca y carga la DLL de extensión (sos.dll) que está en el mismo directorio que el módulo indicado (clr o coreclr). Su ventaja es que, sin necesidad de escribir la ruta completa, permite obtener con seguridad la versión de SOS correspondiente al entorno donde se capturó el volcado.1

En WinDbg y cdb a partir de la versión 10.0.18317.1001, al detectar que el proceso de destino tiene cargado coreclr.dll (o libcoreclr.so en Linux/macOS), la extensión para .NET se carga automáticamente desde Microsoft Extension Gallery.1 El comando .loadby anterior está pensado para cuando la carga automática no funciona bien o se usa una versión anterior del depurador.

3.2 Si no se encuentra SOS

En entornos donde la carga automática no funciona, se puede instalar localmente con la herramienta dotnet-sos.

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

Tras la instalación, también se puede cargar manualmente en WinDbg de la siguiente manera (en depuradores antiguos puede ser necesario).11

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

3.3 Comprobar si se cargó correctamente

!sos.help

O bien, si el destino es de la familia Core, pruebe !Threads; si es Framework, pruebe !sosstatus; si no da error y devuelve alguna información, la carga se realizó con éxito. Si en este punto el comando falla con un error como Unable to find module, casi siempre se debe a la ruta de símbolos o a una discordancia de runtime (por ejemplo, diferencias de bits o de versión entre el entorno donde se capturó el volcado y el runtime local). En la práctica no es raro quedarse atascado aquí, antes incluso de llegar a los comandos del siguiente capítulo.

4. Leer excepciones y pilas — !clrstack y !pe

Es el primer paso para un volcado de un fallo por excepción no controlada.

!threads

Primero use !Threads (en entornos lldb, el alias clrthreads) para revisar la lista de hilos administrados y la columna Exception de cada uno.12 Si algún hilo tiene una excepción, cambie a ese hilo.

La salida es una tabla con un hilo por línea: además del número de orden del depurador, el ID de hilo del CLR y el ID de hilo del sistema operativo, aparecen la columna Domain (el dominio de aplicación en ejecución), la columna APT (el modo de apartamento COM) y la columna Exception (la última excepción lanzada en ese hilo).12 Al principio solo hay que mirar la columna Exception. La línea que contiene un nombre de tipo como System.NullReferenceException es el objetivo de la investigación; anote el número de orden que aparece en el extremo izquierdo de esa línea.

~5s
!clrstack

~<número>s es el comando para cambiar al hilo con el número de orden anotado. A continuación, !CLRStack muestra la traza de pila solo del código administrado.13 Si además quiere ver los argumentos y las variables, añada -a (un atajo que combina -l y -p).

!clrstack -a

La salida tiene esta forma (extracto del ejemplo de clrstack publicado en Microsoft Learn; aunque es un ejemplo de una sesión de dotnet-dump, el !clrstack de WinDbg muestra lo mismo).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]

Qué líneas leer es lo siguiente.

  1. Lea solo la columna Call Site, de abajo hacia arriba. Abajo está quien llama, arriba a quien se llama. En este ejemplo se lee el camino MainFoo1Foo2Foo4 hasta llegar al punto del fallo.
  2. Los corchetes [... .cs @ 54] son el nombre del archivo fuente y el número de línea. Si aparecen, los símbolos se resolvieron. Cuando no aparecen, el tratamiento se explica en el capítulo 8.
  3. Ignore las líneas [HelperMethodFrame_...] y [GCFrame: ...]. Son marcos internos del runtime y no corresponden directamente a errores del código propio.
  4. No hace falta leer las columnas Child SP e IP en una investigación habitual. Son, respectivamente, el puntero de pila y el puntero de instrucción, y solo se usan al pasar direcciones a comandos como !dumpstackobjects.
  • CLRStack enumera los marcos administrados directamente a partir de los metadatos del CLR, por lo que la presencia o ausencia de símbolos no afecta a si se muestran los marcos. Cuando faltan los PDB, lo único que desaparece es el nombre de archivo fuente y el número de línea del punto 2 anterior; los marcos en sí nunca se omiten.13 El tema de los símbolos está reunido en el capítulo 8; consúltelo si no aparecen los números de línea.
  • Si no se ve ni un solo marco de código propio, no sospeche de una falta de símbolos, sino de lo siguiente: que el hilo seleccionado sea otro distinto del que tiene la excepción (un error de selección de hilo), que el fallo se haya producido solo en el lado nativo y por eso no exista ningún marco administrado, o que el tipo de volcado (por ejemplo, Mini) no contenga suficiente información de pila en ese momento.

A continuación, se examina el propio objeto de excepción.

!pe

!PrintException (abreviado !pe) muestra, si no se indica nada, la última excepción lanzada en el hilo actual. Se obtiene el nombre del tipo, el mensaje, la excepción interna (visible con -nested) y hasta la cadena de la traza de pila.14 La salida tiene esta forma (también extraída de un ejemplo de Microsoft Learn, con -lines añadido para mostrar también la información de origen).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

Se leen las primeras cuatro líneas. Exception type indica de qué excepción se trata, Message la describe, y InnerException es «la causa real». Cuando aparece TargetInvocationException, como en este ejemplo, el tipo que se muestra en la línea principal no es más que el envoltorio de una llamada por reflexión, así que hay que abrir la excepción interna pasando la dirección de la línea InnerException como en !pe 00007FB18C038368 (!pe -nested da la misma información).14 HResult es el valor de HRESULT correspondiente a la excepción, y a partir de StackTrace (generated) aparece la traza de pila que lleva consigo la propia excepción: la diferencia es que !clrstack muestra «la pila actual», mientras que esto es «la pila en el momento en que se lanzó la excepción». Si ambas no coinciden, sospeche que la excepción fue capturada y relanzada en otro lugar.

Cuanto menos dice por sí solo el nombre del tipo, como ocurre con System.NullReferenceException, más necesario resulta cotejarlo con los valores de las variables locales visibles con !clrstack -a.

5. Rastrear el montículo y las fugas — !dumpheap -stat y !gcroot

Son los comandos centrales en investigaciones del tipo «la memoria crece poco a poco y el proceso falla al cabo de horas o días». La parte previa, sobre cómo distinguir si es una espera de recolección de basura (GC) o una fuga real, se explicó en detalle en «Distinguir la espera del GC de una fuga de memoria en .NET». Este artículo continúa esa línea y corresponde a la parte en la que se lee un único volcado hasta averiguar «qué es lo que retiene» el objeto.

!dumpheap -stat

La opción -stat muestra únicamente un resumen estadístico del montículo administrado. Los tipos se ordenan de forma aproximada de menor a mayor cantidad y tamaño total, así que primero se identifica «qué tipo está pegando por volumen».15 La salida tiene esta forma (es la salida real publicada en el tutorial de investigación de fugas de memoria de 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

Qué líneas leer es lo siguiente.

  1. Lea desde la última línea. Están ordenadas de menor a mayor TotalSize (en bytes), así que la última línea es el tipo que más consume del montículo. En este ejemplo, System.String ocupa unos 19 MB y Customer, unos 4,8 MB.
  2. Observe por separado las columnas Count y TotalSize. Según se trate de unos pocos arreglos enormes (solo el tamaño es grande) o de cientos de miles de objetos pequeños (solo la cantidad es grande), cambia el lugar donde conviene sospechar.
  3. La línea Free no es una fuga. Es un hueco ya recolectado y todavía sin reutilizar; sirve como indicador de fragmentación.
  4. Anote la columna MT (la dirección de la MethodTable). Al pasarla a !dumpheap -mt <MT> en el siguiente paso, se obtiene la lista de direcciones de las instancias individuales de ese tipo.

En la práctica suelen verse dos patrones.

  • Crece la propia clase de negocio (por ejemplo, cientos de miles de MyApp.Models.Customer): hay una referencia fuerte que se sigue conservando en alguna parte
  • Solo System.String o los arreglos son extremadamente numerosos: suele deberse a los datos internos de una clase de negocio que aparece más arriba, así que antes de examinar instancias individuales, casi siempre es más rápido sospechar primero de esa clase de negocio

Una vez acotado el objetivo, se obtienen las direcciones de las instancias individuales.

!dumpheap -type MyApp.Models.Customer

-type busca por coincidencia parcial del nombre de tipo, y -mt especifica la dirección de la MethodTable anotada en el paso anterior. En ambos casos la salida es una lista con una instancia por línea.15

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

Aquí solo se usa la columna Address del extremo izquierdo. Este valor se pasa al siguiente comando.

A continuación se investiga por qué esa instancia sigue sin ser recolectada por el GC.

!gcroot 000001a2b3c4d5e0

!GCRoot recorre todo el montículo administrado y la tabla de manejadores, y detecta las raíces (variables en la pila, campos estáticos, manejadores de GC, etc.) que llegan hasta el objeto indicado.16 La salida tiene esta forma (también es la salida real publicada en 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.

Qué líneas leer es lo siguiente.

  1. La primera línea de encabezado indica el tipo de raíz. Thread 3f68: significa que la raíz es una variable en la pila del hilo, y HandleTable: significa que es un manejador de GC (se indica también el tipo, como (pinned handle)). Si la raíz es un campo estático, aquí aparece el nombre del tipo que tiene ese campo.
  2. Lea la cadena de -> de arriba hacia abajo. Es la cadena de «quién retiene a quién», y el último eslabón es el objeto indicado.
  3. El culpable no es el eslabón final, sino la colección o el objeto de vida larga que aparece a mitad de la cadena. En este ejemplo se ve que CustomerCache mantiene retenida la List<Customer> sin soltarla. Mientras no se corrija esto, por mucho que se examine el Customer final por separado, el problema no se resuelve.
  4. Confirme la cantidad en la última línea, Found N root. Si aparecen varias raíces, el objeto no se recolectará hasta que se corten todas. No dé por terminado el trabajo tras corregir solo una.

Si en la salida aparece un campo estático de caché o una suscripción a un controlador de eventos, ese es el candidato a culpable de una desuscripción olvidada. Un código como el siguiente, que «deja algo metido en la caché sin liberarlo nunca», es un ejemplo típico.

public static class CustomerCache
{
    // Diccionario estático sin ninguna vía de liberación, que solo crece
    private static readonly Dictionary<int, Customer> _cache = new();

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

Si en la salida de !gcroot aparece un contenedor estático como CustomerCache, eso es un indicio para plantearse, del lado del código, un tiempo de vida límite, un número máximo de elementos o el cambio a WeakReference.15

6. Análisis automático de fallos nativos — !analyze -v

En un fallo nativo (como una infracción de acceso) en el que intervienen DLL de C++, COM o SDK de terceros, este es el primer comando por el que empezar.

!analyze -v

!analyze es un comando de extensión que realiza el análisis automático del fallo o la excepción, y con -v se obtiene la vista detallada.8 La salida ocupa decenas de líneas, pero si se extraen solo los campos principales del ejemplo de análisis en modo de usuario de Microsoft Learn, queda así.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

Dentro de la salida hay tres elementos que conviene mirar especialmente.

  • EXCEPTION_CODE / BUGCHECK_STR: qué tipo de anomalía es (infracción de acceso, desbordamiento de pila, etc.). En el ejemplo anterior es 80000003 (excepción de instrucción de interrupción), y en la línea ExceptionCode de EXCEPTION_RECORD se añade una descripción legible. En los volcados en modo de usuario, BUGCHECK_STR también contiene este código de excepción (el nombre proviene del kernel y puede confundir, pero «bugcheck» no significa pantalla azul).8
  • FAULTING_IP / FOLLOWUP_IP: la dirección de instrucción donde realmente falló el proceso, y el módulo y la función correspondientes. FAULTING_IP es «el lugar donde falló», mientras que FOLLOWUP_IP es el lugar que !analyze estima como «probablemente la causa», y lo habitual es que no coincidan. En el ejemplo anterior el fallo ocurre en ntdll, pero la causa estimada es MyApp!FatalErrorBox, del propio código.
  • MODULE_NAME / IMAGE_NAME: si el lugar del fallo pertenece a un módulo propio o a una DLL de terceros. Junto con SYMBOL_NAME, aquí se indica el módulo al que pertenece FOLLOWUP_IP.

En STACK_TEXT, la primera línea es la más interna (el punto del fallo), y cuanto más abajo, más se aleja hacia quien llama. La forma más rápida de leerlo es buscar de arriba hacia abajo la primera línea en la que aparezca el nombre del módulo propio (en el ejemplo, MyApp!) y mirar el [nombre_de_archivo @ número_de_línea] de esa línea.

Si el fallo ocurre dentro de una DLL de un proveedor en lugar de un módulo propio, para seguir investigando desde ahí se necesita el PDB del proveedor (que casi nunca está disponible). En la práctica, lo realista es remontarse hasta quien llama (los últimos argumentos que pasó el código propio) y sospechar de si el valor pasado era incorrecto. Compruebe en el campo STACK_TEXT de !analyze -v inmediatamente después de qué llamada del código propio se produjo el fallo.

!analyze también se puede usar en volcados que no sean de una excepción. Si se sospecha de un cuelgue, al seleccionar el hilo de destino y ejecutar lo siguiente, se analizan las relaciones de bloqueo entre hilos.

!analyze -hang

7. La alternativa sin WinDbg — dotnet-dump analyze

Si solo quiere examinar código administrado de .NET Core/.NET 5+ (sin que intervengan DLL nativas ni COM), dotnet-dump, más ligero que WinDbg, también es una opción.

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

El subcomando analyze abre una sesión interactiva con SOS preinstalado, en la que se pueden usar tal cual, sin el prefijo !, la mayoría de los comandos presentados hasta ahora, como clrstack, dumpheap y gcroot.5

Estos son los criterios generales para elegir entre uno y otro.

Aspecto WinDbg + SOS dotnet-dump analyze
Marcos de pila nativos Se ven No se ven (solo administrados)5
Análisis automático nativo con !analyze -v Se puede usar No se puede usar
Volcados de Linux Se pueden analizar con WinDbg en Windows (use la versión x64 para volcados x64, la versión x64 para volcados Arm64 y la versión x86 para volcados x86)17 Compatible (use una herramienta con los mismos bits que la plataforma de origen)17
Volcados de macOS No compatible (la compatibilidad de WinDbg con volcados de Linux no incluye macOS) Compatible (.NET 5 en adelante)5
Ligereza de la instalación Instalador o winget Un solo comando de dotnet global tool
Integración en CI multiplataforma Requiere trabajo Es sencilla

El criterio práctico es: WinDbg si «es posible que intervengan COM, P/Invoke o DLL nativas», y dotnet-dump analyze si «es una investigación de fuga de memoria de código puramente administrado y se quiere ejecutar también en CI o en varias plataformas». Como ambos comparten el mismo conjunto de comandos de SOS, los comandos aprendidos en uno funcionan casi sin cambios en el otro. Tenga en cuenta que, al tratar un volcado capturado en macOS, WinDbg no es una opción, así que la única alternativa es dotnet-dump (o LLDB).

8. Sin símbolos legibles, no se avanza

Todo lo relativo a los símbolos (PDB) se reúne en este capítulo. En los capítulos anteriores solo se decía que «si hay PDB, aparece el número de línea»; con leer solo este capítulo basta para entender los detalles.

Ante todo, una parte del procedimiento visto hasta ahora funciona razonablemente bien aunque el PDB no se cargue correctamente. !clrstack, !dumpheap -stat y !gcroot leen directamente los metadatos y los datos del montículo del CLR, por lo que muestran los marcos y la información de tipos aunque no haya PDB. Lo que se pierde por la falta de PDB es el nombre del archivo fuente y el número de línea del código administrado, y el nombre de símbolo de los marcos y módulos nativos (en su lugar solo aparece la dirección, sin el nombre de la función).

Lo que se quiere ver Sin PDB Con PDB
Marcos de pila administrados (nombre del método, tipos de los argumentos) Se muestra Se muestra
Nombre de archivo fuente y número de línea administrados (!clrstack) No se muestra Se muestra13
Tipo, mensaje y excepción interna de la excepción (!pe) Se muestra Se muestra
Nombre de tipo, cantidad y tamaño en el montículo (!dumpheap -stat) Se muestra Se muestra
Ruta de referencia hasta la raíz de GC (!gcroot) Se muestra Se muestra
Nombre de función de los marcos nativos (k / STACK_TEXT de !analyze -v) No se muestra (solo la dirección) Se muestra

En otras palabras, en una situación en la que solo se dispone del volcado y del ejecutable, y lo que se quiere es «al menos hacerse una idea de qué pasó», no hay problema en empezar por !threads!clrstack sin atascarse antes buscando el PDB. Ahora bien, si quiere llegar hasta la línea de código fuente para localizar la causa, la cosa cambia. En el capítulo 2 se añadió la ruta del PDB propio a .sympath+, pero en la práctica es muy frecuente —más de lo que se contó en el artículo de recolección— que la investigación se estanque, sin poder localizar la línea de origen, simplemente porque no se sabe dónde está el PDB correspondiente al EXE/DLL distribuido.

Qué contiene y qué no contiene el propio PDB, el Portable PDB, y Source Link (el mecanismo que incrusta metadatos de control de versiones dentro del ensamblado para que el depurador pueda obtener directamente el código fuente correspondiente al commit de la compilación) están reunidos en un solo artículo, «Qué es un PDB».18 Si se quiere incorporar el análisis de volcados a una operación continua, conservar el PDB de cada compilación y tener habilitado Source Link es una preparación tan importante como la propia configuración de recolección. Si se descuida esto, la salida de !clrstack no mostrará ninguna línea de origen y la investigación quedará reducida a tantear a ciegas con solo direcciones y nombres de tipo.

9. Caso real — análisis de volcado en una investigación de fuga de manejadores

El artículo que escribí antes, «Investigación de un fallo tras un funcionamiento prolongado de una cámara industrial — la parte de la fuga de manejadores», es un caso de investigación de una aplicación de control de cámara industrial que fallaba de pronto tras un largo tiempo de funcionamiento, en el que el culpable resultó ser una fuga de manejadores y no una fuga de memoria. El volcado resulta útil en este tipo de investigación cuando se dan combinaciones como las siguientes.

  1. Confirmar con !dumpheap -stat que el montículo administrado está normal (que la cantidad y el tamaño por tipo no siguen creciendo)
  2. Si, aun así, solo sigue creciendo el número de manejadores del proceso, se puede aislar el problema como una fuga no de objetos administrados, sino de manejadores del sistema operativo (archivos, eventos, manejadores que reserva internamente el SDK de la cámara, etc.)
  3. Si queda un objeto contenedor administrado con un SafeHandle, se rastrea la raíz de GC de ese contenedor con !gcroot para identificar «el lugar donde debería haberse liberado la referencia y sin embargo permanece»

En resumen, !dumpheap -stat funciona como el punto de bifurcación que determina «si el crecimiento está en el montículo administrado o no», y en cuanto se descarta esa posibilidad, el eje de la investigación pasa a herramientas de detección de anomalías en el límite nativo, como Application Verifier. Cómo construir esta base de pruebas de casos anómalos se trata en «Base de pruebas de casos anómalos de Windows con Application Verifier». El análisis de volcados cubre «el estado que está ocurriendo ahora», y Application Verifier, «adelantar y reproducir la anomalía»; combinar ambos es la práctica habitual en la investigación de fallos de sistemas de funcionamiento prolongado.

10. Resumen

Dominar la «lectura» de un volcado de memoria lleva más tiempo que dominar su «obtención», pero los patrones no son tantos.

  1. Instale WinDbg e incluya en la ruta de símbolos tanto el servidor público de símbolos de Microsoft como el PDB propio (capítulo 2)
  2. Si es una aplicación .NET, cargue la extensión SOS (.loadby sos clr / .loadby sos coreclr, o la carga automática; capítulo 3)
  3. Si es de origen en una excepción, empiece por !clrstack!pe; si es un aumento de memoria, por !dumpheap -stat!gcroot; y si es un fallo nativo, por !analyze -v (capítulos 4 a 6)
  4. Si el propósito es una investigación puramente administrada, considere también el ligero dotnet-dump analyze (capítulo 7)

Y la base de todo esto es la gestión del PDB y de los símbolos. Si, al mismo tiempo que se configura la recolección de volcados, se decide también conservar el PDB de cada compilación y habilitar Source Link, el tiempo de investigación cuando ocurra realmente un incidente cambia notablemente. Si le resulta difícil analizarlo internamente o no dispone de tiempo, también podemos encargarnos nosotros del análisis si nos envía el conjunto de volcados y registros.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se ocupa de la investigación de causas de fallos combinando volcados de memoria y registros, del aislamiento de incidentes que solo se producen tras un funcionamiento prolongado, y del asesoramiento sobre el diseño del propio sistema de conservación y análisis de volcados y PDB.

Referencias

  1. Microsoft Learn, Debugging Managed Code Using the Windows Debugger. Sobre que el runtime de .NET Framework es clr.dll y el de .NET Core/.NET 5+ es coreclr.dll, sobre la carga de la extensión desde las cercanías del módulo mediante .loadby, y sobre la carga automática a partir de WinDbg 10.0.18317.1001.  2 3

  2. Microsoft Learn, SOS debugging extension. Sobre el uso de la extensión SOS para recopilar información del montículo administrado, detectar su corrupción y mostrar los tipos de datos internos del runtime, y sobre la sintaxis ![command] en WinDbg.  2 3

  3. Microsoft Learn, Install the Windows debugger. Sobre los métodos de instalación de WinDbg mediante winget o Microsoft Store, los sistemas operativos compatibles (Windows 10 1607 en adelante y Windows 11) y las arquitecturas (x64 y ARM64), y el comportamiento de la actualización automática.  2 3

  4. Microsoft Learn, Symbol path for Windows debuggers. Sobre cómo configurar la ruta de símbolos con la variable de entorno _NT_SYMBOL_PATH y cómo el comando .symfix permite establecer la ruta predeterminada al servidor público de símbolos.  2

  5. Microsoft Learn, Dump collection and analysis utility (dotnet-dump). Sobre que dotnet-dump analyze ofrece una sesión interactiva en la que se pueden usar directamente los comandos de SOS, pero que, al no ser un depurador nativo, no puede mostrar los marcos de pila nativos, y que la compatibilidad con macOS es a partir de .NET 5.  2 3 4 5 6

  6. Microsoft Learn, Open a Dump File with WinDbg. Sobre el procedimiento para abrir un volcado desde Open crash dump (Ctrl+D) en el menú File, y las opciones de línea de comandos -y (ruta de símbolos), -i (ruta de imágenes) y -z (archivo de volcado). 

  7. Microsoft Learn, Windows Debugger WinDbg Overview. Sobre la disposición de las ventanas de herramientas de WinDbg (Command, Source code, Disassembly, Breakpoints, etc.) y la configuración de conexión y otros ajustes desde el menú File

  8. Microsoft Learn, Using the !analyze Extension y !analyze (WinDbg). Sobre el análisis automático de fallos y excepciones con !analyze -v, el significado de campos de la salida como FAULTING_IP y MODULE_NAME, y !analyze -hang para la investigación de cuelgues.  2 3 4

  9. Microsoft Learn, Microsoft public symbol server. Sobre la sintaxis de ruta de símbolos con la forma srv*DownstreamStore*https://msdl.microsoft.com/download/symbols y la configuración con caché local mediante .symfix

  10. Microsoft Learn, ProcDump - Sysinternals. Sobre las opciones -ma (volcado completo), -e (escribir un volcado al producirse una excepción no controlada) y -x <Dump_Folder> <Image_File> (iniciar y supervisar el objetivo indicado), y el nombre de archivo predeterminado del volcado, PROCESSNAME_YYMMDD_HHMMSS.dmp 2

  11. Microsoft Learn, SOS installer (dotnet-sos). Sobre la instalación local de la extensión SOS con dotnet-sos install y el comando de carga manual en versiones antiguas del depurador. 

  12. Microsoft Learn, SOS debugging extension - Commands. Sobre que el comando Threads (con el alias clrthreads en entornos lldb) muestra una lista con el ID, el dominio y la última excepción lanzada de cada hilo, entre otros datos.  2

  13. Microsoft Learn, SOS debugging extension - Commands. Sobre que el comando CLRStack muestra la traza de pila solo del código administrado, que la opción -a muestra tanto las variables locales como los argumentos, y que los símbolos (SYMOPT_LOAD_LINES) solo afectan a si se muestra el nombre de archivo fuente y el número de línea, no a si se muestra el propio marco.  2 3

  14. Microsoft Learn, SOS debugging extension - Commands. Sobre que el comando PrintException (pe) muestra, si se omite la dirección, la última excepción lanzada en el hilo actual, y que -nested permite mostrar también las excepciones anidadas.  2

  15. Microsoft Learn, Debug a memory leak in .NET y Dump collection and analysis utility (dotnet-dump) - Analyze memory leaks and allocations. Sobre el resumen estadístico de cantidad y tamaño total por tipo que muestra dumpheap -stat, y sobre cómo avanzar la investigación a partir de ahí.  2 3 4 5

  16. Microsoft Learn, SOS debugging extension - Commands. Sobre que el comando GCRoot recorre todo el montículo administrado y la tabla de manejadores, y detecta las referencias (raíces) hasta el objeto indicado. 

  17. Microsoft Learn, Debug Linux dumps. Sobre la posibilidad de analizar volcados de Linux en Windows con WinDbg o dotnet-dump, y sobre la necesidad de usar una versión de la herramienta acorde a los bits del entorno de captura (x64/Arm64/x86).  2

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Con qué comando conviene empezar a leer un volcado de memoria de una aplicación .NET?
Hay tres patrones de investigación. Si el proceso terminó por una excepción no controlada, use !threads para localizar el hilo que tiene la excepción, muestre la pila administrada con !clrstack y confirme el tipo, el mensaje y la excepción interna del objeto de excepción con !pe. Si la memoria crece de forma continua, use !dumpheap -stat para identificar los tipos con mayor volumen y luego !gcroot para averiguar qué raíz (un campo estático, la suscripción a un controlador de eventos, etc.) retiene ese objeto. Si se trata de una excepción nativa, comience por el análisis automático de !analyze -v.
¿Cómo se carga la extensión SOS en WinDbg?
Para .NET Framework se usa .loadby sos clr, y para .NET Core/.NET 5+, .loadby sos coreclr. A partir de la versión 10.0.18317.1001, WinDbg carga SOS automáticamente en cuanto detecta que el proceso analizado tiene cargado coreclr.dll. En entornos donde la carga automática no funciona, también puede instalarse localmente con la herramienta dotnet-sos y cargarla manualmente con .load. Para comprobar si se cargó correctamente, ejecute !sos.help o !Threads y verifique que no devuelvan error.
¿Se puede analizar un volcado sin PDB (símbolos)?
Hasta cierto punto, sí. !clrstack, !dumpheap -stat y !gcroot leen directamente los metadatos y los datos del montículo del CLR, por lo que muestran los marcos y la información de tipos aunque no haya PDB. Lo que se pierde es el nombre del archivo fuente y el número de línea del código administrado, así como los nombres de símbolo de los marcos nativos. Si quiere llegar hasta la línea de código fuente para localizar el origen del problema, necesita incluir en la ruta de símbolos tanto el servidor público de símbolos de Microsoft como la ubicación de sus propios PDB. Conservar el PDB de cada compilación y habilitar Source Link son requisitos operativos importantes.
¿Cuándo conviene usar dotnet-dump analyze y cuándo WinDbg?
Como criterio general, use WinDbg si es posible que intervengan COM, P/Invoke o DLL nativas, y dotnet-dump analyze si la investigación es puramente de código administrado. dotnet-dump se instala con un solo comando de dotnet global tool y permite usar tal cual la mayoría de los comandos de SOS, como clrstack y dumpheap, pero no puede manejar marcos de pila nativos ni usar !analyze -v. Los volcados tomados en macOS no se pueden analizar con WinDbg, así que la única opción es dotnet-dump o LLDB. Como ambos comparten el mismo conjunto de comandos de SOS, casi todos los comandos que aprenda funcionan indistintamente en uno u otro.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog