Primeros pasos con el lenguaje ensamblador Intel x86 y la microarquitectura

x86 Linux Hola Mundo Ejemplo

Este es un programa básico de Hello World en ensamblaje NASM para Linux x86 de 32 bits, que usa llamadas al sistema directamente (sin ninguna llamada a la función libc). Es mucho para asimilar, pero con el tiempo se volverá comprensible. Las líneas que comienzan con un punto y coma (;) son comentarios.

Si aún no conoce la programación de sistemas Unix de bajo nivel, es posible que desee simplemente escribir funciones en asm y llamarlas desde programas C o C ++. Luego, puede preocuparse por aprender a manejar los registros y la memoria, sin aprender también la API de llamada al sistema POSIX y la ABI para usarla.


Esto hace dos llamadas al sistema: write(2) y _exit(2) (no el envoltorio libc exit(3) que vacía los búferes stdio y pronto). (Técnicamente, _exit() llama a sys_exit_group, no a sys_exit, pero eso solo importa en un proceso de subprocesos múltiples.) Consulte también syscalls(2) para obtener documentación sobre las llamadas al sistema en general, y la diferencia entre hacerlos directamente y usar las funciones de envoltura de libc.

En resumen, las llamadas al sistema se realizan colocando los argumentos en los registros apropiados y el número de llamada al sistema en eax, luego ejecutando una instrucción int 0x80. Consulte también [¿Cuáles son los valores de retorno de las llamadas al sistema en Assembly?] 6 para obtener más explicaciones sobre cómo se documenta la interfaz asm syscall principalmente con sintaxis C.

