Sommaire
- Introduction
- Installation des outils nécessaires
- Le boot
- Les registres
- registres généraux
- registres d’index
- registres de segments
- registres de pointeurs
- Bootloader
- Afficher un message
- Accès disque
Introduction
Le langage assembleur est un langage de programmation bas niveau qui représente le langage machine sous une forme lisible. Le langage est propre à chaque processeur. Il est utilisé pour créer des applications légères et rapides. Dans cet article j’aborderais l’architecture des processeurs Intel ainsi que leur jeu d’instructions. Nous verrons par la suite comment réaliser un bootloader.
Installation des outils nécessaires
Afin d’éviter toute dégradation sur notre machine :P lors de nos essais, nous utiliserons Bochs, un émulateur open-source de processeur Intel 32 bits.
sudo apt-get install bochs
Ensuite il nous faut un assembleur, soit un logiciel qui permet de traduire le code assembleur en langage machine (binaire). Nous utiliserons nasm.
sudo apt-get install nasm
Le boot
Au démarrage le processeur de l’ordinateur démarre en mode réel (16 bits) pour des soucis de compatibilité avec les différents systèmes. Lors de l’initialisation de la carte mère le BIOS est chargé en RAM, ensuite le processeur vient exécuter ce code situé à l’adresse linéaire FFFF0h. Ce microcode permet d’initialiser les différents périphériques de l’ordinateur.
La ROM VGA initialise les fonctions graphiques, la mémoire vidéo est directement accessible via la RAM (VRAM). La table des vecteurs d’interruptions (IVT) est initialisée par le BIOS, elle permet de gérer les périphériques et les exceptions. Lorsque le CPU reçoit une interruption il déclenche la routine indiquée dans l’IVT pour traiter la demande.
Ensuite le BIOS va charger le code contenu dans le secteur de boot du périphérique choisi (disque dur,cdrom,disquette ou autres) en RAM puis l’exécuter. Ce code nommé bootloader doit tenir dans 1 seul secteur, soit 512 octets, il permet de charger ensuite le système d’exploitation ou un autre bootloader.
Les registres
Les registres permettent de stocker des valeurs pour différentes tâches (calcul, adressage …). Ils sont en nombre limité dans le CPU mais sont accessibles très rapidement.
Les registres généraux
Les registres généraux sont utilisés principalement pour faire des calculs.
bits 0 - 31 | EAX | EBX | ECX | EDX |
---|---|---|---|---|
bits 0 - 15 | AX | BX | CX | DX |
bits 8-15 | AH | BH | CH | DH |
bits 0-7 | AL | BL | CL | DL |
Chacun des 4 registres généraux possède sa particularité :
- Le registre EAX, à l’origine registre Accumulateur, est utilisé en registre de travail par défaut.
- Le registre EBX, à l’origine registre de Base, est utilisé comme référence pour des accès tableau.
- Le registre ECX, à l’origine registre de Compteur, est utilisé pour les instructions répétitives et les opérations de décalage logiques.
- Le registre EDX, est utilisé comme registre de Données, extension du registre EAX, pour former le registre virtuel EDX:EAX il est aussi utilisé comme index lors des accès aux port d’entrées/sorties.
mov eax,5 ; placer 5 dans eax
mov ebx,0x1000 ; placer 0x1000 dans ebx
add ebx,eax ; additionner eax et ebx, le résultat est sauvé dans ebx
Les registres d’index
Les registres d’index sont comme des registres généraux mais il n’y a pas de sous-parties 8 bits. Ils servent d’index lors de l’accès à des chaînes de données.
mov esi,source ; pointeur vers la source
mov edi,dest ; pointeur vers la destination
mov al,[esi] ; lire l'octet à source
mov [edi],al ; écrire l'octet à destination
Les registres de pointeurs
Les registres de pointeurs sont des registres à usage implicite. Ils pointent vers un endroit précis dans la RAM.
bits 0 - 31 | ESP | EBP | EIP |
---|---|---|---|
bits 0 - 15 | SP | BP | IP |
Les registres de segments
L’accès à la mémoire RAM se fait via des registres de segments. Le bus d’adresse en mode réel est de 20 bits, on peut adresser au maximum 1 mégaoctets. La valeur d’un registre de segment est multiplié par 16 pour donner une adresse de base sur 20 bits à laquelle on ajoute un offset.
Registre | Nom | Description |
---|---|---|
CS | Code Segment | Segment de code |
DS | Data Segment | Segment de données |
ES | Extra Segment | Segment de données supplémentaire |
FS | Frame Segment | Segment de données à usage général |
GS | General Segment | Segment de données à usage général |
SS | Stack Segment | Segment de pile |
mov [esp],eax ; mov [ss:esp],eax utilisation implicite du segment de pile
mov al,[bx] ; mov al,[ds:bx] utilisation implicite du segment de données
mov ebx,[cs:eax] ; utilisation du segment cs a la place de ds
On ne peut pas mettre une valeur directement dans un registre de segment il faut passer par un autre registre à usage général,
mov ax,0x700
mov ds,ax
ou via la pile,
push 0x700
pop ds
Maintenant que nous avons quelques bases sur l’assembleur il est temps de te mettre au travail jeune padawan :P .
Bootloader
Le bootloader, c’est quoi ? Eh bah c’est un morceau de code qui va permettre de charger un autre programme plus lourd, comme un noyau de système d’exploitation. La particularité de cette petite pépite informatique est qu’elle doit tenir sur le secteur de boot du périphérique choisi, soit 512 octets.
Ce secteur est chargé en mémoire à l’adresse 0000:7C00. La première chose à faire est d’initialiser les différents registres segments. Car les données seront toutes référencées à partir de l’adresse 0000:7C00. Keep Calm and look this assembly code ;) .
[BITS 16] ; on travaille en 16 bits
[ORG 0x0] ; indique l'offset à ajouter à toutes les adresses référencées
mov ax,0x7C0 ; initialisation des segments de données en 0x7C00 (0x7C0 * 16)
mov ds,ax
mov es,ax
mov ax,0x8000 ; initialisation de la pile en 0x80000
mov ss,ax
mov sp,0xf000 ; sommet de la pile en 0x8F000
Les segments sont maintenant initialisés, on va déclarer une chaîne de caractères puis l’afficher, pour cela il faut créer une procédure “afficher” par exemple, histoire de pouvoir la réutiliser n’importe où dans le code. L’adresse de notre chaîne sera dans le registre si.
afficher:
push ax ; sauvegarde du registre ax
push bx ; sauvegarde du registre bx
.debut:
lodsb ; charge dans al l'octet pointé par le coule ds:si et incrémente si
cmp al,0 ; fin de chaîne ?
jz .fin
mov ah,0x0E ; numéro du service à appeler 0E --> afficher un caractère
mov bx,0x07 ; attribut du caractère
int 0x10 ; interruption bios (service vidéo)
jmp .debut
.fin:
pop bx ; restauration du registre bx
pop ax ; restauration du registre ax
ret
Cette procédure fait appel à un service du bios via une interruption. L’attribut du caractère définit la couleur de fond et du caractère.
Maintenant que nous avons la fonction, il est temps de définir notre petite strings ;P .
msg: db "Call me babe :P",13,0
La chaîne se termine par un saut de ligne et un octet null. Appelons notre fonction avec le code suivant :
mov si,msg
call afficher
Ensuite on laisse tourner la bebête ;) .
next:
jmp next
C’est presque fini ! Mais il y a une contrainte supplémentaire il faut que le code fasse exactement 512 octets, et pour être un bootloader valide le secteur de boot doit terminer par 0xAA55. Insérons le code suivant à la fin.
times 510-($-$$) db 0x90 ; remplir l'espace restant avec des bytes mis par défaut a 0x90
dw 0xAA55 ; mot magique
; 510 + taille du mot magique => 512 octets
Now it’s time to assemble,
nasm -f bin -o bootsect bootsect.asm
cat bootsect /dev/zero | dd of=floppyA bs=512 count=2880
La première commande assemble le code (traduction en binaire), puis la deuxième créée une image brute de 2880 secteurs de 512 octets. Le secteur de boot est placé au début (cat bootsect) et le reste est rempli de zéro (/dev/zero).
Ensuite la commande suivante démarre bochs et boot sur la disquette que l’on a créée précédemment.
$ bochs 'boot:a' 'floppya: 1_44=floppyA, status=inserted'
Pour démarrer la simulation dans bochs appuyer sur 6, puis c dans le terminal (et non la machine virtuelle).