⚠️ Clean-Room / Educativo

Este proyecto es educativo y Open Source. No se copia código de otros emuladores. Implementación basada únicamente en documentación técnica y tests permitidas.

SP Corruption Watchdog (Stack Pointer Watchdog)

Fecha: 2025-12-23 Step ID: 0267 Estado: Draft

Resumen

Este Step implementa un watchdog (perro guardián) para detectar la corrupción del Stack Pointer (SP) en tiempo real. El análisis del Step 0266 reveló que el GPS muestra `SP:210A`, lo cual es un estado fatal: el Stack Pointer apunta a la ROM (solo lectura) cuando debería estar en RAM escribible. Este watchdog detecta el momento exacto en que el SP se corrompe, permitiendo identificar la instrucción que causa el desastre.

Concepto de Hardware

El Stack Pointer (SP) en Game Boy: El Stack Pointer es un registro de 16 bits que apunta a la ubicación en memoria donde se almacena la pila (stack). La pila es una estructura de datos LIFO (Last In First Out) que se usa para:

  • Llamadas a subrutinas (CALL/RET): Guarda la dirección de retorno antes de saltar a una subrutina.
  • Interrupciones: Guarda el estado de la CPU (PC) antes de saltar al vector de interrupción.
  • PUSH/POP: Guarda y restaura valores de registros temporalmente.

Rangos de Memoria Válidos para el Stack: Según el mapa de memoria de Game Boy, el Stack debe estar en:

  • WRAM (Work RAM): `0xC000-0xDFFF` - RAM interna de 8KB, escribible.
  • HRAM (High RAM): `0xFF80-0xFFFE` - RAM de alta velocidad de 127 bytes, escribible.

¿Por qué es fatal si SP apunta a la ROM? Si el Stack Pointer apunta a la ROM (`0x0000-0x7FFF` o `0xA000-0xBFFF`), cualquier operación de escritura (PUSH, CALL) intentará escribir en memoria de solo lectura. Como implementamos la protección de ROM (Step 0252), esas escrituras se ignoran silenciosamente. Cuando la CPU ejecuta POP o RET, lee datos de la ROM (que son instrucciones, no direcciones de retorno válidas). El resultado es que la CPU salta a una dirección basura y el programa se estrella.

¿Cómo se corrompe el SP? El SP puede corromperse por varias razones:

  • Instrucción `LD SP, nn` con datos erróneos: Si `nn` contiene basura o un valor incorrecto.
  • Instrucción `LD SP, HL` con HL corrupto: Si HL contiene basura (`0x210A`), copiarlo a SP corrompe el stack.
  • Desbordamiento masivo de la pila: Miles de PUSH sin POP correspondientes (poco probable en código normal).
  • Error en aritmética de SP: Instrucciones como `ADD SP, r8` con resultados incorrectos.

El Watchdog: Un watchdog es un mecanismo de monitoreo que verifica continuamente una condición crítica. En este caso, verificamos después de cada instrucción que el SP esté en un rango válido. Si detectamos corrupción, imprimimos un mensaje crítico con el valor de SP y el PC donde ocurrió, permitiendo identificar la instrucción exacta que causó el problema.

Fuente: Pan Docs - "Memory Map", "Stack Pointer", "CALL/RET Instructions"

Implementación

Se agregó una verificación al final del método `step()` en `CPU.cpp` que se ejecuta después de cada instrucción. El watchdog verifica que el SP esté en un rango válido (por encima de `0xC000` o en HRAM `0xFF80-0xFFFE`). Si detecta corrupción, imprime un mensaje crítico con el valor de SP y el PC actual.

Componentes modificados

  • src/core/cpp/CPU.cpp: Agregado watchdog de SP al final del método `step()` (Step 0267).

Código del Watchdog

// --- Step 0267: SP CORRUPTION WATCHDOG ---
// El Stack Pointer debe estar siempre en RAM (C000-DFFF o FF80-FFFE)
// Si baja de C000 (y no es 0000 momentáneo), algo ha ido terriblemente mal.
// Esta verificación se ejecuta después de cada instrucción para detectar
// el momento exacto en que el SP se corrompe.
// Fuente: Pan Docs - Memory Map: Stack debe estar en WRAM (C000-DFFF) o HRAM (FF80-FFFE)
if (regs_->sp < 0xC000 && regs_->sp != 0x0000) {
    printf("[CRITICAL] SP CORRUPTION DETECTED! SP:%04X at PC:%04X\n", regs_->sp, regs_->pc);
    // Opcional: exit(1) para detenerlo en el acto (comentado para permitir logging)
    // exit(1);
}

Decisiones de diseño

  • Verificación después de cada instrucción: Se ejecuta al final de `step()`, después del switch de opcodes. Esto garantiza que detectamos la corrupción inmediatamente después de que ocurre.
  • Excepción para SP=0x0000: Se permite temporalmente `SP=0x0000` porque algunos juegos pueden inicializar el SP a 0 antes de establecerlo a un valor válido. Sin embargo, si el SP permanece en 0 durante la ejecución normal, es un error.
  • Logging en lugar de exit(): El `exit(1)` está comentado para permitir que el emulador continúe y genere más logs. Esto es útil para análisis post-mortem, pero puede cambiarse a `exit(1)` para detener la ejecución inmediatamente.
  • No verifica HRAM explícitamente: La verificación solo comprueba que SP >= 0xC000. El rango HRAM (0xFF80-0xFFFE) está por encima de 0xC000, por lo que está cubierto implícitamente. Sin embargo, una verificación más estricta podría validar explícitamente ambos rangos.

