Parcours
/
x86-64 Assembly
x86-64 Assembly
/
Exercices
/
Gestion d'inventaire
Gestion d'inventaire

Gestion d'inventaire

Exercice d'apprentissage

Introduction

Les nombres entiers

Notation binaire

Un entier est une abstraction qui représente des nombres sans partie décimale, comme 4, -2, 0 ou 64532.

Pour représenter un entier sous forme de suite d'octets, on utilise la notation binaire. Dans cette notation, chaque bit de la séquence représente une puissance de deux distincte, la valeur augmentant à mesure que l'indice du bit augmente de la droite vers la gauche.

Nombres non signés

Si le nombre ne peut être que positif ou nul, on parle d'un nombre non signé.

Les nombres non signés sont représentés directement comme la somme des puissances de deux correspondant à tous les bits à 1 de leur séquence.

La plage des entiers positifs ou nuls représentables dans un registre va de 0 (aucun bit à 1) à 2⁶⁴ - 1 (somme des 64 bits à 1).

Pour élargir un nombre non signé vers une taille supérieure, on remplit tous les bits de poids fort avec des 0, de sorte qu'aucun nouveau bit ne contribue à la valeur. C'est ce qu'on appelle l'extension par zéros.

L'instruction movzx (z pour zéro) étend par des zéros un opérande source de 8 ou 16 bits vers un opérande destination plus grand. Un opérande source de 32 bits est toujours étendu par des zéros sur les 64 bits de l'opérande destination avec un simple mov.

Nombres signés

Si un entier peut prendre des valeurs positives ou négatives, on parle d'un nombre signé.

Pour représenter les nombres négatifs, x86-64 utilise la représentation en complément à deux.

En complément à deux, les nombres signés sont eux aussi représentés comme la somme des puissances de deux correspondant aux bits à 1. Cependant, si le bit le plus haut est à 1, il est soustrait au lieu d'être ajouté aux autres.

Comme ce bit correspond à une valeur supérieure à la somme de tous les autres, en pratique cela signifie qu'un nombre dont ce bit est à 1 est toujours négatif. Ce bit particulier est appelé le bit de signe.

Élargir un nombre signé vers une taille supérieure consiste à remplir chaque nouveau bit de poids fort avec une copie du bit de signe, afin que la valeur soit préservée. C'est ce qu'on appelle l'extension de signe.

L'instruction movsx (s pour signe) étend le signe d'un opérande source de 8 ou 16 bits vers un opérande destination plus grand. Une variante de movsx appelée movsxd fait de même d'un opérande source de 32 bits vers un opérande destination de 64 bits.

L'instruction neg permet de changer le signe d'un nombre.

Caution

En assembleur, il n'existe aucun moyen de savoir si une suite d'octets représente un nombre signé ou non signé. C'est au programmeur de donner un sens à ces octets.

L'usage de commentaires peut grandement aider dans cette tâche.

Les immédiats

Dans un concept précédent, on a vu qu'un nombre constant, comme 4 ou -15, peut être utilisé comme opérande source de nombreuses instructions. Ces nombres sont appelés des immédiats.

Un immédiat n'est stocké ni dans un registre ni en mémoire : il est encodé à l'intérieur de l'instruction elle-même. Dans la plupart des instructions, l'espace qui lui est réservé ne fait que 32 bits de large, quelle que soit la taille de l'opérande destination.

Quand l'opérande destination fait 64 bits de large, ces 32 bits sont étendus par le signe pour le remplir. La moitié haute de l'opérande est entièrement remplie de copies du bit le plus haut de l'immédiat, donc seul un nombre de la plage d'un entier signé sur 32 bits peut être écrit de cette façon :

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 nombre en dehors de cette plage ne peut pas être utilisé comme immédiat. L'exception à cette règle est mov, qui accepte un immédiat complet de 64 bits quand l'opérande destination est un registre. Si un immédiat de 64 bits est nécessaire, on utilise d'abord mov pour le charger dans un registre, puis on se sert de ce registre :

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

Note qu'un immédiat négatif et le nombre non signé ayant la même représentation binaire sont équivalents et s'assemblent exactement vers la même valeur :

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

Somme

L'addition de deux nombres peut se calculer avec l'instruction add.

Il existe aussi une instruction inc à un opérande, qui ajoute 1 à la valeur de son opérande :

inc rax ; rax = rax + 1

La somme de deux entiers fonctionne de la même façon pour les nombres non signés et signés.

Soustraction

La soustraction de deux entiers s'effectue avec l'instruction sub.

Il existe aussi une instruction dec à un opérande, qui soustrait 1 à la valeur de son opérande :

dec rax ; rax = rax - 1

La soustraction de deux entiers fonctionne elle aussi de la même façon pour les nombres non signés et signés.

Multiplication

Il existe deux instructions différentes pour effectuer une multiplication entre deux nombres en x86-64. En règle générale, la multiplication non signée utilise l'instruction mul, tandis que la multiplication signée utilise imul.

