<-Précédent | Retour à l'accueil | Contact : etienne"point"sauvage"at"gmail.com | Suivant-> |
Nous allons maintenant faire quelque chose qui va nous faire ressembler KarlOs à un vieux DOS des familles. Ouais, un vieux DOS ! Euh... C'était quoi, le DOS, déjà ?
Sur l'air des Acadiens :
Tous les unixiens, toutes les linuxiennes,
font du shell et pianotent des commandes.
Le shell, ou la coquille, est le "machin" qu'on manipule sur un ordinateur. Il y en a deux versoins : l'ILC (Interface en Ligne de Commande) et l'IUG (Interface Utilisateur Graphique), que les rosbifs appellent CLI et GUI respectivement. Les utilisateurs d'Unix et dérivés ainsi que les vieux de la Vieille connaissent l'ILC. Les blancs-becs et les Apple-eux jouent plus volontiers avec une IUG. Le DOS ne disposait que d'une ILC. L'IUG développée pour DOS s'appelle Windows, never forget. Donc, nous avons annoncé DOS-like, ce qui implique une ligne de commande.
Une ligne de commande commence par n'importe quoi et se termine par un appui sur la touche "Entrée". Soit, en gros, un appel à la fonction "litChaine". La ligne de commande est la boucle principale de KarlOS. Nous allons donc avoir quelque chose comme :
mov ecx, 0x00800 ;Taille maximum d'une ligne : 2048 caractères call malloc mov esi, edi princ: call litChaine mov edi, esi call analyseur jmp princ
On réserve 2048 octets pour stocker notre ligne de commande, et on lit. La source est égale à la destination, la destination est égale à la source, litChaine modifie la destination, on la recharge avec la source. Cette chaîne, on la donne à l'analyseur. Et on recommence, ad vitam aeternam (l'éternité se terminant par un appui sur un interrupteur).
La chaîne lue, on la passe à un analyseur, dont le rôle est de mettre en correspondance une chaîne de caractère et une fonction. Donc, deux étapes :
Pour trouver la fonction demandée, il faut déjà savoir quelles sont les fonctions dont nous disposons. Une fonction n'est pour nous qu'une simple adresse, or la fonction va être appelée par une chaîne de caractères. Hors de question d'entrer l'adresse de la fonction au clavier, il faut lui associer un nom.
Il nous faut d'abord l'ensemble des noms de fonctions :
CommandesDispos: db 30,11,12,22,15,0 ;"table" db 13,26,31,0 ;"cpu" db 0
On a défini deux fonctions : table et cpu. Peu importe ce qu'elles font actuellement. La liste se termine par une chaîne vide
db 0Ca évite d'avoir à indiquer quelque part la taille de la liste, car quand on lit une liste, le problème est toujours de savoir où on s'arrête. Pour résoudre ce problème, le monde n'a imaginé que deux et deux possibilités seulement :
Si vous trouvez une autre idée, gloire et confettis vous submergeront pour les siècles des siècles. En attendant, dans KarlOS, pour la liste des fonctions, on va marquer la fin.
On va ensuite faire un tableau de correspondance entre un nom et une adresse. Pour se faciliter la tâche, les adresses sont des étiquettes, et on va les stocker dans le même ordre que la liste, avec la même convention ( une adresse nulle à la fin ). Théoriquement, nul n'est besoin de cette adresse nulle supplémentaire, mais quand on aime, on ne compte pas. Et en l'occurence ça me facilite la vie. Voilà la liste des adresses :
AdressesCommandes: dd 0, 0, 0Comment ça, vide ? Oui, vide, parce que je ne sais pas faire faire des calculs à NASM à la compilation. Donc, à l'exécution, on va appeler une petite fonction :
chargeFonctions: push edi mov edi, AdressesCommandes mov eax, tabCaracteres stosd mov eax, CPUInfos stosd pop edi ret
On stocke chaque adresse, c'est pas la mort.
EDI contient une commande. Correspond-elle à une de nos fonctions ? Pour le savoir, il va falloir comparer. L'instruction miracle est cmpsb, complétée par un préfixe répéteur, repe. On compare la source à la destination et on avance d'un octet tant que les deux sont égaux. Cette instruction nécessite néanmoins d'avoir le nombre maximal d'itérations dans ECX. Le nombre maximal d'itérations, c'est bien évidemment la taille de l'une des chaînes de caractères. N'importe laquelle fera l'affaire, puisqu'au pire, on s'arrêtera au caractère nul terminal. Voici donc comment je compare deux chaînes :
call tailleChaineB jz .chaineVide repe cmpsb jz .appelFonction
Ah, oui calculer la taille d'une chaîne. Vous allez voir, je me suis foulé :
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; tailleChaineB ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ECX renvoie la longueur de la chaîne EDI ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; tailleChaineB: push edi push eax or ecx, 0xFFFFFFFF xor al, al repne scasb neg ecx dec ecx pop eax pop edi ret
Allez, réfléchissez, j'ai fait dans le bizarre. Tiquez, hurlez, ne restez pas les bras ballants, faites quelque chose, quoi !
Bon, alors, pourquoi est-ce que je n'ai pas appliqué la même technique dans la comparaison de chaîne et dans le calcul de la taille ? Le or ecx, 0xFFFFFFFF, mettant le nombre maximum d'itérations à l'infini (=232 - 1, comme il se doit) ?
Parce qu'en bon père de famille, je m'engage à occuper bourgeoise... pouf, pouf. Parce qu'en bon ingénieur, je prévois le futur, et je sais que je vais devoir comparer mon entrée avec la chaîne représentant la commande suivante : il faut donc que j'avance à la prochaine chaîne. Pour arriver à la prochaine chaîne, j'ajouterai la taille de la chaîne courante (la taille comprend le caractère nul final). Avec ce parcours, nous avons donc l'analyseur :
analyseur: pushad mov edi, AdressesCommandes call tailleChaineD mov eax, edi mov edi, CommandesDispos .boucle: push esi push ecx push eax call tailleChaineB pop eax jz .chaineVide repe cmpsb jnz .chaineVide jz .appelFonction .chaineVide: add edi, ecx pop ecx pop esi add eax, 4 loop .boucle .retour: popad ret
La fonction tailleChaineD retourne la taille d'une chaîne de doubles mots, c'est exactement tailleChaineB à deux variantes près (et encore, on pourrait limiter à une variante).
Par contre, maintenant, il nous faut les appeler. Ce qui est rigolo, c'est qu'on a ce qu'il est convenu d'appeler des pointeurs de fonction, ce qui donne des sueurs froides à bien des développeurs.
Dans la fonction ci-dessus, nous avons l'appel à l'appel de fonction. Relisez tranquillement, ça va aller. C'est le jz .appelFonction. Et attention, le code est super, mais super compliqué :
.appelFonction: call [eax] jmp .chaineVide
Les plus sagaces d'entre vous ont bien évidemment remarqué que ceci ne sert à rien. Les experts en sagacité ont incidemment remarqué que si deux fonctions portent le même nom, elles seront exécutées l'une après l'autre, vu que nous bouclons sur l'ensemble des fonctions à chaque fois. Bon, on supprime cette horreur pour avoir l'analyseur complet :
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Analyseur syntaxique ;; ;; ESI contient la chaîne à analyser ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; analyseur: pushad mov edi, AdressesCommandes call tailleChaineD mov eax, edi mov edi, CommandesDispos .boucle: push esi push ecx push eax call tailleChaineB pop eax jz .chaineVide repe cmpsb jnz .chaineVide call [eax] .chaineVide: add edi, ecx pop ecx pop esi add eax, 4 loop .boucle .retour: popad ret
Il faut retravailler notre belle fonction, afin qu'elle ne puisse stocker plus de caractères que la zone allouée. Sinon, c'est le débordement, et on se retrouve à écrire dans le code système, ce qui est universellement considéré comme mauvais.
Donc, dans ECX, le nombre de caractères à lire. Ca donne la boucle à effectuer.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; litChaine ;; ;;------------------------------------------------------------------;; ;; Ecrit dans la chaîne pointée par EDI les caractères écrits au ;; ;; clavier, jusqu'à un retour chariot. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; litChaine: push eax push ebx push ecx push edi ; Sauvegarde d'EDI pour ne pas remonter avant le début de la chaîne mov byte [derniereTouche], 0 .attend_clavier: cmp byte [derniereTouche], 0 jz .attend_clavier xor eax, eax mov al, [derniereTouche] mov byte [derniereTouche], 0 mov ebx, traductionASCII add ebx, eax mov al, [ebx] ; Le caractère est dans AL cmp al, 110 ;Traitement de l'espace arrière jne .testTab pop eax push eax cmp eax, edi jae .attend_clavier dec edi mov al, 51 call afficheCaractere sub word [curseur+2], 0x10 ; On recule le curseur de 2 caractères js .bordGauche jmp .attend_clavier .testTab: cmp al, 111 ;Traitement de la tabulation avant je .tab .testEntree: cmp al, 112 je .fin_attend_clavier test byte[touchesActives + 5], 0b0000100 ;On teste le shift gauche enfoncé jnz .shift test byte[touchesActives + 6], 0b1000000 ;On teste le shift droit enfoncé jz .RAS .shift: cmp al, 114 ;On ne stocke pas un Shift gauche comme touche je .attend_clavier cmp al, 115 ;On ne stocke pas un Shift droit comme touche je .attend_clavier add al, 73 .RAS: stosb call afficheCaractere .boucle: loop .attend_clavier .fin_attend_clavier: xor al, al ;Stocke une fin de chaîne stosb mov al, 51 ;Supprime le curseur à la fin de la ligne call afficheCaractere add word [curseur], 7 mov word [curseur+2], 0 dec edi .retour: pop ebx pop ecx pop ebx pop eax ret .bordGauche: mov ax, word [XResolution] sub ax, 8 mov word [curseur+2], ax xor ah, ah mov al, byte [lignesParCaractere] sub word [curseur], ax jmp .attend_clavier .tab: mov eax, (51<<24)+(51<<16)+(51<<8)+51 ; La tabulation avant est remplacée par 4 espaces dans la chaîne stosd push ecx mov eax, 51 mov ecx, 4 .afficheTab: call afficheCaractere loop .afficheTab pop ecx sub ecx, 4 jle .fin_attend_clavier ; Ca teste si ZeroFlag == 0 & Sign Flag <> Overflow Flag. Comme OF == 0 a priori jmp .boucle ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Fin litChaine ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
L'ensemble du code de ce chapitre se trouve ici :
Tuto14
Pour la prochaine fois, je vous montre quelque chose qu'un langage de haut niveau ne pourra jamais faire... Ce qui vous expliquera pourquoi le code de ce chapitre est différent de celui que vous venez de lire...