Heisenbug: el bug que cambia mientras se depura

Existen bugs de código que cambian de estado o desaparecen por cuestiones ajenas al propio código. Son los llamados "Heisenbugs".

Los fallos de código (más comúnmente conocidos como bugs) son situaciones en las que el código que hemos programado no cumple la función que debiera. Un fallo de sintaxis (algún caracter de sobra o que falte), un cambio de tipos de datos o la ejecución de código no deseado son varias de las numerosas razones que pueden originar que nuestro código no funcione como se espera.

Existen metodologías (por ejemplo, TDD) que reducen drásticamente los bugs que a priori introducimos en nuestro código, pero ¿qué ocurre cuando hay bugs que alteran su comportamiento mientras los depuramos? Os presento a los heisenbugs.

¿Qué es un heisenbug?

El término heisenbug fue acuñado en honor al físico Werner Heisenberg y se define de la siguiente forma:

Un heisenbug es un bug software que parece desaparecer o alterar su comportamiento cuando se intenta estudiar.

La similitud con el efecto observador de la mecánica cuántica es la que justifica el nombre; el efecto observador es la teoría por la cual se afirma que observar una situación o un fenómeno necesariamente lo modifica.

El término no es nuevo, y ya fue utilizado por Jim Gray en un paper sobre fallos software en el año 1985, y por Jonathan Clark y Zhahai Stewart en la lista de correo comp.risks en 1986.

¿Cómo puede producirse un heisenbug?

Un heisenbug ocurre normalmente cuando se intentan cambiar valores mientras se depura un programa. Al cambiar las direcciones de memoria de las variables y continuar la ejecución, el código no apunta a las direcciones anteriores sino a las nuevas. Consecuentemente el código no reproduce el fallo que estamos intentando resolver.

Un fallo muy típica es el que se da cuando se ejecuta un programa que utiliza un compilador optimizado, pero que luego no es posible reproducir cuando el programa es compilado sin optimización. Cuando se depura, los valores que normalmente se mantienen en registros en un programa optimizado son movidos a la memoria principal. Esto puede afectar, por ejemplo, al resultado de una comparación de punto flotante ya que el valor en memoria podría tener una precisión y rango mayor que el que está en registro.

Otros casos comunes son el uso de variables no inicializadas, las cuales pueden cambiar su dirección y/o su valor durante el inicio de la depuración, o seguir un puntero inválido, que al depurar puede apuntar a una dirección distinta que cuando no se está depurando.

De hecho, los watchers u otras interfaces que suelen traer los depuradores pueden añadir código adicional (como funciones para acceder a propiedades) que se ejecutan de manera invisible y podrían introducir cambios en el estado de nuestro programa.

El tiempo puede también ser un factor que incluya heisenbugs, particularmente en aplicaciones multi-hilo (multi-thread). La ejecución de un programa bajo un depurador puede cambiar el tiempo de ejecución de un programa comparado con una ejecución normal. Bugs relacionados con race conditions pueden no ocurrir si el programa se ejecuta más lentamente al estar usando un depurador.

Ejemplo

Un ejemplo de heisenbug fue posteado en StackOverflow en 2015. El código calculaba el área entre dos curvas usando una estructura do-while (difícil de depurar manualmente usando breakpoints). Para depurarlo más cómodamente el programador introdujo una línea que imprimía por pantalla el valor calculado; sin embargo, si quitaba esta línea el bucle se ejecutaba infinitamente pese a ser una simple línea para imprimir por pantalla.

El problema era que si la instrucción de imprimir por pantalla estaba presente la comparación se hacía en registros de la CPU (guardados como double) y la comparación nunca daba verdadera ocasionando así el bucle infinito. Sin embargo, cuando se imprimía el valor el compilador decidía mover este resultado a la memoria principal donde se truncaba su valor, y por eso el código se ejecutaba correctamente al cumplirse la condición de parada en algún momento.

¿Cómo resolverlos?

Los heisenbugs son difíciles tanto de identificar como de arreglar. De hecho, intentar arreglar uno puede originar un comportamiento todavía más caótico en el sistema. Debido a la naturaleza cambiante del mismo son difíciles de predecir y analizar durante la depuración.

heisenbugs-comic
Viñeta cómica de Geek & Poke titulada “Cómo depurar un Heisenbug”

Existen programas como Jinx Debugger para depurar y determinar bugs de este tipo, pero no hay una forma 100% efectiva de solucionarlos. Microsoft también tiene una herramienta llamada CHESS para depurar este tipo de errores; las últimas publicaciones son de 2010 así que dudo que siga en activo.

Por lo pronto a mí nunca me ha pasado nada similar. Según he leído parece algo más típico de lenguajes compilados pero quién sabe, todavía queda mucho desarrollo por hacer…

Enlaces relacionados