Assembler 8086
LINK AL CUADERNO CON TODA LA INFORMACIÓN DEL CAPITULO:
https://notebooklm.google.com/notebook/85b30ba6-b2c0-4697-ba52-2842f99c0906
Assembler 8086
Qué es código máquina
El código máquina, también conocido como lenguaje de máquina o código de operación, es el conjunto de instrucciones binarias que una computadora puede entender y ejecutar directamente. Estas instrucciones están compuestas por patrones de bits que representan operaciones básicas, como sumar, restar, cargar datos en la memoria, transferir datos entre registros y realizar operaciones lógicas.
El código máquina es el nivel más bajo de lenguaje de programación y es específico para cada arquitectura de procesador. Cada tipo de procesador tiene su propio conjunto de instrucciones y formato de codificación. Estas instrucciones se representan en forma de números binarios, normalmente en código hexadecimal, que se traducen directamente en operaciones físicas realizadas por el hardware de la computadora.
El código máquina es difícil de entender y escribir directamente para los programadores, ya que se compone de secuencias de bits y no es intuitivo. Por lo tanto, se desarrollaron lenguajes de programación de nivel más alto, como el lenguaje ensamblador y los lenguajes de alto nivel, para facilitar la programación y abstraerse de los detalles específicos del hardware.
El código máquina es la representación numérica de las instrucciones que el hardware de la computadora puede ejecutar directamente, y es el lenguaje de programación más básico y directamente comprensible para la máquina.
El lenguaje Assembler
El lenguaje ensamblador, también conocido como lenguaje Assembly o simplemente "Assembler", es un lenguaje de programación de bajo nivel que se encuentra entre el código máquina y los lenguajes de programación de alto nivel. El lenguaje ensamblador utiliza mnemónicos para representar las instrucciones del código máquina de una arquitectura de procesador específica.
A diferencia del código máquina que está compuesto por secuencias de bits, el lenguaje ensamblador utiliza palabras clave o símbolos mnemónicos más legibles para los programadores. Estos mnemónicos están asociados con las instrucciones del procesador, como cargar un valor en un registro, sumar dos valores o saltar a una dirección de memoria específica.
El lenguaje ensamblador permite un mayor nivel de abstracción y legibilidad en comparación con el código máquina, pero aún así está estrechamente relacionado con la arquitectura del procesador. Cada tipo de procesador tiene su propio conjunto de instrucciones y mnemónicos específicos.
Los programas escritos en lenguaje ensamblador deben ser traducidos a código máquina utilizando un programa llamado ensamblador (assembler). Este proceso se conoce como ensamblaje o ensamblado, y el resultado es un archivo ejecutable que la computadora puede entender y ejecutar directamente.
El lenguaje ensamblador es especialmente utilizado en programación de sistemas, donde se requiere un control preciso sobre el hardware de la computadora, como en la programación de controladores de dispositivos, sistemas operativos o software de tiempo real. También puede ser utilizado para optimizar partes críticas de un programa en lenguajes de alto nivel, aprovechando las características específicas del hardware.
El lenguaje ensamblador es un lenguaje de programación de bajo nivel que utiliza mnemónicos para representar las instrucciones del código máquina de una arquitectura de procesador específica. Proporciona un mayor nivel de legibilidad y abstracción en comparación con el código máquina, pero aún está muy relacionado con el hardware de la computadora.
Qué es un compilador
Un compilador es un programa informático que traduce un código fuente escrito en un lenguaje de programación de alto nivel a un código objeto o código ejecutable en un lenguaje de bajo nivel o código máquina. El objetivo principal de un compilador es convertir el código fuente legible por los programadores en un formato que pueda ser entendido y ejecutado directamente por el hardware de la computadora.
El proceso de compilación consta de varias etapas. En primer lugar, el compilador realiza un análisis léxico y sintáctico del código fuente para comprender su estructura y verificar su corrección gramatical. Luego, se realiza un análisis semántico para asegurarse de que las instrucciones del programa cumplan con las reglas y restricciones del lenguaje.
Después de las etapas de análisis, el compilador genera un código objeto, que es una representación intermedia del programa en un lenguaje de bajo nivel o en un formato específico para el sistema objetivo. El código objeto aún no es ejecutable, ya que puede contener referencias a funciones o librerías externas que deben resolverse.
En la etapa de enlace, el compilador resuelve las referencias pendientes y vincula el código objeto con las bibliotecas y rutinas necesarias para generar un archivo ejecutable final. Durante este proceso, se realizan optimizaciones adicionales para mejorar el rendimiento y reducir el tamaño del programa resultante.
El resultado final de la compilación es un archivo ejecutable que puede ser cargado y ejecutado directamente por el sistema operativo o el hardware de la computadora. Este archivo contiene el código en lenguaje de máquina, listo para ser interpretado y ejecutado por el procesador.
Los compiladores son herramientas fundamentales en el desarrollo de software, ya que permiten a los programadores escribir programas en lenguajes de alto nivel más legibles y abstractos, sin tener que preocuparse por los detalles específicos del hardware. Además, los compiladores también pueden realizar optimizaciones que mejoran el rendimiento del programa resultante.
Un compilador es un programa que traduce el código fuente escrito en un lenguaje de programación de alto nivel a un código ejecutable en lenguaje de máquina. Convierte el código fuente en un formato comprensible y ejecutable por el hardware de la computadora, simplificando el proceso de desarrollo de software y permitiendo la creación de programas más potentes y eficientes.
Qué es un intérprete
Un intérprete es un programa informático que lee, analiza y ejecuta instrucciones o comandos de forma secuencial, directamente desde el código fuente de un programa. A diferencia de un compilador, que traduce todo el código fuente a un formato ejecutable antes de su ejecución, un intérprete interpreta y ejecuta las instrucciones línea por línea a medida que se encuentran.
Cuando un intérprete ejecuta un programa, realiza una serie de pasos que incluyen el análisis léxico y sintáctico del código fuente, la interpretación de las instrucciones y la ejecución de las mismas. Cada instrucción es traducida y ejecutada en tiempo real, sin necesidad de crear un archivo ejecutable independiente.
Una característica importante de los intérpretes es que permiten una interacción más directa y dinámica con el programa en tiempo de ejecución. Esto significa que los cambios realizados en el código fuente se pueden probar inmediatamente, sin la necesidad de recompilar todo el programa. Los intérpretes también suelen proporcionar mensajes de error más detallados y flexibilidad para depurar el código.
Un ejemplo común de uso de un intérprete es el intérprete de comandos de un sistema operativo, como la línea de comandos en Unix/Linux o el símbolo del sistema en Windows. En este caso, el intérprete lee y ejecuta los comandos ingresados por el usuario de forma interactiva.
Otro ejemplo es el intérprete de Python, que lee y ejecuta instrucciones escritas en el lenguaje de programación Python. El intérprete de Python interpreta y ejecuta el código fuente de forma secuencial, línea por línea, permitiendo una ejecución rápida y una interacción directa con el programa en tiempo real.
Un intérprete es un programa que lee, analiza y ejecuta instrucciones o comandos de forma secuencial directamente desde el código fuente. Interpreta y ejecuta las instrucciones línea por línea, proporcionando una ejecución más interactiva y permitiendo cambios y pruebas rápidas en el código sin necesidad de recompilar todo el programa.
Arquitectura Intel 8086.
El Intel 8086 es un microprocesador de 16 bits desarrollado por Intel en la década de 1970. Fue el primer miembro de la familia de procesadores x86 y sentó las bases para la arquitectura de los microprocesadores modernos.
La arquitectura del Intel 8086 se caracteriza por los siguientes aspectos:
1. Conjunto de instrucciones: El Intel 8086 utiliza un conjunto de instrucciones de 16 bits, lo que significa que las instrucciones pueden operar en palabras de 16 bits (2 bytes) o en bytes individuales. Ofrece una amplia variedad de instrucciones para manipular datos, realizar operaciones aritméticas y lógicas, transferir datos entre registros y memoria, y controlar el flujo de ejecución.
2. Segmentación: El 8086 utiliza un esquema de segmentación de memoria, lo que significa que la memoria se divide en segmentos de 64 KB. Cada segmento se identifica mediante un puntero de segmento (registro de segmento) y un desplazamiento (registro de desplazamiento). Esta segmentación permite direccionar hasta 1 MB de memoria.
3. Modo de operación: El 8086 puede operar en dos modos: el modo real y el modo protegido. En el modo real, se ejecutan programas de manera compatible con los procesadores más antiguos, como el Intel 8080. En el modo protegido, se proporciona un entorno más avanzado con características de protección de memoria y multitarea.
4. Registros: El 8086 cuenta con una variedad de registros que se utilizan para diferentes propósitos. Incluye registros de propósito general (AX, BX, CX, DX), registros de índice (SI, DI, BP, SP), registros de segmento (CS, DS, SS, ES), registros de apuntador de instrucción (IP) y registros de banderas (FLAGS) para indicar condiciones y resultados de operaciones.
5. Arquitectura interna: El 8086 utiliza una arquitectura de tipo CISC (Complex Instruction Set Computer). Tiene una unidad de control que interpreta y ejecuta las instrucciones, una unidad aritmético-lógica (ALU) para realizar operaciones matemáticas y lógicas, y un conjunto de registros internos para almacenar datos temporales y resultados intermedios.
6. Interrupciones: El 8086 admite el manejo de interrupciones, lo que permite que el procesador responda a eventos externos o internos. Puede cambiar el flujo de ejecución a una rutina de interrupción específica para manejar eventos como entrada/salida, temporizadores o señales de hardware.
La arquitectura del Intel 8086 sentó las bases para los microprocesadores x86 posteriores, que se han utilizado ampliamente en la industria de la informática. Su diseño y conjunto de instrucciones influenciaron en gran medida el desarrollo de los sistemas operativos y el software en general. Aunque ha sido sucedido por procesadores más modernos, el legado del Intel 8086 sigue presente en muchos aspectos de la computación actual.

