Hackeando en 8 bits (y II)
Tags: spanish, spectrum, hacking, retro
Published: 2024-04-19 09:10:31

---
title: Hackeando en 8 bits (y II)
series: Hackeando en 8 bits
published: true
description: Ligeras nociones de ensamblador Z80
tags: #spanish #spectrum #hacking #retro
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-04-17 11:01 +0000
---

Así que quieres hackear **Abu Simbel Profanation**, bribón. Bueno, teniendo en cuenta que este juego salió hace más de treinta años, te lo voy a pasar.

Nos habíamos quedado con el cargador, que era algo así:

```basic
10 border 0:paper 0: ink 0: cls: clear 26000: print at 21, 0; paper 2; ink 7; "DINAMIC PRESENTA......"; flash 1; "ABU SIMBEL"
15 print at 0, 0: poke 23633 + 256 * peek 23634, 111
20 load "" screen$: load "" code: randomize usr 26100
```

Si vamos a cambiar algo del juego, tendremos que hacerlo cuando ya esté cargado en memoria. Lo ideal sería hacer los cambios necesarios (vidas infinitas, por ejemplo), cuando el juego todavía no ha sido ejecutado.

¿Se puede hacer durante la ejecución del juego? Sí, se puede hacer. Si estás ejecutando el juego en un emulador, puedes pararlo cuando quieras y realizar los cambios que desees. Si lo haces en el hardware real, como hace años, necesitarías tener un periférico que permitiera detener la ejecución del juego y cambiar valores en la memoria. Por ejemplo, serviría uno llamado [Transtape](https://worldofspectrum.org/archive/software/utilities/transtape-outlet).

¿Cuándo se carga el juego? Estamos hablando, al margen de la pantalla de carga (una imagen bonita con la que entretenerse mientras carga el juego), de la segunda instrucción de la línea 20: `load "" code`. ¿Cuándo se ejecuta el juego? En la última instrucción de la línea 20: `randomize usr 26100`.

Así, si queremos cambiar el juego tendrá que hacerse entre estas dos líneas. Para cambiar valores en memoria, solo tenemos disponible la instrucción `poke`, que recibe dos parámetros: la dirección a cambiar, y el nuevo valor.

Podríamos modificar el cargador (tras empezar con un `merge ""`, por ejemplo), para dejarlo de la siguiente forma:

```basic
10 border 0:paper 0: ink 0: cls: clear 26000: print at 21, 0; paper 2; ink 7; "DINAMIC PRESENTA......"; flash 1; "ABU SIMBEL"
15 print at 0, 0: poke 23633 + 256 * peek 23634, 111
20 load "" screen$
30 load "" code
40 poke <dir>, <valor>
50 randomize usr 26100
```

La idea es que en la línea 40 coloquemos los *pokes* que deseemos. De acuerdo, pero qué cambiar (o mejor dicho, dónde en la memoria del *Speccy*), y por qué valor.

Para saber qué podemos cambiar, no nos queda más remedio que saber algo de código máquina del Z80. El juego, al fin y al cabo, se ejecuta directamente así (un lenguaje de programación interpretado como BASIC haría el resultado demasiado lento). En varias entrevistas, el programador de Dinamic Víctor Ruíz ha declarado que utilizaba un compilador de BASIC que un conocido les había hecho, pero al ser un compilador, el resultado es el mismo: se ejecuta como código máquina.

En esta [tabla de instrucciones del Z80](https://clrhome.org/table/), podemos encontrar todas las instrucciones que tiene el Z80 y cómo se codifican en memoria.

Por ejemplo, en algún momento (una vez que el jugador sea alcanzado por un malote, por ejemplo), será necesario restar uno del número de vidas del jugador. Dado que el número de vidas es menor que 255, un solo byte es suficiente para almacenarlo. Así, el juego tendría que hacer algo como lo que sigue:

```asm
ld a, ($8500)         ; Carga el contenido de $8500 en A
dec a                 ; Resta 1 a A.
ld $8500, a           ; Carga el contenido de A en $8500.
```

Todos los microprocesadores tienen registros. Estos registros son los que le permiten hacer cálculos antes de guardar los resultados en memoria. En el caso del Z80, los registros genéricos son A, B, C, D, E, H y L. El caso del primer registro, A, es especial, ya que se conoce como *acumulador*. Este el registro utilizado por defecto en muchos *opcodes*, como por ejemplo en la instrucción `sub 1` de más arriba.

Es probable que el código que te quita una vida esté en una subrutina, de forma que se la llama cada vez que se necesite esta funcionalidad. Esto se hace con los *opcodes* **call** y **ret**. La primera permite cambiar el flujo de ejecución a una subrutina, mientras que la segunda permite "volver" una vez que la ejecución de la subrutina ha terminado.

```asm
    ...
    call quita_una_vida
    ...
```
La subrutina tendría como nombre esta etiqueta. Por supuesto, esto solo significa que sería una dirección de memoria que se conoce una vez el código sea compilado.

```asm
quita_una_vida:
    ld a, ($8500)         ; Carga el contenido de $8500 en A
    dec a                 ; Resta 1 a A.
    ld $8500, a           ; Carga el contenido de A en $8500.
    ret                   ; Terminar y volver.
```

Todos los *opcodes*, finalmente, no son más que números en memoria. El código que compartido está en ensamblador, un lenguaje que puede traducirse directamente en código máquina. Por ejemplo, la subrutina más arriba se traduciría como:


```asm
; opcode                  | hex        | dec
    ld a, ($8500)         ; 3A $8500     58 85 33
    dec a                 ; 3D           61
    ld $8500, a           ; 32 $8500     50 85 33
    ret                   ; C9           201
```

Así, si visualizáramos la memoria en el lugar donde ha sido ensamblada esta subrutina `quita_1_vida`, veríamos la secuencia 58 85 33 61 50 85 33 201.

Es importante tener en cuenta que hay instrucciones que ocupan una posición en memoria, mientras que otras ocupan, por ejemplo 3. Así, `dec 1` ocupa solo una posición de memoria (restar uno es suficientemente común como para que le dedicasen un *opcode* dedicado), mientras que `ld a, ($8500)` ocupa tres: uno para el código de instrucción que carga en el acumulador el contenido de una posición de memoria, y en las siguientes dos posiciones la dirección de memoria objetivo.

Existe además (en todos los microprocesadores), una instrucción especial que se conoce como `nop`, que se ensambla al código 0 y que no hace nada.

Así que si tuviéramos este código en memoria, podríamos cambiarlo como:

```asm
; opcode                  | hex        | dec
    nop                   ; era: ld a, $(8500); 58 85 33
    nop
    nop                   ; 00 00 00      0  0  0
    nop                   ; 00            0; era 3D
    nop                   ; era: ld $8500, a; 50 85 33
    nop
    nop                   ; 00 00 00      0  0  0
    ret                   ; C9           201
```

Y es que una vez esté el juego en memoria, no podemos cambiar una subrutina de manera que cambie su tamaño en memoria, solo podemos sobreescribirlo con código del mismo tamaño. Así, ahora el código visto en memoria sería: 0 0 0 0 0 0 0 201.

Lo que hemos conseguido es que no se ejecute nunca más el código para restar una vida.

Si lo pensamos detenidamente, sobreescribir toda la subrutina con ceros es bastante farragoso... en realidad, lo que querríamos hacer es que no se restase uno. Esto podemos hacerlo así:

```asm
; opcode                  | hex        | dec
    ld a, ($8500)         ; 3A $8500     58 85 33
    nop                   ; 00           0
    ld $8500, a           ; 32 $8500     50 85 33
    ret                   ; C9           201
```

Entonces, en memoria veríamos la secuencia 58 85 33 0 50 85 33 201. Solo ha sido necesario modificar una instrucción en memoria, una instrucción que solo ocupa exactamente una posición.

Otra posibilidad es tener en cuenta que cuando se invoque la subrutina se ejecutará la primera instrucción (`ld ($8500), a`), por lo que, en lugar de sobreescribir memoria, podemos "adelantar" el código `ret` para que vuelva enseguida, sin ejecutar ningún código.

```asm
; opcode                  | hex        | dec
    ret                   ; C9           201
    ld d, 1               ; 55           85
    ld hl, nn             ; 21           33      
    dec a                 ; 3D           61
    ld $8500, a           ; 32 $8500     50 85 33
    ret                   ; C9           201
```

Al cambiar la primera instrucción, el resto de posiciones en memoria serían intepretadas de distinta forma. Así, `ld a, ($8500)` que se traducía como 58 85 33, es cambiada por 201 85 33, con lo que 85 se reinterpreta como `ld d, 1` que es el código hexadecimal 55. La siguiente instrucción se reinterpretaría como `ld hl, ...`, que ocupa tres posiciones de memoria. Así, el resto del programa se corrompería, pero no importa por que lo primero que se va a hacer al comenzar la subrutina es... volver. Volver sin ejecutar nada.

Otra posibilidad de modificación es buscar los lugares del código donde se hace alguna comprobación para realizar un salto en la memoria. Si transformarmos ese salto condicional (salta a la dirección *nn* si el jugador **no** ha sido alcanzado por un enemigo), en un salto incondicional (salta siempre a la dirección *nn*), entonces habremos logrado la inmunidad del jugador (otra "ventaja" muy buscada). Aunque hay varios saltos condicionales, `jp nz, nn` (siendo *nn* la dirección de memoria), salta cuando una condición no ha sido satisfecha. El código ensamblador es C2 (194 decimal), mientras que el del salto incondicional es C3 (195 decimal). Por lo tanto, con solo cambiar una posición de memoria (aquella donde hay un 194, por un 195), de nuevo podemos cambiar el significado del programa.

Hemos visto cómo modificar subrutinas en memoria, tanto con *opcodes* `nop` como con *opcodes* `ret`. Teniendo en cuenta que desde BASIC podemos cambiar posiciones de memoria con `poke`, esto explica por qué es tan común ver *pokes* que modifican una posición de memoria dada con un 0, un 195, o con un 201, que son las típicas en las que se obtenían ventajas como vidas infinitas o inmunidad. Claro que, para llegar a obtener estos *pokes* era necesario estudiar el código del juego en profundidad. Esto explica que los [*pokes* de **Abu Simbel Profanation**](https://www.the-tipshop.co.uk/cgi-bin/info.pl?name=Abu%20Simbel%20Profanation) contengan un `poke 47684,0` para vidas infinitas, o un `poke 45295,195` para lograr la inmunidad.

```basic
10 border 0:paper 0: ink 0: cls: clear 26000: print at 21, 0; paper 2; ink 7; "DINAMIC PRESENTA......"; flash 1; "ABU SIMBEL"
15 print at 0, 0: poke 23633 + 256 * peek 23634, 111
20 load "" screen$
30 load "" code
40 poke 45295, 195
50 randomize usr 26100
```

Otra posibilidad es no otorgar vidas infinitas, sino otorgar un número de vidas tan grande que sean virtualmente infinitas. Dado que una posición en memoria es capaz de guardar como mucho el valor 255, es típico cambiar una dirección de memoria con 255 para cambiar las típicas tres o cinco vidas iniciales del juego en 255, que pueden aumentar significativamente nuestras posibilidades de terminarlo.

Pero esto lo veremos en el siguiente capítulo. ¿Y tú, también *pokeabas* en los 80?