Está en la página 1de 29

21/08/2018

Las partes de un programa en ensamblador.


Toda aplicación que se ejecuta en un procesador basado en Intel, suele “partir” al programa en
tres partes:
● La parte de código
● La parte de datos
● Y su pila
La parte del código contiene, obviamente, el código, las instrucciones del programa. La parte de
los datos, únicamente las variables globales del programa, y la pila contiene variables locales,
direcciones de retorno, y argumentos a funciones.

Esto es así, porque cuando un programa es “cargado” en RAM para su ejecución, se carga con
estas tres partes claramente separadas:

Esto de cargar los programas en una sola sección dividida en tres partes, suele conocerse como
modelo de memoria plano​, y de hecho, en 64 bits, es el único modelo de memoria.

Un programa en ensamblador para 64 bits debe ser un claro reflejo de esto. Para ello el ​script
de un programa en ensamblador se parece siempre a esto:

.data
;;Aquí definimos variables globales
.code
main:
;;Aquí definimos las instrucciones del programa
END

En este script, se distinguen los siguientes detalles:


1. Todo entre​ .data​ y ​.code ​son variables globales
2. Todo entre ​main:​ y E ​ ND​ es el código de la aplicación
3. .data​ es una directiva que indica el inicio de la parte de los datos
4. .code​ es la directiva que indica el inicio de la parte del código
5. main: es una ​etiqueta​, es decir, el símbolo ​main: es un marcador especial que, para
nosotros, marca el punto de entrada al código: la primer instrucción a ejecutar
6. END es una directiva que marca el final del script, cualquier instrucción por debajo de
ella será ignorada

Nota: ​una ​directiva ​es una instrucción, pero no para el microprocesador, sino para el
ensamblador
Nota 2: e​ n MASM, una línea que inicia con ; es un comentario

Téngase en cuenta que el ​punto de entrada al programa​ es la primer instrucción a ejecutar.

.code
main: instr​1​ ​;; ← Primer instrucción a ejecutar
instr​2

instr​k
END

Aunque téngase en cuenta lo siguiente: si una etiqueta ha de indicar el punto de entrada, es


necesario asegurarse de hacerla pública​, por ejemplo, antes de la directiva​ .code,​ como en:
.data
;;Variables
public​ ​main
.code
main: instr​1
instr​2

instr​k
END

Otra manera común para indicar el punto de entrada al programa, además de la etiqueta
public​, consiste en hacer de ​main ​una función​ principal, al estilo de C:
.data
;;Variables
public​ ​main
.code
main proc
instr​1
instr​2

instr​k
ret
END
En este estilo, se define una función main, todo entre main proc y main endp se considera el
código de una función que se llama main.
Luego de definir el script de ensamblador, este deberá tener extensión .asm, como en
“ejem1.asm”, es un simple archivo de texto. Deberá:
1. Ensamblarse
2. Luego, enlazarse

Téngase en cuenta que: el ensamblador de Microsoft (ml64) es capaz de ensamblar y enlazar él


mismo, pero para ilustrar mejor el proceso, se estará manejando por separado el ensamble y
enlace.

Para ensamblar:
>>​ ml64 /c ejem1.asm

Si no existen errores que corregir, se habrá generado el archivo “​ejem1.obj​”.


Para enlazar:
>> ​link /subsystem:console /entry:main ejem1.obj

Si no hubo errores, se habrá generado el archivo ​ejem1.exe

Durante el ensamblado:
1. La opción ​/c ​significa que solo se debe ensamblar, no enlazar

Durante el enlazado:
1. La opción ​/subsystem​: indica a cual susbsistema se dirige el programa, entre otros se
tiene:
a. windows​ → el programa usa la interfaz gráfica de Windows
b. console​ → el programa usa la consola o terminal, en modo texto
2. La opción ​/entry:​etiqueta indica el nombre de la etiqueta que marca el punto de
entrada al programa
>> ​ml64 /c ejem1.asm /link /subsystem:console /entry:main
16, 32 y 64 bits
Cualquier computadora se puede caracterizar de acuerdo a sus capacidad de cómputo, su
cronología, clasificándole en cuanto a la máxima cantidad de información que puede manejar en
una sola operación. Así, se admiten tres clases de caracterizaciones

