Los debuggers y el principio de incertidumbre
Hace casi ochenta años que fué enunciado uno de los principios más conocidos de la mecánica cuántica: el principio de incertidumbre o indeterminación de Heinsenberg. Para explicar este principio en lenguaje coloquial se puede decir que mientras más certeza se tenga sobre la posición de una partÃcula menos se conocerá su velocidad, y mientras más se conozca su velocidad, menos se sabrá acerca de su posición. Si se intenta medir de alguna forma la posición de la partÃcula, la propia medición alterará su velocidad, y lo mismo le ocurrirá a la posición si se intenta determinar la velocidad. El principio de incertidumbre de Heinsenberg habla de que todo sistema se altera al ser observado. A nivel atómico no existen observadores absolutamente pasivos. Pero no hay que profundizar tanto en la estructura de la materia para comprobar la validez del principio de incertidumbre, en el mundo de los ceros y los unos también se cumple esta ley universal.
Hace ya algunos añitos estuve escribiendo un driver para Windows 9X, los otrora populares y ahora casi extintos VXDs. El driver tenÃa una función «callback» que proporcionaba al sistema operativo, para que este le notificara acerca de un determinado evento. Hace tanto tiempo que he olvidado los detalles, pero recuerdo perfectamente que en cierto momento, luego de modificar la función «callback», el driver empezó a provocar pantallas azules apenas era cargado. Por aquel entonces (y también por este) mi debugger favorito era SoftICE, y mis sesiones de depuración del driver empezaban colocando una instrucción «__asm INT 3» en el lugar del código que me interesaba examinar. Una vez que SoftICE se detenÃa en la INT 3 insertada en el código, bien cargaba el fichero de sÃmbolos generado por el compilador, o bien depuraba directamente en ensamblador, según me resultara más cómodo. Asà que cuando el driver empezó a provocar las pantallas azules hice lo de siempre, puse la INT 3 para forzar el breakpoint al principio de la función «callback» y me puse a depurar. Llegé al RET y no habÃa pasado nada malo, asà que resumà la ejecución y para mi sorpresa el driver funcionaba bien. Cada vez que paraba en el breakpoint seguÃa el código paso a paso, pero nada, el bug se habÃa esfumado. Quité la INT 3 y volvà a compilar, cargué el driver… y otra vez la pantalla azul. Volvà a poner la INT 3 para parar en la función y mirar más atentamente el código, compilé, cargué el driver, y SoftICE saltó como debÃa. Después de mirar el código en todos sus detalles llegué a la conclusión que no habÃa nada raro, asà que dejé que corriera, y otra vez arreglado, no saltaba la pantalla azul. Después de hacerlo tres o cuatro veces acabé por convencerme de que al poner la INT 3 para depurar con SoftICE el bug desaparecÃa, y que volvÃa a aparecer en cuanto la quitaba, ¡pero entonces no podÃa depurar!. Era un bug que aparecÃa justo cuando no podÃa verlo.
Entonces opté por un análisis estático. Compilé el driver con INT 3 y sin ella, y desensamblé ambas versiones con IDA Pro para ver las diferencias, asà pude comprobar que el compilador generaba código radicalmente distinto por el simple hecho de poner o quitar la INT 3. Yo esperaba que fueran códigos muy similares, con la obvia diferencia que uno tendrÃa la INT 3 embebida y el otro no, pero no resultó asÃ. El código que tenÃa la INT 3 usaba además un marco de pila basado en EBP, accedÃa a las variables locales usando EBP como base, el otro usaba directamente el ESP para acceder a las variables locales, la mayorÃa de las cuales intentaba mantener en los registros para ganar en velocidad. En otras palabras, al colocar la INT 3 el compilador se comportaba como si estuviera en modo «debug», prescindiendo de las optimizaciones y generando todas las funciones con marcos de pila basados en EBP. Al quitar la INT 3, el driver se compilaba en modo «release», con las optimizaciones activadas, eliminando los marcos de pila basados en EBP.
Precisamente la causa del bug estaba relacionada con el registro EBP. El código de Windows que llamaba a la función «callback» dentro de mi driver asumÃa que al retornar de la función el registro EBP se mantendrÃa con el mismo valor. Cuando la función era compilada con marcos de pila basados en EBP no ocurrÃa nada malo, puesto que lo primero que hace una función con estas caracterÃsticas es precisamente guardar el contenido de EBP en la pila para restaurarlo justo antes del retorno, pero al no usar EBP como marco de pila, este registro se usaba con otros propósitos y el compilador no se tomaba el trabajo de guardar su valor inicial y luego restaurarlo. Esto provocaba un fallo en el código de Windows al retornar mi función «callback» y encontrarse con que el valor de EBP habÃa cambiado.
Por supuesto la solución fue usar un #pragma para obligar al compilador a que usara marcos de pilas basados en EBP, al menos para la función «callback», y asunto resuelto, el problema no volvió a ocurrir. Nunca antes habÃa estado ante una situación en la que se viera tan claramente que el intento de depurar un programa puede modificar su comportamiento, e incluso hacer desaparecer el bug que se intenta encontrar. Principio de Heinsenberg en estado puro, y eso que todavÃa no tenemos ordenadores cuánticos.