Introducción al lenguaje Assembler parte 2

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

por Dario Alejandro Alpern

Archivos necesarios

Transcripción

Hola. Mi nombre es Darío Alpern y ahora vamos a ver la segunda parte de la introducción a Assembler de procesadores Intel.

En este video vamos a ver como funciona el Bochs que es un emulador de PC, que emula tanto el microprocesador de Intel como el motherboard de la PC.

Para utilizar este emulador, vamos a usar el programa que vimos en el primer video que es el del cálculo de la raíz cuadrada.

Entonces, vamos a editarlo con el editor Kate.

Apretando CTRL ++ se agrandan las letras y podemos ver los equates que equivalen a defines en lenguaje C para no usar constantes voladoras en el medio del código.

Luego, tenemos el inicio del programa, donde inicializamos puntero de pila y el segmento de datos para que apunten al principio de el espacio direccionable de memoria y luego de eso inicializamos con 76 el valor del radicando, que es el argumento de la raíz cuadrada. Ese es un número que usamos para probar como anda la subrutina.

Luego de ejecutar la subrutina raiz_cuadrada, se ejecuta una instrucción JMP cuyo destino es la misma dirección, por lo tanto termina colgándose.

A continuación tenemos la subrutina raiz_cuadrada que lo que hace es obtener el valor del radicando y le va restando números impares sucesivos hasta que el resultado sea menor que cero.

Entonces, la raíz cuadrada es igual a la cantidad de restas que se hicieron menos uno, como se explicó en el primer video.

Entonces, tenemos la carga del radicando, Después, cargamos al sustraendo el primer número impar que es 1, después ponemos la cantidad de restas a cero, después viene el ciclo principal y finalmente cargamos el resultado con la cantidad de restas que se hicieron y finalmente, se termina la subrutina.

Después de eso, se coloca un relleno de bytes a cero, para que la próxima instrucción que se ejecute esté en la dirección FFF0 hexadecimal. Y luego de eso, se colocan más bytes para rellenar. De esa manera tenemos 64 KB completos de memoria ROM.

Entonces, cerramos, compilamos el programa usando el ensamblador nasm le decimos que la salida con la opción -o sea... Primero ponemos el nombre del programa, después -o el binario y después -l sqrt.lst.

Entonces en el listing se genera información que ustedes pueden ver con las direcciones del programa, el código binario, instrucciones, que son las mismas instrucciones que nosotros escribimos y acá están los diferentes códigos de máquina que se van generando. Eso es el listing. Y una cosa que se puede ver en el listing es que en la dirección inicial del programa que es el EIP que arranca en la dirección FFF0 hexadecimal tenemos realmente la instrucción que corresponde al principio del programa, y después sabemos que la ROM ocupa 64 KB completos.

Entonces, para poder correr el programa Bochs, hacemos bochs la opción -q es "quiet" que hace que no nos pida ningún menú cuando arranca el Bochs y la opción f es para indicar el archivo de configuración que en nuestro caso es bochs.cfg que es el que habíamos tocado en el primer video.

Entonces, arranca el Bochs y nos muestra el debugger gráfico.

En el panel de la izquierda vemos los registros del procesador. Acá tenemos 8 registros de uso general: EAX, EBX,.... hasta ESP y EBP son todos los registros de uso general.

Después tenemos el Instruction Pointer, que nos dice la dirección efectiva donde está corriendo el código. Fíjense que la dirección efectiva después del reset es FFF0 hexadecimal que tiene que tener la instrucción JMP que habíamos visto.

Después están los flags, que son los indicadores.

Luego tenemos los seis registros de segmento, que como habíamos visto, el registro CS el selector inicial después del reset era F000.

Y después tenemos otros registros más que se van a ver cuando veamos modo protegido.

En el panel central tenemos las instrucciones. Entonces, en verde tenemos la próxima instrucción que se va a ejecutar. En este caso tenemos la dirección lineal FFFFFFF0. ¿Por qué tenemos esa dirección lineal? Habíamos dicho que la dirección base del segmento de código es FFFF0000 y ese valor se determina en el reset. Si a eso le sumamos el valor de IP, nos queda FFFFFFF0.

Si se quiere, podemos ver con el comando sreg en consola, podemos ver información del code segment y acá tenemos que el valor inicial es FFFF0000. Le sumamos me da la dirección lineal que vemos en verde.

Después de esto ejecutamos un "step" y se va a ejecutar el JMP y con esto cambia el valor de IP.

La idea es que la información que está en rojo en el panel de la izquierda son los registros que cambiaron con la instrucción que se acaba de ejecutar.

Entonces, la primera instrucción al principio del programa coloca los registros de segmento a cero.

Entonces, carga AX con cero y después va a cargar el selector de DS y SS a cero. En este caso no hay ningún cambio porque en el reset también estaban a cero, entonces no se modifican.

