Tracks
/
x86-64 Assembly
x86-64 Assembly
/
Ejercicios
/
Gestión de inventario
Gestión de inventario

Gestión de inventario

Ejercicio de aprendizaje

Introducción

Números enteros

Notación binaria

Un número entero es una abstracción que representa números completos, como 4, -2, 0 o 64532.

Para representar un número entero como una secuencia de bytes, se usa la notación binaria. En esta notación, cada bit de la secuencia representa una potencia de dos distinta, y el valor aumenta a medida que el índice del bit crece de derecha a izquierda.

Números sin signo

Si el número solo puede ser no negativo, se dice que es un número sin signo.

Los números sin signo se representan directamente como la suma de las potencias de dos correspondientes a todos los bits establecidos en su secuencia.

El rango de números enteros no negativos representables en un registro va de 0 (ningún bit establecido) a 2⁶⁴ - 1 (la suma de los 64 bits establecidos).

Para ampliar un número sin signo a un tamaño mayor, se rellenan todos los bits superiores con 0, de modo que ningún bit nuevo contribuya al valor. Esto se llama extensión con ceros.

La instrucción movzx (la z de zero) extiende con ceros un operando fuente de 8 o 16 bits a un operando destino más grande. Un operando fuente de 32 bits siempre se extiende con ceros a los 64 bits del operando destino con un simple mov.

Números con signo

Si un número entero puede tomar valores positivos o negativos, se dice que es un número con signo.

Para representar números negativos, x86-64 usa la representación en complemento a dos.

En complemento a dos, los números con signo también se representan como la suma de las potencias de dos correspondientes a los bits establecidos. Sin embargo, si el bit más alto está establecido, se resta en lugar de sumarse a los demás.

Como este bit corresponde a un valor mayor que la suma de todos los demás, en la práctica esto significa que un número con este bit establecido siempre es negativo. Este bit especial se llama bit de signo.

Ampliar un número con signo a un tamaño mayor significa rellenar cada bit superior nuevo con una copia del bit de signo, para que el valor se conserve. Esto se llama extensión de signo.

La instrucción movsx (la s de sign) extiende el signo de un operando fuente de 8 o 16 bits a un operando destino más grande. Una variante de movsx llamada movsxd hace lo mismo desde un operando fuente de 32 bits a un operando destino de 64 bits.

La instrucción neg puede usarse para cambiar el signo de un número.

Caution

En ensamblador, no hay forma de saber si una secuencia de bytes representa un número con signo o sin signo. Es responsabilidad de quien programa darles significado a esos bytes.

El uso de comentarios puede ser de gran ayuda en esta tarea.

Inmediatos

En un concepto anterior se mencionó que un número constante, como 4 o -15, puede usarse como operando fuente en muchas instrucciones. Esos números se llaman inmediatos.

Un inmediato no se guarda en un registro ni en memoria: se codifica dentro de la propia instrucción. En la mayoría de las instrucciones, el espacio reservado para él es de solo 32 bits, sin importar lo grande que sea el operando destino.

Cuando el operando destino es de 64 bits, esos 32 bits se extienden con el signo para rellenarlo. La mitad superior del operando se rellena por completo con copias del bit más alto del inmediato, así que solo se puede escribir de esta forma un número dentro del rango de un entero con signo de 32 bits:

add rax, -1          ; the immediate is sign-extended, so all 64 bits of rax are affected
add rax, 2147483647  ; the largest immediate an instruction like this accepts

Un número fuera de ese rango no puede usarse como inmediato. La excepción a esta regla es mov, que puede tomar un inmediato completo de 64 bits cuando el operando destino es un registro. Si necesitas un inmediato de 64 bits, primero usa mov para cargarlo en un registro y después usa ese registro:

mov rax, 3435973837           ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax

Ten en cuenta que un inmediato negativo y el número sin signo con la misma representación de bits son equivalentes y se ensamblan exactamente al mismo valor:

mov rax, -1                   ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1

Suma

La suma de dos números se puede calcular con la instrucción add.

También existe la instrucción inc, de un solo operando, que suma 1 al valor de su operando:

inc rax ; rax = rax + 1

La suma de dos números enteros funciona de la misma manera tanto para números sin signo como con signo.

Resta

La resta de dos números enteros se realiza con la instrucción sub.

También existe la instrucción dec, de un solo operando, que resta 1 al valor de su operando:

dec rax ; rax = rax - 1

La resta de dos números enteros también funciona de la misma manera tanto para números sin signo como con signo.

Multiplicación

Hay dos instrucciones distintas para multiplicar dos números en x86-64. Por regla general, la multiplicación sin signo usa la instrucción mul, mientras que la multiplicación con signo usa imul.

La instrucción mul toma la siguiente forma de un operando, donde src es el operando fuente:

mul src

La instrucción imul puede tomar la forma de uno, dos o tres operandos:

imul src
imul dest, src
imul dest, src1, src2
Multiplicación con un operando

