Un ciclo interminable que no fue: la historia del bicho del Santo Grial

Había una vez un juego para GBA llamado Hello Kitty Collection: Miracle Fashion Maker. Fue un lindo juego basado en la famosa franquicia de Sanrio Hello Kitty y desarrollado por Imagineer. Pero bajo la apariencia de un nombre aparentemente inocente era un problema insidioso. Por alguna razón, este sencillo juego no se ejecutó en ningún emulador de GBA. Pero esto por sí solo no sería suficiente para llamar al problema un error del Santo Grial. Como todos los errores del Santo Grial, este error en sí mismo era completamente confuso. La explicación era simple: en algún momento de la secuencia de lanzamiento del juego, cayó en un ciclo del que nunca salió , esperando que un cierto valor se lea desde una memoria que no existe . Aunque hay errores similares en muchos juegos, por ejemplo, en la introducción popularThe Legend of Zelda: The Minish Cap , confían en un comportamiento especial causado por la lectura de direcciones de memoria no válidas. Pero este ciclo parecía violar tal comportamiento. Sin embargo, el juego funcionó en equipos reales. Además, se produjo exactamente el mismo error al cargar una partida guardada en Sonic Pinball Party después de un reinicio en frío. ¿Podría la expectativa de estas direcciones de memoria no válidas ser de alguna manera errónea? Pero si es así, ¿cómo?


Pero esto es ilegal, ¿verdad?


Espera un minuto: si estás intentando acceder a una memoria no válida, entonces el juego solo necesita bloquearse, ¿verdad? Debe ocurrir una operación no resuelta, segfault u otro error. ¿Derecha?

Bueno, es más como sí. Pero no realmente. Al menos no en el GBA.

En la arquitectura de los procesadores ARM que se usaron en GBA, este estado erróneo se denomina cancelación de datos y ocurre solo cuando intenta acceder a la memoria para la cual el administrador de memoria no ha asignado el permiso de lectura 1 . Cuando se produce la cancelación de datos, el procesador completa lo que estaba haciendo y pasa al vector de excepciónasignado a excepciones de cancelación de datos. Luego, el sistema operativo puede elegir una de las soluciones: eliminar el proceso actual, asignar memoria de falla de página , dejar que el proceso maneje la situación, ya que algunos emuladores JIT lo hacen con "fastmem", o realizar otras acciones.

¿Cómo maneja el GBA el aborto de datos? La entrada del vector de excepción para la anulación de datos se encuentra en la ROM de arranque de la consola GBA (o, como también se le llama, en el BIOS). Si el GBA encuentra aborto de datos, entonces trata de ir al controlador DACS 2si existe, de lo contrario se produce el bloqueo. Ningún juego comercial tiene controladores DACS. Entonces, ¿por qué este juego no se congela? Todo es muy simple: GBA nunca genera aborto de datos. No tiene un administrador de memoria (MMU) (o incluso una unidad de protección de memoria, como en DS), por lo que simplemente sigue funcionando y lee la memoria no válida.

El bus de memoria entra en escena.



¿Qué es la memoria no válida en general? ¿Cómo se ve ella? Este es el principal inconveniente. Esta es una situación difícil: lo que lee el código depende en gran medida de lo que hizo recientemente la CPU o, más precisamente, de lo que hizo recientemente el bus de memoria . En resumen, al acceder a una memoria no válida, la CPU lee lo que fue el último en el bus de memoria. Para entender lo que sigue de esto, debe aprender un poco sobre el bus de memoria y cómo funciona.

Un bus de memoria es parte de un circuito electrónico que conecta la CPU a todos los componentes de memoria de la plataforma. En el GBA, varios dispositivos están conectados al bus de memoria: RAM de trabajo, memoria de video y el bus de cartucho. Cuando la CPU intenta acceder a la memoria, le dice al bus de memoria a qué dirección necesita acceder, y luego se activa el componente correspondiente a esa dirección. Luego, el componente coloca el valor en esta dirección en el bus, que puede tomar varios 3 ciclos , y luego la CPU finalmente puede leer el valor del bus. En el caso del GBA, si no hay ningún equipo asociado con la dirección, entonces no se escribe ningún valor en el bus, y la CPU lee cualquier valor colocado en último lugar en el bus. La situación puede variar de varias maneras, por ejemplo, si la lectura fue de 16 bits y la CPU intenta realizar una lectura de 32 bits, pero en general siempre será un valor del bus. Los desarrolladores llaman a esta característica "bus abierto". Anteriormente, escribí cómo afecta a otros juegos .

Bueno, parece que todo no se ve tan mal ... ¿verdad?


