Convertir un programa C en ensamblador

Abdul Mateen 12 octubre 2023
  1. El lenguaje ensamblador
  2. El lenguaje C
  3. Convertir un programa C a lenguaje ensamblador
Convertir un programa C en ensamblador

Este tutorial discutirá cómo convertir un programa en lenguaje C en código de lenguaje ensamblador.

Discutiremos brevemente los fundamentos de los lenguajes ensamblador y C. Posteriormente, veremos la conversión del programa C a código ensamblador y el desensamblado de un código ensamblador.

El lenguaje ensamblador

El ensamblador es un lenguaje interpretado de bajo nivel. Generalmente, una declaración escrita en lenguaje ensamblador se traduce en una sola instrucción a nivel de máquina.

Sin embargo, es mucho más legible que el lenguaje de máquina porque usa mnemónicos. Los mnemotécnicos son instrucciones similares al inglés o códigos de operación.

Por ejemplo, el mnemotécnico ADD se usa para sumar dos números. Del mismo modo, MOV se utiliza para realizar movimientos de datos.

Asimismo, CMP compara dos expresiones y JMP salta el control de ejecución a alguna etiqueta específica o marcador de ubicación.

El lenguaje ensamblador está muy cerca de la máquina (hardware); por lo tanto, las instrucciones escritas en lenguaje ensamblador son muy rápidas. Sin embargo, el programador necesita tener mucho más conocimiento de hardware que un desarrollador de un lenguaje de alto nivel.

El lenguaje ensamblador generalmente se usa para escribir programas de sistema eficientes como controladores de dispositivos, programas de virus/antivirus, software de sistema incorporado y TSR (programas residentes terminados y permanentes).

Un ensamblador debe ensamblar un programa de lenguaje ensamblador en un programa de lenguaje de máquina ejecutable en la máquina.

El lenguaje C

C es un lenguaje de programación de alto nivel independiente de la máquina. Por lo general, los programas en C no requieren conocimientos de hardware (solo se requiere un poco de conocimiento).

C tiene declaraciones de alto nivel y requiere un programa compilador que traduzca cada declaración del lenguaje C en una o varias declaraciones en lenguaje ensamblador. Por ejemplo, una simple instrucción en lenguaje C, c = a + b, se traduce a las siguientes sentencias en lenguaje ensamblador:

mov edx, DWORD PTR - 12 [rbp] mov eax, DWORD PTR - 8 [rbp] add eax,
    edx mov DWORD PTR - 4 [rbp], eax

Aquí, en la primera y segunda declaración, el valor de las variables de la memoria se mueve a los registros. La instrucción add está sumando dos valores de registro.

En la cuarta declaración, el valor del registro se mueve a una variable en la memoria.

Además, el compilador tiene que hacer mucho trabajo, pero la vida del programador es simple trabajando en lenguaje C. El lenguaje C tiene un amplio espectro de aplicaciones, desde aplicaciones comerciales de alto nivel hasta programas de utilidad de bajo nivel.

Convertir un programa C a lenguaje ensamblador

Por lo general, las personas usan el entorno integrado sofisticado para escribir, editar, compilar, ejecutar, modificar y depurar programas en lenguaje C o el comando gcc para convertir el programa en lenguaje C en programas ejecutables.

Estas herramientas mantienen a los usuarios inconscientes de los pasos necesarios para convertir un código fuente escrito en algún lenguaje de alto nivel como C en un código ejecutable por máquina. Por lo general, los siguientes pasos se realizan en el medio:

  1. Preprocesamiento: un programa de preprocesador realiza tres tareas. La primera tarea es incluir archivos de encabezado, la segunda tarea es reemplazar macros y la tercera tarea es eliminar comentarios del programa fuente.
  2. Compilador: en el segundo paso, el compilador traduce programas en lenguaje de alto nivel a programas en lenguaje ensamblador.
  3. Ensamblador: en el tercer paso, el programa ensamblador toma un programa en lenguaje ensamblador (traducido por el compilador) y lo ensambla en una forma ejecutable por máquina llamada código objeto.
  4. Vinculador: en el cuarto paso, un programa vinculador adjunta archivos de biblioteca compilados con el código objeto para ejecutar este programa de forma independiente.

