Programación en ensamblador de la arquitectura IA-32 Universidad Carlos III de Madrid

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 ta

0 downloads 95 Views 6MB Size

Recommend Stories


UNIVERSIDAD CARLOS III DE MADRID
UNIVERSIDAD CARLOS III DE MADRID Instituto de Derechos Humanos: Bartolomé de las Casas TESINA “¿Son los derechos sociales derechos colectivos?: los d

UNIVERSIDAD CARLOS III DE MADRID
UNIVERSIDAD CARLOS III DE MADRID Departamento de Economía Tema 1: Matrices y sistemas de ecuaciones lineales. Empezaremos por recordar conceptos ya

Universidad Carlos III Madrid
Universidad Carlos III Madrid Proyecto fin de grado Grado en Ingeniería Informática Happy Grow: Videojuego desarrollado con Unity3D para apoyo en la

Story Transcript

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

.text .global main

# Comienzo del código # Obligatorio

main: 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

Get in touch

Social

© Copyright 2013 - 2024 MYDOKUMENT.COM - All rights reserved.