<-Précédent | Retour à l'accueil | Contact : etienne"point"sauvage"at"gmail.com | Suivant-> |
On a écrit à l'écran, dessiné, lu l'entrée clavier. Nous l'avons fait sans utiliser les facilités fournies par le système d'exploitation. Mais nous utilisons toujours le système d'exploitation, parce que nous lui demandons l'exécution de notre programme. Il nous modifie notre fichier, c'est le fameux org 256 que nous devons écrire pour que nos adresses restent valides dans notre programme. Et bien, ça aussi, je n'en veux plus.
Pour que le système d'exploitation n'existe plus, un bon moyen est le "format c:". C'est très bien, mais très gênant, puisque finalement, ça supprime tout, et donc aussi notre éditeur de texte pour écrire notre programme. Et sans éditeur, nous ne pourrons pas écrire quoi que ce soit, et donc il sera impossible de refaire fonctionner l'ordinateur. C'est une mauvaise idée. Mais il se trouve que beaucoup de gens ont besoin d'un ordinateur avec rien dessus. Et à l'ère d'internet, si beaucoup de gens ont besoin de quelque chose, alors quelqu'un l'a fait. Ca s'appelle une machine virtuelle, c'est un logiciel qui simule un ordinateur sur votre ordinateur. Oui, le concept est étrange, mais c'est bien pratique. Il en existe plusieurs, prenez celui que vous voulez tant que l'on peut y configurer un PC sans système d'exploitation. Personnellement, j'ai tenté ces deux-là :
VirtualBox est plus sympa à utiliser que Bochs, mais Bochs a aussi ses avantages, c'est même pour ça que les deux existent.
Ce qu'il nous faut, nous, c'est un PC avec un lecteur de disquettes avec une disquette dedans. La disquette sera de préférence un fichier ".img" mais c'est au choix. L'idée est d'avoir un ordinateur, même faux, sur lequel on va pouvoir jouer autant qu'on veut sans pour autant tout casser.
Au démarrage, un ordinateur a un gros problème : il doit charger en mémoire un programme qui lui permettra de lancer d'autres programmes, qui éventuellement, lanceront d'autres programmes, etc. Mais le premier programme, comment se charge-t-il en mémoire ?
C'est un peu le problème de la poule et de l'oeuf...
L'ordinateur va utiliser la technique du baron de Münchausen : alors que le baron allait se noyer, il appelle à l'aide. Comme personne ne répond, le baron se trouve contraint, pour se sauver, de se tirer lui-même de l'eau par ses tiges de botte. En anglais, ça se dit "bootstrap". En français, on est moins poétique et on appelle le "bootstrap loader" : le chargeur de secteur d'amorçage.
Le BIOS d'un PC va, au démarrage, chercher, sur certains périphériques, un bloc de 512 octets qui se termine par la séquence 0x55AA. 0x55AA est un nombre magique : ça n'a aucune raison particulière d'être ce nombre-là plutôt qu'un autre, simplement, il en faut un, alors, on en sort un du chapeau. Et sortir du chapeau, c'est magique.
Ce bloc de 512 octets, il va le mettre à l'adresse 0x07C0:0000, et lui donner la main. C'est comme ça. C'est 0x07C0:0000 et puis c'est marre.
Ce bloc de 512 octets, nous l'allons écrire.
Bien sûr, on peut utiliser des secteurs d'amorçage déjà faits. Mais mon but est de partir du plus bas. Il me faut donc le mien.
Alors, hardi, compagnons, portons haut la flamberge et écrivons :
org 0x0000 ; On n'a pas de décalage d'adresse jmp 0x07C0:debut ; On est chargé dans le segment 0x07C0 debut: ; Met les segments utiles au segment de code courant mov ax, cs mov ds, ax call detect_cpu initialise_disque: ; Initialise le lecteur de disque xor ax, ax
Ici, une parenthèse : on est censé donner à l'interruption 0x13 l'identifiant du disque sur lequel on est chargé via le registre DL. Or, il se trouve que le BIOS, décidément conçu par des gens qui ne veulent pas s'embêter, nous donne le lecteur sur lequel il nous a trouvé dans le registre DL. Comme notre programme sera sur le même disque, on n'a rien à changer. Fin de la parenthèse.
int 0x13 jc initialise_disque; En cas d'erreur on recommence (sinon, de toute façon, on ne peut rien faire) lire:
De nouveau une parenthèse : on va charger un autre programme que le secteur d'amorçage en mémoire. Où allons-nous le mettre ? Pour l'instant, où on veut, on est l'chef, après tout. 0x1000:0000 n'a pas l'air trop mal en première approximation. Et on va charger 5 secteurs, parce que notre programme fera moins de 2,5 ko. Ah oui, c'est comme ça.
mov ax, 0x1000 ; ES:BX = 1000:0000 xor bx, bx mov es, ax mov ah, 2 ; Fonction 0x02 : chargement mémoire mov al, 6 ; On s'arrête au secteur n° 6 xor ch, ch ; Premier cylindre (n° 0) mov cl, 2 ; Premier secteur (porte le n° 2, le n° 1, on est dedans, et le n° 0 n'existe pas) ; Ca fait donc 5 secteurs xor dh, dh ; Tête de lecture n° 0 ; Toujours pas d'identifiant de disque, c'est toujours le même. int 0x13 ; Lit ! jc lire ; En cas d'erreur, on recommence mov si, sautNoyau ; Un petit message pour rassurer les troupes. call affiche_chaine jmp 0x1000:0000 ; Et on donne la main au programme que nous venons de charger
Alors, pour la fine bouche : on va faire de l'affichage et du test. D'abord, on va tester le processeur, car depuis qu'on a commencé, j'ai appris des trucs : pour savoir si on a à faire à un 8086, 80286 ou 80386, on teste certains bits du registre de drapeaux.
detect_cpu: mov si, processormsg ; Dit à l' utilisateur ce qu'on est en train de faire call affiche_chaine mov si, proc8086 ; De base, on considère qu'il s'agit d'un 8086 pushf ; sauvegarde les valeurs originales des drapeaux ; teste si un 8088/8086 est présent (les bits 12-15 sont à 1) xor ah, ah ; Met les bits 12-15 à 0 call test_drapeaux cmp ah, 0xF0 je finDetectCpu ; 8088/8086 détecté mov si, proc286 ; On considère qu'il s'agit d'un 286 ; teste si un 286 est présent (les bits 12-15 sont effacés) mov ah, 0xF0 ; Met les bits 12-15 à 1 call test_drapeaux jz finDetectCpu ; 286 détecté mov si, proc386 ; aucun 8088/8086 ou 286, donc c'est un 386 ou plus finDetectCpu: popf ; restaure les valeurs originales des flags call affiche_chaine ret test_drapeaux: push ax ; copie AX dans la pile popf ; Récupère AX en tant que registre de drapeaux. Les bits 12-15 sont initialisés pour le test pushf ; Remet le registre de drapeaux sur la pile pop ax ; Les drapeaux sont mis dans AX pour analyse and ah, 0xF0 ; Ne garde que les bits 12-15 ret
Et maintenant, la culte routine d'affichage en mode texte
affiche_chaine: push ax push bx push cx push dx xor bh, bh; RAZ de bh, qui stocke la page d'affichage mov ah, 0x03 int 0x10; appel de l'interruption BIOS qui donne la position du curseur, stockée dans dx mov cx, 1; nombre de fois où l'on va afficher un caractère affiche_suivant: lodsb or al, al;on compare al à zéro pour s'arrêter jz fin_affiche_suivant cmp al, 13 je nouvelle_ligne mov ah, 0x0A;on affiche le caractère courant cx fois int 0x10 inc dl; on passe à la colonne suivante pour la position du curseur cmp dl, 80 jne positionne_curseur nouvelle_ligne: inc dh; on passe à la ligne suivante xor dl, dl; colonne 0 positionne_curseur: mov ah, 0x02;on positionne le curseur int 0x10 jmp affiche_suivant fin_affiche_suivant: pop dx pop cx pop bx pop ax ret ;fin de affiche_chaine proc8086: db '8086', 13, 0 proc286: db '286', 13, 0 proc386: db '386', 13, 0 processormsg: db 'Test du processeur : ', 0 sautNoyau: db 'Saut au noyau', 13, 0
Petit souci : notre secteur d'amorçage doit se terminer par 0x55AA, qui est un mot et qui doit donc être à la fin des 512 octets. Pour le mettre à la fin, nous allons simplement remplir les octets libres jusque 510 avec des 0. Cela fait donc 510 octets, moins l'adresse du dernier octet de code, moins l'adresse du premier octet de la section (qui est égale à 0, mais là n'est pas le problème). NASM fournit '$' comme étant l'adresse du début de la ligne de code courante, et '$$' comme l'adresse de la première instruction de la section. NASM fournit 'times', qui répète ce qui le suit un certain nombre de fois. On utilise times 510 - taille de notre code fois pour écrire 0, et le tour est joué.
times 510-($-$$) db 0 dw 0xAA55 ; Le nombre magique écrit à l'envers parce que M. Intel est grosboutiste, ce qui signifie qu'il inverse les octets d'un mot.
Nous avons maintenant notre secteur d'amorçage, à compiler avec :
nasm -o amorce.com amorce.asm
Et on ne le lance pas bêtement. On attend d'avoir fait la suite.
La suite est un autre programme : nouveau fichier, page blanche... Noooon... pas page blanche, on va être un peu malins, on va partir du chapitre précédent. Simplement, on va, au départ, initialiser les segments utilisés par notre programme. Comme tout est dans un seul segment, on va les initialiser avec la valeur de CS, le segment de code, qui est bon, puisqu'il a été initialisé par le secteur d'amorçage. Et pour ne pas s'emberlificoter, on va utiliser un autre segment pour la pile. Au pif, encore une fois.
Ensuite, techniquement, nous développons ce qu'il est convenu d'appeler un noyau de système d'exploitation. Alors, bon, c'est pas vraiment un noyau, mais on va faire comme si. Et faire comme si, ça veut dire que notre programme ne rend jamais la main. D'ailleurs, à qui la rendrait-il ? Au secteur d'amorçage, qui a fini son travail il y a bien longtemps ? Non. Il ne rendra pas la main. Ainsi, au lieu du "ret" final, ce sera "jmp $", qui fait une boucle infinie. D'autre part, il n'y aura pas d'en-tête du système d'exploitation, vu que le système d'exploitation, c'est lui. Comme le secteur d'amorçage, nous aurons "org 0".
Comment va-t-on utiliser notre programme ?
Conceptuellement, nous allons en faire une disquette de démarrage, oui, une disquette dite "de boot" ! On compile notre programme normalement. Ensuite, on copie bout à bout notre secteur d'amorçage et notre programme dans un seul fichier. Sous Windows, DOS et Cie, la commande est :
copy amorce.com/B+programme.com/B disk.img /Y
"/B" signifie qu'il s'agit d'un fichier binaire, ce qui évitera au DOS de le modifier. "/Y" évite d'avoir la confirmation de réécriture qui ne manque pas d'arriver dès la deuxième fois.
Sous système Unix, je ne connais pas la commande.
Le fichier copié est la disquette de démarrage.
Dans le code assembleur, on termine le fichier comme pour le secteur d'amorçage, avec un "times 4096 - ($ - $$) db 0". Cela sert le même objectif : que le fichier compilé fasse un nombre entier de secteurs de disquette.
Il ne reste plus qu'à configurer le PC virtuel pour qu'il lise le fichier généré comme une disquette : soit on lui donne une vraie disquette qui ne contient que notre fichier, soit on lui précise que la disquette est en fait un fichier image.
Il ne reste plus qu'à démarrer la machine virtuelle, et voilà ! On retrouve notre programme.
Notre programme a été lancé sur une machine virtuelle, et surtout, qu'il en soit ainsi pour le moment : il y a un morceau de code qui écrit sur le premier disque dur. Si on l'exécutait sur un vrai ordinateur, on écrirait là où est le système d'exploitation, ce qui est toujours une mauvaise idée. Ce petit bout de code ne fait que recopier les premiers secteurs de la disquette sur les premiers secteurs du disque dur. En enlevant la disquette de la machine virtuelle, la machine virtuelle démarre toujours, et exécute notre programme.
Le code de ce chapitre se trouve ici :