Primeros pasos con el lenguaje ensamblador

Introducción

El lenguaje ensamblador es una forma legible por humanos de lenguaje de máquina o código de máquina que es la secuencia real de bits y bytes en los que opera la lógica del procesador. Por lo general, es más fácil para los humanos leer y programar en mnemónicos que en binario, octal o hexadecimal, por lo que los humanos generalmente escriben código en lenguaje ensamblador y luego usan uno o más programas para convertirlo al formato de lenguaje de máquina que entiende el procesador.

EJEMPLO:

mov eax, 4
cmp eax, 5
je point

Un ensamblador es un programa que lee el programa en lenguaje ensamblador, lo analiza y produce el lenguaje de máquina correspondiente. Es importante comprender que, a diferencia de un lenguaje como C++, que es un lenguaje único definido en un documento estándar, existen muchos lenguajes ensambladores diferentes. Cada arquitectura de procesador, ARM, MIPS, x86, etc. tiene un código de máquina diferente y, por lo tanto, un lenguaje ensamblador diferente. Además, a veces hay varios lenguajes ensambladores diferentes para la misma arquitectura de procesador. En particular, la familia de procesadores x86 tiene dos formatos populares que a menudo se denominan sintaxis de gas (gas es el nombre del ejecutable para GNU Assembler) y sintaxis de Intel (llamado así por el creador de la familia de procesadores x86). Son diferentes pero equivalentes en el sentido de que normalmente se puede escribir cualquier programa en cualquiera de las dos sintaxis.

Generalmente, el inventor del procesador documenta el procesador y su código de máquina y crea un lenguaje ensamblador. Es común que ese lenguaje ensamblador en particular sea el único que se use, pero a diferencia de los escritores de compiladores que intentan ajustarse a un estándar de lenguaje, el lenguaje ensamblador definido por el inventor del procesador suele ser, aunque no siempre, la versión que usan las personas que escriben ensambladores. .

Hay dos tipos generales de procesadores:

  • CISC (computadora de conjunto de instrucciones complejas): tiene muchas instrucciones de lenguaje de máquina diferentes y, a menudo, complejas

  • RISC (Reduced Instruction set Computers): por el contrario, tiene menos instrucciones y más sencillas

Para un programador de lenguaje ensamblador, la diferencia es que un procesador CISC puede tener muchas instrucciones para aprender, pero a menudo hay instrucciones adecuadas para una tarea en particular, mientras que los procesadores RISC tienen menos instrucciones y son más simples, pero cualquier operación dada puede requerir el programador de lenguaje ensamblador. escribir más instrucciones para hacer lo mismo.

Los compiladores de otros lenguajes de programación a veces producen ensamblador primero, que luego se compila en código de máquina llamando a un ensamblador. Por ejemplo, gcc usando su propio ensamblador gas en la etapa final de compilación. El código de máquina producido a menudo se almacena en archivos objeto, que el programa enlazador puede vincular a un ejecutable.

Una “cadena de herramientas” completa a menudo consta de un compilador, un ensamblador y un enlazador. Entonces uno puede usar ese ensamblador y enlazador directamente para escribir programas en lenguaje ensamblador. En el mundo GNU, el paquete binutils contiene el ensamblador y el enlazador y las herramientas relacionadas; aquellos que estén únicamente interesados ​​en la programación en lenguaje ensamblador no necesitan gcc u otros paquetes compiladores.

Los microcontroladores pequeños a menudo se programan únicamente en lenguaje ensamblador o en una combinación de lenguaje ensamblador y uno o más lenguajes de nivel superior, como C o C++. Esto se hace porque a menudo se pueden usar los aspectos particulares de la arquitectura del conjunto de instrucciones para que dichos dispositivos escriban un código más compacto y eficiente de lo que sería posible en un lenguaje de nivel superior y dichos dispositivos a menudo tienen memoria y registros limitados. Muchos microprocesadores se utilizan en sistemas integrados, que son dispositivos que no son computadoras de propósito general que tienen un microprocesador en su interior. Ejemplos de estos sistemas integrados son los televisores, los hornos de microondas y la unidad de control del motor de un automóvil moderno. Muchos de estos dispositivos no tienen teclado ni pantalla, por lo que un programador generalmente escribe el programa en una computadora de uso general, ejecuta un ensamblador cruzado (llamado así porque este tipo de ensamblador produce código para un tipo de procesador diferente al que en el que se ejecuta) y/o un compilador cruzado y un enlazador cruzado para producir código de máquina.

