Nota del autor

Si la entrada que estás leyendo carece de imágenes, no se ve el vídeo que teóricamente lleva incrustado o el código fuente mostrado aparece sin formato, podéis conocer los motivos aquí. Poco a poco iré restableciendo la normalidad en el blog.
Este blog es un archivo de los artículos situados previamente en Lobosoft.es y ha dejado de ser actualizado. Las nuevas entradas pueden encontrarse en www.lobosoft.es. Un saludo,
Lobosoft.

miércoles, 27 de julio de 2011

Manejando minidumps en .NET

Por mucho cuidado que pongamos en la programación, por exhaustivas que sean las pruebas unitarias que dirigen nuestros desarrollos y meticulosas las personas que realicen las funcionales, de integración o aceptación, lo cierto es que de cuando en cuando nos encontraremos ante la tesitura de tener que depurar un error en nuestras aplicaciones sobre una máquina de producción. En estos equipos no tendremos disponible nuestro IDE favorito y, aparte de las trazas de depuración que podamos habilitar para ayudarnos a determinar el origen del problema, lo cierto es que a priori solo nuestra holmesiana capacidad de deducción nos ayudará a lograr nuestro objetivo.

Sin embargo, no estamos del todo “vendidos”, y además de numerosas herramientas de depuración que nos ofrece Microsoft en su paquete Debugging Tools for Windows o de instalar en .NET Reflector add-ins como Deblector –técnicas en las que entraremos más adelante– podemos hacer uso de la función de la API MiniDumpWriteDump para obtener un archivo con un volcado de memoria que nos ayude, ya en nuestra máquina de desarrollo, a depurar el problema, bien con alguna de las aplicaciones diseñadas a tal fin, bien con el propio Visual Studio que, especialmente en su versión de 2010 sobre el .NET Framework 4, cuenta con poderosas herramientas a tal fin.

Los minidumps permiten la depuración post mórtem de nuestras aplicaciones, es decir, llevarla a cabo cuando la aplicación está “muerta”. Son usados, por ejemplo, por Microsoft, cuando un usuario envía información sobre un error desde el cuadro de diálogo correspondiente de Windows XP. Un minidump consiste en un volcado de la memoria de nuestra aplicación, y mediante la carga del mismo en un depurador podremos estudiar desde el hilo en el que se produjo una determinada excepción al valor de las variables involucradas o el estado de nuestro programa. Esto lo veremos más adelante. Por lo pronto, veamos cómo podemos usar la función nativa de Windows MiniDumpWriteDump desde nuestro código.

Obvia decir que al no tratarse de una función propia del .NET Framework es necesario realizar la llamada mediante Interop. Así, lo primero es definir las estructuras de datos que necesitaremos para hacer uso de aquella y, a continuación, importar las librerías necesarias y las funciones que usaremos, tanto de la Kernel32.dll para obtener información sobre el proceso como, en el caso de MiniDumpWriteDump, Dbghelp.dll.