En la multiplicación con un operando se usan implícitamente dos registros: rax y rdx. Si la multiplicación involucra dos números de 64 bits, los 64 bits inferiores del resultado quedan en rax y los 64 bits superiores en rdx.

Esto suele escribirse como rdx:rax, para indicar que ambos registros se usan en conjunto:

mul rcx ; rax = lower 64 bits of rax * rcx
        ; rdx = upper 64 bits of rax * rcx

Lo mismo ocurre con otros tamaños de operando. Así, por ejemplo, si se multiplican dos números de 32 bits, se usarán eax y edx.

La excepción es la multiplicación entre dos bytes.

En este caso, en lugar de dl:al, se usará ax. La parte inferior de ax (al) recibirá los 8 bits inferiores del producto, mientras que la parte superior (ah) recibirá los 8 bits superiores.

Caution

Los registros que se usan implícitamente en una multiplicación, como rax y rdx, siempre se sobrescriben. Si los valores de esos registros se necesitan más adelante, guárdalos antes de la operación.

Multiplicación con dos operandos

La forma de dos operandos de imul tiene un operando destino explícito y sigue la sintaxis habitual. rdx no se usa. En su lugar, el resultado se trunca para que quepa en el operando destino.

imul r8, r9 ; r8 = lower 64 bits of r8 * r9
Multiplicación con tres operandos

La forma de tres operandos de imul tiene dos operandos fuente, y el segundo es siempre un inmediato (un número constante). Ambos operandos fuente se multiplican, y el resultado se trunca y se coloca en el operando destino:

imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100

Ten en cuenta que el operando destino no se usa en la multiplicación. Solo recibe el resultado.

Cómo manejar el desbordamiento

Tanto la multiplicación con dos operandos como la de tres operandos truncan el resultado para que quepa en el tamaño del operando destino. La multiplicación con un operando conserva el rango completo, pero por lo general se reparte entre dos registros, rdx y rax.

Por eso, a veces es útil ampliar los operandos antes de la multiplicación para dejar espacio al producto completo en un solo registro. Un operando sin signo se extiende con ceros, mientras que uno con signo se extiende con el signo:

movzx eax, di ; di and si hold unsigned 16-bit numbers
movzx ecx, si
mul ecx       ; the 32-bit product fits in eax, and edx is cleared

División

Igual que con la multiplicación, también hay dos instrucciones para dividir dos números. La división sin signo usa la instrucción div, mientras que la división con signo usa idiv.

Ambas instrucciones funcionan con un solo operando:

div src
idiv src

La división de 16, 32 y 64 bits usa dx:ax, edx:eax y rdx:rax como dividendo, respectivamente. En estos casos, ambos registros actúan en conjunto para formar un valor de 2N bits, donde N es el tamaño de la operación (16, 32 o 64 bits). Ese valor se divide después por el operando fuente. El cociente se escribe en ax, eax o rax, y el residuo se escribe en dx, edx o rdx, según el tamaño de la operación.

La división entre bytes es especial: en lugar de usar dl:al, se usa ax. Los 8 bits inferiores de ax (al) recibirán el cociente de la operación y los 8 bits superiores (ah) recibirán el residuo.

Ten en cuenta que todos los bits del dividendo deben estar establecidos de forma adecuada antes de la división. Cualquier bit establecido en rdx (o en ah para la división de 8 bits) contribuye al valor que se divide.

En la división sin signo, cuando el valor que se va a dividir cabe en la mitad inferior, hay que limpiar la mitad superior. Sirve cualquier instrucción que limpie esos bits. Por ejemplo, mov edx, 0 limpia los bits superiores en la división de 32 bits.

En la división con signo, en cambio, el valor debe extenderse con el signo. Hay instrucciones que automatizan este proceso: cbw, cwd, cdq y cqo. La primera establece los bits de ah según el signo de al. Las otras realizan la extensión de signo de ax a dx, de eax a edx y de rax a rdx, respectivamente.

Caution

Los registros que se usan implícitamente en una división, como rax y rdx, siempre se sobrescriben. Si los valores de esos registros se necesitan más adelante, guárdalos antes de la división.

Instrucciones

Una tienda local está mudando su inventario a un almacén más grande. Te contrataron para empacar y mover todo.

Tienes cuatro tareas, todas relacionadas con gestionar el transporte.

Note

Estas son las instrucciones mencionadas en este concepto:

Instrucción Descripción
add a, b a = a + b
inc a a = a + 1
sub a, b a = a - b
dec a a = a - 1
imul a rdx:rax = a * rax (signed)
imul a, b a = a * b (signed, truncated)
imul a, b, c a = b * c (signed, truncated)
mul a rdx:rax = a * rax (unsigned)
div a rax = quotient, rdx = remainder of rdx:rax / a (unsigned)
idiv a rax = quotient, rdx = remainder of rdx:rax / a (signed)
movzx a, b a = b, adding 0 to the extra bits
movsx a, b a = b, adding 1 to the extra bits if b < 0 or 0 otherwise
Note

