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á enmsg.movl $msg_len, %edx→ ¿Cuánto mide el mensaje? Eso está enmsg_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.