He decidido hacer un tutorial de ASM como reopilación de muchos tutoriales muy buenos de Pokecommunity, siendo la base el de FBI. Pero con cambios sustanciales de otros tutoriales y aportes míos.
Muchos de vosotros ya conoceréis la existencia de este tutorial. Se trata de un tutorial de ASM muy completo y fácil de entender. He decido de tomarme el trabajo de traducir el tutorial por partes.
El tutorial no está copiado al pie de la letra. He modificado las cosas que he visto convenientes para mejorar su comprensión. Todos los créditos son para FBI, pues él es el autor de este tutorial.
He aquí un pequeño repaso a los comandos básicos del ASM. Para conceptos como "que es una pila" os recminedo leer otros tutoriales de ASM más básicos, como los de Cheve.
¿Recordais el comando random? ¿No? Pues a aprender scripting ahora mismo! El comando random crea un valor aleatorio de un byte desde 0x0 hasta el que nosotros le indiquemos, su uso es el siguiente:
Y guarda en la variable 0x800D o LASTRESULT, llámalo como quieras, un valor aleatorio entre 0x0 y el byte que tu hayas definido. Imagínate que estás haciendo un script en que añadas el valor de 0x800D a la variable 0x8000. Pero no hay ningún comando que sea sumvar, como podréis observar. Pues haremos nuestra rutina para ello! Abrid el Notepad y empecemos...
Antes de nada necesitamos toda la palabrería siguiente:
Lo primero será pushear los registros que vayamos a usar y despues popearlos de vuelta. Antes de nada debemos saber que el valor de la variable 0x800D está en el offset 0x20370D0 y que es un half word. El de la variable 0x8000 está en el offset 0x20370B8.
Por lo tanto cargaremos en registros dichos offsets:
Ahora vamos a cargar los valores que tienen dichas variables, mejor dicho, dichos offsets:
A continuación sumaremos ambos valores y lo guardaremos en r0, por ejemplo:
Ahora hay que volver a cargar el offset de la variable 0x8000 y almacenar ahí el resultado:
No os olvidéis de borrar todas las anotaciones que he hecho.
Comprobando la rutina...
Ahora solo falta hacer un script en el que veamos si de verdad nuestra rutina funciona. A si que depúes de insertar la rutina usaremos este script:
Si todo ha ido bien debería funcionar, y sino, pues a volver a revisarlo.
Hay veces que necesitamos asignar un valor relativamente grande a un registro. Os pondré un ejemplo. Imaginad que en mi rutina tengo que interactuar con la flag número 0x828 (la flag del menu pokemon valga la redundancia). Antes de comprobar si esa flag está activada o no debo cargar ese mismo número en un registro. ¿Cómo se puede hacer algo así? Tratad de hacerlo por vosotros mismos, olvidaos de toda la palabrería del .txt .align 2 y todo ese rollo, simplemente escribid el cuerpo de la rutina. Después mirad la solución.
Estoy seguro de que más de uno habrá tratado de hacer lo siguiente:
Si habéis tratado de hacerlo así permitidme decir que este método no funciona, ya lo siento. Puesto que el comando mov solo puede cargar en registros valores inmediatos ( del 0x0 al 0xFF). Como 0x828 es más grande que 0xFF pues evidentemente no funcionará. Claro que si hacemos mov r0, r1 cargará en r0 lo que sea que haya en r1, pero claro, con mover valores está restringido a carga valores de byte. Por consiguiente, volvamos a intentarlo.
En comparación con el método anterior este funcionará. Primero cargamos algunos valores en registros, los multiplicamos y sumamos lo que quede.
Bien, este método funciona, innegablemente. Pero se puede optimizar. Es decir, el comando mul es un comando muy lento, como queremos que nuestra rutina se ejecute lo más rápido y eficientemente posible buscaremos otra forma. Por ejemplo, repitiendo sumas:
Bueno... aunque funcione no es que haya quedado muy ideal. Tanto mov como add están restringidos a sumar valores inmediatos de un byte ( como mucho 0xFF) por lo tanto debemos encontrar otra forma. Además, cuando el número sera muy grande, ¿haremos un montón de sumas? La verdad es que hay un método más fácil y eficiente que todos los anteriores.
Recordaremos lo que hace el comando lsl. Dicho comando mueve un bit a la izquierda el valor en binario del registro Es decir, añade un cero a la derecha.. Si no me crees te lo explico en este spoiler.
Volviendo al tema, para poder conseguir 0x828 podemos usar lo aprendido ahora mismo. En la medida de los posible sustituiremos mul por lsl, al ser el segundo mucho más rápido. Por lo tanto, la rutina queda así:
Hay una última forma de hacerlo, y es asignando nombres a los valores, es decir,
En lo personal no me gusta usar demasiado este método a no ser que value sea un número que se repita muchas veces, pero como veais.
Las tablas son en su mayoría (seguramente la única) la forma en la que el juego almacena gestiona sus datos. Las tablas sirven por ejemplo para organizar los nombres de los movimientos, Pokemon, habilidades... Cuando estés haciendo una rutina muchas veces tendrás que consultar dichas tablas, por lo tanto aprendamos a como usarlas.
Generalmente hay dos tipos de tablas, los que por cada posición tienen un byte para finalizar, FF en su mayoría, y los que tienen un número determinado de bytes por posición y unos bytes para finalizar la tabla. Ambos tienen sus datos guardados con una longitud determinada. Aunque está claro que movimientos como BITE y PERISH SONG no ocupan el mismo espacio, por eso existe el relleno, que son los 00, para usar precisamente para rellenar, para que todos los nombres ocupen lo mismo.
En una rutina, para trabajar con tablas necesitamos algunos datos esenciales:
1) La cantidad de bytes que usa cada uno de los elementos, que es constante gracias al relleno.
2) El offset del primero elemento de la tabla
Pongamos en practica lo aprendido y hagamos un ejemplo, vamos a trabajar con los nombres de los movimientos.
1) Cada movimiento ocupa una cantidad de 13 bytes
2) No tiene byte para finalizar, el FF vamos
3) La tabla comienza en el offset 0x8247094
La tarea: No os preocupéis por el .text, .align 2 y esas cosas, escribid lo que de verdad importa. Cargad en r0 el offset del movimiento número 20 ( con trece bytes por elemento ). Trata de hacerlo tú mismo antes de mirar.
Pistas:
1) En este caso es más facil usar mul que lsl, por tanto usad mul
2) Usad ldr =0x8247094 para cargar el offset del inicio de la tabla
Bonus:
¿Puedes cargar la primera letra en el registro r0?
Sabemos cuanto ocupa cada elemento y sabemos cual es el número de indice de nuestro movimiento, pues la multiplicación de ellos será la cantidad de bytes que haya desde el inicio de la tabla hasta nuestro movimiento. Luego le sumamos el principio de la tabla, para conseguir el offset del movimiento deseado.
Finalmente cargamos en r0 la primera letra de ese movimiento.
Y así es como leemos los datos de una tabla sabiendo cual es el número de indice del elemento. ¿Pero como buscamos algo en una tabla? Pongamos en el juego hay una tabla con los IDs de los objetos considerados que se puedan vender. Y pongamos el ejemplo de que yo quiero comprobar si el objeto con ID 0x55 se puede vender o no. ¿Como puedo saber si ese objeto se puede vender o no? Tendrías que buscar en la tabla cada número de indice hasta el final de tabla, o simplemente buscar el 0x55.
Esta tabla está formada por half words, es decir, dos bytes por cada elemento, y la tabla termina con un 0xFFFF. Tratad de comprobar si ese objeto está en la tabla o no, después mirad la solución.
1) La tabla comienza en el offset 0x740000
2) Cada elemento es un half word, dos bytes
Pistas: En vez de usar ldrb usad ldrh
Bonus: Si el objeto está en la tabla cargad en r0 el valor 0x0, de lo contrario 0x1
Supongo que no lo habréis conseguido sin usar el comando cmp y los loops. El comando cmp compara los valores de dos registros o un registro con un valor inmediato. En la siguiente línea pondremos la condición y el label o etiqueta a donde debe ir. Vamos a verlo detalladamente.
Ahora veamos la rutina bien explicada:
Antes de explicar como usar una función quiero remarcar como es una función. Simplemente es una serie de operaciones basadas en 0 o más parámetros que nos devuelve en registros unos valores de salida. Un ejemplo de esto es la función checkflag conocida por todos ( o eso espero ) del scripting.
Esa función es toma el valor de la flag en el registro r0 y tras una serie de operaciones devuelve en r0 0x0 si está desactivada y 0x1 si está activada. Vamos a tratar de comprobar si la flag 0x828 está activada o no.
El bl y bx están explicadas en el anterior apartado, si no lo has entendido bien vuelve a leer la parte que les corresponde y trata de entenderlos mediante este ejemplo.
Este apartado ya es un poco más adelantado pero actualmente creo que lo entenderos bastante bien. Básicamente los comandos lsl y lsr sirven para eliminar la basura no deseada de los valores de los registros. Para entenderlo mejor voy a empezar con un buen ejemplo. Como sabéis, y si no lo sabéis ahora si, cada Pokemon tiene su propio PID o Pokemon ID el cual es un valor semi aleatorio de cuatro bytes o una word. Este valor es una mezcla entre otros dos valores. Imaginad que tengo dos valores XXXXXXXX y YYYYYYYY, pues el PID será así:
[primeros dos bytes de X][ultimos dos bytes de Y]
si X = 0xDE6C3B8F y Y = 0xCA9B23E1
luego PID = [DE6C][23E1], lo que viene siendo PID = 0xDE6C23E1
¿Pero como lo se hace eso Vamos, coger los dos primeros bytes de uno y los últimos de otro... Pues con el lsl, lsr y otro más, no nos adelantemos.
Tenemos que remover los dos últimos bytes de X y los dos primeros bytes de Y, para ello y sabremos en lsl y lsr. Sabemos que ambos trabajan a nivel de bits, en binario, por lo tanto debemos encontrar la equivalencia entre bit y byte, que bien conocido es que dos bytes son 16 bits. Por lo tanto, para remover dos bytes en el lsl o lsr usaremos como parámetro #0x10 (16 en hex)
Por tanto, lsl r0, #0x10 hará que de XXXXXXXX pasemos a XXXX0000, después debemos devolverlos a donde estaban y para eso usaremos lsr, basta de palabrería, veamoslo en un ejemplo:
Voy a explicar el comando orr, veamos este ejemplo:
r0=E4720000
r1=0000FA3D
orr hace una operación entre estos dos valores un poco extraña. Pongamos que en cada uno de los números cada dígito tiene una posición, tal que así:
orr r0, r0, r1 hace que todos los dígitos de r0 que sean ceros sean sustituidos por dígitos que tengan la misma posición tomados del registro r1. Por tanto, en este caso:
orr r0, r0, r1 r0 = E472FA3D
Para poner en practica todo lo aprendido hasta ahora os propongo un pequeño reto. ¿Recordáis el comando random? Pues bien, usando el valor aleatorio que haya genradp este comando haremos un buffer de un movimiento aleatorio.
En scripting random 0xFF devuelve a la variable 0x800D un valor aleatorio entre 0x0 y 0xFF. Consideraremos ese valor aleatorio como el ID de un movimiento. A partir de aquí haced una rutina que lea el valor de la variable 0x800D. Navegar por la tabla de movimientos y cargar el nombre del movimiento indicado aleatoriamente en el offset 0x2021D18
1)La tabla de movimientos empieza en el offset 0x8247094
2) Cada elemento corresponde a un nombre
3) Cada elemento contiene una cantidad de 13 bytes (0xD en hex)
4) Cada movimiento termina con un byte de 0xFF
5) El offset de la RAM de la variable 0x800D es 0x20370D0
El script será el siguiente:
Pistas: Hay una función que copia los bytes de una determinada offset en adelante hasta encontrar un 0xFF y guardarlo en 0x2021D18. La función están en el offset 0x8008D84. Suerte !
Quiero agradecer a @Kate, AliKate y a @CompuMax por ayudarme en este tuto, al primero por resolverme mil y una duda con una paciencia infinita y al segundo por todos los errores que me ha reportado y he podido arreglar.
Nada chicos, espero que os haya servido y que os haya animado a aprender ASM, que no es tan complicado después de todo
Muchos de vosotros ya conoceréis la existencia de este tutorial. Se trata de un tutorial de ASM muy completo y fácil de entender. He decido de tomarme el trabajo de traducir el tutorial por partes.
El tutorial no está copiado al pie de la letra. He modificado las cosas que he visto convenientes para mejorar su comprensión. Todos los créditos son para FBI, pues él es el autor de este tutorial.
Antes de adentrarse en el ASM y sus comandos hay conceptos que a pesar de no ser indispensables es recomendable tenerlos presentes para evitar dudas futuras. Por consiguiente, en este breve prefacio el objetivo será esclarecer ciertos aspectos básicos del ASM y RH en general.
Memorias
Una memoria es un espacio donde se almacena información. Puede ser de diferentes tipos dependiendo del uso y las características. Los dos grandes grupos respecto a las memorias son la ROM “Read Only Memory” y la RAM “Random Acces Memory”.
La memoria se fracciona en celdas, las cuales tienen un offset asociado. Los offsets en GBA son de tres bytes más el byte de la memoria específica a la que se refiere.
ROM
La ROM es el espacio de memoria que solo sirve para lectura de datos a la hora de ejecutar el juego. Esta memoria contiene todo tipo de datos, desde tablas con información de los Pokemon de la Pokedex, paletas, funciones matemáticas para calcular el daño de un movimiento…
En GBA la ROM tiene como prefijo el 08, aunque si la expandimos también tendría el 09. Por lo tanto cualquier offset referente a la ROM será algo así: 0x08 XX XX XX. Recuerdo que el 0x hace referencia a un número hexadecimal.
La idea principal que se debe tener de la ROM es que solo será una memoria que a la hora de hacer nuestras rutinas podremos leer y nunca jamás escribir.
RAM
El nombre puede engañar un poco pero no es más que una terminología para decir que desde cualquier punto de una rutina se puede acceder a ella, sea para leer o escribir. La RAM almacena muchos datos que se van generando a la hora de ejecutar el juego. Por ejemplo, los datos de los Pokemon de tu equipo, el valor de las variables…
La RAM normal tiene como prefijo el 02. Asimismo, como en la ROM, para referirnos a un offset de la RAM usaremos 0x02 XX XX XX.
También hay otras muchas RAM como la WRAM, VRAM… Cada cual con su prefijo adecuado.
Registros
Para que se entienda un registro es un espacio de la memoria del procesador, de 32 bits (en el procesador ARM7 que tiene GBA). Es como una especie de variable de scripting. En el procesador ARM7 hay por lo general que tengan que ver con el RH quince registros.
Low Registers (r0-r7)
Son registros de carácter general que pueden ser usadas por cualquier comando. A la hora de hacer nuestra rutina siempre serán las que vayamos a usar para evitarnos problemas.
High Registers (r8-r12)
Como los Low Registers pero a diferencia de ellas estas solo pueden ser usadas por ciertos comandos. Como los Low Registers son muchos para nuestras rutinas no necesitaremos de ellas.
Programm Counter (r15)
Se que me he saltado el r13 y r14 pero volveré a él más adelante. El Programm Counter o pc es muy sencillo de entender. Guarda el offset de la próxima instrucción a la que estamos ejecutando. Por poner un ejemplo:
Puede que os preguntéis para que demonios sirve algo algo, pues el próximo comando a ejecutar es el siguiente en la rutina. Bueno, ya vereis más adelante para que sirve.
Link Register (r14)
Dentro de la propia ROM a veces una rutina o función puede llamar a otra función. Cuando saltamos a esa otra función debe de haber alguna forma de retornar a la rutina original una vez terminada la función secundaria. Este registro almacena el valor del pc cuando ejecuta un bl o bx (que son los comandos para hacer call). De este modo el procesador sabe a donde debe retornar.
Puede que también te preguntes cómo puede ser que yo llame a una función dentro de otra función dentro de otra función… Porque si solo hay un salto el procesador sabrá donde retornar, pero si hay una función dentro de otra que es llamada el lr se sobreescribirá y no habrá forma de retornar a la rutina principal. Esto lleva implícito el uso de una pila.
Stack Pointer (r13)
Puede que este registro, de hecho es, el más complicado de entender. El stack pointer o sp es el registro que apunta al offset del top de la pila, simple y llanamente explicado. ¿Que es una pila?
No me andaré con analogías para explicar que es una pila pues creo que es mejor explicar que es literalmente. Un stack o pila es una zona de la memoria RAM donde se almacenan los valores que tenían ciertos registros que nosotros guardamos. Para guardar el valor de un registro en el stack se usa el comando push.
Push
Más adelante explicaré su sintaxis pero ahora quiero centrarme en explicar qué es lo que hace este comando.
Cuando nosotros pusheamos un registro a la pila lo que hacemos es un backup del valor de ese registro en ese momento. Es decir, en una zona de la memoria (apuntada por el sp) guardaremos el valor que haya en ese registro. Haremos un ejemplo:
Imaginad que al principio el stack está vacío, y tiene cinco bytes de longitud (por simplificar. Como no hay nada en el sp su valor será sp = 0
Ahora pushearemos el valor de una registro dentro de la pila
El comando push guardará en la pila el valor del registro 0. A continuación el valor de sp se incrementará en uno. Imaginad que r0 = 7B. La pila quedará así:
Es fundamental entender que el valor 7B no está en la pila asociada de alguna manera con el registro. Podemos volver a cargar ese valor en cualquier registro, sea r0 o cualquier otro.
El funcionamiento de la pila es tal y como se ha explicado. Pero también hay que entender que aquello que lo hayamos pusheado primero será lo último que podamos sacar de la pila. Sigue la norma LIFO, Last In First Out, que significa que el valor del top del stack, que ha sido el último en llegar será el primero en irse.
Pop
Eso de guardar los registros en la pila está muy bien pero también habrá que volver a cargar esos datos en registro alguna vez, ¿sino para qué hemos hecho un backup?
El comando para sacar los valores de la pila y cargarlos en registros es el pop. Como antes ahora me centrare en su funcionamiento. El comando pop coge el valor que esté más arriba en la pila (apuntado por el sp) y lo guarda en el registro que queramos.
Siguiendo el caso de antes, si yo ahora hago:
El valor de r1 = 7B, pues es lo que había en el top de la pila. Cada registro que sea cargado de esta manera restará uno al sp. Ahora donde estaba el 7B no habrá nada. Toda cantidad de registros que pusheemos deben ser popeado de nuevo, no necesariamente en los mismos registros (aunque para los r0-r12 si).
Conocimiento general de cómo el procesador sabe en todo momento donde debe ir
Como antes he dicho con cualquier comando que no sea un b, bl o bx (en verdad hay más, pero nosotros usaremos esos). Aparte de hacer su función sumarán en uno el pc. Esto sirve para que el procesador sepa cual es la próxima instrucción a ejecutar. Ahora bien, si yo deseo llamar a una función usaré el comando bl. Un ejemplo:
El comando bl nos lleva al offset que nosotros queramos. Pero lo que realmente hace es mover el valor del pc + 1 al lr (para que sepa a donde debe volver) y después, mover a pc el offset de linker.
Para volver a la rutina principal, debemos usar el comando ret (yo nunca lo he usado) o mover el valor del lr (que era la instrucción siguiente al bl) al pc, para que el procesador siga ejecutando a partir del bl.
Así funciona internamente el procesador, Esto sirve solo para una función dentro de otra. Pero si queremos hacer una cadena, para no sobreescribir el lr (que apunta a donde hay que volver) guardaremos en la pila el lr.
Lo que aquí estamos haciendo es guardar en la pila el valor de lr y aunque el lr sea modificado dentro de la función (llamando a esa función), al cargar ese valor de la pila (antiguo lr) en pc volveremos a la rutina del principio.
Por eso en todas las rutinas que veréis por ahí siempre hay un push {algunos registros,lr} y al final pop {los registros de antes, pc}.
Se que para el RH esto no influye mucho pero en un microprocesador el movimiento de datos entre registros es mucho más rápido que el guardar y cargar de la pila que no es más que memoria RAM. Por lo tanto, si dentro de vuestra rutina no modificais el lr (vamos, no usar un bl o bx) no tenéis por qué pushearlo, y por ende popear el pc. Simplemente al final de la rutina mov pc, lr. Aunque si os sentís más cómodos pusheando y popeando pues también está muy bien.
Memorias
Una memoria es un espacio donde se almacena información. Puede ser de diferentes tipos dependiendo del uso y las características. Los dos grandes grupos respecto a las memorias son la ROM “Read Only Memory” y la RAM “Random Acces Memory”.
La memoria se fracciona en celdas, las cuales tienen un offset asociado. Los offsets en GBA son de tres bytes más el byte de la memoria específica a la que se refiere.
ROM
La ROM es el espacio de memoria que solo sirve para lectura de datos a la hora de ejecutar el juego. Esta memoria contiene todo tipo de datos, desde tablas con información de los Pokemon de la Pokedex, paletas, funciones matemáticas para calcular el daño de un movimiento…
En GBA la ROM tiene como prefijo el 08, aunque si la expandimos también tendría el 09. Por lo tanto cualquier offset referente a la ROM será algo así: 0x08 XX XX XX. Recuerdo que el 0x hace referencia a un número hexadecimal.
La idea principal que se debe tener de la ROM es que solo será una memoria que a la hora de hacer nuestras rutinas podremos leer y nunca jamás escribir.
RAM
El nombre puede engañar un poco pero no es más que una terminología para decir que desde cualquier punto de una rutina se puede acceder a ella, sea para leer o escribir. La RAM almacena muchos datos que se van generando a la hora de ejecutar el juego. Por ejemplo, los datos de los Pokemon de tu equipo, el valor de las variables…
La RAM normal tiene como prefijo el 02. Asimismo, como en la ROM, para referirnos a un offset de la RAM usaremos 0x02 XX XX XX.
También hay otras muchas RAM como la WRAM, VRAM… Cada cual con su prefijo adecuado.
Registros
Para que se entienda un registro es un espacio de la memoria del procesador, de 32 bits (en el procesador ARM7 que tiene GBA). Es como una especie de variable de scripting. En el procesador ARM7 hay por lo general que tengan que ver con el RH quince registros.
Low Registers (r0-r7)
Son registros de carácter general que pueden ser usadas por cualquier comando. A la hora de hacer nuestra rutina siempre serán las que vayamos a usar para evitarnos problemas.
High Registers (r8-r12)
Como los Low Registers pero a diferencia de ellas estas solo pueden ser usadas por ciertos comandos. Como los Low Registers son muchos para nuestras rutinas no necesitaremos de ellas.
Programm Counter (r15)
Se que me he saltado el r13 y r14 pero volveré a él más adelante. El Programm Counter o pc es muy sencillo de entender. Guarda el offset de la próxima instrucción a la que estamos ejecutando. Por poner un ejemplo:
Código:
Offset 800: ldr r0, lastresult pc = 801
Offset 801: ldrh r0, [r0] pc = 802
Link Register (r14)
Dentro de la propia ROM a veces una rutina o función puede llamar a otra función. Cuando saltamos a esa otra función debe de haber alguna forma de retornar a la rutina original una vez terminada la función secundaria. Este registro almacena el valor del pc cuando ejecuta un bl o bx (que son los comandos para hacer call). De este modo el procesador sabe a donde debe retornar.
Puede que también te preguntes cómo puede ser que yo llame a una función dentro de otra función dentro de otra función… Porque si solo hay un salto el procesador sabrá donde retornar, pero si hay una función dentro de otra que es llamada el lr se sobreescribirá y no habrá forma de retornar a la rutina principal. Esto lleva implícito el uso de una pila.
Stack Pointer (r13)
Puede que este registro, de hecho es, el más complicado de entender. El stack pointer o sp es el registro que apunta al offset del top de la pila, simple y llanamente explicado. ¿Que es una pila?
No me andaré con analogías para explicar que es una pila pues creo que es mejor explicar que es literalmente. Un stack o pila es una zona de la memoria RAM donde se almacenan los valores que tenían ciertos registros que nosotros guardamos. Para guardar el valor de un registro en el stack se usa el comando push.
Push
Más adelante explicaré su sintaxis pero ahora quiero centrarme en explicar qué es lo que hace este comando.
Cuando nosotros pusheamos un registro a la pila lo que hacemos es un backup del valor de ese registro en ese momento. Es decir, en una zona de la memoria (apuntada por el sp) guardaremos el valor que haya en ese registro. Haremos un ejemplo:
Imaginad que al principio el stack está vacío, y tiene cinco bytes de longitud (por simplificar. Como no hay nada en el sp su valor será sp = 0
Ahora pushearemos el valor de una registro dentro de la pila
Código:
push {r0}
Es fundamental entender que el valor 7B no está en la pila asociada de alguna manera con el registro. Podemos volver a cargar ese valor en cualquier registro, sea r0 o cualquier otro.
El funcionamiento de la pila es tal y como se ha explicado. Pero también hay que entender que aquello que lo hayamos pusheado primero será lo último que podamos sacar de la pila. Sigue la norma LIFO, Last In First Out, que significa que el valor del top del stack, que ha sido el último en llegar será el primero en irse.
Pop
Eso de guardar los registros en la pila está muy bien pero también habrá que volver a cargar esos datos en registro alguna vez, ¿sino para qué hemos hecho un backup?
El comando para sacar los valores de la pila y cargarlos en registros es el pop. Como antes ahora me centrare en su funcionamiento. El comando pop coge el valor que esté más arriba en la pila (apuntado por el sp) y lo guarda en el registro que queramos.
Siguiendo el caso de antes, si yo ahora hago:
Código:
pop {r1}
Conocimiento general de cómo el procesador sabe en todo momento donde debe ir
Como antes he dicho con cualquier comando que no sea un b, bl o bx (en verdad hay más, pero nosotros usaremos esos). Aparte de hacer su función sumarán en uno el pc. Esto sirve para que el procesador sepa cual es la próxima instrucción a ejecutar. Ahora bien, si yo deseo llamar a una función usaré el comando bl. Un ejemplo:
Código:
...
800 ldr r1, [sp, #4] lr = ??, pc =801
801 bl linker lr = 802, pc 950
…
…
950 add r0, r1 -> offset de la etiqueta linker
…
960 ret/mov pc, lr lr = 802, pc = 802
Para volver a la rutina principal, debemos usar el comando ret (yo nunca lo he usado) o mover el valor del lr (que era la instrucción siguiente al bl) al pc, para que el procesador siga ejecutando a partir del bl.
Así funciona internamente el procesador, Esto sirve solo para una función dentro de otra. Pero si queremos hacer una cadena, para no sobreescribir el lr (que apunta a donde hay que volver) guardaremos en la pila el lr.
Código:
...
800 ldr r1, [sp, #4] lr = ??, pc =801
801 bl linker lr = 802, pc 950
…
…
950 push {lr} -> offset de la etiqueta linker
951 add r0, r1
…
960 pop {pc} lr = 802, pc = 802
Por eso en todas las rutinas que veréis por ahí siempre hay un push {algunos registros,lr} y al final pop {los registros de antes, pc}.
Se que para el RH esto no influye mucho pero en un microprocesador el movimiento de datos entre registros es mucho más rápido que el guardar y cargar de la pila que no es más que memoria RAM. Por lo tanto, si dentro de vuestra rutina no modificais el lr (vamos, no usar un bl o bx) no tenéis por qué pushearlo, y por ende popear el pc. Simplemente al final de la rutina mov pc, lr. Aunque si os sentís más cómodos pusheando y popeando pues también está muy bien.
Este apartado del tutorial trata sobre como compilar una rutina de forma optima y ejecutarla a la hora de jugar sin ningún problema. Antes de nada es obligatorio descargarse las siguientes herramientas:
Este compilador thumb:https://www.mediafire.com/file/bxzzfe543qq7697/Compiler.rar
Editor hexadecimal (HxD): ftp://wa651f4:[email protected]/HxDSetupEN.zip
1) Crea una carpeta separada en algún lugar y pon ahí el compilar thumb que te descargaste recientemente. En mi caso, he creado una carpeta en el escritorio con el nombre de ASM, y dentro de esa carpeta tengo el compilador thumb.
2) Ahora abre tu rutina ASM en un procesador de texto, por ejemplo el Notepad que trae Windows consigo. Guarda la rutina en formato .asm en la carpeta del propio compilador.
Ya has preparado los archivos necesarios como para preparar la rutina!
Antes de que empiece, si vuestro sistema operativo es Windows podéis saltaros este apartado e ir directamente a compilación Windows (if 0xWindows goto @compilación Windows, chiste fácil, lo siento)
1) Abre el menu de inicio y la línea de comando, en Windows este es cmd, la mayoría de los otros sistemas operativos lo llaman Terminal. Sea como sea como lo llames, ábrelo! Debería ser una caja negra o purpura oscuro, depende del SO.
2) Debemos navegar a la carpeta donde está nuestro compilador y nuestra rutina. Para esto usaremos el comando cd (abreviatura de change directory). Puedes encontrar el directorio del archivo que estás usando si navegas a él y clicas el buscador de directorio.
3) Insertaremos en la linea de comandos "cd \file\path" donde las palabras file y path son el camino que debemos navegar hasta donde este la rutina. En mi caso es C:User\Guest\Deskpot\ASM. La linea de comandos actual es:
Y a continuación escribimos lo siguiente, cada cual con sus nombres propios, valga la redundancia:
Cuando ya lo tengas escrito le damos a enter.
4) Correremos el compilador sobre la rutina, tal que así:
Donde filename será el nombre de nuestro archivo .asm y el outputFileName será el nombre que tendrá la rutina compilada.
5) Si la rutina que has compilado no tiene errores te aparecerá el mensaje de "Assambled Successfully"
Si tu sistema operativo no es Windows puedes saltarte este paso, como es evidente.
1) Arrastra tu rutina sobre el compilador thumb
2) Si tu rutina no tiene ningún tipo de error entonces debería crearse un archivo con el mismo nombre que tu archivo .asm pero en formato .bin. Si no se crea ese archivo revisa la rutina hasta encontrar el fallo.
1) Abre mediante un editor hexadecimal (HxD en mi caso) el archivo .bin antes mencionado y la propia ROM en la que quieras insertar la rutina.
2) Copia todo lo que haya en el archivo .bin, es decir, en la rutina compilada, usando Ctrl+C o seleccionando, click derecho y copiar.
3) Encuentra algún espacio vacío en la ROM ( lleno de 0xFF) y pega lo que has copiado antes, usando Ctrl+B. Ten cuidado, debes insertarlo en algún offset terminado por 0, 4, 8 o C. Es muy importante.
Simplemente debes usar el comando callasm 0x(offset de la rutina + 1). Por ejemplo, si la rutina está en el offset 0x800000 usaremos callasm 0x800001.
Para poder saber si habéis entendido los pasos a seguir podéis probar a insertar la rutina que os dejo a continuación. No os preocupéis, dentro de poco aprenderéis a hacer una igual
Para ejecutarlo asignad este script a un mini:
Y probad a hablar con ese mini varias veces, veréis que cada vez os dirá un movimiento diferente. Espero que lo hayáis entendido, sino... pues volved a leerlo, no hay problema.
Este compilador thumb:https://www.mediafire.com/file/bxzzfe543qq7697/Compiler.rar
Editor hexadecimal (HxD): ftp://wa651f4:[email protected]/HxDSetupEN.zip
Preparando las rutinas para compilar
1) Crea una carpeta separada en algún lugar y pon ahí el compilar thumb que te descargaste recientemente. En mi caso, he creado una carpeta en el escritorio con el nombre de ASM, y dentro de esa carpeta tengo el compilador thumb.
2) Ahora abre tu rutina ASM en un procesador de texto, por ejemplo el Notepad que trae Windows consigo. Guarda la rutina en formato .asm en la carpeta del propio compilador.
Ya has preparado los archivos necesarios como para preparar la rutina!
Compilando la rutina
Antes de que empiece, si vuestro sistema operativo es Windows podéis saltaros este apartado e ir directamente a compilación Windows (if 0xWindows goto @compilación Windows, chiste fácil, lo siento)
1) Abre el menu de inicio y la línea de comando, en Windows este es cmd, la mayoría de los otros sistemas operativos lo llaman Terminal. Sea como sea como lo llames, ábrelo! Debería ser una caja negra o purpura oscuro, depende del SO.
2) Debemos navegar a la carpeta donde está nuestro compilador y nuestra rutina. Para esto usaremos el comando cd (abreviatura de change directory). Puedes encontrar el directorio del archivo que estás usando si navegas a él y clicas el buscador de directorio.
3) Insertaremos en la linea de comandos "cd \file\path" donde las palabras file y path son el camino que debemos navegar hasta donde este la rutina. En mi caso es C:User\Guest\Deskpot\ASM. La linea de comandos actual es:
Código:
C:\Users\Guest>
Código:
cd Desktop\ASM
4) Correremos el compilador sobre la rutina, tal que así:
Código:
thumb filename.asm outputFileName.bin
5) Si la rutina que has compilado no tiene errores te aparecerá el mensaje de "Assambled Successfully"
Compilación Windows
Si tu sistema operativo no es Windows puedes saltarte este paso, como es evidente.
1) Arrastra tu rutina sobre el compilador thumb
2) Si tu rutina no tiene ningún tipo de error entonces debería crearse un archivo con el mismo nombre que tu archivo .asm pero en formato .bin. Si no se crea ese archivo revisa la rutina hasta encontrar el fallo.
Insertando la rutina en la ROM
1) Abre mediante un editor hexadecimal (HxD en mi caso) el archivo .bin antes mencionado y la propia ROM en la que quieras insertar la rutina.
2) Copia todo lo que haya en el archivo .bin, es decir, en la rutina compilada, usando Ctrl+C o seleccionando, click derecho y copiar.
3) Encuentra algún espacio vacío en la ROM ( lleno de 0xFF) y pega lo que has copiado antes, usando Ctrl+B. Ten cuidado, debes insertarlo en algún offset terminado por 0, 4, 8 o C. Es muy importante.
Ejecutando la rutina en la ROM
Simplemente debes usar el comando callasm 0x(offset de la rutina + 1). Por ejemplo, si la rutina está en el offset 0x800000 usaremos callasm 0x800001.
Para poder saber si habéis entendido los pasos a seguir podéis probar a insertar la rutina que os dejo a continuación. No os preocupéis, dentro de poco aprenderéis a hacer una igual
Código:
.text
.align 2
.thumb
main:
push {r0-r2,lr}
ldr r0, =0x20370D0
ldrb r0, [r0]
mov r1, #0xD
mul r0, r0, r1
ldr r1, =0x8247094
add r0, r0, r1
ldr r1, =0x2021D18
loop:
mov r2, r0
ldrb r0, [r0]
cmp r0, #0xFF
bEQ end
strb r0, [r1]
add r0, r2, #0x1
add r1, r1, #0x1
b loop
end:
mov r0, #0xFF
strb r0, [r1]
pop {r0-r2,pc}
Código:
#dynamic 0x800000
'---------------
#org @start
lock
faceplayer
random 0xFF
callasm 0x(offset de la rutina + 1)
bufferstring 0x0 0x2021D18
msgbox @string1 MSG_NORMAL '"I love the move [buffer1]"
release
end
'---------
' Strings
'---------
#org @string1
= I love the move [buffer1]!
Comandos
He aquí un pequeño repaso a los comandos básicos del ASM. Para conceptos como "que es una pila" os recminedo leer otros tutoriales de ASM más básicos, como los de Cheve.
Este comando sirve para hacer un pequeño save de nuestros registros para poder mantener intactos sus valores originales. Este comando irá en la mayoría de rutinas. Su sintaxis es la siguiente:
push {r0-rx, lr} Sirve para guardar los valores de todos los registros desde el primero registro hasta el último, ambos incluidos, siendo x desde 0 hasta 7. Es decir, imaginad que tengo push {r0-r2}, esto guarda los valores de r0, r1 y r2 en una pila.
push {rx,lr} Sirve para guardar en una pila el valor del registro que sea, por ejemplo, push {r0}
push {r0,r2,lr} Es para hacer lo mismo de antes pero solo con los valores que hayamos escrito (en este caso r1 no).
Dicho de una forma simple, debes pushear todos los registros que vaayas a usar en la rutina, no tiene más misterio.
push {r0-rx, lr} Sirve para guardar los valores de todos los registros desde el primero registro hasta el último, ambos incluidos, siendo x desde 0 hasta 7. Es decir, imaginad que tengo push {r0-r2}, esto guarda los valores de r0, r1 y r2 en una pila.
push {rx,lr} Sirve para guardar en una pila el valor del registro que sea, por ejemplo, push {r0}
push {r0,r2,lr} Es para hacer lo mismo de antes pero solo con los valores que hayamos escrito (en este caso r1 no).
Dicho de una forma simple, debes pushear todos los registros que vaayas a usar en la rutina, no tiene más misterio.
Devuelve a la RAM los valores originales de los registros. Es decir, vuelve a cargar los valores de los registros anteriormente pusheados.
pop {r0-rx,pc} Devulve los valores originales de todos los registros desde el primero registro hasta el último señalizados, ambos incluidos.
pop {rx,pc} Devuelve el valor original al registro que hayamos puesto entre parentesis.
pop {r0,r2,pc} Lo mismo pero solo para los registros indicados.
Debeis popear todos los registros que hayais pusheado al principio.
pop {r0-rx,pc} Devulve los valores originales de todos los registros desde el primero registro hasta el último señalizados, ambos incluidos.
pop {rx,pc} Devuelve el valor original al registro que hayamos puesto entre parentesis.
pop {r0,r2,pc} Lo mismo pero solo para los registros indicados.
Debeis popear todos los registros que hayais pusheado al principio.
Suma los valores de dos registros o de un registro y un valor inmediato (de 0x0 a 0xFF) y los guarda en el primer registro que hayamos puesto. Por ejemplo:
add r0, r0, r1 Suma los valores de los registros r0 y r1 y guarda el resultado en r0
add r0, r0, #0xAB Suma el valor de r0 con un byte, es decir desde 0x0 hasta 0xFF, en este caso 0xAB
add r0, r0, r1 Suma los valores de los registros r0 y r1 y guarda el resultado en r0
add r0, r0, #0xAB Suma el valor de r0 con un byte, es decir desde 0x0 hasta 0xFF, en este caso 0xAB
Resta el valor de un registro a otro registro, también sirve con valores inmediatos, para finalmente guardar el resultado en el registro indicado. Por ejemplo:
sub r0, r0, r1 Hace al valor de r0 el valor de r1 y guarda el resultado en r0
sub r0, r0, #0xFF Resta al valor de r0 0xFF y guarda el resultado en r0
sub r0, r0, r1 Hace al valor de r0 el valor de r1 y guarda el resultado en r0
sub r0, r0, #0xFF Resta al valor de r0 0xFF y guarda el resultado en r0
Multiplica el valor de dos registros para guardar la operación en el registro indicado. Solo se podrán usar valores de registros, no valores inmediatos. Por ejemplo:
mul r0 , r0, r1 Multiplica los valores de r0 y r1 entre si y guarda el resultado en r0
mul r0 , r0, r1 Multiplica los valores de r0 y r1 entre si y guarda el resultado en r0
Mueve el valor del registro un bit a la izquierda. Es un comando que trabaja a nivel de codigo de maquina, binario. Sirve para valores de un registro, y se especifica la cantidad de de posiciones que se cambian con un valor inmediato, de 0x0 hasta 0xFF. Cabe decir que un valor superior a 0x20 es inutil. Por ejemplo:
lsl r0, r0, #0x3 Mueve tres posiciones a la izquierda el valor de r0 en binario, y lo guarda en r0.
Si no has entendido este comando no te preocupes, lo analizaremos más a fondo más tarde.
lsl r0, r0, #0x3 Mueve tres posiciones a la izquierda el valor de r0 en binario, y lo guarda en r0.
Si no has entendido este comando no te preocupes, lo analizaremos más a fondo más tarde.
Mueve el valor de un registro un bit a la derecha, la mecánica es igual que el del lsl, pero al contrario. Por ejemplo:
lsr r0, r0, #0x3 Mueve tres posiciones a la derecha el valor de r0 y lo guarda en r0
Al igual que el lsl este lo analizaremos más tarde.
lsr r0, r0, #0x3 Mueve tres posiciones a la derecha el valor de r0 y lo guarda en r0
Al igual que el lsl este lo analizaremos más tarde.
Carga en un registro un valor de cuatro bytes. Es decir, un número entre 0x0 a 0xFFFFFFFF. Por ejemplo:
ldr r0, =0x8A450901 Guarda el valor indicado en r0
ldr r1, [r0] Sirve para guardar en r1 los primeros cuatro bytes que haya en la dirección dada por r0
ldr r0, =0x8A450901 Guarda el valor indicado en r0
ldr r1, [r0] Sirve para guardar en r1 los primeros cuatro bytes que haya en la dirección dada por r0
Carga un valor de un byte en un registro, es decir, de 0x0 hasta 0xFF. Por ejemplo:
ldrb r0, =0xAB
ldrb r1, [r0] Carga el byte que esté almacenado en el offset dado por el valor de r0.
ldrb r0, =0xAB
ldrb r1, [r0] Carga el byte que esté almacenado en el offset dado por el valor de r0.
Carga una half word (dos bytes) en un registro, de 0x0 hasta 0xFFFF. Por ejemplo:
ldrh r0, =0xF45A Carga en r0 el valor 0xF45A
ldrh r0, [r1] Carga en r0 los primeros dos bytes que haya en el offset indicado por el valor de r1
ldrh r0, =0xF45A Carga en r0 el valor 0xF45A
ldrh r0, [r1] Carga en r0 los primeros dos bytes que haya en el offset indicado por el valor de r1
Guarda el valor de un registro en la dirección dada por un registro entre corchetes. Es decir:
str r0, [r1] Guarda el valor de r0 en la dirección que indique el valor de r1
str r0, [r1] Guarda el valor de r0 en la dirección que indique el valor de r1
Guarda un byte almacenado en un registro en la dirección dada por el valor de un registro.
strb r0, [r1] Guarda el byte (en caso de ser más grnade solo cogera el primer byte) almacenado en r0 y lo guarda en la dirección especificada por el valor de r1
strb r0, [r1] Guarda el byte (en caso de ser más grnade solo cogera el primer byte) almacenado en r0 y lo guarda en la dirección especificada por el valor de r1
Guarda un half word, dos bytes, del valor de un registro en el offset indicado por el valor de otro registro
strh r0, [r1] Guarda los dos bytes (en caso de ser más grande solo los dos primer bytes) y los guarda en la dirección dada por el valor de r1
strh r0, [r1] Guarda los dos bytes (en caso de ser más grande solo los dos primer bytes) y los guarda en la dirección dada por el valor de r1
Sirve para mover un valor inmediato ( de 0x0 a 0xFF) a un registro o el valor de un registro a otro.
mov r0, #0x4 El valor se r0 será 0x4
mov r0, r2 El valor de r0 será el mismo que el de r2
mov r0, #0x4 El valor se r0 será 0x4
mov r0, r2 El valor de r0 será el mismo que el de r2
La tarea
¿Recordais el comando random? ¿No? Pues a aprender scripting ahora mismo! El comando random crea un valor aleatorio de un byte desde 0x0 hasta el que nosotros le indiquemos, su uso es el siguiente:
Código:
random 0x(el byte que quieras)
Antes de nada necesitamos toda la palabrería siguiente:
Código:
.text
.align 2
.thumb
main:
@Aquí irán los comandos
Por lo tanto cargaremos en registros dichos offsets:
Código:
.text
.align 2
.thumb
main:
push {r0-r1,lr} @Guardamos en la pila los valores de ambos registros
ldr r0, =0x20370B8 @Cargamos la dirección de la var 0x8000 en r0
ldr r1, =0x20370D0 @Cargamos la dirección de la var 0x800D en r1
pop {r0-r1,pc} @Devolvemos a los registros sus valores originales
Código:
.text
.align 2
.thumb
main:
push {r0-r1,lr} @Guardamos en la pila los valores de ambos registros
ldr r0, =0x20370B8 @Cargamos la dirección de la var 0x8000 en r0
ldr r1, =0x20370D0 @Cargamos la dirección de la var 0x800D en r1
ldrb r0, [r0] @Cargamos en r0 el valor de la var 0x8000
ldrb r1, [r1] @Cargamos en r1 el valor de la var 0x800D
pop {r0-r1,pc} @Devolvemos a los registros sus valores originales
Código:
.text
.align 2
.thumb
main:
push {r0-r1,lr} @Guardamos en la pila los valores de ambos registros
ldr r0, =0x20370B8 @Cargamos la dirección de la var 0x8000 en r0
ldr r1, =0x20370D0 @Cargamos la dirección de la var 0x800D en r1
ldrh r0, [r0] @Cargamos en r0 el valor de la var 0x8000
ldrh r1, [r1] @Cargamos en r1 el valor de la var 0x800D
add r0, r0, r1 @Sumamos los valores de ambos registros y guardamos el resultado en r0
pop {r0-r1,pc} @Devolvemos a los registros sus valores originales
Código:
.text
.align 2
.thumb
main:
push {r0-r1,lr} @Guardamos en la pila los valores de ambos registros
ldr r0, =0x20370B8 @Cargamos la dirección de la var 0x8000 en r0
ldr r1, =0x20370D0 @Cargamos la dirección de la var 0x800D en r1
ldrh r0, [r0] @Cargamos en r0 el valor de la var 0x8000
ldrh r1, [r1] @Cargamos en r1 el valor de la var 0x800D
add r0, r0, r1 @Sumamos los valores de ambos registros y guardamos el resultado en r0
ldr r1, =0x20370B8 @Carga el offset de la var 0x8000 en r1
strh r0, [r1] Guardar el half word en r0, la suma vaya, en el offset indicado en r1
pop {r0-r1,pc} @Devolvemos a los registros sus valores originales
Comprobando la rutina...
Ahora solo falta hacer un script en el que veamos si de verdad nuestra rutina funciona. A si que depúes de insertar la rutina usaremos este script:
Código:
#dynamic 0x800000
#org @start
lock
faceplayer
setvar 0x8000 (valor que quieras)
random 0xFF
callasm 0x(offset de la rutina + 1)
buffernumber 0x0 0x8000
msgbox @text 0x6
release
end
#org @text
=The number [buffer1] is great!
Cargando grandes constantes en un registro
Hay veces que necesitamos asignar un valor relativamente grande a un registro. Os pondré un ejemplo. Imaginad que en mi rutina tengo que interactuar con la flag número 0x828 (la flag del menu pokemon valga la redundancia). Antes de comprobar si esa flag está activada o no debo cargar ese mismo número en un registro. ¿Cómo se puede hacer algo así? Tratad de hacerlo por vosotros mismos, olvidaos de toda la palabrería del .txt .align 2 y todo ese rollo, simplemente escribid el cuerpo de la rutina. Después mirad la solución.
Estoy seguro de que más de uno habrá tratado de hacer lo siguiente:
Código:
push {r0, lr}
@tratar de carga 0x828 directamente en r0
mov r0, #0x828
pop {r0, pc}
Código:
push {r0-r1, lr}
@guardar en r0 y r1 algunos valores
mov r0, #0xFF
mov r1, #0x8
@multiplicar r0 y r1 (0xFF * 0x8) = 0x7F8
mul r0, r0, r1
@sumar 0x30 a 0x7F8 para obtener 0x828
add r0, r0, #0x30
pop {r0-r1, pc}
¿Pero como se que valores debo cargar en los registros? Para eso están las matemáticas. Primero dividiremos el número que queramos obtener, 0x828 en este caso, y lo dividiremos por 0xFF. Que más o menos da 8.188. Por lo tanto, 0x8 por 0xFF dará menos que 0x828 y que 0x9 por 0xFF dará más que 0x828. Es decir,
0x8 * 0xFF < 0x828 < 0x9 * 0xFF
Además, se que lo siguiente se cumple:
0x8 * 0xFF + h = 0x828 Siendo h un número. Vamos, que la equiación se queda así:
h = 0x828 - 0x8 * 0xFF = 0x828 – 0x7F8 = 0x30
En resumen:
0x8 * 0xFF + 0x30 = 0x828
Viendo como se queda esta identidad (ecuacion que se cumple siempre, 1 + 1 = 2 por ejemplo) en ASM lo escribiríamos de la forma que he mostrado antes. Es decir,
0x8 * 0xFF < 0x828 < 0x9 * 0xFF
Además, se que lo siguiente se cumple:
0x8 * 0xFF + h = 0x828 Siendo h un número. Vamos, que la equiación se queda así:
h = 0x828 - 0x8 * 0xFF = 0x828 – 0x7F8 = 0x30
En resumen:
0x8 * 0xFF + 0x30 = 0x828
Viendo como se queda esta identidad (ecuacion que se cumple siempre, 1 + 1 = 2 por ejemplo) en ASM lo escribiríamos de la forma que he mostrado antes. Es decir,
Código:
push {r0-r1, lr}
@guardar en r0 y r1 algunos valores
mov r0, #0xFF
mov r1, #0x8
@multiplicar r0 y r1 (0xFF * 0x8) = 0x7F8
mul r0, r0, r1
@sumar 0x30 a 0x7F8 para obtener 0x828
add r0, r0, #0x30
pop {r0-r1, pc}
Bien, este método funciona, innegablemente. Pero se puede optimizar. Es decir, el comando mul es un comando muy lento, como queremos que nuestra rutina se ejecute lo más rápido y eficientemente posible buscaremos otra forma. Por ejemplo, repitiendo sumas:
Código:
main:
push {r0, lr}
mov r0, 0xFF
add r0, r0, #0xFF
add r0, r0, #0xFF
add r0, r0, #0xFF
add r0, r0, #0xFF
add r0, r0, #0xFF
add r0, r0, #0xFF
add r0, r0, #0xFF
add r0, r0, #0x30
pop {r0, pc}
Código:
push {r0, lr}
mov r0, #0xFF
lsl r0, r0, #0x3
add r0, r0, #0x30
pop {r0, pc}
¿Como se traduce esto? Las matemáticas vuelves a explicarlo.
Empezaré con un ejemplo, imaginad que en mi registro tengo el valor 0xFF. Lo que en código de maquina se traduce como 00000000000000000000000011111111 . Aplicando el comando lsl r0, 0x1 conseguiremos lo siguiente:
00000000000000000000000111111110
Claramente aplicando un lsl de una posición haremos que el valor del registro se multiplique por dos. ¿ Qué no te fias? Dejame demostrartelo:
Antes de nada, todos los números del sistema decimal ( base diez) pueden ser expresados por dígitos multiplicado por potencias de diez sumandose. Un ejemplo:
4315 = 4 * 10^3 + 3 * 10^2 + 1 * 10^1 + 5 * 10^0
En otros sistemas como el hexadeciaml, sexagesimal, octal pasa igual. Y como no podría ser de otra manera en el binario pasa lo mismo, por ejemplo:
1101 = 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0
Por lo tanto, si añadimos un cero a la derecha, como bien hace el comando lsl haremos lo siguiente, usando el número del ejemplo anterior:
11010 que se puede expresar como suma de los digitos de una posicion determinada multiplicada por la base del sistema elevado a la posición:
11010 = 1 * 2^4 + 1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 0 * 2^0
Pero ese cero del final no está sumando nada, por lo tanto podemos olvidarlos de él:
11010 = 1 * 2^4 + 1 * 2^3 + 0 * 2^2 + 1 * 2^1
Si ahora sacamos un dos como factoe común:
11010 = ( 1 * 2^3 + 1 * 2^ + 0 * 2^1 + 1 * 2^0 ) * 2
Y recordemos que 1 * 2^3 + 1 * 2^ + 0 * 2^1 + 1 * 2^0 es 1101 Es decir,
11010 = 1101 * 2
Es decir, que al hacer lsl r0, # 0x1 haremos que el valor de r0 se multiplique por dos, si hacemos lsl r0, #0x2 haremos que se multipique ( recordad que ahora sacamos un 2^2 como factor común) por cuato (2^2), si hacemos con #0x3 lo multiplicaremos por 8, 2^3.
El comando lsr trabaja de la misma manera.
¿Queda claro?
Empezaré con un ejemplo, imaginad que en mi registro tengo el valor 0xFF. Lo que en código de maquina se traduce como 00000000000000000000000011111111 . Aplicando el comando lsl r0, 0x1 conseguiremos lo siguiente:
00000000000000000000000111111110
Claramente aplicando un lsl de una posición haremos que el valor del registro se multiplique por dos. ¿ Qué no te fias? Dejame demostrartelo:
Antes de nada, todos los números del sistema decimal ( base diez) pueden ser expresados por dígitos multiplicado por potencias de diez sumandose. Un ejemplo:
4315 = 4 * 10^3 + 3 * 10^2 + 1 * 10^1 + 5 * 10^0
En otros sistemas como el hexadeciaml, sexagesimal, octal pasa igual. Y como no podría ser de otra manera en el binario pasa lo mismo, por ejemplo:
1101 = 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0
Por lo tanto, si añadimos un cero a la derecha, como bien hace el comando lsl haremos lo siguiente, usando el número del ejemplo anterior:
11010 que se puede expresar como suma de los digitos de una posicion determinada multiplicada por la base del sistema elevado a la posición:
11010 = 1 * 2^4 + 1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 0 * 2^0
Pero ese cero del final no está sumando nada, por lo tanto podemos olvidarlos de él:
11010 = 1 * 2^4 + 1 * 2^3 + 0 * 2^2 + 1 * 2^1
Si ahora sacamos un dos como factoe común:
11010 = ( 1 * 2^3 + 1 * 2^ + 0 * 2^1 + 1 * 2^0 ) * 2
Y recordemos que 1 * 2^3 + 1 * 2^ + 0 * 2^1 + 1 * 2^0 es 1101 Es decir,
11010 = 1101 * 2
Es decir, que al hacer lsl r0, # 0x1 haremos que el valor de r0 se multiplique por dos, si hacemos lsl r0, #0x2 haremos que se multipique ( recordad que ahora sacamos un 2^2 como factor común) por cuato (2^2), si hacemos con #0x3 lo multiplicaremos por 8, 2^3.
El comando lsr trabaja de la misma manera.
¿Queda claro?
Volviendo al tema, para poder conseguir 0x828 podemos usar lo aprendido ahora mismo. En la medida de los posible sustituiremos mul por lsl, al ser el segundo mucho más rápido. Por lo tanto, la rutina queda así:
Código:
mov r0, #0xFF @r0 = 0xFF
lsl r0, r0, #0x3 @r0 = r0 * 8 (que es 0x7F8)
add r0, r0, #0x30 @0x7F8 + 0x30 = 0x828
Código:
main:
push {r0, lr}
ldr r0, value
pop {r0, pc}
.align 2
value:
.word 0x828
Trabajando con tablas y punteros
Las tablas son en su mayoría (seguramente la única) la forma en la que el juego almacena gestiona sus datos. Las tablas sirven por ejemplo para organizar los nombres de los movimientos, Pokemon, habilidades... Cuando estés haciendo una rutina muchas veces tendrás que consultar dichas tablas, por lo tanto aprendamos a como usarlas.
Generalmente hay dos tipos de tablas, los que por cada posición tienen un byte para finalizar, FF en su mayoría, y los que tienen un número determinado de bytes por posición y unos bytes para finalizar la tabla. Ambos tienen sus datos guardados con una longitud determinada. Aunque está claro que movimientos como BITE y PERISH SONG no ocupan el mismo espacio, por eso existe el relleno, que son los 00, para usar precisamente para rellenar, para que todos los nombres ocupen lo mismo.
En una rutina, para trabajar con tablas necesitamos algunos datos esenciales:
1) La cantidad de bytes que usa cada uno de los elementos, que es constante gracias al relleno.
2) El offset del primero elemento de la tabla
Pongamos en practica lo aprendido y hagamos un ejemplo, vamos a trabajar con los nombres de los movimientos.
1) Cada movimiento ocupa una cantidad de 13 bytes
2) No tiene byte para finalizar, el FF vamos
3) La tabla comienza en el offset 0x8247094
La tarea: No os preocupéis por el .text, .align 2 y esas cosas, escribid lo que de verdad importa. Cargad en r0 el offset del movimiento número 20 ( con trece bytes por elemento ). Trata de hacerlo tú mismo antes de mirar.
Pistas:
1) En este caso es más facil usar mul que lsl, por tanto usad mul
2) Usad ldr =0x8247094 para cargar el offset del inicio de la tabla
Bonus:
¿Puedes cargar la primera letra en el registro r0?
Código:
push {r0-r1, lr}
mov r0, #0x13 @index size
mov r1, #0x20 @move ID
mul r1, r1, r0 @r1 contains how far in the table we need to go before finding the move's name
ldr r0, =(0x8247094) @the move table
add r0, r0, r1 @the address of the 20th move's name
@si habéis hecho el bonus tendréis estás lineas también
ldrb r0, [r0] @the first letter of the 20th move
pop {r0-r1, pc}
Finalmente cargamos en r0 la primera letra de ese movimiento.
Y así es como leemos los datos de una tabla sabiendo cual es el número de indice del elemento. ¿Pero como buscamos algo en una tabla? Pongamos en el juego hay una tabla con los IDs de los objetos considerados que se puedan vender. Y pongamos el ejemplo de que yo quiero comprobar si el objeto con ID 0x55 se puede vender o no. ¿Como puedo saber si ese objeto se puede vender o no? Tendrías que buscar en la tabla cada número de indice hasta el final de tabla, o simplemente buscar el 0x55.
Esta tabla está formada por half words, es decir, dos bytes por cada elemento, y la tabla termina con un 0xFFFF. Tratad de comprobar si ese objeto está en la tabla o no, después mirad la solución.
1) La tabla comienza en el offset 0x740000
2) Cada elemento es un half word, dos bytes
Pistas: En vez de usar ldrb usad ldrh
Bonus: Si el objeto está en la tabla cargad en r0 el valor 0x0, de lo contrario 0x1
Código:
push {r0-r2, lr}
ldr r0, =(0x8740000)
mov r2, #0xFF
lsl r2, r2, #0x8
add r2, r2, #0xFF
loop:
ldrh r1, [r0]
cmp r1, r2
beq endofTable
cmp r1, #0x55
beq found
add r0, r0, #0x2
b loop
found:
mov r0, #0x0
b end
endOfTable:
mov r0, #0x1
end:
pop {r0-r2, pc}
Cmp: Compara los valores de dos registros o el valor de un registro con un valor inmediato, su sintaxis es la siguiente:
cmp r0, r1
cmp r0, #0x7 ( por ejemplo)
En la practica debemos usar una condición a continuación, las cuales son:
EQ => igual
NE => no igual
CS/HS => mayor o igual (>=)
CC/LO => menor (<)
MI => negativo
PL => positivo o cero
HI => mayor (>)
LS => menor o igual (<=)
AL => siempre
Para entenderlo mejor os pondré un ejemplo:
cmp r0, r1 @Compara los valores r0 y r1
bEQ label @si se cumple la condición ir a la etiqueta label
De forma generica es así:
cmp (primero registro), (segundo registro)
b(condición) (etiqueta)
cmp r0, r1
cmp r0, #0x7 ( por ejemplo)
En la practica debemos usar una condición a continuación, las cuales son:
EQ => igual
NE => no igual
CS/HS => mayor o igual (>=)
CC/LO => menor (<)
MI => negativo
PL => positivo o cero
HI => mayor (>)
LS => menor o igual (<=)
AL => siempre
Para entenderlo mejor os pondré un ejemplo:
cmp r0, r1 @Compara los valores r0 y r1
bEQ label @si se cumple la condición ir a la etiqueta label
De forma generica es así:
cmp (primero registro), (segundo registro)
b(condición) (etiqueta)
Lo que en scripting viene a ser un pointer, si eso de #org @loquesea, dicho de forma rápida y simple. La rutina está repartida en label o etiquetas. Primero se ejecutará la primera etiqueta, y si no hay ningún salto este terminará y pasaremos a la siguiente etiqueta. Un ejemplo:
main:
mov r0, #0xA
loop:
ldr r1, =0xFFFFFF
En este caso primero se ejecutará el main y luego el loop. Bien, pero hay ciertos comandos para saltar de unos a otros. Y son b, bl, bx.
b: lo que en scripting viene siendo un goto. Es decir, si en mi rutina pongo un b (label), al ejecutar esa linea saltará automaticamente al label o etiqueta que hayamos especificado.
bl: Es lo que en scripting viene siendo un call, es decir, es como el b pero tiene una especie de return incorporado. Es decir, al usar bl (label), cuando se ejecute esa línea, pasara a la etiqueta especificada y una vez que termine volverá donde estaba.
bx: Sirve para saltar a otra rutina del juego, para usar funciones ya incorporadas en la ROM más que nada. Requieren de un parametro, ol offset donde está almacenada la rutina: bx r1 por ejemplo, saltará a la rutina del offset dado por r1.
Hay una forma de llamar a otra rutina y una vez que esa se ejecute del todo vuelva a la rutina que lo ha llamado, que es así:
De está forma llamaremos a otra rutina y una vez que termine volverá donde estaba.
Resumiendo, b sirve como si fuera un goto, bl como un call con return incorporado y bx para llamar a otras rutinas.
main:
mov r0, #0xA
loop:
ldr r1, =0xFFFFFF
En este caso primero se ejecutará el main y luego el loop. Bien, pero hay ciertos comandos para saltar de unos a otros. Y son b, bl, bx.
b: lo que en scripting viene siendo un goto. Es decir, si en mi rutina pongo un b (label), al ejecutar esa linea saltará automaticamente al label o etiqueta que hayamos especificado.
bl: Es lo que en scripting viene siendo un call, es decir, es como el b pero tiene una especie de return incorporado. Es decir, al usar bl (label), cuando se ejecute esa línea, pasara a la etiqueta especificada y una vez que termine volverá donde estaba.
bx: Sirve para saltar a otra rutina del juego, para usar funciones ya incorporadas en la ROM más que nada. Requieren de un parametro, ol offset donde está almacenada la rutina: bx r1 por ejemplo, saltará a la rutina del offset dado por r1.
Hay una forma de llamar a otra rutina y una vez que esa se ejecute del todo vuelva a la rutina que lo ha llamado, que es así:
Código:
...
bl linker
...
linker:
bx r1
Resumiendo, b sirve como si fuera un goto, bl como un call con return incorporado y bx para llamar a otras rutinas.
Ahora veamos la rutina bien explicada:
Código:
main:
push {r0-r2}
@carga en r0 el offset del inicio de la tabla
ldr r0, =(0x8740000)
@el lsl que aprendimos hace nada
@cargamos en r2 0xFFFF, que es el final de la tabla
mov r2, #0xFF
lsl r2, r2, #0x8
add r2, r2, #0xFF @r2 = 0xFFFF
loop:
@cargamos un half word almacenado el el offset indicado por el valor de r0 en r1
ldrh r1, [r0]
@si ese half word es igual a 0xFFFF significa que hemos terminado la tabla
cmp r1, r2
beq endofTable
@si ese half word es 0x0055 será que lo hemos encontrado
cmp r1, #0x55
beq found
@todos los demás resultados nos dan igual
@simplemente añadiremos un dos a r0, lo cual hará que cargemos en r0 el siguiente objeto de lista
add r0, r0, #0x2
@vuelta a empezar, volvemos a loop, he aquí la razon por la que lo hemos llamado asi precisamente
b loop
found:
@lo hemos encontrado en la tabla
@cargar 0x0 en r0
mov r0, #0x0
b end
endOfTable:
@hemos terminado la tabla y no lo hemos encontrado
@cargar 0x1 en r0
mov r0, #0x1
end:
pop {r0-r2}
Usando funciones del juego
Antes de explicar como usar una función quiero remarcar como es una función. Simplemente es una serie de operaciones basadas en 0 o más parámetros que nos devuelve en registros unos valores de salida. Un ejemplo de esto es la función checkflag conocida por todos ( o eso espero ) del scripting.
Esa función es toma el valor de la flag en el registro r0 y tras una serie de operaciones devuelve en r0 0x0 si está desactivada y 0x1 si está activada. Vamos a tratar de comprobar si la flag 0x828 está activada o no.
Código:
push {r0-r2, lr}
@cargar 0x828 en r0
mov r0, #0xFF
lsl r0, r0, #0x3
add r0, r0, #0x30
@así es como llamamos a una función
ldr r1, =(0x806E6D0 +1)
bl linker
@comprobar si está activada o no
cmp r0, #0x1
beq set
pop {r0-r2, pc}
set:
@si está activada saltara aquí
linker:
bx r1
Manipulando los registros mucho más
Este apartado ya es un poco más adelantado pero actualmente creo que lo entenderos bastante bien. Básicamente los comandos lsl y lsr sirven para eliminar la basura no deseada de los valores de los registros. Para entenderlo mejor voy a empezar con un buen ejemplo. Como sabéis, y si no lo sabéis ahora si, cada Pokemon tiene su propio PID o Pokemon ID el cual es un valor semi aleatorio de cuatro bytes o una word. Este valor es una mezcla entre otros dos valores. Imaginad que tengo dos valores XXXXXXXX y YYYYYYYY, pues el PID será así:
[primeros dos bytes de X][ultimos dos bytes de Y]
si X = 0xDE6C3B8F y Y = 0xCA9B23E1
luego PID = [DE6C][23E1], lo que viene siendo PID = 0xDE6C23E1
¿Pero como lo se hace eso Vamos, coger los dos primeros bytes de uno y los últimos de otro... Pues con el lsl, lsr y otro más, no nos adelantemos.
Tenemos que remover los dos últimos bytes de X y los dos primeros bytes de Y, para ello y sabremos en lsl y lsr. Sabemos que ambos trabajan a nivel de bits, en binario, por lo tanto debemos encontrar la equivalencia entre bit y byte, que bien conocido es que dos bytes son 16 bits. Por lo tanto, para remover dos bytes en el lsl o lsr usaremos como parámetro #0x10 (16 en hex)
Por tanto, lsl r0, #0x10 hará que de XXXXXXXX pasemos a XXXX0000, después debemos devolverlos a donde estaban y para eso usaremos lsr, basta de palabrería, veamoslo en un ejemplo:
Código:
main:
push {lr}
@borramos la segunda parte r0
@y devolvemos los valores que hayan sobrevivido a donde estaban usando lsl
lsr r0, r0, #0x10
lsl r0, r0, #0x10
@borramos la primera parte de r1
@y los devolvemos a donde estaban
lsl r1, r1, #0x10
lsr r1, r1, #0x10
@ahora explico esto
orr r0, r0, r1
pop {pc}
r0=E4720000
r1=0000FA3D
orr hace una operación entre estos dos valores un poco extraña. Pongamos que en cada uno de los números cada dígito tiene una posición, tal que así:
orr r0, r0, r1 hace que todos los dígitos de r0 que sean ceros sean sustituidos por dígitos que tengan la misma posición tomados del registro r1. Por tanto, en este caso:
orr r0, r0, r1 r0 = E472FA3D
Para terminar un pequeño proyecto...
Para poner en practica todo lo aprendido hasta ahora os propongo un pequeño reto. ¿Recordáis el comando random? Pues bien, usando el valor aleatorio que haya genradp este comando haremos un buffer de un movimiento aleatorio.
En scripting random 0xFF devuelve a la variable 0x800D un valor aleatorio entre 0x0 y 0xFF. Consideraremos ese valor aleatorio como el ID de un movimiento. A partir de aquí haced una rutina que lea el valor de la variable 0x800D. Navegar por la tabla de movimientos y cargar el nombre del movimiento indicado aleatoriamente en el offset 0x2021D18
1)La tabla de movimientos empieza en el offset 0x8247094
2) Cada elemento corresponde a un nombre
3) Cada elemento contiene una cantidad de 13 bytes (0xD en hex)
4) Cada movimiento termina con un byte de 0xFF
5) El offset de la RAM de la variable 0x800D es 0x20370D0
El script será el siguiente:
Código:
#dynamic 0x800000
#org @start
lock
faceplayer
random 0xFF
callasm 0x (offset de la rutina + 1 )
bufferstring 0x0 0x2021D18
msgbox @string 0x6
release
end
#org @string
= I love the move [buffer1] !
Esta es mi ( mio, Metal Kaktus) solución particular, pero al ver que me daba errores usar la funciona dada por la pista lo hice manualmente, la rutina es la siguiente:
Código:
.text
.align 2
.thumb
main:
push {r0-r2,lr}
ldr r0, =0x20370D0
ldrb r0, [r0]
mov r1, #0xD
mul r0, r0, r1
ldr r1, =0x8247094
add r0, r0, r1
ldr r1, =0x2021D18
loop:
mov r2, r0
ldrb r0, [r0]
cmp r0, #0xFF
bEQ end
strb r0, [r1]
add r0, r2, #0x1
add r1, r1, #0x1
b loop
end:
mov r0, #0xFF
strb r0, [r1]
pop {r0-r2,pc}
El objetivo de este apartado es editar las propias rutinas de la ROM para poder llevar a cabo cosas que de otro modo no se podrían hacer, como por ejemplo el conocido Repelente B&W. Para ello dividiremos el apartado en los siguientes procesos:
• Desarrollar un algoritmo
• Buscando un lugar a editar
• Analizar el sitio que vamos a editar
• Editar
• Escribir nuestra propia rutina
En resumen, se trata de encontrar una parte de una rutina ASM y una vez analizados sus parametros hacer que se ejecute una rutina que nosotros queramos. Como siempre usaré un ejemplo para llevar a cabo el tutorial.
Como bien sabréis, y si no lo sabéis pues os lo digo yo, la cantidad de caracteres de los nombres de un objeto esta limitado. Exactamente está limitado a 14 bytes, siendo el ultimo siempre 0xFF por lo que se queda con un total de 13 bytes para útiles.
Nuestro objetivo será expandir esa cantidad máxima a infinito, es decir, eliminar el límite de caracteres.
Antes de empezar es conveniente ver si esto es posible o no. Los nombres de los objetos como otras cosas, se almacenan en tablas. Lo primero será averiguar cual es la estructura de la tabla. Es decir, como se organiza, el inicio de la tabla y el espacio correspondiente a cada uno de los elementos.
Información sobre la tabla de nombres de objetos:
• Cada elemento finaliza con el byte 0xFF
• Cada elemento contiene 14 bytes (0xFF incluido)
Algoritmo:
Como el byte 0xFF es el byte que da a entender que el texto ha finalizado este byte no puede ser usado como parte del nombre. Por lo tanto, si queremos insertar un nombre que supera el limite de caracteres en su correspondiente offset pondremos un 0xFF y un pointer al texto. De esta forma sabremos si debemos saltar a otro offset para leer el nombre o si hay que leerlo en ese mismo offset. El siguiente código os ayudará a entenderlo mejor:
El objetivo será encontrar la rutina encargada de copiar un texto de la ROM a algún sitio de la RAM. Para empezaremos con un ejemplo. Seleccionad el nombre de un objeto, y traducidlo a hexadecimal mediante el uso de esta tabla. Yo usaré el objeto “BURN HEAL”, el cual en hexadecimal es “BCCFCCC800C2BFBBC6”.
Ahora buscaremos esos caracteres hexadecimales en la ROM. No os olvidéis de añadir 0xFF, es decir, el byte que da a entender que el texto ha terminado. Por lo tanto, lo que buscaremos en la ROM será “BCCFCCC800C2BFBBC6FF”.
Dicho todo eso, usaremos “Ctrl + F” y aparecerá una ventana semejante a esta:
En buscar pondremos la cadena de valores hexadecimales del nombre más el 0xFF. En tipos de datos seleccionaremos “Valores Hexadecimales” y en dirección podremos “todo”.
Una ves que hayáis realizado la búsqueda os llevará al offset del nombre de vuestro objeto. En mi caso es el offset 0x0803B2BC. Recordad, que el prefijo para la ROM es 08 y para la RAM 02.
Ahora abrid la ROM y haced un script donde un NPC o lo que sea os de el objeto que hayáis buscado ahora. En mi caso haré un NPC que me de un “BURN HEAL”. Después al ejecutar el juego guardad partida justo antes de recibir el objeto, vamos a necesitar seguir a partir de aquí muchas veces.
Ahora abrid el VBA-SDL-H y una vez que os hayáis asegurado de recibir el objeto deseado activad el modo debugger y haremos un breakpoint. En este caso no detendremos la ejecución en un offset en especifico sino en el momento preciso donde el offset que contiene el nombre del objeto sea leído. Como el nombre ocupa 14 bytes usaremos el siguiente comando:
Ahora presionad la tecla “c” y abriremos la mochila, el breakpoint que hayáis puesto antes debería haberse activado.
Vamos a fijarnos en el comando en el que se ha detenido y el contenido de los registros. Como podréis observar la instrucción anterior en la que se ha parado el juego está cargando un byte de r1 en r2. Veremos que el valor de r1 es efectivamente el offset del inicio del nombre de nuestro objeto.
Ahora ya tenemos un offset desde donde tirar del hilo. Eso quiere decir que es hora de usar el desensamblador del VBA (o el IDA, es mucho mejor, hacedme caso). Es decir, abrimos la ROM con el VBA y nos iremos a “Tools > Disassemble”. En la caja donde pone “Go” escribiremos el offset del comando donde se ha parado el juego. Es decir, en el offset 08008D90 (no os olvidéis de seleccionar la parte donde pone thumb).
Ahora vamos a ir subiendo poco a poco en ese mar de instrucciones en ASM hasta encontrar un push que al menos, pushee el link register (lr). En un momento habremos encontrado el inicio de la función, concretamente en el offset 08008D84. Vamos a tratar de entender que es lo que hace esta función:
Para ir acostumbrándose conviene que sepáis vosotros mismos lo que hacen las funciones, es decir, quiero que leáis el código las veces que haga falta hasta que le veáis el sentido, una vez que lo entendáis o desistáis, aquí os dejo un pseudo-código para entenderlo mejor:
Para que lo entendáis mejor, r1 parece contener el offset de la ROM del nombre del objeto y r0 el offset de la RAM donde vamos a guardar el nombre.
Como podréis apreciar en el código simplificado que he puesto esta es una función que copia un texto terminado en FF. Realmente el código está mal hecho, al ser ineficiente, pero es lo que hay.
En otras palabras, hemos encontrado la función que copia textos de la ROM a la RAM y probablemente de la RAM a la RAM. Ahora bien, vamos a acostumbrarnos a definir los parámetros de las funciones que nos vayamos encontrando. En este caso como antes he dicho los parámetros iniciales son r0 =destino y r1=origen del texto.
Cabe destacar que r1 contiene el pointer al offset del nuestro objeto, es decir, que ese offset ya está guardado antes de que la función sea llamada. Por lo tanto debemos buscar la función que llama a esta función. Para ello usaremos un breakpoint al inicio de la función. Para hacer un breakpoint en un offset en especifico usaremos el siguiente comando en el debugger del VBA-SLD-H:
Por lo tanto vamos poner bt 08008D84. Volvamos al juego, trataremos de abrir la mochila y nuestro breakpoint dará la alarma que la instrucción del offset que hemos especificado está siendo ejecutada.
Pongamos los puntos sobre las ies. Hemos encontrado la función que copia textos. Esto quiere decir que esta función será llamada infinidad de veces, por lo tanto, cuando haya saltado nuestro breakpoint debemos tener presente que r1 contiene el offset de nuestro objeto, en mi caso “BURN HEAL”.
Hasta encontrar nuestro texto deseado pulsaremos “c” hasta que aparezca.
Pero hay un pequeño problema, y es que el comando ejecutado previamente el debugger no es capaz de reconocerlo, me refiero al “blh $0fcc”. La solución a esto es muy sencilla, el comando ejecutado previamente está en el offset del “blh $0fcc” menos 2. Es decir, en el offset 08008DB4.
Otra vez, abriremos el Desensamblador del VBA e iremos al offset 08008DB4, y subiremos hasta encontrar la instrucción push que pushee por lo menos el registro lr.
El código que hemos encontrado es el siguiente:
Tratad de entender que es lo que hace esta función, una vez más, cuando creáis que lo tenéis o simplemente tiréis la toalla mirad el resultado:
Esta función trata sobre unir dos textos en uno solo, como por ejemplo “play” y “ground” y lo convierte en “playground” básicamente busca el offset del final de un texto y se lo entrega como parámetro a nuestra función de copiar como el lugar de destino.
El registro r1, que es el offset del nombre de nuestro objeto no ha sido modificado, eso quiere decir que todavía no hemos encontrado la función que modifica ese r1 y lo convierte en el offset del nombre de nuestro objeto. Por lo tanto, toca volver a hacer el proceso de antes. Primero hacemos un bt (offset de inicio de esta nueva función) y vemos cual ha sido la instrucion ejecutada anterior.
Asegurad que r1 contenga el offset del nombre de nuestro objeto, no lo olvidéis.
Como antes el debugger no es capaz de leer lo que pone en la instrucción ejecutada previamente, eso quiere decir que la instrucción antes ejecutada es la anterior a esa, es decir la que está en el offset: offset de la instrucción ilegible menos dos.
Como en este caso la instrucción previa ha sido “blh $08a0” que está en el offset 08108598 la instrucción previa ejecutada realmente está en el offset 08108596 (es decir, dos menos).
Ahora iremos en el desensamblador al offset 08108596 y subiremos un poco hasta encontrar un push que pushee como mínimo el lr. Ese comando está en el offset 08108560 y la función es la siguiente:
Casi con total seguridad puedo decir la esa función desconocida es la que obtiene el offset del nombre del objeto. Y os lo demostraré.
¿Recordáis cuando hablamos sobre los parámetros de las funciones? Lo normal es que estos sean los primeros cuatro registros bajos. Si hay más de cuatro para metros es otro tema. Bien, cuando una función devuelve valores muchas veces estos sirven como parámetros para otras funciones, almacenadas en r0-r3. Siempre en orden consecutivo, empezando desde el cero.
Como podréis observar al fondo de la función he marcado donde el debugger se ha parado. Esa función es la que unía palabras, como recordareis, donde r0=destino y r1=origen. Obviamente el origen es el pointer al nombre del texto. Pero un poco más arriba hay una línea que pone mov r1, r0. Eso quiere decir que la función desconocida devuelve en r0 el pointer del texto, y antes de que se ejecute esa función no había ni rastro del offset de nuestro texto, ergo, esa función mediante algún parámetro devuelve el offset del nombre del objeto! Eso quiere decir que ya hemos dado con la función a editar.
La función desconocida es la siguiente:
Para saber cuales son los parámetros de una función es bueno usar un break thumb (bt) al inicio de esta, y ver que es lo que contienen los registros, aunque sean solo números pueden indicarnos algo.
Entonces en el debugger pondremos:
Antes he dicho que los registros usados como parámetros son del r0 al r3, aunque eso no quiere decir que todos deban ser usados, para que no os extrañe ver números extraños.
Una vez que el juego se detenga en el offset indicado repararemos en los registros.
r1 contiene un pointer de algo, r2 ni idea de lo que contiene y r3 es otro pointer. Pero el registro r0 tiene el valor 0xF. Espera, el ID de “BURN HEAL” es 0xF, coincidencia? Bueno, probaremos con otro objeto y veremos el resultado es el mismo, r0 contiene el ID del objeto.
Ahora tratemos de entender que es lo que hace esta función, para empezar tenemos esto:
Esto sirve para comprobar que el dato introducido es un half word. Como podeis comprobar se borran los primero 16 bits del registro (un registro tiene 32 bits) y luego deja el número como estaba pero con los primeros 16 bits como 0s.
Ahora veremos que es lo que hace la función que llama el bl.
Ya deberíais saber lo que hacen los comandos lsl y lsr, y sino, repasad los anteriores apartados de este tutorial. Bien, r0=ID del objeto y r1=176. Compara los valores y dependiendo si el ID es más grande convierte el ID en cero y si es más pequeño lo deja como esta. Efectivamente esta es la función que comprueba si el objeto es existente o no, si queréis eliminar el máximo de objetos… pues aquí tenéis el límite, 176.
Volviendo al tema, nuestra función solo usa el registro r0 y los demás no, es decir, solo necesitamos el ID del objeto.
Ahora continuaremos entendiendo la función:
Pues ya está, ahora solo debemos editar esta función para que mire si hay que leer un texto o un pointer, tal y como dijimos al definir el algoritmo.
Bien, siempre que vayamos a editar una rutina necesitaremos 8 bytes, siempre, y hay dos formas de hacerlo.
Primero método:
Segundo método:
En el primero saltamos a un offset que nosotros queramos y en el segundo editamos el registro pc, program counter, para que automáticamente la próxima instrucción a ejecutar sea nuestra rutina.
Nuestro algoritmo trata de ir al offset del nombre del objeto y leer el primero byte para comprobar si es un 0xFF. Por lo tanto, nuestra pequeña modificación debe ser ua vez que ya tengamos el offset, pero debe ser dentro de esa función, recordad.
Una cosa muy importante es que onde vayamos a poner lo de ldr rX, offset… debe empezar en un offset acabado en 0, 4, 8 o C, si o si. Si esto no fuera necesario podríamos editar simplemente las últimas líneas:
Pero como ese ldr, offset de la tabla no acaba en un offset divisible por cuatro debemos editar desde la línea anterior, desde el mul r0, r0, r1.
Para poder hacer esto necesitamos seleccionar un registro seguro. Recordad qe necesitamos cargar en rX el offset de nuestra rutina. r0=ID y r1=0x2C, pero en r2 no ha nada almacenado ni se usa para nada, por lo tanto podemos usarlo para nuestro propósito.
Ahora haced esta rutina y compiladla:
Evidentemente primero encontrad un espacio libre donde poder hacer nuestra rutina y luego sustituid ese offset en esta rutina. Luego compilad y en el offset donde estaba el mul r0, r0, r1 pegais esta rutina compilada, ese offset, en este caso es 0809A8CC.
Es hora de hacer nuestra propia rutina, pero recordad que lo primero será poner los comandos que hemos borrado de la función en nuestra rutina.
Lo dicho, lo primero será escribir los comandos que nos hemos comido antes:
Todavía no he leído el pointer. Ahora debemos leer el pointer indicado después del 0xFF. Un ldr carga un valor de 4 bytes en un registro. Recordad que un ldr necesita un pointer en un offset que acabe en 0, 4, 8 o C. La tabla de objetos son 44 bytes que es divisible entre cuatro, es decir, cada offset de los nombre de los objetos acabará en 0, 4, 8 o C.
Pero nosotros usamos un 0xFF para dar a entender que había que leer un pointer, por lo tanto en ese offset de la ROM tendremos FF XX XX XX XX, las equises son un pointer.
Veréis que el pointer no empieza en ningún offset que acabe en 0, 4, 8 o C. Pero como cada nombre ocupa 14 bytes simplemente usaremos tres bytes de relleno después del 0xFF y después pondremos el pointer, para que este en un offset que acabe en 0, 4, 8 o C. Tal que así: FF 00 00 00 XX XX XX XX.
Ahora podemos usar el ldr sin problemas siguiendo la rutina:
Por lo tanto, la rutina lista insertar y para que todo funcione correctamente:
Y esto es todo por hoy, espero que hayáis aprendido a buscar y editar rutinas, usadlo con sabiduría
• Desarrollar un algoritmo
• Buscando un lugar a editar
• Analizar el sitio que vamos a editar
• Editar
• Escribir nuestra propia rutina
En resumen, se trata de encontrar una parte de una rutina ASM y una vez analizados sus parametros hacer que se ejecute una rutina que nosotros queramos. Como siempre usaré un ejemplo para llevar a cabo el tutorial.
Como bien sabréis, y si no lo sabéis pues os lo digo yo, la cantidad de caracteres de los nombres de un objeto esta limitado. Exactamente está limitado a 14 bytes, siendo el ultimo siempre 0xFF por lo que se queda con un total de 13 bytes para útiles.
Nuestro objetivo será expandir esa cantidad máxima a infinito, es decir, eliminar el límite de caracteres.
Desarrollando un algoritmo...
Antes de empezar es conveniente ver si esto es posible o no. Los nombres de los objetos como otras cosas, se almacenan en tablas. Lo primero será averiguar cual es la estructura de la tabla. Es decir, como se organiza, el inicio de la tabla y el espacio correspondiente a cada uno de los elementos.
Información sobre la tabla de nombres de objetos:
• Cada elemento finaliza con el byte 0xFF
• Cada elemento contiene 14 bytes (0xFF incluido)
Algoritmo:
Como el byte 0xFF es el byte que da a entender que el texto ha finalizado este byte no puede ser usado como parte del nombre. Por lo tanto, si queremos insertar un nombre que supera el limite de caracteres en su correspondiente offset pondremos un 0xFF y un pointer al texto. De esta forma sabremos si debemos saltar a otro offset para leer el nombre o si hay que leerlo en ese mismo offset. El siguiente código os ayudará a entenderlo mejor:
Código:
…
el código original da el offset del texto al juego para leerlo
…
coger el pointer del texto
…
leer primero byte
if 0xFF:
leer un pointer
else:
continuar
...
Buscando un sitio donde editar
El objetivo será encontrar la rutina encargada de copiar un texto de la ROM a algún sitio de la RAM. Para empezaremos con un ejemplo. Seleccionad el nombre de un objeto, y traducidlo a hexadecimal mediante el uso de esta tabla. Yo usaré el objeto “BURN HEAL”, el cual en hexadecimal es “BCCFCCC800C2BFBBC6”.
Ahora buscaremos esos caracteres hexadecimales en la ROM. No os olvidéis de añadir 0xFF, es decir, el byte que da a entender que el texto ha terminado. Por lo tanto, lo que buscaremos en la ROM será “BCCFCCC800C2BFBBC6FF”.
Dicho todo eso, usaremos “Ctrl + F” y aparecerá una ventana semejante a esta:
En buscar pondremos la cadena de valores hexadecimales del nombre más el 0xFF. En tipos de datos seleccionaremos “Valores Hexadecimales” y en dirección podremos “todo”.
Una ves que hayáis realizado la búsqueda os llevará al offset del nombre de vuestro objeto. En mi caso es el offset 0x0803B2BC. Recordad, que el prefijo para la ROM es 08 y para la RAM 02.
Ahora abrid la ROM y haced un script donde un NPC o lo que sea os de el objeto que hayáis buscado ahora. En mi caso haré un NPC que me de un “BURN HEAL”. Después al ejecutar el juego guardad partida justo antes de recibir el objeto, vamos a necesitar seguir a partir de aquí muchas veces.
Ahora abrid el VBA-SDL-H y una vez que os hayáis asegurado de recibir el objeto deseado activad el modo debugger y haremos un breakpoint. En este caso no detendremos la ejecución en un offset en especifico sino en el momento preciso donde el offset que contiene el nombre del objeto sea leído. Como el nombre ocupa 14 bytes usaremos el siguiente comando:
Código:
bpr 0803B2BC 14
Vamos a fijarnos en el comando en el que se ha detenido y el contenido de los registros. Como podréis observar la instrucción anterior en la que se ha parado el juego está cargando un byte de r1 en r2. Veremos que el valor de r1 es efectivamente el offset del inicio del nombre de nuestro objeto.
Ahora ya tenemos un offset desde donde tirar del hilo. Eso quiere decir que es hora de usar el desensamblador del VBA (o el IDA, es mucho mejor, hacedme caso). Es decir, abrimos la ROM con el VBA y nos iremos a “Tools > Disassemble”. En la caja donde pone “Go” escribiremos el offset del comando donde se ha parado el juego. Es decir, en el offset 08008D90 (no os olvidéis de seleccionar la parte donde pone thumb).
Ahora vamos a ir subiendo poco a poco en ese mar de instrucciones en ASM hasta encontrar un push que al menos, pushee el link register (lr). En un momento habremos encontrado el inicio de la función, concretamente en el offset 08008D84. Vamos a tratar de entender que es lo que hace esta función:
Código:
push {lr}
mov r3, r0
b label
label2:
strb r2, [r3]
add r3, #0x1
add r1, #0x1
label:
ldrb r2, [r1]
mov r0, r2
cmp r0, #0xFF
bne label2
mov r0, #0xFF
strb r0, [r3]
mov r0, r3
pop {r1}
bx r1
Código:
while (byte de r1 sea desigual a FF) {
copiar dicho byte en el offset de r0
}
guardar FF en el offset de r0
Como podréis apreciar en el código simplificado que he puesto esta es una función que copia un texto terminado en FF. Realmente el código está mal hecho, al ser ineficiente, pero es lo que hay.
En otras palabras, hemos encontrado la función que copia textos de la ROM a la RAM y probablemente de la RAM a la RAM. Ahora bien, vamos a acostumbrarnos a definir los parámetros de las funciones que nos vayamos encontrando. En este caso como antes he dicho los parámetros iniciales son r0 =destino y r1=origen del texto.
Cabe destacar que r1 contiene el pointer al offset del nuestro objeto, es decir, que ese offset ya está guardado antes de que la función sea llamada. Por lo tanto debemos buscar la función que llama a esta función. Para ello usaremos un breakpoint al inicio de la función. Para hacer un breakpoint en un offset en especifico usaremos el siguiente comando en el debugger del VBA-SLD-H:
Código:
bt offset
Pongamos los puntos sobre las ies. Hemos encontrado la función que copia textos. Esto quiere decir que esta función será llamada infinidad de veces, por lo tanto, cuando haya saltado nuestro breakpoint debemos tener presente que r1 contiene el offset de nuestro objeto, en mi caso “BURN HEAL”.
Hasta encontrar nuestro texto deseado pulsaremos “c” hasta que aparezca.
Pero hay un pequeño problema, y es que el comando ejecutado previamente el debugger no es capaz de reconocerlo, me refiero al “blh $0fcc”. La solución a esto es muy sencilla, el comando ejecutado previamente está en el offset del “blh $0fcc” menos 2. Es decir, en el offset 08008DB4.
Otra vez, abriremos el Desensamblador del VBA e iremos al offset 08008DB4, y subiremos hasta encontrar la instrucción push que pushee por lo menos el registro lr.
El código que hemos encontrado es el siguiente:
Código:
push {lr}
mov r2, r0
b 08008DAC
add r2, #1 (08008DAA)
ldrb r0, [r2] (08008DAC)
cmp r0, #0xFF
bne 08008DAA
mov r0, r2
bl 08008D84 ← la función de copiar que hemos encontrado
pop {r1}
Código:
r0=pointer de algún texto
r1=pointer de nuestro objeto
while (byte leido sea desigual a 0xFF) {
incrementar pointer de r0 en 1
}
llamar a función de copiar con r0=destino y r1=pointer del texto (esto lo sabemos porque hemos analizado los parámetros de esa función).
El registro r1, que es el offset del nombre de nuestro objeto no ha sido modificado, eso quiere decir que todavía no hemos encontrado la función que modifica ese r1 y lo convierte en el offset del nombre de nuestro objeto. Por lo tanto, toca volver a hacer el proceso de antes. Primero hacemos un bt (offset de inicio de esta nueva función) y vemos cual ha sido la instrucion ejecutada anterior.
Asegurad que r1 contenga el offset del nombre de nuestro objeto, no lo olvidéis.
Como antes el debugger no es capaz de leer lo que pone en la instrucción ejecutada previamente, eso quiere decir que la instrucción antes ejecutada es la anterior a esa, es decir la que está en el offset: offset de la instrucción ilegible menos dos.
Como en este caso la instrucción previa ha sido “blh $08a0” que está en el offset 08108598 la instrucción previa ejecutada realmente está en el offset 08108596 (es decir, dos menos).
Ahora iremos en el desensamblador al offset 08108596 y subiremos un poco hasta encontrar un push que pushee como mínimo el lr. Ese comando está en el offset 08108560 y la función es la siguiente:
Código:
push {r4,r5,lr}
mov r4, r0
lsl r1, r1, #0x10
lsr r1, r1, #0x10
ldr r0, =0xFE940000
add r1, r1, r0
lsr r1, r1, #0x10
cmp r1, #1
bhi label
ldr r1, =A489
mov r0, r4
bl 08008D84 ← función de copiar textos
b label
algunos pointer
label:
ldr r1, =08008D84 ← función de copiar textos
mov r0, r5
bl 0809A8BC ← función desconocida
mov r1, r0
mov r0, r4
bl 08008DA4 ← aquí es donde el juego se a detenido (segunda función, antes mencionada)
pop {r4,r5}
pop{r0}
bx r0
¿Recordáis cuando hablamos sobre los parámetros de las funciones? Lo normal es que estos sean los primeros cuatro registros bajos. Si hay más de cuatro para metros es otro tema. Bien, cuando una función devuelve valores muchas veces estos sirven como parámetros para otras funciones, almacenadas en r0-r3. Siempre en orden consecutivo, empezando desde el cero.
Como podréis observar al fondo de la función he marcado donde el debugger se ha parado. Esa función es la que unía palabras, como recordareis, donde r0=destino y r1=origen. Obviamente el origen es el pointer al nombre del texto. Pero un poco más arriba hay una línea que pone mov r1, r0. Eso quiere decir que la función desconocida devuelve en r0 el pointer del texto, y antes de que se ejecute esa función no había ni rastro del offset de nuestro texto, ergo, esa función mediante algún parámetro devuelve el offset del nombre del objeto! Eso quiere decir que ya hemos dado con la función a editar.
Analizar el sitio que vamos a editar
La función desconocida es la siguiente:
Código:
parámetros=???
main:
push {lr}
lsl r0, r0, #0x10
lsr r0, r0, #0x10
bl 0x809A8A9
lsl, r0, r0, #0x10
lsl r0, r0, #0x10
mov r1, #0x2C
mul r0, r0, r1
ldr r1, =0x83DB028 (inicio de la tabla de objetos)
add r0, r0, r1
pop {r1}
bx r1
devuelve el pointer del nombre del objeto
Entonces en el debugger pondremos:
Código:
bt 0809A8A4
Una vez que el juego se detenga en el offset indicado repararemos en los registros.
Código:
r0=0000000F
r1=081086CD
r2=00000F20
r3=08108655
Ahora tratemos de entender que es lo que hace esta función, para empezar tenemos esto:
Código:
push {lr}
lsl r0, r0, #0x10
lsr r0, r0, #0x10
bl 0x809A8A4
lsl r0, r0, #0x10
lsr r0, r0, #0x10
Ahora veremos que es lo que hace la función que llama el bl.
Código:
main:
push {lr}
lsl r0, r0, #0x10
lsr r0, r0, #0x10
mov r0, #0xBB
lsl r0, r0, #0x1
cmp r1, r0
bhi return_zero
add r0, r1, #0x0
b end
return_zero:
mov r0, #0x0
end:
pop{r1}
bx r1
Volviendo al tema, nuestra función solo usa el registro r0 y los demás no, es decir, solo necesitamos el ID del objeto.
Ahora continuaremos entendiendo la función:
Código:
r0=ID del objeto
main:
push {lr}
lsl r0, r0, #0x10
lsr r0, r0, #0x10
bl 0x809A8A9
lsl, r0, r0, #0x10
lsl r0, r0, #0x10 //ID del objeto
mov r1, #0x2C //tamaño de cada elemento de la tabla
mul r0, r0, r1
ldr r1, =0x83DB028 //inicio de la tabla de objetos
add r0, r0, r1 //offset de nuestro objeto
pop {r1}
bx r1
devuelve el pointer del nombre del objeto
Editando la rutina
Bien, siempre que vayamos a editar una rutina necesitaremos 8 bytes, siempre, y hay dos formas de hacerlo.
Primero método:
Código:
ldr rX, =(offset de nuestra propia rutina)
bx rX
Código:
ldr rX, =(offset de nuestra propia rutina)
mov pc, rX
Nuestro algoritmo trata de ir al offset del nombre del objeto y leer el primero byte para comprobar si es un 0xFF. Por lo tanto, nuestra pequeña modificación debe ser ua vez que ya tengamos el offset, pero debe ser dentro de esa función, recordad.
Una cosa muy importante es que onde vayamos a poner lo de ldr rX, offset… debe empezar en un offset acabado en 0, 4, 8 o C, si o si. Si esto no fuera necesario podríamos editar simplemente las últimas líneas:
Código:
ldr r1, =83DB028
add r0, r0, r1
pop {r1}
bx r1
Para poder hacer esto necesitamos seleccionar un registro seguro. Recordad qe necesitamos cargar en rX el offset de nuestra rutina. r0=ID y r1=0x2C, pero en r2 no ha nada almacenado ni se usa para nada, por lo tanto podemos usarlo para nuestro propósito.
Ahora haced esta rutina y compiladla:
Código:
.text
.align 2
.thumb
main:
ldr r2, =0x08[offset de nuestra rutina]+1)
bx r2
.align 2
Es hora de hacer nuestra propia rutina, pero recordad que lo primero será poner los comandos que hemos borrado de la función en nuestra rutina.
Escribiendo la rutina
Lo dicho, lo primero será escribir los comandos que nos hemos comido antes:
Código:
.text
.align 2
.thumb
main:
//aquí va la parte que antes hemos omitido
//omitimos el pop, porque no queremos que la rutina cabe todavía
mul r0, r0, r1
ldr r1, =0x83DB024
add r0, r0, r1 //r0=pointer del nombre
//ahora hay que leer el primero byte y compararlo con 0xFF
ldrb r1, [r0]
cmp r1, #0xFF
bne end //si no es 0xFF usar el pointer original
readpointer:
//aquí vamos a leer el pointer
end:
pop {r1}
bx r1
Pero nosotros usamos un 0xFF para dar a entender que había que leer un pointer, por lo tanto en ese offset de la ROM tendremos FF XX XX XX XX, las equises son un pointer.
Veréis que el pointer no empieza en ningún offset que acabe en 0, 4, 8 o C. Pero como cada nombre ocupa 14 bytes simplemente usaremos tres bytes de relleno después del 0xFF y después pondremos el pointer, para que este en un offset que acabe en 0, 4, 8 o C. Tal que así: FF 00 00 00 XX XX XX XX.
Ahora podemos usar el ldr sin problemas siguiendo la rutina:
Código:
.text
.align 2
.thumb
main:
//aquí va la parte que antes hemos omitido
//omitimos el pop, porque no queremos que la rutina cabe todavía
mul r0, r0, r1
ldr r1, =0x83DB024
add r0, r0, r1 //r0=pointer del nombre
//ahora hay que leer el primero byte y compararlo con 0xFF
ldrb r1, [r0]
cmp r1, #0xFF
bne end //si no es 0xFF usar el pointer original
readpointer:
ldr r0, [r0, #0x4]
end:
pop {r1}
bx r1
Código:
.text
.align 2
.thumb
main:
mul r0, r0, r1
ldr r1, =(0x83DB024)
add r0, r0, r1
ldrb r1, [r0]
cmp r1, #0xFF
bne end
readpointer:
ldr r0, [r0, #0x4]
end:
pop {r1}
bx r1
.align 2
Quiero agradecer a @Kate, AliKate y a @CompuMax por ayudarme en este tuto, al primero por resolverme mil y una duda con una paciencia infinita y al segundo por todos los errores que me ha reportado y he podido arreglar.
Nada chicos, espero que os haya servido y que os haya animado a aprender ASM, que no es tan complicado después de todo
Última edición: