Hola Mundo en Ensamblador
SOLUCIONARIO
Hola Mundo en Ensamblador
2024-09-14
Por
Occam's Razor

Tras haber revisado varios lenguajes de alto nivel es hora de ver que es lo que el ordenador realmente hace cuando le pedimos que muestre un mensaje en pantalla.
    .global _start
    .extern puts
    .extern exit
    .text
    # Esto es un comentario
_start:
    mov  $msg, %rdi     # Podemos poner comentarios en la misma línea
    call puts
    call exit

.section .rodata
msg: .asciz "Hola Mundo GAS y C!!"

El lenguaje ensamblador es el lenguaje de más bajo nivel que podemos utilizar sin escribir código máquina (ceros y unos) directamente. Realmente no se trata de un lenguaje sino de una familia de lenguajes, los cuales son diferentes para cada tipo de plataforma/procesador. En este libro vamos a utilizar la plataforma x86_64, ya que es la más extendida, al menos por el momento. Siempre que sea posible os mostraremos dos versiones de los programas.

La primera usando la librería C estándar y la segunda usando llamadas al sistema, si bien, en este segundo caso tendremos que limitar un poco la funcionalidad de los programas para que no se hagan interminables.

El punto de entrada para los programas ensamblador en GNU/Linux es _start. Las dos funciones que usamos pertenecen a la librería C estándar. puts muestra una cadena de caracteres en consola y exit termina el programa.

SOY NOVATO

La mayoría de lenguajes, compilados o interpretados incluyen automáticamente código para terminar los programas, sin embargo, en ensamblador nosotros tenemos que hacerlo todo. La función exit hace una llamada al sistema operativo para indicarle que el proceso debe terminar. De lo contrario el programa seguiría ejecutándose con cualquiera que fuera el contenido de la memoria a continuación, lo cual normalmente termina con un fallo de segmentación.

Cada sistema operativo define su propio ABI (Application Binary Interface o interfaz binario de aplicación), una serie de reglas y formatos para la ejecución de código binario entre otras cosas. GNU/Linux utiliza el conocido como ABI System V (por el sistema UNIX que lo introdujo). En lo que a nosotros respecta como programadores, la parte del ABI que nos interesa es la que indica como debemos llamar a las funciones y al sistema operativo. Para el ABI System V y plataforma x86_64 esta es la información relevante:

  • RDI, RSI, RDX, RCX, R8 y R9 se utilizan para pasar parámetros a las funciones o system calls. Las reglas se complican un poco más cuando pasamos valores en coma flotante u otros formatos. Todos los detalles están definidos en un documento bastante largo
  • RAX se utiliza como valor de retorno. En el caso de system calls, también se utiliza para indicar el número de la syscall que queremos ejecutar.

Como podéis ver, el programa carga en RDI el puntero a la cadena que queremos imprimir y luego llama a la función.

AHORA SABES PORQUE

Quizás hayas escuchado alguna vez que no es buena práctica definir funciones con muchos parámetros. Como puedes ver, cuando llegamos al nivel del ensamblador, cuantos más parámetros más trabajo tenemos que hacer. Pero lo que es aún peor es que cuando nos quedamos sin registros, los valores se almacenaran en la pila, la cual es mucho más lenta de acceder, añadiendo una penalización extra al tiempo de ejecución de nuestras funciones.

En general, el lenguaje ensamblador hace muy pocas cosas por nosotros. Sin embargo, como contrapartida, nos permite hacer prácticamente lo que queramos.

Para compilar este programa usando el toolchain estándar de GNU debemos usar los siguientes comandos:

$ as -o hello1.o hello1.s
$ ld -I/usr/bin/ld.so -lc -o hello1-asm hello1.o

El primer comando invoca el ensamblador para compilar nuestro código que normalmente almacenaremos en un fichero con extensión .s. Una vez ensamblado, debemos linkarlo para producir un ejecutable y además, en este caso, enlazar la librería C estándar para poder usar las funciones puts y exit.

SOY NOVATO

