Introducción al lenguaje Assembler parte 3

  1. Alpertron
  2. Microprocesadores de la línea Intel
  3. Introducción al lenguaje Assembler parte 3

por Dario Alejandro Alpern

Archivos necesarios

Transcripción

Hola. Mi nombre es Darío Alpern y hoy vamos a ver la tercera parte de Introducción a Assembler de procesadores Intel

En este segundo programa, vamos a ver más instrucciones de Assembler.

Raíz cuadrada usando búsqueda binaria

Queremos hacer otra subrutina de raíz cuadrada, pero esta vez usando búsqueda binaria.

Como el radicando tiene 32 bits, sabemos que la raíz cuadrada va a tener 16 bits. La idea es dividir este rango de 65536 valores en dos partes iguales y determinar si la raíz cuadrada está en la parte inferior o en la superior.

Para ello calculamos el cuadrado del valor que está justo en la mitad 32768 en nuestro caso y lo comparamos contra el radicando. Como el radicando es mayor que el cuadrado, entonces la raíz cuadrada está en la mitad superior.

En el segundo paso calculamos el cuadrado del valor que está en la mitad de este rango, en nuestro caso 49152 y según la comparación, sabremos si está en la mitad inferior (rango 32768 a 49151) o en el superior (49152 a 65535). En nuestro caso está en el rango inferior.

Lo mismo ocurre en el tercer paso. Así seguimos un total de 16 veces hasta agotar los rangos.

Cuando termina este ciclo, ya tenemos la raíz cuadrada.

Mapa de memoria

Otra modificación que vamos a hacer al programa es que en vez de correr de memoria no volátil, vamos a correrlo desde RAM. Para eso, vamos a copiar el programa principal a la dirección lineal 0x20000 y la subrutina a la dirección lineal 0x50000. Luego, saltamos al programa principal en RAM.

A la izquierda, se pueden ver los 64 KB de ROM que arranca con la rutina de copiar de ROM a RAM. Luego, el programa principal inicializa la pila y el valor del radicando en memoria para luego llamar a la subrutina. Finalmente, va a hacer un salto a la misma dirección para colgarse.

La subrutina va a contener el código de la raíz cuadrada que lee el radicando de memoria, ejecuta la búsqueda binaria, y almacena el resultado en memoria.

Luego viene el relleno, la cantidad de bytes necesarias para que el JMP esté en el offset 0xFFF0 y finalmente el relleno para completar los 64 KB.

A la derecha, se puede ver el contenido de la RAM una vez efectuada la copia.

Como ya sabemos, en modo real la base del segmento se calcula como el selector multiplicado por 0x10 Despejando el selector, resulta que el selector es igual a la base del segmento dividido 0x10.

Por lo tanto, el programa principal deberá estar en un segmento de código cuyo selector valga 0x2000 mientras que la subrutina estará en otro segmento de código cuyo selector valga 0x5000.

Copia de datos usando instrucción de cadena

Ahora vamos a ver una instrucción de assembler que es bastante potente, porque es equivalente al memcpy en lenguaje C.

Antes de ejecutar la instrucción, hay que cargar sus parámetros en determinados registros. Los registros DS:SI apuntan al buffer fuente, los registros ES:DI apuntan al buffer destino, El registro CX indica la cantidad de bytes a copiar y finalmente el flag de dirección vale cero o uno si las direcciones crecen o decrecen con la ejecución de la instrucción.

Entonces, hay que ejecutar una de las dos instrucciones CLD o STD y luego REP MOVSB.

La letra B se puede reemplazar por W o por D para copiar words, que son bloques de 16 bits, o dwords, que son bloques de 32 bits.

También, se puede reemplazar el registro DS correspondiente al buffer fuente por cualquier otro registro de segmento. Para eso, se especifica dicho registro entre el REP y el MOVSB.

El registro ES correspondiente al buffer destino, no se puede modificar.

Explicación del programa

Ahora veremos el programa.

Bien, vamos a editar el programa. Acá está la explicación del programa que estuvimos diciéndolo. Después, las direcciones radicando y raíz son las mismas que antes, Luego vienen los selectores ya explicados del programa principal y la subrutina donde van a estar ubicados en 0x20000 para el programa principal y 0x50000 para la subrutina. Esos van a ser los valores de las direcciones lineales.

Luego viene el inicio, y en inicio comienza la rutina de copia.

Entonces, la primera copia se efectúa la copia del programa principal a la RAM. Para ello, primero cargamos en el extra segment el SEL_PROG_PRINCIPAL o sea, 0x2000. Después, en el puntero fuente que es SI, se carga el puntero al inicio del programa principal que se puede ver que está más abajo O sea, lo que yo quiero copiar (si aprieto F11 veo números de línea), lo que quiero copiar está en la línea 37 a la 45 y ahí se ve, fuera del azul (de la selección), se ve inicio_prog_principal y fin_prog_principal, líneas 36 y 46 respectivamente, Y yo quiero copiar ese rango a RAM Entonces, pongo en el registro fuente el puntero a inicio_prog_principal.

