Lasaña

Lasaña

Ejercicio de aprendizaje

Introducción

Conceptos básicos

Comentarios

La pista de x86-64 en Exercism usa NASM (The Netwide Assembler) como ensamblador. En NASM, los comentarios se preceden con un punto y coma (;).

Los comentarios pueden colocarse en cualquier parte del programa, y todo lo que viene después de un ; es ignorado por el ensamblador.

Constantes

Una constante en tiempo de ensamblado se puede definir en NASM con equ. Por ejemplo, esto define una constante llamada UNIVERSE con el valor 42:

UNIVERSE equ 42

Las constantes se evalúan una sola vez, cuando se definen, y no se pueden redefinir después.

Registros de propósito general (GPRs)

En ensamblador, en lugar de variables, normalmente usamos registros para almacenar valores. Un registro de CPU es una porción de memoria rápida dentro del procesador de una computadora. La mayoría de los cálculos se realizan en los registros de la CPU.

Algunos de los registros se usan para una variedad de cálculos distintos y se llaman registros de propósito general (GPRs). Otros tienen propósitos especiales o dedicados.

En x86-64 hay 16 registros de propósito general (GPRs) de 64 bits, a los que también se puede acceder como 32 bits, 16 bits u 8 bits. Los GPRs se describen a continuación, donde n en rn va del 8 al 15: r8, r9, r10, r11, r12, r13, r14 y r15.

64 bits 32 bits 16 bits 8 bits
rax eax ax ah/al
rbx ebx bx bh/bl
rcx ecx cx ch/cl
rdx edx dx dh/dl
rsi esi si sil
rdi edi di dil
rbp ebp bp bpl
rsp esp sp spl
rn rnd rnw rnb

Cuando se usan menos de 64 bits, los bits a los que se accede suelen ser de la parte baja del registro. La excepción a esta regla son ah, bh, ch y dh, que acceden a los 8 bits altos de la parte de 16 bits del registro.

Ilustración de cómo se accede a los bits del registro rax:

+--------+---------------------------------------+
| 64-bit |                  rax                  |
+--------+-------------------+-------------------+
| 32-bit |                   |        eax        |
+--------+-------------------+---------+---------+
| 16-bit |                             |    ax   |
+--------+-----------------------------+----+----+
| 8-bit  |                             | ah | al |
+--------+-----------------------------+----+----+

Algunos de esos registros deben preservarse a través de las llamadas a funciones: rbp, rsp, rbx, r12, r13, r14 y r15. No preservarlos puede provocar un error o un comportamiento indefinido.

Los demás no se preservan y se pueden usar libremente: rax, rcx, rdx, rdi, rsi, r8, r9, r10 y r11.

Caution

Más adelante en el temario aprenderás a preservar registros. Por ahora, usa solo el segundo tipo de registros: rax, rcx, rdx, rdi, rsi, r8, r9, r10 y r11.

Instrucciones

Las instrucciones son fragmentos de cálculo que una CPU puede realizar. Normalmente tienen la siguiente forma:

name destination, source

Entonces, primero se coloca el nombre de la instrucción, luego al menos un espacio en blanco, después el operando destino, una coma (,) y por último un operando fuente. Por lo general, una instrucción no modifica el operando fuente, solo el operando destino.

Ambos operandos deben tener el mismo tamaño, a menos que se indique lo contrario. Por ejemplo, el operando destino también debe tener 16 bits si el operando fuente tiene 16 bits.

Por ejemplo, para almacenar un valor en un registro podemos usar la instrucción mov:

mov rax, rdx  ; rax = rdx
              ; mov is the name, rax is the destination operand and rdx is the source operand

El fragmento anterior copia el contenido de los 64 bits de rdx, el operando fuente, a los 64 bits de rax, que es el operando destino de la instrucción.

En la mayoría de las instrucciones es posible usar números enteros constantes como operandos fuente (pero no como operandos destino):

mov rcx, 42 ; rcx = 42
            ; rcx is the destination operand and 42 is the source operand

