LECCION 5: ---------- ASM POR AESOFT. (lección 5). -------------------------------------------------------------------- - CODIFICACION DE LAS INSTRUCCIONES EN EL 8086. -------------------------------------------------------------------- Hola de nuevo, aplicados alumnos :-) En esta lección vamos a tratar conceptos muy técnicos acerca del formato de las instrucciones en código máquina. Veremos cómo se codifican las instrucciones en el 8086. - CODIFICACION DE LAS INSTRUCCIONES EN EL 8086. (Este apartado es muy técnico. Aunque no es imprescindible comprender lo que se expone a continuación para programar en ensamblador, es muy útil conocer cómo el procesador interpreta lo que le 'pedimos'. Esto nos da un mayor conocimiento acerca de la máquina en cuestión. Y de esta forma entendemos el porqué de ciertas sintaxis de instrucciones. Y resolveremos más fácilmente los errores una vez que se nos presenten). -------------------------------------------------------------------- Cada procesador tiene un conjunto de instrucciones para manejarlo, así como para manejar la máquina por medio de él. Indistintamente del lenguaje de programación que estemos utilizando, cuando obtenemos el ejecutable, éste está compuesto únicamente por ese tipo de instrucciones básicas (instrucciones de código máquina). Dependiendo de la calidad y prestaciones de ese lenguaje de programación, el código resultante, necesitará más instrucciones del procesador o menos. De todos es conocido, que hay lenguajes de alto o medio nivel (como C, pascal, basic, etc.) en los que para una misma tarea, uno dará un ejecutable más grande que otro. Velocidad, aparte. Esto no sucede así con ensamblador, en el que para cada instrucción, existe una y sólo una instrucción en código máquina. Pues bien, ahora vamos a ver la estructura de esas instrucciones básicas o de código máquina. Las instrucciones del 8086 se codifican sobre 4 campos como máximo, y tienen un tamaño de 1 a 6 bytes. Es decir, dependiendo de la instrucción de que se trate, necesitará más o menos bytes para su codificación, así como más o menos campos. Los cuatro campos en una instrucción código máquina son: 1.- Código de operación: Este campo siempre aparece (obviamente). Una vez que el procesador descifra el significado de este campo, sabe si la instrucción consta de más campos o si se trata de una instrucción de un sólo campo. 2.- Modo de direccionamiento (byte EA): Le indica al procesador el número de operandos que acompañan al código de operación, así como el tipo de estos operandos(registros, memoria, valor inmediato). 3.- Desplazamiento del dato (sobre 8 o 16 bits): En caso de existir este campo, supone un desplazamiento sobre la dirección dada por un registro índice o base (especificado este registro mediante el byte EA). 4.- Valor inmediato (sobre 8 o 16 bits): Almacena un valor numérico de 8 o 16 bits, que va a ser utilizado para una transferencia, una operación aritmética, etc. Ahora entramos un poco más en detalle: Primero veremos un esquema de una instrucción código máquina: +-------------------------------------------------------------------+ ¦ 8 bits 2 3 3 8 ó 16 bits 8 ó 16 bits ¦ ¦ +-----------+ +---Ð---Ð---+ +--------------+ +--------------+ ¦ ¦ ¦ código de ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ Valor ¦ ¦ ¦ ¦ operación ¦ ¦MOD¦REG¦R/M¦ ¦Desplazamiento¦ ¦ Inmediato ¦ ¦ ¦ +-----------+ +---¤---¤---+ +--------------+ +--------------+ ¦ ¦ -- 1 byte - -- 1 byte -  1 ó 2 bytes -  1 ó 2 bytes - ¦ +-------------------------------------------------------------------+ - El código de operación está codificado sobre 8 bits. Por medio de este campo se sabe si va a ser necesario cualquier otro de los tres restantes. También el código de operación contiene información acerca de si se va a trabajar con palabras o con bytes. - Byte EA ó Modo de direccionamiento: Contiene 3 campos. Los campos MOD y R/M especifican el modo de direccionamiento, y el campo REG especifica el registro de que se trata en la instrucción. El campo MOD que es de 2 bits puede tener 4 valores diferentes: Los 3 primeros seleccionan el desplazamiento en los modos de direccionamiento de memoria. El cuarto selecciona un registro. Detallemos la función de estos bits en cada una de las 4 posibilidades: 00 ---> No hay desplazamiento. 01 ---> Se usa un byte para codificar el desplazamiento. 10 ---> Se usan 2 bytes (una palabra) para codificar el desplazamiento. 11 ---> Hace que R/M seleccione un registro usando la misma codificación de los registros que para REG (ver más abajo), en lugar de un modo de direccionamiento de la memoria. Es decir, que se produce una transferencia de un registro a otro. El campo REG que es de 3 bits codifica el registro empleado. Por tanto es posible especificar hasta 8 registros diferentes por medio de este campo. Dependiendo de que se trate de acceso a palabras o a octetos, se seleccionará un registro de entre un grupo de 8, o de un segundo grupo de 8 registros. Para cuando se accede a registros de 16 bits, el campo REG codifica los registros de palabra de la siguiente manera: AX (000), CX (001), DX (010), BX (011) SP (100), BP (101), SI (110), DI (111) Cuando se accede a registros de 8 bits, la codificación de los registros de tamaño byte queda como sigue: AL (000), CL (001), DL (010), BL (011) AH (100), CH (101), DH (110), BH (111) El campo R/M indica el segundo registro (si lo hay) o el tipo de direccionamiento a memoria. En caso de que haya segundo registro, éste se codifica de la misma forma que para el campo REG. En caso de que se trate de un modo de direccionamiento de memoria, estos tres bits seleccionan uno de los modos de direccionamiento posibles de acuerdo con la siguiente tabla: 000 desplazamiento final = [BX] + [SI] + desplazamiento 001 desplazamiento final = [BX] + [DI] + desplazamiento 010 desplazamiento final = [BP] + [SI] + desplazamiento 011 desplazamiento final = [BP] + [DI] + desplazamiento 100 desplazamiento final = [SI] + desplazamiento 101 desplazamiento final = [DI] + desplazamiento 110 desplazamiento final = [BP] + desplazamiento 111 desplazamiento final = [BX] + desplazamiento - El desplazamiento en caso de existir, supone un incremento en la dirección dada por un registro índice o base, dando lugar así a un desplazamiento final, dentro de un segmento dado. Es decir, como se ve en la tabla superior, podemos acceder a memoria a través de un registro base (BX) o un registro índice (SI, DI), etc, o bien hacerlo a través de uno de esos registros, pero ayudándonos de un desplazamiento que se suma a la dirección que tienen establecida esos registros. Veremos más adelante la utilidad de utilizar desplazamientos sobre un registro base o índice. Como ejemplo: Tenemos el registro DI apuntando a (con valor igual a) la dirección 3000h (direcciones siempre en hexadecimal). En esa dirección tenemos el comienzo de una cadena de caracteres que queremos convertir a myúsculas. Y una vez que los hemos convertido, los queremos copiar a la memoria de pantalla. Pues bien, podemos ir incrementando DI para tratar cada uno de estos caracteres, o bien podemos utilizar DI junto con un desplazamiento para acceder a cada uno de los caracteres. Es decir, para acceder al primer elemento sería DI+0, para el segundo, sería DI+1, etc. De esta forma, al terminar la tarea, DI seguirá apuntando al principio de la cadena, y podremos copiar la cadena desde el principio a donde corresponda. Si no utilizáramos desplazamiento, tendríamos que tener una variable apuntando al inicio de la cadena, para tenerlo luego localizable. Bueno... Esto es un simple ejemplo. Las posibilidades que nos ofrece el utilizar desplazamientos acompañando al registro base o índice son mucho más interesantes que lo que acabamos de ver en el ejemplo. - El valor inmediato se utiliza cuando hacemos movimientos de datos a registros o a memoria. Por ejemplo queremos introducir en el registro AX la cantidad 37867 (93EBH), pues ese 37867 sería el valor inmediato. En ensamblador la instrucción sería: MOV AX,37867 Simple, ¿no? Mover (MOV) la cantidad 37867 al registro AX. Próximamente se verá el resto de instrucciones en ensamblador, mientras tanto, y por ser necesario ahora, aprenderemos el uso de la instrucción MOV. La instrucción como hemos podido ver, se utiliza para movimientos o transferencias de datos: de registro a registro, de registro a memoria, y de memoria a registro. Pero nunca de memoria a memoria, ya que la arquitectura del procesador y bus no lo permiten. La sintaxis básica de la instrucción es la siguiente: MOV destino,fuente. El destino siempre a la izquierda, y la fuente a la derecha. Ejemplos: * MOV ax,5 ---> mueve el valor inmediato (o dato) 5 al registro AX. Examinemos esta instrucción. Alguien podría pensar que como el valor 5 cabe en un sólo registro de 8 bits (AL en este caso), el registro AH quedaría como estaba antes de la instrucción. Pues no es así. Si le decimos al procesador que introduzca un 5 en AX, así se hará. Poniendo a cero el registro AH, para que AX tenga el valor 5. Veamos cómo se codifica esta instrucción: MOV AX,5 ---> B8 05 00 (Código máquina, siempre en hexadecimal). En primer lugar tenemos el primer byte que contiene el código de operación (B8). Debido a que este código de operación(B8) tiene implícita la utilización del registro AX como destino, no es necesario el byte EA ó byte de direccionamiento, que sí sería necesario para transferencias con otros registros. Como vimos en la primera lección al hablar de registros, el registros AX (AH, AL) se utiliza normalmente como acumulador, de tal manera que existen operaciones especiales para trabajar con él, como la instrucción B8 y otras muchas de movimiento de datos, en las que no se especifica el registro mediante el byte EA, ya que está implícito en el código de operaicón. De esta manera se gana velocidad en la ejecución del programa utilizando los registros para lo que han sido creados. AX acumulador, CX contador, etc. Después del código de operación tenemos dos bytes (1 palabra). Estos dos bytes forman el campo Valor Inmediato, que como vemos aquí es de 16 bits. Como os habreis dado cuenta, de los 4 campos que puede tener una instrucción código máquina, ésta sólo tiene dos: El primero (código de operación), y el último (valor inmediato). Y volviendo de nuevo al campo Valor inmediato y a su tamaño en esta instrucción (2 bytes): El orden de estos bytes es muy significativo. Veamos... Tenemos el valor 5 para introducir en una palabra. Lo normal sería que en el código se almacenara este cinco como (00 05), pues en el 8086 esto no es así. Como siempre, para acelerar el programa cuando se manejan transferencias de datos, se llegó a la conclusión de que si se almacenan los bytes que componen una palabra en orden inverso al normal, luego es mucho más rápido recuperarlos. Y es así como se hace en la práctica. Cada vez que almacenamos una palabra en memoria, el byte de mayor peso queda a la derecha del byte de menor peso. De lo anterior se desprende que el número 5 al introducirlo en una palabra de memoria, quedaría como (05 00). Otro ejemplo: Una vez que almacenamos el número 8BC3H en memoria, si hacemos un volcado de memoria para ver qué tenemos, veremos que en memoria no está el número como 8BC3H, sino que nos encontramos con C38BH. * MOV al,5 ---> Introduce el valor 5 en el registro AL. En este caso, sí que AH queda como estaba antes de la instrucción, ya que en la misma no interviene tal registro de ninguna forma (ni implícita al referirse a AX, ni explícita al referirnos a él en concreto). La instrucción se codifica como: MOV AL,5 ---> B0 05 Este ejemplo es prácticamente como el anterior, excepto que el código de operación en vez de ser B8 es B0, y además ya no hay 2 bytes en el campo valor inmediato, sino que hay uno sólo, ya que vamos a introducir el dato en un registro de tamaño byte. Ejemplo cuando se trata de transferencias entre registros: * MOV CX,SI ---> Introduce el valor del registro SI en el registro CX. La instrucción se codifica como: MOV CX,SI ---> 8B CE En esta instrucción tenemos un código de operando y el byte EA. Mediante este byte EA el procesador sabe qué registros intervienen en la transferencia. Descomponiendo el byte EA en sus dígitos binarios, tenemos: CE ---> 11001110 El campo MOD con valor 11, hace que R/M seleccione un registro como fuente. El campo REG con valor 001, indica que el registro destino es CX. El campo R/M con valor 110, indica que el registro fuente es SI. --- Hemos visto la manera de introducir un dato en un registro. ¿Pero cómo hacemos para introducir un dato en memoria? Bien, para esto se utilizan las variables (que también existen en ensamblador) o bien, se indica una posición de memoria concreta, pasando de variables. Hay una tercera manera que es utilizar registros índice o base. + En el primer caso, es muy simple. Si queremos introducir el valor 70h en la variable X, basta con escribir MOV X,70h. Previamente la variable X la hemos definido y hemos definido también su tamaño: byte, palabra, doble palabra. Una vez que el compilador dé el código ejecutable, lo que antes era la variable X, ahora será la posición de memoria ocupada por la variable. Es decir, que el usar variables es para darnos una gran comodidad a los programadores. Podríamos hacer un programa sin usar variables, indicando posiciones de memoria directamente, pero eso es ya más parecido a código máquina puro que a ensamblador. + En el segundo caso, el de indicar la posición de memoria concreta, hay que tener en cuenta si esa posición de memoria la utilizamos como un byte o como una palabra. Esto es así ya que si por medio del programa queremos guardar un 5 en la posición de memoria 7654h (por ejemplo), el procesador no sabe si queremos guardar un byte o una palabra. ¦ Para que no surja ningún tipo de lios, el lenguaje ensamblador cuenta ¦ con ciertos convencionalismos para tratar estas transferencias a memoria. ¦ Cuando queremos introducir un byte en una posición dada de memoria lo ¦ hacemos con el siguiente formato: MOV BYTE PTR DS:[7654H],5 ¦ ^^^^^^^^ ^^ ^ ^ ¦ BYTE PTR indica que vamos a acceder a una posición de memoria de tipo BYTE. ¦ ¦ Caundo queremos introducir una palabra a partir de una posición de memoria ¦ el formato queda como sigue: MOV WORD PTR DS:[7654H],5 ¦ ^^^^^^^^ ^^ ^ ^ ¦ WORD PTR indica que vamos a acceder a una posición de memoria de tipo WORD. Tened en cuenta también que cuando se quiere acceder a una posición concreta de memoria sin pasar por una variable, se debe indicar entre corchetes, como en los ejemplos de arriba. Pero eso no es todo, se debe indicar un segmento, para que el procesador sepa a qué zona de 64 ks de la memoria pertenece la posición dada entre los corchetes. En este caso indicamos el segmento DS (segmento de datos), que es lo usual. Aunque también podríamos haber seleccionado el segmento ES(segmento extra de datos) para así poder transferir algo fuera de nuestra zona de datos. Obsérvese la manera de indicar una dirección en dirección segmentada, no real. Primero se indica el segmento, luego dos puntos para separar, y luego entre corchetes el offset o desplazamiento dentro de ese segmento. Segmento:[desplazamiento] DS:[2626h], ES:[FFFFh], etc. + En el tercer caso nos valemos de un registro índice o base, el cual contiene la dirección de la posición de memoria que nos interesa, para acceder a dicha posición de memoria. Un ejemplo: MOV BYTE PTR [DI],5 Obsérvese que aquí no es necesario indicar el segmento al que nos referimos. Se coge por defecto el segmento DS. En definitiva, cuando accedemos a memoria a través de registros indice o base, no es necesario indicar el segmento. Mientras que si lo hacemos en forma directa, indicando la posición de memoria tal que [2635h], debemos indicar el segmento con el que vamos a tratar. ------------------- Qué lioooooooooooooooooooooooo... ¿verdad? He intentado ponerlo lo más claro posible, con muchos ejemplos, y como se lo explicaría a una persona que tuviera a mi lado. Pasando de rollos teóricos de libros y demás parafernalia, pero si aún así os resulta lioso o complicado, no os preocupeis. Estoy aquí para re-explicar lo que haga falta. Y además cuando empecemos a hacer programillas, todo esto se verá muy claro en la práctica. -------------------- Sigamos: Veamos ahora cómo se codifica una instrucción en la que se hace acceso a memoria. * MOV WORD PTR DS:[7654H],5 ---> Esta instrucción introduce el valor 5 a partir de la posición de memoria 7654h. Y digo a partir, ya que necesita dos posiciones de memoria para almacenarlo, ya que se trata de un valor inmediato de 16 bits (esto se determina al poner lo del WORD PTR). Con lo cual, la palabra con valor 5, queda almacenada en dos posiciones de memoria, la indicada [7654h] y la contigua [7655h]. Si tenemos en cuenta lo que hemos comentado antes acerca de cómo el 8086 almacena las datos de tipo palabra en memoria, sabremos de antemano que la posición [7654h] contendrá el valor 05, y la posición [7655h] contendrá el valor 00. Veamos cómo se codifica esta instrucción: MOV WORD PTR [7654H],5 ---> C7 06 54 76 05 00 Vemos que esta instrucción ha ocupado el máximo posible (6 bytes). De tal forma que los 4 campos de instrucción están presentes. Vamos a estudiarla detenidamente: Lo primero que tenemos es el código de operación: C7. Este código indica una operación MOV sobre una dirección concreta ó desplazamiento, y con un valor numérico de tipo palabra. El 3º y 4º byte juntos forman el desplazamiento (tener en cuenta lo del tema del orden inverso en los bytes), y los bytes 5º y 6º juntos forman el valor inemdiato a introducir (tener en cuenta de nuevo lo del orden inverso). Y nos queda el 2º byte, que es el byte EA o de direccionamiento. ¿Que por qué lo he dejado para el final? je. Porque llevo 2 o 3 horas intentando descubrir el por qué de que sea 06. No me cuadra por ningún sitio, ya que este 6 indica que no hay desplazamiento, cuando sí lo hay. A ver si para la próxima lección, consigo descifrar el misterio. Un saludo. AESOFT....