Los números de llamadas al sistema para la ABI de 32 bits están en /usr/include/i386-linux-gnu/asm/unistd_32.h (mismo contenido en `/usr/include/x86_64-linux-gnu/asm/unistd_32. h’).

#include <sys/syscall.h> finalmente incluirá el archivo correcto, por lo que puede ejecutar echo '#include <sys/syscall.h>' | gcc -E - -dM | less para ver las definiciones de macro (ver [esta respuesta para obtener más información sobre cómo encontrar constantes para asm en encabezados C] 7)


section .text             ; Executable code goes in the .text section
global _start             ; The linker looks for this symbol to set the process entry point, so execution start here
;;;a name followed by a colon defines a symbol.  The global _start directive modifies it so it's a global symbol, not just one that we can CALL or JMP to from inside the asm.
;;; note that _start isn't really a "function".  You can't return from it, and the kernel passes argc, argv, and env differently than main() would expect.
 _start:
    ;;; write(1, msg, len);
    ; Start by moving the arguments into registers, where the kernel will look for them
    mov     edx,len       ; 3rd arg goes in edx: buffer length
    mov     ecx,msg       ; 2nd arg goes in ecx: pointer to the buffer
    ;Set output to stdout (goes to your terminal, or wherever you redirect or pipe)
    mov     ebx,1         ; 1st arg goes in ebx: Unix file descriptor. 1 = stdout, which is normally connected to the terminal.

    mov     eax,4         ; system call number (from SYS_write / __NR_write from unistd_32.h).
    int     0x80          ; generate an interrupt, activating the kernel's system-call handling code.  64-bit code uses a different instruction, different registers, and different call numbers.
    ;; eax = return value, all other registers unchanged.

    ;;;Second, exit the process.  There's nothing to return to, so we can't use a ret instruction (like we could if this was main() or any function with a caller)
    ;;; If we don't exit, execution continues into whatever bytes are next in the memory page,
    ;;; typically leading to a segmentation fault because the padding 00 00 decodes to  add [eax],al.

    ;;; _exit(0);
    xor     ebx,ebx       ; first arg = exit status = 0.  (will be truncated to 8 bits).  Zeroing registers is a special case on x86, and mov ebx,0 would be less efficient.
                      ;; leaving out the zeroing of ebx would mean we exit(1), i.e. with an error status, since ebx still holds 1 from earlier.
    mov     eax,1         ; put __NR_exit into eax
    int     0x80          ;Execute the Linux function

section     .rodata       ; Section for read-only constants

             ;; msg is a label, and in this context doesn't need to be msg:.  It could be on a separate line.
             ;; db = Data Bytes: assemble some literal bytes into the output file.
msg     db  'Hello, world!',0xa     ; ASCII string constant plus a newline (0x10)

             ;;  No terminating zero byte is needed, because we're using write(), which takes a buffer + length instead of an implicit-length string.
             ;; To make this a C string that we could pass to puts or strlen, we'd need a terminating 0 byte. (e.g. "...", 0x10, 0)

len     equ $ - msg       ; Define an assemble-time constant (not stored by itself in the output file, but will appear as an immediate operand in insns that use it)
                          ; Calculate len = string length.  subtract the address of the start
                          ; of the string from the current position ($)
  ;; equivalently, we could have put a str_end: label after the string and done   len equ str_end - str

En Linux, puede guardar este archivo como Hello.asm y crear un ejecutable de 32 bits con estos comandos:

nasm -felf32 Hello.asm                  # assemble as 32-bit code.  Add -Worphan-labels -g -Fdwarf  for debug symbols and warnings
gcc -nostdlib -m32 Hello.o -o Hello     # link without CRT startup code or libc, making a static binary

Consulte esta respuesta para obtener más detalles sobre la creación de ensamblaje en ejecutables de Linux vinculados dinámicamente o estáticos de 32 o 64 bits, para la sintaxis NASM/YASM o la sintaxis GNU AT&T con directivas GNU as. (Punto clave: asegúrese de usar -m32 o equivalente al compilar código de 32 bits en un host de 64 bits, o tendrá problemas confusos en tiempo de ejecución).

Puedes rastrear su ejecución con strace para ver las llamadas al sistema que hace:

$ strace ./Hello 
execve("./Hello", ["./Hello"], [/* 72 vars */]) = 0
[ Process PID=4019 runs in 32 bit mode. ]
write(1, "Hello, world!\n", 14Hello, world!
)         = 14
_exit(0)                                = ?
+++ exited with 0 +++

El seguimiento en stderr y la salida regular en stdout van a la terminal aquí, por lo que interfieren en la línea con la llamada al sistema write. Redirigir o rastrear a un archivo si te importa. Observe cómo esto nos permite ver fácilmente los valores de retorno de syscall sin tener que agregar código para imprimirlos, y en realidad es incluso más fácil que usar un depurador normal (como gdb) para esto.

La versión x86-64 de este programa sería extremadamente similar, pasando los mismos argumentos a las mismas llamadas al sistema, solo que en diferentes registros. Y usando la instrucción syscall en lugar de int 0x80.

Lenguaje ensamblador x86

La familia de lenguajes ensambladores x86 representa décadas de avances en la arquitectura Intel 8086 original. Además de haber varios dialectos diferentes basados ​​en el ensamblador utilizado, a lo largo de los años se han agregado instrucciones de procesador adicionales, registros y otras características, sin dejar de ser compatible con el ensamblador de 16 bits utilizado en la década de 1980.

El primer paso para trabajar con ensamblaje x86 es determinar cuál es el objetivo. Si está buscando escribir código dentro de un sistema operativo, por ejemplo, querrá determinar adicionalmente si elegirá usar un ensamblador independiente o características de ensamblador en línea integradas de un lenguaje de nivel superior como C. Si desea codificar en el “metal desnudo” sin un sistema operativo, simplemente necesita instalar el ensamblador de su elección y comprender cómo crear un código binario que se puede convertir en memoria flash, imagen de arranque o cargarse en la memoria en el ubicación apropiada para comenzar la ejecución.

Un ensamblador muy popular que es compatible con varias plataformas es NASM (Netwide Assembler), que se puede obtener en http://nasm.us/. En el sitio de NASM, puede descargar la versión más reciente para su plataforma.

Ventanas

Las versiones de NASM de 32 y 64 bits están disponibles para Windows. NASM viene con un instalador conveniente que se puede usar en su host de Windows para instalar el ensamblador automáticamente.

Linux

Bien puede ser que NASM ya esté instalado en su versión de Linux. Para verificar, ejecute:

nasm -v

Si no se encuentra el comando, deberá realizar una instalación. A menos que esté haciendo algo que requiera funciones innovadoras de NASM, el mejor camino es usar su herramienta de administración de paquetes integrada para su distribución de Linux para instalar NASM. Por ejemplo, en sistemas derivados de Debian como Ubuntu y otros, ejecute lo siguiente desde un símbolo del sistema:

sudo apt-get install nasm

Para sistemas basados ​​en RPM, puede probar:

sudo yum install nasm

Mac OS X

Las versiones recientes de OS X (incluidos Yosemite y El Capitan) vienen con una versión anterior de NASM preinstalada. Por ejemplo, El Capitán tiene instalada la versión 0.98.40. Si bien esto probablemente funcionará para casi todos los propósitos normales, en realidad es bastante antiguo. En el momento de escribir este artículo, se publica la versión 2.11 de NASM y la 2.12 tiene varias versiones candidatas disponibles.

Puede obtener el código fuente de NASM del enlace anterior, pero a menos que tenga una necesidad específica de instalar desde la fuente, es mucho más sencillo descargar el paquete binario del directorio de versiones de OS X y descomprimirlo.

Una vez descomprimido, se recomienda enfáticamente que no sobrescriba la versión de NASM instalada en el sistema. En su lugar, puede instalarlo en /usr/local:

 $ sudo su
 <user's password entered to become root>
 # cd /usr/local/bin
 # cp <path/to/unzipped/nasm/files/nasm> ./
 # exit

En este punto, NASM está en /usr/local/bin, pero no está en su camino. Ahora debe agregar la siguiente línea al final de su perfil:

 $ echo 'export PATH=/usr/local/bin:$PATH' >> ~/.bash_profile

Esto antepondrá /usr/local/bin a su ruta. Ejecutar nasm -v en el símbolo del sistema ahora debería mostrar la versión correcta y más nueva.