Que triste será el día en que yo sea abuelo, y cuando mis nietos me pidan que les cuente mis locuras de juventud solo se me ocurran cosas como la noche en que algunos compañeros de Plain Concepts nos pusimos a crackear el buscaminas con el WinDdg a eso de las 2:00 am.
La verdad es que siempre he sido un poco raro para estas cosas: de pequeño no se me daba nada bien jugar a los juegos de plataformas, así que tiraba de SoftIce (el viejo, para DOS, de NuMega 🙂 ) y me dejaba matar algunas vidas, miraba a ver que direcciones de memoria seguían un patrón de decrecimiento, y cuando localizaba la dirección de memoria que almacenaba el número de vidas, lo ponía a FF, y voilá, 256 vidas 😉 ¡El proceso era más divertido muchas veces que el propio juego!
Supongo que debido a este oscuro pasado mío, mis ojos se abrieron como platos cuando descubrí la depuración postmortem de la mano,principalmente, de Nacho Alonso y David Salgado, y del resto de mis compañeros de Soporte en Microsoft, a los que les debo un buen cacho de lo que soy (¡¡un abrazote a todos!!). Desde ese momento me enganche al WinDbg, y hasta hoy me dura la tontería 😉
Espero poder dedicar algunas entradas de este blog a introducir el WinDbg, así como escenarios útiles (y no tan útiles! XD) de uso.
Que es el WinDbg
Microsoft pone a nuestra disposición (bajo licencia BTF, esto es, By The Face XD) las Debugging Tools for Windows, una suite de aplicaciones y documentación destinadas a la depuración en sistemas Windows. WinDbg es una de éstas herramientas: concretamente es un depurador que nos permite adjuntarnos a procesos de usuario y al kernel del sistema operativo (por tanto, soporta tanto user mode debugging como kernel mode debugging), que permite tanto la depuración en vivo (live debugging), que consiste en adjuntarse a un proceso que está en ejecución, como la depuración postmortem, mediante el análisis de volcados de memoria generados por la aplicación o el sistema operativo en momentos determinados. Y además de todo esto, ¡dispone de una interfaz gráfica! Bueno, o eso al menos dicen los de Redmond, porque muy gráfica la verdad es que no lo es 🙂 Os adjunto un pantallazo de una sesión de depuración:
Como podéis ver, la ventana de comandos parece cualquier cosa menos gráfica 🙂 Esto tiene una explicación: todos los depuradores de Microsoft tratan de mantener la compatibilidad a nivel de comandos. Si sabemos utilizar WinDbg, podremos saltar fácilmente a KD, CDB o NTSD, que son los depuradores de consola de Windows.
La diferencia mas notable entre el WinDbg y los otros tres depuradores comentados es la extensibilidad mediante módulos; algunos de las extensiones más conocidas son la Son of Strike (sos.dll), que nos permite depurar código administrado, y la SIEExtPub.dll, que entre otras cosas permite listar regiones criticas.
Un ejemplo: depurando el notepad.exe
Ok, ya sabemos lo que es el WinDbg, y sabemos donde encontrarlo, así que si os animáis, acompañadme en una demo muy corta para familiarizarnos con él. Simplemente vamos a adjuntar el depurador a un proceso existente, en este caso el notepad.exe.
Para empezar, nos descargamos las Debugging Tools for Windows y las instalamos en nuestra máquina. A continuación, arrancamos WinDbg.exe y, ta-dah!, ahí tenemos nuestro depurador gráfico 🙂
Ahora necesitaremos el proceso al que nos vamos a adjuntar (en inglés debugee), que será en este caso el notepad.exe. Vamos a abrir el notepad y simulamos que vamos a abrir un fichero; pulsamos Ctrl+O y nos aparecerá la ventana donde podemos seleccionar el archivo a abrir. Ok, con esto, sin cerrar esa ventana ni abrir ningún fichero, hemos terminado de preparar nuestro debuggee. Debería aparecer como la imagen siguiente:
Volvemos ahora al WinDbg, pulsamos F6 (que es el acceso rápido para adjuntarse a un proceso de la máquina) y nos aparecerá una ventana como la siguiente, donde podemos seleccionar el proceso al cual nos queremos adjuntar.
El checkbox que aparece en la parte inferior de la ventana con el nombre de Noninvasive es muy importante. No vamos a entrar en mucho detalle ya que es el primer artículo de la serie, pero os adelantaré que si ese checkbox no esta marcado, cuando cerremos la sesión de depuración el proceso depurado se cerrará también. Como veis, es un detalle muy importante, sobre todo si estamos trabajando en un servidor de producción: yo me se de alguno que se olvido de poner el modo no invasivo mientras depuraba un SQL Server en producción… titiriti, titiriti…. 😀
El otro detalle a destacar es que si bien los nombres del proceso ayudan, en muchas ocasiones no es suficiente para identificar el proceso exacto al que queremos adjuntarnos. Por ejemplo, si quisiéremos depurar una instancia concreta en un servidor con múltiples instancias, necesitaríamos recurrir al identificador del proceso (PID), ya que el nombre sería el mismo para todas las instancias (sqlservr.exe).
De todos modos, nosotros lo tenemos bien fácil en este caso: seleccionamos el notepad.exe y le damos al Ok. A continuación os aparecerá una ventana en la que se muestra un chorrazo de texto, que aunque no vamos a describir en detalle, os adelanto que es la lista de todos los módulos (ejecutable, dlls, etc) que conforman el proceso notepad.exe.
A partir de aquí el proceso a depurar esta detenido, y nosotros podemos empezar a lanzar comandos al WinDbg para analizar o modificar el proceso. Por ejemplo, si lanzamos el comando lm (acnonimo de List Modules) nos listará todos los módulos en memoria de ese proceso. A continuación os pongo un extracto de ésta salida en mi máquina:
0:011> lm
start end module name
00000000`777d0000 00000000`77901000 kernel32 (deferred)
00000000`77910000 00000000`779da000 USER32 (deferred)
00000000`779e0000 00000000`77b5a000 ntdll (export symbols) C:Windowssystem32ntdll.dll
00000000`fffa0000 00000000`fffcf000 notepad (deferred)
00000001`80000000 00000001`80083000 sfShellTools64 (deferred)
000007fe`f47d0000 000007fe`f4858000 tiptsf (deferred)
000007fe`f5c10000 000007fe`f62d2000 ieframe (deferred)
000007fe`f7080000 000007fe`f7178000 actxprxy (deferred)
000007fe`f7390000 000007fe`f73eb000 ntshrui (deferred)
000007fe`f73f0000 000007fe`f7442000 msshsq (deferred)
000007fe`f7530000 000007fe`f75d7000 cscui (deferred)
...
Como se puede ver, tenemos un modulo que se llama notepad. Podemos tratar de visualizar todas las funciones que expone mediante el comando x. Este nos muestra una lista de todos los indentificadores (nombres de funciones, de variables…)de un modulo, o los que cumplan cierto criterio mediante una expresión regular. En el siguiente ejemplo vamos a pedir que nos muestre todos los identificadores del modulo notepad:
0:011> x notepad!*
*** ERROR: Module load completed but symbols could not be loaded for C:WindowsSystem32notepad.exe
Uuups, algo no está bien. Dice que faltan unos símbolos, y no nos muestra nada… bueno, vamos a ignorar de momento este mensaje. Fijaros simplemente en que la expresion que le pasamos fue <NombreModulo>!<expresionDeBusqueda>.
Vamos a intentar mostrar otra cosa interesante: los volcados de pila de todos los hilos del proceso. Para ello, nada mas simple que el terriblemente intuitivo comando ~*k. En mi máquina me ha devuelto 12 hilos, pero aqui voy a limitar la salida a un solo hilo:
0:011> ~*k
0 Id: 1798.1548 Suspend: 1 Teb: 000007ff`fffdc000 Unfrozen
Child-SP RetAddr Call Site
00000000`000df348 00000000`7780ed73 ntdll!NtWaitForMultipleObjects+0xa
00000000`000df350 00000000`7792e96d kernel32!WaitForMultipleObjectsEx+0x113
00000000`000df460 000007fe`fccb1ab6 USER32!MsgWaitForMultipleObjectsEx+0x13d
00000000`000df500 000007fe`fccb371f DUser+0x1ab6
00000000`000df550 000007fe`fccb3696 DUser!DeleteHandle+0x7df
00000000`000df590 00000000`7791bd1a DUser!DeleteHandle+0x756
00000000`000df5c0 00000000`77a32016 USER32!CopyIcon+0x8a
00000000`000df610 00000000`7792df2a ntdll!KiUserCallbackDispatcher+0x1f
00000000`000df678 00000000`779173e9 USER32!WaitMessage+0xa
00000000`000df680 00000000`7791760a USER32!LockWindowUpdate+0x249
00000000`000df700 00000000`779174c6 USER32!DialogBoxIndirectParamAorW+0x19a
00000000`000df760 00000000`77917918 USER32!DialogBoxIndirectParamAorW+0x56
00000000`000df7a0 000007fe`fe473d2e USER32!DialogBoxIndirectParamW+0x18
00000000`000df7e0 00000000`fffa5146 COMDLG32!GetFileTitleW+0x121e
00000000`000df850 00000000`fffa547e notepad+0x5146
00000000`000df8c0 00000000`fffa5b98 notepad+0x547e
00000000`000df920 00000000`fffa6d33 notepad+0x5b98
00000000`000dfa50 00000000`7792e25a notepad+0x6d33
00000000`000dfaa0 00000000`7792cbaf USER32!GetWindowLongPtrW+0x13a
00000000`000dfb60 00000000`7792cdcd USER32!ReleaseDC+0x9f
1 Id: 1798.310 Suspend: 1 Teb: 000007ff`fffda000 Unfrozen
Child-SP RetAddr Call Site
00000000`0304f528 00000000`7780ed73 ntdll!NtWaitForMultipleObjects+0xa
00000000`0304f530 00000000`7792e96d kernel32!WaitForMultipleObjectsEx+0x113
00000000`0304f640 000007fe`fccb1ab6 USER32!MsgWaitForMultipleObjectsEx+0x13d
00000000`0304f6e0 000007fe`fccb1aef DUser+0x1ab6
00000000`0304f730 000007fe`fccbe4ad DUser+0x1aef
00000000`0304f7a0 000007fe`fccbe3cc DUser!GetMessageExA+0x6d
00000000`0304f7f0 000007fe`fea994e7 DUser!RemoveGadgetProperty+0x59c
00000000`0304f880 000007fe`fea9967d msvcrt!_crtCompareStringW+0xf7
00000000`0304f8b0 00000000`7780cdcd msvcrt!beginthreadex+0x13d
00000000`0304f8e0 00000000`77a2c6e1 kernel32!BaseThreadInitThunk+0xd
00000000`0304f910 00000000`00000000 ntdll!RtlUserThreadStart+0x21
Si nos fijamos, podemos ver que algunos módulos, como kernel32, muestran los nombres de los métodos en cada frame de la pila, mientras que otros, como los de notepad, solamente nos muestran el nombre del módulo y un número hexadecimal al lado. Nuevamente, esto se debe a que no tenemos disponibles símbolos para el modulo notepad.exe (y muchos otros)….
Símbolos: Qué son, para qué nos sirven y dónde los conseguimos
¿Alguna vez os fijasteis en los ficheros de extensión .pdb que acompañan a vuestros binarios en la caperta bindebug? Pues si no los conocíais de antes, os los presento: son los símbolos de vuestro código. Su extensión es un acrónimo de Program Data Base, y su objetivo es servir de traducción entre direcciones de memoria u offsets dentro de un módulo y el nombre que se le ha dado a ese elemento durante el desarrollo, es decir, su identificador.
¿Y para que nos sirven? Precisamente para evitar lo que nos sucede en el ejemplo del notepad de arriba, en el cual no podemos ver casi ninguna información del módulo. A modo de ejemplo, imaginemos que tenemos libreria llamada matematicas.dll y un método dentro de ella llamado Sumar. Si tuviera los símbolos de ésta librería, desde WinDbg podria ver algo como matematicas!Sumar como nombre del método, pero si no tuvieramos los símbolos seguramente veríamos algo parecido a matematicas+0x1ab6, lo cual no nos da ninguna pista de lo que realiza ese método.
Para obtener los símbolos evidentemente hay que recurrir al desarrollador del producto. Si es un producto nuestro, tenemos que asegurarnos de tener los .pdb tanto para las versiones de debug como release (por defecto en Visual Studio, al compilar en modo release, no se generan los símbolos). Si se trata de un producto de terceros dependemos de que la empresa desarrolladora los distribuya. En el caso de Microsoft estamos parcialmente de enhorabuena, ya que gran cantidad de sus productos distribuyen los símbolos; eso si, una versión limitada de ellos, los llamados símbolos públicos.
WinDbg incorpora una gestión de símbolos muy avanzada, de modo que podemos montarnos nuestro propio servidor de símbolos en nuestro disco duro o en un directorio compartido de la red, lo cual es buena idea a medida que la colección va creciendo. Ya veréis como al final acabáis coleccionando símbolos como si fueran cromos 😀
En mi caso, tengo una carpeta local con los símbolos (C:Symbols), y en ella se descargan todos los símbolos que voy obteniendo. El propio WinDbg se encarga de ordenarla en directorios para que no haya problemas de versiones. Sin embargo, con la carpeta local no es suficiente, hay que especificar también servidores desde donde nos podamos descargar los símbolos que no tenemos. En este caso, solo os puedo dar uno, el servidor de símbolos públicos de Microsoft, que se encuentra en http://msdl.microsoft.com/download/symbols. Para especificar la ruta de símbolos podemos establecer la variable de entorno NT_SYMBOL_PATH o modificarla desde la interfaz de usuario del WinDbg (Ctrl+S o menú File -> File Symbol Path…). Para que no digáis que os hago pensar ni un poquitín, os dejo la cadena de símbolos que estoy utilizando para esta demo:
SRV*c:symbols*http://msdl.microsoft.com/download/symbols
Una vez establecido nuestro servidor de símbolos, volvamos a la demo…
… y de vuelta a WinDbg
Con nuestros símbolos en orden, vamos a forzar a WinDbg a que nos vuelva a cargar los módulos y busque por los símbolos apropiados. Para ello emplearemos el comando .reload, que carga los símbolos de nuevo, y le pasamos los parámetros /v (verbose, es decir, nos imprime información detallada de los símbolos durante el proceso de carga) y /f (fuerza la carga de los símbolos. Si no se especifica este parámetro, los símbolos se cargan de modo perezoso, solo cuando son necesitados). A continuación pongo un fragmento de la salida en mi máquina:
0:011> .reload /v /f
Reloading current modules
AddImage: C:Windowssystem32USER32.dll
DllBase = 00000000`77950000
Size = 000ca000
Checksum = 000ce438
TimeDateStamp = 45d3ee19
AddImage: C:Windowssystem32kernel32.dll
DllBase = 00000000`77a20000
Size = 00131000
Checksum = 0013758a
TimeDateStamp = 4549d328
AddImage: C:Windowssystem32ntdll.dll
DllBase = 00000000`77b60000
Size = 0017a000
Checksum = 0017b5ee
TimeDateStamp = 4549d372
AddImage: C:Windowssystem32notepad.exe
DllBase = 00000000`fffe0000
Size = 0002f000
Checksum = 0003024b
TimeDateStamp = 4549bb19
...
Podemos ver como se están cargando los módulos, sus direcciones base, tamaño de los módulos, etc. Si queremos obtener un listado de todos los módulos y su estado, podemos repetir el comando lm que ya lanzamos antes. Esta vez la salida será similar a esta:
0:011> lm
start end module name
00000000`77950000 00000000`77a1a000 USER32 (pdb symbols) (...)
00000000`77a20000 00000000`77b51000 kernel32 (pdb symbols) (...)
00000000`77b60000 00000000`77cda000 ntdll (pdb symbols) (...)
00000000`fffe0000 00000001`0000f000 notepad (pdb symbols) (...)
...
Podemos ver que los módulos ya están cargados (en lugar de deferred, nos aparece la etiqueta pdb symbols, o export symbols, etc.). Vamos a tratar de realizar las dos pruebas de antes, esto es, consultar todos los identificadores expuestos por el modulo notepad, y listar los call stack de algunos hilos para hacernos a la idea de lo que hacen.
En el primer caso, volvemos a lanzar el comando x para consultar información expuesta por el modulo (podemos pensar que x viene de eXplore… estaremos equivocados, pero funciona como regla mnemotécnica :P). En mi máquina tengo la siguiente salida:
0:011> x notepad!*
(...)
00000000`ffff1600 notepad!PrintTime = <no type information>
(...)
00000000`fffe7564 notepad!AlertBox = <no type information>
00000000`ffff0630 notepad!szLoadDrvFail = <no type information>
00000000`fffe25f0 notepad!IID_IQuickFilterPriv = <no type information>
00000000`fffe10b8 notepad!_imp_GetTextExtentPoint32W = <no type information>
(...)
00000000`ffff0578 notepad!szUntitled = <no type information>
(...)
Si recordáis, antes de tener los símbolos cargados no obteníamos ninguna salida al ejecutar este comando, mientras que ahora tenemos una lista enorme con todos los identificadores del módulo. Imaginaros que nos interesa buscar todos los métodos relacionados con la acción de guardar el fichero; podemos intentar lanzar una consulta como la siguiente:
0:011> x notepad!*Save*
00000000`fffe635c notepad!CheckSaveTaskDlgBox = <no type information>
00000000`fffe5068 notepad!ShowOpenSaveDialog = <no type information>
00000000`fffe8268 notepad!SaveGlobals = <no type information>
00000000`fffe54c4 notepad!InvokeLegacySaveDialog = <no type information>
00000000`ffff1384 notepad!fInSaveAsDlg = <no type information>
00000000`fffe32f0 notepad!IID_IViewSaveRestriction = <no type information>
00000000`fffe24d0 notepad!IID_IFileSaveDialogOld = <no type information>
00000000`ffff20c0 notepad!szSaveFilterSpec = <no type information>
00000000`fffe4548 notepad!s_SaveAsHelpIDs = <no type information>
00000000`fffe17a0 notepad!IID_IFileSaveDialog = <no type information>
00000000`fffe55f4 notepad!InvokeSaveDialog = <no type information>
00000000`fffe24a0 notepad!IID_IFileSaveDialogPrivate = <no type information>
00000000`ffff20b0 notepad!g_ftSaveAs = <no type information>
00000000`fffe64c0 notepad!CheckSave = <no type information>
00000000`ffff0620 notepad!szSaveCaption = <no type information>
00000000`fffe7a90 notepad!NpSaveDialogHookProc = <no type information>
00000000`fffe17e0 notepad!CLSID_FileSaveDialog = <no type information>
00000000`fffe33c0 notepad!CLSID_ScreenSaverPage = <no type information>
00000000`fffe9e00 notepad!SaveFile = <no type information>
00000000`fffe1050 notepad!_imp_GetSaveFileNameW = <no type information>
Ahora vamos a comprobar como se ven la pila de llamadas de un hilo del proceso:
0:011> ~*k
0 Id: ff4.6cc Suspend: 1 Teb: 000007ff`fffdc000 Unfrozen
Child-SP RetAddr Call Site
00000000`0010f288 00000000`77a5ed73 ntdll!NtWaitForMultipleObjects+0xa
00000000`0010f290 00000000`7796e96d kernel32!WaitForMultipleObjectsEx+0x10b
00000000`0010f3a0 000007fe`fcf31ab6 USER32!RealMsgWaitForMultipleObjectsEx+0x129
00000000`0010f440 000007fe`fcf3371f DUser!CoreSC::Wait+0x62
00000000`0010f490 000007fe`fcf33696 DUser!CoreSC::WaitMessage+0x6f
00000000`0010f4d0 00000000`7795bd1a DUser!MphWaitMessageEx+0x36
00000000`0010f500 00000000`77bb2016 USER32!_ClientWaitMessageExMPH+0x1a
00000000`0010f550 00000000`7796df2a ntdll!KiUserCallbackDispatcherContinue
00000000`0010f5b8 00000000`779573e9 USER32!ZwUserWaitMessage+0xa
00000000`0010f5c0 00000000`7795760a USER32!DialogBox2+0x261
00000000`0010f640 00000000`779574c6 USER32!InternalDialogBox+0x134
00000000`0010f6a0 00000000`77957918 USER32!DialogBoxIndirectParamAorW+0x58
00000000`0010f6e0 000007fe`fe623d2e USER32!DialogBoxIndirectParamW+0x18
00000000`0010f720 00000000`fffe5146 COMDLG32!CFileOpenSave::Show+0x143
00000000`0010f790 00000000`fffe547e notepad!ShowOpenSaveDialog+0xde
00000000`0010f800 00000000`fffe5b98 notepad!InvokeOpenDialog+0x136
00000000`0010f860 00000000`fffe6d33 notepad!NPCommand+0x380
00000000`0010f990 00000000`7796e25a notepad!NPWndProc+0x55b
00000000`0010f9e0 00000000`7796cbaf USER32!UserCallWinProcCheckWow+0x1ad
00000000`0010faa0 00000000`7796cdcd USER32!DispatchClientMessage+0xc3
(...)
Ahora, a diferencia de cuando los símbolos no estaban cargados, podemos ver mucha mas información en la pila de llamadas, como los nombres de los métodos del módulo de notepad que antes no veíamos. Podemos ver que este hilo en cuestión invoca la ventana para abrir o guardar un fichero, mediante las llamadas a InvokeOpenDialog en notepad, y posteriormente la llamada al método Show de un CFileOpenSave en el modulo COMDLG32, que a su vez llama a… no sigo, que creo que se entiende suficientemente bien y ya me voy cansando 🙂
En fin, con esto hemos dado un repaso general a WinDbg, aunque no hemos hecho nada mas que rascar la superficie. No os preocupéis por los comandos, por las pilas de llamadas ni por nada mas, lo iremos viendo con calma en los siguientes posts (¡si es que os interesan!). Me conformo por ahora con que hayáis podido abrir el WinDbg, adjuntaros a un proceso y comprendido la importancia de los símbolos.
Conclusiones:
En este post hemos comentado las herramientas principales que vamos a emplear, hemos hablado de los símbolos y hemos realizado nuestra primera sesión de depuración con WinDbg: no esta mal. Pero venga… se que lo estáis pensando. A no ser que seáis muy apasionados del bajo nivel, o que tengáis mucho tiempo libre, os estaréis preguntando ¿Para que quiero yo saber todo esto? Pues allá van algunas razones que considero que pueden justificar el pelearse con el WinDbg:
- Si nuestro sistema nos muestra una pantalla azul de la muerte, podemos tratar de analizar el minidump (volcado de memoria de pequeñas dimensiones) que nos genera con WinDbg para determinar que módulo fue el causante del error. Así podemos determinar si el problema se originó en el controlador de disco, de vídeo, en el propio kernel, etc.
- Ciertas aplicaciones, como SQL Server, generan volcados de memoria cuando suceden eventos inesperados, errores, etc. Mediante WinDbg podemos intentar acercarnos al origen del problema, aunque sin los símbolos apropiados (y estos símbolos son, generalmente, privados) será muy dificil profundizar en la investigación. No obstante, al menos tendremos cierto conocimiento de como capturar volcados, que nos facilitará la tarea de colaborar con los servicios de soporte de Microsoft.
- Jugando con WinDbg aumentamos notablemente nuestro conocimiento de la plataforma sobre la que trabajamos, lo cual siempre es positivo. De echo, esta es la razón que personalmente mas me motiva y por la que sigo jugando siempre que puedo con depuradores 🙂
- Tu última sesión de depuración siempre es un buen tema de conversación para esos ratos de cervezas con los colegas… XD
Rock Tip:
Como el artículo de hoy es tan solo una pequeña introducción a lo que espero que sea una serie de artículos sobre WinDbg y depuración en general, el tema ‘Teaser‘, de Yngwie J. Malmsteen, me viene que ni pintado.
Para los que no le conozcáis, el señor Malmsteen es uno de los guitar heroes más conocidos, un virtuoso de corte neoclásico con igual número de seguidores que de detractores (los detractores son por pura envidia cochina xD). Si bien no es mi guitarrista favorito, he de admitir que sus discos mas ochenteros me llegan al alma. Dentro de este estilo os recomendaría especialmente el Eclipse (NOTA: Este link NO lleva a ningún IDE extraño… ¡¡siempre seré fiel a mi Visual Studio!!).