Según la MSDN, la sintaxis de nuestra función es:
BOOL WINAPI MiniDumpWriteDump(
__in HANDLE hProcess,
__in DWORD ProcessId,
__in HANDLE hFile,
__in MINIDUMP_TYPE DumpType,
__in PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
__in PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
__in PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);
Crearemos una clase que nos ofrezca el comportamiento que deseamos: un auxiliar que nos permita volcar a fichero el estado de la memoria cuando nuestro proceso capture una determinada excepción. El código podría quedar así:

    public class MiniDumpWriter
    {
        [Flags]
        public enum DumpType : uint
        {
            MiniDumpNormal = 0x00000000,
            MiniDumpWithDataSegs = 0x00000001,
            MiniDumpWithFullMemory = 0x00000002,
            MiniDumpWithHandleData = 0x00000004,
            MiniDumpFilterMemory = 0x00000008,
            MiniDumpScanMemory = 0x00000010,
            MiniDumpWithUnloadedModules = 0x00000020,
            MiniDumpWithIndirectlyReferencedMemory = 0x00000040,
            MiniDumpFilterModulePaths = 0x00000080,
            MiniDumpWithProcessThreadData = 0x00000100,
            MiniDumpWithPrivateReadWriteMemory = 0x00000200,
            MiniDumpWithoutOptionalData = 0x00000400,
            MiniDumpWithFullMemoryInfo = 0x00000800,
            MiniDumpWithThreadInfo = 0x00001000,
            MiniDumpWithCodeSegs = 0x00002000,
            MiniDumpWithoutAuxiliaryState = 0x00004000,
            MiniDumpWithFullAuxiliaryState = 0x00008000,
            MiniDumpWithPrivateWriteCopyMemory = 0x00010000,
            MiniDumpIgnoreInaccessibleMemory = 0x00020000,
            MiniDumpValidTypeFlags = 0x0003ffff,
        };

        [StructLayout(LayoutKind.Sequential, Pack = 4)]  
        struct MiniDumpExceptionInformation
        {
          public uint ThreadId;
          public IntPtr ExceptionPointers;
          [MarshalAs(UnmanagedType.Bool)]
          public bool ClientPointers;
        }

        [DllImport("dbghelp.dll",
          EntryPoint = "MiniDumpWriteDump",
          CallingConvention = CallingConvention.StdCall,
          CharSet = CharSet.Unicode,
          ExactSpelling = true, SetLastError = true)]

        static extern bool MiniDumpWriteDump(
          IntPtr hProcess,
          uint processId,
          IntPtr hFile,
          uint dumpType,
          ref MiniDumpExceptionInformation expParam,
          IntPtr userStreamParam,
          IntPtr callbackParam);

        [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]
        static extern uint GetCurrentThreadId();

        [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess", ExactSpelling = true)]
        static extern IntPtr GetCurrentProcess();

        [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcessId", ExactSpelling = true)]
        static extern uint GetCurrentProcessId();

        /// <summary>
        /// Generates a memory dump and save it into a file.
        /// </summary>
        /// <param name="fileName">The file where dump must be stored.</param>
        /// <param name="dumpType">The type of memory dump generated.</param>
        /// <returns></returns>       
        public static bool Write(string fileName, DumpType dumpType)
        {
            using (FileStream fs = new FileStream(fileName, 
                                                  FileMode.Create, 
                                                  FileAccess.Write, 
                                                  FileShare.None))
            {
                MiniDumpExceptionInformation exInfo;
                exInfo.ThreadId = GetCurrentThreadId();
                exInfo.ClientPointers = false;
                exInfo.ExceptionPointers = Marshal.GetExceptionPointers();
                
                bool value = MiniDumpWriteDump(GetCurrentProcess(),
                                               GetCurrentProcessId(),
                                               fs.SafeFileHandle.DangerousGetHandle(),
                                               (uint)dumpType,
                                               ref exInfo,
                                               IntPtr.Zero,
                                               IntPtr.Zero);                
                return value;
            }
        }
    }

Y para usarla, simplemente llamaremos al método correspondiente cuando nos interese tener un volcado de memoria, por ejemplo, cuando se produzca una excepción en nuestro programa:

 class Program
    {
        static void Main(string[] args)
        {
            try
            {
                object o = null;

                Console.WriteLine("Press ENTER to continue...");
                Console.ReadLine();

                o.ToString();

            }
            catch (System.Exception e)
            {                
                string dumpFile = @"Something\Like\lobosoft.dmp";                
                MiniDumpWriter.Write(dumpFile, MiniDumpWriter.DumpType.MiniDumpWithFullMemory);
            }
        }
    }

Hay que tener en cuenta que los ficheros generados son bastante voluminosos y que la ruta donde los guardará nuestra aplicación debe establecerse a una ubicación donde contemos con permisos suficientes como para que pueda ser escrito. Una vez generado podremos usar Visual Studio, o la Debugging Diagnostic Tool para analizar el error. Pero esto será objeto de una próxima entrada.