Cómo Crear “Hola Mundo” en Ensamblador x86

Porque deberías hacer esto, o bueno ¿Y por qué no? El software es divertido. Sé que esto suena una locura; ensamblador por diversión.

En este artículo, vamos a saludar al mundo en ensamblador x86 para Linux.

¿Cómo Crear un Hola Mundo en Lenguaje Ensamblador?

Necesitas un entorno Linux. Aunque en Windows puedes usar WSL2.

Crea un archivo llamado hello.s y agrega el siguiente código:

.section  .data
msg:
    .asciz "Hello, World!n"   # Define una cadena terminada en nulo con una nueva línea
msg_len = . - msg              # Longitud desde la ubicación actual hasta msg

.section .text
.globl _start              # Declara _start como un símbolo global (punto de entrada)
_start:
    # --- sys_write(int fd, const void *buf, size_t count) ---
    movl $4, %eax              # sys_write en %eax
    movl $1, %ebx              # fd stdout en %ebx
    movl $msg, %ecx            # Dirección de msg
    movl $msg_len, %edx        # Longitud de msg
    int $0x80                  # Llama al sistema operativo

    # --- sys_exit(int status) ---
    movl $1, %eax              # sys_exit en %eax
    xorl %ebx, %ebx            # Pone a cero %ebx (código de salida 0)
    int $0x80                  # Llama al sistema operativo

Compila el programa usando as. Instalarlo en Linux es simple—solo instala las herramientas GNU con:

sudo apt update && sudo apt install -y build-essential gdb libgtk-3-dev

Luego, compila tu archivo de ensamblador:

as ./hello.s -o hello.o

Este comando crea el archivo de objeto. A continuación, enlázalo:

ld ./hello.o -o hello

Finalmente, ejecuta tu programa:

./hello

Este podría ser el “Hello World” más complejo que hayas visto, especialmente si eres nuevo en ensamblador.

Vamos a desglosarlo.

Secciones en Ensamblador

Los programas en ensamblador se dividen en secciones:

  • .section .data → Donde almacenamos datos, como “Hello, World!”
  • .section .text → Donde escribimos las instrucciones
  • .globl _start → Define el punto de entrada del programa

Símbolos y Etiquetas

Los símbolos son nombres para ubicaciones de memoria.

Por ejemplo, msg es un símbolo. Le dice al ensamblador, “Oye, recuerda esta ubicación—la usaremos más tarde”. Es como una variable.

Cuando agregas un :, se convierte en una etiqueta, que define el valor de un símbolo.

msg:
    .asciz "Hello, World!n"  

Esto significa: “En la ubicación de memoria msg, almacena la cadena ‘Hello, World!n'”.

Este patrón está en todas partes. _start también es una etiqueta:

.globl _start  
_start:

Hacer que _start sea global permite que el ensamblador y el enlazador lo encuentren.

Seguimiento de Memoria

El ensamblador no rastrea la memoria por ti—tienes que hacerlo manualmente.

msg_len = . - msg

¿Qué está pasando aquí?

msg es solo una dirección. Señala el inicio de “Hello, World!n”, pero no sabe dónde termina.

. es la ubicación de memoria actual—justo después de la cadena.

Así que msg_len = . - msg significa:

Toma la dirección después de “Hello, World!n” y réstale la dirección de inicio.

Eso nos da la longitud de la cadena, que necesitamos para la llamada a sys_write.

Arquitectura de la CPU

Los registros almacenan datos temporalmente y operan mucho más rápido que la RAM.

En nuestro programa, %eax es un registro.

Registros de propósito general:
%eax
%ebx
%ecx
%edx
%edi
%esi

Registros de propósito especial:
%ebp
%esp
%eip
%eflags

Así es como movemos datos entre registros:

movl $4, %eax    # sys_write en %eax
movl $1, %ebx    # fd stdout en %ebx
movl $msg, %ecx  # Dirección de msg
movl $msg_len, %edx  # Longitud de msg
int $0x80        # Llama al sistema operativo

Pero antes de poder ejecutar cualquier cosa, tenemos que hablar sobre…

El Kernel

El kernel es el intermediario entre tu programa y el hardware. Cada llamada al sistema (acceso a archivos, asignación de memoria, salir de un programa) pasa por el kernel.

Siempre que veas esto:

int $0x80  # Llamada al kernel

Estás pidiendo ayuda al kernel.

Pero el kernel tiene reglas. Antes de llamarlo, debes configurar los registros con los valores correctos.

Por ejemplo, para salir de un programa, el kernel espera:

movl $1, %eax  # Comando sys_exit
movl $0, %ebx  # Estado de salida

La llamada sys_exit espera:

  • %eax = 1 → “Quiero salir.”
  • %ebx = 0 → “Salida exitosa.”

Es lo mismo que devolver 0 en C:

int main() {
    return 0;  // Estado de salida 0 (éxito)
}

Escribir en la Consola

A diferencia de sys_exit, escribir en la consola requiere más información:

movl $4, %eax    # Comando sys_write
movl $1, %ebx    # Descriptor de archivo 1 (stdout)
movl $msg, %ecx  # Dirección del mensaje
movl $msg_len, %edx  # Longitud del mensaje
int $0x80        # Llamada al sistema operativo

Esto es lo que está pasando:

  • movl $4, %eax → El número 4 le dice al kernel, “Quiero escribir.”
  • movl $1, %ebx → El número 1 significa “escribir en stdout.”
  • movl $msg, %ecx → ¿Dónde está el mensaje? Está en msg.
  • movl $msg_len, %edx → ¿Cuánto mide el mensaje? Eso está en msg_len.
  • int $0x80 → Llama al kernel para ejecutar la escritura.

Una vez que hemos escrito nuestro mensaje, salimos:

movl $1, %eax  # sys_exit
xorl %ebx, %ebx  # Código de salida 0
int $0x80  # Llamada al sistema operativo

AT&T vs. Sintaxis de Intel

Este programa usa la sintaxis AT&T que es común en los ensambladores de GNU.

La sintaxis Intel, utilizada en NASM, se ve ligeramente diferente. Una diferencia importante es que la sintaxis AT&T antepone un % a los registros.

Algunas personas prefieren la sintaxis Intel, pero la mayoría de los libros se adhieren a AT&T, por lo que vale la pena aprenderla.

Vistas: 14