Después cargo el SP a 8000 hexadecimal y acá podemos ver en rojo que se modificó.

Después cargo el valor de EAX en 76 o 4C hexadecimal y acá se ve como se modifica y ese valor se carga en memoria en la dirección indicada por el DS (data segment) cuya base es cero y el offset es 0300 hexadecimal.

Entonces lo ejecuto, y si quiero ver donde se cargó en la memoria lo que hago es poner View Linear Memdump y coloco la dirección 300 hexadecimal.

Y acá vemos los cuatro bytes que se cargaron 4C 00 00 00 en formato Little-Endian que significa que el byte menos significativo va primero.

Entonces, luego de eso, se va a ejecutar el llamado a subrutina.

Entonces hago "step", y fíjense que aparte de modificarse el Instruction Pointer se modificó el stack pointer: de 8000 pasó a 7FFE o sea bajó dos lugares.

Entonces vamos a ver el contenido de la pila con View Linear Mem dump el valor del puntero de pila es 7FFE.

Borro el resto, y acá usamos el scroll bar para llegar a la derecha y acá tenemos 17 00 que como está en Little-Endian es 00 17 hexadecimal.

Significa que el valor que se puso en memoria es 0017 hexadecimal que es el valor del offset correspondiente al IP del regreso de subrutina. Fíjense que el 0017 es la instrucción que viene después del call. Cuando regrese, tiene que regresar al JMP que está a continuación que es la dirección 0017 hexadecimal.

Luego de eso, una vez que leímos la memoria, ejecutamos y se pone ECX a cero. Acá no se nota porque ya valía cero de antes.

Luego de eso incrementamos ECX y cuando se incrementa ECX va a pasar de cero a uno. Al incrementar, suma uno al registro.

Luego colocamos DX a cero, y entramos en el ciclo principal de restas. Donde se resta 76, que era el número que teníamos para probar la raíz cuadrada, que es el radicando le restamos el valor 1, que es el primer número impar.

Fíjense: EAX = 76 y ECX = 1. Y va valer el resultado 75 que es 4B. La idea es restar números impares consecutivos 1, 3, 5 ,7, ... hasta que el resultado sea menor que cero.

Entonces, le doy "step" y se modificó de 4C a 4B el valor de EAX, se restó uno.

Luego viene el salto "jump on below" que es lo mismo que "jump on carry" que habíamos compilado nosotros que salta si el resultado es menor que cero. ¿Y eso qué significa? Significa que en ese caso se iría del ciclo de resta que no es nuestro caso porque pasó de 76 a 75 entonces no hay "borrow".

Entonces, sigue con la instrucción a continuación y va a sumar dos al valor de ECX que es 1, va a pasar del primer número impar que es 1 al siguiente número impar que es 3.

Fíjense que siempre el destino va a la izquierda, fuente a la derecha y el resultado se carga en el registro destino que sería en este caso ECX.

Entonces, si ejecutamos "step" se cambia de 1 a 3 el valor de ECX.

Luego, incrementamos la cantidad de restas y fíjense que pasó de cero a uno.

Y luego viene un JMP incondicional para volver al principio del ciclo de restas, con la instrucción que está en la posición 25 que es otra vez el sub que estaba antes.

Entonces, "step" volvió al offset 25.

Fíjense que IP vale 25 y va a hacer la siguiente resta. Va a restar 75 - 3. El resultado va a ser 72. Como se ve acá 48 hexadecimal es 72. Otra vez no se da el "borrow" entonces, continúa dentro del loop y así sucesivamente va cargando el siguiente número impar de 3 pasa a 5 e incrementa nuevamente la cantidad de restas.

De 1 va a pasar a 2. Y así vamos a ver varias veces hasta que eventualmente sale del loop. Lo que vamos a hacer es pasar varias veces hasta lo que sería la última resta que sería por aquí.

Ahí está. Esta es la última resta. En la última resta, el valor de EAX que es 12 es menor que el valor de ECX que es 17.

Entonces acá, una vez que haga la resta el resultado va a ser negativo y por lo tanto el "jump on below" va a salir del ciclo.

Entonces, ejecuto. Fíjense que EAX ahora se hace negativo y el "jump on below" se cumple y sale del loop. Ahora va a la dirección 31 que está fuera del loop.

Y el valor de DX es 8, que es la raíz cuadrada de 76, como corresponde y se va a cargar en la posición 0304.

Entonces le doy "step" y miro con View, Linear Mem dump Vamos a poner la misma dirección que antes, 300. Y ahí se ve moviendo de nuevo el scroll bar se ve el número 76 en la posición 300 y el número 8 en la posición 304. Es una tabla de doble entrada. Bien.

Entonces, una vez que se cargó en memoria el resultado, viene la instrucción RET que es para volver de la subrutina que lo que hace es leer la pila la posición apuntada por 7FFE y va a leer el 17 que estaba guardado ahí adentro y ese número lo va a cargar en el registro IP.

