Guide de l'assembleur pour débutants
Guide de l'assembleur pour débutants
Votre PC est conçu pour gérer 1 Mo (soit 220 octets) de mémoire vive en mode réel. Il
faut donc 20 bits au minimum pour adresser toute la mémoire. Or en mode réel les bus
d'adresses n’ont que 16 bits. Ils permettent donc d'adresser 216 = 65536 octets = 64 Ko, ce qui
est insuffisant !
Afin de pallier ce manque, on utilise deux nombres pour adresser un octet quelconque de la
RAM. Le premier est appelé adresse de segment, le second adresse d'offset. Ils seront stockés
séparément.
Pour accéder à un octet particulier dans un segment, il suffit de compter le décalage de cet
octet par rapport au début du segment. Ce décalage est obligatoirement inférieur ou égal à
65535 : il tient bien sur 16 bits lui aussi. On appelle ce décalage « offset ».
L'adresse d'un octet se note XXXX:YYYY où XXXX est l'adresse de segment et YYYY est
l'offset (tous deux en notation hexadécimale, bien sûr).
Par exemple, le dix-septième octet de la RAM (le numéro 16) est situé à l'adresse 0000:0010.
De même, l’octet 0000:0100 est l'octet numéro 256. Nous en arrivons à la petite subtilité qu’il
convient de bien saisir, sous peine de ne rien comprendre à certains programmes en
assembleur.
On pourrait penser que l’octet qui se trouve à l’adresse 0001:0000 est le numéro 65536. Il
n’en est rien. C'est l'octet numéro 16.
Les segments ne sont pas situés gentiment les uns à la suite des autres. Ils n'attendent pas que
les segments qui les précédent soient terminés avant de commencer ! Ils se marchent donc sur
les pieds.
Autrement dit, le deuxième segment ne démarre pas à l'octet 65536 comme il devrait le faire
s'il était bien sage, mais à l'octet 16 ! Le troisième démarre à l'octet 32 et ainsi de suite…
La notion de segment n’est pas tant physique que mathématique : elle sert à se repérer dans
la RAM.
on calcule l'adresse effective d'un octet, c'est à dire sa position absolue dans la RAM.
Voici la solution : si l'adresse de l'octet est A17C:022E, alors son adresse effective est A17C
x 16 + 022E, soit A17C0 + 022E = A19EE. On a multiplié par 16, car le segment A17C
débute à l’octet A17C x 16, puis on a simplement ajouté le décalage.
Au final, on a bien une adresse sur 20 bits puisqu'on obtient 5 chiffres hexa. Chaque petit bloc
de 16 octets s’appelle un paragraphe.
Remarque importante : Il est souvent plus simple de considérer qu’un segment est un bloc
de taille quelconque qui débute à une adresse effective multiple de 16 et qui permet, à l’aide
de son adresse de segment et d’un offset, d’adresser le bloc entier (64 Ko au maximum). Cette
définition est celle qui a le plus de sens.
La différence fondamentale est que les programmes COM ne peuvent pas utiliser plus d’un
segment dans la mémoire. Leur taille est ainsi limitée à 64 Ko. Les programmes EXE ne sont
quant à eux limités que par la mémoire disponible dans l’ordinateur.
Lorsqu’il charge un fichier COM, le DOS lui alloue toute la mémoire disponible. Si
celle-ci est insuffisante, il le signale à l’utilisateur par un message et annule toute la procédure
d’exécution. Dans le cas contraire, il crée le PSP du programme au début du bloc de mémoire
réservé, et copie le programme à charger à la suite.
Pour simplifier, le PSP (« Program Segment Prefix ») est une zone de 256 (= 100h) octets qui
contient des informations diverses au sujet du programme. C’est dans le PSP que se trouve la
ligne de commande tapée par l’utilisateur.
Voici à titre indicatif la structure simplifiée du PSP (ne vous souciez pas de ce que vous ne
comprenez pas : pour l'instant, seules les deux dernières lignes nous intéressent vraiment) :
A présent que nous connaissons l’existence du PSP, il nous faut revenir sur un point
important. Comme nous l’avons dit, un programme COM ne peut comporter qu’un seul
segment, bien que le DOS lui réserve la totalité de la mémoire disponible. Ceci a deux
conséquences. La première est que les adresses de segment sont inutiles dans le programme :
les offsets seuls permettent d’adresser n’importe quel octet du segment. La seconde est que le
PSP fait partie de ce segment, ce qui limite à 64 Ko – 256 octets la taille maximale d’un
fichier COM. Cela implique également que le programme lui-même débute à l’offset 100h et
non à l’offset 0h.
Voici un petit programme COM qui écrit le message « Bonjour, monde ! » à l’écran.
Ce code source commence par des directives. Une directive est une information que le
programmeur fournit au compilateur. Elle n’est pas transformée en une instruction en langage
machine. Elle n’ajoute donc aucun octet au programme compilé.
La directive “.386“ indique au compilateur que le programme est destiné à tourner sur des
processeurs INTEL de modèle 386 (ou supérieur). Cela nous autorise à utiliser certaines
instructions qui ne sont pas disponibles sur les modèles antérieurs, comme PUSHA ou POPA.
Dans cet exemple, cette directive aurait très bien pu être omise.
La ligne
sert à déclarer un segment que l’on appelle “code”. On aurait tout aussi bien pu le nommer
“marteau” ou “voiture”. Ce sera le segment de notre programme. N’oubliez pas qu’un fichier
COM ne peut comporter qu’un seul segment. Cette ligne ne sera pas compilée : elle ne sert
qu’à indiquer au compilateur le début d’un segment.
Le mot “use16” indique que les adresses de segment et d’offset sont codées sur 16 bits et non
sur 8 bits. Vous devez systématiquement l’écrire.
La directive
org 100h
signifient qu’il faudra ajouter 100h (soit 256) à tous les offsets. Pourquoi ? Souvenez-vous de
la structure d’un programme COM en mémoire. Si vous n’écrivez pas cette ligne, TASM
considérera que le programme débute à l’offset 0000. Or, lors de l’exécution, le DOS le
chargera après le PSP, c’est-à-dire à l’adresse 100h. C’est pourquoi il est nécessaire de
recalculer les offsets : l’offset 0000 deviendra 0100. Cette directive est en quelque sorte le
trait caractéristique des fichiers COM.
debut :
Lui non plus n’est pas compilé. Il ne sert qu’à représenter l’adresse de l’instruction qui le suit,
c’est-à-dire :
Cette ligne demande au processeur de charger la valeur 9 dans le registre AH. C’est le numéro
de la fonction de l’interruption 21h qui écrit une chaîne de caractères à l’écran. L’offset de
cette chaîne est attendu dans DX. D’où la ligne suivante :
Le mot-clé “offset” sert à extraire l’offset du label “message” qui représente quant à lui
l’adresse du message à écrire (il contient donc une adresse de segment ET un offset).
L’adresse de segment de la chaîne doit être transmise dans DS. Mais il est inutile de changer
ce registre, car il pointe déjà vers notre segment.
int 21h
ret
Lorsque nous aborderons les procédures, vous comprendrez mieux le sens de ce mot et ce
qu’il fait exactement. Pour l’instant, sachez simplement que seul un fichier COM peut se
terminer avec cette instruction.
Il s’agit d’une définition de données. Le mot “db” (« define byte ») signifie que le compilateur
devra écrire les octets qui suivent tels qu’ils sont dans notre code source. Il va donc écrire le
code ASCII du ‘B’, puis celui du ‘o’, etc… Il terminera en écrivant le code ASCII du signe
‘$’. C’est ainsi que la fonction 9 de l’interruption 21h reconnaît la fin de la chaîne à écrire. Si
vous oubliez ce signe, elle écrira tous les octets de la RAM jusqu’à ce qu’elle tombe par
hasard sur lui.
Le mot “message” placé en début de ligne est un label de données. Il représente l’adresse du
code ASCII du ‘B’. Remarquez qu’il n’y a pas de caractère ‘:’ après un label de données.
La ligne
code ends
end debut
informe le compilateur que le fichier est fini, tout comme le “END.” du PASCAL. Le nom du
label “debut” est mentionné : ce sera le point d’entrée de notre programme. C’est vers lui que
pointera CS:IP avant l’exécution.
Remarque : vous n’êtes pas tenu de rendre aux registres la valeur qu’ils avaient au début de
votre programme. De toute façon, avant de charger un programme, le DOS sauvegarde le
contenu de tous les registres puis met le contenu des registres généraux (ainsi que SI, DI et
BP) à zéro. Il les restaurera quand vous lui rendrez la main.
Bien qu’il soit possible de n’utiliser qu’un seul segment à tout faire, la plupart des
programmes EXE ont un segment réservé au code (c’est ainsi qu’on appelle les instructions
du langage machine), un ou deux autres aux données, et un dernier à la pile.
La pile est une mémoire très spéciale qui sert comme son nom l’indique à empiler des
données de 16 bits de façon temporaire. On peut ensuite retrouver ces données en les dépilant.
Le « dépilage » se fait toujours dans l’ordre inverse de l’empilage.
Le PSP a lui aussi son propre segment. Le programme commence donc à l’offset 0h du
segment de code et non à l’offset 100h.
Afin que le programme puisse être chargé et exécuté correctement, il faut que le système
sache où commence et où s'arrête chacun de ces segments. A cet effet, les compilateurs créent
un en-tête (ou « header ») au début de chaque fichier EXE. Ce header ne sera pas copié en
mémoire. Son rôle est simplement d’indiquer au DOS (lors du chargement) la position relative
de chaque segment dans le fichier.
Le programme suivant est un programme EXE qui fait exactement la même chose que
l’exemple précédent.
Examinons-le !
org 100h
a disparu.
En effet, lors de l’exécution, CS pointera vers notre segment de code et non pas vers le PSP. Il
est donc inutile de décaler les offsets de 256.
Le segment “data” est destiné à contenir les données, c’est-à-dire les variables.
Le segment “pile” sera notre segment de pile.
Ainsi, le compilateur est informé que DS pointera vers le segment “data” et SS vers “pile”.
Mov ds, ax
servent à initialiser le registre DS. Celui-ci pointe vers le PSP au début du programme mais
nous voulons le faire pointer vers notre segment de données appelé “data”. Cela est nécessaire
puisque la fonction 9 de l’interruption 21h attend l’adresse de la chaîne dans le couple DS:DX
et que notre message se trouve dans le segment de données.
La première instruction charge l’adresse du segment “data” dans AX. La seconde transfère
cette valeur de AX dans DS.
Mais pourquoi diable utiliser AX comme intermédiaire ? Après tout, on pourrait écrire :
Eh bien non ! Pour la simple raison que DS est un registre de segment et qu’en tant que tel on
ne peut pas lui charger de valeur immédiate.
On appelle « valeur immédiate » toute constante tapée directement dans l’instruction elle-
même.
Remarque : une autre possibilité aurait été d'écrire : “PUSH AX” (empiler AX) puis “POP
DS” (dépiler le dernier nombre empilé et le placer dans DS).
Int 21h
Il nous faut également terminer le programme par un appel de la fonction 4ch de l’interruption
21h. C’est ainsi que se terminent les programmes EXE.
Int 21h
Int 21h
La seule différence est que AL est mis à zéro, ce qui indique au programme à qui on rend le
contrôle (ici le DOS) que notre programme s’est terminé normalement. Les fichiers COM
peuvent également utiliser la fonction 4ch.
code ends
data ends
Il nous reste à écrire le segment de pile. Dans ce programme, il n’était pas absolument
indispensable de le séparer du segment de code. Mais c’est une bonne habitude de le faire.
pile ends
Les mots “db 256 DUP (?)” déclarent 256 octets non initialisés. C’est la « matière » de notre
pile.
Sachez que tout appel d’interruption se traduit par l’empilage des flags et de CS:IP. Il est donc
indispensable d’avoir une pile, même si celle-ci peut éventuellement partager le même
segment que le code, comme dans un fichier COM. Mais dans un programme EXE, il vaut
mieux réserver un segment à la pile. Les 256 octets que nous déclarons ici indiquent
seulement que la pile contient 256 octets. Ainsi, au début de l’exécution, SS:SP pointera vers
la fin de ces octets. Nous verrons ce que signifie le point d’interrogation dans la troisième
partie.
end debut
Le code et les variables d’un programme se trouvent dans la mémoire vive. Pour
accéder à une donnée en mémoire, le processeur place son adresse dans un bus d’adresse. Un
cycle de lecture se met alors en place. Il consiste à retourner la donnée lue au processeur via le
bus de données.
Pour l’écriture, l’adresse de la destination est transmise dans le bus d’adresse et la donnée à
écrire est placée dans le bus de données.
Le microprocesseur est le cœur de l’ordinateur. C’est lui qui est chargé de reconnaître
les instructions et de les exécuter (ou de les faire exécuter). Chaque instruction se présente
sous la forme d’une suite de bits qu’on représente en notation hexadécimale (exemple : B44C,
ou 1011010001001100 en binaire). Une instruction se compose d’un code opérateur (le plus
souvent appelé « opcode ») qui désigne l’action à effectuer (B4 dans notre exemple) et d’un
« champ d’opérandes » sur lesquelles porte l’opération (ici l’opérande est 4C).
Pour travailler, le microprocesseur utilise de petites zones où il peut stocker des données. Ces
zones portent le nom de registres et sont très rapides d'accès puisqu'elles sont implantées dans
le microprocesseur lui-même et non dans la mémoire vive.
Ils ne sont pas réservés à un usage très précis, aussi les utilise-t-on pour manipuler des
données diverses. Ce sont en quelque sorte des registres à tout faire. Chacun de ces quatre
registres peut servir pour la plupart des opérations, mais ils ont tous une fonction principale
qui les caractérise.
Le registre AX sert souvent de registre d'entrée-sortie : on lui donne des paramètres avant
d'appeler une fonction ou une procédure. Il est également utilisé pour de nombreuses
opérations arithmétiques, telles que la multiplication ou la division de nombres entiers. Il est
appelé « accumulateur ».
Exemples d'utilisation :
Le registre BX peut servir de base. Nous verrons plus tard ce que ce terme signifie.
Le registre CX est utilisé comme compteur dans les boucles. Par exemple, pour répéter 15 fois
une instruction en assembleur, on peut mettre la valeur 15 dans CX, écrire l'instruction
précédée d'une étiquette qui représente son adresse en mémoire, puis faire un LOOP à cette
adresse. Lorsqu'il reconnaît l'instruction LOOP, le processeur « sait » que le nombre
d'itérations à exécuter se trouve dans CX. Il se contente alors de décrémenter CX, de vérifier
que CX est différent de 0 puis de faire un saut (« jump ») à l’étiquette mentionnée. Si CX vaut
0, le processeur ne fait pas de saut et passe à l’instruction suivante.
Le registre DX contient souvent l'adresse d'un tampon de données lorsqu'on appelle une
fonction du DOS. Par exemple, pour écrire une chaîne de caractères à l'écran, il faut placer
l’offset de cette chaîne dans DX avant d'appeler la fonction appropriée.
Chacun de ces quatre registres comporte 16 bits. On peut donc y stocker des nombres allant
de 0 à 65535 en arithmétique non signée, et des nombres allant de –32768 à 32767 en
arithmétique signée. Les 8 bits de poids fort d’un registre sont appelés « partie haute » et les 8
autres « partie basse ». Chaque registre est en fait constitué de deux « sous-registres » de 8
bits. La partie haute de AX s’appelle AH, sa partie basse AL. Il en va de même pour les trois
autres.
AX = AH x 256 + AL
BX = BH x 256 + BL
CX = CH x 256 + CL
DX = DH x 256 + DL
Pour stocker un nombre de 32 bits, on peut utiliser des paires de registres. Par exemple,
DX:AX signifie DX x 65535 + AX en arithmétique non signée.
Cette notation (DX:AX) n’est pas reconnue par l’assembleur (ni par la machine). Ne
confondez pas cela avec les adresses de segment et d’offset.
Sur un PC relativement récent, AX, BX, CX, DX ne sont en fait que les parties basses de
registres de 32 bits nommés EAX, EBX, ECX, EDX (« E » pour « Extended »). On a donc un
moyen plus pratique de stocker les grands nombres. En fait, chaque registre (pas seulement
les registres généraux) peut contenir 32 bits.
Contrairement aux registres généraux, ces registres ne peuvent servir pour les opérations
courantes : ils ont un rôle très précis. On ne peut d’ailleurs pas les utiliser aussi facilement
que AX ou BX, et une petite modification de l’un d’eux peut suffire à « planter » le système.
Eh oui ! L’assembleur, ce n’est pas Turbo Pascal ! Il n’y a aucune barrière de protection, si
bien qu’une petite erreur peut planter le DOS. Mais rassurez-vous : tout se répare très bien en
redémarrant l’ordinateur…
Le registre DS est quant à lui destiné à contenir l’adresse du segment des données du
programme en cours. On peut le faire varier à condition de savoir exactement pourquoi on le
fait. Par exemple, on peut avoir deux segments de données dans son programme et vouloir
accéder au deuxième. Il faudra alors faire pointer DS vers ce segment.
ES est un registre qui sert à adresser le segment de son choix. On peut le changer aux mêmes
conditions que DS. Par exemple, si on veut copier des données d’un segment vers un autre, on
pourra faire pointer DS vers le premier et ES vers le second.
Le registre SS adresse le segment de pile. Il est rare qu’on doive y toucher car le programme
n’a qu’une seule pile.
Intéressons-nous à présent aux valeurs que le DOS donne à ces registres lors du chargement
en mémoire d’un fichier exécutable ! Elles diffèrent selon que le fichier est un programme
COM ou EXE. Pour écrire un programme en assembleur, il est nécessaire de connaître ce
tableau par cœur :
Dans un fichier EXE, le header indique au DOS les adresses initiales de chaque segment par
rapport au début du programme (puisque le compilateur n'a aucun moyen de connaître
l'adresse à laquelle le programme sera chargé). Lors du chargement, le DOS ajoutera à ces
valeurs l'adresse d'implantation pour obtenir ainsi les véritables adresses de segment.
Dans le cas d'un fichier COM, tout est plus simple. Le programme ne comporte qu'un seul
segment, donc il suffit tout bêtement au DOS de charger CS, DS, ES et SS avec l'adresse
d'implantation.
Remarque : Pourquoi DS et ES pointent-ils vers le PSP dans le cas d’un fichier EXE ?
Première raison : pour que le programmeur puisse accéder au PSP ! Deuxième raison : parce
qu'un programme EXE peut comporter un nombre quelconque de segments de données. C'est
donc au programmeur d'initialiser ces registres, s'il veut accéder à ses données.
Les voici :
PUSH AX
L’effet de cette instruction est d’empiler le mot contenu dans le registre AX. Autrement dit,
SP est automatiquement décrémenté de 2, puis AX est copié à l’adresse SS:SP.
Lors du dépilage, le mot situé au sommet de la pile, c’est-à-dire le mot adressé par SS:SP, est
transféré dans un registre quelconque choisi par le programmeur, après quoi le stack pointer
est incrémenté de 2.
POP BX
Cette fois, on retire le dernier mot empilé pour le placer dans le registre BX. Evidemment, SP
sera incrémenté de 2 aussitôt après.
La pile est extrêmement utile lorsqu’il s’agit de stocker provisoirement le contenu d’un
registre qui doit être modifié.
Exemple :
Il est important de comprendre qu’on ne peut dépiler que le mot qui se trouve au sommet de
la pile. Le premier mot empilé est le dernier qui sera dépilé. La pile doit être manipulée avec
une extrême précaution. Un dépilage injustifié fait planter la machine presque
systématiquement.
Les trois derniers registres sont beaucoup moins liés au fonctionnement interne du processeur.
Ils sont mis à la disposition du programmeur qui peut les modifier à sa guise et les utiliser
comme des registres généraux. Comme ces derniers cependant, ils ont une fonction qui leur
est propre : servir d’index (SI et DI) ou de base (BP). Nous allons expliciter ces deux termes.
Dans la mémoire, les octets se suivent et forment parfois des chaînes de caractères. Pour
utiliser une chaîne, le programmeur doit pouvoir accéder facilement à tous ses octets, l’un
après l’autre. Or pour effectuer une opération quelconque sur un octet, il faut connaître son
adresse. Cette adresse doit en général être une constante évaluable par le compilateur.
Pourquoi une constante ? Parce que l’adresse est une opérande comme les autres, elle se
trouve immédiatement après l’opcode et doit donc avoir une valeur numérique fixe !
Prenons un exemple :
8A26 est l’opcode (hexa) de l’instruction “MOV AH, [constante quelconque]”, et 0601 est
l’offset de “MonOctet”.
Il serait pourtant fastidieux, dans le cas d’une chaîne de 112 caractères, de traiter les octets
avec 112 instructions dans lesquelles seule l’adresse changerait. Il faudrait pouvoir faire une
boucle sur l’adresse, mais alors celle-ci ne serait plus une constante, d’où le problème.
Par exemple :
sera codé :
8AA50601
8AA5 est l’opcode pour l’instruction “MOV AH, [constante quelconque + DI]”.
Remarque : les registres SI et BP auraient tout aussi bien pu être employés, mais pas les
registres généraux, SAUF BX. En effet, BX peut jouer exactement le même rôle que BP.
N’oubliez pas que BX est appelé registre de « base », et que BP signifie « Base Pointer. »
Nous verrons la différence entre une base et un index lorsque nous commencerons
l’assembleur.
Un programme doit pouvoir faire des choix en fonction des données dont il dispose.
Pour cela, il lui faut par exemple comparer des nombres, examiner leur signe, découvrir si une
erreur a été constatée, etc…
Il existe à cet effet de petits indicateurs, les flags qui sont des bits spéciaux ayant une
signification très précise. De manière générale, les flags fournissent des informations sur les
résultats des opérations précédentes.
Ils sont tous regroupés dans un registre : le registre des indicateurs. Comprenez bien que
chaque bit a un rôle qui lui est propre et que la valeur globale du registre ne signifie rien. Le
programmeur peut lire chacun de ces flags et parfois modifier leur valeur directement.
En mode réel, certains flags ne sont pas accessibles. Nous n’en parlerons pas. Nous ne
commenterons que les flags couramment utilisés.
PF (« Parity Flag ») renseigne sur la parité du résultat. Il vaut 1 ssi ce dernier est pair.
ZF (« Zero Flag ») passe à 1 ssi le résultat d’une opération est égal à zéro.
SF (« Sign Flag ») passe à 1 ssi le résultat d’une opération sur des nombres signés est
négatif .
TF (« Trap Flag ») est utilisé pour le « débuggage » d’un programme. S’il vaut 1, une
routine spéciale du débuggeur est appelée après l’exécution de chaque instruction par le
processeur.
IF (« Interrupt Flag ») sert à empêcher les appels d’interruptions lorsqu’il est positionné
à 1. Cependant, toutes les interruptions ne sont pas « masquables ».
DF (« Direction Flag ») est utilisé pour les opérations sur les chaînes de caractères. S’il
vaut 1, celles-ci seront parcourues dans le sens des adresses décroissantes, sinon les
adresses seront croissantes.
Remarque : Les notations CF, PF, AF, etc… ne sont pas reconnues par l’assembleur. Pour
utiliser les flags, il existe des instructions spécifiques que nous décrirons plus tard.
Les mots “db” (« define byte »), “dw” (« define word »), ”dd” (« define double
word ») permettent de déclarer et d’initialiser une variable. Il faut bien comprendre que leur
seule action est d’écrire les données dans l’exécutable à l’endroit même où elle se trouvent
dans le code source.
Pour accéder à ces données (appelées « variables »), il suffit de connaître leur adresse. Pour
cela, on peut les faire précéder d’un label de données.
Exemple :
TOTO dw 1982
Dans la ligne précédente, TOTO représente l’adresse du nombre 1982 qui est défini juste
après. Comme ce nombre est codé sur deux octets (word = 16 bits), c’est le premier octet qui
est adressé par TOTO.
En BASIC ou en PASCAL, cette définition n’a pas d’équivalent, puisque les variables ne
peuvent être initialisées lors de leur déclaration. Il est donc obligatoire d’ajouter une ligne de
code pour le faire.
Pour définir un message qui serait affiché à l’écran. Voici la ligne à écrire :
Lorsqu’il rencontre une chaîne de caractères, le compilateur écrit les codes ASCII de tous les
caractères, les uns à la suite des autres.
Voici une autre manière d’écrire cette définition de données. Le résultat (i.e. le programme
compilé) est EXACTEMENT LE MÊME :
db ‘our, mo’
db ‘nde !$’
Pour définir plusieurs fois à la suite les mêmes données, on utilise “DUP” (« duplicate ») de
la manière suivante :
Il est fréquent que de nombreuses données n’aient pas besoin d’être initialisées à une valeur
précise. Dans ce cas, on les regroupe à la fin du programme et on les remplace par le caractère
‘?’. Les adresses des labels seront calculées de la même façon mais aucune données ne sera
écrite dans le fichier (et a fortiori dans la RAM). On dit que l’on met ses variables sur le
« tas » (« heap » en anglais). La seule différence avec une définition classique est qu’au début
de l’exécution, nos variables n’auront pas de valeur définie. Leurs valeurs seront
« aléatoires » en ce sens qu’on ne peut les connaître au moment de la compilation.
Exemple :
Remarque : Si elles ne sont pas regroupées en fin de programme, le compilateur sera obligé
d'écrire les données dans le fichier afin de ne pas fausser les adresses des variables (ou du
code) qui suivent. Il remplira alors les points d’interrogation avec des zéros.
Pour indiquer qu’un chiffre est noté en base hexadécimale, on lui ajoute la lettre ‘h’. La lettre
‘b’ signifie que le chiffre est codé en binaire, la lettre 'o' en octal et la lettre ‘d’ en base
décimale (base par défaut).
2.L’adressage
a. dressage immédiat
La ligne
charge le nombre 125h dans le mot de la RAM adressé par DS et l’offset de “TOTO”. La
ligne suivante ajoute 15 au contenu de ce mot.
L’expression “word ptr” devant l’adresse, obligatoire ici, indique la taille de la variable dans
laquelle doit être stocké le nombre 125h.
Si on avait mis “dword ptr”, ce nombre aurait été codé sur 32 bits (00000125h) au lieu de 16
bits (0125h) : on aurait donc écrasé les deux octets qui suivent la variable. Si on avait mis
“byte ptr”, la compilation aurait été impossible car un octet ne peut contenir un nombre
supérieur à FFh.
Le compilateur n’a en effet aucun moyen de connaître cette taille. La variable “TOTO” n’a
pas de taille (malgré le mot “dw”) : ce n’est en fait qu’un pointeur vers le premier octet du
word qu’elle représente.
Une question se pose à présent : que se passe-t-il si le programmeur n’écrit pas le registre de
segment dans l’adresse ?
Si on a utilisé un label, alors le segment sera celui dans lequel est déclaré le label. Mais le
compilateur veut un REGISTRE de segment. Il va donc prendre celui qui est censé pointer
vers le bon segment et pour le savoir, il examine la directive assume.
Voilà pourquoi cette dernière peut nous épargner d’écrire pour chaque variable l’expression
“ds:”.
Comprenez bien que cette directive ne sert à rien d’autre qu’à cela et qu’en aucune façon elle
ne force les registres de segment à prendre quelque valeur que ce soit.
Comme nous l’avons déjà expliqué, il est possible d’utiliser des registres de base ou
d’index pour adresser un octet.
On appelle « base » les registres BX et BP et « index » les registres SI et DI. La différence est
que la base est censée être fixe tandis que l’index varie automatiquement lorsqu’on utilise
certaines opérations, telles que MOVSB.
Remarque : BX, BP, DI et SI peuvent tous être utilisés comme des registres généraux.
Exemples :
Base seule :
Index seul :
Exemple :
Base + Constante :
Exemples :
Index + Constante :
Exemples :
Base + Index :
Exemple :
Exemple :
Remarque : si la constante n’est pas un label, il est parfois impératif de spécifier le registre de
segment ! Tout dépend du contexte…
Une instruction contient au plus 4 champs, en général deux champs sont obligatoires : le mnémonique et
les opérandes, les autres sont facultaifs, les champs sont séparés entre eux par un espace blanc.
Le champs etiquette : Ce champ est obligatoire dans le cas où l’instruction va être repérée par
d’autres instructions (saut, bouclage). Il peut avoir au maximum 31 caractères choisis dans (A à
Z), (a à z) ou (chiffres entiers). On ne peut utiliser un nom d’étiquette qui peut spécifier un
mnémonique ou un nom du registre. Ce champs se termine toujours par :
Le champs Mnémonique : il représente le code opération qui exprime l’opération à effectuer par le
micro processeur. Exemple pour le cop addition, le mnémonique est ADD.
Le champs opérande : il représente les données qui szeront traitées par le code opération.
Le champs commentaire : il est utilisé pour ajouter des explications suplementaires sur
l’instruction et il commence toujours par ; . Dans un programme source, on peut utiliser une ligne
commentaire pour expliquer le rôle de chaque partie du programme. De ce fait cette ligne
commencera par ;.
Remarques préliminaires :
Le principe du langage assembleur est de remplacer chaque opcode hexadécimal par un mot
facile à retenir. Ce mot est appelé mnémonique. Par exemple, “INT” est le mnémonique associé à
l’opcode CDh. Chaque fois que le compilateur rencontrera ce mot, il le remplacera par l’octet
CDh et écrira ensuite l’opérande (ici : le numéro de l’interruption) en hexadécimal.
Cette liste récapitule les instructions que nous connaissons déjà et en présente de nouvelles. Elle
n’est pas exhaustive mais vous sera amplement suffisante pour la plupart de vos programmes. .
Syntaxe : NOP
Remarques : Source et Destination doivent avoir la même taille. On ne peut charger dans un registre de
segment que le contenu d’un registre général (SI, DI et BP sont considérés ici comme des registres
généraux).
MOV word ptr [Variable3], 12 ;Ici, on spécifie que la variable est un word
a. Branchement intra-segment
Le nombre maximum d’octets à sauter parapport à la position de l’instruction JMP (% IP) en avant est
Le nombre maximum d’octets à sauter parapport à la position de l’instruction JMP (% IP) en arrière est -
128.
Le nombre maximum d’octets à sauter parapport à la position de l’instruction JMP (% IP) en avant est
+32767.
Le nombre maximum d’octets à sauter parapport à la position de l’instruction JMP (% IP) en arrière est -
-32767.
Branchement indirect
JMP Nom d’un registre d’adresse (ou nom d’une variable de type mot)
b. Branchement intersegment
Branchement direct
Branchement sans condition à l’étiquette où se trouve l’instruction ésirée données dans un autre segment
code.
Branchement indirect
Le mot le plus significatif sera chargé dans CS te le mot le moins significatif sera chargé dans IP.
Description : Cet opérateur sert à comparer deux nombres : Source et Destination. C'est le registre des
indicateurs qui contient les résultats de la comparaison. Ni Source ni Destination ne sont modifiés.
Remarque : Cet opérateur effectue en fait une soustraction mais contrairement à SUB, le résultat n’est pas
sauvegardé.
Le programme doit pouvoir réagir en fonction des résultats de la comparaison. Pour cela, on utilise les
sauts conditionnels (voir ci-dessous).
Les sauts conditionnels sont terriblement importants car ils permettent au programme de faire des
choix en fonction des données.
Un saut conditionnel n’est effectué qu’à certaines conditions portant sur les flags (par exemple : CF = 1
ou ZF = 0).
Certains mnémoniques de sauts conditionnels sont totalement équivalents, c’est-à-dire qu’ils représentent
le même opcode hexadécimal. C’est pour aider le programmeur qu’ils existent parfois sous plusieurs
formes.
Exemple :
Exemple :
JGE (« Jump if Greater or Equal ») fait un saut au label spécifié si et seulement si SF = OF. On
l’utilise en arithmétique signée pour savoir si un nombre est supérieur ou égal à un autre.
JL (« Jump if Less ») fait un saut au label spécifié si et seulement si SF <> OF. On l’utilise en
arithmétique signée pour savoir si un nombre est inférieur à un autre.
JLE (« Jump if Less Or Equal ») fait un saut au label spécifié si et seulement si SF <> OF ou ZF
Ces instructions testent un flag unique et exécutent ou non le saut selon la valeur de ce flag.
Remarques : Ce mnémonique correspond au même opcode que JB. Il est souvent employé pour vérifier
que l’appel d’une interruption n’a pas déclenché d’erreur.
Exemple :
JNZ (« Jump if not Zero ») fait un saut au label spécifié si et seulement si ZF = 0. Ce mnémonique
correspond au même opcode que JNE.
Exemple : INC CL
Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
Si Source est un mot : AX est multiplié par Source et le résultat est placé dans
DX:AX.
Si Source est un double mot : EAX est multiplié par Source et le résultat est placé dans
EDX:EAX.
Exemples : MUL CX
IMUL Source :
Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
Si Source est un mot : AX est multiplié par Source et le résultat est placé dans
DX:AX.
Si Source est un double mot : EAX est multiplié par Source et le résultat est placé dans
EDX:EAX.
IMUL Destination, Source : Multiplie Destination par Source et place le résultat dans
Destination.
IMUL Destination, Source, Valeur : Multiplie Source par Valeur et place le résultat dans
Destination.
Si Source est un octet : AX est divisé par Source, le quotient est placé dans AL et le
reste dans AH.
Si Source est un mot : DX:AX est divisé par Source, le quotient est placé dans AX et
le reste dans DX.
Si Source est un double mot : EDX:EAX est divisé par Source, le quotient est placé
dans EAX et le reste dans EDX.
Si Source est un octet : AX est divisé par Source, le quotient est placé dans AL et le
reste dans AH.
Si Source est un mot : DX:AX est divisé par Source, le quotient est placé dans AX et
le reste dans DX.
Si Source est un double mot : EDX:EAX est divisé par Source, le quotient est placé
dans EAX et le reste dans EDX.
NEG registre
Description : Effectue un NON logique bit à bit sur Destination (i.e. chaque bit de Destination
est inversé).
b) l’instruction OR (« Logical OR »)
OR registre, constante
Description : Effectue un OU logique inclusif bit à bit entre Destination et Source. Le résultat
est stocké dans Destination.
Description : Effectue un ET logique bit à bit entre Destination et Source. Le résultat est
stocké dans Destination.
Description : Effectue un ET logique bit à bit entre Destination et Source. Le résultat n’est pas
conservé, donc Destination n’est pas modifié. Seuls les flags sont affectés.
Cet opérateur est souvent utilisé pour tester certains bits de Destination.
Exemple :
Description : Effectue un OU logique exclusif bit à bit entre Destination et Source. Le résultat
est stocké dans Destination.
Remarque : Pour remettre un registre à zéro, il est préférable de faire “XOR AX, AX” que
“MOV AX, 0”. En effet, le résultat est le même mais la taille et surtout la vitesse d’exécution
de l’instruction sont très largement optimisées.
Description : Décale les bits de Destination de Source positions vers la gauche. Les bits les
plus à droite sont remplacés par des zéros.
Exemple : SHL AX, 4 ;permet de multiplier par 16 de façon infiniment plus rapide que MUL
Description : Décale les bits de Destination de Source positions vers la droite. Les bits les
plus à gauche sont remplacés par des zéros.
Description : Effectue une rotation des bits de Destination de Source positions vers la gauche.
Le dernier bit à être sorti à gauche et à être rentré à droite est placé dans CF. OF est mis à 1 si
et seulement si le signe de Destination a changé.
Description : Effectue une rotation des bits de Destination de Source positions vers la droite.
Le dernier bit à être sorti à droite et à être rentré à gauche est placé dans CF. OF est mis à 1 si
et seulement si le signe de Destination a changé.
Description : Effectue une rotation des bits de Destination de Source positions vers la gauche.
CF est utilisé comme intermédiaire : chaque bit qui sort à gauche est placé dans CF, et le
contenu de CF est ensuite réinséré à droite. OF est mis à 1 si et seulement si le signe de
Destination a changé.
Description : Effectue une rotation des bits de Destination de Source positions vers la droite.
CF est utilisé comme intermédiaire : chaque bit qui sort à droite est placé dans CF, et le
contenu de CF est ensuite réinséré à gauche. OF est mis à 1 si et seulement si le signe de
Destination a changé.
Syntaxe : CLC
Description : Met CF à 0.
Indicateurs affectés : CF
Syntaxe : STC
Description : Met CF à 1.
Indicateurs affectés : CF
Syntaxe : CLD
Description : Met DF à 0.
Indicateurs affectés : DF
Syntaxe : STD
Description : Met DF à 1.
Indicateurs affectés : DF
Syntaxe : CLI
Description : Met IF à 0.
Syntaxe : STI
Description : Met IF à 1.
Indicateurs affectés : IF
Syntaxe : CMC
Indicateurs affectés : CF
Syntaxe : LAHF
Syntaxe : SAHF
Remarques : Source ne peut être une valeur immédiate. Il est possible d’abréger votre code
source en écrivant par exemple “PUSH AX BX BP”. Le compilateur écrira alors trois fois
l’instruction PUSH du langage machine. Il est possible également d’empiler des doubles
mots.
Description : Dépile le mot qui se trouve au sommet de la pile et le place dans Destination. SP
est incrémenté de 2.
Syntaxe : PUSHF
Syntaxe : POPF
Description : Dépile le mot qui se trouve au sommet de la pile et le place dans le registre des
indicateurs. SP est incrémenté de 2.
Remarque : POPFD est utilisé pour un registre des indicateurs codé sur 32 bits.
Syntaxe : PUSHA
Syntaxe : POPA
Description : Restaure AX, BX, CX, DX, BP, SI, DI et SP à partir de la pile.
Syntaxe : RET
Appel à une procédure d’un autre segment code. FAR PTR informe l’assembleur que la
procédure appartient à un autre segment code.
d) l’instruction RETF
Syntaxe : RETF
a) l’instruction LOOP
Syntaxe : CBW
Description : Convertit l’octet signé stocké dans AL en un mot (signé) stocké dans AX. Ainsi,
si AL est négatif, AH sera rempli de 1 binaires, sinon, AH sera mis à 0.
Syntaxe : CWD
Description : Convertit le mot signé stocké dans AX en un double mot (signé) stocké dans
DX:AX. Ainsi, si AX est négatif, DX sera rempli de 1 binaires, sinon DX sera mis à 0.
Description : Déplace le contenu signé d’un registre de 8 bits dans un registre de 16 bits, ou
bien déplace le contenu signé d’un registre de 16 bits dans un registre de 32 bits. Si Source est
négatif, la partie haute de Destination sera remplie de 1 binaires, sinon elle sera remplie de 0.
Description : Déplace le contenu non signé d’un registre de 8 bits dans un registre de 16 bits,
ou bien déplace le contenu signé d’un registre de 16 bits dans un registre de 32 bits. La partie
haute de Destination sera donc mise à 0.
Description : Charge un octet ou un mot depuis un port d’entrée-sortie dans AL ou AX. Port
peut être DX ou bien une constante de 8 bits.
Description : Ecrit dans Port la valeur contenue dans Source. Source ne peut être que AL ou
AX. Port est une constante ou bien DX.
V. LES MACROS
Il est possible de choisir pour certaines suites d’instructions un nom qui les représente.
Lorsque le compilateur (en fait, le préprocesseur) rencontrera ce nom dans votre code source,
il le remplacera par les lignes de code qu’il désigne. Ces lignes forment une « macro ».
Les macros, à la différence des procédures, n’ont aucune signification pour la machine. Seul
le compilateur comprend leur signification. Elles ne sont qu’un artifice mis à la disposition du
programmeur pour clarifier son programme. Lorsque le compilateur rencontre le nom d’une
macro dans votre code, il le remplace par le code de la macro. Tout se passe exactement
comme si vous aviez tapé vous-même ce code à la place du nom de la macro.
Ainsi, si vous appelez quinze fois une macro dans votre programme, le compilateur écrira
quinze fois le code de cette macro. C’est toute la différence avec les procédures qui ne sont
écrites qu’une seule fois mais peuvent être appelées aussi souvent qu’on veut à l’aide d’un
CALL.
Voici comment écrire une macro : l’exemple suivant sert à écrire un message à l’écran.
Le mot “text?” est un « paramètre ». Le point d’interrogation n’est pas requis ; nous l’avons
mis pour indiquer qu’il s’agit d’un paramètre et non d’une variable. Une macro peut utiliser
plusieurs paramètres séparés par des virgules.
En assembleur, chaque label doit avoir un nom unique. Or, si une macro est appelée plusieurs
fois, les mêmes noms seront utilisés. Il faut donc la plupart du temps inclure la directive
LOCAL qui forcera le compilateur à changer le nom des labels à chaque appel de la macro.
Attention : cette directive doit suivre immédiatement la déclaration de la macro.
Supposons à présent que l’on veuille écrire à l’écran le message « Je suis bien content » et
revenir à la ligne à l’aide de notre macro ecrit_texte.
La syntaxe suivante :
La directive EQU a un rôle voisin de celui des macros. Elle permet de remplacer un
simple mot par d’autres plus complexes. Son intérêt est qu’elle peut être invoquée en plein
milieu d’une ligne.
Quelques exemples :
Version EQU 2
Mettre_dans_AH 4Ch
Interruption_21h
Il est possible d’accéder à des procédures, des macros ou des définitions EQU qui se
trouvent dans d’autres fichiers. Cela permet de se constituer des librairies de macros ou de
procédures que l’on peut réutiliser d’un programme à l’autre.
La condition “if1” indique au compilateur que l’inclusion ne doit s’effectuer que lors de la
première passe. Le fichier TOTO.LIB est un fichier texte tout à fait banal qui contient des
lignes de code en assembleur.
1. Introduction
1) Les interruptions processeur (celles générées par une erreur de calcul interne du genre
"Divide by zero" par exemple).
2) Les interruptions "hardware" ou matérielle, et que nous allons détailler ci-après.
3) Les interruptions "software" ou logicielle (lorsqu’une application a besoin d’avoir
accès à un morceau de code commun contenu et contrôlé par un autre morceau de
code et situé dans une location mémoire très différente et qui peut varier, comme le
BIOS par exemple).
2. Les IRQ
Tout d’abord, IRQ veut dire "Interrupt ReQuest", tout simplement, c’est-à-dire ReQuête
d’Interruption, et cette appellation est exclusivement réservée aux interruptions matérielles.
Elles sont générées, comme leur nom l’indique, par des composants extérieurs au processeur,
mais pas forcément extérieurs au PC lui-même. Le "System Timer" interne, par exemple,
interrompt le processeur en utilisant l’IRQ 0 ; c’est un composant de la carte mère. A chaque
fois que vous pressez une touche du clavier vous interrompez le processeur par l’intermédiaire
de l’IRQ 1. En fait, tout périphérique connecté d’une manière ou d’une autre au processeur et
ayant besoin de se faire entendre devra interrompre le processeur.
Chaque fois que le processeur est interrompu, il va chercher l’adresse du code à exécuter
dans une table. Par exemple, le pilote logiciel fourni avec une carte d’acquisition vidéo et
utilisant l’IRQ 10 devra aller mettre dans cette table, lors de son installation en mémoire,
l’adresse du code qu’il souhaite voir exécuter lorsque son périphérique (la carte
d’acquisition) désirera prendre la main.
Ceci était vrai lorsque les IRQ n’étaient pas partageables… Maintenant c’est un peu plus
compliqué puisqu’un morceau du système d’exploitation va gérer ce processus
dynamiquement, mais nous verrons cela un peu plus tard.
a) Le PIC
Il y a quelques années nous pouvions encore physiquement trouver sur nos cartes mères 2
composants qui géraient les interruptions : les PICs ("Programmable Interrupt Controler").
Maintenant ils sont intégrés au sein de ce que l’on appelle le "Chipset" ("ensemble de
composants" en français, regroupés au sein d’1 ou 2 boîtiers en tout), mais le principe reste
exactement le même. Il y a 2 PICs, sachant chacun gérer 8 interruptions hardware. Le
premier gère les IRQ de 0 à 7, et le second de 8 à 15. La sortie du second PIC est câblée sur
l’entrée correspondant à l’IRQ 2, donc sur le premier PIC, et seule la sortie du premier PIC
part réellement vers le processeur (voir figure ci-dessous).
IRQ 1 = Clavier
IRQ 9 = "disponible"
IRQ 10 = "disponible"
IRQ 11 = "disponible"
On accuse souvent le partage des interruptions, processus décrit un peu plus bas, d’être la
cause de lenteur dans la capture et/ou dans la restitution de la vidéo, et donc de générer des
saccades. Mais en fait, qu’il y ait beaucoup plus de lignes d’IRQ ou pas, en d’autres termes
que l’on ait besoin de les partager ou pas, le problème réel vient simplement du fait qu’il faut
servir plus de choses que le PC n’est capable d’en absorber… Le partage lui-même n’y est
généralement pas pour grand chose.
b) L'APIC
Depuis peu de temps, le hardware de nos PC a évolué. Les chipsets intègrent maintenant, en
plus du PIC standard, un APIC ("Advanced Programmable Interrupt Controler"). Nous
reparlerons un peu plus tard des intérêts qu'apporte l'APIC en terme de gestion des
interruptions. D'un pur point de vue matériel, il est présent sur toutes les machines
multiprocesseurs et sur les nouvelles générations de PC compatibles Windows XP. L'APIC
permet de traiter jusqu'à 256 requêtes d'interruption différentes - les 16 premières étant
équivalentes aux 16 IRQ que nous venons de décrire. En réalité l'APIC est présent dans le
chipset de nombreux PC modernes, au même titre que le simple PIC, par contre, il n'est pas
forcement utilisable. Pour qu'il le soit, il faut que le BIOS d'un tel PC ait été écrit pour cela,
c'est-à-dire qu'il ait configuré cet APIC, en plus du PIC, pour être prêt à travailler. Jusqu'à
présent seuls les PC multiprocesseur étaient dans ce cas, car il s'agit alors d'une obligation
pour pouvoir le faire fonctionner en multiprocesseur justement. On parle alors de BIOS "I/O
APIC" ou même plus directement "IOAPIC" (I/O pour "Input" "Output" - entrée sortie).
Depuis peu, Microsoft impose aux fabricants de PC de systématiser l'utilisation de l'APIC,
même sur les PCs monoprocesseur. C'est une des conditions à l'obtention du logo Microsoft
"Designed for Windows XP" que les constructeurs collent fièrement sur la face avant de
chaque machine. Ceci sera visible, bien-sûr sous Windows XP, mais également sous
Windows 2000 (voir chapitre H). Nous parlerons alors du fonctionnement de Windows 2000
en "mode PIC" ou en "mode APIC". Par contre Windows 98 ne saura pas utiliser ce nouveau
contrôleur d'interruptions et se contentera, même sur ces nouvelles machines pourtant
configurées pour le support de l'APIC, à travailler avec le PIC standard.
Avant même de parler de partage d’interruption, il faut d’abord parler d’attribution. Dans
l’exemple ci-dessus nous avons donc 4 IRQs disponibles. Mais en fait, seulement 4 (9, 10, 11
Ajoutons maintenant une carte son dans notre PC. Il y a 3 cas possible :
Comme nous savons que l’IRQ 5 est disponible et que beaucoup d’anciennes applications
DOS s’attendent à ce que cette carte parle à travers l’IRQ 5 (compatibilité Sound Blaster),
configurons la donc en IRQ 5 avec le bon choix de cavalier.
b) Une carte plus récente, mais toujours ISA, sans cavalier de configuration.
A propos, "PCI" sont les initiales de "Peripheral Component Interconnect" ; il s’agit d’une
norme régie par un groupe d'intérêt .Revenons-en à notre carte PCI : n’y cherchez pas de
cavalier, toutes les cartes PCI sont obligatoirement PnP ; mais c’est une autre sorte de PnP,
beaucoup plus subtil. En effet, maintenant les fameuses lignes d’interruption ne sont plus
connectées au bus PCI, contrairement aux connecteurs ISA. Elles ont été remplacées par
d’autres lignes d’interruptions : les interruptions PCI. C’est finalement le BIOS ou/et l’OS
qui iront au moment voulu connecter ces lignes à une entrée du PIC, donc à une vraie IRQ.
En conclusion, plus de connexion permanente entre les cartes PCI et les IRQ contrairement
aux cartes ISA, donc une totale flexibilité.
A ce niveau là, 2 cas de figure se présentent : soit nous allons charger un OS PnP ou non…
Et oui, ça se complique !
Windows 95 (Win95)
Windows 98 (Win98)
Windows NT (NT4).
Seulement pour qu’un OS PnP puisse se débrouiller tout seul, encore faut-il pouvoir dire au
BIOS de ne pas attribuer les ressources lui-même.
A l’inverse, dans le cas d’un OS non-PnP, il faut impérativement dire au BIOS d’attribuer les
ressources lui-même sinon elles ne pourront tout simplement pas marcher une fois l’OS
chargé.
Pour cela, il y a un champ dans le SETUP des BIOS s’intitulant généralement "PnP OS" où
l’on répond par oui ou par non, ou "Operating System running" où l’on répond par DOS/NT
ou par Windows95/…
Tout composant dit "legacy", terme technique anglo-saxon faisant référence à l’ancienne
norme PC AT et que l’on pourrait avantageusement traduire par "l’héritage", ne peut en
aucun cas accepter le partage des interruptions. La raison est très simple : ces composants
sont physiquement connectés aux entrées du PIC comme nous l’avons déjà vu, alors que pour
pouvoir partager une interruption il faut bien sûr pouvoir gérer une connexion de plusieurs
lignes d’interruption intermédiaires avant d’en brancher physiquement une sur le PIC. Pour
cela, seule la flexibilité qu’offre le bus PCI le permet. De fait, dans le tableau des IRQs ci-
dessus, seules les IRQs 9, 10 et 11 peuvent encore être considérées comme partageables. En
effet, toutes les autres IRQs sont utilisées par des composants dits "legacy", et ce, en
considérant que le PC est équipé d’une carte son connectée sur le bus ISA du PC.
Les BIOS des PCs modernes autorisent maintenant aussi le partage d’interruption sur les
IRQs de 3 à 7, à partir du moment où aucun composant "legacy" n’est connecté dessus. Par
exemple, si vous n’utilisez aucun de vos ports série, vous pouvez alors, depuis le SETUP de
certains BIOS, les déclarer comme inactifs. Ceci correspondra à une réelle déconnexion
physique des ports qui seront alors rendus invisibles, même si l’OS chargé derrière est PnP,
et le bénéfice direct sera la libération des IRQs 3 et 4 dans cet exemple précis.
Les périphériques PCI peuvent partager les mêmes IRQs parce qu'au démarrage du PC le
BIOS construit une table appelée "PCI IRQ routing table". Cette table contient les entrées de
chaque périphérique PCI qui se verra attribuer un numéro d'IRQ ISA (rappelez-vous, les
numéros de 0 à 15 que l'on a décrit plus haut). Ces assignations d'IRQ ISA sont aussi liées,
par construction matérielle cette fois, aux lignes physiques d'interruptions PCI associées aux
différents connecteurs PCI d'un PC donné. L'ensemble des 2 se combine en ce qui s'appelle un
"Link Value". Un Link Value est utilisé à chaque fois qu'une
Ce terme d'IRQ Steering (direction d'IRQ) désigne le processus software au sein de l'OS qui
assigne dynamiquement les IRQs du bus PCI vers les périphériques en ayant besoin. Comme
on a l'a déjà vu, seules les IRQ non déjà assignées à un périphérique ISA (ou legacy)
pourront l'être vers un périphérique PCI, car une IRQ ne peut en aucun cas être assignée à la
fois à un périphérique ISA et à un périphérique PCI. Ce moteur d'assignation des IRQ PCI
(PCI bus IRQ Steering) va donc attribuer les IRQs, que le BIOS l'ait ou non déjà fait. Il est
donc préférable de ne pas le laisser faire au BIOS pour éviter que Windows assigne une IRQ
différente de celle attribuée par le BIOS à un périphérique donné (ceci est très rare, mais peut
toutefois se produire).
Par exemple, imaginons que vous ayez déjà une carte PCI dans votre PC, pour laquelle l'IRQ
10 a été attribuée. Maintenant vous ajoutez une vieille carte ISA non-PnP configurée par
cavalier sur l'IRQ 10 justement. Vous vous trouvez alors dans un cas flagrant de conflit
d'IRQ. Mais pas pour longtemps, parce qu'automatiquement Windows va :
reprogrammer une IRQ libre pour être une IRQ PCI (par exemple la 11),
De ce fait, la carte ISA non-PnP pourra cette fois utiliser sans problème l'IRQ 10, et
inversement notre carte PCI se verra assigner l'IRQ 11 qui a donc été réservée pour cet usage.
Un "IRQ Holder for PCI Steering" est un indicateur montrant qu'une IRQ a été programmée
pour servir des périphériques PCI, même si aucun périphérique ne s'en sert concrètement
(une sorte de réservation en quelque sorte). Bien évidemment aucun périphérique ISA ne
pourra utiliser une IRQ sur laquelle se trouve un IRQ Holder.
La liste de toutes les interruptions est alors affichée, y compris les IRQ Holders, sous la
mention de "IRQ Holder for PCI Steering" pour Win95 à partir de la SR2 ainsi que pour
Win98 dans le cas de machines n'ayant pas de BIOS ACPI (APM), et sous la mention de
"IRQ Holder ACPI pour PCI IRQ Steering" pour Win98 dans le cas de machines ayant un
BIOS ACPI.
"ACPI" sont les initiales de "Advanced Configuration and Power Interface". Il s'agit d'une
évolution de l'ancienne spécification "APM" qui sont les initiales de "Advanced Power
Management". Il s'agit en fait de la gestion de la mise en route et de la mise en veille
progressive jusqu'à l'arrêt du PC. L'énorme avantage de l'ACPI comparé à l'APM est sa
capacité à gérer la mise en route, l'endormissement et l'arrêt des périphériques connectés au
PC. Pour cela, il faut bien, d'une manière ou d'une autre, que la couche ACPI dans Windows
gère les ressources des périphériques, en particulier les IRQs. Pourquoi ? Et bien par
exemple, la norme ACPI permet à un PC éteint ou en veille profonde de se réveiller lorsque
le téléphone sonne pour activer la fonction répondeur ou fax, et ce, grâce à l'IRQ assignée au
modem. Voilà pourquoi la gestion des IRQs est différente entre une ancienne machine
seulement APM et un PC moderne ACPI.
S'assurer que la case "Afficher les périphériques par type" est bien cochée.
Il s'agit ici de la table des IRQ d'un PC récent dont le BIOS est APIC. Mais hélas, comme
nous sommes sous Windows 98 seul le PIC standard est utilisé et donc seules 16 IRQ sont
disponibles.
Il m'a fallu un peu tricher, en passant par MS-Paint, pour réussir à afficher toutes les IRQ en
une seule page (cela se voit par les 2 ascenseurs)…
Cet exemple est intéressant car la configuration est assez chargée. Chose curieuse, l'IRQ 2
apparaît, ce qui est assez rare, et plutôt inutile en fait. Comme nous l'avons vu un peu plus
haut, l'IRQ 2 est utilisée pour cascader le second PIC sur le premier. Elle est donc listée ici
L'IRQ 5 semble partagée. Elle est utilisée par le composant audio qui est intégré dans le
chipset. C'est son rôle principal. Mais elle est également utilisée par le SMBus ("System
Management Bus"). C'est ce qui permet au PC de reprogrammer certaines chose en interne
(du genre mémoire spéciale ou sont stocké les numéros de série, par exemple). Cette
programmation ne se fait généralement pas depuis Windows, ou alors depuis certains
programmes très particuliers seuls habilités à le faire, et utilisés très exceptionnellement. On
ne parlera donc pas vraiment de partage dans ce cas particulier.
Et enfin nous avons un vrai partage de l'IRQ 9 entre le premier contrôleur USB (ce PC a 2
contrôleurs USB de 2 ports chacun, le second étant sur l'IRQ 11) et le LAN ("Local Area
Network" - carte réseau intégrée ici dans le chipset). On y voit donc 2 "IRQ Holder ACPI
pour PCI IRQ Steering", un pour chacun. L'IRQ SCI ("System Configuration Interrupt"), et
aussi connecté à l'IRQ 9, sert ici à la gestion interne de l'ACPI.
Je vous encourage maintenant à poursuivre votre lecture, puis à revenir voir cet exemple
après avoir lu la section sur Windows 2000 en mode APIC. En effet, j'ai utilisé exactement le
même PC, sur lequel j'ai simplement réinstallé W2K. Vous verrez ainsi clairement la
différence !
a) Introduction
Windows 2000 (W2K) a été développé avec l'objectif, parmi beaucoup d'autres bien sûr, de
justement permettre le partage des IRQs pour les périphériques PCI. Tout périphérique PCI,
même s'il se voit attribuer des ressources par le BIOS sera ré-analysé par W2K qui n'hésitera
pas à les changer si nécessaire. C'est donc un comportement normal sous W2K que d'avoir ce
partage, et ce, tout particulièrement pour les PC ACPI, ACPI qui sous Windows 2000 ne
fonctionnera correctement qu'à cette condition.
Sous Windows 2000 vous trouverez généralement l'IRQ 9 assignée à une grande partie des
périphériques PCI, et même probablement à tous ! C'est normal, c'est comme cela que cela
doit être, et on ne peut pas changer les configurations d'IRQ sous W2K.
Dans le détail des IRQs du Gestionnaire de Périphériques vous verrez apparaître la liste des
IRQs ISA (de 0 à 15) avec éventuellement quelques trous pour celles non-utilisées, et en
particulier en face de l'IRQ ISA 9 la mention "Microsoft ACPI-Compliant System", puis en
dessous la liste des IRQ PCI, toutes portant le même numéro (9). En fait seule l'IRQ 9 est
utilisable par le bus PCI pour l'IRQ Steering sous W2K. Pour la première fois dans le mode
des PCs un OS gère différemment les IRQs ISA (donc non-partageables) et les IRQs PCI.
Cette caractéristique permet en toute sécurité d'ajouter d'autres périphériques sans aucun
risque de conflit d'IRQs. Grâce aux Link Values définis par le BIOS et qui permet de savoir
avec qui communiquer, une seule IRQ servira l'ensemble des périphériques. Ce processus
La raison de cette limitation est liée à la gestion des cartes multiprocesseurs, de l'IOAPIC
(caractéristiques des BIOS multiprocesseurs) et des bus PCI multi-branche (donc
globalement des cartes mères beaucoup plus complexes) qu'offre W2K au contraire de
Win98. Dans ces circonstances complexes la re-programmation dynamique des IRQs serait
beaucoup trop risquée et cette caractéristique n'a donc pas été implémentée au sein de W2K.
Si vous êtes dans ce cas précis (PC relativement récent, donc compatible ACPI, et W2K), ne
perdez pas votre temps avec les IRQs, n'essayez pas de changer les configurations de votre
BIOS pour forcer telle ou telle carte PCI sur telle ou telle IRQ : W2K ne s'en occupera pas, il
les laissera toutes sur l'IRQ 9, point-barre ! Mais d'autre part, ne prenez pas peur du fait que
toutes vos cartes PCI partagent cette IRQ 9. Cela marchera sans problème, c'est fait pour. Et
si vous avez des problèmes de performance, de saccades pendant vos captures vidéo, ne les
mettez pas sur le compte du partage de l'IRQ 9, c'est ailleurs qu'il faut chercher la cause de
cette mauvaise performance
Que voyons-nous donc d'intéressant ici ? Déjà la séparation entre les IRQ ISA et les IRQ
PCI. Ensuite nous voyons l'IRQ ISA 9, qui est réservée pour l'ACPI, et donc, nous voyons de
fait l'IRQ PCI 9 (c'est-à-dire la même physiquement que l'IRQ ISA 9 précédemment
réservée) qui se retrouve partagée entre tous les autres périphériques PCI de ce PC (l'USB, le
LAN, l'audio et également le SMBus avec les mêmes remarques que dans l'exemple
précédant à ce sujet).
e) La configuration et la re-programmation des IRQ sous W2K sur des PCs non-ACPI
Dans le cas où votre PC serait assez ancien, avec un BIOS et un hardware ne supportant pas
la norme ACPI, Windows 2000 utilisera lors de son installation une HAL différente. La HAL
(Hardware Abstraction Layer) est la couche de l'OS qui dialogue directement avec les
composants électroniques sur la carte mère et directement avec le BIOS du PC. Normalement
le programme d'installation de W2K installe la HAL ACPI, mais dans ce cas il chargera une
HAL "Standard PC" puisque le BIOS lui aura répondu qu'il n'est pas ACPI.
Ne rêvez pas ! Ce n'est pas une solution à vos problèmes, rappelez-vous : il n'y a pas de
problème d'IRQ !
Mais si vraiment vous y tenez, alors, depuis le BIOS, forcez telle ou telle carte sur l'IRQ que
vous voulez lui assigner, configurez votre BIOS en "PnP Operating System = no", et bootez
W2K. Puis regardez l'effet que ces modifications auront apportées à vos attributions de
ressources au sein de W2K. Ne rêvez pas non plus, vous n'arriverez jamais à faire vraiment
ce que vous voulez, en tous cas jamais du premier coup.
Mais encore une fois, ce n'est pas par ce biais, sauf cas très exceptionnel, que vous pourrez
résoudre vos problèmes, et en tous cas, si l'idée vous venait de vouloir remplacer la HAL
ACPI, normalement chargée sur un système ACPI, par la HAL Standard PC pour gagner en
flexibilité sur les attributions d'IRQ, ne dites surtout pas que l'idée vient de moi et ne comptez
surtout pas sur mon support : les résultats sont imprévisibles ! La seule solution à peu près
sûre pour y arriver sans risque de tout perdre est de réinstaller W2K depuis le début en
pressant F5 pendant la phase d'inspection du hardware (au tout début de l'installation), et en
f) Les IRQ sous W2K sur des PCs ACPI en mode APIC
Comme nous en avons parlé dans le chapitre D, les PC multiprocesseurs et depuis peu
certains PC monoprocesseur (et très bientôt tous les PC) ont été configurés par leur BIOS
pour pouvoir utiliser l'APIC. Cet APIC est capable de gérer 256 interruptions plutôt que
seulement 16. Si W2K a été installé sur une telle machine il utilisera donc l'APIC plutôt que
le PIC. Le résultat sera immédiat et flagrant : plutôt que de partager les 16 IRQ existantes
entre les différents périphériques, W2K va simplement assigner par ordre croissant les 256
numéros disponibles. Les 16 premiers (de 0 à 15) seront généralement réservés aux
périphériques ISA, puis les chiffres au-delà de 15 pour les périphériques PCI.
Toutefois, cela ne va pas fondamentalement changer les choses… Rappelez-vous : les Link
Values définis par le BIOS en mode PIC permettaient de retrouver le pilote à utiliser lors de
l'arrivée d'une interruption sur une ligne partagée. Le mode APIC permet donc de légèrement
simplifier ce processus en attribuant une ligne unique par ressource. Le gain de temps est en
réalité infime, "epsilonesque" même…. Par contre, la tranquillité d'esprit qu'apporte cette
nouvelle caractéristique des PCs d'aujourd'hui devrait clore définitivement ce très
controversé débat sur le partage des interruptions.
Déjà l'inutile IRQ 2 disparaît. Elle disparaît réellement car elle reste réservée pour le PIC
qui, bien que non-utilisé dans ce cas, est toujours physiquement présent dans le chipset.
L'IRQ 5 utilisée par le SMBus est listée plus loin, avec les IRQ PCI. Cela clarifie le fait
qu'il s'agisse d'un composant PCI et non d'un composant ISA.
L'IRQ ISA 9 est toujours utilisée par W2K pour la gestion de l'ACPI, pas de changement
puisqu'il s'agit d'un composant de type ISA cette fois (notez la plus grande simplicité et clarté
des libellés).
9. Questions / Réponses
Question : Avec tout ce que tu as écrit ma recommandation tout azimut serait, hors Windows
2000, de mettre la carte d’acquisition sur l’IRQ 9 et pour l’obtenir créer un profil matériel
retirant le maximum de choses de manière à ce que l’OS ne puisse que vouloir utiliser cette
IRQ de niveau le plus prioritaire pour les slots PCI.
Réponse : Je ne suis pas sûr que le jeu en vaille la chandelle... Déjà, le coup de l'IRQ la plus
prioritaire n'a d'importance que lorsque 2 IRQs arrivent en même temps ou lorsque la
seconde arrive alors que la première n'a pas encore fini d'être traitée. Si une IRQ 10 arrive en
plein traitement de la 9, elle attendra que la 9 ait fini, alors que si c'est l’IRQ 1 qui arrive, le
processus de traitement de la 9 sera effectivement interrompu. La manipulation que tu
suggères n'a d'intérêt que si tu rencontres réellement des problèmes de capture ou
d'exportation vidéo, mais autrement, un bon conseil, si ton PC ne marche pas trop mal, en
d’autres termes si tes captures et tes exportations vidéo se déroulent correctement, alors, ne
touche à rien ! Il est bien évident qu'il faut mettre toutes les chances de son coté pour réussir
une capture et une exportation, en ne faisant rien faire d'autre au PC en même temps. S'il est
assez facile de penser à ne pas jouer à Doom en même temps, il peut être plus difficile de
désactiver la fonction "Répondeur téléphonique" de son modem juste avant de lancer une
capture. Dans ce cas, effectivement Claude, ta solution de créer plusieurs profils utilisateur
est très intéressante, car elle permet assez simplement d'invalider tel ou tel périphérique d'un
profil, en éliminant par exemple le modem pour une utilisation purement "Vidéo" de son PC.
Question : Et un point dont tu ne parles pas c’est la manière dont la position physique d’un
slot influe sur l’attribution de l’IRQ, par exemple où placer une carte modem PCI pour que
son IRQ soit d’un rang moins prioritaire.
Question : Et comment se passe l’attribution des IRQs pour les slots AGP ?
Réponse : De la même manière et en même temps. Le slot AGP (je dis bien "le", car il est
unique dans un PC) est en fait un second bus PCI, juste plus rapide, et dédié pour une seule
carte, la carte graphique. Cette carte graphique n'utilise une interruption que dans un mode
très particulier (et très rare) ou elle doit synchroniser l'application qui la pilote, en fait pour
que cette application sache quand le balayage arrive en fin de ligne et en bas de l'image. Ce
n'est quasiment plus jamais utilisé, sauf pour de très anciennes applications. Oublie ! Jamais
d'interruption de la part de la carte graphique. D’ailleurs, l’interruption utilisée par la carte
graphique est quasiment la seule que Win98 prend le risque de partager avec une autre
(n’oublions pas que W2K est le seul aujourd’hui à vraiment bien gérer le partage des
interruptions).
Réponse : Déjà, les interruptions ne sont reliées à la même IRQ que lorsque l'OS décide de
les y relier, et ce presque exclusivement dans le cas de W2K comme nous l’avons vu. Pour ce
qui est du délai d’exécution, il s’agit en effet que de quelques lignes de code assembleur à
exécuter, donc un délai normalement inférieur au millionième de seconde sur une machine
actuelle !
Question : Par exemple si une carte SCSI partage la même IRQ que la carte d’acquisition
IEEE1394, les demandes peut-être répétées de la carte pour gérer les disques durs ne
peuvent-elles créer des soucis de rapidité / continuité d’un travail de rendu / transfert de
fichier DV effectué en simultané ?
Réponse : Il y a autre chose dont nous n'avons pas parlé, c'est le DMA Bus Mastering !!!
Certaines cartes comme les cartes SCSI ou les cartes IEEE1394 sont généralement des cartes
dites "Bus Master", c'est-à-dire qu'elles sont capables de prendre le contrôle du bus PCI, et de
fait elles ont un accès direct à la mémoire et aux autres cartes ! Typiquement, une carte
Studio DV est capable d'envoyer son flux vidéo directement à la carte SCSI ou au port IDE
(s'il est configuré en DMA) sans même passer par le processeur. Si une IRQ arrive à ce
moment là, le processeur n'aura rien à interrompre réellement puisqu'il ne faisait qu'attendre
que quelqu’un daigne vouloir l'occuper. Aucun risque dans ce cas de gêner la communication
entre le 1394 et le SCSI qui continuera... sauf si le processeur a besoin à son tour du bus PCI
ou, pire encore, des disques SCSI. Mais le DMA c'est un autre débat, bien plus complexe
encore.