16 bits 8086 → 80286

32 bits 80386 → Pentium IV

64 bits Intel Core 2 → iX

De acuerdo con la ​arquitectura Von Neumann, ​una computadora funciona como un sistema
organizado, en mayor o menor medida de la siguiente manera:

Según Von Neumann, una computadora necesita al menos de tres componentes:


1. Un​ CPU​, para llevar a cabo operaciones y darle sentido al uso de la computadora
2. Periféricos​ para comunicarse con el mundo exterior
3. Memoria​ para almacenar temporalmente la información necesaria
A su vez, la CPU tiene dos componentes:
4. Una ​ALU​, para llevar a cabo operaciones lógicas y aritméticas
5. Y una ​unidad lógica de control​, que permite darle sentido a las instrucciones que
recibe la CPU
También son necesarios los buses, que son canales que permiten comunicarse a los
componentes. Podemos distinguir
a) Buses de datos
b) Buses de Direcciones

Los buses de datos permiten transferir información entre componentes, los buses de
direcciones permiten referir o señalar, al periférico o localidad de memoria a quien se dirige, o
de donde viene la información en el bus de datos.

En este contexto, cuando decimos que un sistema es de 16, 32 o 64 bits, en este contexto, nos
referimos a
1. La ​máxima información por celda ​que la memoria puede contener y que la ALU puede
procesar, por operando.
2. El ​tamaño de direcciones​ en el bus de direcciones
3. El ​tamaño de la información que se pueden compartir los componentes en el bus de
datos

El microprocesador toma el papel del CPU, agregándole:


1. Los registros, que son pequeñas localidades de memoria dentro del MP
2. La caché, que de manera similar a los registros, es un tipo de memoria dentro del MP,
pero de muy alta velocidad
3. Y la FPU, similar a la ALU, pero especializada en operaciones de punto flotante
Los registros, el tamaño de los operandos para la FPU, y el tamaño de la información por celda
en la caché, también son dictadas por la capacidad (16, 32, o 64 bits)

Un microprocesador antiguo, uno que operaba en 16 bits, funcionaba en ​modo real​, y solo podía
usar, a lo mucho, hasta 1 MB de RAM
Un MP moderno, funciona en modo protegido…
Tipos de dato en ensamblador

Entre algunos detalles externos, sobresale la forma en que se especifica un operando en algunas
de las instrucciones disponibles.
Esto se debe a que en ensamblador, los datos tienen un tipo de dato, más bien asociado con la
cantidad de información que puedan contener, que con la clase de información que pueda
representar.

En cuanto a la capacidad, un dato puede ser:

Tipo Capacidad (bits)

Byte 8

Word 16

Double word 32

Quad word 64

Double quadword 128

La razón por la cual estos cinco son los tipos de dato por default, es porque los registros
internos del MP tienen estas capacidades. Cuando un operando debe ser indicado en una
instrucción, se tienen pocas opciones para hacerlo. Estas son:
1. Usar l nombre de una ​variable​, o ​direccionamiento directo
2. Usar un valor numérico ​constante, ​o ​direccionamiento inmediato
3. Usar el nombre de un ​registro​, o ​direccionamiento por registro
4. Usar un ​apuntador​, o ​direccionamiento indirecto

Registros
En un procesador de Intel, los registros son:
1. De propósito general: totalmente disponibles al programador
2. De segmento: relacionados con el manejo de RAM, no disponibles al programador
3. De manejo de pila: para manipular la pila, disponibles al programador bajo su propio
riesgo
4. Amputador a instrucción: para uso interno del MP, disponible para lectura al
programador
5. De banderas: para verificar el estado del MP, disponible sólo para lectura
Nomenclatura (legacy) de los registros de
propósito general:
1. Al registro RAX se le suele llamar registro acumulador, porque algunas instrucciones
aritméticas (enteras) lo usan como su depósito por default para almacenar un
operando, o un resultado.
2. Al registro RBX se le suele llamar registro base, porque algunas instrucciones
aritméticas usan arreglos, lo usan para que contenga la dirección base del arreglo, es
decir, la dirección del primer byte, de entre todos ellos.
3. Al registro RCX, se le suele llamar registro contador, porque algunas instrucciones de
ciclo, lo utilizan como contador.
4. Al registro RDX se le llama registro de datos, algunas instrucciones aritméticas
(enteras) esperan en él su único operando a manipular, o esperan a usarlo para
almacenar en él parte de un resultado.

