Programación en ensamblador de la arquitectura IA-32

Anuncio
Programación en ensamblador de la
arquitectura IA-32
1 / 198
Programación en ensamblador de la arquitectura IA-32
Universidad Carlos III de Madrid
Programación en ensamblador de la
arquitectura IA-32
Copyright © 2007 Universidad Carlos III de Madrid
2 / 198
Programación en ensamblador de la
arquitectura IA-32
3 / 198
COLABORADORES
TÍTULO :
REFERENCE :
Programación en ensamblador de la arquitectura IA-32
ACCIÓN
NOMBRE
FECHA
ESCRITO POR
Abelardo Pardo
6 de mayo de 2008
FIRMA
HISTORIAL DE REVISIONES
NÚMERO
FECHA
MODIFICACIONES
NOMBRE
Programación en ensamblador de la
arquitectura IA-32
4 / 198
Índice general
1. Ejecución de programas en un ordenador
15
1.1. Perspectivas de un ordenador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.2. Niveles de abstracción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.2.1. Estudio de un procesador a nivel lenguaje máquina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.3. Estructura de un ordenador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4. Definición de un programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.5. El lenguaje ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.5.1. Programación en lenguaje ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.5.2. Ejecución de un programa en ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.6. Ejecución de un programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.8. Bibliografía . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2. Codificación de la información
29
2.1. Lógica binaria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.1.1. Propiedades de una codificación binaria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.2. Representación de números en diferentes bases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.2.1. Traducción de un número a diferentes bases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.3. Codificación de números naturales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.4. Codificación en bases 8 y 16 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5. Tamaño de una codificación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.6. Codificación de números enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.7. Codificación de números reales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.7.1. Desbordamiento en la representación en coma flotante . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.7.2. El estándar IEEE 754 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.8. Representación de conjuntos de símbolos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.8.1. Codificación de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.8.2. Codificación de instrucciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.8.3. Descripción de un lenguaje máquina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2.9. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2.10. Bibliografía . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Programación en ensamblador de la
arquitectura IA-32
3. Almacenamiento de datos en memoria
5 / 198
51
3.1. La memoria RAM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.2. Operaciones sobre memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.3. Conexión entre memoria y procesador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.4. Almacenamiento de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.4.1. Almacenamiento de booleanos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
3.4.2. Almacenamiento de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
3.4.3. Almacenamiento de enteros y naturales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
3.4.4. Almacenamiento de instrucciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
3.4.5. Tamaño de datos en operaciones de lectura y escritura . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
3.5. Almacenamiento de tablas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.5.1. Almacenamiento de tablas en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.6. Almacenamiento de direcciones de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.6.1. Ejemplos de indirección . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4. Arquitectura IA-32
72
4.1. El entorno de ejecución de la arquitectura IA-32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.1.1. Espacio de direcciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.1.2. Registros de propósito general . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.1.3. Registro de estado y control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.1.4. El registro contador de programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.1.5. Otros registros de la arquitectura IA-32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.1.6. Estado visible de un programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
4.2. Ciclo de ejecución de una instrucción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
4.2.1. Fase de fetch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.2.2. Fase de decodificación inicial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.2.3. Fase de decodificación final . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
4.2.4. Fase de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
4.2.5. Fase de escritura de resultados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.2.6. Ejecución de una instrucción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4.2.7. Ciclo de ejecuciones en procesadores actuales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4.3. La pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.3.1. Instrucciones de manejo de la pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.3.2. El puntero de pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
4.3.3. Valores iniciales del puntero de pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
4.4. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Programación en ensamblador de la
arquitectura IA-32
5. Juego de instrucciones
6 / 198
89
5.1. Tipos de juegos de instrucciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5.2. Formato de instrucciones máquina de la arquitectura IA-32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.3. El lenguaje ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
5.3.1. Formato de instrucción ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
5.3.2. Descripción detallada de las instrucciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
5.3.3. Tipos de operandos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
5.3.4. El sufijo de tamaño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
5.4. Instrucciones más representativas de la arquitectura IA-32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
5.4.1. Instrucciones de transferencia de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
5.4.2. Instrucciones aritméticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
5.4.2.1.
Instrucciones de suma y resta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5.4.2.2.
Instrucciones de multiplicación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5.4.2.3.
Instrucciones de división entera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.4.3. Instrucciones lógicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.4.4. Instrucciones de desplazamiento y rotación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
5.4.4.1.
Instrucciones de desplazamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
5.4.4.2.
Instrucciones de rotación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.4.5. Instrucciones de salto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.4.6. Instrucciones de comparación y comprobación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
5.4.7. Instrucciones de llamada y retorno de subrutina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
6. El programa ensamblador
109
6.1. Creación de un programa ejecutable en ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
6.2. Definición de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
6.2.1. Definición de bytes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.2.2. Definición de enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.2.3. Definición de strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
6.2.4. Definición de espacio en blanco . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
6.3. Uso de etiquetas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
6.4. Gestión de la pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
6.5. Desarrollo de programas en ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
6.6. Ejemplo de programa en ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
6.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
Programación en ensamblador de la
arquitectura IA-32
7. Modos de Direccionamiento
7 / 198
120
7.1. Notación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
7.2. Modos del direccionamiento de la arquitectura IA-32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
7.2.1. Modo inmediato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
7.2.2. Modo registro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
7.2.3. Modo absoluto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
7.2.4. Modo registro indirecto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
7.2.5. Modo auto-incremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
7.2.6. Modo auto-decremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
7.2.7. Modo base + desplazamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
7.2.8. Modo base + índice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
7.2.9. Modo índice escalado + desplazamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
7.2.10. Modo base + índice escalado + desplazamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
7.2.11. Utilización de los modos de direccionamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
7.3. Hardware para el cálculo de la dirección efectiva . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
7.4. Resumen de los modos de direccionamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
7.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
8. Construcciones de alto nivel
141
8.1. Desarrollo de aplicaciones en múltiples ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
8.2. Programas en múltiples ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
8.3. Traducción de construcciones de alto nivel a ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
8.3.1. Traducción de un if/then/else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
8.3.2. Traducción de un switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
8.3.3.
Traducción de un bucle while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
8.3.4. Traducción de un bucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
8.4. Ejecución de subrutinas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
8.4.1. Las instrucciones de llamada y retorno de una subrutina . . . . . . . . . . . . . . . . . . . . . . . . . . 152
8.4.2. Paso de parámetros y devolución de resultados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
8.4.2.1.
Paso de parámetros a través de registro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
8.4.2.2.
Paso de parámetros a través de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
8.4.2.3.
Paso de parámetros a través de la pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
8.4.2.4.
Almacenamiento de variables locales a una subrutina . . . . . . . . . . . . . . . . . . . . . . 157
8.5. Gestión del bloque de activación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
8.6. Ejemplo de evolución del bloque de activación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
8.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
Programación en ensamblador de la
arquitectura IA-32
A. Subconjunto de instrucciones de la arquitectura IA-32
8 / 198
165
A.1. Nomenclatura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A.2. Instrucciones de movimiento de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A.2.1. MOV: Movimiento de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A.2.2. PUSH: Instrucción de carga sobre la pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
A.2.3. POP: Instrucción de descarga de la pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
A.2.4. XCHG: Instrucción de intercabmio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
A.3. Instrucciones aritméticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
A.3.1. ADD: Instrucción de suma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
A.3.2. SUB: Instrucción de resta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
A.3.3. INC: Instrucción de incremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
A.3.4. DEC: Instrucción de decremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
A.3.5. NEG: Instrucción de cambio de signo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
A.3.6. MUL: Instrucción de multiplicación sin signo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
A.3.7. DIV: Instrucción de división sin signo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
A.3.8. IMUL: Instrucción de multiplicación con signo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
A.3.9. IDIV: Instrucción de división con signo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
A.4. Instrucciones lógicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
A.4.1. AND: Instrucción de conjunción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
A.4.2. OR: Instrucción de disyunción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A.4.3. XOR: Instrucción de disyunción exclusiva . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A.4.4. NOT: Instrucción de negación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
A.5. Instrucciones de desplazamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
A.5.1. SAL/SAR: Desplazamiento aritmético . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
A.5.2. SHL/SHR: Desplazamiento lógico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
A.5.3. RCL/RCR: Instrucción de rotación con acarreo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
A.5.4. ROR/ROL: Instrucción de rotación sin acarreo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
A.6. Instrucciones de salto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
A.6.1. JMP: Instrucción de salto incondicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
A.6.2. Jcc: Instrucciones de salto condicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
A.6.3. CALL: Instrucción de llamada a subrutina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
A.6.4. RET: Instrucción de retorno de subrutina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
A.7. Instrucciones de comparación y comprobación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
A.7.1. CMP: Instrucción de comparación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
A.7.2. TEST: Instrucción de comprobación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Programación en ensamblador de la
arquitectura IA-32
B. El depurador
9 / 198
182
B.1. Arranque y parada del depurador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
B.2. Visualización de código . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
B.3. Ejecución controlada de un programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
B.4. Visualización de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
B.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
B.6.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
C. Licencia Creative Commons
194
Programación en ensamblador de la
arquitectura IA-32
10 / 198
Índice de figuras
1.1. Perspectiva del programador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.2. Perspectiva del diseñador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.3. Diferentes perspectivas de un ordenador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.4. Relación entre los niveles de abstracción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.5. Estructura de un ordenador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.6. Creación de un ejecutable en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.7. Creación de un ejecutable a partir de un programa en lenguaje ensamblador . . . . . . . . . . . . . . . . . . . . 24
1.8. Introducción el comando programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.9. Copia del ejecutable de disco a memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.10. Ejecución de la primera instrucción de un programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.1. Rango de enteros codificados por 8 bits en signo y magnitud . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.2. Rango de enteros codificados por 8 bits en complemento a 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.3. Estructura de la representación binaria en coma flotante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.4. Estructura de las instrucciones de ual-1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.5. Formato de codificación del conjunto ual-1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.6. Ejemplo de codificación de instrucción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
2.7. Estructura de las instrucciones de ual-2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
2.8. Estructura de la correspondencia de ual-2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.9. Codificación de los operandos en ual-3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.10. Formato de la codificación de ual-3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.1. Estructura de la memoria RAM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.2. Operaciones sobre memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.3. Señales que conectan el procesador y la memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.4. Memoria real y posible en un procesador con arquitectura IA-32 . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.5. Almacenamiento de booleanos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
3.6. Almacenamiento de un string . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
3.7. Almacenamiento de enteros en memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
3.8. Interpretación de bytes en little endian y big endian . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
3.9. Almacenamiento de instrucciones en formato fijo y variable
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Programación en ensamblador de la
arquitectura IA-32
11 / 198
3.10. Acceso a memoria en grupos de 4 bytes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.11. Acceso doble para obtener 4 bytes consecutivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.12. Dirección de un elemento de una tabla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
3.13. Ejemplo de almacenamiento de una tabla de enteros de 32 bits en memoria . . . . . . . . . . . . . . . . . . . . . 64
3.14. Almacenamiento de una tabla de seis enteros en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.15. Dirección de memoria almacenada como número natural . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
3.16. Una posición de memoria ‘apunta a’ otra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
3.17. Indirección múltiple para acceder a un dato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3.18. Tabla con direcciones de comienzo de strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.19. Dos referencias en Java que apuntan al mismo objeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
3.20. Acceso a un dato mediante doble indirección . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.1. Pentium 4 Northwood (Dic. 2001). Fuente: Intel Technology Journal, vol. 6, núm. 2. . . . . . . . . . . . . . . . 73
4.2. Chip con un procesador Pentium 4 en su interior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.3. Tipos de datos del procesador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.4. Acceso alineado a memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.5. Registros de propósito general . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.6. Registro de estado y control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.7. Contador de programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.8. Ciclos de ejecución de varias instrucciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.9. Fase de fetch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.10. Fase de decodificación inicial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
4.11. Fase de decodificación final . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
4.12. Fase de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.13. Fase de escritura de resultado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.14. Ciclo de ejecución de instrucciones de coma flotante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4.15. Efecto de la instrucción push . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.16. Efecto de la instrucción pop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
4.17. Ejecución de instrucciones de pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
4.18. Valores de la cima para la pila vacía y llena . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
5.1. Formato de Instrucción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.2. Byte ModR/M de las instrucciones de la arquitectura IA-32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.3. Byte SIB de las instrucciones de la arquitectura IA-32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
5.4. Codificación de una instrucción ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
5.5. Desplazamiento aritmético de 1 bit en un número de 8 bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
5.6. Rotación de un operando de 8 bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
6.1. El programa ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
6.2. Estructura de un programa en ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
Programación en ensamblador de la
arquitectura IA-32
12 / 198
6.3. Etiqueta y direcciones relativas a ella . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
7.1. Dirección de un elemento en una tabla de enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
7.2. Funcionalidad de los modos de direccionamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
7.3. Acceso a operando con modo inmediato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
7.4. Acceso a operando con modo registro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
7.5. Acceso a operando con modo absoluto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
7.6. Acceso a operando con modo registro indirecto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
7.7. Acceso a operando con modo auto-incremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
7.8. Acceso a operando con modo auto-decremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
7.9. Acceso a operando con modo base + desplazamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
7.10. Acceso a operando con modo base + índice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
7.11. Acceso a operando con modo índice escalado + desplazamiento . . . . . . . . . . . . . . . . . . . . . . . . . . 134
7.12. Acceso a operando con modo base + índice escalado + desplazamiento
. . . . . . . . . . . . . . . . . . . . . . 136
7.13. Definición de una matriz de enteros almacenada por filas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
7.14. Circuito para el cálculo de la dirección efectiva . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
8.1. Compilación de un programa escrito en un lenguaje de alto nivel . . . . . . . . . . . . . . . . . . . . . . . . . . 142
8.2. Referencia a símbolos en dos ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
8.3. Reubicación de símbolos en la fase de entrelazado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
8.4. Estructura de un if/then/else
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
8.5. Traducción de un if/then/else a ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
8.6. Estructura de un switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
8.7. Traducción de un switch a ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
8.8. Estructura de un bucle while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
8.9. Traducción de un bucle while a ensamblador
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
8.10. Estructura de un bucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
8.11. Traducción de un bucle for a ensamblador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
8.12. Llamada y retorno de una subrutina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
8.13. Invocación anidada de subrutinas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
8.14. Parámetros y resultado de una subrutina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
8.15. Evolución de la pila desde el punto de vista del programa llamador
8.16. Evolución de la pila durante la llamada a cuenta
. . . . . . . . . . . . . . . . . . . . . . . . 160
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
B.1. Primer caso en el que realmente se encuentra un bug (Fuente: U.S. Naval Historical Center Photograph) . . . . . 183
Programación en ensamblador de la
arquitectura IA-32
13 / 198
Índice de tablas
2.1. Potencias de 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.2. Conversión de binario a decimal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.3. Correspondencia entre grupos de 3 bits y dígitos en octal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.4. Traducción de binario a octal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.5. Correspondencia entre grupos de 4 bits y dígitos en hexadecimal . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.6. Traducción de binario a hexadecimal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.7. Ejemplo de codificación de enteros en signo y magnitud . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.8. Representación de números naturales y enteros en binario con n bits . . . . . . . . . . . . . . . . . . . . . . . . 37
2.9. Parámetros del formato IEEE 754 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.10. Rangos representados en los formatos simple y doble . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.11. Ejemplo de símbolos codificados con Unicode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.12. Texto de un programa y su codificación en ASCII . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.13. Representación en hexadecimal de símbolos de ual-1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
2.14. Codificación del operando lugar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
2.15. Representación en binario y hexadecimal de símbolos de ual-2 . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.16. Ejemplos de codificación de operandos en ual-3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.17. Codificación de instrucciones de ual-3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.1. Unidades de almacenamiento de información en bytes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.2. Tipos de datos básicos en el lenguaje Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.1. Nomenclatura para los tamaños de información . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.1. Instrucciones con diferentes tipos de operandos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
5.2. Instrucciones con sufijos de tamaño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
5.3. Instrucciones de transferencia de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
5.4. Instrucciones aritméticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5.5. Instrucciones de multiplicación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.6. Instrucciones de división . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.7. Instrucciones lógicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
5.8. Instrucciones de desplazamiento aritmético . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Programación en ensamblador de la
arquitectura IA-32
14 / 198
5.9. Instrucciones de rotación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.10. Instrucciones de salto condicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
5.11. Resta y bit de desbordamiento de dos enteros de 2 bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.12. Secuencias de instrucciones de comparación y salto condicional . . . . . . . . . . . . . . . . . . . . . . . . . . 107
5.13. Secuencias de instrucciones de comprobación y salto condicional
. . . . . . . . . . . . . . . . . . . . . . . . . 107
7.1. Modos de direccionamiento de la arquitectura IA-32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
8.1. Pasos para la gestión del bloque de activación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
A.1. Opciones de la multiplicación sin signo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
A.2. Opciones de la división sin signo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
A.3. Opciones de la división con signo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
B.1. Programa en ensamblador utilizado como ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
Programación en ensamblador de la
arquitectura IA-32
15 / 198
Capítulo 1
Ejecución de programas en un ordenador
En este capítulo se describen las diferentes formas posibles de percibir un ordenador y su funcionamiento. El usuario común lo
percibe como una máquina capaz de ejecutar ciertas órdenes o comandos que se le comunican utilizando los dispositivos tales
como teclado, ratón, etc. Pero para la persona con nociones básicas de electrónica digital, el ordenador es un circuito complejo que
permite la ejecución de ciertos comandos sencillos. ¿Cómo es posible que el mismo objeto sea percibido de forma tan diferente?
Esto es debido a que existen varios niveles desde los que se puede analizar la estructura de un ordenador. Cuando el usuario
convencional introduce un comando por teclado, el ordenador lo ejecuta mediante un número muy elevado de instrucciones
básicas que permiten la comunicación con los diferentes dispositivos para obtener los datos así como las operaciones básicas en
el interior del procesador. Se verá cómo tareas comunes solicitadas por un usuario requieren la participación de varias partes del
ordenador.
1.1.
Perspectivas de un ordenador
Un ordenador es un objeto que se puede percibir de múltiples formas, y todas ellas correctas. Cuando un usuario se sienta frente
a uno de ellos y comienza a trabajar, el ordenador se percibe como un objeto que permite manipular una serie de programas que
a su vez realizan una serie de tareas. Por ejemplo, al conectarse a Internet y entrar en un chat, el ordenador permite, a través del
programa de chat, el intercambio de mensajes con un conjunto de personas que a su vez están utilizando ese mismo programa
en otros ordenadores en otros lugares. Pero además de utilizar el programa de chat, no se debe olvidar que se se está utilizando
igualmente la pantalla para visualizar los mensajes, el teclado para introducir su texto, y posiblemente el ratón para seleccionar
iconos o ventanas en la pantalla.
Otro ejemplo de utilización del ordenador es cuando un usuario abre un fichero de texto y comienza a escribir una carta. El
ordenador facilita el trabajo de escritura y almacenamiento de este documento que puede incluir elementos especiales tales como
imágenes, diferentes tipos de letras, gráficas con datos, etc. Para realizar esta tarea también se utiliza la pantalla para visualizar
los datos, el teclado para introducir texto, y el ratón para realizar tareas de selección y manipulación de texto. Si este documento
creado de forma electrónica se necesita en papel, el ordenador permite enviarlo a una impresora que se encarga de plasmar en
papel lo que hasta el momento se había mostrado en pantalla.
Tanto la utilización de un programa de chat como la redacción e impresión de una carta son tareas que han sido previamente
introducidas o programadas en el ordenador y por tanto puede ejecutarlas cuando el usuario así lo requiera. Pero los ordenadores
poseen una propiedad que los hace realmente potentes: se pueden programar con tareas nuevas. Es decir, si un ordenador no
dispone del programa necesario para realizar una tarea, y ésta es programable, existen lenguajes de programación tales como
Java que permiten escribir estos programas e instalarlos. A partir de ese momento, el ordenador podrá ejecutar el programa
escrito tantas veces como el usuario desee.
Pero estos lenguajes de programación, a pesar de que sirven para conseguir que el ordenador realice una tarea concreta, no
son los que el ordenador entiende directamente. Antes de instalar un programa es preciso traducirlo al lenguaje básico que se
maneja internamente. A este proceso de traducción se le denomina compilación. Según el diccionario de la lengua española, la
palabra compilar significa ‘preparar un programa en el lenguaje máquina a partir de otro programa de ordenador escrito en otro
lenguaje’. Una vez que el programa ha sido traducido o compilado, el ordenador ya puede ejecutarlo. Esta forma de utilizar un
ordenador, como un objeto al que podemos darle órdenes precisas sobre tareas, mediante un programa escrito en un lenguaje
Programación en ensamblador de la
arquitectura IA-32
16 / 198
de programación, y que se traduce al lenguaje máquina interno del ordenador, es lo que denominaremos la ‘perspectiva del
programador’. La figura 1.1 muestra de forma gráfica los elementos de la perspectiva del programador.
Figura 1.1: Perspectiva del programador
Pero además de los ejemplos cotidianos de uso, es posible percibir un ordenador desde una perspectiva totalmente diferente. En
efecto, si se dispone de nociones básicas de electrónica digital, se sabe que en su interior hay una serie de circuitos electrónicos
interconectados entre sí. Si se abre la caja de un ordenador puede verse todo un conjunto de pequeños circuitos interconectados
por infinidad de cables que atraviesan una superficie rígida conocido genéricamente con el nombre de ‘placa’. Los circuitos, a
su vez están diseñados utilizando puertas lógicas, que a su vez constan de un conjunto de transistores. La manera en que dichas
puertas lógicas están conectadas permite realizar operaciones aritméticas básicas tales como sumas, restas, multiplicaciones,
divisiones, etc. Al igual que todo circuito electrónico, se necesita un cierto voltaje para que funcione que es proporcionado por
otro circuito que es conocido como la fuente de alimentación y que forma parte del ordenador.
Si se observa con más detalle la estructura interna de un ordenador, es posible distinguir algunos de sus componentes. Los discos
duros suelen tener aspecto de caja metálica rectangular con conectores en uno de sus lados. Puede verse cómo algunos de los
conectores que aparecen en la parte posterior de la caja del ordenador están conectados a pequeñas placas con circuitos impresos
que a su vez están conectadas a una placa mayor que ocupa casi la totalidad de la carcasa.
De todos los circuitos o componentes presentes en el ordenador, hay uno especialmente importante. Se distingue, en la mayoría de
los ordenadores, porque es el circuito de mayor tamaño en el equipo y requiere su propio ventilador para evitar recalentamientos
por lo que no suele estar directamente visible. Este circuito es el que se conoce con el nombre de procesador, y es el responsable
de dirigir, controlar y ejecutar las principales tareas que realiza el ordenador. La característica fundamental de este procesador
es que puede ejecutar programas en lenguaje máquina. Por tanto, el lenguaje máquina es aquél que consta de las instrucciones o
comandos que son entendidos por el procesador.
Recordemos que una de las principales capacidades del ordenador es la de ejecutar programas, y una gran parte de esta capacidad
se debe a la presencia del procesador. Esta perspectiva del ordenador como un conjunto de circuitos electrónicos interconectados
entre sí se denominará la perspectiva del diseñador. La figura 1.2 muestra algunos de los elementos de los que consta esta
perspectiva.
Programación en ensamblador de la
arquitectura IA-32
17 / 198
Figura 1.2: Perspectiva del diseñador
Una vez vistas estas dos perspectivas posibles, ¿cómo puede un equipo formado por estos componentes electrónicos permitirnos
tareas tales como, por ejemplo, comprar por Internet?
La respuesta a esta pregunta es larga y sobre todo compleja, y por ello debe ser tratada en diferentes fases. En lo que resta de
este documento se ofrece parte de la respuesta a dicha pregunta mediante la explicación del funcionamiento de un procesador
desde el punto de vista de su programación en lenguaje máquina. La programación en lenguajes como Java es posible porque los
programas se compilan y se obtiene el lenguaje máquina que el procesador es capaz de ejecutar.
El objetivo, por tanto es ofrecer una tercera visión de un ordenador que se encuentra entre la perspectiva del diseñador y la de
usuario. El estudio del ordenador se centra alrededor del procesador y su lenguaje máquina, que es capaz de ofrecer la funcionalidad necesaria para ejecutar los programas diseñados en lenguajes tales como Java. La figura 1.3 ilustra la nueva perspectiva a
estudiar con respecto a las dos anteriores.
Figura 1.3: Diferentes perspectivas de un ordenador
1.2.
Niveles de abstracción
Generalmente, en entornos científicos, cuando se estudia un problema demasiado complejo se utilizan mecanismos que permitan
simplificarlo pero que a la vez se muestren sus aspectos más relevantes. A este proceso de simplificación, en el que ciertos
aspectos se ocultan reduciendo así su complejidad, se le denomina abstracción
Para ilustrar el proceso de abstracción se puede tomar como ejemplo el problema de buscar un edificio en cualquier lugar del
mundo mediante su dirección de correo postal. Las direcciones de correo postal incluyen el país en el que se encuentra el
destinatario. En un primer nivel de búsqueda, se toma la información referida a todos los países del mundo y se selecciona el país
especificado en la dirección. En una segunda fase, se obtiene la información sobre la división interna de un país (estas divisiones
pueden tener diferentes nombres tales como comunidades, provincias, departamentos, estados, etc.) Una vez localizada la unidad
territorial se debe seleccionar una ciudad. Para ello, tan sólo es preciso obtener la información de ciudades incluidas en la unidad
territorial bajo consideración y localizar la del destinatario. Finalmente, dentro de la ciudad se debe obtener la información sobre
las calles y sus números para conocer la posición exacta del edificio que se busca.
La información manipulada durante este proceso se puede dividir en varias categorías:
Programación en ensamblador de la
arquitectura IA-32
18 / 198
Información sobre todos los países del mundo.
Unidades territoriales dentro del país seleccionado.
Ciudades dentro de la unidad seleccionada.
Calles dentro de la ciudad seleccionada.
Números dentro de la calle seleccionada.
Entre estas categorías existe una relación muy intuitiva. Cada una de ellas contiene detalles de la siguiente categoría. La información sobre todos los países del mundo puede incluir además todas las divisiones en unidades territoriales. De igual forma, los
datos sobre todas las ciudades de una unidad territorial puede incluir todas sus calles. Sin embargo, la solución del problema de
localizar un edificio se simplifica sustancialmente si los datos están divididos en las categorías antes mencionadas, es decir, una
categoría incluye la información de su nivel e ignora o abstrae los datos del resto de categorías.
Se puede decir que como cada una de estas categorías ignora detalles de las categorías siguientes, simplifica la información
acerca de la localización de edificios en el mundo pero, a la vez, conservando los aspectos más relevantes. Cada una de estas
categorías puede ser considerada como un nivel de abstracción.
Generalizando este concepto, los niveles de abstracción son diferentes visiones de una misma entidad relacionadas de tal forma
que cada una de ellas provee más detalle que el nivel anterior, pero ignora detalles del nivel siguiente. Como conclusión, entre
los niveles de abstracción se puede establecer una relación con respecto a dos aspectos: la complejidad y el detalle. La figura
1.4 ilustra un ejemplo con cuatro niveles y cómo la complejidad y el detalle se relacionan de manera inversa. A mayor nivel de
complejidad, mayor número de detalles, y cuanto menos complejo es el resultado de una abstracción, menos detalles incluye.
Figura 1.4: Relación entre los niveles de abstracción
Un segundo ejemplo de los diferentes niveles de abstracción es la escala de un mapa topográfico. Un mapa es una representación
de algunos de los aspectos contenidos en una determinada área. El tamaño del mapa y el de dicha área están relacionados
mediante un factor de escala. Es posible tener mapas de la misma área pero realizados a diferente factor de escala. Estos mapas
ofrecen diferentes visiones del lugar pero con diferentes niveles de complejidad y detalle.
La observación clave de estos dos ejemplos es que la resolución de problemas en un ámbito concreto es posible representando la
realidad mediante un nivel de abstracción adecuado. Por ejemplo, si una empresa de distribución de mercancías a nivel mundial
necesita decidir dónde situar sus centros de distribución, no es preciso que utilice la información de qué números hay en cada
calle. Igualmente, un ingeniero que desea diseñar una autopista no puede utilizar un mapa con una escala tan grande que no se
perciban lo suficiente los accidentes orográficos.
1.2.1.
Estudio de un procesador a nivel lenguaje máquina
En el caso del estudio de un procesador y de su integración en un ordenador, existen múltiples niveles de abstracción y cada uno
de ellos contiene diferentes tipos de detalles. A nivel más detallado, un procesador funciona como un circuito electrónico en el
que las intensidades de corriente y los voltajes van cambiando en diferentes partes consiguiendo así su correcto funcionamiento.
Estas corrientes y voltajes son, a su vez, movimientos de electrones a nivel atómico del material semiconductor del que está
hecho el procesador. En el otro extremo se puede estudiar el ordenador en su totalidad a través de las tareas que puede ejecutar y
la comunicación a través de sus dispositivos.
Programación en ensamblador de la
arquitectura IA-32
19 / 198
De entre estos dos niveles de abstracción, el estudio que se presenta en este documento está realizado a nivel del lenguaje máquina
que el procesador es capaz de ejecutar. Para clarificar más los detalles que forman parte de este nivel los compararemos con los
niveles de abstracción adyacentes.
En un nivel más abstracto se puede situar la ejecución de programas en lenguajes de alto nivel tales como Java. Las instrucciones
que se escriben son más sofisticadas (bucles, condicionales, comparaciones, etc) y se ciñen a una sintaxis específica de dicho
lenguaje. Nótese que un mismo procesador puede ejecutar programas escritos en diferentes lenguajes de programación. Tan sólo
es preciso utilizar un procedimiento de compilación adecuado para traducir estos lenguajes al lenguaje máquina del procesador.
A lo largo de los siguientes capítulos se harán breves incursiones desde el lenguaje máquina hasta este nivel de abstracción con
el propósito de ilustrar el proceso de compilación.
En un nivel de abstracción más detallado del lenguaje máquina se encuentra la estructura interna del procesador. Estos detalles
son imprescindibles para entender cómo se ejecutan paso a paso las diferentes instrucciones que forman parte del lenguaje
máquina. Basándose en la estructura interna de un procesador se puede entender, por ejemplo, el tiempo que tarda éste en
ejecutar una determinada instrucción así como los circuitos que utiliza. Análogamente al nivel de abstracción anterior, en los
siguientes capítulos se mencionarán algunos detalles de este nivel para facilitar la comprensión de cómo el procesador ejecuta
las instrucciones en lenguaje máquina.
1.3.
Estructura de un ordenador
La estructura interna o arquitectura de un ordenador varía enormemente de unos modelos a otros sobre todo de la tarea a la que
esté orientado el equipo. Así, un ordenador de los denominados ‘personales’, es decir, orientado a realizar tareas relativas a una
persona, puede tener una estructura diferente a un ordenador que utilizado para el almacenaje masivo y acceso al conjunto de
datos de una empresa.
Como es imposible mostrar la gran variedad de arquitecturas que existen hoy en día en el mercado de ordenadores, se ha optado por presentar un modelo simplificado de la arquitectura de un ordenador personal, también conocido como PC que es la
abreviatura del término inglés personal computer. La figura 1.5 ilustra este modelo.
Programación en ensamblador de la
arquitectura IA-32
20 / 198
Figura 1.5: Estructura de un ordenador
Todo ordenador tiene, como parte fundamental de su arquitectura, al menos un procesador. En su interior se encuentran una serie
de elementos básicos tales como registros (capaces de almacenar pequeñas cantidades de información), unidad aritmético-lógica
o UAL (capaz de realizar simples operaciones aritméticas), unidad de control, etc. Este procesador se comunica con el resto del
ordenador a través de lo que denominaremos el bus del sistema.
La comunicación entre el procesador y el resto del equipo se puede dividir a su vez en dos categorías: la conexión con la memoria
(también llamada memoria principal o memoria RAM), y la conexión con los dispositivos de entrada salida (E/S). El circuito
representado en la figura 1.5 por el nombre Puente E/S es el encargado de separar los datos de estas categorías. La conexión con
la memoria RAM se realiza a través del bus de memoria. Esta memoria es capaz de almacenar información mientras el equipo
esté encendido. Al apagar el equipo, los datos almacenados en este dispositivo se pierden, por este motivo, también se le conoce
como memoria volátil.
Los datos dirigidos a los dispositivos de entrada/salida se envían a través del bus de entrada/salida (o bus E/S). Este bus se
utiliza para conectar todos los dispositivos auxiliares que necesita el ordenador para su funcionamiento. Dichos dispositivos no
se conectan directamente el bus, sino a un circuito encargado de gestionar su comunicación denominado ‘controlador’.
Los dispositivos más comunes presentes en la mayoría de los ordenadores personales son el teclado, el ratón, la pantalla y el
disco duro. Teclado y ratón son dos dispositivos de entrada de datos (el ordenador sólo recibe datos de ellos), la pantalla es de
salida de datos, y el disco duro es de entrada y salida de datos (se realizan operaciones de lectura y escritura). Este disco tiene
una funcionalidad similar a la memoria RAM, es decir, almacenar datos, pero con la diferencia de que si se apaga el equipo, la
información permanece almacenada sin alteraciones. A los dispositivos con esta propiedad (disco duro, CD-ROM, DVD, etc.) se
les denomina memoria no volátil.
Una de las características que ha contribuido a que esta arquitectura haya alcanzado un nivel tan alto de popularidad es la
posibilidad de conectar de forma sencilla dispositivos adicionales. El bus de entrada/salida está preparado para conectar más
dispositivos y así dotar al ordenador de mayor capacidad. De esta forma, el ordenador se puede completar con una impresora, un
scanner, discos duros adicionales, lector/grabador de DVDs, etc. La mayoría de estos dispositivos se conectan al ordenador ya
Programación en ensamblador de la
arquitectura IA-32
21 / 198
sea a través de conectores específicamente incluidos a tal efecto (tal es el caso de la conexión de una impresora al puerto paralelo)
o a través de las clavijas de expansión.
1.4.
Definición de un programa
Un programa es un conjunto de órdenes o instrucciones por las que un ordenador realiza diversas funciones. Los programas están
escritos en un lenguaje de programación mediante la utilización de un editor.
Tómese como ejemplo el programa escrito en el lenguaje de programación Java que se muestra en el ejemplo 1.1. En él se pueden
apreciar detalles comunes a cualquier programa.
Ejemplo 1.1 Programa en Java
// Definición de la clase Programa
public class Programa {
// Definición del método main
public static void main(String[] args) {
String mensaje;
// Inicialización del mensaje a imprimir
mensaje = new String("Este es mi primer programa");
// Imprimir el mensaje
System.out.println(mensaje);
} // Final del método main
} // Final de la clase Programa
La primera observación es que, tal y como se ha dicho anteriormente, las palabras que se utilizan y su significado están estipuladas
por la definición del lenguaje de programación Java. Todo programa define un conjunto de órdenes a ejecutar sobre un conjunto
de datos, por lo que además de las órdenes, también se incluyen en un programa la definición de los datos a utilizar (en el ejemplo
es la definición de una variable con nombre mensaje).
El primer paso para que el ordenador ejecute las órdenes contenidas en un programa es escribir su contenido en un fichero. En el
caso concreto de Java, este fichero ha de contener lo que se denomina texto plano, es decir, un fichero que contenga únicamente el
texto sin información de formato, márgenes, imágenes, etc. Supongamos que este fichero tiene por nombre Programa.java.
El siguiente paso es lo que se denomina la compilación del programa y consiste en traducir el fichero Programa.java al
lenguaje que el procesador es capaz de entender, es decir, a lenguaje máquina. En general, este proceso de traducción consiste en
varios pasos o etapas que son realizadas por varios programas diferentes. Existen lenguajes de programación y ordenadores en
el que el proceso de compilación se realiza en un único paso y por un único programa, y otros en los que se requieren múltiples
pasos y cada uno de ellos lo realiza un programa diferente. En el ejemplo del programa Java, el proceso de traducción a lenguaje
máquina se ilustra en la figura 1.6.
Programación en ensamblador de la
arquitectura IA-32
22 / 198
Figura 1.6: Creación de un ejecutable en Java
En general, no todos los ficheros temporales resultantes de las diferentes etapas de traducción o compilación son visibles al
usuario. En el caso de un programa Java, tal y como ilustra la figura 1.6 se utiliza la herramienta javac que a partir de un fichero
fuente produce un fichero temporal con extensión .class. A partir de este fichero, la herramienta java realiza la traducción que
falta y procede a su ejecución. En este ejemplo, ambas herramientas se invocan a través de un intérprete de comandos.
1.5.
El lenguaje ensamblador
El procesador que contiene todo ordenador es capaz de ejecutar un conjunto de instrucciones definidas en el lenguaje máquina.
La forma de definir estas instrucciones, al igual que el resto de información contenida en un ordenador, es mediante la utilización
de ceros y unos. En otras palabras, todas las instrucciones que es capaz de ejecutar un procesador se deben codificar mediante
ceros y unos de una forma específica e inequívoca. Se puede decir, por tanto, que el lenguaje del procesador es el lenguaje
máquina.
Al igual que en el caso de Java, es posible escribir programas en lenguaje ensamblador para que sean ejecutados por el procesador.
Pero escribir un programa de estas características es impensable, puesto que no sería más que una sucesión de ceros y unos
codificando los datos y las órdenes del programa. Para facilitar esta tarea se define el lenguaje ensamblador que no es más que
una representación de las instrucciones y datos del lenguaje máquina pero utilizando letras y números en lugar de la lógica binaria
(ceros y unos).
La traducción del lenguaje ensamblador a lenguaje máquina se realiza con un programa encargado de producir la codificación en
ceros y unos del programa para su posterior ejecución. Dada la proximidad entre estos dos lenguajes, a menudo sus nombres se
utilizan de manera indistinta.
El ejemplo 1.2 muestra un sencillo programa escrito en lenguaje ensamblador de la arquitectura IA-32®. En los siguientes
capítulos se hace un estudio más detallado del lenguaje máquina de esta arquitectura así como de sus principales características.
Programación en ensamblador de la
arquitectura IA-32
23 / 198
Por el momento, es suficiente saber que el código máquina mostrado corresponde a dicha arquitectura. De la mera inspección
visual de este programa se pueden deducir varios aspectos de la programación en lenguaje ensamblador.
Ejemplo 1.2 Programa en ensamblador
1
2
.data
# Comienza sección de datos
mensaje:.asciz "Este es mi primer programa\n" # Mensaje a imprimir
3
.text
.globl main
4
5
# global
6
7
# Comienza la sección de código
# Declaración de main como símbolo
main:
8
9
push %ebp
mov %esp, %ebp
# Bloque de activación
push $mensaje
# mueve un dato a la pila
call printf
# printf es una función que
# imprime por pantalla los
# datos que recibe
add $4, %esp
# borra la cima de la pila
mov %ebp, %esp
pop %ebp
ret
# Deshacer bloque de activación
10
11
12
13
14
15
16
17
18
19
20
21
# termina el programa
El programa consta de dos secciones claramente diferenciadas. La primera de ellas comienza a partir de la palabra .data y es
la sección de datos tal y como lee el comentario que se encuentra a continuación. En ella podemos encontrar la definición de
los datos que van a ser utilizados por el programa. Nótese cómo se define un mensaje mediante la utilización de los símbolos
mensaje y .asciz.
La segunda sección comienza a partir de la palabra .text como indica el comentario en la misma línea. A continuación
encontramos una línea en la que el símbolo main se define como global (sin saber qué quiere decir esto). Lo más revelador del
programa se encuentra a partir de este punto, pues aparece la palabra main seguida por cuatro líneas que se corresponden con
cuatro instrucciones del lenguaje máquina del procesador.
El punto del código en el que se encuentra la palabra main: corresponde con el punto de arranque del programa. A continuación,
se encuentran varias instrucciones máquina. Estas instrucciones son extremadamente cortas, constan de una única palabra (tal es
el caso de la última línea) o de una palabra seguida de una o dos más separadas por comas. Estas instrucciones pueden incluir
palabras que aparecen previamente en el texto (por ejemplo push $mensaje), números (por ejemplo add $4, %esp), así
como otros identificadores.
Además, y mediante la interpretación de los comentarios incluidos al final de cada línea, se puede deducir que el programa
imprime un mensaje por pantalla, y para eso utiliza una función denominada printf.
Si se compara el programa ensamblador del ejemplo 1.2 con el programa Java del ejemplo 1.1, las diferencias son aparentes.
La sintaxis, el tipo de instrucciones y la definición de datos son radicalmente diferentes. Lo que sí tienen ambos programas
en común es que contienen un conjunto de órdenes y los datos utilizados. Aunque en estos dos ejemplo no es aparente, el
nivel de abstracción que ofrecen estos dos lenguajes con respecto al lenguaje máquina es muy diferente. Mientras el lenguaje
ensamblador es prácticamente idéntico al lenguaje máquina, el lenguaje Java, por el contrario, ofrece construcciones que se
aproximan a construcciones complejas orientadas a ser entendidas por un humano. A este tipo de lenguajes se les denomina
lenguajes de alto nivel. Como consecuencia, el proceso de traducción y producción de un ejecutable es mucho más fácil para el
programa escrito en ensamblador que para el escrito en Java.
1.5.1.
Programación en lenguaje ensamblador
Los lenguajes de alto nivel ofrecen construcciones complejas y potentes para escribir las órdenes de un programa que son
luego traducidas a lenguaje máquina y ejecutadas por el procesador. Por otro lado, el lenguaje ensamblador no es más que una
Programación en ensamblador de la
arquitectura IA-32
24 / 198
representación textual del lenguaje máquina. ¿Qué ventaja tiene escribir un programa en lenguaje ensamblador pudiendo utilizar
un lenguaje de alto nivel como Java con su posterior proceso de compilación?
Esta pregunta se ha venido haciendo desde la aparición de los lenguajes de programación de alto nivel. La propia aparición
de estos lenguajes viene motivada, en parte, para evitar programas en lenguaje ensamblador pues suelen ser muy delicados de
construir. Pero a pesar de ello, la programación en ensamblador sigue vigente en nuestros días. Sus ventajas son múltiples tal y
como enumera Randall Bryant (ver [Bryant03]):
Requiere un conjunto de técnicas propias de cualquier ingeniero.
Para programar en ensamblador es imprescindible entender el funcionamiento de un procesador y esto a su vez facilita la
comprensión de un sistema completo (procesador, memoria, dispositivos, etc.)
Cada vez es más común encontrar dispositivos electrónicos que contienen un procesador.
Un diseñador de procesadores necesita estar familiarizado con los lenguajes máquina y su utilización en programas.
Entender el funcionamiento de un procesador y de su lenguaje máquina permite desarrollar programas extremadamente eficientes y robustos.
Es el único lenguaje que entiende el hardware.
1.5.2.
Ejecución de un programa en ensamblador
Aunque el lenguaje ensamblador es prácticamente idéntico al lenguaje máquina, para crear un programa que sea ejecutado por el
procesador, se requiere un proceso de traducción o compilación. Este proceso consta de varios pasos que pueden ser realizados
por una o varias herramientas y que pueden producir ficheros temporales.
En los ejemplos que se muestran a continuación se asume que el ordenador de trabajo ejecuta el sistema operativo Unix (o Linux).
Dicho sistema incluye un programa denominado ‘intérprete de comandos’ que permite la introducción de órdenes textuales para
la ejecución de programas. La forma habitual de utilizar este programa es mediante una ventana en la que el intérprete muestra un
mensaje denominado prompt y espera a que el usuario teclee la orden a ejecutar. Una vez introducido un comando, el intérprete
lo ejecuta, y al terminar ofrece de nuevo el prompt y espera el siguiente comando. En adelante se utilizará el símbolo $ para
representar el prompt del intérprete de comandos.
La figura 1.7 ilustra un ejemplo de cómo se obtiene un programa ejecutable a partir de un fichero que contiene un programa
escrito en lenguaje ensamblador. La compilación la realiza el programa gcc a la que se le proporciona una opción para que el
ejecutable se almacene en un fichero con nombre programa (texto -o programa). La ejecución de esta herramienta realiza
los pasos que en la figura se representan por las cajas Ensamblador y Enlazador.
Figura 1.7: Creación de un ejecutable a partir de un programa en lenguaje ensamblador
Programación en ensamblador de la
arquitectura IA-32
25 / 198
La herramienta gcc es capaz de realizar múltiples tareas y una de las cuales es la traducción y generación de ejecutables a partir
de lenguaje ensamblador. La funcionalidad de esta herramienta se controla mediante opciones que se añaden a continuación de
su nombre tal y como ilustra la figura 1.7. Además de esta traducción, gcc también es capaz de ejecutar los pasos de ensamblado
y entrelazado de forma separada e incluso combinar múltiples ficheros de código para obtener un ejecutable.
La ejecución del programa obtenido no requiere herramienta alguna, simplemente se escribe su nombre a continuación del prompt
y comienza su ejecución.
1.6.
Ejecución de un programa
En la sección anterior se ha ejecutado el programa previamente escrito en lenguaje ensamblador mediante el comando:
$ programa
Este es mi primer programa
$
Cuando el usuario introduce el nombre del fichero ejecutable programa, el procesador está ejecutando el intérprete de comandos. Cuando el usuario pulsa las teclas para escribir la palabra programa el teclado notifica al procesador de que se ha pulsado
una tecla, y este obtiene la letra del controlador y la muestra por pantalla. Este proceso se repite para cada una de las letras
mientras que el intérprete de comandos se encarga de almacenar la línea entera en un lugar de memoria (ver figura 1.8).
Figura 1.8: Introducción el comando programa
Cuando se pulsa el retorno de carro, el procesador obtiene esa letra del controlador de teclado, la muestra por pantalla y la
almacena en memoria. Pero el retorno de carro es la marca de final de comando que hace que el intérprete de comandos pase
Programación en ensamblador de la
arquitectura IA-32
26 / 198
a procesar el comando. Para ello se explora la línea leída de teclado y detecta que se trata del nombre de un ejecutable. A
continuación se obtiene el contenido de dicho ejecutable del disco duro y se almacena en memoria (ver figura 1.9).
Figura 1.9: Copia del ejecutable de disco a memoria
Esta transferencia de datos se realiza porque el procesador necesita tener el código y los datos necesarios para ejecutar un
programa en memoria principal en donde puede acceder a ellos directamente mediante instrucciones de su lenguaje máquina. El
acceso al disco duro, por el contrario, requiere la ejecución de múltiples instrucciones máquina para calcular el lugar del disco en
el que se encuentran los datos solicitados y para programar el controlador de disco para que obtenga dichos datos y los deposite
en memoria.
Una vez que el programa está almacenado en memoria, el procesador tiene anotado el lugar en el que está su primera instrucción.
El último paso (tras una serie de preparativos que no interesan en este momento) es pasar a ejecutar la primera instrucción del
programa tal y como se muestra en la figura 1.10 que es push $mensaje. A partir de ese instante el procesador está ejecutando
las instrucciones escritas en el programa sobre los datos definidos.
Programación en ensamblador de la
arquitectura IA-32
27 / 198
Figura 1.10: Ejecución de la primera instrucción de un programa
La última instrucción ejecutada es ret. Tras ello, el procesador continúa con la ejecución del intérprete de comandos que a su
vez está diseñado para que cuando se termina la ejecución de un programa se vuelva a mostrar el prompt (el símbolo $) por
pantalla. A lo largo de los pasos descritos el procesador ha ejecutado del orden de miles de instrucciones máquina, y entre ellas,
aquellas contenidas en el fichero programa.s.
1.7.
Ejercicios
1. ¿Cuál es la diferencia entre un lenguaje de programación como Java y el lenguaje máquina? ¿Cómo se denomina al proceso
de traducción de uno al otro? ¿Qué característica específica define al lenguaje máquina?
2. En la arquitectura de un ordenador personal, ¿qué dos tipos de comunicación realiza el procesador con el resto del equipo?
3. ¿Cuántos buses están presentes en la estructura básica de un ordenador personal? ¿Cuál es su propósito?
4. Describir para qué sirve la unidad aritmético-lógica de un procesador.
5. ¿Qué diferencia hay entre lenguaje máquina y lenguaje ensamblador? ¿Cuál de ellos es más fácil de entender por un
humano?
6. ¿Qué es el proceso de compilación? ¿Cuál es el resultado final que se produce? ¿Cuántas herramientas pueden tomar parte
en este proceso?
Programación en ensamblador de la
arquitectura IA-32
28 / 198
7. A la vista de las operaciones que realiza el procesador para ejecutar un proceso cuyo nombre introduce el usuario a través
de teclado, idear y describir una tarea en la que el ordenador tenga que procesar información obtenida a través de las
clavijas de expansión, el disco duro y la memoria.
8. Un procesador tiene un único lenguaje máquina, pero puede tener más de un lenguaje ensamblador, ¿por qué?
9. Los lenguajes máquina de los diferentes procesadores existentes en el mercado son la mayoría de ellos diferentes entre
sí. ¿Puede un fichero ejecutable de un procesador ser ejecutado en general en otro procesador con un lenguaje máquina
diferente? Justificar la respuesta.
10. Utilizando un motor de búsqueda en Internet buscar tres empresas fabricantes de procesadores y para cada una de ellas dos
procesadores diferentes que estén siendo comercializados.
1.8.
Bibliografía
[Bryant03]
Randal E. Bryant y David O’Hallaron, Computer Systems, A Programmer’s Perspective, Prentice Hall, copyright © 2003 Prentice Hall, 0-13-034074-X.
Programación en ensamblador de la
arquitectura IA-32
29 / 198
Capítulo 2
Codificación de la información
En este capítulo se estudian las técnicas de codificación de la información utilizadas por un procesador para poder manipular
elementos tales como números naturales, enteros, reales, instrucciones, operandos, etc. Todo circuito digital trabaja con lógica
binaria, mediante la combinación de múltiples instancias de estos valores se puede construir un esquema de codificación que
permite manipular los datos básicos para realizar cálculos.
2.1.
Lógica binaria
Los circuitos electrónicos digitales se caracterizan por manipular todas sus señales en dos estados posibles: cero y uno. La información que procesan estos circuitos, por tanto, debe ser representada por tantas unidades de información como sean necesarias,
pero siempre y cuando tengan uno de los dos estados posibles. Estas unidades binarias de información se llaman ‘bits’. Los
circuitos digitales, por tanto, están diseñados para operar internamente con bits. Un procesador es un circuito digital capaz de
ejecutar un conjunto previamente definido de instrucciones sencillas que se denomina su ‘lenguaje máquina’.
Pero para la ejecución de este lenguaje máquina es preciso codificar todos los elementos utilizados (números, símbolos, operandos, etc) mediante lógica binaria. Esta codificación es la base para entender cómo un ordenador en cuyo interior sólo existen
circuitos capaces de procesar bits puede realizar tareas mucho más sofisticadas.
2.1.1.
Propiedades de una codificación binaria
Un esquema de codificación con bits se define mediante una relación entre los elementos a codificar y un conjunto de cadenas
de bits. Cada elemento debe tener al menos una representación en binario, y cada posible cadena de bits, debe corresponder
a un elemento. Esta correspondencia debe cumplir una serie de propiedades mínimas para que sea utilizable por los circuitos
digitales. Lo más importante es saber de antemano el número de elementos a codificar. Los circuitos digitales sólo pueden
manipular códigos binarios de una cierta longitud máxima, y por tanto se debe fijar de antemano. Por ejemplo, si el circuito
debe manipular números naturales, se debe escoger de antemano qué subconjunto de estos números se van a utilizar, y de ahí se
deduce el número de bits necesarios para su codificación.
Si se utiliza un único bit, se pueden representar únicamente dos elementos, uno con cada uno de los valores posibles, 0 y 1. Si
se utilizan cadenas de dos bits, el número de combinaciones posibles aumenta a cuatro: 00, 01, 10, 11. Si el número de bits se
incrementa en una unidad, se puede comprobar que el número de combinaciones se duplica pues se pueden obtener dos grupos
de combinaciones, uno de ellos con el bit adicional a uno y el otro a cero.
Si con un solo bit se pueden codificar dos elementos, y por cada bit que se añade se duplica el número de combinaciones posible,
se deduce por tanto que con n bits se pueden codificar un máximo de 2n elementos.
La fórmula anterior calcula el número de combinaciones posibles al utilizar n bits, pero si se dispone de un conjunto de N
elementos, ¿cuántos bits se necesitan para poder codificarlos en binario? Por ejemplo, se dispone de cinco elementos, ¿es posible
codificarlos en binario con dos bits? ¿y con tres? ¿y con cuatro? Se precisa un número de bits tal que el número de combinaciones
posibles sea mayor al número de elementos. En otras palabras, se precisan n bits siempre y cuando n satisfaga la ecuación 2.1.
Programación en ensamblador de la
arquitectura IA-32
30 / 198
N ≤ 2n .
E QUATION 2.1: Relación entre el número de bits y las posibles combinaciones
Dado un conjunto de N elementos, el número mínimo de bits se obtiene mediante la ecuación 2.2.
n ≥ dlog2 Ne
E QUATION 2.2: Número mínimo de bits necesarios para codificar N elementos
donde los símbolos de representan el entero mayor que el logaritmo obtenido. Por ejemplo, para codificar un conjunto con cinco
elementos se debe cumplir n ≥ log2 5, y por tanto n ≥ 2.3219281, es decir, n ≥ 3.
Esta desigualdad establece un mínimo en el número de bits, pero no un máximo. Efectivamente, si en el ejemplo anterior en lugar
de utilizar 3 bits se utilizan más, la codificación es igualmente posible. Para codificar los elementos hay que tener un mínimo de
combinaciones binarias, pero se pueden tener combinaciones extras.
Las dos ecuaciones anteriores se pueden transformar en las dos reglas a tener en cuenta en la codificación binaria de elementos:
1. Con n bits se pueden codificar hasta un máximo de 2n elementos diferentes.
2. Para codificar N elementos se precisan como mínimo dlog2 Ne bits.
2.2.
Representación de números en diferentes bases
Antes de estudiar cómo se codifican los diferentes elementos con los que trabaja un procesador en binario es útil estudiar la
representación de números naturales en diferentes bases. Los números naturales se escriben comúnmente utilizando 10 dígitos, o
lo que es lo mismo, en base 10. Dado un número el dígito de menos peso es aquel que corresponde a las unidades, o en general,
el que se escribe más a la derecha. Análogamente, el dígito de más peso es el que se escribe más a la izquierda. Un número
representado en base b cumple las siguientes condiciones:
Se utilizan b dígitos para representar el número, desde el 0 hasta el b-1.
Al número representado por (b - 1) le sigue el número 10.
Análogamente, al número máximo posible representado por n dígitos le sigue uno con n + 1 dígitos en el que el de más peso
es el 1 y el resto son todo ceros.
Las condiciones anteriores se cumplen para los números representados en base 10 donde los dígitos utilizados son del 0 al 9.
Al número representado por el dígito de más valor (el 9) le sigue un número de dos dígitos con el segundo dígito de más valor
seguido del cero (el 10), y al número máximo posible representado por cuatro dígitos (el 9999) le sigue uno con cinco dígitos
que comienza por 1 seguido de cuatro ceros (el 10000).
El valor de un número se obtiene sumando cada uno de los dígitos multiplicado por un factor que representa su peso. Así, de
derecha a izquierda, el primer dígito se llama unidades, el segundo decenas, el tercero centenas, y así sucesivamente. En base 10,
el valor del número se obtiene multiplicando el dígito de las unidades por uno, el de las decenas por 10, el de las centenas por
100, y así sucesivamente. Tal y como muestra el ejemplo 2.1 para el número 3214.
Ejemplo 2.1 Número como suma de potencias de 10
3214 = 3 ∗ 1000 + 2 ∗ 100 + 1 ∗ 10 + 4
Los factores por los que se multiplican cada uno de los dígitos son las sucesivas potencias de la base utilizada, en este caso 10.
El mismo número se puede reescribir de la siguiente forma:
3214 = 3 ∗ 103 + 2 ∗ 102 + 1 ∗ 101 + 4 ∗ 100 .
Es decir, el número se obtiene multiplicando cada dígito por la base elevada a un exponente igual a la posición que ocupa
comenzando por el de menos peso cuya posición es cero. La fórmula para obtener el valor del número 3214 se puede reescribir
en general como:
Programación en ensamblador de la
arquitectura IA-32
31 / 198
3214 = ∑3i=0 (di ∗ basei ) = (d0 ∗ 100 ) + (d1 ∗ 101 ) + (d2 ∗ 102 ) + (d3 ∗ 103 )
donde di representa el dígito que ocupa la posición i en el número. La fórmula es generalizable para cualquier base. El valor en
base 10 de un número de n dígitos escrito en base b se obtiene mediante la ecuación 2.3.
i
∑n−1
i=0 (di ∗ base )
E QUATION 2.3: Valor de un número de n dígitos en base b
Por ejemplo, el equivalente en base 10 del número 6342 escrito en base 7 se puede obtener mediante la ecuación 2.3:
3
63427
=
∑ (di ∗ 7i )
i=0
= (2 ∗ 70 ) + (4 ∗ 71 ) + (3 ∗ 72 ) + (6 ∗ 73 )
= 2 + 28 + 147 + 2058
= 223510 .
Por tanto, el número 6342 en base 7, corresponde con el número 2235 representado en base 10. El número en base 7 no posee
ningún dígito mayor que 6, pues dicha base utiliza sólo los dígitos del 0 al 6.
Al escribir números en diferentes bases aparece un problema de ambigüedad. El número 2235 del ejemplo anterior es el equivalente en base 10 del número 6342 escrito en base 7. Pero a su vez, 2235 es un número válido en base 7 (que es un número válido
en base 7 pues sus dígitos son todos menores que 7), aunque tiene otro valor equivalente en base 10. Para evitar la confusión, tal
y como muestra la ecuación anterior, cuando se manipulan números en diferentes bases se incluye la base en la que está escrito
a la derecha y como un subíndice.
2.2.1.
Traducción de un número a diferentes bases
La ecuación 2.3 permite obtener la representación en base 10 de un número en cualquier base. El proceso inverso, es decir, de un
número en base 10 obtener su equivalente en una base diferente, es preciso realizar una serie de divisiones sucesivas para obtener
los nuevos dígitos comenzando por el de menos peso. El proceso se ilustra con un ejemplo.
Supongamos que se quiere calcular el equivalente en base 7 del número 867510 . El dígito de menos peso de la nueva representación se obtiene manipulando la ecuación 2.3:
n−1
i
i−1 ))
∑n−1
i=0 (di ∗ base ) = d0 + (base ∗ ∑i=1 (di ∗ base
Si se divide la expresión anterior por la base se obtiene como resto el dígito de menos peso d0 . El cociente contiene el resto de
dígitos y al aplicar sucesivas divisiones se obtiene como resto los dígitos del número en orden creciente de significación.
Por ejemplo, considérese el número 867510 . Para obtener su representación en base 7 se realiza la primera división cuyo resto es
2, y por tanto su dígito de menos peso. El cociente resultante es 1239 que se vuelve a dividir por 7 y se obtiene un resto de 0 y
un nuevo cociente de 177. Si se repite esta operación sucesivamente, el cociente obtenido será eventualmente 0, y los sucesivos
restos corresponden con la representación del número en base 7 tal y como muestra la ecuación 2.4
8675
= (1239 ∗ 7) + 2
= (((177 ∗ 7) + 0) ∗ 7) + 2
= (((((25 ∗ 7) + 2) ∗ 7) + 0) ∗ 7) + 2
= (((((((3 ∗ 7) + 4) ∗ 7) + 2) ∗ 7) + 0) ∗ 7) + 2
E QUATION 2.4: Obtención de los dígitos en base 7
La aplicación sucesiva de divisiones por la base garantiza que el cociente alcanza siempre un valor menor al de la base. En cuanto
se da este caso no es preciso realizar ninguna división más. Se han obtenido los dígitos en base 7 pero la representación sigue
siendo consistente con la ecuación 2.3 si se reorganizan los términos tal y como se muestra en la ecuación 2.4:
Programación en ensamblador de la
arquitectura IA-32
32 / 198
que corresponde con la codificación en base 7 del número 8675, es decir, 867510 =342027 .
Como resumen, dado un número en base 10, su representación en base b se obtiene dígito a dígito comenzando por el de menos
peso como resto de sucesivas divisiones por la base. El proceso se detiene cuando el cociente final es menor que la base.
Se han descrito los dos procedimientos para traducir un número representado en cualquier base a base 10, y viceversa. Combinando ambos procedimientos se pueden traducir números representados en cualquier base.
2.3.
Codificación de números naturales
El conjunto de datos más simple de codificar en binario para que lo manipule un procesador es el de los números naturales. La
representación corresponde con los números en base 2. Los dos únicos dígitos de esta base coinciden con los dos valores que
pueden manipular los circuitos digitales.
Dado un número binario con n dígitos, su equivalente en decimal se obtiene aplicando la ecuación 2.3:
i
∑n−1
i=0 (di ∗ 2 ).
Pero como los dígitos que pueden aparecer en un número binario son 0 o 1, la fórmula anterior se puede interpretar de una forma
simplificada. Dado un número representado en base 2, su equivalente en decimal se obtiene sumando aquellas potencias de 2
cuyo exponente corresponde al lugar en el que se encuentra el dígito 1.
Considérese el número en binario 1101011. Su equivalente en decimal se obtiene mediante la siguiente suma:
1101011 = 26 + 25 + 23 + 21 + 20 = 64 + 32 + 8 + 2 + 1 = 107.
Por tanto, para manipular números codificados en base 2 y saber su equivalente en decimal es importante familiarizarse con los
valores de las potencias de 2 que se muestran el la Tabla 2.1
Bit
0
Peso
20
Decimal 1
1
21
2
2
22
4
3
23
8
4
24
16
5
25
32
6
26
64
7
27
128
8
28
256
9
29
512
10
11
12
13
14
15
210
211
212
213
214
215
1024 2048 4096 8192 16384 32768
Tabla 2.1: Potencias de 2
La Tabla 2.2 muestra ejemplos de cómo obtener la representación en decimal de números en binario de ocho bits.
Peso
Binario
Decimal
128
0
64
0
Binario
Decimal
1
128
0
Binario
Decimal
1
128
1
64
32
1
32
16
0
8
0
1
32
1
16
0
0
1
16
1
8
4
1
4
2
1
2
1
1
1
0
1
2
0
0
1
1
0
Total
39
178
217
Tabla 2.2: Conversión de binario a decimal
La conversión de un número en base 10 a binario se realiza siguiendo el procedimiento descrito en sección 2.2.1. En este caso, el
divisor es 2 y el resto sólo puede ser 0 o 1 produciendo los dígitos de la representación en base 2. Por ejemplo, la representación
binaria de 217 se obtiene tal y como muestra la ecuación 2.5
Programación en ensamblador de la
arquitectura IA-32
33 / 198
217 = (108 ∗ 2) + 1
108 = (54 ∗ 2) + 0
54 = (27 ∗ 2) + 0
27 = (13 ∗ 2) + 1
13 = (6 ∗ 2) + 1
6 = (3 ∗ 2) + 0
3 = (1 ∗ 2) + 1
1 = (0 ∗ 2) + 1
E QUATION 2.5: Traducción de base 10 a base 2
Los dígitos del número en base 2 se obtienen de menor a mayor peso, por tanto el resultado es 11011001 que corresponde con el
contenido de la Tabla 2.2.
La representación en base 2 tiene ciertas propiedades muy útiles para operar con estos números. Para saber si un número es par o
impar basta con mirar el bit de menos peso. Si dicho bit es uno, el número es impar, si es cero, el número es par. La demostración
de esta propiedad es trivial. El equivalente en decimal de un número en binario se obtiene sumando potencias de 2. Todas estas
potencias, a excepción de la primera (20 ) son pares. Por tanto, un número impar debe tener un uno en su bit de menos peso. De
igual forma, todo número par debe tener un cero en su bit de menos peso, pues sólo puede constar de potencias de 2 pares.
La segunda propiedad no sólo aplica a la base 2, sino a cualquier base. Las operaciones de multiplicación y división entera por
la base se realizan añadiendo un cero como dígito de menos peso o quitando el dígito de menos peso respectivamente.
Para los números en base 10, la operación de multiplicación por 10 se realiza añadiendo un cero como dígito de menos peso
al número dado. Por ejemplo: 1345 * 10 = 13450. De igual forma, si un número decimal lo dividimos por diez, el cociente se
obtiene ignorando el dígito de menos peso, que a su vez corresponde con el resto de la división. Siguiendo con el mismo ejemplo,
1345 dividido entre 10 produce un cociente de 134 y un resto de 5.
Volviendo a la representación binaria, en este caso, la multiplicación y división entera por 2 corresponden de forma análoga con
las operaciones de añadir un cero como dígito de menos peso o quitar el dígito de más peso. Por ejemplo, el número binario
100111 que en decimal representa el 39, si se multiplica por 2 se obtiene 1001110 que se puede comprobar que representa el 78
en decimal. Análogamente, si el mismo número se divide entre 2, su cociente es 10011 que representa el número 19, y el resto es
1 (39 = 19 * 2 + 1).
2.4.
Codificación en bases 8 y 16
La codificación en base 8 (también denominada codificación octal) a pesar de que aparentemente no es útil en el contexto de la
lógica digital, cumple una propiedad especial que la hace importante. Aplicando los conceptos presentados en la sección 2.2 los
números codificados en esta base constan de dígitos entre el 0 y el 7. Consecuentemente, tras el 7, el siguiente número es el 10,
y tras el 77 el siguiente número es el 100.
Para traducir un número dado en base 10 a base 8 se procede mediante sucesivas operaciones de división entre 8 de las que se
obtienen los dígitos correspondientes. En principio es posible realizar una traducción de un número en binario a un número en
base 8. Al menos se puede obtener la representación en base 10 del número binario, y luego efectuar su traducción a base 8. Pero,
¿se puede realizar esta traducción directamente?
Analizando las operaciones necesarias para la traducción se descubre esta se puede hacer de forma inmediata. La división entre
8 en números binarios corresponde a una división entre una potencia de la base, más concretamente 23 . Tal y como se ha descrito
en la sección 2.3, esta operación es equivalente a tres divisiones entre 2, o lo que es lo mismo, a eliminar los tres bits de menos
peso del número que a su vez representan el resto de la división. Estos dos resultados son precisamente los que se necesitan para
efectuar la traducción.
Por tanto, para traducir un número directamente de binario a base 8 no hay más que agrupar los bits de tres en tres comenzando
por los de menor peso, y traducir cada grupo de 3 bits a un dígito entre 0 y 7. Con 3 dígitos binarios se pueden representar
exactamente los números del 0 al 7 tal y como muestra la Tabla 2.3.
Programación en ensamblador de la
arquitectura IA-32
Dígito
octal
Binario
34 / 198
0
1
2
3
4
5
6
7
000
001
010
011
100
101
110
111
Tabla 2.3: Correspondencia entre grupos de 3 bits y dígitos en octal
En el caso de que el último grupo no tenga 3 bits, los bits que faltan se consideran con valor cero (pues los ceros a la izquierda
no alteran el valor de un número). La Tabla 2.4 muestra un ejemplo de cómo se realiza esta traducción.
Número en Binario
001001112
101100102
110110012
Grupos de 3 bits
100 = 4
110 = 6
011 = 3
000 = 0
010 = 2
011 = 3
111 = 7
010 = 2
001 = 1
Número en Octal
478
2628
3318
Tabla 2.4: Traducción de binario a octal
Dado lo fácil que es traducir un número de binario a octal y viceversa, esta última base se utiliza como una representación más
compacta de los números binarios. En lugar de escribir un conjunto de unos y ceros, se escribe su equivalente en octal. Tan
común es esta representación que para denotar que un número está escrito en base 8, en lugar de añadir el subíndice tras el dígito
de menos peso, se añade un cero a la izquierda. Por tanto, los números 478 y 047 representan ambos el número 47 en octal.
Este proceso de traducción tan inmediato se deriva de la propiedad de que la base 8 es una potencia de la base 2. Gracias a esta
propiedad, las divisiones sucesivas y obtención del resto no es más que agrupar los bits comenzando por el de menos peso.
La base 8 no es la única que tiene esta propiedad. La siguiente base en orden creciente que es también potencia de dos es la base
16. ¿Es posible escribir números en esta base? Siguiendo los conceptos presentados en la sección 2.2 se necesitan tantos dígitos
como indica la base comenzando desde el cero. Además de utilizar los diez dígitos del 0 al 9 todavía hacen falta seis dígitos
más. La solución es utilizar las seis primeras letras del abecedario: A, B, C, D, E y F. Los 16 dígitos utilizados para representar
números en base 16 son:
Aplicando las reglas descritas anteriormente, por ejemplo, al número F le sigue el 10, al 19 le sigue el 1A, al 1F le sigue el 20, al
99 le sigue el 9A, y al 100 le precede el FF.
A esta codificación se le conoce también con el nombre de codificación hexadecimal. ¿Es posible hacer una traducción directa de
un número en binario a un número en hexadecimal? La operación necesaria para obtener los dígitos es la división entre 16. Pero,
al ser una potencia de 2 (24 ) la operación consiste en descartar los cuatro bits de menos peso del número binario, que a su vez
corresponden con el resto de la división. Por tanto, para obtener un número hexadecimal a partir de un número binario se han de
agrupar los bits de cuatro en cuatro comenzando por los de menos peso. Cada uno de ellos se traduce en un dígito hexadecimal.
Con 4 bits se codifican exactamente los 16 dígitos que utiliza la base 16 tal y como muestra la Tabla 2.5.
Dígito Hexadecimal
Binario
Dígito Hexadecimal
Binario
0
1
2
3
4
5
6
7
0000
0001
0010
0011
0100
0101
0110
0111
8
9
A
B
C
D
E
F
1000
1001
1010
1011
1100
1101
1110
1111
Tabla 2.5: Correspondencia entre grupos de 4 bits y dígitos en hexadecimal
Al igual que con la base 8, la base hexadecimal se utiliza como representación más compacta de los números en binario. Para
evitar la ambigüedad en la representación, un número en hexadecimal se representa añadiendo el prefijo ‘0x’ al comienzo del
número. La Tabla 2.6 muestra ejemplos de correspondencia entre la representación binaria y la hexadecimal.
La conversión de hexadecimal a base 10 se hace de forma idéntica al resto de las bases. Los dígitos de la A a la F tienen valor
numérico del 10 al 15, y la base a utilizar es 16. Por ejemplo:
Programación en ensamblador de la
arquitectura IA-32
Número en Binario
001001112
101100102
110110012
35 / 198
Grupos de 4 bits
0010 = 2
1011 = 11
1101 = 13
0111 = 7
0010 = 2
1001 = 9
Número en Hexadecimal
0x27
0xB2
0xD9
Tabla 2.6: Traducción de binario a hexadecimal
0x27 = 2 ∗ 161 + 7 ∗ 160 = 39
2.5.
0xB2
= 11 ∗ 161 + 2 ∗ 160 = 178
0xD9
= 13 ∗ 161 + 9 ∗ 160 = 217.
Tamaño de una codificación
En las secciones anteriores se ha visto cómo codificar números naturales en binario. La representación de un número corresponde
con un conjunto de bits. Pero ¿cuántos bits son necesarios para representar los números naturales? Dado que hay infinitos números, la respuesta es infinitos bits. Pero para que un circuito digital sea capaz de manipular este tipo de números, su representación
debe tener un tamaño finito.
Esta limitación se traduce en que, además de definir un esquema por el que se codifican los elementos necesarios utilizando el
código binario, también se debe fijar el tamaño de dicha codificación y lo que sucede cuando esta codificación no es suficiente.
Por ejemplo, se decide representar los números naturales en binario con un tamaño de 10 bits. Sólo los números en el intervalo
[0,210 - 1] pueden ser representados. El último número de dicho intervalo es el 1111111111 binario que corresponde con el
número 1023 en decimal.
El problema aparece si el procesador manipula los números naturales con 10 dígitos y se debe ejecutar la operación 1023 +
1. El resultado es calculable, pero su representación con 10 bits no es posible. A esta situación, se obtiene un número cuya
representación no es posible, se le denomina overflow o ‘desbordamiento’. Los procesadores detectan y notifican esta situación
por tratarse de una anomalía en la codificación.
El número de bits utilizado para codificar los naturales es un parámetro que depende del procesador utilizado. Cuantos más
dígitos se utilicen más números se pueden representar, pero a la vez más complicado es el diseño de la lógica interna encargada
de realizar las operaciones. A lo largo de la historia, los procesadores han ido utilizando cada vez más bits para representar los
números naturales, comenzando por 8 bits (que permiten codificar los números del 0 al 255) hasta los 128 bits de procesadores
más avanzados.
El problema del tamaño de la codificación no es único para la representación de números naturales. Cualquier conjunto con un
número infinito de elementos a representar en binario tiene el mismo problema. Dependiendo del tamaño de la codificación, tan
sólo un subconjunto de sus elementos será representado y el procesador debe detectar y notificar cuando se requiere codificar un
elemento cuya representación no es posible.
2.6.
Codificación de números enteros
La codificación de números enteros en binario se puede realizar de diferentes formas. La más sencilla se conoce con el nombre
de ‘signo y magnitud’. Esta codificación está basada en la observación de que todo número entero se puede considerar como un
número natural, su valor absoluto, y un signo positivo o negativo. Por ejemplo, el número -345 se puede representar como el par
(-, 345). El valor absoluto de un entero, por definición es un número natural, y por tanto se puede utilizar la codificación binaria
descrita en la sección 2.3. Respecto al signo, dado que tiene dos posibles valores, su codificación en binario es trivial, con un
único bit en el que el valor 0 representa el signo positivo y el valor 1 el signo negativo.
La traducción de un entero en base 10 a su representación en binario en signo y magnitud es directa: se codifica el valor absoluto
como un número natural y el signo utilizando un bit adicional. El bit de signo se escribe como bit de más peso. La Tabla 2.7
muestra ejemplos de la codificación en signo y magnitud.
Programación en ensamblador de la
arquitectura IA-32
Número en decimal
-342
342
-23
36 / 198
Signo
-=1
+=0
-=1
Valor absoluto
342 = 101010110
342 = 101010110
23 = 10111
Signo y magnitud
1101010110
0101010110
110111
Tabla 2.7: Ejemplo de codificación de enteros en signo y magnitud
Si se utiliza una codificación de números enteros mediante signo y magnitud de n bits, se representan los enteros en el intervalo
[-2n-1 -1,2n-1 -1]. Esta expresión se deriva de que el mayor número posible es aquel que comienza por un cero seguido de unos y
que corresponde precisamente con el valor 2n-1 -1. Análogamente, el menor número representado es el que tiene todos los dígitos
a 1 y corresponde con el anterior límite, pero negativo. La figura 2.1 ilustra este rango para una representación con 8 bits sobre
la recta que de números enteros.
Figura 2.1: Rango de enteros codificados por 8 bits en signo y magnitud
Pero esta técnica de codificación tiene una propiedad no deseada. Con n bits se pueden representar 2n símbolos, pero si se realizan
las operaciones, con la codificación de signo y magnitud, se representan 2n - 1. El problema reside en la codificación del número
cero, pues es un número que no tiene signo, y por tanto la representación formada por sólo ceros y la formada por ceros con un
uno a la izquierda son equivalentes.
Esto quiere decir que un elemento se representa por dos combinaciones de bits, con lo que se está desperdiciando la codificación
de un número adicional. Exista una codificación alternativa a signo y magnitud que no tiene esta anomalía y se denomina
‘complemento a dos’.
La codificación en complemento permite codificar 2n números enteros consecutivos utilizando n bits. Más concretamente, el
rango de enteros representado por n bits es [-(2n-1 ), 2n-1 - 1]. La figura 2.2 muestra un ejemplo de codificación de enteros en
complemento a 2 con 8 bits. El código con ocho unos representa el número -1.
Figura 2.2: Rango de enteros codificados por 8 bits en complemento a 2
Esta codificación tiene múltiples propiedades que la hacen muy eficiente. El problema de la doble codificación para el número
cero presente en la codificación con signo y magnitud no está presente en complemento a dos. El cero tiene una única representación y corresponde con su valor en base 2. El bit de la izquierda o más significativo sigue representando el signo del número
con idéntica codificación que en el caso de signo y magnitud, los números positivos tienen este bit a cero y los negativos a uno.
Los números positivos se codifican de forma idéntica a los números naturales, es decir, con su codificación en base 2.
Pero la propiedad más interesante de esta codificación es que las operaciones de suma y resta de números en complemento a dos
siguen exactamente las mismas reglas que los números codificados en binario. Esta propiedad tiene una importancia enorme en
el contexto de los circuitos digitales puesto que si se implementa un circuito que calcula la suma y resta de números naturales,
dicho circuito se puede utilizar sin modificación alguna para sumar y restar enteros representados en complemento a 2.
La traducción de un número entero en base 10 a su representación con n bits en complemento a 2 se realiza mediante las siguientes
reglas:
Programación en ensamblador de la
arquitectura IA-32
37 / 198
Si el número mayor o igual que cero, su representación corresponde es directamente su traducción a base 2 con n bits.
Si el número es negativo, su representación se obtiene mediante tres operaciones:
a. Obtener la representación del valor absoluto del número.
b. Reemplazar cada cero por un uno y cada uno por un cero. A esta operación también se le conoce como ‘negar’ el número.
c. Sumar el valor 1 al número obtenido.
Por ejemplo, para calcular la representación del número -115 en complemento a dos con 8 bits, primero se calcula la representación en base dos con ocho bits del su valor absoluto 115, que es 01110011 (o su equivalente en hexadecimal 0x73). A
continuación se niega el número y se obtiene 10001100. Finalmente, se suma 1 al número y se obtiene 10001101.
La traducción de un número N en complemento a dos con n bits a su representación en base 10 se realiza mediante las siguientes
reglas:
Si el número N es positivo, es decir, el bit de más peso es cero, el número en base 10 se obtiene igual que en el caso de los
números naturales.
Si el número N es negativo, es decir, el bit de más peso es uno, el número en base 10 se obtiene mediante la fórmula
ABS(N) − (2n )
donde ABS(N) representa el número en base 10 que se obtiene al interpretar los bits en N como un número natural.
Por ejemplo, considérese el cálculo del valor en base 10 del número en complemento a dos de ocho bits 10110110. Como el bit
de más peso es 1, el número es negativo. Su valor en base 10 es, por tanto
ABS(10110110) − (2n ) = ABS(10110110) − 256
= 182 − 256
= −74
La Tabla 2.8 muestra la equivalencia entre las representaciones en base dos y en decimal de los números naturales y los enteros
en complemento a dos.
Números
N ∈ [0, 2n − 1]
Representación en base 10
N = ∑0n−1 2i bi
Enteros positivos.
N ∈ [0, 2n−1 − 1]
N empieza por cero
N = ∑0n−1 2i bi
N empieza por uno.
Enteros negativos.
N ∈ [−2n−1 , −1]
N
= ABS(N) − 2n
n−1
=
∑ 2i bi − 2n
0
Tabla 2.8: Representación de números naturales y enteros en binario con n bits
Si se precisa cambiar el número de bits con el que representar un número en complemento a dos, la operación no es idéntica al
caso de los números naturales. Un número natural en binario no cambia si se le añaden ceros a la izquierda. Los enteros positivos
representados en complemento a dos conservan esta regla. Sin embargo, la extensión de un número negativo representado en
complemento requiere una operación diferente.
Los números negativos en complemento a 2 tiene su bit más significativo a uno, por tanto, para aumentar el número de bits no se
pueden añadir ceros. En complemento a dos, todo número negativo mantiene su valor si se le añaden unos a la izquierda del bit
de más peso. En otras palabras, para extender la representación de un número en complemento a dos basta con extender el valor
que tenga como bit más significativo. A esta operación se le conoce con el nombre de ‘extensión de signo’.
Programación en ensamblador de la
arquitectura IA-32
2.7.
38 / 198
Codificación de números reales
La codificación de números reales utilizando lógica binaria es significativamente más compleja que el caso de los naturales o
enteros. Parte de esta complejidad deriva del hecho de que si bien al utilizar un número finito de bits se representaba un intervalo
concreto de números enteros o naturales, en el caso de los números reales esta técnica no es posible.
Dado que entre dos números reales existe un número infinito de números, no es posible representar todos los números en un
intervalo concreto sino que se representan únicamente un subconjunto de los números contenidos en dicho intervalo. Esta propiedad plantea un inconveniente que debe ser tenido en cuenta no sólo en el diseño de los circuitos digitales capaces de procesar
números reales, sino incluso en los programas que utilizan este tipo de números.
Supóngase que se operan dos números reales representados de forma binaria y que el resultado no corresponde con uno de
los números que pueden ser representados. Esta situación es perfectamente posible dado que entre dos números hay infinitos
números reales. La única solución posible en lógica digital consiste en representar este resultado por el número real más próximo
en la codificación. La consecuencia inmediata es que se ha introducido un error en la representación de este resultado. En
general, cualquier número real fruto de una operación tiene potencialmente el mismo problema. En algunos casos este error no
existe porque el número sí puede ser representado de forma digital, pero en general, la manipulación de números reales puede
introducir un error.
Este posible error introducido por la representación adquiere especial relevancia en aquellos programas que realizan cálculos
masivos con números reales. Existen técnicas de programación orientadas a minimizar el error producido cuando se manipulan
números.
Los números reales se codifican en lógica binaria mediante la técnica conocida como ‘mantisa y exponente’ que está basada en la
siguiente observación: todo número real consta de una parte entera y una parte decimal. La parte entera es aquella a la izquierda
de la coma, y la parte decimal a la derecha. A esta parte decimal se le denomina ‘mantisa’. En el contexto de los números reales,
la multiplicación y división por potencias de la base es equivalente al desplazamiento de la coma a lo largo de los diferentes
dígitos que conforman el número. Una multiplicación por una potencia positiva de la base implica un desplazamiento de la coma
a la derecha, y de forma análoga, la multiplicación por una potencia negativa de la base es equivalente a un desplazamiento de la
coma a la izquierda. El ejemplo 2.2 muestra esta equivalencia para los números reales en base 10.
Ejemplo 2.2 Multiplicación de números reales por potencias de la base
14345.342 = 1434.5342 ∗ 10
= 143.45342 ∗ 102
= 14.345342 ∗ 103
= 1.4345342 ∗ 104
= 0.14345342 ∗ 105
Mediante operaciones de multiplicación y división por la base es posible representar todo número real por una mantisa en la
que el primer dígito significativo está a la derecha de la coma multiplicado por una cierta potencia de la base. La ventaja de la
representación en coma flotante respecto a la representación en coma fija es que permite representar números muy pequeños o
muy grandes de forma muy compacta. El número 0.1*10-10 necesita 11 dígitos para representarse en punto fijo, sin embargo, en
coma flotante utilizando base 10 tan sólo se precisa representar la mantisa 0.1 y el exponente -10.
La representación de los números reales positivos en base 2 sigue las mismas reglas que la representación de los naturales en lo
que respecta al peso de cada uno de sus dígitos. El peso de los bits de la parte decimal se obtiene multiplicando por 2 elevado a
su posición pero con exponente negativo. Por ejemplo:
101.111
= 22 + 20 + 2−1 + 2−2 + 2−3
1 1 1
= 4+1+ + +
2 4 8
= 5.875
La notación de mantisa y exponente se puede aplicar de forma análoga a números codificados en base 2. En este caso las potencias
utilizadas en la multiplicación son potencias de 2. El ejemplo 2.3 muestra esta equivalencia en la representación binaria.
Programación en ensamblador de la
arquitectura IA-32
39 / 198
Ejemplo 2.3 Multiplicación por potencias de 2 en números reales binarios
101.111
= 10.1111 ∗ 21
= 1.01111 ∗ 22
= 0.101111 ∗ 23
Los números reales, por tanto, se representan tal que su primera cifra significativa en la mantisa sea aquella que está a la derecha
de la coma y se multiplica por la potencia pertinente de la base. La codificación de los números reales en base 2 consta de tres
partes: la mantisa, su signo y el exponente. La base sobre la que se aplica el exponente está implícita en la representación, y es la
base 2.
El bit de signo hace referencia al signo de la mantisa y se mantiene la misma convención que en el caso de la codificación de
enteros como signo y magnitud (ver sección 2.6), el signo positivo se representa como 0 y el negativo como 1.
La mantisa representa la parte decimal del número y tiene una influencia directa sobre su precisión. Cuantos más bits se incluyan
en la mantisa, mayor precisión tendrá la codificación. Tiene sentido hablar de ‘precisión’ de la representación porque se está
considerando un subconjunto de todos los números reales posibles.
Para la mantisa se aplica una mejora que permite el incremento de la precisión. Como esta se obtiene desplazando el número
hasta que el primer dígito significativo esté a la derecha de la coma, en base 2, esto quiere decir que dicho dígito siempre es un
uno y por tanto no es preciso almacenarlo. Por tanto, si una representación de números reales utiliza 23 bits para la mantisa, en
realidad está representando 24 bits, pues el primero es siempre 1. A esta técnica se le conoce con el nombre de ‘bit escondido’.
El exponente es un número entero y por tanto se puede utilizar cualquiera de las técnicas de codificación presentadas anteriormente. La más común es complemento a 2. La figura 2.3 muestra la estructura de la representación binaria de un número real en
coma flotante.
Figura 2.3: Estructura de la representación binaria en coma flotante
En este tipo de codificación no basta con saber el tamaño en bits de su representación, sino que es preciso especificar además,
cuántos bits se utilizan para codificar la mantisa y el exponente (el signo siempre se codifica con un bit).
2.7.1.
Desbordamiento en la representación en coma flotante
Al igual que en el caso de los números naturales y enteros, cuando un número está fuera del rango de representación, se produce un
de desbordamiento, pero en el caso de los números reales, esta situación es más compleja. Aparte del problema de representación
de números mayores al rango posible, también aparece un problema cuando el valor absoluto de un número es demasiado
pequeño.
Por ejemplo, supóngase que el valor más pequeño diferente de cero que se puede representar mediante coma flotante es n. Al
dividir dicho número por 3, la representación debe escoger el número real más próximo que pueda ser representado, que en este
caso es el cero.
Pero que un número diferente de cero se represente como cero tiene efectos muy significativos en los cálculos. Por ejemplo, si el
número que ha producido el resultado cero se multiplica por cualquier otro factor, el resultado es cero, y por tanto incorrecto. En
realidad, la propiedad que hace esta situación tan delicada es que, en adelante, no hay forma de acotar el error producido por la
representación en coma flotante. A esta situación se le conoce con el nombre de ‘desbordamiento por abajo’ o underflow.
Las situaciones de desbordamiento al operar números en coma flotante suelen producir excepciones en la ejecución de un programa que pueden ser procesadas por los propios programas para recuperarse, o bien producen la terminación abrupta de su
ejecución.
Programación en ensamblador de la
arquitectura IA-32
2.7.2.
40 / 198
El estándar IEEE 754
La representación de números reales en coma flotante puede seguir diferentes estrategias dependiendo del número de bits para
codificar mantisa y exponente, tipo de codificación del exponente, cómo se tratan condiciones especiales como desbordamiento,
etc.
El Instituto de Ingeniería Eléctrica y Electrónica (en adelante IEEE) ha propuesto dos estándares para la representación de números reales, el IEEE 754 y el IEEE 854, de los cuales, el más utilizado es el IEEE 754. Esta representación tiene cuatro posibles
precisiones: precisión simple, simple extendida, precisión doble, y doble extendida. La Tabla 2.9 muestra las características de
cada una de ellas. Nótese que en algunos de los parámetros, el estándar tan sólo especifica una cota, y no un valor fijo.
Parámetro
bits de la mantisa
máximo exponente
mínimo exponente
bits del exponente
tamaño total
Simple
24
127
-126
8
32
Simple Extendida
32
1023
≤-1022
≤11
43
Doble
53
1023
1022
11
64
Doble Extendida
64
≥16383
≤16382
15
79
Tabla 2.9: Parámetros del formato IEEE 754
La mantisa se representa utilizando la codificación de signo y magnitud y aplicando la técnica de bit escondido. Por ejemplo, en
la precisión simple se utiliza un bit para el signo y 24 para la magnitud. En el bit de signo el 0 representa el signo positivo y el 1
el negativo.
Los exponentes son números enteros su valor se representa utilizando un esquema de desplazamiento o suma de una constante que
para la precisión simple es 127 y para la doble es 1023. El valor real del exponente se obtiene restando la constante pertinente de
su interpretación en base 2. Por ejemplo, en la precisión simple del formato, si el exponente contiene el número 23, corresponde
con el exponente 23 - 127 = -104 y en precisión doble con 23 - 1023 = -1000. Los exponentes con valores todo ceros o todo unos
están reservados para situaciones excepcionales.
Los rangos resultantes de estas representaciones difieren significativamente de los obtenidos en las representaciones de enteros y
naturales. A igual número de bits, la representación en coma flotante cubre un rango más extenso de números pero a condición de
representar sólo un subconjunto de ellos. La Tabla 2.10 muestra los rangos de representación de las precisiones simple y doble.
Precisión
Simple
Doble
Negativos
[−(2 − 2−23 ) ∗ 2127 , −(2−126 )]
Positivos
[2−126 , (2 − 2−23 ) ∗ 2127 ]
[−(2 − 2−52 ) ∗ 21023 , −(2−1022 )]
[2−1022 , (2 − 2−52 ) ∗ 21023 ]
Tabla 2.10: Rangos representados en los formatos simple y doble
Para el caso concreto de la precisión simple, hay cinco rangos numéricos que son imposibles de representar.
1. Números negativos menores que -(2-2-23 )*2127 . Producen un desbordamiento negativo.
2. Números negativos mayores que -2-126 . Producen un desbordamiento por abajo.
3. El número cero
4. Números positivos menores que 2-126 . Producen un desbordamiento por abajo.
5. Números positivos mayores que (2-2-23 )*2127 . Producen un desbordamiento positivo.
El número cero no puede ser representado con este esquema debido a la utilización de la técnica del bit escondido. Para solventar
este problema se le asigna un código especial en el que tanto la mantisa como el exponente tienen todos sus bits a cero y el bit de
signo es indeterminado (con lo que el cero tiene dos posibles representaciones).
Programación en ensamblador de la
arquitectura IA-32
41 / 198
Otros dos valores a los que se les asigna un código especial son +∞ y -∞ que se codifican con el pertinente bit de signo, el
exponente con todo unos y la mantisa con todo ceros.
Otro caso especial a considerar con esta codificación es cuando se produce un resultado que es imposible de representar. Para
esta situación se utiliza la notación ‘NaN’ (acrónimo de Not a Number). Estos valores a su vez se dividen en dos categorías
dependiendo si el resultado es indeterminado o inválido y se denotan respectivamente por los símbolos ‘QNaN’ y ‘SNaN’. En
ambos casos el exponente tiene todos sus bits a uno y la mantisa es diferente de cero. El valor de la mantisa se utiliza para
distinguir entre las dos posibles situaciones.
2.8.
Representación de conjuntos de símbolos
Aparte de manipular números, un procesador debe poder representar y manipular conjuntos arbitrarios de símbolos. Al igual que
en el caso de los números, la lógica digital fuerza a que dichos símbolos se codifiquen con bits pero, a diferencia de los números,
un conjunto de símbolos arbitrario no tiene un conjunto de operaciones específicas que se realizan sobre él.
La definición de una codificación de un conjunto de símbolos precisa tres datos:
el conjunto de símbolos,
el número de bits a utilizar en la codificación,
la correspondencia entre cada símbolo del conjunto y la secuencia de bits que lo representa.
El número de bits a utilizar tiene la restricción derivada de la ecuación 2.1. Con n bits se pueden codificar un máximo de 2n
elementos, por tanto, si se denota por C la cardinalidad del conjunto de símbolos se debe cumplir la ecuación 2.6.
C ≤ 2n
E QUATION 2.6: Relación entre la cardinalidad del conjunto y el número de bits
Por ejemplo, se dispone del conjunto de símbolos para codificar S = { Rojo, Verde, Azul }. Se decide utilizar el mínimo número
de bits para su codificación, en este caso 2. La correspondencia entre los símbolos y su codificación se establece mediante la
siguiente tabla
Símbolo
Rojo
Verde
Azul
Codificación
00
01
10
Como la cardinalidad del conjunto no es una potencia de dos, existen más combinaciones de bits que elementos lo que permite
que existan múltiples posibles correspondencias.
2.8.1.
Codificación de caracteres
Uno de los conjuntos de símbolos que más se utilizan en un procesador son aquellos que se introducen a través del teclado. Desde
la aparición de los primeros procesadores ha existido la necesidad de tener una codificación para estos símbolos. Además, dado
que los ordenadores intercambian entre sí infinidad de datos, esta codificación es deseable que sea idéntica para todos ellos.
Una de las codificaciones de letras que más trascendencia ha tenido en los últimos años es la codificación ASCII (American
Standard Code for Information Interchange). En ella se incluyen so sólo letras y dígitos, sino también códigos especiales para
la transmisión de mensajes a través de teletipos. El tamaño de la representación es de 8 bits y por tanto, el número máximo de
símbolos que puede codificar es 256
El código contiene en sus primeras 32 combinaciones (de la 00000000 a la 0100000) un conjunto de símbolos que no son
imprimibles y que se utilizaban para la transmisión de texto entre dispositivos. Los símbolos con códigos del 33 al 127 son
los que sí pueden imprimirse. Por ejemplo, el espacio en blanco se codifica como 0x20. Las letras del la ‘a’ a la ‘z’ ocupan
Programación en ensamblador de la
arquitectura IA-32
42 / 198
los códigos del 0x61 al 0x7A (en este rango no está incluida la ñ). Sus correspondientes símbolos en mayúsculas ocupan los
códigos del 0x41 al 0x5A (la diferencia numérica entre los códigos de mayúscula y minúscula es idéntica).
Pero el código ASCII no fue el único utilizado para representar letras, sino que ha convivido con otras alternativas similares tales
como EBCDIC (Extended Binary Coded Decimal Interchange Code) que aun se utilizan en algunos de los ordenadores actuales.
Una de las carencias más importantes del código ASCII es la ausencia de codificación para los símbolos que no son parte del
alfabeto anglosajón, como por ejemplo, todas las letras que incluyen tildes, diéresis, etc. Para dar cabida a estos símbolos se
definió lo que se conoce como el código ASCII extendido que incluye en los valores del 128 al 255 estos símbolos así como un
conjunto de símbolos gráficos y matemáticos.
Esta extensión, a pesar de dar cabida a algunos de los alfabetos utilizados en países de habla no inglesa, no fue suficiente para
incorporar alfabetos tales como los asiáticos o los de oriente medio que constan de un número muy elevado de grafías.
La única solución posible ha sido proponer una nueva codificación con el tamaño suficiente para dar cabida a todos los alfabetos
existentes. Con tal motivación se diseño la codificación ‘Unicode’ cuyo objetivo es proveer una única representación numérica
para cada símbolo independientemente de la plataforma, el programa o el lenguaje en el que se manipule.
La codificación Unicode se ha transformado en un estándar adoptado por las principales empresas de hardware y software. Su
presencia no cubre la totalidad de aplicaciones, pero con el paso del tiempo será la única representación utilizada.
El estándar pretende ser lo más genérico posible, y por tanto, en lugar de fijar un único tamaño para la representación, su
codificación la divide en tres posibles formas: 8 bits, 16 bits y 32 bits. Estas codificaciones son diferentes pero todas son parte
del estándar y se conocen con los nombres de ‘UTF-8’, ‘UTF-16’ y ‘UTF-32’ respectivamente. La correspondencia entre la
representación numérica y los símbolos está perfectamente definida y tabulada en cada una de los tres formatos. La Tabla 2.11
muestra un ejemplo de tres símbolos y su codificación en Unicode.
Imagen
z
Símbolo
z minúscula
Código
0x007A
agua en chino
0x6C34
clave de sol
0xD8340xDD1E
Tabla 2.11: Ejemplo de símbolos codificados con Unicode
Una vez definida la codificación de todas las letras y símbolos adicionales, las cadenas de estos símbolos se representan mediante
una secuencia de estos códigos en los que cada número corresponde con una letra. Esta codificación es la utilizada por los editores
de texto plano para la representación interna del texto. Algunos de ellos incluso permiten cambiar la codificación utilizada
entre ASCII y alguno de los formatos Unicode. La Tabla 2.12 muestra el texto de un programa en lenguaje ensamblador y su
representación equivalente utilizando el código ASCII (los valores numéricos están en hexadecimal).
2.8.2.
Codificación de instrucciones
Una de las codificaciones más importante que necesita un procesador es el de su propio conjunto de instrucciones. En él, cada
posible instrucción con sus operandos se considera un elemento. Para su codificación es preciso escoger tanto el número de bits
como la correspondencia entre instrucciones y su codificación binaria.
Para ilustrar este tipo de codificación se utilizará un conjunto inicial de instrucciones sencillas a modo de ejemplo en el que
se irán incorporando de forma gradual más elementos para ver cómo la codificación puede ser adaptada. El conjunto inicial de
instrucciones se denota por el nombre ‘ual-1’.
En lugar de definir el conjunto de instrucciones mediante una simple enumeración de todos sus elementos, se divide la instrucción
en partes y se proponen codificaciones separadas para los valores de cada una de las partes. Las instrucciones del conjunto ual-1
constan de tres partes: una operación y dos operandos que son dos números reales en el rango 0 a 255. El código de operación
Programación en ensamblador de la
arquitectura IA-32
43 / 198
.data
msg: .asciz "Hello world\n"
.text
.globl main
main: push $msg
call printf
add 4, %esp
ret
Texto
Codificación ASCII
20 20 20 20 20 20 2E
6D 73 67 3A 20 20 2E
48 65 6C 6C...
20 20 20 20 20 20 2E
20 20 20 20 20 20 2E
74 61 72 74...
6D 61 69 6E 3A 20 70
67 0A
20 20 20 20 20 20 63
6E 74 66 0A
20 20 20 20 20 20 61
25 65 73 70....
20 20 20 20 20 20 72
64 61 74 61 0A
61 73 63 69 7A 20 22 ←74 65 78 74 0A
67 6C 6F 62 6C 20 73 ←75 73 68 20 24 6D 73 ←61 6C 6C 20 70 72 69 ←64 64 20 24 34 2C 20 ←65 74 0A
Tabla 2.12: Texto de un programa y su codificación en ASCII
puede tener uno de los siguientes cuatro valores: add, sub, mul y div. La figura 2.4 muestra la estructura de las instrucciones
que conforman este conjunto de símbolos así como varias instrucciones de ejemplo.
Figura 2.4: Estructura de las instrucciones de ual-1
El primer paso para diseñar una codificación es calcular el número de elementos que contiene ual-1. La primera parte tiene 22
posibles valores, la segunda 28 y la tercera igualmente 28 posibles valores. Todas las combinaciones son posibles, por tanto el
número de elementos se obtiene multiplicando estos factores:
22 ∗ 28 ∗ 28 = 218 = 262144
El número de bits y la cardinalidad deben satisfacer la ecuación 2.6 y por tanto se precisan al menos 18 bits. En lugar de utilizar
el número mínimo de bits, se decide utilizar el menor múltiplo de 8 bits, es decir, 24 bits (3 bytes).
La correspondencia entre los símbolos y su representación en binario se establece igualmente de forma implícita para evitar la
enumeración de todos los elementos. Las reglas de esta codificación son:
El código de operación se codifica con dos bits y la siguiente correspondencia:
Símbolo
add
sub
mul
div
Codificación
00
01
10
11
El segundo y tercer operandos se codifican con su representación en base 2.
Toda codificación se escribe de izquierda a derecha, y se completa con 6 dígitos consecutivos con valor 0 para obtener un total
de 24 bits.
La figura 2.5 ilustra el formato de la codificación binaria de los elementos de ual-1.
Programación en ensamblador de la
arquitectura IA-32
44 / 198
Figura 2.6: Ejemplo de codificación de instrucción
La propiedad más importante de esta codificación es que la traducción de cualquier instrucción a su representación en binario y
viceversa es un proceso sistemático.. La Tabla 2.13 muestra la codificación de otros símbolos del conjunto ual-1.
Símbolo
ADD 0x27 0xB2
SUB 0x11 0x2F
MUL 0xFA 0x2B
DIV 0x10 0x02
Codificación
0x09EC80
0x444BC0
0xBE8AC0
0xC40080
Tabla 2.13: Representación en hexadecimal de símbolos de ual-1
En general, el número hexadecimal resultante no contiene los mismos dígitos que en el símbolo del que procede. Esto sucede
porque los bits son reorganizados e interpretados de forma diferente, pero la información sigue estando contenida en el resultado.
A continuación se define un nuevo conjunto de símbolos derivado de ual-1 pero con una estructura más compleja. Se añade a
cada símbolo un cuarto elemento que define el lugar donde almacenar el resultado de la operación y que puede tener los valores
LugarA o LugarB. Este nuevo conjunto se denotará por ual-2. La estructura de los símbolos de ual-2 así como ejemplos de
algunos de sus símbolos se muestra en la figura 2.7.
Figura 2.7: Estructura de las instrucciones de ual-2
Tras definir el nuevo conjunto, se obtiene el número de elementos de los que consta. De cada elemento del conjunto ual-1 se
obtienen dos elementos del nuevo conjunto ual-2: uno con el sufijo LugarA y otro con el sufijo LugarB. Por tanto, el número de
elementos de ual-2 es 219 y se requieren al menos 19 bits para su codificación. Al igual que en el caso de ual-1, la representación
se realiza en múltiplos de bytes, y por tanto se utilizarán 24 bits.
La correspondencia entre los elementos de ual-2 y su representación binaria sigue las mismas reglas que para el caso de ual-1
con la modificación de que el operando añadido se codifica con un único bit y según la codificación que se muestra en la Tabla
2.14.
Símbolo
LugarA
LugarB
Codificación
0
1
Tabla 2.14: Codificación del operando lugar
La nueva codificación utiliza 19 bits en lugar de los 18 anteriores y con la estructura que se ilustra en la figura 2.8.
Programación en ensamblador de la
arquitectura IA-32
45 / 198
Figura 2.8: Estructura de la correspondencia de ual-2
El proceso de traducción de símbolo a su representación con seis dígitos hexadecimales y viceversa se puede hacer igualmente
de forma sistemática. La Tabla 2.15 muestra varios ejemplos de representación de símbolos de ual-2 en binario y hexadecimal.
Símbolo
div 0x10 0x02
LugarA
add 0x10 0x02
LugarB
mul 0x10 0x02
LugarA
Binario
11 0001 0000 0000
0010 0 00000
00 0001 0000 0000
0010 1 00000
10 0001 0000 0000
0010 0 00000
Hexadecimal
0xC40080
0x0400A0
0x840080
Tabla 2.15: Representación en binario y hexadecimal de símbolos de ual-2
Al proceso de traducción de un símbolo del conjunto ual-2 a su equivalente en binario (o hexadecimal) se denominará ‘codificación’, y al proceso inverso por el que se obtiene un símbolo a partir de su codificación en binario se denominará ‘decodificación’.
Dadas la reglas de traducción, estos dos procesos se pueden realizar de forma automática.
La decodificación de instrucciones es precisamente lo que realiza un procesador cuando recibe una instrucción a ejecutar. Al igual
que el resto de datos que manipula, las instrucciones están codificadas como secuencias binarias. Una vez recibida se decodifica
para saber qué cálculo se debe realizar.
Los dos esquemas de codificación de los conjuntos ual-1 y ual-2 tienen múltiples decisiones arbitraras: la correspondencia de
las operaciones con sus códigos de dos bits, el orden en el que se concatenan los códigos binarios, el número total de bits a
utilizar en la representación (mientras se respete el mínimo), la colocación de los bits de relleno, etc. Estas decisiones pueden ser
modificadas dando lugar a codificaciones diferentes pero igualmente válidas. La propiedad que se debe mantener es la posibilidad
de codificar y decodificar los símbolos de forma inequívoca.
Los bits de relleno añadidos a la derecha para alcanzar el tamaño de 24 bits ofrecen cierto grado de ambigüedad. Dados dos
números binarios de 24 dígitos, si la única diferencia entre ellos radica en los bits de rellenos, al interpretarse como símbolos
de ual-1 o ual-2, ambos corresponden con el mismo símbolo. Esta propiedad no cambia en nada el proceso de codificación y
decodificación, tan sólo pone de relieve el hecho de que el número de bits utilizado permite más combinaciones que número de
elementos hay en el conjunto.
A continuación se define un nuevo conjunto, que se denotará por ‘ual-3’ y en el que en los dos primeros operandos que hasta
ahora sólo podía haber un número de ocho bits, ahora se permite o un número de ocho bits, o un lugar con valores LugarA o
LugarB. Este nuevo conjunto extiende al conjunto ual-2 con aquellos símbolos en los que la segunda o tercera parte representa
un lugar. Se puede comprobar que ual-2 es un subconjunto de ual-3.
Este nuevo conjunto representa aquellas instrucciones cuyos operandos no sólo son valores numéricos sino que pueden contener
uno de los dos posibles lugares LugarA o LugarB no sólo para almacenar el resultado (como indica el último campo de la
instrucción) sino como lugar de donde obtener un operando. Por ejemplo, el símbolo ADD LugarA 0x10 LugarB forma
parte del nuevo conjunto ual-3 y podría representar la orden por la cual se obtiene el contenido de LugarA, se suma 0x10 y el
resultado se almacena en LugarB.
La cardinalidad de este nuevo conjunto se calcula multiplicando el número de combinaciones posibles que tiene cada una de las
partes de la instrucción. El código de operación sigue teniendo cuatro posibles valores, pero ahora cada uno de los operandos
tiene los 256 posibles valores del número natural más dos posibles valores que representan un lugar. Por tanto el número de
elementos de ual-3 es:
Programación en ensamblador de la
arquitectura IA-32
46 / 198
22 ∗ (28 + 2) ∗ (28 + 2) ∗ 2 = 532512 ≤ 224
Siguiendo la política utilizada hasta ahora, el tamaño de la codificación se realiza en múltiplos de bytes, y por tanto, ocupa 24 bits
que siguen siendo suficientes. La correspondencia entre símbolos y su codificación binaria necesita ser revisada, pues ahora los
valores posibles de los operandos superan los 256 que se codificaban con ocho bits. Se precisa un esquema que permita codificar
los 258 posibles valores de cada uno de los operandos y para ellos se precisan al menos 9 bits, puesto que 258 > 28 .
La codificación con 9 bits ofrece 512 combinaciones, mientras que tan sólo existen 258 posibles valores a codificar. Una posible
solución consiste en dividir los 9 bits en dos grupos de 1 y 8 bits. Si el primer bit es cero indica que en los siguientes 8 se codifica
un número natural entre 0 y 255. Si es uno indica que se codifica uno de los dos valores LugarA o LugarB. En el primer caso
se utilizan todos los bits, mientras que en el segundo, el lugar se codifica en el último de los ocho bits ignorando el valor de los 7
bits restantes. La figura 2.9 ilustra este esquema de codificación.
Figura 2.9: Codificación de los operandos en ual-3
Al haber menos elementos (258) que codificaciones posibles con 9 bits (512), aparecen numerosas combinaciones que codifican
el mismo operando. Concretamente, cuando se codifica un lugar, aquellas combinaciones con un uno en el primer bit, un determinado valor en el último y cualquier combinación en los siete bits restantes representan el mismo operando. La Tabla 2.16
muestra ejemplos de la codificación de operandos en ual-3.
Codificación binaria
000010010
100010011
001111010
100010010
1XXXXXXX0
Operando
0x12
LugarB
0x7A
LugarA
LugarA
Tabla 2.16: Ejemplos de codificación de operandos en ual-3
Tal y como se muestra en la última fila de la Tabla 2.16, cuando el valor de un bit en una secuencia no es relevante, se suele
representar por el valor ‘X’.
Al aplicar la nueva correspondencia, el código binario resultante consta de siete partes: la operación, el bit que indica el tipo de
primer operando, el primer operando, el bit de tipo del segundo operando, el segundo operando, el tercer operando y los bits de
relleno. La figura 2.10 muestra esta estructura.
Programación en ensamblador de la
arquitectura IA-32
47 / 198
Figura 2.10: Formato de la codificación de ual-3
La Tabla 2.17 muestra ejemplos de codificación de instrucciones del conjunto ual-3.
Instrucción
add 0x01 0x00 LugarA
add 0x02 0x00 LugarB
mul LugarB 0x03 LugarA
mul LugarB 0x04 LugarA
Binario
0x002000
0x004008
0xA02030
0xA02040
Tabla 2.17: Codificación de instrucciones de ual-3
Los esquemas de codificación como el mostrado hasta ahora son una simplificación de los utilizados para que un procesador
sepa qué instrucción debe ejecutar. Desde que el procesador recibe el voltaje necesario para funcionar, hasta que dicho voltaje
desaparece, la única tarea que se realiza es la obtención de un conjunto de bits, su interpretación, la ejecución de la instrucción
pertinente y la obtención del siguiente conjunto de bits.
La codificación de una secuencia de instrucciones se transforma en una secuencia de códigos binarios, cada uno de los cuales
codifica una instrucción y que el procesador carga una tras otra y las ejecuta. El lugar donde deben están almacenados estos
valores es la memoria RAM que se comparte con el almacenamiento de los datos.
El número de bits utilizado para codificar un conjunto de símbolos influye directamente en la cantidad de memoria necesaria para
almacenar una secuencia de instrucciones. Cuanto más compacta sea la representación menor cantidad de memoria se utiliza y
más cantidad de información se puede almacenar, por lo que este es un criterio que se tiene en cuenta a la hora de diseñar la
codificación del lenguaje de un procesador.
A la vista del esquema de codificación utilizado para ual-3 cabe preguntarse: ¿es posible codificar las instrucciones utilizando un
número menor de bits para utilizar menos cantidad de memoria? En un primer análisis se puede comprobar que si la instrucción
tiene como sus dos primeros operandos dos números de 8 bits, la codificación no puede ser reducida, pues cada uno de los bits
tiene un cometido.
Sin embargo, en el caso de que alguno de los dos primeros operandos contenga un lugar, parte de los bits no se utilizan, por lo
que se puede considerar un esquema de representación más compacto. Se propone un nuevo esquema en el que la codificación
del primer y segundo operando se modifica de la siguiente forma:
Si el operando es un número de ocho bits se mantiene la codificación anterior con un bit de prefijo con valor 0.
Si el operando es un lugar, se codifica con dos bits, el primero tiene valor 1 y el segundo codifica el lugar.
Con este nuevo esquema de codificación, el símbolo add LugarB 0x10 LugarB se codifica con la secuencia de 14 bits 00
11 0 0001 0000 1. Si se asume que la representación se debe hacer en múltiplos de bytes, se añaden dos bits de relleno al final
para un total de 16 bits o dos bytes: 0x3084.
Programación en ensamblador de la
arquitectura IA-32
48 / 198
La pregunta que cabe hacerse ahora es ¿mantiene esta representación la propiedad de ser una codificación sistemática? La
respuesta es que sí pero con una modificación sustancial. El proceso de codificación queda perfectamente definido por las reglas
anteriormente enunciadas. El proceso de decodificación sí requiere una estrategia diferente a las anteriores puesto que como el
tamaño de la codificación es variable, su interpretación se debe realizar de forma incremental.
Los pasos a seguir consisten en obtener un primer byte y analizar los bits en orden para ver qué codifican. Dependiendo de
los valores presentes se obtienen más o menos bits para decodificar los valores de los operandos. Así, por ejemplo, con la
representación 0x3084 se observa que los dos primeros bits son cero, representando la operación add y que el siguiente bit es
un uno, por lo que en lugar de obtener los ocho siguientes tan sólo se obtiene el siguiente que ahora sí se sabe que codifica uno
de los dos lugares. A continuación se procesan los bits restantes. El siguiente es un cero, con lo que es preciso procesar los ocho
bits siguientes, y acceder al siguiente byte. Una vez obtenidos los campos se concluye que no se necesitan más bytes y se obtiene
el símbolo pertinente.
Este esquema de codificación mantiene tanto la propiedad de ser sistemático y como la premisa de representar las instrucciones
con tamaños que sean múltiplos de bytes. A cambio resultan instrucciones de uno, dos y tres bytes dependiendo de los operandos
que contengan.
Este último ejemplo ilustra las dos categorías en las que se dividen los lenguajes máquina de los procesadores comerciales
ateniéndose a la longitud de su codificación: formato fijo y formato variable.
Formato fijo: todas las instrucciones del lenguaje máquina tienen la misma longitud. Generalmente se dividen en porciones y
cada porción codifica un elemento de la instrucción. Como principal ventaja ofrecen una decodificación muy rápida, pues las
reglas son muy simples. Como principal desventaja se desperdicia memoria al almacenarlas, pues algunos de los campos en
algunas instrucciones no son necesarios.
Formato variable: las instrucciones tienen tamaño diferente dependiendo de la información que contienen. Como principal
ventaja ofrece una representación muy compacta del código y por necesita menos memoria para ser almacenada. Como inconveniente, el proceso de decodificación de una instrucción requiere múltiples pasos así como un acceso gradual a la secuencia
de bits.
La complejidad del proceso de decodificación de una instrucción por el procesador tiene un impacto significativo en su velocidad
de proceso. Esta fase es común a todas las instrucciones, y por tanto, si el proceso es demasiado lento, acaba afectando a la
velocidad global de ejecución. En la actualidad, estas dos categorías están presentes en el panorama de procesadores.
2.8.3.
Descripción de un lenguaje máquina
La codificación de un lenguaje máquina implica un proceso de decodificación que se realiza en el procesador, y que por tanto
debe ser implementado como un circuito digital. Pero para poder escribir programas en ese lenguaje máquina se precisa una
descripción detallada de cada una de las instrucciones así como las reglas que permiten su codificación en binario.
Toda descripción de un lenguaje máquina suele ir precedida por una descripción de la arquitectura del procesador que lo ejecuta.
Esto es así porque en dicho lenguaje se permite la referencia a los elementos del procesador que se ofrecen para su programación.
Tras la descripción de la arquitectura, la forma de describir el lenguaje máquina es agrupando sus operaciones. En lugar de
describir todas las instrucciones posibles con todas las posibles combinaciones de operandos, se suelen describir juntas todas
aquellas que tienen una funcionalidad similar. El apéndice A contiene la descripción del subconjunto de instrucciones máquina
de la arquitectura IA-32 que serán consideradas a lo largo de este documento.
Todo procesador incluye, como parte de su definición, un conjunto de documentos con una descripción detallada de su arquitectura, del entorno de programación, de todas y cada una de sus instrucciones, así como de los mecanismos auxiliares que se
ofrecen para facilitar la ejecución de programas de usuario y del sistema operativo.
2.9.
Ejercicios
1. ¿Cuántos bits se precisan para codificar 34 elementos? ¿y 32? ¿Se pueden codificar en binario 34 elementos con 8 bits?
2. ¿A partir de qué número de elementos se tienen que utilizar al menos 8 bits para su codificación binaria?
Programación en ensamblador de la
arquitectura IA-32
49 / 198
3. Escribir sin utilizar la calculadora las potencias de 2 hasta 216 . ¿Qué representa cada uno de esos números?
Utilizando únicamente los datos que aparecen en esta tabla y sin realizar operación aritmética alguna, ¿se pueden codificar
5642 elementos con 13 bits?
4. De los siguientes números, dos de ellos están escritos incorrectamente, ¿cuáles son? ¿por qué?
100110
10032 (Incorrecto por el dígito 3)
5007
5034 (Incorrecto por el dígito 5)
5556
5. Calcular el equivalente en base 10 de los siguientes números:
437 = 31
11002 = 12
5346 = 202
7778 = 511
1009 = 81
6. Rellenar la siguiente tabla con la representación equivalente en las diferentes bases de los números en base 10 dados.
Base 10
2910
43210
945310
Base 2
11101
110110000
10010011101101
Base 5
104
3212
300303
Base 8
35
660
22355
7. Rellenar la siguiente tabla con la representación equivalente en base 10 o binario de los números dados.
Binario
10101000001
110111
11111111
101010
1000000000001
100000
Decimal
1345
55
255
42
4097
32
8. Rellenar la siguiente tabla con la representación equivalente en octal, hexadecimal o binario de los números dados.
Binario
1011100101
110111
1111001000110001
101010
100000111111
1011101011001010
Octal
01345
67
171061
52
04077
135312
Hexadecimal
0x2E5
37
0xF231
2A
83F
0xBACA
9. La representación de un número de 12 bits en octal es 7??3. Donde ‘?’ representa un dígito desconocido. ¿Cuáles de las
siguientes representaciones de ese mismo número en hexadecimal son posibles?
a. 0xE?3
b. 0xE?C
c. 0xF?3
Programación en ensamblador de la
arquitectura IA-32
50 / 198
d. 0xF?C
10. ¿Qué intervalo de enteros permite representar la codificación en complemento a dos con 11 bits?
11. Realizar las operaciones 101100 - 100011 y 100011 + 001100 donde los operandos son números enteros representados en
complemento a 2 con 6 bits. Comprobar que los resultados obtenidos son consistentes con la representación en base 10 de
los operandos.
12. Rellenar la siguiente tabla con la representación equivalente en complemento a dos con 10 bits o decimal de los siguientes
números.
Complemento a 2
1001000011
1000110111
1000000001
0111010110
Fuera de rango
1000000000
2.10.
Decimal
-445
-457
-511
470
-1012
-512
Bibliografía
[Goldberg91]
David Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic, Association
for Computing Machinery, copyright © 1991 ACM.
Programación en ensamblador de la
arquitectura IA-32
51 / 198
Capítulo 3
Almacenamiento de datos en memoria
En este capitulo se estudia el funcionamiento de la memoria RAM que utiliza el procesador para almacenar todos aquellos datos
y código que precisa para la ejecución de un programa. También se estudia la técnica de la indirección por la que se manipulan
direcciones de memoria que apuntan a otras direcciones en lugar de los propios datos.
3.1.
La memoria RAM
Los circuitos digitales únicamente pueden procesar datos representados con ceros y unos, pero para ello deben estar almacenados
en otro circuito que permita a su vez su modificación. En el contexto de un ordenador este dispositivo suele ser la memoria
RAM (random access memory), un circuito que contiene en su interior una tabla que almacena información en cada uno de sus
compartimentos.
Como toda tabla, es preciso saber dos de sus dimensiones: el tamaño de cada uno de sus elementos, y el número de elementos
de los que dispone. Actualmente, las memorias convencionales almacenan la información en elementos de tamaño 1 byte. Por lo
tanto una memoria se puede ver como una tabla que contiene un determinado número de bytes. Los elementos de esta tabla están
numerados con números naturales comenzando por el cero. El número correspondiente a cada una de los elementos se denomina
‘dirección de memoria’ y se suele representar de forma abreviada por el símbolo ‘@’. Al conjunto de números que representan las
direcciones de una memoria se le denomina su ‘espacio de direcciones’. La figura 3.1 ilustra la estructura, contenido y direcciones
de una memoria RAM.
Programación en ensamblador de la
arquitectura IA-32
52 / 198
Figura 3.1: Estructura de la memoria RAM
El acceso a los datos internos de la memoria viene determinado por el tamaño de sus celdas o elementos. Tal y como está estructurada, la memoria no ofrece acceso directo a cualquiera de sus bits, sino que es preciso primero obtener un byte y posteriormente
acceder al bit pertinente. Los procesadores incluyen en su lenguaje máquina las instrucciones necesarias para poder manipular
los bits de un byte. Si se quiere, por tanto cambiar un bit de un byte de memoria se debe leer el byte entero, utilizar instrucciones
para cambiar su valor, y escribirlo de nuevo en memoria.
Internamente la memoria está implementada por un conjunto de transistores diseñados de tal forma que pueden almacenar la
información dada. La unidad responsable de almacenar un bit de información se denomina ‘celda’. Un chip de memoria no es
más que un circuito que contiene un determinado número de celdas en cuyo interior se almacena un bit. Existen dos técnicas para
el diseño de memoria: estática y dinámica.
La memoria RAM estática o SRAM es un circuito que una vez que se escribe un dato en una de sus celdas lo mantiene intacto
mientras el circuito reciba voltaje. En cuanto el voltaje desaparece, también lo hace la información. La celda de dicha memoria
está compuesta por alrededor de seis transistores conectados de forma similar a un registro. El tiempo de lectura de una posición
de memoria compuesta por ocho celdas suele ser del orden de decenas de nanosegundos (1 nanosegundo son 10-9 segundos).
La memoria RAM dinámica o DRAM es similar a la anterior pues también almacena información, pero su estructura interna es
radicalmente diferente. La celda de memoria dinámica consta únicamente de un transistor y un condensador. Este último es el
que almacena una carga, mientras que el transistor se utilizar para su carga y descarga. La celda de memoria dinámica almacena
el valor 1 cuando el condensador está cargado, y cero cuando está descargado. El problema que presenta esta celda es que, a pesar
de estar conectada continuamente a su alimentación, si el condensador almacena el valor 1 y no se realiza ninguna operación, su
carga se degrada hasta alcanzar el valor 0. Es decir, la celda de esta memoria no es capaz de mantener el valor uno durante un
tiempo arbitrario, sino que acaba perdiéndose. Pero, tal y como está diseñada la lógica de lectura, al leer una celda se refresca
totalmente su valor, y por tanto se recupera la pérdida de carga que pudiera haberse producido. El tiempo que tarda una celda en
perder su información es del orden de milisegundos (1 milisegundo son 10-3 segundos).
Este comportamiento de las celdas puede parecer inútil para almacenar información, pero si el contenido se lee de forma periódica, la memoria dinámica se comporta de forma idéntica a la estática. Los circuitos de memoria dinámica incluyen la lógica
necesaria para que sus celdas sean continuamente leídas independientemente de las operaciones de lectura y escritura realizadas
por el procesador, de esta forma se garantiza que su contenido no se pierde. A esta operación se le conoce con el nombre de
‘refresco’.
La mayoría de ordenadores utilizan memoria dinámica en su memoria principal y las principales razones para ello son el coste
y el espacio. La celda de memoria dinámica con un único transistor y un condensador es aproximadamente la cuarta parte del
Programación en ensamblador de la
arquitectura IA-32
53 / 198
tamaño de la celda de SRAM que consta de alrededor de seis transistores. Pero, además de ser más pequeña, el proceso de diseño
de una celda DRAM tiene un coste mucho menor por lo que los chips de memoria de gran capacidad de almacenamiento se
diseñan con memoria dinámica.
La memoria estática tiene una clara ventaja frente a la dinámica y es que su tiempo de acceso es menor. En la realidad, en un
ordenador se utilizan ambos tipos de memoria. Para aquellos componente en los que se necesite mayor capacidad de almacenamiento la memoria dinámica es la idónea. En aquellos en los que se quiera un tiempo de acceso más reducido se utiliza la
memoria estática.
El diseño de un circuito de memoria es significativamente más simple que el de un procesador. La mayor parte del circuito son
réplicas de la celda que almacena un bit. Además de estas celdas, las memorias incluyen la lógica necesaria para el refresco (si
son DRAM) y para realizar las operaciones de lectura y escritura.
3.2.
Operaciones sobre memoria
Las dos operaciones que permite una memoria son lectura y escritura. En la operación de lectura, la memoria recibe una dirección
y devuelve el byte contenido en el elemento con dicho número. En la operación de escritura la memoria recibe una dirección y
un byte y sin devolver resultado alguno sobreescribe el byte en el elemento correspondiente. Otra forma posible de especificar
estas operaciones es mediante la notación típica de un lenguaje de programación.
byte Lectura(dirección d): Dada una dirección de memoria devuelve el byte almacenado en dicho elemento.
void Escritura(dirección d, byte b): Almacena el byte b en el elemento de dirección d.
El contenido de los datos almacenados inicialmente en la memoria es indefinido. Si al encender el ordenador, la primera operación
que se realiza es de lectura sobre memoria RAM, el resultado es indefinido. De esta propiedad se deduce que toda operación de
lectura se debe ejecutar sobre una posición de memoria que haya sido previamente escrita. La figura 3.2 muestra el efecto de un
conjunto de operaciones sobre memoria.
Programación en ensamblador de la
arquitectura IA-32
54 / 198
Figura 3.2: Operaciones sobre memoria
Al ser la memoria un circuito digital, todos sus datos deben ser codificados igualmente con ceros y unos y esto incluye a los
parámetros que reciben las operaciones de lectura y escritura. El dato a leer o escribir es un byte y por tanto ya está codificado en
binario. Las direcciones también deben estar codificadas en binario, y como son números naturales (son positivos y comienzan
por cero) la codificación utilizada es base dos.
La lectura de un dato consiste en enviar a la memoria los bits que codifican una dirección, y la memoria devuelve ocho bits. La
operación de escritura consisten en enviar a la memoria los bits que codifican una dirección y ocho bits de datos, y éstos últimos
se almacenan en la posición especificada.
La codificación de las direcciones tiene una relación directa con el tamaño de la memoria. Todo byte en memoria tiene una
dirección, y el número de bytes corresponde con el número máximo de dirección que se puede codificar. Al utilizar la codificación
en base 2 se deduce que una memoria cuyas direcciones se codifican con n bits puede tener como máximo un tamaño de 2n bytes
con direcciones desde 0 hasta 2n -1. En consecuencia, el tamaño T de memoria y el número n de bits que se utilizan para codificar
las direcciones están relacionadas por la ecuación
T = 2n
Debido a esta relación entre los bits que codifican una dirección y el número de elementos, las memorias suelen tener un tamaño
potencia de 2. El coste de incluir un número determinado de bits hace que se aprovechen todas sus combinaciones.
El tamaño de la memoria se mide en múltiplos que no siguen las reglas convencionales de multiplicación por potencias de 10
sino por potencias de 2. Así, un kilobyte son 210 bytes o 1024 bytes. Las unidades de medida del tamaño de memoria así como
sus exponentes y los prefijos de su nomenclatura se muestran en la Tabla 3.1.
Programación en ensamblador de la
arquitectura IA-32
Prefijo
kilo
mega
giga
tera
peta
exa
zetta
yotta
55 / 198
Símbolo
K
M
G
T
P
E
Z
Y
Potencia
210
220
230
240
250
260
270
280
Tabla 3.1: Unidades de almacenamiento de información en bytes
3.3.
Conexión entre memoria y procesador
La conexión entre la memoria y el procesador debe permitir que se realicen las operaciones de lectura y escritura de la forma
descrita en la sección 3.2. Para ello son necesarios dos buses. El primero para que la memoria reciba la dirección del procesador,
y el segundo para que el procesador envíe a la memoria el dato a escribir o que la memoria envíe al procesador el dato a leer.
Además de estos dos buses el procesador debe notificar a la memoria el tipo de operación. La figura 3.3 muestra de forma
esquemática cómo están conectadas estas señales.
Figura 3.3: Señales que conectan el procesador y la memoria
Dado el número de bits del bus de direcciones se puede deducir el tamaño de la memoria. ¿Se puede cambiar el tamaño de la
memoria de un ordenador? A la vista de las conexiones que se muestran en la figura 3.3 esto no es factible. El bus de direcciones
es un conjunto de señales fijo y por tanto cambiar el tamaño de memoria significaría cambiar este número. Si un ordenador
duplica su memoria RAM necesita un bit adicional en su bus de direcciones.
Los buses se implementan como pistas de metal en un circuito impreso y sus extremos se conectan a los puertos de entrada del
procesador y la memoria, por lo que añadir un bit más al bus es una operación extremadamente compleja. Sin embargo, en los
equipos actuales sí se ofrece la posibilidad de aumentar la memoria disponible mediante la inserción de circuitos adicionales.
Esto es posible porque el bus de direcciones tiene más bits de los que son necesarios y además, el procesador comprueba que las
direcciones de memoria utilizadas están dentro del rango correcto.
En general, en un procesador, el número de bits de los que consta el bus de direcciones es un parámetro fundamental de su
arquitectura y no puede ser modificado. Por ejemplo, en la arquitectura IA-32, el bus de direcciones es de 32 bits, con lo que se
pueden direccionar hasta un máximo de 4 gigabytes de memoria. En realidad, el procesador puede trabajar con un subconjunto
de las direcciones posibles, es lo que se denomina ‘memoria real’ del ordenador frente a la ‘memoria posible’ que representa la
Programación en ensamblador de la
arquitectura IA-32
56 / 198
memoria máxima que permite direccionar la anchura del bus de direcciones. La figura 3.4 ilustra esta situación para el caso en el
que un procesador de tipo IA-32 dispone de una memoria real de 256 megabytes.
Figura 3.4: Memoria real y posible en un procesador con arquitectura IA-32
El procesador incluye un mecanismo por el que el límite de la memoria real es un dato conocido. Antes de realizar cualquier
operación sobre memoria se comprueba que la dirección está contenida entre ciertos límites. En caso de que así sea, la operación
se realiza, y en caso contrario el procesador detiene el acceso y se produce una excepción en la ejecución.
En el caso concreto de la arquitectura IA-32, el bus de direcciones de 32 bits limita la memoria máxima que puede direccionar a
4 gigabytes. Dada la progresión que ha tenido el precio de la memoria, ordenadores personales que tengan memoria mayor de 4
gigabytes serán pronto una realidad. Un cambio en el bus de direcciones quiere decir una reorganización de la arquitectura entera
del procesador, y este ha sido el caso de la IA-32. La siguiente generación de procesadores ofrece un bus de direcciones y de
datos de 64 bits, por tanto con capacidad para direccionar un máximo de 16 exabytes (264 bytes).
3.4.
Almacenamiento de datos
La única estructura que ofrece la memoria es la organización de sus elementos en bytes. Por tanto, para almacenar los datos
que manipula un procesador es imprescindible saber de antemano su tamaño. El tamaño de algunos datos básicos viene definido
por la arquitectura del propio procesador. Por ejemplo, el lenguaje máquina de la arquitectura IA-32 contiene instrucciones
máquina para operar enteros de 32 bits. Esto no quiere decir que el procesador no pueda manejar enteros de otros tamaños, sino
que el procesador manipula estos de forma mucho más rápida y eficiente. Números de otros tamaños pueden ser manipulados
igualmente pero con un coste mayor en tiempo de ejecución.
Los lenguajes de programación de alto nivel como Java definen un conjunto de datos denominados ‘básicos’ y un conjunto
de mecanismos para definir datos complejos en base a ellos. Como los programas escritos en estos lenguajes deben ejecutar
en diferentes equipos con diferentes procesadores, es difícil definir el tamaño de los datos tal que se ajuste a todos ellos. El
compilador se encarga de transformar las operaciones escritas en lenguaje de alto nivel en las instrucciones más adecuadas para
Programación en ensamblador de la
arquitectura IA-32
57 / 198
manipular los datos en el procesador pertinente. La Tabla 3.2 muestra los tipos de datos básicos definidos en Java así como su
tamaño.
boolean
byte
char
short
Tipo
Contiene
true, false
Entero
Caracter Unicode
Entero
1 bit
8 bits
16 bits
16 bits
Tamaño
int
Entero
32 bits
long
Entero
64 bits
float
IEEE-754 Coma Flotante
32 bits
double
IEEE-754 Coma Flotante
64 bits
Rango
[-128, 127]
[0, 65535]
[-32768, 32767]
[-2147483648,
2147483647]
[-9223372036854775808,
9223372036854775807]
[±1.4012985E-45,
±3.4028235E+38]
[±4.94065645841246544E324,
±1.7976931348623157E+308]
Tabla 3.2: Tipos de datos básicos en el lenguaje Java
La regla para almacenar datos en memoria es utilizar tantos bytes como sean necesarios a partir de una dirección de memoria. En
adelante, la posición de memoria a partir de la cual está almacenado un dato se denominará su dirección de memoria. De forma
análoga, cuando se dice que un dato está en una posición de memoria lo que significa es que está almacenado en esa posición y
las siguientes que se precisen.
3.4.1.
Almacenamiento de booleanos
Los valores booleanos, a pesar de ser los más sencillos, no son los más fáciles de almacenar. La memoria permite el acceso a
grupos de 8 bits (1 byte) por lo que almacenar un único bit significa utilizar una parte que no es directamente accesible sino
que requiere procesado adicional. Por este motivo se intenta almacenar varios booleanos juntos y de esta forma maximizar la
información contenida en un byte. Esta estrategia se utiliza cuando es fácil saber la posición de un booleano dentro del byte. En
el caso de que esto no sea posible, se utiliza un byte para almacenar un único bit, con lo que los 7 bits restantes se desperdician.
La figura 3.5 muestra estas dos posibles situaciones.
Figura 3.5: Almacenamiento de booleanos
Si un conjunto de 8 booleanos se agrupan para ocupar un byte por entero, para acceder a un valor concreto se precisan instrucciones especiales contenidas en prácticamente todos los lenguajes máquina de los procesadores y suelen estar basadas en
instrucciones lógicas tales como la conjunción o la disyunción. En el caso de la arquitectura IA-32, mediante operaciones como
and u or, la utilización de máscaras y la consulta de los bits de estado se pueden manipular los booleanos en un byte.
Programación en ensamblador de la
arquitectura IA-32
3.4.2.
58 / 198
Almacenamiento de caracteres
Tal y como se ha visto en el capítulo 2, la codificación ASCII utiliza 8 bits para representar caracteres. La forma de almacenar
estos datos en memoria es simplemente utilizando un elemento o byte para cada letra. La figura 3.6 muestra cómo se almacenan
en memoria un conjunto de letras representadas por su valor en ASCII.
Figura 3.6: Almacenamiento de un string
Todo símbolo tiene su correspondiente código, incluido el espacio en blanco (0x20). Si la codificación utilizada fuese Unicode
UTF-16, cada símbolo ocupa dos posiciones consecutivas de memoria en lugar de una.
3.4.3.
Almacenamiento de enteros y naturales
Para almacenar un número entero o natural en memoria es imprescindible saber su tamaño en bytes. Las representaciones más
utilizadas incluyen tamaños de 2, 4, 8 o hasta 16 bytes. Siguiendo la regla genérica de almacenamiento, se utilizan tantos bytes
consecutivos a partir de una posición dada como sean precisos. El tamaño de esta representación no sólo influye en el lugar
que ocupan en memoria sino también en el diseño de las partes del procesador que realizan las operaciones. Por ejemplo, si los
enteros se representan con 32 bits, el procesador suele incluir una unidad aritmético lógica con operandos de 32 bits.
Pero en esta representación es esencial saber en qué orden se almacenan estos bytes. Dado un entero que ocupa n bytes a partir
de la posición p de memoria, se pueden almacenar estos bytes comenzando por el byte menos significativo del número o por
el más significativo. Estas dos posibilidades son igualmente factibles. Considérese el ejemplo de un procesador que manipula
números enteros de 32 bits. La representación del entero 70960543 en complemento a 2 es 0x043AC59F y se almacena a partir
de la posición de memoria 0x00001000. La figura 3.7 muestra las dos posibles formas de almacenamiento dependiendo de si
se seleccionan los bytes de menor a mayor significación o al contrario.
Programación en ensamblador de la
arquitectura IA-32
59 / 198
Figura 3.7: Almacenamiento de enteros en memoria
A estas dos formas de almacenar números enteros o naturales de más de un byte en tamaño se les conoce con el nombre de ‘little
endian’ y ‘big endian’. El primero almacena los bytes de menor a mayor significación, mientras el segundo almacena primero el
byte de mayor significación.
Cada procesador utiliza un único método de almacenamiento para todos sus enteros o naturales, y en la actualidad coexisten
procesadores que utilizan little endian con otros que utilizan big endian.
El problema de la existencia de ambas políticas de almacenamiento surge cuando dos procesadores intercambian números. Como
una secuencia de bytes es interpretada de forma diferente por los dos procesadores, se debe realizar un proceso de traducción
por el que se garantiza que ambos manipulan los mismos datos. La figura 3.8 muestra cómo la interpretación de un número de 4
bytes con ambas formas ofrece resultados diferentes.
Figura 3.8: Interpretación de bytes en little endian y big endian
Programación en ensamblador de la
arquitectura IA-32
60 / 198
Existen numerosos argumentos a favor y en contra de ambas notaciones pero ninguno de ellos es concluyente. Quizás el más
intuitivo a favor de la notación little endian es que si un número se almacena siguiendo este esquema y su representación se
extiende en tamaño, únicamente es necesario utilizar más posiciones de memoria sin reorganizar los bytes. En cambio, en el caso
de big endian, la misma operación requiere almacenar los bytes en diferentes posiciones de memoria.
3.4.4.
Almacenamiento de instrucciones
El almacenamiento de instrucciones consiste simplemente en utilizar posiciones consecutivas de memoria para almacenar los
bytes de la codificación de cada una de ellas. Una secuencia de instrucciones, por tanto, requiere tantas posiciones de memoria
como la suma de los tamaños de cada una de las codificaciones.
Tal y como se ha descrito en la sección 2.8.2, existen dos tipos de lenguajes máquina. Los procesadores con formato fijo de
instrucción almacenan las instrucciones en memoria en porciones idénticas. En este caso, dada una porción de memoria que
contiene una secuencia de instrucciones es muy fácil acceder a una de ellas de forma arbitraria, pues todas ocupan lo mismo. El
caso de instrucciones de formato variable es ligeramente más complejo. Dada una porción de memoria, para saber qué posiciones
ocupa cada instrucción es preciso interpretar la información que éstas codifican. Esto es precisamente lo que hace el procesador
al comienzo de la ejecución de cada instrucción, solicita de memoria tantos bytes como sean necesarios para obtener toda la
información referente a la instrucción. Una vez concluida esta fase, la siguiente instrucción comienza en la posición contigua de
memoria. La figura 3.9 muestra un ejemplo de cómo se almacenan los dos posibles formatos de instrucción.
Figura 3.9: Almacenamiento de instrucciones en formato fijo y variable
Programación en ensamblador de la
arquitectura IA-32
61 / 198
En el almacenamiento de instrucciones no es preciso distinguir entre los estilos big endian o little endian pues en la codificación
no existen bytes más significativos que otros. El convenio que se utiliza es que se escriben los bytes de la instrucción en el mismo
orden en el que están almacenados en memoria.
3.4.5.
Tamaño de datos en operaciones de lectura y escritura
La memoria almacena un byte en cada una de sus posiciones que a su vez tiene una dirección única. El funcionamiento de la
memoria está totalmente definido mediante esta estructura. Sin embargo, cuando la memoria forma parte del conjunto de un
ordenador, el tiempo que tarda en realizar una operación es mucho mayor comparado con el que tarda el procesador en ejecutar
una instrucción. En otras palabras, los accesos a memoria requieren tanto tiempo que retrasan la ejecución de las instrucciones
del procesador.
Existen múltiples decisiones de diseño en la arquitectura de un procesador que se utilizan para paliar este retraso. De entre ellas,
una de las más efectivas es realizar las operaciones en memoria en paquetes de información mayores de un byte. Es decir, cuando
el procesador lee y escribe en memoria, en lugar de trabajar con un único byte, los datos están compuestos por más de un byte en
posiciones consecutivas. Esta técnica tiene la ventaja de que un único acceso a memoria para, por ejemplo, lectura, proporciona
más de un byte en posiciones consecutivas. El inconveniente es que es posible que en ciertas ocasiones, se obtenga de memoria
más información de la estrictamente necesaria.
Generalmente, todo procesador ofrece la posibilidad de escribir un cierto tamaño de datos en bytes (mayor que uno) en una única
operación de memoria. La forma en que se implementa este mecanismo es utilizando múltiples módulos de memoria. Por ejemplo, supóngase que se quiere manipular la memoria tal que las operaciones se hagan en grupos de cuatro bytes simultáneamente.
El ejemplo que se describe a continuación se puede realizar con cualquier agrupamiento de información que sea potencia de dos.
La primera decisión es almacenar los datos en cuatro módulos o circuitos independientes de memoria de tal forma que la posición
0 de memoria se almacena en el primer módulo, la posición 1 en el segundo, y así sucesivamente. La quinta posición de memoria
se almacena de nuevo en el primer módulo.
Con este esquema, el módulo en el que está almacenado el dato de la posición p se obtiene mediante la expresión p % 4. La
consecuencia de este patrón de almacenamiento es que se puede acceder a cuatro bytes de memoria en el tiempo en el que se lee
un byte. Dada una dirección de memoria, cada módulo devuelve un byte y se obtienen los cuatro en el tiempo de retardo de un
único módulo pues todos trabajan en paralelo. Por tanto, dada una dirección de memoria d, con esta técnica, la memoria es capaz
de devolver los datos desde la posición d / 4 (donde esta división es división entera) a la posición d / 4 + 3 en el tiempo de retardo
de un único módulo.
Otra interpretación de esta organización es que la memoria contiene grupos de 4 bytes y cada uno de ellos está almacenado en la
posición d / 4. Pero, dada la dirección d, ¿como se obtiene el número d / 4?. La dirección de memoria está codificada en base 2,
y como esta operación es una división por una potencia de la base, equivale a tomar la dirección ignorando los dos bits de menos
peso, pues 4 = 22 . En realidad, dada la dirección d el cociente de la división entera entre cuatro es el número de grupo mientras
que el resto de esta división representa el byte del grupo de 4 al que se refiere d. La figura 3.10 muestra cómo implementar este
esquema de acceso en una memoria con direcciones de 32 bits.
Programación en ensamblador de la
arquitectura IA-32
62 / 198
Figura 3.10: Acceso a memoria en grupos de 4 bytes
Cada módulo de memoria recibe 30 de los 32 bits de la dirección. Esto es así porque la memoria consta de exactamente 230
grupos de cuatro bytes y cada módulo de memoria provee un byte de cada grupo. Con esta configuración se obtienen cuatro bytes
en el tiempo en el que un módulo lee uno de sus bytes, pues los cuatro acceden a su dato respectivo de forma paralela. Además
de los componentes que se muestran en la figura 3.10, la nueva memoria contiene la lógica necesaria para igualmente permitir la
lectura y escritura de un único byte en lugar de cuatro.
Los accesos a esta memoria a direcciones que son múltiplos de 4 se denominan accesos alineados. Pero, ¿qué sucede si el
procesador quiere acceder a 4 bytes consecutivos de esta memoria pero que no comienzan en una posición múltiplo de 4? El
paralelismo se obtiene porque si cada módulo lee la misma dirección de memoria y ofrece su correspondiente byte, pero si el
procesador requiere cuatro bytes que no están en el mismo grupo, este esquema no funciona puesto que no todos los módulos
deben leer de la misma dirección. A este tipo de accesos se le denominan accesos no alineados. En tal caso, la memoria se
ocupa internamente de realizar cuantos accesos sean necesarios para devolver los cuatro bytes que requiere el procesador. No se
precisan más de dos accesos a memoria para servir cualquier petición de cuatro bytes consecutivos del procesador. Por ejemplo,
si el procesador requiere los datos en las posiciones 4 * d + 3 a 4 * d + 6, el procesador selecciona el último byte del grupo con
dirección 4 * d y los tres primeros del grupo con dirección 4 * (d + 1). La figura 3.11 muestra los dos accesos a memoria para
obtener los datos requeridos.
Programación en ensamblador de la
arquitectura IA-32
63 / 198
Figura 3.11: Acceso doble para obtener 4 bytes consecutivos
En el caso concreto de la arquitectura IA-32, se define un bus de direcciones y un bus de datos ambos de tamaño 32 bits. El
procesador puede leer o escribir 4 bytes de datos en memoria de forma simultánea.
3.5.
Almacenamiento de tablas
En la sección 3.4 se ha visto cómo los tipos de datos básicos se almacenan en memoria, pero los programas manipulan estructuras
de datos más complejas compuestas a su vez por datos básicos. Un ejemplo de estas estructuras son las tablas o arrays. Una tabla
es un conjunto de datos agrupados de forma que cada uno de ellos puede ser accedido a través de un índice que se corresponde
con un número natural. El primer elemento está en la posición con índice cero y el último en la posición con índice igual al
número de elementos de la tabla menos uno. En los lenguajes de programación tales como C o Java, si una tabla de elementos se
denomina tabla, el elemento en la posición i se accede mediante la expresión tabla[i]. ¿Cómo se almacenan estos datos
en memoria de forma que puedan ser accedidos por el procesador?
Al igual que en el caso de los datos básicos, la estrategia consiste en utilizar posiciones consecutivas de memoria para almacenar
los elementos. Si una tabla contiene n elementos y cada uno de ellos se codifica con m bytes, el espacio total ocupado por la
tabla es de n * m bytes. Dada la dirección de memoria d a partir de la cual se almacena la tabla y el tamaño m en bytes de
cada elemento la dirección donde está almacenado el elemento en la posición p se obtiene sumando a d los bytes que ocupan los
elementos anteriores, o lo que es lo mismo d + (p * m). La figura 3.12 ilustra cómo se realiza este cálculo.
Programación en ensamblador de la
arquitectura IA-32
64 / 198
Figura 3.12: Dirección de un elemento de una tabla
Considérese, por ejemplo, una tabla de 4 enteros almacenada en la memoria de un procesador con arquitectura IA-32 a partir
de la posición 0x100 y con los números 0x34AF89C4, 0x583B7AF1, 0x97FA7C7E, 0x14C8B9A0 almacenados en ese
mismo orden. La figura 3.13 muestra su disposición en memoria.
Figura 3.13: Ejemplo de almacenamiento de una tabla de enteros de 32 bits en memoria
Pero para manipular tablas de datos no sólo basta con almacenar los elementos en posiciones consecutivas. Considérese el
siguiente ejemplo. Se dispone de una tabla de enteros y se debe calcular la suma total de sus elementos. Para ello se comienza
sumando el primer elemento, a él se le suma el segundo, a este resultado el tercero, y así sucesivamente. Pero ¿cómo se sabe que
se ha llegado al último elemento? Para cualquier tabla, además de la dirección de comienzo y el tamaño de sus elementos, es
preciso saber el número de elementos que contiene.
Existen dos mecanismos para saber cuántos elementos contiene una tabla. El primero de ellos consiste en depositar como último
elemento, un valor que denote el final. Por ejemplo, considérese una tabla de letras que almacena una frase. Cada letra se almacena
con su codificación en ASCII (ver sección 2.8.1), por lo que cada letra ocupa un byte. Al final de la tabla se incluye un byte con
valor 0 que está reservado específicamente en ASCII para codificar el final de una secuencia de letras. Para recorrer todos los
elementos de esta tabla basta con escribir un bucle que se detenga cuando encuentre el valor cero.
Pero la técnica de depositar un valor concreto como último elemento no funciona para todos los tipos de datos. ¿Qué sucede
en el caso de una tabla de números enteros? Cada elemento se codifica con su representación en complemento a 2 que utiliza
la totalidad de posibles combinaciones de bits. Por tanto, no es posible utilizar un valor específico para denotar el final de la
tabla pues se confundiría con la representación de su número entero correspondiente. Para saber el tamaño, simplemente hay que
almacenar este valor en una posición adicional de memoria. De esta forma, si se desea acceder a todos los elementos de la tabla
Programación en ensamblador de la
arquitectura IA-32
65 / 198
de forma secuencial basta con escribir un bucle que compare la posición del elemento con el tamaño. Tanto esta técnica como la
anterior se utilizan de forma frecuente en los lenguajes de programación de alto nivel.
3.5.1.
Almacenamiento de tablas en Java
El lenguaje de programación Java garantiza que el acceso a los elementos de un array se realiza siempre con un índice correcto.
Dado que toda tabla en Java tiene su primer elemento en la posición con índice cero, el índice i con el que se accede a una tabla
de n elementos debe cumplir 0 ≤ i < n.
Pero esta comprobación sólo se puede realizar mientras un programa está en ejecución. Supóngase que un programa Java contiene
la expresión tabla[expresión]. ¿Cómo se puede garantizar que el acceso a la tabla es correcto? La solución consiste en
que antes de que el programa ejecute esta expresión se comprueba que su valor está en los límites correctos, en cuyo caso el
acceso se realiza sin problemas. Si el índice no está entre los límites permitidos el programa produce una excepción del tipo
ArrayIndexOutOfBounds.
Para implementar este mecanismo no sólo toda tabla en Java debe tener almacenado su tamaño sino que cada acceso va precedido
de la comprobación del valor del índice. Se necesita, por tanto, un mecanismo que almacene los datos de una tabla y su tamaño
de forma compacta y que además permita una eficiente comprobación de los accesos a sus elementos.
La solución en Java consiste en almacenar el tamaño de una tabla junto con sus elementos en posiciones consecutivas de memoria.
De entre todas las posibilidades de organizar estos datos, la más lógica es poner el tamaño en las primeras posiciones de memoria
seguido de los elementos. La figura 3.14 muestra cómo se almacena en memoria una tabla de seis enteros de 32 bits en formato
little endian a partir de la posición 0x100.
Figura 3.14: Almacenamiento de una tabla de seis enteros en Java
Antes de cada acceso al elemento i que ocupa t bytes de una tabla con s elementos almacenada a partir de la posición d, el
programa escrito en Java realiza las siguientes operaciones:
Obtiene el entero s almacenado a partir de la posición d.
Comprueba que 0 ≤ i. En caso de que no sea así produce una excepción.
Comprueba que i < s. En caso de que no sea así produce una excepción.
Calcula la dirección donde está el elemento i como d + 4 + (t * i).
3.6.
Almacenamiento de direcciones de memoria
Supongamos que la memoria utilizada tiene un tamaño de 4 Gigabytes y por tanto sus direcciones se representan con 32 bits. Las
direcciones de memoria son números naturales en el rango [0, 232 - 1]. Pero este número natural es susceptible de ser almacenado
Programación en ensamblador de la
arquitectura IA-32
66 / 198
él mismo en memoria. Es decir, se puede almacenar la representación binaria de una dirección de memoria en la propia memoria.
Al tener un tamaño de 32 bits o 4 bytes, se utilizan para ello cuatro posiciones de memoria consecutivas.
Una dirección de memoria, por tanto, se puede considerar de dos formas posibles: o como una dirección de una celda de memoria,
o como un número natural susceptible de ser manipulado como tal. Supóngase que en la posición de memoria 0x00000100
se encuentra almacenado el número entero de 32 bits 0x0153F2AB y que en la posición 0x00000200 se debe almacenar la
dirección de dicho número. Para ello se almacena, a partir de la posición 0x00000200 el número 0x00000100 utilizando los
cuatro bytes a partir de esa posición y se hace en orden creciente de significación al utilizar el esquema little endian. El resultado
se ilustra en la figura 3.15.
Figura 3.15: Dirección de memoria almacenada como número natural
Tras almacenar la dirección de memoria de un dato en la posición 0x00000200, ¿es posible obtener de nuevo el número 0x0153F2AB? La respuesta es afirmativa, pero no de forma inmediata, se debe obtener de memoria primero los cuatro bytes
almacenados en la posición 0x00000200 y utilizarlos como una dirección de memoria de donde obtener los cuatro bytes
contenidos en la posición 0x00000100. El acceso a este último dato se ha realizado de forma indirecta, es decir, mediante un
acceso previo a memoria para obtener la dirección del dato final. Utilizando la notación funcional de operaciones sobre memoria,
el acceso al dato se logra ejecutando Lectura(Lectura(0x00000200)).
A este mecanismo de acceso a un dato en memoria a través de su dirección a su vez almacenada en otra posición se le conoce
con el nombre de ‘indirección’. En el ejemplo anterior se dice que el dato almacenado en la posición 0x00000200 apunta al
dato 0x0153F2AB. La figura 3.16 ilustra esta situación.
Figura 3.16: Una posición de memoria ‘apunta a’ otra
El mecanismo de indirección se puede encadenar de manera arbitrariamente larga. La dirección que contiene la dirección de
Programación en ensamblador de la
arquitectura IA-32
67 / 198
un dato, a su vez se puede almacenar de nuevo en memoria. En tal caso, para acceder al dato final se requieren dos accesos a
memoria en lugar de uno. Por tanto, es posible almacenar las direcciones tal que haya que seguir una cadena de indirecciones
para en última instancia acceder al dato. La figura 3.17 muestra una distribución de datos tal que la posición 0x00000100
contiene ‘la dirección de memoria de la dirección de memoria de la dirección de memoria del dato’.
Figura 3.17: Indirección múltiple para acceder a un dato
De la técnica de indirección se deriva que en memoria no sólo se almacenan datos (naturales, enteros, coma flotante, letras, etc.)
sino también direcciones de memoria. Todos estos datos, a efectos de almacenamiento y su manipulación por el procesador, no
son más que una secuencia de bytes en diferentes celdas. El que una secuencia de bits determinada se interprete como un número
o como una dirección queda totalmente bajo el control del programador. En los programas escritos en ensamblador es preciso
saber qué dato está almacenado en qué posición de memoria pero el propio lenguaje no aporta mecanismo alguno que compruebe
que se el acceso se hace de forma correcta. Si por error en un programa se obtiene un dato de 32 bits de memoria y se interpreta
como una dirección cuando en realidad es un dato numérico o viceversa, lo más probable es que el programa termine de forma
brusca o con resultados incorrectos.
3.6.1.
Ejemplos de indirección
El almacenar una dirección en memoria no parece a primera vista un mecanismo útil, pues esta cumple un único papel que es el
de apuntar al dato en cuestión. Sin embargo, esta técnica se utiliza con frecuencia en la ejecución de programas.
Programación en ensamblador de la
arquitectura IA-32
68 / 198
Ejemplo 3.1 Almacenamiento de una tabla de strings
Supóngase que se dispone de un conjunto de n strings almacenados en otras tantas posiciones de memoria. Aunque las letras
de cada string están almacenadas en posiciones consecutivas, los strings no están uno a continuación de otro sino en zonas de
memoria dispersas. Se quiere imprimir estos strings en orden alfabético. El primer paso es ordenar los strings para a continuación
imprimir cada uno de ellos por orden.
Para ordenar los strings hay dos opciones, o se manipulan todos los caracteres de cada uno de ellos, o se manipulan sus direcciones
de comienzo. Es decir, en lugar de tener los strings ordenados alfabéticamente y almacenados en posiciones consecutivas de
memoria, se almacenan por orden las direcciones de memoria de comienzo de cada string y se ordenan en base a las letras que
contienen. Esta estructura se ilustra en la figura 3.18.
Figura 3.18: Tabla con direcciones de comienzo de strings
La ordenación los strings se puede realizar sin mover ninguna de las letras en memoria. La tabla resultante contiene en cada uno
de sus elementos una indirección a un string, es decir, la dirección en la que se encuentra el string pertinente. Para imprimir los
strings en orden alfabético se itera sobre los elementos de la tabla y mediante doble indirección se accede a las letras de cada
string.
Programación en ensamblador de la
arquitectura IA-32
69 / 198
Ejemplo 3.2 Referencias en el lenguaje Java
El lenguaje de programación Java utiliza el mecanismo de indirección para acceder a los datos almacenados en un objeto.
Supóngase que se ha definido una clase con nombre Dato que a su vez contiene un campo de acceso público, entero y con
nombre valor. Se ejecuta la siguiente porción de código.
Línea
1
2
3
4
5
6
Código
Dato obj1, obj2;
obj1 = new Dato();
obj1.valor = 3;
obj2 = obj1;
obj2.valor = 4;
System.out.println(obj1.valor)
¿Qué valor imprime por pantalla la última línea? El código asigna al campo valor de obj1 el valor 3, a continuación se
produce la asignación obj2 = obj1, luego se asigna el valor 4 al campo valor de obj2 y se imprime el mismo campo pero
de obj1. Al ejecutar este fragmento de código se imprime el valor 4 por pantalla. La línea que explica este comportamiento es
la asignación obj2 = obj1. En Java, todo objeto se manipula a través de una ‘referencia’. Las variables obj1 y obj2 son
referencias y la asignación obj1 = obj2 no transfiere el contenido entero de un objeto a otro, sino que se transfiere el valor de
la referencia. Por tanto, al ejecutar esta asignación, obj2 se refiere al mismo objeto que obj1 y por eso la última línea imprime
el valor 4.
El mecanismo interno que se utiliza a nivel de lenguaje máquina para representar las referencias está basado en el concepto de
indirección. Cuando se crea un objeto se almacenan sus datos en memoria. Cuando un objeto se asigna a una referencia esta pasa
a contener la dirección de memoria a partir de la cual está almacenado. La asignación obj2 = obj1 transfiere la dirección de
memoria contenida en obj1 al contenido de obj2. Cualquier modificación que se haga a través de la referencia obj1 afecta
por tanto al objeto al que apunta obj2 pues ambas referencias apuntan al mismo objeto. La figura 3.19 ilustra cómo se asignan
los valores en memoria para este ejemplo.
Figura 3.19: Dos referencias en Java que apuntan al mismo objeto
El objeto está ubicado en una posición arbitraria de memoria (en la figura 3.19 es la posición 0x00000100). En dos posiciones
de memoria adicionales se almacenan las referencias obj1 y obj2. La primera de ellas recibe su valor al ejecutarse el constructor
de la clase. La segunda recibe el mismo valor cuando se ejecuta la asignación. A partir de este momento, cualquier modificación
realizada en el objeto a través de obj1 será visible si se consulta a través de obj2.
Programación en ensamblador de la
arquitectura IA-32
70 / 198
Ejemplo 3.3 Doble indirección con referencias en el lenguaje Java
Las referencias en Java se utilizan, por tanto, como indirecciones a memoria. Pero las clases definidas en Java pueden contener en
su interior campos que sean referencias a otros objetos. Por ejemplo, si se define una segunda clase Dato2 en cuyo interior existe
un campo con nombre c1 de la clase Dato, este campo es una referencia a un objeto. Supóngase que se ejecuta la siguiente
porción de código.
Línea
1
2
3
4
Código
Dato2 obj2;
obj2 = new Dato2();
obj2.c1 = new Dato();
obj2.c1.valor = 4;
En este caso la referencia obj2 apunta a un objeto de la clase Dato2 que a su vez contiene en su interior una referencia a un
objeto de la clase Dato. Para ejecutar la última línea en la que se asigna el valor 4 al campo valor es preciso realizar una doble
indirección. La referencia obj2 contiene la dirección del objeto de la clase Dato2, y este a su vez contiene en su interior una
referencia que contiene la dirección del objeto de la clase Dato. Tras esta doble indirección se asigna el valor 4 a dicho dato. La
figura 3.20 muestra el acceso a este dato a través de la doble indirección.
Figura 3.20: Acceso a un dato mediante doble indirección
3.7.
Ejercicios
1. Diseñar una memoria tal que ofrezca al procesador la capacidad de acceder a ocho bytes consecutivos de memoria en el
tiempo en el que se lee un único byte. El diseño debe incluir el tamaño de los buses así como su estructura interna.
2. La memoria de un procesador ofrece acceso a cuatro bytes consecutivos de memoria mediante un único acceso siempre y
cuando estén almacenados a partir de una posición que es múltiplo de cuatro. En este procesador se ejecutan dos programas
con idénticas instrucciones que acceden a un array de un millón de enteros de tamaño 32 bits. El primer programa realiza
un total de un millón de accesos a la zona de memoria en la que está almacenado el array. El segundo programa realiza
exactamente el doble de accesos a memoria a la misma zona, ¿cómo es esto posible?
3. Supongamos un procesador que permite operaciones en memoria en bloques de 32 bits (4 bytes). Se ejecutan las siguientes
instrucciones:
Write(0x100, 0x01234567)
Write(0x101, 0x89ABCDEF)
Write(0x102, 0xFFFFFFFF)
Read(0x100)
Programación en ensamblador de la
arquitectura IA-32
71 / 198
¿Cuál es el resultado de la última operación?
4. Una tabla en Java almacena referencias a objetos. Estas referencias ocupan 32 bits cada una. La tabla contiene 23 elementos
y está almacenada a partir de la posición 300 de memoria. ¿En qué dirección está almacenada la referencia del elemento
en tabla[21]? ¿Y la referencia del elemento tabla[23]?
5. ¿Cuántas posiciones de memoria reserva la ejecución de la expresión Java int[] tabla = new int[49]?
6. Supongase un ordenador con memoria principal de 16 Kilobytes (1 Kilobyte = 1024 bytes). Explicar cuantos bits de datos
y dirección se precisan y por qué en los siguientes supuestos:
a. La memoria lee y escribe la información en grupos de 8 bits:
b. La memoria lee y escribe la información en grupos de 32 bits:
Programación en ensamblador de la
arquitectura IA-32
72 / 198
Capítulo 4
Arquitectura IA-32
En este capítulo se estudian los componentes básicos de la arquitectura IA-32: registros, palabra de estado, tamaño del bus, etc.
La arquitectura de un procesador consiste de los elementos internos que permiten la ejecución de las instrucciones de su lenguaje
máquina. La complejidad de un procesador actual es demasiado grande para poder ser estudiada en su totalidad. A modo de
referencia, los modelos del procesador Pentium producidos a comienzos del año 2004 contienen alrededor de 50 millones de
transistores (en diciembre de 1974, el procesador 8080 producido por la misma empresa tenía 6000 transistores). Este tipo de
circuitos se diseñan a lo largo de varios años y por grandes equipos de diseñadores. La figura 4.1 muestra a la izquierda la imagen
del Pentium 4 del año 2001 así como su tamaño1 . En la parte derecha se muestra el mismo chip comparado con una moneda de
un céntimo de euro (16.25 mm de diámetro).
1 Scott Thompson y otros. 130nm Logic Technology Featuring 60nm Transistors, Low-K Dielectrics, and Cu Interconnects. Intel Technology Journal, volumen
6, número 2, Mayo 2002.
Programación en ensamblador de la
arquitectura IA-32
73 / 198
Figura 4.1: Pentium 4 Northwood (Dic. 2001). Fuente: Intel Technology Journal, vol. 6, núm. 2.
La complejidad de diseño de estos circuitos viene acompañada por una tecnología de fabricación que permite un empaquetado
en dispositivos de tamaño extremadamente reducido. La figura 4.2 muestra el aspecto de un Pentium 4 ya empaquetado y listo
para ser instalado en una placa de circuito impreso.
Programación en ensamblador de la
arquitectura IA-32
74 / 198
Figura 4.2: Chip con un procesador Pentium 4 en su interior
Se explican a continuación, los elementos básicos de la arquitectura IA-32 para entender el funcionamiento y la manipulación de
datos sin analizarlo en su totalidad. Se aplica, por tanto, un nivel de abstracción a la arquitectura real y se estudian sólo aquellos
componentes necesarios para la comprensión de las instrucciones que permiten la ejecución de operaciones básicas tales como
cálculo aritmético, implementación de condicionales, llamadas a subrutinas, paso de parámetros, gestión de la pila, etc.
Para el desarrollo de aplicaciones avanzadas sobre un procesador de estas características sí es preciso tener un conocimiento
más profundo de la arquitectura. En tal caso, los documentos en los que se encuentra el nivel de detalle necesario para esta
tarea los proveen los mismos fabricantes. En lugar de explicar en detalle un procesador en concreto, generalmente las empresas
fabricantes de procesadores crean una arquitectura concreta y luego fabrican múltiples chips todos ellos compatibles con esa
arquitectura pero con diferentes prestaciones. En el caso concreto de la arquitectura IA-32, la empresa fabricante ofrece tres
documentos de información agrupados bajo el nombre de IA-32 Intel Architecture Software Developer’s Manual que contienen
todos los aspectos del funcionamiento de la arquitectura denominada IA-32. El primer volumen ofrece una visión global de la
arquitectura, el formato de instrucciones, el entorno de ejecución del procesador, los tipos de datos, las técnicas utilizadas para
las llamadas a procedimientos y las extensiones especiales incluidas en esta arquitectura. El segundo volumen describe todas y
cada una de las instrucciones máquina contenidas en la arquitectura. El tercer volumen contiene la información necesaria para
utilizar el procesador en un sistema completo, como por ejemplo, los bits de control y estado, gestión de memoria, esquema de
protección, manejo de excepciones e interrupciones, gestión de múltiples procesadores, memoria cache, etc.
4.1.
El entorno de ejecución de la arquitectura IA-32
En esta sección se describe el entorno de ejecución del procesador tal y como se ve desde un programa escrito en lenguaje
ensamblador. Este entorno consta, además de otros componentes, de un conjunto de registros, un espacio de direcciones, un
registro de condiciones y estado, y el registro contador de programa.
En adelante las diferentes unidades de información que es capaz de manipular el procesador se denominarán utilizando los
términos que se muestran en la Tabla 4.1. Todos ellos son utilizadas por algún componente del procesador y sus tamaños son
todos múltiplos de bytes.
La figura 4.3 muestra los tamaños relativos de estos datos así como la numeración seguida para referirse a los bytes de los que
están compuestos. Nótese que los bits se comienzan a numerar por el cero el menos significativo.
Programación en ensamblador de la
arquitectura IA-32
75 / 198
Denominación
Byte
Word
Doubleword
Quadword
Double Quadword
Tamaño
8 bits
16 bits, 2 bytes
32 bits, 4 bytes
64 bits, 8 bytes
128 bits, 16 bytes
Tabla 4.1: Nomenclatura para los tamaños de información
Figura 4.3: Tipos de datos del procesador
4.1.1.
Espacio de direcciones
La arquitectura IA-32 permite gestionar el acceso a memoria de dos formas posibles denominadas modelo lineal y modelo
segmentado.
En el modelo lineal la memoria aparece como un único espacio contiguo de tamaño máximo 232 bytes o 4 gigabytes. En él se
almacenan todos los datos, código y demás información necesaria para la ejecución de los programas. Las direcciones en este
modelo tienen un tamaño fijo de 32 bits.
El modelo segmentado es más complejo. El espacio de direcciones se organiza como un grupo de espacios de direcciones
independientes denominados segmentos. La razón por la que se propone esta técnica es para separar código, datos e información
adicional de los programas en diferentes segmentos. La dirección para acceder a un byte en este modelo consta de dos partes,
un identificador de segmento y un desplazamiento dentro de ese segmento. El procesador puede utilizar hasta un total de 16.383
segmentos y cada uno de ellos de un tamaño máximo de 4 gigabytes. La ventaja de gestionar la memoria de esta forma es el
incremento en la seguridad en la ejecución de programas. Mediante la colocación de código y datos en segmentos separados se
puede forzar una política de acceso a datos únicamente dentro del mismo segmento y así detectar fácilmente accesos a zonas de
memoria incorrectas.
En el resto de este documento se utilizará únicamente el modelo lineal de memoria. Toda dirección tiene un tamaño de 32 bits y
se dispone de un espacio de hasta 4 gigabytes de información almacenados de forma contigua.
Tal y como se ha descrito en la sección 3.4.5, para obtener un mejor rendimiento en el uso de memoria, el bus de datos que
conecta al procesador con la memoria tiene un tamaño de 32 bits. Esto quiere decir que el procesador es capaz de manipular 4
Programación en ensamblador de la
arquitectura IA-32
76 / 198
bytes de datos en una sola operación (lectura o escritura) siempre y cuando el acceso sea alineado, es decir, que los datos estén
almacenados a partir de una posición que es múltiplo de cuatro. La figura 4.4 ilustra este mecanismo. El procesador igualmente
es capaz de acceder tanto a tamaños de información más pequeños como a datos no alienados, pero dichas operaciones serán más
lentas.
Figura 4.4: Acceso alineado a memoria
4.1.2.
Registros de propósito general
Los registros son circuitos digitales internos del procesador que se comportan igual que las celdas de memoria, es decir, permiten
las operaciones de lectura y escritura de datos pero a una velocidad mucho mayor, pues no requieren la comunicación con ningún
circuito externo al procesador. Los registros que ofrece un procesador se identifican por su nombre y son susceptibles de ser
utilizados al escribir programas en ensamblador.
La arquitectura IA-32 ofrece 16 registros básicos para la ejecución de programas: 8 registros de propósito general, 6 registros de
segmento, el registro de estado y control, y el registro contador de programa. Los seis registros de segmento no se describen en
detalle puesto que se utilizan para acceder a memoria en el modelo segmentado que no se considera en este documento.
Los registros de propósito general son 8 con nombres %eax, %ebx, %ecx, %edx, %esi, %edi, %ebp y %esp. Todos ellos tienen
un tamaño de 32 bits y su principal cometido es almacenar datos temporales necesarios para la ejecución de programas. Mientras
la mayor parte de datos e instrucciones se almacenan en la memoria principal, en estos registros se guardan temporalmente
aquellos datos que necesita el procesador más a menudo, de esta forma se obtiene un mejor rendimiento en la ejecución. Por
ejemplo, si un dato se utiliza varias veces seguidas, en lugar de leerlo de memoria cada vez es mejor almacenarlo al principio en
un registro y referirse a esa copia cada vez que sea necesario.
El procesador permite referirse a ciertas porciones de los registros de propósito general con nombres diferentes. Así, se permite
manipular únicamente los 16 bits de menos peso de los ocho registros suprimiendo del nombre la letra ‘e’ del comienzo. Por
ejemplo, el registro %ax se refiere a los dos bytes de menos peso del registro %eax. Nótese que no es un registro adicional que
tenga el procesador, sino la posibilidad de utilizar la mitad menos significativa de un registro. Cuando se realiza una operación
sobre una porción de un registro, el resto de bits permanece intacto.
Para los primeros cuatro registros, esto es %eax, %ebx, %ecx y %edx se permite manipular los dos bytes de menos peso de
forma independiente. Los nombres se obtienen mediante la segunda letra del nombre original añadiendo el sufijo ‘h’ para el
de más peso o ‘l’ para el de menos peso. Por tanto, el registro %eax tiene un tamaño de 32 bits, sus 16 bits de menos peso se
manipulan mediante el nombre %ax, el byte de menos peso mediante el nombre %al y el segundo de menos peso con %ah. La
figura 4.5 muestra los ocho registros de propósito general así como los nombres para referirse a las diferentes porciones.
Programación en ensamblador de la
arquitectura IA-32
77 / 198
Figura 4.5: Registros de propósito general
4.1.3.
Registro de estado y control
Durante la ejecución de instrucciones existen situaciones especiales que convienen ser reflejadas en un registro para su posible
consulta. Por ejemplo, si el resultado de una operación aritmética ha producido acarreo, es probable que un programa tenga que
tomar medidas especiales. La forma de ofrecer este tipo de funcionalidad consiste en capturar estas condiciones en un registro
de estado. El número de bits y condiciones que se almacenan en este registro es diferente en cada arquitectura. Un ejemplo de
funcionalidad análoga a esta es el conjunto de luces e indicadores que tiene un equipo de música. Mediante esos indicadores
informan al usuario de algunas de las condiciones de funcionamiento internas (nivel de audio, filtros encendidos, etc). En el
contexto de un procesador es suficiente almacenar estos valores en un registro e incluir en su lenguaje máquina instrucciones
para su manipulación.
Pero aparte de las condiciones de funcionamiento, existe un conjunto de funcionalidades que es preciso activar o desactivar en
ciertos momentos de la ejecución de un procesador. Continuando con la analogía del equipo de música, este ofrece un conjunto
de interruptores o mandos para controlar ciertos aspectos de funcionamiento del dispositivo. Un procesador ofrece también esta
posibilidad a través de los denominados bits de control y que suelen almacenarse también en el registro de estado y control. Por
ejemplo, la arquitectura IA-32 permite que una instrucción sea interrumpida y se pase a ejecutar momentáneamente un conjunto
de instrucciones. Mediante un bit de control se permite o prohibe que estas interrupciones se produzcan.
El registro de estado y control de la arquitectura IA-32 se denomina ‘Eflags’ y consta de 32 bits. La figura 4.6 muestra su
estructura, en la que se comprueba que de los 32 bits tan sólo 18 de ellos contienen información sobre el estado y control, el resto
contienen un valor fijo.
Programación en ensamblador de la
arquitectura IA-32
78 / 198
Figura 4.6: Registro de estado y control
Las condiciones que representan los bits más importantes de este registro son:
Bit de acarreo (CF): Su valor es 1 si una operación aritmética con naturales ha producido acarreo. Este bit se utiliza, por tanto
para detectar situaciones de desbordamiento.
Bit de paridad (PF): Su valor es 1 si el byte menos significativo de una operación aritmética contiene un número impar de
unos.
Bit de ajuste (AF): Su valor es 1 si se produce acarreo en operaciones aritméticas en la codificación BCD.
Bit de cero (ZF): Su valor es 1 si el resultado de la última operación aritmética ha sido cero.
Bit de signo (SF): Su valor es idéntico al bit más significativo del resultado que corresponde con el bit de signo, cero si es
positivo y 1 si es negativo.
Bit de desbordamiento (OF): Su valor es 1 si el entero obtenido como resultado no puede ser representado en complemento a
2 con el número de bits utilizado.
Si se pudiese ver la evolución de los valores de bits de estado durante la ejecución de un programa se podría comprobar cómo
sus valores fluctúan continuamente dependiendo de los resultados aritméticos producidos. El valor de estos bits se mantiene en
el registro eflags mientras no se realice otra operación aritmética. El valor de estos bits modifican el comportamiento de un
subconjunto muy relevante de instrucciones del procesador, entre ellas los saltos condicionales.
4.1.4.
El registro contador de programa
Desde el instante en que un procesador comienza a funcionar, esto es, cuando el circuito recibe el voltaje necesario, hasta que este
voltaje desaparece, su actividad consiste en ejecutar las instrucciones máquina almacenadas en memoria. El procesador obtiene
una instrucción de memoria, la interpreta, ejecuta y al terminar repite el proceso con la siguiente instrucción.
Programación en ensamblador de la
arquitectura IA-32
79 / 198
Como consecuencia, en todo momento se debe saber dónde está almacenada la siguiente instrucción a ejecutar. Es decir, mientras
en el interior del procesador se interpreta la instrucción recibida, se debe almacenar la dirección de memoria a la que hay
que acceder para ejecutar la siguiente instrucción. En la arquitectura IA-32, en el modelo lineal de memoria, esa dirección de
memoria consta de 32 bits y se almacena en el registro con nombre %eip (extended instruction pointer). Si la instrucción que
está ejecutando no indica lo contrario, el procesador continua con la instrucción que está almacenada en las siguientes posiciones
de memoria. Algunas instrucciones, como por ejemplo las de salto, modifican el contenido de este registro, y por tanto modifican
la secuencia de ejecución.
Todo procesador dispone de un registro de estas características y que se conoce generalmente como el ‘contador de programa’ o
PC. En el caso de la arquitectura IA-32 , no es posible acceder a %eip de forma explícita, o sea que no se puede leer ni escribir
directamente un valor. En cambio, sí se puede modificar de forma implícita mediante instrucciones como por ejemplo las de salto
o las de llamadas a subrutina.
La forma que tiene el procesador de cargar la siguiente instrucción a ejecutar consiste en sacar el contenido del registro %eip al
bus de direcciones de memoria y efectuar una operación de lectura tal y como ilustra la figura 4.7. Cuando dicha operación ha
terminado, el procesador obtiene el conjunto de bits que codifican la siguiente instrucción a ejecutar.
Figura 4.7: Contador de programa
4.1.5.
Otros registros de la arquitectura IA-32
Aparte de los descritos anteriormente, el procesador dispone de registros adicionales para efectuar operaciones especializadas,
que aunque no se estudian en detalle, son muy importantes para obtener el mayor rendimiento posible en la ejecución de programas. La arquitectura los agrupa de la siguiente forma:
Ocho registros de 80 bits para almacenar números reales codificados en coma flotante. Por contra, el grupo de registros
descritos anteriormente se utiliza para operar con números naturales, enteros y caracteres.
Tres registros de 16 bits que almacenan bits de estado, control y etiquetado de números en coma flotante. Se utilizan para
codificar condiciones especiales de sus operaciones.
Un registro de 11 bits que contiene el código de operación de la última instrucción con operandos en coma flotante.
Dos registros de 48 bits con la dirección de memoria de la última instrucción con operandos en coma flotante y la dirección
del último operando en coma flotante obtenido de memoria.
Programación en ensamblador de la
arquitectura IA-32
80 / 198
Ocho registros de 64 bits para la ejecución de instrucciones del tipo MMX. Estas 57 instrucciones están orientadas a la
ejecución eficiente de aplicaciones multimedia y procesado de señal de audio y vídeo.
Ocho registros de 128 bits para la ejecución de instrucciones de tipo SIMD (Single Instruction Multiple Data).
Tanto las instrucciones de tipo MMX como las de tipo SIMD persiguen una finalidad similar. Tras analizar el tipo de programas
que ejecutan estos procesadores, se han identificado ciertas instrucciones que aparecen en aplicaciones de procesado de vídeo en
las que es preciso realizar una única instrucción sobre un conjunto muy grande de datos. Por ejemplo, supóngase que se debe
sumar una constante a toda una tabla de números. En lugar de ejecutar esta operación con las instrucciones convencionales, es
decir, realizar la suma elemento a elemento, el procesador ofrece la posibilidad de ejecutar esta instrucción sobre todos los datos a
la vez. De esta posibilidad se deriva su nombre (SIMD, única instrucción, múltiples datos). Dado que el tamaño de los operandos
es mayor que el de las instrucciones convencionales, se requiere un banco de registros especial para ellas.
4.1.6.
Estado visible de un programa
De toda la arquitectura IA-32, en adelante se considerará únicamente la parte encargada de ejecutar instrucciones con enteros,
naturales y caracteres. No se estudiarán ni las instrucciones ni la arquitectura para manipular números en coma flotante ni las
extensiones MMX y SIMD.
Una vez restringido el ámbito de estudio a este subconjunto, los datos que utiliza un procesador para ejecutar las instrucciones
máquina están almacenados en un conjunto de dispositivos concretos. Se define como el ‘estado visible de un programa’ al
conjunto de datos imprescindibles para su ejecución. La forma de decidir qué datos forman parte de este estado es si se considera
la situación en la que un programa en ejecución se detiene y se transfiere a otro procesador. ¿Qué datos deben transferirse para
que la ejecución en este nuevo procesador continúe exactamente igual a como procedería en el procesador origen? El estado está
contenido en los siguientes elementos:
La memoria RAM. Es el lugar en el que están almacenados los datos y el código de un programa por lo que su ejecución
depende de ella.
Los registros de propósito general. En cualquier instante de la ejecución de un programa, estos registros contienen datos
temporales que son resultados parciales u operandos a utilizar en el futuro. Por esta razón, estas ocho palabras de 4 bytes cada
una forman parte del estado visible.
Los bits de estado contenidos en el registro eflags puesto que la ejecución de ciertas instrucciones varía dependiendo de
estos valores.
El contador de programa. Indica qué instrucción está ejecutando el procesador, y por tanto es parte imprescindible de este
estado.
4.2.
Ciclo de ejecución de una instrucción
Se define como el ciclo de ejecución de un procesador a los pasos internos que sigue para ejecutar una instrucción. El número
de pasos y duración de este ciclo varían de procesador a procesador y depende totalmente de su arquitectura. La mayor parte
de las técnicas utilizadas para obtener un mayor rendimiento en la ejecución de instrucciones están orientadas a modificar la
arquitectura para obtener un ciclo de ejecución más rápido.
La complejidad del ciclo de ejecución depende de la arquitectura. La arquitectura IA-32 tiene múltiples ciclos de ejecución
posible dependiendo del tipo de instrucción a ejecutar. A modo de simplificación se estudia el más representativo de ellos que
utilizan las operaciones que manipulan datos enteros. El ciclo de ejecución de estas instrucciones consta de cinco etapas: fetch
(F), decodificación inicial (D1), decodificación final (D2), ejecución (E) y escritura de resultados (W). La figura 4.8 muestra la
secuencia de fases en la ejecución de varias instrucciones.
Programación en ensamblador de la
arquitectura IA-32
81 / 198
Figura 4.8: Ciclos de ejecución de varias instrucciones
A continuación se describen las tareas que se realizan en cada una de estas fases.
4.2.1.
Fase de fetch
En esta fase el procesador obtiene la siguiente instrucción a ejecutar de memoria. Para ello se carga el contenido del registro
contador de programa %eip en el bus de direcciones y se realiza una operación de lectura. El procesador recibe los primeros
bytes de la instrucción y los almacena en el registro de instrucciones (IR) para proceder a su decodificación. Al mismo tiempo
que se obtienen los primeros bytes de la instrucción se calcula el siguiente valor para el contador de programa. Este valor todavía
no se almacena en %eip puesto que la longitud de la instrucción no se sabe con exactitud hasta que se termina la fase de
decodificación. La figura 4.9 muestra una versión simplificada de los componentes internos del procesador que participan en esta
fase.
Figura 4.9: Fase de fetch
4.2.2.
Fase de decodificación inicial
El proceso de decodificación de una instrucción está dividido en dos fases debido principalmente a que la arquitectura IA-32
tiene un formato de instrucción de longitud variable. Durante esta fase los bytes que codifican una instrucción se obtienen de
forma gradual pues no se sabe de antemano su tamaño. La decodificación se realiza a partir de los datos obtenidos en la fase
anterior y depositados en el registro de instrucciones y se obtiene el número de bytes que ocupa la instrucción y sus componentes
básicos.
Lo primero que se obtiene es el código de operación. Dependiendo del valor recibido se procede a obtener el resto de los
elementos de la instrucción con sus respectivos tamaños. Al terminar esta fase ya se sabe con exactitud la operación a realizar y
la forma en que obtener sus operandos. El contador de programa ya puede ser actualizado con el valor de la dirección en la que
comienza la siguiente instrucción. La figura 4.10 muestra los componentes que participan en esta fase.
Programación en ensamblador de la
arquitectura IA-32
82 / 198
Figura 4.10: Fase de decodificación inicial
Las instrucciones de la arquitectura IA-32 pueden tener hasta un máximo de dos operandos que a su vez pueden estar almacenados
en múltiples lugares (registros, memoria, la propia instrucción, etc). Una vez terminada esta fase, el procesador ya sabe qué pasos
seguir para obtener los operandos y ejecutar el resto de la instrucción pero todavía no ha obtenido ninguno de ellos. La razón
por la que existe esta fase de decodificación previa es por la complejidad del lenguaje máquina. Al tener formato variable existen
multitud de comprobaciones que se deben hacer en la información recibida de memoria para saber de qué instrucción se trata.
4.2.3.
Fase de decodificación final
Esta fase se encarga de obtener los operandos que participan en la ejecución de la instrucción y que pueden estar almacenados
en varios lugares: registros, memoria o incluso formar parte de la propia instrucción. En el caso de que un operando esté en
memoria, esta fase necesita ejecutar una operación de lectura de memoria. Previa a esta operación el procesador debe calcular la
dirección efectiva del operando, es decir, su posición en memoria. En general, los procesadores ofrecen un número elevado de
posibilidades para especificar esta dirección en una instrucción máquina. La figura 4.11 ilustra lo que sucede en esta fase para
una instrucción que contiene dos operandos, el primero de ellos está en un registro y el segundo en memoria. El cálculo de la
dirección efectiva puede requerir operaciones aritméticas no triviales.
Figura 4.11: Fase de decodificación final
4.2.4.
Fase de ejecución
Una vez obtenidos los operandos, en esta fase se realizan los cálculos aritméticos especificados en la instrucción. La duración
de esta fase depende del tipo de operación requerida. Por ejemplo, una suma tarda un tiempo mucho más reducido que una
multiplicación o división entera. La duración de esta fase se puede representar o como una fase de duración variable o como
múltiples fases consecutivas de ejecución de duración fija.
Programación en ensamblador de la
arquitectura IA-32
83 / 198
Además del cálculo aritmético, es en esta fase en la que se actualizan los valores de los bits del registro de estado y de control
con los valores derivados del resultado producido. La figura 4.12 muestra los componentes que participan en esta fase.
Figura 4.12: Fase de ejecución
4.2.5.
Fase de escritura de resultados
Una vez terminada la operación aritmético/lógica codificada en la instrucción, el procesador guarda el resultado obtenido en
un destino que puede ser igualmente un registro interno o una posición de memoria. La figura 4.13 muestra los elementos
involucrados en esta fase y las dos posibilidades de escritura.
Figura 4.13: Fase de escritura de resultado
Al terminar esta fase, el procesador tiene en el contador de programa la dirección de memoria en la que está almacenada la
siguiente instrucción. La siguiente fase de fetch comienza la ejecución de una nueva instrucción.
La secuencia de fases descrita anteriormente es una de las múltiples que utiliza el procesador. Existen instrucciones que ejecutan
ligeras variaciones con respecto a esta secuencia de cinco pasos. Como ejemplo de esta variedad se pueden tomar las instrucciones
de coma flotante. La forma en que el procesador opera con números reales aumenta el número de fases hasta ocho. Tras las dos
etapas de decodificación se produce un acceso a memoria. Tras la fase de escritura de resultados, este tipo de instrucciones tiene
una fase adicional de notificación de errores. Situaciones tales como el desbordamiento por arriba o por abajo así como otras
situaciones erróneas (ver el capítulo 2) son notificadas mediante excepciones y suelen detener la ejecución del programa. Tal es
la importancia de estos errores que el procesador dedica una de sus fases de ejecución a estas tareas. La figura 4.14 muestra el
ciclo de ejecución para las instrucciones en coma flotante.
Programación en ensamblador de la
arquitectura IA-32
84 / 198
Figura 4.14: Ciclo de ejecución de instrucciones de coma flotante
En este ciclo de ejecución se puede comprobar como las dos primeras fases son idénticas a las instrucciones de aritmética entera.
Las flechas en las dos fases de ejecución indican que su duración varía dependiendo de la operación y los operandos involucrados.
4.2.6.
Ejecución de una instrucción
Para ilustrar el ciclo de ejecución se analiza a continuación la ejecución detallada de una instrucción concreta del procesador.
Supóngase que en la posición de memoria n se encuentra la instrucción INC %eax que tiene el efecto de incrementar o sumar 1
al contenido del registro de propósito general %eax y depositar el resultado de nuevo en el mismo registro. Esta instrucción se
codifica con un único byte con valor 0x40. El ciclo de ejecución de esta instrucción consta de los siguientes pasos:
1. Fase de fetch: Se obtiene de la posición de memoria n contenida en el contador de programa el byte que codifica la
instrucción. Se calcula el nuevo valor del contador, que en este caso es n + 1 pero todavía no se actualiza.
2. Fase de decodificación inicial: Se detecta que no es preciso obtener más datos de memoria pues con un único byte es
suficiente. Se identifica la operación de incremento y que tiene un único operando que es un registro de propósito general.
Se actualiza el valor del contador de programa a n + 1.
3. Fase de decodificación final: Se obtienen los operandos de la instrucción que en este caso es el valor almacenado en el
registro %eax.
4. Fase de ejecución: Utilizando la unidad aritmético/lógica se realiza la suma del valor del registro obtenido en la fase
anterior y la constante 1. Se actualizan los bits de estado pertinentes en el registro de estado y de control.
5. Fase de escritura de resultado: El resultado obtenido en la fase anterior se escribe de nuevo en el registro %eax.
4.2.7.
Ciclo de ejecuciones en procesadores actuales
La descripción anterior supone una simplificación significativa de la estructura real de la arquitectura IA-32 que permite la ejecución de múltiples instrucciones de forma simultánea. La ejecución se realiza mediante una técnica denominada ‘segmentación’
(en inglés ‘pipelining’) que se asemeja al esquema de cadena de producción. Al dividir la ejecución de instrucciones en fases,
mientras una instrucción está en su fase de ejecución se puede estar decodificando la siguiente y haciendo el fetch de la siguiente.
Al circuito que implementa este esquema de ejecución se le denomina ‘pipeline’.
Además de la técnica de segmentación, la arquitectura IA-32 consigue aumentar la velocidad de ejecución mediante la utilización
de múltiples flujos de ejecución. Es decir, el procesador no sólo simultanea las diferentes fases del ciclo de ejecución de varias
instrucciones sino que dispone de múltiples pipelines que trabajan en paralelo. A los procesadores con esta característica se les
denomina ‘superescalares’. La consecuencia más importante de esta técnica es que el orden en que se ejecutan las instrucciones
puede verse alterado por el paralelismo creado.
De este paralelismo se deriva gran parte de la complejidad de diseño de estos procesadores. A nivel de un programa en ensamblador, el paradigma de ejecución en el que se asume que las instrucciones se ejecutan una tras otra. El procesador por tanto utiliza
las técnicas de segmentación y paralelismo para aumentar la velocidad de ejecución pero debe mantener en todo momento la
consistencia con el esquema secuencial. En otras palabras, internamente un procesador puede reorganizar y paralelizar la ejecución de instrucciones todo lo que pueda siempre y cuando los resultados producidos concuerden con la ejecución secuencial de
instrucciones.
En la actualidad, los procesadores más modernos de la arquitectura IA-32 contienen múltiples pipelines especializados en diferentes tipos de instrucciones (enteros, coma flotante, saltos, etc.) con lo que se consigue una velocidad de ejecución muy elevada.
Programación en ensamblador de la
arquitectura IA-32
4.3.
85 / 198
La pila
Aparte de los componentes de la arquitectura presentados en las secciones anteriores, la mayor parte de procesadores ofrecen la
infraestructura necesaria para manipular una estructura de datos organizada y almacenada en memoria que se denomina ‘la pila’.
La pila es una zona de la memoria sobre la que se pueden escribir y leer datos de forma convencional. Esta zona tiene una
posición especial que se denomina ‘la cima de la pila’. El procesador contiene dos instrucciones de su lenguaje máquina para
realizar las operaciones de ‘apilar’ y ‘desapilar’ datos de la pila. Los datos que se pueden apilar y desapilar, en el caso de la
arquitectura IA-32 son siempre de tamaño 4 bytes.
4.3.1.
Instrucciones de manejo de la pila
La instrucción para apilar un dato en la pila tiene el formato push dato. Es una instrucción con un único operando que
deposita el dato especificado como parámetro en la cima de la pila. Supóngase que la cima de la pila está en la posición @cima.
La instrucción push dato produce el siguiente efecto.
Se resta 4 a la dirección de la cima de la pila, se obtiene, por tanto @cima - 4.
Se escribe el dato de 32 bits dado como único operando en la posición de memoria indicada por @cima - 4 y la dirección
de la cima se asigna a este nuevo valor. El dato que estaba previamente almacenado en esas posiciones se ha perdido.
La figura 4.15 muestra la pila antes y después de ejecutar la instrucción push dato.
Figura 4.15: Efecto de la instrucción push
De la descripción de la instrucción push se deduce que efectúa una operación de escritura en memoria RAM. Si a continuación
de esta instrucción se ejecuta otra del mismo tipo, el dato se almacena a partir de la cuarta posición de memoria antes del último
valor depositado en de la pila.
La instrucción pop destino ejecuta el procedimiento complementario al de push dato. Tiene un único operando que, en
este caso, especifica el lugar en el que almacenar el dato que se encuentra en la cima de la pila. Supóngase que la cima de la pila
está en la posición de memoria @code. La ejecución de la instrucción pop destino tiene el siguiente efecto.
Se lee el dato de 32 bits almacenado en la posición de memoria indicada por la dirección de la cima @cima y se almacena en
el lugar especificado como operando de la instrucción.
Programación en ensamblador de la
arquitectura IA-32
86 / 198
Se suma 4 a la dirección de la cima de la pila, se obtiene, por tanto @cima + 4.
La figura 4.16 muestra la pila antes y después de ejecutar la instrucción pop destino.
Figura 4.16: Efecto de la instrucción pop
Nótese que las instrucciones push y pop tienen estructura y efectos complementarios. La instrucción push recibe como operando el dato a depositar, no es preciso especificar el destino pues se deposita automáticamente en la nueva cima. La instrucción
pop, por contra, recibe como parámetro el lugar en el que almacenar el dato obtenido y no es preciso indicar de dónde se obtiene
pues se lee automáticamente de la cima de la pila. La instrucción push ajusta la cima restando 4 al valor actual, mientras que
pop suma 4 a ese valor. Además, una instrucción realiza una operación de lectura en memoria y la otra una operación de escritura. El dato que la instrucción pop lee de la cima de la pila no desaparece de esa posición de memoria, pues lo único que se hace
es leer ese valor. Sí es cierto que la cima de la pila ya no apunta a ese dato, pero este sigue almacenado en la misma posición.
Los destinos posibles que se pueden especificar en la instrucción pop dependen del lenguaje máquina del procesador, pero en la
arquitectura IA-32 se permite especificar cualquier registro de propósito general de 32 bits como operando de esta instrucción.
Por ejemplo, la instrucción pop %edx lee los cuatro bytes almacenados en la cima de la pila, los copia en el registro %edx y
ajusta la dirección de la cima.
4.3.2.
El puntero de pila
Del funcionamiento de las instrucciones push y pop se deduce que en algún lugar del procesador debe estar almacenada la
dirección de la cima de la pila y que dicho valor es modificado por ambas instrucciones. En el caso de la arquitectura IA-32, esta
dirección de memoria está guardada por defecto en el registro de propósito general %esp. Las dos últimas letras del nombre de
este registro corresponden con las iniciales de las palabras stack pointer o ‘apuntador de pila’. La primera consecuencia de esta
característica del procesador es que, a pesar de que dichos registros están, en principio, disponibles para almacenar valores de
forma temporal, el caso de %esp es especial, pues es donde las instrucciones de manipulación de la pila asumen que se encuentra
la dirección de la cima. El tamaño de este registro es de 32 bits que coincide con el tamaño de toda dirección de memoria en la
arquitectura IA-32.
Si en el instante antes de ejecutar una instrucción push %esp contiene el valor v1 , tras su ejecución contendrá el valor v1 - 4.
De forma análoga, si antes de ejecutar la instrucción pop %esp contiene el valor v2 , tras su ejecución contendrá el valor v2 +
4. La figura 4.17 muestra el efecto de la ejecución de dos instrucciones consecutivas sobre la pila tanto en memoria como en los
registros de propósito general.
Programación en ensamblador de la
arquitectura IA-32
87 / 198
Figura 4.17: Ejecución de instrucciones de pila
El que la dirección de la pila esté contenida en un registro de propósito general permite que su contenido sea manipulado como
cualquier otro registro. Un programa, por tanto, puede leer y escribir cualquier valor de %esp, tan sólo se debe tener en cuenta
que el procesador obtiene de ese registro la dirección de memoria necesaria para ejecutar las instrucciones push y pop.
Supóngase que se ha depositado un cierto dato en la pila mediante la instrucción push y que se encuentra, por tanto en la cima.
La instrucción pop deposita ese valor en el lugar especificado pero, ¿es posible ejecutar la instrucción pop sin ningún operando?.
En otras palabras, la operación que se quiere ejecutar no es la de copiar el dato de la cima, sino simplemente corregir el valor
de la cima al igual que haría pop pero sin depositar el dato en ningún lugar. La instrucción pop, por definición, debe incluir un
único operando, con lo que no se puede utilizar para hacer esta operación.
La solución se deriva del hecho de que %esp es un registro de propósito general y de que todos los datos leídos o extraídos de la
pila son de tamaño 4 bytes. Para corregir el valor de la cima de la pila tal y como hace pop pero sin depositar su valor en destino
alguno es suficiente con sumar 4 al valor de %esp. La instrucción ADD $4, %esp produce exactamente ese efecto. El primer
operando es la constante a sumar, y el segundo es a la vez el otro sumando y el lugar donde dejar el resultado. Esta instrucción
por tanto asigna a %esp su valor incrementado en cuatro unidades. El efecto que esta instrucción tiene sobre la pila es el deseado.
La siguiente instrucción asume que la cima está en la nueva posición contenida en %esp.
4.3.3.
Valores iniciales del puntero de pila
Todo programa en ensamblador comienza ejecutar con un valor en el registro %esp que apunta a la cima de la pila previamente
preparada. Los programas, por tanto, no deben realizar operación alguna para inicializar la pila ni para reservar su espacio. Esta
tarea la lleva a cabo, antes de que comience la ejecución, el sistema operativo.
El sistema operativo es un programa que se encarga de realizar las tareas de administración de todos los dispositivos y recursos
disponibles en el equipo. Entre ellas se encuentra la de permitir la ejecución de programas en ensamblador. Todo programa
antes de comenzar a ejecutar su primera instrucción tiene una zona de memoria reservada para la pila y su puntero a la cima
correctamente inicializado.
Pero la memoria de un equipo es limitada, y por tanto, la pila ocupa un lugar en memoria también limitado. ¿Qué sucede si se
intenta acceder a posiciones de memoria fuera de este límite? Esta situación puede ser provocada al menos por dos situaciones:
se deposita un dato mediante la instrucción push cuando todo el espacio reservado para la pila ya está ocupado o se intenta
obtener un dato de la pila cuando esta no contiene dato alguno.
Supóngase que la pila está almacenada en la zona de memoria que va desde la dirección p hasta la dirección q (ambas inclusive)
y p < q. ¿Qué valores contiene el registro %esp cuando la pila está llena y cuando está vacía? Si la pila está llena entonces la cima
está en la posición de memoria con valor más bajo posible, es decir cima = p. Si en estas condiciones se ejecuta una instrucción
push el procesador detiene la ejecución del programa de forma abrupta.
Programación en ensamblador de la
arquitectura IA-32
88 / 198
Si la pila está vacía quiere decir que no se ha introducido dato alguno en ella y por tanto si se ejecutase la instrucción push se
depositaría el primer dato. Por tanto, la cima de la pila vacía debe estar en la posición q + 1 para que el dato del primer push
se almacene correctamente. La figura 4.18 muestra los valores de la cima para las dos condiciones descritas.
Figura 4.18: Valores de la cima para la pila vacía y llena
4.4.
Ejercicios
1. La instrucción ADD $128, %eax suma la constante 128 al contenido del registro %eax y deposita el resultado de nuevo
en dicho registro. Describir los pasos que sigue el procesador en las cinco fases de ejecución de esta instrucción.
2. ¿Cuántos registros cambian de contenido tras ejecutar la instrucción push %eax?
3. ¿Cuántos registros, como máximo, cambian de contenido tras ejecutar la instrucción pop %eax?
4. ¿Cuántos registros contienen datos diferentes al ejecutar la instrucción push %eax seguida de la instrucción pop %eax?
5. ¿Cuántas posiciones de memoria han modificado su valor, como máximo, tras ejecutar las dos instrucciones de la pregunta
anterior?
6. La instrucción pop %edx no ha modificado el contenido del registro %edx ¿Cómo es esto posible?
7. ¿Qué efecto se produce en la pila si mediante una instrucción se suma 4 al registro %esp? ¿Y si se resta 4?
8. ¿Qué secuencia de cuatro instrucciones de pila se pueden ejecutar para intercambiar los valores de dos registros?
9. ¿Cómo se define el estado visible de un programa?. ¿En qué cuatro elementos está contenido dicho estado en la versión
simplificada del procesador?
10. Escribir la secuencia de instrucciones en ensamblador cuya ejecución sea equivalente a ejecutar la instrucción push %eax.
Explicar la solución propuesta.
11. Escribir la secuencia de instrucciones en ensablador cuya ejecución sea equivalente a ejecutar la instrucción pop %eax.
Explicar la solución propuesta.
Programación en ensamblador de la
arquitectura IA-32
89 / 198
Capítulo 5
Juego de instrucciones
A la definición detallada del conjunto de instrucciones que es capaz de ejecutar un procesador se le denomina su ‘juego de
instrucciones’ (o, en ingles, Instruction Set Architecture). Esta definición es la que determina de forma inequívoca el efecto de
cada instrucción sobre las diferentes partes de la arquitectura del procesador. El número de instrucciones máquina puede llegar a
ser muy elevado debido a que la misma instrucción (por ejemplo, la de suma) se puede ejecutar sobre diferentes tipos de datos y
con diferentes variantes (números naturales, enteros, etc.)
5.1.
Tipos de juegos de instrucciones
La decisión de qué instrucciones es capaz de ejecutar un procesador es una de las más importantes y en buena medida es
determinante en el rendimiento a la hora de ejecutar programas. Además, el juego de instrucciones y la arquitectura del procesador
están interrelacionados. Por ejemplo, generalmente todas las instrucciones del lenguaje máquina de un procesador pueden utilizar
los registros de propósito general, por lo que su número tiene un efecto directo en la codificación de instrucciones.
La decisión de qué instrucciones incluir en un procesador está también influenciada por la complejidad que requiere su diseño. Si
una instrucción realiza una operación muy compleja, el diseño de los componentes digitales necesarios para su ejecución puede
resultar demasiado complejo.
Considérese el siguiente ejemplo. ¿Debe un procesador incluir en su lenguaje máquina una instrucción que dado un número real
y los coeficientes de un polinomio de segundo grado obtenga su valor? Supóngase que esta instrucción se llama EPSG (evaluar
polinomio de segundo grado). Un posible formato de esta instrucción se muestra en el ejemplo 5.1.
Ejemplo 5.1 Formato de la instrucción EPSG
EPSG a, b, c, n, dest
La instrucción realiza los cálculos con los cuatro primeros parámetros tal y como se muestra en la ecuación 5.1 y almacena el
resultado en el lugar especificado por el parámetro dest.
f (n) = an2 + bn + c
E QUATION 5.1: Polinomio de segundo grado para el valor n
La ecuación 5.1 especifica las operaciones a realizar para evaluar el polinomio, en este caso suma y multiplicación. Un procesador
que no disponga de la instrucción máquina EPSG puede obtener el mismo resultado pero ejecutando múltiples instrucciones.
El compromiso a explorar, por tanto, a la hora de decidir si incluir una instrucción en el lenguaje máquina de un procesador está
entre la complejidad de las instrucciones y la complejidad del lenguaje. Si un procesador soporta la ejecución de la instrucción
Programación en ensamblador de la
arquitectura IA-32
90 / 198
EPSG, requiere una estructura interna más compleja, pues debe manipular sus múltiples operandos y ejecutar las operaciones
necesarias. En cambio, si un procesador ofrece la posibilidad de realizar multiplicaciones y sumas, la evaluación del polinomio
es igualmente posible aunque mediante la ejecución de múltiples instrucciones, con lo que no será una ejecución tan rápida. En
general, un lenguaje máquina con instrucciones sofisticadas requiere una implementación más compleja del procesador. De igual
forma, un lenguaje máquina sencillo (pero que ofrezca las operaciones mínimas para poder realizar todo tipo de cálculos) permite
un diseño más simple.
De este compromiso se ha derivado a lo largo de los años una división de los procesadores en dos categorías dependiendo de la
filosofía utilizada para el diseño de su lenguaje máquina:
Los procesadores que ejecutan un conjunto numeroso de instrucciones y algunas de ellas de cierta complejidad se les denomina
de tipo CISC (Complex Instruction Set Computer). Las instrucciones más complejas son las que requieren múltiples cálculos
y accesos a memoria para lectura/escritura de operandos y resultados.
El ejemplo más representativo de esta filosofía es la arquitectura IA-32. Su lenguaje máquina consta de instrucciones capaces
de realizar operaciones complejas. Otro ejemplo de procesador CISC es el Motorola 68000, que aunque en la actualidad ha
dejado paso a otro tipo de procesadores pero que está todavía presente en ciertos productos electrónicos y ha sido la inspiración
de múltiples modelos actuales.
Los procesadores que ejecutan un conjunto reducido de instrucciones simples se denominan de tipo RISC (Reduced Instruction
Set Computer). El número de posibles instrucciones es muy pequeño, pero a cambio, el diseño del procesador se simplifica y
se consiguen tiempos de ejecución muy reducidos con el consiguiente efecto en el rendimiento total del sistema.
Ejemplos de algunos procesadores diseñados con esta filosofía son:
• MIPS (Microprocessor without interlocked pipeline stages): utilizado en encaminadores, consola Nintendo 64, PlayStation
y PlayStation 2 y PlayStation portátil (PSP).
• ARM: presente en ordenadores portátiles, cámaras digitales, teléfonos móviles, televisiones, iPod, etc.
• SPARC (Scalable Processor Architecture): línea de procesadores de la empresa Sun Microsystems. Se utilizan principalmente para servidores de alto rendimiento.
• PowerPC: arquitectura inicialmente creada por el consorcio Apple-IBM-Motorola para ordenadores personales que está
presente en equipos tales como servidores, encaminadores, es la base para el procesador Cell presente en la PlayStation 3,
XBox 360, etc.
En la actualidad, esta división entre procesadores CISC y RISC se ha empezado a difuminar. La propia arquitectura IA-32
decodifica las instrucciones de su lenguaje máquina y las traduce a una secuencia de instrucciones más simples denominadas
‘microinstrucciones’. Se puede considerar, por tanto, que el lenguaje formado por estas microinstrucciones tiene una estructura
cercana a la categoría RISC, mientras que el conjunto de instrucciones máquina es de tipo CISC.
Otra importante decisión a la hora de diseñar un lenguaje máquina es el formato en el que se van a codificar las instrucciones.
Ateniendo a este criterio los procesadores se pueden dividir en:
Formato de longitud fija. Todas las instrucciones máquina se codifican con igual número de bits. De esta característica se
derivan múltiples limitaciones del lenguaje. El número de operandos de una instrucción no puede ser muy elevado, pues todos
ellos deben ser codificados con un conjunto de bits. Al igual que sucede con los operandos, el tipo de operación debe ser
también codificado, y por tanto este tipo de lenguajes no pueden tener un número muy elevado de instrucciones.
Como contrapartida, un formato de instrucción fijo se traduce en una fase de decodificación más simple. El procesador obtiene
de memoria un número fijo de bits en los que sabe de antemano que está contenida la instrucción entera. Los operandos
generalmente se encuentran en posiciones fijas de la instrucción, con lo que su acceso se simplifica enormemente.
El procesador PowerPC es un ejemplo de procesador con formato fijo de instrucción. Todas ellas se codifican con 32 bits. En
general, los procesadores de tipo RISC optan por una codificación con formato de longitud fija.
Formato de longitud variable. Las instrucciones máquina se codifican con diferente longitud. La principal consecuencia es que
la complejidad de una instrucción puede ser arbitraria. En este tipo de lenguaje máquina se puede incluir un número elevado
de instrucciones.
El principal inconveniente es la decodificación de la instrucción pues su tamaño sólo se sabe tras analizar los primeros bytes
con lo que identificar una instrucción y sus operandos es más complejo.
Los procesadores con arquitectura IA-32 son un ejemplo de procesadores con formato variable de instrucciones. Dicho formato
se estudia en mayor detalle en las siguientes secciones.
Programación en ensamblador de la
arquitectura IA-32
5.2.
91 / 198
Formato de instrucciones máquina de la arquitectura IA-32
La arquitectura IA-32 codifica sus instrucciones máquina con un formato de longitud variable. Toda instrucción tiene una longitud
entre 1 y 16 bytes. La figura 5.1 ilustra las diferentes partes de las que puede constar una instrucción así como su tamaño en bytes.
Figura 5.1: Formato de Instrucción
Las instrucciones comienzan por un prefijo de hasta cuatro bytes, seguido de uno o dos bytes que codifican la operación, un byte
de codificación de acceso a operandos, un byte denominado escala-base-índice (scale-base-index), un desplazamiento de hasta
cuatro bytes, y finalmente un operando inmediato de hasta cuatro bytes. Excepto los bytes que codifican la operación, el resto de
componentes son todos opcionales, es decir, su presencia depende del tipo de operación.
Los prefijos son bytes que modifican la ejecución normal de una instrucción de acuerdo a unas propiedades predefinidas. El
procesador agrupa estos prefijos en cuatro categorías y se pueden incluir hasta un máximo de uno por categoría. Por ejemplo, el
prefijo LOCK hace que mientras se ejecuta la instrucción el procesador tiene acceso en exclusiva a cualquier dispositivo que sea
compartido. Este prefijo se utiliza en sistemas en los que se comparte memoria entre múltiples procesadores.
El código de operación codifica sólo el tipo de operación a realizar. Su tamaño puede ser de hasta 2 bytes y en ciertas instrucciones
parte de este código se almacena en el byte siguiente denominado ModR/M. Este byte se utiliza en aquellas instrucciones cuyo
primer operando está almacenado en memoria y sus ocho bits están divididos en tres grupos o campos tal y como ilustra la figura
5.2 y que almacenan los siguientes datos:
Figura 5.2: Byte ModR/M de las instrucciones de la arquitectura IA-32
El campo Mod combinado con el campo R/M codifica uno de los 8 posibles registros de propósito general, o uno de los 24
posibles modos de direccionamiento.
El campo Reg/Opcode codifica uno de los ocho posibles registros de propósito general. En algunas instrucciones estos tres
bits forman parte del código de operación.
El campo R/M codifica o uno de los ocho posibles registros de propósito general, o combinado con el campo Mod uno de los
24 posibles modos de direccionamiento.
Algunas combinaciones de valores en el byte ModR/M requieren información adicional que se codifica en el byte SIB cuya
estructura se muestra en la figura 5.3.
Programación en ensamblador de la
arquitectura IA-32
92 / 198
Figura 5.3: Byte SIB de las instrucciones de la arquitectura IA-32
Algunos de los modos de direccionamiento ofrecidos por el procesador requieren un factor de escala por el que multiplicar
un registro denominado índice, y un registro denominado base. Estos tres operandos se codifican en el byte SIB con los bits
indicados en cada uno de sus campos. Los campos que codifican el registro base y el índice tienen ambos un tamaño de 3 bits, lo
que concuerda con el número de registros de propósito general de los que dispone el procesador. El factor de escala se codifica
únicamente con 2 bits, con lo que sólo se pueden codificar 4 posibles valores.
El campo denominado ‘desplazamiento’ es opcional, codifica un número de 1, 2 o 4 bytes y se utiliza para calcular la dirección
de un operando almacenado en memoria. Finalmente, el campo denominado ‘inmediato’ (también opcional) tiene un tamaño de
1, 2 o 4 bytes y codifica los valores constantes en una instrucción.
La figura 5.4 muestra un ejemplo de como se codifica la instrucción ADDL $4, 14( %eax, %ebx, 8) que suma la constante
4 a un operando de 32 bits almacenado en memoria a partir de la dirección cuya expresión es 14 + %eax + ( %ebx * 8) con 5
bytes con valores 0x8344D80E04.
Figura 5.4: Codificación de una instrucción ensamblador
En este caso, el código de operación está contenido en los primeros 8 bits (valor 0x83) y los 3 bits del campo Reg/Opcode del
byte ModR/M y codifica la instrucción de suma de un valor constante de 8 bits a un valor de 32 bits almacenado en memoria.
Los valores 01 y 100 en los campos Mod y R/M del byte ModR/M respectivamente indican que la instrucción contiene en el
byte SIB los datos que precisa el modo de direccionamiento para acceder al segundo operando así como la dirección en la que se
almacena el resultado.
Los campos del byte SIB contienen los valores 11, 011 y 000 que codifican respectivamente el factor de escala 8, el registro
índice %ebx y el registro base %eax así como el tamaño del desplazamiento que es un byte. La instrucción concluye con un byte
que codifica el desplazamiento, seguido de un byte que codifica la constante a utilizar como primer operando.
5.3.
El lenguaje ensamblador
Para escribir programas que puedan ser ejecutados por un procesador, todas las instrucciones y datos se deben codificar mediante
secuencias de ceros y unos. Estas secuencias son el único formato que entiende el procesador, pero escribir programas enteros en
este formato es, aunque posible, extremadamente laborioso.
Programación en ensamblador de la
arquitectura IA-32
93 / 198
Una solución a este problema consiste en definir un lenguaje que contenga las mismas instrucciones, operandos y formatos que
el lenguaje máquina, pero en lugar de utilizar dígitos binarios, utilizar letras y números que lo hagan más inteligible para el
programador. A este lenguaje se le conoce con el nombre de lenguaje ensamblador.
El lenguaje ensamblador, por tanto, se puede definir como una representación alfanumérica de las instrucciones que forman
parte del lenguaje máquina de un procesador. Tal y como se ha mostrado en la sección 5.2, la traducción de la representación
alfanumérica de una instrucción a su representación binaria consiste en aplicar un proceso de traducción sistemático.
Considérese de nuevo la instrucción de lenguaje ensamblador utilizada en la figura 5.4, ADDL $4, 14( %eax, %ebx, 8).
Una segunda forma de escribir esta instrucción puede ser ADDL 14[ %eax, %ebx * 8], 4. En este nuevo formato se han
cambiado el orden de los operandos así como la sintaxis utilizada. Cualquiera de las dos notaciones es válida siempre y cuando
se disponga del programa que pueda traducirlo a su codificación en binario entendida por el procesador (5 bytes con valores
0x8344D80E04).
5.3.1.
Formato de instrucción ensamblador
El lenguaje ensamblador que se describe a continuación sigue la sintaxis comúnmente conocida con el nombre de ‘AT&T’ y sus
principales características son que los operandos destino se escriben en último lugar en las instrucciones, los registros se escriben
con el prefijo % y las constantes con el prefijo $.
Una sintaxis alternativa utilizada por otros compiladores es la conocida con el nombre de ‘Intel’. En ella, los operandos destino
se escriben los primeros en una instrucción, y los registros y constantes no se escriben con prefijo alguno.
En principio es el programa ensamblador quien estipula la forma en la que se deben escribir las instrucciones. Por tal motivo, es
posible que existan diferentes ensambladores con diferentes definiciones de su lenguaje, pero que produzcan el mismo lenguaje
máquina. Existen también ensambladores capaces de procesar programas escritos en más de un formato, el programa gcc, incluido
con el sistema operativo Linux es uno de ellos. En adelante se utilizará únicamente la sintaxis ‘AT&T’.
Las instrucciones del lenguaje máquina de la arquitectura IA-32 pueden tener uno de los tres siguientes formatos:
Operación. Las instrucciones con este formato no precisan ningún operando, suelen ser fijos y por tanto se incluyen de
forma implícita. Por ejemplo, la instrucción RET retorna de una llamada a una subrutina.
Operación Operando. Estas instrucciones incluyen únicamente un operando. Algunas de ellas pueden referirse de manera
implícita a operandos auxiliares. Un ejemplo de este formato es la instrucción INC %eax que incrementa en uno el valor de
su único operando.
Operación Operando1, Operando2. Un ejemplo de este tipo de instrucciones es ADD $0x10, %eax que toma la
constante 0x10 y el contenido del registro %eax, realiza la suma y deposita el resultado en este mismo registro. Como regla
general, cuando una operación requiere tres operandos, dos fuentes y un destino (por ejemplo, una suma), el segundo operando
desempeña siempre las funciones de fuente y destino y por tanto se pierde su valor inicial.
Algunas de las instrucciones del procesador tienen un formato diferente a estos tres, pero serán tratadas como casos excepcionales. El ejemplo 5.2 muestra instrucciones de los tres tipos descritos anteriormente escritas en lenguaje ensamblador.
Ejemplo 5.2 Instrucciones del lenguaje ensamblador
push ( %ecx)
push 4( %ecx)
push $msg
call printf
add $12, %esp
pop %edx
pop %ecx
pop %eax
ret
Programación en ensamblador de la
arquitectura IA-32
5.3.2.
94 / 198
Descripción detallada de las instrucciones
Para escribir programas en lenguaje ensamblador se necesita una descripción detallada de todas y cada una de sus instrucciones.
Dicha descripción debe incluir todos los formatos de operandos que admite, así como el efecto que tiene su ejecución en el
procesador y los datos. Esta información se incluye en los denominados manuales de programación y acompañan a cualquier
procesador.
En el caso de la arquitectura IA-32, su descripción detallada, el lenguaje máquina y funcionamiento se incluye en el documento
de poco más de 2000 páginas que lleva por título IA-32 Intel Architecture Software Developer’s Manual y cuyo contenido está
dividido en los siguientes tres volúmenes:
Volumen 1. Arquitectura básica (Basic Architecture): describe la arquitectura básica del procesador así como su entorno de
programación.
Volumen 2. Catálogo del juego de instrucciones (Instruction Set Reference): describe cada una de las instrucciones del procesador y su codificación.
Volumen 3. Guía para la programación de sistemas (System Programming Guide): describe el soporte que ofrece esta arquitectura al sistema operativo en aspectos tales como gestión de memoria, protección, gestión de tareas, interrupciones, etc.
El ejemplo 5.3 muestra la definición de la instrucción de suma de enteros que forma parte del lenguaje máquina de la arquitectura
IA-32 tal y como consta en su manual.
Ejemplo 5.3 Descripción de la instrucción de suma de enteros en la arquitectura IA-32
ADD--Add
Opcode
Opcode
04 ib
05 iw
05 id
80 /0 ib
81 /0 iw
81 /0 id
83 /0 ib
83 /0 ib
00 /r
01 /r
01 /r
02 /r
03 /r
03 /r
Instruction
Instruction
ADD AL,imm8
ADD AX,imm16
ADD EAX,imm32
ADD r/m8,imm8
ADD r/m16,imm16
ADD r/m32,imm32
ADD r/m16,imm8
ADD r/m32,imm8
ADD r/m8,r8
ADD r/m16,r16
ADD r/m32,r32
ADD r8,r/m8
ADD r16,r/m16
ADD r32,r/m32
Description
Description
Add imm8 to AL
Add imm16 to AX
Add imm32 to EAX
Add imm8 to r/m8
Add imm16 to r/m16
Add imm32 to r/m32
Add sign-extended imm8 to r/m16
Add sign-extended imm8 to r/m32
Add r8 to r/m8
Add r16 to r/m16
Add r32 to r/m32
Add r/m8 to r8
Add r/m16 to r16
Add r/m32 to r32
Description
Adds the first operand (destination operand) and the second operand (source operand) and stores the result in the
destination operand. The destination operand can be a register or a memory location; the source operand can be an
immediate, a register, or a memory location. (However, two memory operands cannot be used in one instruction.) When
an immediate value is used as an operand, it is sign-extended to the length of the destination operand format. The ADD
instruction performs integer addition. It evaluates the result for both signed and unsigned integer operands and sets the OF
and CF flags to indicate a carry (overflow) in the signed or unsigned result, respectively. The SF flag indicates the sign of
the signed result. This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically.
Operation
DEST ← DEST + SRC
Flags Affected
The OF, SF, ZF, AF, CF, and PF flags are set according to the result.
Programación en ensamblador de la
arquitectura IA-32
95 / 198
La parte superior incluye las diferentes versiones de suma de enteros que soporta el procesador dependiendo de los tipos de
operandos. La primera columna muestra los códigos de operación para cada una de las versiones y la segunda columna muestra
la estructura en lenguaje ensamblador de cada una de ellas. La sintaxis utilizada en este documento es de tipo Intel (ver la sección
5.3.1), por tanto, el operando destino es el primero que se escribe.
La codificación de la instrucción ADD $4, 14( %eax, %ebx, 8) utilizada en el figura 5.4 coincide con la mostrada por esta
tabla en la octava fila ADD r/m32, imm8, o en otras palabras, la suma de una constante de ocho bits (imm8) a un registro o
un dato en memoria (en el ejemplo, un dato en memoria).
En el código de operación, los símbolos ‘ib’, ‘iw’ e ‘id’ significan respectivamente una constante de 8, 16 o 32 bits. El símbolo
‘\r’ representa cualquiera de los registros de propósito general del procesador. En la segunda y tercera columna el prefijo ‘imm’
seguido de un número representa una constante del tamaño en bits indicado por el número. El prefijo ‘r/m’ seguido de un número
significa que el operando es o un registro o un dato en memoria del tamaño del número indicado.
El documento continua con una descripción de palabra de la operación que realiza la instrucción. Se aclara que uno de los
operandos es fuente y destino a la vez, y que no es posible sumar dos operandos que estén ambos en memoria.
La siguiente sección es una descripción funcional de la operación y se utiliza como resumen formal de la descripción textual que
le precede. Algunas instrucciones, debido a su complejidad, son más fácilmente descritas mediante esta notación que mediante
texto. Finalmente se mencionan aquellos bits de la palabra de estado del procesador que se modifican al ejecutar una de estas
instrucciones.
5.3.3.
Tipos de operandos
Los operandos que utilizan las instrucciones de la arquitectura IA-32 se dividen en las siguientes categorías:
Constantes. El valor debe ir precedido del símbolo ‘$’. Se pueden especificar valores numéricos y cualquier letra o símbolo
manipulable por el procesador. Las constantes numéricas se pueden escribir en base hexadecimal si se antepone el prefijo ‘0x’,
en base 8 (u octal) si se antepone el prefijo ‘0’, o en binario si se antepone el prefijo ‘0b’. Una constante numérica sin prefijo
se considera escrita en base 10, por ejemplo: $0xFF23A013, $0763, 0b00101001, $255.
Las constantes que representan letras deben ir precedidas por la comilla simple ’. Por ejemplo, $’A representa la constante
numérica que codifica el valor de la letra a mayúscula.
Registro de propósito general. El nombre del registro contiene el prefijo %. Se pueden utilizar cualquiera de los ocho registros
de propósito general así como sus diferentes porciones (ver la sección 4.1.2), por ejemplo: %eax, %dh, %esp, %bp.
Dirección de memoria. El operando está almacenado a partir de la dirección de memoria dada en la instrucción. Se permite
un amplio catálogo de formas para especificar la dirección de los operandos denominados ‘modos de direccionamiento’ y se
describen de forma detallada en el capítulo 7.
Operando implícito. No constan pero la instrucción hace uso de ellos. Por ejemplo, la instrucción PUSH deposita el único
operando dado en la cima de la pila. La instrucción tiene como operando implícito el registro %esp que contiene la dirección
de memoria en la que está almacenado el dato de la cima y se le resta la constante 4 al final de la operación.
La presencia o ausencia de operandos implícitos está contenida en la descripción detallada de las instrucciones máquina.
En la arquitectura IA-32 no todas las combinaciones posibles de tipos de operandos se pueden dar en todas las instrucciones. La
arquitectura impone la restricción de que no se permite la ejecución de una instrucción con dos operandos explícitos que estén
almacenados ambos en memoria. Además, no todas las combinaciones de instrucciones con tipos de operandos tienen sentido.
La Tabla 5.1 muestra ejemplos de instrucciones en lenguaje ensamblador correctas e incorrectas.
5.3.4.
El sufijo de tamaño
De los tipos de operandos presentados en la sección anterior, no todos tienen definido el tamaño de todos sus componentes. Tal
y como se ha visto en el capítulo 2, cuando se procesan datos es preciso saber el tamaño utilizado para su codificación.
Considérese la instrucción utilizada como último ejemplo en la Tabla 5.1, MOV $-4, contador. A primera vista, la instrucción puede parecer correcta, pues se mueve una constante a una dirección de memoria representada, en este caso, por el símbolo
contador. El primer operando, sin embargo, puede ser representado por un número arbitrario de bits. Lo mismo sucede con
Programación en ensamblador de la
arquitectura IA-32
Instrucción
PUSH $4
POP $0b11011101
96 / 198
Correcta
Sí
No. El operando de esta instrucción es el destino en el que
almacenar el dato en la cima de la pila, y por tanto, no
puede ser una constante.
MOV $-4, %eax
Sí. Primer operando es de tipo constante y el segundo de
tipo registro.
MOV %eax, $0x11011110
No. El segundo operando es el destino de la operación, y
no puede ser una constante.
MOV %eax, contador
Sí. El segundo operando representa una dirección de
memoria.
MOV $’A, %eax
Sí. ¿Qué tamaño de datos se está moviendo en esta
instrucción a %eax?
MOV $65, %eax
Sí. Esta instrucción tiene una codificación idéntica a la
anterior.
MOV contador, resultado
No. Instrucción con dos operandos, y ambos son de tipo
dirección de memoria.
MOV $-4, contador
¿Qué tamaño de datos se transfiere a memoria?
Tabla 5.1: Instrucciones con diferentes tipos de operandos
el segundo operando, pues al ser una dirección de memoria, lo único que se puede asegurar es que se utilizarán tantos bytes de
memoria como sea preciso.
Como conclusión, la instrucción MOV $-4, contador a pesar de tener un formato correcto, es ambigua. El mismo formato
puede representar las instrucciones que almacena la constante -4 representada por un número variable de bytes en la dirección
indicada por contador. La arquitectura IA-32 sólo permite 3 tamaños para sus operandos: 1 byte, 2 bytes (un word), o 4
bytes (un doubleword, ver la Tabla 4.1). Por tanto, la instrucción MOV $-4, contador, puede ser interpretada de tres formas
diferentes dependiendo del tamaño con el que se representa la constante y el número de bytes utilizados para almacenar su valor
en memoria (ambos deben ser el mismo número, 1, 2 o 4).
Para solventar este problema, el lenguaje ensamblador permite la utilización de un sufijo en el código de instrucción que indica
el tamaño de los operandos utilizados. Este sufijo es la letra ‘B’ para operandos de 1 byte, ‘W’ para operandos de 2 bytes (un
word), y ‘L’ para operandos de 4 bytes (un doubleword).
Por tanto, si se quiere codificar la instrucción que almacena la constante -4 representada por 32 bits en la dirección indicada por
contador se debe escribir MOVL $-4, contador.
De todas las instrucciones posibles sólo algunas de ellas son ambiguas. Si alguno de los operandos es un registro, el tamaño del
operando queda fijado por el tamaño del registro. La ambigüedad aparece cuando ninguno de los operandos es un registro, y por
tanto no es posible deducir el tamaño. Se permite el uso del sufijo de tamaño en una instrucción que no lo requiera, siempre y
cuando esté en consonancia con el tamaño de los operandos. La Tabla 5.2 muestra ejemplos de utilización del sufijo de tamaño.
5.4.
Instrucciones más representativas de la arquitectura IA-32
A continuación se describe el subconjunto de instrucciones de la arquitectura IA-32 necesario para poder codificar tareas básicas
de programación y manipulación de datos de tipo entero y strings. La descripción del lenguaje máquina completo se puede
encontrar en la documentación facilitada por el fabricante. Para simplificar su estudio, las instrucciones se dividen en categorías.
Una descripción detallada de cada una de ellas se puede encontrar en el apéndice A.
Programación en ensamblador de la
arquitectura IA-32
97 / 198
Instrucción
Comentario
No es preciso el sufijo, los operandos de la pila son siempre
de 32 bits.
PUSH $4
El sufijo es redundante y concuerda con el tamaño del
operando.
PUSHL $0b11011101
El sufijo es imprescindible porque la instrucción almacena
un único byte que codifica el número -4 en complemento a
dos en la posición de memoria indicada por contador.
No es preciso el sufijo porque la presencia del
operando %ax hace que la constante se represente con 16
bits.
La presencia del registro %eax hace que el operando se
considere de 32 bits, y por tanto el sufijo es redundante
pero correcto.
Esta instrucción es incorrecta porque contiene un error de
sintaxis. El sufijo indica tamaño de 1 byte y el segundo
operando indica 4 bytes. El sufijo es innecesario y la
instrucción transfiere el número que codifica la constante
$’A como número de 32 bits.
La instrucción incrementa el valor de su único operando
que está almacenado en memoria con lo que la ausencia de
sufijo la haría ambigua.
MOVB $-4, contador
MOV $-4, %ax
MOVL %eax, contador
MOVB $’A, %eax
INCL contador
Tabla 5.2: Instrucciones con sufijos de tamaño
5.4.1.
Instrucciones de transferencia de datos
En esta categoría se incluyen las instrucciones que permiten la transferencia de datos entre registros y memoria tales como MOV,
PUSH, POP y XCHG.
La instrucción MOV recibe dos operandos y transfiere el dato indicado por el primer operando al lugar indicado por el segundo.
Dada la restricción que impone el procesador de que en una instrucción con dos operandos no pueden estar ambos en memoria,
si se quiere transferir datos de un lugar de memoria a otro, se deben utilizar dos instrucciones y utilizar un registro de propósito
general.
Las instrucciones PUSH y POP también transfieren datos, aunque en este caso, uno de los operandos es implícito y se refiere
a la cima de la pila. La instrucción PUSH necesita como operando el dato a colocar en la cima de la pila mientras que la
instrucción POP requiere un único operando para indicar el lugar en el que depositar el dato contenido en la cima de la pila.
Ambas instrucciones modifican el registro %esp que contiene la dirección de la cima de la pila (tal y como se ha descrito en la
sección 4.3).
Estas dos instrucciones aceptan como operando una posición de memoria, por ejemplo PUSH contador. El procesador carga
en la pila el dato en memoria en la posición con nombre contador. En este caso, a pesar de que la transferencia se está
realizando de memoria a memoria, la arquitectura sí permite la operación. La restricción de dos operandos en memoria aplica
únicamente a aquellas instrucción con dos operandos explícitos.
La instrucción XCHG (del inglés exchange) consta de dos operandos e intercambia sus valores por lo que modifica los operandos
(a no ser que tengan idéntico valor). No se permite que los operandos estén ambos en memoria.
La Tabla 5.3 muestra ejemplos correctos e incorrectos de la utilización de este tipo de instrucciones. Se asume que los símbolos
contador1 y contador2 se refieren a operandos en memoria.
5.4.2.
Instrucciones aritméticas
En este grupo se incluyen aquellas instrucciones que realizan operaciones aritméticas sencillas con números enteros y naturales
tales como la suma, resta, incremento, decremento, multiplicación y división.
Programación en ensamblador de la
arquitectura IA-32
98 / 198
Instrucción
Comentario
MOV $4, %al
Almacena el valor 4 en el registro de 8 bits %al.
MOV contador1, %esi
Almacena los cuatro bytes que se encuentran en memoria a
partir de la posición que representa contador1 en el
registro %esi.
MOV $4, contador1
Instrucción ambigua, pues no se especifica el tamaño de
datos en ninguno de los dos operandos.
MOVL contador, $4
MOV %al, %ecx
Instrucción incorrecta. El segundo operando es el destino al
que mover el primer operando, por lo tanto, no puede ser
de tipo constante.
Instrucción incorrecta. El tamaño de los dos operandos es
inconsistente. El primero es un registro de 8 bits, y el
segundo es de 32.
PUSH $4
Instrucción correcta. Almacena el valor 4, codificado con
32 bits en la cima de la pila. No precisa sufijo de tamaño.
POP $4
Instrucción incorrecta. El operando indica el lugar en el
que almacenar el contenido de la cima de la pila, por tanto,
no puede ser un valor constante.
XCHG %eax, %ebx
Instrucción correcta.
XCHG %eax, contador1
Instrucción correcta.
XCHG $4, %eax
XCHG contador1, contador2
Instrucción incorrecta. Se intercambian los contenidos de
los dos operandos, por lo que ninguno de ellos puede ser
una constante.
Instrucción incorrecta. Ambos operandos están en
memoria, y el procesador no permite este tipo de
instrucciones.
Tabla 5.3: Instrucciones de transferencia de datos
Programación en ensamblador de la
arquitectura IA-32
5.4.2.1.
99 / 198
Instrucciones de suma y resta
Las instrucciones ADD y SUB realizan la suma y resta respectivamente de sus dos operandos. En el caso de la resta, la operación
realizada es la sustracción del primer operando del segundo. Como tales operaciones precisan de un lugar en el que almacenar el
resultado, el segundo operando desempeña las funciones de fuente y destino por lo que se sustituye el valor del segundo operando
por el valor resultante.
El procesador ofrece también las instrucciones INC y DEC que requieren un único operando y que incrementan y decrementan
respectivamente el operando dado. Aunque las instrucciones ADD $1, operando e INC operando realizan la misma
operación y se podría considerar idénticas, no lo son, pues INC no modifica el bit de acarreo.
La instrucción NEG recibe como único operando un número entero y realiza la operación de cambio de signo.
La Tabla 5.4 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un
operando almacenado en memoria.
Instrucción
ADDL $3, contador
Comentario
Suma la constante 3 al número de 32 bits almacenado a
partir de la posición contador. El tamaño viene
determinado por el sufijo, que en este caso es
imprescindible.
SUB %eax, contador
Deposita en memoria el número de 32 bits resultante de la
operación contador- %eax.
NEGL contador
Cambia de signo el número de 32 bits almacenado en
memoria a partir de la posición contador.
Tabla 5.4: Instrucciones aritméticas
5.4.2.2.
Instrucciones de multiplicación
La instrucción de multiplicación tiene dos variantes, IMUL y MUL para números enteros y naturales respectivamente y su formato
supone un caso especial, pues permite la especificación de entre uno y tres operandos.
La versión de IMUL y MUL con un único operando ofrece, a su vez la posibilidad de multiplicar números de 8, 16 y 32 bits.
Las instrucciones asumen que el segundo multiplicando está almacenado en el registro %al (para números de 8 bits), %ax (para
números de 16 bits) y %eax (para números de 32 bits). El tamaño del número a multiplicar se deduce del operando explícito de
la instrucción.
Si se multiplican dos operandos de n bits, el resultado tiene tamaño doble y debe representarse con 2n bits. Por tanto, si los
operandos son de 8 bits, el resultado de esta instrucción se almacena en %ax, si son de 16 bits se almacena en los 32 bits
resultantes al concatenar los registros %dx: %ax, y si los operandos son de 32 bits, en los 64 bits obtenidos al concatenar los
registros %edx: %eax. En estos dos últimos casos, los registros %dx y %edx contienen los bytes más significativos del resultado.
La versión de IMUL y MUL con dos operandos es más restrictiva que la anterior. El segundo operando puede ser únicamente
uno de los ocho registros de propósito general (no puede ser ni una constante ni un número en memoria) y el tamaño de ambos
operandos puede ser de 16 o 32 bits. Para almacenar el resultado se utiliza el mismo número de bits con los que se representan
los operandos, con lo que se corre el riesgo, si el resultado obtenido es muy elevado, de perder parte del resultado. Esta última
condición se refleja en los bits de estado del procesador.
La versión de IMUL y MUL con tres operandos es la más restrictiva de todas. Los dos primeros operandos son los multiplicandos
y el primero de ellos debe ser una constante. El tercer operando es el lugar en el que se almacena el resultado y sólo puede ser
un registro de propósito general. Al igual que la versión con dos operandos, los únicos tamaños que se permiten son de 16 y 32
bits, y el resultado se almacena en el mismo tamaño que los operandos, por lo que de nuevo se corre el riesgo de pérdida de bits
del resultado.
La Tabla 5.5 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un
operando almacenado en memoria.
Programación en ensamblador de la
arquitectura IA-32
100 / 198
Instrucción
Comentario
Multiplica el número natural 3 representado en 8 bits por el
registro implícito %al y deposita el resultado en %eax. El
tamaño de los operandos lo determina el sufijo B.
Multiplica el número entero almacenado en %eax por sí
mismo (operando implícito). El resultado se almacena en el
registro de 64 bits %edx: %eax.
Multiplica el número natural de 32 bits almacenado a partir
de la posición de memoria representada por contador
por el registro %edi en donde se almacenan los 32 bits de
menos peso del resultado.
Multiplica el número de 32 bits almacenado en memoria a
partir de la posición contador por la constante $123 y
almacena los 32 bits menos significativos del resultado
en %ecx.
MULB $3
IMUL %eax
MUL contador, %edi
IMUL $123, contador, %ecx
Tabla 5.5: Instrucciones de multiplicación
5.4.2.3.
Instrucciones de división entera
Las instrucciones de división de números naturales y enteros devuelven dos resultados, el cociente y el resto, y se almacenan
ambos valores. De manera análoga a las instrucciones de multiplicación, existen dos versiones IDIV y DIV para división de
enteros y naturales respectivamente y el tamaño del dividendo es el doble del divisor. De esta forma, se permite dividir un
número de 16 bits entre uno de 8, uno de 32 entre uno de 16 y uno de 64 entre uno de 32.
Su formato admite de forma explícita un único operando que es el divisor, y que puede ser un número de 8, 16 o 32 bits. El
dividendo es implícito y está almacenado en %ax si el divisor es de 8 bits, en el registro de 32 bits resultante de concatenar %dx: %ax si el divisor es de 16 bits, y en el registro de 64 bits resultante de concatenar %edx: %eax si el divisor es de 32
bits.
Los dos resultados que se devuelven también tienen un destino implícito y depende del tamaño de los operandos. Si el divisor es
de 8 bits el cociente se almacena en %al y el resto en %ah. Si el divisor es de 16 bits, se utilizan %ax y %dx para cociente y resto
respectivamente. En el caso de un divisor de 32 bits, el cociente se devuelve en %eax y el resto en %edx.
La Tabla 5.6 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un
operando almacenado en memoria.
Instrucción
IDIVB $-53
IDIV %eax
DIVW contador
Comentario
Divide el registro %ax por la constante $-53. El cociente
se deposita en %al y el resto en %ah.
Se divide el número de 64 bits obtenido al concatenar los
registros %edx: %eax entre el propio registro %eax.
En %eax se deposita el cociente, y en %edx el resto.
Divide el número de 32 bits almacenado en el registro
obtenido al concatenar %dx: %ax entre el número de 16
bits almacenado a partir de la posición de memoria
indicada por contador. En %ax se almacena el cociente
y en %dx el resto.
Tabla 5.6: Instrucciones de división
5.4.3.
Instrucciones lógicas
En este grupo se incluyen las instrucciones de conjunción, disyunción, disyunción exclusiva y negación. La aplicación práctica
de estas instrucciones no es a primera vista del todo aparente, sin embargo, suelen estar presentes en la mayoría de programas.
Programación en ensamblador de la
arquitectura IA-32
101 / 198
Las cuatro instrucciones lógicas consideradas son AND, OR, NOT y XOR para la conjunción, disyunción, negación y disyunción
exclusiva, respectivamente.
Estas instrucciones tienen en común que realizan sus operaciones ‘bit a bit’. Es decir, el procesador realiza tantas operaciones
lógicas como bits tienen los operandos tomando los bits que ocupan la misma posición y, por tanto, produciendo otros tantos
resultados.
Considérese el caso de la instrucción de conjunción AND con sus dos operandos. Al igual que en el caso de instrucciones como
la de suma o resta, el segundo operando es a la vez fuente y destino. El procesador obtiene un resultado de igual tamaño que sus
operandos y en el que cada bit es el resultado de la conjunción de los bits de idéntica posición de los operandos. Las instrucciones
de disyunción (OR) y disyunción exclusiva (XOR) se comportan de forma análoga.
La instrucción NOT tiene un único operando que es fuente y destino y cambia el valor de cada uno de sus bits.
La Tabla 5.7 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un
operando almacenado en memoria.
Instrucción
Comentario
Calcula la conjunción bit a bit entre la constante $-1 y el
registro %eax. ¿Qué valor tiene %eax tras ejecutar esta
instrucción?
Calcula la disyunción bit a bit entre la constante $1 y el
número de 32 bits almacenado en memoria a partir de la
posición denotada por contador.
Cambia el valor de los 32 bits almacenados a partir de la
posición de memoria que denota contador. El sufijo de
tamaño es necesario para definir el tamaño del operando.
AND $-1, %eax
ORL $1, contador
NOTL contador
Tabla 5.7: Instrucciones lógicas
5.4.4.
Instrucciones de desplazamiento y rotación
En este grupo se incluyen instrucciones que mediante desplazamientos efectúan operaciones aritméticas de multiplicación y división por potencias de dos. Además, se incluyen también instrucciones que manipulan sus operandos como si los bits estuviesen
dispuestos de forma circular y permite rotaciones en ambos sentidos.
5.4.4.1.
Instrucciones de desplazamiento
Las instrucciones de desplazamiento se subdividen a su vez en dos categorías: desplazamiento aritmético y desplazamiento
lógico.
Las instrucciones de desplazamiento aritmético son aquellas que equivalen a multiplicar y dividir un número por potencias de
2. Un desplazamiento de un bit quiere decir que cada uno de ellos pasa a ocupar la siguiente posición (a derecha o izquierda) y
por tanto, dependiendo de cómo se introduzcan nuevos valores y cómo se descarte el bit sobrante, dicha operación es idéntica a
multiplicar por 2.
En adelante se asume que el bit más significativo de un número es el de más a su izquierda. La figura 5.5 muestra un desplazamiento aritmético a izquierda y derecha de un número de 8 bits.
Programación en ensamblador de la
arquitectura IA-32
102 / 198
Figura 5.5: Desplazamiento aritmético de 1 bit en un número de 8 bits
Para que la equivalencia entre los desplazamientos de bits y la operación aritmética de multiplicación y división por 2 sean
realmente equivalentes hay que tener en cuenta una serie de factores.
Si se desplaza un número a la izquierda, el nuevo bit menos significativo debe tener el valor cero.
Si se desplaza a la izquierda un número natural con su bit más significativo a uno se produce desbordamiento.
Si se desplaza un número a la derecha, el nuevo bit más significativo debe tener valor idéntico al antiguo.
Las instrucciones SAL (Shift Arithmetic Left) y SAR (Shift Arithmetic Right) desplazan su segundo operando a izquierda y
derecha respectivamente tantas veces como indica el primer operando. En ambas instrucciones, el último bit que se ha descartado
se almacena en el bit de acarreo CF. Estas instrucciones tienen la limitación adicional de que el primer operando sólo puede ser
una constante o el registro %cl.
La Tabla 5.8 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un
operando almacenado en memoria.
Instrucción
SAR $4, %eax
SALB %cl, contador
Comentario
Desplaza 4 bits a la derecha el contenido del registro %eax.
Esta operación es equivalente a multiplicar por 16 el
registro %eax.
Desplaza el byte almacenado en la posición de memoria
denotada por contador tantas posiciones a la izquierda
como indica el registro %cl. El sufijo de tamaño es
necesario porque a pesar de que el primer operando es un
registro, éste contiene sólo el número de posiciones
desplazar. El tamaño de los datos se deduce, por tanto del
segundo operando.
Tabla 5.8: Instrucciones de desplazamiento aritmético
Las instrucciones de desplazamiento no aritmético son SHR y SHL para desplazar a derecha e izquierda respectivamente. El
comportamiento y restricciones son idénticas a las instrucciones anteriores con una única diferencia. Los nuevos bits que se
insertan en los operandos tienen siempre el valor cero. Por tanto, dependiendo de los valores de los operandos, las instrucciones
SAR y SAL se pueden comportar de forma idéntica.
Programación en ensamblador de la
arquitectura IA-32
5.4.4.2.
103 / 198
Instrucciones de rotación
Las instrucciones de rotación permiten manipular un operando como si sus bits formasen un círculo y se rotan en ambos sentidos
un número determinado de posiciones.
Las instrucciones ROL y ROR rotan a izquierda y derecha respectivamente el contenido de su segundo operando tantas posiciones
como indica el primer operando. El último bit que ha traspasado los límites del operando se almacena en el bit de acarreo CF.
Las instrucciones RCL y RCR son similares a las anteriores con la excepción que el bit de acarreo CF se considera como parte
del operando. El bit que sale del límite del operando se carga en CF y éste a su vez pasa a formar parte del operando.
La figura 5.6 ilustra el funcionamiento de estas instrucciones.
Figura 5.6: Rotación de un operando de 8 bits
Al igual que las instrucciones de desplazamiento aritmético, el primer operando puede ser o una constante o el registro %cl.
El tamaño del dato a manipular se deduce del segundo operando, y si este está en memoria, a través del sufijo de tamaño de la
instrucción.
La Tabla 5.9 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un
operando almacenado en memoria.
Instrucción
RCR $4, %ebx
RCLL %cl, contador
ROR %cl, %eax
ROLL %cl, contador
Comentario
Rota el registro %ebx cuatro posiciones a su derecha
utilizando el bit de acarreo CF.
Rota a la izquierda tantas posiciones como indica el
registro %cl el operando de 32 bits almacenado en
memoria a partir de la posición denotada por contador.
A pesar de que el primer operando es un registro, la
instrucción necesita sufijo de tamaño, pues éste se deduce
únicamente del segundo operando que está en memoria.
Rota a la derecha el registro %eax tantas posiciones como
indica el registro %cl. El bit CF almacena el bit más
significativo del resultado.
Rota a la izquierda tantas posiciones como indica el
registro %cl el número de 32 bits almacenado en memoria
a partir de la posición contador. De nuevo se precisa el
sufijo de tamaño porque éste se deduce únicamente a la
vista del segundo operando.
Tabla 5.9: Instrucciones de rotación
5.4.5.
Instrucciones de salto
El procesador ejecuta una instrucción tras otra de forma secuencial a no ser que dicho flujo de ejecución se modifique. Las
instrucciones de salto sirven para que el procesador, en lugar de ejecutar la siguiente instrucción, pase a ejecutar otra en un lugar
Programación en ensamblador de la
arquitectura IA-32
104 / 198
que se denomina ‘destino del salto’.
La instrucción de salto JMP (del inglés jump) tiene un único operando que representa el lugar en el que el procesador debe
continuar ejecutando. Al llegar a esta instrucción, el procesador no realiza operación alguna y simplemente pasa a ejecutar
la instrucción en el lugar especificado como destino del salto. El único registro, por tanto, que se modifica es el contador de
programa.
A la instrucción JMP se le denomina también de salto incondicional por contraposición a las instrucciones de salto en las que el
procesador puede saltar o no al destino dependiendo de una condición.
La arquitectura IA-32 dispone de 32 instrucciones de salto condicional. Todas ellas comienzan por la letra J seguida de una
abreviatura de la condición que determina si el salto se lleva a cabo o no. Al ejecutar esta instrucción el procesador consulta esta
condición, si es cierta continua ejecutando la instrucción en la dirección destino del salto. Si la condición es falsa, la instrucción
no tienen efecto alguno sobre el procesador y se ejecuta la siguiente instrucción.
Las condiciones en las que se basa la decisión de saltar dependen de los valores de los bits de estado CF, ZF, OF, SF y PF. La
Tabla 5.10 muestra para cada instrucción los valores de estos bits para los que se salta a la instrucción destino.
Instrucción
Condición
Descripción
Salto si mayor,
salto si no menor
o igual (sin
signo)
JA mem
CF = 0 y ZF = 0
JNBE mem
JAE mem
Salto si mayor o
igual, salto si no
menor (sin signo)
CF = 0
JNB mem
JE mem
Salto si igual,
salto si cero.
ZF = 1
JZ mem
JG mem
ZF = 0 y SF = OF
JNLE mem
JGE mem
SF = OF
JNL mem
JC mem
CF = 1
JCXZ mem
%cx = 0
JO mem
OF = 1
JPO mem
PF = 0
JNP mem
JS mem
SF = 1
Salto si mayor, si
no menor o igual
(con signo)
Salto si mayor o
igual, si no
menor (con
signo)
Salto si acarreo
es uno
Salto si
registro %cx es
cero.
Salto si el bit de
desbordamiento
es uno.
Instrucción
Condición
JBE mem
Descripción
CF = 1 ó ZF = 1
Salto si menor o
igual, salto si no
mayor (sin signo)
CF = 1
Salto si menor,
salto si no mayor
o igual (sin
signo)
ZF = 0
Salto si diferente,
salto si no cero.
ZF = 1 ó SF !=
OF
Salto si menor o
igual, si no
mayor (con
signo)
SF != OF
Salto si menor, si
no mayor o igual
(con signo)
JNC mem
CF = 0
Salto si acarreo
es cero
JECXZ mem
%ecx = 0
JNO mem
OF = 0
JNA mem
JB mem
JNAE mem
JNE mem
JNZ mem
JLE mem
JNG mem
JL mem
JNGE mem
Salto si paridad
impar, si no
paridad.
JPE mem
Salto si positivo.
JNS mem
Salto si
registro %ecx es
cero.
Salto si el bit de
desbordamiento
es cero.
PF = 1
Salto si paridad
par, si paridad.
SF = 0
Salto si negativo.
JP mem
Tabla 5.10: Instrucciones de salto condicional
Programación en ensamblador de la
arquitectura IA-32
105 / 198
En la tabla se incluyen instrucciones con diferente nombre e idéntica condición. Estos sinónimos son a nivel de lenguaje ensamblador, es decir, las diferentes instrucciones tienen una codificación idéntica y por tanto corresponden con la misma instrucción
máquina del procesador.
La utilidad de estas instrucciones se debe entender en el contexto del flujo normal de ejecución de un programa. El resto de instrucciones realizan diferentes operaciones sobre los datos, y a la vez modifican los bits de la palabra de estado. Las instrucciones
de salto se utilizan después de haber modificado estos bits y para poder tener dos posibles caminos de ejecución.
El ejemplo 5.4 muestra una porción de código ensamblador muestra un posible uso de las instrucciones de salto.
Ejemplo 5.4 Uso de saltos condicionales
dest2:
MOV $100, %ecx
DEC %ecx
JZ dest1
ADD %ecx, %eax
JMP dest2
La instrucción DEC %ecx decrementa el valor del registro %ecx y modifica los bits de la palabra de estado. La instrucción JZ
provoca un salto si ZF = 1. Como consecuencia, la instrucción ADD %ecx, %eax se ejecuta un total de 100 veces.
Las instrucciones de salto condicional son útiles siempre y cuando los valores de los bits de estado hayan sido previamente
producidos por instrucciones anteriores, como por ejemplo, operaciones aritméticas. Pero en algunos casos, la ejecución de un
salto condicional requiere que se realice una operación aritmética y no se almacene su resultado, sino simplemente que se realice
una comparación. Por ejemplo, si se necesita saltar sólo si un número es igual a cero, en lugar de ejecutar una instrucción ADD,
SUB, INC o DEC para que se modifique el bit ZF sólo se necesita comprobar si tal número es cero y modificar los bits de estado.
Para este cometido el procesador dispone de las instrucciones de comparación y comprobación.
5.4.6.
Instrucciones de comparación y comprobación
Las instrucciones CMP (comparación) y TEST (comprobación) realizan sendas operaciones aritméticas de las que no se guarda
el resultado obtenido sino que únicamente se modifican los bits de estado.
La instrucción CMP recibe dos operandos. El primero de ellos puede ser de tipo constante, registro u operando en memoria. El
segundo puede ser únicamente de tipo registro u operando en memoria. La instrucción no permite que ambos operandos estén
en memoria. Al ejecutar esta instrucción se resta el primer operando del segundo. El valor resultante no se almacena en lugar
alguno, pero sí se modifican los bits de estado del procesador.
Considérese el código mostrado en el ejemplo 5.5. La instrucción de comparación modifica los bits de estado para que la instrucción de salto los interprete y decida si debe saltar o continuar ejecutando la instrucción ADD.
Ejemplo 5.5 Instrucción de comparación antes de salto condicional
CMP $0, %eax
JE destino
ADD %eax, %ebx
# Se calcula %eax - 0
La instrucción JE produce un salto cuando el bit de estado ZF tiene el valor 1. Este bit, a su vez se pone a uno si los operandos
de la instrucción CMP son iguales. Por tanto, la instrucción JE, cuando va a continuación de una instrucción de comparación, se
puede interpretar como ‘salto si los operandos (de la instrucción anterior) son iguales’.
En la mayoría de las instrucciones de salto condicional detalladas en la sección 5.4.5, las últimas letras del nombre hacen
referencia a la condición que se comprueba cuando se ejecutan a continuación de una instrucción de comparación. Por ejemplo,
la instrucción JLE produce un salto cuando los bits de condición cumplen ZF = 1 o SF != OF. Si esta instrucción va precedida de
una instrucción de comparación, ZF es igual a 1 si los dos operandos son iguales. Si SF es diferente a OF la resta ha producido
un bit de signo, y el bit de desbordamiento con valores diferentes. Esta situación se produce si el segundo operando es menor
que el primero, de ahí el sufijo LE (del inglés less or equal) en la instrucción de salto. La Tabla 5.11 muestra las combinaciones
obtenidas del bit de desbordamiento y la resta para el caso de enteros representados con 2 bits.
Programación en ensamblador de la
arquitectura IA-32
B
OF, A-B
(-2) 10
(-1) 11
(0) 00
(1) 01
A
106 / 198
(-2) 10
0, 00
0, 01
1, 10
1, 11
(-1) 11
0, 11
0, 00
0, 01
1, 10
(0) 00
0, 10
0, 11
0, 00
0, 01
(1) 01
1, 01
0, 10
0, 11
0, 00
Tabla 5.11: Resta y bit de desbordamiento de dos enteros de 2 bits
El bit de signo y el de desbordamiento tienen valores diferentes únicamente en el caso en que el primer operando de la resta es
menor que el segundo. Por tanto, la instrucción JLE si se ejecuta a continuación de una instrucción CMP se garantiza que el salto
se lleva a cabo si el segundo operando es menor que el primero.
Las instrucciones de salto cuya condición puede interpretarse con respecto a la instrucción de comparación que le precede son
las que en la descripción mostrada en la tabla Tabla 5.10 incluyen una comparación. Aunque estas instrucciones no debe ir
necesariamente precedidas por una instrucción de comparación porque la condición se evalúa con respecto a los bits de estado,
generalmente se utilizan acompañadas de éstas.
Para interpretar el comportamiento de una instrucción de comparación seguida de una de salto condicional se puede utilizar la
siguiente regla mnemotécnica:
Salto condicional precedido de comparación
Dada la siguiente secuencia de dos instrucciones en ensamblador:
CMP B, A
Jcond
donde A y B son cualquier operando y cond es cualquiera de las condiciones posibles, el salto se lleva a cabo si se cumple A
cond B.
Por ejemplo, si la instrucción CMP $4, %eax va seguida del salto condicional JL destino, el procesador saltará a destino
si %eax < 4.
La Tabla 5.12 muestra posibles secuencias de instrucciones de comparación y salto condicional.
La posibilidad de saltar a una posición de código dependiendo de una condición está presente en la mayoría de lenguajes de
programación de alto nivel. Por ejemplo, en el lenguaje Java, la construcción if () {} else {} se implementa a nivel de
ensamblador basado en instrucción de salto condicional.
La instrucción de comprobación TEST es similar a la de comparación, también consta de dos operandos, el segundo de ellos
puede ser únicamente de tipo registro o memoria y no se permite que ambos sean de tipo memoria. La diferencia con CMP es que
se realiza una conjunción bit a bit de ambos operandos. El resultado de esta conjunción tampoco se almacena, pero sí modifica
los bits de estado OF, CF (ambos se ponen a cero), SF, ZF y PF.
La Tabla 5.13 muestra posibles secuencias de instrucciones de comprobación y salto condicional.
5.4.7.
Instrucciones de llamada y retorno de subrutina
Una de las construcciones más comunes en la ejecución de programas es la invocación de porciones de código denominadas
subrutinas con un conjunto de parámetros. Este mecanismo es en el que está basada la invocación de procedimientos, métodos o
funciones en los lenguajes de programación de alto nivel.
Para implementar este mecanismo, el procesador dispone de dos instrucciones. La instrucción CALL tiene un único parámetro
que es la posición de memoria de la primera instrucción de una subrutina. El efecto de esta instrucción es similar a la de salto
incondicional con la diferencia de que el procesador guarda ciertos datos en lugares para facilitar el retorno una vez terminada la
la ejecución de la subrutina.
Programación en ensamblador de la
arquitectura IA-32
Código
inicio: inc
cmp
jae
...
jmp
final: mov
...
menor:
final:
%eax
$128, %eax
final
inicio
$’A, %cl
cmp $12, %eax
jle menor
mov $10, %eax
....
jmp final
mov $100, %eax
...
inc %ebx
107 / 198
Comentario
El salto a final se produce si el registro %eax contiene
un valor mayor o igual a 128. La condición del salto es
para operandos sin signo, es decir, el resultado de la
comparación se interpreta como si los operandos fuesen
números naturales.
El salto a menor se produce si el registro %eax es menor o
igual que 12. La condición del salto es para operandos con
signo (números enteros).
Tabla 5.12: Secuencias de instrucciones de comparación y salto condicional
Código
testl $0x0080, contador
jz ignora
....
ignora: incl %ebx
pl:
test 0xFF00FF00, %eax
jnz pl
....
jmp final
mov %eax
...
Comentario
El salto a ignora se produce si el operando de 32 bits
almacenado en memoria a partir de la posición contador
tiene su octavo bit igual a cero. Esta instrucción precisa el
sufijo de tamaño.
El salto a pl se produce si alguno de los bits en las
posiciones 8 a 15 o 24 a 31 del registro %eax es igual a
uno.
Tabla 5.13: Secuencias de instrucciones de comprobación y salto condicional
Programación en ensamblador de la
arquitectura IA-32
108 / 198
La instrucción RET es la que se utiliza al final de una subrutina para retomar la ejecución en el punto anterior a la invocación mediante la instrucción CALL. No recibe ningún parámetro y el procesador gestiona internamente el lugar en el que debe continuar
la ejecución.
En el capítulo 8 se estudia con todo detalle la utilización de estas instrucciones para implementar construcciones presentes en
lenguajes de programación de alto nivel.
5.5.
Ejercicios
1. Utilizando cualquier buscador de internet, localiza los tres volúmenes del documento IA-32 Intel Architecture Software
Developer’s Manual. Utilizando el volumen 2, responde a las siguientes preguntas:
a. Una duda común sobre la instrucción de pila POP es la siguiente. El incremento del registro apuntador de pila %esp,
¿se hace antes o después de escribir el dato de la cima de la pila en el lugar indicado en la instrucción?
b. ¿Qué código de operación en hexadecimal tiene la instrucción PUSH $4?
c. ¿Qué hace la instrucción LAHF? ¿Cuántos operandos recibe?
d. ¿Qué hace la operación NOP? ¿Qué diferencia hay entre la instrucción NOP y la instrucción XCHG %eax, %eax?
e. ¿Qué hace la instrucción STC?
f. ¿Qué flags de la palabra de estado modifica la ejecución de una instrucción de resta?
2. Pensar una situación en un programa en la que la única posibilidad de multiplicar dos números sea mediante la instrucción
con un único operando.
3. Enunciar las condiciones que deben cumplir los operandos para que las instrucciones SAL y SHL se comporten de forma
idéntica. Enunciar estas condiciones para las instrucciones SAR y SHR.
Programación en ensamblador de la
arquitectura IA-32
109 / 198
Capítulo 6
El programa ensamblador
Los programas escritos en lenguaje ensamblador, a pesar de representar instrucciones del lenguaje máquina del procesador, no
son directamente ejecutables por éste sino que es necesario traducirlas a su codificación en binario. Este proceso de traducción es
fácilmente automatizable, y por tanto se dispone de programas denominados ensambladores (o más genéricamente compiladores
que se encargan de esta tarea.
El ensamblador es un programa que recibe como datos de entrada uno o varios ficheros de texto plano con un conjunto de
instrucciones y datos escritos en lenguaje ensamblador y produce un fichero binario y ejecutable que contiene la codificación
binaria del programa. La figura 6.1 muestra el funcionamiento del programa ensamblador.
Figura 6.1: El programa ensamblador
En general, a los programas encargados de traducir de un lenguaje de programación a otro se les denomina ‘compiladores’ y todos
ellos trabajan de forma similar. Dado un conjunto de ficheros escritos en un lenguaje, producen como resultado otro fichero que
contiene la traducción a un segundo lenguaje. En el caso del ensamblador, la traducción es de lenguaje ensamblador a lenguaje
máquina.
En adelante se utilizarán los términos ‘compilador’ y ‘ensamblador’ de forma indistinta y siempre en referencia al programa que
traduce de lenguaje ensamblador a lenguaje máquina.
Así como el lenguaje máquina de un procesador es único e inmutable (a no ser que se rediseñe el procesador), pueden coexistir
múltiples lenguajes ensamblador que representen el mismo lenguaje máquina. La representación de las instrucciones mediante
cadenas alfanuméricas es un convenio utilizado para facilitar su escritura, por lo que pueden existir múltiples convenios de este
tipo siempre y cuando se disponga del ensamblador los que traduzca al lenguaje máquina del procesador.
En el caso concreto del sistema operativo Linux, se incluye como parte de las herramientas del sistema un compilador capaz
de traducir de lenguaje ensamblador a lenguaje máquina. Su nombre es as. En la práctica este programa lo suelen invocar otros
compiladores tales como gcc que es un compilador del lenguaje de alto nivel C a lenguaje máquina, pero también permite la
traducción de ficheros con código ensamblador invocando internamente el programa as.
Programación en ensamblador de la
arquitectura IA-32
6.1.
110 / 198
Creación de un programa ejecutable en ensamblador
La figura 6.2 muestra un programa en lenguaje ensamblador creado mediante un editor de texto plano, un programa que guarda
únicamente el texto codificado en formato ASCII o UNICODE sin información alguna sobre estilo. El primer paso, por tanto,
para la obtención de un programa ejecutable es la creación de un fichero de texto que contenga el código.
Figura 6.2: Estructura de un programa en ensamblador
Un programa consta de varias secciones separadas cada una de ellas por palabras clave que comienzan por el símbolo ‘.’. La
palabra .data que aparece en la primera línea no tiene traducción alguna para la ejecución, sino que es la forma de notificar al
ensamblador que a continuación se encuentran definidos conjunto de datos. A este tipo de palabras que comienzan por punto se
les denomina ‘directivas’.
El programa tiene definido un único dato que se representa como una secuencia de caracteres. La línea que contiene .asciz
(también una directiva) seguida del string entre comillas es la que instruye al ensamblador para crear una zona de memoria con
datos, y almacenar en ella el string que se muestra terminado por un byte con valor cero. El efecto de la directiva .asciz es
que, al comienzo de la ejecución de programa, este string esté almacenado en memoria.
Antes de la directiva .asciz se incluye la palabra dato seguida por dos puntos. Esta es la forma de definir una etiqueta o
nombre que luego se utilizará en el código para acceder a estos datos.
La línea siguiente contiene la directiva .text que denota el comienzo de la sección de código. La directiva .global main
comunica al ensamblador que la etiqueta main es globalmente accesible desde cualquier otro programa.
A continuación se encuentran las instrucciones en ensamblador propiamente dichas. Al comienzo del código se define la etiqueta
main que identifica el punto de arranque del programa.
Una vez creado y guardado el fichero de texto con el editor, se debe invocar el compilador. En una ventana en el que se ejecute un
intérprete de comandos y situados en el mismo directorio en el que se encuentra el fichero ejemplo.s se ejecuta el siguiente
comando:
gcc -o ejemplo ejemplo.s
Programación en ensamblador de la
arquitectura IA-32
111 / 198
El compilador realiza una tarea similar a la de un compilador de un lenguaje de alto nivel como Java. Si hay algún error en el
programa se muestra la línea y el motivo. Si el proceso de traducción es correcto, se crea un fichero ejecutable. En el comando
anterior, se ha instruido al ensamblador, por medio de la opción -o ejemplo para que el programa resultante se deposite en el
fichero con nombre ejemplo.
El compilador también es capaz de procesar más de un fichero de forma simultanea. Esto es útil cuando el código de un programa
es muy extenso y está fraccionado en varios ficheros que deben combinarse para obtener un único ejecutable. En tal caso el
comando para compilar debe incluir el nombre de todos los ficheros necesarios.
Si el compilador no detecta ningún error en la traducción, el fichero ejemplo está listo para ser ejecutado por el procesador. Para
ello simplemente se escribe su nombre en el intérprete de comandos (en la siguiente línea, el símbolo $ representa el mensaje
que imprime siempre el intérprete de comandos):
$ ejemplo
Mi Primer Programa Ensamblador
$
Todo programa ensamblador debe seguir el siguiente patrón:
.data
# Comienzo del segmento de datos
<datos del programa>
.text
.global main
# Comienzo del código
# Obligatorio
main:
<Instrucciones>
ret
# Obligatorio
Se pueden incluir comentarios en el código a partir de símbolo ‘#’ hasta el final de línea y son ignorados por el compilador.
Basado en este patrón, el programa de la figura 6.2 ha ejecutado las instrucciones:
push %eax
push %ecx
push %edx
push $dato
call printf
add $4, %esp
pop %edx
pop %ecx
pop %eax
ret
Las primeras tres instrucciones depositan los valores de los registros %eax, %ecx y %edx en la pila. Las tres instrucciones
siguientes se encargan de poner la dirección del string también en la pila (instrucción push), invocar la rutina externa printf
que imprime el string (instrucción call) y sumar una constante al registro %esp para restaurar el valor inicial del puntero a la
cima de la pila. Las tres últimas instrucciones restauran el valor original en los registros previamente guardados en la pila.
A continuación se estudia en detalle la sintaxis de las diferentes construcciones permitidas en el lenguaje ensamblador.
6.2.
Definición de datos
Como todo lenguaje de programación, se permiten definir tipos de datos así como su contenido. En el caso del ensamblador,
estos tipos no permiten estructuras complejas ni heterogéneas. Todas las definiciones deben incluirse en una sección del código
Programación en ensamblador de la
arquitectura IA-32
112 / 198
que comience por la directiva .data. Los datos se almacenan en posiciones contiguas de memoria, es decir, dos definiciones
seguidas hacen que los datos se almacenen uno a continuación de otro.
La principal dificultad para manipular los datos en ensamblador es que cuando el procesador accede a ellos, no se realiza ningún
tipo de comprobación. Aunque se definan datos con cierto tamaño y estructura en memoria, el procesador trata estos datos como
una simple secuencia de bytes. Esta es una diferencia sustancial con los lenguajes de programación de alto nivel tales como
Java. La definición de datos en ensamblador se realiza a través de directivas (descritas a continuación) que únicamente reservan
espacio en memoria con los datos pertinentes, pero no se almacena ningún tipo de información sobre su tamaño.
Los lenguajes de alto nivel contienen lo que se conoce como un ‘sistema de tipos’ que consiste en un conjunto de reglas que
permiten la definición de tipos de datos así como el mecanismo para comprobar su corrección. En ensamblador, al tratarse de
los datos que manipula directamente el procesador, no se dispone de tal sistema, y por tanto se manejan como si fuese simples
secuencias de bytes.
6.2.1.
Definición de bytes
La definición de valores numéricos almacenados en bytes se realiza mediante la directiva .byte seguida de uno o varios valores
separados por comas. Cuando el programa comienza la ejecución, se han inicializado tantas posiciones en memoria como indica
la directiva con los valores dados. El ejemplo 6.1 muestra ejemplos de utilización de la directiva .byte así como los valores
almacenados en memoria.
Ejemplo 6.1 Definición de bytes en ensamblador y sus valores en memoria
datos:
.byte 38, 0b11011101, 0xFF, ’A, ’ ←b
Si el valor numérico especificado es menor que cero o mayor que 255 el compilador notifica la anomalía con un error.
6.2.2.
Definición de enteros
La definición de enteros de 32 bits se hace mediante la directiva .int seguida de un número o una lista de números enteros
separados por comas. Los números se codifican con 4 bytes almacenados en little endian. El ejemplo 6.2 muestra ejemplos de
definiciones de enteros.
Ejemplo 6.2 Definiciones de números enteros y sus valores en memoria
nums:
.int 3, 4, 5
.int 0x12AB, 0x10ab, 0b111000, 0 ←B111000
.int 21
.int 07772
La directiva .long es un sinónimo de .int y también define enteros de 32 bits. Las directivas .word y .quad son análogas
a las anteriores pero definen enteros de 16 y 64 bits respectivamente.
Programación en ensamblador de la
arquitectura IA-32
6.2.3.
113 / 198
Definición de strings
La definición de strings se puede hacer con dos formatos diferentes mediante la utilización de tres directivas. La directiva .ascii permite la definición de uno o más strings entre comillas y separadas por comas. Cada símbolo de cada strings codifica con
un byte en ASCII utilizando posiciones consecutivas de memoria. Se utilizan tantos bytes como la suma de los símbolos de cada
string.
La directiva .asciz es similar a la anterior, se escribe seguida de uno o más strings separados por comas, pero cada uno de
ellos se codifica añadiendo un byte con valor cero a final del string. Este formato se suele utilizar para detectar el final del string.
La directiva .string es un sinónimo de la directiva .asciz.
El ejemplo 6.3 muestra la utilización de las directivas de definición de strings y los valores que se almacenan en memoria. Los
bytes resaltados corresponden son los que añaden las directivas .asciz y .string al final de cada string.
Ejemplo 6.3 Definición de strings y sus valores en memoria
msg:
6.2.4.
.ascii "S 1", "S 2"
.asciz "S 3", "S 4"
.string "S final"
Definición de espacio en blanco
La directiva .space seguida de dos números separados por una coma permite la reserva de espacio en memoria. El primer valor
denota el número de bytes que se reservan y el segundo es el valor que se utiliza para inicializar dichos bytes y debe estar entre
0 y 255. En el caso de que este parámetro se omita, la memoria se reserva inicializada al valor cero.
El uso principal de esta directiva es para reservar espacio que, o se debe inicializar al mismo valor, o su valor será calculado y
modificado por el propio programa. El ejemplo 6.4 muestra el uso de la directiva así como su efecto en memoria.
Ejemplo 6.4 Definiciones de espacio en blanco y su valor en memoria
result: .space 4, 0
.space 4
.space 8, 0xFF
6.3.
Uso de etiquetas
En lenguaje ensamblador se permite la definición de un conjunto de datos y las instrucciones para manipularlos que se traducen a
su codificación binaria y se produce un fichero ejecutable. Antes de comenzar la ejecución del programa, los datos e instrucciones
en binario se cargan en la memoria RAM del sistema. Pero ¿en qué posición de memoria está almacenado el programa?
El valor de esta dirección de memoria, o de la dirección en la que está almacenado cualquier dato o instrucción, no se sabe hasta
el momento en el que se ejecuta el programa porque es el sistema operativo el que lo decide, y tal decisión se aplaza hasta el
Programación en ensamblador de la
arquitectura IA-32
114 / 198
último instante para así poder ubicar cada programa en el lugar más conveniente en memoria. El sistema operativo está ejecutando
múltiples programas de forma simultánea, y por tanto, necesita esta flexibilidad para poder hacer un mejor uso de la memoria.
Pero, si no se sabe el valor de la dirección de memoria de ningún dato ni instrucción, ¿cómo se puede, por ejemplo, acceder a un
dato en memoria? Para ello se precisa su dirección, pero el valor numérico de esta no se sabe cuando se escribe un programa.
El lenguaje ensamblador soluciona este problema mediante el uso de ‘etiquetas’. Las etiquetas no son más que nombres que se
ponen al comienzo de una línea (ya sea definición de datos o una instrucción) seguido por dos puntos. Dicho nombre representa
la posición de memoria en la que está almacenado el dato o instrucción definido justo a continuación.
Estas etiquetas son, por tanto, un punto de referencia en memoria que el ensamblador sabe interpretar de forma correcta y que en
el momento de ejecución serán reemplazados por el valor numérico de la dirección de memoria pertinente.
La definición de una etiqueta no sólo permite referirse a los datos almacenados en esa posición, sino que ofrece un mecanismo
por el que acceder a los datos en posiciones cercanas a ella mediante simples operaciones aritméticas sobre la dirección que
representa. Considérese de nuevo la representación en memoria de los enteros definidos en el ejemplo 6.2. La figura 6.3 ilustra
como se pueden deducir las direcciones de los demás enteros en base al símbolo nums:.
Figura 6.3: Etiqueta y direcciones relativas a ella
Dado que la directiva .int define valores enteros representados por 32 bits, de la definición de la etiqueta nums se pueden
deducir los valores de las direcciones en las que se almacenan el resto de números enteros definidos. Para efectuar estos cálculos
es imprescindible saber el tamaño de la información almacenada a partir de la etiqueta.
Las etiquetas, por tanto, se pueden definir en cualquier lugar del código y son únicamente símbolos que representan una dirección
de memoria cuyo valor no se sabe y se utilizan como puntos de referencia para acceder a los datos en memoria de su alrededor.
Pero su uso en ensamblador tiene dos versiones igualmente útiles. La primera es acceder al valor contenido en la posición de
memoria a la que se refieren. Para ello se incluye en las instrucciones ensamblador el nombre de la etiqueta tal cual se ha definido
(sin los dos puntos).
Pero a menudo es necesario manipular la propia dirección de memoria que representa dicha etiqueta. Aunque dicho valor es
desconocido, nada impide que se escriban instrucciones máquina que operen con él. El ensamblador permite referirse al valor de
la dirección de memoria que representa una etiqueta precediendo su nombre del símbolo $.
Si una instrucción ensamblador contiene como operando el nombre de una etiqueta, este operando es de tipo dirección de memoria
(ver la sección 5.3.3). En cambio, si el operando es el nombre de una etiqueta precedido por $, este operando es de tipo constante.
Esta nomenclatura para diferenciar entre el valor al que apunta una etiqueta y su valor como dirección en memoria es consistente
con la nomenclatura de operandos. Dada una etiqueta, de los dos valores, al que apunta en memoria y su dirección, es este último
el que permanece constante a lo largo de la ejecución, y por tanto se representa con el prefijo $. En cambio, el valor en memoria
al que apunta es variable y por ello se representa únicamente por el nombre de la etiqueta.
El ejemplo 6.5 muestra una porción de código en la que se define una etiqueta y se manipula mediante instrucciones máquina.
Programación en ensamblador de la
arquitectura IA-32
115 / 198
Ejemplo 6.5 Definición y manipulación de etiquetas
dato:
.data
.int 3, 4
.string "Mensaje"
.byte 17
...
mov dato, %eax
add $5, %eax
mov %eax, dato
movl $4, dato
...
mov $dato, %ebx
add $8, %ebx
...
La etiqueta dato corresponde con la dirección de memoria en la que está almacenado el entero de 32 bits con valor 3. En el
primer grupo de instrucciones, la instrucción mov dato, %eax mueve el número 3 al registro %eax. Nótese que el operando
carece del prefijo $ y por tanto se refiere al valor almacenado en memoria. A continuación se suma la constante 5 y se transfiere
el valor en %eax de nuevo a la posición de memoria referida por dato.
La instrucción movl $4, dato requiere especial atención. El sufijo de tamaño es necesario para desambiguarla porque ni la
constante $4 ni el segundo operando contienen información sobre su tamaño.La información de una etiqueta es únicamente la
dirección a la que representa sin ningún tipo de información sobre el tamaño de los datos. Por tanto, a pesar de que dato ha sido
definida en una línea en la que se reserva espacio para enteros, cuando se utiliza en una instrucción y el otro operando tampoco
ofrece información sobre el tamaño, requiere el sufijo.
En el segundo grupo de instrucciones, la instrucción mov $dato, %ebx carga en el registro %ebx el valor de la dirección de
memoria que representa la etiqueta. Este valor es imposible de saber en tiempo de programación, pero se puede manipular al
igual que cualquier otro número. Tras ejecutar la última instrucción, el registro %ebx contiene la dirección de memoria en la que
está almacenada la primera letra del string. Esto se deduce de las definiciones de datos y sus tamaños. Los dos números ocupan
8 bytes, con lo que en la posición $dato + 8 se encuentra la letra ‘M’ del string Mensaje.
Las etiquetas no sólo se utilizan en las definiciones de datos sino también en instrucciones del código. Los destinos de los saltos
reciben como operando una dirección de memoria, que por tanto debe ser una etiqueta.
Se pueden definir tantas etiquetas como sea preciso en un programa sin que por ello se incremente el tamaño del programa. Las
etiquetas son símbolos que utiliza el programa ensamblador para utilizar en lugar de los valores numéricos de las direcciones que
se sabrán cuando el programa comience su ejecución.
El compilador gcc utilizado para traducir de lenguaje ensamblador a lenguaje máquina asume que el punto de comienzo de
programa está marcado por la presencia de la etiqueta con nombre main. Por tanto, al escribir un programa que sea traducido
por gcc se debe definir la etiqueta main en el lugar del código que contenga su primera instrucción máquina.
6.4.
Gestión de la pila
El desarrollo de programas en ensamblador tiene una serie de particularidades derivadas de la proximidad al procesador con la
que se trabaja. Uno de los cometidos de los lenguajes de programación de alto nivel tales como Java es precisamente el ofrecer
al programador un entorno en el que se oculten los aspectos más complejos de la programación en ensamblador.
La pila se utiliza como depósito temporal de datos del programa en ejecución. Las operaciones push y pop permiten depositar
y obtener datos de la pila, pero no son las únicas que modifican su contenido. El propio procesador también utiliza la pila para
almacenar datos temporales durante la ejecución. Esto implica que los programas en ensamblador tienen ciertas restricciones al
manipular la pila.
La más importante de ellas es que la cima de la pila debe ser exactamente la misma antes del comienzo de la primera instrucción
de un programa y antes de la instrucción RET que termina su ejecución. Cuando se arranca un programa, el sistema operativo
reserva espacio para la pila y almacena el valor pertinente en el registro %esp. Por motivos que se explican en detalle en el
capítulo 8, el valor de este registro debe ser el mismo al terminar la ejecución de un programa.
Programación en ensamblador de la
arquitectura IA-32
116 / 198
Nótese que el respetar esta regla no implica que la pila no pueda utilizarse. Al contrario, como la cima debe ser idéntica al
comienzo y final del programa, las instrucciones intermedias sí pueden manipular su contenido siempre y cuando al final del
programa se restaure el valor de la cima que tenía al comienzo.
En un programa, esta limitación se traduce en que cada dato que se deposite en la pila debe ser descargado antes de que finalice
el programa. En el medio del código, la pila puede almacenar los datos que el programador considere oportunos.
Además de inicializar el registro %esp, el sistema operativo también deposita valores en los registros de propósito general. La
ejecución del programa escrito en ensamblador la inicia el sistema mediante una llamada a la subrutina con nombre main (de
ahí que éste sea el punto de comienzo del programa) y por tanto, los registros tienen todos ciertos valores iniciales.
La regla a respetar en los programas ensamblador es que al término de la ejecución de un programa, el valor de los registros de
propósito general debe ser exactamente el mismo que tenían cuando se comenzó la ejecución (en la práctica no todos los registros
deben ser restaurados, pero por simplicidad se ha adoptado la regla para todos ellos).
De nuevo, el que el valor de los registros tenga que ser idéntico al comienzo y al final de un programa no quiere decir que no se
puedan utilizar. Simplemente se deben guardar los valores iniciales de aquellos registros que se utilicen y restaurarlos antes de
terminar la ejecución.
El lugar más apropiado para guardar los valores iniciales de estos registros es precisamente la pila. No es preciso reservar
espacio de antemano, pues la pila ya lo tiene reservado, y mediante varias instrucciones PUSH se depositan los registros que se
modifiquen en el código antes de ejecutar las instrucciones propias del cálculo. Luego, justo antes del final de la ejecución se
restauran mediante las instrucciones POP. El ejemplo 6.6 muestra una porción de un programa que modifica los registros %eax, %ebx, %ecx y %edx,
Ejemplo 6.6 Instrucciones para salvar y restaurar registros
main:
push
push
push
push
%eax
%ebx
%ecx
%edx
# Instrucciones del programa que modifican los 4 registros
pop
pop
pop
pop
%edx
%ecx
%ebx
%eax
ret
Las instrucciones para guardar la copia de los registros que se modifican se realiza justo al principio del código. De forma
análoga, las instrucciones para restaurar estos valores se realizan justo antes de la instrucción RET. Asimismo, el orden en el que
se salvan y restauran los registros es el inverso debido a cómo se almacenan en la pila. El orden en el que se depositan los datos
en la pila es irrelevante, tan sólo se deben restaurar en orden inverso al que se han depositado.
Todo programa ensamblador, por tanto, debe comenzar y terminar con instrucciones de PUSH y POP de los registros que se
modifiquen en su interior.
La utilización de la pila para almacenar los valores de los registros modificados respeta el convenio de mantener la cima de la
pila idéntica al comienzo y final del programa. Como el número de operaciones PUSH es idéntico al número de operaciones POP,
mientras que en el código interno del programa todo dato que se deposite en la pila se extraiga, la cima de la pila es idéntica al
comienzo y final.
A la hora de desarrollar programas en ensamblador, se recomienda primero escribir el código interno de un programa y cuando
dicho código se suponga correcto completarlo con las instrucciones que salvan y restauran los registros que se modifican. En
general, los datos que se almacenan en la pila se hace de forma temporal y deben eliminarse una vez terminada la tarea para la
que se han almacenado.
Programación en ensamblador de la
arquitectura IA-32
6.5.
117 / 198
Desarrollo de programas en ensamblador
El desarrollo de programas en ensamblador requiere un conocimiento en detalle de la arquitectura del procesador y una meticulosidad extrema a la hora de decidir qué instrucciones y datos utilizar. Al trabajar con el lenguaje máquina del procesador, la
comprobación de errores de ejecución es prácticamente inexistente. Si se ejecuta una instrucción con operandos incorrectos, el
procesador los interpretará tal y como estipula su lenguaje máquina, con lo que es posible que la ejecución del programa produzca
resultados inesperados.
Desafortunadamente no existe un conjunto de reglas que garanticen un desarrollo simple de los programas. Esta destreza se
adquiere mediante la práctica y, más importante, mediante el análisis detenido de los errores, pues ponen de manifiesto aspectos
de la programación que se han ignorado.
Las recomendaciones que se hacen para el desarrollo de programas en lenguaje de alto nivel adquieren todavía más relevancia
en el contexto del lenguaje ensamblador. Sin ser de ninguna manera una lista exhaustiva, se incluyen a continuación las más
relevantes.
El valor de los registros y el puntero de pila antes de ejecutar la última instrucción del programa deben ser idénticos a los
valores que tenían al comienzo.
Se deben evitar las operaciones innecesarias. Por ejemplo, salvar y restaurar todos los registros independientemente de si son
utilizados o no.
Debido al gran número de instrucciones disponibles y a su simplicidad siempre existen múltiples forma de realizar una operación. Generalmente elige aquella que proporciona una mayor eficiencia en términos de tiempo de ejecución, utilización de
memoria o registros, etc.
Mantener un estilo de escritura de código que facilite su legibilidad. Escribir las etiquetas a principio de línea, las instrucciones
todas a la misma altura (generalmente mediante ocho espacios), separar los operandos por una coma seguida de un espacio,
etc.
La documentación en el código es imprescindible en cualquier lenguaje de programación, pero en el caso del ensamblador,
es crucial. Hacer uso extensivo de los comentarios en el código facilita la comprensión del mismo además de simplificar
la detección de errores. Los comentarios deben ser lo más detallados posible evitando comentar instrucciones triviales. Es
preferible incluir comentarios de alto nivel sobre la estructura global del programa y los datos manipulados.
La mayor parte de errores se detectan cuando el programa se ejecuta. No existe una técnica concreta para detectar y corregir
un error, pero se debe analizar el código escrito de manera minuciosa. En ensamblador un simple error en el nombre de un
registro puede producir que un programa sea incorrecto.
6.6.
Ejemplo de programa en ensamblador
El ejemplo 6.7 muestra un programa de ejemplo escrito en ensamblador que dados cuatro enteros almacenados en memoria,
suma sus valores y deposita el resultado en el lugar que denota la etiqueta result.
Programación en ensamblador de la
arquitectura IA-32
118 / 198
Ejemplo 6.7 Programa que suma cuatro enteros
.data
# Comienza la sección de datos
num1: .int 10
num2: .int 23
num3: .int 34
num4: .int 43
result: .space 4
# Deposita aquí el resultado
.text
# Comienza la sección de código
.global main
# main es un símbolo global
main:
push %eax
mov
add
add
add
num1,
num2,
num3,
num4,
# Salva registros
%eax
%eax
%eax
%eax
# Carga primer número y acumula
mov %eax, result
pop %eax
ret
# Almacena resultado
# Restaura registros
La sección de definición de datos contiene los cuatro enteros con sus respectivas etiquetas y los cuatro bytes de espacio vacío en
el que se almacenará su suma. La directiva .space tiene un único parámetro, con lo que se reserva espacio en memoria pero no
se inicializa.
El programa utiliza el registro %eax para acumular los valores de los números, por lo que necesita ser salvado en la pila al
comienzo y restaurado al final.
Tras salvar %eax en la pila la siguiente instrucción simplemente mueve el primer número al registro %eax. No es posible sumar
dos números que están almacenados en memoria, por lo que el programa carga el primer valor en un registro y luego suma los
restantes números a este registro.
Finalmente, el programa almacena el resultado de la suma en la posición de memoria con etiqueta result, restaura los registros
utilizados (en este caso sólo %eax) y termina la ejecución con la instrucción RET.
6.7.
Ejercicios
1. Escribir el equivalente de las siguientes definiciones de datos en ensamblador pero utilizando únicamente la directiva
.byte.
.int 12, 0x34, ’A
.space 4, 0b10101010
.ascii "MSG."
.asciz "MSG. "
2. Dada la siguiente definición de datos:
dato:
.int 0x10203040, 0b10
.string "Mensaje en ASCII"
.ascii "Segundo mensaje"
Programación en ensamblador de la
arquitectura IA-32
119 / 198
Si la etiqueta dato ser refiere a la posición de memoria 0x00001000, calcular la dirección de memoria de los siguientes
datos:
El byte con valor 0x30 del primer entero definido.
El byte de más peso del segundo número entero definido.
La letra ‘A’ del primer string definido.
El espacio en blanco entre las dos palabras de la última definición.
Programación en ensamblador de la
arquitectura IA-32
120 / 198
Capítulo 7
Modos de Direccionamiento
En este capítulo se estudia una parte concreta de la ejecución de cada instrucción máquina: la obtención de los operandos. Tras
recibir una instrucción el procesador debe obtener los operandos necesarios que están almacenados en registros de propósito
general, en la propia instrucción o en memoria. El acceso a los dos primeros es sencillo, pero el acceso a memoria puede ser
arbitrariamente complejo.
A pesar de que cada dato almacenado en memoria tiene una dirección, en la práctica, se suelen utilizar un conjunto de operaciones
aritméticas para obtenerla. La figura 7.1 muestra un ejemplo de las operaciones necesarias para obtener la dirección de un
elemento en una tabla de enteros.
Figura 7.1: Dirección de un elemento en una tabla de enteros
Supóngase una tabla de números enteros de 32 bits almacenados a partir de la posición 100 de memoria. El único dato del que se
dispone es dicho valor. ¿Cómo se puede acceder al elemento con índice 3 de la tabla? Sabiendo que los enteros tienen 4 bytes de
tamaño, sumando a la dirección base de la tabla el tamaño de los 3 números anteriores se obtiene la dirección del entero deseado.
Los cálculos para obtener una dirección de memoria suelen requerir operaciones de suma, resta, multiplicación y división y por
tanto pueden realizarse utilizando las instrucciones aritméticas del procesador. Para acceder a un operando en memoria se calcula
primero su dirección con las operaciones pertinentes y luego se accede a memoria.
Pero estos dos pasos resultan ser extremadamente frecuentes en la ejecución de los programas convencionales. Una parte importante de las instrucciones ejecutadas por un procesador están destinadas al cálculo de la dirección de un operando que se necesita
para una operación posterior.
A la vista de esta situación, el diseño de los procesadores ha ido incorporando a la propia instrucción máquina la posibilidad de
realizar ciertos cálculos sencillos para la obtención de la dirección de sus operandos. La estrategia consiste en incluir los cálculos
más comunes como parte de la instrucción y de esta forma conseguir secuencias de instrucciones más compactas, puesto que las
Programación en ensamblador de la
arquitectura IA-32
121 / 198
operaciones aritméticas se hacen igual pero sin necesidad de ejecutar múltiples instrucciones, y por consiguiente se obtiene una
mayor eficiencia en la ejecución.
Para ilustrar la ventaja de los modos de direccionamiento considérese la situación en la que el procesador debe acceder a un
operando en memoria cuya dirección d se obtiene mediante la ecuación 7.1. Supóngase que se debe sumar el valor 300 al número
de 32 bits almacenado en esta posición. Una posible secuencia de instrucciones para realizar tal operación se ilustra en el ejemplo
7.1
d = 1000 + %eax + ( %ebx ∗ 4)
E QUATION 7.1: Expresión de la dirección de un dato en memoria
Ejemplo 7.1 Cálculo de la dirección de un operando mediante instrucciones
mov %ebx, %ecx
sal $2, %ecx
add %eax, %ecx
add $1000, %ecx
addl $300, ( %ecx)
#
#
#
#
#
%ecx = %ebx
%ecx = ( %ebx * 4)
%ecx = %eax + ( %ebx * 4)
%ecx = 1000 + %eax + ( %ebx * 4)
Sumar 300 a la posición indicada por %ecx
Las cuatro primeras instrucciones calculan el valor de la dirección del operando y es únicamente la última la que realiza el acceso
y la suma de la constante $300. Esta última instrucción está accediendo a memoria de una forma especial. La expresión de su
segundo operando ( %ecx) indica que el operando está en memoria en la posición contenida en el registro %ecx. Se precisa
el sufijo de tamaño porque el primer operando es una constante, y el segundo operando, a pesar de contener el nombre de un
registro, en realidad especifica una dirección de memoria.
Supóngase ahora que el lenguaje máquina del procesador permite escribir la siguiente instrucción:
addl $300, 1000( %eax, %ebx, 4)
El efecto de esta instrucción es exactamente el mismo que el de las cinco instrucciones del ejemplo 7.1. Se realiza la suma de
300 y el número de 32 bits almacenado en memoria en la posición obtenida al sumar 1000, el contenido del registro %eax y el
contenido del registro %ebx previamente multiplicado por cuatro. Este segundo operando es a la vez fuente y destino.
La existencia de tal instrucción en el lenguaje máquina tiene la ventaja de que en una instrucción el procesador recibe mucha más
información sobre la operación a realizar. En el primer caso se precisan cinco instrucciones, o lo que es lo mismo, cinco ciclos
de ejecución como los descritos en la sección 4.2. En el segundo caso, con una única instrucción el procesador dispone de todos
los ingredientes para realizar los cálculos. La mejora en rendimiento no se deriva del número de operaciones aritméticas, pues
son exactamente las mismas en ambos casos, sino del número de instrucciones que se ejecutan.
Pero a cambio, el principal inconveniente de esta solución es que la codificación y longitud de las instrucciones se complica tanto
como complejas sean las operaciones que se permiten en una instrucción. En este caso se realizan dos sumas y una multiplicación
para obtener la dirección de un operando que a su vez participa en la suma final.
Al conjunto de métodos que el lenguaje máquina de un procesador ofrece para acceder a sus operandos se les denomina modos de
direccionamiento. El número de métodos diferentes depende de cada procesador y varía enormemente entre diferentes diseños.
Los modos de direccionamiento de la arquitectura IA-32 contemplan el acceso a operandos como los utilizados en la instrucción
addl $300, 1000( %eax, %ebx, 4).
La figura 7.2 ilustra el funcionamiento de los modos de direccionamiento de un procesador.
Programación en ensamblador de la
arquitectura IA-32
122 / 198
Figura 7.2: Funcionalidad de los modos de direccionamiento
En general un modo de direccionamiento es un procedimiento que dado un conjunto de bits o campos de la instrucción calcula
el lugar en el que se encuentra almacenado un operando.
7.1.
Notación
Para especificar en detalle el funcionamiento de los modos de direccionamiento se precisa una notación para referirse a los
componentes de las operaciones necesarias.
La ‘dirección efectiva de un operando’ que se denota por de es el lugar en el que se encuentra almacenado un operando. Esta
dirección no tiene por qué ser una posición de memoria. Existen modos de direccionamiento en los que la dirección efectiva de
un operando es un registro de propósito general.
La instrucción de la que se obtienen los datos necesarios para calcular de se denota por inst, la dirección de memoria a partir
de la cual está almacenada es @inst , y los diferentes campos de esta instrucción se denotan por instc1 , ..., instcn . Por campos
se entienden aquellos bits que forman parte de la instrucción y que codifican los diferentes elementos necesarios para el cálculo
de la dirección efectiva.
Cuando el campo instci codifica uno de los registros de propósito general, dicho registro se denota por Rci . La expresión
MEM[n] denota el contenido de memoria almacenado a partir de la posición n. La acción de modificar el contenido del registro
R con el dato d se denota R ← d.
A continuación se estudian en detalle los modos de direccionamiento disponibles en la arquitectura IA-32 así como su sintaxis en
ensamblador. Para cada uno de ellos se especifica la fórmula utilizada para obtener la dirección efectiva del operando y su valor.
7.2.
Modos del direccionamiento de la arquitectura IA-32
Tal y como se ha visto en la sección 5.3.3, los operandos de una instrucción se dividen en cuatro categorías: constantes, registros,
direcciones de memoria y operandos implícitos. Excepto estos últimos, el resto se obtienen a través de diferentes modos de
direccionamiento. A continuación se presentan en orden creciente de complejidad para finalmente comparar todos ellos con el
más complejo.
7.2.1.
Modo inmediato
Es el modo de direccionamiento utilizado para obtener operandos de tipo constante, es decir, aquellos que tienen el prefijo $ en
ensamblador. El operando está incluido en la propia instrucción. La expresión de su dirección efectiva se muestra en la ecuación
7.2.
de
= @inst + k
operando = MEM[@inst + k]
E QUATION 7.2: Dirección efectiva y operando del modo inmediato
Programación en ensamblador de la
arquitectura IA-32
123 / 198
El valor k representa el número de bytes a partir de la dirección en la que está almacenada instrucción en la que se codifica la
constante. La figura 7.3 ilustra el funcionamiento de este modo de direccionamiento así como un ejemplo.
Figura 7.3: Acceso a operando con modo inmediato
El primer operando de la instrucción es la constante $3 que tal y como se ve en la figura, en lenguaje máquina se codifica en
el último byte. Por tanto, la dirección efectiva del operando es de = @inst + 4. Cuando el procesador necesita este operando, lo
obtiene directamente de los bytes de la instrucción.
A pesar de que el sufijo de la instrucción indica que los operandos han de ser de 32 bits, el primer operando se codifica con un
único byte. Esta aparente inconsistencia no es más que un mecanismo que utiliza el procesador para que el código máquina sea
más compacto. Cuando en una instrucción se codifica un operando en modo inmediato se utilizan 1, 2 o 4 bytes dependiendo de
su valor. En este ejemplo, como el valor es $3 sólo precisa un byte.
7.2.2.
Modo registro
Es el modo de direccionamiento utilizado para obtener operandos almacenados en uno de los ocho registros de propósito general.
La instrucción contiene un campo instc1 de 3 bits que codifica los ocho posibles registros. La expresión de la dirección efectiva
y el valor del operando se muestran en la ecuación 7.3.
de
= instc1
operando = Rc1
E QUATION 7.3: Dirección efectiva y operando del modo registro
La dirección efectiva del operando, en este caso, no es una dirección de memoria, sino la de uno de los registros de propósito
general. La figura 7.4 muestra el funcionamiento de este modo de direccionamiento y un ejemplo.
Programación en ensamblador de la
arquitectura IA-32
124 / 198
Figura 7.4: Acceso a operando con modo registro
En la figura, el código de operación 0x89 no sólo indica que se realiza una operación de mover, sino que el primer operando es
de tipo registro. El nombre del registro está codificado en el segundo byte. También en este byte, se codifica el tipo del segundo
operando, que es igualmente de tipo registro. Para ello se utiliza el campo R/M del byte ModR/M.
7.2.3.
Modo absoluto
Los modos de direccionamiento restantes se refieren todos ellos a operandos almacenados en memoria y se diferencian en los
cálculos para obtener la dirección efectiva de .
En este modo de direccionamiento la dirección efectiva corresponde con una dirección de memoria y forma parte de los bytes
que codifican la instrucción. En otras palabras, la propia instrucción, en su codificación incluye una dirección de la que obtener
uno de sus operandos. La expresión de la dirección efectiva y el valor del operando se muestran en la ecuación 7.4.
de
= MEM[@inst + k]
operando = MEM[MEM[@inst + k]]
E QUATION 7.4: Dirección efectiva y operando del modo absoluto
Como la instrucción contiene la dirección efectiva, ésta está contenida en memoria desplazada k bytes con respecto a la dirección
@inst . El operando está en memoria en la posición que indica de , de ahí la doble expresión MEM[]. La figura 7.5 muestra el
funcionamiento de este modo de direccionamiento.
Programación en ensamblador de la
arquitectura IA-32
125 / 198
Figura 7.5: Acceso a operando con modo absoluto
Como muestra la figura, la dirección de memoria ocupa 4 de los bytes que codifican la instrucción. En el ejemplo que se muestra,
la dirección es 0x0000059A pues los datos se almacenan en little endian. La representación en ensamblador de este modo
de direccionamiento es mediante una etiqueta. El ensamblador asigna a cada una de ellas un valor, y cuando se utiliza en una
instrucción se reemplaza el símbolo por el valor de su dirección de memoria.
7.2.4.
Modo registro indirecto
El modo registro indirecto accede a un operando en memoria utilizando como dirección el valor contenido en uno de los registros
de propósito general. La palabra ‘indirecto’ hace referencia a que primero se obtiene el valor del registro y luego se utiliza dicho
valor como dirección de memoria. La expresión de la dirección efectiva y el valor del operando se muestran en la ecuación 7.5.
de
= Rc1
operando = MEM[Rc1 ]
E QUATION 7.5: Dirección efectiva y operando del modo indirecto
Para codificar este modo de direccionamiento sólo es preciso incluir el código del registro de propósito general a utilizar, por
tanto con 3 bits es suficiente. La responsabilidad de que en el registro utilizado para la indirección esté contenida una dirección de
memoria correcta recae totalmente en el programador. La figura 7.6 ilustra el funcionamiento de este modo de direccionamiento
así como un ejemplo.
Programación en ensamblador de la
arquitectura IA-32
126 / 198
Figura 7.6: Acceso a operando con modo registro indirecto
Tal y como muestra la figura, la sintaxis para denotar este modo de direccionamiento en una instrucción es con el nombre del
registro escrito entre paréntesis. El operando se obtiene de memoria mediante la dirección almacenada en el registro. La principal
ventaja de este modo de direccionamiento es que, al estar almacenada la dirección en un registro, esta se puede manipular con
las operaciones aritméticas que ofrece el procesador como cualquier otro dato.
Por ejemplo, la instrucción MOV ( %esp), %eax carga en el registro %eax el valor almacenado en la cima de la pila. La
instrucción es correcta porque el registro %esp contiene la dirección de memoria de la cima de la pila.
Considérese la secuencia de instrucciones del ejemplo 7.2. La primera instrucción simplemente copia el valor del puntero de
pila en el registro %eax. La siguiente instrucción suma la constante $4 al valor almacenado en la cima de la pila. La instrucción
necesita el sufijo de tamaño porque el segundo operando es un registro indirecto, con lo que especifica la dirección de memoria
del operando, pero no su tamaño.
Ejemplo 7.2 Acceso a los elementos de la pila con el modo registro indirecto
mov %esp, %eax
addl $4, ( %eax)
add $4, %eax
addl $4, ( %eax)
La instrucción add $4, %eax muestra como una dirección de memoria se puede manipular como un dato numérico. Al sumarle
$4 el nuevo valor obtenido es la dirección de memoria del elemento almacenado en la pila justo debajo de la cima. La última
instrucción suma la constante $4 al operando almacenado en memoria en la dirección contenida en el registro %eax, o lo que es
lo mismo, al dato almacenado debajo de la cima de la pila.
En el ejemplo anterior, se ha manipulado el contenido de los datos almacenados en la pila sin modificar el puntero a su cima.
Esto es posible gracias a que se han hecho los cálculos con una dirección de memoria que es una copia del puntero a la cima.
El modo registro indirecto se puede utilizar para acceder a cualquier dato en memoria. En el ejemplo 7.3 se muestra una secuencia
de instrucciones que acceden a elementos de una tabla de datos de 16 bits almacenados a partir de la dirección de memoria
representada por la etiqueta tabla.
Programación en ensamblador de la
arquitectura IA-32
127 / 198
Ejemplo 7.3 Acceso a los elementos de una tabla con el modo registro indirecto
mov
mov
add
mov
add
mov
$tabla, %eax
( %eax), %bx
$2, %eax
( %eax), %cx
$6, %eax
( %eax), %dx
La primera instrucción almacena en el registro %eax la dirección de memoria representada por la etiqueta tabla. El valor
exacto que se carga en el registro es imposible saberlo de antemano, pues depende de dónde en memoria estén almacenados los
datos, pero igualmente se puede utilizar con los modos de direccionamiento.
La segunda instrucción accede al primer elemento de la tabla con el modo registro indirecto. El operando destino es el registro %bx que por tanto, fija el tamaño de dato a ser transferido a 16 bits y no es necesario el sufijo de tamaño. La siguiente instrucción
suma la constante $2 al registro %eax y se accede de nuevo a un elemento de la tabla mediante registro indirecto. En este caso
se almacena en el registro %cx el número de 16 bits almacenado en segunda posición. La instrucción add $6, %eax hace que
el registro contenga la dirección del elemento en la quinta posición, o lo que es equivalente, con índice 4. La última instrucción
accede a este elemento de la tabla y lo almacena en %dx.
7.2.5.
Modo auto-incremento
El funcionamiento del modo auto-incremento es similar al modo registro indirecto, con la salvedad de que el registro, tras ser
utilizado para el acceso a memoria, incrementa su valor en una constante. La expresión de la dirección efectiva y el valor del
operando se muestran en la ecuación 7.6
de
=
Rc1
operando
=
MEM[Rc1 ]
Rc1
← Rc1 + 4
E QUATION 7.6: Dirección efectiva y operando del modo auto-incremento
El efecto de la modificación del valor contenido en el registro es que la dirección pasa ahora a apuntar a una posición de memoria
cuatro bytes más alta que el valor anterior. En principio, cualquier registro de propósito general puede ser utilizado, pero en el
caso concreto de la arquitectura IA-32, este modo únicamente se utiliza en la instrucción POP y con el registro %esp. La figura
7.7 ilustra el funcionamiento de este modo de direccionamiento así como un ejemplo de la instrucción POP.
Programación en ensamblador de la
arquitectura IA-32
128 / 198
Figura 7.7: Acceso a operando con modo auto-incremento
Al tratarse de la única instrucción de la arquitectura IA-32 que utiliza este modo de direccionamiento y que únicamente se utiliza
el registro %esp, la codificación en la instrucción de este modo de direccionamiento es un caso especial, pues está implícita
en el código de operación. El byte con valor 0x58 no sólo codifica la operación POP sino también la utilización del modo
auto-incremento con el registro %esp para obtener el dato a almacenar.
El efecto de este modo de direccionamiento corresponde con el comportamiento de la instrucción POP. El operando implícito es
el dato de la cima de la pila que se obtiene mediante la dirección de memoria almacenada en %esp. Este operando se almacena
donde indica el único operando explícito de la instrucción, y a continuación se incrementa el valor de %esp de forma automática
en 4 unidades. Como consecuencia, el puntero de pila apunta a la nueva cima.
7.2.6.
Modo auto-decremento
El modo auto-decremento es similar al auto-incremento pues realiza una indirección y modifica el registro utilizado, pero la
modificación de la dirección se realiza antes de acceder a memoria. La funcionalidad de este modo de direccionamiento se puede
considerar complementaria a la anterior. La expresión de la dirección efectiva y el valor del operando se muestran en la ecuación
7.7
de
=
Rc1 − 4
operando
=
MEM[Rc1 − 4]
Rc1
←
Rc1 − 4
E QUATION 7.7: Dirección efectiva y operando del modo auto-decremento
La dirección efectiva no está directamente contenida en el registro especificado, sino que previamente se resta la constante 4 a
su valor y se accede a memoria con el valor resultante. Además, este valor, tras acceder a memoria, se almacena de nuevo en
el registro. En principio cualquier registro de propósito general puede utilizarse para este modo de direccionamiento, pero en
el caso de la arquitectura IA-32, este modo únicamente se utiliza en la instrucción PUSH y con el registro %esp. La figura 7.8
ilustra el funcionamiento de este modo de direccionamiento así como un ejemplo de la instrucción PUSH.
Programación en ensamblador de la
arquitectura IA-32
129 / 198
Figura 7.8: Acceso a operando con modo auto-decremento
Al igual que en el caso del modo auto-incremento, al ser PUSH la única instrucción que utiliza este modo en la arquitectura
IA-32, su codificación está implícita en el código de operación 0x6A. El operando implícito, por tanto es la dirección de la nueva
cima en la que almacenar el valor dado como operando explícito y que se obtiene restando 4 del registro %esp.
7.2.7.
Modo base + desplazamiento
El modo de direccionamiento base + desplazamiento obtiene la dirección efectiva del operando mediante la utilización de dos
elementos. Este es el primer ejemplo en el que el procesador, como paso previo para la obtención de los operandos, obtiene más
de un dato de la instrucción y lo utiliza para calcular la dirección efectiva. La expresión de la dirección efectiva y el valor del
operando se muestran en la ecuación 7.8.
de = Rc1 + instc2
operando = MEM[Rc1 + instc2 ]
E QUATION 7.8: Dirección efectiva y operando del modo base + desplazamiento
En este modo, la dirección efectiva no está contenida en ningún lugar, sino que es el resultado de la suma del contenido de un
registro y de un valor almacenado como parte de la instrucción. El procesador obtiene estos dos valores, los suma, y el resultado
lo utiliza para acceder a memoria y obtener el operando. El nombre de este modo se deriva de que al registro utilizado se le conoce
como el nombre de ‘base’ mientras que el valor numérico adicional se conoce como ‘desplazamiento’. Como la operación que
se realiza entre ambos es la suma, se puede considerar que la dirección efectiva del operando se obtiene partiendo de un registro
base cuyo valor se desplaza tanto como indica la constante.
La sintaxis para especificar este modo de direccionamiento tiene dos posibles formatos. El primero consiste en escribir el nombre
del registro entre paréntesis precedido de una constante entera sin prefijo alguno, como por ejemplo la instrucción INCL 12( %ecx). La constante puede ser escrita en cualquiera de los formatos permitidos por el ensamblador: decimal, binario, octal,
hexadecimal o letra.
El segundo formato permitido consiste en escribir el nombre del registro entre paréntesis precedido por el nombre de una etiqueta
previamente definida, como por ejemplo la instrucción SUB %eax, dato( %ecx). El valor dato hace referencia a la dirección
de memoria en la que esta etiqueta ha sido definida, por lo que la dirección efectiva se obtiene sumando la dirección de la etiqueta
con el valor contenido en el registro base. En los programas ensamblador se utilizan ambas notaciones de forma muy frecuente.
La figura 7.9 ilustra el funcionamiento de este modo de direccionamiento así como dos ejemplos.
Programación en ensamblador de la
arquitectura IA-32
130 / 198
Figura 7.9: Acceso a operando con modo base + desplazamiento
La instrucción ADD %eax, 16( %ebx) utiliza el primer formato de este modo en su segundo operando. La instrucción codifica
el desplazamiento con sólo 8 bits al tener un valor entre -128 y 127. El byte ModR/M con valor 0x43 codifica que el modo
de direccionamiento del primer operando es registro y que el del segundo es base + desplazamiento con el registro %ebx y una
constante de 8 bits.
La instrucción ADD %eax, contador( %ecx) tiene una codificación de 6 bytes. Los cuatro últimos codifican la dirección de
memoria que representa la etiqueta contador. El byte ModR/M con valor 0x81 en este caso codifica el modo de direccionamiento del primer operando que es registro y que el del segundo es con registro base %ebx y desplazamiento de 32 bits.
Este modo de direccionamiento ofrece un mecanismo muy eficiente para acceder a tablas de elementos. Supóngase que se ha
definido una tabla de números enteros de 32 bits y se escribe el código que se muestra en el ejemplo 7.4.
Programación en ensamblador de la
arquitectura IA-32
131 / 198
Ejemplo 7.4 Acceso a una tabla de enteros con modo base + desplazamiento
tabla:
.data
.int 12, 32, -34, -1, 1232, 435
.text
.global main
main:
...
mov
mov
ADD
ADD
ADD
ADD
ADD
ADD
...
$0, %ebx
$0, %ecx
tabla( %ebx), %ecx
$4, %ebx
tabla( %ebx), %ecx
$4, %ebx
tabla( %ebx), %ecx
$4, %ebx
Los enteros se almacenan a partir de la posición de memoria que representa la etiqueta tabla. Las dos primeras instrucciones
cargan el valor 0 en los registros %ebx y %ecx. Las siguientes instrucciones suman el valor de los tres primeros de la tabla y
depositan el resultado en el registro %ecx. Para ello, el registro %ebx contiene el valor que sumado a la dirección de tabla se
obtiene la dirección de los sucesivos elementos. La instrucción ADD tabla( %ebx), %ecx se repite sin modificación alguna
y accede a elementos sucesivos porque el registro base va cambiando su valor. Este tipo de instrucciones son muy eficientes si se
quiere procesar todos los elementos de una tabla, pues basta con escribir un bucle que vaya incrementando, en este caso, el valor
del registro %ebx.
La utilización de constantes como desplazamiento se utiliza generalmente para acceder a elementos almacenados en posiciones
alrededor de una dirección dada que se almacena en el registro base. Supóngase que el registro %edx contiene la dirección de memoria a partir de la cual están almacenados, por este orden, dos números enteros y cuatro letras ASCII. El código que se muestra
en la ejemplo 7.5 accede a los enteros y las cuatro letras con el registro %edx como base y con diferentes desplazamientos.
Ejemplo 7.5 Definición y acceso a una tabla de enteros
dato:
.data
.int 34
.int 12, 24
.ascii "abcd"
.text
.global main
main:
...
mov
mov
add
mov
mov
mov
mov
add
...
$dato, %edx
0( %edx), %ecx
4( %edx), %ecx
8( %edx), %ah
9( %edx), %al
10( %edx), %bh
11( %edx), %bl
-4( %edx), %ecx
La primera instrucción almacena en %edx la dirección de memoria que representa la etiqueta dato. En la segunda instrucción
se utiliza el modo de direccionamiento base + desplazamiento pero con un desplazamiento igual a cero. El entero con valor 12
se almacena en el registro %ecx. Esta instrucción pone de manifiesto que el modo base + desplazamiento con un desplazamiento
igual a cero, es equivalente al modo registro indirecto.
La siguiente instrucción add 4( %edx), %ecx suma el valor del segundo entero al registro %ecx. El desplazamiento tiene
el valor 4 debido a que los enteros almacenados a partir de la etiqueta son de 4 bytes. La siguiente instrucción accede a la
Programación en ensamblador de la
arquitectura IA-32
132 / 198
primera letra y la almacena en el registro de 8 bits %ah. En este caso el desplazamiento es 8 porque las letras están almacenadas
en la posición siguiente a la del último byte del último entero de la definición anterior. Las siguientes instrucciones cargan las
siguientes letras en los registros de 8 bits %al, %bh y %bl.
En este modo de direccionamiento, el número que precede al registro entre paréntesis es un entero, y por tanto puede tener valor
negativo. La última instrucción muestra un ejemplo de este caso. De igual forma que se acceden a posiciones a continuación de la
especificada por una etiqueta, también se puede acceder a posiciones previas mediante desplazamientos negativos. El procesador
suma este desplazamiento al valor del registro y accede a esa posición de memoria. Con las definiciones del ejemplo la instrucción
está sumando al registro %ecx el número 34 almacenado justo antes de la etiqueta.
7.2.8.
Modo base + índice
El modo de direccionamiento base + índice es similar al anterior puesto que el procesador obtiene la dirección efectiva sumando
de nuevo dos números, pero la diferencia es que ambos números se obtienen de registros de propósito general. La expresión de
la dirección efectiva y el valor del operando se muestran en la ecuación 7.9.
de
= Rc1 + Rc2
operando = MEM[Rc1 + Rc2 ]
E QUATION 7.9: Dirección efectiva y operando del modo base + índice
Al igual que en caso del modo de direccionamiento anterior, la dirección efectiva no tiene por qué encontrarse en ningún registro
sino que se obtiene sumando el valor contenido en los dos registros especificados. No existe restricción alguna sobre los valores
que contienen los registros, el procesador realiza la suma y obtiene el operando de la posición de memoria obtenida como
resultado. La sintaxis para especificar este modo de direccionamiento es mediante dos registros separados por una coma y entre
paréntesis, por ejemplo CMPL $32, ( %eax, %esi).
A pesar de que este modo se denomina base + índice, los dos registros especificados son idénticos a todos los efectos, con
lo que cualquiera de los dos puede ser el registro base o el índice. La figura 7.10 ilustra el funcionamiento de este modo de
direccionamiento así como un ejemplo.
Figura 7.10: Acceso a operando con modo base + índice
La dirección efectiva se obtiene sumando los registros %ebx y %edi que previamente deben tener los valores pertinentes para
que la dirección de memoria resultante sea la correcta. Supóngase una tabla con 100 elementos, cada uno de ellos, a su vez es
una tabla de 5 enteros. Para acceder a un número se precisan dos índices, el primero para seleccionar uno de los 100 elementos,
Programación en ensamblador de la
arquitectura IA-32
133 / 198
y el segundo para seleccionar uno de los 5 posibles enteros. Se quiere acceder a los cinco números del elemento tabla[32]
almacenados a partir de la dirección de memoria contenida en el registro %eax. El ejemplo 7.6 muestra cómo acceder a estos
elementos utilizando el modo de direccionamiento base + índice.
Ejemplo 7.6 Acceso a los enteros de un elemento de una tabla
....
mov $0, %ebx
mov ( %eax, %ebx),
add $4, %ebx
add ( %eax, %ebx),
add $4, %ebx
add ( %eax, %ebx),
add $4, %ebx
add ( %eax, %ebx),
add $4, %ebx
add ( %eax, %ebx),
...
%ecx
%ecx
%ecx
%ecx
%ecx
La primera instrucción carga el valor cero en %ebx. A continuación se accede al primer entero del elemento de la tabla sumando
la dirección a partir de donde están almacenados, que está contenida en %eax y el valor en %ebx. Como este último registro
tiene el valor cero, el acceso es idéntico al obtenido si se utiliza el modo registro indirecto.
A continuación se suma 4 al registro %ebx y se accede de nuevo con el mismo modo en este caso para sumar el contenido en
memoria al registro %ecx. Tras ejecutar esta instrucción en %ecx se obtiene la suma de los dos primeros elementos. Mediante
sucesivos incrementos del registro %ebx y luego accediendo a los elementos con el modo base + índice se calcula la suma de los
cinco números en el registro %ecx.
La misma secuencia de instrucciones tendría un efecto idéntico si se intercambian los nombres de los registros en el paréntesis
que especifica el modo base + índice.
7.2.9.
Modo índice escalado + desplazamiento
En este modo de direccionamiento toman parte tres elementos de la instrucción para obtener la dirección efectiva del operando.
Un registro, denominado el índice, ofrece un valor que se multiplica por un factor de escala especificado por una constante, y el
resultado se suma a una segunda constante entera denominada desplazamiento. El factor de escala puede tener únicamente los
valores 1, 2, 4 y 8. La expresión de la dirección efectiva y el valor del operando se muestran en la ecuación 7.10.
de
= (Rc1 ∗ instc2 ) + instc3
operando = MEM[(Rc1 ∗ instc2 ) + instc3 ]
instc2
∈
{1, 2, 4, 8}
E QUATION 7.10: Dirección efectiva y operando del modo índice escalado + desplazamiento
La característica más importante de este modo de direccionamiento es el producto entre el registro índice y la escala. Este último
factor, en lugar de ser un número entero, tan sólo puede una de las cuatro primeras potencias de dos. La razón para esta restricción
es que la operación de multiplicación de enteros es extremadamente costosa en tiempo como para que forme parte del cálculo
de la dirección efectiva. En cambio, la multiplicación por estas potencias de dos es muy eficiente pues el resultado se obtiene
mediante el desplazamiento del valor del registro. Si el factor de escala utilizado es 1, este modo de direccionamiento es idéntico
al modo base + desplazamiento.
La sintaxis de este modo es ligeramente contra-intuitiva, pues se especifica el registro índice y el factor de escala separados
por coma pero precedidos por una coma adicional, entre paréntesis y este a su vez precedido por el entero que codifica el
desplazamiento. La figura 7.11 ilustra el funcionamiento de este modo de direccionamiento así como un ejemplo.
Programación en ensamblador de la
arquitectura IA-32
134 / 198
Figura 7.11: Acceso a operando con modo índice escalado + desplazamiento
La dirección efectiva del segundo operando de la instrucción de la figura se obtiene multiplicando el contenido de %eax por el
factor de escala 8 y sumando el desplazamiento. Al igual que en el caso del modo base + desplazamiento, este último elemento
puede ser un número entero o una etiqueta previamente definida. Considérese el ejemplo de una tabla de enteros de 64 bits
almacenados a partir de la etiqueta coeficientes. El ejemplo 7.7 muestra la definición y manipulación de estos datos con el
modo índice escalado + desplazamiento.
Programación en ensamblador de la
arquitectura IA-32
135 / 198
Ejemplo 7.7 Acceso a una tabla de enteros de 64 bits con modo índice escalado + desplazamiento
.data
coeficientes:
.quad 21, 34, 56, 98
.text
.global main
main:
...
mov $0, %eax
mov coeficientes(, %eax,
inc %eax
add coeficientes(, %eax,
inc %eax
add coeficientes(, %eax,
inc %eax
add coeficientes(, %eax,
...
8), %ebx
8), %ebx
8), %ebx
8), %ebx
Los números definidos por la directiva .quad son de 64 bits y por tanto ocupan 8 bytes. Tras cargar el valor 0 en el registro %eax, las siguientes instrucciones acceden a los números de forma sucesiva utilizando este registro como índice. La multiplicación
del registro índice por el factor de escala 8 que coincide con el tamaño de los datos hace que el índice coincida con el valor que
se utilizaría en un lenguaje de alto nivel como Java para acceder a los números: coeficientes[3] se accede mediante la
expresión coeficientes(, %eax, 8) si %eax contiene el valor 3.
7.2.10.
Modo base + índice escalado + desplazamiento
Este modo de direccionamiento es el más complejo que ofrece el procesador y se puede considerar como la combinación de los
modos base + desplazamiento e índice escalado + desplazamiento. La dirección efectiva se calcula sumando tres números: el
desplazamiento, el contenido de un registro base y la multiplicación de un registro índice por un factor de escala que puede tener
los valores 1, 2, 4 u 8. La expresión de la dirección efectiva y el valor del operando se muestran en la ecuación 7.11.
de
= Rc1 + (Rc2 ∗ instc3 ) + instc4
operando = MEM[Rc1 + (Rc2 ∗ instc3 ) + instc4 ]
instc3
∈
{1, 2, 4, 8}
E QUATION 7.11: Dirección efectiva y operando del modo base + índice escalado + desplazamiento
Este modo de direccionamiento precisa cuatro elementos que están contenidos en la codificación de la instrucción. El campo que
más espacio requiere es el desplazamiento que puede ser un valor entero o el nombre de una etiqueta previamente definida, por
lo que se precisan 32 bits. El factor de escala, al poder tomar únicamente cuatro valores, se puede codificar con 2 bits.
La sintaxis de este modo es también una combinación de los anteriores. Entre paréntesis se escribe el registro base, el registro
índice y el factor de escala, por este orden y separados por comas. El paréntesis va precedido del valor del desplazamiento. La
figura 7.12 ilustra el funcionamiento de este modo de direccionamiento así como un ejemplo.
Programación en ensamblador de la
arquitectura IA-32
136 / 198
Figura 7.12: Acceso a operando con modo base + índice escalado + desplazamiento
El efecto de este modo de direccionamiento es que la dirección efectiva se obtiene sumando un registro, una constante (el
desplazamiento) y un segundo registro susceptible de ser escalado. Al igual que en el modo índice escalado + desplazamiento,
la multiplicación por la escala se realiza desplazando el operando cero, una, dos o tres posiciones hacia su bit de más peso. Al
igual que en el resto de modos que utilizan un desplazamiento, este puede ser un número entero o una etiqueta previamente
definida. En la instrucción DIVL 4( %ecx, %edi, 4) mostrada en la figura, la dirección efectiva del operando es 4 + %ecx
+ ( %edi * 4), por lo que se asume que el registro %ecx contiene el valor de una dirección con respecto a la cual se accede
al operando. En la instrucción INCB contador( %ecx, %edi, 4) el desplazamiento es una dirección de memoria en base
de la cual se obtiene la dirección efectiva del operando.
Como ejemplo de utilización de este modo de direccionamiento considérese que se ha definido una tabla de 10 enteros en Java a
partir de la posición de memoria con etiqueta num. A continuación se quiere acceder al elemento de la tabla con índice almacenado en el registro %ecx e incrementar su valor en una unidad. El código mostrado en el ejemplo 7.8 muestra las instrucciones
necesarias para esta operación.
Programación en ensamblador de la
arquitectura IA-32
137 / 198
Ejemplo 7.8 Acceso a una tabla de enteros en Java con modo base + índice escalado + desplazamiento
num:
.data
.int 10, -1, 32, 345, -3556, 4, 21, 23, 15, 6543, 23
.text
.global main
main:
...
mov $4, %ebx
incl num( %ebx, %ecx, 4)
...
Como Java almacena el tamaño de una tabla en los cuatro primeros bytes, la dirección del elemento con índice en %ecx se
obtiene mediante la expresión num + 4 + ( %ecx * 4) que se puede calcular de forma eficiente utilizando el modo base + índice
escalado + desplazamiento. Si se quisiese incrementar el valor de todos los elementos de la tabla se puede escribir un bucle que
incremente el valor del registro %ecx desde cero hasta el tamaño de la tabla menos uno e incremente cada uno de ellos con una
instrucción idéntica a la del ejemplo.
7.2.11.
Utilización de los modos de direccionamiento
Se han estudiado los modos de direccionamiento que ofrece el procesador como mecanismo eficiente de acceso a memoria. No
todos los cálculos de la dirección efectiva de un operando pueden realizarse en una sola instrucción por medio de estos modos,
tan sólo aquellos que requieran el tipo de operaciones que ofrece alguno de ellos. Los modos de direccionamiento son por tanto,
un recurso que el procesador ofrece para ganar eficiencia en la ejecución de programas, pero que de ninguna forma limita la
forma de obtener la dirección efectiva.
A la hora de programar en ensamblador y acceder a datos en memoria, la técnica para acceder a los datos es tener en cuenta qué
operaciones se deben realizar para obtener su dirección, y si éstas pueden ser incluidas en una misma instrucción como modo de
direccionamiento mejor que realizar estos cálculos con instrucciones máquina adicionales.
Supóngase que se define una matriz de enteros con m filas y n columnas a partir de la posición representada por la etiqueta matriz. Los valores m y n son enteros y están almacenados en memoria con etiquetas del mismo nombre. Las matrices se almacenan
en memoria utilizando múltiples estrategias. Las dos más comunes son por filas y por columnas. En el primer formato, se
almacenan los elementos de una fila en posiciones contiguas de memoria y a continuación los de la siguiente fila. En el segundo
formato, los elementos de una columna ocupan posiciones de memoria consecutivas, y a continuación se almacena la siguiente
columna. Supóngase que los elementos de esta matriz están almacenados por filas. La figura 7.13 muestra la distribución de los
datos en memoria mediante su definición en ensamblador. Cada posición de la matriz contiene un número formado por el número
de fila seguido del número de columna.
Figura 7.13: Definición de una matriz de enteros almacenada por filas
Para acceder a un elemento de la matriz se precisan dos índices (i, j), donde 0≤ i < m y 0≤ j < n. Dados los índices (i, j), la
expresión de la dirección efectiva de este elemento según la definición de la figura 7.13 se muestra en la ecuación 7.12.
de
= $matriz + (i ∗ n ∗ 4) + ( j ∗ 4)
E QUATION 7.12: Dirección de un elemento en una matriz de enteros
Programación en ensamblador de la
arquitectura IA-32
138 / 198
Supóngase que se tiene que acceder al elemento en la posición que indican los registros %eax y %ebx para incrementar en una
unidad su valor mediante la instrucción INC. Dada la funcionalidad ofrecida en los modos de direccionamiento, no es posible
acceder al elemento con una única instrucción, pues el cálculo de su dirección efectiva requiere operaciones no contempladas.
Pero una porción de la ecuación 7.12 sí puede ser calculada por el modo de direccionamiento base + índice escalado + desplazamiento. Como desplazamiento se utiliza el valor de la etiqueta matriz, la segunda multiplicación se puede ejecutar como un
índice escalado, por lo que tan sólo es preciso obtener el resultado de (i * n * 4) y almacenarlo en un registro. El ejemplo 7.9
muestra una posible secuencia de instrucciones para acceder e incrementar el elemento.
Ejemplo 7.9 Instrucciones para incrementar un elemento de una matriz de enteros
...
mull n
sal $2, %eax
incl matriz( %eax, %ebx, 4)
...
La instrucción mull n multiplica el número de columnas por el índice que indica el número de fila. Tal y como funciona esta
instrucción, al especificar un operando de 32 bits mediante el sufijo, resultado se almacena en el registro de 64 bits obtenido
al concatenar %edx: %eax. Se asume que el resultado no sobrepasa los 32 bits de %eax. A continuación la instrucción sal
$2, %eax desplaza el registro dos posiciones a su izquierda que equivale a multiplicar por cuatro. Con esto se obtiene en %eax
el término de la ecuación 7.12 que falta para poder utilizar el modo base + índice escalado + desplazamiento tal y como muestra
la instrucción incl matriz( %eax, %ebx, 4).
Como efectos colaterales de este cálculo se ha perdido el valor inicial del índice almacenado en %eax así como el valor del
registro %edx ambos modificados por la instrucción de multiplicación. A modo de comparación, el ejemplo 7.10 muestra una
secuencia alternativa de instrucciones para realizar la misma operación pero que únicamente utiliza el modo registro indirecto.
Ejemplo 7.10 Instrucciones para incrementar un elemento de una matriz de enteros utilizando el modo registro indirecto
...
mull n
sal $2, %eax
sal $2, %ebx
add %ebx, %eax
add $matrix, %eax
incl ( %eax)
...
Las instrucciones adicionales realizan los cálculos equivalentes al modo base + índice escalado + desplazamiento solo que
utilizando instrucciones máquina del procesador. El resultado de esta secuencia es casi idéntico al anterior (en este caso se ha
perdido también el valor dado en el %ebx) pero se ha utilizado un número muy superior de instrucciones, con lo que su ejecución
es mucho menos eficiente.
Este ejemplo pone de manifiesto cómo el acceso a los operandos puede realizarse de múltiples formas, pero para obtener una
ejecución eficiente y código compacto debe seleccionarse aquella que haga uso de los modos de direccionamiento ofrecidos por
el procesador.
7.3.
Hardware para el cálculo de la dirección efectiva
Una vez estudiados los modos de direccionamiento que ofrece el procesador se puede intuir el tipo de circuito digital utilizado
para calcular la dirección efectiva de un operando almacenado en memoria. La figura 7.14 muestra una posible implementación.
Programación en ensamblador de la
arquitectura IA-32
139 / 198
Figura 7.14: Circuito para el cálculo de la dirección efectiva
Este circuito calcula la dirección efectiva para los modos de direccionamiento cuyo operando se encuentra en memoria, es decir,
todos excepto inmediato y registro. De los bits que codifican la instrucción se obtienen los cuatro posibles elementos el cálculo
de la dirección efectiva y mediante la utilización de las señales de control b, i, e y d se activa la participación de cada una de
ellos. Por ejemplo, el modo índice escalado + desplazamiento requiere que la señal i seleccione la entrada del multiplexor que
procede del banco de registros, la señal d seleccione el valor obtenido de la instrucción, y las dos señales restantes seleccionen la
entrada constante de sus respectivos multiplexores.
7.4.
Resumen de los modos de direccionamiento
Los modos de direccionamiento son los diferentes procedimientos que utiliza el procesador dentro de la ejecución de una instrucción para acceder a sus operandos. Las diferentes formas que permite la arquitectura IA-32 para acceder a sus operandos se
muestran en la Tabla 7.1.
Modo de
direccionamiento
Inmediato
Registro
Absoluto
Registro indirecto
Auto-incremento
Auto-decremento
Base + desplazamiento
Base + índice
Índice escalado +
desplazamiento
Base + índice escalado +
desplazamiento
Dirección efectiva
Operando
@inst + k
instc1
MEM[@inst + k]
Rc1
Rc1
Rc1 − 4
Rc1 + instc2
Rc1 + Rc2
MEM[@inst + k]
Rc1
MEM[MEM[@inst + k]]
MEM[Rc1 ]
MEM[Rc1 ]
MEM[Rc1 − 4]
MEM[Rc1 + instc2 ]
MEM[Rc1 + Rc2 ]
MEM[(Rc1 ∗ instc2 ) +
instc3 ]
MEM[Rc1 + (Rc2 ∗
instc3 ) + instc4 ]
(Rc1 ∗ instc2 ) + instc3
Rc1 + (Rc2 ∗ instc3 ) +
instc4
Condiciones adicionales
Rc1 ← Rc1 + 4
Rc1 ← Rc1 − 4
instc2 ∈ {1, 2, 4, 8}
instc3 ∈ {1, 2, 4, 8}
Tabla 7.1: Modos de direccionamiento de la arquitectura IA-32
7.5.
Ejercicios
1. Asumiendo que los campos de una instrucción máquina son ci1 , ci2 , ci3 , ci4 ,... escribir la fórmula del cálculo de la dirección
efectiva del operando y explicar su significado para los siguientes modos de direccionamiento: (Utilícese la notación (x)
para denotar ‘el contenido de x’).
Programación en ensamblador de la
arquitectura IA-32
140 / 198
a. Registro Indirecto:
b. Absoluto:
c. Base + Índice:
d. Base + Índice Escalado + Desplazamiento:
2. Supóngase que de todos los modos de direccionamiento de la arquitectura IA-32, los únicos que se pueden utilizar son
el modo registro, modo inmediato y el modo registro indirecto. Escribir la secuencia de instrucciones equivalentes a las
siguientes: (es decir que si se reemplaza la instrucción con las instrucciones de cada respuesta, el programa resultante es
idéntico).
a.
MOV matrix( %ebx), %eax
b.
MOV table(, %ecx, 4), %eax
3. Un procesador llamado PDP-11 contiene en su juego de instrucciones dos modos de direccionamiento que no posee la
arquitectura IA-32.
Modo Autoincremento Indirecto: Se representa como [Reg]++. El procesador accede a la posición de memoria contenida en el registro Reg y de dicha posición de memoria obtiene la dirección de memoria del operando. El registro Reg
queda incrementado en cuatro unidades.
Modo Indexado Indirecto: Se representa como $desp[Reg]. El procesador accede a la posición de memoria resultante
de sumar Reg y $desp, y de dicha posición de memoria obtiene la dirección de memoria del operando.
Especificar cómo deben traducirse las siguientes instrucciones del PDP-11 a instrucciones de la arquitectura IA-32 para
que la ejecución sea equivalente.
MOV [ %eax]++, %ebx
MOV %ecx, $desp[ %ecx]
4. Considerando el circuito de la figura 7.14, rellenar los valores de las señales b, i, e y d para cada uno de los modos de
direccionamientos de la siguiente tabla. La entrada constante de los multiplexores se selecciona poniendo la señal de
control con valor idéntico a esta.
Modo de
direccionamiento
Absoluto
Registro indirecto
Base +
desplazamiento
Base + índice
Índice escalado +
desplazamiento
Base + índice
escalado +
desplazamiento
Valor de b
Valor de i
Valor de e
Valor de d
0
1
0
0
1
1
1
0
1
0
1
1
1
1
1
0
0
1
0
1
1
1
0
1
Programación en ensamblador de la
arquitectura IA-32
141 / 198
Capítulo 8
Construcciones de alto nivel
Las aplicaciones que se ejecutan en un ordenador están generalmente programadas en alguno de los denominados lenguajes de
alto nivel que los compiladores traducen a ejecutables que contienen secuencias de instrucciones máquina del procesador. Para
facilitar el desarrollo de estas aplicaciones se necesitan mecanismos adicionales tanto a nivel de procesador como de sistema
operativo. Por ejemplo, la posibilidad de fragmentar el código en múltiples ficheros, gestión del acceso de símbolos en otros
ficheros, etc.
En este capítulo se estudian los mecanismos que facilitan la traducción de programas en lenguajes de alto nivel a programas
en ensamblador. Además de la fragmentación de código en múltiples ficheros, se estudia en detalle el procedimiento para la
invocación, paso de parámetros y ejecución de subrutinas, y la traducción de estructuras de control presentes en lenguajes de
programación a lenguaje ensamblador.
8.1.
Desarrollo de aplicaciones en múltiples ficheros
La funcionalidad que ofrece un procesador está basada en sus instrucciones máquina, órdenes muy simples para manipular datos.
Pero para programar operaciones más complejas se precisa un lenguaje que sea más intuitivo y que abstraiga o esconda los
detalles de la arquitectura del procesador. A este tipo de lenguajes se les denomina ‘lenguajes de alto nivel’ por contraposición
al lenguaje ensamblador cuya estructura y construcciones están directamente relacionadas con la arquitectura del procesador que
lo ejecuta. La traducción de las operaciones en un lenguaje de alto nivel a secuencias de instrucciones máquina se lleva a cabo
por el compilador.
Las principales limitaciones que se derivan del uso del lenguaje ensamblador son:
Las aplicaciones que contengan manejo de datos u operaciones complejas requieren secuencias de instrucciones extremadamente largas, y por tanto, es muy fácil que se introduzcan errores.
El lenguaje ensamblador carece de tipos de datos. A pesar de que existen directivas para definir datos, su efecto no es más
que almacenar una secuencia de bytes en memoria. El procesador accede a estos datos como una secuencia de bytes sin
información de tamaño ni de su tipo.
Las subrutinas ofrecen un mecanismo básico para ejecutar porciones de código de forma repetida y con diferentes datos, pero
no se realiza ningún tipo de comprobación de su correcta invocación.
Los lenguajes de alto nivel solventan estas limitaciones ofreciendo un conjunto de mecanismos para definir y manipular datos.
Cada lenguaje tiene su propia sintaxis, o forma de escribir las órdenes, y su semántica, o cómo esas órdenes son traducidas a
secuencias de instrucciones máquina.
Al conjunto de reglas para definir y manipular los tipos de datos en un lenguaje de alto nivel se le denomina el ‘sistema de tipos de
datos’. Aquellos lenguajes que estructuran sus datos en base a objetos que se crean a partir de una definición genérica denominada
‘clase’ se les conoce como ‘lenguajes orientados a objeto’. Java, Smaltalk y C++ son algunos de los múltiples lenguajes con esta
característica.
Programación en ensamblador de la
arquitectura IA-32
142 / 198
El proceso de compilación de los programas escritos en lenguajes de alto nivel es similar al de traducción de lenguaje ensamblador
a lenguaje máquina. Dado un conjunto de ficheros escritos en el lenguaje de entrada, se produce un ejecutable que contiene la
traducción de todos ellos a instrucciones máquina y definiciones de datos. La figura 8.1 muestra el procedimiento por el que dado
un conjunto de ficheros en lenguaje de alto nivel, el compilador obtiene un fichero ejecutable.
Figura 8.1: Compilación de un programa escrito en un lenguaje de alto nivel
El lenguaje de programación Java merece una mención especial, pues no sigue el patrón de traducción que se muestra en la
figura 8.1. Los programas se escriben en múltiples ficheros que contienen la definición de clases con sus campos y métodos. El
proceso de compilación no produce directamente un ejecutable sino un fichero con formato ‘class’ o ‘bytecode’. Este formato
no corresponde con instrucciones máquina del procesador sino con instrucciones de lo que se conoce como ‘máquina virtual de
java’ o JVM (Java Virtual Machine).
La traducción a código de la JVM se realiza para garantizar la ‘portabilidad’ de un programa, es decir, que el fichero generado se
pueda ejecutar sin modificaciones en cualquier procesador. La máquina virtual lee el código escrito en formato class y lo traduce
a instrucciones máquina del procesador sobre el que se ejecuta. Esta traducción se hace en el momento en el que se ejecuta un
programa. Por este motivo se dice que Java es un lenguaje parcialmente compilado y parcialmente interpretado. La compilación
traduce el código inicial a formato bytecode que a su vez es interpretado en tiempo de ejecución por la máquina virtual.
Mediante la presencia de esta máquina virtual, se garantiza la compatibilidad de los programas Java en cualquier procesador.
Para ello es preciso crear una máquina virtual diferente para cada uno de los procesadores existentes en el mercado. Una vez
implementada esta máquina virtual, todos los programas escritos en Java son ejecutables en esa plataforma.
Existen otro tipo de lenguajes de alto nivel que no precisan de un paso previo de compilación para obtener un ejecutable, sino
que se ejecutan directamente a través de un programa auxiliar denominado ‘intérprete’ cuyo cometido es similar al compilador
sólo que su tarea la hace justo en el instante que el programa debe ser ejecutado, y no como paso previo. A estos lenguajes de
alto nivel se les denomina ‘interpretados’ pues el proceso que se lleva a cabo en el momento de la ejecución es una interpretación
del código fuente y su traducción instantánea a código máquina. Perl, Python y TCL son algunos ejemplos de lenguajes de
programación interpretados.
8.2.
Programas en múltiples ficheros
La generación de un programa a partir de un conjunto de ficheros con código fuente procesándolos se realiza en dos pasos. En
el primero se traduce cada fichero por separado a código máquina. En el segundo paso denominado de ‘enlazado’ (ver sección
1.5.2) se combinan las porciones de código generadas en el paso anterior y se crea el fichero ejecutable. Para ello se precisan dos
mecanismos:
Política de acceso a los símbolos definidos en cada uno de los ficheros
Ejecución parcial de un fragmento de código en un fichero diferente al que se está ejecutando y su retorno al mismo punto una
vez terminado.
Programación en ensamblador de la
arquitectura IA-32
143 / 198
Cada fichero ensamblador contiene un conjunto de etiquetas que representan diferentes posiciones de memoria. Para que un
programa pueda ser fragmentado debe ser posible referirse a un símbolo definido en otro fichero. Por ejemplo, una instrucción
de salto debe poder especificar como destino un punto en el código de otro fichero.
Pero si una aplicación consta de múltiples ficheros cada uno de ellos con un número muy elevado de etiquetas definidas, tareas
como la ampliación de un programa se vuelven muy difíciles. Si los símbolos definidos en los ficheros son todos ellos globales,
no se puede utilizar un nombre para una variable o una posición en el código que esté presente en otro fichero.
Para solucionar este problema se adopta la política de gestión opuesta para el ámbito de los símbolos. Todo símbolo definido en
un fichero tiene como ámbito de validez únicamente el propio fichero a no ser que se especifique lo contrario con la directiva de
ensamblador .global. El ensamblador permite definir una etiqueta que coincide con el nombre de otra definida como global.
En este caso, el símbolo local toma precedencia y por tanto el global no es accesible. De esta forma, cuando se escribe código
ensamblador en un fichero, en principio se puede utilizar cualquier nombre para una etiqueta.
En el primer paso de la traducción, todo símbolo que no esta definido en el fichero que se procesa se considera externo, y por
tanto su posición es desconocida. Es en el paso de entrelazado en el que los símbolos son todos conocidos y se pueden traducir
a sus correspondientes valores. El compilador incluye en cada fichero obtenido en el primer paso dos conjuntos de símbolos: el
primero corresponde con las etiquetas definidas en la zona de datos o de código que han sido declaradas globales, mientras que
el segundo contiene aquellos que se utilizan pero cuya definición no se ha encontrado en el fichero. La figura 8.2 muestra un
ejemplo de programa escrito en dos ficheros en los que se producen referencias a símbolos externos.
Figura 8.2: Referencia a símbolos en dos ficheros
En la fase de entrelazado, para cada fichero, el compilador busca los símbolos externos en la lista de símbolos globales del resto
de ficheros. En el caso de que un símbolo externo no esté definido en ninguno de ellos se muestra un mensaje de error. Si dos
ficheros definen el mismo símbolo como global también se muestra un mensaje de error. En ambos casos no se produce fichero
ejecutable.
Además de los símbolos contenidos en cada uno de los ficheros, en la fase de entrelazado el compilador dispone de código
auxiliar en ficheros denominados ‘bibliotecas’ en los que se incluyen rutinas para realizar tareas comunes de cualquier programa
como lectura/escritura de datos a través de diferentes dispositivos (teclado, pantalla, ficheros, etc).
Otro aspecto que debe solventar el compilador para generar un ejecutable a partir de múltiples ficheros fuente es el de la ‘reubicación de código’. Al traducir el código ensamblador contenido en cada fichero, el código binario resultante se almacena a partir
de la posición cero de memoria, pues no se sabe la posición exacta que ocupará a la hora de ejecutar. Pero cuando el código está
en múltiples ficheros, en la fase de entrelazado sólo el código de uno de ellos puede estar en la posición inicial, el resto debe ser
reubicado. La figura 8.3 muestra un ejemplo en el que el ejecutable se obtiene a partir de tres ficheros. El código de dos de ellos
debe ser reubicado.
Programación en ensamblador de la
arquitectura IA-32
144 / 198
Figura 8.3: Reubicación de símbolos en la fase de entrelazado
La reubicación de código consiste en que toda instrucción que contenga en su codificación el valor de una dirección de memoria
(por ejemplo, las que utilizan el modo de direccionamiento absoluto) deben ser modificadas para referirse a la posición de
memoria tras la reubicación. El compilador recorre de nuevo las instrucciones máquina generadas y suma a toda dirección de
memoria un factor de reubicación que corresponde con la dirección utilizada al comienzo del fichero.
Considérese la instrucción call metodo1 que invoca a la subrutina método definida en otro fichero. En la primera fase
de compilación esta instrucción se traduce por el código 0xE8FCFFFFFF. Los cuatro últimos bytes denotan la dirección de
memoria representada por la etiqueta método. En el ejecutable obtenido, el código de esta instrucción pasa a ser 0xE8FA000000 que conserva el primer byte que corresponde con el código de operación pero cambia los cuatro últimos bytes que codifican
la dirección de memoria.
8.3.
Traducción de construcciones de alto nivel a ensamblador
Las construcciones que ofrecen los lenguajes de alto nivel como Java para escribir programas distan mucho de la funcionalidad
ofrecida por el lenguaje máquina del procesador. Por ejemplo, en Java se permite ejecutar una porción de código de forma
iterativa mediante las construcciones for o while hasta que una condición se deje de cumplir. El compilador es el encargado
de producir el código ensamblador tal que su ejecución sea equivalente a la especificada en el lenguaje Java.
A continuación se muestra cómo la funcionalidad ofrecida por el procesador es suficiente para traducir estas construcciones a
secuencias de instrucciones ensamblador con idéntico significado.
8.3.1.
Traducción de un if/then/else
La figura 8.4 muestra las tres partes de las que consta un bloque if/then/else. La palabra reservada if va seguida de una expresión
booleana entre paréntesis. A continuación entre llaves una primera porción de código que puede ir seguida opcionalmente de una
segunda porción también entre llaves y con el prefijo else.
Programación en ensamblador de la
arquitectura IA-32
145 / 198
if (expresión booleana) {
Bloque A
} else {
Bloque B
}
Figura 8.4: Estructura de un if/then/else
Lo más importante para traducir un bloque a ensamblador es saber su significado o semántica. La semántica del bloque if/then/else
es que se evalua la expresión booleana y si el resultado es verdadero se ejecuta el bloque A de código y se ignora el bloque B, y
si es falsa, se ignora el bloque A y se ejecuta el bloque B.
El elemento clave para traducir esta construcción a ensamblador es la instrucción de salto condicional. Este tipo de instrucciones
permiten saltar a un destino si una condición es cierta o seguir la secuencia de ejecución en caso de que sea falsa. Lo único que
se necesita es traducir la expresión booleana de alto nivel a una condición que pueda ser comprobada por una de las instrucciones
de salto condicional ofrecida por el procesador. Supóngase que la expresión es falsa si el resultado de la evaluación es cero y
cierta en caso contrario. Además, tras ejecutar las instrucciones de evaluación, el resultado se almacena en %eax. En la figura
8.5 se muestra la estructura genérica en lenguaje ensamblador resultante de traducir un if/then/else en este supuesto.
...
...
cmp $0, %eax
je bloqueb
...
...
jmp finifthenelse
bloqueb:
...
...
finifthenelse:
...
# Evaluar la expresión booleana
# Resultado en %eax
# Traducción del bloque A
# Fin del bloque A
# Traducción del bloque B
# Fin del bloque B
# Resto del programa
Figura 8.5: Traducción de un if/then/else a ensamblador
Tras la evaluación de la condición, el resultado previamente almacenado en %eax se compara, y si es igual a cero se ejecuta el
salto que evita la ejecución del bloque A. En el caso de un if/then/else sin el bloque B, el salto sería a la etiqueta finifthenelse.
En un bloque genérico de este tipo no es preciso asumir que el resultado de la condición está almacenado en %eax. El ejemplo
8.1 muestra la traducción de un if/then/else con una condición booleana con múltiples operaciones. Se asume que las variables
x, i y j son de tipo entero y están almacenadas en memoria con etiquetas con el mismo nombre.
Programación en ensamblador de la
arquitectura IA-32
146 / 198
Ejemplo 8.1 Traducción de un if/then/else a ensamblador
Código de alto nivel
Código ensamblador
if ((x <= 3) && (i == (j + 1))) {
Bloque A
} else {
Bloque B
}
cmpl $3, x
3
jg bloqueB
bloque B
mov j, %eax
inc %eax
cmp %eax, i
+ 1)
jne bloqueB
bloque B
...
bloque A
...
jmp finifthenelse
bloqueB:
...
bloque B
...
finifthenelse:
traducción
# Comparar si x <= ←←-
# Si falso ir a
# Obtener j + 1
# Comparar i == (j ←a
←-
# Traducción del
←-
# Si falso ir
# Evitar bloque B
# Traducción del
# Final de
←-
←-
La primer expresión de la conjunción se traduce en una única instrucción cmp. Si esta comparación no es cierta, al tratarse de
una conjunción, se debe ejecutar el bloque B sin necesidad de seguir evaluando. Esto se consigue con el salto jg que contiene la
condición contraria a la del código (x <= 3).
Si la primera parte de la conjunción evalúa a cierto, se pasa a evaluar la segunda. Primero se obtiene el valor de j, se copia en un
registro y se incrementa, pues el código de alto nivel no modifica el valor almacenado en memoria. A continuación se compara
con la variable i y de nuevo, mediante un salto condicional, se ejecuta el bloque pertinente.
8.3.2.
Traducción de un switch
A menudo en programación es preciso realizar una operación y ejecutar diferentes bloques de código dependiendo del valor
obtenido. La construcción switch mostrada en la figura 8.6 ofrece exactamente esta funcionalidad
switch (expresión) {
case valor A:
Bloque A
break;
// Opcional
case valor B:
Bloque B
break;
// Opcional
...
default:
Bloque por defecto // Opcional
}
Figura 8.6: Estructura de un switch
La semántica de esta construcción establece que primero se evalúa la condición y a continuación se compara el resultado con los
valores de cada bloque precedido por la palabra clave case. Esta comparación se realiza en el mismo orden en el que se definen
Programación en ensamblador de la
arquitectura IA-32
147 / 198
en el código y si alguna de estas comparaciones es cierta, se pasa a ejecutar el código restante en el bloque (incluyendo el resto
de casos). Si ninguna de las comparaciones es cierta se ejecuta (si está presente) el caso con etiqueta default. La palabra clave
break se puede utilizar para transferir el control a la instrucción que sigue al bloque switch.
La estructura del código ensamblador para implementar esta construcción debe comenzar por el cálculo del valor de la expresión.
A continuación se compara con los valores de los casos siguiendo el orden en el que aparecen en el código. Si una comparación
tiene éxito, se ejecuta el bloque de código que le sigue. Si se encuentra la orden break se debe saltar al final del bloque. En
el caso de que ninguna comparación tenga éxito, se debe pasar a ejecutar el bloque default. Supóngase que la evaluación
de la expresión es un valor que se almacena en el registro %eax. En la figura 8.7 se muestra la estructura genérica en lenguaje
ensamblador resultante de traducir un switch en este supuesto.
...
...
cmp $valorA, %eax
je bloquea
cmp $valorB, %eax
je bloqueb
...
jmp default
# Evaluar la expresión
# Resultado en %eax
# Caso A
...
...
jmp finswitch
# Traducción del bloque A
#
# Si bloque A tiene break
...
...
jmp finswitch
...
...
# Si bloque B tiene break
# Caso B
bloquea:
bloqueb:
default:
...
...
finswitch:
...
# Caso por defecto
# Resto del programa
Figura 8.7: Traducción de un switch a ensamblador
En este esquema se asume que tras obtener el valor de la expresión, éste se mantiene en el registro %eax. La presencia de la línea
break corresponde directamente con la instrucción jmp finswitch. El ejemplo 8.2 muestra la traducción de un switch. Se
asume que las variables son todas de tipo entero y están almacenadas en memoria con etiquetas con el mismo nombre.
Programación en ensamblador de la
arquitectura IA-32
148 / 198
Ejemplo 8.2 Traducción de un switch a ensamblador
Código de alto nivel
Código ensamblador
switch (x + i + 3 + j) {
case 12:
Bloque A
break;
case 14:
case 16:
Bloque B
case 18:
Bloque C
break;
default:
Bloque D
}
mov x, %eax
la expresión
add i, %eax
add $3, %eax
add j, %eax
cmp $12, %eax
je bloquea
cmp $14, %eax
je bloqueb
cmp $16, %eax
je bloqueb
cmp $18, %eax
je bloquec
jmp default
# Evaluar ←-
# Caso 12
# Caso 14
# Caso 16
# Caso 18
bloquea:
# ←Traducción del bloque A
...
#
jmp finswitch
# Bloque
A tiene break
...
←-
bloqueb:
...
...
# ←Traducción del bloque B
bloquec:
...
# ←Traducción del bloque C
...
jmp finswitch
C tiene break
# Bloque
←-
...
# Bloque
←-
default:
...
finswitch:
...
D
# Resto
←-
del programa
La condición del ejemplo es una suma de cuatro operandos, con lo que las instrucciones ensamblador correspondientes obtienen
los operandos y acumulan su suma en %eax. A continuación se comparan los sucesivos casos. Si alguna de ellas tiene éxito se
pasa a ejecutar el correspondiente bloque de código. Si todas ellas fallan, se ejecuta el salto incondicional al bloque default.
En cada uno de los bloques, si está presente la palabra reservada break, ésta se traduce en un salto incondicional al final del
bloque.
8.3.3.
Traducción de un bucle while
Una de las construcciones más comunes en lenguajes de alto nivel para ejecutar código de forma iterativa es el bucle while. La
figura 8.8 muestra su estructura. La palabra reservada while da paso a una expresión booleana entre paréntesis que se evalúa y
en caso de ser cierta pasa a ejecutar el bloque del código interno tras cuyo final se vuelve de nuevo a evaluar la condición.
Programación en ensamblador de la
arquitectura IA-32
149 / 198
while (expresión booleana) {
Código interno
}
Figura 8.8: Estructura de un bucle while
En este bloque es importante tener en cuenta que la expresión booleana se evalúa al menos una vez y se continúa evaluando hasta
que sea falsa. Supóngase que la evaluación de la expresión es cero en caso de ser falsa y diferente de cero si es cierta y el valor
resultante se almacena en %eax. En la figura 8.9 se muestra la estructura genérica en lenguaje ensamblador resultante de traducir
un bucle while en este supuesto.
eval:
...
...
cmp $0, %eax
je finwhile
...
...
jmp eval
finwhile:
...
# Evaluar la expresión booleana
# Resultado en %eax
# Traducción del código interno
# Resto del programa
Figura 8.9: Traducción de un bucle while a ensamblador
Tras evaluar la condición se ejecuta una instrucción que salta al final del bloque si es falsa. En caso de ser cierta se ejecuta el
bloque de código y tras él un salto incondicional a la primera instrucción con la que comenzó la evaluación de la condición.
El destino de este salto no puede ser la instrucción de comparación porque es muy posible que las variables que intervienen en
la condición hayan sido modificadas por lo que la evaluación se debe hacer a partir de estos valores. El ejemplo 8.3 muestra
la traducción de un bucle while con una de estas condiciones. Se asume que las variables x, i y j son de tipo entero y están
almacenadas en memoria con etiquetas con el mismo nombre.
Ejemplo 8.3 Traducción de un bucle while a ensamblador
Código de alto nivel
while ((x == i) || (y < x)) {
Código interno
}
Código ensamblador
# Comienza ←evaluación
mov x, %eax
cmp i, %eax
# Comparar si x == ←i
je codigointerno # Si cierto ←ejecutar código
cmp y, %eax
jle finwhile
# Si falso ir al ←final
codigointerno:
...
# Código interno
...
jmp eval
# Evaluar de nuevo
finwhile:
# Final de ←traducción
eval:
En este caso la condición del bucle es una disyunción con lo que si una de las condiciones es cierta, se puede ejecutar el código
Programación en ensamblador de la
arquitectura IA-32
150 / 198
interno del bucle sin evaluar la segunda. Por este motivo se utiliza el salto condicional je tras la primera comparación. En caso
de éxito se pasa a ejecutar directamente el bloque de código interno al bucle. Si la primera condición es falsa se evalúa la segunda.
El correspondiente salto condicional en este caso tiene una condición inversa a la incluida en el código, pues si ésta es falsa, se
debe transferir el control al final del bucle.
8.3.4.
Traducción de un bucle for
El bucle for, aunque con semántica similar al anterior, tiene una estructura más compleja tal y como se muestra en la figura 8.10.
for (Bloque A; expresión booleana; Bloque B) {
Código interno
}
Figura 8.10: Estructura de un bucle for
El bloque A se ejecuta una única vez antes del bucle, a continuación se evalúa la expresión booleana. En caso de ser cierta se pasa
a ejecutar el código interno del bucle, y si no, se termina la ejecución del bucle. El bloque B se ejecuta a continuación del código
interno y justo antes de saltar de nuevo a la evaluación de la expresión booleana. Supóngase que la evaluación de la expresión
booleana es cero en caso de ser falsa y diferente de cero si es cierta y el valor resultante se almacena en %eax. En la figura 8.11
se muestra la estructura genérica en lenguaje ensamblador resultante de traducir un bucle for en este supuesto.
eval:
...
...
...
...
cmp $0, %eax
je finfor
...
...
...
...
jmp eval
# Traducción del bloque A
# Evaluar la expresión booleana
# Resultado en %eax
# Traducción del código interno
# Traducción del bloque B
finfor:
...
# Resto del programa
Figura 8.11: Traducción de un bucle for a ensamblador
Las primeras instrucciones corresponden a la traducción del bloque A seguidas de las que evalúan la condición. Se necesita
la etiqueta eval como destino del salto incondicional al final del bloque B. A continuación se comprueba el resultado de la
comparación, y si es falso se salta al final del bucle. En caso contrario se ejecuta el código interno que finaliza con las instrucciones
del bloque B y un salto incondicional para que se evalúe de nuevo la condición. El ejemplo 8.4 muestra la traducción de un bucle
for con una de estas condiciones. Se asume que las variables i y j son de tipo entero y están almacenadas en memoria con
etiquetas con el mismo nombre.
Programación en ensamblador de la
arquitectura IA-32
151 / 198
Ejemplo 8.4 Traducción de un bucle for a ensamblador
Código de alto nivel
for (i = 0; i <= --j; i++) {
Código interno
}
Código ensamblador
movl $0, i
eval:
mov i, %eax
decl j
cmp j, %eax
jg finfor
codigointerno:
...
...
incl i
jmp eval
finfor:
# Bloque A
# Expresión booleana
# Comparar si x <= --j
# Si falso ir al final
# Código interno
# Bloque B
# Evaluar de nuevo
# Final de traducción
La traducción del bloque A es una única instrucción que almacena un cero en memoria. La expresión booleana incluye el
decremento de la variable j antes de ser utilizada por la comparación. La traducción del bloque B también requiere una única
instrucción para incrementar el valor de la variable i.
8.4.
Ejecución de subrutinas
En la sección anterior se ha mostrado cómo se obtiene una traducción automática de un programa arbitrariamente complejo. El
compilador primero traduce un bloque a su estructura genérica, luego traduce los bloques internos, y una vez terminado, pasa
al bloque siguiente. La estructura global de un programa es una combinación de bloques para los cuales existe una traducción
sistemática. Esta y la codificación de los datos son las dos principales tareas de un compilador para obtener un ejecutable.
El mecanismo que merece un estudio aparte es el de llamada a subrutinas. El desarrollo de programas modulares se basa en
la posibilidad de ejecutar un bloque de código múltiples veces con diferentes valores de un conjunto de variables denominadas
‘parámetros’ que produce un resultado. Este mecanismo, con diferentes matices, es lo que se denomina como procedimientos,
funciones, subprogramas o métodos y están presentes en prácticamente todos los lenguajes de programación de alto nivel.
En el contexto del lenguaje ensamblador se define una subrutina como una porción de código que realiza una operación en base
a un conjunto de valores dados como parámetros de forma independiente al resto del programa y que puede ser invocado desde
cualquier lugar del código, incluso desde dentro de ella misma.
La ejecución de subrutinas tiene las siguientes ventajas:
Evita código redundante. Durante el diseño de un programa suelen existir ciertos cálculos que deben realizarse en diferentes
lugares del código. La alternativa a replicar las instrucciones es encapsularlas en una subrutina e invocar esta cada vez que sea
necesario lo cual se traduce en código más compacto.
Facilita la descomposición de tareas. La descomposición de tareas complejas en secuencias de subtareas más simples facilita
enormemente el desarrollo de programas. Esta técnica se suele aplicar de forma sucesiva en lo que se denomina ‘diseño
descendente’ de programas. Cada subtarea se implementa como una rutina.
Facilita el encapsulado de código. El agrupar una operación y sus datos en una subrutina y comunicarse con el resto de un
programa a través de sus parámetros y resultados, hace que si en algún momento se cambia su implementación interna, el resto
del programa no requiera cambio alguno.
Además de estas ventajas, el encapsulado de código también facilita la reutilización de su funcionalidad en más de un programa
mediante el uso de ‘bibliotecas’. Una biblioteca de funciones es un conjunto de subrutinas que realizan cálculos muy comunes
Programación en ensamblador de la
arquitectura IA-32
152 / 198
en la ejecución de programas y que pueden ser utilizados por éstos. Java es un ejemplo de lenguaje que dispone de bibliotecas de
clases que en su interior ofrecen multitud de métodos.
La desventaja de las subrutinas es que es necesario establecer un protocolo que defina dónde y cómo se realiza esta transferencia
de datos para la que se requieren múltiples instrucciones máquina adicionales.
8.4.1.
Las instrucciones de llamada y retorno de una subrutina
En ensamblador la llamada a una subrutina se realiza mediante la instrucción CALL cuyo único operando es la dirección de memoria, generalmente una etiqueta, en la que comienza su código. Tras ejecutar esta instrucción el procesador continua ejecutando
la primera instrucción de la subrutina hasta que encuentra la instrucción RET que no tiene operandos y transfiere la ejecución a
la instrucción siguiente al CALL que inició el proceso. La figura 8.12 ilustra esta secuencia.
Figura 8.12: Llamada y retorno de una subrutina
La instrucción CALL tiene una funcionalidad similar a un salto incondicional, su único operando denota la siguiente instrucción
a ejecutar. La instrucción RET no tiene operandos explícitos pero su efecto, el retorno a la siguiente instrucción tras la llamada,
requiere la utilización de operandos implícitos.
Pero la dirección a la que debe retornar el procesador no puede ser un valor fijo para la instrucción RET puesto que depende
del lugar desde donde ha sido invocada la subrutina. Considérese, por ejemplo, una subrutina que se invoca desde dos lugares
diferentes de un programa. La instrucción RET con la que se termina su ejecución es idéntica en ambos casos pero su dirección
de retorno no. Otra característica de las subrutinas es que su invocación se puede hacer de forma anidada, es decir, que desde una
subrutina se invoca a otra y desde ésta a su vez a otra, hasta una profundidad arbitraria. La figura 8.13 muestra un ejemplo de
invocación anidada de subrutinas y se puede comprobar como la subrutina B es invocada desde diferentes lugares del código.
Programación en ensamblador de la
arquitectura IA-32
153 / 198
Figura 8.13: Invocación anidada de subrutinas
La instrucción RET de la subrutina B retorna la ejecución a la subrutina A en su primera ejecución (denotada por la flecha
número 3) y al programa principal en su segunda ejecución (denotada por la flecha número 6). Esto hace suponer, por tanto, que
la dirección de retorno no puede ser decidida cuando se ejecuta esta instrucción sino en un momento anterior.
El instante en el que se sabe dónde ha de retomarse la ejecución tras una subrutina es precisamente en el momento de su
invocación. Cuando el procesador está ejecutando la instrucción CALL obtiene la dirección de retorno como la de la instrucción
siguiente en la secuencia.
Por tanto, el procesador, además de modificar la secuencia, al ejecutar la instrucción CALL debe guardar la dirección de retorno en
un lugar prefijado del cual será obtenido por la instrucción RET. Pero, durante la ejecución de un programa es preciso almacenar
múltiples direcciones de retorno de forma simultanea.
El que las subrutinas se puedan invocar de forma anidada hace que la utilización de los registros de propósito general para
almacenar la dirección de retorno no sea factible. La alternativa es almacenarlas en memoria, pero la instrucción RET debe tener
acceso a su operando implícito siempre en el mismo lugar. Además, esta zona de memoria debe poder almacenar un número
arbitrario de direcciones de retorno, pues la invocación de subrutinas se puede anidar hasta niveles arbitrarios de profundidad.
Por tanto, se necesita un área de memoria que pueda almacenar tantas direcciones de retorno como subrutinas están siendo
invocadas de forma anidada en un momento de la ejecución de un programa. La propiedad que tienen estas direcciones es que se
almacenan por la instrucción CALL en un cierto orden, y son utilizadas por la instrucción RET en el orden inverso.
La estructura especialmente concebida para este propósito es la pila. En ella se almacena la dirección de retorno mientras se
ejecuta el cuerpo de una subrutina. En caso de invocaciones anidadas, las direcciones de retorno pertinentes se guardan en la pila
y están disponibles para la instrucción RET en el orden preciso.
La instrucción CALL, por tanto, realiza dos tareas: pasa a ejecutar la instrucción en la dirección dada como operando y almacena
en la cima de la pila la dirección de la instrucción siguiente (al igual que lo haría una instrucción PUSH) que será la instrucción
de retorno. Por su parte, la instrucción RET obtiene el dato de la cima de la pila (igual que lo haría la instrucción POP) y ejecuta
un salto incondicional al lugar que indica. Ambas instrucciones, por tanto, modifican el contador de programa.
Del funcionamiento de estas instrucciones se concluye que la cima de la pila justo antes de la ejecución de la primera instrucción
de una subrutina contiene la dirección de retorno, y por tanto, antes de ejecutar la instrucción RET debe apuntar exactamente a la
misma posición. Aunque esta condición es esencial para que el retorno de la subrutina se haga al lugar correcto, los procesadores
no realizan comprobación alguna de que así se produce. Por lo tanto, es responsabilidad del programador en ensamblador el
manipular la pila en una subrutina de forma que la cima de la pila al comienzo de la ejecución sea exactamente la misma que
justo antes de ejecutar la última instrucción.
Durante la ejecución de la subrutina se pueden hacer las operaciones necesarias sobre la pila siempre y cuando se conserve la
dirección de retorno. Esta es la explicación de por qué en la sección 6.4 se estipuló la regla de que la pila al comienzo y final de
un programa debe ser la misma. El programa en ensamblador que comienza a ejecutar a partir de la etiqueta main también es
Programación en ensamblador de la
arquitectura IA-32
154 / 198
una subrutina que invoca el sistema operativo, y por lo tanto se debe garantizar que la cima es idéntica al comienzo y al final del
programa pues contiene la dirección de retorno.
8.4.2.
Paso de parámetros y devolución de resultados
En general una subrutina consiste en una porción de código que realiza una operación con un conjunto de valores proporcionados
por el programa que la invoca denominados parámetros, y que devuelve un resultado. Los parámetros son copias de ciertos
valores que se ponen a disposición de la subrutina y que tras acabar su ejecución se descartan. El resultado, en cambio, es un
valor que la subrutina calcula y copia en un lugar para que el programa invocador lo utilice. La figura 8.14 ilustra la manipulación
de parámetros y resultado.
Figura 8.14: Parámetros y resultado de una subrutina
Se necesita establecer las reglas que estipulen cómo y dónde deposita el programa que invoca una subrutina estos valores y cómo
y dónde se deposita el resultado. En adelante, a la porción de código que realiza la llamada a la subrutina se le denominará
‘programa llamador’ mientras que al código de la subrutina se le denominará ‘programa llamado’. Las llamadas a subrutinas se
puede hacer de forma ‘anidada’, es decir, un programa llamado invoca a su vez a otra subrutina con lo que pasa a comportarse
como programa llamador.
8.4.2.1.
Paso de parámetros a través de registro
El paso de parámetros a través de registro consiste en que el programa llamador y el llamado asumen que los parámetros se almacenan en ciertos registros específicos. Antes de la instrucción de llamada el programa llamador deposita los valores pertinentes
en estos registros y la subrutina comienza a procesarlos directamente.
En general, dada una rutina que recibe n parámetros y devuelve m resultados, se necesita definir en qué registro deposita el
programa llamador la copia de cada uno de los n parámetros, y en qué registro deposita la subrutina la copia del resultado
obtenido. El ejemplo 8.5 muestra las instrucciones necesarias en el caso de una subrutina que recibe como parámetros dos
enteros a través de los registros %eax y %ebx y devuelve el resultado a través del registro %ecx.
Programación en ensamblador de la
arquitectura IA-32
155 / 198
Ejemplo 8.5 Paso de parámetros a través de registros
Programa llamador
mov param1, %eax
mov param2, %ebx
call subrutina
mov %ecx, resultado
Programa llamado
subrutina:
push %...
# Salvar registros ←utilizados
push %...
# excepto %eax, %ebx y
%ecx
...
←-
# Realizar cálculos
mov ..., %ecx # Poner resultado en % ←ecx
pop %...
pop %...
ret
# Restaurar registros
Al utilizar los registros %eax y %ebx para pasar los parámetros la subrutina no salva su contenido pues dispone de esos valores
como si fuesen suyos. El registro %ecx, al contener el resultado, tampoco se debe salvar ni restaurar.
El principal inconveniente que tiene este esquema es el número limitado de registros de propósito general. En los lenguajes de
alto nivel no hay límite en el número de parámetros que puede tener una función o método en su definición, y por tanto, si este
número es muy alto, el procesador puede no tener registros suficientes.
A pesar de esta limitación, en el caso de subrutinas con muy pocos parámetros y que devuelve un único resultado, este mecanismo
es muy eficiente pues el procesador no precisa almacenar datos en memoria. Los sistemas operativos suele utilizar esta técnica
para invocaciones de subrutinas internas de estas características.
8.4.2.2.
Paso de parámetros a través de memoria
El paso de parámetros a través de memoria consiste en definir una zona de memoria conocida tanto para el programa llamador
como para el llamado y en ella se copia el valor de los parámetros y el del resultado para su intercambio. La ventaja de esta técnica
radica en que permite tener un número arbitrario de parámetros, pues tan sólo se requiere una zona más grande de memoria.
En general, para una subrutina que recibe n parámetros y devuelve m resultados se define una zona de memoria cuyo tamaño es
la suma de los tamaños de todos ellos así como el orden en el que estarán almacenados. El ejemplo 8.6 muestra la instrucciones
necesarias para el caso de una subrutina que precisa tres parámetros de tamaño 32 bits y devuelve dos resultados de tamaño 8
bits. Se asume que la zona de memoria está definida a partir de la etiqueta params.
Programación en ensamblador de la
arquitectura IA-32
156 / 198
Ejemplo 8.6 Paso de parámetros a través de memoria
Programa llamador
mov params, %eax
mov v1, %ebx
mov %ebx, ( %eax)
mov v2, %ebx
mov %ebx, 4( %eax)
mov v3, %ebx
mov %ebx, 8( %eax)
call subrutina
mov 12( %eax), %ah
mov 13( %eax), %ah
Programa llamado
subrutina:
push %...
# Salvar registros
utilizados
push %...
←-
mov params, %ebx # Acceso a los ←parámetros
mov ( %ebx), ...
mov 4( %ebx), ...
mov 8( %ebx), ...
...
# Realizar cálculos
mov %dh, 12( %ebx) # Poner resultado
mov %dl, 13( %ebx)
pop %...
# Restaurar
registros
pop %...
ret
←-
El principal inconveniente de esta técnica es que necesita tener estas zonas de memoria previamente definidas. Además, en el
caso de invocación anidada de subrutinas, se necesitan múltiples espacios de parámetros y resultados pues mientras la ejecución
de una subrutina no termina, éstos siguen teniendo validez.
El incluir esta definición junto con el código de una subrutina parecería una solución idónea, pues al escribir sus instrucciones se
sabe el número y tamaño de parámetros y resultados. Pero existen subrutinas denominadas ‘recursivas’ que se caracterizan por
contener una invocación a ellas mismas con un conjunto de parámetros diferente.
La conclusión es que se precisan tantas zonas para almacenar parámetros y devolver resultados como invocaciones pendientes de
terminar en cada momento de la ejecución. Pero este requisito de vigencia es idéntico al que tiene la dirección de retorno de una
subrutina. Es más, la dirección de retorno se puede considerar un valor más que el programa llamador pasa al llamado para que
éste lo utilice. En esta observación se basa la siguiente técnica de paso de parámetros.
8.4.2.3.
Paso de parámetros a través de la pila
El paso de parámetros a través de la pila tiene múltiples ventajas. En primer lugar, tanto parámetros como resultados se pueden
considerar resultados temporales que tienen validez en un período muy concreto de la ejecución de un programa por lo que la
pila favorece su manipulación. Además, dada una secuencia de llamadas a subrutinas, el orden de creación y destrucción de estos
parámetros es el inverso tal y como permiten las instrucciones de gestión de la pila.
En general, para una subrutina que recibe n parámetro y devuelve m resultados el programa llamador reserva espacio en la cima
de la pila para almacenar estos datos justo antes de ejecutar la instrucción CALL y lo elimina justo a continuación.
Pero en la subrutina es necesario un mecanismo eficiente para acceder a la zona de parámetros y resultados. Al estar ubicada en
la pila lo más intuitivo es utilizar el registro %esp que apunta a la cima y el modo de direccionamiento base + desplazamiento
mediante la utilización de los desplazamientos pertinentes. Pero el inconveniente de este método es que la cima de la pila puede
fluctuar a lo largo de la ejecución de la subrutina y por tanto los desplazamientos a utilizar varían.
Para que el acceso a los parámetros no dependa de la posición de la cima de la pila y se realice con desplazamientos constantes a
lo largo de la ejecución de la subrutina, las primeras instrucciones almacenan una copia del puntero de pila en otro registro (generalmente %ebp) y al fijar su valor, los accesos a la zona de parámetros y resultados se realizan con desplazamientos constantes.
Pero para preservar el valor de los registros, antes de crear este duplicado es preciso guardar en la pila una copia de este registro.
Programación en ensamblador de la
arquitectura IA-32
157 / 198
El ejemplo 8.7 muestra las instrucciones necesarias para el caso de una subrutina que precisa tres parámetros de tamaño 32 bits
y devuelve un resultado de 8 bits.
Ejemplo 8.7 Paso de parámetros a través de la pila
Programa llamador
sub $4, %esp
push v3
push v2
push v1
call subrutina
add $12, %esp
pop resultado
Programa llamado
subrutina:
push %ebp
# Guardar registro ←%ebp
mov %esp, %ebp
# Apuntar a punto ←fijo en pila
push %...
# Salvar registros ←utilizados
push %...
mov 8( %ebp), ... # Acceso a los ←parámetros
mov 12( %ebp), ...
mov 16( %ebp), ...
...
# Realizar cálculos
mov ..., 20( %ebx) # Poner resultado
pop %...
# Restaurar ←registros
pop %...
mov %ebp, %esp
# Restaurar %esp y
%ebp
pop %ebp
ret
←-
La primera instrucción del programa llamador modifica el puntero de la pila para reservar espacio donde almacenar el resultado.
Al ser una posición de memoria sobre la que se escribirá el resultado no es preciso escribir ningún valor inicial, de ahí que no se
utilice la instrucción push.
A continuación se depositan en la pila los valores de los parámetros. El orden en que se almacenan debe ser conocido por el
programa llamador y el llamado. Tras la ejecución de la subrutina se eliminan de la pila los parámetros, que al desempeñar
ningún papel, basta con corregir el valor de la cima dejando la pila preparada para obtener el valor del resultado.
Por su parte, el programa llamado guarda la copia del registro %ebp para justo a continuación copiar el valor de %esp y por
tanto fija su valor a la cima de la pila. A partir de este instante, cualquier dato que se ponga en la pila no afecta el valor de %ebp y el desplazamiento para acceder a los parámetros es respectivamente de 8, 12 y 16 pues en la posición debajo de la cima
se encuentra la dirección de retorno. Para depositar el resultado se utiliza el desplazamiento 20. Tras terminar el cálculo del
resultado se procede a deshacer la estructura de datos creada en la pila en orden inverso. Primero se descargan de la pila los
registros salvados y a continuación se restaura el valor del registro %ebp dejando en la cima la dirección de retorno que necesita
la instrucción ret.
A la porción de memoria en la pila que contiene el espacio para la devolución de resultados, los parámetros, la dirección de
retorno, la copia de %ebp se le denomina el ‘bloque de activación’. Al registro %ebp que ofrece un punto fijo de referencia a los
datos se le denomina el ‘puntero’ al bloque de activación.
8.4.2.4.
Almacenamiento de variables locales a una subrutina
Además de la capacidad de definir y ejecutar subrutinas, los lenguajes de programación de alto nivel permiten la definición de
variables locales. El ámbito de validez se reduce al instante en que se está ejecutando el código de la subrutina. De nuevo se
Programación en ensamblador de la
arquitectura IA-32
158 / 198
precisa un mecanismo que gestione de forma eficiente estas variables. El ejemplo 8.8 muestra la definición de un método en Java
en el que las variables i, str y p son de ámbito local.
Ejemplo 8.8 Definición de variables locales a un método
int traducir(Representante r, int radix) {
int i;
// Variables locales
String str;
Punto p;
i = 0;
str = new String(...);
punto = ...
...
return i;
}
El ámbito de estas variables no impide que el valor de alguna de ellas sea devuelto como resultado tal y como muestra el método
del ejemplo. La última línea copia el valor de la variable local en el lugar en el que se devuelve el resultado, y por tanto está
disponible para el programa llamador.
El ámbito de estas variables es idéntico al de los parámetros y al de la dirección de retorno, por lo que para almacenar estas
variables se pueden utilizar cualquiera de las tres técnicas descritas anteriormente: en registros, en posiciones arbitrarias de
memoria y en la pila.
El almacenamiento en la pila se hace en el bloque de activación justo a continuación de haber establecido el registro %ebp
como puntero al bloque de activación. De esta forma, como el número de variables locales es siempre el mismo, utilizando
desplazamientos con valores negativos y el registro base %ebp se accede a ellas desde cualquier punto de la subrutina.
8.5.
Gestión del bloque de activación
De las técnicas descritas para la invocación de subrutinas, la que crea el bloque de activación en la pila es la más utilizada por los
lenguajes de alto nivel. En la Tabla 8.1 se muestran los pasos a seguir por el programa llamador y el llamado para crear y destruir
el bloque de activación.
Tras restaurar el valor de los registros utilizados por la subrutina, el estado de la pila es tal que en la cima se encuentra el
espacio para las variables locales y a continuación la copia del valor anterior de %ebp. Como el propio %ebp apunta a esa misma
posición, la forma más fácil de restaurar la cima de la pila al valor correcto es asignándole a %esp el valor de %ebp. De esta
forma no es preciso tener en cuenta el tamaño de la zona reservada para las variables locales. Esta técnica funciona incluso en el
caso de que una subrutina no tenga variables locales.
8.6.
Ejemplo de evolución del bloque de activación
Considérese un programa que invoca a la subrutina cuenta que dada la dirección de un string terminado en cero y un carácter,
devuelve el número de veces que el carácter aparece en el string como entero. La pila que recibe la subrutina tiene, en orden
creciente de desplazamiento desde el puntero al bloque de activación, la dirección de retorno (siempre está presente como primer
valor), la dirección del string, el carácter a comprobar como byte menos significativo del operando en la pila y el espacio para el
resultado. El fragmento de código para invocar a esta subrutina se muestra en el ejemplo 8.9. Se asume que la letra a buscar está
almacenada en la etiqueta letra y el string en la etiqueta mensaje.
Programación en ensamblador de la
arquitectura IA-32
159 / 198
Programa llamador
Programa llamado
1. Reserva espacio en la pila para almacenar los
resultados.
1. Salva el valor de %ebp para utilizar como puntero al
bloque de activación.
2. Carga los parámetros en orden en la pila.
2. Copia el valor de %esp en %ebp.
3. Ejecuta la instrucción de llamada a subrutina.
3. Reserva espacio en la pila para variables locales.
4. Descarga los parámetros de la pila.
4. Salva los registros que se utilizan en la subrutina.
5. Obtiene el resultado de la pila.
5. Ejecuta el código de la subrutina.
6. Deposita el resultado en el espacio reservado a tal
efecto.
7. Restaura el valor de los registros salvados.
8. Iguala la cima de la pila al puntero al bloque de
activación.
9. Restaura el valor del registro %ebp.
10. Ejecuta la instrucción de retorno de subrutina.
Tabla 8.1: Pasos para la gestión del bloque de activación
Ejemplo 8.9 Invocación de la rutina cuenta
...
sub $4, %esp
push letra
push mensaje
call cuenta
add $8, %esp
pop %eax
...
# Espacio para el resultado
# Parámetros en el orden correcto
# Invocación de la subrutina
# Descarga del espacio para parámetros
# Resultado en %eax
La instrucción push letra tiene por operando una etiqueta que apunta a un dato de tamaño byte. Como los operandos de
la pila son de 4 bytes, en ella se depositan la letra y los tres siguientes bytes. Esto no tiene importancia porque la subrutina
únicamente accede al byte de menos peso tal y como se ha especificado en su definición. La figura 8.15 muestra la evolución de
la pila desde el punto de vista del programa llamador.
Programación en ensamblador de la
arquitectura IA-32
Figura 8.15: Evolución de la pila desde el punto de vista del programa llamador
El código de la subrutina cuenta se muestra en el ejemplo 8.10.
160 / 198
Programación en ensamblador de la
arquitectura IA-32
161 / 198
Ejemplo 8.10 Código de la rutina cuenta
cuenta:
push %ebp
mov %esp, %ebp
# Salvar %ebp
# Crear puntero a bloque de activación
sub $4, %esp
# Espacio para variable local: contador
push %eax
push %ebx
push %ecx
# Salvar registros utilizados
movl $0, -4( %ebp)
mov 8( %ebp), %eax
mov 12( %ebp), %cl
mov $0, %ebx
#
#
#
#
cmpb $0, ( %eax, %ebx)
je res
# Detectar final de string
cmpb %cl, ( %eax, %ebx)
jne incr
incl -4( %ebp)
# Compara letra dada con letra en string
# Si iguales incrementar contador
incr:
inc %ebx
jmp bucle
# Incrementar registro índice
res:
mov -4( %ebp), %eax
mov %eax, 16( %ebp)
# Mover contador a resultado
pop %ecx
pop %ebx
pop %eax
# Restaurar registros
mov %ebp, %esp
pop %ebp
ret
# Eliminar variables locales
# Restaurar %ebp
bucle:
Contador = 0
Dirección base del string
Letra a comparar en %cl
Registro índice
La subrutina almacena el número de veces que aparece la letra en el string dado como una variable local en la pila. Tras finalizar
el bucle, su valor se transfiere al lugar en el que lo espera el programa llamador. La figura 8.16 muestra la evolución de la pila
durante la ejecución de la subrutina.
Programación en ensamblador de la
arquitectura IA-32
162 / 198
Figura 8.16: Evolución de la pila durante la llamada a cuenta
8.7.
Ejercicios
1. Partiendo del código del ejemplo 8.1, traducir la nueva versión que se muestra en la siguiente figura con la modificación
en la última parte de la condición:
Código de alto nivel
if ((x <= 3) && (i == j++)) {
Bloque A
} else {
Bloque B
}
¿En qué lugar se debe insertar la instrucción que incrementa la variable j para mantener el significado de esta construcción?
Si la primera sub-expresión de la conjunción es falsa, ¿se incrementa la variable j?
2. Traducir el siguiente bucle en lenguaje de alto nivel a la porción de código equivalente en lenguaje ensamblador de la
arquitectura IA-32. Se asume que todas las variables son de tipo entero.
Programación en ensamblador de la
arquitectura IA-32
163 / 198
for (i = a + b; ((c + d) >= 10) && (i <= (d + e)); i = i + f - g - h ) {
g--;
f++;
a = b + 10;
}
Las variables han sido definidas de la siguiente forma:
a:
b:
c:
d:
e:
f:
g:
h:
i:
.int
.int
.int
.int
.int
.int
.int
.int
.int
10
20
30
40
50
60
70
80
10
La solución puede utilizar cualquier modo de direccionamiento excepto el modo absoluto. Al tratarse de una porción de
código aislada, no es preciso salvar ni restaurar el contenido de los registros. La solución debe ser lo más corta posible y
utilizar el menor número de registros de propósito general.
3. La mayor parte de los lenguajes de alto nivel que ofrecen bucles de tipo while también ofrecen otro bucle de estructura
similar denominado do/while. La diferencia es que el bloque comienza por la palabra clave do seguida de un bloque de
código entre llaves y termina con la palabra while seguida de una condición booleana entre paréntesis.
El bloque de código se ejecuta al menos una vez y tras él se evalúa la condición para determinar si se continúa iterando.
Escribir la estructura en codigo ensamblador resultante de traducir esta construcción. Se puede asumir que el resultado de
evaluar la condición está almacenado en %eax.
4. Escribir las reglas para el paso de parámetros y devolución de resultado a través de la pila:
Por parte del programa que llama a la subrutina:
Por parte de la subrutina:
5. Un programador ha escrito la siguiente rutina:
rutina:
push %ebp
mov %esp, %ebp
add $13, 4( %ebp)
pop %ebp
ret
El programa principal que invoca a esta rutina contiene las siguientes instrucciones tal y como las muestra el compilador
en su informe sobre el restulado del ensamblaje:
1
2
2
2
2
2
3
4
5
6
7
8
9
10
0000 50726F67
72616D61
20746572
6D696E61
646F2E0A
msg:
.data
.asciz "Programa terminado.\n"
.text
.globl main
main:
0000 50
0001 51
0002 52
push %eax
push %ecx
push %edx
Programación en ensamblador de la
arquitectura IA-32
11
11
12
13
13
14
14
15
16
17
18
19
20
164 / 198
0003 E8FCFFFF
FF
call rutina
0008 68000000
00
000d E8FCFFFF
FF
0012 83C404
push $msg
0015
0016
0017
0018
pop %edx
pop %ecx
pop %eax
ret
5A
59
58
C3
# Llamada a rutina dada
call printf
add $4, %esp
Explicar detalladamente cuál es el resultado de la ejecución de este programa. ¿Ejecuta correctamente? ¿Qué escribe por
pantalla? ¿Por qué?
6. El paso de resultados y devolución de resultados se puede hacer a través de registros, memoria o la pila, pero nada impide
utilizar una técnica diferente para parámetros y resultados. Describir las reglas de creación del bloque de activación para
los siguientes supuestos.
a. Parámetros en la pila y resultados a través de registros.
b. Parámetros en registro y resultados a través de la pila.
Programación en ensamblador de la
arquitectura IA-32
165 / 198
Apéndice A
Subconjunto de instrucciones de la arquitectura
IA-32
La arquitectura IA-32 dispone de un lenguaje máquina con cientos de instrucciones para múltiples tipos de tareas diferentes. En
este apéndice se describen en detalla tan sólo un subconjunto mínimo que permite realizar operaciones sencillas sobre tipos de
datos tales como strings y enteros.
A.1.
Nomenclatura
Para la descripción detallada del subconjunto de instrucciones de esta arquitectura se utiliza la siguiente nomenclatura:
%reg: Denota cualquiera de los ocho registros de propósito general.
inm: Denota una constante numérica. En ensamblador, el valor numérico debe ir precedido por el símbolo $. También se
incluyen en esta categoría las expresiones $etiq, donde etiq corresponde con el nombre de una de las etiquetas definidas
en el código.
mem: Denota el nombre de una etiqueta definida en el código. Nótese que en este caso no se utiliza el prefijo $ pues si fuese
así se trataría de una constante.
INSs: Cuando el código de operación de una instrucción termina en s esto denota que la instrucción requiere un sufijo de
tamaño B, W o L.
A.2.
Instrucciones de movimiento de datos
A.2.1.
MOV: Movimiento de datos
Instrucción
Descripción
MOV %regA, %regB
Mueve el contenido de %regA al registro %regB.
MOV $inm, %reg
Mueve inm al registro %reg.
MOV mem, %reg
Mueve el contenido almacenado en la posición mem al
registro %reg.
Programación en ensamblador de la
arquitectura IA-32
Instrucción
166 / 198
Descripción
MOV %reg, mem
Mueve el contenido de %reg a la posición mem.
MOVs $inm, mem
Mueve inm, codificado con los bits especificados por el
sufijo s al dato cuyo tamaño está especificado por el sufijo
s y que está almacenado a partir de la posición mem.
La instrucción de mover recibe dos operandos y mueve el primero al lugar donde indica el segundo. Esta instrucción no modifica
ninguno de los flags de la palabra de estado.
A.2.2.
PUSH: Instrucción de carga sobre la pila
Instrucción
Descripción
PUSH %reg
Almacena el contenido de %reg en la posición anterior a la
que apunta el puntero de pila.
PUSH mem
Almacena el dato de 32 bits que está almacenado a partir
de la posición mem en la posición anterior a la que apunta
el puntero de pila.
PUSH $inm
Almacena inm codificado con 32 bits en la posición
anterior a la que apunta el puntero de pila.
Esta instrucción recibe un único operando y manipula siempre operandos de 32 bits, por lo tanto no es preciso utilizar ningún
sufijo de tamaño. El procesador toma el valor del registro puntero de pila, le resta 4 y almacena el operando dado en los cuatro
bytes de memoria a partir de la posición del puntero de pila. Esta instrucción no modifica ninguno de los flags de la palabra de
estado.
A.2.3.
POP: Instrucción de descarga de la pila
Instrucción
Descripción
POP %reg
Almacena el contenido al que apunta el puntero de pila
en %reg. Modifica el puntero a la cima para que apunte a
la siguiente posición de la pila.
POP mem
Almacena los 32 bits a los que apunta el puntero de pila a
partir de la posición mem. Modifica el puntero a la cima
para que apunte a la siguiente posición de la pila.
Esta instrucción recibe un único operando y manipula siempre operandos de 32 bits, por lo tanto no es preciso utilizar ningún
sufijo de tamaño. El procesador toma el valor del registro puntero de pila y mueve ese dato al lugar que le indique el operando
de la instrucción. Tras esta transferencia, se suma el valor 4 al registro puntero de pila. Esta instrucción no modifica ninguno de
los flags de la palabra de estado.
A.2.4.
XCHG: Instrucción de intercabmio
Programación en ensamblador de la
arquitectura IA-32
Instrucción
167 / 198
Descripción
Intercambia los valores de sus dos operandos. Cuando un
operando está en memoria, se intercambia el dato
almacenado a partir de la posición de memoria dada. Al
menos uno de los operandos debe ser de tipo registro.
XCHG %regA, %regB
XCHG %reg, mem
XCHG mem, %reg
Esta instrucción utiliza un registro temporal interno del procesador para intercambiar los operandos. No se modifica ninguno de
los flags de la palabra de estado.
A.3.
Instrucciones aritméticas
A.3.1.
ADD: Instrucción de suma
Instrucción
Descripción
ADD %regA, %regB
Suma el contenido de %regA al contenido de %regB.
ADD $inm, %reg
Suma inm al contenido de %reg.
ADD mem, %reg
Suma el contenido almacenado en la posición mem al
contenido de %reg.
ADD %reg, mem
Suma el contenido de %reg al contenido almacenado en la
posición mem.
ADDs $inm, mem
Suma inm, codificado con los bits especificados por el
sufijo s al dato cuyo tamaño está especificado por el sufijo
s y que está almacenado a partir de la posición mem.
La instrucción de suma recibe dos operandos, los suma y deposita el resultado en el lugar especificado por el segundo operando.
Se pierde, por tanto, el valor del segundo operando. Los flags de la palabra de estado OF, SF, ZF, PF y CF se modifican de
acuerdo al resultado obtenido.
A.3.2.
SUB: Instrucción de resta
Instrucción
Descripción
SUB %regA, %regB
Resta el contenido de %regA del contenido de %regB.
SUB $inm, %reg
Resta inm del contenido de %reg.
Programación en ensamblador de la
arquitectura IA-32
Instrucción
168 / 198
Descripción
SUB mem, %reg
Resta el contenido almacenado en la posición mem del
contenido de %reg.
SUB %reg, mem
Resta el contenido de %reg del contenido almacenado en
la posición mem.
SUBs $inm, mem
Resta inm, codificado con los bits especificados por el
sufijo s del dato cuyo tamaño está especificado por el
sufijo s y que está almacenado a partir de la posición mem.
La instrucción de resta recibe dos operandos op1 y op2, realiza la operación op2 - op1 y almacena el resultado en el lugar
especificado por el segundo operando. Se pierde, por tanto, el valor del segundo operando. Los flags de la palabra de estado OF,
SF, ZF, PF y CF se modifican de acuerdo al resultado obtenido.
A.3.3.
INC: Instrucción de incremento
Instrucción
Descripción
INC %reg
Suma uno al contenido de %reg.
INCs mem
Suma uno, codificado con los bits especificados por el
sufijo s al dato cuyo tamaño está especificado por el sufijo
s y que está almacenado a partir de la posición mem.
La instrucción de incremento recibe un único operando al que le suma el valor 1. Esta instrucción tiene la particularidad de que
no modifica el flag de acarreo (CF). Los flags de la palabra de estado OF, SF, PF y ZF sí se modifican de acuerdo al resultado
obtenido.
A.3.4.
DEC: Instrucción de decremento
Instrucción
Descripción
DEC %reg
Resta uno del contenido de %reg.
DECs mem
Resta uno, codificado con los bits especificados por el
sufijo s del dato cuyo tamaño está especificado por el
sufijo s y que está almacenado a partir de la posición mem.
La instrucción de decremento recibe un único operando al que le resta el valor 1. Esta instrucción tiene la particularidad de que
no modifica el flag de acarreo (CF). Los flags de la palabra de estado OF, SF, PF y ZF sí se modifican de acuerdo al resultado
obtenido.
A.3.5.
NEG: Instrucción de cambio de signo
Programación en ensamblador de la
arquitectura IA-32
169 / 198
Instrucción
Descripción
NEG %reg
Toma el contenido de %reg, cambia su signo y se
almacena en %reg.
NEGs mem
Toma el número codificado con los bits especificados por el
sufijo s y almacenado a partir de la posición mem y le
cambia el signo.
La instrucción de decremento recibe un único operando y la operación que realiza es equivalente a multiplicar por -1. El operando
se asume que está codificado en complemento a 2. Esta instrucción tiene la particularidad de que asigna directamente el valor 1
al flag de acarreo (CF) excepto si el operando tiene el valor 0. Los flags de la palabra de estado OF, SF, PF y ZF sí se modifican
de acuerdo al resultado obtenido.
A.3.6.
MUL: Instrucción de multiplicación sin signo
Instrucción
MUL %reg
MULs mem
Descripción
Multiplica el contenido de %reg por el registro %al, %ax
o %eax dependiendo de si el tamaño del operando es de 8,
16 o 32 bits respectivamente. El resultado se almacena
en %ax, el registro de 32 bits resultante de
concatenar %dx: %ax ( %dx el más significativo) o el
registro de 64 bits resultante de concatenar %edx: %eax
( %edx el más significativo) dependiendo de si el operando
dado es de 8, 16 o 32 bits respectivamente.
Multiplica el dato codificado con los bits especificado por
el sufijo s y almacenado a partir de la posición mem por el
registro %al, %ax o %eax dependiendo de si el tamaño del
operando es de 8, 16 o 32 bits respectivamente. El resultado
se almacena en %ax, el registro de 32 bits resultante de
concatenar %dx: %ax ( %dx el más significativo) o el
registro de 64 bits resultante de concatenar %edx: %eax
( %edx el más significativo) dependiendo de si el sufijo
especifica 8, 16 o 32 bits respectivamente.
Esta instrucción utiliza dos operandos, pero uno de ellos es implícito, es decir, no aparece en la instrucción. Este multiplicando
se obtiene de %al, %ax o %eax dependiendo de si la operación debe utilizar datos de 8, 16 o 32 bits.
Se permite, por tanto, la utilización de 3 posibles tamaños de operandos: 8, 16 y 32 bits. Ambos operandos tienen idéntico
tamaño. El problema de la multiplicación es que si en general se multiplican dos números de tamaño n y m bits, se precisan
n + m bits para representar el restulado. Como consecuencia de esto, el resultado de esta instrucción se almacena en %ax si la
multiplicación es de dos números de 8 bits, en el registro de 32 bits resultante de concatenar %dx: %ax, con %dx como parte
más significativa, si se multiplican dos números de 16 bits, y en caso de multiplicación de dos números de 32 bits, se almacena
en la concatenación %edx: %ecx como un registro de 64 bits con %edx como parte más significativa.
La razón por la que no se utiliza uno de los registros de 32 bits para almacenar el producto de dos operandos de 16 bits es
histórica. Han existido procesadores anteriores a este en el que sólo había registros de 16 bits. Para almacenar un operando de
32 bits había que realizar esta concatenación. Los nuevos procesadores disponen de registros de 32 bits, pero por mantener el
lenguaje máquina compatible con las versiones anteriores, esta instrucción conserva la descripción anterior. La Tabla A.1 muestra
la relación entre el tamaño de los operandos y el lugar en el que se almacena el resultado.
Los únicos flags que se modifican con esta instrucción son CF y OF. Ambos bits se ponen a 1 cuando la mitad más significativa
del resultado tiene alguno de sus bits a 1. El resto de flags tienen valores no definidos tras ejecutar esta instrucción.
Programación en ensamblador de la
arquitectura IA-32
Tamaño de Operando
Byte
Word (2 bytes)
Doubleword (4 bytes)
170 / 198
Operando Implícito
%al
%ax
%eax
Operando Explícito
%reg ó mem
%reg ó mem
%reg ó mem
Resultado
%ax
%dx: %ax
%edx: %eax
Tabla A.1: Opciones de la multiplicación sin signo
A.3.7.
DIV: Instrucción de división sin signo
Instrucción
Descripción
Esta instrucción tiene tres versiones posibles. Divide el
registro %ax, los 32 bits obtenidos al concatenar los
registros %dx: %ax o los 64 bits obtenidos al concatenar
los registros %edx: %eax entre %reg. Como resultado se
deposita el cociente en %al, %ax o %eax y el resto
en %ah, %dx o %edx respectivamente. Se consideran todos
los operandos como números naturales.
Esta instrucción tiene tres versiones posibles. Divide el
registro %ax, los 32 bits obtenidos al concatenar los
registros %dx: %ax o los 64 bits obtenidos al concatenar
los registros %edx: %eax entre el dato codificado según el
sufijo s y almacenado a partir de la posición de memoria
mem. Como resultado se deposita el cociente en %al, %ax
o %eax y el resto en %ah, %dx o %edx respectivamente.
DIV %reg
DIVs mem
Esta instrucción utiliza dos operandos: dividendo y divisor. Tan sólo se especifica en la instrucción el divisor. El dividendo es
implícito y tiene tamaño doble al del divisor y se obtiene de %ax (16 bits), la concatenación de los registros %dx: %ax (32 bits)
o la concatenación de los registros %edx: %eax (64 bits) dependiendo de si el divisor es de 8, 16 o 32 bits respectivamente.
La instrucción devuelve dos resultados: cociente y resto. El cociente se devuelve en %al, %ax o %eax y el resto en %ah, %dx
o %edx. La Tabla A.2 muestra la relación entre el tamaño de los operandos y el lugar en el que se almacena el resultado.
Tamaño de
Operandos
Dividendo
Word/Byte
%ax
Doubleword/Word
%dx: %ax
Quadword/Doubleword%edx: %eax
Divisor
1 byte %reg ó
mem
2 bytes %reg ó
mem
4 bytes %reg ó
mem
Cociente
Resto
Valor Máximo
Cociente
%al
%ah
255
%ax
%dx
65.535
%eax
%edx
232 - 1
Tabla A.2: Opciones de la división sin signo
Ninguno de los flags de la palabra de estado tiene valor definido tras ejecutar esta instrucción.
A.3.8.
IMUL: Instrucción de multiplicación con signo
La instrucción para multiplicar dos enteros con signo tiene tres posibles formatos dependiendo del número de operandos explícitos.
El formato con un único operando se interpreta de forma idéntica a la instrucción de multiplicación sin signo MUL (ver sección
A.3.6). El segundo operando es implícito y tiene idéntico tamaño al explícito. El resultado tiene tamaño doble que los operandos.
Instrucción
IMUL %reg
Descripción
Multiplica el contenido de %reg por el registro %al, %ax
o %eax dependiendo de si el tamaño del operando es de 8,
16 o 32 bits respectivamente. El resultado se almacena
en %ax, el registro de 32 bits resultante de
concatenar %dx: %ax ( %dx el más significativo) o el
registro de 64 bits resultante de concatenar %edx: %eax
Programación en ensamblador de la
arquitectura IA-32
Instrucción
IMULs mem
171 / 198
Descripción
Multiplica el dato codificado con los bits especificado por
el sufijo s y almacenado a partir de la posición mem por el
registro %al, %ax o %eax dependiendo de si el tamaño del
operando es de 8, 16 o 32 bits respectivamente. El resultado
se almacena en %ax, el registro de 32 bits resultante de
concatenar %dx: %ax ( %dx el más significativo) o el
registro de 64 bits resultante de concatenar %edx: %eax
( %edx el más significativo) dependiendo de si el sufijo
especifica 8, 16 o 32 bits respectivamente.
La segunda versión de esta instrucción contiene dos operandos. El segundo de ellos es a la vez multiplicando y destino del
resultado y debe ser uno de los registro de propósito general. En esta versión, tanto operandos como resultado tienen idéntico
tamaño, con lo que hay una mayor probabilidad de overflow.
Instrucción
Descripción
MUL %regA, %regB
Multiplica el contenido de %regA por el contenido
de %regB. Los registros deben tener idéntico tamaño y
sólo pueden ser de 16 o 32 bits, no de 8.
MUL $inm, %reg
Multiplica inm por el contenido de %reg. El registro sólo
puede ser de 16 o 32 bits.
MUL mem, %reg
Multiplica el contenido almacenado a partir de la posición
mem por el contenido de %reg. El registro sólo puede ser
de 16 o 32 bits.
La tercera versión de la instrucción de multiplicación consta de tres operandos. El primero es un multiplicando y debe ser una
constante, el segundo es también un multiplicando y debe ser una posición de memoria o un registro. El tercer operando es donde
se guarda el resultado y debe ser un registro de propósito general.
Instrucción
Descripción
MUL $inm, %regA, %regB
Multiplica $inm por el contenido de %regA y almacena el
restulado en %regB. Los registros deben tener idéntico
tamaño y sólo pueden ser de 16 o 32 bits, no de 8.
MUL $inm, mem, %reg
Multiplica $inm por el dato almacenado a partir de la
posición mem y almacena el resultado en %reg. El registro
sólo puede ser de 16 o 32 bits.
De las tres versiones posibles para esta operación, sólo la primera deposita el resultado del tamaño apropiado para evitar desbordamientos. El formato con dos y tres operandos realiza la multiplicación, obtiene todos los bits del resultado y posteriormente
los trunca para almacenar en destino. Los únicos flags que se modifican con esta instrucción son CF y OF. Ambos bits se ponen
a 1 cuando la mitad más significativa del resultado tiene alguno de sus bits a 1. Nótese que estos dos flags son los que indican, en
el caso de la instrucción con dos y tres operandos, si el resultado obtenido ha sido truncado para almacenarse en destino. El resto
de flags tienen valores no definidos tras ejecutar esta instrucción.
A.3.9.
IDIV: Instrucción de división con signo
El comportamiento de esta instrucción es idéntico al de la instrucción DIV (ver sección A.3.7) con la diferencia de que los
operandos son números enteros.
Programación en ensamblador de la
arquitectura IA-32
172 / 198
Instrucción
Descripción
Esta instrucción tiene tres versiones posibles. Divide el
registro %ax, los 32 bits obtenidos al concatenar los
registros %dx: %ax o los 64 bits obtenidos al concatenar
los registros %edx: %eax entre %reg. Como resultado se
deposita el cociente en %al, %ax o %eax y el resto
en %ah, %dx o %edx respectivamente. Los operandos se
tratan como números enteros.
Esta instrucción tiene tres versiones posibles. Divide el
registro %ax, los 32 bits obtenidos al concatenar los
registros %dx: %ax o los 64 bits obtenidos al concatenar
los registros %edx: %eax entre el dato codificado según el
sufijo s y almacenado a partir de la posición de memoria
mem. Como resultado se deposita el cociente en %al, %ax
o %eax y el resto en %ah, %dx o %edx respectivamente.
Los operandos se tratan como números enteros.
IDIV %reg
IDIVs mem
Al igual que la instrucción DIV, esta instrucción utiliza dos operandos: dividendo y divisor. Tan sólo se especifica en la instrucción
el divisor. El dividendo es implícito y tiene tamaño doble al del divisor y se obtiene de %ax (16 bits), la concatenación de los
registros %dx: %ax (32 bits) o la concatenación de los registros %edx: %eax (64 bits) dependiendo de si el divisor es de 8, 16
o 32 bits respectivamente.
La instrucción devuelve dos resultados: cociente y resto. El cociente se devuelve en %al, %ax o %eax y el resto en %ah, %dx
o %edx. La Tabla A.2 muestra la relación entre el tamaño de los operandos y el lugar en el que se almacena el resultado.
Tamaño de
Operandos
Dividendo
Word/Byte
%ax
Doubleword/Word
%dx: %ax
Divisor
1 byte %reg ó
mem
2 bytes %reg ó
mem
4 bytes %reg ó
mem
Quadword/Doubleword%edx: %eax
Cociente
Resto
Rango del
Cociente
%al
%ah
-128 a 127
%ax
%dx
-32.768 a 32.767
%eax
%edx
-231 - 1 a 231
Tabla A.3: Opciones de la división con signo
Ninguno de los flags en la palabra de estado tiene valores definidos tras ejecutar esta instrucción.
A.4.
Instrucciones lógicas
A.4.1.
AND: Instrucción de conjunción
Instrucción
Descripción
AND %regA, %regB
Realiza la conjunción bit a bit del contenido de %regA con
el contenido de %regB. Deposita el resultado en %regB.
AND $inm, %reg
Realiza la conjunción bit a bit entre inm y el contenido
de %reg. Deposita el resultado en %reg.
AND mem, %reg
Realiza la conjunción bit a bit entre el contenido
almacenado en la posición mem y el contenido de %reg.
Deposita el resultado en %reg.
Programación en ensamblador de la
arquitectura IA-32
Instrucción
AND %reg, mem
ANDs $inm, mem
173 / 198
Descripción
Realiza la conjunción bit a bit entre el contenido de %reg y
el contenido almacenado a partir de la posición mem. El
resultado se almacena en memoria a partir de la posición
mem.
Realiza la conjunción bit a bit entre inm, codificado con
los bits especificados por el sufijo s con el dato cuyo
tamaño está especificado por el sufijo s y que está
almacenado a partir de la posición mem. El resultado se
almacena en memoria a partir de la posición mem.
La conjunción bit a bit significa que ambos operandos deben tener el mismo tamaño y que cada bit del resultado se calcula
haciendo la conjunción de los correspondientes bits de ambos operandos. Los flags OF y CF se ponen a cero. Los flags SF, ZF y
PF se modifican de acuerdo con el resultado.
A.4.2.
OR: Instrucción de disyunción
Instrucción
Descripción
OR %regA, %regB
Realiza la disyunción bit a bit del contenido de %regA con
el contenido de %regB. Deposita el resultado en %regB.
OR $inm, %reg
Realiza la disyunción bit a bit entre inm y el contenido
de %reg. Deposita el resultado en %reg.
OR mem, %reg
Realiza la disyunción bit a bit entre el contenido
almacenado en la posición mem y el contenido de %reg.
Deposita el resultado en %reg.
Realiza la disyunción bit a bit entre el contenido de %reg y
el contenido almacenado a partir de la posición mem. El
resultado se almacena en memoria a partir de la posición
mem.
Realiza la disyunción bit a bit entre inm, codificado con
los bits especificados por el sufijo s con el dato cuyo
tamaño está especificado por el sufijo s y que está
almacenado a partir de la posición mem. El resultado se
almacena en memoria a partir de la posición mem.
OR %reg, mem
ORs $inm, mem
La disyunción bit a bit significa que ambos operandos deben tener el mismo tamaño y que cada bit del resultado se calcula
haciendo la disyunción de los correspondientes bits de ambos operandos. Los flags OF y CF se ponen a cero. Los flags SF, ZF y
PF se modifican de acuerdo con el resultado.
A.4.3.
XOR: Instrucción de disyunción exclusiva
Instrucción
Descripción
XOR %regA, %regB
Realiza la disyunción exclusiva bit a bit del contenido
de %regA con el contenido de %regB. Deposita el
resultado en %regB.
XOR $inm, %reg
Realiza la disyunción exclusiva bit a bit entre inm y el
contenido de %reg. Deposita el resultado en %reg.
Programación en ensamblador de la
arquitectura IA-32
174 / 198
Instrucción
Descripción
Realiza la disyunción exclusiva bit a bit entre el contenido
almacenado en la posición mem y el contenido de %reg.
Deposita el resultado en %reg.
XOR mem, %reg
Realiza la disyunción exclusiva bit a bit entre el contenido
de %reg y el contenido almacenado a partir de la posición
mem. El resultado se almacena en memoria a partir de la
posición mem.
Realiza la disyunción exclusiva bit a bit entre inm,
codificado con los bits especificados por el sufijo s con el
dato cuyo tamaño está especificado por el sufijo s y que
está almacenado a partir de la posición mem. El resultado
se almacena en memoria a partir de la posición mem.
XOR %reg, mem
XORs $inm, mem
La disyunción exclusiva bit a bit significa que ambos operandos deben tener el mismo tamaño y que cada bit del resultado se
calcula haciendo la disyunción de los correspondientes bits de ambos operandos. Los flags OF y CF se ponen a cero. Los flags
SF, ZF y PF se modifican de acuerdo con el resultado.
A.4.4.
NOT: Instrucción de negación
Instrucción
Descripción
NOT %reg
Niega bit a bit el contenido de %reg.
NOTs mem
Niega bit a bit al dato cuyo tamaño está especificado por el
sufijo s y que está almacenado a partir de la posición mem.
Esta instrucción no modifica ninguno de los flags de la palabra de estado.
A.5.
Instrucciones de desplazamiento
A.5.1.
SAL/SAR: Desplazamiento aritmético
Instrucción
SAL $inm, %reg
SAR $inm, %reg
SAL %cl, %reg
Descripción
Desplaza el contenido de %reg a la izquierda tantas
posiciones como indica inm. Para cada desplazamiento, el
bit más significativo se carga en el flag CF y el menos
significativo se pone a cero.
Desplaza el contenido de %reg a la derecha tantas
posiciones como indica inm. Para cada desplazamiento el
bit menos significativo se carga en el flag CF y el nuevo bit
más significativo se pone al mismo valor del anterior
(extensión de signo).
Desplaza el contenido de %reg a la izquierda tantas
posiciones como indica el registro %cl. Para cada
desplazamiento, el bit más significativo se carga en el flag
CF y el menos significativo se pone a cero.
Programación en ensamblador de la
arquitectura IA-32
Instrucción
SAR %cl, %reg
SALs $inm, mem
SARs $inm, mem
SALs %cl, mem
SARs %cl, mem
175 / 198
Descripción
Desplaza el contenido de %reg a la derecha tantas
posiciones como indica %cl. Para cada desplazamiento el
bit menos significativo se carga en el flag CF y el nuevo bit
más significativo se pone al mismo valor del anterior
(extensión de signo).
Desplaza el contenido del dato cuyo tamaño lo especifica el
sufijo s y que está almacenado a partir de la posición de
memoria mem a la izquierda tantas posiciones como indica
inm. Para cada desplazamiento, el bit más significativo del
segundo operando se carga en el flag CF y el menos
significativo del resultado se pone a cero.
Desplaza el contenido del dato cuyo tamaño lo especifica el
sufijo s y que está almacenado a partir de la posición de
memoria mem a la derecha tantas posiciones como indica
inm. Para cada desplazamiento el bit menos significativo
del segundo operando se carga en el flag CF y el nuevo bit
más significativo del resultado se pone al mismo valor del
anterior (extensión de signo).
Desplaza el contenido del dato cuyo tamaño lo especifica el
sufijo s y que está almacenado a partir de la posición de
memoria mem a la izquierda tantas posiciones como indica
el registro %cl. Para cada desplazamiento, el bit más
significativo del segundo operando se carga en el flag CF y
el menos significativo del resultado se pone a cero.
Desplaza el contenido del dato cuyo tamaño lo especifica el
sufijo s y que está almacenado a partir de la posición de
memoria mem a la derecha tantas posiciones como indica el
registro %cl. Para cada desplazamiento el bit menos
significativo del segundo operando se carga en el flag CF y
el nuevo bit más significativo del resultado se pone al
mismo valor del anterior (extensión de signo).
El flag CF contiene el valor del último bit que ha sido desplazado. El flag OF se modifica sólo en el caso de desplazamientos de 1
bit. Para desplazamientos a izquierda, este flag se pone a cero si los dos bits más significativos del operando antes de desplazarse
son ambos cero. En caso contrario se pone a 1. Si el desplazamiento es a la derecha, el valor es siempre cero. Los flags SF, ZF y
PF se modifican de acuerdo con el resultado obtenido.
A.5.2.
SHL/SHR: Desplazamiento lógico
Instrucción
SHL $inm, %reg
SHR $inm, %reg
SHL %cl, %reg
Descripción
Desplaza el contenido de %reg a la izquierda tantas
posiciones como indica inm. Para cada desplazamiento, el
bit más significativo se carga en el flag CF y el menos
significativo se pone a cero.
Desplaza el contenido de %reg a la derecha tantas
posiciones como indica inm. Para cada desplazamiento el
bit menos significativo se carga en el flag CF y el nuevo bit
más significativo se pone a cero.
Desplaza el contenido de %reg a la izquierda tantas
posiciones como indica el registro %cl. Para cada
desplazamiento, el bit más significativo se carga en el flag
CF y el menos significativo se pone a cero.
Programación en ensamblador de la
arquitectura IA-32
Instrucción
SHR %cl, %reg
SHLs $inm, mem
SHRs $inm, mem
SHLs %cl, mem
SHRs %cl, mem
176 / 198
Descripción
Desplaza el contenido de %reg a la derecha tantas
posiciones como indica %cl. Para cada desplazamiento el
bit menos significativo se carga en el flag CF y el nuevo bit
más significativo se pone a cero.
Desplaza el contenido del dato cuyo tamaño lo especifica el
sufijo s y que está almacenado a partir de la posición de
memoria mem a la izquierda tantas posiciones como indica
inm. Para cada desplazamiento, el bit más significativo del
segundo operando se carga en el flag CF y el menos
significativo del resultado se pone a cero.
Desplaza el contenido del dato cuyo tamaño lo especifica el
sufijo s y que está almacenado a partir de la posición de
memoria mem a la derecha tantas posiciones como indica
inm. Para cada desplazamiento el bit menos significativo
del segundo operando se carga en el flag CF y el nuevo bit
más significativo del resultado se pone a cero.
Desplaza el contenido del dato cuyo tamaño lo especifica el
sufijo s y que está almacenado a partir de la posición de
memoria mem a la izquierda tantas posiciones como indica
el registro %cl. Para cada desplazamiento, el bit más
significativo del segundo operando se carga en el flag CF y
el menos significativo del resultado se pone a cero.
Desplaza el contenido del dato cuyo tamaño lo especifica el
sufijo s y que está almacenado a partir de la posición de
memoria mem a la derecha tantas posiciones como indica el
registro %cl. Para cada desplazamiento el bit menos
significativo del segundo operando se carga en el flag CF y
el nuevo bit más significativo del resultado se pone a cero.
El flag CF contiene el valor del último bit que ha sido desplazado. El flag OF se modifica sólo en el caso de desplazamientos de 1
bit. Para desplazamientos a izquierda, este flag se pone a cero si los dos bits más significativos del operando antes de desplazarse
son ambos cero. En caso contrario se pone a 1. Si el desplazamiento es a la derecha, el valor es el bit más significativo del segundo
operando. Los flags SF, ZF y PF se modifican de acuerdo con el resultado obtenido.
A.5.3.
RCL/RCR: Instrucción de rotación con acarreo
Instrucción
RCL $inm, %reg
RCR $inm, %reg
RCL %cl, %reg
RCR %cl, %reg
Descripción
Rota el contenido de %reg concatenado con el flag CF a la
izquierda (RCL) o derecha (RCR) tantas posiciones como
indica inm. Si el desplazamiento es a la izquierda, para
cada desplazamiento el bit más significativo se carga en el
flag CF y éste pasa a ser el bit menos significativo. Si el
desplazamiento es a la derecha, para cada desplazamiento
el bit menos significativo se carga en el flag CF y éste pasa
a ser el bit más significativo.
Rota el contenido de %reg concatenado con el flag CF a la
izquierda (RCL) o derecha (RCR) tantas posiciones como
indica el registro %cl. Si el desplazamiento es a la
izquierda, para cada desplazamiento el bit más significativo
se carga en el flag CF y éste pasa a ser el bit menos
significativo. Si el desplazamiento es a la derecha, para
cada desplazamiento el bit menos significativo se carga en
el flag CF y éste pasa a ser el bit más significativo.
Programación en ensamblador de la
arquitectura IA-32
Instrucción
RCLs $inm, mem
RCRs $inm, mem
RCLs %cl, mem
RCRs %cl, mem
177 / 198
Descripción
Rota el dato cuyo tamaño viene especificado por el sufijo s
y que está almacenado en memoria a partir de la posición
mem concatenado con el flag CF a la izquierda (RCL) o
derecha (RCR) tantas posiciones como indica inm. Si el
desplazamiento es a la izquierda, para cada desplazamiento
el bit más significativo se carga en el flag CF y éste pasa a
ser el bit menos significativo. Si el desplazamiento es a la
derecha, para cada desplazamiento el bit menos
significativo se carga en el flag CF y éste pasa a ser el bit
más significativo.
Rota el dato cuyo tamaño viene especificado por el sufijo s
y que está almacenado en memoria a partir de la posición
mem concatenado con el flag CF a la izquierda (RCL) o
derecha (RCR) tantas posiciones como indica el
registro %cl. Si el desplazamiento es a la izquierda, para
cada desplazamiento el bit más significativo se carga en el
flag CF y éste pasa a ser el bit menos significativo. Si el
desplazamiento es a la derecha, para cada desplazamiento
el bit menos significativo se carga en el flag CF y éste pasa
a ser el bit más significativo.
Estas instrucciones hacen que el operando destino, concatenado con el flag de acarreo CF se comporte igual que si tuviese una
estructura circular. Es decir, el bit más significativo pasa al accarreo y éste al menos significativo si se desplaza a izquierda, y al
revés si se desplaza a derecha.
Esta instrucción se utiliza para posicionar un determinado bit de un dato en el bit de acarreo y así poder consultar su valor.
Mediante la operación inversa, permite dejar tanto el operando como el flag con su valor anterior.
Aparte del flag CF que almacena uno de los bits del operando, el otro flag que se modifica es OF pero sólo en los casos en los
que el desplazamiento es de un bit. Para desplazamientos a izquierda OF contiene el resultado de la disyunción exclusiva entre
CF tras el desplazamiento y el bit más significativo del resultado. Para desplazamientos a la derecha, OF es igual a la disyunción
exclusiva de los dos bits más significativos del resultado.
El resto de flags SF, ZF y PF no se modifican.
A.5.4.
ROR/ROL: Instrucción de rotación sin acarreo
Instrucción
ROL $inm, %reg
ROR $inm, %reg
ROL %cl, %reg
ROR %cl, %reg
Descripción
Rota el contenido de %reg a la izquierda (ROL) o derecha
(ROR) tantas posiciones como indica inm. Si el
desplazamiento es a la izquierda, para cada desplazamiento
el bit más significativo pasa a ser el menos significativo. Si
el desplazamiento es a la derecha, para cada
desplazamiento el bit menos significativo pasa a ser el más
significativo.
Rota el contenido de %reg a la izquierda (ROL) o derecha
(ROR) tantas posiciones como indica el registro %cl. Si el
desplazamiento es a la izquierda, para cada desplazamiento
el bit más significativo pasa a ser el bit menos significativo.
Si el desplazamiento es a la derecha, para cada
desplazamiento el bit menos significativo pasa a ser el bit
más significativo.
Programación en ensamblador de la
arquitectura IA-32
Instrucción
ROLs $inm, mem
RORs $inm, mem
ROLs %cl, mem
RORs %cl, mem
178 / 198
Descripción
Rota el dato cuyo tamaño viene especificado por el sufijo s
y que está almacenado en memoria a partir de la posición
mem a la izquierda (ROL) o derecha (ROR) tantas
posiciones como indica inm. Si el desplazamiento es a la
izquierda, para cada desplazamiento el bit más significativo
pasa a ser el bit menos significativo. Si el desplazamiento
es a la derecha, para cada desplazamiento el bit menos
significativo pasa a ser el bit más significativo.
Rota el dato cuyo tamaño viene especificado por el sufijo s
y que está almacenado en memoria a partir de la posición
mem a la izquierda (ROL) o derecha (ROR) tantas
posiciones como indica el registro %cl. Si el
desplazamiento es a la izquierda, para cada desplazamiento
el bit más significativo pasa a ser el bit menos significativo.
Si el desplazamiento es a la derecha, para cada
desplazamiento el bit menos significativo pasa a ser el bit
más significativo.
Estas instrucciones hacen que el operando destino se comporte igual que si tuviese una estructura circular. Es decir, el bit más
significativo pasa a ser el bit de menos peso si se desplaza a izquierda, y al revés si se desplaza a derecha.
El flag CF almacena el bit más significativo si se desplaza a izquierda, y el menos significativo si se desplaza a derecha. El flag OF
se modifica sólo en los casos en los que el desplazamiento es de un bit. Para desplazamientos a izquierda OF contiene el resultado
de la disyunción exclusiva entre CF tras el desplazamiento y el bit más significativo del resultado. Para desplazamientos a la
derecha, OF es igual a la disyunción exclusiva de los dos bits más significativos del resultado.
El resto de flags SF, ZF y PF no se modifican.
A.6.
Instrucciones de salto
A.6.1.
JMP: Instrucción de salto incondicional
Instrucción
Descripción
JMP mem
Pasa a ejecutar a continuación la instrucción almacenada a
partir de la posición de memoria mem.
JMP * %reg
Pasa a ejecutar a continuación la instrucción almacenada a
partir de la posición de memoria en %reg
Esta instrucción simplemente cambia la secuencia de ejecución del procesador que ejecuta la instrucción indicada en la posición
de memoria dada como operando. El uso más común es con una etiqueta como operando, que es donde pasa a ejecutar el
procesador. Si el segundo operando es un registro con el prefijo * el valor del registro se carga en el contador de programa y se
ejecuta la instrucción en la posición de memoria con ese valor.
A.6.2.
Jcc: Instrucciones de salto condicional
Programación en ensamblador de la
arquitectura IA-32
Instrucción
JA mem
Condición
CF = 0 y ZF = 0
JNBE mem
JAE mem
CF = 0
JNB mem
JE mem
ZF = 1
179 / 198
Descripción
Salto si mayor,
salto si no menor
o igual (sin
signo)
Salto si mayor o
igual, salto si no
menor (sin signo)
Salto si igual,
salto si cero.
JZ mem
JG mem
ZF = 0 y SF = OF
SF = OF
JNL mem
Salto si mayor, si
no menor o igual
(con signo)
Salto si mayor o
igual, si no
menor (con
signo)
JC mem
CF = 1
Salto si acarreo
es uno
JCXZ mem
%cx = 0
Salto si
registro %cx es
cero.
JO mem
OF = 1
Salto si el bit de
desbordamiento
es uno.
PF = 0
Salto si paridad
impar, si no
paridad.
JPO mem
JNP mem
JS mem
A.6.3.
Condición
Descripción
CF = 1 ó ZF = 1
Salto si menor o
igual, salto si no
mayor (sin signo)
CF = 1
Salto si menor,
salto si no mayor
o igual (sin
signo)
ZF = 0
Salto si diferente,
salto si no cero.
ZF = 1 ó SF !=
OF
Salto si menor o
igual, si no
mayor (con
signo)
SF != OF
Salto si menor, si
no mayor o igual
(con signo)
JNC mem
CF = 0
Salto si acarreo
es cero
JECXZ mem
%ecx = 0
Salto si
registro %ecx es
cero.
JNO mem
OF = 0
Salto si el bit de
desbordamiento
es cero.
PF = 1
Salto si paridad
par, si paridad.
SF = 0
Salto si negativo.
JBE mem
JNA mem
JB mem
JNAE mem
JNE mem
JNZ mem
JNLE mem
JGE mem
Instrucción
SF = 1
Salto si positivo.
JLE mem
JNG mem
JL mem
JNGE mem
JPE mem
JP mem
JNS mem
CALL: Instrucción de llamada a subrutina
Instrucción
CALL mem
Descripción
Invoca la subrutina cuya primera instrucción está en la
posición de memoria mem. Se salva el contador de
programa en la cima de la pila.
Programación en ensamblador de la
arquitectura IA-32
180 / 198
Instrucción
Descripción
Invoca la subrutina cuya primera instrucción está en la
posición de memoria contenida en el registro %reg.
CALLs * %reg
El efecto relevante de esta instrucción es que deposita en la cima de la pila la dirección de retorno, o lo que es lo mismo, la
dirección de la instrucción que sigue a esta en el flujo de ejecución. Esta instrucción no modifica los flags de la palabra de estado.
A.6.4.
RET: Instrucción de retorno de subrutina
Instrucción
Descripción
Retorna la ejecución a la instrucción cuya dirección está
almacenada en la cima de la pila. Esta dirección se saca de
la pila.
RET
El aspecto relevante de esta instrucción es que supone que la dirección a la que debe retornar está almacenada en la cima de la
pila. Esta instrucción no modifica los flags de la palabra de estado.
A.7.
Instrucciones de comparación y comprobación
A.7.1.
CMP: Instrucción de comparación
Instrucción
Descripción
CMP %regA, %regB
Realiza la operación %regB - %regA y modifica los flags
con el resultado que no se almacena en lugar alguno.
CMP $inm, %reg
Realiza la operación %reg - inm y modifica los flags con
el restulado que no se almacena en lugar alguno.
CMP mem, %reg
Realiza la operación mem - %reg y modifica los flags con
el restulado que no se almacena en lugar alguno.
CMP %reg, mem
Realiza la operación mem - %reg y modifica los flags con
el resultado que no se almacena en lugar alguno.
CMPs $inm, mem
Resta el dato almacenado a partir de la posición de
memoria mem y cuyo tamaño está especificado por el sufijo
s del valor inm codificado con tantos bits como indica el
sufijo s. Se modifican los flags con el resultado de esta
resta que no se almacena en lugar alguno.
El efecto de esta instrucción se refleja únicamente en los flags de la palabra de estado. Estos flags se pueden utilizar para, por
ejemplo, cambiar el flujo de ejecución mediante una instrucción de salto condicional.
A.7.2.
TEST: Instrucción de comprobación
Programación en ensamblador de la
arquitectura IA-32
Instrucción
181 / 198
Descripción
TEST %regA, %regB
Realiza la conjunción bit a bit entre %regB y %regA y
modifica los flags con el resultado que no se almacena en
lugar alguno.
TEST $inm, %reg
Realiza la conjunción bit a bit entre %reg e inm y
modifica los flags con el restulado que no se almacena en
lugar alguno.
TEST mem, %reg
Realiza la conjunción bit a bit entre mem y %reg y
modifica los flags con el restulado que no se almacena en
lugar alguno.
TEST %reg, mem
Realiza la conjunción bit a bit entre mem y %reg y
modifica los flags con el resultado que no se almacena en
lugar alguno.
TESTs $inm, mem
Realiza la conjunción bit a bit entre el dato almacenado a
partir de la posición de memoria mem y cuyo tamaño está
especificado por el sufijo s y el valor inm codificado con
tantos bits como indica el sufijo s. Se modifican los flags
con el resultado de esta resta que no se almacena en lugar
alguno.
El efecto de esta instrucción se refleja únicamente en los flags de la palabra de estado. Los flags OF y CF se ponen a cero. Los
flags SF, ZF y PF se modifican de acuerdo con el resultado.
Programación en ensamblador de la
arquitectura IA-32
182 / 198
Apéndice B
El depurador
Uno de los principales problemas al escribir programas son los errores de ejecución. Compilar un programa no es garantía
suficiente de que funciona de la manera prevista. Es más, el ciclo de desarrollo de un programa está ocupado, en su mayoría
por las tareas de diagnosticar y corregir los errores de ejecución. A los errores de ejecución en programas en inglés se les suele
denominar bugs (bichos).
El origen de la utilización del término bug para describir los errores en un program es un poco confuso, pero hay una referencia
documentada a la que se le suele atribuir este mérito.
La invención del término se atribuye generalmente a la ingeniera Grace Hopper que en 1946 estaba en el laboratorio de computación de la universidad de Harvard trabajando en los ordenadores con nombre Mark II y Mark III. Los operadores descubrieron
que la causa de un error detectado en el Mark II era una polilla que se había quedado atrapada entre los contactos de un relé (por
aquel entonces el elemento básido de un ordenador) que a su vez era parte de la lógica interna del ordenador. Estos operadores
estaban familiarizados con el término bug e incluso pegaron el insecto en su libro de notas con la anotación ‘First actual case of
bug being found’ (primer caso en el que realmente se encuentra un bug) tal y como ilustra la figura B.1.
Programación en ensamblador de la
arquitectura IA-32
183 / 198
Figura B.1: Primer caso en el que realmente se encuentra un bug (Fuente: U.S. Naval Historical Center Photograph)
Hoy en día, los métodos que se utilizan para depurar los errores de un programa son múltiples y con diferentes niveles de eficacia.
El método consistente en insertar líneas de código que escriben en pantalla mensajes es quizás el más ineficiente de todos ellos.
En realidad lo que se precisa es una herramienta que permita ejecutar de forma controlada un programa, que permita suspender
la ejecución en cualquier punto para poder realizar comprobaciones, ver el contenido de las variables, etc.
Esta herramienta se conoce con el nombre de depurador o, su término inglés, debugger. El depurador es un ejecutable cuya misión
es permitir la ejecución controlada de un segundo ejecutable. Se comporta como un envoltorio dentro del cual se desarrolla una
ejecución normal de un programa, pero a la vez permite realizar una serie de operaciones específicas para visualizar el entorno
de ejecución en cualquier instante.
Más concretamente, el depurador permite:
ejecutar un programa línea a línea
detener la ejecución temporalmente en una línea de código concreta
detener temporalmente la ejecución bajo determinadas condiciones
Programación en ensamblador de la
arquitectura IA-32
184 / 198
visualizar el contenido de los datos en un determinado momento de la ejecución
cambiar el valor del entorno de ejecución para poder ver su efecto de una corrección en el programa
Uno de los depuradores más utilizados en entornos Linux es gdb (Debugger de GNU). En este documento se describen los
comandos más relevantes de este depurador para ser utilizados con un programa escrito en C. Todos los ejemplos utilizados en
el resto de esta sección se basan en el programa cuyo código fuente se muestra en la Tabla B.1 y que se incluye en el fichero
gdbuse.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.data
# Comienza ←sección de datos
nums:
.int 2, 3, 2, 7, 5, 4, 9
# Secuencia ←de números a imprimir
tamano: .int 7
# Tamaño de ←la secuencia
formato:.string " %d\n"
# String ←para imprimir un número
.text
# Comienza ←la sección de código
.globl main
# main es un ←símbolo global
main:
push %ebp
# Bloque de ←activación
mov %esp, %ebp
push %eax
# Guardar ←copia de los registros en la pila
push %ebx
push %ecx
push %edx
mov $0, %ebx
bucle: cmp %ebx, tamano
je termina
push nums(, %ebx,4)
# pone el ←número en la pila
push $formato
# pone el ←formato en la pila
call printf
# imprime ←los datos que recibe
add $8, %esp
# borra los ←datos de la cima de la pila
inc %ebx
jmp bucle
termina:pop %edx
# restaurar ←el valor de los registros
pop %ecx
pop %ebx
pop %eax
mov %ebp, %esp
# Deshacer ←bloque de activación
pop %ebp
ret
# termina el ←programa
Tabla B.1: Programa en ensamblador utilizado como ejemplo
Programación en ensamblador de la
arquitectura IA-32
B.1.
185 / 198
Arranque y parada del depurador
Para que un programa escrito en ensamblador pueda ser manipulado por gdb es preciso realizar una compilación que incluya
como parte del ejecutable, un conjunto de datos adicionales. Esto se consigue incluyendo la opción -gstabs+ al invocar el
compilador:
shell$ gcc -gstabs+ -o gdbuse gdbuse.s
Si el programa se ha escrito correctamente este comando ha generado el fichero ejecutable con nombre gdbuse. Una vez este
fichero se invoca el depurador con el comando:
shell$ gdb gdbuse
Tras arrancar el depurador se muestra por pantalla un mensaje seguido del prompt (gdb):
shell$ gdb gdbuse
GNU gdb Red Hat Linux (6.0post-0.20040223.19rh)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db library "/ ←lib/tls/libthread_db.so.1".
(gdb)
En este instante, el programa depurador ha arrancado, pero la ejecución del programa gdbuse (que se ha pasado como primer
argumento) todavía no. La interacción con el depurador se realiza a través de comandos introducidos a continuación del prompt,
de forma similar a como se proporcionan comandos a un shell o intérprete de comandos en Linux.
Para arrancar la ejecución del programa se utiliza el comando run (o su abreviatura r). Tras introducir este comando, el programa
se ejecuta de forma normal y se muestra por pantalla de nuevo el prompt (gdb). Por ejemplo:
(gdb) r
Starting program: /home/test/gdbuse
2
3
2
7
5
4
9
Program exited normally.
(gdb)
En el ejemplo, se puede comprobar como el programa termina correctamenta (tal y como denota el mensaje que aparece por
pantalla). Cuando se produce un error en la ejecución, el depurador se detiene y muestra de nuevo el prompt.
Si se desea detener un programa mientras se está ejecutando se debe pulsar Crtl-C (la tecla control, y mientras se mantiene
pulsada, se pulsa C). La interrupción del programa es capturada por el depurador, y el control lo retoma el intérprete de comandos
de gdb. En este instante, la ejecución del programa ha sido detenida pero no terminada. Prueba de ello, es que la ejecución puede
continuarse mediante el comando continue (que se puede abreviar simplemente con la letra c).
Programación en ensamblador de la
arquitectura IA-32
186 / 198
Para salir del depurador se utiliza el comando quit (abreviado por la letra q). Si se pretende terminar la sesión del depurador
mientras el programa está en ejecución se pide confirmación para terminar dicha ejecución.
(gdb) q
The program is running.
shell$
Exit anyway? (y or n) y
El comando help muestra la información referente a todos los comandos y sus opciones. Si se invoca sin parámetros, se muestran las categorías en las que se clasifican los comandos. El comando help seguido del nombre de una categoría, proporciona
información detallada sobre sus comandos. Si se invoca seguido de un comando, describe su utilización.
B.2.
Visualización de código
El código fuente del programa en ejecución se puede mostrar por pantalla mediante el comando list (abreviado l). Sin opciones,
este comando muestra la porción de código alrededor de la línea que está siendo ejecutada en el instante en el que está detenido el
programa. Si el programa no está en ejecución, se muestra el código a partir de la etiqueta main. El comando list acepta opciones
para mostrar una línea en concreto, una línea en un fichero, una etiqueta en un fichero, e incluso el código almacenado en una
dirección de memoria completa. El comando help list muestra todas las opciones posibles.
(gdb) l main
3
4
tamano: .int 7
5
formato:.string " %d\n"
6
.text
7
.globl main
8
main:
push %ebp
9
mov %esp, %ebp
10
push %eax
11
push %ebx
12
push %ecx
(gdb)
B.3.
#
#
#
#
#
#
Secuencia de números a imprimir
Tamaño de la secuencia
String para imprimir un número
Comienza la sección de código
main es un símbolo global
Bloque de activación
# Guardar copia de los registros en la pila
Ejecución controlada de un programa
Aparte de detener la ejecución de un programa con Crtl-C, lo más útil es detener la ejecución en una línea concreta del código.
Para ello es preciso insertar un punto de parada (en inglés breakpoint). Dicho punto es una marca que almacena el depurador, y
cada vez que la ejecución del programa pasa por dicho punto, suspende la ejecución y devuelve el control al usuario. Para insertar
un punto de parada se utiliza el comando break (abreviado b) seguido de la línea en la que se desea introducir.
Programación en ensamblador de la
arquitectura IA-32
(gdb) l 14
9
10
11
12
13
14
15
bucle:
16
17
18
(gdb) b 14
Breakpoint 1 at
(gdb)
mov %esp, %ebp
push %eax
push %ebx
push %ecx
push %edx
mov $0, %ebx
cmp %ebx, tamano
je termina
push nums(, %ebx,4)
push $formato
187 / 198
# Guardar copia de los registros en la pila
# pone el número en la pila
# pone el formato en la pila
0x8048377: file gdbuse.s, line 14.
Se pueden introducir tantos puntos de parada como sean necesarios en diferentes lugares del código. El depurador asigna un
número a cada uno de ellos comenzando por el 1. En la última línea del mensaje anterior se puede ver como al punto introducido
en la línea 14 del fichero gdbuse.s se le ha asignado el número 1.
El comando info breakpoints (o su abreviatura info b) muestra por pantalla la lista de puntos de parada que contiene el depurador.
(gdb) l 21
16
je termina
17
push nums(, %ebx,4)
# pone el número en la pila
18
push $formato
# pone el formato en la pila
19
call printf
# imprime los datos que recibe
20
add $8, %esp
# borra los datos de la cima de la pila
21
inc %ebx
22
jmp bucle
23
termina:pop %edx
# restaurar el valor de los registros
24
pop %ecx
25
pop %ebx
(gdb) b 21
Breakpoint 2 at 0x8048398: file gdbuse.s, line 21.
(gdb) info breakpoints
Num Type
Disp Enb Address
What
1
breakpoint
keep y
0x08048377 gdbuse.s:14
2
breakpoint
keep y
0x08048398 gdbuse.s:21
(gdb)
Los puntos de parada se pueden introducir en cualquier momento de la ejecución de un proceso. Una vez introducidos, si se
comienza la ejecución del programa mediante el comando run (o su abreviatura r), ésta se detiene en cuanto se ejecuta una línea
con un punto de parada.
Programación en ensamblador de la
arquitectura IA-32
188 / 198
(gdb) r
Starting program: /home/test/gdbuse
Breakpoint 1, main () at gdbuse.s:14
14
mov $0, %ebx
(gdb) c
Continuing.
2
Breakpoint 2, bucle () at gdbuse.s:21
21
inc %ebx
(gdb)
Nótese que el depurador primero se ha detenido en el punto de parada 1, tras introducir el comando continue se ha detenido en
el punto de parada 2.
Cada punto de parada puede ser temporalmente desactivado/activado de manera independiente. Los comandos enable y disable
seguido de un número de punto de parada activan y desactivan respectivamente dichos puntos.
Para reanudar la ejecución del programa previamente suspendida hay tres comandos posibles. El primero que ya se ha visto es
continue (o c). Este comando continua la ejecución del programa y no se detendrá hasta que se encuentre otro punto de parada,
se termine la ejecución, o se produzca un error. El segundo comando para continuar la ejecución es stepi (o su abreviatura si).
Este comando ejecuta únicamente la instrucción en la que está detenido el programa y vuelve de nuevo a suspender la ejecución.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/test/gdbuse
Breakpoint 1, main () at gdbuse.s:14
14
mov $0, %ebx
(gdb) si
bucle () at gdbuse.s:15
15
bucle: cmp %ebx, tamano
(gdb) si
16
je termina
(gdb)
Con la utilización de este comando se puede conseguir ejecutar un programa ensamblador instrucción a instrucción de forma
que se pueda ver qué está sucediendo en los registros del procesador y en los datos en memoria. Mediante la combinación del
mecanismo de puntos de parada y el comando stepi se puede ejecutar un programa hasta un cierto punto, y a partir de él ir
instrucción a instrucción. Este proceso es fundamental para detectar los errores en los programas.
El comando stepi tiene un inconveniente. Cuando la instrucción a ejecutar es una llamada a subrutina (por ejemplo la instrucción
call printf), el depurador ejecuta la instrucción call y se detiene en la primera instrucción de la subrutina. Este comportamiento es deseable siempre y cuando se quiera ver el código de la subrutina, pero si dicho código pertenece a una librería del
sistema, lo que se necesita es un comando que permita ejecutar la llamada a la subrutina entera y detenerse en la instrucción que
le sigue. Esto se puede conseguir si, al estar a punto de ejecutar una instrucción call se utiliza el comando nexti en lugar de
stepi.
Programación en ensamblador de la
arquitectura IA-32
189 / 198
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/bin/gdbuse
Breakpoint 1, main () at gdbuse.s:14
14
mov $0, %ebx
(gdb) si
bucle () at gdbuse.s:15
15
bucle: cmp %ebx, tamano
(gdb) si
16
je termina
(gdb) si
17
push nums(, %ebx,4)
(gdb) si
bucle () at gdbuse.s:18
18
push $formato
(gdb) si
bucle () at gdbuse.s:19
19
call printf
(gdb) ni
2
20
add $8, %esp
(gdb)
# pone el número en la pila
# pone el formato en la pila
# imprime los datos que recibe
# borra los datos de la cima de la pila
En general, cuando se produce un error en un programa ensamblador, mediante la utilización de los puntos de parada se permite
llegar al programa al lugar aproximado del código en el que se supone que está el error, y luego mediante la utilización de stepi se
ejecuta instrucción a instrucción teniendo cuidado de utilizar nexti cuando se quiera ejecutar una instrucción call que incluya
la llamada entera.
B.4.
Visualización de datos
Los comandos descritos hasta ahora permiten una ejecución controlada de un programa, pero cuando el depurador es realmente
eficiente es cuando hay que localizar un error de ejecución. Generalmente, ese error se manifiesta como una terminación abrupta (por ejemplo segmentation fault). Cuando el programa se ejecuta desde el depurador, esa terminación retorna el control al
depurador con lo que es posible utilizar comandos para inspeccionar el estado en el que ha quedado el programa.
Uno de los comandos más útiles del depurador es print (o su abreviatura p). Como argumento recibe una expresión, y su efecto
es imprimir el valor resultante de evaluar dicha expresión. Este comando puede recibir el nombre de cualquier símbolo que esté
visible en ese instante en la ejecución del programa. El contenido de uno de estos símbolos se muestra por pantalla simplemente
escribiendo el comando print seguido del nombre.
Programación en ensamblador de la
arquitectura IA-32
190 / 198
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/bin/gdbuse
Breakpoint 1, main () at gdbuse.s:14
14
mov $0, %ebx
(gdb) p tamano
$7 = 7
(gdb)
Aparte de nombres de etiquetas que apuntan a datos, print acepta expresiones que se refieren a los registros del procesador:
$eax, $ebx, etc.
Breakpoint 1, main () at gdbuse.s:14
14
mov $0, %ebx
(gdb) p tamano
$7 = 7
(gdb) p $eax
$8 = 0
(gdb) p $ebx
$9 = 9105372
(gdb) p/x $ebx
$10 = 0x8aefdc
(gdb)
Nótese que el último comando print tiene el sufijo /x que hace que el resultado se muestre en hexadecimal, en lugar de decimal.
Si se quiere ver el contenido de todos los registros del procesador se puede utilizar el comando info registers.
(gdb) info registers
eax
0x0
0
ecx
0xfefff5ec
edx
0xfefff5e4
ebx
0x8aefdc 9105372
esp
0xfefff548
ebp
0xfefff558
esi
0x1
1
edi
0x8b10dc 9113820
eip
0x8048377
eflags
0x200246 2097734
cs
0x73
115
ss
0x7b
123
ds
0x7b
123
es
0x7b
123
fs
0x0
0
gs
0x33
51
(gdb)
-16779796
-16779804
0xfefff548
0xfefff558
0x8048377
Nótese que para cada registro se muestra su valor en hexadecimal seguido por su representación en decimal. No todos los registros
Programación en ensamblador de la
arquitectura IA-32
191 / 198
que muestra este comando son manipulables desde un programa ensamblador, tan sólo los ocho primeros.
El comando print permite igualmente visualizar arrays de valores consecutivos en memoria. Para ello es preciso especificar en
el comando el tipo de datos que contiene el array y su longitud. El formato utilizado es incluir entre paréntesis el tipo seguido
por el tamaño entre corchetes. En el programa dado como ejemplo el comando para imprimir los siete números enteros que se
definen en la etiqueta nums es:
(gdb) p/x (int[7])nums
$13 = {0x2, 0x3, 0x2, 0x7, 0x5, 0x4, 0x9}
(gdb)
Si lo que se necesita es visualizar los bytes almacenados en un lugar concreto de memoria, el comando examine (o su abreviatura
‘x’) imprime una determinada porción de memoria por pantalla. La sintaxis de este comando es ‘x/NFU dirección’. Las letras
NFU representan opciones del comando. La N representa un entero que codifica el número de unidades de información en
memoria a mostrar. La F representa el formato en el que se muestran los datos (al igual que el comando print, la ‘x’ quiere decir
hexadecimal). La letra U representa el tamaño de las unidades a mostrar. Sus posibles valores son ‘b’ para bytes, ‘h’ para palabras
de 2 bytes, ‘w’ para palabras de 4 bytes y ‘g’ para palabras de ocho bytes.
La dirección a partir de la cual se muestra el contenido se puede dar como una constante en hexadecimal, o como el nombre de
una etiqueta precedido del carácter ‘&’. Por ejemplo, para mostrar el contenido de las 7 palabras de 4 bytes almacenadas a partir
de la etiqueta nums el comando es:
(gdb) x/7xw &nums
0x804957c >nums<:
0x804958c >nums+16<:
(gdb)
0x00000002
0x00000005
0x00000003
0x00000004
0x00000002
0x00000009
0x00000007
Este comando se puede utilizar para mostrar el contenido de una porción de memoria a la que apunta un determinado registro. Por
ejemplo, para mostrar las cuatro palabras de memoria almacenadas en la cima de la pila se puede utilizar el siguiente comando:
(gdb) x/16xb $esp
0xfefff548:
0xe4
0xfefff550:
0xdc
(gdb)
0xf5
0xef
0xff
0x8a
0xfe
0x00
0xec
0x00
0xf5
0x00
0xff
0x00
0xfe
0x00
Además de visualizar datos en registros o memoria, el depurador permite también manipular estos datos mientras el programa
está detenido. El comando set permite la asignación de un valor numérico tanto a porciones de memoria como a registros. Para
asignar el valor 10 al registro %eax se utiliza el comando:
(gdb) set $eax=10
(gdb) p $eax
$14 = 10
(gdb)
Este comando es útil cuando se detecta un valor erróneo en un registro y se puede corregir para mostrar si el programa puede
continuar normalmente.
Programación en ensamblador de la
arquitectura IA-32
192 / 198
Al igual que se permite modificar datos en registros, también se pueden modificar datos en memoria. Para ello es necesario
especificar el tipo de dato que se está almacenando entre llaves seguido de la dirección de memoria. De esta forma se especifica
dónde almacenar el valor que se proporciona a continuación tras el símbolo de igual. Por ejemplo:
(gdb) set {int}0x83040 = 4
(gdb)
El comando anterior almacena el valor 4 en la posición de memoria cuya dirección es 0x83040 y almacena 4 bytes porque se
refiere a ella como un entero.
B.5.
Ejercicios
Para la realización de los siguientes ejercicios se utiliza el código fuente utilizado como ejemplo, mostrado en la Tabla B.1 y
contenido en el fichero gdbuse.s. Se supone que el programa ha sido compilado, el ejecutable producido y el depurador arrancado.
1. ¿Qué comando hay que utilizar para mostrar por pantalla todos los bytes que codifican el string con nombre formato?
¿Cuál es el valor del último byte?
2. Se sabe que las instrucciones del tipo push :registro: se codifican mediante un único byte y mov :registro:, :registro: mediante dos bytes. Utilizando únicamente el depurador, decir cuál es el código hexadecimal de las
siguientes instrucciones:
push %ebp
push %eax
push %ebx
push %ecx
push %edx
3. Situar un punto de parada en la instrucción call printf. ¿Qué comando es necesario para mostrar por pantalla el valor
que deposita en la cima de la pila la instrucción push $formato?
4. Introducir un punto de parada en la línea 10 del código (en la instrucción push %eax). Mostrar por pantalla mediante el
comando print el valor de los registros %eax, %ebx, %ecx y %edx. Apuntar estos valores.
A continuación introducir un segundo punto de parada en la línea 14 (en la instrucción mov $0, %ebx). Mediante el
comando continue continuar la ejecución hasta ese punto.
¿Qué comando hay que utilizar para mostrar por pantalla el contenido de las cuatro palabras de memoria que se encuentran
en la cima de la pila? Comprobar que estos valores son idénticos a los mostrados en el primer punto de parada.
5. La instrucción inc %ebx aumenta el valor de dicho registro en una unidad. Este registro contiene el índice del siguiente
elemento a imprimir. Poner un punto de parada en la instrucción siguiente a esta y con el programa detenido modificar el
valor de este registro con un número entre cero y seis (ambos inclusive). Explica qué es lo que sucede y por qué.
6. Utilizando la ejecución instrucción a instrucción que permite el depurador, ¿qué instrucción se ejecuta justo antes de la
instrucción pop %edx?
7. La instrucción push nums(, %ebx,4) deposita un cierto valor en la pila. Introducir un punto de parada en la siguiente
instrucción, y una vez detenido el programa, poner en la cima de la pila otro número arbitrario mediante el comando set.
Explica qué efecto tiene esto y por qué.
Programación en ensamblador de la
arquitectura IA-32
193 / 198
B.6.
Introducir todos los valores en hexadecimal y con el prefijo 0x. Responder a todas las preguntas antes de pulsar el botón de envío
al final del formulario.
0. ¿Qué comando hay que utilizar para mostrar por pantalla todos los bytes que codifican el string con nombre
formato? text IntroToGdb:A1_stringCod 16 16
¿Cuál es el valor del último byte? text IntroToGdb:A2_stringLastByte 16 16
0. Se sabe que las instrucciones del tipo push :registro: se codifican mediante un único byte y mov
:registro:, :registro: mediante dos bytes. Utilizando únicamente el depurador, decir cuál es el código
hexadecimal de las siguientes instrucciones:
push %ebp
push %eax
push %ebx
push %ecx
push %edx
text IntroToGdb:B1_push 16 16
text IntroToGdb:B2_push 16 16
text IntroToGdb:B3_push 16 16
text IntroToGdb:B4_push 16 16
text IntroToGdb:B5_push 16 16
0. Situar un punto de parada en la instrucción call printf. ¿Qué comando es necesario para mostrar por pantalla
el valor que deposita en la cima de la pila la instrucción push $formato? text IntroToGdb:C_pushformat 16 16
0. ¿Qué comando hay que utilizar para mostrar por pantalla el contenido de las cuatro palabras de memoria que se
encuentran en la cima de la pila? Comprobar que estos valores son idénticos a los mostrados en el primer punto de
parada. text IntroToGdb:D_stackTop 16 16
0. La instrucción inc %ebx aumenta el valor de dicho registro en una unidad. Este registro contiene el índice del
siguiente elemento a imprimir. Poner un punto de parada en la instrucción siguiente a esta y con el programa
detenido modificar el valor de este registro con un número entre cero y seis (ambos inclusive). Explica qué es lo que
sucede y por qué.
IntroToGdb:E_arrayindex 80 10
0. Utilizando la ejecución instrucción a instrucción que permite el depurador, ¿qué instrucción se ejecuta justo antes
de la instrucción pop %edx? text IntroToGdb:F_beforepop 16 16
0. La instrucción push nums(, %ebx,4) deposita un cierto valor en la pila. Introducir un punto de parada en la
siguiente instrucción, y una vez detenido el programa, poner en la cima de la pila otro número arbitrario mediante el
comando set. Explica qué efecto tiene esto y por qué.
IntroToGdb:G_modifyvalue 80 10
Programación en ensamblador de la
arquitectura IA-32
194 / 198
Apéndice C
Licencia Creative Commons
Reconocimiento-NoComercial-CompartirIgual 2.5 España
CREATIVE COMMONS CORPORATION NO ES UN DESPACHO DE ABOGADOS Y NO PROPORCIONA SERVICIOS
JURÍDICOS. LA DISTRIBUCIÓN DE ESTA LICENCIA NO CREA UNA RELACIÓN ABOGADO-CLIENTE. CREATIVE
COMMONS PROPORCIONA ESTA INFORMACIÓN TAL CUAL (ON AN "AS-IS" BASIS). CREATIVE COMMONS NO
OFRECE GARANTÍA ALGUNA RESPECTO DE LA INFORMACIÓN PROPORCIONADA, NI ASUME RESPONSABILIDAD ALGUNA POR DAÑOS PRODUCIDOS A CONSECUENCIA DE SU USO.
Licencia
LA OBRA (SEGÚN SE DEFINE MÁS ADELANTE) SE PROPORCIONA BAJO LOS TÉRMINOS DE ESTA LICENCIA
PÚBLICA DE CREATIVE COMMONS ("CCPL" O "LICENCIA"). LA OBRA SE ENCUENTRA PROTEGIDA POR LA LEY
ESPAÑOLA DE PROPIEDAD INTELECTUAL Y/O CUALESQUIERA OTRAS NORMAS RESULTEN DE APLICACIÓN.
QUEDA PROHIBIDO CUALQUIER USO DE LA OBRA DIFERENTE A LO AUTORIZADO BAJO ESTA LICENCIA O LO
DISPUESTO EN LAS LEYES DE PROPIEDAD INTELECTUAL.
MEDIANTE EL EJERCICIO DE CUALQUIER DERECHO SOBRE LA OBRA, USTED ACEPTA Y CONSIENTE LAS LIMITACIONES Y OBLIGACIONES DE ESTA LICENCIA. EL LICENCIADOR LE CEDE LOS DERECHOS CONTENIDOS
EN ESTA LICENCIA, SIEMPRE QUE USTED ACEPTE LOS PRESENTES TÉRMINOS Y CONDICIONES.
1. Definiciones
a. La "obra" es la creación literaria, artística o científica ofrecida bajo los términos de esta licencia.
b. El "autor" es la persona o la entidad que creó la obra.
c. Se considerará "obra conjunta" aquella susceptible de ser incluida en alguna de las siguientes categorías:
1. "Obra en colaboración", entendiendo por tal aquella que sea resultado unitario de la colaboración de varios autores.
2. "Obra colectiva", entendiendo por tal la creada por la iniciativa y bajo la coordinación de una persona natural o
jurídica que la edite y divulgue bajo su nombre y que esté constituida por la reunión de aportaciones de diferentes
autores cuya contribución personal se funde en una creación única y autónoma, para la cual haya sido concebida sin
que sea posible atribuir separadamente a cualquiera de ellos un derecho sobre el conjunto de la obra realizada.
3. "Obra compuesta e independiente", entendiendo por tal la obra nueva que incorpore una obra preexistente sin la
colaboración del autor de esta última.
d. Se considerarán "obras derivadas" aquellas que se encuentren basadas en una obra o en una obra y otras preexistentes,
tales como: las traducciones y adaptaciones; las revisiones, actualizaciones y anotaciones; los compendios, resúmenes y
extractos; los arreglos musicales y, en general, cualesquiera transformaciones de una obra literaria, artística o científica,
salvo que la obra resultante tenga el carácter de obra conjunta en cuyo caso no será considerada como una obra derivada a
los efectos de esta licencia. Para evitar la duda, si la obra consiste en una composición musical o grabación de sonidos, la
sincronización temporal de la obra con una imagen en movimiento ("synching") será considerada como una obra derivada
a los efectos de esta licencia.
Programación en ensamblador de la
arquitectura IA-32
195 / 198
e. Tendrán la consideración de "obras audiovisuales" las creaciones expresadas mediante una serie de imágenes asociadas,
con o sin sonorización incorporada, así como las composiciones musicales, que estén destinadas esencialmente a ser
mostradas a través de aparatos de proyección o por cualquier otro medio de comunicación pública de la imagen y del
sonido, con independencia de la naturaleza de los soportes materiales de dichas obras.
f. El "licenciador" es la persona o la entidad que ofrece la obra bajo los términos de esta licencia y le cede los derechos de
explotación de la misma conforme a lo dispuesto en ella.
g. "Usted" es la persona o la entidad que ejercita los derechos cedidos mediante esta licencia y que no ha violado previamente
los términos de la misma con respecto a la obra, o que ha recibido el permiso expreso del licenciador de ejercitar los
derechos cedidos mediante esta licencia a pesar de una violación anterior.
h. La "transformación" de una obra comprende su traducción, adaptación y cualquier otra modificación en su forma de
la que se derive una obra diferente. Cuando se trate de una base de datos según se define más adelante, se considerará
también transformación la reordenación de la misma. La creación resultante de la transformación de una obra tendrá la
consideración de obra derivada.
i. Se entiende por "reproducción" la fijación de la obra en un medio que permita su comunicación y la obtención de copias
de toda o parte de ella.
j. Se entiende por "distribución" la puesta a disposición del público del original o copias de la obra mediante su venta,
alquiler, préstamo o de cualquier otra forma.
k. Se entenderá por "comunicación pública" todo acto por el cual una pluralidad de personas pueda tener acceso a la obra sin
previa distribución de ejemplares a cada una de ellas. No se considerará pública la comunicación cuando se celebre dentro
de un ámbito estrictamente doméstico que no esté integrado o conectado a una red de difusión de cualquier tipo. A efectos
de esta licencia se considerará comunicación pública la puesta a disposición del público de la obra por procedimientos
alámbricos o inalámbricos, incluida la puesta a disposición del público de la obra de tal forma que cualquier persona pueda
acceder a ella desde el lugar y en el momento que elija.
l. La "explotación" de la obra comprende su reproducción, distribución, comunicación pública y transformación.
m. Tendrán la consideración de "bases de datos" las colecciones de obras ajenas, de datos o de otros elementos independientes
como las antologías y las bases de datos propiamente dichas que por la selección o disposición de sus contenidos constituyan creaciones intelectuales, sin perjuicio, en su caso, de los derechos que pudieran subsistir sobre dichos contenidos.
n. Los "elementos de la licencia" son las características principales de la licencia según la selección efectuada por el licenciador e indicadas en el título de esta licencia: Reconocimiento de autoría (Reconocimiento), Sin uso comercial (NoComercial), Compartir de manera igual (CompartirIgual).
2. Límites y uso legítimo de los derechos. Nada en esta licencia pretende reducir o restringir cualesquiera límites legales de
los derechos exclusivos del titular de los derechos de propiedad intelectual de acuerdo con la Ley de Propiedad Intelectual o
cualesquiera otras leyes aplicables, ya sean derivados de usos legítimos, tales como el derecho de copia privada o el derecho a
cita, u otras limitaciones como la derivada de la primera venta de ejemplares.
3. Concesión de licencia. Conforme a los términos y a las condiciones de esta licencia, el licenciador concede (durante toda la
vigencia de los derechos de propiedad intelectual) una licencia de ámbito mundial, sin derecho de remuneración, no exclusiva e
indefinida que incluye la cesión de los siguientes derechos:
a. Derecho de reproducción, distribución y comunicación pública sobre la obra.
b. Derecho a incorporarla en una o más obras conjuntas o bases de datos y para su reproducción en tanto que incorporada a
dichas obras conjuntas o bases de datos.
c. Derecho para efectuar cualquier transformación sobre la obra y crear y reproducir obras derivadas.
d. Derecho de distribución y comunicación pública de copias o grabaciones de la obra, como incorporada a obras conjuntas
o bases de datos.
e. Derecho de distribución y comunicación pública de copias o grabaciones de la obra, por medio de una obra derivada.
Programación en ensamblador de la
arquitectura IA-32
196 / 198
Los anteriores derechos se pueden ejercitar en todos los medios y formatos, tangibles o intangibles, conocidos o por conocer.
Los derechos mencionados incluyen el derecho a efectuar las modificaciones que sean precisas técnicamente para el ejercicio
de los derechos en otros medios y formatos. Todos los derechos no cedidos expresamente por el licenciador quedan reservados,
incluyendo, a título enunciativo pero no limitativo, los establecidos en la sección 4e.
4. Restricciones. La cesión de derechos que supone esta licencia se encuentra sujeta y limitada a las restricciones siguientes:
a. Usted puede reproducir, distribuir o comunicar públicamente la obra solamente bajo los términos de esta licencia y debe
incluir una copia de la misma, o su Identificador Uniforme de Recurso (URI), con cada copia o grabación de la obra que
usted reproduzca, distribuya o comunique públicamente. Usted no puede ofrecer o imponer ningún término sobre la obra
que altere o restrinja los términos de esta licencia o el ejercicio de sus derechos por parte de los cesionarios de la misma.
Usted no puede sublicenciar la obra. Usted debe mantener intactos todos los avisos que se refieran a esta licencia y a la
ausencia de garantías. Usted no puede reproducir, distribuir o comunicar públicamente la obra con medidas tecnológicas
que controlen el acceso o uso de la obra de una manera contraria a los términos de esta licencia. Lo anterior se aplica a
una obra en tanto que incorporada a una obra conjunta o base de datos, pero no implica que éstas, al margen de la obra
objeto de esta licencia, tengan que estar sujetas a los términos de la misma. Si usted crea una obra conjunta o base de datos,
previa comunicación del licenciador, usted deberá quitar de la obra conjunta o base de datos cualquier referencia crédito
requerido en el apartado 4d, según lo que se le requiera y en la medida de lo posible. Si usted crea una obra derivada, previa
comunicación del licenciador, usted deberá quitar de la obra derivada cualquier crédito requerido en el apartado 4d, según
lo que se le requiera y en la medida de lo posible.
b. Usted puede reproducir, distribuir o comunicar públicamente una obra derivada solamente bajo los términos de esta licencia, o de una versión posterior de esta licencia con sus mismos elementos principales, o de una licencia iCommons de Creative Commons que contenga los mismos elementos principales que esta licencia (ejemplo: Reconocimiento-NoComercialCompartir 2.5 Japón). Usted debe incluir una copia de la esta licencia o de la mencionada anteriormente, o bien su Identificador Uniforme de Recurso (URI), con cada copia o grabación de la obra que usted reproduzca, distribuya o comunique
públicamente. Usted no puede ofrecer o imponer ningún término respecto de las obras derivadas o sus transformaciones
que alteren o restrinjan los términos de esta licencia o el ejercicio de sus derechos por parte de los cesionarios de la misma.
Usted debe mantener intactos todos los avisos que se refieran a esta licencia y a la ausencia de garantías. Usted no puede
reproducir, distribuir o comunicar públicamente la obra derivada con medidas tecnológicas que controlen el acceso o uso
de la obra de una manera contraria a los términos de esta licencia. Lo anterior se aplica a una obra derivada en tanto que
incorporada a una obra conjunta o base de datos, pero no implica que éstas, al margen de la obra objeto de esta licencia,
tengan que estar sujetas a los términos de esta licencia.
c. Usted no puede ejercitar ninguno de los derechos cedidos en la sección 3 anterior de manera que pretenda principalmente o
se encuentre dirigida hacia la obtención de un beneficio mercantil o la remuneración monetaria privada. El intercambio de
la obra por otras obras protegidas por la propiedad intelectual mediante sistemas de compartir archivos no se considerará
como una manera que pretenda principalmente o se encuentre dirigida hacia la obtención de un beneficio mercantil o la
remuneración monetaria privada, siempre que no haya ningún pago de cualquier remuneración monetaria en relación con
el intercambio de las obras protegidas.
d. Si usted reproduce, distribuye o comunica públicamente la obra o cualquier obra derivada, conjunta o base datos que la
incorpore, usted debe mantener intactos todos los avisos sobre la propiedad intelectual de la obra y reconocer al autor
original, de manera razonable conforme al medio o a los medios que usted esté utilizando, indicando el nombre (o el seudónimo, en su caso) del autor original si es facilitado, y/o reconocer a aquellas partes (por ejemplo: institución, publicación,
revista) que el autor original y/o el licenciador designen para ser reconocidos en el aviso legal, las condiciones de uso, o
de cualquier otra manera razonable; el título de la obra si es facilitado; de manera razonable, el Identificador Uniforme
de Recurso (URI), si existe, que el licenciador especifica para ser vinculado a la obra, a menos que tal URI no se refiera
al aviso sobre propiedad intelectual o a la información sobre la licencia de la obra; y en el caso de una obra derivada, un
aviso que identifique el uso de la obra en la obra derivada (e.g., "traducción castellana de la obra de Autor Original," o
"guión basado en obra original de Autor Original"). Tal aviso se puede desarrollar de cualquier manera razonable; con tal
de que, sin embargo, en el caso de una obra derivada, conjunta o base datos, aparezca como mínimo este aviso allá donde
aparezcan los avisos correspondientes a otros autores y de forma comparable a los mismos.
e. Para evitar la duda, sin perjuicio de la preceptiva autorización del licenciador, y especialmente cuando la obra se trate de
una obra audiovisual, el licenciador se reserva el derecho exclusivo a percibir, tanto individualmente como mediante una
entidad de gestión de derechos, o varias, (por ejemplo: SGAE, Dama, VEGAP), los derechos de explotación de la obra,
así como los derivados de obras derivadas, conjuntas o bases de datos, si dicha explotación pretende principalmente o se
encuentra dirigida hacia la obtención de un beneficio mercantil o la remuneración monetaria privada.
Programación en ensamblador de la
arquitectura IA-32
197 / 198
f. En el caso de la inclusión de la obra en alguna base de datos o recopilación, el propietario o el gestor de la base de datos
deberá renunciar a cualquier derecho relacionado con esta inclusión y concerniente a los usos de la obra una vez extraída
de las bases de datos, ya sea de manera individual o conjuntamente con otros materiales.
5. Exoneración de responsabilidad
A MENOS QUE SE ACUERDE MUTUAMENTE ENTRE LAS PARTES, EL LICENCIADOR OFRECE LA OBRA TAL
CUAL (ON AN "AS-IS" BASIS) Y NO CONFIERE NINGUNA GARANTÍA DE CUALQUIER TIPO RESPECTO DE LA
OBRA O DE LA PRESENCIA O AUSENCIA DE ERRORES QUE PUEDAN O NO SER DESCUBIERTOS. ALGUNAS
JURISDICCIONES NO PERMITEN LA EXCLUSIÓN DE TALES GARANTÍAS, POR LO QUE TAL EXCLUSIÓN PUEDE
NO SER DE APLICACIÓN A USTED.
6. Limitación de responsabilidad.
SALVO QUE LO DISPONGA EXPRESA E IMPERATIVAMENTE LA LEY APLICABLE, EN NINGÚN CASO EL LICENCIADOR SERÁ RESPONSABLE ANTE USTED POR CUALQUIER TEORÍA LEGAL DE CUALESQUIERA DAÑOS RESULTANTES, GENERALES O ESPECIALES (INCLUIDO EL DAÑO EMERGENTE Y EL LUCRO CESANTE), FORTUITOS O CAUSALES, DIRECTOS O INDIRECTOS, PRODUCIDOS EN CONEXIÓN CON ESTA LICENCIA O EL USO DE
LA OBRA, INCLUSO SI EL LICENCIADOR HUBIERA SIDO INFORMADO DE LA POSIBILIDAD DE TALES DAÑOS.
7. Finalización de la licencia
a. Esta licencia y la cesión de los derechos que contiene terminarán automáticamente en caso de cualquier incumplimiento de
los términos de la misma. Las personas o entidades que hayan recibido obras derivadas, conjuntas o bases de datos de usted
bajo esta licencia, sin embargo, no verán sus licencias finalizadas, siempre que tales personas o entidades se mantengan en
el cumplimiento íntegro de esta licencia. Las secciones 1, 2, 5, 6, 7 y 8 permanecerán vigentes pese a cualquier finalización
de esta licencia.
b. Conforme a las condiciones y términos anteriores, la cesión de derechos de esta licencia es perpetua (durante toda la
vigencia de los derechos de propiedad intelectual aplicables a la obra). A pesar de lo anterior, el licenciador se reserva el
derecho a divulgar o publicar la obra en condiciones distintas a las presentes, o de retirar la obra en cualquier momento.
No obstante, ello no supondrá dar por concluida esta licencia (o cualquier otra licencia que haya sido concedida, o sea
necesario ser concedida, bajo los términos de esta licencia), que continuará vigente y con efectos completos a no ser que
haya finalizado conforme a lo establecido anteriormente.
8. Miscelánea
a. Cada vez que usted explote de alguna forma la obra, o una obra conjunta o una base datos que la incorpore, el licenciador
original ofrece a los terceros y sucesivos licenciatarios la cesión de derechos sobre la obra en las mismas condiciones y
términos que la licencia concedida a usted.
b. Cada vez que usted explote de alguna forma una obra derivada, el licenciador original ofrece a los terceros y sucesivos
licenciatarios la cesión de derechos sobre la obra original en las mismas condiciones y términos que la licencia concedida
a usted.
c. Si alguna disposición de esta licencia resulta inválida o inaplicable según la Ley vigente, ello no afectará la validez o
aplicabilidad del resto de los términos de esta licencia y, sin ninguna acción adicional por cualquiera las partes de este
acuerdo, tal disposición se entenderá reformada en lo estrictamente necesario para hacer que tal disposición sea válida y
ejecutiva.
d. No se entenderá que existe renuncia respecto de algún término o disposición de esta licencia, ni que se consiente violación
alguna de la misma, a menos que tal renuncia o consentimiento figure por escrito y lleve la firma de la parte que renuncie
o consienta.
e. Esta licencia constituye el acuerdo pleno entre las partes con respecto a la obra objeto de la licencia. No caben interpretaciones, acuerdos o términos con respecto a la obra que no se encuentren expresamente especificados en la presente licencia.
El licenciador no estará obligado por ninguna disposición complementaria que pueda aparecer en cualquier comunicación
de usted. Esta licencia no se puede modificar sin el mutuo acuerdo por escrito entre el licenciador y usted.
Programación en ensamblador de la
arquitectura IA-32
198 / 198
Creative Commons no es parte de esta licencia, y no ofrece ninguna garantía en relación con la obra. Creative Commons no será
responsable frente a usted o a cualquier parte, por cualquier teoría legal de cualesquiera daños resultantes, incluyendo, pero no
limitado, daños generales o especiales (incluido el daño emergente y el lucro cesante), fortuitos o causales, en conexión con esta
licencia. A pesar de las dos (2) oraciones anteriores, si Creative Commons se ha identificado expresamente como el licenciador,
tendrá todos los derechos y obligaciones del licenciador.
Salvo para el propósito limitado de indicar al público que la obra está licenciada bajo la CCPL, ninguna parte utilizará la
marca registrada "Creative Commons" o cualquier marca registrada o insignia relacionada con "Creative Commons" sin su
consentimiento por escrito. Cualquier uso permitido se hará de conformidad con las pautas vigentes en cada momento sobre el
uso de la marca registrada por "Creative Commons", en tanto que sean publicadas su sitio web (website) o sean proporcionadas
a petición previa.
Puede contactar con Creative Commons en: http://creativecommons.org/.
Descargar