VI. Assembleur : et si on en avait encore moins ?▲
Relu par ClaudeLELOUP.
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.
VI-1. Des machines virtuelles▲
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 de l'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.
VI-2. Du chargeur de tige de botte▲
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.
VI-3. D'un secteur d'amorçage pour nous▲
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.
VI-4. Passer par noyau planète▲
La suite est un autre programme : nouveau fichier, page blanche... Noooon... pas page blanche, on va être un peu malin, 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".
VI-5. Des 45 tours▲
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.
VI-6. Du massacre de disque▲
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.
VI-7. Du code▲
Le code de ce chapitre se trouve ici :