- Conceptos básicos
- Registros de la CPU
- Modos de Direccionamiento
- Los Assemblers más comunes
- Instrucciones en Assembly
- Multiplicación y división, MUL y DIV:
- Ejercicios (1)
- Instrucción de salto incondicional, JMP
- FLAGS
- Intrucciones de comparación, CMP y TEST
- La Pila
- Llamados a subrutinas, CALL
- Respuestas de los ejercicios
Este material es un trabajo práctico que se realizó en base a lo dictado en la materia "Arquitectura de las Computadoras II", que pertence a la carrera Analista Universitario en Sistemas, del Instituo Politécnico Superior Gral. San Martín (Universidad Nacional de Rosario, Argentina).
Antes de comenzar, conviene aclarar que el lenguaje se denomina Assembly y no Assembler como normalmente se lo conoce. Cuando se escribe un programa en Assembly, hay otro programa "ensamblador" que se encarga de traducir lo diseñado por el programador al código de máquina. Esta clase de programa se llama traductor. Los compiladores e intérpretes son traductores que pasan a código máquina los programas escritos en los distintos lenguajes. Assembler es el programa traductor de Assembly a código de máquina, y que es propio de cada modelo de procesador. Cada microprocesador tiene su propio Assembler con su particular set de instrucciones y sus varios modos de direccionamiento que dependen de la arquitectura propia del sistema, y cada nuevo modelo de procesador que saca al mercado un fabricante, puede que agregue algunas instrucciones a su set de instrucciones para mejorar el rendimiento del mismo y hasta agregar algún nuevo registro (o extender la capacidad de bits de los existentes), pero siempre conservan las instrucciones y registros de los modelos anteriores por razones de compatibilidad.
Dado que las mayoría de las PC usan procesadores Intel o sus clones, desarrollaremos ejercicios para esta arquitectura de procesadores.
Registros de la CPU:
Los primeros procesadores como el 8088 y el 8086 nos proveían de los registros de 16 bits AX, BX, CX, DX, SI, DI, BP, SP, CS, DS, SS, ES, IP, y FLAGS. El procesador soportaba hasta 1Mb de memoria y solo podía operar en modo Real. En este modo el programa podía acceder a cualquier dirección de memoria, incluso a direcciones utilizadas por otros programas. Esto hacía muy difícil la depuración del programa (debugging), y por lo tanto tampoco era muy seguro. Los datos de los programas se alojaban en memoria todo el tiempo y eran divididos en segmentos de hasta 64Kb.
Sus cuatro registros principales, AX, BX, CX, y DX están divididos en dos registros de 8 bits cada uno. Por ejemplo, el registro AX posee una parte que contiene los primeros 8 bits denominada AH (high) y una parte que contiene los últimos 8 bits denominada AL (low), y así sucesivamente con cada uno de los registros mencionados.
16 bits | ||
8 bits | 8 bits | |
AX | AH | AL |
BX | BH | BL |
CX | CH | CL |
DX | DH | DL |
Este tipo de registros se usa especialmente en operaciones aritméticas, ya que nos permite manejarnos con comodidad cuando trabajamos con datos que no superan un byte, pero se debe tener cuidado ya que AH y AL no son independientes de AX, o sea que si hacemos algún movimiento de datos referenciando a AX, también estamos cambiando los valores de AH y AL.
Los registros SI y DI son utilizados generalmente como punteros.
Los registros BP y SP se conocen como los punteros de pila. Se utilizan para moverse dentro de la pila.
CS, DS, SS, y ES son los segments registers. Son los encargados de direccionar las distintas partes de cada programa:
- CS para el code segment, donde se guardan los datos del código de máquina de las instrucciones que constituyen el programa.
- DS para el data segment, que guarda los datos que el programa debe operar y los resultados de la ejecución del mismo.
- SS para el stack segment, Almacena datos y direcciones necesarias durante la ejecución de cada parte del programa y que es localizada dentro del segmento mediante el registro SP (stack pointer).
- ES para el extra segment, utilizado para guardar datos tipo strings, también como prolongación del DS (data segment), y como registro temporal. Cada dato es apuntado dentro del segmento por el registro puntero DI.
Estos registros cumplían una importante función en el direccionamiento a memoria ya que el 8086 podía manejar solo hasta 1Mb de memoria (2bytes). El rango válido de direcciones de memoria era de 0x0000 a 0xFFFFF. Este rango de direcciones requería de un número de 20 bits, pero el 8086 tenía registros de 16 bits. Intel resolvió el problema usando dos segment registers de 16 bits para determinar la dirección. A los primeros 16 bits se los denominaba selector y a los segundos offset. La dirección física real referenciada por el par de 32 bits formado por el Nº selector:offset estaba dada por la fórmula:
16* selector + offset
Ejemplo:
Queremos calcular la dirección física referenciada por el par 047C:0048.
Primero debemos multiplicar a 047C * 16. Cuando multiplicamos por 16 en hexa, agregamos un cero a la derecha y listo (=047C0),
luego hacemos la suma,
047C0 |
+0048 |
04808 |
y obtenemos que la dirección en memoria es la 0x04808 (que es un nº de 20 bits.)
El registro IP (instruction pointer) es utilizado para mantener una pista de la dirección de la próxima instrucción a ejecutarse por el procesador. Normalmente cuando se ejecuta una instrucción, IP se adelanta a apuntar a la próxima instrucción en memoria.
El FLAGS es un registro de 16 bits que se divide en 16 partes de 1 bit. Cada uno de estos bits guarda información importante sobre el resultado de la instrucción anterior. Este resultado se guarda con un solo un bit que puede ser 1 ó 0. Por ejemplo, el bit Z es 1 si el resultado de la instrucción anterior fue 0, y es 0 si el resultado fue 1. El bit C será 1 si la última operación produjo acarreo, y 0 si no lo hizo.
El primer procesador para PC’s de clase AT fue el 80286. Este incorporaba nuevas instrucciones a su set de instrucciones y presentaba su nueva forma de procesar en modo Protegido de 16 bits. En este modo el procesador podía acceder hasta a 16Mb de memoria y proteger a los programas entre sí en los accesos a la memoria. La configuración de sus registros era la misma que la de su antecesor, y los programas seguían estando divididos en segmentos de 64Kb. Pero estos segmentos ya no estaban en la memoria todo el tiempo, solo se mantenía a los que necesitaba el programa en el momento de ejecución. El resto de los datos y código era alojado en forma temporal en el disco hasta ser necesitados por el programa. Esta técnica se la conoce con el nombre de memoria virtual, y es la que se utiliza en la mayoría de los sistemas actuales. A cada segmento se le asigna un index dentro de una tabla descriptora (descriptor table), este index contiene toda la información que el sistema necesita conocer sobre el segmento, incluyendo si está corriendo actualmente en memoria, si está en memoria, si está en el disco, los permisos de acceso (lectura/escritura), etc. Este index identificador del segmento es un valor que se guarda en los segment registers.
Con la aparición del 80386 se produce un gran salto en el diseño. Primero y principal se extiende la mayoría de sus registros a 32 bits (renombrándolos como EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP) y se agregan dos registros nuevos de 16 bits (FS, y GS). Para guardar compatibilidad con los diseños anteriores se conviene de que al hacer referencia a AX, se hace referencia a los últimos 16 bits de EAX (lo mismo que era AL de AX); pero no se puede tener acceso directo a los primeros 16 bits de EAX.
Segundo, el procesador presenta su nuevo modo de trabajo denominado modo protegido de 32 bits. Este modo presenta dos grandes diferencias con el modo protegido de 16 bits:
- Se puede acceder hasta a 4Gb de memoria. Los programas se seguían dividiendo en segmentos, pero ahora cada segmento podía tener hasta 4Gb de tamaño.
- Los segmentos se pueden subdividir en pequeñas unidades de 4Kb de tamaño llamadas páginas. La memoria virtual trabaja ahora con páginas en lugar de segmentos. De esta manera, solo una parte del segmento puede estar en cualquier momento. En el modo protegido de 16 bits, solo se puede mover el segmento entero a la memoria, esto no hubiera sido muy práctico si consideramos los largos segmentos de 32 bits del 80386.
Existen cuatro modos principales para manejar los datos entre los registros, las direcciones de memoria, y las constantes. NUNCA podemos pasar un dato constante a una dirección de memoria directamente, siempre deberemos llevar el dato a un registro, y luego de este a la memoria.
Para ello debemos primero conocer que toda instrucción en Assembly, esta construida de la siguiente forma <instrucción> <destino>,<fuente>.
Estos modos se denominan:
- Modo directo: Se denomina así cuando en una instrucción se da directamente la dirección de memoria del dato a procesar. Ej.: MOV EAX,[5000] ,al estar el nro. 5000 entre [], le estamos indicando al procesador que mueva el dato que está en la dirección de memoria 0x5000 al registro EAX.
- Modo registro: Cuando el dato que necesitamos procesar ya se encuentra en un registro.
Ej.: MOV EAX,EBX ,aquí estamos indicando que lleve una copia del dato que está en EBX a EAX.
- Modo inmediato: Se denomina así cuando ordenamos pasar una constante a un registro.
Ej.: MOV EAX,3000 ,estamos pasando el nro. 3000 al registro EAX.
- Modo indirecto por registro: Cuando queremos pasar a un registro, el contenido del contenido de otro, es decir. EJ.: MOV EAX,[EBX], el contenido de la dirección de memoria almacenada actualmente en EBX, se cargará en EAX.
- Netwide Assembler (NASM).
- Microsoft Assembler (MASN).
- Borland Assembler (TASM).
Como existen algunas diferencias de sintaxis entre el MASM, TASM, y el NASM, hemos elegido a este último para el desarrollo de algunos de nuestros ejercicios de 32 bits. El mismo se puede obtener en forma gratuita de http://sourceforge.net/projects/nasm.
Para los ejercicios basados en 16 bits, usaremos el DEBUG de DOS. Simplemente salimos desde cualquier versión de Windows al MS-DOS Prompt (o símbolo de sistema en la versiones 2000 y XP), y tipeamos DEBUG. Una vez que entramos al programa, elegimos una dirección de memoria inicial para cargar las instucciones (Ej.: A100), al finalizar con este proceso ingresamos ENTER y luego tipeamos la "t" para ver el estado de los registros en cada una de las instrucciones. Para salir tipeamos "q".
Por fin vamos a empezar con instrucciones en Assembly. Y comenzaremos con la más sencilla, pero curiosamente la más utilizada en este lenguaje:
La instrucción MOV:
La función de la instrucción MOV, es como su nombre da a entender, "mover" un valor. Pongamos un ejemplo:
MOV AX,BX
Esta instrucción copia el contenido de BX en AX, conservando el valor de BX.
Veamos algunos ejemplos más:
MOV AX,2000
MOV [a100],AX
En este caso introducimos el valor 2000 en AX, y luego lo llevamos a la dirección de memoria A100.
En resumen, con la instrucción MOV podemos hacer las siguientes operaciones:
Tipo de operación | Ejemplo |
MOV registro,registro | MOV AX,BX |
MOV memoria,registro | MOV [2000],AX |
MOV registro,memoria | MOV BX,[1500] |
MOV registro,constante | MOV AX,1000 |
Las instrucciones INC y DEC:
Son las más básicas a la hora de hacer operaciones con registros:
INC, incrementa el valor de un registro (o de cualquier posición de memoria ) en una unidad.
DEC, lo decrementa en el mismo valor. Ejemplos:
INC AX
DEC AX
Incrementa en una unidad el valor de AX y lo decrementa, por lo que no ha cambiado su valor.
Estas dos instrucciones nos van a servir mucho en el uso de contadores en los bucles. Son equivalentes a ++ y — del lenguaje C.
Las instrucciones ADD y SUB:
Se trata de dos operadores que contienen todos los lenguajes de programación: la suma y la resta. Tienen dos operandos, uno de destino y otro fuente.
Para la suma, se suman los dos operandos y se almacena el resultado en el primero (destino).
Para la resta, se le resta al primero el segundo, y se almacena el resultado en el primero (destino).
Ejemplos:
ADD AX,BX ;Suma a AX con BX, y lo guarda en AX. (AX+BX) -> AX
ADD AX,[SI] ;Se suman AX y el contenido de lo que esta apuntado por SI
;y se almacena en AX. (AX+[SI]) -> AX
ADD AX,3 ;Suma 3 a AX y lo almacena en AX. (AX+3) -> AX
SUB AX,AX ;Resta AX de AX. Se utiliza para poner a AX en 0.
;(AX-AX=0) -> AX
SUB CX,DX ;Se resta el valor de CX con DX y se almacena en CX.
;(CX-DX) -> CX
SUB CX,20 ;Se resta de CX el valor 20, y queda en CX el resultado.
;(CX-20) -> CX
(Introducimos el uso de la ";" para hacer comentarios. Es lo mismo que usar en lenguaje C la // ó /*…….*/)
Estas operaciones pueden modificar los FLAGS de estado si se producen acarreos en las sumas o números negativos en las restas.
Estos casos los trataremos más adelante.
Multiplicación y división, MUL y DIV:
Estas operaciones multiplican o dividen al acumulador (AX) por el operando indicado. Si el operando es de 8 bits (1 byte ), el acumulador es AL. Si el operando es de 16 bits, el acumulador es AX. En el caso de procesadores de 32 bits, si el operando tiene 8 ó 16 bits el acumulador será AX. El resultado se almacena en AX, ó en el par DX, AX respectivamente, si el operando es de 8 bits ó 16 bits.
En el caso de la división, si hubiera resto se guardará en DX.
Ejemplos:
Multiplicación de dos números de 8 bits:
MOV CL,20 ;Cargo en CL un número de 8 bits (0x20)
MOV AL,30 ;Cargo en AL un número de 8 bits (0x30)
MUL CL ;Multiplico CL por AL, y guardo el resultado en AX
;(AL*CL) -> AX
Multiplicación de dos números de 16 bits:
MOV BX,1000 ;Cargo en CX un número de 16 bits (0x2000)
MOX AX,1200 ;Cargo en AX un número de 16 bits (0x1200)
MUL BX ;Multiplico BX por AX, y guardo el resultado en DX (mitad
;superior y en AX (mitad inferior). (AX*BX) -> (DX y AX)
División de dos números de 8 bits:
MOV AX,30 ;Cargo en AX un número de 8 bits (0x30)
MOV BX,12 ;Cargo en BX un número de 8 bits (0x10)
DIV BX ;Divido AX/BX, y guardo el resultado en AX. El resto de la
;división se guardará en DX
División de un número de 32 bits por uno de 16 bits:
MOV AX,5000 ;Cargo en AX los 16 bits inferiores de un número de 32
;bits (0x65005000)
MOV DX,6500 ;Cargo en DX los 16 bits superiores del número
;anterior (0x65005000)
MOV CX,2500 ;Cargo en CX un número de 16 bits que será el divisor
;(0x2500)
DIV CX ;Divido (DX,AX)/CX y guardo el resultado en AX. El
;resto de la división se guardará en DX
Pero el procesador puede diferenciar entre operaciones con números con signo y sin signo. Cuando utilicemos números que involucran las operaciones multiplicación y división de números con signo, utilizaremos las instrucciones IMUL y IDIV respectivamente.
Son algunos ejercicios de operaciones aritméticas y modos de direccionamiento:
1) Indique que realizan las siguientes instrucciones, aclarando en cada caso los modos de direccionamiento utilizados:
MOV AX,10
MOV BX,[100]
ADD CX,[AX]
ADD BX,2
DEC CX
2) Realice un programa que haga la siguiente operación A=B+B-C. El dato B se encuentra en la dirección 5000, el C en 5006, y el resultado A hay que alojarlo en 5010.
3) Realice un programa que haga la siguiente operación A=((B+C)*B)/C. Utilice los mismos datos del ejercicio anterior, haciendo que el resto de la división se aloje en la dirección 2000.
Operaciones lógicas NEG, NOT, AND, OR, XOR:
NEG, pone el registro o el contenido de una dirección de memoria en negativo según la aritmética de complemento a dos (NEG AX, NEG [2000]).
NOT, ordena invertir cada bit del operando. Supongamos que AX = 0x1000, al hacer NOT AX, el contenido de AX cambiará a 0xEFFF.
AND, realiza la operación lógica entre dos valores y guarda el resultado en el valor destino.
OR, realiza la operación lógica entre dos valores y guarda el resultado en el valor destino.
XOR, realiza la operación lógica entre dos valores y guarda el resultado en el valor destino.
Instrucción de salto incondicional, JMP:
Se puede cambiar el control a cualquier punto del programa sin condiciones.
Se utiliza de la siguiente forma:
JMP 03424h
Donde 03424h, es la dirección a la cual queremos saltar.
¿Pero como hacemos para calcular la dirección de donde va a estar la instrucción a la cual queremos avanzar?
Para ello utilizaremos etiquetas y saltaremos directamente al nombre de la etiqueta.
Ejemplo: (entre paréntesis se muestra el orden de ejecución de estas instrucciones)
(1) MOV AX,1000 ;Cargo en AX el número 0x1000
(2) MOV CL,22 ;Cargo en CL el número 0x22
(3) JMP AVANZA ;Llamo a la instrucción etiquetada como "AVANZA"
(6) VUELVE: ADD BX,AX ;Sumo a BX el valor de AX (BX=0x2000)
(7) JMP FIN ;Llamo a la instrucción etiquetada como "FIN"
(4) AVANZA: MOV BX,AX ;Cargo en BX el valor de AX (BX=0x1000)
(5) JMP VUELVE ;Llamada a la etiqueta "VUELVE"
(8) FIN: SUB CX,BX ;Sumo a CX el valor de BX (CX=0x2022)
Las etiquetas tienen que estar continuadas por dos puntos ':', y pueden ser llamadas desde cualquier lugar del programa. También podremos hacer un MOV AX,[AVANZA], como hacíamos antes con un MOV AX,[BX], pero asignando a AX el valor que haya en la dirección de memoria en la que está "AVANZA".
La explicación de los "flags" está relacionada con el uso de los saltos condicionales. Utilizaremos algunos de estos flags para instrucciones donde haya que tomar decisiones (del tipo IF-THEN-ELSE, WHILE, ó FOR en otros lenguajes).
Como se explicó en la introducción de este apunte, los flags (banderas) los agrupa un solo registro de 16 bits (denominado registro de estados S). Aunque este no esté utilizado por completo, ya que cada flag ocupa un solo bit, cada uno de estos pueden valer 1 ó 0 y dependiendo de su valor indican varias cosas. El registro de flags es el siguiente:
Número de bit | Nombre | Utilización |
0 | C | Carry. Se pone en 1 si se produjo acarreo en la última operación realizada. |
1 | — | No utilizado. |
2 | P | Paridad. Se utiliza especialmente en la transmisión de datos para la comprobación de errores, ya que comprueba si el resultado de la última operación aritmética ó lógica realizada tiene un número par o impar de bits puestos a uno. Se pondrá en 1 cuando haya un número par de bits, y a 0 cuando sea impar. |
3 | — | No utilizado. |
4 | A | Auxiliar Carry. Se utiliza como verificador auxiliar de acarreos. |
5 | — | No utilizado. |
6 | Z | Zero. Es el que más interviene en las operaciones con bucles. Simplemente se activa a 1 cuando el resultado de una operación aritmética ó lógica es cero o devuelve verdadero. Por ejemplo, si tenemos dos valores iguales en CX y AX y hacemos SUB CX,AX el flag Z se pondrá en 1. Lo mismo pasa con una operación lógica, si esta es verdadera, Z se pondra en 1. |
7 | S | Sign (o signo). Indica cuando tras una operación aritmética ó lógica el resultado es un número en complemento a dos, por lo tanto, cuando vale 1 es que el número es negativo y si vale 0 es positivo. |
8 | T | Trap. Trampa, si está activado ejecuta una interrupción INT 1h cada vez que quiera ejecutarse otra instrucción. |
9 | I | Interruption. Es el flag de interrupción, cuando está activado evita la posibilidad de interrupciones en secciones críticas de código, |
10 | D | Flag de rumbo ó dirección y sentido. Determina si se han de autoincrementar ó autodecrementar los punteros SI y DI. |
11 | O | Overflow (o desbordamiento). Es bastante parecido al de acarreo pero actúa con números en complemento a dos, y se activa cuando se pasa del mayor número positivo (127 en un solo byte), al menor negativo (-128 en un solo byte). Este flag, al contrario que el de acarreo, sí es afectado por las instrucciones de incremento y decremento. |
12 | — | No utilizado. |
13 | — | No utilizado. |
14 | — | No utilizado. |
15 | — | No utilizado. |
Intrucciones de comparación, CMP y TEST:
CMP compara dos registros, o un registro y una dirección de memoria. Tiene el mismo formato que el SUB (por ejemplo CMP AX,BX), tan solo que ninguno de los registros es alterado. Si por ejemplo son iguales, el flag de cero Z se pondrá en 1. Funciona como un SUB del que no se almacena el resultado.
TEST realiza una comprobación, trabaja igual que AND, pero no guarda el resultado en ningún lado, aunque sí se modifican los flags.
Saltos condicionales:
Los saltos condicionales dependen exclusivamente de los Flags, son condicionados al estado 1 ó 0 del Flag que verifican, y son los siguientes:
(Aclaración: todos los saltos se preceden de la comparación que desea verificar una condición, dichas comparaciones se expresan comparando al primer miembro con el segundo)
JO: | Jump if Overflow. Salta si el flag de desbordamiento está en 1. |
JNO: | Jump if Not Overflow. Salta si el flag de desbordamiento esta en 0. |
JC: | Jump if Carry. Salta si el flag de acarreo es 1. |
JNAE: | Jump if Not Above or Equal. Salta si no es mayor o igual. |
JB: | Jump if Below. Salta si es menor. |
JNC: | Jump if Not Carry. Salta si el flag de acarreo es 0. |
JAE: | Jump if Above or Equal. Salta si es mayor o igual. |
JNB: | Jump if Not Below. Salta si no es menor. |
JZ: | Jump if Zero. Salta si el flag de cero Z es 1. |
JE: | Jump if Equal. Salta si dos instrucciones comparadas son iguales (CMP 0,0) |
JNZ: | Jump if Not Zero. Salta si el flag de cero Z es 0. |
JNE: | Jump if Not Equal. Salta si dos instrucciones comparadas son distintas (CMP 3,4) |
JBE: | Jump if Below or Equal. Salta si es menor o igual. |
JNA: | Jump if Not Above. Salta si no es mayor. |
JA: | Jump if Above. Salta si es mayor. |
JNBE: | Jump if Not Below or Equal. Salta si no es menor o igual. |
JS: | Jump if Sign. Salta si el flag de signo S es 1. |
JNS: | Jump if Not Sign. Salta si el flag de signo S es 0. |
JP: | Jump if Parity. Salta si el flag de paridad P es 1. |
JPE: | Jump if Parity Even. Idem anterior. |
JNP: | Jump if Not Parity. Salta si el flag de paridad P es 0. |
JPO: | Jump if Parity Odd. Idem anterior. |
JL: | Jump if Less. Salta si es menor. (para números con signo) |
JNGE: | Jump if Not Greater or Equal. Salta si no es mayor o igual. (para números con signo) |
JGE: | Jump if Greater or Equal. Salta si es mayor o igual. (para números con signo) |
JNL: | Jump if Not Less. Salta si no es menor. (para números con signo) |
JLE: | Jump if Lower or Equal. Salta si es menor o igual. (para números con signo) |
JNG: | Jump if Not Greater. Salta si no es mayor. (para números con signo) |
JG: | Jump if Greater. Salta si es mayor. (para números con signo) |
JNLE: | Jump if Not Lower or Equal. Salta si no es menor o igual. (para números con signo) |
Veamos algunos ejemplos de los más utilizados:
MOV AX,1111 ;Cargo 0x1111 en AX
MOV BX,1112 ;Cargo 0x1112 en BX
CMP AX,BX ;Comparo AX con BX (en este caso AX es menor que BX)
JB AVANZA ;Saltar a "AVANZA" (si AX<BX)
AVANZA: DEC BX ;Ahora BX vale 0x1111
CMP AX,BX ;Comparo AX con BX (ahora valen lo mismo)
JNE FIN ;Saltar a "FIN" (si AX!=BX, como son iguales no salta)
JE CONT ;Saltar a "CONT" (si AX=BX, como son iguales salta)
CONT: DEC BX ;Ahora BX vale 0x1110
CMP AX,BX ;Comparo AX con BX (ahora AX es mayor que BX)
JE FIN ;Saltar a "FIN" (si AX=BX, como no son iguales no salta)
JB FIN ;Saltar a "FIN" (si AX<BX, como no es menor, no salta)
JG FIN ;Saltar a "FIN" (si AX>BX, Es mayor, ahora si salta)
FIN: SUB AX,AX ;Pone en cero AX
XOR BX,BX ;Hace XOR de BX con BX (otra manera de poner en cero)
La pila es una especie de "almacén de variables" que se encuentra en una dirección determinada de memoria (dirección que viene indicada por SS:SP).
Entonces nos encontramos con dos órdenes básicas respecto al manejo de la pila, que son PUSH y POP. La orden PUSH empuja una variable a la pila, y la orden POP la saca.
Sin embargo, no podemos sacar el que queramos, debemos cuidar el orden natural de la estructura de una pila. Esta se denomina LIFO (siglas en inglés que indican "Last In First Out"). Esto significa que al hacer un POP, se saca el último valor introducido en la pila.
Veamos un par de ejemplos:
PUSH DX ;Mete en la pila el contenido de DX.
PUSH CX ;Mete en la pila el contenido de CX.
POP AX ;Saca de la pila su último valor (CX), y lo coloca en AX
POP BP ;Saca de la pila su último valor (DX), y se lo asigna a BP.
MOV DX,300 ;Cargo en DX el número 0x0300.
PUSH DX ;Empuja DX a la pila (0x0300).
MOV CX,200 ;Cargo en CX el número 0x0200.
PUSH CX ;Meto en la pila el contenido de CX (0x0200).
POP AX ;Saco de la pila el número 0x0200 y lo cargo en AX.
POP BX ;Saco de la pila el número 0x0300 y lo cargo en BX.
ADD AX,BX ;Sumo BX en AX. AX vale 0x0500.
La pila se puede operar con los registros AX, BX, CX, DX, SI, DI, BP, SP, CS, DS y ES, sin embargo no se puede hacer un POP CS, solamente empujarlo a la pila.
Como última recomendación, hay que tener bastante cuidado con los PUSH y POP, sacar tantos valores de la pila como se metan, y estar pendiente de que lo que se saca es lo que se tiene que sacar. La pila bien aprovechada es fundamental para hacer programas bien optimizados.
Y finalmente, hay otras dos órdenes interesantes respecto a la pila, PUSHF y POPF, que empujan el registro de estado de 16 bits (flags) a la pila, y lo saca respectivamente.
Se trata de una orden que se utiliza para llamar a subrutinas, y está relacionada con el uso de la pila,
La sintáxis del CALL es casi la de un JMP, pudiéndose también utilizar etiquetas, direcciones inmediatas o registros.
Su forma de trabajo es sencilla. Empuja a la pila los valores de CS e IP (o sea, los del punto en el que estén en ese momento del programa) aunque IP aumentado en el tamaño del call para apuntar a la siguiente instrucción, y hace un salto a la dirección indicada. Cuando encuentre una instrucción RET, sacará CS e IP de la pila, y así retornará al lugar de origen. Veamos un ejemplo:
SUB AX,AX ;Ax vale ahora 0
CALL CHAU ;Mete CS e IP a la pila y salta a "CHAU"
INT 20 ;Finaliza
CHAU: MOV AX,30 ;Carga un número en AX
RET ;Saca a CS e IP de la pila y vuelve a la instrucción
;siguiente al punto de llamada, o sea, a "INT 20"
Ejercicios (2):
Luego de la introducción de todos estos conceptos, vamos a hacer unos ejercicios.
1) Realice un programa que calcule al producto de dos números, como una suma. M*N=(M+M+M+……M) (N veces). (M está en 3000, N en 2000, y el resultado guardarlo en AX)
2) Dada una lista de números de un byte que empieza en la dirección 2000, y su longitud total esta guardada en la dirección 1500, encuentre el número mayor de toda la lista y guárdelo en la dirección 3000.
3) Se tiene una lista de números que comienzan en la dirección 2000, y su longitud está en 1500. Contar cuantos números negativos y positivos hay y separarlos en dos listas que empiecen en 3000 y 4000 respectivamente. El total de negativos guardarlo en 1600, y el de positivos en 1700.
4) Se tiene una lista de números enteros de un byte cada uno que empieza en la dirección 2000, y su longitud está en 1500. Pasar los números a otra lista donde cada número ocupe 16 bits, de modo que a los números con bit de signo 0 se le agreguen 8 ceros a la izquierda, y a los números con bit de signo 1 se le agreguen 8 unos a la izquierda.
5) Realice el cálculo del determinante de una matriz de 2×2.
6) Realice el cálculo del determinante de un matriz NxN. Ayuda: Intente comparar los pasos que va realizando, con los hechos en otro lenguaje de más alto nivel (tipo C, ó C++)
Ejercicios sección (1):
1)
1º) Cargo en AX el valor 0x10 en modo inmediato.
2º) Cargo en BX el valor que está almacenado en la dirección de memoria 0x0100 en modo directo.
3º) Sumo a CX el valor que está en la dirección de memoria 0x0010, ya que por el 1er. paso AX tiene el dato 0x10. Este modo se denomina indirecto por registro.
4º) Sumo 2 a BX.
5º) Resto uno a CX.
2)
MOV AX,[5000] ;Cargo en AX el dato B que está en 5000 y 5001 de memoria
ADD AX,[5000] ;Sumo en AX el dato B que está en 5000 y 5001 de memoria
SUB AX,[5006] ;Resto a AX el dato C que está en 5006 y 5007 de memoria
MOV [5010],AX ;Cargo en 5010 y 5011 de memoria, el contenido de AX
3)
MOV AX,[5000] ;Cargo en AX el dato B que está en 5000 y 5001 de memoria
ADD AX,[5006] ;Sumo a AX el dato C que está en 5006 y 5007 de memoria
MOV CX,[5000] ;Cargo en CX el dato B
IMUL CX ;Hace la operación CX*AX y guarda el resultado en (DX,AX)
MOV CX,[5006] ;Cargo en CX el dato C que está en 5006 y 5007 de memoria
IDIV CX ;Realizo la división (DX,AX)/CX
MOV [5010],AX ;Llevo a la dirección 5010 el resultado que está en AX
MOV [2000],DX ;Llevo a la dirección 2000 el resto de la división que
;está en DX.
Ejercicios de la sección (2):
1)
MOV CX,[2000] ;Llevo a CX el valor de N
MOV BX,[3000] ;Llevo a BX el valor de M
SUB AX,AX ;Pongo en cero a AX
RUTINA: ADD AX,BX ;Sumo M a AX
DEC CX ;Resto uno a CX (cuando CX sea 0, Z=1)
JNZ RUTINA ;Si luego de la instrucción anterior Z=0, saltar a
;"RUTINA"
INT 20 ;Fin
2)
MOV CL,[1500] ;Cargo en CL la longitud de la lista
MOV SI,2000 ;Apunto SI al comienzo de la lista de datos
MOV AL,[SI] ;Llevo el primer elemento de la lista a AL
INC SI ;Incremento el puntero para buscar el segundo dato
DEC CL ;Decremento el contador de elementos de la lista
VOLVER: CMP AL,[SI] ;Comparo al número que está en AL con el que está
;siendo apuntado por SI
JA RUTINA ;Si AL>[SI] ir a "RUTINA"
MOV AL,[SI] ;Carga en AL el nuevo número que apunta SI
RUTINA: INC SI ;Incrementa SI para buscar el próximo elemento
DEC CL ;Decrementa el contador de elementos de la lista
JNZ VOLVER ;Mientras Z=0, volver a "VOLVER"
MOV [3000],AL ;Guarda en 3000 el resultado (el número mayor)
INT 20 ;Finaliza
3)
MOV CL,[1500] ;Cargo la longitud de la lista en el contador
MOV SI,2000 ;El registro SI apunta al comienzo de la lista
MOV DI,3000 ;DI apunta al comienzo de la lista que guardará
;los negativos
MOV BX,4000 ;BX apunta al comienzo de la lista que guardará
;los positivos
SUB DX,DX ;Llevo DX a cero
SUB_1: MOV AX,[SI] ;Cargo en AX el primer dato apuntado por SI
CMP AX,0 ;Comparo este dato con el 0
JL SUB_2 ;Si AX<0, saltar a "SUB_2"
JG SUB_3 ;Si AX>0, saltar a "SUB_3"
SUB_4: ADD SI,2 ;Suma 2 bytes a SI para buscar el próximo elemento
DEC CL ;Decrementa el contador
JNZ SUB_1 ;Si Z=0, volver a "SUB_1"
MOV [1600],DL ;Guarda el negativo en 1600
MOV [1700],DH ;Guarda el positivo en 1700
INT 20 ;Fin
SUB_2: INC DL ;Incrementa a DL
MOV [DI],AX ;Guarda el negativo en la lista apuntada por DI
ADD DI,2 ;Suma 2 bytes a DI
JMP SUB_4 ;Saltar a "SUB_4"
SUB_3: INC DH ;Incrementa a DH
MOV [BX],AX ;Guarda el positivo en la lista apuntada por BX
ADD BX,2 ;Suma 2 bytes a BX
JMP SUB_4 ;Saltar a "SUB_4"
4)
MOV CL,[1500] ;Cargo en CX la longitud de la lista
MOV SI,2000 ;Apunto a SI al comienzo de los datos
MOV DI,3000 ;Apunto DI al comienzo de la lista resultados
CARGA: MOV AL,[SI] ;Carga en AL el primer elemento de la lista
CMP AL,0 ;Compara AL contra 0
JGE SUB_1 ;Si AL>=0, saltar a "SUB_1"
JL SUB_2 ;Si AL<0, saltar a "SUB_2"
REFRESH: INC SI ;Incremento SI para buscar el próximo elemento
ADD DI,2 ;Sumo 2 a DI (los resultados ocupan 2 bytes)
DEC CL ;Decremento en uno el contador (CL=0 -> Z=1)
JNZ CARGA ;Si Z=0, saltar a "CARGA"
INT 20 ;Fin
SUB_1: MOV AH,00 ;Carga 8 ceros en AH
MOV [DI],AX ;Carga el número resultante en la lista que
;apunta DI (número de 16 bits)
JMP REFRESH ;Saltar a "REFRESH"
SUB_2: MOV AH,FF ;Carga 8 unos en AH
MOV [DI],AX ;Carga el número resultante en la lista que
;apunta DI (número de 16 bits)
JMP REFRESH ;Saltar a "REFRESH"
5)
;NASM-IDE ASM Assistant Assembler Project File
BITS 16 ;Set code generation to 16 bit mode
ORG 0x0100 ;Set code start address to 0100h
SEGMENT .data ;Initialised data segment
matriz1 dw 1,2,3,4 ;matriz inicializada con datos 1,2,3,4
inicioFila dw 0 ;por donde comienzo a multiplicar
SEGMENT .bss ;Uninitialised data segment
matriz2 resw 4 ;reservo espacio para la matriz resultado
resul resw 1
contador resw 1
SEGMENT .text ;Main code segment
mov si,matriz1
mov di,matriz2
call CALCULAR
mov word [inicioFila],4 ;siempre debo especificar el tipo cuando son cte
call CALCULAR
mov ax, $4C00 ;Prepare to exit
int 20 ;Terminate program
CALCULAR:
xor cx,cx ;inicializo contador
BUCLE:
mov [contador],cx ;guardo contador ya que voy a usar cx
mov bx,[inicioFila] ;por que fila examino la matriz1
mov cx,[si+bx] ;valor en la posici¢n examinada
mov bx,[contador] ;por que columna examino matriz1
mov ax,[si+bx] ;valor
imul cx ;multiplico
mov [resul],ax ;guardo resultado parcial
mov bx,[inicioFila] ;restauro bx
mov cx,[si+bx+2] ;por que fila examino la matriz1
mov bx,[contador] ;por que columna examino matriz1
mov ax,[si+bx+4] ;valor
imul cx ;multiplico
add [resul],ax ;obtengo resultado final Aij
mov bx,[inicioFila] ;fila
add bx,[contador] ;adiciono a fila, la columna
mov ax,[resul] ;coloco el resultado final en ax
mov [di+bx],ax ;guardo resultado en matriz2
mov cx,[contador] ;restauro valor contador
add word cx,2 ;incremento en 1 word el contador (2 bytes)
cmp cx,6 ;si contador != 6
jne BUCLE ;volver a BUCLE
ret ;si no, regresar
6) Antes de resolver este ejercicio, es bueno estudiar un poco el proceso de cálculo en un lenguaje de mayor nivel, ya que son muchos los llamados a subrutinas y va a resultar un poco engorroso entederlo directamente. Este ejemplo esta realizado en C++.
int det(int *M,int N){
if(N==2){
return((M[0]*M[3])–(M[1]*M[2]));
}
else{
int new_dimension=(N-1);
int tdet =0;
int *temp =new int[(N-1)*(N-1)];
for(int ref_ele=0;ref_ele<N;ref_ele++){
int counter=0;
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
if((j!= ref_ele)&&(i!=0)){
temp[counter]=M[(N*i + j)];
counter++;
}
}
}
int t = det(temp, new_dimension);
if((ref_ele % 2)!= 0)
t =(-1)*t;
tdet += M[ref_ele]*t;
}
return tdet;
}
}
El algoritmo trabaja haciendo llamados recursivos de sub-cálculos si N>2 hasta que sea N=2 para hacer el cálculo en forma directa de filas por columnas. Para cada matriz M(i,j), se necesita determinar la sub-matriz que queda formada de no contener a la fila y columna del dato que queremos calcular
SEGMENT .bss
M resw 153 ;Reservo espacio para las matrices temporales
;(25+(5*16)+(4*9)+(4*3))
N resw 1 ;Reservo espacio para el N de la matriz
SEGMENT .text
MOV [N],3 ;cargo el orden de la matriz en N (Ej. 3)
MOV SI,M ;inicio de la matriz
MOV AX,[N] ;cargo el N de la matriz en ax (3)
PUSH SI ;coloco en la pila a si
PUSH AX ;coloco en la pila a ax (N)
CALL det ;Llamo a det
ADD SP,4 ;renuevo parametros de la pila
INT 20 ;Fin
det:
PUSH BP ;guardo el estado del puntero base
MOV BP,SP ;apunto bp al tope de la pila
CMP [BP+4],2 ;comparo si N=2
JNE else ;si no es igual, ir a else
MOV SI,[BP+6] ;si es igual, comienza el calculo empezando por apuntar
;a SI al comienzo de la matriz de 2×2 (elemento a00)
MOV AX,[SI] ;cargo en ax el primer elemento de la matriz (el a00)
MOV BX,[SI+6] ;cargo en bx el ultimo elemento de la matriz (el a11)
IMUL BX ;multiplico bx*ax (a00 * a11)
MOV CX,AX ;guardo el resultado de la primera multiplicacion en cx
MOV AX,[SI+2] ;cargo en ax el ultimo elemento de la fila 0 (el a01)
MOV BX,[SI+4] ;cargo en bx el primer elemento de la fila 1 (el a10)
IMUL BX ;multiplico bx*ax (a01 * a10)
SUB CX,AX ;resto el resultado de ambas multiplicaciones
MOV AX,CX ;devuelvo el resultado final en ax
MOV SP,BP ;restauro el tope de la pila con el valor anterior de
;la funcion
POP BP ;restauro el puntero base con el valor anterior de la
;funcion
RET ;Retorno
else:
SUB SP,16 ;guardo espacio para 8 variables que voy a utilizar
MOV AX,[BP+4] ;guardo en ax el valor N de la matriz original
DEC AX ;decremento AX
MOV [BP-2],AX ;bp-2 es una nueva variable (newdim) que contiene la
;nueva dimension de la matriz (N-1)
MOV [BP-4],0 ;bp-4 es una nueva variable =0 (dettemp) que usare como
;determinante temporal
MOV SI,[BP+6] ;apunto si al comienzo de la matriz (*M)
MOV AX,[BP+4] ;guardo el orden de la matriz
IMUL AX ;opero N * N (en este ejemplo = 9)
MOV BX,2 ;cargo 2 en bx
IMUL BX ;multiplico ax*2 (=18, bytes que utilizare para
;reservar en memoria)
ADD SI,AX ;apunto si al comienzo de la nueva matriz temporal
;(+ 18 bytes)
MOV [BP-6],SI ;bp-6 es una nueva variable (*temp) que contiene la
;dirección de la nueva matriz temporal (o adjunta)
MOV [BP-8],0 ;bp-8 es una nueva variable (que llamare ref_ele)
for1:
MOV CX,[BP+4] ;cargo N en cx
CMP [BP-8],CX ;comparo si ref_ele es < N
JNL endfor1 ;Saltar si [BP-8] no es menor que CX
MOV [BP-10],0 ;bp-10 es una nueva variable =0 (contador)
MOV [BP-12],0 ;bp-12 es una nueva variable =0 (i)
for2:
CMP [BP-12],CX ;comparo a i con N
JNL endfor2 ;saltar si [BP-12] no es menor que CX
MOV [BP-14],0 ;bp-14 es una nueva variable =0 (j)
for3:
CMP [BP-14],CX ;comparo a j con N
JNL endfor3 ;saltar si [BP-14] no es menor que CX
MOV AX,[BP-14] ;en ax cargue j
if1:
CMP AX,[BP-8] ;comparo si j es != ref_ele
JE endif1 ;Saltar si AX=[BP-8]
CMP [BP-12),0 ;comparo si i!=0
JE endif1 ;saltar si [BP-12]=0
MOV AX,[BP-12] ;cargo i en ax
IMUL CX ; N * i
ADD AX,[BP-14] ; N * i + j (formula para recorrer matrices)
MOV BX,2 ;cargo 2 en BX
IMUL BX ;multiplico a ax por 2 ya que recorro la matriz de a 2
;bytes
MOV BX,AX ;guardo el desplazamiento de N*i+j en bx
MOV SI,[BP+6] ;apunto si a M (primer elemento)
MOV DX,[SI+BX] ;coloco en dx M[(N*i+j)]
MOV BX,[BP-10] ;coloco en bx a contador
MOV SI,[BP-6] ;apunto si a la matriz temporal
MOV [SI+BX],DX ;temp[contador]=M[(N*i+j)]
ADD [BP-10],2 ;actualizo a contador (+2 bytes)
endif1:
ADD [BP-14],2 ;j++
JMP for3
endfor3:
ADD [BP-12],2 ;i++
JMP for2
endfor2:
MOV BX,[BP-6] ;inicio de matriz
MOV AX,[BP-2] ;orden matriz
PUSH BX ;coloco este parametro en pila
PUSH AX ;coloco este parametro en pila
CALL det ;Llamo a det
ADD SP,4 ;renuevo la pila con los parametros anteriores
MOV [BP-16],AX ;devuelvo el resultado de la funcion en ax
if2:
MOV AX,[BP-8] ;ax=ref_ele
MOV BX,2 ;cargo 2 en bx
IDIV BX ;relizo división
CMP DX,0 ;(ref_ele%2)!=0
JE endif2 ;saltar si DX=0
MOV AX,[BP-16] ;ax=t
MOV BX,-1 ;cargar –1 en BX
IMUL BX ;t=(-1)*t
endif2:
MOV BX,[BP-8] ;cargo a bx con ref_ele
MOV SI, [BP+6] ;apunto a M
MOV AX,[SI+BX] ;M[ref_ele]
IMUL [BP-16] ;M[ref_ele]*3
ADD [BP-4],AX ;tdet+=M[ref_ele]*3
ADD [BP-6],2 ;incremento a ref_ele (2 bytes)
JMP for1
endfor1:
MOV AX,[BP-4] ;en ax devuelvo el det
MOV SP,BP ;restauro la cima de la pila
POP BP ;restauro el puntero base
RET
Este trabajo se realizó con muy poca bibliografía. Se basó principalmente en las clases y consultas a profesores, papers correspondientes a la arquitectura de los procesadores Intel, y prácticas desarrolladas a modo de "prueba y error" en PC.
Los únicos libros consultados son:
- "Organización y Arquitectura de Computadores" William Stallings (4ta. Edición).
- "Principios de la Arquitectura de Computadoras" Miles Murdocca & Vincent Heuring (1999).
Agradecimientos:
- A todos los docentes que me ayudaron para poder recopilar toda la información aquí contenida.
- Intel Corporation, que me facilitó los papers de los procesadores que ya no fabrica hace muchos años.
- AT&T, que tambíen me facilitó algunos papers sobre el funcionamiento de algunos algoritmos que aquí se utilizan.
Diego Bellini
Rosario – República Argentina
Simplemente espero que les sea de mucha utilidad.