<-Précédent Retour à l'accueil Contact : etienne"point"sauvage"at"gmail.com Suivant->
  1. Des machines virtuelles
  2. Du chargeur de tige de botte
  3. D'un secteur d'amorçage pour nous
  4. Passer par noyau planète
  5. Des 45 tours
  6. Du massacre de disque
  7. Du code

    Assembleur : et si on en avait encore moins ?

    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.

  1. Des machines virtuelles
  2. 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.

  3. Du chargeur de tige de botte.
  4. 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.

  5. D'un secteur d'amorçage pour nous.
  6. 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.

  7. Passer par noyau planète
  8. 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".

  9. Des 45 tours
  10. 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.

  11. Du massacre de disque
  12. 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.

  13. Du code
  14. Le code de ce chapitre se trouve ici :