⚙️ Assembler — desde 1949

⬅️ 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

📜 Historia

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

🏭 Dónde sobrevive hoy

🧠 Por qué no ha muerto

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.

🔄 Lo que se ha modernizado

⚙️ Cómo se ejecuta hoy

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

🧪 El programa de la clase 041 en ensamblador x86-64

⚠️ 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.

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.

🔍 Qué reconocer si vienes de otro lenguaje

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

⚠️ Errores comunes al leerlo

📚 Fuentes y bibliografía


⏮️ Volver al Atlas · 🧟 Los lenguajes que siguen vivos · 🔗 Relacionadas: C · C++ · Fortran