Exemples en assembleur Intel x86

Un article de Haypo.

Retour à la page précédente Retour à l'assembleur

Sommaire

[modifier] Lecture d'une touche au clavier

[modifier] Principe

L'interruption matérielle 16h gère le clavier. Elle possède deux fonctions intéressantes :

  • 00h : Lecture d'une touche;
  • 01h : Vérification de la présence d'une touche dans le tampon clavier.

Mais si une touche est étendue (touches fléchées, « page haut », etc.), il faudrait appeler deux fois la fonction 00h pour obtenir le code de la touche : la première fois, un code null (00h) est renvoyé, la seconde fois le code étendu de la touche. Sachant que le code ASCII est toujours inférieur à 128, on pourra ajouter 128 aux codes étendus pour, au final, n'avoir à appeler notre nouvelle fonction qu'une seule fois.

[modifier] Solution Turbo Pascal

(uses Dos;)
const MasqueFlagsCF = 1; { Masque pour isoler le bit CF des flags } (1)

function TouchPresse : Boolean;
var Regs: Registers; (2)
begin
   with Regs do begin
      AH := $01; { Fonction 01h: Vérification de la présence d'une touche }
      Intr ($16,Regs); (3)
      TouchPresse := (Flags and MasqueFlagsCF = MasqueFlagsCF); (1)
   end;
end;

function LitTouche : Char;
var Regs: Registers; 
begin
   with Regs do begin
      AH := $00; { Fonction 00h : Lecture d'une touche }
      Intr ($16,Regs);
 
      if AL=0 then (4)
         LitTouche := Char(AH or 128) (5)
      else
         LitTouche := Char(AL);
   end;
end;

Analyse :

  • (1) - Le type Registre
  • (2) nous met à disposition les [#Flags Flags], met dans un mot d'ensemble (Word). Pour isoler le flag [#ListeFlag CF] (bit #0), on utilisera le truc : "if Flags and MASQUE = MASQUE" où MASQUE est une puissance de 2, la puissance est la position du bit (en partant de zéro). CF étant à la position 0 (bit #0), le MASQUE est donc 1 (2^0).(2) - Pour appeler une interruption, Turbo Pascal nous met à disposition (par l'unité DOS) le type "Registers" qui permet d'accéder de façon virtuelle aux registres. Virtuelle car celà ne modifie par directement les registres, les registres sont tous envoyés comme "paramètre", puis sont lus après l'appel de "Intr" (3) (appelle d'une l'interruption).
  • (3) - Enfin pour appeler une interruption, on utilise la fonction "Intr" avec comme paramètre le numéro de l'interruption et les registres. Ici on utilise la fonction 00h de l'interruption 16h. Celle-ci retourne dans AL le code ASCII de la touche et AH contient le code étendu de la touche (si AL=0).
  • (4) - Pour savoir si une touche est étendue, on vérifie que AL = #0.
  • (5) - Pour ajouter 128 à la valeur de la touche étendue, on peut également faire "OR 128", celà pose dans tous les cas le bit #7 de AH. Ca ne fonctionne que pour les puissances de 2 (1, 2, 4, 8, 16, 32, 64, 128, 256, ...).

[modifier] Solution Assembleur

function TouchPresse : boolean; assembler; 
asm
 mov ah,01h { Fonction 01h : Vérifie qu'une touche soit disponnible }
 int 16h { Appel l'interruption clavier }

 mov al,1 { Une touche est présente (TRUE) }
 jnz @PasVide (1)
 xor al,al { Pas de touche touche (FALSE) }

@PasVide:
end; 

function LitTouche : char; assembler;
asm
  xor ah,ah { Fonction 00h = Lit une touche du clavier }
  int 16h { Appel l'interruption clavier }

  or al,al { Est-ce une touche étendue? (AL=0) } (2)
  jz @Etendue { Ouais -> AL = code étendu (AH) + 128 } (2)

  ret { Touche standard -> on se tire }

@Etendue:
  mov al,ah { AL = Code ASCII étendu }
  or al,128 { Ajoute 128 à celui-ci pour le distinguer }
end; 

Analyse :

  • (1) - La fonction 01h de l'interruption 16h renvoi la présence d'une touche par le flag ZF. Pour le tester on utilise JNZ : ZF=0, ne fait rien; ZF=1 : Saute !
  • (2) - La fonction 00h de l'interruption 16h renvoi le code ASCII de la touche dans le registre AL. Si celui-ci vaut 0, alors la touche est étendue, et le code est stocké dans AH. Pour savoir si AL=0, on peut faire "cmp al,0; jz @Saut", mais il est plus rapide de faire le test par "or". Celui-ci modifie le flag ZF : Si AL=0, ZF=1 (ZF = Zero Flag !); si AL<>0, ZF=0.

[modifier] Effacement de l'écran dans le mode VGA

Mode VGA : 320x200 pixels en 256 couleurs.

Dans le mode VGA, la mémoire vidéo est placée à l'adresse $A000:$0000, et prend 320*200=64 000 octets. Le but du jeu est de remplir ces 64 000 octets avec une couleur précise, c'est à dire un valeur codée sur un octet.

[modifier] Solution en Turbo Pascal

fillchar (Mem[$A000:0],64000,Couleur); { Couleur étant un "Byte", un octet }

Celà fonctionne très bien, mais Turbo Pascal étant développé pour fonctionner avec les 286, processeur en 16 bits, fillchar fonctionnera donc en 16 bits (voir en 8 bits, je ne sais pas trop). On va donc l'optimiser en passant écrivant dans la mémoire avec les instructions 32 bits.

[modifier] Solution Assembleur (1)

(intégrée dans un programme Turbo Pascal, première version, dite "éductive")

Procedure EffaceEcran (Coul: byte);
begin
   asm (1)
      mov ax,0A000h(2)
      mov es,ax { ES = A000h }
 
      mov di,0 { Maintenant ES:DI pointe sur l'adresse A000:0000 } 
      mov al,[coul] { On lit la couleur passée en paramètre } (3)
      mov cx,64000 { 64 000 octets à remplir}
      rep stosb { Ecrit CX fois l'octet AL à l'adresse ES:DI }
   end; (1)
end; 

Analyse :

  • (1) - En turbo Pascal, on peut intégrer de l'assembleur n'importe où en tapant "asm { les instructions } end;".
  • (2) - Les nombres hexadécimaux débutants par des lettres peuvent être confondus avec des noms de variables, il faut alors rajouter un zéro en préfixe.
  • (3) - Pour lire les paramètres, rien de plus simple : taper son nom (les crochets ne sont pas obligatoires).

[modifier] Solution Assembleur (2)

(version optimisée 32 bits)

Procedure EffaceEcran (Coul: byte);
assembler; asm (1)
   mov ax,0A000h
   mov es,ax
 
   { xor edi, edi } (2)db 66h; xor di,di (3) { ES:DI pointe 
   sur l'adresse A000:0000 }
   mov al,[coul] { On lit la couleur passée en paramètre }
   mov ah, al { Copie AL dans AH, donc AX = deux fois la couleur } (4)
 
   mov bx, ax { Sauve AX dans le registre BX } (4)
   { shl eax,16 } db 66h; shl ax,16 { Déplace les deux octets de poids faible }
                                    { de EAX dans les octets de poids fort } (4)
   mov ax, bx { Relit AX du registre BX } (4)
   mov cx,64000/4 { 16 000 double-mots à remplir } (5)
   {rep stosd } db 66h; rep stosw { Ecrit ECX fois le double-mot EAX à l'adresse ES:EDI } (6)
end; (1)

Analyse :

  • (1) Encore mieux, en turbo Pascal on peut déclarer une procédure qui n'utilise que l'assembleur, ce qui évite d'avoir à taper "begin" et "end;" inutiles.
  • (2) - Pour mettre un registre à zéro, la méthode la plus rapide est "xor reg,reg", car XOR est une fonction de base du processeur.
  • (3) - Turbo Pascal 7 ne connaissant pas les instructions 32 bits, on peut toujours les écrire en langage machine. Pour avoir le code machine d'une instruction, on peut compiler un programme en assembleur ne contenant que cette instruction en demandant au compileur de créer un listing (avec le paramètre "/Z" pour TASM 1.0 et 2.0), c'est à dire un fichier contenant le code assembleur décomposée, interprété et traduit en code machine. Sinon, il existe aussi des programmes effectuant la convertion, mais je n'en connais pas directement. Pour revenir à notre "xor edi, edi", l'équivalent en code machine est "66 33 FF", or "xor di,di" est équivaut à "33 FF", il suffit donc de rajouter le code "66h" en préfixe, préfixe de nombreuses instructions 32 bits en fait.
  • (4) - En 32 bits, l'instruction "STOSD" écrira EAX et non AL, il faut donc répéter "Coul" quatre fois dans EAX. On commence par l'écrire dans AL, puis le copier dans AH, et enfin un "truc" pour le copier dans EAX. Le truc est en fait de décaler les octets de EAX de 16 bits vers la gauche à l'aide de l'instruction SHL, puis recopier l'ancienne valeur de AX dans AX (qui est la partie basse de EAX pour souvenir !). Remarque : l'instruction "SHL" nécessite de compiler en mode 286/287, on peut rajouter "{$G+}" au début du programme pour ce fait.
  • (5) - STOSD écrit des double-mots (4 octets), or nous voulons remplir 64 000 octets. Il nous faudra donc écrire 64 000/4 fois EAX. Remarque : pour STOSW, on divisera par 2 tout simplement :-)
  • (6) - Une fois de plus, on peut étendre une instruction 16 bits à son équivalant en 32 bits en rajoutant le préfixe "66h". En réalité "REP STOSW" est "F3 AB" (F3 = "REP"), et "REP STOSD" est "F3 66 AB". Mais le processeur est tolérant, et le code est d'autant plus clair avec "db 66h; stosw", alors pourquoi s'en priver.