Registros de segmento
Estos registros son un reminiscente de los días en que lo único que había eran sistemas de 16
bits, en esos tiempos un programa se cargaba en RAM en al menos 3 segmentos o secciones
separadas e independientes:

Segmento de código

CS

Segmento de datos
DS

Segmento de pila
SS

Segmento extra
ES

En esos tiempos, existían 4 registros de segmento, usados para apuntar al primer byte de cada
una de esas secciones:
1. El registro CS solía usarse para “apuntar” a la dirección del primer byte de la sección de
código del programa en RAM
2. El registro DS para apuntar al primer byte del segmento de datos
3. El segmento SS para apuntar al primer byte del segmento de pila
4. Y, de ser necesario, existía la posibilidad de un segmento extra, que podía contener
código o datos, y el registro ES apuntaba al primero de los bytes en este espacio.
Con un registro de segmento apuntando al primer byte del segmento (dirección base), cualquier
información dentro de él era direccionada con una dirección de desplazamiento, es decir, la
cantidad de bytes que se deben recorrer a partir de la dirección base, para llegar al primer byte
de tal información:

dato2
dato2 dir. despl = 4

dato1
dir. despl = 2
dir. base

Así, la dirección lógica de cualquier información, en un segmento, es:


dir. base: dir. desplazam
Mientras que la dirección física de esa misma información es:
dir. base + dir. desplazam
Actualmente, en 64 bits, estos registros aún existen, con los nombres de:

✔ RCS
✔✔ RDS
✔✔✔ RSS
✔✔✔✔ RES

Manejar la memoria por segmentos separados para el código, los datos y la pila (y un segmento
extra) era una particularidad de los sistemas de 16 bits, y se llamaba modelo de memoria
segmentada.
Actualmente, en 64 bits, esto no se usa. Ahora, existe el modelo de memoria plana, que usa un
único segmento en donde reside todo: pila, datos y código; ya no es necesario usar nada de lo
que existía en 16 bits, pues ya no existen segmentos. Entonces, ¿para qué se usan actualmente
los registros de segmento?
1. Para manejar accesos entre anillos de protección
2. Para manejo de descriptores, una forma de acceder a la información en la llamada
memoria virtual
Nota: en 64 bits el uso de los registros de segmento es privado al microprocesador. Para el
programador son inaccesibles.
Registros para manejo de pila
La pila es parte esencial en todo programa de bajo nivel. De la pila dependen cuestiones como
llamar a una función, ejecutarla y regresar de ella.
La pila de un programa se usa para contener:
1. Argumentos a funciones
2. Direcciones de retorno
Y, como es una pila, funciona como tal: lo último que ingresó a la pila, es lo primero en salir.
Debido a este comportamiento, existen dos registros apuntadores asociados a la pila del
programa:
1. El registro RSP, que funciona como la dirección del tope de la pila, es decir, contiene la
dirección de desplazamiento del último dato insertado en la pila.
2. El registro RBP, que funciona como la dirección base de la pila, de tal forma que RSP
siempre se expresa como la dirección de desplazamiento a partir de la base en RBP.

dato1 RBD
dato1
dato1
dato2
dato3
dato3
dato3 RSP = 5

Registro apuntador a instrucción


Este registro, llamado RIP, contiene siempre la dirección en la sección de código del programa
de la siguiente instrucción a ejecutar. Únicamente el microprocesador lo puede manipular, el
programador únicamente tiene permitido leerlo.
Registro de banderas:
Este es otro registro cuyo propósito es meramente informativo. Es especial, en el sentido de
que su importancia consiste en leer bits individuales en él, algunos de los bits entre los primeros
16, únicamente. Conocido como el registro RFLAGS, su utilidad consiste en que refleja, luego de
cada instrucción ejecutada en el microprocesador, el estado del mismo. Así, algunos de sus
primeros 16 bits, de manera individual, indican la presencia o no de algún evento de
importancia, por ejemplo:

