Dans les concepts précédents, nous avons vu que la directive global rend une fonction ou une variable définie dans un fichier source visible depuis les autres fichiers. De même, la directive extern informe l'assembleur qu'une fonction ou une variable utilisée dans le fichier source courant est définie dans un autre fichier.
Ces deux directives sont le principal moyen pour le code assembleur d'interagir avec d'autres fichiers source afin d'obtenir de la modularisation.
Pour ce qui est de ces directives, peu importe que le code externe soit défini ou utilisé en assembleur ou dans un langage de haut niveau. Ainsi, par exemple, même une fonction définie dans un langage de haut niveau peut être utilisée dans du code x86-64 si elle est déclarée extern.
De la même manière, la convention d'appel est partagée par toutes ces fonctions, qu'elles soient définies en assembleur ou dans un langage de haut niveau, et même avec le système d'exploitation (OS).
Cependant, comme en assembleur une donnée n'est qu'une séquence d'octets, il est important de garder à l'esprit la manière dont le langage de haut niveau qui réalise l'interface gère la mémoire.
Le langage C est un choix courant de langage de haut niveau pour interagir avec du code assembleur. Il facilite l'interaction avec le système d'exploitation, en offrant des surcouches de haut niveau pour de nombreuses tâches importantes, comme afficher à l'écran, lire depuis un clavier ou allouer de la mémoire dynamique.
Dans ce parcours, les tests sont écrits en C, il est donc important d'en donner un aperçu général.
N'importe lequel des types indiqués ci-dessous peut être qualifié par const.
Cela les rend en lecture seule.
Il existe de nombreux types primitifs en C et leur taille, en octets, peut varier. Certains d'entre eux sont résumés dans le tableau suivant, avec leur taille typique dans un système x86-64 :
| type | nombre d'octets | entier/nombre à virgule flottante |
|---|---|---|
| _Bool | 1 | entier |
| char | 1 | entier |
| short | 2 | entier |
| int | 4 | entier |
| long | 4 ou 8 | entier |
| long long | 8 | entier |
| float | 4 | nombre à virgule flottante |
| double | 8 | nombre à virgule flottante |
Chacun de ces types entiers peut être signé (par défaut) ou non signé. Lorsqu'ils sont non signés, cela doit être précisé, par exemple : unsigned int.
Il existe des alias pour les types entiers, définis pour offrir un contrôle précis sur leur taille :
| alias | nombre d'octets |
|---|---|
| int8_t | 1 |
| int16_t | 2 |
| int32_t | 4 |
| int64_t | 8 |
Ces alias peuvent être préfixés par u pour indiquer un type non signé, par exemple : uint64_t.
D'autres alias méritent d'être signalés :
| alias | type |
|---|---|
| bool | _Bool |
| size_t | généralement un entier non signé de 8 octets |
Un _Bool, ou son alias bool, est particulier : bien qu'il occupe un octet entier, il ne peut prendre que l'une de deux valeurs, true (1) et false (0).
Les valeurs de ces types primitifs sont passées aux fonctions et renvoyées par elles selon les règles habituelles pour les entiers et les valeurs à virgule flottante.
Un enum est un type qui peut prendre l'un d'un certain nombre d'éléments indiqués explicitement :
enum example {
example_1,
example_2,
example_3,
example_4
};
En C, les enum sont implicitement convertis vers un type entier, généralement un int, c'est-à-dire un entier de 4 octets.
Par défaut, le premier élément possible d'un enum est converti en 0 et tous les éléments suivants sont convertis en l'entier suivant. Ainsi, dans l'enum example défini ci-dessus, example_1 aurait la valeur 0, example_2 aurait la valeur 1, et ainsi de suite.
Il est possible d'indiquer la valeur de n'importe quel élément d'un enum, et par défaut l'élément suivant prend alors l'entier suivant comme valeur :
enum example_2 {
example_1 = 4,
example_2 = 8,
example_3
}; // example_3 is equal to 9
En C, une adresse mémoire est référencée par un pointeur vers un type et notée avec l'opérateur *. Par exemple, int64_t * désigne un emplacement mémoire pour un ou plusieurs int64_t.
Les adresses sont traitées comme des entiers de 8 octets, comme d'habitude.
Les tableaux sont passés aux fonctions, et renvoyés par elles, sous forme de pointeurs vers leur premier élément.
int64_t example_arr[] = {1, 2, 3}; // this is an array of 3 signed 8-byte integers
fn(example_arr); // this is a function that passes a pointer (int64_t *) to the beginning of the array as an argument
Chaque élément d'un tableau a la taille du type des éléments de ce tableau. Le tableau example_arr défini ci-dessus, par exemple, contient 3 éléments de 8 octets chacun, soit 24 octets au total.
Comme les tableaux ne stockent pas leur longueur, une valeur distincte est généralement nécessaire pour indiquer le nombre d'éléments, à moins qu'une valeur sentinelle ne marque la fin.
Les strings en C sont un tableau de char, un type de 1 octet. La plupart des strings sont constituées de caractères ASCII et terminées par NUL : elles s'arrêtent donc lorsqu'un octet de valeur 0 est trouvé. Cela signifie que la longueur d'une string n'a généralement pas besoin d'être passée comme argument distinct.
Ton ami Chandler prévoit de visiter des pays exotiques aux quatre coins du monde. Malheureusement, Chandler n'est pas doué en maths. Il a bien peur de se faire arnaquer par des bureaux de change pendant son voyage, et il veut que tu lui fabriques un calculateur de devises.
Voici ses spécifications pour l'application.
Les fonctions de cet exercice sont déclarées dans un fichier source C avec les signatures fournies. Ton code en assembleur x86-64 doit respecter ces spécifications.
Tu as créé une enum currency_t pour garder la trace des différentes devises que Chandler utilise dans ses voyages :
enum currency_t {
GBP,
EUR,
JPY,
AUD,
BRL,
CNY,
CAD,
INR
};
Tu as parfois besoin d'afficher ces devises à l'écran pour vérifier qu'elles sont correctes.
Crée la fonction stringify_currency :
void stringify_currency(char *buffer, enum currency_t currency);
Cette fonction n'a pas de valeur de retour et doit stocker une string de style C à l'emplacement indiqué par buffer.
La string doit représenter visuellement le type de devise passé à la fonction :
char buffer[4];
stringify_currency(buffer, CAD);
// => "CAD"
Crée la fonction exchange_rate :
double exchange_rate(enum currency_t domestic_currency, enum currency_t foreign_currency, const double *value_in_US_dollars);
L'argument domestic_currency représente la devise locale de Chandler, à échanger contre une foreign_currency.
value_in_US_dollars est un pointeur vers un tableau de double qui contient la valeur d'une unité de chaque devise en dollars américains.
Les devises sont des indices dans ce tableau.
Ainsi, si EUR est un élément valide de enum currency_t, alors value_in_US_dollars[EUR] donne la valeur de 1 EUR en dollars américains.
Cette fonction doit renvoyer la valeur d'une unité de devise étrangère exprimée dans la devise locale.
const double value_in_US_dollars[8] = {
1.33329, 1.16443, 0.00654374, 0.650888,
0.185577, 0.140420, 0.714142, 0.0113863
};
exchange_rate(JPY, GBP, value_in_US_dollars);
// => 203.750454633
Crée la fonction get_value_of_bills :
uint64_t get_value_of_bills(unsigned long long denomination, unsigned short number_of_bills);
L'argument denomination est la valeur d'un seul billet, tandis que number_of_bills est le nombre total de billets.
Ce bureau de change ne traite qu'avec des espèces par multiples d'une certaine valeur. Le total que tu reçois doit être divisible par la valeur d'un « billet » ou d'une unité, ce qui peut laisser une fraction ou un reste. Ta fonction doit renvoyer uniquement la valeur totale des billets (sans les montants fractionnaires) que le bureau te rendrait. Malheureusement, le bureau garde le reste, ou la monnaie, en prime.
get_value_of_bills(5, 128);
// => 640
Crée la fonction get_number_of_bills :
unsigned int get_number_of_bills(float amount, unsigned long long denomination);
Cette fonction doit renvoyer le nombre de billets que tu peux recevoir pour le montant donné. Autrement dit : combien de billets entiers tiennent dans le montant de départ ? N'oublie pas : tu ne peux recevoir que des billets entiers, pas des fractions de billet, alors pense à diviser en conséquence. En pratique, tu arrondis à l'inférieur au billet entier le plus proche.
get_number_of_bills(127.5, 5);
// => 25
Crée la fonction exchangeable_value :
uint32_t exchangeable_value(float budget, double exchange_rate, uint8_t spread, unsigned long long denomination);
Le paramètre spread est le pourcentage prélevé au titre des frais de change, écrit sous forme d'entier.
Il faut le convertir en décimal en le divisant par 100.
Note que la marge s'ajoute au taux de change, de sorte qu'une marge de 10 donne un taux de change supérieur de 10 %.
Cette fonction doit renvoyer la valeur maximale de la nouvelle devise après avoir calculé le taux de change ajusté par la marge. Rappelle-toi que la coupure de la devise est un nombre entier et ne peut pas être subdivisée.
exchangeable_value(127.25, 0,873350884, 10, 20);
// => 120
exchangeable_value(127.25, 0,873350884, 10, 5);
// => 130
Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.