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.
Diagnóstico de Segmentation Fault con Trazas Nativas
Resumen
El emulador está crasheando con un Segmentation Fault al ejecutar ROMs, lo que indica un error de acceso a memoria en el núcleo C++. Para diagnosticar el problema, se ha instrumentado la CPU C++ con logging detallado usando std::cout que imprime el estado de la CPU (PC, opcode, registros) en cada ciclo de instrucción. Esto permitirá identificar la última instrucción ejecutada antes del crash y, específicamente, detectar si alguna instrucción de salto (JP, JR, CALL, RET) está calculando una dirección de destino inválida.
Concepto de Hardware
Un Segmentation Fault (o "access violation" en Windows) es un error de bajo nivel que ocurre cuando un programa intenta acceder a una dirección de memoria que no le pertenece o que está protegida por el sistema operativo. En el contexto de un emulador, esto generalmente significa que:
- La CPU intentó leer un opcode desde una dirección de memoria inválida (fuera del rango 0x0000-0xFFFF).
- Una instrucción de salto calculó una dirección de destino corrupta o fuera de rango.
- El puntero PC (Program Counter) se corrompió y apunta a memoria no mapeada.
En la Game Boy, todas las direcciones de memoria son válidas en el rango de 16 bits (0x0000-0xFFFF), pero el sistema operativo del host puede proteger ciertas regiones de memoria. Si la CPU C++ intenta hacer fetch_byte() desde una dirección que el sistema operativo considera inválida (por ejemplo, si PC se corrompió y tiene un valor como 0xFFFFFFFF), el sistema operativo detiene el programa con un segfault.
Estrategia de Depuración: El logging en C++ es una técnica clásica de depuración de bajo nivel. Al imprimir el estado de la CPU antes de cada instrucción, creamos una "caja negra" que nos cuenta todo lo que hace. Cuando el programa crashea, la última línea impresa en la consola será la instrucción que causó el problema. Esto es especialmente útil para encontrar bugs en instrucciones de salto, ya que podemos ver exactamente qué dirección calculó y compararla con lo que debería ser.
Implementación
Se ha añadido logging temporal a la CPU C++ para rastrear cada ciclo de instrucción. El logging se implementa en dos niveles:
1. Logging General en CPU::step()
Justo después de leer el opcode (antes del switch), se imprime el estado completo de la CPU:
- PC: Dirección actual del Program Counter (antes de leer el opcode)
- Opcode: Código de operación leído de memoria
- Registros: AF, BC, DE, HL, SP (en hexadecimal)
Este log se imprime para cada instrucción, lo que generará una salida masiva pero permitirá ver exactamente qué estaba haciendo la CPU cuando crasheó.
2. Logging Específico en Instrucciones de Salto
Se añadió logging adicional en las instrucciones que modifican PC:
- JP nn (0xC3): Muestra la dirección destino calculada
- JR e (0x18): Muestra el offset (con signo) y el nuevo PC calculado
- JR NZ, e (0x20): Muestra si saltó o no (según el flag Z) y el nuevo PC si saltó
- CALL nn (0xCD): Muestra la dirección destino y la dirección de retorno guardada en la pila
- RET (0xC9): Muestra la dirección de retorno recuperada de la pila
Este logging específico es crucial porque los saltos son la causa más probable del segfault: si una instrucción calcula una dirección inválida, el siguiente fetch_byte() intentará leer desde esa dirección y crasheará.
Decisiones de Diseño
- Logging Temporal: Este logging es temporal y se eliminará después de encontrar y arreglar el bug. El I/O de consola es extremadamente lento y no debe estar en el bucle crítico de emulación en producción.
- Formato Hexadecimal: Todos los valores se imprimen en hexadecimal con padding de ceros para facilitar la lectura y comparación con documentación técnica.
- PC Antes del Fetch: Se guarda el PC antes de llamar a
fetch_byte(), porquefetch_byte()incrementa PC. Esto nos muestra la dirección exacta donde estaba la CPU cuando leyó el opcode.
Archivos Afectados
src/core/cpp/CPU.hpp- Añadidos includes paraiostreameiomanipsrc/core/cpp/CPU.cpp- Añadido logging enCPU::step()y en instrucciones de salto (JP, JR, CALL, RET)
Tests y Verificación
Nota: Este paso es de diagnóstico, no de implementación. Los tests se ejecutarán después de identificar y corregir el bug.
Proceso de Depuración:
- Recompilar el módulo C++ con el logging:
.\rebuild_cpp.ps1 - Ejecutar el emulador con una ROM:
python main.py roms/tetris.gb - Observar la salida de la consola hasta que crashee
- Analizar las últimas 5-10 líneas impresas antes del crash
- Identificar la instrucción problemática y la dirección calculada
- Corregir el bug en la instrucción identificada
- Eliminar el logging temporal
- Recompilar y verificar que el crash se haya solucionado
Validación Nativa: El logging se ejecuta directamente en C++, sin pasar por Python, lo que garantiza que capturamos el estado exacto de la CPU justo antes del crash.
Fuentes Consultadas
- Pan Docs: CPU Instruction Set - Referencia para las instrucciones de salto
- Documentación de C++: I/O Manipulators - Para formateo hexadecimal con
std::hex,std::setw,std::setfill
Integridad Educativa
Lo que Entiendo Ahora
- Segmentation Fault: Es un error de acceso a memoria que ocurre cuando el programa intenta leer/escribir en una dirección inválida. En nuestro caso, probablemente PC se corrompió y apunta fuera del rango válido de memoria.
- Logging en C++: Aunque es lento, el logging con
std::coutes una herramienta poderosa para depuración de bajo nivel. Nos permite ver el estado exacto de la CPU en cada ciclo. - Debugging de Emuladores: Los segfaults en emuladores suelen ser causados por instrucciones de salto que calculan direcciones incorrectas. El logging nos permite identificar exactamente qué instrucción y qué dirección causó el problema.
Lo que Falta Confirmar
- Instrucción Culpable: Necesitamos ejecutar el emulador con el logging y analizar la salida para identificar qué instrucción está calculando una dirección inválida.
- Causa Raíz: Una vez identificada la instrucción, necesitamos entender por qué está calculando mal la dirección. Podría ser un error en la lógica de cálculo, un problema con el formato Little-Endian, o un bug en el manejo de offsets con signo.
- Otros Saltos: Aunque hemos añadido logging a los saltos más comunes, podría haber otros saltos condicionales (JR Z, JR C, etc.) que también necesiten revisión.
Hipótesis y Suposiciones
Hipótesis Principal: El bug está en una instrucción de salto que calcula mal la dirección de destino. Las causas más probables son:
- Error en el cálculo del offset con signo en
JR(el offset se lee comouint8_tpero debe interpretarse comoint8_t). - Error en el formato Little-Endian al leer direcciones de 16 bits en
JPoCALL. - Corrupción de la pila que hace que
RETrecupere una dirección inválida.
El logging nos permitirá confirmar o refutar estas hipótesis.
Próximos Pasos
- [ ] Recompilar el módulo C++ con el logging:
.\rebuild_cpp.ps1 - [ ] Ejecutar el emulador y capturar la salida hasta el crash
- [ ] Analizar las últimas líneas del log para identificar la instrucción problemática
- [ ] Corregir el bug en la instrucción identificada
- [ ] Eliminar el logging temporal del código
- [ ] Recompilar y verificar que el segfault se haya solucionado
- [ ] Ejecutar tests para asegurar que no se rompió nada más