J'ai écris cette "introduction" à l'assembleur
assez rapidement, et croyant que vous vous y connaissiez un peu dans
ce domaine. Alors si vous trouvez une erreur dans ce cours, ou quelque
chose vous est incompréhensible, écrivez-moi : vous
m'aiderez pour mon cours, et vous aiderez les prochains lecteurs de
ce cours => Critique
du cours du l'assembleur (email pour Haypo).
Sommaire :
Introduction (revenir au Sommaire)
:
Le processeur d'un ordinateur ne comprend aucun langage de programmation
ni Turbo Pascal, ni assembleur, ni même C++ ou Java (dumoins
aucun des processeurs compatibles Intel 80x86). Il ne connait qu'une
chose : le langage machine. C'est une liste de nombre 8 bits
en hexadécimal du style : "B0h 12h", pas très
parlant. L'assembleur c'est la version "humaine" du code
machine, pour notre exemple celà donne : "mov al, 12h"
(comprendre "copie la valeur 12h dans le registre AL").
Comme vous pouvez le constater, c'est le langage de programmation
le plus proche du processeur (mise à part si vous arrivez
à programmer en langage machine).
Question qui vous saute à l'esprit : à quoi ça
sert ? L'intérêt premier est la vitesse,
car c'est le langage avec lequel on peut faire les programmes les
plus rapides. Le deuxième intérêt, qui n'est
peut-être plus d'actualité aujourd'hui est l'accès
aux interruptions DOS qui permettent un accès direct au matériel
tel la souris, l'écran, ou la carte vidéo. Certains
vétérants de la programmation n'utilisent que l'assembleur,
certains pirates en particulier. Pourquoi ? Pour faire les plus
petits programmes possibles, car le compileur
ne fait que traduire l'assembleur en code machine (ce qu'on pourrait
presque faire manuellement avec de l'entrainement). Et en même
savoir exactement ce que contiendra notre fichier binaire (fichier
exécutable, ".exe" ou ".com" sur les
plate-formes Microsoft).
Personellement, je vous déconseille vivement du faire du
100% ASM (ASseMbleur), car on se perd vite dans le code source (qui
atteind souvent plusieurs milliers de lignes car chaque instruction
prend une ligne), et ce n'est pas très parlant comme langage.
Je vous le recommande (uniquement) pour optimiser des fonctions
déjà existante dans votre langage de prédilection
(Pascal, C++, ...) demandant une vitesse optimale, souvent pour
les fonctions les plus souvent appelées : tracé de
ligne, copie d'un bloc mémoire, etc.
Commençons par le vocabulaire (revenir
au Sommaire) :
Compilateur -
Hexadécimal - Bit
- Octet - Mot - Double-mot
- Segment
et offset - Interruption - Pile
- Flags
- Compilateur : Programme traduisant
un code source (fichier texte, pour l'assembleur : fichiers portant
l'extension ".asm") en code machine, c'est à
dire produisant un fichier binaire (ou fichier "exécutable",
portant l'extension ".exe" ou ".com" sur les
plate-formes Microsoft).
- Hexadécimal : Dans la notation
des nombres actuelle, on utilise les chiffres arabes compris entre
0 et 9, on a donc 10 chiffres, et le chiffre le plus grand est
9 (10-1). On dit de ce système de notation qu'il est en
base 10 (décimal, déci = 10 en grec).
Pour écrire 451 par exemple, on peut le décomposer
comme 4*100 +5*10 +1*1 = 4*(10^2) +5*(10^1) + 1*(10^0).
Le système hexadécimal au contraire est en base
16. Pour écrire 10 on utilise la lettre "a",
11 : "b", 12 : "c", 13 : "d", 14 :
"e", et 15 : "f". Donc par exemple 20 (16*1+4)
s'écrira 14h. "h" est le suffixe des nombres
hexadécimaux en assembleur, en Turbo Pascal on utilise
le préfixe "$" ($14) et en C le préfixe
"0x" (0x14). Pour convertir de l'hexadécimal
en décimal, on procède à sa décomposition
: 3DAh = 3*(16^2) +D*(16^1) +A*(16^0) = 3*256 +13*16 + 10*1 =
986.
Remarque: Le processeur central (CPU : Central Processor Unit)
travaille uniquement en binaire (uniquement avec bits),
notation en base 2. Par exemple 9 s'écrira 1*8 +1 = 1*(2^3)
+0*(2^2) +0*(2^1) +1*(2^0) = "1001b" ("b"
étant le suffixe des nombres binaires). Même le coprocesseur
arithmétique qui calcule le cosinus et l'exposantiel travaille
en binaire !
- Bit : Un bit est une donnée
pouvant prendre soit la valeur 0 (assimilée à false,
faux en anglais) ou 1 (true, vrai en anglais). C'est l'unité
du système binaire (voir la remarque de l'hexadécimal),
un octet par exemple est un groupement de
8 bits.
- Octet : Un octet est une suite de
8 bits. S'il est signé (+ ou -), il peut prendre une valeur
comprise entre -128 et +127 (128 = 2 puissance (8-1) et 127 =
2 puissance (8-1) -1), car le premier bit est celui du signe (TRUE
= "-") et donc la valeur est en 7 bits. S'il n'est pas
signé, il peut prendre une valeur comprise entre 0 et +255
(255 = 2^8 -1, "-1" car le zéro est une valeur à part entière
!).
- Retour à la liste des mots de vocabulaire
-
- Mot : Deux octets = 16 bits. Signé
: valeurs entre -32768 (2^15) et +32767 (2^15 -1). Non signé
: entre 0 à +65535 (2^16 -1).
- Double-mot : Quatres octets =
2 doubles mots = 32 bits. Signé: valeurs de -2 147
483 648 (-2^31 = -2 147 millions) et +2 147 483 647
(2^31 -1), non-signé : entre 0 et +4 294 967 295
(quelques 4 294 millions).
- Registre : Espace mémoire placé
physiquement dans le coeur du processeur dans lequel sont stockés
des valeurs. Les registres étaient au début codés sur 8 bits (un
octet), ils étaient nommés ?L (où "?" peut être A,B,C ou D), puis
ils sont passés en 16 bits (un mot) et deviennent ?X. Le truc
magique est que si on modifie ?L, on modifie la "partie basse"
du mot ?X, c'est à dire ?X est en faite composé d'un octet et
de ?L. Puis avec l'arrivé du 386, on passe en 32 bits (double-mot),
et les registres deviennent E?X, là aussi, si on modifie ?L ou
?X on modifie une partie de E?X.
Consultez la Liste des registres.
- Segment et Offset : Les adresses
mémoires (emplacement des octets dans les barrettes de mémoire)
sont définies par deux registres : Le segment (partie haute de
l'adresse) et l'offset (partie basse de l'offset). On note l'adresse
: "segment :[ offset ]"
(":" est le séparateur, les crochets "["
et "]" ne sont pas obligatoires), exemple: "DS:[DI]".
Avec le 386, les offsets sont passés en 32 bits pour accéder à
plus de mémoire (plus de 16 Mo), les offsets avec le préfixe
"E" sont apparus (ESI, EDI, ESP, EBP), et comme pours
les registres 32 bits, les registres 16 bits, les offsets SI,DI,SP,BP
en sont leur partie basse.
Consultez la Liste des registres.
- Retour à la liste des mots de vocabulaire
-
- Interruption : Une interruption
est comme un petit programme stocké en mémoire qui
est appelé plus ou moins régulièrement et
qui a une tâche spécifique. Il y en a 256 au maximum,
et les premières sont intégrés dans le BIOS
(programme lancé au démarrage de l'ordinateur qui
gère le matériel : disque dur, accès mémoire,
...) comme les interruptions de la carte vidéo ou le clavier.
Certaines sont appelées à une fréquence constance
: l'interruption 1Ch par exemple qui est un compteur qui incrémente
(ajoute 1) une valeur 18.6 fois/seconde; d'autres sont appelées
uniquement si on a besoin d'elle : l'interruption 09h du clavier
est appelée à chaque pression ou relachement d'une
touche.
Voir la liste complète des interruptions.
- Pile : La pile pourrait être comparée
à une pile d'assiette, on entasse des assiettes (ouais bon, c'est
une image) avec l'instruction "PUSH" puis on les enlêve
avec l'instruction "POP". La hauteur de la pile est la valeur
du registre [E]BP. On voir ce qu'il y a à l'étage X avec une instruction
dans le style "MOV AX,[BP-4]", mais pas l'enlever, car si on enlève
une assiette au milieu de la pile, la pile dégringole (=
plantage du PC).
- Flags (drapeaux) : Ceux sont des
bits internes au processeur.
Consultez la Liste des drapeaux.
- Retour à la liste des mots de vocabulaire
-
Liste des registres (revenir au Sommaire)
:
Il existe plusieurs registres ayant un sens plus ou moins précis
(ils peuvent avoir une utilisation différente) :
- AL/AH/EAX : Registre général, sa valeur change très
vite.
- BL/BH/EBX : Registre général, peut servir d'offset mémoire
(exemple : "mov al, byte ptr ds:[bx+10]").
- CL/CH/ECX : Sert en général de compteur pour les boucles
(exemple : "mov ecx, 5 ; rep movsd" : copie 5 doubles mots).
- DL/DH/EDX : Registre général, obligatoire pour l'accès
aux ports (moyen de communiquer avec toutes les puces de l'ordinateur,
par exemple les ports 42h et 43h servent à contrôler
le haut-parleur interne, voir IN et OUT).
- CS : Segment mémoire du code.
- DS : Segment mémoire des données.
- ES : Segment mémoire.
- FS : Autre segment mémoire.
- GS : Autre segment mémoire.
- SS : Segment mémoire de la pile ("S" = Stack = Pile).
- BP : Offset mémoire, très souvent une copie de SP à laquelle
on soustrait une valeur pour lire dans la pile (on ne doit pas
modifier SP).
- EDI/DI : Offset mémoire utilisé avec ES (ou FS ou GS
si spécifié, exemple : "mov al, byte ptr gs:[10]").
- EIP/IP : Offset mémoire du code (inaccessible directement,
modifiable indirectement avec l'instruction CALL, JMP, ou J[cas]).
- ESI/SI : Offset mémoire utilisé avec DS.
- ESP/SP : Offset mémoire de la pile.
Liste des drapeaux (revenir au Sommaire)
:
- AF : Auxilliary Flag = Indicateur de retenue auxilliaire.
- CF : Carry Flag = Indicateur de retenue.
- DR : Dirrection Flag = Indicateur de direction de traitement
des chaînes de caractères.
- IF : Interrupt Flag = Indicateur d'exécution des
interruptions dites "masquables".
- OF : Overflow Flag = Indicateur de débordement.
- PF : Parity Flag = Indicateur de parité ->
PF=0 : Impaire, PF=1 : Paire.
- SF : Sign Flag = Indicateur de signe -> SF=0 : Positif,
SF=1 : Négatif.
- TF : Single Step Flag = Indicateur de débogage.
- ZF : Zero Flag = Indique une valeur nulle.
Liste des instructions (revenir au
Sommaire) :
Les caractères "{" et "}" délimitent un paramètre.
Il peut être un registre, une zone mémoire ou une immédiate (une
valeur, la plupard du temps c'est un nombre).
Le suffixe "[B/W/D]" terminant une instruction doit être
remplacé soit par B, soit par W ou soit par D :
- "B" signifit que l'instruction est en 8 bits (utilise si nécéssaire
le registre AL).
- "W" signifit que l'instruction est en 16 bits (utilise si nécéssaire
AX).
- "D" signifit que l'instruction est en 32 bits (utilise si nécéssaire
EAX).
AND - CALL
- CMP - CMPSx - IN
- IRET - JMP - J[cas]
- LEA - LDS - LES
- LFS - LGS - LSS
- LODSx - MOV - MOVSx
- MOVZX - MUL - NOT
- OUT - PUSH - POP
- RET - REP - SHL
- SHR - STOSx - SCASx
- TEST - XOR
- AND {destination},{masque} : Applique
un "et" à {destination} par {masque}.
Tout bit de {destination} est mis à 0 si le bit correspondant
de {masque} vaut 0, et est inchangé si le bit correspondant
vaut 1 :
0 AND 0 -> 0
0 AND 1 -> 0
1 AND 0 -> 0
1 AND 1 -> 1
Equivalant en Turbo Pascal : "{destination} := {destination}
and {masque}".
Equivalant en C : "{destion} &= {masque}".
Voir aussi NOT, OR et XOR.
- CALL {adresse} : Appelle une procédure
qui est à l'adresse {adresse}. Si une ou plusieurs instructions
PUSH précèdent un CALL, ceux sont des paramètres qui sont stockés
dans la pile. Dans ce cas la procédure commence par "push [e]bp
; mov [e]bp, [e]sp" et on peut trouver la lecture des paramètres
avec des instructions du genre "mov {registre},[ebp-{valeur}]".
Il ne faudra surtout pas oublier le "RET [valeur]" à la fin de
la procédure (voir plus bas).
- CMP {a}, {b} : Compare les deux variables
{a} et {b}. Toujours suivit d'un saut conditionnel ("J[cas]
{offset}", voir sa signification plus bas).
- CMPS[B/D/W] : Compare l'octet/le
mot/le double-mot DS:ESI à ES:EDI.
- IN {destination},{port} : Lit une valeur
8 bits sur le port {port} (16 bits) et la stocke dans {destination}.
Le seul registre autorisé pour {port} est DX. Voir
aussi OUT.
- IRET {valeur} : Quitte l'interruption.
Est uniquement utilisé pour les programmes résident
comme le pilote de la souris par exemple. Voir aussi RET.
- Retour à la liste des instructions
-
- JMP {offset} : Va ("J" =
Jump : Sauter) à l'adresse {offset}.
- J[cas] {offset} : Va à l'adresse {offset}
si la condition [cas] est exacte. [cas] est une condition relative
aux "flags" (drapeaux), elle doit être traduite par «
Si le résultat d'une opération logique ... »
(je prends comme exemple "CMP {a},{b}") :
- (non signée)
- JA : est supérieur (a > b), si CF=ZF=0.
- JAE ou JNB ou JNC : est supérieur
ou égal (a => b), si CF=0.
- JB ou JC : est inférieur (a <b), si
CF=1.
- JBE : est inférieur ou égal (a <=
b), si CF=ZF=1.
- (signée)
- JG : est supérieur (a > b), si SF=ZF=0.
- JGE : est supérieur ou égal (a =>
b), si SF=OF.
- JL : est inférieur (a < b), si SF<>OF.
- JLE : est inférieur ou égal (a <=
b), si SF<>OF et ZF=1.
- (égalité)
- JE ou JZ : est égal (a = b), si ZF =
1.
- JNE ou JNZ : est différent (a <>
b), si ZF = 0.
- LEA {destination},{source} : Ecrit
l'adresse de {source} dans {destination}. Equivaut
à "MOV {destination}, OFFSET {source}".
- LDS {destination},{adresse} : Copie
l'adresse {adresse} en 32 bits dans le registre DS,
son segment, et dans {destination}
(16 bits), son offset.
- LES {destination},{adresse} : Copie
l'adresse {adresse} en 32 bits dans le registre ES,
son segment, et dans {destination}
(16 bits), son offset.
- LFS {destination},{adresse} : Copie
l'adresse {adresse} en 32 bits dans le registre FS,
son segment, et dans {destination}
(16 bits), son offset.
- LGS {destination},{adresse} : Copie
l'adresse {adresse} en 32 bits dans le registre GS,
son segment, et dans {destination}
(16 bits), son offset.
- LSS {destination},{adresse} : Copie
l'adresse {adresse} en 32 bits dans le registre SS,
son segment, et dans {destination}
(16 bits), son offset.
- LODS[B/D/W] : Copie l'octet/le mot/le
double-mot ES:EDI dans AL/AX/EAX (instruction inverse de STOS[B/W/D]).
- Retour à la liste des instructions
-
- MOV {dst},{src} : Copie la valeur {src}
dans {dst}.
- MOVS[B/D/W] : Copie l'octet/le mot/le
double-mot DS:ESI dans ES:EDI.
- MOVZX {dst},{src} : Etend à
32 bits le nombre contenu dans {src} (8 bits) et transfert le
résultat dans {dst} (16 ou 32 bits).
Exemple: MOVZX ax,al : Efface la partie haute de AX (AH).
- MUL {source} : Multiplie la destination
explicite par {source}, les deux nombres sont considérés
comme non signés. Utiliser "JC {adresse}"
pour tester le débordement. La destination est fonction
de la taille de source :
- 8 bits : la destination est AX (le multiplicateur
est AL)
- 16 bits : la destination est DX:AX, c'est à
dire AX contient la partie basse et DX la partie haute (le
multiplicateur est AX)
- 32 bits : la destination est EDX:EAX, c'est à
dire EAX contient la partie basse et EDX la partie haute (le
multiplicateur est EAX)
- NOT {destination} : Inverse les bits
de {destination} :
NOT 1 -> 0
NOT 0 -> 1
Equivalant en Turbo Pascal : "{destination} := not
{destination}".
Equivalant en C : "{destion} = ~{destination}".
Voir aussi AND, OR et XOR.
- OR {destination}, {masque} : Applique
un "OU logique" à {destination} par {masque}.
Tout bit de {destination} est mis à 1 si le bit correspondant
de {masque} vaut 1, et laissé inchangé si le bit
correspondant vaut 0.
0 OR 0 -> 0
0 OR 1 -> 1
1 OR 0 -> 1
1 OR 1 -> 1
Equivalant en Turbo Pascal : "{destination} := {destination}
or {masque};".
Equivalant en C : "{destion} |= {masque}".
Voir aussi AND, NOT et XOR.
- OUT {source},{port} : Ecrit la valeur
{source} (8 bits) sur le port {port} (16 bits). Le seul
registre autorisé pour {port} est DX. Voir aussi
IN.
- PUSH {valeur} : Met une [valeur] dans
la pile.
- Retour à la liste des instructions
-
- POP {registre} : Sort une valeur de
la pile et la stocke dans un [registre].
- REP {instruction}: Répète l'instruction
[instuction] ECX fois.
- RET {valeur} : Quitte la procédure
en cours. Si des paramètres ont été envoyés au CALL, [xxxx] est
le nombre d'octets envoyés qui sont à sortir de la pile. Voir
aussi IRET.
- SHL {registre},{valeur} : Décalage
binaire du {registre} de {valeur} vers la gauche
(L = Left), les bits apparaissant à droite sont complétés
par des zéros. Exemple : "mov al, 3; shl al,2"
donne al = 12, car 3 = 0011 et son décalage 1100. En fait
décaler de X revient à multiplier par 2^X (3*2^2
= 3*4 = 3*2*2 = 12).
- SHR {registre},{valeur} : Décalage
binaire du {registre} de {valeur} vers la droite
(R = Right), les bits apparaissant à gauche sont complétés
par des zéros. Exemple : "mov al, 12; shr al,2"
donne al = 3, car 12 = 1100 et son décalage 0011. En fait
décaler de X revient à diviser par 2^X (12/(2^2)
= 12/4 = 3).
- STOS[B/D/W] : Copie AL/AX/EAX dans
l'octet/le mot/le double-mot ES:EDI (inverse de LODS[B/W/D]).
- SCAS[B/D/W] : Compare AL/AX/EAX à
l'octet/le mot/le double-mot ES:EDI (permet de rechercher une
valeur dans une chaine de caractères).
- Retour à la liste des instructions
-
- TEST {source},{masque} : Teste si
les bits {masque} de {source} sont posés
ou non, et modifie ZF en conséquence (ZF posé si
les bits de {source} sont posés, sinon ZF=0), ce qui sera
exploitable avec "JZ" ou "JNZ"
par la suite. L'instruction permet de tester un bit particulier
de {source}.
En particulier : TEST {a},{a} = Teste si la variable {a}
est à zéro (pose ou non le drapeau ZF).
- XOR {destination},{masque} : Applique
un "ou exclusif" à {destination} par {masque}.
Tout bit de {destination} est mis à 1 s'il diffère
du bit correspondant de {masque}, et est mis à 0 s'il a
la même valeur :
0 XOR 0 -> 0
0 XOR 1 -> 1
1 XOR 0 -> 1
1 XOR 1 -> 0
(c'est le même système que pour la multiplication
de nombres relatifs : (+2)x(-3) = (-6), et (-3)x(-3)=9 par exemple)
XOR est utilisé en cryptographique car appliquer deux fois
XOR à un même nombre avec le même "masque"
redonne le nombre. Exemple :
24 xor 3 -> 27
27 xor 3 -> 27
XOR {a},{a} : Met à la variable {a} à zéro, beaucoup
plus rapidement que "MOV {a},0" car XOR est une instruction
de base du processeur.
Equivalant en Turbo Pascal : "{destination} := {destination}
xor {masque}".
Equivalant en C : "{destion} ^= {masque}".
Voir aussi AND, NOT et OR.
- Retour à la liste des instructions
-
Liste des interruptions (revenir au Sommaire)
:
J'ai trouvé des informations par-ci, par-là, donc
ne me demandez pas de tout détailler, je n'en sais pas plus.
- 01h: Lancement d'un programme en mode pas-à-pas
(permet de le débogueur).
- 02h: Interruption non masquable.
- 03h: Erreur de rupture.
- 04h: Erreur de dépassement (par calcul, exemple
: "mov al, 200 ; add al,140" -> 340 > 255 !!!).
- 05h "Imprime écran", imprime une copie
de l'écran en mode texte.
- 08h: Horloge tournant à 18.6 clics/seconde.
- 09h: Lecture du clavier. La touche est codée avec
un 'code clavier' (scan code en anglais) traduit en code standart
ASCII par l'interruption 16h.
- 0Bh: Gestion du port COM2.
- 0Ch: Gestion du port COM1.
- 10h: Gestion de la carte vidéo.
- 11h : Liste de configuration (mémoire, nombre
de ports COM, co-processeur, ...).
- 12h: Taille de la mémoire basse (640 Ko maximum).
- 13h: Gestion des différents disques.
- 14h: Gestion de l'interface série (ports COM,
voir les interruptions 0Bh et 0Ch).
- 15h: Manette de jeu, cassette et TopView.
- 16h: Conversion du code de la touche (lu par l'interruption
09h) en code standart ASCII.
- 17h: Gestion de l'imprimante.
- 18h: Rom BASIC.
- 19h: Rountine de chargement du DOS.
- 1Ah: Gestion de l'heure réelle.
- 1Bh: Surveille la pression de la combinaison de touche
'CTRL + C'.
- 1Ch: Chronomètre clic/clic à la vitesse
de l'horloge 08h : 18.6 Hz. Sa valeur est stockée à
l'emplacement 0040h: 0060h.
- 1Dh: La table d'initialisation vidéo.
- 1Eh: La table de paramètre des disquettes.
- 1Fh: La table des caractères graphiques.
- 20h: L'interruption DOS : Fin d'un programme au format
COM (le format EXE est largement plus répendu aujourd'hui).
- 21h: L'interruption DOS : Fonctions universelles (disque
dur, horloge, ...).
- 22h: L'interruption DOS : Adresse de fin de processus.
- 23h: L'interruption DOS : Surveille CTRL + PAUSE (ou
CTRL + BREAK).
- 24h: L'interruption DOS : Erreur fatale d'un vecteur
d'interruption.
- 25h: L'interruption DOS : Lecture directe d'un disque.
- 26h: L'interruption DOS : Ecriture directe sur un disque.
- 27h: L'interruption DOS : Programmes résidents.
- 28h: Fin d'un programme restant résidant en mémoire.
- 2Fh: Interruption pour plusieurs sous programmes. Gestion
du réseau, driver CD-Rom MSCDEX, ...
Exemples concrets (revenir au
Sommaire) :
1) Lecture d'une touche au clavier
Principe : l'interruption 16h gère
le clavier. Elle possède deux fonctions intéressantes
: 00h, lecture d'une touche; et 01h, vérification de la présence
d'une touche dans le tampon clavier. Mais si une touche est étendue,
par exemple les touches fléchées, ou les touches "page
haut", "insertion", etc., la fonction 00h nous reverra
un code null (00h) comme code ASCII, puis le code ASCII de la touche
étendue. Sachant que le code ASCII est toujours inférieur
à 128, on pourra ajouter 128 aux codes étendus pour
au final n'avoir à appeler notre future fonction de lecture
d'une touche qu'une seule fois.
* 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, met
dans un mot d'ensemble (Word). Pour isoler le flag 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'appelle
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, ...).
* Solution Assembleur :
function TouchPresse : boolean; assembler;
asm
mov ah,01h { Fonction 01h = Vérifit
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 standart -> 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.
2) Effacement de l'écran dans le 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.* 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.
* Solution Assembleur (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).
* Solution Assembleur (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'équivalant 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.
Historique du document (revenir au Sommaire)
:
11 avril 2001:
- Ajout d'exemples concrets (lecture du clavier, effacement de
l'écran) en Turbo Pascal.
- Correction des doubles mots : ne peut que stocker des millions
(pas des milliards, désolé, j'ai du mal avec la
- calcultrice de Windows :-).
- Correction de liens internes rompus (OUT et PUSH).
- Première correction des fautes d'orthographe (j'ai pas
le courrage de tout corrigé :-/ ).
|