Después, el offset de destino es cero. Luego la cantidad de bytes es fin_prog_principal - inicio_prog_principal, la diferencia entre los offsets, y luego efectúo la copia pero observando que el registro de segmento fuente es el code segment. que es el que contiene el código de programa.

Luego hacemos lo mismo con la subrutina, Como selector en vez de SEL_PROG_PRINCIPAL usamos SEL_SUBRUTINA Después en el registro fuente usamos inicio_subrutina como puntero. Luego offset destino (también DI) lo ponemos a cero, y finalmente tenemos el contador de cantidad de bytes como fin_subrutina - inicio_subrutina. En este caso como podemos ver más abajo, vamos a copiar desde inicio_subrutina hasta fin_subrutina exclusive, o sea toda esta cantidad de bytes en azul. Todo eso va a ir a parar a RAM. Luego de ello, hacemos otra vez REP CS MOVSB, y hacemos el segundo memcpy.

Finalmente saltamos a RAM con un formato de JMP intersegmento donde se cambia el code segment y el instruction pointer. Entonces, el valor de la izquierda es el que va a parar a CS y el de la derecha es el offset que va a parar a IP. Esto va a hacer un salto a la dirección 0x20000.

El programa principal, que es lo que vemos acá a continuación, que es más o menos parecido a lo que habíamos visto en el programa anterior, que inicializaba el puntero de pila. Ahí inicialzamos el segmento de datos. Cargamos con 1000000 el valor del radicando en vez de 76. para hacer un número diferente, y luego llamamos a la subrutina con un CALL intersegmento De nuevo se modifica CS y EIP. Acá es necesario un CALL intersegmento porque estamos yendo a una zona de memoria completamente diferente del programa principal. Luego de eso está la rutina de colgarse.

¿Cómo funciona la raíz cuadrada mediante la búsqueda binaria? Leemos el radicando, luego inicializo la posible raíz cuadrada a cero. Verifico si radicando vale cero. Si es así, salto porque es el valor correcto. Y si no es cero tengo que hacer la búsqueda binaria.

Entonces, cargo el delta en la mitad del rango, o sea, 0x8000. y después en la búsqueda binaria lo que hago es sumar el principio del rango que está en SI más el delta, que es DI, y eso va a parar a EAX.

Luego, multiplico EAX por sí mismo y eso me reemplaza EAX por el cuadrado de EAX. Entonces, en EAX tengo el cuadrado de la mitad del rango.

Luego comparo el radicando contra el cuadrado. La instrucción CMP hace lo mismo que SUB pero sin reemplazar el destino. Lo único que hace es modificar los flags. Y entonces, ya con los flags modificados puedo saltar si es menor o igual, "jump on below or equal".

Si es menor o igual, salteo la suma del delta. como habíamos dicho anteriormente con la búsqueda binaria. En caso contrario sumo delta a la posible raíz porque eso significa que estamos en la mitad superior. Si saltamos sin sumar es porque estamos en la mitad inferior.

Luego de eso, una vez que ya sumamos o no, sabemos que estamos en la mitad superior o inferior, lo que hay que hacer con delta es dividirlo por 2. O sea, de 0x8000 debe pasar a 0x4000. Para eso hacemos una instrucción "shift right", SHR, donde el primer registro es el destino que sería DI en este caso y acá está la cantidad de bits que yo quiero shiftear que es 1. Entonces, shifteo a la derecha un lugar que significa dividir por 2.

Si no es cero, significa que todavía tenemos rango para comparar si es menor o mayor Si no es cero vuelvo otra vez al ciclo de búsqueda binaria, y si es cero, se terminó el loop, pero el SI que era lo que yo estaba buscando me quedó un número de menos entonces lo incremento para ajustarlo.

Y acá tengo un "magic breakpoint", se acuerdan, con xchg bx,bx y luego cargo el resultado con MOV [RAIZ],SI Eso guarda la raíz cuadrada en la posición de memoria que esperamos nosotros, que es la 0x304.

Y luego, en vez de RET, tenemos la instrucción RETF, que es un retorno de subrutina lejano, FAR. Eso se usa cuando hay un CALL intersegmento hay que aparearlo con RETF (ret far). Y con eso terminamos la subrutina.

Después hacemos lo mismo que con el programa anterior relleno, el JMP del reset, y después el otro relleno y eso es todo.

Ejecución del programa en Bochs

Bueno, ahora vamos a correr este programa. Para eso primero compilamos, La salida primero puse sqrt.bin en vez de sqrt2 así aprovechamos el seteo del bochs.cfg, así no lo modifico. Corremos el Bochs. y ahí arrancó, bien.

Saltamos en el reset inicial y estamos al principio de la zona de copia.

Cargo 0x2000 en ES. Si nosotros vemos con sreg, que son los registros de segmento y vamos al ES podemos ver que se cargó el selector con 0x2000 y la base se carga automáticamente, como estamos en modo real ven que acá dice Real Mode, se carga con 0x20000, que es 0x2000 por 0x10.

