Apunte 3
Apunte 3
Arquitectura x86-64
Diego Feroldi
*
Actualizado 17 de octubre de 2024 (D. Feroldi, [email protected])
Índice
1. La arquitectura x86-64 1
1.1. Registros de propósito general . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Registros especiales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2.1. Puntero de pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2.2. Puntero de instrucciones . . . . . . . . . . . . . . . . . . . . . . . 2
1.2.3. Registros de segmentos . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.4. Registro de banderas (rflags) . . . . . . . . . . . . . . . . . . . . 3
1.3. Registros SSE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4. Operandos inmediatos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.5. Lenguaje de máquina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.6. Lenguaje Ensamblador de x86-64 . . . . . . . . . . . . . . . . . . . . . . 6
1.7. Directivas al Ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.8. Etiquetas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.9. Definir una etiqueta global . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2. Instrucciones 10
2.1. Instrucciones de transferencia de datos . . . . . . . . . . . . . . . . . . . 10
2.1.1. Instrucción MOV . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.1.2. Instrucción PUSH . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.1.3. Instrucción POP . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.1.4. Instrucción XCHG . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2. Instrucciones aritméticas . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.1. Instrucción ADD . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.2. Instrucción ADC . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.2.3. Instrucción SUB . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.2.4. Instrucción SBB . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.2.5. Instrucción INC . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.2.6. Instrucción DEC . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.2.7. Instrucción IMUL . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.2.8. Instrucción MUL . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2.9. Instrucción IDIV . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2.10. Instrucción DIV . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2.11. Instrucción NEG . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3. Instrucciones de comparación . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.1. Instrucción CMP . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.3.2. Instrucción TEST . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4. Instrucciones lógicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4.1. Instrucción AND . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4.2. Instrucción OR . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.4.3. Instrucción XOR . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.4.4. Instrucción NOT . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.5. Instrucciones rotación y desplazamiento . . . . . . . . . . . . . . . . . . . 20
2.5.1. Instrucción SAL/SHL . . . . . . . . . . . . . . . . . . . . . . . . 21
2.5.2. Instrucción SAR . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.5.3. Instrucción SHR . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.5.4. Instrucción ROL . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2
2.5.5. Instrucción ROR . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.5.6. Instrucción RCL . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.5.7. Instrucción RCR . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.6. Instrucciones para saltos incondicionales . . . . . . . . . . . . . . . . . . 22
2.6.1. Instrucción JMP . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.7. Instrucciones para saltos condicionales . . . . . . . . . . . . . . . . . . . 23
2.8. Otras instrucciones de ruptura de secuencia . . . . . . . . . . . . . . . . 25
2.8.1. Instrucción LOOP . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.8.2. Instrucción CALL . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.8.3. Instrucción RET . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.9. Instrucciones para el registro de banderas . . . . . . . . . . . . . . . . . . 26
2.10. Instrucciones de entrada/salida . . . . . . . . . . . . . . . . . . . . . . . 26
2.11. Instrucciones de conversión . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.11.1. Instrucciones CXX/CXXE . . . . . . . . . . . . . . . . . . . . . . 27
2.11.2. Instrucciones CXTX . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.11.3. Instrucciones MOVSXX . . . . . . . . . . . . . . . . . . . . . . . 28
2.11.4. Instrucciones MOVZXX . . . . . . . . . . . . . . . . . . . . . . . 28
3
6. Aritmética de Punto Flotante 53
6.1. Copias y conversiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.2. Operaciones de punto flotante . . . . . . . . . . . . . . . . . . . . . . . . 54
6.3. Instrucciones SIMD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
Este apunte no es para nada una referencia completa del lenguaje ensam-
blador ni de la arquitectura sino que debe ser utilizado como material
complementario con lo visto en las clases teóricas. Para una información
más detallada consultar las referencias. En particular, consultar [14] para
una información más detallada sobre las instrucciones.
1. La arquitectura x86-64
La arquitectura x86-64 es una extensión de la arquitectura x86, la cual fue introducida
por Intel con el procesador Intel 8086 en 1978 como una arquitectura de 16 bits. Esta
arquitectura evolucionó a 32 bits con el lanzamiento del procesador Intel 80386 en 1985,
inicialmente conocida como i386 o x86-32, y más tarde como IA-32. Entre 1999 y 2003,
AMD amplió esta arquitectura de 32 bits a una de 64 bits, denominándola x86-64 en los
primeros documentos, y posteriormente AMD64. Intel adoptó rápidamente las extensiones
de AMD bajo los nombres IA-32e o EM64T, y finalmente la llamó Intel 64.
La arquitectura x86-64 (también conocida como AMD64 o Intel 64) de 64 bits ofre-
ce un soporte significativamente mayor para el espacio de direcciones virtuales y fı́sicas.
Proporciona registros de propósito general de 64 bits, ası́ como buses de datos y direc-
ciones también de 64 bits, lo que permite que las direcciones de memoria (punteros) sean
valores de 64 bits. Aunque cuenta con registros de 64 bits, también permite operaciones
con valores de 256, 128, 32, 16 y 8 bits.
1
La mayorı́a de los registros de 64 bits están divididos en subregistros de 32, 16 y 8
bits. Ası́, el registro rax de 64 bits contiene en sus 32 bits más bajos al subregistro eax
(la e es por extended ), en sus 16 bits más bajos al subregistro ax y a su vez ax se divide
en dos registros de 8 bits, llamados ah (por high) y al (por low ), respectivamente. Por
razones históricas, esta última división en dos registros de 8 bits sólo se realiza para los
registros rax, rbx, rcx y rdx. Para el resto de los registros sólo existe la parte baja de 8
bits.
Los registros introducidos en la versión de 64 bits (r8-r15) se dividen en r8d (por doble
word, 32 bits), r8w (de word, 16 bits) y r8b (por byte, 8 bits). En la Fig. 1 vemos (casi)
todos los registros de propósito general del x86-64 con sus subregistros y su uso durante
una llamada a función. Asimismo, vemos su rol en la convención de llamada (caller saved
o callee saved ) y si son preservado. Esto será visto en detalle en la Sección 7.
Uso Convención Preservado?
rax eax ax ah al Valor de retorno Caller saved No
rbx ebx bx bh bl Callee saved Sí
rcx ecx cx ch cl 4º argumento Caller saved No
rdx edx dx dh dl 3º argumento Caller saved No
rsi esi si sil 2º argumento Caller saved No
rdi edi di dil 1º argumento Caller saved No
rbp ebp bp bpl Puntero base de pila Callee saved Sí
rsp esp sp spl Puntero tope de pila Callee saved Sí
r8 r8d r8w r8b 5º argumento Caller saved No
r9 r9d r9w r9b 6º argumento Caller saved No
r10 r10d r10w r10b Temporal Caller saved No
r11 r11d r11w r11b Temporal Caller saved No
r12 r12d r12w r12b Callee saved Sí
r13 r13d r13w r13b Callee saved Sí
r14 r14d r14w r14b Callee saved Sí
r15 r15d r15w r15b Callee saved Sí
63 31 15 7 0
2
1.2.3. Registros de segmentos
Los registros de segmento contienen los selectores que se utilizan para acceder a los
segmentos de memoria. Son seis registros de 16 bits cada uno:
cs (Code Segment): Indica cuál es el segmento de código. En este segmento debe alo-
jarse el código ejecutable del programa. En general este segmento es marcado como
sólo lectura.
ds (Data Segment): Indica cuál es el segmento de datos. Allı́ se alojan los datos del
programa (como variables globales).
es, fs, gs: Estos registros tienen un uso especial en algunas instrucciones (las de cadena)
y también pueden ser utilizados para referir a uno o más segmentos extras.
Observación
En modo de 64 bits se utiliza un modelo de segmentación plana de la memoria vir-
tual. Esto significa que el espacio de memoria virtual de 64 bits se trata como un único
espacio de direcciones plano (no segmentado), lo que reduce la utilidad de los registros
de segmentos. El tema de Segmentación se abordará por separado cuando estudiemos
Memoria Virtual.
3
Figura 2: Registro EFLAGS
Ejemplo
Luego de realizarse la suma resulta al=0x91=-111, SF=1, CF=0 y OF=1. El estado de las
banderas indica que el resultado de la suma con los operando interpretados como números
con signo es negativo y es incorrecto. En cambio, si los operandos se toman como números
sin signo, el resultado es correcto (0x91=145).
4
tienen un alias a los respectivos registros SSE de 128 bits (xmm0-xmm15). La utilidad de
estos registros será vista con mayor detalle en la Sección 6.3.
Ejemplos
Observación
Es interesante ver el equivalente en lenguaje de máquina de la última instrucción del
ejemplo anterior. Esto se puede lograr utilizando GDB, con el comando disassemble/r.
Obtenemos el siguiente equivalente en lenguaje de máquina (en formato hexadecimal):
48 c7 c0 44 33 22 11
Aquı́ se ve de manera explı́cita que el valor inmediato está contenido dentro de la
propia instrucción. El motivo por el cual se ve invertido lo veremos en detalle en la
Sección 5.2.
5
Ejemplo
El fragmento de código de una función para sumar dos enteros que se encuentran
guardados en registros se escribirı́a en ensamblador x86-64 como:
El nombre de los registros comienza con %. Por ejemplo, el registro rax se escribe
como %rax.
Las constantes se prefijan con $. Ası́, la constante 5 se escribe como $5. Un caso
particular que veremos luego son las etiquetas.
Las instrucciones que manipulan datos (tanto registros como memoria) se sufijan
con el tamaño del dato. Por ejemplo, agregar el sufijo q a la instrucción mov resul-
tando movq.
Los sufijos posibles son los siguientes:
6
Sufijo Denominación Declaración Tamaño Equivalente GDB
(en .data) (bytes) en C
b Byte .byte 1 char b
w Word .word o .short 2 short h
l Double word .long 4 int w
q Quad word .quad 8 long int g
s Single precision float .float 4 float w
d Double precision float .double 8 double g
En el ensamblador de GNU (as) este sufijo es opcional cuando el tamaño de los ope-
randos puede ser deducido, aunque es conveniente escribirlo siempre para detectar
posibles errores.
Observación
En este apunte utilizaremos la sintaxis de AT&T de lenguaje ensamblador ya que es la
utilizada por defecto en GNU Assembler (GAS)2 . Las principales diferencias entre ambas
son las siguientes:
Intel AT&T
Orden de los operandos destino ← origen origen → destino
Comentarios ; #
Operadores Sin sufijo: add Con sufijo: addq
Registros eax, ebx, etc. %eax, %ebx, etc.
Valores inmediatos 0x100 $0x100
Direccionamiento indirecto [eax] (%eax)
Direc. (forma general) [base+(ı́ndice*scale)+K] K(base, ı́ndice, scale)
7
Ejemplos
.data
(A partir de aquı́ un segmento con datos inicializados)
.bss
(A partir de aquı́ un segmento con datos no inicializados)
.text
(A partir de aquı́ un segmento con código de programa)
Inicializar valores: Esta clase de directivas emite valores constantes indicados por el
programador directamente en el bloque, es decir no se hace traducción. Dentro de
esta clase tenemos:
asciz, ascii Permiten inicializar una lista de cadenas con y sin carácter nulo al
final de cada una.
Ejemplos
Ejemplos
double, float Inicializa una lista de valores de punto flotante de doble y simple
precisión, respectivamente.
Ejemplos
8
short, long, quad Emite una lista de valores enteros de 2, 4 y 8 bytes, respecti-
vamente.
Ejemplos
Ejemplos
.space 128
.space 5000, 0
.zero 5000
Esta directiva es útil para obtener un bloque de memoria de tamaño dado (ya
sea inicializado o no).
Observación
Es importante notar que todas estas directivas toman como argumento una lista
de valores a inicializar. Un error muy común es no indicar ningún elemento en esa
lista, por ejemplo:
.long
lo cual NO reserva espacio. La versión correcta serı́a .long 0 o alternativamente
.space 8.
1.8. Etiquetas
Las etiquetas son una parte fundamental del lenguaje ensamblador, ya que hacen
referencia a elementos dentro del programa. Su función principal es facilitar al progra-
mador la tarea de referenciar diferentes partes del programa, como constantes, variables
o posiciones del código, que se utilizan como operandos en las instrucciones o directivas.
Por ejemplo, cuando se define una variable en C (long i;), se le indica al compila-
dor que reserve un espacio de memoria para un entero y que este espacio se referenciará
9
mediante el identificador i. Tanto en C como en ensamblador, nombrar un espacio de me-
moria es útil para el programador, pero esta información no es utilizada directamente por
la computadora; en su lugar, una etiqueta se convierte en una dirección de memoria.
En ensamblador con sintaxis AT&T una etiqueta es un nombre seguido del sı́mbo-
lo “:”.
Ejemplo
a: .quad 126
Aquı́ se crea una variable de tipo quad (8 bytes) inicializada en 126 en una dirección
de memoria marcada con la etiqueta a.
Ejemplos
.global main
.global sum
2. Instrucciones
Como vimos previamente, las instrucciones de ensamblador en la arquitectura x86-64
están compuestas por un operador (por ejemplo, suma, resta, comparación, etc.) acom-
pañada de operandos (por ejemplo, valores a sumar). En algunos casos las instrucciones
no toman operandos o sus operandos son implı́citos. Por ejemplo, la instrucción ret no
toma operandos, mientras que inc solo toma un operando y lo incrementa en uno (el uno
está implı́cito).
El juego de instrucciones de los procesadores x86-64 es muy amplio y en esta sección
veremos las principales instrucciones para operar con valores enteros. Luego, en la Sec-
ción 6 se verán la instrucciones para operar con datos en punto flotante. Para una mayor
información sobre las instrucciones en x86-64 ver [14].
10
instrucciones para hacer copias de datos siendo la más importante la instrucción mov.
donde “S” es el sufijo que indica el tamaño de los operandos (que deben ser del mismo
tamaño) según lo visto en la Tabla de la página 7.
Observación
El operando destino es el argumento de la derecha, por lo que la instrucción
movq %rax, %rbx
representa rax → rbx. Es decir, copia el valor de rax a rbx. Después de ejecutar la
instrucción, el valor de rbx será igual al de rax. Es importante destacar que el valor del
registro rax permanece sin cambios. En realidad, más que un movimiento, es una copia
de datos.
Ejemplos
movb $65, %al # al=‘A’
movq %rax, %rcx # rcx=rax
movw (%rax), %dx # Copia en dx dos bytes comenzando en la
# dirección guardada en rax.
movw dx, (%rax) # Copia dx en la dirección guardada en rax.
movl 16(%rbp), %ecx # Copia en ecx cuatro bytes (debido al
# sufijo l) comenzando en la dirección rbp+16.
movb $45, a # Copia el valor 45 en la dirección de memoria
# con etiqueta a
11
Observación
La relación entre subregistros de diferentes tamaños es la siguiente:
Esto puede parecer un poco arbitrario pero es ası́ por una cuestión de compatibilidades
a medida que fueron apareciendo procesadores con registros con mayor cantidad de bits.
Ejemplo
12
Ejemplo
popq %rax
Guarda en el registro rax el valor apuntado por el registro rsp.
rsp=0x7fffffffebc8 luego de ser ejecutada si antes era rsp=0x7fffffffebc0 y
rax=45, continuando con el ejemplo anterior
Observaciones
En la arquitectura X86-64 las instrucciones push y pop solo admiten los sufijos w
y q. Sin embargo, lo usual es solo usar dichas instrucciones con el sufijo q para
manener la alineación de la pila.
En la Sección 5.7 veremos en detalle cómo se utilizan las instrucciones push y pop
para manejar la “pila”.
Ejemplo
13
addS <operando fuente>, <operando destino>
Ejemplo
La instrucción add realiza la suma entera. Notar que evalúa el resultado tanto para
la operación sin signo como con signo y establece las banderas CF y OF para indicar si el
resultado es correcto. La bandera SF indica el signo del resultado signado y la bandera
ZF si el resultado es nulo.
Resulta:
<operando destino=operando fuente + operando destino + acarreo>
Ejemplo
14
Realiza la resta: operando destino = operando destino - operando fuente.
Ejemplo
La instrucción sub realiza la resta entera. Notar que evalúa el resultado tanto para
la operación sin signo como con signo y establece las banderas CF y OF para indicar si el
resultado es correcto. La bandera SF indica el signo del resultado signado y la bandera
ZF si el resultado es nulo.
Resulta:
operando destino = operando destino - operando fuente - acarreo.
Ejemplo
Ejemplo
Esta instrucción es equivalente a addq $1, %rax. Sin embargo, la instrucción inc no
modifica el valor de la bandera CF. El resto de las banderas son modificadas de acuerdo
al resultado.
15
2.2.6. Instrucción DEC
Decrementa el operando en una unidad:
decS <operando>
Ejemplo
Esta instrucción es equivalente a subq $1, %rax. Sin embargo, la instrucción dec no
modifica el valor de la bandera CF. El resto de las banderas son modificadas de acuerdo
al resultado.
El formato con un operando utiliza los registros rax y rdx (o una parte) de forma
implı́cita. Es decir, si el operando es de 64 bits multiplica el valor del operando con
rax y el resultado queda en rdx:rax. Notar que el resultado es de 128 bits y los
64 bits menos significativos quedan en rax mientras que los 64 más significativos
en rdx.
De manera análoga, se puede trabajar con operandos de 32 y 16 bits. Es decir, si se
multiplica el valor de un operando de 32 con eax, el resultado queda en edx:eax. Si
se multiplica el valor de un operando de 16 con ax, el resultado queda en dx:ax. Sin
embargo, si se multiplica el valor de un operando de 8 con al, el resultado queda
en ah:al.
Con tres operandos: imulS <op. fuente 1>, <op. fuente 2>, <op. destino>
Este formato requiere dos operandos fuentes y un operando destino. El segundo
operando fuente (que puede ser un registro de propósito general o un valor en
memoria) es multiplicado por el primer operando fuente (un valor inmediato). El
resultado intermedio (el doble de tamaño que el operando fuente) es truncado y
guardado en el operando destino.
16
Ejemplos
Notar que en la última multiplicación el resultado es erróneo dado que el verdadero re-
sultado (1.8447 × 1019 ) no entra en 64 bits.
Ejemplo
17
Ejemplo
Ejemplo
negS <operando>
Ejemplo
18
2.3.1. Instrucción CMP
Esta instrucción realiza la comparación de los dos operandos:
cmpS <operando fuente>, <operando destino>
Ejemplo
En la primera instrucción cmp el operando destino es menor que el operando fuente. Por
lo tanto ZF=0, SF=1. En la segunda instrucción cmp el operando destino es mayor que el
operando fuente. Por lo tanto ZF=0, SF=0. En ninguna de las instrucciones el operador
destino fue modificado.
Realiza la operación lógica and bit a bit sin guardar el resultado. Las banderas
y CF se establecen en 0, mientras que las banderas SF, ZF y PF se ajustan según el
resultado.
Ejemplo
testb %cl, %cl # ZF=1 si cl=0 y SF=1 si cl<0
Ejemplo
19
2.4.2. Instrucción OR
Operación or lógica bit a bit.
Ejemplo
Ejemplo
Ejemplo
20
2.5.1. Instrucción SAL/SHL
Desplazamiento aritmético/lógico a la izquierda.
Ejemplo
Ejemplo
Ejemplo
Ejemplo
21
2.5.5. Instrucción ROR
Rotación lógica a la derecha.
Ejemplo
Ejemplo
Ejemplo
22
La instrucción jmp realiza un salto incondicional a la dirección de memoria indicada
en su operando, el cual puede ser una etiqueta o un registro.
Observación
Dado que el operando de las instrucciones de salto es siempre una dirección de me-
moria, estas instrucciones no llevan sufijo de tamaño.
Ejemplo
............
jmp etiqueta
movq %rax, %rbx
etiqueta:
movq $45, %rcx
............
Ejemplo
............
movq $cont, %rax
jmp *%rax
movq $1, %rax
cont:
movq $2, %rax
............
En este ejemplo jmp *%rax realiza un salto a la dirección contenida en rax, que es la
dirección de cont. Observar el uso del * antes del nombre del registro, lo cual es requerido
por la sintaxis.
donde CC es un sufijo que depende de la condición que se debe cumplir para realizar
el salto. Es decir, salta a la etiqueta si se cumple la condición indicada con CC. De lo
contrario, ejecuta la siguiente instrucción. Por lo tanto, antes de la instrucción jCC debe
haber alguna instrucción que modifique las banderas correspondientes (por ejemplo, una
instrucción de comparación o una instrucción aritmética). En la Tabla 1 mostramos un
listado completo de instrucciones jCC y los valores requeridos en las banderas, donde CC
es el sufijo que depende de la condición que se debe verificar.
23
Tabla 1: Instrucciones jCC y sus correspondientes rFLAGS.
Mnemónico Estado de banderas requerido Descripción
JO OF = 1 Jump near if overflow
JNO OF = 0 Jump near if not overflow
JB CF = 1 Jump near if below
JC Jump near if carry
JNAE Jump near if not above or equal
JNB CF = 0 Jump near if not below
JNC Jump near if not carry
JAE Jump near if above or equal
JZ ZF = 1 Jump near if zero
JE Jump near if equal
JNZ ZF = 0 Jump near if not zero
JNE Jump near if not equal
JNA CF = 1 or ZF = 1 Jump near if not above
JBE Jump near if below or equal
JNBE CF = 0 and ZF = 0 Jump near if not below or equal
JA Jump near if above
JS SF = 1 Jump near if sign
JNS SF = 0 Jump near if not sign
JP PF = 1 Jump near if parity
JPE Jump near if parity even
JNP PF = 0 Jump near if not parity
JPO Jump near if parity odd
JL SF ̸= OF Jump near if less
JNGE Jump near if not greater or equal
JGE SF = OF Jump near if greater or equal
JNL Jump near if not less
JNG ZF = 1 or SF ̸= OF Jump near if not greater
JLE Jump near if less or equal
JNLE ZF = 0 and SF = OF Jump near if not less or equal
JG Jump near if greater
24
Ejemplo
En la instrucción cmpq %rax, %rbx se comparan los operandos y si son iguales ZF=1.
Luego la instrucción je verifica la bandera ZF y si la encuentra seteada salta a etiqueta,
salteando las instrucciones posteriores. Si la bandera no esta seteada, entonces sı́ las
ejecuta.
Decrementa en uno el registro rcx. Aquı́ vemos que rcx tiene un uso especial.
Ejemplo
La instrucción incq %rax se ejecuta 10 veces. Por lo tanto, luego de ejecutarse el código
anterior rax=10.
25
2.8.2. Instrucción CALL
call etiqueta
call *operando
Esta instrucción se utiliza para hace una llamada a subrutina. Esta instrucción y la
siguiente se ven en detalle en la Sección 7.
Prender un bit: stc (set carry flag), std (set direction flag), sti (set interruption
flag).
Sumar añadiendo el carry: adc toma dos operandos, los suma junto con el bit
de carry y lo guarda en el destino.
Acceder al registro: lahf y sahf copian ciertos bits del registro ah hacia el
rflags y viceversa, popfq guarda en la pila el registro rflags y pushfq trae de la
pila el registro rflags.
El uso del registro rflags se verá más claro en breve cuando expliquemos cómo se
usa el registro para hacer saltos condicionales en la Sección 3.
out destino, fuente: escritura del valor especificado por el operando fuente en el
puerto de E/S especificado en el operando destino.
26
2.11.1. Instrucciones CXX/CXXE
Existe un conjunto de instrucciones que doblan el tamaño del registro correspondiente,
extendiendo con el signo el valor almacenado, que tienen la siguiente forma:
cXX
cXXe
donde XX son dos sufijos de tamaño de acuerdo al tamaño del origen y del destino. Estas
instrucciones no tienen operandos explı́citos y operan de manera implı́cita con e registro
rax o sus subregistros.
Aquı́ vemos algunas de las instrucciones disponibles:
Instrucción Descripción
cbw Extiende (con signo) al a ax.
cwde Extiende (con signo) ax a eax.
cwd Extiende (con signo) ax a dx:ax.
cdq Extiende (con signo) eax a edx:eax.
cdqe Extiende (con signo) eax a rax.
cqo Extiende (con signo) rax a rdx:rax.
Observación
Notar que las instrucciones anteriores trabajan con operados implı́citos. Notar también
que hay instrucciones muy parecidas que difieren en que terminan con el sufijo e. Es decir,
son instrucciones con diferentes nombres que hacen la misma conversión pero en el caso
de las instrucciones que termina con e el resultado queda todo en un subregistro y no
repartido en dos subregistros. Para un listado completo de las instrucciones de conversión
consultar [14].
Ejemplo
cXtX
27
que también se usan para hacer conversiones donde se dobla el tamaño del dato. De
manera análoga al conjunto de instrucciones vistas en la sección anterior se usan dos
sufijos de acuerdo al tamaño del origen y del destino. Sin embargo, en este grupo de
instrucciones los sufijos están separados por una t correspondiente a la palabra en inglés
“to”.
Aquı́ vemos algunas de las instrucciones disponibles:
Instrucción Descripción
cwtl Extiende (con signo) ax en eax.
cltq Extiende (con signo) eax en rax.
cqto Extiende (con signo) rax en rdx:rax.
Observación
Los sufijos de tamaño de las instrucciones en esta sección y la anterior corresponden
a lo visto en la tabla de de la Página 7. Sin embargo, notar que aquı́ el sufijo d hace
referencia a “doble word”, es decir 32 bits, y no a doble precisión. De hecho, todas estas
instrucciones utilizan datos de tipo entero. Veremos en la Sección 6.1 las instrucciones
de conversión para datos de tipo flotante.
Ejemplos
28
Ejemplos
movzbl %al, %eax # convierte un byte a 4 bytes
movzwl %ax, %eax # convierte un word a 4 bytes
movzwq %ax, %rax # convierte un word a 8 bytes
Ejemplo
29
cero. Como vimos en la Sección 1.2.4, el procesador mantiene en el registro rflags el
estado de la última operación realizada. Luego, los saltos condicionales de x86-64 hacen
uso de este registro y realizan el salto dependiendo del valor de determinados bits del
registro rflags dependiendo de la instrucción utilizada . De hecho, por cada bit de estado
del registro rflags hay dos saltos condicionales, por ejemplo jz realiza el salto si el bit
ZF está en uno y jnz lo realiza si el bit ZF no está en uno.
Observación
Tanto los saltos condicionales como los incondicionales no llevan sufijo ya que su
operando es siempre una dirección de memoria (dentro del segmento de código).
Junto con los saltos condicionales la arquitectura x86-64 incluye instrucciones pa-
ra comparar dos valores. Una de estas instrucciones es la instrucción cmp. Como ya se
mencionó, esta instrucción realiza una diferencia (resta) entre sus dos operandos, descar-
tando el resultado pero prendiendo los bits del registro rflags acorde al resultado
obtenido.
Siguiendo la lógica de la instrucción sub,
realiza la resta rbx-rax, prende el bit SF (que indica negatividad) si rax es mayor que
rbx pero a diferencia de sub, no modifica el valor del registro destino rbx. Notar
que si ambos valores son iguales la resta tendrá un resultado nulo, prendiendo el bit ZF.
Como la relación que guardan dos valores (cuál es menor y cuál es mayor) depende
de si dichos números se asumen con signo o sin signo, existen dos versiones de saltos
condicionales por comparación de desigualdad. Por ejemplo:
Ejemplo
30
Luego de ejecutarse cmpq %rbx, %rcx las banderas quedan seteadas de la siguiente ma-
nera: SF=1 y OF=0. Por lo tanto, luego de ejecutarse jl menor salta directamente a la
etiqueta menor.
Observación
Es necesario que la instrucción de comparación esté ubicada inmediatamente antes que
la instrucción de salto condicional. Si se colocan otras instrucciones entre la comparación
y el salto condicional, el registro rFlag puede ser alterado y por lo tanto es posible que el
salto condicional no refleje la condición correcta.
long a=0;
if (a==100) {
a++;
}
// seguir
Teniendo en cuenta lo que vimos sobre saltos y comparaciones, una posible traducción
serı́a:
.global main
main:
movq $0, %rax
cmpq $100, %rax
jz igual_a_cien
jmp seguir
igual_a_cien:
incq %rax
jmp seguir
seguir:
....
En este código comparamos el valor de rax con la constante 100. Si el resultado dio cero
(rax-100) es porque son iguales. En este caso debemos incrementar rax.
31
Inmediatamente después de hacer la comparación realizamos el salto condicional. De
tener más instrucciones en el medio, éstas podrı́an modificar el estado del registro
rflags.
Por la naturaleza del if, debemos definir dos etiquetas, una para saltar cuando la
condición es verdadera (igual_a_cien) y otra para continuar la ejecución tanto si
la condición fue verdadera o no (seguir). Notar que si la condición resulta falsa el
programa salteará el bloque igual_a_cien.
long a;
if (a==100) {
a++;
} else {
a--;
}
// seguir
En este código comparamos el valor de rax con la constante 100. Si el resultado dio cero
(rax-100) es porque son iguales. En este caso debemos incrementar rax.
Como ambas ramas del if deben unificarse, luego de hacer el decremento saltamos
a seguir “salteando” la rama verdadera del if.
Notar que como la etiqueta seguir está a continuación del bloque igual_a_cien
el salto puede ser obviado.
32
3.4. Iteraciones
Otra estructura común en los lenguajes de alto nivel son las iteraciones, bucles o lazos.
Con lo visto hasta ahora podemos ya traducir la mayorı́a de las estructuras iterativas.
Ejemplo
Supongamos que queremos traducir la siguiente estructura tipo while:
long int i;
while (i!=0) {
cuerpo_del_while();
i--;
}
Como antes, asumiremos que en ensamblador i es una etiqueta que aloja lugar para
un entero de ocho bytes. Esto puede traducirse como:
while_1:
cmpq $0, i # Evaluar la condición
je fin_1 # Si resulta falsa, el lazo termina
Las estructuras del tipo for son también muy comunes en lenguajes de alto nivel.
Una forma particular de for es repetir un bloque de código una cantidad de veces dadas.
Ejemplo
Dada la siguiente estructura tipo for:
int i;
for (i=100;i>0;i--) {
cuerpo_del_for();
}
33
while_1:
Falsa
Evaluar la
condición
Verdadera
cuerpo_del_while_1:
Cuerpo del
while
fin_1:
34
indica al procesador si debe incrementar o decrementar los registros de ı́ndice (se puede
apagar con cld para que se incrementen o prender con std para que se decrementen).
A continuación veremos las diferentes instrucciones de manejo de arreglos y cadenas con
sus respectivos ejemplos.
movw (%rsi),%ax
addq $2,%rsi
Notar que aquı́ se utiliza el subregistro ax para compatibilizar con el sufijo w de word y
que por lo tanto el incremento es dos bytes.
movb (%rsi),%regtemp
movb %regtemp, (%rdi)
addq $1, %rsi
addq $1, %rdi
siendo regtemp un registro temporario del procesador (en realidad no existe ese registro).
35
Observación
Notar que las instrucciones para manejo de arreglos y cadenas trabajan con operan-
dos implı́citos, es decir, los operandos no se declaran explı́citamente sino que ya viene
prefijado con que operandos se trabaja.
Ejemplo
Un caso tı́pico de uso de estas instrucciones de cadena es para traducir el siguiente
fragmento C:
int f(char *a, char *b) {
int i;
for (i=0;i<100;i++)
a[i]=b[i];
}
Al repetir 100 veces la instrucción movsb copiamos los 100 bytes de b hacia a. El
mismo efecto se podrı́a haber obtenido copiando 50 veces un word (con movsw), 25 veces
un long (con movsl) o 12 veces un quad (con movsq) y un long extra.
Supongamos que ahora debemos modificar el arreglo como sigue:
int f(int *a) {
int i;
for (i=0;i<100;i++)
a[i]++;
}
36
movq $100, %rcx # iteramos 100 veces
cld # iremos incrementando rsi y rdi (DF=0)
l:
lodsl # cargamos en eax el elemento del arreglo (apuntado por rsi)
incl %eax # lo incrementamos
stosl # lo guardamos en el arreglo (apuntado por rdi)
loop l # pasamos al siguiente elemento
ret
Vemos que en este caso el uso del registro eax es útil para obtener el valor original del
elemento (con lodsl), modificar el registro (con incl) y luego guardarlo de nuevo (con
stosl). Notar también que en este caso el arreglo destino y origen son el mismo, por ello
copiamos rdi en rsi al iniciar la función.
Ejemplo
Veamos un caso de uso de las instrucciones de búsquedas y comparaciones de cadenas.
Supongamos que queremos implementar en ensamblador la siguiente función C que busca
un elemento en un arreglo.
37
Esta función puede ser implementada en ensamblador como sigue:
.global find
find:
cld # iremos incrementando rdi (DF=0)
movq $100, %rcx # iteramos 100 veces
movl %esi, %eax # buscamos el 2do argumento
sigue:
scasl # comparamos el elemento actual con eax
je found # si lo encontramos terminamos
loop sigue # si no seguimos
movq $0, %rax # no lo encontramos, retornar 0
jmp fin
found:
movq $1, %rax # lo encontramos, retornar 1
fin:
ret
.global f
f:
# por convención de llamada tenemos en rdi el puntero al arreglo a
# y en rsi el puntero al arreglo b
movq $100, %rcx # debemos iterar 100 veces
cld # iremos incrementando rsi y rdi (DF=0)
rep movsb # repite movsb 100 veces
ret
Al igual que existen los saltos condicionales, existen los prefijos de repetición con-
dicionales. Ası́, los prefijos repe y repne repiten la instrucción mientras el bit Z esté
prendido/apagado a lo sumo rcx veces. El ejemplo de la búsqueda de un entero de la
Sección 4.2 puede ser reescrito utilizando prefijos de repetición condicional como:
38
.global find
find:
cld # iremos incrementando rdi (DF=0)
movq $100, %rcx # iteramos 100 veces
movl %esi, %eax # buscamos el 2do argumento
repne scasl # repetimos mientra sea distinto o a lo sumo rcx veces
je found # si lo encontramos terminamos
movq $0, %rax # no lo encontramos, retornar 0
jmp fin
found:
movq $1, %rax # lo encontramos, retornar 1
fin:
ret
Observación
Notemos que el prefijo repne repite la instrucción mientras la comparación resulte
distinta y a lo sumo rcx veces, pero ¿cómo saber por cuál de las dos causas finalizó la
repetición?
Cuando la condición del prefijo resulta falsa los registros rsi,rdi son incrementados
o decrementados según corresponda y el registro rcx es decrementado pero los bits del
registro rflags quedan intactos dejando allı́ el valor de la última comparación. Por lo
tanto, podemos realizar un salto condicional para ver si la última comparación dio igual
o distinto.
39
Segmento de datos: En este segmento están todos los datos estáticos inicializados
cuando se inicia el programa. Este segmento tampoco crece de manera dinámica.
El segmento de datos está dividido en dos partes:
Segmento heap: En este segmento están los datos asignados por malloc o new. A
diferencia de los segmentos anteriores, el heap crece de manera dinámica.
direcciones de retorno
algunos parámetros de la función
variables locales de funciones
espacio para variables temporales
5.2. Endianness
El término inglés endianness designa el formato en el que se almacenan en memoria
los datos de más de un byte. El problema es similar a los idiomas en los que se escribe de
7
El tema Gestión de la Pila será visto con mayor detalle en la Sección 5.7.
8
Esto es realidad no es exactamente ası́. La región superior del espacio de direcciones está reservada
para el núcleo (Memoria virtual del núcleo) pero para los fines prácticos podemos asumir que la región
superior es la pila.
40
Figura 4: Modelo de memoria de un proceso en Linux [16].
derecha a izquierda, como el árabe, o el hebreo, frente a los que se escriben de izquierda
a derecha, pero trasladado de la escritura al almacenamiento en memoria.
Supongamos que tenemos que almacenar el entero 168496141 en la dirección de me-
moria a. Este valor se representa mediante los cuatro bytes 0x0A 0x0B 0x0C 0x0D (es-
cribiendo más a la izquierda el byte más representativo).
Una opción es guardar el byte más significativo (0x0A) en la dirección a, el segundo
(0x0B) en la dirección a+1, y ası́ sucesivamente. Esto se conoce como convención Big-
Endian como puede verse en la Fig. 5(a).
Register
Register
Memory 0A0B0C0D 0A0B0C0D Memory
...
...
a: 0A a: 0D
a+1: 0B a+1: 0C
a+2: 0C a+2: 0B
a+3: 0D a+3: 0A
Big-endian Little-endian
...
...
41
siguiente (0x0C) en la dirección a+1 y ası́ sucesivamente. Esta última convención se
denomina Little-Endian y es la utilizada por las arquitecturas x86 y por la tanto también
por x86-64 . La Fig. 5(b) muestra la convención Little-Endian.
Ejemplo
.data
var1: .byte 0
var2: .byte 0x61
var3: .word 0x0200
var4: .long 0x0001E26C
Las variables se encontrarán en memoria tal como muestra la siguiente tabla (supo-
niendo que la variable var1 está en la dirección 0x600880):
42
Ejemplo
Veamos ahora un ejemplo completo que engloba todos los conceptos vistos en las sec-
ciones anteriores:
.data
i: .long 0
f: .double 3.14
str: .asciz "Hola mundo"
.bss
a: .quad
.text
.global main
main:
movq $40, %rax # rax=40
movl i, %ebx # ebx=0
movq $-1, a # a=0xffffffffffffffff (-1)
movq f, %rdx # rdx=0x40091eb851eb851f (3.14)
movl str, %ecx # ecx=0x616c6f48 ("aloH")
retq
Aquı́ vemos que la etiqueta i (dentro del segmento de datos .data) define la posición de
memoria donde el ensamblador alojará un entero inicializado en 0 (4 bytes). Luego en
f un valor de punto flotante inicializado en 3.14 (8 bytes). Luego en str arranca una
cadena de caracteres de 11 bytes (el byte final corresponde al cero final). En el segmento
.bss se crea una variable tipo quad sin inicializar. Finalmente, vemos que dentro del
segmento de código se define una etiqueta global llamada main. Este será el punto de
inicio del programa. Luego, a medida que se vayan ejecutando las instrucciones siguientes
los registros y locaciones de memoria irán quedando con los valores indicados en los
comentarios.
Notar que luego de ejecutarse la instrucción movl str, %ecx el valor del subregistro
ecx es "aloH", que corresponde a los primeros 4 bytes de la cadena str pero con los
caracteres en orden invertido debido al formato little-endian. Sin embargo, hay que notar
que las cadenas de caracteres se almacenan en la memoria “concatenando” los caracteres
consecutivamente desde el primer carácter hasta el último comenzando en las posiciones
más bajas de memoria. Por lo tanto, el carácter ’H’ estará almacenado en la posición
str, el carácter ’o’ en la posición str+1, y ası́ sucesivamente hasta llegar al último
carácter que es el null.
Observación
Como hemos visto, podemos acceder a un dato en memoria utilizando la etiqueta
que define la dirección de memoria donde dicho dato comienza. Ahora supongamos que
queremos incrementar el valor de una variable definida por la etiqueta i, esto podemos
43
hacerlo simplemente escribiendo:
incq i
Si antes era i=23, ahora es i=24. Es importante notar que aunque la etiqueta i es
una constante, es decir, la dirección de memoria donde se aloja ese valor, la etiqueta i
no lleva el signo $.
Si ahora quisiéramos sumar i con el registro rax podemos escribir:
addq i, %rax
Sin embargo, notar que addq $i, %rax produce un efecto muy diferente. En este caso
sumará una constante (la dirección de i) y no el valor alojado en i.
Muchas veces es útil conocer la dirección de memoria donde está alojado un valor.
Esto en C se conoce como obtener un puntero al dato. Ası́, si tenemos una variable
long int i; podemos obtener un puntero a dicha variable utilizando el operador de re-
ferencia, escribiendo &i. Como antes mencionamos, en ensamblador una etiqueta es una
dirección de memoria constante. Por ello si quisiéramos obtener el valor de esa dirección
podrı́amos escribir:
Ejemplo
Este ejemplo es interesante para ver la diferencia entre usar una etiqueta y el valor
allı́ guardado.
.data
str: .asciz "hola mundo"
.text
.global main
main:
movq str , %rax # Instruccion 1
movq $str, %rax # Instruccion 2
retq
¿Qué diferencia hay entre la instrucción 1 y la 2? Aunque casi similares, las dos
instrucciones son muy distintas entre sı́. Ambas son un movimiento con destino a rax,
pero veamos qué mueven.
Al ejecutar la primera, rax toma el valor de 7959387902288097128. ¿Qué ha ocurrido
aquı́? La instrucción le indica al procesador que debe copiar 8 bytes (ya que es un quad)
desde la región de memoria indicada por la etiqueta str a rax. Como en esa región de me-
moria se aloja la cadena de caracteres "hola mundo" los primeros 8 bytes son hola mun
y de allı́ el valor tan extraño. El valor 7959387902288097128 se puede descomponer en
44
hexadecimal en los siguientes bytes 0x6e 0x75 0x6d 0x20 0x61 0x6c 0x6f 0x68, don-
de cada uno corresponde en decimal a 110 117 109 32 97 108 111 104 y al convertirlo
en caracteres ASCII son “num aloh” (notar que la frase aparece al revés por ser x86-64
little endian).
Al ejecutar la segunda lo que ocurrirá es que en rax se guardará la dirección de
memoria donde está guardada la cadena de caracteres. Este valor dependerá del proceso
de compilación y carga. Notemos que en este caso ningún carácter de esa cadena será
copiado a rax. De hecho esa instrucción no accede a la memoria.
Ejemplos
movq a, %rax
Carga en el registro rax 8 bytes a partir de la dirección cuya etiqueta es a.
45
Ejemplo
Ejemplos
donde la base y el ı́ndice pueden ser cualquier registro de propósito general, la escala
puede ser 1, 2, 4 u 8 y el desplazamiento ha de ser un número representable con 32 bits.
De esta manera la dirección especificada resulta:
46
Ejemplos
Ejemplo
movq (%rax), %rbx
Copia en el registro rbx lo apuntado por el registro rax y no el contenido del mismo.
Es decir, copia los 8 bytes (debido al sufijo q) a partir de la dirección de memoria guardada
en el registro rax en el registro rbx.
Esta notación también permite formas más complejas utilizando los modos de direc-
cionamiento vistos en las secciones anteriores:
K(%reg) refiere al valor apuntado por reg más un corrimiento de K bytes, donde
K es entero. El valor de K puede ser negativo, por lo cual se puede conseguir un
corrimiento ascendente o descendente. Notar que aquı́ la constante K no lleva el
sı́mbolo $.
Ejemplos
47
movq (%rax,%rax,2), %rbx # rbx <--- *(rax+rax*2)
movq -4(%rbp, %rdx, 4), %rbx # rbx <--- *(rbp+rdx*4-4)
movq 8(,%rax,4), %rbx # rbx <--- *(rax*4+8)
Ejemplo
Si tenemos un arreglo de enteros de 32 bits (4 bytes) apuntado por rax y queremos
acceder el sexto elemento podemos hacer:
Notar que a pesar de que el primer operando parece ser una referencia de memoria,
en lugar de leer desde la ubicación designada, en realidad la instrucción solo copia la
dirección efectiva al destino y NO accede a memoria. Esta instrucción es equivalente al
operador & utilizado en el lenguaje C.
Ejemplo
leaq str, %rax # En rax queda la dirección de la etiqueta str
movq $str, %rax # Esta instrucción es equivalente a la anterior
movq (%rax), %rbx # Se dereferencia la dirección str
48
La instrucción lea a menudo se usa como un “truco” para hacer ciertos cálculos,
aunque ese no sea su propósito principal. Usando sintaxis AT&T, los modos de direccio-
namiento útiles con lea son los siguientes:
lo cual corresponde a:
%dest = desplazamiento + %base
%dest = %ı́ndice * multiplicador
%dest = desplazamiento + %ı́ndice * multiplicador
%dest = %base + %ı́ndice * multiplicador
%dest = desplazamiento + %base + %ı́ndice * multiplicador
Ejemplos
La instrucción lea se puede usar para multiplicar un registro por 2, 3, 4, 5, 8, o 9:
donde %src y %dst pueden ser el mismo registro. Además, se le puede sumar una cons-
tante, todo en un solo paso.
49
Almacenamiento temporal: por ejemplo, las variables automáticas en C se al-
macenan en la pila.
Aunque la arquitectura permite utilizar la pila con cualquier fin, es muy común que
cada función utilice una porción de la pila para guardar sus variables locales, argumen-
tos, dirección de retorno, etc. A esta sub-porción de pila se la conoce como marco de
activación de la función. En la Fig. 6 vemos un posible estado de la pila con diferentes
marcos de activación (sólo uno está activo en un momento dado, el de la función que se
está ejecutando).
Observación
En la Fig. 6 se ve que el último elemento insertado en la pila está ubicado en di-
recciones más bajas de memoria, es decir, en la implementación de x86-64 la pila
crece hacia direcciones más bajas. Esto es ası́ por cuestiones históricas y para permitir
que tanto el segmento de datos como el de pila crezcan de forma de optimizar el espacio
libre (el de datos crece desde abajo hacia arriba y el de pila desde arriba hacia abajo).
rsp (stack pointer ): Es un registro de 64 bits que apunta (guarda la dirección de memoria)
al último elemento apilado dentro del segmento de pila (tope).
rbp (base pointer ): Es un registro de 64 bits que apunta al inicio de la sub-pila o marco
de activación.
Aunque ambos registros tienen este uso particular pueden ser manipulados por las ins-
trucciones habituales (add, mov, etc). Como se menciono en las Secciones 2.1.2 y 2.1.2, la
arquitectura x86-64 ofrece también dos instrucciones especiales para “apilar”/“desapilar”
elementos:
pushq Primero decrementa el registro rsp en 8 (recordemos que la pila “crece” hacia
direcciones más bajas) y luego almacena en esa dirección el valor que toma como
argumento. Ası́, la instrucción pushq $0x12345678 es equivalente a
50
……
Comienzo de la pila
…………………….
Marcos de
activación
anteriores
Direcciones Crecimiento
de memoria de la pila
crecientes
……
Argumento n
Marco de
activación del
……...
llamante
+16 Argumento 7
+8 Dirección de retorno
%rbp guardado
Puntero base %rbp (opcional)
-8
Registros guardados,
variables locales y
temporales
Marco de
activación
actual
Área de construcción
de argumentos
Zona roja
(opcional)
128 bytes
……
popq Primero copia el valor apuntado por el registro rsp en el operando que toma como
argumento, luego incrementa el registro rsp en 8 (la pila decrece hacia direcciones
más altas). Ası́, la instrucción popq %rax es equivalente a
51
0x1008 ocupado 0x1008 ocupado
Observaciones
Si bien en las instrucciones push y pop podemos utilizar tanto el sufijo w como el
sufijo q, por cuestiones de alineación de la pila en general los datos insertados en
la pila deben ser de 8 bytes utilizando el sufijo q.
¿Qué significa que un dato o un marco de pila está alineado a una cantidad deter-
minada de bytes? Significa que su dirección de memoria es divisible por la cantidad
de bytes en cuestión. Por ejemplo: la dirección de memoria 0x404030 está alineada
a 16 bytes mientras que la dirección 0x7fffffffebb8 está alienada a 8 bytes.
52
6. Aritmética de Punto Flotante
La arquitectura x86-64 soporta aritmética de datos de punto flotante utilizando el
estándar IEEE 754 tanto para simple como doble precisión. Las operaciones de punto
flotante se realizan a través de una extensión de la arquitectura que podemos considerar
separada conceptualmente de la ALU (llamada SSE -Streamming SIMD Extension)
Por lo tanto se utilizan otros registros e instrucciones. Para esto hay 16 registros de
128 bits (16 bytes): xmm0 a xmm15. Cada registro puede contener un elemento (i.e.: un
flotante de simple o doble precisión) en cuyo caso el valor se considera “escalar” (scalar)
y se usa sólo una parte del registro, o puede contener múltiples elementos del mismo
tamaño (formato “empaquetado” -packed-). Por ejemplo, en xmm0 entran 4 flotantes de
simple precisión o también 16 enteros de 1 byte (chars). El formato empaquetado permite
que algunas instrucciones realicen la misma operacion sobre varios datos a la vez (SIMD:
Single Instruction Multiple Data).
Las instrucciones siguen algunas reglas:
Las letras s (por “scalar”) y p (“packed”) indican qué formato se utiliza.
Las letras s (por “single”), d (“double”) e i (ı̈nteger”) indican el tipo de datos
involucrado. Además q indica que un entero es tamaño quadword (i.e.: 8 bytes).
Por ejemplo, cvtsi2sdq permite convertir un entero almacenado en un quadword a
un double en formato escalar. Se interpreta ası́:
cvt: convert (convertir)
si: scalar integer (un entero con signo)
2: two (“two” suena como “to” - a -)
sd: scalar double (un flotante escalar de doble precisión)
q: quadword (el entero mencionado es un quadword)
Veremos primero las instrucciones de copias y conversiones, luego las operaciones
aritméticas escalares y luego las operaciones sobre datos empaquetados (SIMD).
Ejemplo
Veamos el procedimiento para inicializar una variable de tipo double (en el registro
xmm0) con el valor 1.0:
movq $1, %rax # Copiar un 1 entero a rax
cvtsi2sdq %rax, %xmm0 # Convierte el 1 de rax al double 1.0 en xmm0
53
Tabla 2: Instrucciones de copia y conversiones para punto flotante [5].
Instrucción S D Descripción
movss S, D M32/X X Copiar precisión simple
movss S, D X M32 Copiar precisión simple
movsd S, D M64/X X Copiar precisión doble
movsd S, D X M64 Copiar precisión doble
cvtss2sd S, D M32/X X Convertir de simple a doble precisión
cvtsd2ss S, D M64/X X Convertir de doble a simple precisión
cvtsi2ss S, D M32/R32 X Convertir entero a simple precisión
cvtsi2sd S, D M32/R32 X Convertir entero a doble precisión
cvtsi2ssq S, D M64/R64 X Convertir quadword entero a simple precisión
cvtsi2sdq S, D M64/R64 X Convertir quadword entero a doble precisión
cvttss2si S, D X/M32 R32 Convertir (truncado) simple precisión a entero
cvttsd2si S, D X/M64 R32 Convertir (truncado) doble precisión a entero
cvttss2siq S, D X/M32 R64 Convertir (truncado) simple precisión a quadword entero
cvttsd2siq S, D X/M64 R64 Convertir (truncado) doble precisión a quadword entero
Ejemplo
Veamos, con lo que tenemos cómo traducir la siguiente función C:
double convert(double t) {
return t*1.8 + 32;
}
Veremos en la Sección 7 que la convención de llamada indica que los argumentos de punto
flotante se pasan por los registros xmm y el valor de retorno se deja en el registro xmm0.
Sabiendo esto podemos escribir:
.global convert
convert:
# en xmm0 viene t por convención de llamada
54
Tabla 3: Instrucciones aritméticas en punto flotante[5].
Al igual que con los valores enteros la arquitectura ofrece comparaciones de valores
de punto flotante. Las instrucciones de comparación comparan dos valores (haciendo
una resta virtual) y prenden las banderas correspondientes en el registro rflags. La
comparación se comporta como una comparación de datos unsigned (i.e.: conviene utilizar
jae para saltar por mayor o igual). Además, si los valores son incomparables (alguno es
NaN) se prende la bandera PF (Parity Flag). Las instrucciones de comparación en
punto flotante se muestran en la Tabla 4
Las instrucciones de comparación de punto flotante establecen tres banderas de con-
dición: la bandera cero ZF, la bandera de acarreo CF y la bandera de paridad PF. Los
banderas de condición se establecen de la siguiente manera:
55
Orden CF ZF PF
“desordenado” 1 1 1
S1 < S2 1 0 0
S1 = S2 0 1 0
S1 > S2 0 0 0
Instrucciones de conversión.
Instrucciones aritméticas.
56
Tabla 5: Algunas intrucciones packed.
Mnemotécnico Descripción
movaps Mueve cuatro flotantes simple precisión alineados entre registros XMM
o memoria.
movapd Mueve dos flotantes dobles precisión alineados entre registros XMM o
memoria.
addps Suma flotantes simple precisión empaquetados.
divps Divide flotantes simple precisión empaquetados.
divss Divide flotantes simple precisión escalares.
mulps Multiplica flotantes simple precisión empaquetados.
subps Resta flotantes simple precisión empaquetados.
cmpps Compara flotantes simple precisión empaquetados.
andnps Realiza la operación AND NOT bit a bit de flotantes simple precisión
empaquetados.
andps Realiza la operación AND bit a bit de flotantes simple precisión em-
paquetados.
orps Realiza la operación OR bit a bit de flotantes simple precisión empa-
quetados.
xorps Realiza la operación XOR bit a bit de flotantes simple precisión em-
paquetados.
Instrucciones lógicas.
La Tabla 5 muestra algunas instrucciones. Sin embargo, las extensiones SSE contienen
muchas más instrucciones. En este apunte sólo se pretende dar una introducción. Un
listado completo se puede consultar en [8] o [9]. Por otra parte, en el 2011 se introdujo
una nueva tecnologı́a de instrucciones SIMD llamadas AVX de 256 bits, pero estas no
serán vistas en este apunte.
Ejemplo
Veamos un ejemplo de instrucciones packed. La siguiente función suma cuatro flo-
tantes almacenados a partir de la dirección etiquetada con a con los cuatro flotantes
almacenados a partir de la dirección etiquetada con b:
.data
.align 16
a: .float 1.0, 2.0, 3.0, 4.0
b: .float 1.0, 2.0, 3.0, 4.0
.text
.global main
main:
movq $a, %rdi # rdi apunta a "a"
movq $b, %rsi # rsi apunta a "b"
movaps (%rdi), %xmm0 # copia los 4 floats de "a" a xmm0
movaps (%rsi), %xmm1 # copia los 4 floats de "b" a xmm1
57
addps %xmm0, %xmm1 # suma los 4 floats a la vez
movaps %xmm1, (%rdi) # guarda el resultado en "a"
ret
Aquı́ la instrucción interesante es addps que suma los 4 valores flotantes de precisión
simple a la vez. La Fig. 10 ilustra esta instrucción. Notar que para poder usar la instruc-
ción movaps los datos tienen que estar alineados a 16 bytes. Esto se puede lograr con la
directiva .align.
xmm0
+ + + +
xmm1
xmm1
...
i++;
printf("%d\n",i);
i--;
...
58
Aquı́ vemos tres instrucciones. La segunda es una llamada a la función printf con dos
argumentos, una cadena de caracteres "%d\n" y el valor de i. Luego de finalizada la
impresión por parte de printf el código debe seguir con el decremento de i. Pero ¿cómo
sabe printf que debe continuar con esa instrucción (siendo que printf podrı́a ser lla-
mada de múltiples lugares distintos)? La respuesta es que no lo sabe, sino que el código
que invoca a esta función debe indicarle adonde continuar la ejecución luego de finalizar
la llamada. Esta dirección donde debe continuar se conoce como dirección de retorno.
Para realizar llamadas a función, la arquitectura x86-64 provee dos instrucciones:
call Realiza la invocación a la función indicada como operando (la etiqueta que la define)
guardando en la pila la dirección de retorno (la dirección de la próxima instrucción
al call). Ası́ la instrucción call f serı́a equivalente a
pushq $direccion_de_retorno
jmp f
direccion_de_retorno:
ret Retorna de una función sacando el valor de retorno que se encuentra en el tope de la
pila (puesto allı́ por el call) y salta a ese lugar. Ası́ la instrucción ret equivale a
popq %rdi
jmp *%rdi
aunque ret no modifica ningún registro (más que el %rip) y el registro rdi solo se
ha usado para ilustrar el funcionamiento con un código equivalente. En este código
el asterisco es necesario por la sintaxis.
Cuando las funciones son “llamadas” dentro de un programa se reconocen dos actores
en cuanto a responsabilidades:
El llamante (caller ) es la parte de código que invoca a la función en cuestión. El caller
quiere computar el valor de la función para ciertos valores de argumentos y luego
seguir computando con el resultado obtenido.
59
¿Qué registros mantendrán su valor luego de la llamada?
Los seis primeros argumentos a la función son pasados por registro en el siguiente
orden: %rdi, %rsi, %rdx, %rcx, %r8, %r9 (cuando los argumentos son valores enteros
o direcciones de memoria).
Si los argumentos son valores de punto flotantes pueden utilizarse hasta 8 de los
registros xmm en el siguiente orden: %xmm0, %xmm1, %xmm2, %xmm3, %xmm4, %xmm5,
%xmm6 y %xmm7.
Parámetros grandes mayores a 64 bits, por ejemplo estructuras pasadas por valor,
se pasan utilizando la pila.
Cuando la función toma como argumento una mezcla de valores enteros y flotantes
rdi será el primer valor entero, xmm0 el primer valor flotante, y ası́ sucesivamente.
Ası́, en la función void f(int, double, int, double) los argumentos irán en
rdi,xmm0,rsi,xmm1.
Si hubiera más argumentos de los que se pueden pasar por registros, éstos son
pasados a la función utilizando la pila.
Los otros registros (incluso los utilizados para pasar los argumentos) pueden ser
modificados libremente por la función sin necesidad de restaurar sus valores. Si
el llamante desea preservar sus valores es responsabilidad de él, por lo cual estos
registros se conocen como caller saved. En la Fig. 1 se puede observar el rol de los
registros en la llamada a función.
El puntero de la pila (rsp) debe estar alineado a 16 bytes antes de realizar una lla-
mada a función. Esto asegura que cualquier instrucción de operaciones con registros
SIMD (usados para procesar datos en paralelo) funcione correctamente.
Como %rbp y %rsp son preservados durante una llamada a función, el estado de la
pila del llamante se mantiene.
60
Respecto a este último punto (la preservación de la pila), es muy común que cada
función demarque el comienzo de su porción de pila utilizando el %rbp. Como este
registro es calle saved debe ser preservado por el llamado. Por esta razón, es común
encontrar secciones denominadas prólogo y epı́logo en una función, como se muestra a
continuación:
#prólogo
pushq %rbp # Guardar el valor del rbp del llamante
movq %rsp, %rbp # La pila para esta función comienza en el tope (vacı́a)
..............
CÓDIGO DE LA FUNCIÓN
..............
#epı́logo
movq %rbp, %rsp # El registro rsp vuelve a apuntar al tope de la pila anterior.
popq %rbp # Restaurar el rbp del llamante
Ejemplo
Veamos cómo llamarı́amos a la función sum antes vista con los argumentos 40 y 45:
...
movq $40, %rdi # el primer argumento es 40 y va en el registro rdi
movq $45, %rsi # el segundo argumento es 45 y va en el registro rsi
call sum # guarda la dirección de retorno en pila y salta a sum
movq %rax, i # aquı́ %rax contiene el resultado (85)
...
# Epı́logo
movq %rbp, %rsp
popq %rbp
Ejemplo
En este ejemplo vemos cómo llamar a la función printf para imprimir un entero y
un flotante doble precisión:
61
.data
str: .asciz "%d %f\n"
a: .long 45
f: .double 3.14
.text
.global main
main:
pushq %rbp # Alineamos el stack
leaq str, %rdi # Le pasamos la direc. de la cadena de formato
movl a, %esi # Le pasamos el segundo argumento
movsd f, %xmm0 # le pasamos el tercer argumento
movb $1, %al # Cantidad de argumentos de punto flotante
call printf # Llamamos a la función printf
popq %rbp # Desapilamos para preservar el valor de rsp
xorl %eax, %eax # Retornamos cero
ret
Notar que para poder utilizar la función printf la convención de llamada AMD64 System
V ABI[11] requiere varias cuestiones:
Justo antes de una instrucción call la pila debe estar alineada al menos con 16
bytes.
También debemos terminar la cadena de formato con NULL. Por lo tanto, en lugar
de utilizar .ascii, utilizar .asciz.
Apéndices
A. Compilando código ensamblador con GNU as
Un programador puede escribir todo su programa en ensamblador. El único requeri-
miento es que el código defina
una etiqueta global dentro del segmento de código llamada main. Una vez escrito el
código, el programa puede ser compilado utilizando gcc:
gcc sum.s
62
También podemos usar la opción -o:
./sum
Ejemplo
./main
y obtendremos el resultado:
15.140000
63
B. Depurando el código con GDB
GDB o GNU Debugger es el depurador estándar para el compilador GNU. Se puede
utilizar tanto para programas escritos en lenguajes de alto nivel como C y C ++ como
para programas de código ensamblador.
Continuando con el ejemplo anterior, compilamos de la siguiente manera agregando
la opción -g para incluir información en el archivo objeto para relacionarlo con el archivo
fuente:
gdb ./main
Una vez iniciada la sesión, tenemos comandos para ejecutar el código lı́nea por lı́nea, de
a tramos, visualizar contenido de memoria, registros, etc.
Para ver una guı́a detallada de los comandos consultar los documentos [12] y [13].
Ambos se encuentran en la Sección Apuntes varios del Campus Virtual de la asignatura.
También hay disponible un vı́deo tutorial en la Sección Ejemplos.
-S
Esta bandera permite obtener el código Assembler
Ejemplo
gcc -S -o hola hola.c
Se obtiene un archivo hola.s con el código equivalente en Assembler.
-fverbose-asm
Se utiliza para generar un archivo de ensamblador que contiene comentarios adicio-
nales explicativos.
-fomit-frame-pointer
Elimina el uso del puntero base (frame pointer ) para todas las funciones. Puede
ser útil en funciones pequeñas, pero puede dificultar la depuración de funciones
complejas.
-no-pie
Se utiliza al compilar programas para indicar que el ejecutable generado no debe ser
un ejecutable independiente (Position-Independent Executable, PIE). En su lugar,
el compilador generará un ejecutable en el que las direcciones de memoria son fijas.
En general, lo utilizaremos siempre que exista un segmento .data.
64
Referencias
[1] Andrew S. Tanembaum, Organización de computadoras: Un enfoque estructurado,
cuarta edición, Pearson Education, 2000
[3] M. Morris Mano, Computer system architecture, tercera edición, Prentice-Hall, 1993.
[4] Randall Hyde, The art of assembly language, segunda edición, No Starch Pr, 2003.
[6] Bryant, Randal E, David Richard, O’Hallaron y David Richard, O’Hallaron, Computer
systems: A programmer’s perspective, Prentice Hall, 2003.
[8] AMD64 Architecture Programmer’s Manual Volume 4: 128-Bit and 256-Bit Media
Instructions, AMD64 Technology, 2015.
[14] Intel 64 and IA-32 Arquitectures Software Developer’s Manual, Volume 2, Intel, di-
ciembre 2021.
[15] Richard Blum, Professional Assembly Language, Wiley Publishing, Inc., 2005.
[16] Ray Seyfarth, Introduction to 64 Bit Intel Assembly Language Programming, 2011.
65