⬅️ Atlas · 🧟 Lenguajes que siguen vivos · 📚 Índice de clases
Nunca desapareció; se concentró. El ensamblador dejó de usarse para escribir aplicaciones y se replegó a las capas donde alguien tiene que hablarle a la máquina sin intermediarios: el arranque del sistema, el cambio de contexto del planificador, el firmware, la criptografía de tiempo constante y los núcleos vectoriales que hacen rápido a todo lo demás.
🎯 Por qué está en este programa
Criterio de inclusión: se escribe ensamblador hoy, todos los días. Cada núcleo de sistema operativo tiene su capa de arranque y de conmutación de tareas en ensamblador; cada biblioteca criptográfica seria (OpenSSL, BoringSSL) lleva rutinas escritas a mano por seguridad y rendimiento; cada microcontrolador tiene su vector de interrupciones; y en el mundo mainframe, HLASM sigue siendo un lenguaje de desarrollo activo en IBM Z.
Entra porque es el suelo de todo el programa. La Parte 8 trata de cómo funcionan los lenguajes por dentro: qué es una llamada a función, qué es la pila, qué significa "paso por valor", por qué una variable local es más barata que una del montículo, qué hace realmente un
for. Todas esas preguntas tienen una sola respuesta definitiva, y está aquí. Leer el ensamblador que genera tu compilador no es nostalgia: es la única forma de dejar de creer y empezar a saber.
| Año | Finales de los 40 — el primer ensamblador simbólico se atribuye al EDSAC (1949) |
| Autoría | No hay una: cada arquitectura define el suyo |
| Familia | Lenguajes de bajo nivel, correspondencia casi 1:1 con el código máquina |
| Paradigma | Imperativo puro: mover datos, operar, saltar |
| Tipado | Ninguno. Solo hay bytes; el significado lo pone la instrucción que los usa |
| Memoria | Totalmente manual: registros, pila y direcciones |
| Ejecución | Ensamblado a código máquina; ejecución directa por el procesador |
| Estado | 🟢 Vivo donde importa — firmware, núcleos, criptografía, SIMD, embebidos, seguridad |
Los primeros ordenadores se programaban en código máquina: números escritos a mano, con las direcciones calculadas por el programador. Cambiar una instrucción en medio de un programa obligaba a recalcular todos los saltos.
El ensamblador resolvió eso con dos ideas simples y decisivas: nombres simbólicos para las
instrucciones (MOV en vez de un opcode numérico) y, sobre todo, etiquetas para las direcciones,
de modo que el programa se reubica solo. Kathleen Booth es reconocida como autora del primer
lenguaje ensamblador junto a su trabajo en el ARC2 a finales de los 40, y Maurice Wilkes y su
equipo desarrollaron el Initial Orders del EDSAC en 1949, uno de los primeros ensambladores
funcionales.
Durante los años 50 y 60, el ensamblador era la programación. FORTRAN tuvo que demostrar que un compilador podía generar código comparable, y COBOL tuvo que demostrar que la legibilidad valía la pena. Cuando ambas cosas quedaron probadas, el ensamblador empezó a retirarse de la escritura de aplicaciones — sin desaparecer nunca de las capas bajas.
Un punto que conviene fijar: "ensamblador" no es un lenguaje, es una categoría. Hay tantos como arquitecturas, y no se parecen entre sí:
| Arquitectura | Dónde vive | Rasgo |
|---|---|---|
| x86-64 | PC, servidores, nube | Muy complejo (CISC), instrucciones de longitud variable, dos sintaxis rivales (Intel y AT&T) |
| ARM / AArch64 | Móviles, Apple Silicon, embebidos, servidores | RISC, instrucciones de tamaño fijo, muchos registros |
| RISC-V | Embebidos, académico, industria creciente | RISC abierto y modular, sin licencia |
| z/Architecture (HLASM) | Mainframe IBM Z | Ensamblador empresarial con macros muy potentes |
| AVR / PIC / MSP430 | Microcontroladores | Recursos mínimos, control directo de patillas |
| PTX / SASS | GPU NVIDIA | Ensamblador paralelo para miles de hilos |
1. Hay cosas que ningún lenguaje de alto nivel puede expresar. No existe forma en C de decir "escribe este valor en el registro de control del procesador", "invalida la TLB" o "ejecuta esta instrucción atómica concreta". El código de arranque, el cambio de contexto y los manejadores de interrupción tienen que ser ensamblador.
2. El compilador es muy bueno, pero no lo sabe todo. En el 99 % de los casos, escribir C bien es más rápido que escribir ensamblador a mano. En el 1 % restante —transformaciones vectoriales con patrones de acceso concretos, bucles internos de un códec— la persona que conoce el procesador gana, y gana por factores, no por porcentajes.
3. Garantías que el compilador no ofrece. El tiempo constante en criptografía es el ejemplo canónico: el compilador tiene derecho a convertir tu operación sin ramas en un salto condicional, y eso abre un canal lateral. La única forma de garantizarlo es escribir las instrucciones.
4. Leerlo es una destreza imprescindible. Depurar un fallo de optimización, entender un volcado de memoria, analizar un binario sin fuentes, verificar que una mitigación de seguridad se aplicó de verdad: todo eso es lectura de ensamblador.
5. Es el único lugar donde las abstracciones se pueden comprobar. Cuando en la Parte 5 se afirma que "una llamada a función guarda la dirección de retorno en la pila", eso es una afirmación verificable: se compila, se mira el ensamblador y se ve.
objdump y paciencia hoy es inmediato.asm de GCC/Clang, asm! de Rust): permite insertar
instrucciones concretas dentro de código de alto nivel, describiendo al compilador qué registros y
qué memoria se tocan, para que siga optimizando alrededor con seguridad.# Ensamblar y enlazar (GNU as, sintaxis AT&T, x86-64 Linux)
gcc -no-pie total.s -o total
echo "15000 2 0.10" | ./total
# Total: 27000.00
# Ver el ensamblador que genera TU código C — el uso más frecuente hoy:
gcc -O2 -S -masm=intel programa.c -o programa.s
# Desensamblar un binario existente:
objdump -d -M intel ./programa | less
Herramientas: GAS (as, el ensamblador de GNU, sintaxis AT&T por defecto), NASM y
YASM (sintaxis Intel, muy usados en proyectos multimedia), MASM (Microsoft), LLVM-MC.
Para leer y analizar: objdump, Ghidra (libre, de la NSA), IDA Pro, radare2/rizin y
Compiler Explorer.
Las dos sintaxis de x86, que confunden a todo el mundo:
AT&T (GNU as): movq $5, %rax # destino a la DERECHA, $ para literales, % para registros
Intel (NASM): mov rax, 5 # destino a la IZQUIERDA, sin prefijos
Es el mismo código máquina escrito de dos maneras. Al leer documentación conviene comprobar siempre cuál se está usando.
⚠️ Específico de una arquitectura, y declarado. Este código es x86-64 con la ABI System V de Linux y sintaxis AT&T. No es portable: en ARM64, en RISC-V o en Windows sería otro programa. Esa dependencia es, precisamente, la característica que define al ensamblador.
.section .rodata
fmt_in: .asciz "%lf %lf %lf"
fmt_out: .asciz "Total: %.2f\n"
uno: .double 1.0
.text
.globl main
main:
pushq %rbp # prólogo: guardar el marco del llamante
movq %rsp, %rbp # establecer el marco propio
subq $32, %rsp # reservar 32 bytes en la pila (3 doubles + alineación)
# --- scanf("%lf %lf %lf", &precio, &cantidad, &descuento) ---
leaq fmt_in(%rip), %rdi # 1.er argumento: el formato
leaq -8(%rbp), %rsi # 2.º: &precio
leaq -16(%rbp), %rdx # 3.º: &cantidad
leaq -24(%rbp), %rcx # 4.º: &descuento
xorl %eax, %eax # 0 argumentos en registros vectoriales
call scanf@PLT
# --- total = precio * cantidad * (1 - descuento) ---
movsd uno(%rip), %xmm0 # xmm0 = 1.0
subsd -24(%rbp), %xmm0 # xmm0 = 1.0 - descuento
mulsd -8(%rbp), %xmm0 # xmm0 *= precio
mulsd -16(%rbp), %xmm0 # xmm0 *= cantidad
# --- printf("Total: %.2f\n", total) ---
leaq fmt_out(%rip), %rdi # 1.er argumento: el formato
movl $1, %eax # 1 argumento en registros vectoriales (xmm0)
call printf@PLT
xorl %eax, %eax # return 0
leave # epílogo: deshacer el marco
ret
Recorrido, y lo que enseña cada bloque.
precio, cantidad y descuento no existen: hay tres huecos de 8 bytes en
la pila, en -8(%rbp), -16(%rbp) y -24(%rbp). El nombre era una comodidad del compilador. Esto
es lo que es una variable local: un desplazamiento respecto al puntero de marco.pushq %rbp / movq %rsp, %rbp / leave es el marco de pila, y aparece idéntico en el código
generado por cualquier compilador. Cuando en la
Parte 5 se habla de "el ámbito de una
función" o de "la pila de llamadas", esto es la pila de llamadas.subq $32, %rsp es reservar memoria local, y por eso es gratis comparado con el montículo: una
resta. Liberarla es leave. Ahí está, en dos instrucciones, la respuesta a por qué una variable
local es más barata que una asignación dinámica.%rdi, %rsi, %rdx, %rcx, %r8, %r9, y los reales en %xmm0–%xmm7. Nadie lo
comprueba: si te equivocas de registro, el programa lee basura. Esa convención es lo que hace
posible que Rust llame a C, que Python cargue una biblioteca .so y que la
Parte 10 tenga
sentido. La interoperabilidad entre lenguajes se define en este nivel, no en el sintáctico.movl $1, %eax antes de printf no es adorno. En una función variádica, %eax debe contener
cuántos argumentos viajan en registros vectoriales, porque printf lo necesita para recorrerlos.
Olvidarlo suele producir un fallo de segmentación desconcertante. Es un detalle de la ABI que casi
nadie conoce hasta que lo sufre.subsd, mulsd: aritmética de doble precisión en registros SSE. La s final es scalar (un
valor por registro); las variantes subpd/mulpd operan sobre varios valores a la vez, y en
esa diferencia de una letra está todo el paralelismo SIMD.(%rip) es direccionamiento relativo al contador de programa, lo que permite que el código sea
reubicable. Es la base técnica de las bibliotecas compartidas y de las mitigaciones de seguridad
como ASLR.@PLT indica que el símbolo se resuelve en tiempo de enlace dinámico, a través de la tabla de
enlace de procedimientos. Es cómo funciona realmente cargar una biblioteca.leaq y como real con movsd. El
procesador no sabe qué son: lo decide la instrucción. Cuando en la
Parte 3 se dice que "el tipo es una
interpretación de los bits", esta es la demostración.El uso real de esto no es escribirlo, es leerlo. Compila la versión en C de la clase 041 con
gcc -O2 -S y compárala con este código: verás que el compilador elimina el marco de pila, mantiene
los valores en registros y reordena las multiplicaciones. Ese ejercicio —tres minutos en
Compiler Explorer— enseña más sobre optimización que cualquier explicación.
| Si conoces… | En ensamblador es… |
|---|---|
| Variable local | Un desplazamiento en la pila: -8(%rbp) |
| Variable global | Una etiqueta en .data o .bss |
| Constante | Una etiqueta en .rodata |
x = y |
mov — copiar bytes de un sitio a otro |
x + y |
add (enteros) o addsd (reales de doble precisión) |
if (a > b) |
cmp seguido de un salto condicional (jg, jle, …) |
while / for |
Una etiqueta y un salto hacia atrás |
| Llamada a función | call — apila la dirección de retorno y salta |
return |
ret — desapila la dirección y salta a ella |
| Argumentos | Registros según la ABI, y la pila a partir del séptimo |
| Valor devuelto | %rax (entero) o %xmm0 (real) |
struct |
Un bloque de bytes; los campos son desplazamientos |
| Puntero | Una dirección; leaq la calcula, movq (%reg) la sigue |
mov %rax, %rbx mueve rax→rbx en AT&T y rbx→rax en Intel. La misma
línea, sentido contrario. Comprueba siempre cuál estás leyendo.%rsp esté alineado a 16 bytes en el momento
del call. Si no lo está, una instrucción SSE dentro de printf provoca un fallo de segmentación
aparentemente aleatorio. Es el error más frustrante para quien empieza.%rbx, %rbp, %r12–%r15) y los que puede destruir. Suponer lo contrario produce
errores que solo aparecen a veces.-O2, el compilador reordena, fusiona y
elimina. La correspondencia línea a línea con el fuente desaparece. Para estudiar, empieza con
-O0.⏮️ Volver al Atlas · 🧟 Los lenguajes que siguen vivos · 🔗 Relacionadas: C · C++ · Fortran