L'instruction mul prend la forme à un opérande suivante, où src est l'opérande source :

mul src

L'instruction imul peut prendre une forme à un, deux ou trois opérandes :

imul src
imul dest, src
imul dest, src1, src2
Multiplication à un opérande

Deux registres sont implicitement utilisés pour effectuer une multiplication sous la forme à un opérande : rax et rdx. Si la multiplication porte sur deux nombres de 64 bits, les 64 bits de poids faible du résultat se trouvent dans rax et les 64 bits de poids fort dans rdx.

On appelle généralement cela rdx:rax, pour indiquer que les deux registres sont utilisés de concert :

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

Il en va de même pour les autres tailles d'opérande. Ainsi, par exemple, si l'on multiplie deux nombres de 32 bits, ce sont eax et edx qui sont utilisés.

L'exception est la multiplication entre deux octets.

Dans ce cas, au lieu de dl:al, c'est ax qui est utilisé. La partie basse de ax (al) reçoit les 8 bits de poids faible du produit, tandis que la partie haute (ah) reçoit les 8 bits de poids fort.

Caution

Les registres implicitement utilisés dans une multiplication, comme rax et rdx, sont toujours écrasés. Il faut sauvegarder les valeurs de ces registres avant l'opération si on en a besoin plus tard.

Multiplication à deux opérandes

La forme à deux opérandes de imul a un opérande destination explicite et suit la syntaxe habituelle. rdx n'est pas utilisé. À la place, le résultat est tronqué pour tenir dans l'opérande destination.

imul r8, r9 ; r8 = lower 64 bits of r8 * r9
Multiplication à trois opérandes

La forme à trois opérandes de imul a deux opérandes source, dont le second est toujours un immédiat (un nombre constant). Les deux opérandes source sont multipliés, puis le résultat est tronqué et placé dans l'opérande destination :

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

Note que l'opérande destination n'est pas utilisé dans la multiplication. Il ne fait que recevoir le résultat.

Gère le débordement

Les multiplications à deux et à trois opérandes tronquent le résultat pour qu'il tienne dans la taille de l'opérande destination. Une multiplication à un opérande préserve toute la plage, mais le résultat est généralement réparti sur deux registres, rdx et rax.

Il est donc parfois utile d'élargir les opérandes avant la multiplication pour faire tenir tout le produit dans un seul registre. Un opérande non signé est étendu par des zéros, tandis qu'un opérande signé est étendu par le signe :

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

Comme pour la multiplication, il existe également deux instructions pour effectuer une division entre deux nombres. La division non signée utilise l'instruction div, tandis que la division signée utilise idiv.

Les deux instructions ne fonctionnent qu'avec un seul opérande :

div src
idiv src

Les divisions sur 16, 32 et 64 bits utilisent respectivement dx:ax, edx:eax et rdx:rax comme dividende. Dans ces cas, les deux registres agissent de concert pour former une valeur de 2N bits, où N est la taille de l'opération (16, 32 ou 64 bits). Cette valeur est ensuite divisée par l'opérande source. Le quotient est écrit dans ax, eax ou rax et le reste dans dx, edx ou rdx, selon la taille de l'opération.

La division entre octets est particulière : au lieu d'utiliser dl:al, c'est ax qui est utilisé. Les 8 bits de poids faible de ax (al) reçoivent le quotient de l'opération et les 8 bits de poids fort (ah) reçoivent le reste.

Note que tous les bits du dividende doivent être correctement positionnés avant la division. Tout bit à 1 dans rdx (ou dans ah pour une division sur 8 bits) contribue à la valeur à diviser.

Dans une division non signée, quand la valeur à diviser tient dans la moitié basse, il faut mettre à zéro la moitié haute. N'importe quelle instruction qui met ces bits à zéro fait l'affaire. Par exemple, mov edx, 0 met à zéro les bits de poids fort dans une division sur 32 bits.

Dans une division signée, il faut plutôt étendre la valeur par le signe. Certaines instructions automatisent ce processus : cbw, cwd, cdq et cqo. La première positionne les bits de ah selon le signe de al. Les autres réalisent une extension de signe respectivement de ax vers dx, de eax vers edx et de rax vers rdx.

Caution

Les registres implicitement utilisés dans une division, comme rax et rdx, sont toujours écrasés. Il faut sauvegarder les valeurs de ces registres avant la division si on en a besoin plus tard.

Instructions

Un commerce de quartier déménage son stock vers un entrepôt plus grand. Tu as été engagé pour tout emballer et tout déménager.

Tu as quatre tâches, toutes liées à la gestion du transport.

Note

Voici les instructions mentionnées dans ce 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

Rappelle-toi que tu peux accéder au même registre avec des tailles différentes en changeant le nom de l'opérande. Par exemple : rax (64 bits), eax (32 bits), ax (16 bits), al (8 bits).