Nombre de la Significado
bandera
0 La última operación no generó acarreo
Bit de acarreo (CF) CF = {
1 La última operación sí generó acarreo
0 La última operación generó un resultado positivo
Bit de signo (SF) SF = {
1 La última operación generó un resultado negativo
0 La última operación no produjo un desbordamiento
Bit de desbordamiento (OF) OF = {
1 La última operación sí produjo un desbordamiento
0 La paridad del último resultado es par
Bit de paridad (PF) PF = {
1 La paridad del último resultado es non

0 Si la última operación fue una división, el dividendo no fue cero


Bit de cero (ZF) ZF = {
1 El dividendo fue cero
Bandera de interrupción 0 Deshabilitar las interrupciones por software
IF = {
(IF) 1 Habilitar las interrupciones por software

Estas banderas están diseñadas para leerlas, usando la nomenclatura indicada en la tabla. La
única bandera que permite ser editada es la bandera de interrupción (IF).

Registros de punto flotante


Son registros de propósito general, pero para la unidad de punto flotante, o FPU. Son 8
registros, que originalmente eran de 64 bits. En la actualidad, son de 80 bits.

MMX0 FPR0
MMX1 FPR1
MMX2 FPR2
MMX3 FPR3
MMX4 FPR4
MMX5 FPR5
MMX6 FPR6
MMX7 FPR7
79 63 0

Aquí, FPRi para i = 0,…,7 es la versión de 80 bits mientras que MMXi es la versión para 64 bits,
mantenidos solo por compatibilidad. Se usan exclusivamente para aritmética de punto flotante.
Registros SIMD
En algunos casos, resulta muy útil llevar a cabo una misma operación sobre un conjunto de
datos diferentes. A esta forma de operar se le conoce como operación SIMD: Single Instruction
on Multiple Data.

Datos1 Result1

Instrucción
Datos2 Result2

Datosk Resultk

La instrucción se ejecuta en paralelo, al mismo tiempo, sobre k datos.


Los microprocesadores de Intel, a partir de que surgen en 32 bits, pueden operar ciertas
instrucciones en modo SIMD, con el entendido de que 1 ≤ 𝑘 ≤ 4. Para ello, los MP de Intel
proveen al programador con 16 registros SIMD de 128 bits cada uno:
XMM0
XMM1
XMM2
XMM3
XMM4
XMM5
XMM6
XMM7
XMM8
XMM9
XMM10
XMM11
XMM12
XMM13
XMM14
XMM15
Cada uno de esos registros se puede usar para contener:
a) 1 operando de 128 bits
b) 2 operandos de 64 bits
c) 4 operandos de 32 bits
Si se considera únicamente al registro xMMi, para contener a estos operandos, se divide así:
Lenguaje ensamblador básico
1. Como declarar variables (globales)
2. Como intercambiar información entre datos
3. Como llevar a cabo operaciones aritméticas y lógicas con los datos
4. Como organizar el flujo de información en el programa principal

Variables y sus tipos en ensamblador


En un script de ensamblador, toda variable global debe ser declarada y posiblemente inicializada
antes de usarse. Para ello es que existe la llamada sección de datos en todo script de ensamblador.
Existen dos modalidades para la sección de datos.

1. La sección .data para declarar e inicializar variables globales.


2. La sección .data?, para declarar variables globales sin inicializar.

Para declarar una variable se usa la siguiente convención:


nombre_var tipo valor_inicial

En ensamblador, el tipo se refiere a la capacidad que tendrá esa variable para contener información.
Puede afirmarse que existen solo cuatro tipos. Por ejemplo, si los datos son enteros, los cuatro
tipos son los siguientes:

Tipo Longitud (bits) Instrucción


Byte 8 DB
Word 16 DW
Doubleword 32 DD
Quadword 64 DQ
DB viene de “define byte”, DW de “define word”, etc. Si los datos son datos reales o de punto
flotante, los cuatro tipos son:

Tipo Longitud (bits) Instrucción


Byte 8 REAL8
Word 16 REAL16
Doubleword 32 REAL32
Quadword 64 REAL64

»Ejemplo: Definir una variable word inicializada con 100, una quadword inicializada con 2500, y una
double word sin valor inicial.

.data
var1 DW 100
var2 DQ 2500
.data?
var3 DD
»Definir dos variables quadword inicializadas con 25.25 y 2578.66, respectivamente y una double
word sin inicializar, pero de punto flotante

.data
var1 REAL64 25.25
var2 REAL64 2578.66
.data?
var3 REAL32

¿Qué sucede si se define una variable de la siguiente manera?

.data
var1 REAL64 2578.66
Esto no generaría ningún error de compilación, el problema sería cuando se intente operar sobre la
variable:

1. No se puede usar con aritmética entera pues no es en realidad una variable entera.
2. No se puede operar con la FPU, pues se necesita declararla tipo REAL64, no DQ.

Los arreglos no existen como un tipo explícito de datos, como en un lenguaje de alto nivel. Sin
embargo, podemos tener una funcionalidad análoga. Por ejemplo, en C podríamos tener una
declaración como:

int datos[] = {10,20,30,40};


En ensamblador, podría lucir de alguna de las siguientes formas:

datos1 DD 10,20,30,40
datos2 DQ 10,20,30,40
datos3 DW 10,20,30,40 ;; Realmente solo estas dos son las
datos4 DB 10,20,30,40 ;; análogas a C
La diferencia entre cada una de estas declaraciones, es la cantidad en RAM que cada dato va a
requerir. En todo caso, es la utilidad que se le va a dar al arrelo, lo que dicta el tamaño que cada
dato va a requerir.
Arreglos
Normalmente, la instrucción mov, cuando los operandos (fuente y destino), son de
direccionamiento directo, de registro o inmediato, no tiene problema en “cargarlos” correctamente.

¿Qué pasa cuando, por ejemplo, se debe “cargar” un arreglo? Este es el trabajo típico de un
apuntador. En pocas palabras, un apuntador es una variable, por lo general un registro, cuyo
contenido no es un operando como tal, sino una dirección en RAM.

Cualquier registro puede servir para “apuntar” a una dirección en RAM, pero tradicionalmente se
utilizan los registros RBX, RBP, RDI y RSI.

Cuando un arreglo se debe acceder a un arreglo, lo primero es apuntar a él, esto es, obtener la
dirección del primero de sus bytes en RAM, en un registro. Para ello se utiliza una particularización
de la instrucción mov:

mov registro, offset nom_arreglo


Por ejemplo:

mov rbx, offset datos


Este mov pone a rbx a “apuntar” al primero de los bytes alojados para el arreglo de datos

rbx

Una vez tenemos un apuntador al objeto, debemos realizar un direccionamiento indirecto para
poder leer los datos. Para ello, se utiliza la siguiente directiva
indicador_de_tamaño[apuntador]

Aquí, indicador_de_tamaño es una directiva que indica al ensamblador cuántos bytes debe
leer o escribir, a partir de la dirección en el apuntador. Normalmente existen cuatro de estas
directivas:

1. byte ptr: 1 byte


2. word ptr: 2 bytes
3. dword ptr: 4 bytes
4. qword ptr: 8 bytes

Ejemplo: Se declara el siguiente arreglo:

datos1 DB 10,20,30,40
En memoria, luciría más o menos así

40
30
20
10
RDI

Podemos utilizar RDI para apuntar a datos1

mov rdi, offset datos


Para copiar el contenido del primer byte de datos1 al registro AL, haríamos

mov al, byte ptr[rdi] ;; al = 10


Ahora, para copiar el segundo byte en datos1 en BL, haríamos-

mov bl, byte ptr[rdi+1] ;; bl = 20


Y así sucesivamente:

mov cl, byte ptr[rdi+2] ;; cl = 30


mov dl, byte ptr[rdi+3] ;; dl = 40
Hay que recalcar que rdi+4 no implica que en lenguaje ensamblador las sumas se hagan con el
operador “+”. Esto solo funciona con el indireccionamiento.

Una manera más compacta de apuntar a un arreglo es

lea registro, nom_arreglo ;; lea = load effective address

Suma y resta de enteros


Suma de dos operandos enteros:

add oper1, oper2 ;; oper1 = oper1 + oper2


Resta de dos operandos enteros:

sub oper1, oper2 ;; oper1 = oper1 + oper2


Las mismas restricciones que aplican a mov se aplican a las instrucciones add y sub.
Lo que hicimos anteriormente, se pudo haber realizado de la siguiente forma:

lea rdr, datos1


mov al, byte ptr[rdi]
add rdr, 1
mov bl, byte ptr[rdi]
add rdr, 1
mov cl, byte ptr[rdi]
add rdr, 1
mov dl, byte ptr[rdi]
Multiplicación y división de enteros
La multiplicación de dos enteros, asume que se cumple lo siguiente:

resultado = operando1 ∗ operando2


2 ∗ tamaño𝑚 tamaño𝑚 tamaño𝑚
Los operandos deben tener el mismo tamaño, y el resultado tendrá entonces el doble del tamaño
de los operandos. La instrucción para multiplicar en ensamblador es

mul operando2
El direccionamiento al operando2 puede ser directo, indirecto o de registro. Por default, el
operando1 es:

Registro Tamaño operando1


AL byte
AX word
EAX dword
RAX qword
Luego el resultado por default es:

DL:AL si AL* operando2


DX:AX si AX* operando2
EDX:EAX si EAX* operando2
RDX:RAX si RAX* operando2
Tenemos que:

dividendo = divisor ∗ cociente + residuo


Entonces el dividendo es de tamaño 2𝑡𝑚 y el tamaño del cociente, divisor y residuo es
Saltos
La comparación de dos operandos ocurre para verificar si:

1. El primero es menor al segundo


2. Si es mayor
3. O si es igual

Basándose en el resultado de la comparación, la siguiente instrucción a ejecutar será muy específica,


una cuya referencia está en la etiqueta “eti”. Conocida la instrucción a ejecutar dependiendo de la
comparación ¿cómo se lleva a cabo tal “bifurcación”?

Para esto, existen las instrucciones de salto. Estas rompen la ejecución secuencial del código del
programa, forzando a que la siguiente instrucción a ejecutar, pueda ser otra distinta a la siguiente
instrucción en la secuencia. Existen dos tipos de instrucciones de salto:

1. Condicionales: dependiendo de la comparación, saltar o no, a una instrucción etiquetada


2. Incondicionales: saltar necesariamente a la instrucción etiquetada

Las instrucciones de salto condicional, requieren que, justo antes de su ejecución, una comparación
entre dos operandos haya tenido lugar. De acuerdo con la comparación, el salto condicional puede
ser

Salto Significado
je eti Salta si 𝑜𝑝1 = 𝑜𝑝2
jne eti Salta si 𝑜𝑝1 ≠ 𝑜𝑝2
jg eti Salta si 𝑜𝑝1 > 𝑜𝑝2
jge eti Salta si 𝑜𝑝1 ≥ 𝑜𝑝2
jl eti Salta si 𝑜𝑝1 < 𝑜𝑝2
jle eti Salta si 𝑜𝑝1 ≤ 𝑜𝑝2
Se puede utilizar jz en lugar de je, o jnz en lugar de jne. El significado en inglés de cada salto
condicional en inglés es:

je → jump if equal
jne → jump if not equal
jg → jump if greater
jge → jump if greater or equal
jl → jump if lower
jle → jump if lower or equal
jz → jump if zero
jnz → jump if not zero
jb → jump if bigger
jbe → jump if bigger or equal

Los saltos jb y jbe son idénticos a los saltos jg y jge, respectivamente. Los saltos jg, jz,y jnz comparan
magnitud sin signo, jb, je y jne comparan con signo.
La púnica instrucción para un salto incondicional es jmp etiqueta

Ejemplo. Queremos emular el funcionamiento del siguiente fragmento de C:

while(a<0){
a = a+2;
}

Asumiendo que a es de tipo qword inicializada en 0.

ciclo:cmp a, 10
jge afuera
add a, 2
jmp ciclo
afuera:

Obsérvese que:

1. La comparación en el ciclo while, en ensamblador es “al revés”


2. Si la condición del salto condicional no se cumple, siempre se ejecuta la siguiente
instrucción que esté después del salto.

Ahora para el siguiente fragmento:

a = 0;
do {
a = a + 2;
} while (a < 10);
En ensamblador:

mov a,0
ciclo:
add a,2
cmp a, 10
jl ciclo
Para

for (i = 0;i < 10; i++){


a = a + 2;
}
tenemos

mov rcx, 10
ciclo: add a, 2
loop ciclo
Aquí aparece la instrucción loop. Esta instrucción es lo más parecido en ensamblador a un ciclo
for.

1. Exige que RCX tenga la cantidad de ocasiones que el ciclo se va a ejecutar, antes de la
primera iteración
2. Su único argumento es la etiqueta de la primera instrucción adentro del ciclo.

Otro ejemplo:

a = 0;
for (i = 1; i < 10; i++){
for(j = 0; j < 5; j++){
a = a + 1;
}
}
Ensamblador:

mov a, 0
mov rcx, 10
mov rbx, rcs
ciclo1: mov rcx, 5
ciclo2: add a, 1
loop ciclo2
mov rcx, rbx
loop ciclo1
ini
Unidad 3: Ensamblador no tan básico
Uno de los aspectos más útiles de cualquier lenguaje de programación, consiste en Instrucción 1
que permita organizar el código del programa en módulos, o funciones.
Instrucción 2
En ensamblador de 64 bits, las funciones son total responsabilidad del programador.
Todos los detalles acerca de manipular la pila del programa quedan a su cargo.
Instrucción 3
ini Normalmente en ensamblador un programa ejecuta
instrucción por instrucción, desde la primera hasta la
Instrucción 4
Instrucción 1 última, como en la figura de la derecha.

Lo anterior es verdad, a menos que se utilicen saltos y/o Instrucción 5


Instrucción 2
comparaciones (izquierda)

Otra forma de romper una ejecución secuencial, es Instrucción 6


Instrucción 3
usando una función.
ret
Instrucción 4 Nótese que una función es algo que debe ser invocado
(call) para ejecutarse, lo que obliga al microprocesador a ejecutar un
cmp grupo de instrucciones diferente al que ya estaba ejecutando. Este
grupo de instrucciones nuevo deberá terminar con un ret,
jxx afuera

Instrucción 7

jmp ciclo

Instrucción 9

Instrucción 10

ret
Ejemplos de ciclos
Supónganse dos arreglos de tipo qword, como en:

datos1 dq 10,20,30,40,50
datos2 dq 0,0,0,0,0

Ilustrar, mediante un programa en ensamblador, como copiar el arreglo datos1, en datos2,


usando:

a) Un ciclo for
b) Un ciclo while

Para un ciclo for, el programa es:

.data
datos1 dq 10,20,30,40,50
datos2 dq 0,0,0,0,0
public ini
.code
ini proc
lea rsi,datos1
lea rdi,datos2
mov rcx,5
ciclo:
mov rax,qword ptr[rsi]
mov qword ptr[rdi],rax
add rsi,8
add rdi,8
loop ciclo
ret
int endp
end
Para el ciclo while, alteraremos un poco el asunto:

data1 dq 10,20,30,40,50,-10
data2 dq 0,0,0,0,0
El -10 indica el fin del arreglo
.data
datos1 dq 10,20,30,40,50,-10
datos2 dq 0,0,0,0,0
public ini
.code
ini proc
lea rsi,datos1
lea rdi,datos2
ciclo:
mov rax,qword ptr[rsi]
cmp rax,-10
je afuera
mov qword ptr[rdi],rax
add rsi,8
add rdi,8
jmp ciclo
afuera:
ret
int endp
end

También podría gustarte