Toolchain es el nombre genérico que reciben el conjunto de herramientas que son necesarias para poder producir programas para una determinada plataforma. Suele estar formado por los siguientes programas:

addr2line  Convierte direcciones de memoria a números de líneas en código fuente (ADDRess to Line)
ar         Archivador de ficheros. Utilizado para generar librerías estáticas (ARquiver)
as         Ensamblador (Assembler)
c++filt    Decodifica nombres de symbolos C++
cpp        Pre-procesador C (C Pre-Processor)
elfedit    Permite modificar cabecera y propiedades de programas ELF
gcc        El compilador de C (GNU C Compiler)
gcov       Herramienta para pruebas de cobertura (GNU Coverage tool)
gcov-dump  Herramienta para mostrar información sobre cobertura de programas
gprof      Herramienta para análisis de rendimiento (GNU Profiler)
ld         El linker
lto-dump   Utilizada para mostrar ficheros LTO (Link-Time Optimisations) 
nm         Lista símbolos en objetos
objcopy    Manipulación de ficheros objetos
objdump    Muestra información sobre ficheros objeto
ranlib     Genera índices para archivos (librerías estáticas) para acelerar  compilación
readelf    Muestra información sobre ficheros ELF
strings    Muestra cadenas de caracteres en ficheros binarios
size       Tamaño del fichero por secciones de memoria
strip      Elimina partes innecesarias de los ficheros ejecutables

La versión ensamblador usando llamadas al sistema la mostraremos con ensamblador con sintaxis intel y usando nasm como ensamblador. El ensamblador de GNU (gas) utiliza la sintaxis de AT&T por defecto, notad como fuente y destino en las instrucciones cambian de orden. Observad las sutiles diferencias en la forma de definir los elementos del programa en uno y otro caso. El programa sería el siguiente:

    global _start
    
_start:
    ;;  Comentarios comienzan con ;
    mov rax, 1      ; Comentario en la misma lína
    mov rdi, 1
    mov rsi, msg
    mov rdx, size
    syscall

    ;; Exit program
    mov rax, 0x3c ; SYS_exit = 0x3c
    mov rdi, 0

    syscall

msg:
    db 'Hola Mundo nasm!',0x0a  
size:   equ $ - msg

Para implementar el programa con llamadas al sistema debemos utilizar dos de esas llamadas. La llamada SYS_wite que nos permite mostrar mensajes en pantalla si escribimos a la salida estándar (descripto de ficheros número 1), Y la llamada al sistemas SYS_exit, para terminar el programa ordenadamente.

Como podéis ver el ABI es el mismo que para la llamada a funciones con la salvedad de que el registro RAX debe cargarse con el valor de la llamada al sistema que queremos ejecutar.

Para compilar este programa con nasm podemos utilizar los siguientes comandos:

  $ nasm -f elf64 -o hello2.o hello2.asm
  $ ld -o hello2-asm hello2.o

La diferencia principal es que ahora no necesitamos LibC y nuestro programa es mucho más pequeño y no depdende de ninguna librería externa.

A diferencia de otros lenguajes no podemos ofreceros un nombre y una fecha de creación. El lenguaje ensamblador nació con el primer procesador y una nueva versión surge cada vez que un nuevo procesador se desarrolla o mejora.

Resumen

  • Lenguaje Compilado
  • Extensión: asm/s
  • Terminador de líneas : Retorno de carro. Cada línea es una instrucción
  • Agrupa instrucciones con : No existe
  • Comentarios multilinea : No existen
  • Comentarios misma línea: ; o @ o #
  • Delimitador de cadenas: "cadena"
  • No hay forma de escribir texto directamente
  • Soporte cadenas múltilínea: No soportadas.
  • No Soporta HERE-DOCS
  • Punto de Entrada: _start

SOBRE Occam's Razor
Somos una revista libre que intenta explicar de forma sencilla conceptos tecnológicos y científicos de los que no podemos escapar. Al final, todo es más fácil de lo que parece!

 
Tu publicidad aquí :)