Tu peux consulter le concept précédent pour obtenir le tableau complet.

1. Calcule le poids de chaque boîte

Les articles sont emballés dans des boîtes qui doivent porter une étiquette indiquant leur poids. Il n'y a pas de balance dans les environs, mais heureusement, tu connais le poids moyen de chaque article.

Pour mieux organiser les choses, une boîte ne contient que des articles de deux produits différents.

Définis une fonction get_box_weight qui renvoie le poids total d'une boîte, en g. Cette fonction prend comme paramètres, dans cet ordre :

  • Le nombre d'articles du premier produit dans la boîte
  • Le poids de chaque article du premier produit, en g
  • Le nombre d'articles du second produit dans la boîte
  • Le poids de chaque article du second produit, en g

Considère qu'une boîte vide pèse 500 g. Une constante WEIGHT_OF_EMPTY_BOX est définie en haut du fichier de solution.

Exemple :

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

Tous les arguments sont des entiers non négatifs sur 16 bits, et la valeur de retour est un entier non négatif sur 32 bits.

2. Calcule combien de boîtes tiennent dans le camion

Les boîtes sont empilées et transportées vers le nouvel entrepôt dans un camion. Cependant, l'espace vertical dans le camion est limité.

Définis une fonction max_number_of_boxes qui renvoie combien de boîtes d'une certaine hauteur peuvent être empilées verticalement (l'une au-dessus de l'autre) dans le camion.

Cette fonction prend comme paramètre la hauteur de la boîte, en cm. Considère que la hauteur intérieure du camion est de 300 cm. Une constante TRUCK_HEIGHT est définie en haut du fichier de solution.

Exemple :

max_number_of_boxes(30);
// => 10

L'argument et la valeur de retour sont des entiers non négatifs sur 8 bits. La hauteur des boîtes est toujours d'au moins 2, donc le résultat tient sur 8 bits.

3. Vérifie que tous les produits sont comptabilisés

Il y a une liste de contrôle dans le nouvel entrepôt, avec le nombre d'articles encore non comptabilisés pour chaque produit. Pour chaque nouvelle boîte qui y est déplacée, tu dois calculer la nouvelle valeur de la liste de contrôle pour chaque produit présent dans la boîte.

Définis une fonction items_to_be_moved qui renvoie combien d'articles restent à déplacer vers le nouvel entrepôt pour un produit donné. Cette fonction prend comme paramètres, dans cet ordre :

  • Le nombre d'articles encore non comptabilisés pour un produit
  • Le nombre d'articles du produit dans une boîte

Exemple :

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

Les arguments sont des entiers non négatifs sur 32 bits. La valeur de retour est un entier sur 32 bits. En cas d'erreur dans le processus, il est possible que le résultat soit un nombre négatif.

4. Reçois ton paiement

Ton paiement dépend du nombre de boîtes déplacées et du nombre de trajets en camion nécessaires. Pour chaque boîte, tu seras payé 5 dollars et pour chaque trajet, tu seras payé 220 dollars. Les constantes PAY_PER_BOX et PAY_PER_TRUCK_TRIP sont définies en haut du fichier de solution.

Note que tu as peut-être déjà reçu une partie de ce paiement à l'avance pour couvrir les coûts initiaux, et ce paiement anticipé doit être soustrait du paiement final. De plus, certains produits ne sont pas couverts par l'assurance et ton paiement sera également réduit de la valeur des articles cassés ou manquants. Si tu ne fais pas attention, il est possible que tu finisses par devoir de l'argent !

Cela signifie que le montant net qu'on te doit, ou que tu dois, est :

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

Ce paiement, ou cette dette, sera réparti équitablement entre toi et un certain nombre d'ouvriers que tu as embauchés. L'argent restant, ou la dette restante, te revient. Par exemple, si le montant net est de 100 à partager entre 6 personnes (toi et 5 ouvriers), tu obtiens 20 (100/(5 + 1) = 16 plus le reste 4).

Définis une fonction calculate_payment qui renvoie combien tu dois être payé, ou payer, à la fin. Cette fonction prend comme paramètres, dans cet ordre :

  • Le montant que tu as reçu à l'avance, sous forme d'entier non négatif sur 64 bits
  • Le nombre total de boîtes déplacées, sous forme d'entier non négatif sur 32 bits
  • Le nombre de trajets en camion effectués, sous forme d'entier non négatif sur 32 bits
  • Le nombre d'articles cassés ou manquants, sous forme d'entier non négatif sur 32 bits
  • La valeur de chaque article perdu, sous forme d'entier non négatif sur 64 bits
  • Le nombre d'ouvriers avec qui partager le paiement ou la dette, sous forme d'entier positif sur 8 bits

Exemple :

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

La valeur de retour est un entier sur 64 bits.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
x86-64 Assembly Exercism

Prêt à commencer Gestion d'inventaire ?

Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.