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:
- 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.
- Compilador: en el segundo paso, el compilador traduce programas en lenguaje de alto nivel a programas en lenguaje ensamblador.
- 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.
- 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.