Entonces, ¿puedes guardar en caché el último acceso a la memoria? ¿Y luego traerlo de nuevo? En el caso general, este enfoque funcionará, pero existen ciertas dificultades. Primero, debe asegurarse de que todas las operaciones de acceso a la memoria estén en el orden correcto. Esto es más complicado de lo que parece, porque la CPU accede a la memoria con cada instrucción para obtener la siguiente instrucción en la tubería. Y, de hecho, en el caso general *, la memoria atascada en el bus es la última instrucción recibida. Esto simplifica el proceso, ya que solo necesita obtener este último valor preseleccionado. Pero dado que el último valor preseleccionado depende solo de dónde estamos ejecutando actualmente desde la memoria, siempre debe ser el mismo. Incluso si la dirección recibida cambia mientras no es válida,siempre obtendrás el mismo recuerdo.

Uh ... para. Pero este ciclo existe y no se puede salir si este valor está preseleccionado. ¿Entonces qué está pasando? Si recibe constantemente las siguientes instrucciones, ¿qué sucede entre estas operaciones? Traté de ejecutar tales bucles sin fin en las ROM de prueba para verificar si, por ejemplo, el valor podría ir mal. Esto definitivamente puede suceder si el valor no se ha actualizado recientemente, pero el valor se actualiza en cada instrucción, por lo que no tiene tiempo para corromperse. Mis pruebas nunca salieron del bucle. Hice algo diferente que en estos juegos, aunque recreé el ciclo exactamente. ¿Qué hice mal?

Pokémon Esmeralda y ACE, que ocurren solo en hierro


Avance rápido en el tiempo, en enero de 2020. El informe de error en la fiesta de Sonic Pinball en ese momento tenía aproximadamente tres años y medio. En otros emuladores, fue conocido por muchos años. Me he quedado sin teorías de trabajo. A finales de este mes, un usuario con el apodo merrpse unió a la comunidad Discord del emulador mGBA y dijo que Pokémon Emerald tiene una nueva falla de ejecución de código arbitrario (ACE) que solo funciona en hardware. Además, esta falla probablemente sea utilizada por los corredores de velocidad, que pueden querer practicar el emulador. Obviamente, este error se ha convertido en un objetivo atractivo para corregir el error, aunque sería mejor si me enterara antes de la versión 0.8.0. Comencé a investigar el problema técnico y confirme la observación de merrp de que solo funciona en hardware. En todos los emuladores que probé, el juego colgaba con una pantalla negra. Pero merrp me informó que se cuelga de la lectura de la memoria no válida en un bucle, y me di cuenta de que lo más probable es que no pueda solucionar el error en el futuro cercano. Este es nuevamente el mismo error.

Esta vez, aprender sobre las funciones de bucle me dio una ventaja. Gracias al proyecto de descompilación pokeemerald, pude hacer fácilmente cambios específicos en la función para tratar de descubrir cómo se las arregló para salir del círculo. Una versión simplificada de este bucle se parece a esto:

uint16_t type = /* ... */;
for (int32_t i = 0; table[type][i] != 0xFFFF; ++i) {
	uint16_t value = table[type][i] & 0xFE00;
	if (value > 0x7E00) {
		break;
	}
	/* ... */
}

El bucle realiza una tarea bastante simple. Hay una tabla de valores bidimensional. En cada fila de esta tabla de columnas, el typebucle primero intenta determinar si el valor es un cierto valor centinela. Si es así, el ciclo termina. De lo contrario, aplica una máscara al valor y comprueba si es mayor que el valor que se está comprobando. De lo contrario, baja el ciclo. En un caso particular de una falla, el valor typeva más allá de los límites de la tabla, lo que lleva a la aparición de un puntero no válido. Esto significa que cuando intentas accederiPara este elemento de esta columna inexistente, siempre accederemos a memoria no válida. Aunque el desplazamiento de la tabla aumenta con cada iteración del bucle antes de volver a la memoria real, puede necesitar cientos de millones de repeticiones. Por lo tanto, es obvio que no lo hace. Entonces, ¿cómo sale un programa de un ciclo?

Para investigar esto, cambié el ciclo y miré lo que sucedería si saliera instantáneamente del ciclo. Todo resultó ser bastante simple: en este momento, ACE trabajó tanto en el hardware como en el emulador, y nada se colgó. Entonces, en cambio, traté de establecer el color de la pantalla en el valor que lee el programa cuando sale del bucle y se congela para que el color no cambie. Recopilé el código y lo ejecuté en un GBA real. Después de unos segundos de congelación en una pantalla negra, se convirtió en un hermoso tono azul.


MUY AZUL

Pero el emulador todavía colgaba en una pantalla negra. ¿Qué valor leerá si leyó el valor recibido anteriormente? En cambio, se convirtió en un turquesa oscuro.


Fu.

Es decir, el programa, antes de que lograra salir del ciclo, seguramente lo aprobó al menos una vez. También resultó que el tiempo requerido para escapar del ciclo con hierro varía. Esto usualmente tomó de 2 a 30 segundos. Que esta pasando?

