Assembleur et bootloader

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.

Liste des registres généraux
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
EIP pointe sur l'instruction en cours. Il ne peut pas être manipulé directement, ni en lecture, ni en écriture. Le registre ESP pointe sur le sommet de la pile, et EBP la base de la pile. Voici quelques instructions utilise la pile : ```x86asm mov [esp],8 ; modifier le sommet de la pile push eax ; empiler le contenu de eax sur la pile pop ebx ; dépiler, la valeur contenue au sommet est copié dans ebx ```

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
Toute action avec la mémoire implique l'utilisation d'un registre de segment,
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).