Parcours
/
x86-64 Assembly
x86-64 Assembly
/
Exercices
/
Palette de couleurs
Palette de couleurs

Palette de couleurs

Exercice d'apprentissage

Introduction

Mémoire

La mémoire est généralement organisée pour un programme par le système d'exploitation (OS) selon une disposition générale :

adresse région mémoire
haute pile
...
tas
segment lecture-écriture
segment code/lecture seule
basse réservée

La mémoire en segments est organisée en sections, avec différentes permissions.

Les fonctions que l'on a définies jusqu'à présent se trouvaient toutes dans la section .text. Cette section contient des données exécutables en lecture seule. D'autres sections servent à déclarer des variables de données, qui peuvent être en lecture seule ou en lecture-écriture, mais qui ne sont pas exécutables.

Section .data

Les données initialisées sont déclarées dans la section .data.

Dans NASM (The Netwide Assembler, l'assembleur utilisé par ce parcours), une variable initialisée possède un nom, une directive qui indique la taille des données, et une liste de valeurs séparées par des virgules. Chacun de ces éléments est séparé des autres par un espace, et l'étiquette peut éventuellement être suivie d'un :.

Les principales directives et leurs tailles de données associées sont :

directive taille
db 1 octet
dw 2 octets
dd 4 octets
dq 8 octets

Par exemple, ceci déclare une variable d'un octet nommée space avec la valeur 10 :

section .data
    space db 10

Les variables déclarées dans section .data sont mutables, c'est-à-dire qu'elles sont en lecture-écriture. Elles ont aussi une durée de stockage statique, ce qui signifie qu'elles existent pendant toute la durée d'exécution du programme.

Section .rodata

La section .rodata est similaire à section .data. Les deux sections contiennent des données initialisées, qui sont déclarées de la même manière et ont la même durée de stockage.

La principale différence entre elles est que les données dans section .rodata sont immuables, c'est-à-dire en lecture seule.

Note

Les constantes définies avec equ sont différentes de celles définies dans section .rodata.

Une constante définie avec equ n'occupe pas d'espace en mémoire et est directement remplacée par sa valeur par l'assembleur. Il s'agit en fait d'un espace réservé pour cette valeur.

En revanche, les constantes définies dans section .rodata sont réellement stockées en mémoire, et possèdent une adresse.

Accès aux données

Étiquettes et indirection

Les données déclarées doivent avoir un nom qui leur est associé. On appelle ce nom une étiquette.

Une étiquette est un symbole qui encode l'adresse spécifique des données en mémoire. Les adresses en x86-64 sont des valeurs de 64 bits.

Dans NASM, essayer d'accéder directement aux données avec leur étiquette ne donne pas la mémoire allouée, mais son adresse :

section .data
    example dq 27 ; this declares a 8-byte variable initialized with 27

section .text
fn:
    mov rax, example ; this stores the address of the declared variable in rax, not its contents
    ...

Pour accéder au contenu d'une adresse mémoire, il est nécessaire de la déréférencer. C'est ce qu'on appelle l'indirection.

Dans NASM, cela se fait avec [] :

section .data
    example dq -27 ; this declares a 8-byte variable initialized with -27

section .text
fn:
    mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
    ...

Cependant, il existe certaines situations où il peut y avoir une ambiguïté concernant la taille de la mémoire déréférencée. Dans ces cas, un préfixe spécifiant cette taille doit être utilisé.

Voici les préfixes les plus importants et leurs tailles dans un programme x86-64 typique :

préfixe taille
byte 1 octet
word 2 octets
dword 4 octets
qword 8 octets

Le même chargement peut être écrit avec la taille indiquée explicitement :

    mov rax, qword [example] ; same dereference, size stated explicitly

C'est une bonne pratique d'utiliser toujours un préfixe lorsqu'on déréférence de la mémoire.

Écriture en mémoire

L'écriture en mémoire se fait de la même manière, en déréférençant une adresse :

section .data
    example1 db 10            ; example1 is a 1-byte memory location initialized with value 10
    example2 dq -456          ; example2 is a 8-byte memory location initialized with value -456
    example3 dd 54            ; example3 is a 4-byte memory location initialized with value 54

section .text
fn:
    mov byte [example1], 20   ; example1 now has value 20
    mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
    mov dword [example3], eax ; example3 now has value equal to the contents in eax

Note que l'on peut utiliser des opérandes mémoire dans la plupart des instructions sans d'abord charger le contenu dans un registre. Cependant, il n'est généralement pas possible de les utiliser à la fois dans l'opérande source et l'opérande destination, mais seulement dans l'un des deux :

section .data
    example4 dw 4
    example5 dq -8
    example6 dd 15

section .text
fn:
    add word [example4], 5     ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
    imul rax, qword [example5] ; rax = rax * (-8)
    ; this is not possible -> sub dword [example6], dword [example6]
L'instruction LEA

Bien qu'un mov puisse être utilisé pour stocker l'adresse d'une variable dans un registre, il existe une instruction conçue spécifiquement pour cela : lea.

Cette instruction utilise un opérande sous forme mémoire, mais elle ne lit pas la mémoire. À la place, elle calcule l'expression d'adresse effective et écrit le résultat dans l'opérande de destination :

lea rax, [example] ; this stores the address of 'example' in rax

Il est plus idiomatique d'utiliser lea pour calculer et stocker des adresses mémoire dans des registres.

Adressage relatif

Lors de l'accès à des emplacements mémoire, le comportement par défaut de NASM est de générer des adresses absolues, c'est-à-dire des adresses mémoire fixes.

Pour des raisons de sécurité, les exécutables sont souvent construits en PIE (Position Independent Executable), où les régions mémoire sont placées à des emplacements aléatoires. Dans un PIE, l'adresse finale d'une variable n'est pas connue au moment de l'édition de liens. Ainsi, le code calcule plutôt les adresses comme un décalage par rapport à la valeur d'un registre spécial appelé rip, qui pointe vers la prochaine instruction à exécuter.

C'est ce qu'on appelle généralement l'adressage relatif à RIP.

Dans NASM, on peut demander un accès relatif à RIP avec l'opérateur rel :

mov rax, qword [rel variable]

L'adressage relatif peut aussi être défini par défaut pour un fichier source avec default rel en haut.

Tous les exercices de ce parcours sont compilés et liés en PIE, donc rel doit être utilisé pour générer des adresses relatives.

Visibilité

Les étiquettes (fonctions et données) définies dans n'importe quelle section (par exemple .text, .data, .rodata) sont visibles dans le même fichier source. Si elles sont déclarées global, elles sont aussi visibles par les autres fichiers source.

Inversement, les étiquettes définies dans d'autres fichiers source sont visibles pour le fichier source courant si elles sont déclarées extern. Dans ce cas, il n'y a pas d'indication de taille des données dans l'assembleur ; cette taille doit être connue à l'avance.

default rel

section .data

global number1 ; 'number1' is a variable visible to other source files
number1 db 200

extern number2 ; 'number2' is a variable visible to the current source file, but defined in another

section .text

extern sum ; sum is a function visible to the current source file, but defined in another

fn:
    mov dil, byte [number1]
    mov sil, byte [number2]
    call sum
    ...

Instructions

Ton ami José est enseignant dans une école locale. Il a eu l'idée d'expériences amusantes pour montrer comment on peut combiner les couleurs afin d'en produire d'autres.

Il t'a demandé de l'aider pour ces expériences.

Note

Dans cet exercice, une couleur est représentée par un nombre de 32 bits (4 octets), qui encode sa valeur RGB.

Une valeur RGB se compose de 3 canaux, rouge, vert et bleu, qui occupent chacun 8 bits (1 octet). Le quatrième octet est généralement réservé au canal Alpha, mais dans cet exercice sa valeur sera vide (0).

1. Obtiens la valeur RGB d'une couleur

Les valeurs de chaque couleur sont déjà stockées dans une table, définie dans un autre fichier source. Une couleur est identifiée par une adresse unique dans cette table.

Définis une fonction get_color_value qui renvoie la valeur de 32 bits d'une couleur. Cette fonction prend en paramètre une adresse valide de cette couleur dans la table des couleurs.

get_color_value(black)
// => 0

Indice : 32 bits équivalent à 4 octets.

2. Ajoute une couleur de base

Pour mélanger différentes couleurs, José va d'abord fixer une couleur de base, puis ne changer que la couleur secondaire avec laquelle elle est combinée.

Définis une fonction add_base_color qui enregistre la valeur de 32 bits d'une couleur dans la variable base_color, afin qu'elle puisse être utilisée plus tard. Cette fonction ne renvoie pas de valeur et prend en paramètre l'adresse de la couleur dans la table des couleurs.

C'est toi qui définis la variable base_color, et elle doit être accessible depuis d'autres fichiers source.

Il n'y aura jamais plus d'une couleur de base à la fois. Si une nouvelle couleur de base est ajoutée, l'ancienne est abandonnée.

Par défaut, au démarrage du programme, base_color doit être initialisée avec la valeur de 32 bits du blanc, qui est 0xFFFFFF00.

Indice : NASM accepte les nombres définis en hexadécimal avec 0x au début, comme dans 0xFFFFFF00.

3. Définis des constantes pour les couleurs primaires

José compte faire de nombreuses combinaisons avec les couleurs primaires ; il veut donc les avoir séparées pour y accéder rapidement. Comme il utilise RGB pour représenter les couleurs, les couleurs primaires sont :

  • RED, avec la valeur 0xFF000000.
  • GREEN, avec la valeur 0x00FF0000.
  • BLUE, avec la valeur 0x0000FF00.

Définis une constante pour chacune de ces couleurs. Ces constantes doivent être accessibles depuis d'autres fichiers source.

4. Combine les couleurs

Les couleurs doivent être combinées selon une combining_function définie dans un autre fichier source. Cette fonction prend en paramètres les valeurs de 32 bits de base_color et d'une couleur secondaire à mélanger avec elle. Elle renvoie la valeur de 32 bits de la couleur combinée.

Définis une fonction make_color_combination qui combine deux couleurs et enregistre le résultat en mémoire. Cette fonction ne renvoie pas de valeur et prend en paramètres, dans cet ordre :

  • L'adresse où la valeur de 32 bits de la couleur combinée doit être stockée.
  • L'adresse d'une couleur secondaire dans la table des couleurs, à combiner avec la couleur de base.
Caution

Remarque que combining_function peut modifier les valeurs des registres que tu utilises. Pense à enregistrer en mémoire toute variable dont tu as besoin avant d'appeler la fonction.

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

Prêt à commencer Palette de couleurs ?

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