Nueva teoría de trabajo


Entonces noté la diferencia entre mi ROM de prueba y el Pokémon Esmeralda cuando colgó. Pokémon tocaba música. Sonic Pinball Party también tocó música. Hello Kitty no tocó música, pero me dio una idea. ¿Qué sucede si se produce una interrupción entre la captación previa y la carga de datos? ¿El programa comienza a buscar previamente el vector de interrupción antes de acceder a la memoria no válida? Rápidamente creé un diseño para esta situación en mGBA, activé las interrupciones en la ROM de prueba y, por supuesto, se salió del circuito. Luego probé la misma ROM de prueba en hardware y ... no salió del circuito. Y así surgió la teoría. Al final, me di cuenta de algo. Estoy seguro de que notó un asterisco arriba, así que sí, puede haber un evento entre la captación previa y el acceso a la memoria,pero solo si, entre la captación previa y el acceso a la memoria no válida, el bus de memoria envía una solicitud no a la CPU, sino a otra cosa.

Dije que el bus de memoria está controlado por la CPU. En su mayor parte, esto es cierto, pero hay otros equipos importantes que también tienen acceso al bus de memoria sin pasar por el procesador. Este proceso se llama acceso directo a la memoria . Hablé sobre DMA en un artículo anterior , así que ahora no entraré en los principios de su trabajo. Si vuelve a leer el artículo, puede notar que dije que la CPU principal se detiene mientras DMA se está ejecutando. Esto significa que mientras DMA se está ejecutando, el valor en el bus ahora será el último acceso a la memoria de DMA. Esto es principalmente importante si el DMA va más allá de la memoria real a una región no válida; sin embargo, duplica el último buen valor.

Hace tiempo que se sabe que si carga memoria no válida en DMA, obtendrá el último valor de DMA, pero lo implementé en mGBA durante mucho tiempo y ya lo olvidé. Cuando vi esto en el código de acceso para memoria no válida al estudiar el error, algo hizo clic en mi cabeza. ¿Qué pasa si el valor DMA permanece en el bus para una instrucción? Si la primera instrucción después de DMA termina de cargar la memoria no válida antes de que obtenga el siguiente valor, en teoría esto debería conducir a recargar el valor de DMA. Además, reproducir música en GBA generalmente usa DMA para transmitir la salida de audio. Para la correcta implementación de esto, se requiere un emulador de precisión táctil que pueda bloquear la CPU en el medio de la ejecución de la instrucción, entre el inicio de la instrucción y el acceso a la memoria, y la emulación de consola GBA en el emulador mGBA no es precisa al tacto.Y esto es algo para mí.recuerda . Afortunadamente, logré solucionar este problema. La solución es imperfecta, pero ahora puedo comparar la dirección de CPU esperada para la instrucción después de DMA con la dirección de CPU actual para una carga no válida y usar una sola dirección en lugar del valor preseleccionado para este valor de DMA.

La decisión tan esperada


Encendí las operaciones de DMA para H-blank en la ROM de prueba y las sincronicé con V-blank para que los tiempos fueran estables, lo ejecuté en hardware y ... ¡esta vez funcionó! La ROM de prueba salió constantemente del bucle después del mismo número de iteraciones cuando se leyó el valor DMA desde el bus. ¡Yo tenía razón! Para la correcta implementación de esto en mGBA, se requirieron varios intentos, pero ahora el programa sale del ciclo con los mismos resultados que en el hardware. Finalmente obtuve un tono de azul en mGBA. Hello Kitty ha arrancado. Ahorrar en la fiesta de Sonic Pinball ha ganado.

Lo hice.

Este fue probablemente el tiempo más largo que pasé en un solo error. En el transcurso de tres años, invertí tanto tiempo en depurarlo que perdí la cuenta, y estoy seguro de que otros desarrolladores también enfrentaron situaciones similares en sus emuladores. Sin esta idea, me podría haber llevado otro año, o incluso más, pero la pantalla negra, en la que no sucedió nada excepto tocar música, se convirtió en ese dominó que condujo al colapso de todo el problema.

Ahora que se encuentra la solución, se puede implementar en otros emuladores de GBA, poniendo fin a este error. El error se solucionará en mGBA 0.9.0, que, espero, se lanzará este año, y ya se ha solucionado en las versiones de prueba. Finalmente puedes jugar Hello Kitty Collection: Miracle Fashion Maker. A menos que, por supuesto, lo desees, no me corresponde juzgarte.

imagen

  1. Si intenta ejecutar memoria que no tiene permisos de ejecución, esto se llama aborto de captación previa.
  2. DACS (abreviatura de Depuración y Sistema de comunicación) es parte del kit de desarrollo GBA.
  3. Estos ciclos inactivos mientras se lee desde el bus a veces se llaman estados de espera.

All Articles