Lenguaje ensamblador
No para programar en él, sino para poder leerlo: entender qué generó el compilador es lo que permite explicar un perfilado, un core dump o una optimización que no ocurrió.
Para este tema conviene tener claro:Ciclo de instrucción y camino de datos
Nadie escribe aplicaciones en ensamblador, y sin embargo saber leerlo cambia cómo se depura. Es la única forma de ver qué hizo realmente el compilador con el código: qué eliminó, qué reordenó, qué guardó en un registro y qué fue a memoria.
Casi uno a uno con el código de máquina
El ensamblador es una correspondencia casi uno a uno con el código de máquina: cada línea es una instrucción, con un nombre legible en vez de bits. El ensamblador —el programa— traduce eso a binario y resuelve las etiquetas.
Es específico del repertorio de instrucciones: x86-64 y ARM son lenguajes distintos. Y hay dos sintaxis para x86, Intel y AT&T, que invierten el orden de los operandos, lo que explica mucha confusión al leer ejemplos de distintas fuentes.
Antes de seguir, predecí
Las pocas categorías de instrucciones
Las instrucciones caen en pocas categorías. Movimiento de datos entre registros y memoria; aritmética y lógica; comparaciones, que dejan banderas; saltos condicionales que leen esas banderas; y llamada y retorno de funciones.
El modelo de trabajo es: cargar de memoria a registros, operar en registros, guardar a memoria. Los registros son escasos —dieciséis de propósito general en x86-64— y esa escasez es lo que el compilador administra cuando asigna registros.
La línea a traducir es total = precio + envio. Los dos valores están en memoria y ningún registro tiene nada útil.
La pila, estructura central del tiempo de ejecución
La pila es la estructura central del tiempo de ejecución. Crece hacia direcciones bajas, y un registro dedicado apunta a su tope. Cada llamada empuja un marco: dirección de retorno, registros guardados, variables locales.
Ahí viven las variables locales que no entraron en registros, y ahí está la dirección de retorno que
ret va a usar. Un desborde de buffer local que sobrescribe esa dirección es, textualmente, tomar el
control del flujo: por eso existen los canarios de pila y la aleatorización de direcciones.
La convención de llamada, que es un acuerdo
Para que funciones compiladas por separado se entiendan hace falta un acuerdo: la convención de llamada. Define qué registros llevan los argumentos, dónde va el valor de retorno, y quién guarda cada registro.
Esa última parte divide los registros en dos: los que preserva quien llama y los que preserva la función llamada. Es lo que permite enlazar código de distintos lenguajes y compiladores, y es lo que hay que respetar para llamar a C desde otro lenguaje.
Leer la salida del compilador enseña más que escribirlo
Leer la salida del compilador enseña más que escribirlo. Con optimizaciones activadas aparecen cosas que sorprenden: ciclos desenrollados, funciones cortas insertadas en el llamador, cuentas resueltas en tiempo de compilación, variables que nunca tocan memoria.
También aparece lo que no hizo: una función que no pudo insertar por ser virtual, una lectura que no pudo eliminar porque la variable es volátil, un acceso a memoria repetido porque el compilador no pudo descartar que dos punteros apunten al mismo lugar. Esa última es la razón de ser de las palabras clave de aliasing.
Cuándo vale la pena mirarlo
Los casos donde vale la pena mirarlo son concretos: interpretar un perfilado que señala una función caliente, entender un volcado de memoria sin símbolos, verificar que una optimización esperada ocurrió, o trabajar con código sin fuente.
Escribirlo a mano queda para rincones muy chicos: rutinas criptográficas de tiempo constante, inicialización antes de que exista el runtime, instrucciones vectoriales que el compilador no aprovecha. En todos esos casos se escriben unas pocas líneas, no un programa.
Para qué sirve saber leerlo
| Situación | Qué aporta el ensamblador |
|---|---|
| Un ciclo caliente que no rinde | ver si el compilador vectorizó o no |
| Un comportamiento raro con optimizaciones activadas | ver qué eliminó el compilador |
| Depurar sin símbolos | es lo único que hay |
| Entender un aviso de seguridad | los desbordamientos se explican en la pila |
| Escribirlo a mano | casi nunca: el compilador gana |
Cierre
Cargar, operar, guardar, con registros escasos y una pila que sostiene los marcos de llamada. La convención de llamada es lo que permite enlazar código ajeno, y leer la salida del compilador es la forma más directa de ver qué optimizó y qué no.
Autoevaluación
¿Lo entendiste?
Práctica