Tracks
/
x86-64 Assembly
x86-64 Assembly
/
Exercises
/
Inventory Management
Inventory Management

Inventory Management

Learning Exercise

Introduction

Integers

Binary Notation

An integer is an abstraction that represents whole numbers, such as 4, -2, 0 or 64532.

To represent an integer as a sequence of bytes, the binary notation is used. In this notation, each bit in the sequence represents a distinct power of two, with the value increasing as the index of the bit increases from right to left.

Unsigned Numbers

If the number can only be non-negative, it's called an unsigned number.

Unsigned numbers are represented directly as the sum of the powers of two corresponding to all set bits in their sequence.

The range of representable non-negative integers in a register goes from 0 (no bit set) to 2⁶⁴ - 1 (sum of all 64 bits set).

Widening an unsigned number to a larger size is done by filling all upper bits with 0, so that no new bit contributes to the value. This is called zero-extension.

The instruction movzx (z for zero) zero-extends an 8-bit or 16-bit source operand to a larger destination operand. A 32-bit source operand is always zero-extended to all 64 bits of the destination operand with a simple mov.

Signed Numbers

If an integer can assume positive or negative values, it's called a signed number.

To represent negative numbers, x86-64 uses the two's complement representation.

In two's complement, signed numbers are also represented as the sum of the powers of two corresponding to set bits. However, if the uppermost bit is set, it is subtracted instead of added to the others.

Since this bit corresponds to a higher value than the sum of all the others, in practice this means a number with this bit set is always negative. This special bit is called the sign bit.

Widening a signed number to a larger size means filling every new upper bit with a copy of the sign bit, so that the value is preserved. This is called sign-extension.

The instruction movsx (s for sign) sign-extends an 8-bit or 16-bit source operand to a larger destination operand. A variant of movsx called movsxd does the same from a 32-bit source operand to a 64-bit destination operand.

The neg instruction can be used to change the sign of a number.

Caution

In assembly, there's no way to tell if a sequence of bytes represents a signed or an unsigned number. It's the programmer's responsibility to give meaning to those bytes.

The use of comments can be a great aid in this task.

Immediates

In a previous concept, it was mentioned that a constant number, such as 4 or -15, can be used as source operand to many instructions. Those numbers are called immediates.

An immediate is not held in a register or in memory: it is encoded inside the instruction itself. In most instructions, the space reserved for it is only 32 bits wide, no matter how large the destination operand is.

When the destination operand is 64 bits wide, those 32 bits are sign-extended to fill it. The upper half of the operand is entirely filled with copies of the uppermost bit of the immediate, so only a number in the range of a 32-bit signed integer can be written this way:

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

A number outside that range can not be used as an immediate. The exception to this rule is mov, which can take a full 64-bit immediate when the destination operand is a register. If a 64-bit immediate is needed, first use mov to load it into a register, then use that register:

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

Note that a negative immediate and the unsigned number with the same bit representation are equivalent and assemble to exactly the same value:

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

Sum

The addition of two numbers can be calculated using the add instruction.

There's also an inc one-operand instruction, that adds 1 to the value in its operand:

inc rax ; rax = rax + 1

The sum of two integers operates in the same way for both unsigned and signed numbers.

Subtraction

The subtraction of two integers is performed using the sub instruction.

There's also a dec one-operand instruction, that subtracts 1 from the value in its operand:

dec rax ; rax = rax - 1

The subtraction of two integers also operates in the same way for both unsigned and signed numbers.

Multiplication

There are two different instructions to perform multiplication between two numbers in x86-64. As a rule, unsigned multiplication uses the instruction mul, while signed multiplication uses imul.

The mul instruction takes the following one-operand form, where src is the source operand:

mul src

The imul instruction can take a one-operand, two-operand or three-operand form:

imul src
imul dest, src
imul dest, src1, src2
One-Operand Multiplication

Two registers are implicitly used to perform a multiplication in one-operand form: rax and rdx. If the multiplication involves two 64-bit numbers, then the lower 64 bits of the result will be in rax and the upper 64 bits will be in rdx.

This is usually called rdx:rax, to indicate that both registers are used in tandem:

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

The same happens for other operand sizes. So, for instance, if two 32-bit numbers are being multiplied, eax and edx will be used.

The exception is the multiplication between two bytes.

In this case, instead of dl:al, ax will be used. The lower portion of ax (al) will get the lower 8 bits of the product, while the upper portion (ah) will get the upper 8 bits.

Caution

Registers implicitly used in a multiplication, such as rax and rdx, are always overwritten. The values in those registers should be saved before the operation if they are necessary later.

Two-Operand Multiplication

The two-operand form of imul has an explicit destination operand and follows the usual syntax. rdx is not used. Instead, the result is truncated to fit into the destination operand.

imul r8, r9 ; r8 = lower 64 bits of r8 * r9
Three-Operand Multiplication

The three-operand form of imul has two source operands, the second of which is always an immediate (a constant number). Both source operands are multiplied and the result is truncated and placed in the destination operand:

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

Note that the destination operand is not used in the multiplication. It just receives the result.

Handling Overflow

Both two-operand and three-operand multiplication truncate the result to fit into the size of the destination operand. A one-operand multiplication preserves the full range, but it is usually split into two registers, rdx and rax.

