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 :