A la caza del elefante poco común

Cazar bichos es divertido. Y de vez en cuando sales vivo con una historia con la que aburrir a tus nietos ("En mis tiempos, aún cazábamos bichos con palos y piedras" y todo eso).
La GDC 2014 nos tenía reservado otro safari de caza digno de trofeo. Estábamos a cinco días de presentar Unity 5 al mundo cuando "descubrimos" (bueno, era difícil no darse cuenta) un pequeño y feo error: nuestro nuevo editor de 64 bits se bloqueaba aleatoriamente en OSX hasta el punto de ser completamente inutilizable. No hay nada como estar en el escenario para mostrar lo increíble que es tu reportero de bichos cada dos minutos.
Así que Levi, Jonathan y yo dejamos todas las cosas increíbles en las que estamos trabajando (más historias con las que queremos aburrir a nuestros nietos) y nos fuimos a acechar. Todo lo que sabíamos en ese momento era que se bloqueaba en algún punto del código nativo que Mono genera en tiempo de ejecución.
Como todo programador sabe, cuando te enfrentas a un fallo que no es obvio, simplemente empiezas por reunir pruebas. Una vez que hayas aprendido lo suficiente sobre los patrones de comportamiento del bicho, al final conseguirás dispararle. Y con el reloj en marcha, estábamos listos para disparar a casi cualquier cosa.
Pero estábamos perplejos. Para ser un elefante, el bicho resultó ser sorprendentemente ágil y escurridizo.
Parecía suceder sólo en OSX 10.9 aunque Kim vio algo que parecía notablemente similar en Windows con su rama de depuración de memoria de alta resistencia. Y si activabas Guard Malloc en versiones anteriores de OSX, también obtenías algo bastante parecido. Sin embargo, como fallaba en código de script aleatorio a profundidades arbitrarias en la jerarquía de llamadas, era difícil decir con certeza qué era el mismo fallo y qué no. Y el choque puede ser constante durante diez carreras consecutivas para ser totalmente diferente en las cinco siguientes.
Así que mientras Kim y yo nos adentrábamos hasta las rodillas en la memoria y hasta los muslos en el código ensamblador, Levi realizaba un seguimiento exhaustivo de todas las actividades secretas y no tan secretas de Mono para generar un registro de gigabytes y un editor que funcionaba a la velocidad de mi abuela. Esto yielded the first interesting insight: apparently we were always compiling the method we crash in right before things got ugly.
Pero, ¿qué hizo que se estrellara? La causa inmediata era que estábamos intentando ejecutar código desde una dirección no válida. ¿Cómo hemos llegado hasta allí? ¿Un error en el manejo de señales de Mono por el que no se reanuda correctamente? ¿Un error en el compilador JIT de Mono que no salta correctamente al código compilado? ¿Un hilo diferente corrompiendo la memoria de la pila en el hilo principal? ¿Hadas y grumkins? (por un momento, esto último parecía lo más probable).
Tras dos días de caza, el elefante seguía bien vivo y en libertad.
Así que el sábado por la noche me equipé con un cuaderno, cuatro bolígrafos de colores diferentes y un amplio suministro de cerveza de nuestra característica nevera Unity (asegurándome cuidadosamente de no tocar la horrible cerveza de Navidad en lata que aún tenemos pegada en sus grietas). Luego puse en marcha las instancias de Unity hasta que tuve cuatro cuelgues diferentes congelados en el depurador, los etiqueté como "cuelgue rojo", "cuelgue azul", "cuelgue verde" y "cuelgue negro" y me puse a trabajar con mis bolígrafos de colores respectivamente para tomar notas y dibujar algunos diagramas no muy bonitos de todo lo que encontré.
Aquí están mis notas para Blue Crash:
Y fue entonces cuando hice mi primer descubrimiento: en todos los casos, ¡la pila era 16 bytes mayor de lo que debería ser!
Esto nos llevó al siguiente descubrimiento: en todas las caídas, al mirar esos 16 bytes extra, aparecía una dirección de retorno a la función en la que nos habíamos caído. A partir de un rastreo estaba claro que en todos los casos ya habíamos ejecutado algunas llamadas del mismo método, y al principio pensé que la dirección era de la última llamada que habíamos rastreado. Sin embargo, una inspección más detallada reveló que en realidad era la dirección de retorno de una llamada cuyo método aún no se había compilado.
Esto me desconcertó por un momento, ya que en algunos casos había varias llamadas entre el último método rastreado y esta llamada que tampoco se habían compilado todavía. Sin embargo, una mirada más atenta reveló que siempre habíamos saltado a su alrededor.
Entonces, miré esa función de la que aparentemente debíamos regresar...
Y ahí lo tenemos (resaltado en azul): ¡Estábamos saltando en la dirección equivocada!
Lo que Mono hace aquí es crear pequeñas funciones "trampolín" que sólo contienen una llamada al compilador JIT y algunos datos codificados en el flujo de instrucciones después de la llamada (utilizados por el compilador JIT para saber qué método compilar). Una vez que el compilador JIT haya hecho su trabajo, eliminará esos trampolines y borrará todo rastro de haberse enganchado a la llamada al método.
Sin embargo, la instrucción de llamada que ves ahí es lo que se llama una "llamada cercana" que, por cierto, utiliza un desplazamiento de 32 bits con signo para saltar en relación con la siguiente instrucción.
Y como un número de 32 bits con signo sólo puede alcanzar 2 GB de arriba abajo y aquí estamos ejecutando 64 bits, de repente supimos por qué la disposición de la memoria heap desempeñaba un papel tan crucial en la reproducción del fallo: una vez que los trampolines de Mono se alejaban más de 2 GB del compilador JIT, los offsets ya no cabían en 32 bits y se truncaban al emitir la instrucción call.
En ese momento, Jonathan encontró rápidamente la solución adecuada y, para cuando terminó su domingo, teníamos una versión estable lista a tiempo para la GDC.
Todos conocéis la historia de allí. Presentamos con éxito Unity 5 en la GDC 2014 con críticas muy favorables y, tras su lanzamiento, se convirtió rápidamente en el software más querido de la historia. Oh, espera, esa parte está aún por llegar...
Antes de ese lanzamiento, hay un montón más de fallos negros y azules que arreglar :).