Hay muchos proveedores de tales herramientas, que son tan variados como los procesadores para los que producen el código. Muchos, pero no todos los procesadores también tienen una solución de código abierto como GNU, sdcc, llvm u otros.

Hola mundo para Linux x86_64 (Intel 64 bit)

section .data
    msg db "Hello world!",10      ; 10 is the ASCII code for a new line (LF)

section .text
    global _start

_start:
    mov rax, 1
    mov rdi, 1
    mov rsi, msg
    mov rdx, 13
    syscall
    
    mov rax, 60
    mov rdi, 0
    syscall

Si desea ejecutar este programa, primero necesita el Netwide Assembler, nasm, porque este código usa su sintaxis. Luego use los siguientes comandos (asumiendo que el código está en el archivo helloworld.asm). Son necesarios para ensamblar, enlazar y ejecutar, respectivamente.

  • nasm -felf64 holamundo.asm
  • ld holamundo.o -o holamundo
  • ./holamundo

El código hace uso de la llamada al sistema sys_write de Linux. Aquí puede ver una lista de todas las llamadas al sistema para x86_64 arquitectura. Cuando también tomas las páginas man de write y exit en cuenta, puede traducir el programa anterior a uno C que hace lo mismo y es mucho más legible:

#include <unistd.h>

#define STDOUT 1

int main()
{
    write(STDOUT, "Hello world!\n", 13);
    _exit(0);
}

Solo se necesitan dos comandos aquí para compilar y vincular (el primero) y ejecutar:

  • gcc holamundo_c.c -o holamundo_c.
  • ./holamundo_c

Codigo de maquina

El código de máquina es un término para los datos en un formato de máquina nativo particular, que son procesados ​​directamente por la máquina, generalmente por el procesador llamado CPU (Unidad central de procesamiento).

La arquitectura informática común (arquitectura de von Neumann) consta de un procesador de uso general (CPU), una memoria de uso general, que almacena tanto el programa (ROM/RAM) como los datos procesados ​​y dispositivos de entrada y salida (dispositivos I/O).

La principal ventaja de esta arquitectura es la relativa simplicidad y universalidad de cada uno de los componentes, en comparación con las máquinas informáticas anteriores (con un programa cableado en la construcción de la máquina), o arquitecturas competidoras (por ejemplo, la arquitectura de Harvard que separa la memoria del programa de la memoria del datos). La desventaja es un poco peor rendimiento general. A largo plazo, la universalidad permitió un uso flexible, que generalmente superó el costo de rendimiento.

¿Cómo se relaciona esto con el código máquina?

El programa y los datos se almacenan en estas computadoras como números, en la memoria. No existe una forma genuina de diferenciar el código de los datos, por lo que los sistemas operativos y los operadores de la máquina dan pistas a la CPU, en qué punto de entrada de la memoria inicia el programa, después de cargar todos los números en la memoria. Luego, la CPU lee la instrucción (número) almacenada en el punto de entrada y la procesa rigurosamente, leyendo secuencialmente los siguientes números como instrucciones adicionales, a menos que el propio programa le indique a la CPU que continúe con la ejecución en otro lugar.

Por ejemplo, dos números de 8 bits (8 bits agrupados equivalen a 1 byte, que es un número entero sin signo dentro del rango 0-255): 60 201, cuando se ejecuta como código en la CPU Zilog Z80 se procesará como dos instrucciones: INC a (aumentando el valor en el registro a en uno) y RET (regresando de la subrutina, apuntando a la CPU para ejecutar instrucciones desde otra parte de la memoria).