La computadora a bajo nivel es una máquina MUY simple.
La potencia de la computadora no está en ningunos de sus niveles específicamente sino en la posibilidad de ejecutar MUY RAPIDO instrucciones MUY SIMPLES.
A nivel de Assembler, la computadora tiene REGISTROS, que son unas memorias básicas para realizar operaciones. No puedo operar nada que no esté en registros. Y el resultado va en registros también.
También está la memoria, que es un gran array. Puedo llevar y traer de memoria a registros.
También están los buses por los cuales interactuamos para las interrupciones, etc.
También tenemos el stack que nos permite apilar y desapilar. Stack es LIFO. Last in first out.
Assembler 8086.
Es la programación de la computadora a bajo nivel. Cada instrucción de ASM se corresponde con una de CM.
Los registros del 8086
La arquitectura del Intel 8086 incluye una variedad de registros utilizados para diferentes propósitos. A continuación, describiré en detalle todos los registros presentes en el 8086:
1. Registros de propósito general de 16 bits:
- AX (Acumulador): Es utilizado para operaciones aritméticas y lógicas. Puede dividirse en dos registros de 8 bits, AH (byte alto) y AL (byte bajo), para acceder a los datos por separado.
- BX (Base): Se utiliza como un registro de base en operaciones de direccionamiento de memoria.
- CX (Contador): Es utilizado en instrucciones de bucles y como contador en operaciones de repetición.
- DX (Datos): Se utiliza como un registro de propósito general en operaciones aritméticas y lógicas. También es utilizado en operaciones de entrada y salida.
2. Registros de segmento de 16 bits:
- CS (Segmento de código): Almacena el segmento de código actual.
- DS (Segmento de datos): Almacena el segmento de datos actual.
- SS (Segmento de pila): Almacena el segmento de pila actual.
- ES (Segmento extra): Puede ser utilizado como un segmento adicional para almacenar datos.
3. Registros de índice de 16 bits:
- SI (Índice fuente): Se utiliza en operaciones de copia de bloques de datos.
- DI (Índice destino): Se utiliza en operaciones de copia de bloques de datos.
- BP (Base de pila): Se utiliza como un registro de base para acceder a datos en la pila.
- SP (Puntero de pila): Almacena la posición actual de la cima de la pila.
4. Registros de apuntador de instrucción de 16 bits:
- IP (Puntero de instrucción): Almacena la dirección de la próxima instrucción a ejecutar.
5. Registros de banderas de 16 bits (FLAGS):
- Los registros de banderas contienen bits que indican el estado o resultado de operaciones realizadas por el procesador. Algunas de las banderas importantes incluyen:
- CF (Carry flag): Indica un acarreo o desbordamiento en operaciones aritméticas.
- ZF (Zero flag): Indica si el resultado de una operación es cero.
- SF (Sign flag): Indica el signo del resultado de una operación.
- OF (Overflow flag): Indica un desbordamiento en operaciones aritméticas con signo.
Además de estos registros, el 8086 también tiene registros de propósito especial, como el registro de control de segmento (CS, DS, SS, ES), que almacenan los valores base de los segmentos, y el registro de temporizador (TIMER), que se utiliza para contar eventos de temporización.
Cabe destacar que en el modo real del 8086, los registros de segmento y el registro IP se combinan para formar una dirección física de 20 bits, mientras que en el modo protegido, se utilizan esquemas de paginación y segmentación más complejos.
Estos registros juegan un papel fundamental en el procesamiento y la ejecución de instrucciones en el 8086, permitiendo el acceso a datos, el control del flujo de ejecución y el almacenamiento de resultados temporales.
Registros de 16 bits. Con sus subregistros superior e inferior de 8 bits cada uno.
Son de uso general aunque normalmente AX es el acumulador, BX guarda operandos, CX contador.
Hay otros registros.
SP stack pointer BP base pointer……. Indican el stack
IP instruction ponter…………. Indica por dónde vamos ejecutando.
Hay registros específicos para la gestión de segmentos, CS DS ES SS, que son un truco que permite trabajar direcciones de 20 bits en base a un bus de 16 bits.
Operaciones aritméticas en el Assembler 8086
En el lenguaje ensamblador para el procesador Intel 8086, se pueden realizar las operaciones aritméticas básicas, como la suma, la resta, la multiplicación y la división. A continuación, cómo se pueden llevar a cabo estas operaciones:
Suma:
Para sumar dos números en ensamblador 8086, puedes utilizar la instrucción "ADD" (añadir). Por ejemplo, si quieres sumar los valores de dos registros AX y BX y almacenar el resultado en el registro AX, puedes utilizar la siguiente instrucción:
ADD AX, BX
Esto sumará los valores de los registros AX y BX y almacenará el resultado en el registro AX.
Resta:
Para restar dos números en ensamblador 8086, se utiliza la instrucción "SUB" (restar). Por ejemplo, si quieres restar el valor de un registro BX del registro AX y almacenar el resultado en el registro AX, puedes utilizar la siguiente instrucción:
SUB AX, BX
Esto restará el valor del registro BX del registro AX y almacenará el resultado en el registro AX.
Multiplicación:
La multiplicación en ensamblador 8086 se realiza mediante la instrucción "MUL" (multiplicar). Sin embargo, la instrucción "MUL" solo puede multiplicar números de 8 o 16 bits, y el resultado se almacena en un par de registros: DX (registro superior) y AX (registro inferior). Por ejemplo, si quieres multiplicar el valor de un registro BX por el registro AX y almacenar el resultado en los registros DX y AX, puedes utilizar la siguiente instrucción:
MUL BX
Esto multiplicará el valor del registro BX por el registro AX y almacenará el resultado de 16 bits en los registros DX y AX.
División:
La división en ensamblador 8086 se realiza mediante la instrucción "DIV" (dividir). Al igual que la multiplicación, la instrucción "DIV" trabaja con números de 8 o 16 bits y utiliza los registros DX y AX para almacenar el resultado. Para realizar una división, debes colocar el dividendo en los registros DX (registro superior) y AX (registro inferior) y el divisor en un registro específico, como BX. Por ejemplo, si quieres dividir el valor de los registros DX y AX por el valor del registro BX y almacenar el cociente en el registro AX y el residuo en el registro DX, puedes utilizar la siguiente instrucción:
DIV BX
Esto dividirá el valor de los registros DX y AX por el valor del registro BX y almacenará el cociente en el registro AX y el residuo en el registro DX.
En el lenguaje ensamblador, es importante manejar correctamente los tamaños de los datos y asegurarse de que los registros estén inicializados adecuadamente antes de realizar operaciones aritméticas. Además, el lenguaje ensamblador 8086 utiliza convenciones específicas para el manejo de registros y argumentos, por lo que es importante consultar la documentación o los manuales de referencia para obtener información detallada sobre el uso de las instrucciones en ensamblador 8086.
Operaciones lógicas
En el lenguaje ensamblador para el procesador Intel 8086, se pueden realizar operaciones lógicas básicas, como AND, OR, XOR y NOT. Estas operaciones se realizan a nivel de bits y permiten manipular y comparar datos binarios. A continuación, cada una de estas operaciones:
AND lógico:
La operación AND lógico se realiza utilizando la instrucción "AND". Esta operación realiza una operación AND bit a bit entre dos operandos y almacena el resultado en el operando de destino. Por ejemplo, para realizar una operación AND lógico entre los registros AX y BX y almacenar el resultado en AX, puedes utilizar la siguiente instrucción:
AND AX, BX
Esto realizará una operación AND lógico bit a bit entre los valores de los registros AX y BX y almacenará el resultado en el registro AX.
OR lógico:
La operación OR lógico se realiza utilizando la instrucción "OR". Esta operación realiza una operación OR bit a bit entre dos operandos y almacena el resultado en el operando de destino. Por ejemplo, para realizar una operación OR lógico entre los registros AX y BX y almacenar el resultado en AX, puedes utilizar la siguiente instrucción:
OR AX, BX
Esto realizará una operación OR lógico bit a bit entre los valores de los registros AX y BX y almacenará el resultado en el registro AX.
XOR lógico:
La operación XOR lógico se realiza utilizando la instrucción "XOR". Esta operación realiza una operación XOR bit a bit entre dos operandos y almacena el resultado en el operando de destino. Por ejemplo, para realizar una operación XOR lógico entre los registros AX y BX y almacenar el resultado en AX, puedes utilizar la siguiente instrucción:
XOR AX, BX
Esto realizará una operación XOR lógico bit a bit entre los valores de los registros AX y BX y almacenará el resultado en el registro AX.
NOT lógico:
La operación NOT lógico se realiza utilizando la instrucción "NOT". Esta operación invierte cada bit de un operando y almacena el resultado en el operando de destino. Por ejemplo, para realizar una operación NOT lógico en el registro AX y almacenar el resultado nuevamente en AX, puedes utilizar la siguiente instrucción:
NOT AX
Esto invertirá cada bit del registro AX y almacenará el resultado nuevamente en el registro AX.
Estas operaciones lógicas son útiles para manipular y comparar datos binarios, como flags o máscaras de bits. Recuerda que estas operaciones se realizan a nivel de bits y es importante tener en cuenta los tamaños de los datos y los operandos para obtener los resultados deseados. Además, es importante consultar la documentación o los manuales de referencia para obtener información detallada sobre el uso de las instrucciones lógicas en el ensamblador 8086.
Operaciones de shifts y corrimientos
En el lenguaje ensamblador para el procesador Intel 8086, se pueden realizar operaciones de desplazamiento (shifts) y corrimientos (rotations) para desplazar los bits de un valor hacia la izquierda o hacia la derecha. Estas operaciones son útiles para manipular y acceder a bits individuales de datos y realizar operaciones lógicas más complejas. A continuación, las operaciones de desplazamiento y corrimiento disponibles en el ensamblador 8086:
Operaciones de desplazamiento (shifts):
1. Desplazamiento a la izquierda (SHL/SL):
La operación de desplazamiento a la izquierda se realiza utilizando las instrucciones SHL (Shift Left) o SL (Shift Left). Estas instrucciones desplazan los bits de un operando hacia la izquierda y llenan los bits vacantes con ceros. Por ejemplo, para desplazar el contenido del registro AX dos posiciones hacia la izquierda, puedes utilizar la siguiente instrucción:
SHL AX, 2
Esto desplazará los bits de AX dos posiciones hacia la izquierda y llenará los bits vacantes con ceros.
2. Desplazamiento a la derecha aritmético (SAR):
La operación de desplazamiento a la derecha aritmético se realiza utilizando la instrucción SAR (Shift Arithmetic Right). Esta instrucción desplaza los bits de un operando hacia la derecha y rellena los bits vacantes con el bit de signo, lo que significa que se mantiene el bit más significativo (el bit de signo) durante el desplazamiento. Por ejemplo, para desplazar el contenido del registro AX una posición hacia la derecha, puedes utilizar la siguiente instrucción:
SAR AX, 1
Esto desplazará los bits de AX una posición hacia la derecha y rellenará los bits vacantes con el bit de signo.
Operaciones de corrimiento (rotations):
1. Corrimiento circular a la izquierda (ROL):
La operación de corrimiento circular a la izquierda se realiza utilizando la instrucción ROL (Rotate Left). Esta instrucción desplaza los bits de un operando hacia la izquierda y el bit más significativo que sale por el extremo izquierdo se introduce por el extremo derecho. Por ejemplo, para realizar un corrimiento circular a la izquierda en el registro AX, puedes utilizar la siguiente instrucción:
ROL AX, 1
Esto realizará un corrimiento circular a la izquierda en el registro AX.
2. Corrimiento circular a la derecha (ROR):
La operación de corrimiento circular a la derecha se realiza utilizando la instrucción ROR (Rotate Right). Esta instrucción desplaza los bits de un operando hacia la derecha y el bit menos significativo que sale por el extremo derecho se introduce por el extremo izquierdo. Por ejemplo, para realizar un corrimiento circular a la derecha en el registro AX, puedes utilizar la siguiente instrucción:
ROR AX, 1
Esto realizará un corrimiento circular a la derecha en el registro AX.
Es importante tener en cuenta que en todas estas operaciones de desplazamiento y corrimiento, es necesario considerar el tamaño de los datos y los efectos en los bits de signo y de carry.
Transferencia entre registros
En el lenguaje ensamblador del procesador Intel 8086, las operaciones de transferencia entre registros son esenciales para manipular y procesar datos en el procesador. Estas operaciones permiten copiar el contenido de un registro en otro. A continuación, las principales operaciones de transferencia entre registros:
1. Movimiento directo entre registros:
La operación más común para transferir datos entre registros es la instrucción MOV (Move). Esta instrucción copia el valor de origen en el registro de destino. La sintaxis general es:
MOV destino, origen
Aquí, "destino" es el registro que recibirá el valor copiado y "origen" es el registro del cual se copiará el valor. Por ejemplo, para transferir el contenido del registro AX al registro BX, se utilizaría la siguiente instrucción:
MOV BX, AX
Esto copiará el contenido del registro AX en el registro BX.
2. Transferencia entre registros de segmento:
En el procesador 8086, los registros de segmento (CS, DS, SS, ES) se utilizan para apuntar a diferentes segmentos de memoria. Estos registros son importantes para acceder a datos almacenados en diferentes áreas de memoria. Para transferir datos entre registros de segmento, también se utiliza la instrucción MOV. Por ejemplo, para transferir el contenido del registro DS al registro ES, se utilizaría la siguiente instrucción:
MOV ES, DS
Esto copiará el contenido del registro DS en el registro ES.
3. Transferencia entre registros de índice:
Los registros de índice (SI, DI, BP, SP) se utilizan comúnmente para acceder a elementos en matrices y estructuras de datos. Para transferir datos entre registros de índice, también se emplea la instrucción MOV. Por ejemplo, para transferir el contenido del registro SI al registro DI, se utilizaría la siguiente instrucción:
MOV DI, SI
Esto copiará el contenido del registro SI en el registro DI.
Es importante tener en cuenta que el tamaño de los datos debe ser compatible entre los registros involucrados en la transferencia. Por ejemplo, si se desea transferir una palabra de 16 bits, se debe utilizar la instrucción MOV para registros de 16 bits como AX, BX, CX, etc.
Estas operaciones de transferencia entre registros son fundamentales en la programación en ensamblador, ya que permiten el movimiento eficiente de datos en el procesador. Es importante utilizar las instrucciones de transferencia correctas y considerar los tamaños de datos adecuados para garantizar una transferencia precisa y sin errores.
Transferencia desde y hacia memoria
En el lenguaje ensamblador para el procesador Intel 8086, se pueden realizar operaciones de transferencia desde y hacia la memoria para mover datos entre registros y ubicaciones de memoria. Estas operaciones son fundamentales para el manejo de datos y permiten almacenar, cargar y manipular información en la memoria del sistema. A continuación, describiré las principales operaciones de transferencia desde y hacia la memoria en el ensamblador 8086:
Transferencia desde la memoria:
1. LOAD (Carga):
La operación LOAD se utiliza para transferir datos desde una ubicación de memoria a un registro. La instrucción más común para cargar datos desde la memoria es "MOV" (Move). Por ejemplo, para cargar un valor de 16 bits desde una ubicación de memoria identificada por la etiqueta "Datos" en el registro AX, puedes utilizar la siguiente instrucción:
MOV AX, [Datos]
Esto copiará el valor almacenado en la ubicación de memoria "Datos" en el registro AX.
2. LODESB y LODSW:
Estas instrucciones se utilizan para cargar un byte (LODESB) o una palabra (LODSW) desde una ubicación de memoria apuntada por el registro SI (Source Index) en un registro (AL o AX, respectivamente). Después de la carga, el registro SI se incrementa automáticamente. Por ejemplo:
LODSB ; Carga un byte desde [SI] en AL y aumenta SI en 1
LODSW ; Carga una palabra desde [SI] en AX y aumenta SI en 2
Transferencia hacia la memoria:
1. STORE (Almacenamiento):
La operación STORE se utiliza para transferir datos desde un registro a una ubicación de memoria. Al igual que en la transferencia desde la memoria, la instrucción más común para almacenar datos en la memoria es "MOV". Por ejemplo, para almacenar el valor del registro AX en una ubicación de memoria identificada por la etiqueta "Resultado", puedes utilizar la siguiente instrucción:
MOV [Resultado], AX
Esto copiará el valor almacenado en el registro AX en la ubicación de memoria "Resultado".
2. STOSB y STOSW:
Estas instrucciones se utilizan para almacenar un byte (STOSB) o una palabra (STOSW) desde un registro (AL o AX, respectivamente) en una ubicación de memoria apuntada por el registro DI (Destination Index). Después del almacenamiento, el registro DI se incrementa automáticamente. Por ejemplo:
STOSB ; Almacena el contenido de AL en [DI] y aumenta DI en 1
STOSW ; Almacena el contenido de AX en [DI] y aumenta DI en 2
Estas son las operaciones básicas de transferencia desde y hacia la memoria en el ensamblador 8086. Es importante tener en cuenta los tamaños de los datos y las ubicaciones de memoria correctas para garantizar una transferencia precisa. Además, es fundamental asegurarse de que las ubicaciones de memoria estén correctamente inicializadas antes de realizar operaciones de transferencia.
El manejo del stack
En el lenguaje ensamblador para el procesador Intel 8086, el stack (pila) es una estructura de datos que se utiliza para almacenar temporalmente datos durante la ejecución del programa. El stack es una parte importante de la memoria del sistema y se utiliza para almacenar valores de registros, variables locales, direcciones de retorno de subrutinas y otros datos relevantes para la ejecución del programa.
El stack en el procesador 8086 se organiza como una estructura de datos LIFO (Last-In-First-Out), lo que significa que el último elemento que se coloca en el stack es el primero en ser retirado. El stack crece hacia direcciones de memoria más bajas, es decir, los elementos se colocan en direcciones de memoria inferiores a medida que se agregan al stack.
El stack en el ensamblador 8086 se maneja mediante dos registros especiales: el registro SP (Stack Pointer) y el registro BP (Base Pointer). Estos registros son utilizados para realizar operaciones de manejo del stack, como la inserción (push) y la extracción (pop) de elementos.
Operaciones de manejo del stack en el ensamblador 8086:
1. PUSH: La operación PUSH se utiliza para insertar (empujar) un valor en el stack. El valor se coloca en la parte superior del stack (dirección de memoria más baja) y el puntero del stack (registro SP) se decrementa para apuntar al nuevo elemento. Por ejemplo:
PUSH AX
Esto insertará el valor del registro AX en el stack.
2. POP: La operación POP se utiliza para extraer (sacar) un valor del stack. El valor en la parte superior del stack se copia en un registro o en una ubicación de memoria y luego el puntero del stack (registro SP) se incrementa para apuntar al siguiente elemento en el stack. Por ejemplo:
POP BX
Esto extraerá el valor en la parte superior del stack y lo colocará en el registro BX.
3. MOV con registros de stack: Además de las operaciones PUSH y POP, también puedes utilizar la instrucción MOV para copiar valores hacia y desde registros de stack. Por ejemplo:
MOV [SP], AX ; Copia el valor de AX en la ubicación de memoria apuntada por SP
MOV CX, [SP] ; Copia el valor en la ubicación de memoria apuntada por SP en el registro CX
Es importante tener en cuenta que el registro SP debe inicializarse adecuadamente antes de utilizar el stack en un programa. Por lo general, se inicializa con la dirección de memoria más alta disponible para el stack.
El stack es una herramienta muy útil en el ensamblador 8086 para gestionar datos temporales, pasar parámetros a subrutinas y almacenar direcciones de retorno. Sin embargo, es fundamental utilizarlo correctamente y mantener un seguimiento adecuado de las operaciones de inserción y extracción para evitar problemas como desbordamientos o accesos incorrectos a la memoria.
Modos de direccionamiento
El lenguaje ensamblador del procesador Intel 8086 utiliza varios modos de direccionamiento para acceder a los datos y operandos necesarios durante la ejecución de instrucciones. Estos modos de direccionamiento definen cómo se especifica la ubicación de memoria o registro para leer o escribir datos. A continuación, los modos de direccionamiento más comunes en el ensamblador 8086:
1. Direccionamiento inmediato:
En el direccionamiento inmediato, los datos se especifican directamente en la instrucción. Los valores se escriben directamente después de la instrucción. Por ejemplo:
MOV AX, 42 ; Carga el valor inmediato 42 en el registro AX
En este caso, el valor 42 se carga directamente en el registro AX.
2. Direccionamiento directo:
El direccionamiento directo se utiliza cuando se accede a una ubicación de memoria específica. La dirección de memoria se especifica directamente en la instrucción. Por ejemplo:
MOV AX, [1234] ; Carga el valor almacenado en la dirección de memoria 1234 en el registro AX
En este caso, el contenido almacenado en la dirección de memoria 1234 se carga en el registro AX.
3. Direccionamiento por registro:
En el direccionamiento por registro, se utiliza el contenido de un registro como dirección de memoria para acceder a datos. El registro se especifica en la instrucción. Por ejemplo:
MOV AX, [BX] ; Carga el valor almacenado en la dirección de memoria apuntada por el registro BX en el registro AX
En este caso, el contenido almacenado en la dirección de memoria apuntada por el registro BX se carga en el registro AX.
4. Direccionamiento indexado:
El direccionamiento indexado combina el uso de un registro base y un registro de índice para acceder a elementos en matrices y estructuras de datos. El registro base se utiliza para especificar la dirección inicial y el registro de índice se utiliza para desplazarse dentro de la estructura. Por ejemplo:
MOV AX, [BX+SI] ; Carga el valor almacenado en la dirección de memoria [BX+SI] en el registro AX
En este caso, se suma el contenido de los registros BX y SI y se utiliza el resultado como dirección de memoria para cargar el valor en el registro AX.
Estos son solo algunos ejemplos de los modos de direccionamiento utilizados en el ensamblador 8086. También existen otros modos de direccionamiento, como el direccionamiento por desplazamiento, el direccionamiento por registro y desplazamiento, entre otros. Cada modo de direccionamiento tiene sus propias características y se utiliza en diferentes situaciones para acceder a los datos requeridos.
Saltos condicionales e incondicionales
En el lenguaje ensamblador del procesador Intel 8086, existen diferentes tipos de saltos (branching) que permiten alterar el flujo de ejecución del programa según ciertas condiciones o ubicaciones específicas en el código. Estos saltos son esenciales para implementar estructuras de control como bucles y condiciones. A continuación, los principales tipos de saltos en el ensamblador 8086:
1. Salto incondicional (JMP):
El salto incondicional se utiliza para desviar el flujo de ejecución a una ubicación de memoria específica sin considerar ninguna condición. La instrucción JMP (Jump) se utiliza para implementar saltos incondicionales. Por ejemplo:
JMP etiqueta ; Salta a la ubicación de memoria identificada por "etiqueta"
La ejecución del programa se desviará inmediatamente a la ubicación de memoria especificada por la etiqueta.
2. Salto condicional:
El salto condicional se utiliza para desviar el flujo de ejecución a una ubicación de memoria específica basándose en una condición evaluada. Existen varias instrucciones de salto condicional que se utilizan junto con comparaciones y banderas para controlar el flujo del programa. Algunos ejemplos de instrucciones de salto condicional son:
- JZ (Jump if Zero): Salta si la bandera Zero (ZF) está activada (el resultado de una operación anterior fue cero).
- JNZ (Jump if Not Zero): Salta si la bandera Zero (ZF) está desactivada.
- JC (Jump if Carry): Salta si la bandera Carry (CF) está activada (ocurre un acarreo en una operación aritmética).
- JNC (Jump if Not Carry): Salta si la bandera Carry (CF) está desactivada.
- JS (Jump if Sign): Salta si la bandera Sign (SF) está activada (el resultado de una operación anterior fue negativo).
- JNS (Jump if Not Sign): Salta si la bandera Sign (SF) está desactivada.
Por ejemplo:
CMP AX, BX ; Compara los registros AX y BX
JE etiqueta ; Salta a la ubicación de memoria identificada por "etiqueta" si son iguales (la bandera Zero está activada)
En este caso, si los registros AX y BX son iguales, la ejecución se desviará a la ubicación de memoria especificada por la etiqueta.
3. Salto basado en tabla (JMP indirecto):
El salto basado en tabla se utiliza para saltar a una ubicación de memoria determinada por el contenido de un registro o dirección de memoria. La instrucción JMP indirecta se utiliza para implementar este tipo de salto. Por ejemplo:
JMP BX ; Salta a la ubicación de memoria apuntada por el registro BX
En este caso, la ejecución se desviará a la ubicación de memoria apuntada por el contenido del registro BX.
Estos son los principales tipos de saltos utilizados en el ensamblador 8086. Estos saltos permiten controlar el flujo de ejecución de un programa y tomar decisiones basadas en condiciones o ubicaciones específicas en el código.
Ejemplos de JZ y JNZ
1. Ejemplo de JZ:
Supongamos que queremos verificar si el contenido del registro AX es cero y, en caso afirmativo, saltar a una etiqueta específica. El siguiente código ilustra cómo se puede usar JZ para lograr esto:
CMP AX, 0 ; Compara el registro AX con cero
JZ etiqueta ; Salta a la ubicación de memoria identificada por "etiqueta" si AX es cero
En este caso, si el contenido del registro AX es cero, la ejecución del programa se desviará a la ubicación de memoria especificada por la etiqueta.
2. Ejemplo de JNZ:
Supongamos que queremos verificar si el resultado de una operación es diferente de cero y, en caso afirmativo, saltar a una etiqueta específica. El siguiente código muestra cómo se puede usar JNZ para lograr esto:
MOV AX, 42 ; Carga el valor 42 en el registro AX
SUB AX, 40 ; Resta 40 al registro AX
JNZ etiqueta ; Salta a la ubicación de memoria identificada por "etiqueta" si el resultado no es cero
En este ejemplo, el registro AX se inicializa con el valor 42 y luego se le resta 40. Si el resultado es diferente de cero (es decir, no es igual a cero), la ejecución se desviará a la ubicación de memoria especificada por la etiqueta.
En ambos casos, las instrucciones JZ y JNZ permiten controlar el flujo del programa basado en condiciones específicas, lo que brinda flexibilidad en la toma de decisiones durante la ejecución del código.
Ejemplo de JE y JNE
1. Ejemplo de JE:
Supongamos que queremos verificar si dos registros, AX y BX, son iguales y, en caso afirmativo, saltar a una etiqueta específica. El siguiente código muestra cómo se puede usar JE para lograr esto:
CMP AX, BX ; Compara los registros AX y BX
JE etiqueta ; Salta a la ubicación de memoria identificada por "etiqueta" si son iguales (la bandera Zero está activada)
En este ejemplo, si los registros AX y BX son iguales (la bandera Zero está activada), la ejecución se desviará a la ubicación de memoria especificada por la etiqueta.
2. Ejemplo de JNE:
Supongamos que queremos verificar si el contenido de un registro es diferente de cero y, en caso afirmativo, saltar a una etiqueta específica. El siguiente código muestra cómo se puede usar JNE para lograr esto:
MOV AX, 42 ; Carga el valor 42 en el registro AX
CMP AX, 0 ; Compara el registro AX con cero
JNE etiqueta ; Salta a la ubicación de memoria identificada por "etiqueta" si AX no es cero
En este ejemplo, si el contenido del registro AX es diferente de cero, la ejecución se desviará a la ubicación de memoria especificada por la etiqueta.
En ambos casos, las instrucciones JE y JNE permiten controlar el flujo del programa basado en comparaciones y condiciones específicas, lo que brinda flexibilidad en la toma de decisiones durante la ejecución del código.
Ejemplo de JGE y JGT
1. Ejemplo de JGT:
Supongamos que queremos verificar si el contenido de un registro AX es mayor que un valor específico y, en caso afirmativo, saltar a una etiqueta específica. El siguiente código muestra cómo se puede usar JGT para lograr esto:
MOV AX, 10 ; Carga el valor 10 en el registro AX
CMP AX, 5 ; Compara el registro AX con 5
JGT etiqueta ; Salta a la ubicación de memoria identificada por "etiqueta" si AX es mayor que 5 (la bandera Carry está desactivada y la bandera Zero está desactivada)
En este ejemplo, si el contenido del registro AX es mayor que 5 (la bandera Carry está desactivada y la bandera Zero está desactivada), la ejecución se desviará a la ubicación de memoria especificada por la etiqueta.
2. Ejemplo de JGE:
Supongamos que queremos verificar si el contenido de un registro AX es mayor o igual que un valor específico y, en caso afirmativo, saltar a una etiqueta específica. El siguiente código muestra cómo se puede usar JGE para lograr esto:
MOV AX, 10 ; Carga el valor 10 en el registro AX
CMP AX, 10 ; Compara el registro AX con 10
JGE etiqueta ; Salta a la ubicación de memoria identificada por "etiqueta" si AX es mayor o igual que 10 (la bandera Carry está desactivada)
En este ejemplo, si el contenido del registro AX es mayor o igual que 10 (la bandera Carry está desactivada), la ejecución se desviará a la ubicación de memoria especificada por la etiqueta.
En ambos casos, las instrucciones JGT y JGE permiten controlar el flujo del programa basado en comparaciones y condiciones específicas relacionadas con la relación "mayor que" y "mayor o igual que". Estas instrucciones brindan flexibilidad en la toma de decisiones durante la ejecución del código.
Una lista de operaciones comunes
Operaciones Aritméticas:
- ADD: Suma dos operandos.
- ADC: Suma dos operandos más el Carry Flag (acarreo).
- SUB: Resta dos operandos.
- SBB: Resta dos operandos más el Carry Flag (acarreo).
- MUL: Multiplica dos operandos y almacena el resultado en registros específicos.
- IMUL: Multiplica dos operandos con signo y almacena el resultado en registros específicos.
- DIV: Divide el registro DX:AX (doble palabra) por un operando y almacena el cociente en AX y el residuo en DX.
- IDIV: Divide el registro DX:AX (doble palabra) con signo por un operando y almacena el cociente en AX y el residuo en DX.
- INC: Incrementa el valor de un registro o una ubicación de memoria en uno.
- DEC: Decrementa el valor de un registro o una ubicación de memoria en uno.
- NEG: Realiza la negación aritmética de un operando.
Operaciones Lógicas:
- AND: Realiza una operación lógica AND bit a bit entre dos operandos.
- OR: Realiza una operación lógica OR bit a bit entre dos operandos.
- XOR: Realiza una operación lógica XOR bit a bit entre dos operandos.
- NOT: Realiza una operación lógica NOT (complemento) en un operando.
- TEST: Realiza una operación lógica AND bit a bit entre dos operandos, pero no almacena el resultado.
Operaciones de Desplazamiento y Rotación:
- SAL/SHL: Desplaza los bits de un operando hacia la izquierda, llenando los bits vacíos con ceros.
- SAR: Desplaza los bits de un operando hacia la derecha, conservando el bit de signo.
- SHR: Desplaza los bits de un operando hacia la derecha, rellenando los bits vacíos con ceros.
- ROL: Rota los bits de un operando hacia la izquierda, moviendo el bit más significativo al bit menos significativo y rotando los demás bits.
- ROR: Rota los bits de un operando hacia la derecha, moviendo el bit menos significativo al bit más significativo y rotando los demás bits.
- RCL: Rota los bits de un operando hacia la izquierda, moviendo el Carry Flag al bit menos significativo y rotando los demás bits.
- RCR: Rota los bits de un operando hacia la derecha, moviendo el Carry Flag al bit más significativo y rotando los demás bits.
Operaciones de Transferencia de Datos:
- MOV: Transfiere datos de una ubicación a otra.
- XCHG: Intercambia los contenidos de dos registros o una ubicación de memoria y un registro.
- LEA: Carga la dirección efectiva de un operando en un registro.
Operaciones de Transferencia entre Registros y Memoria:
- PUSH: Inserta un valor en la pila (memoria) desde un registro o una ubicación de memoria.
- POP: Extrae un valor de la pila (memoria) y lo coloca en un registro o una ubicación de memoria.
Operaciones de Salto y Ramificación:
- JMP: Salto incondicional a una dirección de memoria específ
ica.
- JZ/JE: Salto si la condición es igual a cero.
- JNZ/JNE: Salto si la condición no es igual a cero.
- JC: Salto si hay acarreo (Carry Flag).
- JNC: Salto si no hay acarreo (Carry Flag).
- JO: Salto si hay desbordamiento (Overflow Flag).
- JNO: Salto si no hay desbordamiento (Overflow Flag).
- JS: Salto si el signo está activado (Sign Flag).
- JNS: Salto si el signo no está activado (Sign Flag).
- JP/JPE: Salto si hay paridad (Parity Flag).
- JNP/JPO: Salto si no hay paridad (Parity Flag).
- JL/JNGE: Salto si la condición es menor que (no mayor o igual que).
- JLE/JNG: Salto si la condición es menor o igual que (no mayor que).
- JG/JNLE: Salto si la condición es mayor que (no menor o igual que).
- JGE/JNL: Salto si la condición es mayor o igual que (no menor que).
Algunos ejemplos de operaciones básicas con los registros
ADD AX BX //AX=AX+BX
SUB
MUL
DIV
INC AX // AX++
DEC CL // CL- -
AND //and bit a bit de 2 registros
Son todas operaciones básicas que podemos realizar con el contenido de los registros.
Operaciones con el stack
PUSH AX // deja el contenido de AX en el tope del Stack
POP BX // saca el tope del stack a BX
Operaciones de manejo de memoria
Mov AX, #dir carga el número dir en AX
Mov AX, [#dir] carga el CONTENIDO DE dir en AX
Mov AX BX // AX=BX
Desvíos en el flujo de ejecución
JMP etiqueta //manda el flujo de ejecución a cierta etiqueta en el código
SUB AX BX //resta AX y BX
JZ loop // si la resta da 0 entonces salta a loop
Cada operación deja su estado en un resgistro llamado FLAGS.
Z Cero
P paridad
O Overflow
N Negativo
Cuando hace el sub AX BX hace la resta, pero como toda operación, deja su impacto en el registro de FLAGS. Si la resto da cero, la flag Z se prende. Y el JZ mira dicha flag y en función de su valor, salta.
Tengo también un CMP compara y JE salta si iguales, JL salta si menor, JNE salta si distintos, etc
JE JNE JL JLE etc
Un ejemplo de call y ret: llamado a subrutinas
Un ejemplo de cómo usar las instrucciones `call` y `ret`:
; Definición de la función
miFuncion PROC
; Código de la función aquí
; ...
ret ; Retorna de la función
miFuncion ENDP
; Código principal
; ...
call miFuncion ; Llama a la función miFuncion
; Resto del código aquí
; ...
Explicación:
- `call` se utiliza para llamar a una función o subrutina. En el ejemplo, se llama a la función `miFuncion` utilizando la instrucción `call miFuncion`.
- Dentro de `miFuncion`, se escribe el código que realiza alguna operación o tarea específica.
- `ret` se utiliza para retornar de una función o subrutina y regresar al punto de origen desde donde se realizó la llamada. En el ejemplo, la instrucción `ret` se utiliza al final de `miFuncion` para retornar al código principal.
- Después de `call`, el flujo de ejecución se dirige al inicio de `miFuncion`, y una vez que se ejecuta `ret`, el flujo de ejecución regresa al punto justo después de la instrucción `call`, continuando con la ejecución del código principal.
Se debe asegurar de establecer correctamente el contexto y la pila antes de realizar llamadas a funciones y usar `ret` para retornar de ellas.
Un ejemplo: programa que realiza un swap entre dos direcciones de memoria.
Aquí tienes una función en lenguaje C que recibe dos direcciones de memoria y realiza un intercambio (swap) de sus contenidos:
void swap(int* dir1, int* dir2) {
int temp = *dir1;
*dir1 = *dir2;
*dir2 = temp;
}
Explicación:
- La función `swap` recibe dos punteros a enteros como parámetros (`int* dir1` y `int* dir2`).
- Se crea una variable `temp` para almacenar temporalmente el contenido de la primera dirección de memoria.
- Se asigna el contenido de la segunda dirección de memoria a la primera dirección de memoria mediante `*dir1 = *dir2`.
- Se asigna el contenido almacenado en `temp` a la segunda dirección de memoria mediante `*dir2 = temp`.
Puedes llamar a esta función y pasarle las direcciones de memoria que deseas intercambiar. Por ejemplo:
int main() {
int num1 = 10;
int num2 = 20;
printf("Antes del intercambio: num1 = %d, num2 = %d\n", num1, num2);
swap(&num1, &num2);
printf("Después del intercambio: num1 = %d, num2 = %d\n", num1, num2);
return 0;
}
Este programa imprimirá:
Antes del intercambio: num1 = 10, num2 = 20
Después del intercambio: num1 = 20, num2 = 10
Como puedes ver, la función `swap` intercambia correctamente los contenidos de las dos direcciones de memoria que se le pasan.
Ahora queremos ver su código compilado en Assembler 8086
El código en lenguaje ensamblador para el procesador 8086 que realiza el intercambio (swap) de los contenidos de dos direcciones de memoria:
swap PROC
mov bx, [bp+6] ; bx = dirección de memoria 1
mov dx, [bp+4] ; dx = dirección de memoria 2
mov ax, [bx] ; ax = contenido de dirección de memoria 1
mov cx, [dx] ; cx = contenido de dirección de memoria 2
mov [bx], cx ; contenido de dirección de memoria 1 = contenido de dirección de memoria 2
mov [dx], ax ; contenido de dirección de memoria 2 = contenido de dirección de memoria 1
ret
swap ENDP
Explicación:
- La función `swap` recibe dos parámetros: las direcciones de memoria 1 y 2.
- Los parámetros se pasan en la pila a través de los desplazamientos `[bp+6]` y `[bp+4]`.
- Se mueve la dirección de memoria 1 a `bx` y la dirección de memoria 2 a `dx`.
- Se guarda el contenido de la dirección de memoria 1 en `ax` y el contenido de la dirección de memoria 2 en `cx`.
- Luego se intercambian los contenidos: se guarda el contenido de `cx` en la dirección de memoria 1 y el contenido de `ax` en la dirección de memoria 2.
- Finalmente, se retorna de la función con la instrucción `ret`.
Se puede llamar a esta función desde un programa principal en ensamblador 8086, pasando las direcciones de memoria que deseas intercambiar. Se deben definir correctamente las direcciones de memoria y establecer adecuadamente el contexto y la pila antes de llamar a la función `swap`.
Aquí una versión más detallada y compleja del Swap en Assembler
Este ejemplo completo en lenguaje ensamblador 8086 que realiza el intercambio (swap) de los contenidos de dos direcciones de memoria utilizando las instrucciones `call` y `ret`:
DATA SEGMENT
num1 DW 10 ; Dirección de memoria 1
num2 DW 20 ; Dirección de memoria 2
DATA ENDS
CODE SEGMENT
ASSUME CS:CODE, DS:DATA
START:
MOV AX, DATA
MOV DS, AX ; Establecer el segmento de datos
; Imprimir los valores antes del intercambio
MOV AX, num1
MOV DX, num2
CALL imprimirValores
; Llamar a la función swap para intercambiar los valores
MOV AX, OFFSET num1
MOV DX, OFFSET num2
CALL swap
; Imprimir los valores después del intercambio
MOV AX, num1
MOV DX, num2
CALL imprimirValores
MOV AH, 4CH ; Terminar el programa
INT 21H
imprimirValores PROC
MOV CX, 5 ; Número de dígitos a imprimir
LOOP_START:
MOV BX, 10
XOR DX, DX
DIV BX ; Divide AX por 10, el residuo está en DX
ADD DL, 30H ; Convertir el dígito a ASCII
PUSH DX ; Empujar el dígito en la pila
DEC CX ; Decrementar el contador
CMP AX, 0 ; Verificar si se han procesado todos los dígitos
JNE LOOP_START ; Si no se han procesado todos, saltar al inicio del bucle
PRINT_START:
POP DX ; Pop el dígito de la pila
MOV AH, 02H ; Función de servicio de impresión de carácter
INT 21H ; Imprimir el dígito
LOOP PRINT_START ; Repetir hasta que se hayan impreso todos los dígitos
RET
imprimirValores ENDP
swap PROC
PUSH AX ; Guardar los registros en la pila
PUSH BX
PUSH CX
PUSH DX
MOV BX, [BP + 4] ; BX = dirección de memoria 1
MOV DX, [BP + 2] ; DX = dirección de memoria 2
MOV AX, [BX] ; AX = contenido de dirección de memoria 1
MOV CX, [DX] ; CX = contenido de dirección de memoria 2
MOV [BX], CX ; contenido de dirección de memoria 1 = contenido de dirección de memoria 2
MOV [DX], AX ; contenido de dirección de memoria 2 = contenido de dirección de memoria 1
POP DX ; Restaurar los registros desde la pila
POP CX
POP BX
POP AX
RET
swap ENDP
CODE ENDS
END START
En este ejemplo, se define un segmento de datos (`DATA SEGMENT`) que contiene las variables `num1` y `num2`, las cuales representan las direcciones de memoria 1 y 2, respectivamente.
En el segmento de código (CODE SEGMENT), el programa principal comienza en la etiqueta START. Se establece el segmento de datos (MOV AX, DATA y MOV DS, AX) y se llama a la función imprimirValores para mostrar los valores iniciales de num1 y num2. Luego, se llama a la función swap para intercambiar los contenidos de las direcciones de memoria. Finalmente, se llama nuevamente a imprimirValores para mostrar los valores después del intercambio.
La función imprimirValores recibe los valores de num1 y num2, los convierte en su representación ASCII y los imprime en pantalla utilizando la interrupción INT 21H con la función de servicio AH=02H.
La función swap recibe las direcciones de memoria como parámetros y realiza el intercambio de sus contenidos.
Se debe ensamblar y ejecutar este código en un emulador o ambiente de desarrollo adecuado para el procesador 8086.
Ahora lo compilamos para el Assembler del Procesador i3 de 64 bits
El procesador Intel Core i3 utiliza la arquitectura x86-64, que es una extensión del conjunto de instrucciones x86. A continuación un ejemplo de código en ensamblador para un procesador Intel Core i3, utilizando la sintaxis de Intel:
section .data
num1 dd 10 ; Dirección de memoria 1
num2 dd 20 ; Dirección de memoria 2
section .text
global _start
_start:
; Imprimir los valores antes del intercambio
mov eax, [num1]
mov ebx, [num2]
call imprimirValores
; Llamar a la función swap para intercambiar los valores
lea esi, [num1]
lea edi, [num2]
call swap
; Imprimir los valores después del intercambio
mov eax, [num1]
mov ebx, [num2]
call imprimirValores
; Salir del programa
mov eax, 60 ; Código de salida del programa
xor edi, edi ; Código de error (0)
syscall
imprimirValores:
pusha ; Guardar todos los registros en la pila
mov ecx, 10 ; Número de dígitos a imprimir
loop_start:
mov edx, 0
mov eax, dword [ebx]
mov ebx, 10
div ebx ; Divide eax por 10, el residuo está en edx
add dl, '0' ; Convertir el dígito a ASCII
push edx ; Empujar el dígito en la pila
dec ecx ; Decrementar el contador
cmp eax, 0 ; Verificar si se han procesado todos los dígitos
jne loop_start ; Si no se han procesado todos, saltar al inicio del bucle
print_start:
pop edx ; Pop el dígito de la pila
mov eax, 4 ; Función de servicio de impresión de carácter
mov ebx, 1 ; Descriptor de archivo (stdout)
mov ecx, edx ; Carácter a imprimir
mov edx, 1 ; Longitud del carácter (1 byte)
int 0x80 ; Interrupción del sistema
loop print_start ; Repetir hasta que se hayan impreso todos los dígitos
popa ; Restaurar todos los registros desde la pila
ret
swap:
pusha ; Guardar todos los registros en la pila
mov eax, [esi] ; eax = contenido de dirección de memoria 1
mov ebx, [edi] ; ebx = contenido de dirección de memoria 2
mov [esi], ebx ; contenido de dirección de memoria 1 = contenido de dirección de memoria 2
mov [edi], eax ; contenido de dirección de memoria 2 = contenido de dirección de memoria 1
popa ; Restaurar todos los registros desde la pila
ret
En este ejemplo, se utiliza la sección `.data` para declarar las variables `num1` y `num2`, y la sección `.text` para el código principal y las funciones `imprimirValores` y `swap`.
El programa comienza en la etiqueta `_start`, donde se llaman las funciones `imprimirValores
` y `swap`, y se muestran los valores antes y después del intercambio. Luego, el programa llama a la interrupción del sistema `syscall` para salir del programa.
La función `imprimirValores` convierte los valores en su representación ASCII y los imprime en pantalla utilizando la llamada al sistema `int 0x80`.
La función `swap` intercambia los contenidos de las direcciones de memoria utilizando los registros `eax` y `ebx`.
Se debe ensamblar y ejecutar este código en un sistema compatible con la arquitectura x86-64, como un procesador Intel Core i3.
Ahora compilado para un procesador Z80 de 8 bits:
ORG 100h ; Dirección de inicio del programa
LD HL, num1 ; Cargar dirección de memoria 1 en HL
LD DE, num2 ; Cargar dirección de memoria 2 en DE
LD A, (HL) ; Cargar contenido de dirección de memoria 1 en A
LD B, (DE) ; Cargar contenido de dirección de memoria 2 en B
LD (HL), B ; Guardar contenido de B en dirección de memoria 1
LD (DE), A ; Guardar contenido de A en dirección de memoria 2
; Resto del código aquí
HALT ; Detener la ejecución del programa
num1 DEFW 10 ; Dirección de memoria 1
num2 DEFW 20 ; Dirección de memoria 2
En este ejemplo, se utiliza la directiva `ORG` para especificar la dirección de inicio del programa en `100h`.
El código principal carga las direcciones de memoria 1 y 2 en los registros `HL` y `DE`, respectivamente. Luego, se carga el contenido de las direcciones de memoria en los registros `A` y `B`.
A continuación, se intercambian los contenidos utilizando las instrucciones `LD` para cargar y guardar datos en las direcciones de memoria.
El procesador Z80 utiliza una arquitectura de 8 bits y tiene un conjunto de instrucciones diferente al procesador Intel Core i3, por lo que el código de ensamblador también es diferente. Se debe utilizar un ensamblador compatible con el procesador Z80 para compilar y ejecutar este código.
Ahora en código máquina (sobre 8086)
org 100h ; Dirección de inicio del programa
B8 0000 mov ax, 0000 ; Establecer el segmento de datos
8E D8 mov ds, ax
; Imprimir los valores antes del intercambio
B8 1000 mov ax, 1000 ; Dirección de memoria num1
8E C2 mov es, ax
8B 06 0000 mov ax, [es:0000] ; Cargar el contenido de num1 en ax
B8 1200 mov ax, 1200 ; Dirección de memoria num2
8E C2 mov es, ax
8B 16 0000 mov dx, [es:0000] ; Cargar el contenido de num2 en dx
B4 09 mov ah, 09h ; Función de servicio de impresión de carácter
BA 0200 mov dx, 0200h ; Offset del mensaje de impresión
CD 21 int 21h
; Llamar a la función swap para intercambiar los valores
B8 1000 mov ax, 1000 ; Dirección de memoria num1
8E C2 mov es, ax
8B 06 0000 mov ax, [es:0000] ; Cargar el contenido de num1 en ax
B8 1200 mov ax, 1200 ; Dirección de memoria num2
8E C2 mov es, ax
8B 16 0000 mov dx, [es:0000] ; Cargar el contenido de num2 en dx
50 push ax ; Guardar ax en la pila
52 push dx ; Guardar dx en la pila
B8 1000 mov ax, 1000 ; Dirección de memoria num1
8E C2 mov es, ax
5A pop dx ; Sacar el contenido de la pila a dx
89 16 0000 mov [es:0000], dx ; Guardar el contenido de dx en num1
B8 1200 mov ax, 1200 ; Dirección de memoria num2
8E C2 mov es, ax
58 pop ax ; Sacar el contenido de la pila a ax
89 06 0000 mov [es:0000], ax ; Guardar el contenido de ax en num2
; Imprimir los valores después del intercambio
B8 1000 mov ax, 1000 ; Dirección de memoria num1
8E C2 mov es, ax
8B 06 0000 mov ax, [es:0000] ; Cargar el contenido de num1 en ax
B8 1200 mov ax, 1200 ; Dirección de memoria num2
8E C2 mov es, ax
8B 16 0000 mov dx, [es:0000] ; Cargar el contenido de num2 en dx
B4 09 mov ah, 09h ; Función de servicio de impresión de carácter
BA 0206 mov dx, 0206h ; Offset del mensaje de impresión
CD 21 int 21h
B
4 4C mov ah, 4Ch ; Terminar el programa
CD 21 int 21h
DB "Valores antes del intercambio$"
DB "Valores después del intercambio$"
times 510-($-$$) db 0
DW 0AA55h
Este código en lenguaje máquina hexadecimal representa el programa en código máquina para realizar el intercambio de los contenidos de dos direcciones de memoria en el procesador 8086. Se debe ensamblar y ejecutar este código en un emulador o ambiente de desarrollo compatible con el procesador 8086.
COMPILEMOS otros ejemplos
If a < b then a++ else b++
Mov AX [#a] // traemos a AX el contenido de a
Mov BX [#b] //traemos a BX el contenido de b
Sub AX BX // AX=AX-BX
JNeg if // si dio negativo, entonces voy al else
Inc AX
Mov #a AX
JMP fin
If: Dec BX
Mov #b BX
Fin:
Swap de los registros AX y BX
Usando el Stack:
PUSH AX
PUSH BX
POP AX
POP BX
Usando CX:
Mov CX AX
Mov AX BX
Mov BX CX
Si queremos hacer un swap de dos direcciones de memoria a y b
(el Mov no soporta Mov dir, dir siempre uno de los dos debe ser registro)
Mov AX [#a]
Mov BX [#b]
Mov #a BX
Mov #b AX
Compilemos
a++
Mov AX [#a]
Inc AX
Mov #a AX
Qué hace este programa ?
Loop: Add AX BX
Dec BX
JZ fin
Jmp loop
Fin:
Supongamos que AX vale 3 y BX vale 4
AX 3 ---- 7 ----10 ----12 -----13
BX 4 ---3 -----2 -----1 -----0
Deja AX en 13 y BX en 0
Qué hace este programa ?
Loop: Add AX AX
Dec BX
JZ fin
Jmp loop
Fin:
Supongamos que AX vale 3 y BX vale 4
AX 3 --- 6 ---- 12 ---- 24 --- 48
BX 4 ---3 -----2 -----1 -----0
AX x (BX**2)
Qué hace este programa ?
Loop: Inc AX
Dec BX
JZ fin
Jmp loop
Fin:
Supongamos que AX vale 3 y BX vale 4
AX 3 ----4 ----5----6------7
BX 4 ---3 -----2 -----1 -----0
AX queda con AX+BX
La ALU es básicamente un combinacional. Entonces todas estas operaciones deberían estar contempladas por sus circuitos, caso a caso. Pero en realidad hay un nivel intermedio de programación a más bajo nivel, llamado Microcódigo.
Otro ejemplo: un programa en C que lea tres números y escriba el mayor de los 3
#include <stdio.h>
int main() {
int num1, num2, num3;
printf("Ingrese tres números: ");
scanf("%d %d %d", &num1, &num2, &num3);
int mayor = num1; // Suponemos que el primer número es el mayor
if (num2 > mayor) {
mayor = num2;
}
if (num3 > mayor) {
mayor = num3;
}
printf("El mayor de los tres números es: %d\n", mayor);
return 0;
}
En este programa, se utilizan variables `num1`, `num2` y `num3` para almacenar los tres números ingresados por el usuario. Luego, se compara cada número con el actual mayor (`mayor`) y, si se encuentra un número mayor, se actualiza el valor de `mayor`. Finalmente, se imprime el valor de `mayor`, que corresponde al mayor de los tres números ingresados.
Compilado en Assembler de 8086
DATA SEGMENT
num1 DW ?
num2 DW ?
num3 DW ?
mayor DW ?
mensaje DB "Ingrese tres números: $"
resultado DB "El mayor de los tres números es: $"
DATA ENDS
CODE SEGMENT
ASSUME CS:CODE, DS:DATA
START:
MOV AX, DATA
MOV DS, AX ; Establecer el segmento de datos
MOV AH, 9
MOV DX, OFFSET mensaje
INT 21H ; Imprimir el mensaje
; Leer los tres números ingresados por el usuario
MOV AH, 1
INT 21H ; Leer el primer número
SUB AL, 30H ; Convertir de ASCII a valor numérico
MOV num1, AX
MOV AH, 1
INT 21H ; Leer el segundo número
SUB AL, 30H ; Convertir de ASCII a valor numérico
MOV num2, AX
MOV AH, 1
INT 21H ; Leer el tercer número
SUB AL, 30H ; Convertir de ASCII a valor numérico
MOV num3, AX
; Comparar los números y encontrar el mayor
MOV AX, num1 ; Suponemos que el primer número es el mayor
MOV mayor, AX
MOV AX, num2
CMP AX, mayor
JG ACTUALIZAR_MAYOR
MOV AX, num3
CMP AX, mayor
JG ACTUALIZAR_MAYOR
ACTUALIZAR_MAYOR:
MOV mayor, AX
; Imprimir el resultado
MOV AH, 9
MOV DX, OFFSET resultado
INT 21H ; Imprimir el mensaje
MOV AX, mayor
ADD AL, 30H ; Convertir de valor numérico a ASCII
MOV DL, AL
MOV AH, 2
INT 21H ; Imprimir el mayor
MOV AH, 4CH
INT 21H ; Terminar el programa
CODE ENDS
END START
En este programa en ensamblador 8086, se utilizan las interrupciones de DOS (`INT 21H`) para imprimir mensajes y leer los números ingresados por el usuario. Se comparan los números y se actualiza el valor de `mayor` si se encuentra un número mayor. Finalmente, se imprime el valor de `mayor` como resultado. Se debe ensamblar y ejecutar este código en un emulador o ambiente de desarrollo compatible con el procesador 8086.
.png)
.png)
No hay comentarios:
Publicar un comentario