Programando en ensamblador

2022/01/20

La última fué la semana de refrescar ensamblador revisando https://asmtutor.com/ luego de llegar allí mediante los posts Learn Assembly y Assembly Nights.

Sobre el sitio https://asmtutor.com/:

Learn Assembly Language

This project was put together to teach myself NASM x86 assembly language on linux.

Leyendo por arriba, dos detalles:

  1. El sitio es sobre (la arquitectura) x86 y allí se utiliza la int 80h para llamar a las funciones del kernel linux.

    Si se está más interesado en la arquitectura amd64 se encuentra que existen varias formas para llamar al código del kernel, siendo la preferida el uso de la instrucción syscall (man2 syscall).

    Teniendo esto en cuentra, el código de ejemplo helloworld64.asm (adaptado de helloworld.asm) nos quedaría como sigue:

    _start:
        mov     rax, 1      ; invoke SYS_WRITE
        mov     rdi, 1      ; write to the STDOUT file (file descriptor)
        mov     rsi, msg    ; move the memory address of our message string into ecx
        mov     rdx, 13     ; number of bytes to write - one for each letter plus 0Ah (line feed character)
        syscall
    

    Los números de las llamadas al sistema cambian respecto de x86. Los números de cada syscall para amd64 se pueden encontrar en el código del kernel y hay posts como Linux System Call Table for x86 64 en los que se muestra una tabla con las system calls y la utilización que se le da a los registros.

  2. Si bien hay ejemplos en los que se utilizan loops y se introducen subrutinas no se indica como pasar construcciones de pseudo-código tales como expresiones o control de flujo a ensamblador. Esto solo indica que el autor da por sentado cierta familiaridad con ensamblador x86.

    Si no se tiene familiaridad con ensamblador sugiero leer los capítulos de generación de código de libros de diseño de compiladores tales como Engineering a Compiler o Compilers: Principles, Techniques, and Tools ya que la mayoría de los libros de ensamblador que he visto no tratan en profundidad como transformar sentencias condicionales o de bucle escritas en pseudo-código a código ensamblador.

    Como complemento a lo anterior está el sitio Compiler Explorer, que permite introducir código en C o C++ y convertirlo en código ensamblador utilizando distintos compiladores.

  3. Se utiliza la sintaxis de intel (nasm). Para un buen comentario de porqué conviene utilizar la sintaxis de intel sobre la de at&t se puede ver el artículo Why no one should use the AT&T syntax ever, for any reason, under any circumstances.

Redoblando la apuesta - ensamblador de risc-v

Ya que la mayoría de las lecciones implican utilizar llamadas al kernel, ¿porqué no intentar hacer un programa de risc-v y dejar que qemu realice la emulación en modo usuario?.

Nuevamente, vuelve a cambiar la forma de llamar al kernel como se indica en la página del manual (2) syscall:

Arch/ABI    Instruction           System  Ret  Ret  Error    Notes
                                  call #  val  val2
───────────────────────────────────────────────────────────────────
riscv       ecall                 a7      a0   a1   -

Arch/ABI      arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
──────────────────────────────────────────────────────────────
riscv         a0    a1    a2    a3    a4    a5    -

El número de instrucción a utilizar se debe obtener del archivo include/uapi/asm-generic/unistd.h como se indica en Adding a New System Call:

Some architectures (e.g. x86) have their own architecture-specific syscall tables, but several other architectures share a generic syscall table. Add your new system call to the generic list by adding an entry to the list in include/uapi/asm-generic/unistd.h

Para ver un programa de ejemplo se implementa el programa helloworldrv64.s pero ahora en risc-v:

# Hello World Program
# Compile with: riscv64-linux-gnu-as helloworldrv64.s -o helloworldrv64.o
# Link with:    riscv64-linux-gnu-ld helloworldrv64.o -o helloworldrv64
# Run with:     qemu-riscv64-static ./helloworldrv64

        .data
msg:    .asciz  "Hello World!\n"        # assign msg variable with your message string

        .text
        .global _start

_start:
        li        a7, 64        # invoke SYS_WRITE
        li        a0, 1         # write to the STDOUT file (file descriptor)
        la        a1, msg       # move the memory address of our message string
        li        a2, 13        # number of bytes to write - one for each character
        ecall

        li        a7, 93        # invoke SYS_EXIT
        li        a0, 0         # return 0 status on exit - 'No Errors'
        ecall

end:
        j end

Algunos links sobre ensamblador de risc-v:

Para una alternativa a utilizar modo de usuario realizando la emulación del hardware puede verse https://mth.st/blog/riscv-qemu/.

Conclusiones

¿Porqué ensamblador risc-v? Creo que es simple, limpio, tiene muy pocas instrucciones y seguro va a ser cada vez más relevante. Utilizar intel o arm no me estimula por la cantidad de instrucciones que tienen (intel) o lo denso del conjunto de instrucciones (arm).

Tampoco me convence el realizar como hobbie programas en Z80, 6502 u otra arquitectura que está volviendo de a ser de moda (¿retro?) porque no creo que sea un conocimiento que pueda capitalizar a futuro o quizás estar pensando en como programar una PPU de nintendo o los gráficos de amiga.

Apéndice - Instalación de programas

Para instalar los programas utilizados en este post, en debian, se puede utilizar:

$ doas apt-get install -Vy nasm binutils-riscv64-linux-gnu qemu-user-static