El lenguaje ensamblador ("assembler" o assembly language en inglés, también la abreviatura asm) es un lenguaje de programación de bajo nivel. Consiste en un conjunto de mnemónicos que representan instrucciones soportadas por computadoras, microprocesadores, microcontroladores y otros circuitos integrados programables. Implementa una representación simbólica de los códigos de máquina binarios y otras constantes necesarias para programar una arquitectura de procesador. En este lenguaje, cada línea de código asm se traduce exactamente en una instrucción máquina ejecutada por el hardware subyacente (procesador o CPU).
El lenguaje ensamblador refleja directamente la arquitectura e instrucciones de una CPU, y pueden ser muy diferentes de una arquitectura de CPU a otra. Cada arquitectura de microprocesador tiene su propio set de instrucciones, y en consecuencia su propio lenguaje ensamblador ya que este se encuentra muy ligado a la estructura del hardware para el cual se programa.
A diferencia de un lenguaje de programación de alto nivel, cuando se escriben programas en lenguaje ensamblador se carece de librerías o cualquier otro tipo de ayuda. Sólo se dispone del conjunto de instrucciones provistas por el procesador (CPU) y las llamadas al sistema operativo (funciones implementadas por el sistema operativo, en este caso Linux). Por lo tanto, para programar en lenguaje ensamblador se debe tener a mano tanto el set de instrucciones destino como la tabla de syscalls del kernel.
Para el caso de las arquitecturas x86 y amd64 existen diferentes ensambladores y sintaxis dependiendo del fabricante. Hay dos principales ramas en cuanto a sintaxis se refiere: Intel y AT&T. Se diferencian principalmente en el orden de los operandos, especificación de tamaños de parámetros, valores inmediatos y direcciones efectivas. No va al caso de este artículo hacer una explicación detallada de ambas sintaxis, sino que dejo varios enlaces interesantes al final del mismo. La sintaxis Intel es la más popular y algunos ensambladores que la utilizan son NASM, YASM, FASM, MASM, TASM y GAS.
Este artículo demuestra el proceso de ensamblado y linkeado de un programa en lenguaje Yasm utilizando el ensamblador yasm
y el linker ld
desde línea de comandos en GNU/Linux para las arquitecturas x86 y amd64. No se trata de un tutorial de programación en lenguaje ensamblador en sí, aunque se dejan buenos tutoriales en la sección de referencias.
Yasm es una reimplementación del ensamblador NASM bajo licencia BSD. Se trata de un ensamblador modular que soporta los sets de instrucciones x86 y amd64, acepta la sintaxis NASM (Intel) y GAS (AT&T) y muchos formatos de archivo objeto compatibles con sistemas Linux, Windows e iOS (ELF32, ELF64, Mach-O, RDOFF2, COFF, Win32, Wind64). Además es capaz de generar símbolos de debug en formato STABS, DWARF 2y CodeView 8.
Para instalar Yasm en Debian y derivados, ejecutar:
# apt-get install yasm
¡Hola Mundo!
Veamos un ejemplo de programa escrito en lenguaje ensamblador. En la sección de datos se declara y reserva memoria para el string que contiene la frase "¡Hola Mundo!", mientras que en la sección de texto se define el programa:
emi@hal9000:~/yasm$ nano hola.asm
; Ejemplo de "Hola Mundo" en lenguaje ensamblador section .text global _start _start: ; Imprimo "Hola Mundo" por pantalla: mov EAX, 4 ; selecciono la llamada sys_write mov EBX, 1 ; escribo en stdout mov ECX, mensaje ; puntero al mensaje mov EDX, longitud ; longitud del mensaje int 0x80 ; interrupción 80h ; Termino el proceso con éxito mov EAX, 1 ; selecciono la llamada sys_exit mov EBX, 0 ; retorno 0 (exit status) int 0x80 ; llamo al sistema operativo section .data mensaje db "¡Hola Mundo!",0xa longitud equ $ - mensaje
Este programa hace uso de la llamada al sistema sys_write
del kernel Linux, escribiendo el mensaje en la salida estándar (pantalla). Luego invoca a sys_exit
para terminar la ejecución. Notar como se hace un fuerte uso de las llamadas al sistema operativo Linux.
La guía x86 Assembly Guide conforma un excelente tutorial en el uso de registros; modos de direccionamiento de memoria; principales instrucciones en el set de instrucciones Intel (de datos, aritméticas, lógicas y de control); convenciones de llamada a funciones y subrutinas; uso de la pila (stack) y programación en general. La recomiendo ampliamente para comenzar a desarrollar programas en lenguaje ensamblador ya que explica todos estos conceptos de forma simple y directa.
Se observa que, como todo código fuente, el tipo de archivo es de texto plano (tipo MIME es "text/plain"):
emi@hal9000:~/yasm$ file hola.asm hola.asm: UTF-8 Unicode text emi@hal9000:~/yasm$ mimetype hola.asm hola.asm: text/plain emi@hal9000:~/yasm$ mimetype -d hola.asm hola.asm: plain text document
Para ensamblar el programa con Yasm para la arquitectura x86 (32 bit), incluyendo los símbolos de depuración, ejecutar:
emi@hal9000:~/yasm$ yasm -f elf -g stabs hola.asm
La opción -g
agrega los símbolos de depuración (debug), en este ejemplo en formato STABS. Esto es necesario si se requiere depurar el programa utilizando un debugger como ddd
u otro.
Si no se desean incluir los símbolos de depuración (por ejemplo al liberar la versión distribuible del programa) ejecutar:
emi@hal9000:~/yasm$ yasm -f elf hola.asm
El ensamblador yasm
genera el archivo objeto:
emi@hal9000:~/yasm$ ll hola* -rw-r--r-- 1 emi emi 554 Nov 21 11:18 hola.asm -rw-r--r-- 1 emi emi 976 Nov 21 11:19 hola.o
Se trata del código binario del programa, en formato ELF32, pero sin enlazar:
emi@hal9000:~/yasm$ file hola.o hola.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
Para enlazar ("linkear") el binario y convertirlo en ejecutable, utilizar el linker ld
:
$ ld -o hola hola.o
La opción -o
define el nombre del archivo ejecutable de salida. En caso de enlazar el archivo objeto en formato ELF32 desde u sistema de 64 bit (amd64), es necesario emular la arquitectura con la opción -m
. Esta indica que el formato del archivo de salida debe ser para la arquitectura i386:
emi@hal9000:~/yasm$ ld -o hola -m elf_i386 hola.o
El linker genera el binario ejecutable:
emi@hal9000:~/yasm$ ll hola* -rwxr-xr-x 1 emi emi 9064 Nov 21 11:19 hola -rw-r--r-- 1 emi emi 554 Nov 21 11:18 hola.asm -rw-r--r-- 1 emi emi 976 Nov 21 11:19 hola.o emi@hal9000:~/yasm$ file hola hola: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
Ejecutarlo para verificar el funcionamiento:
emi@hal9000:~/yasm$ ./hola ¡Hola Mundo!
Si se desea ensablar y linkear el mismo programa para arquitectura amd64 (64 bit), ejecutar los siguientes pasos:
emi@hal9000:~/yasm$ yasm -f elf64 hola.asm emi@hal9000:~/yasm$ ld -o hola hola.o emi@hal9000:~/yasm$ file hola hola: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Script de compilación
Para automatizar el ensamblado y linkeo, es posible crear un pequeño script en lenguaje Bash (al que he llamado simplemente ensamblar32
):
#!/bin/bash # Si no hay parámetros de entrada, imprimir la ayuda if [ $# -lt 1 ] then echo "Uso: $0 <archivo>.asm" exit 1 fi # Eliminar extensión del nombre de archivo pasado como parámetro FILE=$(echo $1 | cut -d '.' -f 1) # Ensamblar yasm -f elf -g stabs $1 # Si no hay error, linkear el archivo objeto if [ $? -eq 0 ] then ld -o $FILE -m elf_i386 $FILE.o fi
El mismo script, pero para 64 bits (ensamblar64
):
#!/bin/bash # Si no hay parámetros de entrada, imprimir la ayuda if [ $# -lt 1 ] then echo "Uso: $0 <archivo>.asm" exit 1 fi # Eliminar extensión del nombre de archivo pasado como parámetro FILE=$(echo $1 | cut -d '.' -f 1) # Ensamblar yasm -f elf64 -g stabs $1 # Si no hay error, linkear el archivo objeto if [ $? -eq 0 ] then ld -o $FILE $FILE.o fi
Comparación con un lenguaje de alto nivel (C)
Los lenguajes ensambladores son utilizados en la actualidad en ambientes académicos y de investigación cuando se requiere la manipulación directa del hardware, alto rendimiento, o un uso de recursos controlado y reducido. igualmente es utilizado en el desarrollo de controladores de dispositivo (drivers) y en el desarrollo de sistemas operativos, debido a la necesidad del acceso directo a las instrucciones de la máquina para implementar secciones críticas. Muchos dispositivos programables (como los microcontroladores) aún cuentan con el ensamblador como la única manera de ser manipulados.
Al hacer un uso mínimo y eficiente de las instrucciones máquina, los programas escritos en lenguaje ensamblador resultan más eficientes y reducidos que aquellos escritos en lenguajes de alto nivel. Veamos un ejemplo comparando el mismo programa, pero escrito en lenguaje C:
emi@hal9000:~/yasm$ nano holac.c
Como es de esperarse, el código resulta mucho más reducido:
#include <stdio.h> int main(int argc, char** argv) { printf("¡Hola Mundo!\n"); }
Compilar el programa con gcc
:
emi@hal9000:~/yasm$ gcc -o holac -Wall holac.c
Comparar el tamaño del ejecutable en C versus la versión de ensamblador para x86 incluyendo los símbolos de debug:
emi@hal9000:~/yasm$ ll hola holac -rwxr-xr-x 1 emi emi 9064 Nov 21 11:43 hola -rwxr-xr-x 1 emi emi 16608 Nov 21 11:43 holac
Si se ensambla sin símbolos de debug, el tamaño se reduce aún más:
emi@hal9000:~/yasm$ yasm -f elf64 hola.asm emi@hal9000:~/yasm$ ld -o hola hola.o emi@hal9000:~/yasm$ ll hola holac -rwxr-xr-x 1 emi emi 8992 Nov 21 12:34 hola -rwxr-xr-x 1 emi emi 16608 Nov 21 11:43 holac
Ahora, la misma comparación eliminando todos los símbolos de los ejecutables con strip
:
emi@hal9000:~/yasm$ strip hola holac emi@hal9000:~/yasm$ ll hola holac -rwxr-xr-x 1 emi emi 8568 Nov 21 12:36 hola -rwxr-xr-x 1 emi emi 14408 Nov 21 12:36 holac
Por último, quisiera compartir mi propia versión de la herramienta cat
escrita para ensamblar con Yasm, con el objetivo de comprender la complejidad que acarrea desarrollar programas en lenguaje ensamblador.
acat.asm
:
; Mi implementación del comando 'cat' en lenguaje ensamblador %define STDIN 0 %define STDOUT 1 %define STDERR 2 %define sys_exit 1 %define sys_read 3 %define sys_write 4 %define sys_open 5 %define O_RDONLY 0 %define bfsize 256 ; tamaño del buffer a utilizar section .data fdarchivo dd 0x0 ; descriptor de archivo section .bss buffer resb 256 ; reservo un buffer de 256 bytes section .text global _start _start: ; Obtener los parámetros pop EAX ; argc cmp EAX,1 ; argc < 2 ? je salirerr ; fin pop EAX ; arg[0] - nombre del programa pop EBX ; arg[1] - nombre de archivo ; ahora EBX contiene un puntero al string que contiene el nombre ; de archivo pasado como parámetro ; Abrir el archivo mov EAX,sys_open ; abrir archivo ;mov EBX,archivo ; descriptor de archivo a abrir mov ECX,O_RDONLY ; modo de sólo lectura int 0x80 ; llamada al sistema cmp EAX,0 ; verifico si EAX < 0 (error) jl salirerr ; hubo un error ; ahora EAX contiene el descriptor de archivo mov [fdarchivo],EAX ; guardo el descriptor de archivo en memoria leer: ; comienzo a leer el archivo, de a bfsize bytes por vez mov EAX, sys_read ; leer archivo mov EBX, [fdarchivo] ; descriptor de archivo a leer mov ECX, buffer ; buffer de lectura mov EDX, bfsize ; leer bfsize bytes int 0x80 ; llamada al sistema cmp EAX,0 ; verifico si EAX < 0 (error) jl salirerr ; hubo un error ; ahora EAX contiene la cantidad de bytes leídos mov EDX,EAX ; guardo la cantidad de bytes leídos en EDX ; imprimo por pantalla lo leído desde el archivo mov EAX,sys_write ; escribir archivo mov EBX,STDOUT ; descriptor de archivo a escribir mov ECX,buffer ; puntero al buffer ;EDX ya contiene la cantidad de bytes a imprimir int 0x80 ; llamada al sistema ; verifico si hubo error cmp EAX,0 ; comparo para ver si EAX < 0 jl salirerr ; salgo con el error cargado en EBX ; ahora ya imprimí por pantalla lo leído del archivo cmp EDX,bfsize ; comparo la cantidad de bytes leídos con bfsize je leer ; leyo bfsize? seguir leyendo, sino fin salirok: ; salir mov EAX,sys_exit ; salir mov EBX,0 ; no hubo errores int 0x80 ; llamada al sistema salirerr: ; salir mov EBX,EAX ; cargo el error que haya en EAX mov EAX,sys_exit ; salir int 0x80 ; llamada al sistema
Este programa toma un nombre de archivo como parámetro, lo abre en modo de sólo lectura, lee su contenido de a 256 bytes por vez, al tiempo en que lo muestra por pantalla.
Referencias
- The Yasm Modular Assembler Project
- x86 Assembly Guide
- Linux Assembly Tutorial - Step-by-Step Guide
- NASM Tutorial
- List of Linux/i386 system calls
- Linux Syscall Reference
man yasm
man ld
man file
man mimetype
man gcc
man strip