Ten en cuenta que escribir en un registro de 32 bits también borra los bits altos, así que mov eax, 42 es lo mismo que mov rax, 42. Esto no es cierto para los registros de 16 bits y de 8 bits.

Para las operaciones aritméticas de suma, resta y multiplicación podemos usar las instrucciones add, sub e imul:

add rax, rsi ; rax = rax + rsi
imul rax, rdi ; rax = rax * rdi
sub rax, r8 ; rax = rax - r8

Funciones

Las instrucciones se organizan en funciones. Todas las funciones se colocan en la section .text del archivo fuente.

La declaración de una función consta de:

  1. Una etiqueta con el nombre de la función, seguida de un :.
  2. Las instrucciones que definen la función.
  3. La instrucción de retorno, ret.

Esta pista usa la convención de llamadas System V AMD64 ABI, y los seis primeros argumentos enteros se pasan a una función en registros. Se pasan en el siguiente orden: rdi, rsi, rdx, rcx, r8 y r9.

Un valor entero se devuelve desde la función en el registro rax.

Para llamar a una función usamos la instrucción call. Se puede llamar a una función desde cualquier parte del mismo archivo fuente. Para que una función sea visible desde otros archivos fuente, se debe usar la directiva global.

Por ejemplo, esto declara una función sum:

section .text ; functions are placed here

global sum ; sum is visible to other source files

sum:
    ; first argument is passed in rdi
    ; second argument is passed in rsi
    ; return value is placed in rax

    mov rax, rdi ; rax is now equal to rdi
    add rax, rsi ; rax = rax + rsi

    ret ; function return

Y esto llama a nuestra función sum con los argumentos 3 y 5:

mov rdi, 3  ; First argument in rdi
mov rsi, 5  ; Second argument in rsi
call sum

; The rax register now contains the value 8 (3 + 5), after sum returns

Instrucciones

En este ejercicio vas a escribir algo de código para ayudarte a cocinar una lasaña espectacular del libro de cocina que más te guste.

Tienes cuatro tareas, todas relacionadas con el tiempo que se tarda en cocinar la lasaña.

Note

Estas son las instrucciones que se mencionan en este concepto:

Instrucción Descripción
mov a, b copia el contenido de b a a
add a, b a = a + b
sub a, b a = a - b
imul a, b a = a * b
call a llama a la función a
ret regresa de una función

1. Define el tiempo esperado en el horno, en minutos

Define la función expected_minutes_in_oven, que no recibe ningún parámetro y devuelve cuántos minutos debe estar la lasaña en el horno. Según el libro de cocina, el tiempo esperado en el horno es de 40 minutos:

expected_minutes_in_oven();
// => 40

2. Calcula el tiempo restante en el horno, en minutos

Define la función remaining_minutes_in_oven, que recibe como parámetro los minutos que la lasaña ya lleva en el horno y devuelve cuántos minutos le quedan por estar en el horno, según el tiempo esperado en el horno de la tarea anterior.

remaining_minutes_in_oven(30);
// => 10

3. Calcula el tiempo de preparación, en minutos

Define la función preparation_time_in_minutes, que recibe como parámetro la cantidad de capas que le agregaste a la lasaña y devuelve cuántos minutos tardaste en prepararla, suponiendo que cada capa te toma 2 minutos.

preparation_time_in_minutes(2);
// => 4

4. Calcula el tiempo transcurrido, en minutos

Define la función elapsed_time_in_minutes, que recibe dos parámetros, en este orden:

  1. La cantidad de capas que le agregaste a la lasaña.
  2. La cantidad de minutos que la lasaña lleva en el horno.

La función debe devolver cuántos minutos has dedicado a cocinar la lasaña. Es la suma del tiempo de preparación, en minutos, y el tiempo, en minutos, que la lasaña lleva en el horno en ese momento.

elapsed_time_in_minutes(3, 20);
// => 26
Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
x86-64 Assembly Exercism

¿Todo listo para empezar Lasaña?

Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.