Luego tenemos el puntero fuente que es 0x27. Ese 0x27 si ustedes miran más abajo es justamente donde arranca el programa principal. Está acá, ¿no? Que es cuando se inicializa la pila y diferentes cosas. El 0x27 ya está.

Después si ejecutamos un "step" más. Este es el puntero destino, que se pone a cero, cantidad de bytes 0x1B y después hacemos el REP MOVSB que lo que hace es hacer el memcpy.

Si yo hago "step" varias veces, se queda ahí incrementando, como pueden ver en rojo las direcciones porque hace una copia por vez. Entonces, para hacer todas las copias juntas, pongo un punto de parada en la instrucción siguiente y le doy "continue" y ahí copió todo.

Después de eso, hacemos lo mismo con la subrutina. Vamos directamente a poner un punto de parada y copió también la subrutina en RAM.

Y ahora lo que vamos a hacer, es hacer el salto lejano al otro segmento de código, a la dirección 0x2000:0x0000 Entonces, cuando le doy "step", ahí se puede ver que se modificó el CS a 0x2000 y el IP se modificó a cero. Y estamos en la dirección lineal 0x20000.

Y entonces, ahora hacemos "step" "step", "step", "step" Ahí se cargó con 1000000 el valor del radicando.

Y acá viene el call intersegmento a la dirección 0x5000:0x0000. Acá se va a cargar 0x5000 en CS y 0x0000 el IP. Pero como es un CALL se va a cargar la dirección de retorno en la pila, entonces ahora vamos a mirar qué hay en la pila.

Ejecuto "step", y vemos que la pila en vez de ir a 0x7FFE como la vez pasada, ahora va a 0x7FFC O sea hay cuatro bytes en vez de dos. Vamos a ver qué hay en los cuatro bytes. Ponemos View > Linear Memdump. Entonces, vamos con el scroll hacia la derecha, y ahí vemos que se grabaron cuatro bytes. Primero siempre se graba el offset. El offset es 0x19 y luego va el selector que es 0x2000. Acuérdense que están al revés los bytes de lo que uno espera porque están en "little-endian". Entonces, el offset es 0x19 y el selector 0x2000, que es la dirección de retorno.

Entonces, ahora EBX se cargó con el valor 0xF4240 que es 1000000. Después, si se carga con el valor supuesto de la raíz cuadrada, que obviamente es cero por ahora.

Vemos si EBX vale cero. Obviamente no es. Entonces el flag de cero es cero. Significa que el resultado no es cero.

"step" y no saltó, obviamente.

Ahora DI es el delta, que se carga con la mitad del rango, que es 32768.

"step" Y ahora lo que hago es cargar EAX luego de la suma. Fíjense que EAX vale 0x8000 que es justamente la mitad del rango.

Entonces hago "step". Acá hizo la multiplicación: 0x8000 por 0x8000 da 0x40000000. Luego de eso, Comparamos EBX que es el radicando contra EAX que es el cuadrado Y acá cambia los flags según corresponde y va a saltar si es menor o igual EBX con respecto a EAX. y EBX es menor que EAX como se ve acá. O sea, un millón es menor que 1600 millones, más o menos, que es lo de arriba.

Entonces, ¿qué va a pasar? Va a pasar que no suma el delta porque estamos en la mitad inferior.

Luego de eso, el valor de DI, que es el delta tengo que dividirlo por 2 con la instrucción SHR DI,1 shiftea un lugar a la derecha. y de 0x8000 pasó a 0x4000. o sea, dividió por 2.

Obviamente 0x4000 no es cero, entonces vuelve y ejecuta de nuevo el loop así 16 veces y para no aburrirnos y hacerlo 16 veces voy a hacer "continue" hasta el xchg bx,bx que es el "magic breakpoint". Ahí ejecutó hasta el xchg bx, bx y muestra la instrucción siguiente a ejecutar.

Y fíjense que SI vale 0x3E8 que, si yo muevo el cursor a la derecha, fíjense que es el valor 1000, que es la raíz cuadrada de un millón.

O sea quiere decir que el código parece andar bien.

Ahora guardo el valor 1000 dentro de la memoria Y ahora de nuevo nos vamos a asegurar que haya grabado todo bien. View > Linear Memdump y acá se puede ver los cuatro bytes del radicando, que siempre se leen de derecha a izquierda, y la raíz es 00 00 03 E8, que es 1000, como vimos recién.

Y luego va a ejecutar RETF. RETF lee el valor que está en la pila o sea 0x7FFC y habíamos visto que en 0x7FFC teníamos el selector a 0x2000, vamos a ver de nuevo. View > Linear Memdump y acá ponemos 0x7FFC Y acá se ve: el selector está a 0x2000 y el offset 0x19. Entonces cuando yo le doy "step" pasó CS a 0x2000 e IP a 0x19, que es lo esperado y fíjense que SP volvió a 0x8000 o sea, que estamos con el stack balanceado que es lo que esperamos y salta, justamente, a la instrucción JMP donde se cuelga.

Bueno, creo que con esto ya tienen otro ejemplo para mirar. Espero que les haya interesado y hasta luego.