Assembleur Intel avec NASM sur machine virtuelle sans système d'exploitation
Date de publication : 20 mars 2012.
II. Un shell pour KarlOS
II.1. Du shell
II.2. De la ligne de commande
II.3. De l'analyseur
II.3.a. Trouver la fonction demandée
II.3.b. L'appeler
II.4. De la lecture de la chaîne
II.5. Du code
II. Un shell pour KarlOS
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à ?
II.1. Du shell
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 versions : 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.
II.2. De la 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
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).
II.3. De l'analyseur
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 :
- Trouver la fonction demandée
- L'appeler
II.3.a. Trouver la fonction demandée
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
db 13 ,26 ,31 ,0
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 videdb 0
Ca é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 :
- avant de parcourir la liste, indiquer le nombre d'éléments à lire,
- marquer la fin.
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 , 0
|
Comment ç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 :
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).
II.3.b. L'appeler
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 :
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
|
II.4. De la lecture de la chaîne
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 :
push eax
push ebx
push ecx
push edi
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 ]
cmp al , 110
jne .testTab
pop eax
push eax
cmp eax , edi
jae .attend_clavier
dec edi
mov al , 51
call afficheCaractere
sub word [curseur+ 2 ], 0x10
js .bordGauche
jmp .attend_clavier
.testTab :
cmp al , 111
je .tab
.testEntree :
cmp al , 112
je .fin_attend_clavier
test byte [touchesActives + 5 ], 0b0000100
jnz .shift
test byte [touchesActives + 6 ], 0b1000000
jz .RAS
.shift :
cmp al , 114
je .attend_clavier
cmp al , 115
je .attend_clavier
add al , 73
.RAS :
stosb
call afficheCaractere
.boucle :
loop .attend_clavier
.fin_attend_clavier :
xor al , al
stosb
mov al , 51
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
stosd
push ecx
mov eax , 51
mov ecx , 4
.afficheTab :
call afficheCaractere
loop .afficheTab
pop ecx
sub ecx , 4
jle .fin_attend_clavier
jmp .boucle
|
II.5. Du code
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...