.
.
Conocimientos previos
- Sistemas de numeración binario y hexadecimal
- Operaciones a nivel de bit (and, or, xor, desplazamiento/rotación de bits)
- Conceptos muy básicos sobre memorias y direccionamiento: bit, byte, word, capacidad, memoria ROM, memoria RAM, banco, puntero...
Introducción
El porqué de saber lenguaje ensamblador para, en nuestro caso concreto, hackear juegos de Pokemon en GB/C, es muy simple. Dichos juegos están escritos en este lenguaje. La pieza clave de la Game Boy es su CPU o procesador, y el lenguaje ensamblador o de forma abreviada, asm, es un lenguaje de bajo nivel empleado por (micro)procesadores. Al contrario de los lenguajes de alto (o medio) nivel como C o Java, un lenguaje de bajo nivel es mas cercano al lenguaje maquina entendido por el hardware pero mas alejado de la lógica empleada por el ser humano.
Evidentemente, a la hora de hackear, hay muchísimas posibilidades sin necesidad de saber programar asm, pero la realidad es que éste, inevitablemente, se encuentra detrás de todo. Los scripts, por ejemplo, no son mas que una forma abreviada de ejecutar una serie de código asm asociado a cada script. Todas las estructuras o tablas de Pokemon salvajes, atributos y características de los Pokemon, eventos de un mapa etc. no tendrían ningún sentido sin las rutinas que las definen o utilizan.
Generalidades
Empecemos con una serie de conceptos generales de la CPU de la Game Boy que conviene tener en cuenta a la hora de programar en ensamblador y que nos ayudarán a entender el funcionamiento de la Game Boy con el objetivo de programar de forma mas eficiente. El procesador de la Game Boy es muy similar al procesador Zilog Z80, aunque no dispone de su juego completo de instrucciones. Es también muy similar al Intel 8080, ya que el Z80 surgió a partir de éste. En cualquier caso, el procesador de la Game Boy tiene como nombre oficial Sharp LR35902. Este procesador es de 8 bits, es decir, en cada dirección de memoria hay almacenados 8 bits o 1 byte. Por otro lado, el procesador es capaz de direccionar hasta 64KB (2^16 bytes) de memoria , al igual que el Z80. Esto significa que podemos direccionar la memoria RAM mediante 16 bits o, lo que es lo mismo, 2 bytes (0000-FFFF). La Game Boy trabaja aproximadamente a una frecuencia de 4.2 MHz de ciclo de reloj (aunque la Game Boy Color podría trabajar al doble de frecuencia, a costa de mayor consumo) lo que significa que las instrucciones mas elementales (de 4 ciclos de reloj o 1 ciclo de maquina) tardan alrededor de un microsegundo en ejecutarse.
Capacidad y Mapa de Memoria
La Game Boy es compatible con cartuchos de varios tamaños de memorias ROM. En los casos que nos interesan a nosotros mas concrétamente, Pokemon Red, Blue y Yellow utilizan 1MB (2^20 bytes) de ROM, mientras que Pokemon Gold, Silver y Crystal emplean 2MB (2^21 bytes). Como tamaños de ROM tan grandes no pueden ser mapeados por el procesador (como vimos, puede direccionar hasta 64KB), la memoria ROM de la Game Boy esta paginada en bancos de 16kB, lo que nos da un total de 128 bancos en GSC y 64 bancos en RBY.
La memoria ROM es, como su nobre indica, una memoria de solo lectura y por tanto no puede ser modificada durante la ejecución del programa (del juego). La memoría que se estará manipulando será la RAM. Sin embargo, la memoria ROM es, a fin de cuentas, la información el juego en sí, y por tanto contendrá las instrucciones, codificadas, que se tienen que ejecutar en cada momento. Para poder acceder a dichas instrucciones de la memoria ROM del cartucho desde la memoria RAM de la Game Boy, existe espacio en está última donde se encuentran, mapeados, dos bancos de la ROM, que serán los que podrán ser accedidos en un determinado momento. Veamos primero el mapa de la memoria RAM de la Game Boy para poder detallar cada parte a continuación:
Para ejecutar instrucciones contenidas en un determinado banco de la ROM, debemos cargar este en RAM:4000-7FFF (banco de ROM 01..NN conmutable) mediante las instrucciones pertinentes. El control de bancos se hace mediante un dispositivo hardware llamdo Memory Bank Controller (MBC) que se encuentra en el cartucho. El banco 00 (ROM:0000-3FFF) es siempre accesible, ya que se encuentra fijo en RAM:0000-3FFF. El banco 00 contiene, como cabe esperar, las instrucciones necesarias en el arranque del programa, así como rutinas que son mas genéricas o que son accedidas mas frecuentemente.
El resto de direcciones de la memoria RAM se utilizan para almacenar, temporalmente, las diferentes variables y los datos que se usan y manipulan durante el juego. Mayoritariamente nos manejaremos con la WRAM ya que en general contiene la información mas interesante para nosotros; en los casos de los juegos de Pokemon podríamos destacar todos los datos correspondientes a los Pokemon de nuestro equipo, información sobre el tipo de batalla, flags de scripts, información referente a los canales de música, información sobre conexiones de mapas... En la Game Boy Color (CGB) se dispone de un total de 32KB de memoria WRAM, de los cuales 28KB (7 bancos de 4KB) son accedidos en D000-DFFF, mientras que en la Game Boy (GB) se dispone de 8KB. ECHO RAM no es mas que un 'eco' de la WRAM. Es decir, cualquier evento que ocurra sobre una dirección entre [C000-DDFF] también ocurrirá sobre [C000-DDFF]+2000 y viceversa. El área correspondiente a la RAM externa o SRAM (switchable RAM, RAM conmutable) está reservada para memoria RAM externa a la Game Boy localizada dentro de cada cartucho. Esta memoria esta mantenida por la batería del cartucho y está mayoritariamente relacionada con datos de guardado. La VRAM, como su nombre indica, contiene información gráfica que se mostrará en pantalla (tiles); en CGB esta memoria es de 16KB (esta dividida en dos bancos de 8KB), mientras que en GB es de 8KB. La OAM guarda relación con la parte gráfica correspondiente a datos de sprites. La HRAM contiene la información de mayor prioridad; por ejemplo, el buffer de datos, información sobre las teclas presionadas, contadores de tiempo, o valores empleados para la generación de numeros aleatorios. Por último, entre los puertos o registros de entrada y salida (I/O Ports) podemos encontrar registros de control del sonido, entrada de datos a traves del joypad, temporizadores, datos para transferir via cable link, flags de interrupciones...
Flujo del Programa
La dirección de inicio del programa de un cartucho de Game Boy es 0100, es decir, que éste empieza a ejecutarse en la dirección 0100 de la memoria. Desde de este punto de inicio correspondiente a las direcciones 0100-0103, se ejecuta un salto a una dirección mayor o igual que 0150 (puede variar dependiendo de que juego se trate), donde se empezará a ejecutar el programa en sí. Las direcciones 0104-014F no contienen instrucciones, sino que conforman la cabecera del cartucho, la cual debe ser comprobada inmediatamente después de encender la Game Boy (mientras se está mostrando el logo de Nintendo en pantalla). De darse algún tipo de error durante la comprobación, la Game Boy dejará de operar y no dará paso a la ejecución del programa del cartucho (programa que comienza el la previamente mencionada dirección 0100). La cabecera contiene información interna sobre el cartucho, como su nombre, si es compatible con Game Boy y/o Game Boy Color (CGB), los tamaños de ROM y RAM, y el checksum (suma de comprobación de errores), así como el gráfico correspondiente al logo de Nintendo.
Contador de Programa
La propia definición de programa implica la existencia de una secuencia de instrucciones. El procesador va ejecutando las instrucciones una tras otra según su dirección en memoria. Para ello, éste dispone de un registro llamado Contador de Programa (Program Counter, PC) que apunta, en cada momento, a la instrucción que se debe de tomar. Una vez que la instrucción ha sido tomada, se incrementa el contador de programa para apuntar a la siguiente instrucción, y se ejecuta la instrucción que había sido tomada. Entonces se procede a tomar la instrucción indicada por el recién incrementado PC, y así sucesivamente. El registro PC ocupa dos bytes, de forma que es posible de cubrir todo el rango de direcciones de la memoria RAM. En nuestro caso concreto de la Game Boy, este registro solo deberá tomar valores entre 0000 y 7FFF, ya que fuera del área de direcciones donde se encuentra mapeada la ROM (es decir, en 8000-FFFF), no existen instrucciones, sino datos almacenados.
Saltos y Llamadas
La única forma de romper el flujo secuencial en el que se ejecutan las instrucciones es mediante instrucciones de salto o de llamada, o como consecuencia de interrupciones. Las instrucciones de salto se emplearán cuando se requiera ejecutar instrucciones que se encuentran en una dirección diferente de la memoria. Este salto supondrá un cambio en el contador de programa a modo de que este apunte a la nueva dirección.
Por otro lado, se encuentran las instrucciones de llamada. La diferencia que caracteriza a ambas es el hecho de que una llamada supondrá el retorno a la secuencia que se estaba ejecutando cuando se realizó la llamada. Una vez efectuada la llamada, el programa realizará las instrucciones correspondientes a la subrutina que fue llamada, hasta que encuentre una instrucción de retorno que le haga volver a la dirección que indicaba el registro Program Counter en el momento en que se ejecutó la llamada.
Memoria de Pila y Puntero de Pila
Las llamadas introducen la necesidad de una memoria adicional para almacenar la dirección de retorno. A esta memoria se la denomina Pila o Stack. Por otro lado, hay que tener en cuenta que, en caso de que la llamada a subrutina tenga lugar dentro de otra subrutina (es decir, de forma anidada), sería necesario que el programa "recuerde" al menos dos direcciones de retorno; una por cada subrutina que fue llamada de forma anidada. Es por ello que la pila debe tener capacidad para almacenar mas de una dirección. En concreto, en la Game Boy, esta memoria es de 256 bytes y se encuentra en las primeras posiciones de la WRAM, entre C000 y C0FF. Hay que tener en cuenta también que cada dirección almacenada en la pila va a ocupar 2 bytes, al igual que el registro PC.
Cuando se ejecuta una instrucción de llamada, la dirección de retorno (es decir, el valor del registro PC) se almacena en la pila, mientras que cuando se ejecuta una instrucción de retorno, se toma la dirección almacenada en la pila para actualizar el contador de programa. Lo que hace posible controlar el almacenamiento de mas de un valor en la pila es el registro llamado Puntero de Pila o Stack Pointer (SP). En cada momento, el puntero de pila tendrá un determinado valor en función de las operaciones realizadas previamente sobre la pila. El procedimiento a la hora de introducir o tomar valores en la pila es el siguiente: cuando se almacena un valor en la pila (llamada), se decrementa el Stack Pointer y, a continuación, se almacena el valor en la dirección apuntada por el SP. Cuando la operación que se va a realizar es la toma de un valor (retorno), primero tomamos el contenido de la dirección apuntada por el SP, y luego incrementamos éste. Este tipo de funcionamiento permite el manejo de subrutinas anidadas, ya que el SP apuntará a la dirección de retorno de la última subrutina que fue llamada, que será la primera de la que retornemos.
Interrupciones
Por último, nos queda hablar sobre las interrupciones. A grandes rasgos, podemos dividir éstas entre interrupciones externas (por hardware) e interrupciones internas (por software). Una interrupción externa, o simplemente, interrupción, ocurre cuando un dispositivo hardware que esta conectado al procesador envía una señal a éste para indicarle que se requiere su atención. En el caso de la Game Boy, un evento interruptor puede ser, por ejemplo, la pulsación de una tecla a traves del joypad, el envío de información a través del Cable Link, o la finalización del refrescado de la última línea de la pantalla (interrupción VBlank). Cuando tiene lugar una interrupción, el procesador debe interrumpir el curso actual del programa para ejecutar la correspondiente rutina de atención a interrupción, que contendrá instrucciones especificas a ejecutar en dicha situación. Mientras se está ejecutando una interrupción, el resto de las interrupciones permanecen deshabilitadas por defecto. En la Game Boy, las interrupciones están organizadas en vectores de interrupción, que se encuentran en el rango de direcciones 0040-0060; cuando ocurre una interrupción, se llama a su vector de interrupción correspondiente, que consiste en una instrucción de salto a otra dirección donde se encuentra la rutina de atención en sí. Una vez que la rutina de atención a concluido, se retornará al estado previó a la interrupción mediante una instrucción especial de retorno desde interrupción que vuelva a habilitar las interrupciones.
Las interrupciones internas, al contrario que las externas, no son originadas por dispositivos hardware, si no que son originadas por código del propio programa y pueden ser llamadas por éste. Este tipo de interrupciones se encuentran organizadas en vectores de reset en el rango de direcciones 0000-0038. Éstas no son interrupciones propiamente dichas, ya que funcionan de una forma muy similar a las instrucciones de llamada y no es necesario retornar con una instrucción especial de retorno desde interrupción. Mediante estos vectores de reset podemos, por ejemplo, realizar un cambio de banco (rst 10), o hacer un salto (a modo de llamada) a cualquier dirección de cualquier banco (rst 8). El vector de reset de la dirección 0000 (rst 0) es el que se encarga en un primer momento de saltar a la dirección 0100 de inicio.
Registros Generales y Salvado de Registros
Como hemos visto, un procesador ejecuta instrucciones. Estas instrucciones actuan sobre una serie de registros, es decir, sobre una serie de pequeñas memorias de almacenamiento del propio procesador que en su conjunto definen el estado actual del programa. Dos de estos registros ya los hemos visto: el contador de programa, el cual indica la dirección de memoria en la que nos encontramos y puede ser modificado directamente mediante saltos o llamadas, y el puntero de pila, el cual se ve modificado al introducir o tomar un valor de la pila. Sin embargo, existe una serie de registros adicionales llamados registros generales que, como su nombre indica, son registros de propósito general que nos permiten manipular y trabajar con las diferentes direcciones y datos de la memoria. Estos registros de 8 bits son: A (acumulador), F (flags), H (high), L (low), B, C, D, E.
Los Registros Generales
El registro A es el registro principal a la hora de realizar operaciones de 1 byte, tanto aritméticas como lógicas. En general, es el registro principal de trabajo. El registro F no es un registro que puede ser manipulado directamente. Ciertas instrucciones pueden afectar a los diferentes bits de este registro en función del resultado. El registro F es, por tanto, un registro de estado cuyos bits funcionan como banderas o flags. Por ejemplo, una instrucción de suma o resta pondrá el bit o flag Z (Zero) del registro F a 1 si el resultado de la operación fue cero y lo pondrá a 0 si el resultado fue distinto de cero. Esa misma instrucción también actualizará el flag C (Carry) de F; lo pondra a 1 si hubo desbordamiento y a 0 si no lo hubo). Sin embargo, una instrucción de llamada no actualizará ninguno de estos dos flags y mantendrá sus valores actuales. El registro F se puede utilizar para realizar saltos condicionales o bifurcaciones; por ejemplo, realizar un salto solo si Z es 1.
Agrupación en Pares
Los seis registros restantes se pueden utilizar tanto individualmente como en forma de pares de registros. Agrupar los registros en pares nos permite doblar su capacidad hasta 16 bits, 8 por cada registro. Estos registros no pueden ser agrupados de cualquier manera, ya que solo existen instrucciones para BC, DE, y HL. B, D y H (high) harían referencia a los 8 bits mas significativos, mientras que C, E y L (low), contendrían los 8 bits menos significativos. El hecho de que podamos almacenar valores de 2 bytes supone que las funciones principales de los pares de registros son trabajar con direcciones (que, como vimos, ocupan 16 bits), y realizar operaciones con números de 2 bytes. A la hora de trabajar con direcciones, el par principal es el HL, ya que se le asocia un mayor número de instrucciones. Un ejemplo de uso de estos pares de registros puede ser cargar una dirección de 16 bits en HL, añadirle BC o DE, y cargar el contenido de la nueva dirección indicada por HL (la dirección ocupa 16 bits, pero su contenido ocupa un único byte) en A. Sin embargo, también es posible tratar con estos seis registros de forma individual, ya sea orientado a trabajar con direcciones o no. Por ejemplo, podemos cargar el valor de A en L a modo de que A se convierta en la parte menos significativa de una dirección, o emplear el registro B para hacer un AND de A y B.
Salvado de registros
En cada instante, cada uno de los diez registros hasta ahora mencionados tendrá un valor diferente, y el funcionamiento del programa variará en función de ello. Si en cualquier momento uno o mas de los registros fuese modificado aleatoriamente, sería muy probable que el programa dejase de funcionar como debería. Según vimos antes, una interrupción puede ocurrir en cualquier momento y ésta debe ser atendida inmediatamente. Como cualquier otra rutina, una rutina de atención a interrupción tendrá que manejarse con los registros generales para hacer lo que tenga que hacer. Por definición, retornar desde la rutina de interrupción supondrá que los registros PC y SP retomen sus valores originales, pero los ocho registros generales tendrán valores diferentes en función del código de la rutina de interrupción. Esto evidentemente no es bueno; si, por ejemplo, en el instante anterior de que saltase la interrupción se había cargado una dirección en HL a la que el programa debía saltar a continuación, y la rutina de atención a interrupción modificó el valor del registro HL para sus propósitos, la dirección a la que se saltará será otra distinta. O un ejemplo mas sencillo de ver en nuestro caso; si en el registro A se acaba de cargar el movimiento que va usar un Pokemon en batalla y una interrupción modifica dicho registro, el Pokemon hará un movimiento totalmente distinto! Si no hacemos nada para evitar esto, teniendo en cuenta ocurren interrupciones en intervalos de tiempo incluso menores al milisegundo y que estas suelen modificar todos o casi todos los registros, el resultado será desastroso. Es por ello que se hace necesario el salvado de registros en estos casos, con el objetivo de, tras una interrupción, retomar los valores que tenían los registros justo antes de entrar a la misma.
El salvado de registros se hace mediante unas instrucciones especificas y se realiza a través de la pila. La pila, como vimos, almacena valores de 16 bits, por lo que los registros deben ser salvados en pares. Además de las tres agrupaciones que vimos antes (BC, DE, y HL), el registro A debe agruparse con el registro F (formando AF) para ser guardado en la pila. El guardado y toma de registros en el stack funciona de forma similar al guardado y toma de direcciones durante las instrucciones de llamada. Previo a almacenar el valor de un registro para ser guardado, el puntero de pila es decrementado, mientras que, inmediatamente después de recuperar el valor de un registro, el stack pointer se incrementará. Es por ello que los registros deben ser recuperados en el orden opuesto al que fueron introducidos en la pila, ya que último valor que introducimos en la pila es el primero que sale.
El concepto de salvado de registros puede extenderse mas allá de las interrupciones. Por ejemplo, es muy útil introducir las instrucciones de salvado y recuperación de registros pertinentes en ciertas rutinas. De ese modo, no importa los efectos que dicha subrutina tenga sobre los registros, ya que al volver de ésta a la rutina llamante, los registros habrán recuperado los valores que tenían en el momento que se ejecutó la llamada. Evidentemente, el empleo del salvado de registros dependerá de la función de la rutina; si se trata de una rutina cuya función es multiplicar dos números y almacenar el resultado en BC, hacer un salvado del registro BC sería como no haber llamado a dicha rutina. Sin embargo, si se trata de una rutina que imprime un cuadro de texto en la pantalla, entonces el salvado de registros si que sería util siempre y cuando alguna de las rutinas que llaman a dicha (sub)rutina lo requieran (de lo contrario sería redundante). Para poder manejar en la pila direcciones de retorno junto con registros, es necesario hacer las cosas en orden. Si, por ejemplo, hacemos un guardado de registro seguido de una llamada a subrutina, debemos retornar de la rutina antes de recuperar el registro. Si estas acciones se hubiesen realizado en el orden contrario, el resultado habría sido cargar la dirección de retorno en el registro, y retornar a la dirección indicada por el registro en el momento de salvarlo (que ni siquiera tiene por que almacenar una dirección).
Las Instrucciones
En general, en lugar de codificar directamente las instrucciones en lenguaje máquina, la forma de programar en lenguaje ensamblador es mediante el uso de mnemónicos. Estos son palabras que sustituyen a los op-codes o códigos de operación a los que serán posteriormente traducidos para poder ser entendidos por el procesador. El objetivo de los mnemónicos es facilitar la programación; por ejemplo, es mas fácil recordar que la instrucción de retorno tiene como mnemonico ret, que recordar que su op-code es C9 en hexadecimal o 11001001 en binario. A continuación, vamos a ver cuales son las instrucciones mas importantes que nos ofrece la CPU de la Gameboy. Usaremos la siguiente nomenclatura:
Instrucciones de carga
Este tipo de instrucciones nos permiten la carga de un valor o de un registro o par de registros, en otro registro o par de registros. Ninguna de estas instrucciones tiene efecto sobre las flags del registro F. Existen un total de 85 instrucciones de este tipo, pero evidentemente no hace falta ver todas para entenderlas! Empezemos con unas sencillas:
ld a,nn ; cargar el valor nn en a
ld a,d ; cargar el valor del registro d en a
ld h,a ; cargar el valor del registro a en h
ld bc,nnnn ; cargar el valor nnnn en bc
ld a,$27 ; a = 27
ld b,a ; a = 27 ; b = 27
ld hl,$734A ; hl = 734A ; h = 73 ; l = 4A
ld b,h ; b = 73
ld c,l ; c = 4A ; bc = 734A
Un poco mas complicado:
ld (hl),nn ; cargar el valor nn en el contenido de la dirección de memoria hl
ld (de),a ; cargar el valor del registro a en el contenido de la dirección de memoria de
ld a,(de) ; cargar el contenido de la dirección de memoria de en a
ld (nnnn),a ; cargar el valor del registro a en la dirección de memoria nnnn
ld a,$D5 ; a = D5
ld hl,$B21C ; hl = B21C
ld (hl),a ; la dirección de memoria B21C pasa a contener D5
ld a,$D5 ; a = D5
ld ($B21C),a ; la dirección de memoria B21C pasa a contener D5
ld hl,$B21C ; hl = B21C
ld (hl),$D5 ; la dirección de memoria B21C pasa a contener D5
ld d,(hl) ; d = D5
No existen instrucciones para todas las combinaciones de registros. Como se dijo anteriormente, los registros principales son el A y el par HL; por ejemplo es posible hacer ld (de),a y ld (hl),b, pero no es posible hacer ld (de),b.
Por último vamos a ver las cuatro instrucciones existentes que ademas de funcionar como instrucciones de carga también afectan al registro HL, incrementándolo o decrementándolo:
ldi (hl),a ; cargar el valor del registro a en el contenido de la dirección de memoria hl, y, a continuación, incrementar hl
ldd (hl),a ; cargar el valor del registro a en el contenido de la dirección de memoria hl, y, a continuación, decrementar hl
ldi a,(hl) ; cargar el contenido de la dirección de memoria hl en a, y, a continuación, incrementar hl
ldd a,(hl),a ; cargar el contenido de la dirección de memoria hl en a, y, a continuación, decrementar hl
ld a,$79 ; a = 79
ld hl,$D4B6 ; hl = D4B6
ldi (hl),a ; la dirección de memoria D4B6 pasa a contener 79 ; a continuación, hl = D4B7
ld (hl),a ; la dirección de memoria D4B7 pasa a contener 79 ; hl sigue siendo D4B7
Instrucciones de incremento y decremento
Estas sencillas instrucciones aritméticas nos permiten incrementar o decrementar una unidad el valor de un registro o par de registros, o el contenido de la dirección apuntada por el par de registros HL. Este tipo de instrucciones, excepto las que actuan sobre un par de registros, actualizan la flag Z (Zero) del registro F correspondientemente. (Nota: aunque en este y en muchos otros casos hay mas flags afectadas, solo van a ser comentadas la flag Z y la flag C (Carry) por ser, con diferencia, las mas importantes, y más aún para nosotros en particular. En los diferentes ejemplos, se considerará el estado de estas flags tan solo en los casos que sean de especial interés para ello.)
inc a ; incrementar el valor del registro a
dec c ; decrementar el valor del registro c
inc de ; incrementar el valor del par de registros de
dec (hl) ; decrementar el contenido de la dirección de memoria hl
ld d,$FE ; d = FE ; Z = ?
inc d ; d = FF ; Z = 0
inc d ; d = 00 ; Z = 1
ld h,$01 ; d = 00 ; h = 01 ; Z = 1
dec d ; d = FF ; h = 01 ; Z = 0
dec h ; d = FF ; h = 00 ; Z = 1
ld bc,$3A74 ; b = 3A ; c = 74 ; bc = 3A74
inc bc ; b = 3A ; c = 75 ; bc = 3A75
inc c ; b = 3A ; c = 76 ; bc = 3A76
inc b ; b = 3B ; c = 76 ; bc = 3B76
ld hl,$58FF ; h = 58 ; l = FF ; hl = 58FF ; Z = ?
inc hl ; h = 59 ; l = 00 ; hl = 5900 ; Z = ?
ld hl,$58FF ; h = 58 ; l = FF ; hl = 58FF ; Z = ?
inc l ; h = 58 ; l = 00 ; hl = 5800 ; Z = 1
Instrucciones de suma y resta
Como su nombre indica, estas instrucciones nos permiten realizar las dos funciones matemáticas mas elementales: la suma y la resta. Disponemos de instrucciones que pueden realizar sumas de 8 bits o de 16 bits (a través de un par de registros), pero solo podremos hacer restas de números de 8 bits directamente. Como todos sabemos, las sumas y las restas requieren dos parámetros (dos números) para tener sentido. El juego de instrucciones de la Game Boy nos limita a que uno de esos parámetros sea el registro A para operaciones de 8 bits, o el registro HL para sumas de 16 bits., y, además, dichos registros serán siempre el origen y el destino de estas operaciones. Por otro lado, las instrucciones de suma y resta actualizan las flags Z y C correspondientemente, con la excepción de que las sumas de 16 bits, que no actualizan la flag Z.
add c ; realizar la operación a + c y guardar el resultado en el registro a
sub b ; realizar la operación a - b y guardar el resultado en el registro a
add nn ; realizar la operación a + nn y guardar el resultado en el registro a
sub (hl) ; realizar la operación a - (hl) y guardar el resultado en el registro a
add hl,de ; realizar la operación hl + de y guardar el resultado en el par de registros hl
ld a,$10 ; a = 10
ld b,$25 ; b = 25
ld c,$15 ; c = 15
add b ; a = 35 ; b = 25
sub c ; a = 20 ; c = 15
ld a,$03 ; a = 3
ld hl,$5000 ; hl = 5000
ld bc,$027E ; bc = 275
add $BC ; a = BF ; hl = 5000 ; bc = 27E
add hl,bc ; a = BF ; hl = 527E ; bc = 27E
ld a,$F0 ; a = F0 ; Z = ? ; C = ?
ld b,$08 ; a = F0 ; b = 8 ; Z = ? ; C = ?
add b ; a = F8 ; Z = 0 ; C = 0
add b ; a = 00 ; Z = 1 ; C = 1
sub $01 ; a = FF ; Z = 0 ; C = 1
sub b ; a = F7 ; Z = 0 ; C = 0
add $10 ; a = 07 ; Z = 0 ; C = 1
Instrucciones AND, OR, XOR
Estas instrucciones son permiten realizar las
operaciones lógicas a nivel de bit AND, OR y XOR entre el registro a y un registro o valor de 8 bits. La flag Z se verá afectada por estas instrucciones.
and c ; realizar un AND entre a y c
or nn ; realizar un OR entre a y nn
xor (hl) ; realizar un XOR entre a y (hl)
ld b,$00 ; a = ?? ; b = 00000000b = 00h ; Z = ?
ld c,$FF ; a = ?? ; c = 11111111b = FFh ; Z = ?
and b ; a = 00000000b = 00h ; Z = 1
or c ; a = 11111111b = FFh ; Z = 0
ld a,%11100011 ; a = E3h
and %00111110 ; a = 00100010b = 22h
or %00000111 ; a = 00100111b = 27h
and %11011111 ; a = 00000111b = 07h
xor %11001100 ; a = 11001011b = CBh
A través de las operaciones lógicas es posible, mediante una instrucción mas rápida y corta que una istrucción de carga, cargar 00 en el registro a mediante la instrucción xor a. Por otro lado, las instrucciones and a y or a indistintamente, nos permiten actualizar la flag Z según el valor actual del registro a sin que (el valor de) ningún registro se vea afectado. La propia definición de XOR implica que realizar la operación XOR de un número con si mismo da un resultado de 0, mientras que realizar un AND o un OR de un número con si mismo da como resultado el propio número.
ld a,$8B ; a = 8B ; Z = ?
and a ; a = 8B ; Z = 0
xor a ; a = 00 ; Z = 1
ld b,$44 ; a = 00 ; b = 44 ; Z = 1
inc b ; a = 00 ; b = 45 ; Z = 0
and a ; a = 00 ; Z = 1
Instrucciones de comparación
Estas instrucciones nos permiten comparar el valor del registro A con otro registro o valor de 8 bits. La utilidad de estas instrucciones consiste en que las flags Z y C se ven modificadas en función del resultado de la comparación. Por un lado, la flag Z se pondrá a 1 cuando A coincida con el valor con que se esta comparando, y a 0 en el resto de los casos. Por otro lado, la flag C se pondrá a 1 cuando A sea menor que el valor con que se compara, y a 0 cuando sea mayor o igual. Dicho de otro modo, las instrucciones de comparación consisten en instrucciones de resta que no actualizan el registro A.
cp b ; comparar el registro a con el registro b
cp nn ; comparar el registro a con el valor nn
cp (hl) ; comparar el registro a con el contenido de la dirección de memoria hl
ld a,$37 ; a = 37 ; Z = ? ; C = ?
ld d,a ; a = 37 ; d = 37 ; Z = ? ; C = ?
cp d ; a = 37 ; d = 37 ; Z = 1 ; C = 0
cp $E6 ; a = 37 ; Z = 0 ; C = 1
ld (hl),$1F ; a = 37 ; (hl) = 1F ; Z = 0 ; C = 1
cp (hl) ; a = 37 ; (hl) = 1F ; Z = 0 ; C = 0
Instrucciones de manipulación de bits
Este tipo de instrucciones nos permiten poner a 0 (set), poner a 1 (reset), o comprobar el estado de cualquier bit de un registro o de cualquier bit del contenido de la dirección de memoria apuntada por HL. Las instrucciones de comprobación de bits, pondrán la flag Z a 1 cuando el bit comprobado esté a 0, y viceversa. El resto de instrucciones de manipulación de bits no tienen efecto sobre ninguna flag.
bit 2,b ; comprobar el estado del bit 2 del registro b
res 0,e ; poner el bit 0 (bit menos significativo) del registro e a 0
set 7,(hl) ; poner el bit 7 (bit mas significativo) del contenido de la dirección de memoria hl a 1
ld c,%11110000 ; c = 11110000b = F0h ; Z = ?
bit 6,c ; c = 11110000b = F0h ; Z = 0
bit 0,c ; c = 11110000b = F0h ; Z = 1
set 1,c ; c = 11110010b = F2h ; Z = 1
set 5,c ; c = 11110010b = F2h ; Z = 1
res 7,c ; c = 01110010b = 72h ; Z = 1
Instrucciones de desplazamiento y rotación de bits
Las instrucciones de desplazamiento y rotación de bits son muy similares entre sí y nos permiten mover los bits de un registro o de (HL) hacia la derecha o hacia la izquierda una posición. Aunque hay mas instrucciones de este tipo, veremos las cuatro que nos serán mas útiles. Estas instrucciones afectan tanto a la flag Z como a la flag C. La forma en la que la flag Carry se ve afectada variará en función de la instrucción empleada. En este caso veremos mas en detalle el funcionamiento de estas instrucciones con el objetivo de poder distinguir mejor sus diferencias:
- srl (Shift Right Logical ó desplazamiento lógico hacia la derecha): Se pone el bit 0 en la flag C y 0 en el bit 7.
Ejemplo: srl %11010001 (209 en decimal) coloca 1 en C y da como resultado 01101000b (104 en decimal). Realizar n veces la instrucción srl sobre el mismo registro o valor supone dividirlo entre 2^n, con redondeo hacia abajo.
La instrucción sra (desplazamiento aritmético hacia la derecha), se emplearía también para realizar divisiones entre multiplos de 2, pero cuando el bit mas significativo es el bit de signo. En general, nosotros tan solo necesitaremos utilizar srl.
- sla (Shift Left Arithmetic ó desplazamiento aritmético hacia la izquierda): Se pone el bit 7 en la flag C y 0 en el bit 0.
Ejemplo: sla %01101000 (104 en decimal) coloca 0 en C y da como resultado 11010000b (208 en decimal). Realizar n veces la instrucción sla sobre el mismo registro o valor supone multiplicarlo por 2^n.
La razon por la que en este caso usamos el desplazamiento aritmético en vez del lógico, es que, hacia la izquierda, ambos terminan siendo lo mismo. Es por ello que solo disponemos de la instrucción sla y no de una hipotética sll, ya que sería redundante.
- rr (Rotate Right ó rotación hacia la derecha): Se pone el contenido de la flag C en el bit 7 y el bit 0 en la flag C.
La principal utilidad de esta instrucción es servir de apoyo a la instrucción srl para realizar divisiones (entre 2^n) de numeros de mas de 8 bits, donde la flag C hará las veces de llevada.
- rl (Rotate Left ó rotación hacia la izquierda): Se pone el contenido de la flag C en el bit 0 y el bit 7 en la flag C.
La principal utilidad de esta instrucción es servir de apoyo a la instrucción sla para realizar multiplicaciones (por 2^n) de numeros de mas de 8 bits, donde la flag C hará las veces de llevada.
srl c ; realizar un desplazamiento lógico hacia la derecha de los bits del registro c
rl (hl) ; realizar una rotación hacia la izquierda de los bits del contenido de la dirección apuntada por hl
ld b,%00100000 ; b = 01000000b = 40h ; C = ? ; Z = ?
sla b ; b = 10000000b = 80h ; C = 0 ; Z = 0
sla b ; b = 00000000b = 00h ; C = 1 ; Z = 1
En el siguiente ejemplo vamos a ver como combinar las instrucciones srl y rr para dividir un numero de 2 bytes entre 8 (2^3):
ld bc,820E ; b = 82h ; c = 0Eh
srl b ; b = 41h ; c = 0Eh ; C = 0
rr c ; b = 41h ; c = 07h ; bc = 4107
srl b ; b = 20h ; c = 07h ; C = 1
rr c ; b = 20h ; c = 83h ; bc = 2083
srl b ; b = 10h ; c = 83h ; C = 0
rr c ; b = 10h ; c = 41h ; bc = 1041
Instrucciones de salto
Las instrucciones de salto son las instrucciones que modifican el valor del contador de programa sin afectar a la pila o al puntero de pila. Mediante estas instrucciones podemos pasar a ejecutar instrucciones localizadas en una posición diferente de la memoria. Existen dos tipos de instrucciones de salto: los saltos relativos y los saltos absolutos (o, simplemente, saltos). Los primeros permiten saltar tan solo a direcciones cercanas, mientras que los segundos ofrecen la posibilidad de saltar a cualquier punto de la memoria RAM. Las instrucciones de salto relativo se codifican mediante 2 bytes, mientras que las de salto absoluto requieren 3 bytes y un (ligéramente) mayor tiempo de ejecución.
Las instrucciones de salto relativo consisten en un desplazamiento del contador de programa. Estas emplean el mnemónico jr nn, donde el argumento nn es un número de 8 bits que indica el número de posiciones que el PC debe avanzar o retroceder. Un valor nn entre 0x01 y 0x7F supone un desplazamiento positivo y un valor entre 0x80 y 0xFF supone un desplazamiento negativo. Es este último caso, 0xFF equivale a un desplazamiento de -1 posición, 0xFE a -2 posiciones, y así sucesivamente. El valor 0x00 supondría un desplazamiento nulo. En cualquier caso, siempre hemos de tener en cuenta que una vez que el procesador a leído la instrucción, éste ya ha incrementado el contador de programa antes de ejecutarla. Por tanto, el desplazamiento tenemos que empezar a contarlo desde la posición del primer byte de la instrucción siguiente al salto relativo.
En algunos casos tambien es posible escribir estas instrucciones indicando como argumento la dirección absoluta (de 16 bits) en lugar del desplazamiento, de forma que el propio ensamblador se encargue de "traducir" dicha dirección a desplazamiento relativo a nuestra posición. En cualquier caso, debemos de tener en cuenta que al usar saltos relativos estamos limitados a desplazamientos positivos o negativos de hasta 127 posiciones por ambos lados.
Vamos a ver dos ejemplos básicos:
7F00: jr $03 ; saltar a la posición 7F05
00C0: jr $F7 ; saltar a la posición de memoria 00B9
Y tres instrucciones que nunca debemos de utilizar...
jr $00 ; similar a 2 instrucciones nop, pero mas lento de procesar
jr $FF ; saltar a la dirección donde se encuentra $FF, el cual pasa a ejecutarse como instrucción RST 38
jr $FE ; saltar de vuelta a la misma instrucción, lo que implica un bucle infinito de saltos
En el caso de las instrucciones de salto absoluto se indica diréctamente la dirección de destino de 2 bytes (0x0000 - 0xFFFF). Esto nos permite trasladarnos a cualquier punto del banco 0, del banco de la ROM que esta actualmente cargado en el banco conmutable de la RAM (es decir, del banco desde el cual se realizo el salto), o incluso del resto de direcciones de la memoria RAM entre 0x8000 y 0xFFFF. Sin embargo, estas direcciones no contienen instrucciones, por lo que no tendría ningún sentido saltar a ellas.
31E4: jp $634A ; saltar a la posición 634A (banco nn actual)
76F3: jp $104D ; saltar a la posición 104D (banco 0)
En general, la principal utilidad de las instrucciones de salto la realizan las instrucciones de salto condicional. Gracias a ellas, en función del resultado de instrucciones anteriores podemos decidir si realizar el salto o por el contrario seguir el fujo actual del programa. Los saltos condicionales se basan en la comprobación de las flags Z o C del registro F, las cuales hemos visto como pueden ser actualizadas por otras instrucciones como por ejemplo and, inc o cp.
jr z,$05 ; desplazarse 5 posiciones si la flag Z es 1
jp nz,$5560 ; saltar a la posición 5560 si la flag Z es 0
jp c,$2AED ; satar a la posición 2AED si la flag Z es 1
jr nc,$F9 ; desplazarse -7 posiciones si la flag C es 0
Las instrucciones de salto condicional también nos ofrecen la posibilidad de crear bucles, de forma que se ejecute una serie de instrucciones un número determinado de veces. El ejemplo de bucle mas sencillo consiste en emplear uno de los registros a modo de contador, decrementado éste en cada bucle y terminando cuando dicho registro alcance el valor 0.
ld b,$05 ; cargar en el registro b el número de veces que ha de ejecutarse el bucle
5000:
(...) ; instrucciones dentro del bucle
dec b ; si el registro b pasa a ser 0, Z = 1. En cualquier otro caso, Z = 0
jr nz,$5000 ; saltar al inicio del bucle si Z = 0. No hacer nada si Z = 1 (salir del bucle)
(...) ; instrucciones fuera del bucle
Un ejemplo mas concreto de utilización de bucles puede darse cuando queramos leer un determinado valor de una tabla. Supongamos una tabla con 255 entradas de la que queremos leer el contenido de la posición indicada por el registro B, cuyo valor inicial distinto de 0 es desconocido para la rutina que se encarga de realizar el bucle. Vamos a suponer que cada entrada de la tabla es de tamaño 16 bytes y que la primera de ellas está situada en la posición 2000h.
Para hacer esto, inicialmente hemos de posicionar nuestro "puntero a la tabla" (registro HL) en la dirección de memoria 2000h - 10h = 1FF0h. Hemos de incrementar dicho puntero en 10 unidades al mismo tiempo que decrementamos el registro B, en cada bucle. De esta forma, cuando B alcance 0, nos habremos asegurado que el registro HL apunta a la dirección correcta de la tabla, es decir, a 2000h + (b-1)*10h. Será entonces cuando podamos tomar el contenido de HL. Obsérvese como se haber cargado inicialmente 2000h en lugar de 1FF0h en HL habría resultado en tomar el contenido de la entrada siguiente a la que en realidad estamos buscando.
ld de,$0010 ; cargar en el registro de el tamaño de cada entrada
ld hl,$1ff0 ; apuntar al inicio de la tabla mediante el registro hl
bucle:
add hl,de ; añadir 0010 al valor actual del registro hl
dec b ; si el registro b pasa a ser 0, Z = 1. En cualquier otro caso, Z = 0
jr nz,bucle ; saltar al inicio del bucle si Z = 0. No hacer nada si Z = 1 (salir del bucle)
ld a,(hl) ; guardar el contenido de hl en el registro a para futura utilización
Instrucciones de llamada y retorno
A diferencia de como ocurría con las instrucciones de salto, tras la ejecución de una instrucción de llamada se almacena la dirección de retorno en la pila. Cuando se alcance la instrucción de retorno, el contador de programa retomará el valor que se almacenó en la pila tras realizar la llamada, siempre y cuando no haya tenido lugar otro tipo de operaciones sobre el contenido de la pila o sobre el valor del puntero de pila.
5D80: call $71B1 ; llamar a la rutina situada en la dirección 71B1 (banco nn actual). 5D83 se almacena en la pila y se decrementa el puntero de pila.
71B1: ret ; retornar a la dirección 5D83 (banco nn actual). Se incrementa el puntero de pila.
En la práctica se dan casos de subrutinas anidadas, es decir llamadas a subrutina dentro de una subrutina que ha sido llamada previamente. En estos casos tan solo hemos de tener en mente que la instrucción de retorno debe provocar un retorno a la rutina inmediatamente anterior.
5D80: call $71B1 ; llamar a la rutina situada en la dirección 71B1
71B1: call $6C45 ; llamar a la rutina situada en la dirección 6C45
6C45: ret ; retornar a la dirección 71B4
71B4: ret ; retornar a la dirección 5D83
Al igual que ocurría con las instrucciones de salto, existen instrucciones de llamada condicional. Aunque por lo general se usan con mucha menos frecuencia, nos ofrecen la posibilidad de decidir en que casos ha de llamarse a una determinada subrutina y en cuales no.
call z,$42F1 ; llamar a la rutina 42F1 si la flag Z es 1
call nc,$0796 ; llamar a la rutina 0796 si la flag C es 0
Instrucciones PUSH y POP
El concepto general en el uso de subrutinas consiste en que éstas se encarguen de realizar una tarea especifica para la rutina principal. En muchos casos es útil que las subrutinas sean transparentes para la rutina principal, ya sea parcial o totalmente. Para que las subrutinas realicen su trabajo, estas requieren de la manipulación de los registros generales, que son los mismos que utiliza la rutina llamante. Si el valor de algún registro crucial para la rutina principal se pierde durante las operaciones de una subrutina, el comportamiento del programa se verá afectado.
Gracias a las instrucciones push y pop, podemos almacenar el valor de cualquier par de registros en la pila para recuperas dicho valor en el futuro. Mediante la instruccion push se almacena en la pila el valor del par de registros indicado en el argumento de dicha instrucción, mientras que pop toma el valor de la pila apuntado por el puntero de pila y lo carga en el par de registros indicado en el argumento de la instrucción. El hecho de realizar operaciones sobre la pila conlleva que hemos de tener especial cuidado al combinar estas instrucciones con las instrucciones de llamada y retorno.
push af ; almacenar el contenido del registro af en la pila. Se decrementa el puntero de pila.
pop hl ; cargar el valor de la dirección apuntada por el puntero de pila en hl. Se incrementa el puntero de pila.
Por lo general, las instrucciones push y pop se incluyen dentro del código de la subrutina para que tengan efecto cada vez que ésta sea llamada, e independientemente desde que rutina sean llamadas.
ld a,$73 ; a = 73
ld b,$E6 ; a = 73 ; b = E6
ld e,$41 ; a = 73 ; b = E6 ; e = 41
ld hl,$60C8 ; a = 73 ; b = E6 ; e = 41 ; h = 60 ; l = C8
and a ; a = 73 ; b = E6 ; e = 41 ; h = 60 ; l = C8 ; Z = 0
call subrutina ; a = F0 ; b = E6 ; e = 41 ; h = 60 ; l = C8 ; Z = 0
subrutina: ; a = F0 ; b = E6 ; e = 41 ; h = 60 ; l = C8 ; Z = 0
push hl ; a = F0 ; b = E6 ; e = 41 ; h = 60 ; l = C8 ; Z = 0
push de ; a = F0 ; b = E6 ; e = 41 ; h = 60 ; l = C8 ; Z = 0
push bc ; a = F0 ; b = E6 ; e = 41 ; h = 60 ; l = C8 ; Z = 0
push af ; a = F0 ; b = E6 ; e = 41 ; h = 60 ; l = C8 ; Z = 0
ld b,$3C ; a = F0 ; b = 3C ; e = 41 ; h = 60 ; l = C8 ; Z = 0
xor a ; a = 00 ; b = 3C ; e = 41 ; h = 60 ; l = C8 ; Z = 1
ld hl,$E77D ; a = 00 ; b = 3C ; e = 41 ; h = E7 ; l = 7D ; Z = 1
ld e,$A9 ; a = 00 ; b = 3C ; e = A9 ; h = E7 ; l = 7D ; Z = 1
pop af ; a = F0 ; b = 3C ; e = A9 ; h = E7 ; l = 7D ; Z = 0
pop bc ; a = F0 ; b = E6 ; e = A9 ; h = E7 ; l = 7D ; Z = 0
pop de ; a = F0 ; b = E6 ; e = 41 ; h = E7 ; l = 7D ; Z = 0
pop hl ; a = F0 ; b = E6 ; e = 41 ; h = 60 ; l = C8 ; Z = 0
ret ; a = F0 ; b = E6 ; e = 41 ; h = 60 ; l = C8 ; Z = 0
.
.
Hijoles, justo cuando empezaba a entender un poco el ASM aparece tu tutorial y me saturo mas jaja.
Esta muy bueno el tutorial, muchas cosas las entiendo mejor pero otras tantas me confundo. Definitivamente este no es un tutorial para principiantes, pero vaya que si ayuda mucho.
Espero el momento que haya ejemplos por que si hacen falta para que no sea tan agresivo. Destaco que tienes bien uso dela pedagogia y se agradece.
A mi siempre me ha resultado curioso saber cuantos push y pop se pueden hacer, pero dejaré las preguntas para después.
Gracias por traer algo asi a la comunidad hispana.
He cambiado de idea. Como estoy seguro de que subir todo de golpe sería mucho para aguantarlo, he decidirlo hacerlo poquito a poco. Intentaré subir un poco mas cada día y no todo de una vez así que podréis irlo asimilando poco a poco y comentar cualquier duda respecto a lo que ya está subido cuando queráis. Intentare también manteneros informados a traves de este tema de nuevas actualizaciones (estad atentos al campo de última edicion para ver nuevas actualizaciones).
Como digo al inicio del post principal, la información restante del tutorial está dedicada a la explicación de las intrucciones y a ejemplos de su uso. Hoy, por ejemplo, he añadido las instrucciones de carga y las de decremento / incremento, que son algunas de las mas elementales. Por supuesto, si encontraseis algún error notificarmelo, que es muy probable que alguno haya! Pues en fin, eso es todo :D. Espero que no os parezca muy complicado y que los ejemplos que he añadido ayuden a entender todo mejor!
Añadidas instrucciones de suma y resta, instrucciones AND/OR/XOR, instrucciones de comparación, instrucciones de manipulación de bits, e instrucciones de desplazamiento y rotación de bits.
Por hacer: instrucciones de llamada, instrucciones de salto, e instrucciones PUSH y POP.
Por ultimo, tengo pensado hacer una ultima parte mas orientada a como aplicar esto al hacking, una vez que conocemos las instrucciones principales. Tengo pensado explicar el funcionamiento del debugger de BGB, como crear rutinas o modificar rutinas existentes, y como utilizar los breakpoints para encontrar lo que buscamos.
Digamos que el 50% de lo que explicas ya lo sabia, pero hay mas cosas que no sabia, aunque al estar enfocado al GBC hacking imagino que vale para cualquier juego de GBC, cierto?
si claro, sirve para cualquier juego de gbc o de gb, ya que el procesador de la game boy es el mismo y el juego de instrucciones tambien.
Me he leido la actualizacion y me ha gustado mucho.
Es que bueno que se sienten las bases del ASM hispano con tu guia.
Pro mir parte, algo que no sabia y es bueno a tener en cuenta, son los parametros que no podemos utilzar con los jr:
jr $00 ; similar a 2 instrucciones nop, pero mas lento de procesar
jr $FF ; saltar a la dirección donde se encuentra $FF, el cual pasa a ejecutarse como instrucción RST 38
jr $FE ; saltar de vuelta a la misma instrucción, lo que implica un bucle infinito de saltos
Sigue asi!
Saludos!
Hey compa, me ha ayudado mucho esta guia, es bastante intuitiva, y he aprendido sobre algunas instrucciones que ni sabia para que servian, en fin, gracias por este supermanual, y seguire esperando mas actualizaciones.
Por fin encontre algo de tiempo para terminar todo esto. Ultima actualizacion: Añadidas instrucciones de llamada/retorno y push/pop.
Felicidades por terminar por fin la guia, es la mejor (y unica) guía con ejemplos practicos.
Sera que hace mucho no la leia pero me sirvio en gran medida el apartado de rotacion.
Saludos!
[MENTION=28012]Crystal_[/MENTION] , wao compa tremenda biblia que te has compuesto jeje, me la paso leendo y esta muy bien detallado, aparte de hacer este tremendo tutorial, me gustaría aprenderlo en la practica , nose si te animarías a abrir una escuela de dicho tema, quiero aprender de esto pero no tengo nada de conocimiento, o si pudieras brindarme clases o ejercicios privados, como una especie de tarea me seria de gran ayuda aprender de tus conocimientos :D ojala tenga una respuesta positiva por parte tuya... de todas formas agradezco las intenciones :D
Gracias y saludos