Para definir este programa, un humano puede ingresar esos números mediante algún editor de memoria/archivo, por ejemplo, en el editor hexadecimal como dos bytes: 3C C9 (números decimales 60 y 201 escritos en codificación base 16). Eso sería programar en código máquina.

Para hacer que la tarea de programación de la CPU sea más fácil para los humanos, se crearon programas Ensambladores, capaces de leer archivos de texto que contienen algo como:

subroutineIncrementA:
    INC   a
    RET

dataValueDefinedInAssemblerSource:
    DB    60          ; define byte with value 60 right after the ret

salida de la secuencia de números hexadecimales de bytes 3C C9 3C, envuelta con números adicionales opcionales específicos para la plataforma de destino: marcar qué parte de dicho binario es código ejecutable, dónde está el punto de entrada para el programa (la primera instrucción del mismo), qué partes son datos codificados (no ejecutables), etc.

Observe cómo el programador especificó el último byte con el valor 60 como “datos”, pero desde la perspectiva de la CPU no difiere de ninguna manera del byte INC a. Depende del programa en ejecución navegar correctamente por la CPU a través de los bytes preparados como instrucciones y procesar los bytes de datos solo como datos para las instrucciones.

Dicha salida generalmente se almacena en un archivo en el dispositivo de almacenamiento, el sistema operativo (Sistema operativo: un código de máquina que ya se ejecuta en la computadora, que ayuda a manipular con la computadora) lo carga más tarde en la memoria antes de ejecutarlo, y finalmente apunta la CPU en el punto de entrada del programa.

La CPU puede procesar y ejecutar solo código de máquina, pero cualquier contenido de la memoria, incluso uno aleatorio, puede procesarse como tal, aunque el resultado puede ser aleatorio, desde “* bloqueo *” detectado y manejado por el sistema operativo hasta borrado accidental de datos de Dispositivos de E/S, o daño de equipos sensibles conectados a la computadora (no es un caso común para las computadoras domésticas :)).

El proceso similar es seguido por muchos otros lenguajes de programación de alto nivel, compilando la fuente (forma de programa de texto legible por humanos) en números, ya sea representando el código de la máquina (instrucciones nativas de la CPU), o en el caso de interpretado/híbrido idiomas en algún código de máquina virtual específico del idioma general, que se decodifica aún más en código de máquina nativo durante la ejecución por parte del intérprete o la máquina virtual.

Algunos compiladores usan el Ensamblador como etapa intermedia de la compilación, traduciendo la fuente en primer lugar al formato Ensamblador, luego ejecutando la herramienta ensamblador para obtener el código de máquina final (ejemplo de GCC: ejecute gcc -S helloworld.c para obtener una versión en ensamblador del programa C helloworld.c).

Hello World para OS X (x86_64, gas de sintaxis Intel)

.intel_syntax noprefix

.data

.align 16
hello_msg:
    .asciz "Hello, World!"

.text

.global _main
_main:
    push rbp
    mov rbp, rsp

    lea rdi, [rip+hello_msg]
    call _puts

    xor rax, rax
    leave
    ret

Armar:

clang main.s -o hello
./hello

Notas:

Ejecutando ensamblado x86 en Visual Studio 2015

Paso 1: Cree un proyecto vacío a través de Archivo -> Nuevo proyecto.

ingrese la descripción de la imagen aquí

Paso 2: haga clic con el botón derecho en la solución del proyecto y seleccione Crear dependencias->Crear personalizaciones.

Paso 3: Marque la casilla de verificación ".masm".

ingrese la descripción de la imagen aquí

Paso 4: Presione el botón “ok”.

Paso 5: Cree su archivo de ensamblaje y escriba esto:

.386
    .model small
        .code

            public main
                main proc

                    ; Ends program normally

                    ret
                main endp
            end main

Paso 6: ¡Compila!

ingrese la descripción de la imagen aquí