IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

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 ;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).


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 ;"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 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													;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 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).


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 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

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														;;
;;------------------------------------------------------------------;;
;; 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													;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

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...

 

Valid XHTML 1.0 TransitionalValid CSS!