Comandos para convertir código C en un equivalente de ensamblado

Por lo general, los usuarios de la línea de comandos escriben gcc program_name.c, que genera un archivo ejecutable (en caso de que no haya errores). Si no se proporciona el nombre del archivo de destino, está disponible con a.out en la familia de sistemas operativos UNIX o program_name.exe en el sistema operativo Windows.

Sin embargo, el comando gcc cuenta con una amplia lista de parámetros para realizar tareas específicas. Este tutorial discutirá solo las banderas -s y -C.

La bandera -S genera un programa en lenguaje ensamblador a partir del código fuente C. Entendamos esta bandera usando el siguiente ejemplo donde tenemos test.c como archivo fuente:

// test.c
int main() {
  int a = 2, b = 3, c;
  c = a + b;
  return 0;
}

El siguiente comando generará el código de lenguaje ensamblador de destino con la extensión .S:

$ gcc -S test.c
$ ls
test.c test.s

El comando no ha creado código de lenguaje de máquina; solo se genera el código de lenguaje ensamblador. Mostremos el contenido de este código ensamblado generado usando el comando cat en Bash:

$ cat test.s
    .file   "Test.c"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $2, -12(%rbp)
    movl    $3, -8(%rbp)
    movl    -12(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    movl    %eax, -4(%rbp)
    ...

El código ensamblado generado puede no ser familiar para muchos programadores que tienen experiencia escribiendo códigos ensamblados para la arquitectura Intel x86.

Si queremos el código ensamblador de destino para las arquitecturas Intel x86, el siguiente comando lo hará por nosotros:

$ gcc -S -masm=intel  Test.c

Nuevamente, la salida se generará en el archivo Test.s, que se puede ver usando el comando cat en la terminal Bash. En Windows, podemos abrirlo en algún editor como el Bloc de notas o un editor mejor.

De todos modos, veamos el contenido del código ensamblador generado por el comando anterior:

 cat Test.s
    .file   "Test.c"
    .intel_syntax noprefix
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    push    rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    mov rbp, rsp
    .cfi_def_cfa_register 6
    mov DWORD PTR -12[rbp], 2
    mov DWORD PTR -8[rbp], 3
    mov edx, DWORD PTR -12[rbp]
    mov eax, DWORD PTR -8[rbp]
    add eax, edx
    mov DWORD PTR -4[rbp], eax
    ...

La salida es ligeramente diferente; los comandos mov y add son muy claros.

Desensamblar un código de objeto

Además de convertir un programa en lenguaje C a lenguaje ensamblador, es posible que desee desensamblar el código binario (código de máquina) para ver el código en lenguaje ensamblador equivalente. Podemos usar la utilidad objdump en Linux para hacer eso.

Ejemplo:

Supongamos que ejecutamos el comando gcc -c Test.c para compilar el archivo Test.c en una terminal Bash. Crea un archivo objeto (código en lenguaje máquina) con el nombre Test.o.

Ahora, si queremos volver a convertir/desensamblar este código objeto al código ensamblador equivalente, podemos hacerlo usando el siguiente comando Bash:

$ objdump -d Test.o

Test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   %rbp
   5   48 89 e5                 mov    %rsp,%rbp
   8:   c7 45 f4 02 00 00 00    movl   $0x2,-0xc(%rbp)
   f:   c7 45 f8 03 00 00 00    movl   $0x3,-0x8(%rbp)
  16:   8b 55 f4                mov    -0xc(%rbp),%edx
  19:   8b 45 f8                mov    -0x8(%rbp),%eax
  1c:   01 d0                   add    %edx,%eax
  1e:   89 45 fc                mov    %eax,-0x4(%rbp)
  21:   b8 00 00 00 00          mov    $0x0,%eax
  26:   5d                      pop    %rbp

En esta salida, el código de la izquierda es el código binario en hexadecimal. En el lado derecho, se ve el código en lenguaje ensamblador en forma legible.