So, it is sometimes useful to widen the operands before the multiplication to make room for the whole product in a single register. An unsigned operand is zero-extended, while a signed one is sign-extended:

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

Division

As is the case with multiplication, there are also two instructions to perform division between two numbers. Unsigned division uses the instruction div, while signed division uses idiv.

Both instructions work with only one operand:

div src
idiv src

16-bit, 32-bit and 64-bit division use dx:ax, edx:eax and rdx:rax as the dividend, respectively. In these cases, both registers act in tandem to create a 2N-bit value, where N is the size of the operation (16-bit, 32-bit or 64-bit). This value is then divided by the source operand. The quotient is written to ax, eax or rax and the remainder is written to dx, edx or rdx, according to the size of the operation.

The division between bytes is special: instead of using dl:al, ax is used. The lower 8 bits of ax (al) will get the quotient of the operation and the higher 8 bits (ah) will get the remainder.

Note that all bits in the dividend should be appropriately set before the division. Any bit set in rdx (or in ah for 8-bit division) contributes to the value being divided.

In unsigned division, when the value to be divided fits in the lower half, the upper half should be cleared. Any instruction that clears those bits will do. For example, mov edx, 0 clears the upper bits in 32-bit division.

In signed division, the value should be sign-extended instead. There are instructions that automate this process: cbw, cwd, cdq and cqo. The first sets the bits in ah according to the sign of al. The others perform sign-extension from ax to dx, from eax to edx and from rax to rdx, respectively.

Caution

Registers implicitly used in a division, such as rax and rdx, are always overwritten. The values in those registers should be saved before the division if they are necessary later.

Instructions

A local store is moving its inventory to a larger warehouse. You were hired to pack and move everything.

You have four tasks, all related to managing the transport.

Note

These are the instructions mentioned in this concept:

Instruction Description
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

Remember that you can access the same register with different sizes by changing the name of the operand. For example: rax (64-bit), eax (32-bit), ax (16-bit), al (8-bit).

You can refer to the previous concept for the full table.

1. Get weight of each box

Items are being packed in boxes that must be labeled with their weight. There is no scale around, but luckily you know how much each item weighs on average.

To better organize things, a box holds only items of two different products.

Define a function get_box_weight that returns the total weight of a box, in g. This function takes as parameters, in this order:

  • The number of items for the first product in the box
  • The weight of each item of the first product, in g
  • The number of items for the second product in the box
  • The weight of each item of the second product, in g

Consider that an empty box weighs 500 g. A constant WEIGHT_OF_EMPTY_BOX is defined at the top of the solution file.

Example:

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

All arguments are 16-bit non-negative integers, and the return value is a 32-bit non-negative integer.

2. Calculate how many boxes fit into the truck

Boxes are being stacked and moved to the new warehouse in a truck. However, there is only so much vertical space in the truck.

Define a function max_number_of_boxes that returns how many boxes of a certain height can be stacked vertically (one on top of another) within the truck.

This function takes as parameter the height of the box, in cm. Consider that the truck interior height is 300 cm. A constant TRUCK_HEIGHT is defined at the top of the solution file.

Example:

max_number_of_boxes(30);
// => 10

The argument and the return value are 8-bit non-negative integers. The box height is always at least 2, so the result fits in 8 bits.

3. Check if all products are accounted for

There is a checklist in the new warehouse with the number of items still unaccounted for each product. For each new box moved there, you need to calculate the new value in the checklist for each product in the box.

Define a function items_to_be_moved that returns how many items remain to be moved to the new warehouse for a given product. This function takes as parameters, in this order:

  • The number of items still unaccounted for a product
  • The number of items for the product in a box

Example:

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

The arguments are 32-bit non-negative integers. The return value is a 32-bit integer. In case of an error in the process, it is possible that the result is a negative number.

4. Get payment

Your payment is based on how many boxes were moved and how many truck trips were necessary. For each box, you will be paid 5 dollars and for each trip, you will be paid 220 dollars. Constants PAY_PER_BOX and PAY_PER_TRUCK_TRIP are defined at the top of the solution file.

Note that you may have received part of this payment up front to cover initial costs, and this up-front payment should be subtracted from the final pay. In addition, some products are not covered by insurance and your payment will also be reduced by the value of any of those items broken or missing. It is possible that you end up owing money if you are not careful!

This means the net amount you are owed, or owe, is:

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

This payment, or debt, will be divided equally between you and a number of workers you hired. Any remaining money, or debt, is yours. For example, if the net amount of money is 100 being shared amongst 6 people (you and 5 workers), you get 20 (100/(5 + 1) = 16 plus the remaining 4).

Define a function calculate_payment that returns how much you should be paid, or pay, at the end. This function takes as parameters, in this order:

  • How much you have received up front, as a 64-bit non-negative integer
  • The total number of boxes moved, as a 32-bit non-negative integer
  • The number of truck trips made, as a 32-bit non-negative integer
  • The number of broken or missing items, as a 32-bit non-negative integer
  • The value of each lost item, as a 64-bit non-negative integer
  • The number of workers to split the payment or debt with you, as an 8-bit positive integer

Example:

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

The return value is a 64-bit integer.

Edit via GitHub The link opens in a new window or tab
x86-64 Assembly Exercism

Ready to start Inventory Management?

Sign up to Exercism to learn and master x86-64 Assembly with 22 concepts130 exercises, and real human mentoring, all for free.