Documentos de Académico
Documentos de Profesional
Documentos de Cultura
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
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
.code
main: instr1 ;; ← Primer instrucción a ejecutar
instr2
⋮
instrk
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
instr1
instr2
⋮
instrk
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
Para ensamblar:
>> ml64 /c ejem1.asm
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
De acuerdo con la arquitectura Von Neumann, una computadora funciona como un sistema
organizado, en mayor o menor medida de la siguiente manera:
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
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.
Byte 8
Word 16
Double word 32
Quad word 64
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
✔ 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
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
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).
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
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:
»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
.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:
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:
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:
datos1 DB 10,20,30,40
En memoria, luciría más o menos así
40
30
20
10
RDI
mul operando2
El direccionamiento al operando2 puede ser directo, indirecto o de registro. Por default, el
operando1 es:
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:
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
while(a<0){
a = a+2;
}
ciclo:cmp a, 10
jge afuera
add a, 2
jmp ciclo
afuera:
Obsérvese que:
a = 0;
do {
a = a + 2;
} while (a < 10);
En ensamblador:
mov a,0
ciclo:
add a,2
cmp a, 10
jl ciclo
Para
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.
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
a) Un ciclo for
b) Un ciclo while
.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