Entonces le damos "step", cargó 17 en el registro IP, y salió de la subrutina.

Fíjense que el stack pointer o sea el puntero de pila, volvió al valor inicial. Siempre tiene que terminar igual que como comenzó. O sea, la pila queda balanceada en este caso.

Una vez que llega este JMP, si yo le doy "step" sigue indefinidamente en el JMP porque el destino del JMP es 17 que es la misma dirección que está el JMP. Entonces indefinidamente va a seguir en 17.

Fíjense los valores de los flags en este momento. Acá tenemos en minúsculas los flags que están a cero y en mayúsculas los flags que están a uno.

Entonces, por ejemplo, en el caso de flag de signo, la última instrucción aritmético-lógica fue la resta que se encuentra en esta dirección y se acuerdan que habíamos restado 12 - 17 que el resultado es -5, que es lo que se ve acá en complemento a dos y el flag de signo repite el bit más significativo del resultado que indica que es negativo, el bit está a uno.

Luego el flag de cero está a cero porque el resultado no es cero.

Después, el carry flag vale uno porque la resta dio overflow con respecto a números no signados porque no hay ningún número no signado que valga -5, por lo tanto se tiene que prender necesariamente el flag de carry.

Entonces, con esto ejecutamos ya el programa completo.

Vamos a hacer una pruebita. Supongamos que salimos de nuevo y entramos otra vez.

Entonces, entramos otra vez, y lo que vamos a hacer ahora es poner un punto de parada.

El punto de parada es para no tener que ejecutar paso a paso quizá cientos o miles de instrucciones y lo que voy a hacer es poner punto de parada después del loop o del ciclo de restas.

Entonces, después del ciclo de restas es cuando se escribe en la memoria el resultado, la raíz.

Entonces, para habilitar el punto de parada lo que tengo que hacer es un doble click en la dirección. Cuando hago doble click se me pone la instrucción en rojo y en itálica. Ya con eso sé que tiene un breakpoint habilitado.

Entonces, yo le doy "continue", esto está todo a cero porque recién arranca. Le doy "continue" ¿Y qué pasó? Aparece la misma información que estaba antes EAX vale -5, ECX 17 que era el último número impar que hizo la resta, 8 era la raíz cuadrada, ESP vale 7FFE porque todavía no salió de la subrutina y EFLAGS vale lo que dijimos antes.

Después se pueden ver más cosas, por ejemplo, se pueden ver el contenido de los registros usando "reg". Parte del panel de la izquierda lo puedo ver en formato texto, si yo quiero copiar la información de los registros y pegarlo en algun editor de texto, lo puedo hacer.

Estos breakpoints son útiles si queremos para en una dirección cercana a la que estamos ejecutando pero si nosotros tenemos un código muy largo el problema que se plantea es que no sabemos donde poner el punto de parada.

Entonces, hay un método adicional que permite el Bochs que se llama "magic breakpoints". Lo que vamos a hacer es: paramos el emulador y luego editamos el archivo de configuración del Bochs y buscamos la palabra "magic" y en este caso está habilitado. Podría estar deshabilitado. Hay que asegurarse que no esté el numeral puesto adelante. En este caso el magic breakpoint está habilitado. Bien.

¿Cómo funciona esto? Hay que editar el programa donde queremos poner el breakpoint y vamos a suponer que queremos poner el breakpoint antes y después del ciclo de restas.

Voy a apretar F11 y con F11 veo los números de línea.

El ciclo de restas está entre la línea 28 y la línea 32 es el ciclo de restas.

Entonces, agrego una instrucción xchg bx, bx es un magic breakpoint.

Y luego voy a poner antes del ciclo y luego pongo otro después del ciclo otro magic breakpoint. Entonces, la idea de esto que se pueda parar la ejecución del código cuando se llega a un magic breakpoint.

Salimos de acá, salvamos, salimos, compilamos y luego ejecutamos el Bochs de nuevo. Y está otra vez el Bochs.

Si yo le doy "continue" va a ejecutar hasta el primer magic breakpoint.

Entonces, fíjense que ahora estamos en la resta que es donde comienza el ciclo.

El registro ECX está con el primer número impar, EAX vale 76 que es el valor que queríamos probar, y ahora le damos "continue" de nuevo.

Se paró la ejecución del código justo después del magic breakpoint, que es donde se va a grabar el resultado final que es la raíz se graba la cantidad de restas menos uno que está almacenado en DX.

Entonces ahí se puede ver que paró en ambos magic breakpoints que nosotros habíamos puesto.

Si le damos "continue" y paramos con "break" queda en el JMP que habíamos puesto para que se termine colgando el código Se puede seguir con más información como poner breakpoints usando la consola o bien, hacer más cosas pero por hoy es suficiente con todo lo que vimos hasta ahora.

Bueno, hasta luego.