Recuerda que puedes acceder al mismo registro con distintos tamaños cambiando el nombre del operando. Por ejemplo: rax (64 bits), eax (32 bits), ax (16 bits), al (8 bits).

Puedes consultar el concepto anterior para ver la tabla completa.

1. Obtén el peso de cada caja

Los artículos se están empacando en cajas que deben etiquetarse con su peso. No hay ninguna balanza cerca, pero por suerte sabes cuánto pesa cada artículo en promedio.

Para organizar mejor las cosas, cada caja contiene solo artículos de dos productos diferentes.

Define una función get_box_weight que devuelva el peso total de una caja, en g. Esta función recibe como parámetros, en este orden:

  • La cantidad de artículos del primer producto en la caja
  • El peso de cada artículo del primer producto, en g
  • La cantidad de artículos del segundo producto en la caja
  • El peso de cada artículo del segundo producto, en g

Ten en cuenta que una caja vacía pesa 500 g. La constante WEIGHT_OF_EMPTY_BOX está definida al inicio del archivo de solución.

Ejemplo:

get_box_weight(30, 40, 50, 20);
// => 2700

Todos los argumentos son enteros de 16 bits no negativos, y el valor de retorno es un entero de 32 bits no negativo.

2. Calcula cuántas cajas caben en el camión

Las cajas se apilan y se trasladan al nuevo almacén en un camión. Sin embargo, el espacio vertical del camión es limitado.

Define una función max_number_of_boxes que devuelva cuántas cajas de cierta altura se pueden apilar verticalmente (una encima de otra) dentro del camión.

Esta función recibe como parámetro la altura de la caja, en cm. Ten en cuenta que la altura interior del camión es de 300 cm. La constante TRUCK_HEIGHT está definida al inicio del archivo de solución.

Ejemplo:

max_number_of_boxes(30);
// => 10

El argumento y el valor de retorno son enteros de 8 bits no negativos. La altura de la caja siempre es al menos 2, así que el resultado cabe en 8 bits.

3. Comprueba si todos los productos están contabilizados

En el nuevo almacén hay una lista de verificación con la cantidad de artículos que todavía no se han contabilizado de cada producto. Por cada caja nueva que se mueve allí, necesitas calcular el nuevo valor en la lista de verificación para cada producto de la caja.

Define una función items_to_be_moved que devuelva cuántos artículos quedan por mover al nuevo almacén para un producto dado. Esta función recibe como parámetros, en este orden:

  • La cantidad de artículos que todavía no se han contabilizado de un producto
  • La cantidad de artículos del producto en una caja

Ejemplo:

items_to_be_moved(76532, 120);
// => 76412

Los argumentos son enteros de 32 bits no negativos. El valor de retorno es un entero de 32 bits. Si ocurre un error en el proceso, es posible que el resultado sea un número negativo.

4. Recibe el pago

Tu pago depende de cuántas cajas se movieron y cuántos viajes en camión fueron necesarios. Por cada caja te pagarán 5 dólares y por cada viaje te pagarán 220 dólares. Las constantes PAY_PER_BOX y PAY_PER_TRUCK_TRIP están definidas al inicio del archivo de solución.

Ten en cuenta que es posible que hayas recibido parte de este pago por adelantado para cubrir los costos iniciales, y ese pago por adelantado debe restarse del pago final. Además, algunos productos no están cubiertos por el seguro y tu pago también se reducirá por el valor de cualquiera de esos artículos que se rompa o falte. ¡Es posible que termines debiendo dinero si no tienes cuidado!

Esto significa que el monto neto que te deben, o que debes, es:

net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value

Este pago, o deuda, se repartirá en partes iguales entre tú y los trabajadores que hayas contratado. El dinero restante, o la deuda restante, es tuyo. Por ejemplo, si el monto neto de dinero es 100 y se reparte entre 6 personas (tú y 5 trabajadores), te quedas con 20 (100/(5 + 1) = 16 más los 4 restantes).

Define una función calculate_payment que devuelva cuánto te deben pagar, o cuánto debes pagar, al final. Esta función recibe como parámetros, en este orden:

  • Cuánto has recibido por adelantado, como un entero de 64 bits no negativo
  • La cantidad total de cajas movidas, como un entero de 32 bits no negativo
  • La cantidad de viajes en camión realizados, como un entero de 32 bits no negativo
  • La cantidad de artículos rotos o faltantes, como un entero de 32 bits no negativo
  • El valor de cada artículo perdido, como un entero de 64 bits no negativo
  • La cantidad de trabajadores con quienes repartir el pago o la deuda, como un entero positivo de 8 bits

Ejemplo:

calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029

El valor de retorno es un entero de 64 bits.

Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
x86-64 Assembly Exercism

¿Todo listo para empezar Gestión de inventario?

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