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/.