<-Précédent Retour à l'accueil Contact : etienne"point"sauvage"at"gmail.com Suivant->
  1. Du shell
  2. De la ligne de commande
  3. De l'analyseur
  4. De la lecture de la chaîne
  5. Du code

    Assembleur : 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à ?

  1. Du shell
  2. 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.

  3. De la ligne de commande
  4. 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).

  5. De l'analyseur
  6. 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 :

    1. Trouver la fonction demandée
    2. L'appeler

    1. Trouver la fonction demandée
    2. 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 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 :

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

    3. L'appeler
    4. 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
      

  7. De la lecture de la chaîne
  8. 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													;;
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    

  9. Du code
  10. 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...