Rutas
/
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 sin parte fraccionaria, como 4, -2, 0 o 64532.

Para representar un número entero como una secuencia de bytes, se utiliza 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 lo hace el índice del bit de derecha a izquierda.

Números sin signo

Si el número solo puede ser no negativo, se denomina 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 activados de su secuencia.

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

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

La instrucción movzx (z de cero) extiende con ceros un operando origen de 8 o 16 bits a un operando destino de mayor tamaño. Un operando origen 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 denomina número con signo.

Para representar números negativos, x86-64 utiliza 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 activados. Sin embargo, si el bit más alto está activado, 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 activado siempre es negativo. Este bit especial se denomina bit de signo.

Ensanchar un número con signo a un tamaño mayor consiste en rellenar cada nuevo bit superior con una copia del bit de signo, de modo que se conserve el valor. Esto se denomina extensión de signo.

La instrucción movsx (s de signo) extiende el signo de un operando origen de 8 o 16 bits a un operando destino de mayor tamaño. Una variante de movsx llamada movsxd hace lo mismo desde un operando origen de 32 bits a un operando destino de 64 bits.

La instrucción neg se puede usar 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 uno sin signo. Es responsabilidad de quien programa dar 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 origen de muchas instrucciones. Esos números se denominan inmediatos.

Un inmediato no se guarda en un registro ni en memoria: está codificado 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 el tamaño del operando destino.

Cuando el operando destino es de 64 bits, esos 32 bits se extienden con signo para rellenarlo. La mitad superior del operando se rellena por completo con copias del bit más alto del inmediato, por lo 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 se necesita un inmediato de 64 bits, usa primero mov para cargarlo en un registro y luego 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 igual 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 igual tanto para números sin signo como con signo.

Multiplicación

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

La instrucción mul adopta la siguiente forma de un solo operando, donde src es el operando origen:

mul src

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

imul src
imul dest, src
imul dest, src1, src2
Multiplicación de un solo operando

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

Esto se suele denominar 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. Los valores de esos registros deben guardarse antes de la operación si se van a necesitar después.

Multiplicación de dos operandos

La forma de dos operandos de imul tiene un operando destino explícito y sigue la sintaxis habitual. No se usa rdx. 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 de tres operandos

La forma de tres operandos de imul tiene dos operandos origen, el segundo de los cuales es siempre un inmediato (un número constante). Se multiplican ambos operandos origen 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.

Gestión del desbordamiento

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

Por eso, a veces es útil ensanchar los operandos antes de la multiplicación para hacer sitio 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

Al igual que ocurre 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 crear un valor de 2N bits, donde N es el tamaño de la operación (16, 32 o 64 bits). Este valor se divide entonces entre el operando origen. 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 debidamente establecidos antes de la división. Cualquier bit activado 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, la mitad superior debe ponerse a cero. Sirve cualquier instrucción que ponga esos bits a cero. Por ejemplo, mov edx, 0 pone a cero 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 demás 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. Los valores de esos registros deben guardarse antes de la división si se van a necesitar después.

Instrucciones

Una tienda local está trasladando su inventario a un almacén más grande. Te han contratado para empaquetar y trasladarlo todo.

Tienes cuatro tareas, todas relacionadas con la gestión del 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 (con signo)
imul a, b a = a * b (con signo, truncado)
imul a, b, c a = b * c (con signo, truncado)
mul a rdx:rax = a * rax (sin signo)
div a rax = cociente, rdx = residuo de rdx:rax / a (sin signo)
idiv a rax = cociente, rdx = residuo de rdx:rax / a (con signo)
movzx a, b a = b, añadiendo 0 a los bits adicionales
movsx a, b a = b, añadiendo 1 a los bits adicionales si b < 0 o 0 en caso contrario
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. Obtener el peso de cada caja

Los artículos se empaquetan en cajas que deben etiquetarse con su peso. No hay ninguna báscula a mano, pero por suerte sabes cuánto pesa de media cada artículo.

Para organizarlo mejor, cada caja solo contiene artículos de dos productos distintos.

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:

  • El número de artículos del primer producto que hay en la caja
  • El peso de cada artículo del primer producto, en g
  • El número de artículos del segundo producto que hay 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. Al principio del archivo de solución se define la constante WEIGHT_OF_EMPTY_BOX.

Ejemplo:

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

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

2. Calcular 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 una altura determinada 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. Al principio del archivo de solución se define la constante TRUCK_HEIGHT.

Ejemplo:

max_number_of_boxes(30);
// => 10

El argumento y el valor devuelto son enteros no negativos de 8 bits. La altura de la caja siempre es como mínimo 2, así que el resultado cabe en 8 bits.

3. Comprobar si todos los productos están contabilizados

En el nuevo almacén hay una lista de comprobación con el número de artículos que aún quedan pendientes de contabilizar de cada producto. Por cada caja nueva que se traslade allí, tienes que calcular el nuevo valor de la lista de comprobación para cada producto que haya en la caja.

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

  • El número de artículos que aún quedan pendientes de contabilizar de un producto
  • El número de artículos de ese producto que hay en una caja

Ejemplo:

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

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

4. Recibir el pago

Tu pago depende de cuántas cajas se hayan trasladado y de cuántos viajes en camión hayan sido necesarios. Por cada caja te pagarán 5 dólares y por cada viaje te pagarán 220 dólares. Al principio del archivo de solución se definen las constantes PAY_PER_BOX y PAY_PER_TRUCK_TRIP.

Ten en cuenta que puede que hayas recibido por adelantado parte de este pago para cubrir los costes 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 desaparezca. Es posible que acabes debiendo dinero si no tienes cuidado.

Esto significa que la cantidad neta 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 esta deuda, se repartirá a partes iguales entre tú y un número de trabajadores que hayas contratado. Cualquier dinero restante, o deuda, es tuyo. Por ejemplo, si la cantidad neta de dinero es 100 y se reparte entre 6 personas (tú y 5 trabajadores), tú recibes 20 (100/(5 + 1) = 16 más los 4 que sobran).

Define una función calculate_payment que devuelva cuánto deberían pagarte, o cuánto debes pagar tú, al final. Esta función recibe como parámetros, en este orden:

  • Cuánto has recibido por adelantado, como entero no negativo de 64 bits
  • El número total de cajas trasladadas, como entero no negativo de 32 bits
  • El número de viajes en camión realizados, como entero no negativo de 32 bits
  • El número de artículos rotos o desaparecidos, como entero no negativo de 32 bits
  • El valor de cada artículo perdido, como entero no negativo de 64 bits
  • El número de trabajadores con los que repartir el pago o la deuda, como entero positivo de 8 bits

Ejemplo:

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

El valor devuelto es un entero de 64 bits.

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

¿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.