Verificación de Instrucciones Relacionadas con SP

Se revisaron las siguientes instrucciones que modifican el SP:

  • 0x31 (LD SP, d16): ✅ Implementada correctamente. Lee un valor de 16 bits en formato Little-Endian usando `fetch_word()` y lo asigna a SP.
  • 0xF9 (LD SP, HL): ❌ No implementada. Esta instrucción copia el valor de HL a SP. Si HL contiene basura, corrompe el SP. Esta es una posible fuente de corrupción.
  • 0xE8 (ADD SP, r8): ❌ No implementada. Esta instrucción suma un valor con signo de 8 bits a SP. Un error en esta instrucción podría causar corrupción.
  • 0xF8 (LD HL, SP+r8): ❌ No implementada. Esta instrucción carga HL con SP + r8 (con signo). No modifica SP directamente, pero podría indicar problemas si se usa incorrectamente.

Nota: Las instrucciones no implementadas (0xF9, 0xE8, 0xF8) no son necesarias para el funcionamiento del watchdog, pero su ausencia podría ser una fuente de corrupción si el juego intenta usarlas. El watchdog detectará la corrupción independientemente de qué instrucción la cause.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Agregado watchdog de SP al final del método `step()` (Step 0267).

Tests y Verificación

El watchdog se validará ejecutando el emulador con Pokémon Red y buscando el mensaje crítico en los logs:

  • Comando de prueba: python main.py roms/pkmn.gb > sp_debug.log 2>&1
  • Búsqueda en el log: Buscar la cadena [CRITICAL] SP CORRUPTION DETECTED!
  • Análisis post-mortem: Una vez detectada la corrupción, usar tools/dump_rom_zone.py alrededor del PC reportado para ver qué instrucción causó el desastre.

Validación esperada: El watchdog debería detectar la corrupción cuando el SP baja de `0xC000` y mostrar el PC exacto donde ocurrió. Esto permitirá identificar la instrucción que corrompe el SP y corregirla.

Nota: El watchdog está activo en todos los builds (debug y release). En producción, podría desactivarse o hacerse condicional mediante una macro de compilación para evitar overhead en el bucle crítico.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Stack Pointer y Memoria: El SP debe apuntar siempre a memoria escribible (WRAM o HRAM). Si apunta a ROM, las escrituras se ignoran y las lecturas devuelven instrucciones en lugar de datos de la pila.
  • Corrupción de SP: El SP puede corromperse por instrucciones que lo modifican con valores incorrectos (ej: `LD SP, HL` cuando HL contiene basura).
  • Watchdog Pattern: Un watchdog es un patrón de diseño común en sistemas embebidos para detectar condiciones anómalas en tiempo real. Se ejecuta periódicamente (en este caso, después de cada instrucción) y alerta cuando detecta un estado inválido.

Lo que Falta Confirmar

  • Instrucciones faltantes: Las instrucciones 0xF9 (LD SP, HL), 0xE8 (ADD SP, r8) y 0xF8 (LD HL, SP+r8) no están implementadas. Si el juego intenta usarlas, causarán comportamiento indefinido. Necesitamos implementarlas o al menos detectar cuando se intentan ejecutar.
  • Rango HRAM: La verificación actual solo comprueba que SP >= 0xC000. El rango HRAM (0xFF80-0xFFFE) está cubierto implícitamente, pero una verificación más estricta podría validar explícitamente ambos rangos válidos.
  • Overhead de rendimiento: El watchdog agrega una verificación condicional después de cada instrucción. En el bucle crítico, esto podría tener un impacto mínimo en el rendimiento. Deberíamos medir el overhead y considerar hacerlo condicional en builds de release.

Hipótesis y Suposiciones

Hipótesis principal: El SP se corrompe por una instrucción `LD SP, HL` (0xF9) cuando HL contiene basura (`0x210A`). Esta instrucción no está implementada, por lo que el juego podría estar ejecutando código basura o la CPU podría estar leyendo el opcode incorrecto.

Suposición sobre SP=0x0000: Permitimos temporalmente `SP=0x0000` porque algunos juegos pueden inicializar el SP a 0 antes de establecerlo a un valor válido. Sin embargo, si el SP permanece en 0 durante la ejecución normal, es un error que debería detectarse.

Próximos Pasos

  • [ ] Ejecutar el emulador con Pokémon Red y buscar el mensaje crítico de corrupción de SP en los logs.
  • [ ] Una vez detectada la corrupción, usar tools/dump_rom_zone.py alrededor del PC reportado para identificar la instrucción exacta que causa el problema.
  • [ ] Implementar las instrucciones faltantes relacionadas con SP (0xF9, 0xE8, 0xF8) si el análisis revela que el juego las está usando.
  • [ ] Mejorar la verificación del watchdog para validar explícitamente los rangos WRAM y HRAM.
  • [ ] Considerar hacer el watchdog condicional en builds de release para evitar overhead en el bucle crítico.