Sommaire

BONJOUR
Musique "Invention BWV784" J.S. BACH 1685-1750

Tutoriel et Help

ASM and Samples

Nous veillons aux respects des droits d'auteur : si par erreur un contenu protégé est disponible sans indication sur ce site, merci de contacter l'administrateur, il sera retiré ou mis à jour dans les plus brefs délais.


L'assembleur avec C++ Builder

Table des matières Vu dans http://glouise.developpez.com/assembleur

  • I. Petit rappel improvisé
  • II. Principe de l'assembleur
  • III. La mémoire
  • IV. Syntaxe d'accès avec C++Builder
  • V. Premier cas : mémoire déclarée dans la pile
  • VI. Deuxième cas : mémoire déclarée dans le tas
  • VII. Utilisation de l'assembleur sous DOS
  • VIII. Test sous C++Builder

Dans ce qui suit, nous nous limitons d'une part à C++ Builder pour PC et d'autre part à l'assembleur du 80386 de chez Intel. Pour utiliser cet assembleur, vous avez deux possibilités : soit insérer dans votre programme C/C++ des lignes d'assembleur, soit utiliser l'assembleur en tant que tel sous DOS via une "invite de commandes" et donc écrire un programme intégralement en assembleur. Nous abordons l'un et l'autre aspect de la question.

I. Petit rappel improvisé

C'est en 1974 que naît le premier microprocesseur de la série, le 8080. Son nom évoque probablement les années 80 à venir, lesquelles inaugurent l'explosion de l'informatique, tout comme son très proche cousin, le Z-80, surs ensemble du 8080 avec lequel il est full compatible.
Le microprocesseur 8080 est une pure merveille. Mais d'une part, c'était un microprocesseur 8 bits (cela signifie que son bus de lecture-écriture est de huit fils parallèles en conséquence de quoi on ne peut lire ou écrire en mémoire que huit bits à la fois soit un octet) et d'autre part il ne pouvait adresser que 64K0, ce qui s'est vite avéré insuffisant. Sa structure interne est d'une rare simplicité avec ses huit registres 8 bits A, B, C, D, E, H, L et M. Un registre est une sorte de variable, c'est un circuit qui se trouve à l'intérieur du microprocesseur (ce pourquoi on parle de "registre interne") et cette variable peut recevoir une valeur. Comme ces registres sont sur 8 bits, on peut leur donner une valeur comprise entre 0 et 255 i.e. en binaire entre 00000000 et 11111111. Le registre A est appelé "accumulateur" car il accumule les résultats, suite à une opération arithmétique, le résultat se trouve dans A. M est un faux registre, il s'agit du contenu pointé par HL où H signifie high et L low. H est donc l'octet haut d'adresse et L l'octet bas d'adresse, HL pointe donc une des 65536 cases mémoire numérotées (en décimal) de 0 à 65535 et M est l'octet pointé par HL. Par exemple l'instruction mov A,M signifie que le contenu pointé par HL est lu dans le registre A (l'accumulateur). Il est intéressant de remarquer la structure octalisante du 8080 avec des huit registres, ses huit types d'opérations (addition, addition avec carry, soustraction, soustraction avec carry, ET logique, OU logique, OU Exclusif logique et comparaison), ses huit types de sauts conditionnels en fonction de quatre indicateurs Z, C, S et P (zéro, carry, signe et parité). C'est pourquoi, la programmation en octal était très aisée, en une après-midi on connaissait pratiquement par cœur tous les codes octaux et on lisait l'assembleur directement en octal (base 8). Les deux premiers bits du code opératoire indiquent la nature de l'opération et les deux chiffres octaux suivants indiquent soit un type d'opération soit un registre impliqué dans l'opération. Ainsi si le code opératoire commence par "01", cela signifie "mov", ensuite viennent les deux registres impliqués. Les registres étaient indicés de 0 à 7 (BCDEHLMA). Par exemple mov A,B (le registre B est recopié dans A) s'écrit en octal 170 (soit 01111000 en binaire) car 7 signifie A et 0 signifie B. Inversement mov B,A s'écrit 107. Bref, le 8080 fut un microprocesseur minimal mais parfaitement efficace puisqu'on pouvait tout programmer avec. L'expérience montre qu'un jeu d'instructions réduit suffit largement à la programmation en assembleur, le 8080 en a donné la preuve avec élégance.
Vient ensuite le 8086. Deux grandes différences avec le 8080, d'une part c'est un microprocesseur 16 bits et d'autre part il pointe la mémoire par segments. La segmentation fut donc la grande différence avec le 8080. Avec le 8080, la mémoire était pointée directement par une valeur 16 bits alors qu'avec le 8086 on pointe la mémoire avec deux valeurs, un registre de segment et un offset c'est-à-dire une sorte d'indice ou de pointeur à l'intérieur du segment. Le registre de segment correspond aux 16 bits de poids fort d'une adresse 20 bits (en conséquence de quoi un segment pointe toujours une adresse divisible par 16) et l'offset une valeur 16 bits qui sert de pointeur dans le segment, l'addition de ces deux valeurs, la valeur 16 bits du segment auquel on adjoint quatre zéros à droite pour former une valeur 20 bits divisible par 16 et la valeur 16 bits de l'offset, l'addition de ces deux valeurs donc fournit l'adresse 20 bits invoquée. Le 8086 peut donc adresser 16 fois 64KO soit un MO (un méga octet). Il a quatre registres de segment, CS (code segment), DS (data segment), ES (extra segment) et SS (stack segment). Il a quatre registres généraux 16 bits, AX, BX, CX et DX (accumulateur, base, compteur et données) lesquels sont accessibles séparément en deux fois huit bits avec AH et AL pour AX (AH étant la partie haute de AX et AL sa partie basse donc AX=AH*256+AL), de même BH et BL pour BX, CH et CL pour CX, DH et DL pour DX. Par exemple l'instruction

MOV AX, 1027

qui signifie que le registre AX reçoit la valeur 1027, équivaut au couple d'instructions

MOV AH,4
MOV AL,3

puisque 4*256+3=1027.
Il a également deux registres d'index SI et DI (source index et destination index) et BP (base pointeur) utilisé dans certains modes d'adressage.
Ainsi, le 8086 ne pointe pas directement la mémoire. C'est toujours un couple de valeurs qui simulent une adresse 20 bits, par exemple DS:SI pointe l'adresse DS*16+SI. Puisque IP (instruction pointer) pointe par définition le code segment CS, le code opératoire qui va être lu est à l'adresse CS*16+IP. Par défaut, tous les modes d'adressage mémoire font intervenir le segment de données prévu à cet effet à savoir DS sauf les modes avec BP qui se calculent avec le registre de segment SS (stack segment). Par exemple l'instruction

MOV AL,[SI]

signifie que le contenu pointé par SI est lu dans le registre 8 bits AL. Mais le registre SI n'est que l'offset à l'intérieur d'un segment, le registre de segment impliqué est ici DS (data segment), l'adresse 20 bits lue sera donc DS*16+SI. Sinon, si on veut utiliser un autre registre de segment, il faut alors préfixer l'instruction et écrire par exemple

MOV AL,ES:[SI]

qui signifie qu'on ne calcule plus l'adresse via DS mais ES, l'octet lu dans AL est donc celui situé à l'adresse ES*16+SI. D'ailleurs le désassemblage donnerait

ES:
MOV AL,[SI]

ce qui montre clairement qu'on prévient ainsi le microprocesseur que le calcul de l'adresse se fera pour l'instruction suivante avec ES et non DS. Le quatre pointeurs (SI, DI, BX et BP) et huit modes d'adressage :

BX+SI+dep
BX+DI+dep
BP+SI+dep
BP+DI+dep
SI+dep
DI+dep
BP+dep
BX+dep

Toutes les syntaxes admettent un déplacement compris entre -32768 et +32767. En l'absence de préfixation, le registre de segment impliqué est DS sauf pour les trois modes où BP intervient.
Arrive enfin le 80386, le microprocesseur 32 bits. Sa structure est grosso modo identique à celle du 8086 mais tous les registres sont maintenant sur 32 bits et sont préfixés par la lettre E pour étendu (extended). On a donc les registres généraux EAX, EBX, ECX et EDX, les deux registres d'index ESI et EDI, le pointeur de base EBP. Puisque 32 bits d'adressage correspondent à 4GO, la segmentation n'est plus nécessaire mais les registres de segments sont maintenus pour des raisons de compatibilité ascendante. Pour plus d'informations sur le 80386 en tant que tel, je vous renvoie à d'autres informations.
On peut considérer le 80386 et ses successeurs comme des microprocesseurs bancals car il incluent la notion de segmentation simplement pour être compatibles avec leurs prédécesseurs alors qu'avec 32 fils d'adresse on accède à 4GO de mémoire vive, en conséquence de quoi la notion de segmentation est devenue totalement inutile.

II. Principe de l'assembleur

Le registre 32 bits EIP (instruction pointer) lit l'instruction qu'il pointe en mémoire vive. Le premier octet de chaque instruction est un code opératoire en fonction duquel le microprocesseur déduit les opérandes, il sait donc en fonction du code opératoire qu'il décode sur combien d'octets est codée l'instruction et la signification de ces octets. Puis le microprocesseur exécute cette instruction lue. Si un sous-programme est appelé par l'instruction CALL, l'adresse 32 bits pointée par EIP juste après le CALL qu'il vient de lire est sauvegardée dans la pile. La pile est un espace mémoire spécial où l'on peut sauvegarder des valeurs pour les récupérer ensuite. Quand on écrit par exemple

PUSH EAX

cela signifie que le registre 32 bits EAX est sauvegardé dans cet espace particulier, vous n'avez pas besoin de savoir où. Pour récupérer ensuite EAX on écrit simplement

POP EAX

Bien entendu c'est au programmeur à gérer ses PUSH et ses POP. Si vous sauvegardez EAX par PUSH EAX et que vous écriviez par la suite POP EBX, il est clair que vous récupérez dans EBX la valeur de EAX. En général, les PUSH correspondent aux POP en sens inverse, ils obéissent à la loi LIFO (last in, first out). Par exemple, imaginons une fonction qui va utiliser les registres EAX, EBX et ESI, on écrira

PUSH EAX
PUSH EBX
PUSH ESI

ici se trouve le corps de la fonction
utilisant EAX, EBX et ESI

POP ESI
POP EBX
POP EAX
RET

On voit donc que les POP s'effectuent dans l'ordre inverse des PUSH, la dernière valeur empilée étant ESI, c'est la première à être dépilée de manière à ce qu'elle soit bien lue dans ESI. L'intérêt d'une telle démarche est que le programme appelant n'a pas perdu le contenu de ses registres. Si par exemple EAX est égal à 2 avant cet appel, on aura bien EAX égal à 2 au retour de la fonction. Si toutefois votre fonction assembleur renvoie une valeur dans EAX que la fonction est censée calculer, il ne faut alors plus programmer le couple PUSH EAX/POP EAX sinon la valeur ne serait pas retournée. En C++ on se pose la question de savoir si on envoie un paramètre en tant que valeur ou en tant que référence via l'opérateur &, en assembleur on se pose la question de savoir si on doit ou non "pusher" les registres. Par défaut en C++, un paramètre est du type valeur (cela correspond au couple PUSH/POP en assembleur) sinon, si on utilise l'opérateur &, la valeur est retournée à l'appelant (ce qui équivaut à l'absence de PUSH/POP pour le registre concerné, son contenu est donc renvoyé à l'appelant).
Le registre EIP pointe toujours juste après l'instruction que le microprocesseur va exécuter. Si cette instruction est un CALL, EIP pointe précisément l'adresse de retour, la valeur de EIP est donc sauvegardée en pile (comme si finalement on écrivait PUSH EIP). Puis EIP pointe le début de la fonction puisqu'un CALL correspond aussi à un saut à une adresse, un saut correspondant à une affectation de EIP. L'instruction CALL FONCTION où FONCTION est l'adresse où débute une fonction correspond donc à PUSH EIP et à LEA EIP, FONCTION (mais ces instructions n'existent pas, c'est uniquement pour vous donner une correspondance). Quand le microprocesseur rencontre l'instruction RET, il suppose qu'il y a en pile l'adresse de retour (c'est au programmeur à faire en sorte qu'il en soit ainsi), il lit donc cette adresse dans EIP qui pointe donc, si la programmation est correcte, l'adresse sauvegardée c'est-à-dire juste après le CALL. On comprend donc que l'instruction RET équivaudrait à l'instruction moins parlante POP EIP qui n'existe pas.
Bien entendu, c'est à vous de gérer correctement la pile. Imaginons que dans l'exemple précédent, nous ayons oublié de programmer le POP EAX final juste avant le RET. Que va-t-il se passer? Le microprocesseur continue sa route, il décode l'instruction RET, il charge donc dans EIP l'adresse qu'il suppose être l'adresse de retour qu'il dépile mais comme il s'agit de EAX oublié, EIP va pointé à l'adresse EAX c'est-à-dire n'importe où en mémoire où le programme se plantera inévitablement.
Un des principes de base de l'assembleur est donc la gestion correcte de la pile, en gros, sauf astuce grossière et rarissime, autant de PUSH que de POP, autant de CALL que de RET. Cela dit, dans la programmation Windows, cette règle n'est plus vraie, il est fréquent de mettre en pile via PUSH les arguments d'une fonction Windows puis de l'appeler par CALL, la fonction "sait" combien d'arguments se trouvent en pile, elle les lit puis c'est elle qui se charge de les dépiler, on voit donc dans un programme assembleur Windows régulièrement une série de PUSH suivie d'un CALL à la fonction Windows mais sans les POP qui deviendraient fautifs puisque c'est la fonction elle-même qui a régularisé la pile.
Le principe général de l'assembleur est très simple : à chaque instruction exécutée, le microprocesseur positionne des flags en fonction du résultat de l'opération.
Ces flags sont à disposition du programmeur qui peut ou non les tester. Flag signifie "drapeau" en anglais et un drapeau "indique" quelque chose, le drapeau vert au bord de la mer "indique" qu'il est autorisé de se baigner, le drapeau d'une pendule d'échecs indique si le joueur est "tombé" ou non c'est-à-dire s'il a ou non dépassé le temps de réflexion. Les flags sont donc des indicateurs qui indiquent la façon dont la dernière opération s'est déroulée. Les deux flags les plus importants sont ZF et CF, "zéro flag" et "carry flag". On teste un flag en programmant un saut conditionnel en fonction de la position de ce flag. Si la condition testée est réalisée, le saut a lieu (EIP change de valeur, il pointe l'adresse de saut et le programme continue là) sinon, si la condition n'est pas réalisée, EIP ne saute pas et donc le programme continue comme si de rien n'était (puisque rien n'est). Imaginons que l'on veuille savoir si le registre AL est nul ou non, on écrira

TEST AL,AL
JZ ALNUL // saute à l'adresse ALNUL si ZF=1
ici le saut n'a pas eu lieu donc AL n'est pas nul

ALNUL:
ici le saut a eu lieu donc AL est nul

D'une manière générale, il faut considérer que les flags répondent à une question. Ainsi le flag ZF (zéro flag) répond à la question : le résultat de la dernière opération est-il nul? ZF=1 oui, ZF=0 non. JZ signifie "saute à l'adresse indiquée si Z c'est-à-dire si ZF=1", JNZ signifie "saute à l'adresse indiquée si Non Z c'est-à-dire si ZF=0".
Le flag CF (carry flag) répond à la question : le résultat de la dernière opération a-t-il provoqué un dépassement de capacité? CF=1 oui, CF=0 non. On appelle "dépassement de capacité" le fait que le résultat de l'opération ne peut pas tenir dans le registre qui reçoit le résultat. Imaginons l'instruction

add al,6

Cette instruction signifie qu'on ajoute 6 au registre 8 bits AL. Si le résultat "tient" sur 8 bits, on aura CF=0 mais si on dépasse le maximum autorisé à savoir 255 puisqu'on est sur 8 bits, on aura CF=1 et le résultat 8 bits ne sera plus le résultat réel mais le résultat modulo 256. On en conclut aisément que CF est dans tous les cas le bit 8 de l'opération (le neuvième bit), lequel bien sûr est à 0 s'il n'y a pas de dépassement et à 1 s'il y a dépassement, il s'agit tout simplement de la retenue binaire de l'opération. Si CF=0 après une opération 8 bits, cela signifie que le résultat est correct tel quel mais si CF=1 on n'a alors que le résultat modulo 256. De même suite à une opération 16 bits (par exemple add ax,6), CF représentera le bit 16 de l'opération (le dix septième bit) et suite à une opération 32 bits (par exemple add eax,6), CF représentera le bit 32 de l'opération (le trente troisième bit). À noter que, suite à un ADD, si ZF=1, on a alors obligatoirement CF=1 aussi car si la résultat est nul, il y a forcément eu dépassement de capacité. JC signifie "saute à l'adresse indiquée si CF=1", JNC signifie "saute à l'adresse indiquée si CF=0". En réalité ZF traite de l'égalité ou de l'inégalité alors que CF traite de la supériorité ou de l'infériorité. Par exemple pour savoir si ax est égal à bx on écrira

cmp ax,bx

La question que pose cette instruction est : ax=bx? et c'est l'indicateur ZF qui y répond, ZF=1 oui, ZF=0 non.
Si l'on veut savoir si bx est strictement inférieur à ax, on écrira la même instruction à savoir

cmp ax,bx

mais on testera cette fois-ci CF qui répond à la question : ax<bx? CF=1 oui, CF=0 non (ou bien sûr la question associée inverse équivalente bx>ax?). Ceci se comprend aisément car la comparaison exécute la soustraction virtuelle ax-bx. Or, le maximum que l'on peut soustraire à ax est ax lui-même mais au-delà, il y aura un dépassement de capacité. C'est pourquoi, ces deux indicateurs couvrent à eux seuls 99% des besoins, on ne trouve dans les listings que les sauts JZ/JNZ/JC/JNC et bien sûr le saut inconditionnel JMP. Les autres indicateurs sont très rarement testés.
Bien entendu, c'est au programmeur à être cohérent dans sa programmation. Si vous programmez un saut en fonction de Z suite à une instruction qui ne positionne pas Z, le saut aura lieu ou non en fonction de l'état de Z à ce moment-là, le microprocesseur ne s'occupe pas de savoir si vous avez précédemment positionné de façon cohérente l'indicateur testé. C'est pourquoi il ne suffit pas de connaître le jeu d'instructions, mais il faut pour chaque type d'instruction connaître le comportement des indicateurs. D'une manière générale, voici ce qu'il faut à peu près savoir.
Les instructions d'affectation du type MOV, PUSH, POP, LEA (load effective adresse), XCHG (swap de deux registres), XLAT (chargement dans AL de l'octet pointé par BX+AL), les manipulation de chaînes (REP MOVSB, REP MOVSW, REP MOVSD, qui recopient ECX contenus mémoire de ESI source vers EDI destination), les initialisations mémoire (REP STOSB, REP STOSW, REP STOSD qui recopient ECX fois respectivement AL, AX ou EAX à partir de EDI) etc. ne positionnent aucun indicateur. C'est d'ailleurs assez pratique car on peut insérer ce type d'instructions avant un saut conditionnel puisque ces chargements ne modifient pas les flags. De même les sauts conditionnels ou non et les appels qui ne sont autres que des chargements divers et variés de EIP.
Les incrémentations (INC) et décrémentations (DEC) positionnent tous les indicateurs sauf CF. On épargne ici CF qui reste inchangé. D'ailleurs, si ZF=1 après une incrémentation, le résultat est nul et il y a eu dépassement de capacité, ZF eût donc fait double emploi avec CF suite à une incrémentation, ce pourquoi il a été épargné.
Les opérations arithmétiques, addition (ADD), addition avec CF (ADC, cela signifie qu'on ajoute en plus l'indicateur CF, ceci revient à incrémenter le résultat si CF=1, ADC équivaut à ADD si CF=0), soustraction (SUB), soustraction avec carry (SBB, on soustrait en plus CF), comparaison (CMP) positionnent tous les indicateurs.
Les instructions logiques, ET logique (AND), OU logique (OR), OU exclusif (XOR) positionnent tous les indicateurs et remettent CF à 0. C'est très pratique, s'il y a eu une instruction logique, on sait qu'on a CF=0, c'est très fréquemment utile. À noter l'instruction TEST qui est un ET logique non destructif. Quand vous écrivez AND BH,DH, vous faites un ET logique bit à bit entre les deux registres 8 bits BH et DH et le résultat de ce ET logique se trouve après exécution dans BH mais quand vous faites TEST BH,DH, le résultat n'est transféré nulle part, cette instruction ne fait que positionner les indicateurs en fonction du résultat d'ailleurs perdu. TEST est donc un ET logique virtuel de même que CMP est une soustraction virtuelle, le résultat est perdu mais on a positionné les indicateurs en fonction de ce résultat, si donc ZF=1 après une comparaison, cela signifie que le résultat de la soustraction eût été nul, en conséquence de quoi les deux opérandes comparées sont égales. À noter l'instruction XOR AL,AL qui est un Ou exclusif entre AL et lui-même, c'est une petite astuce pour remettre AL à 0 (ou bien sûr AX ou EAX car ces syntaxes sont extensibles, on peut écrire XOR AX,AX ou XOR EAX,EAX). Imaginons maintenant qu'on veuille savoir la position du bit 3 et AL, on écrira TEST AL, 00001000b, si ZF=1 après exécution de cette instruction alors le résultat du ET logique aurait été nul en conséquence de quoi le bit 3 testé est nul donc au zéro logique et si ZF=0 alors le résultat du ET logique n'aurait pas été nul en conséquence de quoi le bit 3 est au 1 logique, le bit testé est donc l'inverse de ZF, il est à 1 si ZF=0 et à 0 si ZF=1. Imaginons qu'on veuille positionner au 1 logique les bits 2 et 10 de BX, on écrira OR BX, 0000010000000100b. Imaginons qu'on veuille annuler le bit 8 de EDX, on écrira AND EDX, 11111111111111111111111011111111b ou encore, si l'on veut éviter ce type d'écriture binaire AND EDX, 0FFFFFEFFh(dans la notation assembleur, on doit faire précéder les valeurs hexadécimales par 0 lorsque celles-ci commencent par une lettre pour éviter la confusion possible avec un label). Imaginons qu'on veuille inverser la valeur du bit 5 de CH, on écrira XOR CH,00100000b. En règle générale, on utilise le ET logique pour lire les bits ou les positionner au 0 logique, le OU logique pour positionner les bits au 1 logique et le OU exclusif pour inverser l'état logique des bits.
Les instructions de rotations et décalages, ROL et ROR (rotation circulaire à gauche et à droite, ROR rotation left, ROR rotation right), RCL et RCR (décalage avec CF entrant), SHL et SHR (décalage avec 0 entrant, SHL et SHR équivalent donc à RCL et RCR si CF=0, shift left et shift right) et enfin SAR (décalage à droite avec réinjection du bit de signe, shift arithmétique right) ne positionnent que CF qui représente donc le bit sortant du décalage. À noter qu'il n'y a que 7 types de rotations et décalages (et non 8) car SAR n'a pas sa réplique à gauche (le très gentil tasm32 accepte cependant l'instruction SAL mais le convertit en SHL sans autre forme de procès). En effet, s'il est signifiant de réinjecter le bit de signe i.e. le bit le plus à gauche sur un décalage à droite, il n'est pas signifiant de réinjecter le bit 0 sur un décalage à gauche. SAR permet de diviser par deux en maintenant le signe tel quel, si donc la valeur était négative (en considérant le complément à 2), elle restera négative après SAR car le bit de signe a été réinjecté. À noter que s'agissant des rotations et décalages, on a le choix entre l'exécuter une seule fois (e.g. ROR AL,1) ou CL fois (e.g. ROR AL,CL), dans ce cas bien sûr il faut renseigner CL par un mov juste avant par exemple MOV CL,3, ce qui aura pour effet de faire exécuter trois fois le décalage ou la rotation.
En résumé, les instructions
d'affection ne positionnent aucun indicateur
d'incrémentation positionnent tous les indicateurs sauf CF
arithmétiques positionnent tous les indicateurs
logiques positionnent tous les indicateurs sauf CF qui est remis à 0
de rotations et décalages ne positionnent que CF.

III. La mémoire

La mémoire n'est jamais qu'une suite d'octets consécutifs. Le 8080 ne pouvait lire ou écrire que huit bits à la fois (byte), le 8086 pouvait lire ou écrire deux octets consécutifs (word), le 80386, lui, peut lire ou écrire jusqu'à quatre octets consécutifs (double word). Mais que la mémoire soit pointée par byte, word ou double word, elle reste une suite d'octets numérotés et ces numéros ou indices sont des adresses. Par conséquent, la règle est simple :
en assembleur, il n'y a que des adresses et des contenus.
Le plus important donc, après avoir compris la pile et les indicateurs (flags) en relation avec les différents types d'instruction, est de comprendre la mémoire. Et pour comprendre la mémoire, il faut savoir pointer une adresse et savoir lire et écrire à cette adresse, c'est tout. Tout le reste en découlera. De même en langage C, si P est un pointeur de type char*, *P est le contenu pointé par P et &P est l'adresse ou se trouve écrit le pointeur P. P est le contenu d'adresse &P et *P le contenu d'adresse P. On voit donc que P est à la fois un contenu et une adresse, ce qui se simulera en assembleur par le fait qu'un pointeur par exemple ESI soit lui-même écrit à une adresse A. L'adresse A équivaut à &P, si je lis le contenu 32 bits à cette adresse dans ESI, ESI équivaut à P qui pointe lui-même quelque part et si je lis l'octet pointé par ESI, cet octet lu correspond à *P.

IV. Syntaxe d'accès avec C++Builder

Il y a deux cas, le cas où la mémoire à pointer se trouve dans la pile et le cas où elle se trouve dans le tas (heap en anglais). C'est sur ce plan-là que les syntaxes pour pointer la mémoire diffèrent. Quand vous déclarez une zone mémoire char zone[50] à l'intérieur d'une fonction, cette zone se situe en pile mais quand vous déclarez un pointeur P de type char* et que vous allouiez pour P de la mémoire par new ou malloc, seul le pointeur déclaré (char* P;) se trouve en pile mais la mémoire accordée par le système se situe dans le tas. Dans le premier cas, on fera pointer par exemple ESI à l'adresse zone par lea esi, zone alors que dans le deuxième cas, il faut lire le contenu P dans ESI pour que ESI pointe la mémoire allouée par new ou malloc et on doit alors écrire mov esi, [P] (les crochets entourant P sont facultatifs, on voit que cette notation est un peu ambiguë car en réalité on lit dans esi quatre octets à partir de l'adresse &P ce qui fait que dans cette instruction assembleur P ne signifie pas P du C++ mais &P et esi équivaut alors après exécution au P du C++). ESI pointant correctement la mémoire (puisqu'il équivaut au P du C++), la suite du programme sera la même, simplement dans le premier cas ESI pointe une zone dans la pile et dans le deuxième il pointe une zone dans le tas.

V. Premier cas : mémoire déclarée dans la pile

Vérifions par un petit exemple. Entrez dans C++Builder, sauvez le projet vierge pour l'instant dans un répertoire de test en gardant unitxx et projectxx (où xx est un nombre sur deux chiffres, par exemple si vous en êtes à votre test numéro 14, sauvegardez le projet avec les noms unit14 et projet14). Commencez par rajouter au début de unitxx.cpp l'instruction #pragma inline, c'est cette instruction qui déclare qu'il y aura de l'assembleur dans le source cpp. Donc pour l'instant unitxx.cpp va ressembler à ceci.

#include <vcl.h>
#pragma hdrstop

#include "Unit14.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
#pragma inline
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}

Nous allons vérifiez qu'on pointe parfaitement la mémoire en mettant un bouton sur Form1, en créant un gestionnaire d'événement OnClick associé à ce bouton (il suffit de double-cliquer sur le bouton après l'avoir déposé n'importe où sur Form1). À l'intérieur de ce gestionnaire nous allons déclarer une chaîne de caractères que l'on va initialiser avec un message quelconque, on va déclarer une autre zone de caractères que l'on ne va pas du tout initialiser puis en assembleur, nous allons recopier la chaîne initialisée dans la zone prévue à cet effet puis nous allons afficher dans un MessageBox le contenu de cette zone où s'est effectuée la recopie. On constatera alors que la chaîne a bien été recopiée puisque c'est la chaîne recopiée que MessageBox va afficher et ce sera pour nous la preuve qu'on pointe correctement la mémoire.

void __fastcall TForm1::Button1Click(TObject *Sender)
{
char Message[]="Ceci est un message, vive l'assembleur!";
char Zone[100];

_asm
{
          cld
          lea esi,Message
          lea edi,Zone
          mov ecx,40
          REP MOVSB
}

Application->MessageBox(Zone, "ok",MB_OK);

}

On déclare d'abord notre message puis une zone mémoire arbitrairement assez longue pour accueillir une recopie du message initialisé. Ensuite on déclare qu'on va maintenant écrire en assembleur avec la directive _asm (on peut aussi écrire asm tout court ou encore __asm avec deux underscores). L'instruction cld signifie "clear DF" c'est-à-dire "mets à zéro le flag DF", ce flag "direction flag" indique si la copie répétitive qui suit doit se faire par incrémentation ou décrémentation, à 0 ce sera par incrémentation et à 1 ce sera par décrémentation. C'est un flag particulier que l'utilisateur positionne et que le microprocesseur utilise. Ensuite on fait pointer esi sur Message (esi pointe donc le premier code ascii du message) puis edi pointe le premier octet de la zone où la recopie va se faire puis on met le compteur ecx à 40 car c'est le nombre de caractères incluant le zéro de fin de chaîne à recopier puis on se contente d'écrire REP MOVSB ce qui signifie de recopier par incrémentation (car nous avons mis DF à 0) et par byte (MOVSW serait une recopie par word et MOVSD par double word) la source pointée par ESI dans la destination pointée par EDI. Vous remarquez qu'on n'utilise pas du tout les registres de segment qui sont initialisés une fois pour toutes par le système, on ne s'en occupe donc pas du tout (alors qu'en 8086 il fallait impérativement initialiser DS, le data segment, pour pointer correctement la mémoire).
Bien entendu, dans ce tout premier essai il nous a fallu calculer à la main la longueur du message et nous avons trouvé 40 incluant le zéro de fin de chaîne. Nous allons améliorer cette première tentative en écrivant une petite routine assembleur qui va nous calculer la longueur du message. On va donner à cette routine l'adresse ESI qui pointera le début du message, cette routine renverra le nombre de caractères dans le registre ECX utilisé comme compteur.

_asm
{
LongECX:   push esi
           mov ecx,0
LongECX10: mov al,
           inc ecx
           inc esi
           test al,al
           jnz LongECX10
           pop esi
           ret
}

Cette routine est une fonction comme une autre donc vous l'insérez dans le source C/C++ où vous voulez entre deux fonctions C++. En entrant dans le sous-programme LongECX, ESI est censé pointer correctement la mémoire. Cela dit, on sauvegarde son adresse en pile, ainsi au retour, esi pointera toujours le début de la chaîne à recopier et ce, bien qu'il va bouger dans la routine elle-même. On initialise le compteur ECX à 0. Puis la boucle commence. On lit dans AL l'octet pointé par esi. À la place de mov AL, [esi] nous aurions pu écrire mov AL, byte ptr mais la précision byte ptr n'est pas utile ici puisque le compilateur sait bien que AL est sur 8 bits. On n'écrit donc byte ptr ou word ptr ou encore dword ptr que lorsque cette précision est nécessaire. Par exemple, si l'on veut écrire 0 à l'adresse ESI, il faudra préciser s'il s'agit de 0 sur 8 bits, 0 sur 16 bits ou 0 sur 32 bits et donc choisir entre mov byte ptr [esi],0ou mov word ptr ,0ou encore mov dword ptr ,0. L'octet étant lu dans AL, on incrémente le compteur ecx et le pointeur esi puis on teste al par l'instruction test al,al qui fait un ET logique entre al et lui-même et ce, à seule fin de positionner les flags car nous voulons savoir si l'octet lu est nul ou non. Cette instruction est strictement équivalente à and al,al ou même or al,al car le ET logique et le OU logique sont idempotents. Si l'octet lu dans AL n'est pas nul, on saute à l'adresse LongECX10 où l'on va lire encore la mémoire (mais esi a avancé d'une case entre temps donc on va lire l'octet suivant). Tant qu'on aura à ce stade ZF=0, le saut aura lieu. Quand ZF sera au 1 logique après le test, la boucle s'arrêtera et ce, parce que le saut n'aura plus lieu. En effet, le saut a lieu si NZ mais n'a pas lieu si Z. On restitue alors à esi sa position d'origine par pop esi puis on retourne à l'appelant par l'instruction ret avec ECX qui contient la longueur de la chaîne incluant le zéro de fin. On voit qu'ici une erreur de programmation aurait été d'écrire le couple PUSH ECX/POP ECX car alors le contenu de ECX ne serait pas retourné. La relation entre le C++ et l'assembleur est inversée au sens où rien en C++ correspond au PUSH/POP en assembleur (argument du type valeur) alors que l'opérateur & en C++ correspond à rien en assembleur (absence de PUSH/POP, argument du type référence). Dans ces conditions, la fonction de recopie précédente sera légèrement modifiée, notamment ECX étant calculé par la fonction LongECX, il n'a plus à être initialisé par programme. Donc après avoir donné l'adresse Message à esi par lea esi, Message on appelle la fonction LongECX par call LongECX, puis on positionne EDI mais comme ECX a été calculé par la routine LongECX, on supprime l'instruction qui lui donnait dans notre exemple précédent la valeur 40. Voici l'ensemble de l'unité cpp qui contient maintenant plus d'assembleur que de C++. Remarquez bien la place de la routine LongECX à savoir comme une fonction C/C++ entre deux fonctions.

#include <vcl.h>
#pragma hdrstop

#include "Unit14.h"
//-------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
#pragma inline
TForm1 *Form1;
//-------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//-------------------------------------------------------
_asm
{
LongECX:       push esi
               mov ecx,0
LongECX10:     mov al,
               inc ecx
               inc esi
               test al,al
               jnz LongECX10
               pop esi
               ret
}
//-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
char Message[]="Ceci est un message, vive l'assembleur!";
char Zone[100];

_asm
{
               cld
               lea esi,Message
               call LongECX
               lea edi,Zone
               REP MOVSB
}

Application->MessageBox(Zone, "ok",MB_OK);

}
//-------------------------------------------------------

Remarquez que l'assembleur commence juste après le constructeur de TForm1. En effet, C++Builder ne permet pas de commencer par des lignes d'assembleur, cela provoque une erreur à la compilation. Les premières directives asm se situent donc juste après Tform1::TForm1.

VI. Deuxième cas : mémoire déclarée dans le tas

Modifions le programme précédent. Nous allons déclarer notre message de la même façon mais nous allons déclarer un pointeur P de type char* puis nous allons allouer à ce pointeur une zone de 100 octets par new. Cette zone va alors se situer dans le tas. On ne va donc plus pointer la zone via l'instruction lea (load effective adresse) car lea signifie qu'on donne au pointeur l'adresse directement et cette adresse nous ne la connaissons pas mais nous savons qu'elle se situe à l'adresse &P donc on va écrire mov edi, [P](car &P du C++ correspond à P en assembleur, il en est ainsi dans la notation), ce qui signifie que les 32 bits situés à partir de l'adresse &P sont lus dans EDI, le microprocesseur va donc lire quatre contenus aux adresses consécutives &P, &P+1, &P+2 et &P+3, ce qui va faire quatre octets qu'il va donner au registre 32 bits edi (adresse de destination de la future recopie du message initialisé). Dans ces conditions, EDI pointe la mémoire allouée par new. La recopie s'effectue de la même façon.

void __fastcall TForm1::Button1Click(TObject *Sender)
{
char Message[]="Ceci est un message, vive l'assembleur!";
char *P;
P=new char[100];

_asm
{
               cld
               lea esi,Message
               call LongECX           //ECX=longueur de la chaîne
               mov edi, [P]
               REP MOVSB
}

Application->MessageBox(P, "ok",MB_OK);
delete P;
}

Vous voyez clairement le rapport entre l'assembleur et le C. Si j'écris par exemple lea edi, P, cela signifie que edi pointe en pile l'adresse où se trouve le pointeur P, edi est alors égal à &P, si j'écris comme dans le programme mov edi, [P] cela signifie que je lis le pointeur P (les crochets sont d'ailleurs facultatifs) donc EDI équivaut alors à P et alors byte ptr [EDI] équivaut à *P que ce soit en lecture ou en écriture. Pour résumer :

lea edi, P signifie edi=&P
mov edi, P signifie edi=P
mov [EDI], AL signifie *P=AL
mov al, [EDI] signifie AL=*P
inc edi signifie P++
dec edi signifie P--

Vous pouvez consulter le programme assembleur créé par C++Builder, il suffit de l'ouvrir, son nom est unitxx.asm. Pour faire du pas à pas en assembleur, positionnez votre curseur sur la première instruction assembleur push esi puis faites F4 (exécution jusqu'au curseur). Là, le programme s'exécute, cliquez sur le bouton, cela provoque l'arrêt du programme à l'endroit où vous avez positionné le curseur. Faites Alt v d c, vous obtenez la fenêtre CPU (Alt active le menu, v pour voir, d pour debug et c pour CPU) ou encore appuyez simultanément sur ctrl et Alt et faites c, la fenêtre CPU s'affiche. Le mieux est le mode mixte où l'on vous donne à la fois le C++ et l'assembleur, cliquez à droite dans la fenêtre où se trouve le code et sélectionnez l'option "mixte" si elle n'est pas activée, cela permet le double affichage C++/assembleur. Si vous vous êtes perdu suite à des scrolls pour consulter le code de la fenêtre assembleur, cliquez à droite et faites "allez à l'EIP en cours", vous vous retrouvez à l'instruction où l'on est arrêté. En haut à droite, vous avez les registres internes du microprocesseur, en bas à gauche une zone mémoire quelconque et en bas à droite la pile. Regardez la valeur de esp, le pointeur de pile, vous constatez que la fenêtre de pile vous affiche l'état de la pile, une petite flèche verte vous montre la position du pointeur de pile esp et en regard la dernière valeur empilée. Si vous voulez consulter de la mémoire, utilisez la fenêtre en bas à gauche prévue à cet effet, cliquez à droite et choisissez "aller à l'adresse". En général on veut consulter une adresse contenue dans un registre, donc on donne le nom de ce registre et on vous affiche le contenu de la mémoire à cet endroit. Mais vous pouvez aussi donner une adresse, dans ce cas n'oubliez pas d'entrer le préfixe "0x" de manière à donner une valeur en hexadécimal (sinon C++Builder considère que c'est du décimal).

VII. Utilisation de l'assembleur sous DOS

Nous allons maintenant essayer notre assembleur sous DOS via une "invite de commandes" et non plus sous C++Builder. Commençons par créer un répertoire asm à l'intérieur du répertoire CBuilder5. Vous pouvez créer ce répertoire sous Windows. Sinon, utilisez une "invite de commandes", allez sous le répertoire Cbuilder5. Par exemple sous Windows98, l'invite de commandes vous fait aller au répertoire Windows, le prompt est donc ceci :

C:\Windows>

On commence par descendre à la racine :

C:\Windows>cd ..

Le prompt devient alors :

C:\>

Puis on se rend alors au répertoire Cbuilder5 :

C:\>cd "program files"\borland\cbuilder5

Là on crée le répertoire asm :

C:\Program Files"\Borland\CBuilder5>mkdir asm

C'est dans ce répertoire que nous allons développer en assembleur. Ce répertoire étant créé, sortons de l'invite de commandes puis rentrons-y à nouveau. On se retrouve sous le répertoire Windows. Là créons un petit go.bat qui va nous faire aller directement au répertoire asm. Pour cela entrons sous Edit, le petit éditeur de texte du DOS (vous pouvez créer ce petit "point bat" sous Windows avec NotePad, le bloc-notes standard, ce sera la même chose). Donc entrons sous Edit :

C:>\Windows>edit go.bat

L'éditeur s'ouvre et vous donne la main, entrez simplement la ligne :

cd ..\"program files"\borland\cbuilder5\asm

puis sauvez le document, faites Alt (pour activer le menu) puis f (pour activer l'option "fichier") puis e (pour enregistrer) puis de nouveau Alt puis f puis q pour quitter.
Vous êtes de nouveau sous le répertoire Windows, il ne vous reste plus qu'à entrer la commande go pour exécuter ce go.bat qu'on vient d'écrire.

C:>\Windows>go

On se rend ainsi à notre répertoire asm plus facilement. Le prompt est donc (maintenant que nous sommes "chez nous") :

C:>\Program Files\Borland\CBuilder5\asm>

Là nous créons un petit c.bat pour compiler et lier c'est-à-dire créer l'exécutable exe à partir d'un fichier assembleur asm. Donc on entre sous Edit :

C:>\Program Files\Borland\CBuilder5\asm>edit c.bat

Là, vous êtes sous éditeur de texte, entrez ces deux lignes :

tasm32 -ml prg
ilink32 -x -c prg ,,,..\lib\import32

puis sauvez ce document, faites Alt (pour activer le menu) puis f (pour activer l'option "fichier") puis e (pour enregistrer) puis de nouveau Alt puis f puis q pour quitter. Il suffit d'apprendre une fois pour toutes cette série de six touches pour sauvegarder un document sous Edit : Alt f e Alt f q.
On suppose là que notre programme va s'appeler prg.asm. Attention à la syntaxe, les espaces sont importants ainsi que le nombre de virgules. Comme vous le voyez, l'assembleur s'appelle tasm32 (turbo assembleur 32 bits) et le lieur ilink32 (incremental link 32 bits). Pour connaître les options de l'assembleur et du lieur, il suffit d'entrer leur nom sous DOS, la commande tasm32 vous donnera donc toutes les options de l'assembleur et la commande ilink32 toutes les options du lieur. Remarquez aussi que nous faisons le lien avec la librairie import32, ce qui nous permettra d'accéder aux fonctions Windows. Nous sommes prêts pour écrire notre premier petit programme en assembleur. Vous pouvez également remplacer prg par %1 qui représente le premier argument qui sera donné à la commande c (il faut donc faire deux fois ce remplacement car prg apparaît deux fois), dans ce cas on ne compilera pas par c tout court mais par c suivi du nom du programme par exemple c prg. Entrons maintenant sous Edit et créons le programme prg.asm.

C:>\Program Files\Borland\CBuilder5\asm>edit prg.asm

Là, entrez le programme suivant ou mieux, comme c'est un peu long, faites un copier-coller sous Windows avec NotePad, le résultat sera le même, sauvegardez ce fichier sous le nom prg.asm.

.386
locals
jumps
.model flat,STDCALL

extern MessageBoxA : Proc

.data
titre           db "In girum imus nocte et consumimur igni",0
texte           db "Bienvenue dans les tutoriaux",10,10
                db "Vive l'assembleur!",0

.code
Programme:
                push 0                  ; type de fenêtre
                push offset titre
                push offset texte
                push 0
                call MessageBoxA
                ret                      ; retour sous DOS
End Programme

Vous reconnaissez le "data segment" annoncé par .data à l'intérieur duquel nous avons créé des chaînes de caractères via la directive db (define byte), le "code segment" annoncé par .code, là où se trouve le programme. Il est important de commencer par un libellé, ici Programme, et de terminer par End suivi de ce même nom donc End Programme. Ce programme est très court car il s'agit d'un essai. On sauvegarde en pile le type de fenêtre voulu (ici 0 qui correspond à une fenêtre simple avec le bouton OK, correspondant à MB_OK) puis l'adresse "titre" (où se trouve le titre de la fenêtre, chaîne de caractères terminée par zéro), puis l'adresse "texte" puis enfin 0 (zéro, indication supplémentaire utilisée par le système). Puis on appelle MessageBoxA (qui se trouve dans import32, la librairie standard avec laquelle nous avons lié le programme dans notre c.bat) puis on termine par l'instruction ret, retour au programme appelant donc retour sous DOS.
Il ne reste plus qu'à compiler et lier ce programme pour en obtenir l'exécutable donc on entre simplement la commande c qui va exécuter notre petit c.bat créé précédemment.

C:>\Program Files\Borland\CBuilder5\asm>c

Le programme se compile sans erreur. On l'exécute maintenant en entrant son nom, donc :

C:>\Program Files\Borland\CBuilder5\asm>prg

Une fenêtre Windows s'affiche avec le bouton OK.
Vous constatez qu'ici nous n'avons pas programmé les POP alors qu'il y a quatre PUSH, cela est dû à la syntaxe d'appel des fonctions Windows, les arguments se situent dans la pile avant l'appel. Comme la fonction le "sait", c'est elle qui se charge de dépiler ces valeurs de manière à ce que la pile soit régularisée au retour. Si vous voulez un listing de votre programme avec les codes hexadécimaux, rajouter l'option -l à la commande tasm32 c'est-à-dire écrivez dans le c.bat la ligne

tasm32 -ml -l prg

C'est la même ligne que précédemment mais on a rajouté l'option -l. Dans ces conditions, notre commande c de création de l'exécutable fournira en plus le fichier prg.lst qui est le listing complet du programme totalement construit et très bien présenté, à consulter via NotePad sous Windows ou via Edit sous DOS.

VIII. Test sous C++Builder

Maintenant, si vous voulez tester le programme assembleur DOS précédent sous C++Builder pour faire du pas à pas, ce n'est pas immédiat, il faut alors quelque peu modifier la présentation du programme précédent pour lui donner une écorce C++ car il est en assembleur pur. Créez sous Windows un nouveau répertoire de travail. C'est une précaution importante car nous allons modifier ce programme en le faisant devenir un programme C++ avec la directive _asm. Or, dans ces conditions, C++Builder crée lui-même un asm suite à la compilation et donc écraserait le prg.asm d'origine si l'on travaillait dans le même répertoire (ou alors il faudrait changer le nom du programme mais un répertoire nouveau est préférable pour ne pas mélanger l'assembleur avec le C++).
Le même programme que précédemment pourrait se présenter de la façon suivante :

#pragma inline
void main(void)
{
_asm
{
Programme:
                push 0
                push offset titre
                push offset texte
                push 0
                call MessageBoxA
                jmp fin

titre           db "In girum imus nocte et consumimur igni",0
texte           db "Bienvenue dans les tutoriaux",10,10
                db "Vive l''assembleur!",0

extern MessageBoxA : Proc

fin:
}
}

On commence par #pragma inline pour indiquer au compilateur C++ qu'il y aura de l'assembleur. On crée le programme maître void main(void){} et à l'intérieur des parenthèses on crée la directive _asm{}, on recopie le programme mais on remplace ret par jmp fin, on crée le label fin juste avant la parenthèse fermante de la directive asm, on recopie les chaînes de caractères mais on rajoute une quote au message "vive l''assembleur" (une double quote n'en vaut qu'une en assembleur), on déclare la fonction externe MessageBoxA, on supprime "End Programme" qui ne vaut qu'en assembleur pur. Ce fichier étant au point, sauvez-le dans le répertoire nouveau précédemment créé sous le nom prg.cpp. Puis faites "Fichier|Nouveau" et choisissez "Expert console", là donnez dans la fenêtre le nom du programme prg.cpp avec le chemin correct et cochez la case "Spécifiez le source du projet". Puis faites "Fichier|Enregistrer le projet sous" et donnez un nom à ce projet (laissez par exemple Projet1 proposé) car même un simple programme console doit avoir un nom de projet associé en C++Builder. Maintenant, vous pouvez exécutez le programme et donc aussi l'exécuter au pas à pas en mode debug avec la fenêtre CPU.


VIII-A. Structures et C++

VIII. Structures et C++

VIII-A. Structures vu dans ... Paul Carter

VIII-A-1. Introduction

Les structures sont utilisées en C pour regrouper des données ayant un rapport entre elles dans une variable composite. Cette technique a plusieurs avantages :

1. Cela clarifie le code en montrant que les données définies dans la structure sont intimement liées ;

2. Cela simplifie le passage des données aux fonctions. Au lieu de passer plusieurs variables séparément, elles peuvent être passées en une seule entité ;

3. Cela augmente la localité (32) du code.

Du point de vue de l'assembleur, une structure peut être considérée comme un tableau avec des éléments de tailles variables. Les éléments des vrais tableaux sont toujours de la même taille et du même type. C'est cette propriété qui permet de calculer l'adresse de n'importe quel élément en connaissant l'adresse de début du tableau, la taille des éléments et l'indice de l'élément voulu.

Les éléments d'une structure ne sont pas nécessairement de la même taille (et habituellement, ils ne le sont pas). À cause de cela, chaque élément d'une structure doit être explicitement spécifié et doit recevoir un tag (ou nom) au lieu d'un indice numérique.

En assembleur, on accède à un élément d'une structure d'une façon similaire à l'accès à un élément de tableau. Pour accéder à un élément, il faut connaître l'adresse de départ de la structure et le déplacement relatif de cet un tableau où ce déplacement peut être calculé grâce à l'indice de l'élément, c'est le compilateur qui affecte un déplacement aux éléments d'une structure.

Par exemple, considérons la structure suivante :

Sélectionnez

struct S {

short int x ; / entier sur 2 octets /

int y ; / entier sur 4 octets /

double z ; / ottant sur 8 octets /

};

La Figure 7.1 montre à quoi pourrait ressembler une variable de type S en mémoire.

Image non disponible

Le standard ANSI C indique que les éléments d'une structure sont organisés en mémoire dans le même ordre que celui de leur définition dans le struct. Il indique également que le premier élément est au tout début de la structure (i.e. au déplacement zéro). Il définit également une macro utile dans le fichier d'en-tête stddef.h appelée offsetof(). Cette macro calcule et renvoie le déplacement de n'importe quel élément d'une structure. La macro prend deux paramètres, le premier est le nom du type de la structure, le second est le nom de l'élément dont on veut le déplacement. Donc le résultat de offsetof(S, y) serait 2 d'après la Figure 7.1.

VIII-A-2. Alignement en mémoire

Si l'on utilise la macro offsetof pour trouver le déplacement de y en utilisant le compilateur gcc, on s'aperçoit qu'elle renvoie 4, pas 2 ! Pourquoi ?

Parce que gcc (et beaucoup d'autres compilateurs) aligne les variables sur des multiples de double-mot par défaut.

Souvenez-vous qu'une adresse est sur un multiple de double-mot si elle est divisible par 4.

En mode protégé 32 bits, le processeur lit la mémoire plus vite si la donnée commence sur un multiple de double-mot. La Figure 7.2 montre à quoi ressemble la structure S en utilisant gcc.

Image non disponible

Le compilateur insère deux octets inutilisés dans la structure pour aligner y (et z) sur un multiple de double-mot. Cela montre pourquoi c'est une bonne idée d'utiliser offsetof pour calculer les déplacements, au lieu de les calculer soi-même lorsqu'on utilise des structures en C.

Bien sûr, si la structure est utilisée uniquement en assembleur, le programmeur peut déterminer les déplacements lui-même. Cependant, si l'on interface du C et de l'assembleur, il est très important que l'assembleur et le C s'accordent sur les déplacements des éléments de la structure ! Une des complications est que des compilateurs C différents peuvent donner des déplacements différents aux éléments. Par exemple, comme nous l'avons vu, le compilateur gcc crée une structure S qui ressemble à la Figure 7.2, cependant, le compilateur de Borland créerait une structure qui ressemble à la Figure 7.1. Les compilateurs C fournissent le moyen de spécifier l'alignement utilisé pour les données. Cependant, le standard ANSI C ne spécifie pas comment cela doit être fait et donc, des compilateurs différents procèdent différemment.

Le compilateur gcc a une méthode flexible et compliquée de spécifier l'alignement. Le compilateur permet de spécifier l'alignement de n'importe quel type en utilisant une syntaxe spéciale. Par exemple, la ligne suivante :

Sélectionnez

typedef short int unaligned_int __attribute__((aligned(1)));

définit un nouveau type appelé unaligned_int qui est aligné sur des multiples d'octet (oui, toutes les parenthèses suivant __attribute__ sont nécessaires !) Le paramètre 1 de aligned peut être remplacé par d'autres puissances de 2 pour spécifier d'autres alignements (2 pour s'aligner sur les mots, 4 sur les doubles-mots, etc.). Si l'élément y de la structure était changé en un type unaligned_int, gcc placerait y au déplacement 2. Cependant, z serait toujours au déplacement 8 puisque les doubles sont également alignés sur des doubles-mots par défaut. La définition du type de z devrait aussi être changée pour le placer au déplacement 6.

Le compilateur gcc permet également de comprimer (pack) une structure.

Cela indique au compilateur d'utiliser le minimum d'espace possible pour la structure. La Figure 7.3 montre comment S pourrait être réécrite de cette façon. Cette forme de S utiliserait le moins d'octets possible, soit 14 octets.

Image non disponible

Les compilateurs de Microsoft et Borland supportent tous les deux la même méthode pour indiquer l'alignement par le biais d'une directive #pragma.

Sélectionnez

#pragma pack(1)

La directive ci-dessus indique au compilateur d'aligner les éléments des structures sur des multiples d'un octet (i.e., sans décalage superflu). Le 1 peut être remplacé par 2, 4, 8 ou 16 pour spécifier un alignement sur des multiples de mots, doubles-mots, quadruples-mots ou de paragraphe, respectivement. La directive reste active jusqu'à ce qu'elle soit écrasée par une autre. Cela peut poser des problèmes puisque ces directives sont souvent utilisées dans des fichiers d'en-tête. Si le fichier d'en-tête est inclus avant d'autres fichiers d'en-tête définissant des structures, ces structures peuvent être organisées différemment de ce qu'elles auraient été par défaut. Cela peut conduire à des erreurs très difficiles à localiser. Les différents modules d'un programme devraient organiser les éléments des structures à différents endroits !

Il y a une façon d'éviter ce problème. Microsoft et Borland permettent la sauvegarde de l'état de l'alignement courant et sa restauration. La Figure 7.4 montre comment on l'utilise.

Image non disponible

VIII-A-3. Champs de bits

Les champs de bits permettent de déclarer des membres d'une structure qui n'utilisent qu'un nombre de bits donné. La taille en bits n'a pas besoin d'être un multiple de 8. Un membre champ de bit est défini comme un unsigned int ou un int suivi de deux-points et de sa taille en bits. La Figure 7.5 en montre un exemple.

Image non disponible

Elle définit une variable 32 bits décomposée comme suit :

Image non disponible

Le premier champ de bits est assigné aux bits les moins significatifs du double-mot (33).

Néanmoins, le format n'est pas si simple si l'on observe comment les bits sont stockés en mémoire. La difficulté apparaît lorsque les champs de bits sont à cheval sur des multiples d'octets. Car les octets, sur un processeur little endian seront inversés en mémoire. Par exemple, les champs de bits de la structure S ressembleront à cela en mémoire :

Image non disponible

L'étiquette f2l fait référence aux cinq derniers bits (i.e., les cinq bits les moins significatifs) du champ de bits f2. L'étiquette f2m fait référence aux cinq bits les plus significatifs de f2. Les lignes verticales doubles montrent les limites d'octets. Si l'on inverse tous les octets, les morceaux des champs f2 et f3 seront réunis correctement.

L'organisation de la mémoire physique n'est habituellement pas importante à moins que des données soient transférées depuis ou vers le programme (ce qui est en fait assez courant avec les champs de bits). Il est courant que les interfaces de périphériques matériels utilisent des nombres impairs de bits dont les champs de bits facilitent la représentation.

Un bon exemple est SCSI (34). Une commande de lecture directe pour un périphérique SCSI est spécifiée en envoyant un message de six octets au périphérique selon le format indiqué dans la Figure 7.6.

Image non disponible

La difficulté de représentation en utilisant les champs de bits est l'adresse de bloc logique qui est à cheval sur trois octets différents de la commande. D'après la Figure 7.6, on constate que les données sont stockées au format big endian. La Figure 7.7 montre une définition qui essaie de fonctionner avec tous les compilateurs.

Image non disponible

Les deux premières lignes définissent une macro qui est vraie si le code est compilé avec un compilateur Borland ou Microsoft. La partie qui peut porter à confusion va des lignes 11 à 14. Tout d'abord, on peut se demander pourquoi les champs lba_mid et lba_lsb sont définis séparément et non pas comme un champ unique de 16 bits. C'est parce que les données sont stockées au format big endian. Un champ de 16 bits serait stocké au format little endian par le compilateur. Ensuite, les champs lba_msb et logical_unit semblent être inversés, cependant, ce n'est pas le cas. Ils doivent être placés dans cet ordre. La Figure 7.8 montre comment les champs sont organisés sous la forme d'une entité de 48 bits (les limites d'octets sont là encore représentées par des

lignes doubles).

Image non disponible

Lorsqu'elle est stockée en mémoire au format little endian, les bits sont réarrangés au format voulu (Figure 7.6).

Pour compliquer encore plus le problème, la définition de SCSI_read_cmd ne fonctionne pas correctement avec le C Microsoft. Si l'expression sizeof(SCSI_read_cmd) est évaluée, le C Microsoft renvoie 8 et non pas 6 ! C'est parce que le compilateur Microsoft utilise le type du champ de bits pour déterminer comment organiser les bits. Comme tous les bits sont déclarés comme unsigned, le compilateur ajoute deux octets à la fin de la structure pour qu'elle comporte un nombre entier de doubles-mots. Il est possible d'y remédier en déclarant tous les champs unsigned short. Maintenant, le compilateur Microsoft n'a plus besoin d'ajouter d'octets d'alignement puisque six octets forment un nombre entier de mots de deux octets (35). Les autres compilateurs fonctionnent également correctement avec ce changement. La Figure 7.9 montre une autre définition qui fonctionne sur les trois compilateurs.

Image non disponible

Elle ne déclare plus que deux champs de bits en utilisant le type unsigned char.

Le lecteur ne doit pas se décourager s'il trouve la discussion ci-dessus confuse. C'est confus ! L'auteur trouve souvent moins confus d'éviter d'utiliser des champs de bits en utilisant des opérations niveau bit pour examiner et modifier les bits manuellement.

VIII-A-4. Utiliser des structures en assembleur

Comme nous l'avons dit plus haut, accéder à une structure en assembleur ressemble beaucoup à accéder à un tableau. Prenons un exemple simple, regardons comment l'on pourrait écrire une routine assembleur qui mettrait à zéro l'élément y d'une structure S. Supposons que le prototype de la routine soit :

Sélectionnez

void zero_y( S s_p );

La routine assembleur serait :

Sélectionnez

1 ?fine y_offset 4
2 _zero_y :
3 enter 0,0
4 mov eax, [ebp + 8] ; récupère s_p depuis la pile
5 mov dword [eax + y_offset], 0
6 leave
7 ret

Le C permet de passer une structure par valeur à une fonction. Cependant, c'est une mauvaise idée la plupart du temps. Toutes les données de la structure doivent être copiées sur la pile puis récupérées par la routine. Il est beaucoup plus efficace de passer un pointeur vers la structure à la place.

Le C permet aussi qu'une fonction renvoie une structure. Évidemment, une structure ne peut pas être retournée dans le registre EAX. Des compilateurs différents gèrent cette situation de façon différente. Une situation courante que les compilateurs utilisent est de réécrire la fonction en interne de façon à ce qu'elle prenne un pointeur sur la structure en paramètre. Le pointeur est utilisé pour placer la valeur de retour dans une structure définie en dehors de la routine appelée.

La plupart des assembleurs (y compris NASM) ont un support intégré pour définir des structures dans votre code assembleur. Reportez-vous à votre documentation pour plus de détails.

VIII-B. Assembleur et C++

Le langage de programmation C++ est une extension du langage C. Beaucoup des règles valables pour interfacer le C et l'assembleur s'appliquent également au C++. Cependant, certaines règles doivent être modifiées. De plus, certaines extensions du C++ sont plus faciles à comprendre en connaissant le langage assembleur. Cette section suppose une connaissance basique du C++.

VIII-B-1. Surcharge et décoration de noms

Le C++ permet de définir des fonctions (et des fonctions membres) différentes avec le même nom. Lorsque plus d'une fonction partage le même nom, les fonctions sont dites surchargées. Si deux fonctions sont définies avec le même nom en C, l'éditeur de liens produira une erreur, car il trouvera deux définitions pour le même symbole dans les fichiers objet qu'il est en train de lier. Par exemple, prenons le code de la Figure 7.10.

Image non disponible

Le code assembleur équivalent définirait deux étiquettes appelées _f ce qui serait bien sûr une

erreur. Le C++ utilise le même procédé d'édition de liens que le C, mais évite cette erreur en effectuant une décoration de nom (name mangling) ou en modifiant le symbole utilisé pour nommer une fonction. D'une certaine façon, le C utilise déjà la décoration de nom. Il ajoute un caractère de soulignement au nom de la fonction C lorsqu'il crée l'étiquette pour la fonction. Cependant, il décorera le nom des deux fonctions de la Figure 7.10 de la même façon et produira une erreur. Le C++ utilise un procédé de décoration plus sophistiqué qui produit deux étiquettes différentes pour les fonctions. Par exemple, la première fonction de la Figure 7.10 recevrait l'étiquette _f__Fi et la seconde, _f__Fd, sous DJGPP. Cela évite toute erreur d'édition de liens.

Malheureusement, il n'y a pas de standard sur la gestion des noms en C++ et des compilateurs différents décorent les noms de façon différente.

Par exemple, Borland C++ utiliserait les étiquettes @f$qi et @f$qd pour les deux fonctions de la Figure 7.10. Cependant, les règles ne sont pas totalement arbitraires. Le nom décoré encode la signature de la fonction. La signature d'une fonction est donnée par l'ordre et le type de ses paramètres. Notez que la fonction qui ne prend qu'un argument int a un i à la fin de son nom décoré (à la fois sous DJGPP et Borland) et que celle qui prend un argument double a un d à la fin de son nom décoré. S'il y avait une fonction appelée f avec le prototype suivant :

Sélectionnez

void f ( int x , int y , double z);

DJGPP décorerait son nom en _f__Fiid et Borland en @f$qiid.

Le type de la fonction ne fait pas partie de la signature d'une fonction et n'est pas encodé dans le nom décoré. Ce fait explique une règle de la surcharge en C++. Seules les fonctions dont les signatures sont uniques peuvent être surchargées. Comme on le voit, si deux fonctions avec le même nom et la même signature sont définies en C++, elles donneront le même nom décoré et créeront une erreur lors de l'édition de liens. Par défaut, toutes les fonctions C++ sont décorées, même celles qui ne sont pas surchargées. Lorsqu'il compile un fichier, le compilateur n'a aucun moyen de savoir si une fonction particulière est surchargée ou non, il décore donc tous les noms. En fait, il décore également les noms des variables globales en encodant le type de la variable d'une façon similaire à celle utilisée pour les signatures de fonctions.

Donc, si l'on définit une variable globale dans un fichier avec un certain type puis que l'on essaie de l'utiliser dans un autre fichier avec le mauvais type, l'éditeur de liens produira une erreur. Cette caractéristique du C++ est connue sous le nom de typesafe linking (édition de liens avec respect des types) . Cela crée un autre type d'erreurs, les prototypes inconsistants. Cela arrive lorsque la définition d'une fonction dans un module ne correspond pas avec le prototype utilisé par un autre module. En C, cela peut être un problème très difficile à corriger. Le C ne détecte pas cette erreur. Le programme compilera et sera lié, mais aura un comportement imprévisible, car le code appelant placera sur la pile des types différents de ceux que la fonction attend. En C++ cela produira une erreur lors de l'édition de liens.

Lorsque le compilateur C++ analyse un appel de fonction, il recherche la fonction correspondante en observant les arguments qui lui sont passés (36).

S'il trouve une correspondance, il crée un CALL vers la fonction adéquate en utilisant les règles de décoration du compilateur.

Comme des compilateurs différents utilisent différentes règles de décoration de nom, il est possible que des codes C++ compilés par des compilateurs différents ne puissent pas être liés ensemble. C'est important lorsque l'on a l'intention d'utiliser une bibliothèque C++ précompilée ! Si l'on veut écrire une fonction en assembleur qui sera utilisée avec du code C++, il faut connaître les règles de décoration de nom du compilateur C++ utilisé (ou utiliser la technique expliquée plus bas).

L'étudiant astucieux pourrait se demander si le code de la Figure 7.10 fonctionnera de la façon attendue. Comme le C++ décore toutes les fonctions, alors la fonction printf sera décorée et le compilateur ne produira pas un CALL à l'étiquette _printf. C'est une question pertinente. Si le prototype de printf était simplement placé au début du fichier, cela arriverait. Son prototype est :

Sélectionnez

int printf ( const char , ...);

DJGPP décorerait ce nom en _printf__FPCce (F pour fonction, P pour pointeur, C pour const, c pour char et e pour ellipse). Cela n'appellerait pas la fonction printf de la bibliothèque C standard ! Bien sûr, il doit y avoir un moyen pour que le C++ puisse appeler du code C. C'est très important, car il existe une énorme quantité de vieux code C utile. En plus de permettre l'accès au code hérité de C, le C++ permet également d'appeler du code assembleur en utilisant les conventions de décoration standard du C.

Le C++ étend le mot-clé extern pour lui permettre de spécifier que la fonction ou la variable globale qu'il modifie utilise les conventions C normales.

Dans la terminologie C++, la fonction ou la variable globale utilise une édition de liens C. Par exemple, pour déclarer la fonction printf comme ayant une édition de liens C, utilisez le prototype :

Sélectionnez

extern "C" int printf ( const char , ... );

Cela impose au compilateur de ne pas utiliser les règles de décoration de nom du C++ sur la fonction, mais d'utiliser les règles C à la place. Cependant, en faisant cela, la fonction printf ne peut pas être surchargée. Cela constitue la façon la plus simple d'interfacer du C++ et de l'assembleur, définir une fonction comme utilisant une édition de liens C puis utiliser la convention d'appel C.

Pour plus de facilité, le C++ permet également de définir une édition de liens C sur un bloc de fonctions et de variables globales. Le bloc est indiqué en utilisant les accolades habituelles.

Sélectionnez

extern "C" {

/ variables globales et prototypes des fonctions ayant une édition de liens C /

}

Si l'on examine les fichiers d'en-tête ANSI C fournis avec les compilateurs C/C++ actuels, on trouve ce qui suit vers le début de chaque fichier d'entête :

Sélectionnez

#ifdef __cplusplus

extern "C" {

#endif

Et une construction similaire, vers la fin, contenant une accolade fermante.

Les compilateurs C++ définissent la macro __cplusplus (avec deux caractères de soulignement au début). L'extrait ci-dessus entoure tout le fichier d'en-tête dans un bloc extern "C" si le fichier d'en-tête est compilé en C++, mais ne fait rien s'il est compilé en C (puisqu'un compilateur C génèrerait une erreur de syntaxe sur extern "C"). La même technique peut être utilisée par n'importe quel programmeur pour créer un fichier d'en-tête pour des routines assembleur pouvant être utilisées en C ou en C++.

VIII-B-2. Références

Les références sont une autre nouvelle fonctionnalité du C++. Elles permettent de passer des paramètres à une fonction sans utiliser explicitement de pointeur. Par exemple, considérons le code de la Figure 7.11.

Image non disponible

En fait, les paramètres par référence sont plutôt simples, ce sont des pointeurs. Le compilateur masque simplement ce fait aux yeux du programmeur (exactement de la même façon que les compilateurs Pascal qui implémentent les paramètres var comme des pointeurs). Lorsque le compilateur génère l'assembleur pour l'appel de fonction ligne 7, il passe l'adresse de y. Si l'on écrivait la fonction f en assembleur, on ferait comme si le prototype était (37) :

Sélectionnez

void f ( int xp);

Les références sont juste une facilité qui est particulièrement utile pour la surcharge d'opérateurs. C'est une autre fonctionnalité du C++ qui permet de donner une signification aux opérateurs de base lorsqu'ils sont appliqués à des structures ou des classes. Par exemple, une utilisation courante est de définir l'opérateur plus (+) de façon à ce qu'il concatène les objets string.

Donc, si a et b sont des strings, a + b renverra la concaténation des chaînes a et b. Le C++ appellerait en réalité une fonction pour ce faire (en fait, cette expression pourrait être réécrite avec une notation fonction sous la forme operator +(a,b)). Pour plus d'efficacité, il est souhaitable de passer l'adresse des objets string à la place de les passer par valeur. Sans référence, cela pourrait être fait en écrivant operator +(&a,&b), mais cela imposerait d'écrire l'opérateur avec la syntaxe &a + &b. Cela serait très maladroit et confus. Par contre, en utilisant les références, il est possible d'écrire a + b, ce qui semble très naturel.

VIII-B-3. Fonctions inline

Les fonctions inline sont encore une autre fonctionnalité du C++ (38). Les fonctions inline sont destinées à remplacer les macros du préprocesseur qui prennent des paramètres, sources d'erreur. Sourvenez-vous en C, une macro qui élève un nombre au carré ressemble à cela :

Sélectionnez

#define SQR(x) ((x)(x))

Comme le préprocesseur ne comprend pas le C et ne fait que de simples substitutions, les parenthèses sont requises pour calculer le résultat correct dans la plupart des cas. Cependant, même cette version ne donnerait pas la bonne réponse pour SQR(x++).

Les macros sont utilisées, car elles éliminent la surcharge d'un appel pour une fonction simple. Comme le chapitre sur les sous-programmes l'a démontré, effectuer un appel de fonction implique plusieurs étapes. Pour une fonction très simple, le temps passé à l'appeler peut être plus grand que celui passé dans la fonction ! Les fonctions inline sont une façon beaucoup plus pratique d'écrire du code qui ressemble à une fonction, mais qui n'effectue pas de CALL à un bloc commun. Au lieu de cela, les appels à des fonctions inline sont remplacés par le code de la fonction. Le C++ permet de rendre une fonction inline en plaçant le mot-clé inline au début de sa définition.

Par exemple, considérons les fonctions déclarées dans la Figure 7.12.

Image non disponible

L'appel à la fonction f, ligne 10, est un appel de fonction normal (en assembleur, en supposant que x est à l'adresse ebp-8 et y en ebp-4) :

Sélectionnez

1 push dword [ebp-8]

2 call _f

3 pop ecx

4 mov [ebp-4], eax

Cependant, l'appel à la fonction inline_f, ligne 11 ressemblerait à :

Sélectionnez

1 mov eax, [ebp-8]

2 imul eax, eax

3 mov [ebp-4], eax

Dans ce cas, il y a deux avantages à inliner. Tout d'abord, la fonction inline est plus rapide. Aucun paramètre n'est placé sur la pile, aucun cadre de pile n'est créé puis détruit, aucun branchement n'est effectué. Ensuite, l'appel à la fonction inline utilise moins de code ! Ce dernier point est vrai pour cet exemple, mais ne reste pas vrai dans tous les cas.

La principal inconvénient de l'inlining est que le code inline n'est pas lié et donc le code d'une fonction inline doit être disponible pour tous les fichiers qui l'utilisent. L'exemple de code assembleur précédent le montre. L'appel de la fonction non-inline ne nécessite que la connaissance des paramètres, du type de valeur de retour, de la convention d'appel et du nom de l'étiquette de la fonction. Toutes ces informations sont disponibles par le biais du prototype de la fonction. Cependant, l'utilisation de la fonction inline nécessite la connaissance de tout le code de la fonction. Cela signifie que si n'importe quelle partie de la fonction change, tous les fichiers source qui utilisent la fonction doivent être recompilés. Souvenez-vous que pour les fonctions non inline, si le prototype ne change pas, souvent les fichiers qui utilisent la fonction n'ont pas besoin d'être recompilés. Pour toutes ces raisons, le code des fonctions inline est généralement placé dans les fichiers d'en-tête. Cette pratique est contraire à la règle stricte habituelle du C selon laquelle on ne doit jamais placer de code exécutable dans les fichiers d'en-tête.

VIII-B-4. Classes

Une classe C++ décrit un type d'objet. Un objet possède à la fois des membres données et des membres fonctions (39). En d'autres termes, il s'agit d'une structure à laquelle sont associées des données et des fonctions. Considérons la classe simple dénie dans la Figure 7.13.

Image non disponible

Une variable de type Simple ressemblerait à une structure C normale avec un seul membre int.

Les fonctions ne sont pas affectées à la structure en mémoire. Cependant, les fonctions membres sont différentes des autres fonctions. On leur passe un paramètre caché. Ce paramètre est un pointeur vers l'objet sur lequel agit la fonction.

En fait, le C++ utilise le mot-clé this pour accéder au pointeur vers l'objet lorsque l'on se trouve à l'intérieur d'une fonction membre.

Par exemple, considérons la méthode set_data de la classe Simple de la Figure 7.13. Si elle était écrite en C, elle ressemblerait à une fonction à laquelle on passerait explicitement un pointeur vers l'objet sur lequel elle agit comme le montre le code de la Figure 7.14.

Image non disponible

L'option -S du compilateur DJGPP (et des compilateurs gcc et Borland également) indique au compilateur de produire un fichier assembleur contenant l'équivalent assembleur du code. Pour DJGPP et gcc le fichier assembleur possède une extension .s et utilise malheureusement la syntaxe du langage assembleur AT&T qui est assez différente des syntaxes NASM et MASM (40) (les compilateurs Borland et MS génèrent un fichier avec l'extension .asm utilisant la syntaxe MASM). La Figure 7.15 montre la sortie de DJGPP convertie en syntaxe NASM avec des commentaires supplémentaires pour clarifier le but des instructions.

Image non disponible

Sur la toute première ligne, notez que la méthode set_data reçoit une étiquette décorée qui encode le nom de la méthode, le nom de la classe et les paramètres.

Le nom de la classe est encodé, car d'autres classes peuvent avoir une méthode appelée set_data et les deux méthodes doivent recevoir des étiquettes différentes. Les paramètres sont encodés afin que la classe puisse surcharger la méthode set_data afin qu'elle prenne d'autres paramètres, comme les fonctions C++ normales. Cependant, comme précédemment, des compilateurs différents encoderont ces informations différemment dans l'étiquette.

Ensuite, aux lignes 2 et 3 nous retrouvons le prologue habituel. À la ligne 5, le premier paramètre sur la pile est stocké dans EAX. Ce n'est pas le paramètre x ! Il s'agit du paramètre caché (41) qui pointe vers l'objet sur lequel on agit.

La ligne 6 stocke le paramètre x dans EDX et la ligne 7 stocke EDX dans le double-mot sur lequel pointe EAX. Il s'agit du membre data de l'objet Simple sur lequel on agit, qui, étant la seule donnée de la classe, est stocké au déplacement 0 de la structure Simple.

VIII-B-4-a. Exemple

Cette section utilise les idées de ce chapitre pour créer une classe C++ qui représente un entier non signé d'une taille arbitraire. Comme l'entier peut faire n'importe quelle taille, il sera stocké dans un tableau d'entiers non signés (doubles-mots). Il peut faire n'importe quelle taille en utilisant l'allocation dynamique. Les doubles-mots sont stockés dans l'ordre inverse (42) (i.e. le double-mot le moins significatif est au déplacement 0). La Figure 7.16 montre la définition de la classe Big_int (43).

Image non disponible

Fig. 7.16 Dénition de la classe Big_int

La taille d'un Big_int est mesurée par la taille du tableau d'unsigned utilisé pour stocker les données.

La donnée membre size_ de la classe est affectée au déplacement 0 et le membre number_ est affecté au déplacement 4.

Pour simplifier l'exemple, seuls les objets ayant des tableaux de la même taille peuvent être additionnés entre eux.

La classe a trois constructeurs : le premier (ligne 9) initialise l'instance de la classe en utilisant un entier non signé normal, le second (ligne 19) initalise l'instance en utilisant une chaîne qui contient une valeur hexadécimale. Le troisième constructeur (ligne 22) est le constructeur par copie.

Cette explication se concentre sur la façon dont fonctionnent les opérateurs d'addition et de soustraction, car c'est là que l'on utilise de l'assembleur.

La Figure 7.17 montre les parties du fichier d'en-tête relatives à ces opérateurs.

Image non disponible

Elles montrent comment les opérateurs sont paramétrés pour appeler des routines assembleur. Comme des compilateurs différents utilisent des règles de décoration radicalement différentes pour les fonctions opérateur, des fonctions opérateur inline sont utilisées pour initialiser les appels aux routines assembleur liées au format C. Cela les rend relativement simples à porter sur des compilateurs différents et est aussi rapide qu'un appel direct.

Cette technique élimine également le besoin de soulever une exception depuis l'assembleur !

Pourquoi l'assembleur n'est-il utilisé qu'ici ? Souvenez-vous que pour effectuer de l'arithmétique en précision multiple, la retenue doit être ajoutée au double mot significatif suivant. Le C++ (et le C) ne permet pas au programmeur d'accéder au drapeau de retenue du processeur. On ne pourrait effectuer l'addition qu'en recalculant indépendamment en C++ la valeur du drapeau de retenue et en l'ajoutant de façon conditionnelle au double-mot suivant. Il est beaucoup plus efficace d'écrire le code en assembleur à partir duquel on peut accéder au drapeau de retenue et utiliser l'instruction ADC qui ajoute automatiquement le drapeau de retenue.

Par concision, seule la routine assembleur add_big_ints sera expliquée ici. Voici le code de cette routine (contenu dans big_math.asm) :

big_math.asm

Sélectionnez

1 segment .text
2 global add_big_ints, sub_big_ints
3 ?fine size_offset 0
4 ?fine number_offset 4
5
6 ?fine EXIT_OK 0
7 ?fine EXIT_OVERFLOW 1
8 ?fine EXIT_SIZE_MISMATCH 2
9
10 ; Paramètres des routines add et sub
11 ?fine res ebp+8
12 ?fine op1 ebp+12
13 ?fine op2 ebp+16
14
15 add_big_ints:
16 push ebp
17 mov ebp, esp
18 push ebx
19 push esi
20 push edi
21 ;
22 ; initialise esi pour pointer vers op1
23 ; edi pour pointer vers op2
24 ; ebx pour pointer vers res
25 mov esi, [op1]
26 mov edi, [op2]
27 mov ebx, [res]
28 ;
29 ; s'assure que les 3 Big_int ont la même taille
30 ;
31 mov eax, [esi + size_offset]
32 cmp eax, [edi + size_offset]
33 jne sizes_not_equal ; op1.size_ != op2.size_
34 cmp eax, [ebx + size_offset]
35 jne sizes_not_equal ; op1.size_ != res.size_
36
37 mov ecx, eax ; ecx = taille des Big_int
38 ;
39 ; initialise les registres pour qu'ils pointent vers leurs tableaux respectifs
40 ; esi = op1.number_
41 ; edi = op2.number_
42 ; ebx = res.number_
43 ;
44 mov ebx, [ebx + number_offset]
45 mov esi, [esi + number_offset]
46 mov edi, [edi + number_offset]
47
48 clc ; met le drapeau de retenue à 0
49 xor edx, edx ; edx = 0
50 ;
51 ; boucle d'addition
52 add_loop:
53 mov eax, [edi+4*edx]
54 adc eax, [esi+4*edx]
55 mov [ebx + 4*edx], eax
56 inc edx ; ne modifie pas le drapeau de retenue
57 loop add_loop
58
59 jc overflow
60 ok_done:
61 xor eax, eax ; valeur de retour = EXIT_OK
62 jmp done
63 overflow:
64 mov eax, EXIT_OVERFLOW
65 jmp done
66 sizes_not_equal:
67 mov eax, EXIT_SIZE_MISMATCH
68 done:
69 pop edi
70 pop esi
71 pop ebx
72 leave
73 ret

Heureusement, la majorité de ce code devrait être compréhensible pour le lecteur maintenant. Les lignes 25 à 27 stockent les pointeurs vers les objets Big_int passés à la fonction via des registres. Souvenez-vous que les références sont des pointeurs. Les lignes 31 à 35 vérifient que les tailles des trois objets sont les mêmes (notez que le déplacement de size_ est ajouté au pointeur pour accéder à la donnée membre). Les lignes 44 à 46 ajustent les registres pour qu'ils pointent vers les tableaux utilisés par leurs objets respectifs au lieu des objets eux-mêmes (là encore, le déplacement du membre number_ est ajouté au pointeur sur l'objet).

La boucle des lignes 52 à 57 additionne les entiers stockés dans les tableaux en additionnant le double-mot le moins significatif en premier, puis les doubles-mots suivants, etc. L'addition doit être effectuée dans cet ordre pour l'arithmétique en précision étendue (voir Section III.A.5).

La ligne 59 vérifie qu'il n'y a pas de dépassement de capacité, lors d'un dépassement de capacité, le drapeau de retenue sera allumé par la dernière addition du double-mot le plus significatif. Comme les doubles-mots du tableau sont stockés dans l'ordre little endian, la boucle commence au début du tableau et avance jusqu'à la fin.

La Figure 7.18 montre un court exemple utilisant la classe Big_int.

Image non disponible

Notez que les constantes de Big_int doivent être déclarées explicitement comme à la ligne ProductID="16. C" w:st="on">16. C'est nécessaire pour deux raisons. Tout d'abord, il n'y a pas de constructeur de conversion qui convertisse un entier non signé en Big_int. Ensuite, seuls les Big_int de même taille peuvent être additionnés. Cela rend la conversion problématique puisqu'il serait difficile de savoir vers quelle taille convertir. Une implémentation plus sophistiquée de la classe permettrait d'additionner n'importe quelle taille avec n'importe quelle autre. L'auteur ne voulait pas compliquer inutilement cet exemple en l'implémentant ici (cependant, le lecteur est encouragé à le faire).

VIII-B-5. Héritage et polymorphisme

L'héritage permet à une classe d'hériter des données et des méthodes d'une autre. Par exemple, considérons le code de la Figure 7.19.

Image non disponible

Il montre deux classes, A et B, où la classe B hérite de A. La sortie du programme est :

Sélectionnez

Taille de a : 4 Déplacement de ad : 0

Taille de b : 8 Déplacement de ad: 0 Déplacement de bd: 4

A::m()

A::m()

Notez que les membres ad des deux classes (B l'hérite de A) sont au même déplacement. C'est important puisque l'on peut passer à la fonction f soit un pointeur vers un objet A soit un pointeur vers un objet de n'importe quel type dérivé de (i.e. qui hérite de) A. La Figure 7.20 montre le code assembleur (édité) de la fonction (généré par gcc).

Image non disponible

Notez que la sortie de la méthode m de A a été produite à la fois par l'objet a et l'objet b. D'après l'assembleur, on peut voir que l'appel à A::m() est codé en dur dans la fonction. Dans le cadre d'une vraie programmation orientée objet, la méthode appelée devrait dépendre du type d'objet passé à la fonction. On appelle cela le polymorphisme. Le C++ désactive cette fonctionnalité par défaut. On utilise le mot-clé virtual pour l'activer. La Figure 7.21 montre comment les deux classes seraient modifiées.

Image non disponible

Rien dans le restet du code n'a besoin d'être changé. Le polymorphisme peut être implémenté de beaucoup de manières. Malheureusement, l'implémentation de gcc est en transition au moment d'écrire ces lignes et devient beaucoup plus compliquée que l'implémentation initiale. Afin de simplifier cette explication, l'auteur ne couvrira que l'implémentation du polymorphisme que les compilateurs Microsoft et Borland utilisent. Cette implémentation n'a pas changé depuis des années et ne changera probablement pas dans un futur proche.

Avec ces changements, la sortie du programme change :

Sélectionnez

Size of a: 8 Déplacement de ad : 4

Size of b: 12 Déplacement de ad : 4 Déplacement de bd : 8

A::m()

B::m()

Maintenant, le second appel à f appelle la méthode B::m(), car on lui passe un objet B. Ce n'est pas le seul changement cependant. La taille d'un A vaut maintenant 8 (et 12 pour B). De plus, le déplacement de ad vaut maintenant 4, plus 0. Qu'y a-t-il au déplacement 0 ? La réponse à cette question est liée à la façon dont est implémenté le polymorphisme.

Une classe C++ qui a une (ou plusieurs) méthode(s) virtuelle(s) a un champ caché qui est un pointeur vers un tableau de pointeurs sur des méthodes(44). Cette table est souvent appelée la vtable. Pour les classes A et B ce pointeur est stocké au déplacement 0. Les compilateurs Windows placent toujours ce pointeur au début de la classe au sommet de l'arbre d'héritage.

En regardant le code assembleur (Figure 7.22) généré pour la fonction f (de la Figure 7.19) dans la version du programme avec les méthodes virtuelles, on peut voir que l'appel à la méthode m ne se fait pas via une étiquette.

Image non disponible

La ligne 9 trouve l'adresse de la vtable de l'objet. L'adresse de l'objet est placée sur la pile ligne 11. La ligne 12 appelle la méthode virtuelle en se branchant à la première adresse dans la vtable (45). Cet appel n'utilise pas d'étiquette, il se branche à l'adresse du code sur lequel pointe EDX. Ce type d'appel est un exemple de liaison tardive (late binding). La liaison tardive repousse le choix de la méthode à appeler au moment de l'exécution du code. Cela permet d'appeler la méthode correspondant à l'objet. Le cas normal (Figure 7.20) code en dur un appel à une certaine méthode et est appelé liaison précoce (early binding), car la méthode est liée au moment de la compilation.

Le lecteur attentif se demandera pourquoi les méthodes de classe de la Figure 7.21 sont explicitement déclarées pour utiliser la convention d'appel C en utilisant le mot-clé __cdecl. Par défaut, Microsoft utilise une convention différente de la convention C standard pour les méthodes de classe C++. Il passe le pointeur sur l'objet sur lequel agit la méthode via le registre ECX au lieu d'utiliser la pile. La pile est toujours utilisée pour les autres paramètres explicites de la méthode. Le modificateur __cdecl demande l'utilisation de la convention d'appel C standard. Borland C++ utilise la convention d'appel C par défaut.

Observons maintenant un exemple légèrement plus compliqué (Figure 7.23).

Image non disponible

Dans celui-là, les classes A et B ont chacune deux méthodes : m1 et m2. Souvenez-vous que comme la classe B ne définit pas sa propre méthode m2, elle hérite de la méthode de classe de A. La Figure 7.24 montre comment l'objet b apparaît en mémoire.

Image non disponible

La Figure 7.25 montre la sortie du programme.

Image non disponible

Tout d'abord, regardons l'adresse de la vtable de chaque objet. Les adresses des deux objets B sont les mêmes, ils partagent donc la même vtable. Une vtable appartient à une classe pas à un objet (comme une donnée membre static). Ensuite, regardons les adresses dans les vtables. En regardant la sortie assembleur, on peut déterminer que le pointeur sur la méthode m1 est au déplacement 0 (ou dword 0) et celui sur m2 est au déplacement 4 (dword 1). Les pointeurs sur la méthode m2 sont les mêmes pour les vtables des classes A et B, car la classe B hérite de la méthode m2 de la classe A.

Les lignes 25 à 32 montrent comment l'on pourrait appeler une fonction virtuelle en lisant son adresse depuis la vtable de l'objet (46). L'adresse de la méthode est stockée dans un pointeur de fonction de type C avec un pointeur this explicite. D'après la sortie de la Figure 7.25, on peut voir comment cela fonctionne. Cependant, s'il vous plaît, n'écrivez pas de code de ce genre ! Il n'est utilisé que pour illustrer le fait que les méthodes virtuelles utilisent la vtable.

Il y a plusieurs leçons pratiques à tirer de cela. Un fait important est qu'il faut être très attentif lorsque l'on écrit ou qu'on lit des variables de type classe depuis un fichier binaire. On ne peut pas utiliser simplement une lecture ou une écriture binaire, car cela lirait ou écrirait le pointeur vtable depuis le fichier ! C'est un pointeur sur l'endroit où la vtable réside dans la mémoire du programme et il varie d'un programme à l'autre. La même chose peut arriver avec les structures en C, mais en C, les structures n'ont des pointeurs que si le programmeur en définit explicitement. Il n'y a pas de pointeurs explicites déclarés dans les classes A ou B.

Une fois encore, il est nécessaire de réaliser que des compilateurs différents implémentent les méthodes virtuelles différemment. Sous Windows, les objets de la classe COM (Component Object Model) utilisent des vtables pour implémenter les interfaces COM (47). Seuls les compilateurs qui implémentent les vtables des méthodes virtuelles comme le fait Microsoft peuvent créer des classes COM. C'est pourquoi Borland utilise la même implémentation que Microsoft et une des raisons pour lesquelles gcc ne peut pas être utilisé pour créer des classes COM.

Le code des méthodes virtuelles est identique à celui des méthodes non virtuelles.

Seul le code d'appel est différent. Si le compilateur peut être absolument sûr de la méthode virtuelle qui sera appelée, il peut ignorer la vtable et appeler la méthode directement (p.e., en utilisant la liaison précoce).

VIII-B-6. Autres fonctionnalités C++

Le fonctionnement des autres fonctionnalités du C++ (p.e., le RunTime Type Information, la gestion des exceptions et l'héritage multiple) dépasse le cadre de ce livre. Si le lecteur veut aller plus loin, The Annotated C++ Reference Manual de Ellis et Stroustrup et The Design and Evolution of C++ de Stroustrup constituent un bon point de départ.


Les Générateurs de Nombres Aléatoires

Table des matières Plier Déplier

  • 1. Introduction
  • 2. Génération pseudo-aléatoire
  • 3. Générateur congruentiel linéaire (LCG)
    • 3-1. Théorie
    • 3-2. RANDU
    • 3-3. Windows
    • 3-4. Central Randomizer de Hoole
    • 3-5. La bibliothèque C standard
    • 3-6. Bilan des LCG
  • 4. Xorshift
  • 5. Mersenne Twister
  • 6. Utilisation de la graine
  • 7. Distribution normale
  • 8. Conclusion
    • 8-1. Remerciements

1. Introduction

Jmm ... appeler GetTickCount (un tick 18.2 fois par seconde) ou QueryPerformanceCounter ... Pour les nombre aléatoire sans se prendre le choux tu peut simplement lire le timer du PC (8253)

Comme dans toute activité de développement logiciel, le développement des jeux vidéo nécessite des tests et des corrections (et même beaucoup) ce qui implique souvent la nécessité de pouvoir reproduire un certain état du jeu. En cas de bogue dans un système de combat par exemple, on peut avoir besoin de reproduire à volonté et à l'identique un certain comportement des adversaires du joueur pour localiser le problème et ensuite le corriger.

La spécificité des jeux vidéo par rapport à d'autres applications, dans le domaine de la gestion par exemple, est que ce sont des applications qui font très souvent appel, et parfois très massivement, à la génération de nombres aléatoires. Mais ce caractère intrinsèquement aléatoire de bon nombre de jeux entre souvent en conflit avec les besoins de reproductibilité.

Dans la plupart des langages, il existe une fonction ou méthode permettant de générer des nombres aléatoires à partir de ce qui est communément appelé graine ou seed en anglais, et qui permet de reproduire à l'identique une séquence de nombres aléatoires.

Mais ce n'est pas le cas de tous les langages. Par exemple, en JavaScript, la fonction de base, Math.random(), ne donne aucun moyen au développeur de définir une graine et n'a donc aucun moyen de reproduire une séquence aléatoire générée plus tôt. C'est un peu comme au cinéma où le spectateur n'a aucune possibilité de pause ou de retour arrière. Même si cela fait partie de l'expérience du cinéma, dans le domaine du développement c'est autrement plus gênant.

Le but de cet article sera de présenter différents générateurs de nombres aléatoires (ou RNG pour Random Number Generator en anglais) se basant sur une graine pour générer une séquence de nombres aléatoires.

Cet article peut s'adresser aux développeurs utilisant d'autres langages que JavaScript. Soit parce que leur langage ne fournit pas non plus un générateur de séries aléatoires reproductibles, soit parce que la qualité du générateur par défaut n'est pas suffisante pour les besoins.

Le langage utilisé pour illustrer les algorithmes sera TypeScript qui est un sur-ensemble de JavaScript avec typage et héritage. Une présentation de ce langage est disponible ici.

Aussi, une démonstration en ligne de cet article est disponible ici.

2. Génération pseudo-aléatoire

Image non disponible

Bien que le hasard soit pratiquement omniprésent dans la vie de tous les jours, si on admet que les lois de la physique quantique sont vraies, il est loin d'être trivial, encore à l'heure actuelle de produire des nombres véritablement aléatoires à l'aide d'un ordinateur à cause de son mode de fonctionnement purement déterministe (à condition de faire abstraction des bogues et des plantages). Sauf à le relier à un processus physique comme la désintégration d'un élément radioactif ou un bruit de fond, les ordinateurs actuels doivent recourir à des algorithmes déterministes pour simuler le hasard, ce qui est quelque part paradoxal.

Mais de l'ordre peut naître le chaos. Un exemple basique est de considérer le cas du nombre PI qui est un nombre parfaitement déterministe mais dont le développement décimal est dans une certaine mesure imprévisible, en tout cas revêt beaucoup des caractéristiques du hasard. (cf. Preuss)

Dans le domaine qui nous intéresse, les jeux vidéo, qui s'apparente pour beaucoup à celui de la simulation numérique, il faut être capable de produire beaucoup de nombres aléatoires, et rapidement, sans avoir nécessairement la chance de jouer sur un supercalculateur. Pour cela, les mathématiciens ont trouvé au fil du temps diverses méthodes pour produire des nombres qui ont les apparences du hasard, même s'il faut toujours garder à l'esprit que ce n'est qu'une illusion.

Parmi les générateurs de nombres aléatoires, celui qui s'est imposé avec l'avènement des ordinateurs est le générateur congruentiel linéaire, et il est encore présent dans d'innombrables applications encore aujourd'hui.

À noter qu'en toute rigueur, les nombres générés par une procédure algorithmique déterministe comme ce sera le cas dans ce document, sont appelés des nombres pseudo-aléatoires. Cependant pour simplifier le propos, ils seront simplement appelés nombres aléatoires tout comme nous appellerons générateurs aléatoires, des procédures stricto sensu pseudo-aléatoires.

3. Générateur congruentiel linéaire (LCG)

Image non disponible

3-1. Théorie

Un Générateur Congruentiel Linéaire ou LCG pour son acronyme en anglais est essentiellement une suite mathématique relativement simple, définie de la manière suivante :

  • Xn+1 = (a * Xn + c) mod m

où mod désigne l'opérateur modulo, le reste de la division entière.

Le premier élément de la suite, X0 est souvent nommé seed dans le jargon informatique. C'est la graine qui engendrera tous les autres nombres pseudo-aléatoires.

Les coefficients <a, c, m> (resp. multiplicateur, incrément, modulo) définissent complètement un LCG et doivent obéir à certaines contraintes pour que le LCG produise des nombres aléatoires de qualité satisfaisante, ce qui n'est pas toujours le cas comme nous le verrons un peu plus bas.

C'est sur la base de ce modèle simple voire simpliste qu'encore aujourd'hui une majorité de nombres aléatoires sont produits sur nos ordinateurs. Cette famille de générateurs de nombres aléatoires a des défauts que nous allons voir, mais a trois principales qualités :

  1. Les LCG sont très simples à implémenter.
  2. Les LCG s'appuient sur un processus reproductible via la graine.
  3. Les LCG sont rapides par rapport à d'autres générateurs de nombres aléatoires.

Ces trois qualités suffisent à expliquer la présence encore très importante des LCG dans les implémentations de langages.

Passons donc en revue les différents LCG.

3-2. RANDU

C'est le nom d'un algorithme mis au point chez IBM dans les années 1960. L'algorithme RANDU fait partie d'une classe particulière de LCG nommée Park-Miller d'où dérive un autre générateur bien connu, le MINSTD. Les LCG de la classe Park-Miller ont la particularité d'avoir un incrément c qui vaut zéro. Les générateurs de ce type sont donc définis par seulement deux nombres <a, m> au lieu de trois avec la formule suivante :

  • Xn+1 = (a * Xn) mod m

Les contraintes sont plus fortes que dans le cas général des LCG, avec par exemple le modulo m qui doit être un nombre premier et aussi une contrainte sur la graine initiale X0 qui doit être impaire, et idéalement un nombre premier.

L'algorithme RANDU est défini par le couple <65539, 231> soit la formule de récurrence :

  • Xn+1 = (65539 * Xn) mod 231

On constate que le modulo n'est pas un nombre impair et encore moins un nombre premier, même s'il n'est pas loin de 231-1, ce qui posera problème si on analyse un minimum la séquence des nombres aléatoires générés par un tel algorithme.

Son implémentation en TypeScript pourrait être la suivante :

Sélectionnez
var randomSeed = Date.now();

function randU(): number {
  randomSeed = (randomSeed * 65539) % 2147483648;
  return randomSeed / 2147483648.0;
} // randU

où randomSeed est une variable globale initialisée au préalable d'un appel à la fonction. Même s'il est une pratique courante d'initialiser la graine avec l'heure courante, en toute rigueur il faudrait l'initialiser avec une valeur plus chaotique (ayant plus d'entropie) comme le mouvement de la souris.

Il existe tout un tas de tests pour s'assurer de la qualité des générateurs de nombres aléatoires. Nous nous limiterons dans cet article à une simple analyse visuelle. Une série de nombres aléatoires binaires peut être représentée sous la forme d'une suite linéaire de pixels noirs ou blancs avec retour à la ligne. C'est ce qu'on pourrait appeler la représentation pile ou face. Un générateur aléatoire de qualité doit donner une image sans motif particulier. Un bon brouillard uniforme.

Image non disponible
Représentations pile ou face typiquement aléatoires

Une autre façon simple de représenter une série de nombres aléatoires est une représentation dans un plan où deux nombres aléatoires consécutifs fournissent l'abscisse et l'ordonnée d'un point. Cela donne un nuage de points qui permet de voir certaines régularités. Évidemment, un générateur parfaitement aléatoire doit engendrer un nuage de points sans motif particulier et bien dispersé.

Image non disponible
Nuages de points typiquement aléatoires

Dans le cas de RANDU, voici ce que nous obtenons :

Image non disponible
Image non disponible
Représentations pile ou face et nuages de points de RANDU

On peut constater que pour certaines valeurs de graines, la série de nombres engendrée par RANDU comporte des régularités flagrantes ce qui explique pourquoi cet algorithme est aujourd'hui tombé en désuétude.

L'algorithme RANDU est en fait célèbre, non pas pour son efficacité, très médiocre voire mauvaise comme on peut le constater, mais justement pour être un bon exemple de ce que ne doit pas être un LCG.

Depuis heureusement, on a fait mieux, quoique…

3-3. Windows

Après IBM, on va s'attaquer à un autre mastodonte de l'informatique : Microsoft. Car pour ceux qui ont utilisé les API C/C++ de Windows, il existe une fonction rand() par défaut, fournie dans les API du célèbre système d'exploitation.

Il se trouve que le générateur aléatoire par défaut de Windows est aussi un LCG, du moins jusqu'à la version XP.

Ses paramètres sont <214013, 2531011, 216> ce qui donne la suite :

  • Xn+1 = (214013 * Xn + 2531011) mod 216

En TypeScript l'implémentation pourrait être celle-ci :

Sélectionnez
var randomSeed = Date.now();

function randMSWin(): number {
  randomSeed = randomSeed * 214013 + 2531011;
  randomSeed = (randomSeed / 65536) % 32768; // extract bits 30..16
  return randomSeed / 32767.0;
} // randMSWin

On remarque une petite variante qui consiste à n'extraire qu'une partie du résultat. En effet, il a été démontré que dans le cas des LCG, les bits de poids faibles avaient tendance à ne pas être très aléatoires, ce qui explique pourquoi dans beaucoup d'implémentations informatiques de LCG, une opération d'extraction des bits de poids forts est réalisée.

Si maintenant on visualise ce que nous donne ce générateur, on obtient ceci :

Image non disponible
Représentations pile ou face et nuages de points du générateur de Windows

Si on ne considère que la représentation pile ou face, le générateur de Windows semble tout à fait valable dans la mesure où il est impossible de distinguer des régularités à l'œil nu.

Cependant, si on considère la représentation en nuage de points, c'est une autre affaire. On constate que la plupart des points sont alignés le long de quelques droites et ce de façon systématique. Donc même si c'est moins pathologique que pour RANDU, le générateur aléatoire de Windows est à éviter dans la mesure du possible.

En remarque, quand on voit ce que des firmes telles qu'IBM et Microsoft avec les moyens qui vont avec, aient pu produire des générateurs d'aussi piètre qualité, cela devrait rassurer les modestes développeurs que nous sommes.

3-4. Central Randomizer de Hoole

À une époque maintenant reculée, où il fallait se connecter à Internet via un modem bruyant et que le HTML n'était que vaguement implémenté dans les quelques navigateurs de l'époque, il n'existait pas de fonction native en JavaScript pour générer un nombre aléatoire. C'est à cette époque qu'un jeune homme, Paul Hoole de son état, a mis au point un petit LCG capable de combler cette lacune.

Ce qui a fini par s'appeler le Central Randomizer de Hoole est un LCG définit par le triplet <9301, 49297, 233280> et dont la suite caractéristique est :

  • Xn+1 = (9301 * Xn + 49297) mod 233280
Sélectionnez
var randomSeed = Date.now();

function randCentral(): number {
  randomSeed = (randomSeed * 9301 + 49297) % 233280;
  return randomSeed / 233280.0;
} //randCentral

La principale différence par rapport à ses prédécesseurs est que le multiplicateur utilisé est petit par rapport à ceux utilisés plus haut. Cela tend à limiter les débordements de calcul lors de la multiplication avec la graine qui est souvent un grand nombre et permet dans l'absolu une meilleure rapidité bien que cela fût surtout important à l'époque des premiers navigateurs.

Une rapide analyse visuelle nous montre que les nombres aléatoires générés par ce générateur ne sont pas mauvais, même s'il est possible de relever de légères régularités.

Image non disponible
Représentations pile ou face et nuages de points du générateur de Hoole

Dans un environnement optimal, on préférera toujours un autre générateur que celui-ci. Cependant, dans le cas d'une application JavaScript nécessitant un générateur de nombres aléatoires devant utiliser des graines, excluant donc la fonction Math.random(), et dans un environnement contraint comme pour les mobiles (même si les progrès dans ce domaine sont impressionnants), le Central Randomizer de Hoole peut être un compromis intéressant et facile à mettre en œuvre.

3-5. La bibliothèque C standard

On en arrive enfin à un LCG valable, celui décrit par Kernighan et Ritchie dans l'ouvrage de référence sur le C (1978), et qui continue à être présent dans plusieurs implémentations des compilateurs C/C++.

Ce LCG est caractérisé par le triplet <1103515245, 12345, 216> et par la suite :

  • Xn+1 = (1103515245 * Xn + 12345) mod 216
Sélectionnez
var randomSeed = Date.now();

function randCLib(): number {
  randomSeed = randomSeed * 1103515245 + 12345;
  randomSeed = (randomSeed / 65536) % 32768; // extract bits 30..16

  return randomSeed / 32767.0;
} // randCLib

À noter qu'à l'heure actuelle, des variantes plus sophistiquées de cet algorithme sont implémentées dans les compilateurs C/C++.

On notera que pour la grande majorité des graines, les résultats visuels sont satisfaisants.

Image non disponible
Représentations pile ou face et nuages de points du générateur de C

3-6. Bilan des LCG

Le LCG du langage C est-il donc la solution à notre besoin ? Disons que cela dépend. Si on fait abstraction de la cryptographie qui nécessite des générateurs autrement plus complexes, la simulation numérique et à fortiori les jeux vidéo, notamment ceux utilisant intensivement la génération procédurale et les processus stochastiques (eg. phénomènes physiques, IA), la quantité requise de nombres aléatoires fait que fatalement ce LCG, comme n'importe quel autre, biaisera d'une façon ou d'une autre les nombres aléatoires générés, et ceci en vertu d'un théorème affirmant de façon schématique que les nombres générés par les LCG n'occupent pas tout l'espace des possibles mais uniquement une fraction. Le LCG de Windows en est un exemple flagrant mais le LCG de la bibliothèque C comporte également ce défaut, même si c'est dans une bien moindre mesure.

Par conséquent, pour avoir des nombres aléatoires occupant de façon homogène l'ensemble des possibles il faudra faire appel à d'autres générateurs un peu plus élaborés que les LCG.

4. Xorshift

Le Xorshift est un générateur aléatoire mis au point en 2003 par George Marsaglia, qui utilise l'opérateur ou exclusif (xor) et le décalage de bits (shift). Comme les LCG, il a l'avantage d'être simple et rapide, mais n'a pas tous les défauts d'un LCG, même s'il n'est pas parfait.

L'implémentation de cet algorithme pourrait donner ceci en TypeScript :

Sélectionnez
var randomSeed = Date.now();

// Xorshift initialization
var x = 123456789;
var y = 362436069;
var z = 521288629;

function randXorshift(): number {
  var t = (x ^ (x << 11)) & 0x7fffffff;
  x = y;
  y = z;
  z = randomSeed;
  randomSeed = (randomSeed ^ (randomSeed >> 19) ^ (t ^ (t >> 8)));
  return randomSeed / 2147483648.0;
} // randXorshift

En termes de résultats visuels, rien ne permet de distinguer une série générée par Xorshift de celle générée par le véritable hasard.

Image non disponible
Représentations pile ou face et nuages de points du Xorshift

Des études scientifiques ont démontré que cet algorithme est supérieur aux LCG. Il représente donc un meilleur choix en cas d'utilisation intensive. Je soupçonne d'ailleurs Firefox d'avoir implémenté le Xorshift pour la fonction JavaScript standard Math.random(). Néanmoins, la version de base du Xorshift présentée ici comporte quelques défauts qu'il serait hors de propos d'aborder ici. Retenons juste que pour pallier ces défauts, il existe une variante du Xorshift, nommée Xorshift*, qui est considérée par les spécialistes comme l'un des plus rapides générateurs de nombres aléatoires de haute qualité.

5. Mersenne Twister

Venons-en à l'un des plus populaires générateurs de haute qualité, le Mersenne Twister, mis au point en 1997 par Makoto Matsumoto et Takuji Nishimura. Il doit son nom au fait que cet algorithme se base sur les propriétés des nombres premiers de Mersenne. Pour ce qui est d'une explication de l'algorithme, cela dépasse les compétences de votre humble serviteur, mais il faut savoir que ce générateur est celui utilisé par défaut dans de nombreux langages comme Python, Ruby ou PHP.

Voici une implémentation possible de cet algorithme en TypeScript :

Sélectionnez
var randomSeed = Date.now();

var N = 624;
var M = 397;
var MATRIX_A = 0x9908b0df; /* constant vector a */
var UPPER_MASK = 0x80000000; /* most significant w-r bits */
var LOWER_MASK = 0x7fffffff; /* least significant r bits */
var mt = new Array(N); /* the array for the state vector */
var mti = N + 1; /* mti==N+1 means mt[N] is not initialized */

/* initializes mt[N] with a seed */
mt[0] = randomSeed >>> 0;
for (mti = 1; mti < N; mti++) {
  var s = mt[mti - 1] ^ (mt[mti - 1] >>> 30);
  mt[mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253)
  + mti;
  /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */
  /* In the previous versions, MSBs of the seed affect */
  /* only MSBs of the array mt[]. */
  /* 2002/01/09 modified by Makoto Matsumoto */
  mt[mti] >>>= 0;
  /* for >32 bit machines */
} // for mti

function randMersenne(): number {
  var y;
  var mag01 = new Array(0x0, MATRIX_A);
  /* mag01[x] = x * MATRIX_A for x=0,1 */

  if (mti >= N) { /* generate N words at one time */
    var kk;

    for (kk = 0; kk < N - M; kk++) {
      y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK);
      mt[kk] = mt[kk + M] ^ (y >>> 1) ^ mag01[y & 0x1];
    }
    for (; kk < N - 1; kk++) {
      y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK);
      mt[kk] = mt[kk + (M - N)] ^ (y >>> 1) ^ mag01[y & 0x1];
    }
    y = (mt[N - 1] & UPPER_MASK) | (mt[0] & LOWER_MASK);
    mt[N - 1] = mt[M - 1] ^ (y >>> 1) ^ mag01[y & 0x1];

    mti = 0;
  }

  y = mt[mti++];

  /* Tempering */
  y ^= (y >>> 11);
  y ^= (y << 7) & 0x9d2c5680;
  y ^= (y << 15) & 0xefc60000;
  y ^= (y >>> 18);

  return (y >>> 0) * (1.0 / 4294967295.0);
} // randMersenne

Comme on s'en doutait, il n'est pas possible de distinguer visuellement une série générée par le Mersenne Twister d'une série réellement aléatoire :

Image non disponible
Représentations pile ou face et nuages de points du Mersenne Twister

Malgré la haute qualité des nombres aléatoires générés, le Mersenne Twister a un défaut non négligeable qui est une relative lenteur. Pour cette raison, il n'est pas forcément pertinent d'utiliser aveuglément cet algorithme dans des situations où les performances sont cruciales. Dans une telle situation, on lui préférera par exemple le Xorshift ou ses variantes.

6. Utilisation de la graine

Maintenant que nous avons un premier panorama de différents générateurs, on peut s'atteler à leur utilisation dans une application, en se donnant la possibilité de paramétrer la graine si besoin est. Ceci afin de répondre à l'exigence de reproductibilité que nous nous étions fixés en introduction.

En effet, à partir d'une même valeur de graine, la séquence du générateur sera toujours identique dans le cadre des algorithmes présentés qui sont tous déterministes. Il suffit donc de mémoriser la graine pour reproduire à l'envie une même séquence.

Si on s'appuie sur l'exemple de générateur aléatoire d'univers présenté dans un autre sujet, une même graine peut donc reproduire un même univers. D'une certaine façon, cela peut être considéré comme une forme de compression.

Image non disponible
Univers généré à partir de la graine 1411743524410

Une démonstration en ligne du générateur d'univers est disponible ici.

Dans certaines circonstances, la graine peut donc être mise à profit dans des jeux pour économiser de l'espace mémoire. Il est évidemment moins coûteux de stocker un seul nombre, plutôt que des milliers voire des millions dans le cas des mondes ouverts.

7. Distribution normale

Les générateurs présentés ici produisent des nombres aléatoires qui suivent la distribution uniforme. C'est-à-dire qu'en théorie, chaque valeur possible a la même probabilité d'apparition. Or, il est assez fréquent qu'on ait besoin d'une autre distribution, par exemple lorsqu'il s'agit de modéliser des phénomènes naturels ou s'en approchant.

La distribution normale aussi appelée distribution de Gauss est la plus utilisée, pour la simple raison que c'est en quelque sorte « la distribution des distributions » en vertu du Théorème de la Limite Centrale sur lequel le lecteur est invité à se documenter par lui-même.

Pour produire des nombres aléatoires suivant la distribution normale, il existe tout un tas de méthodes algorithmiques dont la plus répandue est celle s'appuyant sur la méthode de Box-Muller en coordonnées cartésiennes dont voici une implémentation en TypeScript :

Sélectionnez
function randNorm() {
  var y1, y2, rad;

  do {
    y1 = 2 * rand() - 1;
    y2 = 2 * rand() - 1;
    rad = y1 * y1 + y2 * y2;
  } while (rad >= 1 || rad == 0);

  var c = Math.sqrt(-2 * Math.log(rad) / rad);

  return y1 * c;
} // randNorm

La fonction rand() qui apparaît deux fois, doit être une fonction renvoyant un nombre aléatoire flottant compris entre 0 et 1.

Si on applique une distribution normale à notre d'univers de graine 1411743524410, cela donne ceci :

Image non disponible
Univers généré à partir de la graine 1411743524410 en distribution normale

8. Conclusion

De par l'importance des nombres aléatoires dans le domaine de la simulation numérique et des jeux vidéo, il n'est pas inutile de s'intéresser un minimum aux générateurs de nombres aléatoires.

Nous avons vu différents types de générateurs de nombres aléatoires. Mais il en existe bien d'autres.

Les LCG sont une bonne première approche, à condition de bien choisir ses coefficients, mais ils ont des limitations intrinsèques que seules d'autres approches peuvent surmonter comme l'algorithme du Xorshift ou le Mersenne Twister. Finalement, le choix d'un générateur est principalement un compromis entre la qualité des nombres aléatoires produits et la rapidité d'exécution de l'algorithme.

Il convient de garder à l'esprit que tous les générateurs présentés dans cet article ne sont pas exploitables dans le domaine de la cryptographie. Il faut pour cela utiliser des routines véritablement spécialisées et validées par des spécialistes. Pour un complément sur le sujet on peut se référer utilement à cette Introduction à la cryptographie.

Enfin, si l'exigence de reproductibilité des nombres aléatoires n'est pas requise, il est possible d'obtenir des nombres réellement aléatoires, issus de processus physiques, via des dispositifs matériels comme des puces, des cartes ou des clés USB, ou encore via divers services Web comme le QRNG Service de l'Université Humbolt de Berlin, ou celui de la société random.org.

Le code source des différents algorithmes présentés est disponible sur mon compte Github.

8-1. Remerciements

Je remercie ram-0000 pour la mise en forme, chrtophe pour la relecture technique ainsi que f-leb et Wachter pour la relecture orthographique de cet article.

Les commentaires et les suggestions d'amélioration sont les bienvenus, alors, après votre lecture, n'hésitez pas. Commentez1.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+

Copyright © 2014 yahiko. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique Réseau
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2017 - www.developpez.com


Vu dans ... https://benoit-m.developpez.com/assembleur/tutoriel/index.php

Apprendre l'assembleur

I. Introduction

« Désormais, pour apprendre le français, il faudra SAVOIR le français ! » disait Coluche. De même, ce tutoriel s'adresse à tous ceux qui connaissent déjà les rudiments d'un langage évolué tel que le BASIC, le FORTRAN, le C ou le PASCAL et qui souhaitent apprendre l'assembleur. Une connaissance même sommaire d'un de ces langages suffit ! Aucune connaissance en programmation système n'est requise : le but de ce cours est justement d'en introduire les fondements.

Contrairement aux langages évolués, l'assembleur, ou « langage d'assemblage » est constitué d'instructions directement compréhensibles par le microprocesseur : c'est ce qu'on appelle un langage de bas niveau. Il est donc intimement lié au fonctionnement de la machine. C'est pourquoi il est relativement difficile à assimiler, en tout cas beaucoup plus que les langages de haut niveau.

Cela explique également pourquoi il existe au moins autant de langages d'assemblage que de modèles de microprocesseurs.

Avant d'apprendre l'assembleur INTEL 80x86, il est donc primordial de s'intéresser à quelques notions de base concernant par exemple la mémoire ou le microprocesseur. C'est là en effet que se trouvent les principales difficultés pour le débutant. Ne soyez pas rebuté par l'abstraction des concepts présentés dans les premiers paragraphes : il est normal que durant la lecture, beaucoup de choses ne soient pas claires dans votre esprit. Tout vous semblera beaucoup plus simple quand nous passerons à la pratique dans le langage assembleur.

Le microprocesseur peut fonctionner sous deux modes : le mode réel et le mode protégé. Le mode protégé permet d'accéder à 232 octets de mémoire vive, alors que le mode réel ne peut en adresser que 220 = 1 Mo. Nous ne traiterons dans ce cours que le mode réel. C'est celui qu'utilisent la plupart des programmes DOS.

Convention : toutes les adresses sont écrites en notation hexadécimale. Les autres nombres seront la plupart du temps représentés en base décimale. Dans le cas contraire, nous ajouterons la lettre ‘h' après les chiffres.

II. PREMIÈRE PARTIE

II-A. L'ARITHMÉTIQUE SIGNÉE

On appelle arithmétique non signée l'arithmétique dans laquelle tous les entiers sont positifs. En arithmétique signée au contraire, les nombres peuvent être soit positifs, soit négatifs. Un nombre signé n'est donc pas forcément négatif.

Les données informatiques se présentent sous la forme d'une succession de chiffres binaires, les bits. Nous supposerons que les systèmes de numération binaire et hexadécimal vous sont déjà familiers.

Il est en revanche fort possible que vous ne connaissiez pas la façon dont un ordinateur représente les nombres négatifs. Il existe deux conventions : la notation en signe et valeur absolue et la notation en complément à 2.

La première est extrêmement simple : le premier bit représente le signe, et les autres bits la valeur absolue du nombre. Le bit de signe vaut 1 si le nombre est strictement négatif, 0 sinon.

Par exemple, le nombre (-14) codé sur 8 bits s'écrit ainsi :

10001110

Cette convention n'est quasiment jamais utilisée en informatique. On lui préfère la représentation en complément à 2 dont le principe est le suivant :

  • les nombres positifs sont codés de la même façon qu'en convention « signe et valeur absolue » ;
  • les nombres négatifs sont obtenus en inversant tous les bits, puis en ajoutant 1.

Exemple : le nombre 14 codé sur 8 bits est représenté ainsi :

00001110

et (-14) ainsi :

  • inversion des bits : 11110001 ;
  • ajout d'une unité : 11110010 ;
  • résultat : 11110010.

Remarque : le résultat intermédiaire, 11110001, est appelé « complément à 1 ».

Vous allez immédiatement comprendre l'avantage de cette représentation. Faisons la somme de 14 et de (-14), de la même façon que s'il s'agissait d'entiers positifs :

00001110 + 11110010 = 100000000

Le résultat étant codé sur 8 bits, le 1 situé à gauche n'est pas pris en compte. On obtient donc 14 + (-14) = 0.

L'intérêt évident est que la différence de deux nombres peut se calculer avec le même algorithme que leur somme. Il suffit de transformer au préalable le nombre retranché en son opposé. Cette conversion est très simple et très rapide.

Au contraire, en représentation « signe et valeur absolue », on aurait eu besoin de nombreux algorithmes, car plusieurs cas se présentent.

Remarque : la représentation en complément à 2 revient en fait à écrire (-1) comme ceci :

11111111

(-2) comme cela :

11111110

(-3) comme cela :

11111101

etc.

Il y a ainsi une symétrie entre les nombres positifs et les nombres négatifs. Il en résulte que le bit le plus à gauche représente le signe, de la même façon qu'en notation « signe et valeur absolue ». Avant tout calcul, nous pouvons donc affirmer que le nombre 10010101 est négatif.

II-B. LA MÉMOIRE VIVE

II-B-1. La segmentation de la mémoire

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.

La mémoire est découpée en segments de 64 Ko chacun. Un segment est donc en quelque sorte un gros bloc de mémoire auquel on peut accéder grâce à une adresse de segment qui désigne son numéro. Par exemple, le premier segment est le segment 0000 (en hexa), le deuxième est le 0001, le quarante-deuxième est le 0029, etc. Chaque numéro est codé sur 16 bits, c'est-à-dire 4 chiffres hexa.

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.

Eh oui ! Les segments ne sont pas situés gentiment les uns à la suite des autres. Ils sont fort mal élevés et 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.

La conséquence immédiate de tout cela est qu'un octet n'a pas une adresse unique. Par exemple, l'octet numéro 66 peut être adressé par 0000:0042, mais aussi par 0001:0032, par 0002:0022, par 0003:0012 ou encore par 0004:0002. Toutes ces adresses sont équivalentes.

Voilà pour la subtilité. Si vous avez compris, vous devriez être capable de trouver facilement comment on calcule l'adresse effective d'un octet, c'est-à-dire sa position absolue dans la RAM.

Allez, un petit effort !

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.

Image non disponible

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 à notre avis celle qui a le plus de sens, et nous l'utiliserons tout au long de ce cours.

II-B-2. Structure d'un programme en mémoire

Lorsque l'utilisateur exécute un programme, celui-ci est d'abord chargé en mémoire par le système. Le DOS distingue deux modèles de programmes exécutables : les fichiers COM et les fichiers EXE.

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.

II-B-3. Les fichiers COM

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.

Mais qu'est-ce donc qu'un PSP ?

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. Par exemple, le PSP d'un programme appelé MONPROG, exécuté avec la commande « MONPROG monfic.txt /S /H », contiendra la chaîne de caractères suivante : « monfic.txt /S /H ». Le programmeur a ainsi la possibilité d'accéder aux paramètres.

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

Offset | Description | Taille (octets)
00 h | Appel de l'int 20h | 2
02 h | Adresse du 1er segment qui se trouve au-delà du prog. | 2
04 h | Réservé | 1
05 h | Far call de l'int 21h (inutilisé) | 5
0A h | Vecteur de l'int 22h | 4
0E h | Vecteur de l'int 23h | 4
12 h | Vecteur de l'int 24h | 4
16 h | Réservé | 22
2C h | Segment du bloc d'environnement | 2
2E h | Réservé | 82
80 h | Nombre de caractères dans la ligne de commande sans compter le code ASCII 13 (retour chariot) | 1
81 h | Ligne de commande (à partir du caractère espace qui suit le nom du programme) + code ASCII 13 | 127

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

II-B-4. Les fichiers EXE

« Pour un fichier EXE, tout est toujours un peu plus compliqué. »
(Devise du programmeur débutant en assembleur)

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

II-B-5. Les cycles de lecture-écriture

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.

Ouf !

Nous aurons l'occasion de revenir sur la mémoire vive dans les autres chapitres pour apporter quelques précisions. Vous devriez mieux comprendre toutes ces notions en lisant la suite, car jusqu'ici nous n'avons pas encore parlé de la façon dont l'ordinateur se sert des adresses pour accéder à des données ou pour exécuter du code. C'est là qu'intervient le microprocesseur…

II-C. LE MICROPROCESSEUR - LES REGISTRES

II-C-1. Généralités sur les registres

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

Les registres des processeurs INTEL se classent en quatre catégories :

  • les registres généraux (16 bits) ;
  • les registres de segment (16 bits) ;
  • les registres d'offset (16 bits) ;
  • le registre des indicateurs (16 bits).

II-C-2. Les registres généraux

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.

Nom | Fonction privilégiée
AX | Accumulateur
BX | Base
CX | Compteur
DX | Données

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 :

  • l'instruction “MOV AX, 1982” place l'entier 1982 dans AX ;
  • “ADD AX, 1983” ajoute à AX le nombre 1983 et place le résultat dans AX.

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.

Exemple :

Image non disponible

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.

En arithmétique non signée, il en découle la formule suivante :

AX = AH x 256 + AL

ainsi que leurs homologues :

BX = BH x 256 + BL

CX = CH x 256 + CL

DX = DH x 256 + DL

Il va de soi qu'en modifiant AL ou AH, on modifie également AX.

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.

II-C-3. Les registres de segment

Ils sont au nombre de quatre :

Nom | Nom complet | Traduction
CS | Code segment | Segment de code
DS | Data segment | Segment de données
ES | Extra segment | Extra segment
SS | Stack segment | Segment de pile

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…

Dans le registre CS est stockée l'adresse de segment de la prochaine instruction à exécuter. La raison pour laquelle il ne faut surtout pas changer sa valeur directement est évidente. De toute façon, vous ne le pouvez pas. Le seul moyen viable de le faire est d'utiliser des instructions telles que des sauts (JMP) ou des appels (CALL) vers un autre segment. CS sera alors automatiquement actualisé par le processeur en fonction de l'adresse d'arrivée.

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 :

Registre | Valeur avant l'exécution
 | Fichier COM | Fichier EXE
CS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de code
DS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
ES | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
SS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de pile

Le schéma suivant montre mieux la situation :

Image non disponible

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.

II-C-4. Les registres d'offset

Les voici :

Nom | Nom complet | Traduction
IP | Instruction pointer | Pointeur d'instruction
SP | Stack pointer | Pointeur de pile
SI | Source index | Index de source
DI | Destination index | Index de destination
BP | Base pointer | Pointeur de base

Le registre IP désigne l'offset de la prochaine instruction à exécuter, par rapport au segment adressé par CS. La combinaison de ces deux registres (i.e. CS:IP) suffit donc à connaître l'adresse absolue de cette instruction. Le processeur peut alors aller la chercher en mémoire et l'exécuter. De plus, il actualise IP en l'incrémentant de la taille de l'instruction en octets. Tout comme CS, il est impossible de modifier IP directement.

Le registre SP désigne le sommet de la pile. Il faut bien comprendre le fonctionnement de la pile, aussi allons-nous insister sur ce point. La pile ne peut stocker que des mots. On appelle un mot (« word » en anglais) un nombre codé sur deux octets (soit 16 bits). Prenons un exemple simple : un programme COM. Le segment de pile (adressé par SS) et le segment de code ne font qu'un. Avant l'exécution, SP vaut FFFE. C'est l'offset de la dernière donnée de 16 bits empilée, par rapport à SS bien sûr. Pourquoi FFFE ? Tout simplement parce que la pile se remplit à l'envers, c'est-à-dire en partant de la fin du segment et en remontant vers le début. Le premier mot empilé se trouve à l'offset FFFE. Il tient sur deux octets : l'octet FFFE et l'octet FFFF. Mais comment se fait-il qu'un mot soit déjà empilé avant le début du programme ? Ce mot est un zéro que le DOS place sur la pile avant l'exécution de tout programme COM. Nous en verrons la raison plus tard.

À présent, que se passe-t-il si à un instant quelconque une instruction ordonne au processeur d'empiler un mot ? Eh bien le stack pointer sera décrémenté de 2 et le mot sera copié à l'endroit pointé par SP. Rappelez-vous que la pile se remplit à l'envers ! C'est pour cette raison que SP est décrémenté à chaque empilage et non pas incrémenté.

Un petit exemple pour rendre les choses plus concrètes :

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.

Exemple :

POP BX

Cette fois, on retire le dernier mot empilé pour le placer dans le registre BX. Évidemment, 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 :

Image non disponible

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

MOV AH, [MonOctet]

Pas de panique ! Cette instruction en assembleur signifie « Mettre dans AH la valeur de l'octet adressé par le label MonOctet ! ». À la compilation, « MonOctet » sera remplacé par la valeur numérique qu'il représente et on obtiendra alors une instruction en langage machine telle que :

8A260601

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.

Pour les constructeurs du microprocesseur, la seule solution était de créer de nouveaux opcodes pour chaque opération portant sur un octet en mémoire. Ces opcodes spéciaux feraient la même action que ceux dont ils seraient dérivés, mais l'adresse passée en paramètre serait alors considérée comme un décalage par rapport à un registre spécial. Il suffirait donc de faire varier ce registre, et le processeur y ajouterait automatiquement la valeur de l'opérande pour obtenir l'adresse réelle ! C'est à cela que servent SI, DI et BP.

Par exemple :

MOV AH, [MonOctet + DI]

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.

II-C-5. Le registre des indicateurs

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.

Nous verrons quelle utilisation on peut faire de ces indicateurs dans la troisième partie de ce tutoriel.

  • CF (« Carry Flag ») est l'indicateur de retenue. Il est positionné à 1 si et seulement si l'opération précédemment effectuée a produit une retenue. De nombreuses fonctions du DOS l'utilisent comme indicateur d'erreur : CF passe alors à 1 en cas de problème.
  • PF (« Parity Flag ») renseigne sur la parité du résultat. Il vaut 1 si ce dernier est pair.
  • AF (« Auxiliary Carry Flag ») est peu utilisé.
  • ZF (« Zero Flag ») passe à 1 si le résultat d'une opération est égal à zéro.
  • SF (« Sign Flag ») passe à 1 si le résultat d'une opération sur des nombres signés est négatif.
  • TF (« Trap Flag ») est utilisé pour le « débogage » d'un programme. S'il vaut 1, une routine spéciale du débogueur 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.
  • OF (« Overflow Flag ») indique qu'un débordement s'est produit, c'est-à-dire que la capacité de stockage a été dépassée. Il est utile en arithmétique signée. Avec des nombres non signés, il faut utiliser ZF et SF.

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.

II-D. LES INTERRUPTIONS

II-D-1. Introduction

Le microprocesseur ne peut exécuter qu'une seule instruction à la fois. Pour connaître son adresse, il utilise le couple de registres CS:IP dont la valeur est incrémentée automatiquement. Par conséquent, le code du programme courant est exécuté de manière linéaire.

Imaginons cependant qu'un événement extérieur demande l'attention de l'ordinateur, par exemple la pression d'une touche du clavier. La machine doit pouvoir réagir immédiatement, sans attendre que le programme en cours d'exécution se termine. Pour cela, elle interrompt ce dernier pendant un bref instant, le temps de traiter l'événement survenu puis rend le contrôle au programme interrompu.

Une interruption n'est rien d'autre que l'appel d'une routine spéciale présente en mémoire appelée ISR (« Interrupt Service Routine »).

Les interruptions se divisent en trois catégories :

  • les interruptions électroniques, par exemple : le clavier ;
  • les interruptions du BIOS, par exemple : l'accès aux disques ;
  • les interruptions du DOS, par exemple : l'accès aux systèmes de fichiers.

Comment sont-elles déclenchées ?

Une interruption peut être déclenchée par votre matériel. C'est ce qui arrive lorsque vous appuyez sur une touche du clavier. Aucun logiciel n'intervient et le contrôle est passé directement à la routine qui gère le clavier : ce sont les interruptions matérielles.

Les interruptions logicielles sont quant à elles appelées par des instructions en langage machine au sein d'un programme. Leur importance est capitale. Rappelez-vous que contrairement au PASCAL ou au C, l'assembleur ne dispose pas de fonction préprogrammée. Chaque instruction doit être directement traduisible en langage machine.

Mais alors, comment fait-on pour écrire une chaîne de caractères à l'écran ? Ou bien pour lire un caractère entré au clavier ?

Eh bien on le fait de la même façon que le DOS lui-même ! On déclenche les interruptions appropriées à l'aide de l'instruction « INT » du langage machine. C'est donc une routine du DOS (ou parfois du BIOS) qui fera tout le travail. Les paramètres (ou leurs adresses) sont passés dans les registres.

Voici un petit exemple en assembleur qui écrit la lettre ‘A' à l'écran :

Image non disponible

Examinons-le ligne par ligne :

  • l'instruction « MOV DL, ‘A' » demande au processeur de mettre dans le registre DL le code ASCII de la lettre ‘A', c'est-à-dire 65, ou 41h ;
  • « MOV AH, 02 » : mettre le nombre 2 dans AH ;
  • enfin, la dernière instruction appelle l'interruption numéro 21h. Il existe 256 interruptions. Toutes sont notées en base hexadécimale.

Explications :

Comme vous aurez très vite l'occasion de vous en rendre compte, l'interruption 21h est l'interruption du DOS par excellence. Elle permet d'appeler de nombreuses fonctions très diverses. Pour cela, il suffit de mentionner leur numéro dans le registre AH.

Il est très difficile de mémoriser le rôle de chaque interruption, et a fortiori de chaque fonction ou sous-fonction, d'autant plus qu'elles sont désignées par des numéros hexadécimaux et qu'elles attendent des paramètres dans des registres précis. C'est pourquoi tout programmeur se doit d'avoir à sa disposition une liste des interruptions pour travailler. Celle de Ralph Brown est très connue, et vous la trouverez sur l'Internet.

Revenons à notre exemple. La fonction numéro 2 de l'interruption 21h sert à écrire un caractère à l'écran. Il faut pour cela écrire le code ASCII du caractère dans le registre DL et bien sûr placer le nombre 2 dans AH.

Une fois que AH et DL ont été ajustés, l'interruption 21h peut être appelée à l'aide de l'instruction INT.

D'autres interruptions ne remplissent qu'une seule tâche. Vous n'avez donc pas besoin de mettre un numéro de fonction dans AH. L'appel de l'interruption suffit.

II-D-2. La table des vecteurs d'interruptions

À chaque appel d'interruption, l'ordinateur doit pouvoir trouver l'adresse de l'ISR correspondante. Pour cela, il dispose de la table des vecteurs d'interruptions (TVI, ou IVT : « Interrupt Vector Table »). Cette table est implantée à l'adresse 0000:0000 c'est-à-dire au début de la RAM.

La table est organisée comme suit :

Adresse | Taille (octets) | Valeur
0000:0000 | 2 | Adresse d'offset de l'interruption numéro 0
0000:0002 | 2 | Adresse de segment de l'interruption numéro 0
0000:0004 | 2 | Adresse d'offset de l'interruption numéro 1
0000:0006 | 2 | Adresse de segment de l'interruption numéro 1
0000:0008 | 2 | Adresse d'offset de l'interruption numéro 2
0000:000A | 2 | Adresse de segment de l'interruption numéro 2
0000:000C | 2 | Adresse d'offset de l'interruption numéro 3
0000:000E | 2 | Adresse de segment de l'interruption numéro 3
etc. etc. etc.
0000:03FC | 2 | Adresse d'offset de l'interruption numéro 255
0000:03FE | 2 | Adresse de segment de l'interruption numéro 255

La raison pour laquelle les offsets précèdent les adresses de segment vient du codage en « little endian » utilisé par INTEL. Les processeurs de cette marque (contrairement à la plupart des autres, qui travaillent en « big endian ») ont une représentation des données en mémoire aussi curieuse qu'insupportable pour le programmeur : ils placent le poids fort après le poids faible. Ainsi, le mot 4A28h sera codé 284Ah. Puisque l'adresse de l'ISR tient sur 32 bits (16 + 16), elle est représentée comme un double-mot (« dword »), donc l'adresse d'offset, qui est le mot de poids faible, se trouve au début. Eh oui, c'est pénible, mais il faudra s'y faire !

Remarque : pour calculer l'adresse de l'entrée dans la TVI correspondant à l'interruption numéro X, il suffit de multiplier X par 4.

Exemple : l'adresse de l'ISR numéro 21h est stockée à 0000:0084.

II-D-3. Sauvegarde de l'état des registres lors de l'appel

Un appel d'interruption obéit à certaines règles, car il est indispensable, une fois l'ISR exécutée, que le programme interrompu retrouve les registres dans le même état qu'ils étaient auparavant. Tout doit se passer comme si l'interruption n'avait jamais eu lieu. C'est pourquoi avant de faire un saut à l'ISR pointée par l'entrée correspondante dans la TVI, l'ordinateur sauvegarde tous les registres sur la pile courante. Il restaurera leur contenu avant de rendre le contrôle. Cette procédure est automatique. Il n'incombe pas au programmeur de prendre toutes ces précautions.

III. DEUXIÈME PARTIE

Cette partie a pour but de vous présenter l'architecture du langage assembleur à travers de courts exemples. Ne vous inquiétez pas si vous ne comprenez pas parfaitement certaines instructions : toutes seront détaillées dans la troisième partie. L'essentiel est que vous compreniez grossièrement ce qu'on fait et pourquoi on le fait . Les explications qui accompagnent ces exemples devraient y suffire. Vous pourrez ensuite apprendre le langage en lui-même en consultant la partie suivante.

Remarques pratiques préliminaires

  • Les mots qui sont imprimés en italiques sont ceux qui ne font pas partie du langage en lui-même. Ils sont choisis par le programmeur.
  • Tous les programmes présentés ci-dessous suivent la syntaxe du compilateur TASM. Il existe de très légères différences d'un compilateur à l'autre.
  • Le code peut être écrit en majuscules ou en minuscules. Chaque mot est séparé des autres par des espaces ou des tabulations.
  • Les commentaires sont précédés du signe ‘;'.
  • Les nombres doivent toujours commencer par un chiffre (entre 0 et 9), même les nombres hexadécimaux. Il faut donc écrire 0F2Ah et non pas F2Ah. Par contre, l'écriture 5CFh est correcte.
  • Les apostrophes et les guillemets sont équivalents. Il est cependant plus commode de réserver l'usage des guillemets aux chaînes de plusieurs caractères et celui des apostrophes aux caractères isolés.
  • Vous pouvez taper vos programmes avec l'éditeur de texte du DOS (EDIT.COM). Donnez-leur l'extension ‘.asm'.
  • La compilation se fait en tapant “TASM /m9 MONPROG” si votre source s'appelle ‘MONPROG.ASM'.

    Le paramètre ‘/m9', facultatif, indique au compilateur qu'il devra effectuer 9 passes, c'est-à-dire qu'il examinera 9 fois le code source. Comme chaque examen se fait de manière linéaire, le compilateur trouve parfois des instructions qui font référence à des labels placés plus loin dans le programme. Il manque donc d'informations, mais il continue son examen jusqu'à la fin. Quand il a terminé, il recommence tout depuis le début pour résoudre les problèmes qui s'étaient posés.

    La compilation crée un fichier objet (‘.obj'). Pour obtenir un fichier EXE, tapez ”TLINK MONPROG”. Pour un fichier COM, tapez “TLINK /tdc MONPROG”.

    Après l'édition des liens (le « linkage »), vous pouvez supprimer les fichiers MONPROG.obj' et ‘MONPROG.map'.

  • Il est possible de créer un fichier BAT qui s'occupe de toutes ces étapes. Ouvrez l'éditeur EDIT du DOS et tapez un programme tel que celui-ci :

    Enregistrez-le et nommez-le MAKE.BAT. Vous pouvez compiler et linker en tapant “MAKE MONPROG”. Pour compiler des fichiers COM, ajoutez le paramètre '/tdc' à la commande TLINK.

    Sélectionnez
    @ECHO OFF
TASM /m9 %1.asm
IF NOT EXIST %1.obj GOTO FIN
TLINK %1.obj REM : ajouter ici /tdc pour obtenir un fichier COM
ERASE %1.map
ERASE %1.obj
:FIN
  • Pour pouvoir lancer TASM et TLINK quel que soit le dossier courant, changez la variable PATH ainsi :

    PATH = %path%;c:\MonChemin\DossierTASM

    Vous pouvez inclure cette ligne à la fin de votre fichier AUTOEXEC.BAT afin qu'elle soit exécutée à chaque démarrage.

  • Vous pouvez déboguer vos programmes (par exemple les exécuter instruction par instruction en observant les changements induits dans la RAM et dans les registres…) avec le logiciel Turbo Debugger 16/32 bits qui est très pratique et très performant. À défaut, vous pouvez vous rabattre sur l'archaïque DEBUG.COM (il se trouve dans votre dossier de commandes DOS). Mais alors bon courage !

Le seul travail du compilateur (et du linkeur) est de convertir chacune de vos instructions en son équivalent en langage machine. Le programme compilé présentera donc exactement la même structure et la même linéarité que votre code source .

III-A. PREMIER EXEMPLE : LES FICHIERS COM

Voici un petit programme COM qui écrit le message « Bonjour, monde ! » à l'écran.

Image non disponible

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

code segment use16

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

assume cs:code, ds:code, ss:code

informe le compilateur que tout au long du programme, CS, DS et SS pointeront de façon privilégiée vers le segment “code”, ce qui est évident d'ailleurs puisque c'est le seul segment… Vous comprendrez le sens exact de “assume” dans la troisième partie du cours.

Enfin, les mots

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.

Nous en arrivons au programme proprement dit. Il commence par un label :

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 :

mov ah, 09h

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 :

mov dx, offset message

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.

Il nous reste à appeler l'interruption 21h :

int 21h

et à rendre la main au DOS :

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.

Nous arrivons à la ligne :

message db “Bonjour, monde !”, ‘$'

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

indique la fin du segment « code », et enfin

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.

III-B. DEUXIÈME EXEMPLE : LES FICHIERS EXE

Le programme suivant est un programme EXE qui fait exactement la même chose que l'exemple précédent.

Image non disponible

Examinons-le !

Tout d'abord, la directive

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.

Remarquons que notre programme dispose de deux segments supplémentaires :

  • le segment « data » est destiné à contenir les données, c'est-à-dire les variables ;
  • le segment « pile » sera notre segment de pile.

Notre directive « assume » devient donc :

assume cs:code, ds:data, ss:pile

Ainsi, le compilateur est informé que DS pointera vers le segment « data » et SS vers « pile ».

Les deux lignes suivantes,

mov ax, data

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 :

mov ds, data

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.

Exemples de chargement de valeurs immédiates :

mov ax, 135 ;charge 135 dans AX

mov bx, offset message ;charge l'offset de message dans BX

mov bx, offset fin - offset debut ;charge le nombre d'octets entre fin et debut dans BX

mov es, 10 ;instruction illicite, car ES est un registre de segment !

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

Les trois lignes suivantes :

mov ah, 09h

mov dx, offset message

int 21h

n'ont pas changé.

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.

mov ah, 4ch

int 21h

Remarque : on aurait également pu écrire :

mov ax, 4c00h

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.

Le segment de code se termine :

code ends

et le segment de données commence à sa suite :

data segment use16

message db “Bonjour, monde !”, ‘$'

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

remplissage db 256 DUP (?)

pile ends

Le mot-clé « stack » indique que ce segment est le segment de pile.

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.

La fin du fichier et le point d'entrée sont signalés par :

end debut

Et voilà ! Vous avez découvert l'allure d'un programme en assembleur. Nous pouvons donc à présent nous pencher sur l'étude du langage.

IV. TROISIÈME PARTIE

IV-A. DÉFINITION DE DONNÉES ET ADRESSAGE

IV-A-1. Les définitions de données

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

Le compilateur se contentera d'écrire le nombre 1982 à l'endroit où se trouve la déclaration.

Remarque : en C, on écrirait ceci :

int TOTO = 1982;

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.

Dans la partie précédente, nous avons eu à définir le message qui serait affiché à l'écran. Voici la ligne que nous avons écrite :

message db “Bonjour, monde !”, ‘$'

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 :

message db 42h, 111, ‘nj'

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 :

TOTO db 100 dup(“TOTO EST BEAU”)

Cela revient à écrire :

TOTO db “TOTO EST BEAU”

TOTO db “TOTO EST BEAU”

TOTO db “TOTO EST BEAU”

;… etc. (100 fois) …

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ée 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 :

Image non disponible

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 nombre est noté en base hexadécimale, on lui ajoute la lettre ‘h'. La lettre ‘b' signifie que le nombre est codé en binaire, la lettre 'o' en octal et la lettre ‘d' en base décimale (base par défaut).

IV-A-2. L'adressage

IV-A-2-a. L'adressage immédiat

On appelle « adressage immédiat » l'adressage qui ne fait intervenir que des constantes.

Considérons cette partie de programme qui stocke le nombre 125h dans une variable appelée TOTO, lui ajoute 15 puis charge le résultat dans AX.

Image non disponible

La ligne

mov word ptr ds:[TOTO], 125h

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.

En revanche, la troisième instruction ne fait pas apparaître l'expression “word ptr”. Cette fois-ci elle est facultative du fait que la destination (AX = 16 bits) impose la taille.

On peut évidemment remplacer “TOTO” par une constante numérique :

mov word ptr ds:[004Ch], 125h

Le mot 125h sera alors écrit à l'adresse DS:4C.

Une question se pose à présent : que se passe-t-il si le programmeur n'écrit pas le registre de segment dans l'adresse ?

C'est là qu'intervient la directive “assume”. Le compilateur va devoir trouver lui-même quel registre l'utilisateur a voulu sous-entendre.

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.

IV-A-2-b. L'adressage indexé et/ou basé

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.

Voici les adressages possibles :

  • Constante seule (= adressage immédiat) :

Exemples :

MOV AH, byte ptr [TOTO]

MOV BX, word ptr ds:[1045h]

MOV ES:[10 + TOTO x 2], BL

  • Base seule :

Exemple :

MOV dword ptr ds:[BP], 15

  • Index seul :

Exemple :

MOV dword ptr es:[DI], 142

  • Base + Constante :

Exemples :

MOV byte ptr ds:[BP + 1], 12

MOV ds:[BX + (TOTO - BOBO)/10], AX

  • Index + Constante :

Exemples :

MOV CX, [TOTO + DI]

MOV AL, ds:[1 + SI]

  • Base + Index :

Exemple :

MOV ds:[BP + SI], AH

  • Base + Index + Constante :

Exemple :

MOV AX, word ptr [TOTO + BX + DI + 1]

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…

IV-B. SAUTS INCONDITIONNELS, PROCÉDURES ET MACROS

IV-B-1. Les sauts inconditionnels

Ce paragraphe répond à la question : « Mais comment fait-on un GOTO en assembleur ? ».

L'instruction équivalente est JMP qui signifie « jump ».

La syntaxe est

JMP MonLabel

La référence à l'étiquette MonLabel sera remplacée lors de la compilation par la distance (signée) en octets qui sépare l'instruction qui suit immédiatement le JMP de l'instruction adressée par MonLabel. En pratique, le JMP s'utilise exactement comme un GOTO en BASIC.

Cette instruction permet de faire un saut inconditionnel : le programme fera ce saut quoi qu'il arrive.

Exemple :

Image non disponible

Le mot “short” ajouté après “jmp” indique au compilateur que le label “COUCOU” se trouve à une distance (signée) qui peut être stockée sur un seul octet. Le compilateur ne le sait pas encore lorsqu'il essaie de compiler l'instruction de saut, car le label se trouve en deçà de cette instruction. C'est pourquoi il prévoit deux octets pour écrire le saut, au cas où l'adresse d'arrivée est éloignée de plus de 128 octets. Lorsqu'il effectue une deuxième « passe », s'il s'aperçoit qu'un seul octet aurait suffi il remplit alors l'octet inutile avec l'instruction NOP (« No Operation ») qui ne fait rien du tout. Le programme marchera parfaitement (encore heureux !), mais cela gaspille un octet. Le programmeur a la possibilité d'aider le compilateur en ajoutant le mot “short”, ainsi un seul octet est prévu pour la distance de saut. Si le label est placé plus haut que l'instruction de saut, il est inutile de l'écrire.

Remarque : Ce mot peut également être utilisé avec les instructions de saut conditionnel que nous étudierons plus loin.

IV-B-1-a. Les procédures
IV-B-1-a-i. appels de procédures

Ceux qui ont déjà programmé en BASIC connaissent les instructions GOSUB et RETURN. Leurs équivalents en assembleur sont CALL et RET.

  • L'instruction CALL sert à appeler une procédure :

Syntaxe :

CALL MonLabel

Action : empile l'offset de l'instruction suivante puis fait un simple saut à l'adresse représentée par MonLabel.

  • RET (ou RETN) permet de retourner à l'instruction qui suit immédiatement le CALL :

Syntaxe :

RET

Action : dépile l'adresse de retour et la met dans IP. Le programme continue donc à l'adresse qui suit le CALL.

Remarque : Rappelez-vous qu'il est possible de terminer un programme COM avec l'instruction RET. Puisque le DOS empile un zéro de deux octets au chargement du programme, IP prendra la valeur 0000 lorsque le processeur exécutera cette instruction : il pointera donc vers le début du PSP. Or le PSP commence toujours par les deux octets suivants : CDh 20h (ce qui s'écrit INT 20h en assembleur). L'interruption 20h, qui permet de terminer un programme COM, sera donc appelée et le contrôle sera rendu au DOS.

Voici un exemple d'utilisation des procédures aussi simple que possible : ce programme COM appelle 12 fois une procédure qui écrit un message à l'écran et rend la main au DOS. Il n'est d'aucune utilité et n'est pas optimisé du tout.

Image non disponible

Remarque : Les codes ASCII 10 et 13 représentent respectivement la fin de ligne et le retour chariot. Grâce à eux, on revient à la ligne chaque fois qu'on a écrit le message.

Ce programme fonctionne parfaitement. Toutefois, les conventions d'écriture veulent que les procédures soient écrites comme suit :

Image non disponible

Le programme compilé est toujours le même, mais le code est plus lisible puisqu'on distingue les procédures des simples labels. Le mot “near” signifie que l'adresse de cette procédure est réduite à un offset. Il n'y a pas d'adresse de segment. La procédure ne pourra donc être appelée que de l'intérieur même du segment.

Le mot qui a le sens contraire de “near” est “far”. Les « call far » sont assez peu utilisés.

IV-B-1-a-ii. le passage des paramètres

Il existe deux moyens de passer des paramètres à une procédure : les registres et la pile.

  • Le passage par registres :

Il consiste à transmettre les paramètres dans des registres convenus à l'avance, par exemple AX et BX. Les deux gros avantages de cette méthode sont sa simplicité et la vitesse d'exécution. L'inconvénient majeur est le nombre limité de registres, d'autant plus qu'au moment de l'appel, certains d'entre eux ne seront peut-être pas disponibles.

  • Le passage par la pile :

C'est la méthode qu'utilisent les langages de haut niveau tels que le C ou le PASCAL. Avant l'appel, on empile les paramètres un à un. La procédure appelée se chargera de les lire.

Pour cela on utilise le registre BP de la manière suivante : on transfère la valeur de SP dans BP à l'aide de l'instruction “MOV BP, SP”. L'adresse de retour (qui sera dépilée quand le processeur rencontrera l'instruction “RET”) se trouve à l'adresse SS:[BP], le dernier paramètre est adressé par SS:[BP + 2], l'avant-dernier par SS:[BP + 4], etc. On peut ainsi lire n'importe quel paramètre sans le dépiler.

Attention cependant ! À la fin de la procédure, il faudra tout de même rendre à SP la valeur qu'il avait avant l'empilage des paramètres. Pour cela, on fait suivre l'instruction RET du nombre de paramètres, multiplié par 2 (car chaque paramètre comporte deux octets).

Exemple :

Image non disponible

Soyez toujours très vigilant avec les appels de procédures : pensez que l'adresse de retour sera dépilée lorsque le programme rencontrera l'instruction RET. Il serait donc suicidaire de laisser une donnée empilée avant d'appeler cette instruction.

IV-B-1-b. Les macros

Étant donné que certaines instructions se répètent constamment dans un programme, l'écriture de macrofonctions (ou macros) est un moyen pratique de rendre votre code source plus lisible.

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.

Image non disponible

Le code précédent peut être écrit n'importe où dans votre programme, à condition qu'il se trouve avant tout appel de cette macro. Afin d'éviter les ennuis, il est fortement conseillé de réunir vos macros au début du code, avant toute autre ligne. De toute façon, il ne sera pas compilé à l'endroit où vous l'avez écrit, mais aux endroits où se trouvent les appels de macros.

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.

Pour appeler cette macro, il vous suffit d'écrire la ligne

ecrit_texte “Coucou ! Ceci est un essai !”

Le compilateur se chargera alors de la remplacer par les instructions comprises entre la première et la dernière ligne de cet exemple, en prenant le soin de remplacer le mot “text?” par le message fourni en paramètre.

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 :

ecrit_texte “Coucou ! Ceci est un essai !”, 10, 13

est incorrecte, car le compilateur croirait que l'on a écrit trois paramètres ! Il faut alors entourer notre unique paramètre par les signes ‘<' et ‘>' :

ecrit_texte <”Coucou ! Ceci est un essai !”, 10, 13>

IV-B-1-c. La directive EQU

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 :

Longueur EQU (fin - debut)

Message EQU “Bonjour messieurs ! Comment allez-vous ?”, ‘$'

Version EQU 2

Quitter EQU ret

Quitter2 EQU int 20h

Mettre_dans_AH EQU mov ah,

Interruption_21h EQU int 21h

Un programme qui contient de telles directives peut se terminer ainsi :

Mettre_dans_AH 4Ch

Interruption_21h

Ou ainsi (si c'est un COM) :

Quitter2

IV-B-1-d. L'inclusion de fichiers

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 bibliothèques de macros ou de procédures que l'on peut réutiliser d'un programme à l'autre.

Pour inclure le fichier TOTO.LIB, écrivez au début de votre code source :

Image non disponible

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.

IV-C. LES PRINCIPALES INSTRUCTIONS DU LANGAGE MACHINE

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.
  • Certaines instructions, comme PUSHA, ne sont disponibles que pour des modèles de processeurs plus évolués que le 8086, par exemple le 286. N'oubliez pas la directive .386 si vous les utilisez.

IV-C-1. L'instruction NOP (« No Operation »)

Syntaxe : NOP

Description : Ne fait rien ! Mais alors RIEN ! Que dalle ! Niet !

IV-C-2. L'instruction MOV (« Move »)

Syntaxe : MOV Destination, Source

Description : Copie le contenu de Source dans Destination.

Mouvements autorisés :

MOV Registre général, Registre quelconque

MOV Mémoire, Registre quelconque

MOV Registre général, Mémoire

MOV Registre général, Constante

MOV Mémoire, Constante

MOV Registre de segment, Registre général

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

Exemples :

MOV AX, 5

MOV ES, DX

MOV AL, [Variable1] ; Copie un octet, car AL contient 8 bits

MOV [Variable2], DS ; Copie un word car DS contient 16 bits

MOV word ptr [Variable3], 12 ; Ici, on spécifie que la variable est un word

IV-C-3. L'instruction XCHG (« Exchange »)

Syntaxe : XCHG Destination, Source

Description : Échange les contenus de Source et de Destination.

Mouvements autorisés :

XCHG Registre général, Registre général

XCHG Registre général, Mémoire

XCHG Mémoire, Registre général

IV-C-4. L'instruction JMP (« Jump »)

Syntaxe : JMP MonLabel

Description : Saute à l'instruction pointée par MonLabel.

IV-C-5. L'opérateur CMP (« Compare »)

Syntaxe : CMP Destination, Source

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.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

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

IV-C-6. Les instructions de saut conditionnel

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.

IV-C-6-a. les sauts de comparaison
  • JE (« Jump if Equal ») fait un saut au label spécifié si et seulement si ZF = 1. Rappelez-vous que ce flag est à 1 si et seulement si le résultat de l'opération précédente vaut zéro. Comme CMP réalise une soustraction, on utilise généralement JE pour savoir si deux nombres sont égaux.

Exemple :

Image non disponible

Mnémonique équivalent : JZ (« Jump if Zero »)

  • JG (« Jump if Greater ») fait un saut au label spécifié si et seulement si ZF = 0 et SF = OF. On l'utilise en arithmétique signée pour savoir si un nombre est supérieur à un autre.

Exemple:

Image non disponible

Mnémonique équivalent : JNLE (« Jump if Not Less Or Equal »)

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

Mnémonique équivalent : JNL (« Jump if Not Less »)

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

Mnémonique équivalent : JNGE (« Jump if Not Greater Or Equal »)

  • JLE (« Jump if Less Or Equal ») fait un saut au label spécifié si et seulement si SF <> OF ou ZF = 1. On l'utilise en arithmétique signée pour savoir si un nombre est inférieur ou égal à un autre.

Mnémonique équivalent : JNG (« Jump if Not Greater »)

  • JA (« Jump if Above ») fait un saut au label spécifié si et seulement si ZF = 0 et CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur à un autre.

Mnémonique équivalent : JNBE (« Jump if Not Below Or Equal »)

  • JAE (« Jump if Above or Equal ») fait un saut au label spécifié si et seulement si CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur ou égal à un autre.

Mnémonique équivalent : JNB (« Jump if Not Below »)

  • JB (« Jump if Below ») fait un saut au label spécifié si et seulement si CF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur à un autre.

Mnémonique équivalent : JNAE (« Jump if Not Above Or Equal »)

  • JBE (« Jump if Below or Equal ») fait un saut au label spécifié si et seulement si CF = 1 ou ZF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur ou égal à un autre.

Mnémonique équivalent : JNA (« Jump if Not Above »)

IV-C-6-a-i. les sauts de test sur les flags

Ces instructions testent un flag unique et exécutent ou non le saut selon la valeur de ce flag.

  • JC (« Jump if Carry ») fait un saut au label spécifié si et seulement si CF = 1.

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 :

Image non disponible

Si l'appel de la fonction 3Eh de l'interruption 21H se solde par une erreur, alors la CF vaudra 1 et le saut sera accompli. Dans le cas opposé, l'exécution continuera normalement de manière linéaire.

  • JNC (« Jump if not Carry ») fait un saut au label spécifié si et seulement si CF = 0.
  • JZ (« Jump if Zero ») fait un saut au label spécifié si et seulement si ZF = 1. Ce mnémonique correspond au même opcode que JE.
  • 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.
  • JS (« Jump if Sign ») fait un saut au label spécifié si et seulement si SF = 1.
  • JNS (« Jump if not Sign ») fait un saut au label spécifié si et seulement si SF = 0.
  • JO (« Jump if Overflow ») fait un saut au label spécifié si et seulement si OF = 1.
  • JNO (« Jump if not Overflow ») fait un saut au label spécifié si et seulement si OF = 0.
  • JP (« Jump if Parity ») fait un saut au label spécifié si et seulement si PF = 1.
  • JNP (« Jump if not Parity ») fait un saut au label spécifié si et seulement si PF = 0.
IV-C-6-a-ii. le saut de test sur le registre CX

JCXZ (« Jump if CX = Zero ») fait un saut au label spécifié si et seulement si CX = 0.

IV-C-6-b. Les instructions arithmétiques
IV-C-6-b-i. L'instruction INC (« Increment »)

Syntaxe : INC Destination

Description : Incrémente Destination .

Indicateurs affectés : AF, OF, PF, SF, ZF

Exemple : INC CL

IV-C-6-b-ii. L'instruction ADD (« Addition »)

Syntaxe : ADD Destination , Source

Description : Ajoute Source à Destination.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Exemple : ADD byte ptr [ VARIABLE + DI], 5

IV-C-6-b-iii. L'instruction ADC (« Add with Carry »)

Syntaxe : ADC Destination , Source

Description : Ajoute ( Source + CF) à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-iv. L'instruction DEC (« Decrement »)

Syntaxe : DEC Destination

Description : Décrémente Destination .

Indicateurs affectés : AF, OF, PF, SF, ZF

IV-C-6-b-v. L'instruction SUB (« Substract »)

Syntaxe : SUB Destination , Source

Description : Soustrait Source à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-vi. L'instruction SBB (« Substract with Borrow »)

Syntaxe : SBB Destination , Source

Description : Soustrait ( Source + CF) à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-vii. L'instruction MUL (« Multiply »)

Syntaxe : MUL Source

Description : Effectue une multiplication d'entiers non signés .

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

Indicateurs affectés : CF, OF

Remarque : Source ne peut être une valeur immédiate.

Exemples : MUL CX

MUL byte ptr [ TOTO ]

IV-C-6-b-viii. L'instruction IMUL (« Integer Multiply »)

Syntaxe : IMUL Source

IMUL Destination , Source

IMUL Destination , Source , Valeur

Description : Effectue une multiplication d'entiers signés .

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 .

Indicateurs affectés : CF, OF

IV-C-6-b-ix. L'instruction DIV (« Divide »)

Syntaxe : DIV Source

Description : Effectue une division euclidienne d'entiers non signés .

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

Indicateurs affectés : AF, OF, PF, SF, ZF

Remarque : Source ne peut être une valeur immédiate.

IV-C-6-b-x. L'instruction IDIV (« Integer Divide »)

Syntaxe : IDIV Source

Description : Effectue une division euclidienne d'entiers signés .

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

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-xi. L'instruction NEG (« Negation »)

Syntaxe : NEG Destination

Description : Forme le complément à 2 de Destination , i.e. prend l'opposé de Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Remarque : À ne pas confondre avec NOT, qui forme le complément à 1.

IV-C-6-c. Les instructions logiques
IV-C-6-c-i. L'instruction NOT (« Logical NOT »)

Syntaxe : NOT Destination

Description : Effectue un NON logique bit à bit sur Destination (i.e. chaque bit de Destination est inversé).

IV-C-6-c-ii. L'instruction OR (« Logical OR »)

Syntaxe : OR Destination , Source

Description : Effectue un OU logique inclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Remarque : Afin d'optimiser la taille et les performances du programme, on peut utiliser l'instruction “OR AX, AX” à la place de “CMP AX, 0”.
En effet, un OU bit à bit entre deux nombres identiques ne modifie pas Destination et est exécuté « infiniment » plus rapidement qu'une soustraction. Comme les flags sont affectés, les sauts conditionnels sont possibles.

IV-C-6-c-iii. L'instruction AND (« Logical AND »)

Syntaxe : AND Destination , Source

Description : Effectue un ET logique bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

IV-C-6-c-iv. L'instruction TEST (« Test for bit pattern »)

Syntaxe : TEST Destination , Source

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 .

Indicateurs affectés : CF, OF, PF, SF, ZF

Exemple :

IV-C-6-c-v. L'instruction XOR (« Exclusive logical OR »)

Syntaxe : XOR Destination , Source

Description : Effectue un OU logique exclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

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.

IV-C-6-c-vi. L'instruction SHL (« Shift logical Left »)

Syntaxe : SHL Destination , Source

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.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Exemple : SHL AX, 4 ;permet de multiplier par 16 de façon infiniment plus rapide que MUL

Mnémonique équivalent : SAL (« Shift Arithmetical Left »)

IV-C-6-c-vii. L'instruction SHR (« Shift logical Right »)

Syntaxe : SHR Destination , Source

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.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Mnémonique équivalent : SAR (« Shift Arithmetical Right »)

IV-C-6-c-viii. L'instruction ROL (« Rotate Left »)

Syntaxe : ROL Destination , Source

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

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-ix. L'instruction ROR (« Rotate Right »)

Syntaxe : ROR Destination , Source

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

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-x. L'instruction RCL (« Rotate through Carry Left »)

Syntaxe : RCL Destination , Source

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

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-xi. L'instruction RCR (« Rotate through Carry Right »)

Syntaxe : RCR Destination , Source

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

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-d. Les instructions de manipulation des flags
IV-C-6-d-i. L'instruction CLC (« Clear Carry flag »)

Syntaxe : CLC

Description : Met CF à 0.

Indicateurs affectés : CF

IV-C-6-d-ii. L'instruction STC (« Set Carry flag »)

Syntaxe : STC

Description : Met CF à 1.

Indicateurs affectés : CF

IV-C-6-d-iii. L'instruction CLD (« Clear Direction flag »)

Syntaxe : CLD

Description : Met DF à 0.

Indicateurs affectés : DF

IV-C-6-d-iv. L'instruction STD (« Set Direction flag »)

Syntaxe : STD

Description : Met DF à 1.

Indicateurs affectés : DF

IV-C-6-d-v. L'instruction CLI (« Clear Interrupt flag »)

Syntaxe : CLI

Description : Met IF à 0.

Indicateurs affectés : IF

IV-C-6-d-vi. L'instruction STI (« Set Interrupt flag »)

Syntaxe : STI

Description : Met IF à 1.

Indicateurs affectés : IF

IV-C-6-d-vii. L'instruction CMC (« Complement Carry flag »)

Syntaxe : CMC

Description : Inverse CF.

Indicateurs affectés : CF

IV-C-6-d-viii. L'instruction LAHF (« Load AH from Flags »)

Syntaxe : LAHF

Description : Charge dans AH l'octet de poids faible du registre des indicateurs.

IV-C-6-d-ix. L'instruction SAHF (« Store AH into Flags »)

Syntaxe : SAHF

Description : Stocke les bits de AH dans le registre des indicateurs.

IV-C-6-e. Les instructions de gestion de la pile
IV-C-6-e-i. L'instruction PUSH (« Push Word onto Stack »)

Syntaxe : PUSH Source

Description : Empile le mot Source. SP est décrémenté de 2.

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.

IV-C-6-e-ii. L'instruction POP (« Pop Word off Stack »)

Syntaxe : POP Destination

Description : Dépile le mot qui se trouve au sommet de la pile et le place dans Destination. SP est incrémenté de 2.

IV-C-6-e-iii. L'instruction PUSHF (« Push Flags onto Stack »)

Syntaxe : PUSHF

Description : Empile le registre des indicateurs. SP est décrémenté de 2.

Remarque : PUSHFD empile le registre des indicateurs codé sur 32 bits.

IV-C-6-e-iv. L'instruction POPF (« Pop Flags off Stack »)

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.

Indicateurs affectés : Tous

Remarque : POPFD est utilisé pour un registre des indicateurs codé sur 32 bits.

IV-C-6-e-v. L'instruction PUSHA (« Push All registers onto Stack »)

Syntaxe : PUSHA

Description : Empile AX, BX, CX, DX, BP, SI, DI et SP.

Remarque : PUSHAD est utilisé pour des registres de 32 bits.

IV-C-6-e-vi. L'instruction POPA (« Pop All registers off Stack »)

Syntaxe : POPA

Description : Restaure AX, BX, CX, DX, BP, SI, DI et SP à partir de la pile.

Remarque : POPAD est utilisé pour des registres de 32 bits.

IV-C-6-f. Les instructions de gestion des chaînes d'octets
IV-C-6-f-i. L'instruction MOVSB (« Move String Byte »)

Syntaxe : MOVSB

Description : Copie l'octet adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés, sinon ils sont décrémentés.

Remarque : Pour copier plusieurs octets, faire REP MOVSB (« Repeat Move String Byte »). Le nombre d'octets à copier doit être transmis dans CX de même que pour un LOOP.

Exemple :

Image non disponible
IV-C-6-f-ii. L'instruction SCASB (« Scan String Byte »)

Syntaxe : SCASB

Description : Compare l'octet adressé par ES:DI avec AL. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

Remarques : Pour comparer plusieurs octets, faire “REP SCASB” ou “REPE SCASB” (« Repeat until Egal »), ou encore “REPZ SCASB” (« Repeat until Zero »). Ces trois préfixes sont équivalents.
Le nombre d'octets à comparer doit être transmis dans CX. La boucle ainsi créée s'arrête si CX = 0 ou si le caractère pointé par ES:DI est le même que celui contenu dans AL (i.e. si ZF = 1). On peut ainsi rechercher un caractère dans une chaîne.
Pour répéter au contraire la comparaison jusqu'à ce que ZF = 0, c'est-à-dire jusqu'à ce que AL et le caractère adressé par ES:DI diffèrent, utiliser REPNE ou REPNZ.

Exemple :

Image non disponible
IV-C-6-f-iii. L'instruction LODSB (« Load String Byte »)

Syntaxe : LODSB

Description : Charge dans AL l'octet adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté, sinon il est décrémenté.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour MOVSB.

Syntaxe : STOSB

Description : Stocke le contenu de AL dans l'octet adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

Syntaxe : CMPSB

Description : Compare l'octet adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés, sinon ils sont décrémentés.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-g. Les instructions de gestion des chaînes de mots

Ce sont les mêmes que les précédentes, hormis qu'elles traitent des mots et non des octets et que leur nom se termine par un ‘W' au lieu du ‘B'.

IV-C-6-g-i. L'instruction MOVSW (« Move String Word »)

Syntaxe : MOVSW

Description : Copie le mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

Remarque : Pour copier plusieurs mots, faire “REP MOVSW”. Le nombre de mots à copier doit être transmis dans CX.

IV-C-6-g-ii. L'instruction SCASW (« Scan String Word »)

Syntaxe : SCASW

Description : Compare le mot adressé par ES:DI avec AX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-g-iii. L'instruction LODSW (« Load String Word »)

Syntaxe : LODSW

Description : Charge dans AX le mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

IV-C-6-g-iv. L'instruction STOSW (« Store String Word »)

Syntaxe : STOSW

Description : Stocke le contenu de AX dans le mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

IV-C-6-g-v. L'instruction CMPSW (« Compare String Word »)

Syntaxe : CMPSW

Description : Compare le mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

IV-C-6-h. Les instructions de gestion des chaînes de doubles-mots

Elles traitent des doubles-mots et leur nom se termine par un ‘D'.

IV-C-6-h-i. L'instruction MOVSD (« Move String Double Word »)

Syntaxe : MOVSD

Description : Copie le double-mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

Remarque : Pour copier plusieurs doubles-mots, faire “REP MOVSD”. Le nombre de doubles-mots à copier doit être transmis dans CX.

IV-C-6-h-ii. L'instruction SCASD (« Scan String Double Word »)

Syntaxe : SCASD

Description : Compare le double-mot adressé par ES:DI avec EAX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-h-iii. L'instruction LODSD (« Load String Double Word »)

Syntaxe : LODSD

Description : Charge dans EAX le double-mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

IV-C-6-h-iv. L'instruction STOSD (« Store String Double Word »)

Syntaxe : STOSD

Description : Stocke le contenu de EAX dans le double-mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

IV-C-6-h-v. L'instruction CMPSD (« Compare String Double Word »)

Syntaxe : CMPSD

Description : Compare le double-mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

IV-C-6-i. Les instructions d'appel
IV-C-6-i-i. L'instruction CALL (« Procedure Call »)

Syntaxe : CALL MaProc

Description : Appel de procédure. Si MaProc se trouve dans un segment extérieur, le processeur empile CS. Ensuite, dans tous les cas, il empile IP et fait un saut à l'étiquette MaProc.

IV-C-6-i-ii. L'instruction RET (ou RETN)

Syntaxe : RET

Description : Retour de procédure se trouvant à l'intérieur du segment (NEAR). Un mot est dépilé et placé dans IP. Le contrôle retourne donc à la procédure appelante.

IV-C-6-i-iii. L'instruction RETF

Syntaxe : RETF

Description : Retour de procédure se trouvant à l'extérieur du segment (FAR). Deux mots sont dépilés et placés dans CS:IP. Le contrôle retourne donc à la procédure appelante.

IV-C-6-j. Les instructions de boucle
IV-C-6-j-i. L'instruction LOOP

Syntaxe : LOOP MonLabel

Description : Décrémente CX, puis, si CX <> 0, fait un saut à MonLabel.

IV-C-6-j-ii. L'instruction LOOPE (« Loop while Equal »)

Syntaxe : LOOPE MonLabel

Description : Décrémente CX, puis, si CX <> 0 et ZF = 1, fait un saut à MonLabel.

Mnémonique équivalent : LOOPZ

IV-C-6-j-iii. L'instruction LOOPNE (« Loop while not Equal »)

Syntaxe : LOOPNE MonLabel

Description : Décrémente CX, puis, si CX <> 0 et ZF = 0, fait un saut à MonLabel.

Mnémonique équivalent : LOOPNZ

IV-C-6-k. Les instructions d'adressage
IV-C-6-k-i. L'instruction LEA (« Load effective address »)

Syntaxe : LEA Destination, Source

Description : Charge l'offset de la source dans le registre Destination.

Exemple : LEA BP, word ptr [BP + TOTO]

IV-C-6-k-ii. L'instruction LDS (« Load pointer using DS »)

Syntaxe : LDS Destination, Source

Description : Transfère dans DS:Destination le contenu de la mémoire adressée par Source.

IV-C-6-k-iii. L'instruction LES (« Load pointer using ES »)

Syntaxe : LES Destination, Source

Description : Transfère dans ES:Destination le contenu de la mémoire adressée par Source.

IV-C-6-l. Les instructions de conversion arithmétique

Remarque préliminaire : Nous n'expliciterons pas toutes ces instructions.

IV-C-6-l-i. L'instruction AAA (« ASCII Adjust for Addition »)
IV-C-6-l-ii. L'instruction AAD (« ASCII Adjust for Division »)
IV-C-6-l-iii. L'instruction AAM (« ASCII Adjust for Multiplication »)
IV-C-6-l-iv. L'instruction AAS (« ASCII Adjust for Substraction »)
IV-C-6-l-v. L'instruction CBW (« Convert Byte to Word »)

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.

IV-C-6-l-vi. L'instruction CWD (« Convert Word to Double Word »)

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.

IV-C-6-l-vii. L'instruction DAA (« Decimal Adjust for Addition »)
IV-C-6-l-viii. L'instruction DAS (« Decimal Adjust for Substraction »)
IV-C-6-l-ix. L'instruction MOVSX (« Move with Sign Extend »)

Syntaxe : MOVSX Destination, Source

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.

IV-C-6-l-x. L'instruction MOVZX (« Move with Zero Extend »)

Syntaxe : MOVZX Destination, Source

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.

IV-C-6-l-xi. L'instruction XLAT (« Translate »)
IV-C-6-m. Les instructions d'entrée-sortie
IV-C-6-m-i. L'instruction IN (« Input from port »)

Syntaxe : IN Destination, Port

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.

IV-C-6-m-ii. L'instruction OUT (« Output to port »)

Syntaxe : OUT Port, Source

Description : Écrit dans Port la valeur contenue dans Source. Source ne peut être que AL ou AX. Port est une constante ou bien DX.

V. QUATRIÈME PARTIE

Remarque préliminaire : Cette partie présente quelques-unes des interruptions du DOS qui servent à manier les fichiers d'un disque. Nous la conclurons par l'étude d'un exemple de programme qui crypte un fichier choisi par l'utilisateur.

V-A. LECTURE ET ÉCRITURE DE FICHIERS AVEC LES HANDLES

Sous DOS, il existe deux méthodes pour accéder aux fichiers et pour chacune d'elles un lot d'interruptions spécifiques. La première est la méthode des FCB (« File contol block »). Elle est rarement utilisée, aussi ne l'aborderons-nous pas. Nous étudierons le principe de la méthode des « handles ».

Pour lire ou écrire des données dans un fichier, il est nécessaire de l'ouvrir, c'est-à-dire de le charger en mémoire. Quand toutes les opérations de lecture et d'écriture auront été effectuées, le fichier devra être refermé afin d'enregistrer les éventuelles dernières modifications et surtout de libérer la mémoire occupée.

V-A-1. Ouverture d'un fichier

On ouvre un fichier en appelant la fonction 3dh de l'interruption 21h. Celle-ci attend comme paramètre dans DS:DX l'adresse d'une chaîne de caractères qui contient le chemin d'accès au fichier sur un disque, par exemple “C:\MonDoss\MonFic.txt”.

Remarque : il n'est pas indispensable de mentionner le chemin d'accès complet : par défaut, le fichier sera cherché à partir du dossier courant.

Remarque importante : La chaîne doit être impérativement suivie de l'octet 00h qui sert à marquer sa fin.

Il nous faut également spécifier le mode d'accès en écrivant dans AL un 0 (si on veut ouvrir le fichier en lecture seule), un 1 (si on veut l'ouvrir en écriture seule) ou un 2 (lecture ET écriture).

Si l'interruption échoue, le flag CF sera mis à 1 sans que le fichier soit ouvert. Dans le cas contraire, CF est mis 0 et le registre AX contient un petit nombre entier (par exemple 5) appelé « handle » (ce qui signifie « poignée ») du fichier. Ce handle représente le fichier. C'est lui qu'il faudra désormais invoquer pour effectuer des opérations de lecture ou d'écriture, et non pas le chemin d'accès.
En effet, les chemins d'accès ne sont donc plus d'aucune utilité puisque les fichiers sont ouverts dans la mémoire vive.

V-A-2. Lecture dans un fichier

Une fois le fichier ouvert, on peut le lire avec la fonction 3fh. Il suffit de mentionner le handle dans BX, le nombre d'octets à lire dans CX, et l'adresse d'un buffer dans DS:DX.
Au cas où vous ne sauriez pas ce qu'est un buffer (ou tampon), sachez que c'est simplement une variable (généralement une chaîne de caractères) destinée à recevoir des données (ou à en fournir). Dans notre cas, le buffer va recevoir les octets lus dans le fichier.

Après l'appel, AX contient le nombre d'octets qui ont été effectivement lus (il peut être inférieur à la taille demandée si le fichier n'est pas assez long). En cas de problème, CF sera mis à 1.

V-A-3. Écriture dans un fichier

Pour écrire des données, on procède de même avec la fonction 40h. Les paramètres sont les mêmes que pour la fonction 3fh. Le buffer contient cette fois les octets à écrire. Après l'appel, le nombre d'octets qui ont été effectivement écrits est stocké dans AX (il sera être inférieur à la taille spécifiée si le disque est plein).

Les données sont écrites sur le disque dur que le tampon (dans la mémoire vive) est plein.

V-A-4. Existence d'un pointeur de fichier

Une question se pose cependant : à quel endroit du fichier les données sont-elles lues (ou écrites) ?

Réponse : quand un fichier est ouvert, un pointeur spécial pointant vers le début du fichier est créé. La première opération de lecture (ou d'écriture) se fera donc au début du fichier.
Mais entre chaque opération, le pointeur est incrémenté de la taille des données que l'on a lues (ou écrites). La deuxième opération se fera donc sur les octets qui suivent ceux de la première.

Remarque : Il est possible de modifier directement le pointeur de fichier : voyez pour cela la fonction 42h…

V-A-5. Fermeture d'un fichier

Pour terminer, le fichier doit être refermé. Les modifications éventuellement apportées et non enregistrées seront écrites sur le disque, et le handle sera libéré. C'est la fonction 3eh qui se charge de tout cela. Elle attend simplement le handle du fichier dans BX. Et comme d'habitude, CF vaut 1 après l'appel si des erreurs ont été rencontrées.

Remarque : Attention lorsque vous laissez le fichier ouvert longtemps afin d'y ajouter progressivement des données ! Si le système plante, vous perdrez les données qui se trouvent dans le tampon à ce moment. C'est pourquoi il est conseillé de forcer régulièrement l'écriture sur le disque en refermant le fichier.

V-A-6. Conclusion

Le tableau suivant récapitule ces différentes étapes :

Fonction | Description | Paramètres
3d h | Ouvrir le fichier | - DS:DX : adresse d'une chaîne contenant le chemin d'accès - AL : mode d'accès
3e h | Fermer le fichier | - BX : handle
3f h | Lire le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer
40 h | Écrire dans le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer

V-B. LES FONCTIONS DE RECHERCHE DE FICHIERS

Pour rechercher un fichier (ou un dossier), on se sert des fonctions 4eh (« Find First ») et 4fh (« Find Next »).

Imaginons par exemple que nous voulions chercher dans le dossier courant tous les fichiers qui portent l'extension “ .com ” afin de les supprimer. Comment devons-nous nous y prendre ?

V-B-1. La fonction 4eh

La fonction 4eh sert à définir des critères de recherche et à trouver le premier fichier qui correspond à ces critères (s'il existe).

On doit passer dans DS:DX l'adresse de la chaîne de caractères qui contient le masque de recherche (dans notre exemple, ce masque est “ *.com ”). Par défaut, les fichiers sont cherchés dans le dossier courant. Mais on peut évidemment spécifier un autre chemin dans le masque.

Remarque importante : afin que le DOS puisse connaître sa taille, le masque doit impérativement être terminé par l'octet 00h !

On écrit également dans CX les attributs des fichiers que l'on désire trouver. Si CX vaut 0, seuls les fichiers « normaux » pourront être trouvés. En fait, chaque bit de CL représente un attribut, comme le montre le tableau ci-dessous :

Bit | Signification
1 | Lecture seule
2 | Fichier caché
3 | Fichier système
4 | Volume
5 | Répertoire
6 | Fichier
7 | (Aucune…)
8 | (Aucune…)

Pour demander à la fonction 4eh de ne pas oublier les fichiers cachés, il suffit donc de charger CX avec la valeur 2 (bit numéro 2 = 1). De même, l'attribut 00000111 b (soit 7) nous permettra de trouver les fichiers en lecture seule, les fichiers cachés et les fichiers systèmes.

Remarque : Ne vous souciez pas trop des bits numéro 4 et 6. Laissez-les à 0.

Une fois que les paramètres ont été ajustés, on peut appeler la fonction 4eh. Si aucun fichier n'a été trouvé, le flag CF est mis à 1. On doit donc faire un test sur CF pour savoir si la recherche peut continuer ou si elle doit s'arrêter.
Si au contraire la fonction a trouvé un fichier, les caractéristiques de ce fichier (i.e. son nom, sa taille, ses attributs…) sont inscrites dans une zone de la mémoire appelée DTA (« Disk Transfer Area »).

Mais où se trouve donc cette DTA et à quoi ressemble-t-elle ?

Réponse : par défaut, le DOS place la DTA dans le PSP de votre programme, à l'offset 80h.

Remarque : il vous est naturellement possible de la déplacer en faisant appel à la fonction 1ah.

Voici la structure de la DTA :

Adresse | Description | Taille (octets)
00 h | Lettre du lecteur (0=courant, 1=A, 2=B…) sur lequel se trouve le fichier | 1
01 h | Modèle de la recherche | 11
0C h | Réservé | 9
15 h | Attributs du fichier | 1
16 h | Heure du fichier | 2
18 h | Date du fichier | 2
1A h | Taille du fichier | 4
1E h | Nom du fichier avec l'extension | 13

Puisque par défaut la DTA est située dans le PSP à l'offset 80h, le nom du fichier trouvé est écrit à l'offset 80h + 1eh = 9eh. De même, la taille se trouve à l'offset 9ah, etc.

V-B-2. La fonction 4fh

Jusqu'à présent, nous n'avons trouvé qu'un seul fichier ! Pour poursuivre la recherche, appelez la fonction 4fh sans écrire aucun paramètre dans les registres. Les caractéristiques de votre recherche ont été mémorisées dans la DTA : vous n'avez donc pas à les rappeler. De même que pour la fonction 4eh, CF est mis à 1 si aucun nouveau fichier n'a été trouvé. C'est le signe que vous pouvez arrêter votre recherche.

V-B-3. Conclusion

Résumons-nous :

Fonction | Description | Paramètres
4e h | Trouver le premier fichier qui correspond aux caractéristiques spécifiées | - DS:DX : adresse d'une chaîne contenant le masque - CX : attributs
4f h | Trouver le prochain fichier | Aucun !

À titre d'exemple, écrivons à présent le programme que nous évoquions tout à l'heure : “ trouver tous les fichiers COM du dossier courant et les effacer ”.

V-B-4. EXEMPLE DE PROGRAMME

V-B-5. Entrée bufferisée au clavier

L'exemple que nous proposons ici fait appel à une saisie au clavier. Comme nous n'en avons pas encore rencontré, ce paragraphe explique brièvement comment utiliser les fonctions 0ah et 0ch.

On peut se servir de la fonction 0ah pour lire une chaîne de caractères au clavier. Le seul paramètre à fournir est l'adresse d'un buffer. Comme il est souvent indispensable d'effacer le buffer avant la lecture, il est préférable de ne pas utiliser la fonction 0ah directement, mais d'appeler la fonction 0ch. En effet, celle-ci commence par effacer le buffer, puis appelle la fonction 0ah (ou une fonction voisine dont le numéro doit être transmis dans AL).

Voici le code à écrire :

Image non disponible

La seule difficulté est dans la déclaration du buffer. Voici comment vous devez vous y prendre si vous considérez que l'utilisateur pourra entrer au plus n caractères :

Image non disponible

Le premier octet de “maChaine” doit contenir le nombre d'octets maximal qui pourront être entrés, plus 1. Pourquoi plus 1 ? Tout simplement parce que le DOS met à la fin des caractères tapés le code ASCII 13 (retour chariot).

Pour comprendre la suite de la déclaration, il faut savoir de quelle manière le DOS transmet les caractères qui ont été lus. Après la lecture, le deuxième octet du buffer contiendra le nombre exact d'octets lus (sans compter le 13 final). La chaîne proprement dite ne commencera donc qu'au troisième octet. Puisqu'un octet est utilisé pour donner au DOS le nombre maximal de caractères autorisés, un autre pour recevoir le nombre de caractères lus effectivement, et encore un autre pour recevoir le code ASCII 13, le buffer doit comporter trois octets de plus que la taille maximale de la chaîne. C'est pourquoi on écrit :

maChaine db n+1, ?, n dup(?), ?

V-B-6. Le programme

Le programme suivant demande à l'utilisateur d'entrer le nom d'un fichier se trouvant dans le dossier courant puis crypte ce fichier en appliquant un NOT logique sur chaque octet. Naturellement, il n'est pas optimisé du tout. Ce n'est qu'un exemple !

Image non disponible

Suggestion : quand vous aurez compris ce petit programme, essayez donc de l'améliorer ! Par exemple, il faudrait que l'utilisateur puisse taper le nom du fichier à crypter directement comme paramètre du programme (i.e. “CRY MONFICH.EXE”). Voyez pour cela la structure du PSP telle que présentée dans la première partie. De plus, le programme devrait pouvoir trouver automatiquement le nom du fichier d'arrivée : ce serait le même que celui du fichier à crypter, mais avec une extension différente (par exemple “.cry”).
Vous pourriez également étendre les possibilités du logiciel en permettant à l'utilisateur de saisir non plus un simple nom de fichier, mais un masque complexe avec des chemins d'accès et des caractères “jokers” comme '*' et '?'. Vous aurez donc besoin des fonctions de recherche 4eh et 4fh. L'utilisateur doit de surcroît pouvoir choisir s'il veut crypter un fichier ou bien le décrypter…

VI. CONCLUSION

Voilà. Nous espérons que vous avez compris l'essentiel de ce cours, à savoir la logique de la programmation en assembleur. Les connaissances que vous avez acquises en lisant la fin du cours devraient vous permettre de mieux saisir les premiers chapitres. C'est pourquoi nous vous invitons à relire ce tutoriel.

Certaines notions doivent encore vous paraître obscures. Maintenant que vous vous êtes familiarisé avec l'assembleur, commencez par écrire et tester quelques petits programmes COM inutiles et très simples. Vous verrez qu'au bout de quelques essais, tout ce que vous avez lu vous semblera très concret.

Lorsque vous aurez écrit vous-même quelques tout petits programmes qui fonctionnent, l'assimilation du langage deviendra alors très rapide, car l'assembleur est un langage très logique et très cohérent.

Nous vous conseillons d'écrire une bibliothèque de petites macros ou procédures qui vous serviront dans tous vos programmes. Par exemple, vous pouvez faire une macro qui affiche un entier à l'écran, ou bien qui convertit une chaîne de caractères en un nombre entier.

Voici un exemple de macro qui renvoie dans AL le nombre de chiffres d'un entier non signé de deux octets passé dans AX :

Image non disponible

N.B. Pensez avant toute chose à vous munir d'une liste des interruptions du DOS. Vous ne pourrez rien faire si vous n'en avez pas !

Bon courage et bonne chance !


Tutoriel : En profondeur avec l'assembleur

Table des matières

En profondeur avec l'assembleur
Programmer ? C'est quoi ?
Le programme
Les instructions
L'assembleur
MASM
Les systèmes de numération (Partie 1)
Les systèmes de numération
Les chiffres et les nombres
La notation positionnelle
Le système décimal
Le système binaire
Le système hexadécimal
Une petite pause, les outils
Découvrons les outils
Téléchargements
Installation
Configuration de RadAsm
Comment ça marche (1/3)
LA question
Avant de commencer
La mémoire en détails
Encore un peu de binaire
Une mémoire bien organisée
Le sens du mot byte
Comment ça marche (2/3)
Un peu d'histoire
Le 8086 & Cie
Le cerveau de la bande
L'UAL
Comment ça marche (3/3)
Transports en commun
Bus à impériale
Capacité d'adressage
Rapide historique
Un minimum de code
Créer un nouveau projet
Présentation de l'environnement
Les mots-clés
Code minimal
Enfin un programme !
Commentons, commentons
Première instruction : mov
La macro print
Un petit problème
Un petit problème...bis

En profondeur avec l'assembleur

Bienvenue dans ce tuto sur l'assembleur ! :)

En commençant ce tuto, vous entrez dans la communauté très restreinte de programmeurs qui comprennent véritablement ce qu'ils font...

Malheureusement, l'assembleur a une mauvaise réputation. Celle d'être très difficile...
Sachez le tout de suite : C'est faux et archifaux ! :colere2:
N'importe qui, je dis bien n'importe qui, peut l'apprendre.
Le problème étant de trouver un cours clair, qui partirait de zéro et qui avancerait progressivement (c'est-à-dire sans parler du registre FLAGS au deuxième paragraphe).
Mon tuto le sera. Enfin, je l'espère...

Vous êtes prêts ? Vous avez intérêt, car on commence tout de suite...

Programmer ? C'est quoi ?

Le programme

Dans ce chapitre, nous allons ensemble comprendre les bases de la programmation. Ne vous inquiétez pas, nous allons partir de zéro, aucune connaissance n'est requise pour suivre ce tutoriel.

Le programme

Programmer ? C'est quoi ? Les instructions

Le programme

Tout d'abord, commençons par une question toute bête :

Qu'est-ce qu'un programme ?

J'en vois déjà plusieurs qui lèvent la main...
Vous ! Oui, vous, au fond. Parlez plus fort s'il vous plait, on n'entend rien !
Bon, je vais répéter ce qu'il a dit pour tout le monde :-° :

Un programme, c'est tout ce qui fonctionne sur votre ordinateur, par exemple : Un jeu vidéo, un lecteur vidéo (comme Windows Media Player par exemple), ou même un truc tout simple comme le bloc-notes de Windows.

Maintenant, une seconde question :

Comment ça marche ?

Silence. Ah ! Vous faites moins les malins maintenant...
Eh bien voilà, la réponse, c'est ce chapitre. ;)

Un programme est une suite d'instructions données à l'ordinateur. Elles sont écrites à l'avance par le programmeur. Lorsque vous mettez le programme en marche, l'ordinateur applique en ordre les instructions écrites dans le programme, ce qui donne à l'écran le programme que vous vouliez.

Retenez donc bien : une instruction est un ordre que l'ordinateur doit suivre. L'ordinateur va suivre les instructions dans l'ordre où vous les avez écrites.

Voici un schéma pour illustrer ceci :

Les instructions apparaissent en vert. Comme vous pouvez le voir, le programme est constitué d'instructions. L'ordinateur va les lire et les appliquer en ordre.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Programmer ? C'est quoi ? Les instructions

Les instructions

Le programme L'assembleur

Les instructions

Parlons maintenant des instructions elles-mêmes.
Tout d'abord, il y a une chose très importante que vous devez absolument savoir au sujet des instructions :

Vous ne pouvez pas inventer d'instruction.
Il existe une liste d'instructions déjà existantes, inventées par les créateurs de l'ordinateur. Nous les apprendrons plus tard.
L'ordinateur ne connait que ces instructions. Vous devrez donc combiner intelligemment ces instructions pour créer votre programme.

J'ai une question qui me trotte dans la tête depuis un bon moment... A quoi ressemble une instruction ?

Très bonne question.
Je vous le dis tout de suite, les instructions ne sont pas écrites en français. Ça aurait été trop beau... Elles ne sont pas non plus en anglais. L'ordinateur ne comprend aucune langue humaine.

Eh bien voilà, nous arrivons maintenant au coeur du problème : Nous devons écrire des instructions à l'ordinateur, mais l'ordinateur ne comprend pas notre langue. Alors, comment faire ?

Réponse : Simplement en écrivant les instructions dans la langue de l'ordinateur.

Quoi !? l'ordinateur comprend une langue ? o_O

Oui, et ça s'appelle le binaire. Ce n'est pas une langue très riche, puisqu'elle ne possède que deux caractères...deux chiffres, pour être précis : Le 1 et le 0.
A la base, le binaire servait en mathématiques à d'autres choses, mais les inventeurs de l'ordinateur l'ont utilisé pour écrire les instructions.

Mais on ne peut rien écrire dans une langue composée uniquement de deux chiffres !!!

Eh bien, si... Je ne vais pas vous expliquer tout de suite comment ça marche exactement, ce n'est pas vraiment le moment. Mais sachez que les instructions ressemblent vraiment à des trucs comme ça : 00001101010101101110 ou bien comme ça 1011110101010111111010.....
Chaque instruction a son code composé de 1 et de 0. Il est unique et non modifiable.

Ces instructions écrites en binaire forment ce qu'on appelle le langage machine. C'est-à-dire le langage compréhensible par la machine. ;)

En résumé, tous les programmes utilisables sur un ordinateur sont écrits en langage machine. C'est-à-dire qu'ils sont composés uniquement d'instructions écrites avec des 0 et des 1.

Vous vous souvenez de notre schéma d'un programme ? Voici donc maintenant, à quoi ça ressemble en réalité, c'est-à-dire avec les instructions codées en binaire.

De temps en temps, je mettrai des listes de mots de vocabulaire à retenir absolument, elle seront précédées du titre " zDico "

Attention ! Les listes zDico comporteront parfois des mots qui n'auront pas été vus du tout lors des explications.
Lisez-les donc avec beaucoup d'attention.

zDico

Binaire : Qui n'est composé que de 0 et de 1.

Langage machine : Ensemble des instructions compréhensibles par l'ordinateur, elles sont écrites en binaire.

Bit : Unité du binaire. Chaque 1 ou 0 est appelé un bit. Par exemple, 1101 contient 4 bits. Ce mot vient de l'anglais.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Le programme L'assembleur

L'assembleur

Les instructions MASM

L'assembleur

Eh attends ! On va vraiment devoir taper des zéros et des uns pour programmer !? Ca parait un peu soûlant....

Pas un peu. Très. Imaginez que pour créer ne serait-ce qu'un petit programme, il faudrait écrire au moins plusieurs milliers de bits :waw: . C'est possible, mais extrêmement compliqué. Si vous vous trompez sur un seul bit, mettant un 1 au lieu d'un 0, par exemple, le programme a beaucoup de chances de ne pas fonctionner du tout. Et vous le savez, errare humanum est, l'erreur est humaine.

Heureusement que de gentils programmeurs ont pensé à nous, pauvres humains, et ont inventé un système qui nous facilitera grandement les choses.
A chaque instruction en langage machine, (c'est-à-dire, je vous le rappelle, codée en binaire), ils ont fait correspondre un ou plusieurs mots composés de lettres et de chiffres humains. Ca s'appelle le langage assembleur.
On peut donc taper notre programme en langage assembleur. Puis, lorsqu'on a terminé, on lance un logiciel, qui va remplacer les instructions écrites en langage assembleur par les instructions correspondantes en langage machine.
Sans ça, l'ordinateur ne comprendra rien, puisque la seule langue qu'il connait est le langage machine.

Voici un schéma qui montre un programme écrit en langage assembleur :

zDico

Voici encore une liste de mots de vocabulaire à savoir absolument. (Certains n'ont pas encore été vus) :

Le langage assembleur : Il possède les mêmes instructions que le langage machine. A la seule différence que ces instructions sont écrites en lettres humaines. Chaque instruction en langage machine a une instruction en langage assembleur qui lui correspond et qui veut dire exactement la même chose. On l'appelle parfois "l'assembleur" tout court, à ne pas confondre avec "l'Assembleur".

L'Assembleur : C'est le nom du programme qui traduit du langage assembleur en langage machine. A ne surtout pas confondre avec le langage assembleur. Ce dernier étant le langage dans lequel on écrit, alors que l'Assembleur est le programme traducteur. Pour empêcher la confusion, lorsque je parlerai du langage, j'écrirai "assembleur" sans majuscule, et lorsque je ferai référence au programme j'écrirai "l'Assembleur" avec un "A" majuscule.

L'assemblage : C'est l'acte de traduire du langage assembleur en langage machine. On dit que l'Assembleur assemble un programme. C'est-à-dire qu'il le traduit en langage machine. Nous allons peu à peu utiliser ce mot là, à la place du mot "traduire".

asm ou ASM : C'est l'abréviation du langage assembleur. Le langage ASseMbleur ;) .

Et voici maintenant un schéma qui montre le travail de l'Assembleur. Ce schéma là apprenez-le plus que par coeur, je veux que vous en rêviez la nuit. C'est le schéma le plus important du chapitre, car il résume tout :

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Les instructions MASM

MASM

L'assembleur Les systèmes de numération (Partie 1)

MASM

Il y a une chose que je ne vous ai pas encore dite : Il existe plusieurs Assembleurs !

Chacun a son propre langage, que l'on appelle une syntaxe. Il y a donc autant de langages assembleur qu'il y a d'Assembleurs.
Soyons bien d'accord, le résultat en langage machine est exactement le même, peu importe l'Assembleur utilisé. L'unique différence réside dans le langage utilisé qui différera légèrement selon l'Assembleur.

Voici une liste non exhaustive des Assembleurs existants :

  • MASM

  • GoASM

  • NASM

  • FASM

  • TASM

  • ...

Dans ce tuto nous allons apprendre à utiliser, pour l'instant en tout cas, un seul Assembleur. Croyez-moi, c'est déjà pas mal...
Il se nomme MASM. Ce qui veut dire Macro Assembler. C'est l'Assembleur de Microsoft. Comme on l'a dit, ainsi que chaque Assembleur, il possède son propre langage assembleur. Nous l'appelerons la syntaxe de MASM.

Comment ça l'Assembleur de Microsoft ? On est obligé d'avoir Windows alors ? Et si j'ai Linux ou Mac ? :(

Ne vous inquiétez pas, il existe des versions de Masm pour Linux et Mac. Nous en parlerons plus tard.

La seule chose que vous devez savoir est qu'un programme assemblé sur un OS, ne fonctionnera pas sur un autre OS. Il faudra l'assembler à nouveau dans l'OS dans lequel vous voulez qu'il fonctionne.

Pour résumer très brièvement ce chapitre, nous avons vu qu'il existe deux façons d'écrire un programme :

  1. En langage machine

  2. En langage assembleur

Retenez bien que l'on utilise exactement les mêmes instructions en langage assembleur qu'en langage machine, la seule et unique différence réside dans la façon de les écrire. (Souvenez-vous des schémas).

On n'utilisera, bien entendu, jamais la première technique, la seconde étant mille fois plus simple (et je n'exagère presque pas). Mais sachez que pendant la seconde guerre mondiale, le langage assembleur n'ayant pas encore été inventé, les programmes gérant les trajectoires des bombes étaient écrits directement en langage machine. Et sans y avoir été je vous le garantis, les programmeurs de la seconde guerre mondiale, ils ont bien galéré.... :p

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
L'assembleur Les systèmes de numération (Partie 1)

Les systèmes de numération (Partie 1)

MASM Les systèmes de numération

Les systèmes de numération

Les systèmes de numération (Partie 1) Les chiffres et les nombres

Les systèmes de numération

Ne vous sauvez pas ! Je sais que le titre fait un peu peur, mais c'est normal, il a été inventé par des mathématiciens, et on n'y peut rien, ils raffolent des noms compliqués...
Il n’empêche que ce que nous allons apprendre dans ce chapitre est d'une simplicité étonnante, pour des maths...

Quoi !? Des Maths ? Mais je croyais que nous allions apprendre de l'informatique !

Oui, c'est vrai, mais vous aurez besoin de quelques bases en Maths avant de commencer...
Ne vous inquiétez pas, ce sera effroyablement simple.

Je vous préviens tout de suite, il est totalement impossible d'apprendre à programmer en assembleur sans ces bases. Alors s'il y en a un qui envisage de sauter ces quelques chapitres d'introduction, je lui fais sa fête. :diable: Faites-moi confiance, lisez-les et apprenez-les, même si pour l'instant le rapport avec la programmation vous paraît lointain. Et pour vous encourager, dites-vous que les grands programmeurs sont aussi passés par là, un jour.

Alors, en avant ! Plongeons-nous dans ce que nos chers confrères mathématiciens ont appelé "les systèmes de numération".

Tout d'abord, traduisons ce mot barbare, et pous cela décomposons-le :

système : Ca vous savez ce que c'est...
de : ...Vous ne trouvez pas que vous exagérez un peu ?
numération : Ce mot est de la même famille que "numéro", la numération est donc l'acte de compter.

En bref, un système de numération est une manière de compter.

On va apprendre à compter ?!

Eh oui ! Je vous avais dit que ça allait être simple ! (Si je l'avais dit dès le début, je n'aurais pas perdu la moitié de mes lecteurs...)

Je ne parle pas de la façon d'énoncer oralement les nombres, ça, ça dépend de la langue parlée (français, anglais...). Je parle de la façon d'écrire les nombres.

Car vous le savez sûrement, il existe plusieurs manières d'écrire les nombres (plusieurs dizaines, si vous voulez savoir). Il y a celle qu'on utilise tous les jours (0, 1, 2, 3...), mais il y en a d'autres comme par exemple le système romain (vu en CE2, je crois ^^ ), qui utilise des chiffres différents ainsi qu'une logique différente.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Les systèmes de numération (Partie 1) Les chiffres et les nombres

Les chiffres et les nombres

Les systèmes de numération La notation positionnelle

Les chiffres et les nombres

Avant d'étudier les systèmes de numération d'un peu plus près, je voudrais m'assurer d'une chose : Savez-vous quelle est la différence entre un chiffre et un nombre ?

Pour ceux à qui la réponse paraît évidente, sachez que j'ai posé cette question à plusieurs élèves de lycée et leurs réponses ont été des plus vagues...

Ouvrons donc le zDico, même si je doute qu'il contienne une chose aussi simple...

Ah si ! Vous en avez du pot...

zDico

Chiffre : Un chiffre est un symbole utilisé pour l'écriture des nombres.

Nombre : Un nombre est un moyen de représenter des grandeurs (des quantités).

En résumé, dans le nombre 354, le 5 est un chiffre, il n'est qu'un signe utilisé pour "coder" le nombre 354, il ne définit pas de quantité. Par contre, quand je dis "j'ai mangé 5 pizzas", le 5 est un nombre, car il définit une quantité.

Voilà, j'espère avoir été clair. :)

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Les systèmes de numération La notation positionnelle

La notation positionnelle

Les chiffres et les nombres Le système décimal

La notation positionnelle

Revenons à nos moutons. Les systèmes de numération.

Je l'ai déjà dit, il existe un grand nombre de systèmes de numération. Je vais maintenant ajouter une précision :

Les systèmes de numération sont divisés par groupes.

Chaque groupe contient plusieurs systèmes qui fonctionnent tous selon la même règle. Il y a, bien entendu, des différences entre les membres d'un groupe, mais la règle de base reste la même.

Après avoir appris la règle d'un groupe, vous serez facilement capables de comprendre les systèmes qu'il contient.
Rassurez-vous, pour la programmation en assembleur, nous n'aurons besoin de maîtriser qu'un seul groupe, et encore, pas tous les membres du groupe seront nécessaires.
Le groupe que allons apprendre utilise une unique règle : La notation positionnelle par base. Nous allons bientôt expliquer ce que cela signifie.

Vous avez de la chance. Le système que vous utilisez pour écrire les nombres dans la vie de tous les jours, appartient au groupe que nous allons apprendre. Vous appliquez donc régulièrement, de manière inconsciente, la règle de la notation positionnelle.
Ce système se nomme le système décimal. Les autres systèmes que nous allons étudier sont : Le système binaire et le système hexadécimal.

Je vais prendre comme exemple le système décimal, pour vous présenter la notation positionnelle. Ensuite, pour les autres membres du groupe, ça ira comme sur des roulettes.

Voici un schéma des groupes des systèmes de numération, le groupe que nous étudierons est encadré en rouge. Je n'ai pas détaillé les autres groupes, car ils nous seront inutiles. Ils ne sont là qu'à titre d'exemples.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Les chiffres et les nombres Le système décimal

Le système décimal

La notation positionnelle Le système binaire

Le système décimal

Comme je l'ai expliqué plus haut, je vais commencer par le système décimal. Ce système sera notre exemple pour comprendre la règle qui gère le groupe. Vous pourrez ensuite comprendre plus facilement les deux autres systèmes (binaire et hexadécimal).

Dans la vie de tous les jours, nous écrivons les nombres en utilisant le système décimal.

Voici à quoi ça ressemble, au cas où vous auriez un trou :

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12....612, 613, 614....1 223 555....etc

Ca va ? Vous les reconnaissez ? :-°

Ceci est le système décimal.

Analysons ensemble la façon dont il fonctionne.

Vous avez remarqué que ce système utilise dix signes différents, que l'on appelle des chiffres (Le zéro est aussi un chiffre).
On écrit les dix premiers nombres avec un seul chiffre jusqu'à neuf. Ensuite, on combine deux ou plusieurs chiffres pour créer les nombres.

Prenons par exemple le nombre 5823. Lorsque vous voyez ce nombre, vous comprenez automatiquement qu'il signifie cinq mille huit cent vingt-trois. Pourtant, il n'y a qu'un 5, un 8, un 2 et un 3. Que se passe-t-il dans votre tête, inconsciement, pour que vous sachiez que le nombre représenté avec ces chiffres, sans grandes valeurs, vaut plusieurs milliers ? :magicien:

Non, non, ce n'est pas de la magie. En fait, vous faites un calcul. Un calcul basé sur la place des chiffres. C'est la place des chiffres qui déterminera la valeur du nombre, vous allez tout de suite voir comment.
Avant cela, remarquez que vous comprenez maintenant pourquoi on l'appelle "la notation positionnelle", la position du chiffre étant déterminante. ;)

Voyons maintenant comment cela fonctionne avec un exemple :
Regardez le 8, dans notre nombre 5823. Comment savez vous qu'il vaut 800, en réalité ?
Ce que vous faites c'est que vous utilisez ce qui s'appelle la base du système décimal.

Qui ça ? La quoi ?

Oui, vous avez bien lu, la base. C'est le truc le plus important du chapitre, alors ouvrez bien vos oreilles yeux.
Chaque système possède une base. Celle-ci correspond au nombre de chiffres (= symboles) que possède le système. Avec la base, nous pouvons calculer la valeur des chiffres (nous verrons comment dans un instant).
Je répète : Chaque système a une base.

Dans le système décimal, la base est 10 ("décimal" vient du mot "dix").
Il y a donc dix chiffres dans le système décimal et les voici : 0 1 2 3 4 5 6 7 8 9 .

Ok, donc la base est un nombre, et chaque système en a une. En l’occurrence, dans le système décimal, c'est 10. Mais...ça sert à quoi, en fait ?

Un peu de patience, j'y viens... On utilise la base pour calculer la valeur des chiffres dans le nombre.
Voilà comment on fait (je reprends notre exemple "5823") :

  • Le premier chiffre, tout à droite, garde sa valeur normale, c'est à dire 3.

  • Le second se multiplie une fois par la base, ce qui nous donne 2 x 10.

  • Le troisième se multiplie deux fois par la base : 8 x 10 x 10 (= 8 x 100).

  • Et le quatrième par trois fois la base, comme ça : 5 x 10 x 10 x 10 (= 5 x 1000).

  • On additionne le tout.

Comme vous le voyez, il y a un "décalage". Je veux dire par là qu'on va toujours garder le premier chiffre (en partant de la droite) tel qu'il est, on ne le multipliera pas. On ne commence à multiplier les chiffres qu'à partir du second. On multiplie donc le deuxième chiffre par 10, une seule fois. Le troisième, deux fois. Le quatrième trois fois, etc...
En résumé : Le nombre de fois que l'on multiplie par la base est inférieur de 1 à la place que le chiffre occupe(prenez le temps de la digérer celle-là !). Par exemple, si un chiffre est à la trentième place, on le multipliera 29 fois par la base.

Ensuite, après avoir multiplié les chiffres comme il faut, on additionne le tout.

Pour notre exemple, ça nous donne :

5823 = (5 x 10 x 10 x 10) + (8 x 10 x 10) + (2 x 10) + 3
5823 = (5 x 1000) + (8 x 100) + (2 x 10) + 3
5823 = 5000 + 800 + 20 + 3

En fait, lorsque j'écris 5823, c'est comme si j'écrivais 5000 + 800 + 20 + 3. C'est seulement une façon plus simple de l'écrire.

Ce système de numération se nomme le système décimal, car il utilise 10 comme base.

Voilà pour terminer un schéma qui illustre cet exemple :

J'ai pris un exemple à quatre chiffres, mais je suppose qu'il est inutile de préciser que c'est exactement la même chose pour n'importe quel nombre.

Voici un résumé à retenir à tout prix:

  • Chaque système a une base, et fonctionne selon elle.

  • La base sert à multiplier correctement les chiffres, selon leur place, afin de connaître la valeur du nombre.

  • Le premier chiffre tout à droite garde sa valeur.

  • Le second se multiplie par la base, une fois.

  • Le troisième se multiplie deux fois par la base.

  • Le quatrième se multiplie trois fois par la base et ainsi de suite...

  • On additionne les valeurs trouvées ensemble.

  • Et on a le résultat ! :)

Tu avais parlé de plusieurs systèmes de numération, je n'en vois qu'un seul, là...

Effectivement, nous n'en n’avons vu qu'un seul. Mais grâce à lui, nous avons appris à utiliser la règle qui sera la même dans les autres systèmes ; l'unique différence sera la base utilisée.
Retenez donc bien comment on utilise la base dans le système décimal, on fera la même chose dans les autres systèmes, mais avec des bases différentes.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
La notation positionnelle Le système binaire

Le système binaire

Le système décimal Le système hexadécimal

Le système binaire

C'est parti pour un nouveau système !
Maintenant que vous savez utiliser la base, tout sera simple comme bonjour.

Comme vous le savez, nous allons simplement changer de base : Nous passons maintenant en base 2 (au lieu de base 10).

Le nom de ce système est le système binaire. Le mot binaire vient de bini, qui veut dire deux en latin.

Puisqu'on est en base 2, alors il n'y aura que deux chiffres ?

Eh oui, c'est exactement ça. Le 0 et le 1.

Nous allons maintenant faire la même chose qu'avec le système décimal. Prenons le nombre 1011 comme exemple :

Rappel : Dans tout les calculs que l'on fait avec les bases, on commence toujours par le chiffre le plus à droite. On continue ensuite de la droite vers la gauche.

  • Le chiffre tout à droite garde sa valeur, 1.

  • Le second chiffre est multiplié par la base une seule fois : 1 x 2 = 2

  • Le troisième chiffre est multiplié par la base deux fois : 0 x 2 x 2 = 0 x 4 = 0

  • Le quatrième chiffre est multiplié par la base trois fois : 1 x 2 x 2 x 2 = 1 x 8 = 8

  • On additionne le tout : 1 + 2 + 0 + 8 = 11

Voici un schéma pour résumer :

Astuce : Le système binaire n'est pas très simple à lire. Tous ces 0 et 1 collés ne simplifient pas les choses. Imaginez par exemple le nombre 1000110110. On a déjà vu plus clair, n'est-ce pas ?
Nous allons donc dorénavant placer des espaces tout les quatre bits : Au lieu de 1000110110, on écrira 10 0011 0110. C'est mieux, non ? ;)

Maintenant pour terminer, ouvrons le zDico pour apprendre quelques notions importantes du système binaire.

zDico

Rappel : Bit : Unité du binaire. Chaque 1 ou 0 est appelé un bit. Par exemple, 1101 contient 4 bits. Ce mot vient de l'anglais.

Le poids : Il existe une notion de poids dans les bits. Le bit qui est tout à droite, est le bit de poids le plus faible. On dit qu'il est de poids 0. Ensuite vient le bit de poids 1, le second en partant de la droite. Ainsi de suite, jusqu'au bit de poids le plus fort, complètement à gauche.

Poids du bit = nombre de fois que l'on multiplie par la base

A partir de maintenant, j'utiliserais le poids pour indiquer de quel bit je parle. Lorsque je dirais le bit de poids 3, je ferais référence au quatrième bit en partant de la droite. Par commodité, j'écrirais plus souvent le bit 3.

MSB : Le Most Significant Bit ou MSB est le bit de poids le plus fort du nombre (tout à gauche).

LSB : Le Least Significant Bit ou LSB est le bit de poids le plus faible du nombre (tout à droite). Il est de poids 0.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Le système décimal Le système hexadécimal

Le système hexadécimal

Le système binaire Une petite pause, les outils

Le système hexadécimal

Allez ! Un dernier système et je vous laisse tranquilles avec les maths ! :)

Nous allons maintenant aborder un système dont le seul nom en a fait fuir plus d'un, je parle du système hexadécimal.

Décomposons d'abord son nom : hexa veut dire 6 et décimal 10. Ce qui nous donne 16.

Bon, les plus attentifs l'auront compris, ce système utilise 16 comme base.

Il y aura donc 16 chiffres ! On va les trouver où ? J'en connais que dix, moi !

Eh oui, bonne question. Il nous manque des chiffres pour ce système. Six pour être précis. Pour résoudre ce problème, les mathématiciens ont eu une idée : utiliser des lettres. Voici donc la liste des chiffres du système hexadécimal, en ordre :

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F.

Comptez, ça fait seize !

Le A vaut donc 10 (en décimal), le B vaut 11 ; le C, 12...jusqu'au F qui vaut 15. ;)

Comme d'habitude, nous allons prendre un exemple et le convertir ensemble en décimal : C8FB.

  • On laisse le B comme il est. Ça nous donne donc 11.

  • On a vu plus tôt que le F valait 15. On multiplie donc 15 par la base (qui est 16) : 15 x 16 = 240

  • On multiplie le 8 deux fois par la base : 8 x 16 x 16 = 2048

  • Le C vaut 12. On multiplie donc 12 trois fois par la base : 12 x 16 x 16 x 16 = 49 152

  • On additionne le tout : 49 152 + 2048 + 240 + 11 = 51451

Non, nous n'en n'avons pas encore terminé avec les systèmes de numération. On verra plus tard d'autres notions (convertir d'un système à l'autre, les opérations...). Mais pour l'instant, je vous laisse cogiter sur ça. :-°

Si vous voulez déjà convertir d'un système à l'autre, ou faire des opérations dans des systèmes autres que le système décimal, vous pouvez pour l'instant utiliser la calculatrice de Windows en mode Programmeur.
Pour cela, ouvrez la calculatrice et allez dans Affichage > Programmeur. Vous pouvez convertir les nombres d'un système à l'autre en utilisant les boutons sur le côté gauche.

Dans le prochain chapitre nous allons faire une petite pause en découvrant les outils nécessaires à la programmation en assembleur.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Le système binaire Une petite pause, les outils

Une petite pause, les outils

Le système hexadécimal Découvrons les outils

Non, nous n'en n'avons pas encore fini avec la théorie. :diable: Vous avez encore deux-trois trucs à apprendre avant de pouvoir commencer la programmation en assembleur proprement dite. Mais ne vous inquiétez pas, c'est bientôt terminé.
J'aurais pu vous mettre encore un chapitre théorique, mais dans ma grande gentillesse :-° , j'ai préféré faire une pause avec un chapitre dans lequel le cerveau n'est pas indispensable.

Qu'allons nous donc faire dans ce chapitre ?

Eh bien nous allons tout simplement télécharger et installer tous ce dont nous avons besoin pour programmer en assembleur.

Découvrons les outils

Une petite pause, les outils Téléchargements

Découvrons les outils

Donc nous y voici : De quels outils avons nous besoin pour programmer en assembleur ?

Si vous avez été attentifs, vous devriez pouvoir imaginer l'un d'entre eux.
D'ailleurs pour ceux qui n'y parviennent pas, je conseille vivement un retour à la case départ (nan, je plaisante).

Voici la réponse : l'Assembleur.

Vous vous souvenez j'espère de ce programme qui traduit ("assemble", pour les pros) les instructions écrites en langage assembleur en langage machine.

Voilà à nouveau le schéma, pour ceux qui auraient un trou :

Vous vous rappelerez aussi qu'il existe plusieurs langages assembleur, et autant d'Assembleurs. C'est très simple, chaque Assembleur a son langage assembleur. Il pourra traduire uniquement ce langage là en langage machine.

Comme je l'ai dit dans le chapitre 1, nous allons utiliser MASM comme Assembleur.

Voyons maintenant les autres outils indispensables :

  • Un éditeur de texte : C'est le programme dans lequel vous allez écrire votre code en langage assembleur avant l'assemblage. On pourrait utiliser n'importe quel programme qui permet l'affichage du texte (comme "bloc-note"), mais certains éditeurs de texte colorent le code de façon à le rendre plus clair, et croyez-moi ou pas, c'est très utile !

  • Un débbuger : C'est un programme qui va lire votre code et vous faire part des erreurs qu'il contient (parfois un peu brutalement, je l'avoue...). Si vous ne faites aucune erreur, vous pourrez vous en passer, mais je peux vous assurer que c'est impossible...

  • Autres : Il y en a encore d'autres (comme l’Éditeur de liens, par exemple), mais vous n'avez pas besoin de les connaître (en tout cas pas pour l'instant).

Quoi ?! On va devoir acheter tous ces programmes ?

Bien sûr que non, ils sont gratuits. Nous allons les télécharger en quelques minutes seulement.
De plus, nous n'aurons pas besoin de les télécharger séparement, car il existe des logiciels "tout en 1", qui regroupent tous les outils indispensables à la programmation en assembleur. On les appelle des IDE (Environnements de Développement Intégrés). La seule chose qui ne sera pas incluse dans l'IDE est l'Assembleur, MASM, en l'occurence.

Passons maintenant aux téléchargements proprement dits.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Une petite pause, les outils Téléchargements

Téléchargements

Découvrons les outils Installation

Téléchargements

Pour résumer, nous allons devoir télécharger :

  • l'IDE qui contient pratiquement tout ce dont nous avons besoin.

  • L'Assembleur, la seule chose qui n'est pas incluse dans l'IDE.

Il existe plusieurs IDE téléchargeables gratuitement sur le web.

Je vais vous indiquer la marche à suivre pour télécharger puis installer l'un d'entre eux : RadAsm.

Pour cela rendez vous sur cette page.

Voici une capture de la page. Les trois liens à télécharger sont mis en évidence (ça, pour de l'évidence...).
Les deux premiers sont obligatoires. Le dernier est un pack de traduction pour traduire l'IDE en français (ou en ukrainien, si ça vous chante :-° ).

Maintenant téléchargez MASM en allant ici. Cliquez ensuite sur l'un des liens pour lancer le téléchargement ("US Site 1", par exemple).

Mettez tous les téléchargements dans le même dossier, ça devrait vous faciliter les choses. Nommez le "Assembleur", par exemple (en fait vous pouvez le nommer comme vous voulez, mais gare à vous si vous le laissez se nommer "Nouveau dossier"... :diable: ).

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Découvrons les outils Installation

Installation

Téléchargements Configuration de RadAsm

Installation

Avant de passer à la suite, vérifiez que vous avez bien les fichiers ci-dessous :

Si vous avez tous les fichiers nécessaires, vous pouvez passer à l'installation.
Je vous préviens, ça ne sera pas que du "next>next>next>OK", si vous voyez ce que je veux dire. Mais si vous suivez mes instructions à la lettre vous réussirez sans problème une parfaite installation.

Alors, on commence ?

Tout d'abord faisons quelques manipulations.
Ouvrez le dossier Assembly, vous devrez voir une liste de fichiers ressemblant à celle-ci apparaître :

Copiez tous les fichiers à part celui nommé "Addins". Collez-les dans le dossier appelé "RadASM" (Vous le trouverez dans le dossier parent que nous avons nommé "Assembleur").

Maintenant, ouvrez le dossier "RadLNG". Il est censé contenir un dossier "Languages". Placez ce dernier dans le dossier "RadASM".

Il ne nous reste plus qu'à installer MASM. Ne vous inquiétez pas, ce sera très simple, je vais vous inonder de captures d'écran. :D

Ouvrez le dossier "m32v10r" et cliquez sur le programme d'installation "install".
Voici ce qui apparaîtra :

Cliquez sur le disque C, puis sur "start".

Ensuite, vous aurez cela :

Cliquez sur "OK".

Puis dans cette fenêtre, cliquez sur "Oui" :

Dans celle-ci, cliquez sur "OK" :

Puis sur "Extract" :

Patientez une minute :

Encore un "OK" :

Vous aurez maintenant une fenêtre toute noire qui va commencer à faire des trucs bizarres : Pas de panique, rien ne va exploser. :p Et pour vous prouver que c'est normal, regardez, moi aussi j'ai eu la même... :-°

Patientez jusqu'à que ça se termine. A la fin, vous aurez une fenêtre comme celle-ci :

Ça veut dire en français : "L'installation s'est bien passée, appuyez sur n'importe quelle touche du clavier pour continuer". Oui je sais, moi aussi, quand j'ai vu ça, j'ai été rassuré... :D

Vous aurez ensuite un truc de ce genre :

Pour changer des OK, on va mettre "Yes".

Ce programme va alors s'ouvrir :

Il ne nous servira pas. Fermez-le en cliquant sur la croix en haut.

Et voilà ! C'est terminé ! :soleil:

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Téléchargements Configuration de RadAsm

Configuration de RadAsm

Installation Comment ça marche (1/3)

Configuration de RadAsm

Il ne nous reste plus qu'à configurer RadASM. Vous allez voir, c'est très simple.

Dans notre dossier "Assembleur", ouvrez le dossier "RadASM". Cliquez sur l’exécutable du même nom : vous le verrez sur fond gris dans la capture d'écran.

Voilà, vous avez ouvert RadAsm pour la première fois. J'espère que vous aimez, car c'est là qu'on va passer le plus clair de notre temps. :-°

Commençons la configuration.

Tout d'abord, traduisons RadASM de la langue de Shakespeare à celle de Molière. Pour cela, allez dans l'onglet "Option" et cliquez sur langue. Choisissez le français et cliquez sur Valider. (Bon, je ne vous mets pas de capture d'écran, mais ça devrait aller sans...)

Maintenant, passons à des choses plus sérieuses. Il faut indiquer à RadASM quel Assembleur nous allons utiliser ainsi que l'endroit où il se trouve.
Toujours dans l'onglet "Options", cliquez cette fois sur "Langages de programmation".
Vous verrez cette fenêtre apparaître.

A moins que vous vouliez utiliser RadASM pour coder en HTML, supprimez ce dernier en cliquant sur le bouton "Supprimer".
Cliquez maintenant sur le bouton avec les points de suspensions.
Cette fenêtre apparaîtra :

Choisissez "masm" (en bleu dans ma capture d'ecran), puis cliquez sur "Ouvrir".
Puis cliquez sur "Ajouter" et pour terminer, sur "Valider".

C'est bien beau tout ça, on fait ce que tu dis, mais on comprend pas à quoi ça sert...

En verité, ce n'est pas vraiment utile de le savoir, ce n'est que de la configuration. Mais pour faire court, nous venons d'indiquer à RadASM quel Assembleur nous allons utiliser.
Par contre, nous ne lui avons pas encore dit où se trouve cet Assembleur dans le disque dur.
C'est ce que nous allons faire maintenant.

Dans l'onglet "Option", cliquez sur "Emplacements par défaut", une fenêtre ressemblant à celle-ci va apparaître :

En fait, la vôtre sera un peu différente. Vous n'aurez pas les mêmes textes que moi dans les cases. Eh bien, tout ce que vous avez à faire, c'est de recopier ma fenêtre, puis cliquez sur "valider" (laissez la seconde case vide, comme dans la capture d'écran).

Voilà, un nouveau chapitre de terminé.

Profitez bien de votre pause, car au prochain chapitre on repart à l'assaut de la théorie...

Je vous rassure, ça sera quand même plus sympa que les systèmes de numération. :D

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Installation Comment ça marche (1/3)

Comment ça marche (1/3)

Configuration de RadAsm LA question

Oui, encore un chapitre théorique, désolé. :euh: Mais je vous rassure, celui-ci sera plus intéressant que les autres :
Nous allons plonger au coeur de l'ordinateur pour comprendre comment il fonctionne. En effet, il est indispensable de comprendre comment marche la machine pour être capable de programmer en assembleur.

Bien entendu, comprendre le fonctionnement de l'ordinateur dans ses moindres détails prend en réalité énormément de temps. Je vais donc vous présenter une version simplifiée, qui sera amplement suffisante pour le moment.
Au fur et à mesure du tuto, lorsque le besoin se fera sentir, j'ajouterai éventuellement des informations.

Bonne chance et ne vous perdez pas en chemin ! :D

LA question

Comment ça marche (1/3) Avant de commencer

LA question

Tout d'abord je vais commencer par poser LA question. Cette question, j'imagine que beaucoup d'entre vous se la sont déjà posée :

Comment un objet uniquement composé de fer et de plastique peut-il "réfléchir" ? o_O

En fait, ceci équivaut à demander tout simplement : "Comment marche un ordinateur ?".

La réponse complète à cette question n'est pas simple, vous vous en doutez. Elle pourrait en réalité remplir plusieurs livres.

Dans ce tuto, je vais simplement vous présenter les bases, afin que vous ayez une vue générale du ventre de la bête.

Croyez moi, ça fait déjà beaucoup...

Je vais commencer par énoncer la liste des composants principaux d'un ordinateur. J'ajouterai à chacun d'eux une courte description. Ensuite, nous reprendrons les plus importants, pour les étudier plus en profondeur :

Cette liste ne cite pas tous les composants de l'ordinateur. Il y en existe d'autres. Ceux-ci n'étant pas utiles pour le moment, je ne vous embêterai pas avec tout de suite.

Le processeur aussi appelé CPU : C'est, on peut dire, le composant principal. C'est lui qui "réfléchit". Bon, je n'en dis pas plus pour le moment à son sujet, mais le prochain chapitre lui est réservé ; nous y découvrirons notamment qu'il est lui-même composé de plusieurs parties.

La mémoire : Pour fonctionner, l'ordinateur a besoin de stocker des données. Il possède deux endroits réservés à cet effet :

  • Le disque dur : C'est là bas que sont stockées les données dont l'ordinateur n'a pas besoin de façon constante. Tous vos fichiers (comme vos musiques par exemple) sont enregistrés dans le disque dur.
    Mais le disque dur a un inconvénient, il est lent. Je veux dire par là, que l'ordinateur prend beaucoup de temps pour accéder aux données qui y sont enregistrées. Un exemple : Si vous cliquez sur une musique vous verrez que l'ordinateur prend un certain temps pour la mettre en marche. La musique étant stockée dans le disque dur, l'ordinateur prend du temps à l'atteindre.
    D'un autre coté, le disque dur a un avantage, il ne coûte pas cher. C'est pour cette raison que les ordinateurs ont des disques durs de très grande taille.

  • La mémoire vive ou RAM pour les intimes : Là bas sont stockées les données dont l'ordinateur a besoin constamment. Principalement, les données des programmes en cours d’exécution sont stockées dans cette mémoire (par exemple, la vie d'un personnage, dans un jeu).
    La RAM est tout le contraire du disque dur : elle est rapide, mais coûte très cher. C'est justement pour cette raison que les données dont l'ordinateur a besoin constamment y sont stockées, étant donné qu'il doit y accéder très rapidement. (Vous vous imaginez si l'ordinateur devait s’arrêter plusieurs secondes au milieu du jeu, pour savoir combien de vie a le personnage ?) :colere:
    Cependant, à cause de son coût, l'ordinateur ne possède pas une très grande mémoire vive.

Lorsqu'un programme n'est pas en cours d’exécution, il est stocké sur le disque dur. Lorsque vous le lancez, l'ordinateur enregistre toutes les données du programme du disque dur vers la mémoire vive. Cela lui permet d'y avoir accès plus rapidement (le programme est donc plus rapide).

Dans ce tuto nous allons nous concentrer sur le fonctionnement de la mémoire vive, puisque c'est elle qui est utilisée pour les programmes en cours. Il n'y a pas d'intêret à expliquer le fonctionnement du disque dur.
Dorénavant, lorsque je parlerai de la mémoire, sans préciser laquelle, je ferai toujours référence à la mémoire vive.

Les bus : Non, pas les transports en commun, mais ça y ressemble... Eh oui, les bus servent à transporter les données d'un composant de l'ordinateur à un autre. (Par exemple, du processeur vers la mémoire et vice versa.)

Maintenant que les présentations sont faites, passons à quelque chose de plus sérieux...

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Comment ça marche (1/3) Avant de commencer

Avant de commencer

LA question La mémoire en détails

Avant de commencer

Avant que l'on commence à analyser chacun des composants, apprenons à donner un nom à ce que nous allons faire : Nous allons étudier l'architecture de l'ordinateur.

zDico

L'architecture est la façon dont l'ordinateur est construit à l'intérieur : Ses composants, la façon dont ils sont connectés les uns aux autres, etc. Bref, toute l'organisation interne de l'ordinateur.
Retenez bien cette définition, elle est cruciale pour la suite.

L'architecture de l'ordinateur est ce que nous allons étudier durant ce chapitre et les deux suivants. D'ailleurs, à la place du titre du chapitre "Comment ça marche", j'aurais pû mettre "Architecture de l'ordinateur", mais ça aurait fait fuir pas mal de monde... ^^

Apprenons en maintenant un peu plus à ce sujet :

Il existe dans le monde de la machine deux types d'architectures (entendez donc deux façons d'organiser les composants internes) : L'architecture von Neumann et l'architecture Harvard.

L'architecture influe bien entendu sur les capacités. En termes de performance, on ne peut pas vraiment dire que l'une est meilleure que l'autre, chacune possédant des atouts et des défauts que l'autre n'a pas.
En fait, chacune est plus adaptée à un type d'appareil. (Car bien entendu, les ordinateurs ne sont pas les seuls à posséder un processeur, une mémoire, des bus, etc ; d'autres appareils en ont aussi.)

Quoi qu'il en soit, la plupart des ordinateurs sont construits selon l'architecture von Neumann.

Voici maintenant une règle importante à retenir :

Un programme écrit pour une architecture ne fonctionnera pas sur une autre.

C'est à dire que vous ne pourrez pas faire marcher un programme écrit pour une architecture Harvard sur un ordinateur construit selon la von Neumann, et vice versa.
En langage de pro on dit que l'architecture Harvard et l'architecture von Neumann ne sont pas compatibles.

Puisque notre but est d'apprendre à écrire des programmes pour ordinateur, c'est l'architecture von Neumann que nous allons étudier.
Tout ce que je dirai ici concernera donc uniquement l'architecture von Neumann.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
LA question La mémoire en détails

La mémoire en détails

Avant de commencer Encore un peu de binaire

La mémoire en détails

La liste de composants citée précédement n'était qu'un avant-goût de ce qui vous attend. Vous avez encore beaucoup à apprendre au sujet des ces composants. Mais avant cela, nous allons nous intéresser à un problème capital.

Vous vous souvenez que l'objectif de ce chapitre est d'expliquer la façon dont l'ordinateur réfléchit. Penchons nous donc attentivement sur cette question.

Avant tout, il faut savoir que toute intelligence, afin de pouvoir réfléchir, doit obligatoirement posséder une chose : La mémoire.

Prenons un exemple. Imaginez que je vous demande de calculer 8 + 5. Avez vous la moindre chance de me répondre si vous êtes incapables de retenir les nombres que je vous demande d'additionner ? Bien sûr que non... Vous devez savoir quels sont les nombres afin de pouvoir calculer. Il vous faudra aussi retenir que l'opération demandée est l'addition. Sans cela, vous ne saurez que faire de ces deux nombres. ;)

Pour l'ordinateur c'est exactement la même chose. Pour que le processeur puisse faire le calcul démandé, il faut que les informations nécessaires aient étés enregistrées au préalable dans la mémoire.

Mais on fait comment pour enregistrer des informations dans un ordinateur, après tout c'est pas humain ?

Réponse : Nous allons utiliser la seule chose dont nous disposons dans un appareil électrique : De l'électricité.

Quoi ?! On peut stocker des données avec de l'électricité ?

Eh oui, c'est exactement ça ! On peut enregistrer énormement de données en utilisant l'électricité. Pour que vous compreniez cela, je vais vous raconter une petite histoire... (Ce sera sans doute l'histoire la plus débile que vous aurez entendu cette année, mais peut-être l'une des plus utile).

Il était une fois, un pirate qui s'appelait Barbe-verte. Il parcourait les mers avec son équipage. Lorsqu'il rencontrait une île, il la pillait de tout ses biens. :pirate:

Mais Barbe-verte avait un sérieux problème : il était sans arrêt dans la crainte que les soldats de la Marine ne l’attrapent. A cause de cela, il ne participait jamais aux attaques. Il envoyait son équipage piller les îles sans lui, laissant à son second la direction des opérations. Lui-même restait à bonne distance de l'île dans son bateau, prêt à s'enfuir en cas de pépin.

Mais comment savoir si les choses tournaient mal, tout en restant si loin de l'île ?

Barbe-verte était peu valeureux mais très futé. Il inventa un moyen pour savoir si la Marine était présente sur l'île, tout en restant en sécurité sur son bateau : Lorsqu'il n'y avait aucun danger, son second allumait une lampe. Quand Barbe-verte la voyait briller, il savait qu'il pouvait rejoindre son équipage en toute sécurité. Dans le cas contraire, si l'équipage se faisait capturer, la lampe restait éteinte et Barbe-verte savait qu'il ne lui restait plus qu'à lever l'ancre et s'enfuir aussi vite que possible. :ninja:

Il n'y a rien à dire, Barbe-verte a beau être un misérable poltron, son idée était très ingénieuse. Plusieurs années heureuses (et sanglantes) passèrent ainsi.

Un jour, Barbe-verte voulut en outre être prévenu à l'avance lorsqu'un trésor était découvert sur l'île. Il donna alors à son second une autre lampe à allumer en cas de trésor trouvé. Pour ne pas confondre les deux signaux, il décida que la lampe de droite serait celle qui indiquerait la présence de la marine et que celle de gauche indiquerait qu'un trésor avait été trouvé.

Barbe-verte venait d'inventer un système parfait. Il lui permettait de recevoir à distance des informations de son second. Ce dernier n'avait qu'à "enregistrer" la bonne information dans les lampes.

Pour chaque information, Barbe-verte devait donner à son second une lampe supplémentaire.

En réalité, le système de Barbe-verte est plus puissant que vous ne l'imaginez. Il peut aussi servir à transmettre des nombres (par exemple, le nombre de trésors trouvés...ou de soldats abattus). Comment ? Eh bien en utilisant le système binaire. Eh oui, je vous avais prévenus qu'on allait le revoir celui-là...

Euh...quel rapport entre le système binaire et Barbe-verte ? :euh:

En fait si vous réfléchissez deux secondes vous verrez que c'est exactement la même chose. Chaque lampe de Barbe-verte est comme un chiffre en binaire. Une lampe peut être soit allumée soit éteinte. De la même façon, un chiffre binaire peut être soit 1 soit 0. Une lampe allumée signifiera 1, et une éteinte 0.

Par exemple, si l'équipage trouve 5 trésors, le second allumera la première lampe tout à droite, il laissera la seconde éteinte et allumera la troisième. Cela équivaut a écrire 101 en binaire, qui est égal à 5 en décimal.

Voici un schéma pour illustrer ceci :

Les lampes allumées sont en jaune, et la lampe éteinte en noir.

Bon, maintenant que le rapport entre les lampes et le binaire est clair, approfondissons un peu...

Dans le cas où Barbe-verte donnerait à son second uniquement quatre lampes, ce dernier ne pourra pas lui indiquer un nombre supérieur à 15, puisqu'en binaire, avec quatre bits, on ne peut pas dépasser 15 (= 1111), le nombre 16 nécessitant l'ajout d'un cinquième bit : 1 0000.
S'il lui donne huit lampes, il ne pourra pas dépasser 255 (= 1111 1111) et pour transmettre 256, il lui faudra une neuvième lampe.

Elle est belle ton histoire, mais quel rapport avec l'informatique ?

Le rapport ? C'est tout simple : La mémoire de l'ordinateur fonctionne exactement selon le système de Barbe-verte.
La seule différence est que nous allons remplacer les lampes par de minuscules composants électriques qu'on appelle des bascules (du voc à retenir). Comme les lampes, les bascules ont deux modes : chargée ou non chargée.

Une bascule chargée d'électricité équivaut à une lampe allumée (= 1). Une bascule non chargée est comme une lampe éteinte (= 0).

Lorsqu'il veut enregistrer des données, l'ordinateur fait comme le second de Barbe-verte, il utilise le système binaire. Mais au lieu d'allumer des lampes, il charge les bascules d'électricité. Au final, cela revient au même... La seule différence étant que l'ordinateur peut charger des milliers de bascules en un instant, et donc enregistrer énormément d'informations très rapidement. Par contre, le second de Barbe-verte, pour allumer des milliers de lampes... :lol:

Résumons avec un exemple. Imaginons qu'au cours d'un programme on demande à l'ordinateur d'enregistrer une donnée. Par exemple, la vie d'un personnage (oui je sais, je prends toujours les mêmes exemples).
Eh bien il va charger les bascules en suivant le système binaire : Si la vie est de 40 (= 10 1000 en binaire), il utilisera 6 bascules, car en système binaire, on a besoin de 6 bits pour écrire ce nombre. Il chargera la quatrième et la sixième (en partant de la droite), et laissera les autres non chargées. Exactement comme dans le nombre binaire : 10 1000.

Voici un schéma :

Dans ce schéma, vous pouvez voir clairement l'enregistrement des données (ici le nombre 40) en chargeant les bascules selon le système binaire.

Mais alors on peut enregistrer que des nombres ?

Bien sûr que non ! On peut enregistrer n'importe quel type de données : Du texte, des couleurs, des sons...
Mais tous seront codés sous forme de nombres binaires. Comment ? Nous en parlerons plus tard. Pour l'instant, retenez seulement que tout, je dis bien tout, ce qui est stocké dans la mémoire de l'ordinateur, l'est sous forme de nombres binaires, et est enregistré au moyen de bascules.

Vous vous souvenez que dans le premier chapitre je vous avais dit que les instructions qui composent un programme étaient écrites en binaire. Eh bien vous comprenez maintenant pourquoi. Etant stockées en mémoire, sur les bascules, elles ne peuvent l'être autrement que sous forme binaire. Comme toutes les autres données d'ailleurs... ;)

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Avant de commencer Encore un peu de binaire

Encore un peu de binaire

La mémoire en détails Une mémoire bien organisée

Encore un peu de binaire

Prenons une petite pause dans l'explication du fonctionnement de la mémoire, pour découvrir quelques mots importants utilisés dans le système binaire.

Car maintenant, vous commencez sûrement à comprendre l'importance de ce système pour la compréhension de la mémoire.

Ouvrons donc le zDico :

zDico

Rappel : Bit : Unité du binaire. Chaque 1 ou 0 est appelé un bit. Par exemple, 1101 contient 4 bits. Ce mot vient de l'anglais.

Quartet : C'est un ensemble de quatre bits. Par exemple, 1110 est un quartet. Avec un quartet, on ne peut pas dépasser le nombre 15 (= 1111 en binaire). Un quartet se dit en anglais "nibble".

Octet : C'est un ensemble de huit bits. Par exemple, 1010 0111 est un octet. Il y a deux quartets dans un octet (logique !). Avec un octet, on ne peut pas dépasser le nombre 255 (= 1111 1111 en binaire). Un octet se dit en anglais..."octet", tout simplement.

Ceux qui n'ont jamais entendu le mot "byte" peuvent directement passer au mot suivant. Les autres écoutez bien :

Ne surtout pas confondre "octet" et "byte".
Il existe une erreur courante de croire que le mot "byte" est la traduction anglaise du mot "octet". Il n'y a rien de plus faux !
Le mot octet signifie un ensemble de huit bits, en français et en anglais.
Le mot byte veut dire quelque chose de complètement différent. Nous verrons plus tard ce qu'il signifie réellement.

Mot : C'est un ensemble de seize bits. Par exemple, 1011 0100 1111 0011 est un mot. Un mot contient deux octets (= quatre quartets). Avec un mot, on ne peut pas dépasser le nombre 65 535 (= 1111 1111 1111 1111). Un mot se dit en anglais "word".

Double mot : C'est un ensemble de trente-deux bits. Par exemple, 1011 0100 1101 0011 1111 0011 0100 1111 est un double mot (à ne pas lire à voix haute, sous peine d'internement). Un double mot contient deux mots (= quatre octets = huit quartets). Avec un double mot, on ne peut pas dépasser le nombre 4 294 967 295 (= 1111 1111 1111 1111 1111 1111 1111 1111). Un double mot se dit en anglais "double word".

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
La mémoire en détails Une mémoire bien organisée

Une mémoire bien organisée

Encore un peu de binaire Le sens du mot byte

Une mémoire bien organisée

J'ai un problème. Si tout est vraiment enregistré dans les bascules, comment l'ordinateur fait-il pour ne pas tout mélanger ? Il y a des millions de bascules dans la mémoire, certaines contiennent des informations sur la vie du personnage, d'autres sur la couleur du chapeau du personnage, certaines sur la musique du jeu etc...
Comment l'ordinateur fait-il pour s'y retrouver et savoir quelles bascules contiennent quelles informations ?

Excellente question ! C'est un vrai Big-problème ! Heureusement qu'il y a ce qu'on appelle l'organisation de la mémoire. Eh oui, comme dans tout, pour que ça marche, il faut de l'ordre. Et dans la mémoire, l'ordre ne manque pas, tout est organisé de façon très précise.

Voici donc un schéma qui montre comment la mémoire est organisée :

Comme vous pouvez le voir, les bascules sont disposées par barrettes de huit. La mémoire est ainsi composée de millions de barrettes chacune comportant huit bascules.
Une barrette peut stocker un octet (puisqu'elle contient huit bascules).

Maintenant, il y a une chose hyper importante à savoir : Les barrettes sont numérotées. La première porte le numéro 0, la seconde le numéro 1 etc...
On appelle ce numéro une adresse.

C'est grace à l'adresse que l'ordinateur arrive à se repérer dans la mémoire. Lorsqu'il stocke une donnée, il la met à une adresse précise. Plus tard dans le programme, lorsque vous lui demandez d'utiliser cette donnée il va aller la chercher à l'adresse où il l'avait placée.

Ajoutons les adresses à notre schéma :

Voici maintenant une règle à retenir :

On ne peut stocker qu'une seule donnée par adresse. Retenez bien cela c'est important. Cela implique que même si j'ai une donnée qui prend moins d'un octet je suis quand même obligé d'utiliser toute la barrette.
Imaginez par exemple que je veuille stocker la vie d'un personnage, et que celle-ci soit de 18 (=1 0010 en binaire). Normalement, je n'ai besoin que de cinq bits. Pourtant, je dois quand même utiliser toute la mémoire qu'il y a à cette adresse, c'est à dire un octet (qui vaut huit bits, je rappelle pour les rêveurs). Je vais donc enregistrer 0001 0010 en mémoire. Les zéros placés avant le nombre ne modifient pas sa valeur (de la même façon qu'en décimal : 0018 = 18).

D'accord, je ne peux pas utiliser moins qu'un octet, mais est-ce-que je peux utiliser plus qu'un octet ? Parce qu'avec un seul octet, on ne peut pas dépasser 255. Alors je fais comment si je veux stocker une donnée plus grande que ça ?

Enfin un qui suit, ça fait plaisir ! :D

La réponse est toute simple : On va stocker notre donnée sur plusieurs barrettes consécutives. Prenons un exemple : Imaginons que je veuille stocker un mot à l'adresse numéro 2, disons le nombre binaire 1111 1111 0101 0101 (= 65 365 en décimal). Voici ce que je vais faire : Je vais dire à l'ordinateur : "Enregistre le mot 1111 1111 0101 0101 à l'adresse 2". L'ordinateur va automatiquement enregistrer 0101 0101 à l'adresse 2 et 1111 1111 à l'adresse 3. Ensuite, lorsque je demanderai à l'ordinateur "Donne-moi le mot stocké à l'adresse 2", il m'enverra automatiquement le contenu de l'adresse 2 et de l'adresse 3. Autrement dit, il m'enverra 1111 1111 0101 0101.
Par contre, si je demande l'octet situé à l'adresse 2, il m'enverra uniquement 0101 0101. De la même manière, si je demande l'octet placé à l'adresse 3, je recevrai 1111 1111.

Jetez un coup d'oeil à ce schéma qui résume bien la situation au sein de la mémoire, lorsqu'on stocke le nombre 1111 1111 0101 0101 à l'adresse 2 :

Limpide ! Enfin, pour moi en tous cas... :-°
Ne vous inquiétez pas si la fin (à partir de la dernière question) n'est pas super claire pour vous. C'est tout à fait normal. Relisez la plusieurs fois, et si ça ne vous dit toujours rien, ça viendra plus tard en pratiquant. ;)

zDico

A partir de maintenant on va appeler les choses par leur nom : Ce que j'appelle "barrette" depuis le début s'appelle en réalité un emplacement mémoire.
Retenez bien ce mot, on va le revoir et pas qu'une fois !

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Encore un peu de binaire Le sens du mot byte

Le sens du mot byte

Une mémoire bien organisée Comment ça marche (2/3)

Le sens du mot byte

Nous allons maintenant nous occuper d'expliquer un mot qui est pour beaucoup l'objet d'une confusion : le mot "byte". (Je ne parle pas du bit avec un 'i' qui se prononce comme il s'écrit, mais du byte avec un 'y' qui se lit 'baït'.)

Comme le quartet, l'octet, le mot et le double mot, le byte indique une quantité de bits. Mais contrairement à eux, ce n'est pas une quantité absolue, elle peut changer d'une machine à l'autre.

Car un byte signifie en fait : "Une quantité de bits égale au nombre de bits contenus dans un emplacement mémoire".

Combien de bits peut-on stocker dans un seul emplacement mémoire ? Ceux qui auront suivi jusque là me répondront : "8, tu l'as dit dans ce chapitre". Et ils auront tout à fait raison. ^^

Néanmoins, il faut savoir que je parlais de nos ordinateurs. Mais il existe d'autres machines dans lesquelles un emplacement mémoire ne fait pas 8 bits mais un nombre différent.

Par exemple, si l'on parle d'une machine dont les emplacements mémoire contiennent chacune 4 bascules (et qui peuvent donc stocker 4 bits), un byte mesurera 4 bits.
Mais quand je parlerai d'un ordinateur comme les nôtres, dont les emplacements mémoire ont 8 bascules, le byte équivaudra à 8 bits.

Dans nos ordinateurs donc, un byte vaut un octet, mais cela ne veut pas dire pour autant que ce sont des synonymes !

Sachez que j'utiliserai souvent le mot byte dans ce tuto pour indiquer la taille des données.

En parlant de taille de données, vous avez certainement déjà vu des abréviations comme Ko, Mb, Go, GB...j'en passe et des meilleurs. :-°
Vous devez sûrement savoir qu'elles indiquent la taille des données. Mais savez-vous ce qu'elles signifient précisément ? Eh bien figurez-vous que vous êtes à présent tout à fait capables de les comprendre :

Il y a trois unités représentées par des abréviations :

  1. L'octet, représenté par un 'o'. Exemple : 100 o = 100 octets.

  2. Le bit, représenté par un 'b' (minuscule). Exemple : 100 b = 100 bits.

  3. Le byte, représenté par un 'B' (majuscule). Exemple : 100 B = 100 bytes.

Il est possible d'ajouter des lettres avant ces trois unités pour les multiplier :

K : Lorsqu'on ajoute un K avant l'une des trois unités, on la multiplie par 1024. Ce K représente le mot kilo.
Exemples :

  • 1 kiloctet = 1 Ko = 1024 o = 1024 octets.

  • 1 kilobit = 1 Kb = 1024 b = 1024 bits.

  • 1 kilobyte = 1 KB = 1024 B = 1024 bytes.

M : Lorsqu'on ajoute un M, on multiplie l'unité par 1 048 576. Ce M représente le mot méga.
Exemple :

  • 1 mégaoctet = 1 Mo = 1024 Ko = 1 048 576 o.

  • 1 mégabit = 1 Mb = 1024 Kb = 1 048 576 b.

  • 1 mégabyte = 1 MB = 1024 KB = 1 048 576 B.

G : Lorsqu'on ajoute un G, on multiplie l'unité par 1 073 741 824. Ce G représente le mot giga.
Exemple :

  • 1 gigaoctet = 1 Go = 1024 Mo = 1 048 576 Ko = 1 073 741 824 o.

  • 1 gigabit = 1 Gb = 1024 Mb = 1 048 576 Kb = 1 073 741 824 b.

  • 1 gigabyte = 1 GB = 1024 MB = 1 048 576 KB = 1 073 741 824 B.

Vous connaissez maintenant les unités principales utilisées dans le monde informatique. Et à la différence de beaucoup de monde, vous savez exactement ce qu'elles signifient au niveau électronique.

Dans ce chapitre nous avons appris le fonctionnement de la mémoire, l'un des trois composants principaux de l'ordinateur.

Dans le prochain chapitre, nous parlerons du processeur.

Allez, encore un petit effort et nous pourrons passer à la pratique ! :)

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Une mémoire bien organisée Comment ça marche (2/3)

Comment ça marche (2/3)

Le sens du mot byte Un peu d'histoire

Maintenant que le fonctionnement de la mémoire est clair pour tout le monde, nous allons passer à l'explication des autres composants de l'ordinateur.

Vous vous souvenez j'espère qu'il y a trois composants principaux à comprendre avant de commencer l'assembleur proprement dit :

  • La mémoire

  • Le processeur

  • Les bus

Bon, la mémoire, on lui a déjà réglé son compte. Il ne nous reste maintenant "qu'à" étudier les deux autres. :-°

On va commencer par le processeur, accrochez-vous ça va barder !

Un peu d'histoire

Comment ça marche (2/3) Le 8086 & Cie

Un peu d'histoire

Au cas où la définition du processeur serait déjà ressortie par l'autre oreille, la voici à nouveau :

Le processeur ou CPU est le composant principal de l'ordinateur. C'est lui qui "réflechit". Le processeur est lui même composé de plusieurs parties. Nous découvrirons les plus importantes plus tard dans ce chapitre.

Bien que depuis le début du tuto je l'appelle "processeur", le mot correct est en réalité : Microprocesseur.

C'est quoi ça ? o_O

Pour bien comprendre le sens ce mot il faut remonter longtemps en arrière. A l'époque où l'ordinateur fut inventé.

Laissez-moi vous raconter cela...

Il y a bien longtemps, peu avant que la seconde guerre mondiale n'éclate, un homme nommé Alan Turing, imagina une théorie permettant la création d'une machine capable de calculer. Mais à ce moment, rien ne fut encore réellement fabriqué. On dût attendre jusqu'en pleine guerre, pour que le premier ordinateur voit le jour.
Il fut rapidement suivi par plusieurs autres, chacun plus performant que son prédécesseur.

Mais il y avait un problème : Les ordinateurs étaient énormes... Ceci était principalement dû à la taille du processeur. Les différentes parties de celui-ci étaient séparées les unes des autres et prenaient beaucoup de place.

C'est là que débarquent nos deux héros, Marcian Hoff et Frederico Faggin, tous deux ingénieurs dans la société Intel. Il réussirent le miraculeux exploit de miniaturiser les différentes parties du processeur et de les intégrer dans un seul composant.

Le processeur "Tout en un" était né. Mais les ingénieurs lui donnèrent un autre nom, il l'appelèrent : Le microprocesseur. (Micro - ou plutôt μικρός - signifiant "petit" en grec ancien.)

Depuis ce jour, tous les ordinateurs contiennent ce type de processeur miniaturisé. Néanmoins, par commodité, j'emploierai plus souvent le mot "processeur". Bien entendu, il est évident que je ferai en réalité référence au microprocesseur, les anciens processeurs n'étant plus du tout utilisés de nos jours.

Mais continuons l'histoire...

En 1971, le premier microprocesseur, répondant au doux nom de Intel 4004, fut commercialisé. Il fut suivi par d'autres processeurs plus puissants, mais affublés de noms tout aussi barbares. :p

1978 est une année à retenir. Cette année là, un problème qui déclencha un véritable bouleversement dans l'histoire du processeur apparut. Voici ce qui se passa : Intel créa un nouveau microprocesseur nommé 8086. L'informatique étant à cette époque en plein essor, énormément de programmes furent écrits pour ce processeur. Beaucoup plus que ce qui avait été écrit pour les précédents.

Et alors ? En quoi le fait que beaucoup de programmes furent créés pour ce processeur est-il un problème ?

Oups, sorry ! Il y a une chose que je ne vous ai pas dite au sujet des architectures :

Un programme écrit pour un ordinateur dont l'architecture est von Neumann, ne fonctionnera pas obligatoirement pour tous les ordinateurs possédant cette architecture.

Je m'explique : Rappelez-vous qu'il existe deux grands types d'architectures : l'architecture von Neumann et l'architecture Harvard. Toutes les machines existantes au monde suivent l'une ou l'autre. (Quoique ce soit en réalité un petit peu plus compliqué...mais peu importe.)
Donc chaque machine sera soit une von Neumann, soit une Harvard. Comme je l'ai dit dans le chapitre précédent, un programme écrit pour la von Neumann ne fonctionnera pas sur la Harvard, et vice-versa.
De notre coté, nous allons nous concentrer sur la von Neumann, puisque c'est selon elle que sont construits nos ordinateurs.

Si j'ai pris la peine de reprendre cela, ce n'est pas pour rien, vous vous en doutez. Vous allez maintenant découvrir quelque chose de nouveau à ce sujet : Les ordinateurs construits selon la von Neumann sont divisés en familles. Chaque famille possède des particularités dans son architecture qui font sa différence avec les autre familles.
Bien entendu, ces différences sont minimes par rapport à celles qui existent entre l'architecture Harvard et l'architecture von Neumann. Il n'empêche qu'un programme écrit pour l'une des familles ne fonctionnera pas sur un ordinateur appartenant à une autre famille. Oui, les différentes familles ne sont pas compatibles entre elles.

Vous allez très bientôt comprendre pourquoi je vous ai parlé de cela. ;)

Pour l'instant, reprenons où nous en étions...
Donc comme je le disais avant cette interruption, énormément de programmes furent créés pour le 8086. Tous avaient été écrits selon l'architecture de ce dernier. A cause de cela, Intel ne pouvait plus changer d'architecture dans ses nouveaux processeurs, car s'il le faisait, tous les programmes ne serviraient plus à rien et il faudrait tout réécrire.

Et alors ? Quel est le problème ? Ils n'avaient qu'à garder la même architecture !

Eh bien c'est ce qu'ils ont fait, finalement. Mais le problème est qu'il était difficile d'améliorer le 8086 sans modifier son architecture. A cause de cela, Intel dû mettre au point des techniques complexes afin de pouvoir améliorer les processeurs tout en conservant l'architecture du 8086.

Le 8086 devint depuis lors le père de tous les processeurs Intel, puisqu'ils suivent tous son architecture. C'est ainsi que naquit la famille x86. Tous les processeurs qu'Intel créa par la suite firent partie de cette famille qui suit l'architecture du 8086.

Bien entendu, quand je dis que les processeurs suivent le 8086, ce n'est pas qu'ils ne comportent pas d'améliorations (sinon à quoi cela servirait de créer de nouveaux processeurs...). Mais ces améliorations ne doivent jamais transgresser cette règle sine qua none :

Un nouveau processeur doit posséder toutes les fonctionnalités qui ont existé dans tous les processeurs précédents depuis le 8086.

Par exemple : Lorsque Intel créa le successeur du 8086, le 80186, il y inclut toutes les fonctionnalités du 8086, en plus des nouvelles fonctionnalités propres au 80186.
De la même façon, le 80286 qui vint juste après, contenait, à part ses nouvelles fonctionnalités, toutes celles du 8086 et du 80186. Et ainsi de suite. En fait, même les tout nouveaux processeurs d'Intel qui sortent actuellement, contiennent eux-aussi les fonctionnalités de tous les processeurs qui les ont précédés depuis le 8086 ! :waw:

Même dans le cas où l'on a créé une nouvelle fonctionnalité qui peut sans problème remplacer une ancienne, il faut toujours veiller à conserver aussi l'ancienne. Grâce à cela, un programme qui utilise l'ancienne fonctionnalité pourra toujours fonctionner sur les nouveaux processeurs.

En résumé, lorsqu'on crée un nouveau processeur, on ne peut qu'ajouter de nouvelles fonctionnalités en plus de celles du précédent, jamais en supprimer.

On appelle cela : La compatibilité ascendante. C'est à dire que les nouveaux processeurs doivent être compatibles avec les anciens, de façon à ce que les anciens programmes fonctionnent aussi sur les nouveaux processeurs.
C'est grâce à cela qu'il est possible même de nos jours utiliser d'anciens programmes sortis dans les années 80.

Reprenons l'histoire :

Tous les trois ou quatre ans environ, Intel mettait sur le marché un nouveau processeur qui comportait des améliorations par rapport au précédent.

Mais comme vous pouvez l'imaginer, Intel ne resta pas longtemps seul sur le marché. Il fut rapidement rejoint par d'autres constructeurs de microprocesseurs pour ordinateur.
Et bien que tous ces constructeurs suivirent eux aussi l'architecture von Neumann, certains présentèrent dans leur architecture quelques différences avec celle d'Intel.
À cause de cela, un programme écrit pour l'un de ces processeurs (différents de ceux d'Intel), ne fonctionne pas sur un processeur Intel ; et vice-versa.

Dès lors, les processeurs furent divisés en plusieurs familles.

Comme je l'ai dit plus haut, les membres d'une même famille suivent tous la même architecture de base et ne sont compatibles qu'entre eux.
C'est à dire qu'un programme écrit sur l'un des membres ne fonctionnera que sur les membres de sa famille, mais pas sur les membres des autres familles.

La famille dont on parle depuis le début est la plus grande, c'est celle d'Intel : La famille x86. Elle contient les processeurs dont l'architecture est basée sur celle du 8086 d'Intel. D'où le nom x86.

Cette famille contient tous les processeurs créés par Intel depuis le 8086, qui suivent tous, comme je l'ai dit, l'architecture de ce dernier.

Mais d'autres constructeurs à part Intel font aussi partie de cette famille. Ils suivent eux aussi l'architecture du 8086. En voici quelques-uns :

  • AMD

  • VIA

  • Cyrix

  • Rise Technology

Tous les processeurs de cette famille sont donc compatibles entre eux. C'est à dire qu'un programme écrit pour un processeur de Rise Technology, par exemple, fonctionnera sur un processeur AMD, et vice-versa.

Tu avais dit qu'il existait d'autres familles, tu peux nous dire lesquelles ?

Il y a par exemple la famille m68k de Motorola.
Mais en réalité, il n'y a aucun intérêt à savoir quelles sont les autres familles, tout simplement parce que MASM ne fonctionne qu'avec la famille x86.

Mais alors si mon architecture n'est pas x86, je ne pourrai pas suivre ce tuto ?

Non, en effet. Mais il n'y a pas vraiment de quoi s'inquiéter, puisque Intel et AMD dominent largement le marché des microprocesseurs. Et comme je l'ai dit plus haut, tout deux suivent l'architecture x86. Il y a donc très peu de chances pour que vous ayez un microprocesseur appartenant à une autre famille.

Si votre processeur n'est pas un x86, vous pourrez tout de même programmer en assembleur, bien entendu. Mais il vous faudra apprendre un autre assembleur que MASM.

En résumé :

  • Intel est le premier fabricant de microprocesseurs.

  • Depuis 1978, tous ses processeurs sont basés sur l'architecture du 8086.

  • D'autres constructeurs se sont aussi basés sur le 8086. Le plus connu étant AMD, principal concurrent d'Intel.

  • Les microprocesseurs basés sur l'architecture du 8086 forment la famille x86.

  • Il existe d'autres familles de microprocesseurs. La m68k de Motorola par exemple.

  • MASM ne fonctionne que sur la famille x86.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Comment ça marche (2/3) Le 8086 & Cie

Le 8086 & Cie

Un peu d'histoire Le cerveau de la bande

Le 8086 & Cie

Comme je l'ai dit plus haut, depuis le 8086, Intel mettait tous les trois-quatre ans environ un nouveau processeur de la famille x86 sur le marché.
Bien entendu, il apportait à chaque fois des améliorations. Nous étudierons une partie de ces améliorations plus tard.
Voici donc la liste des différents processeurs qu'Intel a créé depuis le 8086. Comme dit (et redit :p ), ces processeurs ne modifient pas l'architecture du 8086. Ils ajoutent seulement des fonctionnalités.

Je ne vais pas dépasser le premier Pentium, car après celui-ci les nouvelles fonctionnalités sont extrêmement complexes.

Processeur | Année de sortie
8086 | 1978
80186 | 1982
80286 | 1982
80386 | 1985
80486 | 1989
Pentium | 1993

Comme vous le voyez, Intel ajouta un 1 avant le 86 pour son second processeur de la famille x86. Ensuite vient le 80286, puis le 80386, etc. Tout ce qu'il y a de plus simple à retenir.
Bon, je vous dispense d'apprendre les dates... Mais retenez absolument ces noms, ils nous seront utiles plus tard, lorsque nous étudierons les améliorations qui ont été apportées.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Un peu d'histoire Le cerveau de la bande

Le cerveau de la bande

Le 8086 & Cie L'UAL

Le cerveau de la bande

Pour bien comprendre cette partie, je vous conseille de relire ce zDico, principalement les notions de poids et la définition du mot : "Mot". ^^

Vous vous souvenez que dans le chapitre précédent, nous avions vu la mémoire de l'ordinateur. Je vais maintenant expliquer la "réflexion" de l'ordinateur proprement dite.

Reprenons l'exemple du chapitre précédent. Lorsque vous demandez à l'ordinateur de calculer 8 + 5, comment fait-il pour savoir que la réponse est 13 ? Nous savons déjà comment il stocke le 8 et le 5 dans la mémoire, il ne nous reste plus qu'à savoir comment il fait pour les additionner.

Comme vous le savez, ceci est le travail du processeur. Nous allons donc étudier le fonctionnement du processeur afin de comprendre comment l'ordinateur fait pour "réfléchir". Mais le processeur étant un composant extrêmement complexe de l'ordinateur, je vais bien entendu simplifier les explications au strict minimum.

Le processeur est composé de deux éléments principaux :

  • L'UAL : L'Unité Arithmétique et Logique.

  • Les registres.

L'UAL, appelée en anglais ALU (Arithmetic and Logical Unit), est le coeur du processeur. C'est là-bas que la "réflexion" se passe. Mais patience, j'expliquerai son fonctionnement plus tard. Je vais d'abord vous expliquer ce que sont les registres, ensuite seulement nous passerons à l'UAL.

Les registres

Les registres sont des emplacements où l'on peut stocker des données.
Ils sont constitués de bascules identiques à celles contenues dans la mémoire. Mais contrairement à la mémoire qui est à l'extérieur du processeur, les registres sont placés à l'intérieur du processeur lui-même.

Mais quel intérêt y a-t-il à stocker des données dans le processeur ? Ne peut-on pas le faire dans la mémoire vive ?

Oui, mais les registres, étant placés dans le processeur lui-même, sont beaucoup plus rapide d’accès.

C'est pour cette raison que lorsque l'on programmera en assembleur, on utilisera très souvent les registres.

Alors si les registres sont plus rapides, pourquoi avons nous besoin de la mémoire ? On pourrait tout enregistrer dans les registres !

Non, non, c'est impossible. :( Tout simplement parce qu'il y a très peu de registres. Le processeur n'est pas assez vaste pour en contenir beaucoup, il y en a donc seulement quelques dizaines en tout. (Les plus grands d'entre eux ne peuvent pas stocker plus de 32 bits.)

On utilisera donc les registres pour stocker des données de petite taille dont l’accès doit être très rapide.

Les registres sont divisés en cinq groupes :

  • Les registres de travail

  • Les registres d'index

  • Les registres de segments

  • Le registre flags

  • Le registre ip

Ces deux derniers sont seuls dans leur groupe.

Contrairement aux emplacements mémoire qui sont désignés par des adresses, les registres ont chacun un nom ! :-°

Voici les principaux, 14 en tout :

  • Registres de travail : ax, bx, cx, dx.

  • Registres d'index : di, si, sp, bp.

  • Registres de segments : cs, ds, es, ss.

  • Le registre flags.

  • Le registre ip.

zDico

L'ensemble des registres se nomme le jeu de registres.

Expliquons maintenant la constitution des registres :
Chacun des registres est constitué de 16 bascules, et peut donc contenir 16 bits (2 octets). Regardez le schéma :

Ce schéma montre le registre ax, mais c'est exactement pareil pour les autres registres.

Regardez, on commence à compter à partir de 0. Le premier bit est donc le bit 0 et le seizième le bit 15. Faites bien attention à cela, c'est souvent une source d'erreurs pour les débutants...

Chaque registre peut contenir un nombre de 0 à 65 535.
Mais contrairement à la mémoire, vous ne pouvez pas mettre ce que vous voulez dans les registres. Chaque registre a en général une fonction particulière, mais nous les découvrirons plus tard, lorsque ce sera nécessaire.

Pour le moment, retenez simplement leurs noms, de façon à ce que vous ne me preniez pas pour un martien lorsque je parlerai par la suite de ax, ou d'un autre registre.
Plus tard, lorsque nous commencerons à programmer, je vous indiquerai leurs utilisations.

Avant de continuer, vous devez encore savoir une chose au sujet des registres de travail (ax, bx, cx et dx) : Ils sont divisés en deux parties.
Chacune des deux parties pèse un octet (= 8 bits, je vous rappelle).

Regardez ce schéma :

Comme vous le voyez, ax est composé de deux parties : ah et al. al est l'octet de poids faible, il commence au bit 0 et se termine au bit 7, inclus. ah est l'octet de poids fort, il va du bit 8 au bit 15.

Le 'h' dans 'ah' signifie "high" qui veut dire "haut" en anglais et le 'l' de 'al' signifie "low" qui veut dire "bas".
Nous avons donc l'octet "haut" (de poids fort) et l'octet "bas" (de poids faible).

Chacune des deux parties peut contenir une valeur de 0 à 255.
Grâce à cette séparation, on peut utiliser les deux parties individuellement. Il est donc possible de stocker une valeur dans ah et une autre dans al.
Mais bien entendu, modifier ah ou al change du même coup la valeur de ax, puisque ils font partie de ce dernier. ;)

Tout ceci s'applique aussi aux registres bx, cx et dx : bx est composé de bh et bl, cx de ch et cl et dx de dh et dl. (Moi aussi, la fin de cette phrase me fait bugger... :p )

Maintenant, il faut que vous sachiez que les registres cités plus haut sont ceux du 8086. Mais au fur et à mesure qu'Intel créait de nouveaux processeurs il apportait des améliorations aux registres.

Tous les registres du 8086 existent encore dans les processeurs actuels, car comme je l'ai dit plus haut, Intel ne supprime jamais rien de ce qu'il y a dans un processeur précédent. Les registres qui existaient dans le 8086 n'ont donc jamais été supprimés ni modifiés.

Il existe deux façon d'améliorer le jeu de registre, sans empêcher l'utilisation des anciens registres. Intel les a employées toutes les deux :

  • L'ajout de nouveaux registres : C'est assez simple à comprendre. Intel ajoute parfois de nouveaux registres qui n'existaient pas dans les processeurs précédents.
    Par exemple, dans le 80386, Intel ajouta deux registres de segments, gs et fs.
    Je ne vais pas plus parler de cette amélioration. Sachez simplement qu'Intel a ajouté au fur et à mesure des années un certain nombre de nouveaux registres.
    Néanmoins, pour nous, les 14 registres originaux contenus dans le 8086 seront largement suffisants.

    Concentrons-nous plutôt sur la seconde amélioration, qui elle, est cruciale :

  • L'extension d'anciens registres : C'est l'agrandissement de registres déjà existants. Regardez ce schéma :

Vous pouvez voir à droite notre registre ax. Mais vous remarquerez qu'il s'est rallongé sur la gauche pour atteindre le bit 31. On appelle ce registre étendu eax.
Bien entendu, ax existe toujours et il est toujours utilisable comme avant, mais il n'est maintenant qu'une partie du grand registre eax. Ce dernier pèse 32 bits et est donc composé de deux mots, le mot de poids faible étant ax.
En modifiant ax, vous modifiez automatiquement eax. De la même façon qu'en modifiant ah ou al vous modifiez ax...ce qui, vous l'aurez compris modifie par la même occasion eax, puisqu'il les contient.

Je récapitule le tout : eax fait 32 bits et possède ax comme mot de poids faible. Ce dernier est lui même composé de ah et al.

Dans les registres 32 bits, on peut enregistrer des valeurs de 0 à 4 294 967 295 !

Nous verrons plus tard à quel point l'extension des registres est pratique, pour l'instant contentez vous de comprendre ce que c'est.

Dans le 80386 les registres ax, bx, cx, dx, si, di, bp, sp, ip et le registre de flags ont été étendu en eax, ebx, ecx, edx, esi, edi, ebp, esp, eip et eflags. Il est le premier processeur d'Intel à posséder des registres étendus. Avant lui, les processeurs avaient des registres de 16 bits seulement.

Cette extension est l'une des plus importantes améliorations qu'Intel a apporté à ses processeurs. ;)

Le 'e' ajouté avant le nom des registres signifie "extended" qui veut dire "étendu" en anglais.

Voici un petit tableau qui résume les registres :

Groupe | Contient | Divisé en deux parties | Étendu à 32 bits
Registres de travail | ax, bx, cx, dx | Oui | Oui
Registres d'index | di, si, sp, bp | Non | Oui
Registres de segments | cs, ds, es, ss | Non | Non
Flags | Flags | Non | Oui
ip | ip | Non | Oui

Bon, eh bien je crois qu'on a fait le tour. Avant de passer à la suite, lisez bien ce résumé.

Pour terminer, un petit résumé :

  • Les registres sont des emplacements de mémoire situés dans le processeur.

  • Ils ont en général chacun une fonction particulière.

  • Les principaux sont ax, bx, cx, dx, si, di, bp, sp, cs, ds, es, ss, flags et ip.

  • Les quatre premiers sont divisés en deux parties, utilisables indifféremment l'une de l'autre.

  • La modification de ces parties entraîne automatiquement la modification du grand registre.

  • A partir du 80386, tous les registres à part les registres de segments ont subi une extension.

  • On ajoute un 'e' au début du nom d'un registre pour désigner la version étendue.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Le 8086 & Cie L'UAL

L'UAL

Le cerveau de la bande Comment ça marche (3/3)

L'UAL

Maintenant que nous en avons terminé avec les registres, passons au second composant du processeur : L'UAL.

Nous arrivons enfin au coeur du problème : Comment l'ordinateur réfléchit-il ?

Reprenons l'exemple cité plus haut : Je veux que l'ordinateur calcule 8 + 5.

Tout d'abord, il faut stocker les valeurs à additionner. Bon, ça, c'est déjà réglé. On va les stocker en binaire dans la mémoire en utilisant des bascules. Lorsqu'une bascule est chargée d'électricité elle signifie 1, et lorsqu'elle est non chargée elle vaut zéro.
D'ailleurs, on peut aussi les stocker dans les registres si on veut.
Bon, mais c'est pas tout, il faut maintenant les additionner. Eh bien pour cela, nous allons encore une fois utiliser l’électricité .

En réalité, retenez cela comme règle définitivement :

Tout ce qui paraît être une réflexion ou une mémorisation de données ou n'importe quelle autre chose que fait l'ordinateur, n'est en réalité qu'une utilisation très astucieuse de l’électricité. Dans l'ordinateur tout n'est qu’électricité.

Souvenez-vous bien de cette règle. Elle paraît peut-être évidente, mais vous allez voir qu'elle n'est pas toujours si simple à réaliser en pratique...

Bon, on a déjà fait un grand pas. Nous savons que pour calculer il faut utiliser de l’électricité...mais comment diable fait-on pour faire des opérations avec l’électricité ? o_O

Eh bien c'est très simple, nous allons utiliser de minuscules composants électroniques qui ont des propriétés bien particulières : Ils ne laissent passer le courant que dans certaines conditions.

Pour l'instant, laissons notre addition de coté, nous y reviendrons plus tard. Je vais d'abord vous présenter ces fameux composants. Ensuite seulement, nous serons en mesure de comprendre comment l'ordinateur peut calculer.

Ces composants s'appellent des portes logiques. Les voici :

  • La porte ET

  • La porte OU

  • La porte NON

Rassurez-vous, c'est super simple. Je vais expliquer chacune des portes en détails. Personne n'y laissera la vie. :lol:

La porte ET

La porte ET, appelée porte AND en anglais est un composant qui ressemble à ça :

Tant qu'à faire, je vous ai fait le schéma officiel. C'est ainsi qu'on représente la porte ET selon la norme Européenne d'électronique.

Vous pouvez voir la porte ET au milieu ; et de chaque coté, des fils qui y entrent et sortent : Le courant entre par les deux fils à gauche et ressort par le fil de droite.

La particularité de la porte ET est qu'elle laisse passer le courant uniquement si les deux fils qui y entrent à gauche sont chargés. Alors seulement, le courant ressortira par le fil à droite. Mais si l'un des deux n'est pas chargé alors le courant ne passera pas et le fil de droite ne sera pas chargé. De la même façon, si aucun des deux fils d'entrée n'est chargé, le fil de sortie ne le sera pas non plus.

Donc, comme son nom l'indique, il faut que la première entrée ET la seconde soient chargées pour que la sortie le soit aussi.

Voici donc ce que fait cette porte, en résumé. Comme d'habitude, 1 veut dire chargé et 0 non chargé :

  • Si Entrée 1 = 0 et Entrée 2 = 0 alors Sortie = 0

  • Si Entrée 1 = 1 et Entrée 2 = 0 alors Sortie = 0

  • Si Entrée 1 = 0 et Entrée 2 = 1 alors Sortie = 0

  • Si Entrée 1 = 1 et Entrée 2 = 1 alors Sortie = 1

Pour représenter cela, on utilise généralement ce qu'on appelle une table de vérité :

Entrée 1 | Entrée 2 | Sortie
0 | 0 | 0
1 | 0 | 0
0 | 1 | 0
1 | 1 | 1

Ce tableau, très simple à lire, est utilisé en électronique pour décrire les fonctions des portes. Les deux premières colonnes définissent les entrées et la dernière la sortie. A chaque porte que je vous décrirai par la suite, j'ajouterai sa table de vérité pour plus de clarté.

La porte OU

La porte OU, appelée porte OR en anglais ressemble à ça :

Comme la porte ET, cette porte a deux entrées et une sortie. Par contre, elle laisse passer le courant dans des conditions différentes.

Avant d'aller plus loin, penchons-nous sur la signification du mot "ou" en français. Si vous réfléchissez bien, vous vous apercevrez que ce mot a en réalité deux significations ! :waw:
Voici des exemples pour les illustrer :

Signification n°1 : "Si Luc se casse le pied gauche OU le pied droit, il doit aller à l'hôpital."

Cette phrase est claire. Je vous pose maintenant une question : Si Luc se casse les deux pieds, que fait-il ? Bien entendu, il y va aussi. (Euh, ça ira là sans schéma ?)
Le OU de cette phrase n'exclut pas le cas où les deux possibilités arrivent en même temps.
On appelle ce OU, un OU inclusif, car il inclut le cas où les deux choses se passent.

Signification n°2 : Dans une recette : "Ajoutez 300 grammes de beurre OU de margarine."

Cette fois, c'est différent. Je dois mettre ou bien du beurre ou bien de la margarine. Si je mets les deux, le chef me vire illico...
Le OU de cette phrase exclut le cas où les deux possibilités arrivent.
C'est pour cela qu'on l'appelle le OU exclusif. On appelle parfois le OU exclusif : XOR.

Maintenant que tout cela est clair, sachez qu'à chaque fois que je dirai OU sans préciser, ce sera le OU inclusif (celui de l'hôpital). Pour le OU exclusif (celui de la recette), je dirai XOR.

Le OU dont on va parler maintenant est le OU inclusif. Nous parlerons plus tard du XOR.

Voici donc les règles de la porte OU :

  • Si l'une des deux entrées est chargée => le courant passe. (= Luc se casse l'un des deux pieds => Il va à l'hôpital)

  • Si les deux entrées sont chargées => le courant passe aussi. (= Il se casse les deux => Il va aussi à l'hôpital)

  • Si aucune des deux entrées n'est chargée => alors la sortie ne l'est pas. (= Il se casse rien => Il n'y va pas - A moins qu'il soit médecin :D )

Et pour couronner le tout, la table de vérité :

Entrée 1 | Entrée 2 | Sortie
0 | 0 | 0
1 | 0 | 1
0 | 1 | 1
1 | 1 | 1

Tout est clair ? Alors on passe à la dernière porte.

La porte NON

La porte NON, appelée porte NOT en anglais, est la troisième et dernière porte. La voici :

Vous pouvez voir que cette porte est très différente des deux premières : Elle ne possède qu'une seule entrée.

Son fonctionnement est très simple. La porte NON inverse tout simplement le courant :

  • Si l'entrée est chargée, la sortie ne le sera pas.

  • Si l'entrée n'est pas chargée, la sortie le sera.

Et comme d'habitude, la table de vérité :

Entrée | Sortie
1 | 0
0 | 1

Voilà, nous avons vu trois des portes logiques. Il y en a d'autres, mais je ne veux pas vous surcharger... ;)
Je vous en ai présenté quelques unes pour que vous vous fassiez une idée de ce que est une porte. Le principal est de savoir ceci :

C'est grâce aux portes que l'ordinateur est capable de réfléchir.

En combinant intelligemment les portes, on peut créer des circuits de portes qui permettent de réaliser des opérations.
Oui, en réalité, toutes les opérations qu'exécute l'ordinateur sont en fait uniquement des combinaisons de portes.

On appelle une combinaison de portes : un circuit logique combinatoire, ou plus simplement un circuit logique.

Ces circuits logiques peuvent être très complexes, on ne va donc pas les voir ici dans les détails. D'autant plus que cela n'a pas vraiment d’intérêt pour la programmation en assembleur. L'important est que vous sachiez qu'ils sont constitués de portes.

A titre d'exemple, je vais quand même vous en montrer un, assez simple :

Il est inutile d'essayer de comprendre comment ce circuit fonctionne. Je ne vous l'ai mis uniquement pour que vous vous fassiez une idée de ce qu'est un circuit logique.
Ce circuit applique l'opération logique XOR (que j'ai mentionné plus haut - rappelez-vous, la recette ^^ ). Il est composé de deux portes NON, deux portes ET et d'une porte OU.

Sachez en passant qu'il existe des circuits beaucoup plus complexes que ça...

Revenons à nos moutons...

L'UAL est donc composée d'un grand nombre de portes logiques, combinées pour former des circuits logiques.

On envoie à l'UAL sous forme de courant électrique les données dont elle a besoin pour faire l'opération.

Il y a deux types de données à envoyer à l'UAL, pour chaque opération :

  • Les nombres sur lesquels ont veut faire l'opération, (par exemple le 8 et le 5).

  • L'opération que l'on veut exécuter, (par exemple l'addition).

Tout cela bien entendu sous forme de courant électrique.

Ces courants vont donc passer par les circuits logiques à l’intérieur de l'UAL. Ceux-ci sont organisés de façon à ce que le courant sortant indique le résultat.

Ce n'est pas un cours d'électronique, je ne suis donc pas entré dans tous les détails. Mais le plus important y est : Vous maîtrisez maintenant les bases du fonctionnement du processeur.

Prochain chapitre : Les bus. Après cela, nous pourrons enfin commencer la programmation.

Ne flanchez pas si près du but ! :p

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Le cerveau de la bande Comment ça marche (3/3)

Comment ça marche (3/3)

L'UAL Transports en commun

Nous voici enfin dans la dernière ligne droite : Les bus.

Rassurez-vous, le plus dur est derrière. Si vous avez compris les monstres que sont la mémoire et le processeur, comprendre les bus sera un véritable jeu d'enfant.

Let's go ! :pirate:

Transports en commun

Comment ça marche (3/3) Bus à impériale

Transports en commun

Commençons par une définition très simple :

Les bus servent à acheminer les données d'un endroit à l'autre de l'ordinateur. Ils sont constitués de fils électriques.

Vous pouvez les imaginer comme des câbles très fins composés d'un certain nombre de fils électriques.
Un bus peut transporter autant de bits qu'il contient de fils. Car chaque fil peut transporter un bit : il vaut 1 quand il est chargé et 0 quand il ne l'est pas. (Comme d'hab', quoi... :D )

Ce qui nous donne, par exemple :

  • Un bus qui contient 8 fils électriques, peut transmettre 8 bits, c'est à dire un octet.

  • Un bus qui contient 16 fils, peut transmettre 16 bits, donc un mot.

  • Un bus qui contient 32 fils, peut transmettre 32 bits, ce qui fait un double mot.

Voici un schéma de bus :

Comme indiqué en légende, ce bus transporte le nombre binaire 0101 0110.

Allons maintenant plus loin :

Il existe trois bus principaux dans un ordinateur, nous allons pour l'instant en voir seulement deux :

  • Le bus de données

  • Le bus d'adresse

Le bus de données

Le bus de données sert à transporter des données stockées en mémoire vers le processeur, et vice-versa (des données stockées dans le processeur vers la mémoire).
C'est donc un bus à deux sens : De la mémoire vers le processeur et du processeur vers la mémoire. On dit qu'il est bidirectionnel (bi = 2, du latin).

Le bus d'adresse

Ce bus relie le processeur à la mémoire vive. Comme son nom l'indique, il sert à transporter des adresses. Lorsque le processeur a besoin d'une donnée, il envoie son adresse à la mémoire via le bus d'adresse. Cela permet à la mémoire de savoir quelle donnée le processeur désire. Elle peut donc ensuite la lui envoyer via le bus de données.
Contrairement au bus de données, le bus d'adresse ne fonctionne que dans un sens : Du processeur vers la mémoire. On dit qu'il est unidirectionnel.

En réalité, il y a deux situations lors desquelles le processeur utilise ce bus :

  • Quand il a besoin d'une donnée : Il envoie à la mémoire l'adresse de l'emplacement mémoire où elle est située et la mémoire lui renvoie la donnée.

  • Quand il veut stocker une donnée : Il envoie l'adresse de l'emplacement mémoire où il veut stocker cette donnée. (Ainsi que la donnée à stocker, via le bus de données.)

Pour résumer, regardons le déroulement d'une opération dans l'ordinateur (très simplifié).

Le programme demande la résolution de l'opération 8 + 5. Ces deux nombres sont stockés dans la mémoire, chacun à une adresse précise : le nombre 8 à l'adresse 1090 et le nombre 5 à l'adresse 1230.
Le processeur doit aller les chercher, faire le calcul puis enregistrer le résultat en mémoire dans l'emplacement mémoire situé à l'adresse 1500 :

  1. Le processeur envoie 1090 via le bus d'adresse.

  2. La mémoire envoie au processeur le nombre situé à l'adresse reçue, via le bus de données.
    => Le processeur a reçu le nombre : 8.

    A nouveau, on fait exactement la même chose pour le second nombre :

  3. Le processeur envoie l'adresse où est stocké le second nombre via le bus d'adresse : l'adresse 1230.

  4. La mémoire envoie le nombre situé à l'adresse reçue au processeur, en utilisant le bus de données.
    => Le processeur a reçu le second nombre : 5.

  5. Le processeur fait le calcul en utilisant l'UAL.

  6. Le processeur envoie le résultat à la mémoire via le bus de données. Il envoie aussi l'adresse 1500 via le bus d'adresse, pour indiquer à la mémoire où il veut que le résultat soit stocké.

    7. => L'emplacement mémoire à l'adresse 1500 contient le résultat : 13.

Bien entendu, j'ai beaucoup simplifié le plan. Mais ça ira comme ça... ^^

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Comment ça marche (3/3) Bus à impériale

Bus à impériale

Transports en commun Capacité d'adressage

Bus à impériale

Parlons maintenant de la taille des bus. Je ne parle pas de leur longueur, mais du nombre de fils qu'ils contiennent. En termes d'électronique on appelle ça la largeur d'un bus.

Plus un bus contient de fils, plus il peut transporter de bits en un seul transport.

Par exemple un bus de 16 bits est plus performant qu'un bus de 8 bits. Puisque le bus de 8 bits peut transporter au maximum le nombre 255 (1111 1111 en binaire). Par contre, le bus de 16 bits pourra transporter les nombre jusqu'à 65 535 (1111 1111 1111 1111 en binaire).

Il en résulte un transport plus rapide des données lorsque le bus est plus large.

Voyons cela : Imaginons par exemple le nombre binaire 1010 1010 1111 1111 (43 775 en décimal). Le voici stocké en mémoire à l'adresse 23. (Si vous avez oublié le fonctionnement exact du stockage en mémoire, revoyez cette partie du chapitre 4.)

Mettons par exemple, que le processeur ait besoin de ce nombre stocké en mémoire, et que mon bus de données soit large de 16 bits. Cela ne me prendra qu'un seul transport pour envoyer tout le nombre de la mémoire au processeur :

Par contre, si mon bus de données ne mesure que 8 bits, cela me coûtera deux transports. J'envoie tout d'abord l'octet de poids faible situé à l'adresse 23 :

Puis, j'envoie l'octet de poids fort situé à l'adresse 24 :

Au final, j'aurai transmis le nombre, mais en deux fois. Cela m'aura donc prit deux fois plus de temps qu'avec le bus 16 bits.

Vous comprenez donc que la largeur des bus est cruciale. Nous avons tout intérêt à avoir des bus larges, on y gagne beaucoup en vitesse. Le bus de données de 16 bits me permet de transmettre une donnée deux fois plus rapidement que le bus de 8 bits. ;)

Différence entre le bus de données et le bus d'adresse

Sachez maintenant quelque chose d'important : Il existe une différence fondamentale entre le transport de données et le transport d'adresses :

Comme on vient à l'instant de le voir, on peut transporter une donnée en plusieurs fois. Si la donnée est trop grande pour le bus de données, on peut faire plusieurs transports.

Par contre, lorsqu'on veut transporter une adresse, il faut le faire en une seule fois.
On ne peut pas faire plusieurs transports pour les adresses.
Il est donc totalement impossible de transporter une adresse plus grande que le bus d'adresse.

Par exemple, si on a un bus d'adresse de 20 bits, on ne pourra pas transporter une adresse plus élevée qu'un million (1 048 575, exactement). Avec un bus d'adresse de 24 bits, on ne pourra pas dépasser les 16 millions (16 777 215).

Ceci nous amène automatiquement à une constatation : Le nombre d'emplacements mémoire utilisables est limité par la largeur du bus d'adresse.

C'est logique : puisque chaque emplacement mémoire a une adresse, si on ne peut pas avoir plus d'1 million d'adresses différentes, on ne peut pas utiliser plus d'1 million d'emplacements mémoire.
Prenons un ordinateur qui posséde un bus d'adresse de 20 bits. Etant donné que ce bus ne peut pas transporter un nombre supérieur à 1 048 575, cet ordinateur pourra donc utiliser seulement les emplacements mémoire de l'adresse numéro 0 jusqu'à l'adresse numéro 1 048 575. Et même si vous lui mettez 10 GB de mémoire, il ne pourra pas utiliser plus d'1 MB, à cause de son bus d'adresse.

En d'autres termes, pour ceux qui n'auraient pas encore compris : Le processeur de cet ordinateur ne pourra jamais dire à la mémoire : "Mets cette donnée dans l'emplacement mémoire situé à l'adresse 2 999 787", puisque cette adresse doit être transmise via le bus d'adresse et que celui-ci ne peut pas contenir un nombre aussi élevé.

Cet ordinateur sera donc assez limité en terme de mémoire vive, puisqu'il ne pourra pas stocker plus d'un million de bytes.

On dit que sa capacité d'adressage est d'1 Megabyte. Il ne peut pas adresser plus.

Un ordinateur ayant un bus d'adresse de 24 bits, par contre, pourra stocker plus de 16 millions de bytes en mémoire. On dira qu'il a une capacité d'adressage de 16 MB.

En résumé :

  • La largeur du bus d'adresse influe sur la capacité d'adressage, c'est à dire la quantité de mémoire utilisable par l'ordinateur.

  • La largeur du bus de données influe sur la vitesse de l'ordinateur. Plus il est large, moins de transports sont nécessaires pour transmettre les données.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Transports en commun Capacité d'adressage

Capacité d'adressage

Bus à impériale Rapide historique

Capacité d'adressage

Puisqu'on en parle, ouvrons donc une petite parenthèse au sujet de la capacité d'adressage, cela nous permettra d'approfondir un peu plus ce thème. Nous reprendrons les explications concernant les bus eux-même plus tard.

J'ai expliqué plus haut comment la capacité d'adressage dépendait du bus d'adresse. En réalité, elle dépend aussi d'un autre critère. Essayons de le trouver en réfléchissant un peu.

Mettons nous un instant à la place du processeur. Il a besoin d'une donnée. Pour cela, il doit envoyer à la mémoire l'adresse de l'emplacement mémoire où est stocké la donnée qu'il désire.
Une petite question : Pour envoyer à la mémoire cette adresse, il faut bien qu'il l'ait enregistrée quelque part au préalable. Où ça ?

Réponse toute simple : Dans un registre.

En résumé, le processeur enregistre l'adresse dans un registre et envoie le contenu du registre via le bus d'adresse. Ainsi, la mémoire reçoit l'adresse de l'emplacement mémoire désiré.

Imaginons maintenant que le registre où l'on veut stocker l'adresse ne fasse que 16 bits. On ne pourra pas y stocker une adresse qui dépassera 16 bits !
Donc, même si le bus d'adresse fait 20 bits, si le registre est plus petit, il limitera la taille de l'adresse que l'on peut envoyer.

On vient donc de découvrir un nouveau critère dont dépend la capacité d'adressage : La taille des registres.

Dans le 8086 où les registres mesuraient 16 bits, l'adresse la plus haute possible était 65 535. Ce qui fait, en comptant l'adresse 0, 65 536 emplacements mémoire utilisables en tout. La capacité d'adressage était donc de 64 KB seulement (puisqu'1 KB contient 1024 bytes - faites le calcul) !

Imaginez un instant ce que cela signifie : seulement 65 536 emplacements mémoire - d'un octet chacun je rappelle - disponibles pour programmer ! Rien que d'y penser, j'en ai la chair de poule... :p
Les programmeurs de l'époque devait être extrêmement économes quant à la mémoire qu'ils utilisaient... Ils imaginèrent d'ailleurs toutes sortes de techniques pour utiliser le minimum de mémoire afin de ne pas dépasser la limite.

Après s'être bien creusé la tête, Intel finit par mettre au point une solution : Utiliser deux registres au lieu d'un seul pour enregistrer les adresses.
Ça parait peut-être tout simple dit comme ça, mais ça ne l'est pas, je peux vous l'assurer. C'est toute une technique, à laquelle on donné un nom d'ailleurs : La segmentation.
Bon, je ne vais pas en dire plus au sujet de la segmentation maintenant, je ferai certainement un chapitre (beaucoup) plus tard à ce sujet. Pour l'instant, il vous suffit de comprendre qu'avec deux registres (de 16 bits chacun) pour indiquer les adresses, les programmeurs avaient à leur disposition plus de 4 milliards d'emplacements mémoire ! (4 GB)
Bien entendu, cela dépend aussi du bus d'adresse : S'il fait 32 bits, alors on a effectivement ces 4 milliards d'adresses, car on peut envoyer le contenu des deux registres de 16 bits en une fois. Mais s'il est moins large, cela réduit la capacité d'adressage en conséquence.

En résumé, la segmentation résout le problème de stockage des adresses dans les registres trop petits : elle permet de stocker une adresse sur deux registres. Par contre le problème du bus d'adresse reste le même : Qu'elle soit stockée sur un ou sur deux registres, une adresse ne peut être envoyée qu'en un seul transport.

Comme je l'ai dit la segmentation est en fait un peu plus compliquée que ça, mais je n'en dirai pas plus.

Mais si tu nous expliques pas la segmentation, on pourra pas faire des programmes de plus de 64 KB ?!

Aïe ! Vous avez oublié une chose : Depuis le 80386 les registres ont été étendus à 32 bits. On a donc plus besoin de la segmentation puisqu'un seul registre de 32 bits suffit pour avoir les 4 GB de mémoire : Un registre de 32 bits, c'est pareil que deux registres de 16 bits, n'est ce pas ?

En fait, la segmentation n'a été utile aux programmeurs qu'avant l’extension des registres.
Vous comprenez maintenant à quel point l'extension des registres est utile. Elle nous permet d'avoir une grande capacité d'adressage sans avoir à utiliser la segmentation. :)

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Bus à impériale Rapide historique

Rapide historique

Capacité d'adressage Un minimum de code

Rapide historique

Maintenant que vous maîtrisez parfaitement les bus et la notion de capacité d'adressage, je vais faire un historique des processeurs depuis le 8086 décrivant leurs caractéristiques concernant ces sujets.

Pour chaque processeur j'indiquerai :

  • La taille du bus d'adresse et des registres et leur conséquence : La capacité d'adressage.

  • La taille du bus de données et sa conséquence : La vitesse de transmission de données.

Prêts ?

8086

Ce processeur posséde des registres de 16 bits et un bus d'adresse de 20 bits.

  • Sans utiliser la segmentation, c'est à dire en utilisant un seul registre pour enregistrer les adresses au sein du processeur, la capacité d'adressage était limitée à seulement 64 KB (à cause des registres 16 bits).

  • En utilisant la segmentation, la taille des registres n'est plus un problème, puisqu'on en utilise deux pour stocker l'adresse. Par contre, la taille du bus d'adresse qui est de 20 bits seulement limite la capacité d'adressage à 1 MB.

Son bus de données est de 16 bits : Transmettre un nombre de 64 bits demande 4 transports.

80186

Ce processeur ne présente pas de différences avec le 8086 concernant ces points. Soit dit en passant, ce processeur n'a été que peu utilisé, très vite remplacé par le 80286.

80286

Tiens, quand on parle du loup...

Le 80286 possède des registres de 16 bits et un bus d'adresse de 24 bits.

  • Sans utiliser la segmentation, la capacité d'adressage est limitée à 64 KB, puisqu'un registre ne fait que 16 bits.

  • En utilisant la segmentation, on peut stocker les adresses sur deux registres ; la taille des registres n'est donc plus un problème. C'est la taille du bus d'adresse, 24 bits, qui va limiter la capacité d'adressage à 16 MB.

Son bus de données est de 16 bits, comme le 8086.

80386

Le 80386 possède des registres de 32 bits et un bus d'adresse 32 bits.

Depuis ce processeur, la segmentation est devenue inutile, puisque sans elle, il a déjà une capacité d'adressage de 4 GB. (Il n'empêche qu'elle existe toujours, quoiqu'elle a reçu quelques modifications dans son fonctionnement...mais cela n'a pas d'importance puisque de toutes les façons nous programmerons sans segmentation, du moins pour le moment.)

Son bus de données est de 32 bits, il transmet un nombre de 64 bits en 2 transports.

80486 et Pentium

Ces processeurs sont similaires au 80386 concernant la capacité d'adressage.

Au sujet du bus de données, seul le Pentium est différent, le sien mesure 64 bits : Il peut transmettre un nombre de 64 bits en un seul transport.

Vous pouvez souffler, on a terminé la théorie...pour le moment en tout cas.

Maintenant, direction le chapitre 7, où nous apprendrons au début à utiliser RadASM, puis, nous commencerons à programmer.

Vous verrez, ce sera plus simple que les chapitres précédents. :)

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Capacité d'adressage Un minimum de code

Un minimum de code

Rapide historique Créer un nouveau projet

Oui, la théorie est bien finie !

Dans ce chapitre nous allons découvrir RadASM et écrire notre premier code.

Créer un nouveau projet

Un minimum de code Présentation de l'environnement

Créer un nouveau projet

Nous allons maintenant apprendre à créer un Nouveau projet. Avant chaque nouveau programme que vous écrirez, il vous faudra faire de même. Suivez bien la marche à suivre...

Tout d'abord, ouvrez RadASM. Vous vous souvenez j'espère comment on fait : On ouvre le dossier nommé RadASM et on clique sur l'application du même nom. (Vous la reconnaîtrez aisément à son icône rouge formant un R et un A à l'envers.)

C'est fait ? Bien.

Cliquez ensuite sur l'onglet Fichier tout en haut à gauche, puis sur Nouveau projet, le premier du menu déroulant.
Vous aurez une fenêtre comme celle-ci qui apparaîtra :

Comme vous pouvez le voir, j'ai ajouté des numéros sur la capture d'écran.
En effet, vous aurez cinq choses à faire sur cette fenêtre. Ces numéros vous aideront à vous repérer :

1) Tout en haut, en dessous de "Assembleur utilisé", vous avez un menu déroulant.
Il vous permet de choisir l'Assembleur que vous désirez utiliser. Normalement vous n'aurez qu'une possibilité, vu que l'on n'a installé que masm. ;)
Choisissez donc masm.

2) Ensuite, juste en dessous, sous le titre "Type de projet", vous pouvez voir une liste de boutons à cocher. Comme l'indique ce titre, vous pouvez ici choisir le type de projet que vous voulez réaliser.
Choisissez Console App. Vous indiquez par cela à RadASM que vous allez créer un projet sur console. Nous verrons bientôt ce que cela veut dire.

3) Nom du projet : Ici, vous devrez écrire le nom de votre projet. Nous allons mettre "Premier prog", mais vous pouvez mettre ce que vous voulez.

4) Description du projet : Le titre est clair, je suppose. Vous pouvez écrire une courte description de votre projet. Contrairement à la case précédente, vous n'êtes pas obligés de remplir celle-ci.

5) En dessous, vous devrez choisir l'emplacement dans lequel vous voulez que votre projet soit enregistré.
Normalement, le chemin d’accès est automatiquement écrit comme il faut. Il doit se terminer par "RadASM\Masm\Projects". Si ce n'est pas déjà fait, indiquez-le en vous servant du bouton aux points de suspensions.

Autre méthode : Pour avoir le chemin facilement, ouvrez le dossier "Masm" dans le dossier "RadASM". Ouvrez ensuite le dossier "Projects". Faites maintenant un clic sur la barre qui affiche le chemin, tout en haut de la fenêtre. Vous pouvez maintenant le copier facilement. Vous n'avez plus qu'à le coller dans la case.

Au final, vous devez avoir ceci :

Voilà ! Vous pouvez maintenant cliquer sur le bouton "Next>", trois fois, sans vous préoccuper des autres fenêtres qui apparaîssent.
Enfin, cliquez sur "Finish" et c'est terminé ! :)

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Un minimum de code Présentation de l'environnement

Présentation de l'environnement

Créer un nouveau projet Les mots-clés

Présentation de l'environnement

Je vais maintenant vous présenter les principales fonctionnalités de RadASM.

Regardez cet écran :

Vous voyez les flèches rouges ? Vous devez cliquer sur les boutons qu'elles indiquent (clic simple sur le numéro 1 et double clic sur le numéro 2).

Normalement, après cela, RadASM devrait apparaître ainsi :

Si ça vous gêne ainsi, je vous la mets aussi en miniature ; ouvrez-la dans un autre onglet pour pouvoir suivre les explications en parallèle :

Comme vous pouvez le voir, RadASM est composé de plusieurs parties que j'ai numéroté sur la capture d'écran.
Voici une description pour chacune d'entre elles :

1. Tout en haut, vous avez deux barres d'outils, l'une au dessus de l'autre. La première vous permet d'accéder à toutes les fonctionnalités de RadASM. La seconde contient de petites icônes qui sont en fait des raccourcis vers les fonctionnalités les plus utilisées. Lorsque vous pointez votre curseur sur l'une d'entre elles, une bulle descriptive apparaît.

Voici celle que l'on utilisera le plus :

Cette icône sert à assembler et exécuter le programme. Vous pouvez aussi faire Ctrl+F5, c'est exactement pareil.

2. Au centre, la fenêtre principale. C'est là que nous écrirons notre programme.
Oui, je sais c'est tout noir, mais vous pourrez changer ça, si ça vous gène. On s'en occupera très bientôt, juste après notre premier programme, (vous comprendrez alors pourquoi je ne l'ai pas fait tout de suite).

3. Ceci est une zone de votre écran que vous allez sans doute haïr durant les prochains jours. C'est là que le debuggeur vous indiquera les erreurs de votre programme. D'autant plus qu'il le fera en anglais, et pas dans le plus clair... Mais ne vous inquiétez pas, même si vous ne lisez pas l'anglais, je vous expliquerai tout ce qu'il y a comprendre.
(Il n'empêche que connaître l'anglais est un atout crucial en programmation, qui plus est en assembleur. :-° )

4. C'est ici que vous apercevrez les fichiers contenus dans votre projet. Nous allons travailler pour l'instant uniquement dans celui qui se termine par ".asm". (Celui que je vous ai demandé d'ouvrir tout à l'heure.)

5. Ceci est un outil très utile qu'on apprendra à utiliser plus tard.

Avant de commencer notre premier programme, il faut que vous sachiez une chose. Pour l'instant, nous n'allons pas écrire de programme avec fenêtres mais seulement sur console. Si vous ne connaissez pas la différence, je vous invite à lire ce qu'a écrit M@téo21 à ce sujet dans son tuto sur le langage C.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Créer un nouveau projet Les mots-clés

Les mots-clés

Présentation de l'environnement Code minimal

Les mots-clés

Nous avons enfin créé un nouveau projet. Super. Nous sommes maintenant face à un écran noir comme la mort, et je vous entends d'ici penser : Mais qu'est-ce qu'on va bien pouvoir y écrire...?
Rassurez-vous, je me suis posé la même question lorsque je me suis retrouvé dans cette situation pour la première fois...
Pour vous simplifier les choses, je vais vous faire un petit rappel-introduction qui vous sera d'une grande aide par la suite. Même si vous avez une mémoire d'éléphant, lisez tout, je vais y ajouter de nouvelles informations importantes. ;)

Tout d'abord, un petit rappel :

Un programme est composé d'une suite d'instructions. L'ordinateur lit les instructions et les applique dans l'ordre.
Il existe un nombre fixe d'instructions existantes. Elles constituent ce qu'on appelle le jeu d'instructions.
Le nombre d'instructions disponibles peut varier d'une architecture à l'autre. Dans notre architecture, la 80x86, le jeu d'instructions contient environ une centaine d'instructions.

Tous les programmes ne sont donc en réalité que des combinaisons de ces instructions. Uniquement de ces instructions. Rien d'autre.

A quoi ressemblent ces instructions ? A des trucs comme ça : 0110 1000 1110 1001. Oui, les instructions sont écrites en binaire.

A la base, le programme est enregistré sur le disque dur. Puis, lorsque vous le lancez, toutes ses instructions sont copiées du disque dur vers la mémoire vive. Ensuite, le processeur va chercher la première instruction et l'appliquer, il va ensuite chercher la seconde et faire de même... Ainsi de suite jusqu'à qu'il ait tout terminé.

(Bien entendu ceci est le déroulement d'un programme vu en gros. Plus tard, nous en apprendrons les détails.)

A l'époque de l'invention des ordinateurs, les programmeurs écrivaient les instructions en binaire. Mais cela provoquait beaucoup d'erreurs, car quoi que l'on dise, le binaire n'est pas fait pour les humains : Vous vous trompez d'un bit, et c'est une toute autre instruction que l'ordinateur reçoit...

On a donc inventé le langage assembleur. Son principe est très simple : On représente chaque instruction par un petit mot écrit en lettres humaines. On peut donc écrire le programme avec ces mots là. Puis, on met en marche un logiciel qui va remplacer chacun de ces mots par l'instruction binaire équivalente : L'ordinateur peut maintenant lire notre programme.

Ce logiciel "traducteur" se nomme l'Assembleur (avec un 'A' majuscule). L'Assembleur que nous allons utiliser dans ce tuto se nomme MASM.

On appelle ces mots de remplacement des mnémoniques, car en plus d'être écrits en lettres humaines, chacun a été inventé de façon à faire penser automatiquement à l'instruction qu'il représente.
Par exemple, add est le symbole mnémonique de l'instruction qui sert à additioner.

(Et là on peut dire merci aux inventeurs de l'Assembleur, qui, j'en suis sûr, n'auraient pas été dérangés outre mesure par des symboles comme GfRd, UUtE et autres joyeusetés. Ils ont pensé à nous en se servant de mnémoniques, c'est sympa de leur part, non ? :-° )

Nous pouvons donc maintenant répondre en partie à notre question : "Que va-t-on écrire dans l'écran noir ?"

Vous connaissez maintenant la réponse : Des instructions. (Sous forme de mnémoniques, bien entendu.)
Nous allons donc apprendre à utiliser ces instructions, puis, nous apprendrons à les associer afin de créer des programmes.

Attention ! Je vais maintenant introduire des notions totalement nouvelles, cruciales pour la suite. Ouvrez grand vos yeux. Ce ne sera pas difficile à comprendre, mais comme tout ce qui est nouveau, cela nécessite une attention toute particulière. N'oubliez pas que c'est la toute dernière chose à digérer avant que l'on commence à programmer réellement.
Alors, du nerf, le bout du tunnel apparaît enfin !

Vous avez peut-être remarqué que j'ai précisé au dessus "en partie".

Car les instructions ne seront pas les seules composantes de notre code.

Quoi ! Mais tu nous as dit au moins dix fois qu'un programme était composé uniquement d'instructions !!

Oui, c'est vrai. Mais soyons plus précis : Le programme que l'ordinateur lira à la fin sera composé uniquement d'instructions.

Il n'empêche que, dans notre code, nous utiliserons parfois d'autres types de mots-clés que les instructions. Car il existe d'autres types de mots-clés que les instructions.

En voici deux nouveaux (en plus des instructions) :

  1. Les directives

  2. Les macros

Je vais les expliquer un à un. Commençons d'abord par les directives :

Les directives sont des ordres que l'on donne à l'Assembleur lui-même, autrement dit, MASM. Elles concernent seulement et uniquement MASM.
On les utilise pour demander certaines choses à MASM, notamment comment on veut qu'il assemble le programme. Ces directives seront donc lues par MASM avant l'assemblage. Puis, en fonction de ce qu'elles lui demandent, MASM assemblera le programme en langage machine.

Au final, les directives ne feront donc pas partie du programme en langage machine, puisqu'elles ne sont destinées qu'à l'Assembleur et non au processeur.

Nous apprendrons un certain nombre de directives au cours de ce tuto. L'important est de toujours garder en tête que l'ordinateur ne les verra pas dans le programme final, puisqu'elles ne servent qu'à MASM.

En résumé :

  • Les instructions sont lues par le processeur durant l'exécution du programme, et sont donc assemblées.

  • Les directives sont lues par MASM avant l'assemblage du programme. Elles ne sont donc pas assemblées.

Nous verrons très bientôt nos premières directives (avant la fin du chapitre).

Passons maintenant aux macros :

Vous savez peut-être qu'écrire un programme en assembleur est en général assez long.
Rien que pour demander à l'ordinateur d'afficher du texte à l'écran, il faut un très grand nombre d'instructions.

A l'époque de l'invention de l'assembleur, c'était comme ça, il fallait écrire des centaines de lignes pour faire des programmes très simples. o_O

Le plus frustrant était que la plupart des tâches sont répétées très souvent. Par exemple : afficher du texte à l'écran, enregistrer ce qui est tapé au clavier, etc. A chaque fois que l'on voulait ordonner à l'ordinateur d'accomplir l'une de ces tâches, il fallait réécrire à nouveau tout le code nécessaire à son déroulement. Et comme ces tâches étaient très courantes, c'était vraiment du temps perdu pour rien...

Jusqu'à ce que les programmeurs aient une idée géniale pour raccourcir considérablement l'écriture des programmes : Les macros.

Voici en quoi cela consiste : Ils écrivirent à l'avance le code de toutes les tâches les plus couramment utilisées, et les enregistrèrent dans des fichiers. Par ailleurs, ils associèrent à chacune de ces tâches un nom. Bien entendu, chaque nom est en général évocateur quant à la tâche qu'il représente. Grâce à cela, écrire un programme est beaucoup plus facile : Quand on arrive à une tâche déjà écrite par les programmeurs, il suffit d'écrire le nom qui correspond à cette tâche. Ensuite, lorsqu'on lance l'Assembleur, ce dernier remplace chaque nom par le code correspondant (celui que les programmeurs avaient écrit à l'avance). Puis, il passe à l'assemblage du programme.

Ces raccourcis sont très, très utilisés. Ce sont les macros. Il en existe un grand nombre.

Nous aussi, nous allons utiliser les macros. Cela nous facilitera grandement les choses. Mais il ne faut surtout pas oublier que derrière chacun de ces petits noms d'apparence anodine se cache un long code complexe.

Mais bien entendu, il ne sera pas question d'utiliser bêtement des codes écrits par d'autres toute notre vie, du moins, pas sans comprendre parfaitement leur fonctionnement.
C'est pour cette raison que, plus tard, lorsque nous en serons capables, je vous montrerai où trouver ces codes, et nous les lirons ensemble. Vous comprendrez alors de quoi sont faites les macros que l'on aura si souvent utilisé. Vous pourrez alors, si vous le désirez, écrire ces tâches sans l'aide des macros. (Par ailleurs, nous apprendrons aussi à créer nous-mêmes nos propres macros... Mais patience, le chemin est long jusque là !)

En attendant, nous allons "bêtement" utiliser les macros sans savoir comment elles sont codées, mais il n'y a pas de honte à avoir, tout le monde est passé par là !

Vous comprenez maintenant que je ne vous ai pas menti. Finalement, un programme est bien constitué uniquement d'instructions :

  • Les directives ne sont pas integrées au programme.

  • Les macros sont en réalité eux-même composées d'instructions. Lorsque vous utilisez une macro, elle est remplacée par la suite d'instructions préalablement préparée par les programmeurs.

En résumé, nous connaissons maintenant trois types de mots-clés :

  • Les instructions

  • Les directives

  • Les macros

Non, ce ne sont pas les seuls... Nous en découvrirons d'autres plus tard. Mais pour l'instant, ceux-là seront suffisants...

Tout cela peut vous paraître flou et confus. C'est tout à fait normal : uniquement de la théorie, sans jamais pratiquer, n'est pas facile à digérer. C'est pour cela que je vous propose de commencer à coder tout de suite notre premier programme. :)

Vous allez peu à peu faire connaissance avec les instructions, les macros et les directives. Ils vous paraissent pour l'instant étranges sans doute, mais vous ne tarderez pas à les apprivoiser.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Présentation de l'environnement Code minimal

Code minimal

Les mots-clés Enfin un programme !

Code minimal

Ca y est ! On va enfin commencer à coder pour de bon. :)

Ouvrez bien vos yeux, vous allez découvrir ce qu'on appelle le code minimal, c'est à dire le code que tout programme contient obligatoirement :

.386
.model flat, stdcall
option casemap : none

include \masm32\include\masm32rt.inc

.code

salut :

exit

end salut

Euh, là on pige rien...

Ne vous inquiétez pas, c'est tout à fait normal : vous êtes sensé ne rien comprendre.

Tout sera expliqué comme il faut, en temps et en heure. Je dis "en temps et en heure", car en effet, vous n'avez pas encore le niveau nécessaire pour comprendre entièrement ce code.

Je vais donc expliquer ce qui est maintenant compréhensible. Par la suite, j'ajouterai des explications au fur et à mesure de votre progression.
Mais pas d'inquiétude ! Vous pourrez parfaitement apprendre à programmer sans comprendre toutes ces lignes.

Copiez simplement ce code dans RadASM, et lisez les explications que je peux vous offrir pour l'instant.

Commençons par la première ligne :

.386 : Voilà. Vous avez devant vous votre première directive ! Alors, ça fait quel effet ? :D

Oui, .386 est une directive. Elle indique à MASM pour quel processeur nous allons programmer. Le 80386.

Nous indiquons par cela à MASM que nous allons utiliser dans notre code uniquement les fonctionnalités présentes dans le 80386, mais pas celles qui ont été ajoutées plus tard dans les processeurs ultérieurs tel que le 80486 ou le Pentium. Si on transgresse cette règle et que l'on utilise de nouvelles fonctionnalités, le debuggeur ne manquera pas cette occasion de nous tomber dessus...

Le programme créé sera donc compatible avec le 80386 ainsi qu'avec tous les processeurs qui l'ont suivis, (puisque ceux-ci contiennent aussi les fonctionnalités du 80386). Mais il ne marchera pas sur les processeurs précédents (comme le 80286), car il utilisera des fonctionnalités inconnues pour eux.

Pour chaque processeur, il existe une directive qui lui fait référence :

  • .8086 pour le 8086

  • .186 pour le 80186

  • .286 pour le 80286

  • .386 pour le 80386

  • .486 pour le 80486

  • .586 pour le Pentium

Il vous suffit donc d'écrire la directive que vous voulez tout au début de votre code.

Si vous ne mettez rien, MASM fera comme si vous avez mis ".8086", et va donc assembler uniquement si le code est compatible avec le premier des processeurs, et donc automatiquement avec tous les processeurs (puisque tous ont aussi les capacités du premier).

.model flat, stdcall : Encore une directive. Mais vous n'avez pas encore le niveau requis pour comprendre ce qu'elle fait. Je l'expliquerai plus tard.

option casemap : none : Cette ligne est aussi une directive. Elle indique à MASM que nous voulons faire une difference entre les majuscules et les minuscules. Nous verrons bientôt des exemples qui vous permettront de comprendre cela.

include \masm32\include\masm32rt.inc : Cette directive est aussi pour plus tard.

.code : Cette directive indique à MASM qu'à partir d'ici nous commençons à écrire notre programme proprement dit.

salut : et end salut : Tout le code du programme doit être écrit entre "salut :" et "end salut" (ligne 11 et 15). En réalité, libre à vous de choisir le mot que vous voulez, du moment que vous mettez le même les deux fois : Avant les deux points et après le "end", à la fin.
Nous decouvrons ici un nouveau type de mot. Ce n'est ni une instruction, ni une directive, ni une macro. C'est un label. Je ne vous en ai pas parlé plus tôt pour ne pas vous surcharger. D'autant plus que nous ne verrons que plus tard à quoi cela sert exactement.

Pour l'instant ne retenez qu'une seule chose : Vous devez écrire tout votre code entre ces deux mots : "salut :" et "end salut".

exit : Ceci n'est ni une instruction, ni une directive. Oui ! Vous avez deviné, c'est une macro.

Normalement, pour quitter convenablement un programme, il faut écrire quelques lignes de code à la fin. Au lieu de cela, nous allons utiliser cette macro qui va faire le travail à notre place.

Exit est une macro qui sert à quitter le programme.

Il va sans dire que exit devra être le dernier mot de votre code (à part end label), puisque l'ordinateur quitte le programme dès qu'il le lit. Toutes les instructions qui suivront ce mot ne seront donc pas exécutées.

Voilà, c'est terminé. Au risque de me répéter, je dirai que tous vos programmes devront au moins contenir ce code. C'est le minimum vital.

Bon, eh bien maintenant vous pouvez lancer la sauce. Appuyez sur le bouton "Construire et exécuter" ou faites Ctrl+F5. MASM assemblera votre code et l'exécutable sera lancé. (Vous aurez sûrement une boîte de dialogue vous demandant "Accept the command etc...". Faites OK.)

Alors ? C'est comment ?

Ben c'est nul ! Ca a rien fait ! Juste une console qui apparaît et disparaît plus vite que son ombre ! C'est normal ou j'ai raté un truc ? :(

Oui, c'est tout à fait normal. Le contraire aurait été inquiétant. D'ailleurs, ceux qui ont eu un super jeu vidéo qui s'est lancé...euh...c'est pas vraiment normal. (Profitez du jeu quand même, hein ! :-° )

La raison de cette apparition/disparition est simple : Nous n'avons pas mis le programme en pause.

Voici ce qui se passe : L'ordinateur lit le code et la première chose qu'il voit est le "exit", qui lui demande de quitter le programme, alors il obéit et quitte le programme. C'est pour cela que la console apparaît et disparaît aussitôt.

Pour remédier à ce problème, il faut mettre le programme en pause, pour cela nous devons encore utiliser une macro.
La voici :

inkey

Ajoutez cette macro juste avant exit et appuyez sur 'Construire et exécuter'.

Voilà ! La consôle reste avec nous jusqu'à ce que vous appuyez sur une touche. C'est ce que le message en anglais veut dire : "Appuyez sur une touche pour continuer...".
Lorsque vous appuyez sur une touche du clavier, le programme se termine.

Vous pouvez aussi remplacer le message en anglais avec celui que vous voulez. Comme ceci :

inkey "Appuyez sur une touche pour continuer..."

Je vous met maintenant le code en entier, en surlignant la nouvelle ligne en jaune (qui c'est qui a dit que le zCode c'était pourri ? :soleil: ) :

.386
.model flat, stdcall
option casemap : none

include \masm32\include\masm32rt.inc

.code

salut :

inkey "Appuyez sur une touche pour continuer..."
exit

end salut

Perfecto ! :D

Voilà, la console est prête, il ne nous reste plus qu'à apprendre à programmer, pour y afficher quelque chose... :p

Nous allons donc découvrir de ce pas notre première instruction, afin que vous puissiez enfin donner des ordres à l'ordinateur.

Rendez-vous au chapitre 8 !

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Les mots-clés Enfin un programme !

Enfin un programme !

Code minimal Commentons, commentons

Maintenant que nous connaissons le code minimal, il va falloir aller de l'avant.
Ce chapitre sera donc plein de nouvelles choses ; à la fin, vous serez capables de créer vos propres petits programmes (très simples, bien entendu ^^ ).

Commentons, commentons

Enfin un programme ! Première instruction : mov

Commentons, commentons

Imaginez qu'un jour vous écriviez un programme et que vous l'oubliez ensuite dans quelque coin obscur de votre ordinateur. Trois mois plus tard, vous désirez améliorer ce programme, vous l'ouvrez donc avec RadASM.
Et là, stupeur, vous n'y comprenez que dalle... Vous avez sous vos yeux effarés des milliers d'instructions les unes à la suite des autres ; certes, c'est vous qui les avez écrites, mais vous ne vous souvenez plus de rien, vous n'arrivez plus à vous repérer dans le code.
Vous devez maintenant le relire entièrement pour trouver la partie que vous vouliez améliorer... Et croyez-moi, ça peut prendre, beaucoup, beaucoup de temps.

Si seulement on avait pû écrire des commentaires en français dans notre code. Ainsi, lorsque l'on reviendrait sur le code d'un programme longtemps après, on pourrait s'y repérer plus facilement.

Eh bien...on peut le faire, et c'est rudement utile.

Si vous n'avez encore jamais programmé, vous ne pouvez pas vraiment comprendre en quoi les commentaires sont indispensables. Mais faites-moi confiance, un jour, vous ne pourrez plus vous en passer. :-°

Une autre situation où les commentaires sont indispensables : Si vous voulez montrer votre code à un autre programmeur. Par exemple, dans le forum du site. Dans ce cas, il est fortement conseillé de bien commenter votre code. Sans cela, un programmeur mal luné vous remontera les bretelles en moins de deux. (Essayez, vous verrez...)

Bien entendu, les commentaires ne sont pas assemblés en langage machine. L'ordinateur n'en n'a pas besoin (de toutes les façons, il n'y comprendrait rien). Ils ne serviront qu'à vous (ou aux autres).

Je parle, je parle, mais je ne vous ai toujours pas dit comment on fait pour en mettre. Il y a en fait deux façons :

Première méthode

Dans la première, il vous suffit de tapez un ';' avant chaque commentaire. Regardez cet exemple :

.386                      ;directive qui indique que le programme doit être compatible avec tous les processeurs depuis le 80386
.model flat, stdcall      ;autre directive
option casemap : none     ;directive qui indique qu'on fait une différence entre les majuscules et les minuscules

include \masm32\include\masm32rt.inc     ;encore une autre directive

.code                     ;directive qui indique le début du code proprement dit

salut :                   ;label de début de code

inkey "Appuyez sur une touche pour continuer..."     ;macro de mise en pause de la console
exit                                                 ;macro qui quitte le programme

end salut                 ;label de fin de code

Comme vous le voyez, les commentaires me permettent de commenter chaque ligne.

Deuxième méthode

La deuxième, bien moins utilisée, fonctionne ainsi : Vous tapez le mot comment, qui est une directive (et qui veut dire "commentaire" en anglais). Puis mettez un signe que vous voulez, disons un '&'. Vous écrivez ensuite votre commentaire. Vous pouvez l'écrire sur plusieurs lignes. Puis, mettez un nouveau '&' pour dire que le commentaire s'arrête.

Regardez un exemple :

comment& Voici le code minimal d''un programme en assembleur.
         Tous nos programmes contiendront au moins ceci.      &

.386                      ;directive qui indique que le programme doit être compatible avec tous les processeurs depuis le 80386
.model flat, stdcall      ;autre directive
option casemap : none     ;directive qui indique qu'on fait une différence entre les majuscules et les minuscules

include \masm32\include\masm32rt.inc     ;encore une autre directive

.code                     ;directive qui indique le début du code proprement dit

salut :                   ;label de début de code

inkey "Appuyez sur une touche pour continuer..."     ;macro de mise en pause de la console
exit                                                 ;macro qui quitte le programme

end salut                 ;label de fin de code

Vous n'êtes pas obligé de choisir un '&'. N'importe quel autre caractère marchera, même une lettre (bien que ceci soit fortement déconseillé). La seule chose à ne pas oublier est de mettre le même caractère avant et après le commentaire.

Faites attention aussi de ne pas utiliser le signe que vous avez choisi à l'intérieur de votre commentaire, cela interrompera le commentaire, et générera une erreur.
C'est pour cela qu'il vaut mieux toujours se servir d'un signe qu'on n'utilise jamais dans nos phrases.

A partir de maintenant, j'utiliserai donc le '#' comme signe. (Avec ça on est tranquille...)

Ce type de commentaire est avantageux dans le cas où on veut commenter sur plusieurs lignes ; bien qu'en réalité, on peut parfaitement se débrouiller avec le premier type de commentaire en mettant un ';' avant chaque ligne :

; Voici le code minimal d'un programme en assembleur.
; Tous nos programmes contiendront au moins ceci.

.386                      ;directive qui indique que le programme doit être compatible avec tous les processeurs depuis le 80386
.model flat, stdcall      ;autre directive
option casemap : none     ;directive qui indique qu'on fait une différence entre les majuscules et les minuscules

include \masm32\include\masm32rt.inc     ;encore une autre directive

.code                     ;directive qui indique le début du code proprement dit

salut :                   ;label de début de code

inkey "Appuyez sur une touche pour continuer..."     ;macro de mise en pause de la console
exit                                                 ;macro qui quitte le programme

end salut                 ;label de fin de code

Néanmoins, dans le cas on l'on a un long texte à écrire en commentaire, on préférera la technique du comment.

C'est tout pour les commentaires, on peut passer à notre première instruction !

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Enfin un programme ! Première instruction : mov

Première instruction : mov

Commentons, commentons La macro print

Première instruction : mov

Voici notre première instruction : mov ; elle sert à enregistrer une valeur dans un espace de stockage.

Un espace de stockage est un endroit où l'on peut stocker des données. Nous connaissons deux types d'espaces de stockage : Les registres et la mémoire.

Pour l'instant, nous allons apprendre seulement à placer une valeur dans un registre. Nous apprendrons plus tard à travailler avec la mémoire, (c'est un peu plus compliqué).

Regardez comment on utilise l'instruction mov :

mov destination, source

On écrit donc tout d'abord "mov", puis on met le nom de la destination, c'est-à-dire de l'espace de stockage où l'on veut mettre notre valeur (par exemple eax). On place ensuite une virgule. Enfin, on indique la source, c'est-à-dire la valeur à enregistrer.

Il existe plusieurs façons d'indiquer la source. Nous allons pour l'instant en apprendre deux.

Première façon d'indiquer la source

On écrit directement la valeur que l'on veut enregistrer.

Exemple :

mov eax, 12      ; eax contiendra 12

Si vous ne precisez rien, le nombre sera en décimal. Si vous voulez l'écrire en binaire ou en hexadécimal, vous devrez le préciser en ajoutant un 'b' (pour le binaire) ou un 'h' (pour hexadécimal), juste après le nombre. Exemple :

  • Binaire :

    mov eax, 110b   ; eax contiendra le nombre binaire 110, c'est-à-dire 6 en décimal

    C'est comme si vous aviez écrit : mov eax, 6

  • Hexadécimal :

    mov eax, 0CD4h   ; eax contiendra le nombre hexadécimal CD4, c'est-à-dire 3284 en décimal

    C'est comme si vous aviez écrit : mov eax, 3284

Bien que si vous ne mettez rien, le nombre sera automatiquement considéré comme décimal, vous pouvez si vous voulez le préciser en ajoutant un 'd' après le nombre :

mov eax, 12d      ; Ceci enregistre le nombre décimal 12 dans eax

N'oubliez pas que les registres ne sont pas infinis. Il faut que le nombre que vous y mettiez soit capable d'y entrer.
Par exemple : mov   eax,   2 345 236 675 => fonctionne parfaitement, mais mov   eax,   5 434 955 786 => provoque une erreur.
C'est logique, puisque le nombre maximal stockable dans un registre 32 bits est 4 294 967 295.

Seconde façon d'indiquer la source

On indique un espace de stockage qui contient la valeur qu'on veut enregistrer.
Pour l'instant on va faire ça avec un registre.

Exemple :

mov eax, ebx    ; on enregistre le contenu d'ebx dans eax

Si ebx contient le nombre 45, par exemple, alors eax contiendra aussi ce nombre. Le processeur lit la valeur contenue dans ebx et la copie dans eax. Après l'instruction, eax et ebx contiennent tout deux la même valeur. (On ne change pas ebx, on copie seulement ce qu'il contient dans eax.)

Il y a une chose très importante qu'il faut savoir au sujet de cette seconde façon d'indiquer la source :
Les deux espaces de stockages, la destination et la source, doivent obligatoirement être de la même taille. Peu importe ce qu'ils contiennent, il faut toujours qu'ils soient de la même taille : ou bien tous les deux 32 bits, ou bien tous les deux 16 bits, ou bien tous les deux 8 bits.

C'est-à-dire que même dans le cas où eax contient seulement le nombre 5, ceci ne fonctionnera pas :

mov dx, eax    ; provoquera une erreur car eax mesure 32 bits et dx seulement 16

L'inverse ne marche pas non plus :

mov ebx, cx    ; créera une erreur car ebx mesure 32 bits et cx seulement 16

Terminons de faire la connaissance avec l'instruction mov en regardant ce code complet, qui enregistrera le nombre décimal 67 dans eax.

.386
.model flat, stdcall
option casemap : none

include \masm32\include\masm32rt.inc

.code

salut :

   mov eax, 67

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

Lancez-le.

Mais ça change rien ! C'est exactement comme avant ! :o

Non, en réalité eax a bien été modifié et il contient bien le nombre 67. Mais il n'apparaît pas à l'écran car on n'a pas demandé à l'ordinateur de l'afficher.

L'ordinateur fait ce qu'on lui demande, ni plus ni moins. On lui a dit de mettre 67 dans eax, il l'a fait fait. On ne lui a pas demandé d'afficher eax à l'écran, pourquoi le ferait-il ?

Il nous suffit d'ajouter dans notre programme un ordre à l'ordinateur lui enjoignant d'afficher eax. Alors, nous verrons le nombre 67 apparaître à l'écran.

Pour cela nous allons encore utiliser une macro : print.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Commentons, commentons La macro print

La macro print

Première instruction : mov Un petit problème

La macro print

Nous découvrons maintenant une nouvelle macro qui va nous permettre d'afficher des choses à l'écran : print.

Pour l'instant, nous allons apprendre à afficher avec print :

  1. Du texte.

  2. Le contenu d'un registre.

Affichage d'un texte

Commençons par la première :

print  "Salut les zeros !"

Comme vous le voyez, j'écris le texte entre des guillemets, juste après print.

Voici le code complet :

.code

salut :

 print "Salut les zeros!"

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

A partir de maintenant je ne mettrai les codes complets qu'à partir de la directive .code ; sauf bien sûr dans le cas où les directives précédentes sont modifiées.

Essayez !

Euh, c'est tout collé, là... Il n'y aurait pas un moyen d'aller à la ligne, par hasard ?

Si, si. Essayez ça :

.code

salut :

 print "Salut les zeros!", 13, 10

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

Ces nombres provoquent un retour à la ligne. N'oubliez pas de les mettre après les guillemets. Sinon, ils feront partie du texte et apparaîtront à l'écran... :D

Pourquoi ces nombres et pas des autres ?

Encore une fois, il va falloir que vous me fassiez confiance. Mais ne vous inquiétez pas, je vous expliquerai tout cela bientôt. Pour l'instant, il vous suffit de savoir que ces nombres sont une espèce de code qui provoque un retour à la ligne.

Voilà, vous pouvez maintenant vous amuser à afficher les messages que vous voulez à l'écran.

Affichage d'un registre

Passons maintenant à l'affichage du contenu d'un registre :

Voici comment on fait :

print str$ (registre)

Mais c'est quoi ce truc : "str$" ? Tu ne nous en a pas parlé !

Ceci est une macro qui rend le contenu du registre apte à se faire afficher par print. Ne vous en occupez pas pour l'instant. J'expliquerai plus tard ce que cette macro fait dans les détails.

Pour l'instant apprenez seulement à l'utiliser. ;)

Pour eax, par exemple, ce sera donc :

print str$ (eax)

C'est clair ? Bien.

Nous pouvons maintenant afficher eax, après y avoir mis un nombre :

.code

salut :

   mov eax, 67     ; met 67 dans eax
 print str$ (eax)   ; affiche eax

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

C'est déjà mieux... Mais pas très compréhensible, car il n'y a pas de retour à la ligne.

Ajoutons donc juste après l'affichage d'eax :

print " ", 13, 10

Ce print va créer un retour à la ligne.
Voici maintenant le code final, avec le retour à la ligne.

.code

salut :

   mov eax, 67     ; met 67 dans eax
 print str$ (eax)   ; affiche eax
 print " ", 13, 10  ; retour à la ligne

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

Lancez... Parfait ! :)

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Première instruction : mov Un petit problème

Un petit problème

La macro print Un petit problème...bis

Un petit problème

Sans vouloir paraître rabat-joie, afficher juste 67 comme ça, c'est un peu nul. Il faudrait pouvoir faire quelque chose comme :

Contenu de eax : 67
Appuyez sur une touche pour continuer...

"C'est simple !", me diriez-vous... Il suffit de faire :

.code

salut :

   mov eax, 67

 print "Contenu de eax : "
 print str$ (eax)
 print " ", 13, 10

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

Oui, selon ce qu'on a étudié, cela devrait parfaitement fonctionner. Je vous laisse essayer... Allez-y, lancez l'assemblage !

...

...

Euh...ça me donne un autre nombre... J'ai fait une gaffe ? :euh:

Non, pas du tout, vous avez fait exactement ce qu'il fallait.

Alors pourquoi ça n'affiche pas eax ?

Ah ah ! Mystère...

Vous savez quoi ? Je ne vais pas vous le dire ! On va faire quelques expériences, et vous allez essayer de deviner par vous même pourquoi ça n'a pas marché !

Allons-y !

Expérience N°1

Tout d'abord essayons cela :

.code

salut :

; Premier affichage : sans le message "Contenu de eax"

   mov eax, 67

 print str$ (eax)
 print " ", 13, 10

; Deuxième affichage : avec le message "Contenu de eax"

   mov eax, 67

 print "Contenu de eax : "
 print str$ (eax)
 print " ", 13, 10

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

Ce qu'on fait ici, c'est qu'on affiche eax une première fois sans le message "Contenu de eax". Cela devrait normalement marcher, puisque tout à l'heure on l'a fait sans problème.
Juste après, on ajoute un affichge d'eax, mais cette fois précédé du message "Contenu de eax".

On lance et...

67
Contenu de eax : 17
Appuyez sur une touche pour continuer...

Oui, le premier marche bien puisqu'on voit 67. Mais ensuite la deuxième fois, ça bug.

Nous déduisons donc que le bug vient de la ligne print "Contenu de eax : ", puisque la première fois sans cette ligne, le print str$ (eax) fonctionnait. C'est seulement la seconde fois, lorsqu'on ajoute le print "Contenu de eax : ", il affiche un autre nombre.

Expérience N°2

On va faire une autre expérience :

.code

salut :

   mov eax, 67

; On affiche eax une première fois
 print str$ (eax)
 print " ", 13, 10

; On affiche eax une seconde fois
 print str$ (eax)
 print " ", 13, 10

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

Encore une fois, ça bug. La première fois on a le bon nombre, 67. Et ensuite, on a un autre nombre.

Par contre, si on fait ça :

.code

salut :

   mov eax, 67

 print str$ (eax)    ; On affiche eax une première fois
 print " ", 13, 10

   mov eax, 67       ; On remet le nombre 67 dans eax

 print str$ (eax)    ; On affiche eax une seconde fois
 print " ", 13, 10

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

Ca marche !

Il faut nous rendre à l'évidence : print modifie la valeur d'eax.

A chaque fois qu'on utilise print, la valeur d'eax est modifiée. C'est pour cette raison que lorsqu'on recharge à nouveau la valeur dans eax, ça marche.

Si vous refléchissez bien, vous avez une chance d'en trouver la raison... Bon, j'avoue, ce n'est pas simple... Mais essayez quand même. (Vous pouvez d'ailleurs faire d'autres expériences, pour vous aider.)

Et ne vous inquiétez pas, ne pas trouver est tout à fait normal. :)

Voici la réponse :

Réponse

Rappelez-vous, print est une macro. C'est à dire qu'elle représente en fait un bout de code, écrit à l'avance par d'autres programmeurs. Lorsque MASM arrive à un print, il remplace ce mot par le code de ces programmeurs.
C'est ce code qui affiche le texte ou le registre à l'écran.

Mais nous, est-ce qu'on sait ce qui se passe dans ce code ? Non, pas du tout...

Eh bien moi je vais vous le dire : A la fin de ce code, eax est modifié ! Oui, il y a une espèce de mov eax, machintruc qui change la valeur d'eax...

C'est pour cela que lorsque vous mettez un print après un autre, le deuxième donne une valeur bizarre. Tout simplement parce que tout à la fin du code du premier print, eax est modifié.

Une dernière expérience

Ou plutôt trois : Essayez le code des deux print d'affilé avec les registres ebx, ecx et edx.

...

Oui, tous sont modifiés, à part ebx.

Nous arrivons donc à une règle cruciale en assembleur :

Conclusion : La règle à retenir

Lorsqu'on utilise une macro écrite par un autre programmeur, et qu'on ne connaît pas son code, il faut s'attendre à ce que les registres soient modifiés durant cette macro.

Pour vérifier si une macro modifie un registre, on a deux solutions :

  • Lire le code de la macro.

  • Faire des essais (comme on vient de faire).

Nous sommes incapable pour le moment de réaliser la première possibilité, mais il nous reste la seconde.

Sachez que souvent, la plupart de registres sont modifiés.

Alors on fait comment pour stocker des données, si les registres sont sans arrêt modifiés par les macros ? :colere2:

C'est simple, nous allons stocker nos données en mémoire. Là-bas, les données seront à l'abri de toutes modifications. Bien entendu, les registres gardent leur utilité dans certains cas.

Je parlerai en détails du stockage de données en mémoire dans le chapitre suivant.

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
La macro print Un petit problème...bis

Un petit problème...bis

Un petit problème

Un petit problème...bis

Encore un dernier petit problème à régler...

Encore un problème ?!

Oui, un petit dernier pour la route. Mais après c'est terminé, promis.

Une question pour vous : Que feriez-vous, si je vous demande d'afficher le contenu du registre ax dans la console ? (Je précise ax, pas eax.)

Bon, vous ne pouvez pas répondre, alors voici ce que moi j'aurais fait :

.386
.model flat, stdcall
option casemap : none

include \masm32\include\masm32rt.inc

.code

salut :

   mov ax, 50

 print str$ (ax)

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

Mais ça ne marche pas ! Essayez, vous verrez, ça produit un beau bug !
La question que tout le monde se pose est : Pourquoi ?
Après tout, lorsqu'on a fait la même chose avec eax ça fonctionnait très bien.

En réalité, le problème concerne tous les registres de moins de 32 bits. Essayez d'afficher bx par exemple, ou un autre registre 16 bits, vous aurez le même bug.
Les registres 8 bits ne font pas exception : Tentez d'afficher ah, al, bh, etc. Impossible.

Bon, j'arrête de vous faire attendre et je vous dis tout de suite la raison de ce bug : La macro str$ peut gérer uniquement les espaces de stockage mesurant 32 bits.

Ce sont les programmeurs qui l'on codée comme ça...je ne sais pas pourquoi d'ailleurs. Après tout, ils auraient très bien pû la créer de façon à ce qu'elle marche aussi pour les autres tailles, mais ils ne l'ont pas fait. (En fait vous serez vous même, avant la fin de ce tuto, capables de créer une macro comme str$ qui fonctionnera aussi pour des tailles de 16 et 8 bits.) Mais pour l'instant, nous n'en sommes pas là. On va donc devoir se débrouiller avec la macro str$.

Voici donc ce que je vous propose : On va mettre le contenu d'ax dans eax, et afficher eax !

.code

salut :

   mov ax, 50       ; enregistrement du nombre dans ax

   mov eax, ax      ; copie du registre ax dans le registre eax
 print str$ (eax)   ; affichage de eax

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

N'importe quoi ! Tu nous as toi-même dit qu'il était impossible d'utiliser mov avec des registres de tailles inégales !

Désolé...c'était juste pour voir si vous suivez. :ange:

Oui, vous avez parfaitement raison, on ne peut pas faire ça. C'est pour cela que l'on va devoir utiliser une nouvelle instruction, un cousin pas très éloigné de mov... Je vous présente : movzx.

Non, ce n'est pas une faute de frappe, c'est son vrai nom. Il veut dire mov Zero eXtend. C'est à dire "mov avec extention par zéro".
Pas de panique, c'est très simple : movzx va agrandir le contenu du petit registre pour qu'il fasse la même taille que le grand puis il va copier ce nombre dans le grand registre.

Un exemple : Supposons que j'aie le nombre 4 dans bx. En binaire, c'est 0100. C'est à dire que bx contient :

0000 0000 0000 0100

(Notez que j'ai mis 16 bits, car bx est un registre 16 bits. Bien entendu, les zéros avant le nombre ne le modifient pas, tout comme en décimal. Donc : 100 = 0100 = 0000 0000 0000 0100)

Voici donc nos 16 bits. Je veux maintenant les mettre dans eax. Je fais alors movzx eax, bx et voici ce qu'il se passe : Tout d'abord, movzx étend notre nombre de 16 bits à 32 bits, en ajoutant des zéros avant celui-ci :

0000 0000 0000 0000 0000 0000 0000 0100

Puis, il peut enfin agir comme un mov normal : Il copie cette valeur dans eax.

Vous comprenez maintenant le sens du mot extension par zéro.

Voici donc le code pour afficher ax en utilisant movzx :

.code

salut :

   mov ax, 50         ; enregistrement du nombre dans ax

   movzx eax, ax      ; copie du registre ax dans le registre eax en utilisant movzx
 print str$ (eax)     ; affichage de eax
 print " ", 13, 10

 inkey "Appuyez sur une touche pour continuer..."
 exit

end salut

Excellent, ça fonctionne !

Maintenant vous pouvez vous entrainer à afficher d'autres registres de petites tailles. Tenez ! Un petit exercice : Affichez-moi les registres ch et cl après y avoir stocké un nombre. Le programme doit donner quelque chose comme ça :

Contenu de ch : 40
Contenu de cl : 35
Appuyez sur une touche pour continuer...

Bonne chance ! :)

Avant de nous quitter, je tiens à remplir une promesse. J'avais promis de vous montrer comment changer les couleur de RadASM, pour ceux qui n'aimeraient pas le noir.

Cliquez en haut sur l'onglet "Options", puis sur "Couleurs et mots clés". Dans la fenêtre qui apparaît, cliquez sur le menu déroulant en haut. Vous avez le choix entre plusieurs thèmes. Le thème actuel est 'black night', (il l'a pas volé). Choisissez un thème et cliquez sur 'Charger', puis faites 'Valider' ou 'Appliquer' , en bas.

Ne touchez à rien d'autre dans cette fenêtre, vous pourriez faire des choses irréparables ! :-°

Un long chapitre plein de nouveautés se termine... Des nouveautés toutefois différentes de celles auxquelles je vous ai habitués jusqu'ici ; car nous avons enfin fait le grand pas : un grand pas dans la pratique.
Et ce n'est pas terminé, le chapitre suivant ira plus haut, toujours plus haut... :-°

  • En profondeur avec l'assembleur
  • Partie 1 Les bases +
    • Programmer ? C'est quoi ? +
      • Le programme
      • Les instructions
      • L'assembleur
      • MASM
    • Les systèmes de numération (Partie 1) +
      • Les systèmes de numération
      • Les chiffres et les nombres
      • La notation positionnelle
      • Le système décimal
      • Le système binaire
      • Le système hexadécimal
    • Une petite pause, les outils +
      • Découvrons les outils
      • Téléchargements
      • Installation
      • Configuration de RadAsm
    • Comment ça marche (1/3) +
      • LA question
      • Avant de commencer
      • La mémoire en détails
      • Encore un peu de binaire
      • Une mémoire bien organisée
      • Le sens du mot byte
    • Comment ça marche (2/3) +
      • Un peu d'histoire
      • Le 8086 & Cie
      • Le cerveau de la bande
      • L'UAL
    • Comment ça marche (3/3) +
      • Transports en commun
      • Bus à impériale
      • Capacité d'adressage
      • Rapide historique
    • Un minimum de code +
      • Créer un nouveau projet
      • Présentation de l'environnement
      • Les mots-clés
      • Code minimal
    • Enfin un programme ! +
      • Commentons, commentons
      • Première instruction : mov
      • La macro print
      • Un petit problème
      • Un petit problème...bis
Un petit problème


Vu dans ... https://asm.developpez.com/intro/

Introduction au langage assembleur Intel

I. Introduction

Le processeur d'un ordinateur ne comprend aucun langage de programmation ni Turbo Pascal, ni assembleur, ni même C++ ou Java (du moins 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 cela 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 (mis à 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 tels la souris, l'écran, ou la carte vidéo. Certains vétérans de la programmation n'utilisent que l'assembleur, certains pirates en particulier. Pourquoi ? Pour faire les plus petits programmes possible, car le compilateur ne fait que traduire l'assembleur en code machine (ce qu'on pourrait presque faire manuellement avec de l'entrainement). Et en même temps savoir exactement ce que contiendra notre fichier binaire (fichier exécutable, « .exe » ou « .com » sur les plates-formes Microsoft).

Personnellement, je vous déconseille vivement du faire du 100 % ASM (ASseMbleur), car on se perd vite dans le code source (qui atteint 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à existantes 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.

II. Commençons par le vocabulaire

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 plates-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'exponentiel 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 !).
  • 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 : quatre octets = 2 doubles mots = 32 bits. Signé : valeurs de -2 147 483 648 (-2^31 = -2147 millions) et +2 147 483 647 (2^31 -1), non signé : entre 0 et +4 294 967 295 (quelque 4294 millions).
  • Registre : espace mémoire placé physiquement dans le cœur du processeur dans lequel sont stockées 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ée 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émoire (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 pour les registres 32 bits, les registres 16 bits, les offsets SI,DI,SP,BP en sont leur partie basse.
    Consultez la Liste des registres.
  • 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ées 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 constante : 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'elles : l'interruption 09h du clavier est appelée à chaque pression ou relâchement d'une touche.
    Voir la liste complète des interruptions.
  • Pile : la pile pourrait être comparée à une pile d'assiettes, 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 voit 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-ci sont des bits internes au processeur.
    Consultez la Liste des drapeaux.

III. Liste des registres

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/S : offset mémoire de la pile.

IV. Liste des drapeaux

  • AF : Auxilliary Flag = indicateur de retenue auxiliaire.
  • 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.

V. Liste des instructions

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 plupart 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 » signifie que l'instruction est en 8 bits (utilise si nécessaire le registre AL).
  • « W » signifie que l'instruction est en 16 bits (utilise si nécessaire AX).
  • « D » signifie que l'instruction est en 32 bits (utilise si nécessaire 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

    Équivalant en Turbo Pascal : « {destination} := {destination} and {masque} ».
    Équivalant 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-ci 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 suivi 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ésidents comme le pilote de la souris par exemple. Voir aussi RET.
  • 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é | 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é | 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} : écrit l'adresse de {source} dans {destination}. Équivaut à « 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]).
  • 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} : étend à 32 bits le nombre contenu dans {src} (8 bits) et transfère 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

    Équivalant en Turbo Pascal : « {destination} := not {destination} ».
    Équivalant 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

    Équivalant en Turbo Pascal : « {destination} := {destination} or {masque}; ».
    Équivalent en C : « {destion} |= {masque} ».

    Voir aussi AND, NOT et XOR.
  • OUT {source},{port} : écrit 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.
  • POP {registre} : sort une valeur de la pile et la stocke dans un [registre].
  • REP {instruction}: répète l'instruction [instruction] 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).
  • 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.

    Équivalant en Turbo Pascal : « {destination} := {destination} xor {masque} ».
    Équivalant en C : « {destion} ^= {masque} ».

    Voir aussi AND, NOT et OR.

VI. Liste des interruptions

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éboguer).
  • 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 standard 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, coprocesseur…).
  • 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 standard 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ètres 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épandu 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 : Eerreur fatale d'un vecteur d'interruption.
  • 25h : l'interruption DOS : lecture directe d'un disque.
  • 26h : l'interruption DOS : écriture directe sur un disque.
  • 27h : l'interruption DOS : programmes résidents.
  • 28h : fin d'un programme restant résident en mémoire.
  • 2Fh : interruption pour plusieurs sous-programmes. Gestion du réseau, driver CD-Rom MSCDEX…

VII. Exemples concrets

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


Vu dans ... http://venom630.free.fr/geo/tutz/programmation/asm/intro_masm32/

Débuter l'assembleur avec MASM32

Eh oui, ça y est, je me décide *enfin* à écrire un tutoriel sur le langage assembleur avec MASM32. Je me suis souvent dis que ça ne tiendrait pas la route, en raison de mes vagues connaissances à ce sujet. Mais c'est le fait d'écrire ce tutoriel (je ne sais pas s'il sera grand, ça, on verra plus tard) qui consolidera peut-être ce savoir. Et, bien évidemment, j'espère que ce "savoir" se diffusera aussi aux lecteurs, c'est-à-dire vous !

Pour ceux qui ne connaissent pas le codage de l'information et d'autres pré-requis, absolument pas de panique. Ayant envie d'entrer dans le vif du sujet dès le début, je mettrai les pré-requis en annexes. Car normalement, pour commencer l'assembleur, il faut être patient. Les tutoriels vous demandent de maîtriser le codage de l'information en bases 2, 10 et 16 ; c'est pas bien dur, hein, on s'y fait. Ils vous listent également l'histoire de l'architecture des processeurs INTEL, qui sont le cerveau de l'ordinateur. Ce qu'il faut savoir - je pense - c'est que le langage assembleur est en fait un intermédiaire entre le langage machine compris par le processeur, et l'humain. Le langage assembleur est donc, après le langage machine, le langage le plus bas niveau dans la programmation. Un schéma, pour résumer :

Schéma hiérarchique des langages

Je vous préviens, j'ai écrit n'importe quoi dans "assembleur" et "binaire" ; c'est juste pour vous schématiser la tronche des langages, quoi.

En résumé, ce tutoriel va vous apprendre à "parler" le langage du processeur. Ca serait bête de vous apprendre à écrire une suite d'octets en hexadécimal (même si ça se fait, pour concevoir des shellcodes ou taper dans les fichiers binaires via un éditeur hexadécimal) alors qu'on a le langage assembleur pour faire ça.

Rhaaa, je vous mets en appétit, mais je dois tout de même vous mettre en garde sur certains pré-requis à avoir.
  • Faire de l'assembleur, je ne vous le cache pas, c'est très compliqué. En plus, on va en faire sous Windows, et je vous demande d'avoir des bases en C. C'est impératif, et vous comprendrez plus tard pourquoi.
  • Tourner sur un windows 32 bits. Si vous êtes sur une architecture 64 bits, j'ai le regret de vous annoncer que vous ne pourrez vous servir de ce tutoriel qu'à des fins théoriques.
  • J'allais oublier the most important : avoir windows.

Enfin, je tiens à préciser que ce tutoriel s'adresse aux curieux comme moi, et à ceux qui veulent un peu comprendre comment ça fonctionne. Je ne ferai jamais de vous des programmeurs en assembleur. Ce langage est dépassé et ne vous servira pas à écrire une application complète (quel intérêt, quand on a des langages évolués ?). C'est juste pour l'amusement.

Petits trucs "de base" à savoir

Dans notre ordinateur, on a plusieurs types de mémoire :
  • RAM (Random Access Memory) : la mémoire vive, donc tous les logiciels se servent pour stocker des informations dedans. Cette mémoire est grande, rapide, et se vide lorsqu'il n'y a plus de courant qui l'alimente. Néanmoins, on peut en trouver certaines qui sont constamment alimentées avec une pile de lithium ;
  • ROM (Read-Only Mémory) : la mémoire morte, ou "en lecture seule" comme on le traduit de l'anglais. Cette mémoire sert à stocker des informations permanentes, notamment sur le système d'exploitation de votre ordinateur, de votre BIOS, bref, des informations qui restent écrites et effaçable par réécriture. Pour réécrire la mémoire, il y a différents moyens - à titre informatif - comme exposer la mémoire aux rayons UV, par exemple (ouais, j'ai étudié ça en électronique, c'était marrant). Vous comprenez, par ailleurs, pourquoi on parle de CD-ROM. Parce qu'une fois gravés, on ne peut pas modifier les données, à condition que le CD soit "RW", mais là, il faut réécrire par dessus. ;
  • Disque dur : Grosse capacité de mémoire. Permet de stocker des fichiers à long terme. La mémoire des disques durs actuels va jusqu'à se mesurer en Terra-Octets, soit 1000 Go. Cette mémoire est lente, si on compare la rapidité d'accès de la RAM ;
  • Les registres : Certes, pour les non-connaisseurs, je vous prends en traitre. Voyons plus bas ce que sont les registres !

Les registres

Les registres sont de petites mémoires dont la taille dépend de notre architecture logicielle. Je m'explique : vous avez un système d'exploitation 32 bits, cela signifie donc que la taille de vos registres fait... 32 bits ! C'est très petit, me direz-vous, et "quel intérêt ?", vous me demandez donc. Patience, j'y viens.

Les registres sont de mémoires minuscules et très rapides. Elles sont, pour certaines, internes au processeur, et ce dernier s'en sert constamment pour faire les opérations de bases, que ça soit logiques - AND, OR, XOR... - et arithmétiques - addition, soustraction, multiplication, division, ... et donc pour traiter l'info. Il faut le rappeler : un ordinateur, c'est con ; on va pas lui demander en langage machine "Affiche-moi coucou à l'écran" en une seule instruction. Il y a plusieurs milliers d'instructions assembleur derrière. Enfin, je vous rassure, on n'ira pas écrire ces milliers d'instructions pour faire un bête "Hello world". C'est juste que c'est pas comme le C, où vous n'avez qu'à faire ça :
Code : c
#include <stdio.h>
int main() {
        printf("Hello, world!\n");
        return 0;
}

Mais enfin, je m'écarte du sujet, là. Voyons les quatre registres de base (eh oui, ils ont un nom !) :
  • Registre accumulateur : on s'en sert conventionnellement pour stocker des valeurs de retour de fonctions. On le note "eax" lorsqu'il s'agit de manipuler toute sa taille
  • Registre de base : conventionnellement... je sais pas à quoi ça peut servir. Un registre de base, quoi. Ca va pas vous empêcher de l'utiliser. On le note "ebx"
  • Registre de compteur : conventionnellement, ça sert pour faire des boucles. Noté "ecx"
  • Registre de données : "edx". On s'en sert - probablement - pour manipuler des... données !

Chaque registre a une taille de 32 bits, mais il est possible de les manipuler par plus petite taille. Prenons par exemple eax. On peut manipuler sa partie basse sur 16 bits, à savoir "ax". Cette même partie, ax, contient deux sous-parties. La partie haute est notée ah, la partie basse est notée al.

Un schéma pour résumer le tout :

Schéma de la structure des registres de base

Rassurez-vous, ça n'est pas bien grave si vous ne retenez pas tout ce remue-ménage. Tout ce qu'il faut savoir, c'est que les registres peuvent êtres manipulés par tranche de 32, 16 et 8 bits.

Notez qu'il existe d'autres registres que nous verrons dans la suite du tutoriel ; ça serait dommage de vous barber dès le début avec ça. Voyons maintenant la notion d'adresses :

Les adresses

Il faut impérativement comprendre cette notion pour continuer. En assembleur, quand on programme, on adressera nos données et nos sections de code par des labels, ou étiquettes. Sauf qu'une fois le code compilé, le tout est accessible par des adresses mémoires. Ces adresses sont codées sur....... 32 bits ! (ce qui explique pourquoi on parle d'OS 32 bits).

On représente souvent ses adresses en hexadécimal. Exemple : 0x7f3d215a, le 0x devant pour préciser que ce qui suit est de l'hexadécimal. La RAM contient des adresses mémoires, qui contiennent elle-même des données. Si vous avez fait du C et que vous êtes familiarisé avec la notion de "pointeur", vous devriez savoir de quoi je parle.

Et bien, sachez qu'en plus des valeurs de variables, les intructions assembleur sont aussi adressées. Une fois notre programme chargé en mémoire, les octets qui disent au processeur de mettre à zéro un registre - par exemple, hein - se trouvent à une adresse mémoire. Mais enfin, nous utiliserons OllyDbg - outil de debuging puissant - pour mieux comprendre comment ça fonctionne.

Ce qu'il nous faudra étudier, maintenant, c'est le concept de pile !

La pile

Les gens l'appellent souvent "stack". La pile est une zone de mémoire située dans la mémoire accessible en écriture allouée par le programme. Le principe de fonctionnement se résume par ce sigle : LIFO => Last In, First Out, ou "Dernier entré, premier sorti".

Prenez par exemple une pile d'assiettes :
|__________________|
|__________________|
|__________________|
|__________________|

On a quatre assiettes empilées. Si on veut retirer la deuxième assiette :
|__________________|
|__________________|
|__________________|
|__________________|

En partant du haut, il va falloir dépiler la première assiette. On se retrouve donc avec l'assiette rouge au sommet :
|__________________|
|__________________|
|__________________|

Pour enfin la dépiler. Elle a disparu :

|__________________|
|__________________|

Je vais prendre un exemple qui se rapproche plus de la situation réelle. La pile est une zone de mémoire, comme je vous l'ai dit. Elle a donc ses adresses mémoires. Elle est délimitée par deux registres qui font office de pointeurs :
  • esp : "Extended Stack Pointer", ou "pointeur étendu de pile". Ce registre contient et contiendra toujours, en tant que valeur, l'adresse mémoire qui pointe sur le sommet de la pile. Par exemple, si esp contient 0x00405632, alors le sommet de la pile pointera sur l'adresse mémoire 0x00405632, qui contiendra une valeur spécifique ;
  • ebp : "Extended Base Pointer", ou "Pointeur étendu de base". A ne pas confondre avec ebx. Le registre ebp pointe sur le bas de la pile.

Enfin, quelque chose à savoir : esp sera toujours plus petit qu'ebp. Plus l'on va vers le haut de la pile, plus les adresses seront basses. A l'inverse, plus l'on ira bas dans la pile, plus les adresses seront hautes.

Allez, encore un schéma qui vaudra mieux qu'un long discours :

La pile au beau milieu de la mémoire

Notez que j'ai volontairement mis des valeurs aléatoire dans la pile. C'est pour les représenter au beau milieu de notre zone de mémoire. On voit que le registre esp pointe sur le sommet de la pile, qui est adressée à 0xbfffffcb et la valeur à ce sommet est 1561561 (adresse que j'ai mise au pif). Cette adresse est basse, comparée à 0xbfffffff qui est pointée par ebp.

On remarque que la valeur 1561561 est au sommet de la pile. Si on la dépile, au final, on aura :

La pile au beau milieu de la mémoire, après dépilage de 1561561

Vous l'aurez compris, le registre esp s'est mis à pointer sur la valeur du dessous : 27527272. Notez qu'on peut aussi modifier une valeur au milieu de la pile - qui n'est donc pas nécessairement au sommet - mais on ne pourra pas la dépiler comme ça.

Quel est l'intérêt d'utiliser la pile ?

La pile est utilisée pour beaucoup de choses. Notamment pour sauvegarder la valeur des registres lorsqu'on appelle une routine (ou fonction) en assembleur. On se sert également de la pile pour empiler nos arguments avant d'appeler une fonction qui va elle-même les dépiler.

Je pense avoir dit le nécessaire pour vous faire *enfin* programmer !

All you need is...

Nous allons enfin passer à la pratique. Téléchargez Masm32 à cette adresse : http://www.masm32.com/masmdl.htm. Installez-le ensuite. Il se trouvera normalement dans C:\masm32. Je vous demanderai de créer un dossier nommé "asm_sources" également à la racine du site. Ça facilitera votre organisation. C'est dans ce dossier que nous ferons des sous-dossiers pour chaque programme compilé. Une fois le dossier asm_sources créé, allez dedans, et créez un nouveau dossier nommé hello. Dedans, créez le fichier hello.asm, et inserez-y le code suivant :
Code : asm
.386
.model flat, stdcall
option casemap :none

include \masm32\include\msvcrt.inc
include \masm32\include\kernel32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\msvcrt.lib

.data
    Phrase db "Hello !",10
    Taille dd 8

.code
start:
    mov eax, Taille
    push eax
    push offset Phrase
    push 1
    call crt__write

    push 0
    call ExitProcess
end start

Tout ce code permet de faire :
Hello !

Pour le compiler, faites un fichier "make.bat" dans le répertoire du fichier, et mettez-y les instructions suivantes :
Code : dos
@echo off
\masm32\bin\ml /c /Zd /coff hello.asm
\masm32\bin\Link /SUBSYSTEM:CONSOLE hello.obj
pause

Il vous suffira simplement de double-cliquer sur "make.bat" pour voir, normalement, apparaître :
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997. All rights reserved.

Assembling: hello.asm
Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

Appuyez sur une touche pour continuer...

La ligne 2 du fichier bat réalise la transformation du code source en code objet ; du code machine directement compréhensible par le processeur. Enfin, la troisième ligne réalise l'édition de liens grâce au fichier objet hello.obj qui a été généré lors de l'instruction en ligne 2. Au final, vous avez un fichier exécutable pondu dans le même répertoire que vos autres fichiers. Pour le lancer :

Microsoft Windows XP [version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

C:\Documents and Settings\Geoffrey>cd ../../asm_sources/hello

C:\asm_sources\hello>hello
Hello !

C:\asm_sources\hello>

Sans trop de difficulté, on a fait notre premier programme en assembleur. Voyons maintenant pas à pas la signification des lignes du code source (quand même !)

Les directives

En assembleur masm32, on utilise des directives pour indiquer certaines options au compilateur. Ici, nous avons :
Code : asm
.386
.model flat, stdcall
option casemap :none

include \masm32\include\msvcrt.inc
include \masm32\include\kernel32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\msvcrt.lib

Première ligne : .386. Cette directive indique que l'on va utiliser le jeu d'instructions d'un processeur x86 32 bits. Ne m'en demandez pas plus à ce sujet, je sais juste qu'il est important de mettre cette ligne.
Seconde ligne : .model. Cette directive spécifie le modèle de mémoire du programme. "flat" signifie qu'on va travailler avec des adresses mémoires "lointaines" ou "prêtes". "stdcall" indique que, lors d'appels aux fonctions, les arguments devront être empilés de droite à gauche.

Qu'est-ce que ça signifie ?

Et bien, reprenons le code assembleur, où je fais appel à la fonction write. Sa signature en C est la suivante :
Code : C
#include <unistd.h>
ssize_t write(int fildes, const void *buf, size_t nbyte);

La fonction write prend trois arguments :
  • fildes : "FILe DEScriptor", ou descripteur de fichier. La fonction write peut, en effet, écrire dans des fichiers, ou dans les flux standards tel que stdout (la sortie, l'écran) ;
  • buf : "BUFfer", ou "tampon". Il s'agit d'un pointeur vers les données à écrire ; il s'agit généralement d'une chaîne de caractères.
  • nbyte : "Number of BYTE", ou "nombre d'octets". Indique le nombre d'octets à écrire.

Il y aura trois arguments à empiler, et comme on fonctionne de "droite à gauche", on empilera d'abord nbyte, puis *buf, et enfin fdes. Enfin, continuons après cette brève parenthèse.

option casemap :none indique au compilateur que les noms d'étiquettes seront sensibles à la casse. C'est-à-dire que l'étiquette hello - qui pointera sur un bloc de données - et Hello - qui pointera sur un autre bloc de données - seront différentes. C'est comme les noms de fonction en C : on vous a normalement appris que Main() et main(), c'est pas pareil !

Passons aux include et includelib (je récapitule) :
Code : asm
include \masm32\include\msvcrt.inc
include \masm32\include\kernel32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\msvcrt.lib

la directive "include" en masm32 correspond à un "#include" en C. Les fichiers .inc contiennent les signatures des fonctions à utiliser. Eh oui, sous Windows, on doit se servir des fonctions "mappées" dans les fichiers DLL. Les fichiers inc contiennent les définitions des fonctions.

Enfin, la directive includelib indique au compilateur qu'on veut se servir des fichiers lib qui, eux, contiennent carrément le code des fonctions.

Vous avez vu que je me sers de kernel32.lib/inc et msvcrt.lib/inc. Kernel32 contient la fonction ExitProcess(), qui consiste à quitter proprement le programme pour rendre la main au système d'exploitation (il y a du code qui s'en charge, et dont on ne doit pas se soucier) et msvcrt contient le code de la fonction write(), que je vous ai décrite plus haut.

Les sections

Dans notre code, nous avons déclaré deux sections. Voyons la première :
Code : asm
.data
    Phrase db "Hello !",10
    Taille dd 8

La directive ".data" indique le début de notre section data, qui contient des données initialisées. C'est là ça devient intéressant. En fait, cette section sert à déclarer des données dont nous nous servirons dans le code.

Code : asm
Phrase db "Hello !",10

Cette instruction signifie que je déclare une chaîne de caractères qui contient le mot "Hello !", suivi de l'octet 10, qui correspond à un retour à la ligne. Phrase désigne un label afin de pointer aisément sur notre chaîne de caractères. enfin, l'inscruction db signifie "declare byte", afin de préciser que les unités ont pour taille 1 octet (8 bits).

Code : asm
Taille dd 8

Même principe, sauf que dd signifie "declare double". Un "double", ici, correspond à quatre octets (32 bits). Le label se nomme Taille, et la donnée est de '8' ; étant codée sur quatre octets, on aura donc, en hexadécimal, 0x00000008. Cette donnée correspond en fait à la taille de la chaîne de caractère "Hello !",10. Voici, après compilation, l'organisation de la section data :
Code :
+---+---+---+---+---+---+---+----+----+----+----+----+-------------
|'H'|'e'|'l'|'l'|'o'|' '|'!'|0x0A|0x08|0x00|0x00|0x00| ..........
+---+---+---+---+---+---+---+----+----+----+----+----+-------------
  ^                                ^
  |                                |
  |                                |
Phrase                            Taille

Vous verrez que j'ai volontairement représenté certains octets sous leur forme hexadécimale. 0x0A correspond, en décimal, à la valeur 10, donc à notre retour à la ligne. Enfin, vous verrez que Taille pointe sur 0x00000008, qui est une valeur écrite à l'envers en mémoire. Elle est écrite à l'envers car on ne veut pas lire "quatre octets à la suite", "mais un entier à la fois". Dès lors, pour représenter, par exemple, le nombre 0x65231489 en mémoire, ça sera :
Code :
+----+----+----+----+----------
|0x89|0x14|0x23|0x65|..........
+----+----+----+----+----------

Mais revenons-en à notre schéma plus au-dessus, où j'ai représenté les données du programme. J'ai montré que Phrase et Taille pointaient sur ces données. Ces labels correspondront en fait, après la compilations, à des adresses mémoires.

Si Phrase correspond à l'adresse mémoire 0x00401000, alors Taille correspondra à l'adresse mémoire 0x40100008, car Phrase pointe sur huit octets. Enfin, vous verrez que si vous vous mettez à pointer sur Phrase+8, vous retomberez sur Taille.

Ce qu'il faut savoir, en résumé, c'est que le fait de déclarer des données de façon séquentielle reviens à les écrire de la même façon dans la section data de notre exécutable.

Étudions à présent la section .code, qui correspond à la section exécutable de notre programme. Cela signifie que les octets - qui seront en fait représentés par des instructions en assembleur - écrits dans cette section seront lus par le processeur et exécutés.
Code : asm
.code
start:

La directive "start:", elle, indique le point d'entrée du programme. C'est comme la fonction main() en C. C'est après cette directive que les choses deviennent intéressantes, mais intéressons-nous à l'algorithme logique du programme.

Comme nous l'avons vu, le programme s'est contenté d'afficher bêtement "Hello !", et de rendre la main au système d'exploitation. Il a donc fallu appeler write(), et ExitProcess(). En premier lieu, il faut donc empiler les arguments de write(), appeler la fonction, puis empiler les arguments de ExitProcess() et enfin appeler cette fonction.

Code : asm
    mov eax, Taille
    push eax
    push offset Phrase
    push 1
    call crt__write

    push 0
    call ExitProcess

La première ligne indique au processeur de mettre dans le registre eax la valeur pointée par le label Taille, à savoir 0x00000008. Le registre eax, après cette instruction, contiendra - sur 32 bits, soit 4 octets - 0x00000008. En assembleur intel, on a la synthaxe suivante :

mov destination, source

Et "destination", par l'intruction "mov", recevra en nouvelle valeur "source" et aura son ancienne valeur écrasée.

Code : asm
push eax

L'instruction push, en assembleur, consiste à mettre une donnée au sommet de la pile (à l'empiler, quoi... Je me complique la vie !). Ici, on a empiler la valeur d'eax, à savoir, 8. On a empilé notre troisième argument de write() ; vous vous souvenez quand je vous ai dit qu'il fallait empiler les arguments de droite à gauche, donc dans l'ordre inverse ?

Code : asm
push offset Phrase

On va encore empiler quelque chose, sauf qu'ici, il ne s'agit pas d'empiler une chaîne entière, mais un pointeur vers celle-ci. L'instruction offset indique que l'on va empiler l'adresse mémoire de notre chaîne. En C, on passe des pointeurs en paramètres. Et que contiennent les pointeurs ? Des adresses mémoires. C'est la même, vous dis-je !

Code : asm
push 1

Enfin, on empile la valeur 1. Cette valeur est représentée par défaut sur 32 bits (4 octets) puisque nous sommes sur un système d'exploitation 32 bits. Le nombre 1 correspond à la sortie standard, soit l'écran. Cela indique que l'on va écrire nos octets à l'écran. Et enfin, on appelle la fonction :

Code : asm
call crt__write

Pourquoi "crt__write" et pas "write" ?

Je ne sais pas ce que signifie crt (à vrai dire, j'ai pas vraiment cherché ; c'est pas bien, je sais) mais j'ai mené une petite recherche dans les fichiers .inc, et j'ai vu que la fonction write() était déclarée en "crt__write".

L'instruction call correspond à un saut très très lointain sur une autre adresse mémoire. Lors de la compilation, le compilateur se débrouillera pour remplacer crt__write par une adresse mémoire sur laquelle sauter. Oui, les fonctions qui se situent dans les DLL se situent à des adresses, toute donnée est adressée. Le call se charge aussi d'empiler l'adresse de l'instruction qui se situe juste derrière ; cela permettra au processeur de revenir où il en était après l'exécution de la fonction.

On vient donc d'exécuter cet équivalent en C :
Code : C
write(1,"Hello !\n",8);

Il nous faut enfin quitter le programme proprement. En C, cela reviendra à utiliser :
Code : C
ExitProcess(0);

la fonction prend un unique argument ; sa valeur, 0, indique que le déroulement du programme a été normal, et qu'on le quitte proprement. On peut utiliser d'autres codes en fonction d'éventuelles erreurs produites pendant le déroulement du programme, que ça soit 1, -1... 1 reste la valeur conventionnelle aussi bien sous Windows que Linux (je connais pas Mac, par contre. J'aime pas les pommes).

De ce fait :
Code : asm
    push 0
    call ExitProcess

Consiste à mettre la valeur 0x00000000 sur la pile et d'appeler la fonction ExitProcess(), qui va terminer proprement le processus en rendant la main au système d'exploitation (qui, lui, aura récupéré le 0 passé à la fonction. On peut traiter ça avec des algos, mais nous n'expliciterons pas la chose).

Et enfin, on indique la fin de la routine principale :
Code : asm
end start

Dites-moi, sérieusement... Il y avait quoi de dur ?

Désassembler le programme

Je vous avais promis une session de debug avec OllyDbg. Commencez par le télécharger à cette adresse : http://www.ollydbg.de/odbg110.zip. Décompressez-le dans un dossier quelconque, que ça soit sur le Bureau ou dans "Mes Documents", ça n'a pas d'importances. Ouvrez la bête, et faites File > Open, enfin, cherchez votre fameux hello.exe que l'on a compilé ensemble. Voici une sous-fenêtre que vous devriez normalement obtenir :

Sous-fenêtre d'Olly Dbg

Pour ceux qui n'ont jamais utilisé cet outil, ne vous en faites pas, j'ai numéroté les parties pour vous expliquer en détail.
  1. La section code de notre programme. Il y a, tout en haut, la première instruction qui sera exécutée. La "scrollbar" s'est, en effet, positionnée au point d'entrée. Dans cette fenêtre, on a quatre colonnes qui représente respectivement : l'adresse mémoire de l'instruction, l'équivalent hexadécimal de l'instruction (son équivalent en langage machine pur et dur, quoi), l'instruction assembleur, et, enfin, des informations supplémentaires sur l'instruction.
  2. Cette frame donne l'état des registres. Vous remarquez qu'il y a EAX, EBX, ECX, EDX, EBP, ESP et d'autres que je n'ai pas explicités. C'est le moment, si on veut. ESI (Extended Source Index) et EDI (Extended Data Index) sont des registres qui sont manipulables dans le code. A quoi ils servent... Je ne saurais pas vous dire. EIP, lui, signifie Extended Instruction Pointer. Ce registre contient l'adresse de l'instruction suivante. Ici, EIP contient 00401000, ce qui signifie que l'instruction qui va être exécutée sera celle à l'adresse 00401000, soit MOV EAX,DWORD PTR DS:[403008], qui correspond, dans notre code, à "mov eax, Taille".
  3. Cette fenêtre montre la pile. En haut de la frame, on a le sommet de la pile. Pour être fixé, regardez la valeur d'ESP - 0012ffc4 - et l'adresse en haut de la frame : ce sont les mêmes.
  4. La section data. On remarque, par ailleurs, que nos données sont bien écrites les unes à la suite des autres, comme je vous l'avais schématisé.

Pour exécuter une instruction à la fois, appuyez sur F7. Hop, vous venez d'exécuter MOV EAX,DWORD PTR DS:[403008]. regardez à présent la valeur d'EAX : le registre contient 0x00000008. Ensuite, tracez jusqu'à CALL DWORD PTR DS:[402008]. Là, vous avez deux options :
  • Soit vous appuyez sur F7, et vous tracez pas à pas le code assembleur de la fonction write(), qui correspond à un gros bordel que moi-même je ne connais pas.
  • Sois vous appuyez sur F8, et vous tracez le CALL sans entrer dedans. C'est comme si vous traciez une instruction normale.

Que dire d'autre à propos d'Ollydbg ? Il est pratique si vous avez fait un programme et qu'il y a un bug que vous avez du mal à résoudre. Ca vous permet de debugger facilement vos futurs programmes écrits en MASM32, en tout cas.

Conclusion

J'ai explicité pas mal de trucs dans cette introduction, tout comme j'en ai oubliés certains. Il existe pas mal de tutoriels qui vous explique bien plus de fondamental que ça, à savoir, les instructions de saut conditionnels/inconditionnels (pour implémenter les boucles ou les conditions, par exemple), le pop qui permet de dépiler une valeur (on l'a pas vu, mais c'est pas bien grave), les registres d'état, etc. Des liens pour vous guider :

http://win32assembly.online.fr/. Bien que le site soit en anglais, c'est pour moi une mine d'or pour les curieux.
http://jeanfrancoisdelnero.free.fr/prog.htm Contient quelques progs en assembleur, et une petite section lien en bas de page.

Ce que j'ai à dire pour ma part : l'assembleur, c'est pas méchant. C'est sympa si on veut comprendre comment fonctionne la programmation au plus bas niveau. Sous windows, on fait des appels de fonction. Sous linux, le noyau fournit directement des appels noyau grâce à des interruptions, ce qui rend la programmation encore plus simple puisqu'il ne faut pas nécessairement faire de CALL. Enfin, ça, c'est une autre affaire.

Enfin, je tiens à dire que je suis assez amateur, et que ce tutoriel n'est pas exhaustif et contient de très nombreux défauts. J'espère juste que ceux qui l'ont lu ne s'en sortiront pas sans n'avoir rien appris ! Pour toute question : geo [point] 669 [at] gmail [point] com.
J'oubliais : énorme merci à Overcl0k, sans qui la rédaction de ce tutoriel n'aurait probablement pas eu lieu.

Geo


Logiciel - MASM - Présentation du logiciel

Section courante

Gladir.com - Logiciel - MASM - Présentation du logiciel

A propos

  • Accueil de Gladir.com
  • Notes légales de Gladir.com
  • Flux RSS des nouvelles du site

Section administrative du site

  • Section du logiciel DOS «MonsterBook»
  • Inventaire de la bibliothèque de Gladir.com
  • Entrée administrateur
Fiche technique
Système d'exploitation : | DOS/Windows/OS/2
Auteur : | Microsoft
Date de publication : | 1981 à actuellement
Catégorie : | Compilateur assembleur

Sommaire

Le logiciel Macro Assembler de Microsoft est un compilateur pour le langage de programmation Assembleur 80x86 et 64 bits. Il s'agit de l'un des compilateurs assembleurs 8086 les plus ancien à avoir survécu à tous les changements d'environnement matériel et logiciels s'étant produit depuis les 40 ans dernières années. Jusqu'à la version 5.0, le compilateur était uniquement disponible pour le système d'exploitation MS-DOS. Ensuite, à partir de la version 5.1 jusqu'à la version 6.11, il était disponible pour les systèmes d'exploitation MS-DOS et OS/2. Ensuite, il fut uniquement proposé pour le système d'exploitation Windows avec l'exécutable «ML.EXE» ou «ML64».

Version

Voici les différentes versions du MASM :

Version | Année | Description
MACRO-86 | 1981 | Produit originale, inclus le M86 (prédécesseur de MASM), LINK, LIB and CREF
1.00 | 1981 | Première version officielle de MASM
1.00 | 1981 | Version proposé par IBM
1.10 | 1982 | Support des instructions du coprocesseur mathématique 8087 et du DOS 2.0 (fichier gérer par des Handle)
1.25 | 1983 | Support de l'émulation du 8087
1.27 | 1984 | Corrections de bogues mineurs
2.00 | 1984 | Version proposé par IBM, support d'instructions 80186, 80286 et 80287, ajout de directives .186, .286c, .287, .8086, .8087
3.00 | 1984 | Support des instructions du mode protégé du 80286, ajout des utilitaires MAKE, SYMDEB et MAPSYM, ajout de directive .286p.
3.01 | 1985 | Mise à jour mineurs sur les utilitaires SYMDEB et MAPSYM.
4.00 | 1985 | Ajout des utilitaires EXEMOD et EXEPACK, ajout de directives .err, .err1, .err2, .errb, .errdef, .errdif, .erre, .erridn, .errnb, .errndef et .errnz.
5.00 | 1987 | Support des instructions 386 et 387, simplification des directives de segment, support de multiples modèle de mémoire, variables communes, utilisation des mémoires d'environnement MASM, INCLUDE et TMP, aide de la ligne de commande, ajout de CodeView (CV) (remplaçant les utilitaires SYMDEB et MAPSYM) et CVPACK, ajout des directives .286, .386, .386c, .386p, .387, align, .alpha, byte, .code, comm, .const, .data, .data?, df, dosseg, dword, .errdifi, .erridni, .fardata, .fardata?, fword, ifdifi, ifidni, .model, .msfloat, qword, .seq, .stack, tbyte et word
MASM/2 | 1987 | Version proposé par IBM, version spécifique au système d'exploitation OS/2 basée sur MASM 4.00, inclut les versions liées de MASM, LINK, LIB, CREF, MAKE et EXEMOD, comprend la version DOS (CV) et la version OS/2 (CVP) de CodeView.
5.10 | 1988 | Ajout des étiquettes locales (@@, @B, @F), macro de fonctions de chaînes de caractères (CATSTR, INSTR, SIZESTR, SUBSTR), un support OS/2, ajout des utilitaires BIND, EXEHDR et IMPLIB, ajout des directives catstr, elseif, elseif1, elseif2, elseifb, elseife, elseifdef, elseifdif, elseifdifi, elseifidn, elseifidni, instr, sizestr et substr.
5.10A | 1989 | Corrections de bogues mineurs.
5.10B | 1989 | Inclus avec le Windows 3.1 DDK
6.00 | 1991 | ML au lieu de MASM, ajout du pilote de compatibilité MASM, support d'instructions 486, assemblage multi-passes, prise en charge du modèle de mémoire FLAT, prise en charge du modèle TINY (pour les fichiers .COM), meilleur traitement des macros, allongement automatique du saut conditionnel, prototypes de fonctions de langage de haut niveau et appels (INVOKE et PROTO), ajout de NMAKE (remplaçant MAKE) et de H2INC, ajout de QuickHelp (QH), ajout de PWB (Programmer's Workbench), enlève les utilitaires CREF, EXEMOD et EXEPACK, ajout de directives .486, .486p, .dosseg, echo, .exit, extern, externdef, for, forc, goto, includelib, invoke, .listall, .listif, .listmacro, .listmacroall, .no87, .nocref, .nolist, .nolistif, .nolistmacro, option, popcontext, proto, pushcontext, real4, real8, real10, repeat, sbyte, sdword, .startup, struct, subtitle, sword, textequ, typedef, union et while.
6.00B | 1992 | Correction de bogues mineurs, c'est la dernière version à avoir un pure exécutable DOS.
6.10 | 1992 | Support de COFF, Windows, utilise le DOSXNT DOS extender (exécutable DOS/Windows), enlève le support OS/2, BIND et CVP.
6.10a | 1992 | Distribution de patch, correction de bogues mineurs.
6.11 | 1993 | Support d'instructions Pentium, dernière version commerciale, ajout de directive .586, .586p et alias.
6.11a | 1994 | Distribution de patch, correction de bogues mineurs.
6.11c | 1994 | Inclus avec le Windows 95 DDK et le Windows NT 3.5 DDK.
6.11d | 1995 | Distribution de patch, correction de bogues mineurs, dernière version DOS.
6.12 | 1997 | Distribution de patch, support du Pentium Pro et des instructions MMX, ajout des directives .686, .686p et .mmx.
6.13 | 1997 | Distribution de patch, support d'instructions K3D d'AMD, ajout de la directive .k3d.
6.14 | 1999 | Distribution de patch, support d'instructions SSE, ajout des directives oword et .xmm.
6.15 | 2000 | Inclus avec le Visual Studio 6.0 Processor Pack, support d'instructions SSE2.
7.00 | 2002 | Inclus avec le Visual Studio .NET 2002, le COFF est le format d'objet par défaut, le fichier «ML.ERR» est enlevé, les messages textes sont intégrés dans le ML.EXE.
7.10 | 2003 | Inclus avec le Visual Studio .NET 2003.
8.00 | 2005 | Inclus avec le Visual Studio 2005, support des instructions SSE3, ajout de l'exécutable ML64.
9.00 | 2008 | Inclus avec le Visual Studio 2008, support des instructions SSSE3, SSE4.1 et SSE4.2.
10.00 | 2010 | Inclus avec le Visual Studio 2010, support des instructions AVX et x64, enlève le support d'assemblage 16 bits.

Concurrent

Les concurrents du MASM sont FASM (Flat Assembler) de Tomasz Grysztar, gas (GNU Assembler), NASM (Netwide Assembler), TASM de Borland et le Yasm.

Dernière mise à jour : Dimanche, le 3 juillet 2016


Accueil

Accueil CHM

Normal, dit comme ça,
vous ne risquez pas de comprendre.


ASM : Créer une dll - CodeS SourceS

Appeler une routine ASM à partir d'un prog en C (mode console)

Pour les débutants comme moi, voici comment utiliser un prog ASM dans un prog C et inversement en mode console.

----------------------------------------------------------------------------------------------------------------------

module C;

     #include <stdio.h>
     #include <stdlib.h>

     extern int asm_main

     int main()
     {
      int retour;
      retour=asm_main();
      printf("le resultat de la routine asm_main est : %ld",retour);
      return 0;
      }

module asm;

        .586
        .model small,c

        asm_main	PROTO C

        .code

        asm_main	PROC C
            mov eax,10
            mov ebx,20
            add eax,ebx
            ret
        asm_main	endp

        end

vous assembler le module asm,vous renommer en .o au lieu de .obj,et vous lier avec le module C.

Appeler une routine C dans un prog asm (mode console)
-----------------------------------------------------

Vous devez utiliser un module C comme "lanceur" du prog asm.
C'est cela qui permet d'utiliser les librairies C.

module C;

     #include <stdio.h>
     #include <stdlib.h>

     extern int asm_main

     int main()
     {
      int retour;
      retour=asm_main();
      return 0;
      }

module asm:

    .586
    .model small,c

    printf PROTO NEAR,string:DWORD,P1:DWORD
    asm_main	PROTO

    .data

    format	byte '%ld',13,0

    .code

    asm_main PROC PUBLIC uses ecx

        mov eax,30
        mov ecx,30
        add eax,ecx
        invoke printf,offset format,eax

        mov eax,0
        ret

    asm_main endp

    end

Pour tranformer les protypes des fonctions C en syntaxe MASM,il faut utiliser H2INC.exe (fournit avec masm 615 normallement)

Bonne prog.
parki

A voir également

  • Interface c et asm
  • Interface c et asm » Sites
  • Informe de la capaciter des disques durs(interface win32) » Codes sources - C / C++ / C++.NET (Fichiers / Disque)
  • Comment obtenir des infos sur votre processeur par l'instruction asm cpuid... » Codes sources - Visual Basic / VB.NET (API)
  • Création et modification de raccourcis windows (.lnk) avec interface ishelllink » Codes sources - Visual Basic / VB.NET (Shell)


FAIRE UNE ROUTINE EN ASM POUR DU C++ [MASM32,VC++] | Source N° 37

FAIRE UNE ROUTINE EN ASM POUR DU C++ [MASM32,VC++]
Catégorie | : | c++ et asm
Date | : | 04/08/2002
Auteur | : | PierreF [ 2 Sources ] [ Ecrire un message ]
Niveau | : | 1
Note moyenne | : | [ Note moyenne : Pas de Note pour le moment ]
Zip | : | Sources du Projet Fichier Source du Projet (vous devez être connecté pour récupérer le fichier)
N'EXECUTEZ PAS DE FICHIER EXE, DLL, OCX ETC... QUI POURRAIENT ÊTRE DANS LE FICHIER ZIP SANS AVOIR UN ANTI-VIRUS A JOUR DE LANCE SUR VOTRE MACHINE ! DE PREFERENCE OUVREZ LES FICHIERS SOURCES PLUTOT QU'UN EXE
SI VOUS ETES NOUVEAU ? ====> A LIRE AVANT TOUTE CHOSE !

une simple petite routine en ASM qui fait une addition (c'est pour l'exemple)
Utilise MASM32 et VC++
;******************************
;***    code assembleur     ***
;***        c_mod.asm       ***
;*** Utilise MASM32 et VC++ ***
;******************************

.386             ;il faut un proc 32 bits (VC++ c'est du 32 bits)
 .model FLAT,c    ; Le C est obligatoire.
.CODE
PUBLIC addition  ;addition est un fonction public

addition PROC    ;la fonction addition

                 ;Quand on crée un fonction en asm, il faut respecter un impératif:
                 ;ne pas modifier EBP (ou alors le sauvegarder)

                 ;Et bien sur il faut que les PUSH soit associer à des POP
                 ;(donc ESP doit êtres à la fin à la même valeur que au début)

                 ;quand un programme appelle un fonction il met sur la pile les arguments
                 ;(offset ou valeur selon que soit un pointeur ou pas)

                 ;ici VC++ met sur la pile les 2 valeurs (17 et 21), il met la dernière (20) puis le première (12)
                 ;REM: les 2 arguments sont des short(2 octet) mais VC++ met sur la pile 4 octets par argument
                 ;(donc 8 octets)
                 ;VC++ appelle notre fonction avec un CALL, donc est mit sur la pile les 4 octets de retour.
                 ;
                 ;Voila à quoi ressemble la pile aprés le CALL
          pointeur(ESP)
          |
  PILE    |OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|........
          |      |      |      |      |      |      |      |      |      |      |      |      |

            --- adresse de retour ---   --- 1er argument (17) ---   --- 2eme argument (21)---
          0                           4                           8
    PUSH EBP          ;ESP se déplace de 4 (ESP=ESP-8)
    MOV  EBP,ESP      ;
                      ;Voila à quoi ressemble la pile aprés le PUSH de EBP

          pointeur(ESP)
          |
  PILE    |OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|........
          |      |      |      |      |      |      |      |      |      |      |      |      |      |      |      |      |

           ------- PUSH de EBP ------   --- adresse de retour ---   --- 1er argument (17) ---   --- 2eme argument (21)---
          0                           4                           8                           12                          16

    MOV EAX,[EBP+8]   ;on passe par dessus EBP et l'adresse de retour, on a le 1er argument
    MOV EBX,[EBP+8+4] ;on passe par dessus EBP ,  l'adresse de retour et le 1er argument, on a le 2eme argument

        ADD EAX,EBX   ;l'addition

    MOV ESP,EBP       ;je sais pas trop à quoi ca sert, c'était dans le livre que j'ai utilisé pour savoir
                      ;comment faire les routine en ASM pour le C ou le basic
    POP EBP           ;on reprend EBP
    RET               ;Fin, la valeur de retour est dans EAX

 addition ENDP

END

   ***************************
   ***  Code en C++(ou C)  ***
   ***       main.cpp      ***
   ***************************

   #include <stdio.h>

   extern "C" unsigned short addition (unsigned short ,unsigned short );

   void main()
   {
     int somme;
     somme=addition(17,21);
     printf("12+20 (en ASM!)= %d\n",somme);
   }

Les commentaires ne sont pas disponibles dans la compil'. Vous pouvez les consulter sur le site ou sur le CD.
 Voir cette source "on-line" sur le site avec tous les commentaires et le reste

Ajoutez vos sources
(vous devez être connecté à internet)

<= Source Précedente -- Source suivant =>

Site Réalisé Par N.SOREL (Nix)
ASM Codes Sources
CodeS-SourceS.com© tous droits réservés


CSharp tutoriel 2008

            FAIRE UNE ROUTINE EN ASM POUR DU C++ [MASM32,VC++]

            une simple petit routine en ASM qui fait un addition (c'est pour l'exemple)   Utilise MASM32 et VC++

            ;*************************
            ;***    code asmembleur ***
            ;***        c_mod.asm   ***
            ;*************************

            .386                 ; il faut un proc 32 bits (VC++ c'est du 32 bits)
            .model FLAT,c        ; le model Small marche aussi, je sais pas ce quie est le mieux
                                 ; le C est obligatoire
            .CODE                ; le code

            PUBLIC addition ;addition est un fonction public

            addition PROC;la fonction addition

            ;Quand on crée un fonction en asm, il faut respécter un impératif:
            ;ne pas modifier EBP (ou alors le sauvgarder)

            ;Et bien sur il faut que les PUSH soit associer à des POP (donc ESP doit êtres à la fin à la même valeur que au debut)

            ;quand un programme appelle un fonction il met sur la pile les arguments (offset ou valeur selon que soit un pointeur ou pas)
            ;ici VC++ met sur la pile les 2 valeurs (12 et 20), il met la dernière (20) puis le première (12)
            ;REM: les 2 argument sont des short(2 octect) mais VC++ met sur la pile 4 octect par argument (donc 8 octect)
            ;VC++ appelle notre fonction avec un CALL, donc est mit sur la pile les 4 octect de retour.
            ;voila à quoi resemble la pile

                                  ;   pointeur(ESP)                   4                           8                           8+4
                                  ;       |
                                  ;PILE   |OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT|OCTECT| ...
                                  ;       |      |      |      |      |      |      |      |      |      |      |      |      |
                                  ;        ----addresse de retour-----î----1er argument (12)------î-----2eme argument (20)----î

                PUSH EBP          ;ESP se déplace de 4 (ESP=ESP-8)
                MOV EBP,ESP

                MOV EAX,[EBP+8]   ;on passe par dessus EBP et l'addresse de retour, on a le 1er argument
                MOV EBX,[EBP+8+4] ;on passe par dessus EBP , l'addresse de retour et le 1er argument, on a le 2eme argument

                ADD EAX,EBX       ;l'addition

                POP EBP           ;on reprend EBP
                RET               ;Fin, la valeur de retour est dans EAX

            addition ENDP

            END

            //***************************
            //***   Code en C++(ou C) ***
            //***       main.cpp      ***
            //***************************

            #include

            extern "C" unsigned short addition (unsigned short ,unsigned short );

            void main()
            {
                int somme;
                somme=addition(12,20);
                printf("12+20 (en ASM!)= %d\n",somme);
            }


 Introduction

Introduction to using asm with LCC-Win32

The material for the x86 OpCodes was taken from two old OpCode help files an Intel doc and an AMD doc. Many of the unnecessary references to such things as 16 bit addressing have removed and the operand order has been changed from the Intel to the AT&T syntax but no doubt some will have been missed. It is hoped that there are not too many errors or omissions. Not all instructions have been checked so there will be errors, please report any mistakes and I will endeavor to correct them. (John e-mail)

Assembler Syntax Used With LCC-Win32

General Purpose Registers

Gaining Access To Parameters Passed To Functions

When Compiling Optimised

The Return Value

Addressing Modes

How

Writing assembler with lcc-win32 requires an extended syntax which is a little clumsy compared to writing code for one of the other assemblers but an advantage is that all one's code can be debugged in Wedit's comfortable debugger. Provided one writes the assembler instructions as separate lines one can step through the code as one would with C code.

For example one could write the instructions with a new line character added so that all asm instructions become one long line.

_asm("fldt 8(%ebp)\n\
        fld %st(0) \n\
        fmul %st \n\
        fld1 \n\
        fsubp \n\
        fsqrt \n\
        fxch %st(1) \n\
        fpatan \n");

Which is quite neat but unfortunately the debugger will see that as one long line so that using 'step' (F8) will cause all those instructions to be executed as if they were only one instruction - at least as far as the debugger is concerned.

The same code written as separate lines would be as follows.

    _asm("fldt
8(%ebp);
_asm("fld %st");
_asm("fmul %st(0)");
_asm("fld1");
_asm("fsubp");
_asm("fsqrt");
_asm("fxch %st(1)");
_asm("fpatan");

Now each separate instruction can be 'stepped' through and if desired one can note how the MPU's registers are changing to reflect what each instruction has done.

The extra characters _asm(" and "); can be quickly inserted using a Wedit keyboard macro so one keystroke will give _asm(""); one can then insert the instruction as required without too much bother.

Assembler Syntax Used With LCC-Win32

There are basically two syntax's used when code-ing with asm on the P.C. One is Intel's syntax, the other is called AT&T. The LCC_Win32 system uses AT&T's syntax.

Here is an example of an instruction (add) that the LCC-Win32 compiler will accept

_asm("addl $8,%eax");

This next piece shows another instruction (mov) using both the Intel and AT&T syntax. Both instructions copy what is at the address pointed to by the value in ebp plus an offset of 8 bytes.

_asm("movl 8(%ebp), %eax");    // AT&T Syntax used with LCC-Win32

mov eax, dword ptr [ebp+8]     // Intel's Syntax

If ebp has the value 0xC098E2 the instruction will copy what is at address 0xC098EA into eax.

0xC098E2 + 8 == 0xC098EA

Another way to write the same but using the hex notation (0x) would be

_asm("movl 0x8(%ebp), %eax");

Many mnemonics require a postfix to specify the size of the operand, for example movb, movw, movl for copying a byte, a word or a longword respectively. It is indicated throughout this help file where these are needed but not all opcodes do require it and I have not found any documents to correlate with.

When using an immediate operand one adds the $ character either in hex or dec.

_asm("movl $32,%eax");   // dec

_asm("movl $0x20,%eax"); // hex

For any mistakes made, my apologies! Report them to me.

General Purpose Registers

There are eight 32 bit general purpose registers on the x86 processor: eax, ebx, ecx, edx, esi, edi, ebp, and esp. You will use these registers extensively - the others are used only rarely, except by debuggers and the like. These general purpose registers can be used for any purpose but many instructions work more efficiently when used with particular registers and some instructions absolutely require a specific register.

The eax register (accumulator) is where you will do most arithmetic and logical computations. You can do most arithmetic and logical operations in other registers but it is often more efficient to use the eax register.

The ebx register (base) is routinely used to hold indirect addresses.

The ecx register (count) is used particularly well as the count register much like one would use 'i' in for loops in C. One instruction in particular jecxz (jump if ecx == zero) uses this register to branch to another section of code.

The edx register (data) has special purposes too. When an addition overflows for example, the overflow is held in this register.

The esi and edi registers (source index, destination index) are used extensively when accessing memory. Strings for example, or arrays.

The ebp register (base pointer), also known as the frame pointer, is often used to store a stack reference position from which to access function arguments and local variables.

The esp register (stack pointer) is crucial to a program in many ways - it points to the program stack which holds the return address from a subroutine and passed values to that subroutine. If this register become corrupted you program will crash.

Within these registers there are what I'll call sub-registers, meaning that the 32 bit registers can be used as parts rather than the whole 32 bits. Intel calls these words sized (16 bit) registers ax, bx, cx, dx, si, di, bp. Some of these in turn are split in half to give two 8 bit registers per 16 bit register.

So we get

ah and al from ax

bh and bl from bx

ch and cl from cx

dh and dl from dx

The eight, 8 bit registers are not independent, ah is the high byte of ax and al is the low byte of ax. You can deduce the other from the diagram below that shows the general purpose register set plus the segment registers.

EAX | Accumulator | most arithmetic and logical computations
EBX | Base | commonly used to hold indirect addresses
ECX | Count | use it to count off the number of iterations in a loop
EDX | Data | holds the overflow from certain arithmetic operations
ESI | Source Index | use these two registers esi & edi as pointers
EDI | Destination Index | also use with string instructions
EBP | Base Pointer | access parameters and local variables
ESP | Stack Pointer | generally not altered
CS | Code Segment | points at the segment containing the currently executing code
DS | Data Segment | points to the data segment of running process
ES | Extra Segment |
FS |  |
GS |  |
SS | Stack Segment |
EFLAGS | Condition Codes | FLAGS -- carry, parity, zero, sign, and overflow flags

EBX ESI EDI EBP and ESP should always be saved before and restored after if changed in your routine. To re-iterate, although you are at liberty to use registers for anything, certain registers are best used for specific purposes - here are two: the jump instruction jecxz looks for zero in ecx, the string instructions such as lods and stos use esi and edi as pointers to memory where loading and storing of characters will take place.

Gaining Access To Parameters Passed To Functions

Assuming one is compiling without optimisation and that integer variables are being passed to your function the first parameter is at 8(%ebp), the second at 12(%ebp) and so on. Storing the first parameter in eax would be accomplished like so

_asm("movl 8(%ebp), %eax");

The second in ecx

_asm("movl 12(%ebp), %ecx");

These offsets from ebp are valid when compiling without optimisation.

Here is a str_len example passing the parameter suitable for compiling non-optimised.

size_t str_len(char *s)
{
_asm("movl 8(%ebp),%eax");   // first arg
in eax
_asm("xorl %eax,%eax");
// clear eax
_asm("jecxz exit");
// check for '0' NULL in ecx
_asm("decl %eax");
// -1
_asm("lb1:");
_asm("incl %eax");
_asm("cmpb $0,(%ecx,%eax)"); // compares '0'
(string end) with the byte
// pointed to by ecx with an offset of eax.
_asm("jne lb1");
// not finished yet?
_asm("exit:");
}

When Compiling Optimised

Above I showed that accessing parameters passed to a function was done by using an offset from the ebp register and copying the value to a general purpose register. Unfortunately the situation changes slightly when compiling optimised - I say unfortunately because ones asm has normally to be different in each case.

To recap, the first parameter was at offset 8 from ebp when compiling non-optimised.

_asm("movl 8(%ebp), %eax");

When compiling optimised this same parameter is at offset 4 of esp

_asm("movl 4(%esp), %eax");

Here is the str_len example passing the parameter suitable for compiling optimised.

size_t str_len(char *s)
{
_asm("movl 4(%esp),%eax");   // first arg
in eax
_asm("xorl %eax,%eax");
// clear eax
_asm("jecxz exit");
// check for '0' NULL in ecx
_asm("decl %eax");
// -1
_asm("lb1:");
_asm("incl %eax");
_asm("cmpb $0,(%ecx,%eax)"); // compares '0'
(string end) with the byte
// pointed to by ecx with an offset of eax.
_asm("jne lb1");
// not finished yet?
_asm("exit:");
}

However the best solution when compiling asm code with LCC-Win32 is to use __declspec(naked), this informs the compiler that no epilogue or prologue code should be added to the function, which means that we are in full control of what happens inside our function.

It also means that we have to look after the stack ourselves which will be covered in more depth later. For the str_len example this does not require more work on our part because the function is simple but for more demanding functions which we will see later the stack must be managed.

str_len using __declspec(naked)

size_t __declspec(naked) str_len(char
      *s)
{
_asm("movl 4(%esp),%eax");   //
      first arg in eax
_asm("xorl %eax,%eax");
      // clear eax
_asm("jecxz exit");
      // check for '0' NULL in ecx
_asm("decl %eax");
      // -1
_asm("lb1:");
_asm("incl %eax");
_asm("cmpb $0,(%ecx,%eax)"); // compares '0'
      (string end) with the byte
// pointed to by ecx with an offset of eax.
_asm("jne lb1");
      // not finished yet?
_asm("exit:");
     _asm("ret");
      // new instruction added, the return
}

You will notice that a new instruction has been added, the 'ret' (return) instruction. Remember, when using __declspec(naked) no epilogue or prologue code is added to our function by the compiler so we have to 'ret' ourselves.

The Return Value

For all integer returns you need to put the value in eax before returning. This is the most efficient way of returning a value from an asm routine. When returning a floating point value eax is not used but rather the return value is taken from the top of the floating point stack.

Of course one can explicitly return a value using the return statement. Here are two examples, one showing an automatic return, and one showing how to explicitly return the value.

First the automatic return, this uses the register eax or part of it depending on the size of the return value, byte, word, or long word.

char foo(char a, char b)
{
_asm("movb 8(%ebp), %al");  // load a into al
_asm("movb 12(%ebp), %cl"); // load b into cl
_asm("addb %cl, %al");
// add them putting the result in al
// the automatic return will be al (low byte of ax (low word
of eax))
}

Now explicit returning

char foo(char a, char b)
{
char c;
_asm("movb 8(%ebp), %al");  // load a into al
_asm("movb 12(%ebp), %cl"); // load b into ecx
_asm("addb %cl, %al");
// add them putting the result in al
_asm("movb %al, %c");
// move (copy) al into c
return c;
}

Both the above examples are for compiling non-optimised, the following is for compiling with __declspec(naked), optimsed or non-optimised.

char __declspec(naked) foo(char
a, char b)
{
_asm("movb 4(%esp), %al");  // load a into al
_asm("movb 8(%esp), %cl");  // load b into cl
_asm("addb %cl, %al");
// add them putting the result in al
_asm("ret");
// the return will be al (char)
}

For larger integers the return will be in ax (shorts) and eax (longs).

int __declspec(naked) foo(int a, int b)
{
    _asm("movl 4(%esp), %eax"); // load a into eax
    _asm("movl 8(%ebp), %ecx"); // load b into ecx
    _asm("addl %ecx, %eax");    // add them putting the result in eax
    _asm("ret");                // the return will be eax (long)
}

Addressing Modes

The x86 processor has various addressing modes that we will look at in more detail.

_asm("movw $0x08, %ax"); | The immediate addressing mode copies a value immediately to a register.
_asm("movl %ecx, %eax");
The register addressing mode copies one register to another ecx -> eax.
_asm("movl (%eax), %ecx"); | The indirect addressing mode copies from the address pointed to by eax to the register ecx.
_asm("movl 16(%eax), %ecx"); | The indexed addressing mode copies from the address pointed to by eax plus 16 to the register ecx.
_asm("movl (1000), %eax");

The direct addressing mode. Copies the contents of address 1000 to eax.
_asm("movl(%edx,%ecx,4),%eax"); | The indirect addressing EDX + (ECX * 4)

You might have noticed that the use of parenthesis with a register (%ecx) means -- at the address pointed to by the register, while without parenthesis, %ecx means operating on the register directly.

The example above in the str_len routine used what's called indirect addressing with offset.

_asm("cmpb $0,(%ecx,%eax)");

The address at ( offset($0) + (address pointed to by ecx + index (value in eax) ) ). This is quite a powerful combination and can be used effectively when writing asm code.


MASM et C++

Le c,c++,traduit en assembleur avec l'option FA, peuvent répondre à beaucoup de questions en matière de code,de calculs,d'optimisation.Cest pourquoi editmasm vous permet de créer des projets c ou c++.Ces ambitions restent modestes,il vous fera gagner un temps précieux à la compilation sur de courts exemples en c,c++.

Il vous permet,en particulier,d'utiliser les nombreux bouts de code fournis par MSDN.

Le windows.sdk permet d'utiliser la bibliotèque de fonctions c++ sans se poser beaucoup de questions.Il vous faut le c++ express téléchargeable gratuitement.Quelques exemples viendront illustrer içi la traduction des deux langages.Les fonctions de la bibliothèque sont fort bien expliquées içi avec des exemples.

Editmasm permet d'utiliser cl en ligne de commande.Avantage,l'aide est disponible même si vous n'avez pas visual studio.La version express du c++ ne la fournit pas .

Principales différences:

L'assembleur travaille avec des registres eax,ecx,.. le VC++ avec des noms définis par l'utilisateur

L'assembleur connait offset,addr,lea, [registre] pour définir une adresse,sinon on a affaire a une valeur.

Seul les déclarations utilisateur du c++ permettent de définir une adresse ou une valeur.

invoke mafonction,valeur,addr adresse ;l'absence de addr identifie valeur comme une valeur

en c++ on doit lire les déclarations pour savoir si on utilise une adresse ou valeur.

RECT rcCurrent = {0,0,20,20},*LPrcCurrent; on définit un pointeur sur rcCurrent

LPrcCurrent dd offset rcCurrent ;en assembleur

OffsetRect(LPrcCurrent, rc.right / 2, rc.bottom / 2);

Si on suprime le *LPrcCurrent,il faut écrire:

OffsetRect(&rcCurrent, rc.right / 2, rc.bottom / 2);

Les déclarations du c++ sont nombreuses et très pointilleuses

retour = SendMessage(hToolTip, TTM_ADDTOOL, 0, &toolInfo)

On veut passer l'adresse de la structure toolInfo a la fonction et on obtient le message:

d:\vc++\tooltip\tooltip\tooltip.cpp(222): error C2664: 'SendMessageA' : impossible de convertir le paramètre 4 de 'TTTOOLINFOA *' en 'LPARAM'

Aucun contexte dans lequel cette conversion est possible

En général,içi on s'arrache un peu la tête.Le c++ est un macro langage,c'est une macro qui remplit les registres nécessaires a l'éxécution du code.Dans ce cas,il faut rajouter des précisions pour que la macro puisse traduire.

On rajoute (LPARAM)(LPTOOLINFO) et la macro comprend que l'on veut placer un * TOOLINFO,pointeur sur TOOLINFO dans un LPARAM = * PARAM ,un pointeur sur PARAM.La macro voit l'équivalence entre deux pointeurs et tout va mieux.

retour = SendMessage(hToolTip, TTM_ADDTOOL, 0,(LPARAM)(LPTOOLINFO) &toolInfo)

le reinterpret_cast:

hbrush = CreatePatternBrush(hbm);
hbrushOld = SelectObject(hdc, hbrush);

d:\vc++\brush\brush.cpp(386): error C2440: '=' : impossible de convertir de 'HGDIOBJ' en 'HBRUSH'
La conversion de 'void*' en pointeur vers non 'void' nécessite un cast explicite

ce sont deux dword,il ne devrait pas y avoir de probleme,le tout est de savoir comment.

On regarde le proto:

__gdi_entry WINGDIAPI HGDIOBJ WINAPI SelectObject(__in HDC hdc, __in HGDIOBJ h);

si on ecrit:

hbrushOld = (HBRUSH)SelectObject(hdc, hbrush);

on demande au vc++ de reinterpréter l'appel,et ça passe

METHODE de RECHERCHE: Internet tapez c++ HBRUSH HGDIOBJ dans google

-------------------- pointilleuse syntaxe --------------------------------------------------------------------

BOOL CALLBACK BrosseDlgProc( HWND , UINT, WPARAM , LPARAM)

n'est pas

BOOL CALLBACK BrosseDlgProc( HWND , UINT, LONG ,LONG)

parce qu'une procédure dialog est défini ainsi:

typedef INT_PTR (CALLBACK* DLGPROC)(HWND, UINT, WPARAM, LPARAM);

Si on utilise la deuxieme forme,les erreurs deviennent fort peu clairs.

write

C exemple de conversion

#include <io.h>

#include <stdio.h>

#include <stdlib.h>

#include <fcntl.h>

#include <string.h>

#define FILENAME "write.dat"

int main(void)

{

int fh;

char buffer[20];

memset(buffer, 'a', 20); /* initialize storage */

printf("\nCreating " FILENAME ".\n");

system("echo Sample Program > " FILENAME);

if (-1 == (fh = open(FILENAME, O_RDWR|O_APPEND))) {

perror("Unable to open " FILENAME);

return EXIT_FAILURE;

}

if (5 != write(fh, buffer, 5)) {

perror("Unable to write to " FILENAME);

close(fh);

return EXIT_FAILURE;

}

printf("Successfully appended 5 characters.\n");

close(fh);

return 0;

/****************************************************************************

The program should create a file "write.dat" containing:

Sample Program

aaaaa

The output should be:

Creating write.dat.

Successfully appended 5 characters.

****************************************************************************/

TRADUCTION MASM

;link /SUBSYSTEM:CONSOLE

.586

.model flat, stdcall

option casemap :none ; case sensitive

main PROTO C

include \masm32\sdk\translate.inc ;le seul fichier ajouté

include \masm32\sdk\io.sdk

include \masm32\sdk\stdio.sdk

include \masm32\sdk\stdlib.sdk

include \masm32\sdk\fcntl.sdk

include \masm32\sdk\string.sdk

includelib \PROGRA~1\MICROS~1.0\VC\lib\libcmt.lib ;msvcrt.lib

includelib \PROGRA~1\MICROS~1.0\VC\lib\OLDNAMES.lib

; #########################################################################

.const

FILENAME equ "write.dat"

.data

sortie db FILENAME,0

message db 13,10,"Creating ", FILENAME,13,10,0

commande db "echo Sample Program > ", FILENAME,0

meserroropen db "Unable to open to ",FILENAME,0

meserrorwrite db "Unable to write to ",FILENAME,0

messuccess db "Successfully appended 5 characters.",13,10,0

;testé les erreurs RAJOUT

badfile db "machin.dat",0

.code

main PROC C

LOCAL fh:DWORD

LOCAL buffer[20]:BYTE

;remplir buffer avec des a

invoke memset,addr buffer, "a", sizeof buffer ; /* initialize storage */

invoke printf,addr message

invoke system,addr commande

invoke _open,addr sortie,O_RDWR or O_APPEND

mov fh,eax

.if eax == -1 ;INVALID_HANDLE_VALUE

invoke perror,addr meserroropen

mov eax, EXIT_FAILURE

ret

.endif

invoke _write,fh,addr buffer, 5

.if eax != 5

invoke perror,addr meserrorwrite

invoke close,fh

mov eax, EXIT_FAILURE

ret

.endif

invoke printf,addr messuccess

invoke _close,fh

mov eax,0

ret

main endp

Fonctions ayant un nom incompatible avec Masm

Certaines fonctions C sont des noms réservés dans masm,exemple dup.Pour masm,elle signifie une longueur de variable,pour le C c'est une fonction (un proc) de duplication.Il y a deux solutions pour résoudre le problème:

1)Se connecter dynamiquement à la fonction:

;déclarations

Pcrt_fabs TYPEDEF PROTO C :QWORD

Fcrt_fabs TYPEDEF PTR Pcrt_fabs

crt_fabs TEXTEQU <Fcrt_fabs ptr Acrt_fabs>

.data

Acrt_fabs dd 0

Il faut charger la dll contenant la fonction (loadlibrary ntdll.dll)

Utiliser getprocadress et mettre la valeur retourné dans Acrt_fabs

La fonction pourra être accédé par le nom,crt_fabs (invoke crt_fabs,value)

Les prototypes incompatibles sont mis en commentaire dans le windows.sdk

dup est deprecated,utiliser _dup

Pour d'autres fonctions,remplacé crt_fabs par l'autre nom de fonction dans toutes les déclarations

2)Utilisé la librairie masm_msvcrt:

Cette librairie fabriquée avec lib porte le nom de msvcrt.lib.Toutes les fonctions sont renommées

D'autres librairies windows,directx... utilisent les fonctions de cette librairie et cela pose de nombreux problèmes si on l'utilise.

Msvcrt et Libcmt

Les deux librairies contiennent les mêmes fonctions.

La msvcrt produit des éxécutables plus petit ( 50 Ko) mais demande le plus souvent,un manifeste

;dans le .rc

#define RT_MANIFEST 24

#define APP_MANIFEST 1

APP_MANIFEST RT_MANIFEST DISCARDABLE "Microsoft.VC80.CRT.manifest"

Le manifeste se trouve dans

C:\Program Files\Microsoft Visual Studio 9.0\VC\redist

La libcmt n'a pas besoin de manifeste et présente moins de problèmes a l'initialisation.

Sous-titre 4

Texte

Sous-titre 5

Texte

Sous-titre 6

Texte


un prog ASM dans un prog C

Appeler une routine ASM à partir d'un prog en C (mode console)

Pour les débutants comme moi,voici comment utiliser un prog ASM dans un prog C et inversement en mode console.

- - -
Vu dans ... http://codes-sources.commentcamarche.net/source/53466-interface-c-et-asm

module C;
         #include <stdio.h>
         #include <stdlib.h>

         extern int asm_main

         int main()
         {
          int retour;
          retour=asm_main();
          printf("le resultat de la routine asm_main est : %ld",retour);
          return 0;
          }

module asm;

                .586
                .model small,c

                asm_main        PROTO C

                .code

                asm_main        PROC C
                        mov eax,10
                        mov ebx,20
                        add eax,ebx
                        ret
                asm_main        endp

                end

vous assembler le module asm,vous renommer en .o au lieu de .obj,et vous lier avec le module C.

 Appeler une routine C dans un prog asm (mode console)
 -----------------------------------------------------

Vous devez utiliser un module C comme "lanceur" du prog asm.
C'est cela qui permet d'utiliser les librairies C.

module C;

         #include <stdio.h>
         #include <stdlib.h>

         extern int asm_main

         int main()
         {
          int retour;
          retour=asm_main();
          return 0;
          }

module asm:

        .586
        .model small,c

        printf PROTO NEAR,string:DWORD,P1:DWORD
        asm_main        PROTO

        .data

        format  byte '%ld',13,0

        .code

        asm_main PROC PUBLIC uses ecx

                mov eax,30
                mov ecx,30
                add eax,ecx
                invoke printf,offset format,eax

                mov eax,0
                ret

        asm_main endp

        end

Pour tranformer les protypes des fonctions C en syntaxe MASM,il faut utiliser H2INC.exe (fournit avec masm 615 normallement)

Bonne prog.


Appeler une routine ASM à partir d'un prog en C (mode console)

Vu dans ... http://codes-sources.commentcamarche.net/source/53466-interface-c-et-asm

module C;

         #include
         #include

         extern int asm_main

         int main()
         {
          int retour;
          retour=asm_main();
          printf("le resultat de la routine asm_main est : %ld",retour);
          return 0;
          }

module asm;

                .586
                .model small,c

                asm_main        PROTO C

                .code

                asm_main        PROC C
                        mov eax,10
                        mov ebx,20
                        add eax,ebx
                        ret
                asm_main        endp

                end

vous assembler le module asm,vous renommer en .o au lieu de .obj,et vous lier avec le module C.

Appeler une routine C dans un prog asm (mode console)
-----------------------------------------------------

Vous devez utiliser un module C comme "lanceur" du prog asm.
C'est cela qui permet d'utiliser les librairies C.

module C;

         #include
         #include

         extern int asm_main

         int main()
         {
          int retour;
          retour=asm_main();
          return 0;
          }

module asm:

        .586
        .model small,c

        printf PROTO NEAR,string:DWORD,P1:DWORD
        asm_main        PROTO

        .data

        format  byte '%ld',13,0

        .code

        asm_main PROC PUBLIC uses ecx

                mov eax,30
                mov ecx,30
                add eax,ecx
                invoke printf,offset format,eax

                mov eax,0
                ret

        asm_main endp

        end

Pour tranformer les protypes des fonctions C en syntaxe MASM,il faut utiliser H2INC.exe (fournit avec masm 615 normallement)


D:\COM_CHM\Documentation CHM\_ASM\ASM__DOC\Tasm\ASMCHAP5.rtf

CHAPITRE 5 - Assembleur & C

L'inclusion de modules en assembleur dans les programmes de haut niveau constitue un objectif fondamental de la programmation en assembleur. Si les langages de haut niveau présentent beaucoup d'avantages et sont plus agréables à manier que l'assembleur, il leur arrive de ne pas connaître certaines fonctions indispensable à un projet. Également, pour le traitement des boucles interne (inner loops), il est carrément impératif d'utiliser l'assembleur. Il existe 3 façon d’inclure de l’assembleur dans nos sources C. On peut avoir recours à l’assembleur interne du compilateur, utiliser un assembleur externe directement dans notre source ou à l’extérieur de celui-ci.

Utilisation du compilateur interne

Tous les compilateurs modernes ont un assembleur interne qui permet d’inclure directement dans du code source des lignes de codes assembleur. En Borland C++, il s’agit de l’instruction asm. Nous avons déjà vu sont utilisation, comme par exemple :

void mode_graphique()
{
asm {
MOV AX, 0x13
INT 0x10
}
}

L’assembleur interne est cependant beaucoup moins efficace qu’un assembleur externe tel MASM ou TASM. Je n’expliquerai pas ici en détails tous les routines traduites en assembleur, car à présent vous devriez être capable de suivre le déroulement d’une fonction assembleur à partir du code source. J’ai néanmoins commenté le code pour vous aider dans cette tâche.

Inclure des routines de bas niveau dans le C

Ce processus s'effectue en 2 étapes: premièrement, il faut coder les fonctions elles-mêmes dans un fichier source ASM. Ensuite, il faut que le compilateur C puisse y référer. Cependant, il faudra légèrement modifier notre code asm pour que nos fonctions soient à même d'être adressées et encore plus important, qu'elles puissent recevoir des paramètres.

Tous les langages de haut niveau ont une exigence commune vis-à-vis du module en assembleur: ils réclament l'installation d'un cadre de pile. Il s'agit en fait d'un dédoublement du segment de pile dans le registre BP. Comme la pile du langage de haut niveau de doit pas être modifier, le contenu du pointeur de pile est copié en BP. Avant cela, l'ancienne valeur de BP est sauvegardée sur la pile. Donc, tous modules assembleurs destinés à être incorporé dans du code de haut niveau doit commencer par les instructions:
PUSH BP
MOV BP,SP

Également, chaques fonctions doit être déclarée PUBLIC, c'est à dire la rendre accessible à tous les modules du projet. Pour restaurer la pile à son état original, il ne faut pas oublier l'évidence:

POP BP

Le nom de notre fonction dans le module assembleur devra être précédé du caractère de soulignement, _ ("underscore"). La commande ARG nous permet de spécifier les paramètres que notre fonction recevra. Par exemple, notre fonction pour choisir un mode vidéo par le BIOS deviendrait, si on veut l'utiliser avec un module externe, la suivante:

PUBLIC _setmode ; Utilisable par tous les modules

_setmode PROC ; PROCédure setmode (avec le _)
ARG mode:WORD ; Paramètre "mode"
PUSH BP ; sauvegarde bp
MOV BP,SP ; sur la pile
MOV AX, mode ; ax = mode
INT 10h ; interruption vidéo
POP BP ; on ramène la pile originale
RET ; toujours mettre RET
_setmode ENDP ; fin de la procédure

Modifications dans notre code C

Pour compiler correctement notre code dans l'environnement C, il faudra créer un fichier project. Pour ce faire, dans le menu Project, il faut ouvrir un nouveau projet, avec Open Project. Donnez un nom à votre projet, par exemple, test. Un nouveau fichier PRJ sera créé. Cette fois, dans le menu PRJ, aller sur Add Item. Ajouter votre fichier .ASM, ainsi que votre fichier CPP. Donc:

PROJECT --> OPEN PROJECT
PROJECT --> ADD ITEM -> "fichier.ASM"
PROJECT --> ADD ITEM -> "fichier.CPP"

La fenêtre "Project" devrait apparaître en bas de l'écran avec la liste des fichiers que vous avez inclus. Pour rendre accessible les modules Assembleur, il faut déclarer nos fonctions avec le mot-clef "extern". Cela signifie que la définition de la fonction se trouve dans un module exterieur au C. Par
exemple:

extern "C"
{
void setmode(unsigned int mode);.

etc...
}

Maintenant, vous êtes prêt à compiler. Assurez-vous que TASM.EXE et TASM2MSG sont tous les deux présents dans votre PATH (pour être sûr, copier ces 2 fichiers dans c:\windows\command). Si tout ce passe bien, votre code asm sera compilé par TASM, et le CPP par le compilateur C. Si l'environnement Borland vous envoie un message style "Cannot find executable : TASM", il faut que vous spécifier le chemin de tasm.exe:

OPTIONS --> TRANSFER...
EDIT --> ~Turbo Assembler
PATH --> (le chemin), ex: C:\TC\BIN\

Invoquer un assembleur externe sans utiliser le fichier projet

Il est possible d’utiliser de l’assembleur beaucoup plus simplement, en l’incluant directement dans le code source. Il existe en effet une instruction du pré-processeur qui signale que l’assembleur interne doit faire place à un assembleur externe pour le traitement des lignes de codes assembleur. Il s’agit de #pragma inline. Par exemple :

// Exemple d'appel d'inline assembly par TASM

#pragma inline
asm .386p
asm MESSAGE DB "Allo de Turbo Assembleur dans C++$",13,10

void main()
{
asm MOV AX, 3
asm INT 0x10
asm MOV AH, 9
asm XOR AL,AL
asm MOV DX, OFFSET MESSAGE
asm INT 0x21
}

Programmes exemples

Les programmes exemples mettent en pratique les différentes façon d’appeler de l’assembleur dans des programmes C. Règle générale, l’assembleur interne est plus facile d’utilisation mais est moins efficace que l’assembleur externe.


ASM Mode Reel

.MODEL COMPACT                       ; modele memoire compact
.486                                 ; instructions 486
.CODE                                ; segment de code
PUBLIC _setmode,_clear,_hline,_copy  ; declaration des fonctions externes

; ----------------------------
; *	SETMODE              *
; ----------------------------

_setmode	PROC                 ; Specifie le mode video passer
                     ; en parametre
    ARG	mode:WORD            ; parametre mode
    PUSH	BP                   ; pour appel de fonctions
    MOV	BP,SP                ; par programme externe de haut niveau

    MOV	AX, mode             ; numero du mode dans AX
    INT	10h                  ; interruption video

    POP	BP                   ; sortir de la fonction
    RET                          ; fin de la fonction

_setmode	ENDP

; ----------------------------
; *          CLEAR           *
; ----------------------------

_clear		PROC
    ARG	ecran:DWORD
    PUSH	BP                  ; pour appel de fonctions
    MOV	BP,SP               ; par programme externe de haut niveau
    PUSH DI

    LES  DI,ecran               ; ES:DI pointe sur l'ecran
    MOV  CX,16000               ; 16000 iterations (64000/4)
    XOR  AX,AX                  ; AX a 0
    REP  STOSD                  ; repete deplace 4 octet de valeur AX (0)
                    ; vers ES:DI
    POP  DI
    POP	BP                  ; sortir de la fonction
    RET                         ; fin de la fonction

_clear		ENDP

; ----------------------------
; *          CLEAR           *
; ----------------------------
_copy	PROC
    ARG	ecran:DWORD
    PUSH	BP                 ; pour appel de fonctions
    MOV	BP,SP              ; par programme externe de haut niveau
    push es
    push	ds
    push	di
    push	si

    MOV  AX, 0A000h            ; destination ecran
    MOV  ES, AX
    XOR  DI, DI                ; coin superieur gauche
    LDS  SI, ecran             ; DS:SI = ecran virtuel
    MOV  CX, 16000             ; 16000 iterations (64000/4)
    REP  MOVSD                 ; repete deplace 4 octet

    pop	si
    pop	di
    pop	ds
    pop  es
    POP	BP                 ; sortir de la fonction
    RET                        ; fin de la fonction
_copy     ENDP

; ----------------------------
; *	    HLINE            *
; ----------------------------

_hline  PROC
    ARG  y:WORD, col:BYTE, ecran:DWORD

    PUSH	BP                ; pour appel de fonctions
    MOV	BP,SP             ; par programme externe de haut niveau
    PUSH DI

    LES  DI,ecran
    MOV  AX,y
    MOV  DI,AX
    SHL  DI,8
    SHL  AX,6
    ADD  DI,AX
    ADD  DI,4
    MOV  AL,col
    MOV  AH,AL
    MOV  CX,160
    REP  STOSW

     POP  DI
    POP	BP               ; sortir de la fonction
    RET                      ; fin de la fonction

_hline  ENDP

END


ASM Mode Reel

#include <time.h>
#include <conio.h>
#include <stdio.h>

#define vga 0xA000
#define MIN_Y 20
#define MAX_Y 160

char *virtuel = new char[64000L];

extern "C"
{
    void setmode(int mode);
    void clear(char *);
    void copy(char *);
    void hline(int,unsigned char,char *);
}

//----------------------------------------------------------------------//
// setpal - fixe les attributs r,g,b                                    //
//----------------------------------------------------------------------//
void setpal(unsigned char col,unsigned char r, unsigned char g, unsigned char b)
{
  outp (0x03C8,col); // Envoie les informations au DAC
  outp (0x03C9,r);
  outp (0x03C9,g);
  outp (0x03C9,b);
}

//----------------------------------------------------------------------//
// preparepal - modifie la palette a mon gout                           //
//----------------------------------------------------------------------//
void preparepal()
{
  for (int i=1;i<=10;i++)  setpal(i,0,0,i*3);
  for (i=11;i<=20;i++)	  setpal(i,(i-10)*3,0,0);
  for (i=21;i<=30;i++)	  setpal(i,0,(i-20)*3,0);
  for (i=31;i<=40;i++)     setpal(i,0,(i-30)*3,(i-30)*3);
  for (i=51;i<=60;i++)     setpal(i,(i-50)*3,(i-50)*3,(i-50)*3);
  for (i=61;i<=70;i++)     setpal(i,(i-60)*3,(i-60)*3,0);
  for (i=71;i<=80;i++)     setpal(i,(i-70)*2,(i-70)*3,(i-70));
}

//----------------------------------------------------------------------//
// Barre - Anime trois barres sur l'ecran                               //
//----------------------------------------------------------------------//
void barre(int &y, unsigned char coul, int &sens)
{
  int ligne,i,x;

  for(ligne=0;ligne<=10;ligne++)
  {
    hline(y+ligne,coul,virtuel);
    if(ligne < 5) coul++;
    if(ligne > 5) coul--;
    if(y>=MAX_Y)  sens=-1;
    if(y<=MIN_Y)  sens=1;
  }
  y+=sens;
}

void main()
{
  int sensg =  1;
  int sensb =  1;
  int sensr = -1;
  int sensv =  1;
  int sensa =  1;
  int sensc =  1;
  int sensd =  1;

  int yb = MIN_Y;
  int yr = MIN_Y + 20;
  int yg = MIN_Y + 20;
  int yv = MIN_Y + 40;
  int ya = MIN_Y + 60;
  int yc = MIN_Y + 80;
  int yd = MIN_Y + 100;
  unsigned long tempdeb, tempfin, frame;

  setmode(0x13);
  preparepal();

  frame=0;tempdeb=0;tempfin=0;
  tempdeb = clock();
  do
  {
    frame++;
    clear(virtuel);
    barre(yr,15,sensr);
    barre(yb,5, sensb);
    barre(yg,25,sensg);
    barre(yv,35,sensv);
    barre(ya,55,sensa);
    barre(yc,65,sensc);
    barre(yd,75,sensd);
    copy(virtuel);
  } while (frame < 5000);
  tempfin = clock();

  setmode(0x03);
  printf("Temps pour dessiner 5000 frames: %u\n", tempfin - tempdeb);
  printf("\nShaun Dore\ndores@videotron.ca\nhttp://pages.infinit.net/shaun/");
}


ASM Mode Reel

//----------------------------------------------------------------------//
// FICHIER              : 2DCHAP4.CPP       asmch5p3.ccp                //
// AUTEUR               : Shaun Dore                                    //
// DESCRIPTION          : Animations et ecrans virtuels                 //
// DATE DE MODIFICATION : 02-02-98	                                //
// COMPILATEUR          : Borland Turbo C++ Real Mode 16-bit compiler   //
// NOTES                : Compiler avec modele memoire Compact          //
//----------------------------------------------------------------------//

//----------------------------------------------------------------------//
// Fichiers include                                                     //
//----------------------------------------------------------------------//

#include <mem.h>
#include <time.h>
#include <stdio.h>
#include <conio.h>

//----------------------------------------------------------------------//
// Constantes                                                           //
//----------------------------------------------------------------------//

#define MIN_Y 10
#define MAX_Y 170

//----------------------------------------------------------------------//
// Variables globales                                                   //
//----------------------------------------------------------------------//

char *ecran  = (char *) (0xA0000000L);       // ptr sur RAM Video
char *virtuel = new unsigned char[64000L];   // ptr sur ecran virtuel

//----------------------------------------------------------------------//
// setmode - Appelle le mode passer en parametre                        //
//----------------------------------------------------------------------//
void setmode(unsigned int mode)
{
  asm {
       MOV AX, [mode]
       INT 0x10
     }
}

//----------------------------------------------------------------------//
// clsvirt - vide l'ecran                                               //
//----------------------------------------------------------------------//
void clsvirt()
{
  memset(virtuel,0,64000L);
}

//----------------------------------------------------------------------//
// cpyvirt - copie un ecran vers un autre                               //
//----------------------------------------------------------------------//
void cpyvirt()
{
  memcpy(ecran,virtuel,64000L);
}

//----------------------------------------------------------------------//
// putpixel - Affiche un pixel                                          //
//----------------------------------------------------------------------//
void putpixel(int x, int y, unsigned char col)
{
  virtuel[(y*320)+x] = col;
}

//----------------------------------------------------------------------//
// hline - Dessine une ligne horizontale                                //
//----------------------------------------------------------------------//
void hline(int y, unsigned char coul)
{
  memset(virtuel+(y*320),coul,320);
}

//----------------------------------------------------------------------//
// setpal - fixe les attributs r,g,b                                    //
//----------------------------------------------------------------------//
void setpal(unsigned char col,unsigned char r, unsigned char g, unsigned char b)
{
  outp (0x03C8,col); // Envoie les informations au DAC
  outp (0x03C9,r);
  outp (0x03C9,g);
  outp (0x03C9,b);
}

//----------------------------------------------------------------------//
// preparepal - modifie la palette a mon gout                           //
//----------------------------------------------------------------------//
void preparepal()
{
  for (int i=1;i<=10;i++)  setpal(i,0,0,i*3);
  for (i=11;i<=20;i++)	  setpal(i,(i-10)*3,0,0);
  for (i=21;i<=30;i++)	  setpal(i,0,(i-20)*3,0);
  for (i=31;i<=40;i++)     setpal(i,0,(i-30)*3,(i-30)*3);
  for (i=41;i<=50;i++)     setpal(i,(i-40)*3,(i-40)*3,(i-40)*3);
  for (i=61;i<=70;i++)     setpal(i,(i-60)*3,(i-60)*3,0);
  for (i=71;i<=80;i++)     setpal(i,(i-70)*2,(i-70)*3,(i-70));
}

//----------------------------------------------------------------------//
// Barre - Anime trois barres sur l'ecran                               //
//----------------------------------------------------------------------//
void barre(int &y, unsigned char coul, int &sens)
{
  int ligne,i,x;

  for(ligne=0;ligne<=10;ligne++)
  {
    hline(y+ligne,coul);
    if(ligne < 5) coul++;
    if(ligne > 5) coul--;

    if(y>=MAX_Y)  sens=-1;
    if(y<=MIN_Y)  sens=1;
  }
  y+=sens;
}

//----------------------------------------------------------------------//
// Fonction MAIN                                                        //
//----------------------------------------------------------------------//

void main()
{
  int sensg =  1;
  int sensb =  1;
  int sensr = -1;
  int sensv =  1;
  int sensa =  1;
  int sensc =  1;
  int sensd =  1;

  int yb = MIN_Y;
  int yr = MIN_Y + 20;
  int yg = MIN_Y + 20;
  int yv = MIN_Y + 40;
  int ya = MIN_Y + 60;
  int yc = MIN_Y + 80;
  int yd = MIN_Y + 100;
  unsigned long tempdeb, tempfin, frame;

  setmode(0x13);
  preparepal();

  frame=0;tempdeb=0;tempfin=0;
  tempdeb = clock();
  do
  {
    frame++;
    clsvirt();
    barre(yr,15,sensr);
    barre(yb,5, sensb);
    barre(yg,25,sensg);
    barre(yv,35,sensv);
    barre(ya,45,sensa);
    barre(yc,65,sensc);
    barre(yd,75,sensd);
    cpyvirt();
  } while (frame < 5000);
  tempfin = clock();         // clock() pour le temps de fin
  setmode(0x03);

  printf("Temps pour 5000 frames %u\n", tempfin - tempdeb);
  printf("\nShaun Dore\ndores@videotron.ca\nhttp://pages.infinit.net/shaun/");
}


STUCTURE d'un source ASM et GESTION des EVENEMENTS

STRUCTURE d'un source ASM

et GESTION des EVENEMENTS

Constituants:
Des fichiers textes avec l'extension .asm,.inc ou .rc.Le .asm est destiné a être interprété par ml pour créer un objet .obj.
Les .inc sont un moyen de découper ce fichier en petits morceaux.
Le .rc est écrit en langage C.Il est destiné à contenir des ressources,images,icones,menus...Il est interprété par cvtres et ensuite par rc.Sa présence n'est pas obligatoire.
Le linker (link.exe) va regrouper tous les objets créés pour former l'éxécutable

L'Entête du fichier .asm

Les Lignes essentielles

.686P ;Ml limite le jeu d'instructions au pentium pro (instructions Priviliégées autorisées

.XMM ;intructions SIMD

.MMX ;MMX instructions

.model flat, stdcall ;Model:Format de creation binaire Flat:les adresses commencent à 400000h++.

;stdcall le Stackframe du PROC,l'appel du proc est standard,voir _cdecl pour un appel C

option casemap :none ; les instructions masm invoke...peuvent s'écrire en majuscules ou minuscules.

ExitProcess PROTO :DWORD ;déclaration du prototype utilisé,normalement contenue dans des "include file"

Includelib kernel32.lib ;demande à LINK.exe de rechercher le prog ExitProcess dans cette librairie

.const ;Içi on place les déclarations,constantes,structures...

.data ;Segment des données DB,DD,...

.code ;Le Segment code commence à .code

start: ;Point d'entrée:L'éxécution du code commence içi (etiquette: ....... end etiquette),

invoke ExitProcess,0 ;l'éxécution du code se termine içi et renvoie 0 comme code de terminaison

end start ;Le end start marque la fin du code compilé et déclare l'entrée du code sur start:

;on peut ne trouver que end sans le ExitProcess,Le code continue ailleurs

        En fonction de la position des proc par rapport a start,on peut ou non s'abstenir de déclarer des PROTOS

        ------------------------------------------------------------------------------------------------

.386

.model flat, stdcall ;Lignes destinées au compilateur et au linker,format de compilation

option casemap :none

;déclarations de protos non nécessaires , n'ayant pas que des avantages

.const

.data

.code

    premier Proc argument:DWORD

    premier endp

start: ;L'éxécution du code commence içi (etiquette: ....... end etiquette),
 ......

winmain proc

winmain endp

end start

        ------------------------------------------------------------------------------------------------

.386

.model flat, stdcall ;Lignes destinées au compilateur et au linker,format de compilation

option casemap :none

premier PROTO :DWORD;déclarations de protos n'ayant pas que des inconvénients

.const

.data

.code

start: ;L'éxécution du code commence içi (etiquette: ....... end etiquette),

    premier Proc argument:DWORD

    premier endp

winmain proc

winmain endp

end start

    Le point d'entrée du code peut se trouver dans une librairie

STRUCTURE:

Connaitre la structure d'un source asm MASM32 est necessaire a l'écriture et au debugage

Section des déclarations: (ne génère ni code , ni data)

.386
.model flat, stdcall
option casemap :none ; trois lignes importantes pour la compilation

;.STACK 10000 ;par défaut fixé a 1024 octets

include \masm32\include\windows.inc ;toujours en premier

include \masm32\include\user32.inc ;contient les protos (déclarations des PROCs)
includelib \masm32\lib\user32.lib ;contient le code compilé des PROC

GENREPROG PROTO ;déclaration d'un nom de PROC
 return MACRO arg
mov eax, arg
ret
ENDM

CONSTANTES

.const
 CONSTANTE EQU 14

DONNEES içi commence la génération des octets du programme éxécutable

.data
chaine DB "salut",0
Hinstance dd 0
.data? ;L'espace mémoire de ce segment sera alloué au chargement,réduit la taille du fichier
Virtuel dd 100h dup (?) ;on ne peut attribuer une valeur,l'espace nécessaire n'existe pas encore

CODE

.code
START:
INVOKE GetModuleHandle, NULL
mov hInstance, eax
.
INVOKE WinMain,hInstance,NULL, CommandLine, SW_SHOWDEFAULT
.
invoke ExitProcess,MainExit ;FIN du programme
------------------------------------------------------------------------------------------------------------------
;winmain est un simple PROC défini par l'utilisateur .
;Son utilisation permet simplement de bénéficier de variables locales.
;En mettant ses variables locales en data ,on peut le supprimer
WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine :DWORD,CmdShow:DWORD
LOCAL wc :WNDCLASSEX
.
mov wc.lpfnWndProc, offset WndProc ;definition de la fenêtre
.
.
StartLoop: ;boucle d'attente
.
return msg.wParam ;ret
WinMain endp
;Le proto de WndProc est défini par windows,il n'est pas modifiable
;Il tire son nom de la fonction qui le termine DefWindowProc
WndProc proc hWndParent:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
.
.if uMsg == WM_COMMAND
.
invoke GENREPROG
return 0.
.elseif uMsg == WM_CREATE
.
.elseif uMsg == WM_...........
.
.else
.
.end if
invoke DefWindowProc,hWndParent,uMsg,wParam,lParam
ret
 WndProc endp
-------------------- les Proc définis par l'utilisateur et la définition d'un sub --------------------
GENREPROG proc
.
call SousProg
.

Findegenreprog:
ret
SousProg: L'emplacement du sub , juste après le ret, a son importance
FindeSousProg: Le sub accède normalement a toutes les variables du PROC
retn Il est inutile de lui passer des variables
GENREPROG endp

END START

GESTION des EVENEMENTS

Le nom du programme declaré dans la structure wc (içi WndProc) a pour vocation la gestion des evenements.On peut en distinguer deux types,générés par le systeme (WM_........) et générés par les éléments rajoutés sur la feuille.Par exemples:
pour les tab TCM_........
pour les combobox CB_......
et ainsi de suite
Les procédures d'événements traitées doivent retournées une valeur precise. La fonction DefWindowProc placés a la fin de WndProc s'assure que tous les évènements soient traités correctement et complète éventuellement ceux déclarés comme traités.On peut d'ailleurs créer un wndProc n'éxécutant que cette seule fonction.Lorsque DefWindowProc complète l'évènement cela est préciser dans WinHelp. Pour chaque événement traité , il faut s'assurer (winhelp.hlp)que la valeur de retour est correcte.Le plus souvent c'est zero mais pas toujours. Pour s'assurer que tous les événements sont gérés correctement on peut placer la fonction DefWindowProc dans le .else de la boucle if . La, seule les événements non traités passe par la fonction.Si on met la fonction en dehors du bloc IF , des événements déja traités peuvent repasser par elle.Certains comportement aléatoire du programme peuvent s'expliquer par des valeurs de retour incorrecte. Il faut alors faire un tri parmi les messages non traités .La valeur du message etant connu ,on peut retrouver sa designation litterale dans windows.inc .Cela est important l'orsqu'un événement géré en génère d'autres.

Les feuilles MDI utilisent une fonction particulière en fin de boucle d'évènements.La boucle d'évènements ne s'appelle plus WndProc mais FrameProc

Pour les boites de dialogues,Il n'y a pas de fonction de traitement des évènements.Les retours s'écrivent comme suit:

.else ;pas de traitement
mov eax,FALSE ;DialogProc si utilisation d'une classe pour boite
ret
.endif
mov eax,TRUE ;l'évènement a été traité
ret
DlgProc endp

Excepté pour le message WM_INITDIALOG retour FALSE si on a attribué le focus (point d'insertion )

Tout Fonctionne Normalement Mais,.....?

Vous avez eu la patience d'étudier la page et vous commencez à vous doutez qu'en assembleur on doit respecter des règles précises .La question est , mais ... qu'en est il des règles , mal traduites,pas vus , mal expliqués , mal comprises ....Là est le mais provoquant un fonctionnement plus ou moins stable de l'application.Je citerais deux exemples:

1° exemple:Les Handles de feuilles mères et filles

CreateWindowEx est la fonction permettant de créer tout et n'importe quoi.Elle renvoie un handle identifiant l'objet créer.Que se passe-t-il quand le système nous renvoie deux handles pour le même objet ?.C'est ce qui se produit à la création d'une fenêtre

Un des rôles de Winmain est de créer les feuilles principales.On appelle CreateWindowEx , la fonction crée la fenêtre et déclenche l'évènement WM_CREATE du wndproc associé à cette fenêtre.

Une chose doit nous préoccuper immédiatement , c'est que le système nous passe en premier paramètre du proc WndProg le handle de la fenêtre en question.CreateWindowEx nous en fournit un autre (comparer les valeurs).Il est bien tentant de réutiliser ce même handle à l'intérieur de WndProg et là,hélas, on commet une approximation .La tentation est d'autant plus forte que cela marche le plus souvent,mais .... En vérité,si l'on compare la valeur hexadécimale de ces deux handles ,désignant la même fenêtre, on à la surprise de constater leurs différences.Le problème étant posé , on aimerais bien pouvoir réutiliser la même déclaration de variable .Chance , une fois l'étape d'init dépassée , Winmain rentre dans une boucle et n'utilise plus ce handle.On tient donc la solution , rajouter deux lignes immédiatement après WndProg.

WndProc proc uses ebx esi edi hWndParent:DWORD, wmsg, wparam, lparam

mov eax,hWndParent ;handle en pile passé par windows

mov HWndProc,eax ;Handle que nous avons saugardé dans Winmain

Le choix d'utiliser le handle fourni par le WndProc s'est fait après plusieurs essais , en essayant les deux .Sous XP,on ne rencontre que des problèmes en utilisant le handle fourni par CreateWindowEx et il faut considérer comme inutile de le conserver.Dans la suite de winmain , si on doit utiliser le handle de la fenêtre nouvellement créer , on peut utiliser celui fournit par le WndProc sans soucis.

conclusion:Dans le cas précis d'une création de fenêtre , ne pas sauvegarder le handle fourni par CreateWindowEx

2° exemple:Le Multitâche:

De quoi s'agit-il ?.
Exemple :
La liaison internet est d'une extrème lenteur , pour le microprocesseur,et l'utilisateur ,encore plus lent,tape une adresse internet dans l'explorateur.Le microprocesseur va partager son temps entre ces deux tâches sans que l'on constate la moindre anomalie dans son fonctionnement.Le multitâche peut aussi être matériel en faisant travailler plusieurs microprocesseurs,mais c'est un autre problème.
L'exemple le plus simple est donné par le batch MAKE servant a produire le .lib des librairies.Le répertoire C:\masm32\M32LIB contient toutes les sources de la librairie masm32.Le batch MAKE se charge de toutes les opérations nécessaires à leur utilisation.En premier, il utilise ML pour produire les .obj.En second il utilise LINK pour réunir les .obj dans un .lib.En troisieme il efface tous les .obj devenus inutiles.Du au fonctionnement normal de windows,le link demandant un certain temps,les opérations de suppression des .obj s'éxécutent en même temps,et le link produit une librairie vide.La solution consiste a insérer "pause" dans le batch pour permettre la fin des opérations du link avant d'effacer les .obj.

Autre exemple pratique :
Dans editmasm on a ouvert un projet (plusieurs fichiers) et on aimerait bien en ouvrir un autre.Notre PROC va bien faire les choses,et va fermer le projet ouvert avant d'en ouvrir un autre.Pour cela , il va appeler un proc spécialiser dans la fermeture des projets.Le microprocesseur se retrouve avec deux tâches à accomplir et partage son temps entre les deux .Cela donne ,Vidé un vase d'un côté et le remplir de l'autre .Résultat,sans être planté , le programme est bloqué

Solution:Utiliser un témoin (variable) pour dire si un projet est présent et interdire le chargement d'un projet lorsqu'un autre existe.

Modale ou Modeless ?:
La boite de dialogue modale joue le même rôle que pause insérer dans le fichier batch MAKE

Si vous voyez d'autres attrapes nigots du même genre , n'hésitez pas à m'en faire part.

Point d'entrée du code:

On a utiliser la séquence etiquette: end etiquette pour définir le point d'entrée du code à etiquette.On peut aussi le définir dans une librairie.Pour cela il faut définir deux proc dans la librairie,mainCRTStartup et WinMainCRTStartup

On n'utilise plus l'étiquette.Lorsque link émet une erreur "couldn't find WinMainCRTStartup" cela veut dire qu'une librairie utilise ce type d'entrée et demande un nom de prog particulier (WinMain).

Librairie

La librairie qui suit est a créer avec LIB.EXE

.code

.........

mainCRTStartup proc c public

;SUBSYSTEM:CONSOLE option de link

invoke Init

invoke StartupMasm ;StartupMasm est le nom du proc ou va commençer le code dans l'éxécutable

invoke ExitProcess,eax

mainCRTStartup endp

WinMainCRTStartup proc c public

;SUBSYSTEM:WINDOWS

invoke Init

invoke StartupMasm

invoke FreeRicheditDll

invoke ExitProcess,eax

WinMainCRTStartup endp

..........

end

Executable

Normalement créer avec LINK.EXE

.code

.........

StartupMasm PROC

ret

StartupMasm ENDP

...........

end ;pas d'étiquette

Pour utiliser msvcrt.lib et la libcmt.lib

WinMain PROC STDCALL public uses esi edi ebx, hInst:DWORD,hPrev:DWORD,\

lpCmdLine:DWORD,nShowCmd:SDWORD

;...................................................

ret

WinMain endp

CONSOLE

main PROTO C :DWORD, :DWORD


Debug (command)

From Wikipedia, the free encyclopedia
Jump to: navigation, search
This article may be too technical for most readers to understand. Please help improve this article to make it understandable to non-experts, without removing the technical details. The talk page may contain suggestions. (November 2014) (Learn how and when to remove this template message)
Debug
Original author(s) | Tim Paterson
Developer(s) | Microsoft
Operating system | DOS, MS-DOS, OS/2, Microsoft Windows
Type | Debugger
License | Proprietary commercial software
Website | Debug

debug is a command in DOS, OS/2 and Microsoft Windows (only in 32bit[1]) which runs the program debug.exe (or DEBUG.COM in older versions of DOS).[2] Debug can act as an assembler, disassembler, or hex dump program allowing users to interactively examine memory contents (in assembly language, hexadecimal or ASCII), make changes, and selectively execute COM, EXE and other file types. It also has several subcommands which are used to access specific disk sectors, I/O ports and memory addresses.[3]

Contents

[hide]
  • 1 Background
  • 2 Syntax
  • 3 Using for non-debugging purposes
  • 4 See also
  • 5 References

Background

Traditionally, all computers and operating systems have included a maintenance function, used to determine whether a program is working correctly.[citation needed] Debug was written by Tim Paterson to serve this purpose in QDOS. When Paterson began working for Microsoft in the early 1980s he brought the program with him. Debug was part of DOS 1.00 and has been included in MS-DOS and Microsoft Windows. DOS debug has several limitations:

  • It can only access 16-bit registers and not 32-bit extended registers.
  • When the "n" subcommand for naming files is used, the filename is stored from offset DS:5D to DS:67 (the Program Segment Prefix File Control Block area), meaning that the program can only save files in FAT 8.3 filename format.

Enhanced DEBUG package includes a 32-bit clone "DEBUGX" version supporting 32-bit DPMI programs as well.[4] Andreas "Japheth" Grech, the author of the HX DOS extender, developed enhanced DEBUG versions 0.98…1.25, and former PC DOS developer Vernon Brooks added versions 1.26…1.32.[5]

Syntax

debug [[drive:][path] filename [parameters]]

When Debug is started without any parameters the Debug prompt, a "-" appears. The user can then enter one of several one or two-letter subcommands, including "a" to enter the assembler mode, "d" to perform a hexadecimal dump, "t" to trace and "u" to unassemble (disassemble) a program in memory.[6] Debug can also be used as a "debug script" interpreter using the following syntax.

debug < filename

A script file may contain Debug subcommands and assembly language instructions.[3] This method can be used to create or edit binary files from batch files.[2]

Using for non-debugging purposes

The Debug utility was useful for editing binary files in an environment where only MS-DOS is installed without anything else. It can also be used to edit disk sectors, which was one method of removing boot-sector viruses in the MS-DOS era.


L'utilitaire DEBUG

17

Cette page en format PDF

DEBUG

Debug est un utilitaire mis à la disposition des utilisateurs des PC pour la mise au point des programmes. Il convenait parfaitement à cette tâche quand on programmait les premiers PC en assembleur ce qui ne se fait plus que très exceptionnellement aujourd'hui. Le langage assembleur ne sert plus que pour des tâches dites de bas niveau. Entendons par-là les fonctions très proches du hardware.
Actuellement les programmeurs utilisent des programmes de mise au point beaucoup plus performants. Les débogueurs actuels sont des outils graphiques adaptés aux langages de haut niveau et au développement d'applications graphiques.

L'intérêt du programme DEBUG est qu'il est fourni avec tous les systèmes d'exploitations de Microsoft depuis le DOS 2.0 jusqu'aux plus récents comme Windows XP. Ce petit programme est présent dans tous les PC et même sur les disquettes de démarrage. Nous utiliserons DEBUG lors des prochaines séances de laboratoire pour explorer la mémoire et des entrées / sorties ou même pour écrire quelques lignes d'assembleur en espérant acquérir de cette manière une meilleure représentation du coeur du système.

Essai du programme DEBUG

Ouvrez une fenêtre DOS et appelez le programme de DEBUG :
C:\>DEBUG
-
 La réponse de DEBUG est un tiret placé au début de la ligne. C'est "l'invite " de DEBUG. Elle annonce que le programme est prêt à recevoir une commande. Chaque commande se fait en tapant une seule lettre (D pour " Dump ", Q pour " Quit ") suivie de paramètres quand c'est nécessaire. La commande la plus pratique est celle pour demander de l'aide, le point d'interrogation. Voici ce que cela donne :
-?
assemble (assembler)         A [adresse]
compare (comparer)           C plage adresse
dump (lister)                D [plage]
enter (entrer)               E adresse [liste]
fill (remplir)               F Plage liste
go (exécuter)                G [=adresse] [adresses]
hex (hexadécimal)            H valeur1 valeur2
input (entrer depuis port)   I port
load (charger)               L [adresse] [lecteur] [secteur] [nbre]
move (déplacer)              M plage adresse
name (désigner)              N [nchemin] [listearg]
output (envoyer sur port)    O port val
proceed (avancer)            P [=adresse] [nbre]
quit (quitter)               Q
register (registre)          R [registre]
search (rechercher)          S plage liste
trace (tracer)               T [=adresse] [valeur]
unassemble (désassembler)    U [plage]
write (écrire)               W [adresse] [lecteur] [secteur] [nbre]

allocate expanded memory (allouer EMS)        XA [#pages]
deallocate expanded memory (désallouer EMS)   XD [desc]
map expanded memory pages (affecter)          XM [pageL] [pageP] [desc]
display expanded memory status (état EMS)     XS

La commande debug peut être invoquée en précisant le nom d'un fichier à charger en mémoire. Le nom du fichier doit obligatoirement être précisé avec son extension.
Exemple : DEBUG test.exe

Les adresses mémoire

L'unité de mémoire est l'octet. Chaque octet a une adresse différente.
Les deux seules opérations possibles en mémoire sont la lecture et l'écriture.
L'adresse mémoire est codée en binaire mais nous la condensons par du code hexadécimal.
Elle se note en deux parties : le segment et l'offset
Adresse physique = Segment * 16 + Offset

Exemple : 1234:0000 = 12340

On peut donc accéder à la même adresse mémoire à partir de segments distincts
Exemple : les adresses logiques C000:0010 et C001:0000 correspondent toutes deux à la même adresse physique C0010

Cette répartition de l'adresse en segment et offset est due au fait que les microprocesseurs des premiers PC n'avaient que 16 bits par registre alors qu'ils avaient 20 bits pour former leurs adresses. La plus grande adresse écrite avec 20 bits est donc FFFFF soit 1 MB. C'est la taille de la mémoire conventionnelle, la mémoire vue par le DOS.

On peut ainsi diviser la mémoire en un certain nombre de segments. La taille de chaque segment est un multiple de 16 octets ( de 16 à 65536)
Les adresses des programmes et des données sont spécifiées relativement à des segments. Les adresses de base de ces segments sont inscrites dans des registres de segment du CPU.
Les registres CS, DS, SS et ES sont des registres de segment. Une fois dans DEBUG vous pouvez voir le contenu des registres avec la commande R.

A La commande "assemble" permet d'enregistrer des instructions en langage assembleur.
Ex. Entrez ce petit programme de 4 instructions qui additionne les nombres 1234 et 5678, en hexadécimal.

-A CS:100
150F:0100 MOV AX,1234
150F:0103 MOV BX,5678
150F:0106 ADD AX,BX
150F:0108 INT 20
150F:010A

Terminez la saisie de ce code par une ligne sans instruction.

D La commande "Dump" permet d'afficher le contenu d'une zone mémoire

Ex. Faite avec le programme EDIT un petit fichier texte et appelez-le Toto.txt
Nous chargeons ce texte en mémoire avec la commande DEBUG ensuite nous examinons son code en mémoire :

C:\>DEBUG toto.txt
-D
150F:0100 43 65 20 70 65 74 69 74-20 74 65 78 74 65 20 73 Ce petit texte s
150F:0110 65 72 74 20 64 27 65 78-65 6D 70 6C 65 0D 0A 14 ert d'exemple...
150F:0120 07 89 46 FA 0B C0 75 06-46 39 76 FE 77 DA 8A 45 ..F...u.F9v.w..E

Chaque ligne commence par une adresse sous la forme Segment:Offset
Viennent ensuite les valeurs de 16 octets successifs affichées en hexadécimal.
Au bout de la ligne on retrouve un format texte quand c'est possible.
Les octets qui ne correspondent pas à des codes ASCII imprimables y sont représentés par des points.
La commande Dump accepte des paramètres.

D offset | L'offset combiné avec l'adresse de base contenue dans DS indique le premier octet à afficher. Le nombre de byte n'étant pas précisé, le programme affiche 128 bytes par défaut (8 lignes de 16 octets).
D ES:offset | Affichage à partir de l'adresse [ES] :offset , l'offset étant un nombre sous forme hexadécimale. ES est ici un exemple, les registres de segment DS, SS et CS peuvent aussi être mentionnés de cette manière. Si aucune adresse de base n'est spécifiée, la commande dump considère que le registre DS contient l'adresse de base du segment.
D | La commande D employée seule affiche les 128 suivants
D base:offset | Base et Offset sont deux nombres de 16 bits écrits en hexadécimal.
D début fin | Affichage d'une zone mémoire délimitée entre deux adresses
D début L long | Idem en indiquant la taille de la zone plutôt que l'adresse où elle se termine

Exemple : La date du BIOS est stockée dans chaque PC à l'adresse FFFF5. Voici comment l'obtenir :

C:\>DEBUG
-D FFFF:5 L 8
FFFF:0000 30 37 2F-32 35 2F 30 32 07/25/02
-Q

Attention la date est sous la forme américaine mois/jour/année. Le BIOS de cet exemple date donc du 25 juillet 2002.

E "Enter " est la commande pour entrer de nouvelles valeurs en mémoire.
On lui indique l'adresse du début de la zone mémoire à examiner puis les valeurs à y inscrire.

-E CS:100 41,42,43,44,45,46,47

ou ce qui revient au même puisque (41 est le code ASCII de A, 42 de B etc.)

-E CS:100 "ABCDEFGH"
-D CS:100
150F:0100 41 42 43 44 45 46 47 48-20 74 65 78 74 65 20 73     ABCDEFGH texte s
150F:0110 65 72 74 20 64 27 65 78-65 6D 70 6C 65 0D 0A 14     ert d'exemple...

La commande E permet aussi d'examiner la mémoire byte par byte et des les modifier un à un. Tapez E puis l'adresse du byte mémoire que l'on veut et ENTER
L'adresse s'affiche à la ligne suivante. Elle est suivie du contenu du byte examiné puis d'un point.

-E B800:09F0
B800:00A0 43.41 07. 3A.42 07.

Tapez une autre valeur puis un espace pour passer au byte suivant. Si vous voulez passer au byte suivant sans modifier la valeur du byte affiché il suffit de taper de suite un espace (en s'interdisant de faire un retour chariot). La touche 'moins' permet de revenir en arrière. Vous pouvez examiner ainsi plusieurs bytes en les modifiants ou non. Pressez la touche ENTER pour terminer les modifications.

F " fill " Rempli une zone mémoire avec le ou les codes spécifiés.
Syntaxe : F plage liste
Exemples :
(Attention à bien respecter les adresses indiquées pour ne pas aller jardiner n'importe où !)

- F B800:1E0 L A0 6E
Remplit la plage mémoire qui part de l'adresse B800 :01E0 et qui a une longueur de 160 bytes avec le ode 6E. (Vous devriez voir la quatrième ligne de votre écran se remplir avec des caractères n en jaune sur fond kaki. L'adresse B8000 était celle du début de la mémoire vidéo à l'époque du DOS. Cette mémoire vidéo est parfaitement simulée par WINDOWS)

- F B800:1E0 L A0 41 4E
Remplit la quatrième ligne de l'écran avec des A majuscules en jaune sur fond rouge.

G " Go " Lance le programme. La commande accepte deux paramètres mais ils sont facultatifs : l'adresse de départ que l'on fait précéder du signe égale et une éventuelle adresse de point d'arrêt.
Exemples :

- G | Commande go sans paramètres. Le programme démarre donc depuis l'adresse CS :IP
- G =100 | Lance le programme à partir de l'adresse CS:0100
- G =100 108 | Lance le programme depuis l'adresse CS:0100 en plaçant un point d'arrêt à l'adresse CS:0108

H " HEX " sert de calculatrice hexadécimale. On lui spécifie deux nombres et on obtient la somme et la différence.

- H 10 2
0012 000E
- H 10 20
0030 FFF0

I " Input port "

Lecture d'un port d'entrée/sortie. Le seul paramètre de la commande indique le numéro du port que l'on désire lire.
- I 378 affiche le byte qui est contenu à l'adresse 378 des entrées / sorties.
NB. Cela permet de lire les ports d'entrées mais aussi relire aux adresses des ports de sorties les codes qui y ont été écrits.

L " Load "

Chargement en mémoire du fichier dont le nom a déjà été spécifié comme argument de la commande DEBUG ou à l'aide de la commande N
La commande L permet aussi la lecture des secteurs d'un disque sans tenir compte du système de fichier.

Syntaxe : L adresse N°disque N°SecteurDeDépart NombreDeSecteurs

Exemple : L 100 2 0 1
= Charger à l'adresse 100, le secteur n° 0 du disque n° 2 (A=0, ... C= 2)

Les registres BX:CX donnent sur 32 bits la taille des données chargées en mémoire.

M " Move "

Recopie le contenu d'une plage mémoire vers une nouvelle adresse.
- M 100 10F 140
Copie en DS:0140 le contenu de la plage mémoire allant de DS:0100 à DS:010F

- M 100 L 10 140
Cette commande a le même effet que la précédente. La seule différence est qu'ici la plage à recopier n'est pas donnée par les adresses de début et de fin ( 100 et 10F) mais par son adresse de départ et par sa taille ( 100 et 10)

N " Name "

Indiquer le nom du fichier à ouvrir pour le lire ultérieurement avec la commande L (Load) ou y écrire avec la commande W (write).

O " Output port value "

Ecriture dans un port de sortie
- O 378 FF
écrit le code FF dans le port dont l'adresse est 378

Q " Quit "

Sortir du programme DEBUG.

P " Proceed " Exécution du programme pas à pas, un peu de la même manière que la commande T (trace) mais sans entrer dans les routines appelées.

Voir les commandes similaires : T et G

R " Read " Commande de visualisation des registres

-R
AX=0000 BX=0000 CX=0021 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=150F ES=150F SS=150F CS=150F IP=0100 NV UP EI PL NZ NA PO NC
150F:0100 41 INC CX

Le registre CS "Code Segment" contient l'adresse de base du segment où se trouve le code du programme que l'on teste avec DEBUG. C'est dans l'exemple ci-dessus le segment 150F. Le registre IP est le registre d'instruction. Il contient ici la valeur 0100.
La première instruction du programme est donc à l'adresse 150F:0100 ce qui équivaut à l'adresse physique 151F0. ( Segment x 1OH + Offset)

Remarquez que si debug vient d'être appelé avec comme argument le nom d'un fichier, les registres BX :CX contiennent la taille du fichier chargé. 21H dans l'exemple ci-dessus.

La commande R permet aussi de modifier un registre mais il faut alors préciser lequel.

-R CX | demander l'affichage du registre CX
CX 0021 | CX contient la valeur hexadécimale 0021
:ABCD | les ':' sont une invite à la modification
- |

T " Trace " Exécution des instructions pas à pas en affichant après chacune d'elles l'état des registres et le code de l'instruction suivante.

Exemples :
- T Exécute et trace l'instruction qui se trouve à l'adresse CS:IP
AX=1234 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=150F ES=150F SS=150F CS=150F IP=0103 NV UP EI PL ZR NA PE NC
150F:0103 BB7856 MOV BX,5678
-

La commande T accepte deux paramètres :
- l'adresse de départ à faire précéder du signe égale ( ex. T =100)
- le nombre d'instruction à tracer ( ex. T 3)

Ces paramètre peuvent s'utiliser simultanément ( ex. T =100 3 )

Commandes similaires : G et P

U " Unassemble " Commande de "désassemblage". Affiche le contenu de la mémoire en utilisant les mnémoniques du langage assembleur pour afficher les instructions.

Exemples :
-U 100 106 Demande l'affichage des instructions comprises entre les adresses CS:0100 et CS:0106
150F:0100 MOV AX,1234
150F:0103 MOV BX,5678
150F:0106 ADD AX,BX
-
 Syntaxe : U [plage]

Commande associée : A

W "write" Ecriture sur le disque.
Le nom du fichier à écrire est soit le nom du fichier chargé par la commande debug soit un nom donné par la commande N.
La taille de la zone mémoire à enregistrer sur le disque peut être donnée en 32 bits via les registres BX:CX.
L'adresse de départ de la zone mémoire est CS :100 par défaut à moins que vous ne précisiez une autre.
La commande W permet aussi l'écriture d'un disque en précisant uniquement le ou les secteurs à écrire. Inutile de préciser qu'il vaut mieux savoir ce que l'on fait avant d'écrire une telle commande sinon c'est la scoumoune !!!

Syntaxe : W [adresse] [lecteur] [secteur] [nbr]

Autres pages en rapport avec ce sujet

- Introduction à l'assembleur - Etapes du développement d'un programme -

CoursTechInfo > Système d'exploitation > Le programme DEBUG
XHTML 1.0 validé!


LA COMMANDE DEBUG

Commande externe. Fonctionne sur la ligne de commande ou dans un batch.

L'utilitaire Debug permet officiellement de deboger un programme (de le mettre au point), mais surtout il offre l'opportunite de faire ses premieres armes en assembleur (un cours est prevu, patience...). De plus il est encore present dans windows 95...(mais avec une aide ridicule).

Syntaxe

DEBUG [[lecteur:][chemin]fichier [param]]

Parametres

[lecteur:][chemin]fichier

Precisent l'emplacement et le nom du fichier executable a tester.

param

Precise toute information, a inclure dans la ligne de commande, necessaire au fichier executable que vous voulez tester.

Si vous utilisez la commande DEBUG sans preciser aucun emplacement ou nom de fichier, vous devez taper toutes les commandes a la suite de l'invite de Debug (un tiret).

Description des commandes de DEBUG

? [Affiche une liste des commandes de Debug]

A [Assemble des mnemoniques 8086/8087/8088 (marche aussi pour processeur 32 bits)]

C [Compare deux zones de la memoire]

D [Affiche le contenu d'une zone de la memoire]

E [Entre des donnees en memoire a partir d'une adresse donnee]

F [Remplit une zone de memoire avec des valeurs donnees]

G [Execute le fichier executable charge en memoire]

H [Realise des operations arithmetiques sur des valeurs hexadecimales]

I [Affiche la valeur d'un octet d'un port donne]

L [Charge en memoire un fichier ou des secteurs de disque]

M [Copie le contenu d'un bloc de memoire]

N [Specifie un nom de fichier pour les commandes L et W ou specifie les parametres pour le fichier mis au point]

O [Envoie la valeur d'un octet vers un port de sortie]

P [Execute une boucle, une instruction sur chaine repetee, une interruption logicielle ou une sous routine]

Q [Quitte Debug]

R [Affiche ou modifie le contenu d'un ou plusieurs registres]

S [Recherche, dans une zone de memoire, une sequence d'octets donnee]

T [Execute une instruction puis affiche le contenu de tous les registres, l'etat de tous les indicateurs et la forme decodee de l'instruction que Debug executera ensuite]

U [Desassemble des octets et affiche les instructions source correspondantes]

W [Ecrit le fichier en cours de mise au point sur disque]

XA [Alloue de la memoire paginee]

XD [Desactive l'allocation d'un descripteur de memoire paginee]

XM [Affecte des pages logiques aux pages physiques de la memoire paginee]

XS [Affiche des informations sur l'etat de la memoire paginee]

Resume des commandes reconnues par Debug et syntaxe

MNEMONIQUE | SYNTAXE
assemble (assembler) | A [adresse]
compare (comparer) | C plage adresse
dump (lister) | D [plage]
enter (entrer) | E adresse [liste]
fill (remplir) | F plage liste
go (executer) | G [=adresse] [adresses]
hex (hexadecimal) | H valeur1 valeur2
input (entrer depuis un port) | I port
load (charger) | L [adresse] [lecteur] [secteur] [nombre]
move (deplacer) | M plage adresse
name (designer) | N [nchemin] [listearg]
output (envoyer sur port) | O port val
proceed (avancer) | P [=adresse] [nbre]
quit (quitter) | Q
register (registre) | R [registre]
search (rechercher) | S plage liste
trace (tracer) | T [=adresse] [valeur]
unassemble (desassembler) | U [plage]
write (ecrire) | W [adresse] [lecteur] [secteur] [nombre]
allocate expanded memory (allouer EMS) | XA [#pages]
deallocate expanded memory (desallouer EMS) | XD [desc]
map expanded memory pages (affecter) | XM [pageL] [pageP] [desc]
display expanded memory status (etat EMS) | XS
help debug (aide en ligne de Debug) | ?

Separation des parametres d'une commande

Toutes les commandes de Debug acceptent des parametres, a l'exception de la commande Q. Vous pouvez separer les parametres par des virgules ou des espaces mais ces separateurs ne sont requis qu'entre deux valeurs hexadecimales. En consequence, les commandes suivantes sont equivalentes:

dcs:100 110

d cs:100 110

d,cs:100,110

Adresses valides

Dans une commande de Debug, le parametre adresse specifie un emplacement en memoire. L'adresse est une designation en deux parties contenant d'une part un nom de registre de segment ou une adresse de segment sur quatre chiffres, d'autre part une valeur de decalage. Le nom ou l'adresse du segment sont facultatifs. Le segment par defaut pour les commandes A, G, L, T, U et W est CS. La valeur par defaut pour les autres commandes est DS. Toutes les valeurs numeriques sont en format hexadecimal.

Exemples d'adresses valides

CS:0100

04BA:0100

Les deux points (:) entre le nom de segment et la valeur de deplacement sont requis.

Plages valides

Dans une commande de Debug, le parametre plage specifie une plage de memoire. Vous avez le choix entre deux formats: une adresse de debut et une adresse de fin, ou bien une adresse de debut et la longueur (indiquee par L) de la plage.

Par exemple, les deux syntaxes suivantes specifient la plage de 16 octets qui commence a l'adresse CS:100:

cs:100 10f

cs:100 l 10

A (Assemble) Retour en haut

Assemble les mnemoniques 8086/8087/8088 directement en memoire. Fonctionne egalement pour les processeur 32 bits.

Cette commande cree du code machine executable a partir d'instructions en langage assembleur. Toutes les valeurs numeriques sont en format hexadecimal et vous devez les taper sous la forme d'une serie de 1 a 4 caracteres. Specifiez le prefixe mnemonique avant le code operatoire (opcode) auquel il fait reference.

Syntaxe

A [adresse]

Parametres

adresse

Specifie l'emplacement ou vous tapez les mnemoniques du langage assembleur. Utilisez une valeur hexadecimale en tant qu'adresse et tapez chaque valeur sans le caractere "h" final. Si vous ne precisez pas d'adresse, la commande A commence l'assemblage a l'adresse a laquelle elle s'est arretee lors de son utilisation precedente.

Remarques

Utilisation des mnemoniques

Les mnemoniques de specification de segment sont CS:, DS:, ES: et SS:. Le mnemonique de Retour eloigne (far) est RETF. Les mnemoniques de manipulation de chaine doivent specifier explicitement la taille de la chaine. Par exemple, utilisez MOVSW pour deplacer des chaines de mots (16 bits) et MOVSB pour deplacer des chaines d'octets (8 bits).

Assemblage de branchements et d'appels

L'assembleur assemble automatiquement un branchement ou un appel court (short), proche (near) ou eloigne (far) selon le deplacement d'octets vers l'adresse de destination. Vous pouvez utiliser un prefixe NEAR ou FAR pour imposer le type de branchement ou d'appel, comme illustre ci-apres:

-a0100:0500

0100:0500 jmp 502 ; branchement court de 2 octets

0100:0502 jmp near 505 ; branchement proche de 3 octets

0100:0505 jmp far 50a ; branchement eloigne de 5 octets

Le prefixe NEAR peut etre abrege en NE.

Distinction entre les adresses de mots et d'octets

Lorsqu'un operande peut faire reference a une adresse de mot ou d'octet, vous devez explicitement preciser le type de donnees par l'un des prefixes WORD PTR ou BYTE PTR. Les abreviations acceptees sont respectivement WO et BY. L'exemple suivant illustre ces deux formats:

ec wo [si]

neg byte ptr [128]

Specification des operandes

Debug utilise la convention usuelle indiquant qu'un operande entre crochets fait reference a l'adresse memoire. La raison en est que Debug ne peut distinguer un operande immediat d'un operande faisant reference a une adresse memoire. L'exemple suivant illustre ces deux formats:

mov ax,21 ; charge AX avec la valeur 21h

mov ax,[21] ; charge AX avec le contenu de l'adresse memoire 21h

Utilisation de pseudo-instructions

Deux pseudo-instructions sont disponibles avec la commande A, les codes operatoires DB et DW, qui effectuent respectivement l'assemblage d'octets et de mots directement en memoire. Voici quelques exemples de ces pseudo-instructions:

db 1,2,3,4,"VOICI UN EXEMPLE"

db "CECI EST UNE ETOILE: *"

db "CECI EST UNE APOSTROPHE: '"

dw 1000,2000,3000,"BACH"

Exemple

La commande A reconnait toutes les formes de commandes portant sur des registres de facon indirecte. Par exemple:

add bx,34[bp+2].[si-1]

pop [bp+di]

push [si]

Tous les codes operatoires synonymes sont egalement acceptes, comme illustre dans l'exemple suivant:

loopz 100

loope 100

ja 200

jnbe 200

Pour les codes operatoires de 8087, les prefixes WAIT et FWAIT doivent etre specifies, comme illustre dans les exemples suivants:

fwait fadd st,st(3) ; cette ligne assemble un prefixe fwait

C (Compare) Retour en haut

Compare deux zones de la memoire.

Syntaxe

C plage adresse

Parametres

plage

Specifie les adresses de debut et de fin, ou l'adresse de debut et la longueur de la premiere zone de memoire a comparer.

adresse

Precise l'adresse de debut de la deuxieme zone de memoire a comparer.

Remarques

Si les deux parties de memoire definies par plage et adresse sont identiques, Debug n'affiche rien et vous renvoie directement a l'invite de Debug. S'il existe des differences, Debug les affiche selon le format suivant:

adresse1 octet1 octet2 adresse2

Exemple

Les deux commandes suivantes ont le meme effet: c100,10f 300 ou c100l10 300 Chacune compare le bloc de memoire s'etendant de 100h a 10Fh a celui s'etendant de 300h a 30Fh. Debug repond dans les deux cas avec un listing similaire a celui-ci (en supposant que DS = 197F):

197F:0100 4D E4 197F:0300

197F:0101 67 99 197F:0301

197F:0102 A3 27 197F:0302

197F:0103 35 F3 197F:0303

197F:0104 97 BD 197F:0304

197F:0105 04 35 197F:0305

197F:0107 76 71 197F:0307

197F:0108 E6 11 197F:0308

197F:0109 19 2C 197F:0309

197F:010A 80 0A 197F:030A

197F:010B 36 7F 197F:030B

197F:010C BE 22 197F:030C

197F:010D 83 93 197F:030D

197F:010E 49 77 197F:030E

197F:010F 4F 8A 197F:030F

Notez que les adresses 197F:0106 et 197F:0306 ne sont pas dans la liste. Cela signifie que les valeurs contenues a ces adresses sont identiques.

D (Dump) Retour en haut

Affiche le contenu d'une plage d'adresses memoire.

Syntaxe

D [plage]

Parametre

plage

Specifie les adresses de debut et de fin, ou l'adresse de debut et la longueur de la zone memoire a afficher.

Remarques

Lorsque vous utilisez la commande D, Debug affiche le contenu de la memoire en deux parties: l'une au format hexadecimal (chaque octet est represente dans ce format), l'autre au format texte ASCII (les octets sont affiches sous forme de caracteres ASCII). Chaque caractere non imprimable est affiche sous la forme d'un point (.) dans la partie ASCII de l'affichage. Chaque ligne permet d'afficher le contenu de 16 octets avec un tiret entre le 8eme et le 9eme octet. Chaque ligne affichee commence par un octet d'adresse multiple de 16.

Exemple

Si vous tapez la commande: dcs:100 10f Debug affiche le contenu de la memoire sous le format:

04BA:0100 54 4F 4D 00 53 41 57 59-45 52 00 00 00 00 00 00

TOM.SAWYER......

Si vous tapez la commande D sans parametre, l'affichage se presente comme ci dessus. Chaque ligne commence par une adresse dont la valeur est de 16 octets de plus que l'adresse precedente (ou de huit octets si vous avez un ecran de 40 colonnes). Pour chaque commande D suivante tapee sans parametre, Debug affiche les octets qui suivent immediatement les derniers octets affiches par la commande precedente.

Si vous tapez la commande suivante, Debug affiche le contenu de 20h octets en commencant a l'adresse CS:100:

dcs:100 l 20

Si vous tapez la commande suivante, tous les octets dans la plage d'adresse de 100h a 115h du segment CS sont affiches:

dcs:100 115

E (Enter) Retour en haut

Permet d'entrer des donnees en memoire a l'adresse specifiee.

Vous pouvez taper des donnees en format hexadecimal ou ASCII. Toute donnee precedemment stockee a l'adresse specifiee est perdue.

Syntaxe

E adresse [liste]

Parametres

adresse

Specifie la premiere adresse en memoire a laquelle vous voulez placer les donnees.

liste

Specifie les donnees que vous voulez placer dans les octets successifs de la memoire.

Remarques

Utilisation du parametre adresse

Si vous entrez une valeur pour adresse et pas de valeur pour le parametre liste facultatif, Debug affiche l'adresse et son contenu, reaffiche l'adresse sur la ligne suivante et attend votre entree. Vous pouvez alors realiser l'une des actions suivantes:

Remplacer la valeur d'octet par une autre. Pour cela, tapez la valeur souhaitee apres la valeur courante. Si vous n'entrez pas une valeur hexadecimale valide ou si elle a plus de deux chiffres, le caractere non valide ou excedentaire ne recoit pas d'echo.

Passer a l'octet suivant. Pour cela, appuyez sur la touche ESPACE. Pour changer la valeur dans cet octet, tapez la nouvelle valeur apres la valeur en cours. Si vous avancez de plus de 8 octets avec la touche ESPACE, Debug affiche une nouvelle ligne et une nouvelle adresse au debut de celle-ci.

Passer a l'octet precedent. Pour cela, tapez un TIRET. Repetez l'operation pour reculer de plusieurs octets. Lorsque vous tapez un TIRET, Debug affiche une nouvelle ligne, l'adresse en cours et la valeur de l'octet.

Arreter la commande E. Pour cela, appuyez sur ENTREE. Vous pouvez utiliser cette touche a tout moment.

Utilisation du parametre liste

Si vous precisez des valeurs pour le parametre liste, la commande E utilise sequentiellement ces valeurs pour remplacer les valeurs existantes. Si une erreur est detectee, aucune valeur n'est remplacee. Les valeurs de liste peuvent etre des valeurs hexadecimales d'octets ou des chaines. Vous devez les separer par un espace, une virgule ou un caractere de tabulation. Vous devez egalement placer les chaines entre apostrophes ou entre guillemets.

Exemple

Supposons que vous tapiez la commande suivante: ecs:100 Debug affiche le contenu du premier octet dans le format suivant:

04BA:0100 EB._

Pour attribuer a cet octet la valeur 41, tapez 41 au niveau du curseur, comme suit:

04BA:0100 EB.41_

Vous pouvez taper des valeurs d'octet consecutives au moyen de la commande E. Pour passer aux octets suivants, appuyez sur la touche ESPACE plutot que sur ENTREE. Debug affiche la valeur suivante. Dans cet exemple, si vous appuyez trois fois sur la touche ESPACE, Debug affiche les valeurs suivantes:

04BA:0100 EB.41 10. 00. BC._

Pour attribuer a la valeur hexadecimale BC la valeur 42, tapez simplement 42 au niveau du curseur, comme suit:

04BA:0100 EB.41 10. 00. BC.42_

Supposons maintenant que vous souhaitiez modifier la valeur 10 en 6F. Pour cela, tapez deux fois le TIRET jusqu'a ce que l'adresse 0101 soit affichee (valeur 10). Debug affiche:

04BA:0100 EB.41 10. 00. BC.42-

04BA:0102 00.-

04BA:0101 10._

Tapez 6f au niveau du curseur pour changer la valeur:

04BA:0101 10.6f_

Appuyez sur la touche ENTREE pour arreter la commande E et revenir a l'invite Debug. L'exemple suivant illustre l'entree d'une chaine:

eds:100 "Ceci est un exemple de texte" Cette chaine occupera 28 octets a partir de DS:100.

F (Fill) Retour en haut

Remplit une zone de memoire specifiee avec les valeurs que vous precisez.

Vous pouvez specifier des donnees en format hexadecimal ou ASCII. Toute donnee precedemment stockee a l'adresse specifiee est perdue.

Syntaxe

F plage liste

Parametres

plage

Specifie les adresses de debut et de fin, ou l'adresse de debut et la longueur de la zone a remplir.

liste

Specifie les donnees a entrer. Le parametre liste peut se composer de nombres hexadecimaux ou d'une chaine entre guillemets.

Remarques

Utilisation du parametre plage

Si plage contient plus d'octets que le nombre de valeurs indique dans liste, la commande Debug repete les valeurs de liste jusqu'a ce que tous les octets de plage soient remplis. Si l'une des adresses memoire de plage n'est pas valide ou n'existe pas,Debug affiche un message d'erreur et arrete la commande F.

Utilisation du parametre liste

Si liste contient plus de valeurs que le nombre d'octets de plage, les valeurs de liste excedentaires sont ignorees.

Exemple

Si vous tapez la commande: f04ba:100l100 42 45 52 54 41

Debug modifie le contenu des adresses memoires 04BA:100 a 04BA:1FF au moyen des valeurs specifiees. Les cinq valeurs sont repetees jusqu'a ce que les 100h octets soient remplis.

G (Go) Retour en haut

Execute le programme charge en memoire.

Syntaxe

G [=adresse] [interruptions]

Parametres

=adresse

Specifie l'adresse a laquelle vous voulez executer la commande G. Si vous ne precisez pas d'adresse, le DOS commence l'execution du programme a l'adresse en cours dans les registres CS:IP.

interruptions

Specifie entre 1 et 10 points d'interruption temporaires pour la commande G.

Remarques

Utilisation du parametre adresse

Vous devez faire preceder le parametre adresse d'un signe egal (=) afin de distinguer l'adresse de debut (adresse) des points d'interruption (interruptions).

Specification des points d'interruption

Le programme arrete l'execution au premier point d'interruption rencontre, independamment de sa position dans la liste des interruptions. Debug remplace l'instruction d'origine de chaque point par un code d'interruption. Quand le programme atteint un point d'interruption, Debug restaure toutes les adresses de point d'interruption a leurs instructions d'origine, puis affiche le contenu de tous les registres, l'etat de tous les indicateurs et la forme decodee de la derniere instruction executee. Debug affiche les memes informations qu'avec la commande R (register) suivie de l'adresse du point d'interruption. Si vous n'arretez pas le programme a l'un des points d'interruption, les codes d'interruption ne sont pas remplaces par les instructions d'origine.

Limitations pour la definition des points d'interruption

Vous pouvez placer des points d'interruption uniquement aux adresses contenant le premier octet d'un code operatoire 8086. Si vous definissez plus de dix points d'interruption, Debug affiche le message d'erreur suivant: Erreur bp

Conditions requises pour le pointeur de la pile de l'utilisateur

Le pointeur de la pile de l'utilisateur doit etre valide et contenir au moins 6 octets pour la commande G. Cette commande utilise une instruction IRET pour provoquer un branchement dans le programme en cours de mise au point. Debug positionne le pointeur de la pile de l'utilisateur et place les indicateurs, le registre de segment de code et le pointeur d'instruction dans la pile. Une adresse de pile non valide ou trop petite peut provoquer un arret du systeme d'exploitation. Debug place le code d'interruption (0CCh) dans la ou les adresses de point d'interruption indiquees.

Relance d'un programme

N'essayez pas de relancer un programme lorsque le DOS affiche le message: Fin normale du programme. Vous devez recharger le programme avec les commandes N (name) et L (load) pour l'executer correctement.

Exemple

Si vous tapez: gcs:7550 Le DOS execute le programme en memoire jusqu'a l'adresse 7550 du segment CS. Debug affiche le contenu des registres et l'etat des indicateurs, apres quoi la commande G est terminee.

La commande suivante definit deux points d'interruption: gcs:7550, cs:8000 Si vous tapez a nouveau la commande G apres que Debug a rencontre un point d'interruption, l'execution demarre a l'instruction suivant le point d'interruption au lieu de demarrer a son adresse de debut habituelle.

H (Hex) Retour en haut

Effectue un calcul en notation hexadecimale sur les deux parametres specifies.

Syntaxe

H valeur1 valeur2

Parametres

valeur1

Ce parametre represente un nombre hexadecimal compris entre 0 et FFFFh.

valeur2

Ce parametre represente un second nombre hexadecimal compris entre 0 et FFFFh.

Remarques

Debug additionne les deux parametres, puis soustrait le second du premier et affiche le resultat du calcul sur une ligne: leur somme en premier, puis leur difference.

Exemple

Si vous tapez: h19f 10a Debug effectue les calculs et affiche le resultat suivant: 02A9 0095

I (Input) Retour en haut

Lit et affiche la valeur d'un octet venant du port specifie.

Syntaxe

I port

Parametre

port

Specifie l'adresse du port d'entree (toujours en hexadecimale). L'adresse peut etre codee sur 16 bits.

Exemple

Supposons que vous tapiez: i2f8

Supposons aussi que la valeur d'octets au port specifie soit 42h. Debug lit l'octet et affiche sa valeur sous la forme: 42

Voir egalement la commande O (ouput) qui envoie une valeur donnee a un port de sortie donnee.

L (Load) Retour en haut

Charge un fichier ou le contenu de secteurs de disque specifiques en memoire.

Pour charger le contenu du nombre d'octets precises dans les registres BX:CX d'un fichier sur disque, utilisez la syntaxe:

Syntaxe

L [adresse]

Pour contourner le systeme de fichiers du DOS et charger directement des secteurs specifiques, utilisez la syntaxe:

L adresse lecteur debut nombre

Parametres

adresse

Specifie l'adresse a laquelle vous voulez charger le fichier ou le contenu du secteur. Si vous ne specifiez pas d'adresse, Debug utilise l'adresse contenue dans le registre CS.

Lecteur

Specifie le lecteur qui contient la disquette sur laquelle les secteurs specifiques doivent etre lus. Cette valeur est numerique: 0 = A:, 1 = B:, 2 = C: et ainsi de suite. N'utilisez les parametres lecteur, nombre et debut que si vous voulez charger le contenu de secteurs specifiques plutot que le fichier specifie dans la ligne de commande de DEBUG ou dans la commande N (name) la plus recente.

debut

Specifie (en notation hexadecimale) le numero du premier secteur a charger.

nombre

Specifie (en notation hexadecimale) le nombre de secteurs consecutifs a charger.

Remarques

Utilisation de la commande L sans parametre

Si vous utilisez la commande L sans parametre, le fichier indique apres la commande DEBUG est charge en memoire a partir de l'adresse CS:100. Debug place dans les registres BX and CX le nombre d'octets charges. Si vous ne specifiez pas de fichier dans la ligne de commande DEBUG, le fichier charge est celui qu'indique la commande N (name) la plus recente.

Utilisation de la commande L avec le parametre adresse

Si vous utilisez la commande L avec le parametre adresse, Debug commence le chargement du fichier ou du contenu des secteurs specifies, a l'adresse indiquee.

Utilisation de la commande L avec tous les parametres

Si vous utilisez la commande L avec tous les parametres, Debug charge des secteurs specifiques au lieu d'un fichier.

Chargement du contenu de secteurs specifiques

Chaque secteur de la plage que vous specifiez est lu sur le lecteur. Debug commence le chargement a partir de debut et continue jusqu'a ce que le nombre de secteurs specifies par nombre soit charge.

Chargement d'un fichier .EXE

Debug ignore le parametre adresse pour les fichiers .EXE. Si vous specifiez un fichier .EXE, Debug le place a l'adresse de chargement precisee dans l'en-tete du fichier .EXE. L'en-tete est elimine du fichier .EXE avant le chargement en memoire. De ce fait, la taille d'un fichier .EXE sur disque differe de sa taille en memoire. Si vous voulez examiner un fichier .EXE complet, renommez-le en utilisant une autre extension.

Ouverture d'un fichier hex

Un fichier hexadecimal utilise le format hexadecimal d'Intel, decrit dans l'Encyclopedie de MS DOS (MS DOS Encyclopedia). Debug presume que les fichiers suivis de l'extension .HEX sont en format hexadecimal. Vous pouvez taper la commande L sans parametre pour charger un fichier hexadecimal commencant a l'adresse specifiee dans le fichier .HEX. Si la commande L tapee contient le parametre adresse, Debug ajoute l'adresse specifiee a celle trouvee dans le fichier hexadecimal pour determiner l'adresse de debut.

Exemple

Supposons que vous lancez Debug et tapez: nfile.com

Il suffit, pour charger FILE.COM., d'executer la commande L. Debug charge le fichier FILE.COM. et affiche l'invite de Debug. Supposons que vous vouliez ensuite charger en memoire, a partir de l'adresse 04BA:0100, le contenu de 109 (6Dh) secteurs du lecteur C: et en commencant par le secteur logique 15 (0Fh). Pour cela, tapez: l04ba:100 2 0f 6d

M (Move) Retour en haut

Deplace le contenu d'un bloc de memoire vers un autre bloc de memoire.

Syntaxe

M plage adresse

Parametres

plage

Specifie les adresses de debut et de fin, ou l'adresse de debut et la longueur de la zone memoire dont vous voulez copier le contenu.

adresse

Specifie l'adresse de debut de l'emplacement ou vous voulez copier le contenu de la plage.

Remarques

Effets de la copie sur les donnees existantes

Si aucune nouvelle donnee n'est ecrite dans les adresses du bloc en cours de copie, les donnees d'origine restent intactes. Toutefois, si le bloc de destination contient deja des donnees, comme dans le cas d'une copie avec recouvrement, ces donnees sont effacees. (Une copie avec recouvrement est une copie ou une partie du bloc destinataire recouvre une partie du bloc source).

Copie avec recouvrement

La commande M effectue des operations de copie avec recouvrement sans perdre les donnees aux adresses destinataires. Le contenu des adresses qui seront recouvertes est copie en premier. En consequence, si les donnees doivent etre copiees des adresses hautes vers des adresses plus basses, la copie commence a partir du bloc d'adresses source le plus bas et continue vers les adresses les plus hautes. En revanche, si les donnees doivent etre copiees des adresses basses vers des adresses plus hautes, la copie commence au bloc d'adresses le plus haut et continue vers les adresses les plus basses.

Exemple

Supposons que vous tapez: mcs:100 110 cs:500 Debug copie d'abord le contenu de l'adresse CS:110 en CS:510, puis celui de CS:10F en CS:50F, et ainsi de suite jusqu'a ce que le contenu de CS:100 soit copie en CS:500. Pour voir les resultats, vous pouvez utiliser la commande D (dump), en precisant l'adresse destinataire utilisee dans la commande M.

N (Name) Retour en haut

Specifie un nom de fichier pour une commande L (load) ou W (write), ou specifie des parametres pour le fichier executable en cours de mise au point.

Syntaxe

N [lecteur:][chemin]fichier

Pour specifier les parametres du fichier executable que vous mettez au point, utilisez la syntaxe:

N param

Pour effacer les specifications en cours, utilisez la syntaxe:

N

Parametres

[lecteur:][chemin]fichier

Specifie l'emplacement et le nom du fichier executable que vous voulez mettre au point.

param

Specifie les parametres et commutateurs du fichier executable a mettre au point.

Remarques

Les deux fonctions de la commande N

La commande N sert tout d'abord a specifier un fichier qui pourra etre utilise ulterieurement par une commande L (load) ou W (write). Si vous lancez Debug sans indiquer le fichier a mettre au point, vous devez taper la commande Nfichier avant d'utiliser la commande L (load) pour charger le fichier. Le nom du fichier est formate correctement pour un bloc de controle de fichier a l'adresse CS:5C. La deuxieme fonction de la commande N est de specifier des parametres et des commutateurs dans la ligne de commande, pour le fichier en cours de mise au point.

Zones de memoire

Quatre zones de la memoire peuvent etre attribuees par la commande N:

Emplacement de memoire | Contenu
CS:5C | Bloc de controle de fichier (FCB) pour fichier1
CS:6C | Bloc de controle de fichier (FCB) pour fichier2
CS:80 | Longueur de la ligne de commande N (en caracteres)
CS:81 | Debut de la ligne de commande N

Le premier nom de fichier specifie comme parametre de la commande N est place dans un bloc de controle de fichier (FCB; File Control Block) a l'adresse CS:5C. Si un deuxieme fichier est specifie, son nom est place dans un FCB a l'adresse CS:6C. Le nombre de caracteres tapes dans la ligne de commande N (le caractere N excepte) est stocke en CS:80. Les caracteres effectifs (N excepte) sont stockes a partir de l'adresse CS:81. Ils peuvent representer tout commutateur ou separateur accepte au niveau de l'invite du DOS.

Exemple

Supposons que vous avez lance Debug et charge le programme PROG.COM pour une mise au point, et que vous decidez par la suite de specifier deux parametres pour PROG.COM, puis d'executer le programme. Voici la sequence de commandes a employer:

debug prog.com

nparam1 param2

g

Dans ce cas, la commande G (go) execute le programme comme si vous aviez tape la sequence suivante a la suite de l'invite du DOS:

prog param1 param2

Le test et la mise au point d'un programme se font donc selon l'environnement normal d'execution du programme PROG.COM.

Dans la sequence de commandes qui suit, la premiere commande N specifie NOMFI1.EXE comme fichier a utiliser dans la commande L (load) suivante qui chargera ce fichier en memoire. La seconde commande N indique les parametres qui seront utilises lors du lancement du programme NOMFI1.EXE. Enfin, la commande G (go) execute NOMFI1.EXE, comme si vous aviez tape la sequence NOMFI1 NOMFI2.DAT NOMFI3.DAT au niveau de l'invite du DOS.

nnomfi1.exe

l

nnomfi2.dat nomfi3.dat

g

Notez que vous n'utilisez pas la commande L (load) apres la deuxieme forme de la commande N. Notez egalement que si vous utilisez maintenant la commande W (write), le DOS sauvegarde NOMFI1.EXE (en cours de mise au point) sous le nom NOMFI2.DAT. Pour eviter ce resultat, vous devez toujours executer la premiere forme de la commande N immediatement avant une commande L (load) ou W (write).

O (Output) Retour en haut

Envoie la valeur d'un octet vers un port de sortie.

Syntaxe

O port valeur

Parametres

port

Specifie le port de sortie par le biais d'une adresse (toujours en hexadecimale). L'adresse du port peut etre specifiee sur 16 bits.

valeur

Specifie la valeur de l'octet a envoyer au port.

Exemple

Pour envoyer l'octet de valeur 4Fh (valeur en decimale = 79) sur le port de sortie a l'adresse 2F8h (valeur en decimale = 760), tapez: o2f8 4f

Voir egalement la commande I (input) qui lit et affiche la valeur d'un port d'entree donnee.

P (Proceed) Retour en haut

Execute une boucle, une instruction sur chaine repetee, une interruption logicielle ou une sous routine, ou trace toute autre instruction.

Syntaxe

P [=adresse] [nombre]

Parametres

=adresse

Specifie l'adresse de la premiere instruction a executer. Si vous ne specifiez pas d'adresse, la valeur par defaut est l'adresse en cours specifiee dans les registres CS:IP.

nombre

Specifie le nombre d'instructions a executer avant de rendre le controle a Debug. La valeur par defaut est 1.

Remarques

Transfert du controle au programme en cours de mise au point

Lorsque la commande P transfere le controle de Debug au programme en cours de mise au point, ce programme s'execute sans interruption jusqu'a ce que la boucle, l'instruction sur chaine repetee, l'interruption logicielle ou une sous routine a l'adresse specifiee soit terminee, ou jusqu'a ce que le nombre specifie d'instructions machine ait ete execute. Le controle retourne alors a Debug.

Limitations du parametre adresse

Si le parametre adresse ne specifie pas de segment, Debug utilise le registre CS du programme en cours de mise au point. Si vous omettez le parametre, l'execution du programme commence a l'adresse specifiee par ses propres registres CS:IP. Vous devez faire preceder le parametre adresse d'un signe egal (=) pour le differencier du parametre nombre. Si l'instruction presente a l'adresse specifiee n'est pas une boucle, une instruction sur chaine repetee, une interruption logicielle ou une sous-routine, la commande P fonctionne comme la commande T (trace).

Messages affiches par la commande P

Une fois qu'une instruction a ete executee par P, Debug affiche le contenu des registres du programme, l'etat de ses indicateurs et la forme decodee de l'instruction suivante a executer.

ATTENTION: Vous ne pouvez pas utiliser la commande P pour suivre l'execution dans la memoire morte (ROM). Utilisez pour ce faire la commande T (trace).

Exemple

Supposons que le programme en cours de mise au point contienne une instruction CALL a l'adresse CS:143F. Pour executer la sous-routine qui est la destination de CALL et retourner ensuite le controle a Debug, tapez: p=143f Debug affiche le resultat sous la forme suivante:

AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000

DS=2246 ES=2246 SS=2246 CS=2246 IP=1443 NV UP EI PL NZ AC PO NC

2246:1442 7505 JNZ 144A

Q (Quit) Retour en haut

Quitte Debug sans enregistrer le fichier en cours de mise au point.

Lorsque vous avez tape Q, vous revenez au niveau de l'invite du DOS.

Syntaxe

Q (sans parametres)

R (Register) Retour en haut

Affiche ou modifie le contenu d'un ou de plusieurs registres du processeur.

Syntaxe

R [registre]

Pour afficher le contenu de tous les registres et de tous les indicateurs dans la zone de stockage de registres, utilisez la syntaxe:

R

Parametre

registre

Specifie le nom du registre dont vous voulez afficher le contenu.

Remarques

Utilisation de la commande R

Si vous specifiez un nom de registre, le DOS affiche la valeur 16 bits de ce registre en notation hexadecimale et affiche un signe deux points (:) comme invite. Si vous voulez modifier le contenu du registre, tapez une nouvelle valeur et appuyer sur ENTREE; sinon, appuyez simplement sur ENTREE pour retourner a l'invite de Debug.

Noms de registres valides

Liste des noms de registres acceptes: AX, BX, CX, DX, SP, BP, SI, DI, DS, ES, SS, CS, IP, PC et F. Les valeurs IP et PC font toutes deux reference au pointeur d'instruction.

Si vous specifiez un nom de registre autre que ceux figurant dans la liste ci dessus, le DOS affiche le message d'erreur:

Erreur br

Utilisation du caractere F comme registre

Si vous tapez le caractere F au lieu d'un nom de registre, Debug affiche l'etat en cours de chaque indicateur au moyen d'un code alphabetique de deux caracteres et affiche l'invite de Debug. Pour modifier l'etat d'un indicateur, tapez le code de deux lettres qui se trouve en regard du nom dans le tableau:

Nom de l'indicateur | Active | Desactive
Depassement de capacite | OV | NV
Direction | DN (decrement) | UP (increment)
Interruption | EI (active) | DI (inactive)
Signe | ZR | NZ
Report auxiliaire | AC | NA
Parite | PE (paire) | PO (impaire)
Report | CY | NC

Vous pouvez taper de nouvelles valeurs pour les indicateurs dans n'importe quel ordre. Il n'est pas necessaire de separer ces valeurs par des espaces. Pour arreter la commande R, il suffit d'appuyer sur la touche ENTREE. Tout indicateur pour lequel vous n'avez pas specifie de nouvelle valeur reste inchange.

Messages affiches avec la commande R

Si vous specifiez plusieurs valeurs pour un indicateur, Debug renvoie le message: Erreur df

Si vous tapez un code d'indicateur different de ceux indiques dans le tableau precedent, Debug renvoie le message: Erreur bf

Dans les deux cas, Debug ignore toutes les valeurs specifiees a la suite de l'entree non valide.

Valeurs par defaut pour Debug

Lors du demarrage de Debug, les registres de segment pointent vers la plus petite adresse de memoire libre. Le pointeur d'instruction a la valeur 0100h, tous les indicateurs sont initialises et les autres registres prennent la valeur zero, a l'exception de SP qui est defini a FFEEh.

Exemple

Pour visualiser le contenu de tous les registres, l'etat de tous les indicateurs et la forme decodee de l'instruction qui se trouve a l'adresse courante, tapez: r Si l'adresse courante est CS:11A par Exemple:, l'affichage se presente comme suit:

AX=0E00 BX=00FF CX=0007 DX=01FF SP=039D BP=0000 SI=005C DI=0000

DS=04BA ES=04BA SS=04BA CS=O4BA IP=011A NV UP DI NG NZ AC PE NC

04BA:011A CD21 INT 21

Pour ne visualiser que l'etat des indicateurs, tapez: rf

Debug affiche les informations dans le format suivant: NV UP DI NG NZ AC PE NC - _

Vous pouvez maintenant taper une ou plusieurs valeurs d'indicateur valides, dans un ordre quelconque, avec ou sans espace, comme dans la commande: nv up di ng nz ac pe nc - pleicy Debug arrete la commande R et affiche l'invite de Debug. Les commandes R et RF permettent d'afficher les modifications: NV UP EI PL NZ AC PE CY - _

Appuyez sur ENTREE pour retourner a l'invite de Debug.

S (Search) Retour en haut

Cherche une liste d'octets dans la plage d'adresses specifiee.

Syntaxe

S plage liste

Parametres

plage

Specifie les adresses de debut et de fin de la plage a examiner.

liste

Specifie la ou les valeur(s) d'octet ou de chaine a rechercher. Separez les octets par des espaces ou des virgules. Placez les valeurs de chaine entre guillemets.

Remarques

Si le parametre liste contient plus d'une valeur d'octet, Debug n'affiche que la premiere adresse ou la valeur d'octet est presente. Si ce parametre contient une seule valeur d'octet, Debug affiche toutes les adresses de la plage specifiee ou la valeur est presente.

Exemple

Supposons que vous vouliez trouver toutes les adresses de la plage CS:100 a CS:110 contenant la valeur 41. Pour cela, tapez: scs:100 110 41 Debug affiche les resultats dans le format suivant:

04BA:0104

04BA:010D

-

La commande suivante cherche la chaine "Ph" dans la plage CS:100 a CS:1A0: scs:100 1a0 "Ph"

T (Trace) Retour en haut

Execute une instruction et affiche le contenu de tous les registres, l'etat de tous les indicateurs et la forme decodee de l'instruction executee.

Syntaxe

T [=adresse] [nombre]

Parametres

=adresse

Specifie l'adresse a laquelle Debug doit commencer les instructions de tracage. Si vous omettez le parametre adresse, le tracage commence a l'adresse specifiee par les registres CS:IP du programme.

nombre

Specifie le nombre d'instructions a tracer. Cette valeur doit etre un nombre hexadecimal. La valeur par defaut est 1.

Remarques

Instructions de tracage en memoire morte (ROM)

La commande T utilise le mode trace materiel des microprocesseurs 8086 ou 8088. Par consequent, vous pouvez egalement tracer des instructions qui se trouvent en memoire morte (ROM).

Utilisation du parametre adresse

Vous devez faire preceder le parametre adresse du signe egal (=) pour le differencier du parametre nombre.

Exemple

Pour executer une instruction (indiquee par CS:IP) et afficher le contenu des registres, l'etat des indicateurs et la forme decodee de l'instruction, tapez: t Si la position de l'instruction dans le programme est 04BA:011A, Debug affiche les informations suivantes:

AX=0E00 BX=00FF CX=0007 DX=01FF SP=039D BP=0000 SI=005C DI=0000

DS=04BA ES=04BA SS=04BA CS=O4BA IP=011A NV UP DI NG NZ AC PE NC

04BA:011A CD21 INT 21

U (Unassemble) Retour en haut

Desassemble des octets et affiche les instructions source correspondantes, ainsi que les adresses et les valeurs des octets. Le code desassemble ressemble au listage d'un fichier d'assemblage (sans les commentaires helas).

Syntaxe

U [plage]

Pour desassembler 20h octets (le nombre par defaut) en commencant a la premiere adresse qui suit celle affichee par la commande U precedente, utilisez la syntaxe:

u

Parametre

plage

Specifie les adresses de debut et de fin du code a desassembler, ou l'adresse de debut et la longueur du code.

Exemple

Pour desassembler 16 (10h) octets a partir de l'adresse 04BA:0100, tapez: u04ba:100l10 Debug affiche les resultats dans le format suivant:

04BA:0100 206472 AND [SI+72],AH

04BA:0103 69 DB 69

04BA:0104 7665 JBE 016B

04BA:0106 207370 AND [BP+DI+70],DH

04BA:0109 65 DB 65

04BA:010A 63 DB 63

04BA:010B 69 DB 69

04BA:010C 66 DB 66

04BA:010D 69 DB 69

04BA:010E 63 DB 63

04BA:010F 61 DB 61

Pour n'afficher que les informations concernant les adresses specifiques 04BA:0100 a 04BA:0108, tapez: u04ba:0100 0108 Debug affiche:

04BA:0100 206472 AND [SI+72],AH

04BA:0103 69 DB 69

04BA:0104 7665 JBE 016B

04BA:0106 207370 AND [BP+DI+70],DH

W (Write) Retour en haut

Ecrit sur disque un fichier ou des secteurs specifiques.

Il faut avoir specifie le nom du fichier disque lors du lancement de Debug ou avec la commande N (name) la plus recente. Chacune de ces methodes permet de sauvegarder correctement le nom de fichier pour un bloc de controle de fichier a partir de l'adresse CS:5C. Pour ecrire dans un fichier disque le contenu du nombre d'octets specifie dans les registres BX:CX, utilisez la syntaxe:

Syntaxe

W [adresse]

Pour contourner le systeme de fichiers du DOS et ecrire directement des secteurs specifiques, utilisez la syntaxe:

W adresse lecteur debut nombre

ATTENTION: L'ecriture de secteurs specifiques est tres risquee, car ce processus court circuite le gestionnaire de fichiers du DOS. La structure des fichiers sur disque peut facilement etre endommagee si vous utilisez des valeurs incorrectes.

Parametres

adresse

Specifie l'adresse du debut du fichier ou de la partie de fichier que vous voulez ecrire sur disque. Si vous ne precisez pas d'adresse, Debug commence a partir de CS:100.

lecteur

Specifie le lecteur contenant le disque cible. Cette valeur est une valeur numerique: 0 = A:, 1 = B:, 2 = C: et ainsi de suite.

debut

Specifie, en format hexadecimal, le numero du premier secteur ou vous voulez que l'ecriture du fichier commence.

nombre

Specifie le nombre total de secteurs dans lesquels ecrire.

Remarques

Reinitialisation de BX:CX avant d'utiliser la commande W sans parametre

Si vous avez utilise une commande G (go), T (trace), P (proceed), ou R (register), vous devez reinitialiser les registres BX:CX avant d'utiliser la commande W sans parametre.

Ecriture sur disque d'un fichier modifie

Si vous modifiez un fichier sans changer son nom, sa longueur ou son adresse de debut, Debug peut encore ecrire correctement le fichier a son emplacement d'origine sur le disque.

Limitation de la commande W

Cette commande ne vous permet pas d'ecrire un fichier .EXE ou .HEX mais uniquement un fichier .COM.

Exemple

Supposons que vous vouliez ecrire le contenu de la memoire depuis l'adresse CS:100 sur la disquette presente dans le lecteur A:. Vous voulez ecrire 2Bh secteurs en commencant au secteur logique 37h de la disquette. Pour cela, tapez: wcs:100 1 37 2b Quand l'ecriture est terminee, Debug affiche l'invite.

XA (Allocate Expanded Memory) Retour en haut

Alloue le nombre specifie de pages de memoire paginee (EMS). Pour utiliser la memoire paginee, vous devez avoir installe un gestionnaire de memoire conforme a la version 4.0 de la norme de gestion de la memoire paginee LIM EMS (Lotus/Intel/Microsoft Expanded Memory Specification).

Syntaxe

XA [nombre]

Parametre

nombre

Specifie le nombre de pages de 16 Ko a allouer a la memoire paginee.

Remarques

Debug affiche un message pour indiquer le numero hexadecimal du descripteur cree, si le nombre de pages specifie est disponible, et un message d'erreur dans le cas contraire.

Exemple

Pour allouer 8 pages de memoire paginee, tapez: xa8

Si la commande est executee avec succes, Debug affiche un message similaire au suivant: Descripteur cree=0003

XD (Deallocate Expanded Memory) Retour en haut

Desactive l'allocation d'un descripteur a la memoire paginee.

Pour utiliser la memoire paginee, vous devez avoir installe un gestionnaire de memoire conforme a la version 4.0 de la norme de gestion de la memoire paginee LIM EMS (Lotus/Intel/Microsoft Expanded Memory Specification).

Syntaxe

XD [descripteur]

Parametre

Descripteur

Precise le descripteur dont vous voulez desactiver l'allocation.

Exemple

Pour desactiver l'allocation du descripteur 0003, tapez: xd 0003 Si la commande est executee avec succes, Debug affiche un message similaire au suivant: Descripteur 0003 desaffecte

XM (Map Expanded Memory Pages) Retour en haut

Affecte une page logique de memoire paginee, appartenant au descripteur specifie, a une page physique de memoire paginee. Pour utiliser la memoire paginee, vous devez avoir installe un gestionnaire de memoire conforme a la version 4.0 de la norme de gestion de la memoire paginee LIM EMS (Lotus/Intel/Microsoft Expanded Memory Specification).

Syntaxe

XM [lpage] [ppage] [descripteur]

Parametres

lpage

Specifie le numero de la page logique de memoire paginee que vous voulez allouer a la page physique ppage.

ppage

Specifie le numero de la page physique a laquelle lpage doit etre affecte.

descripteur

Precise le descripteur.

Exemple

Pour afficher la page logique 5 du descripteur 0003 de la memoire paginee a la page physique 2, tapez: xm 5 2 0003

Si l'operation est executee avec succes, Debug affiche le message suivant: Page logique 05 affectee a la page physique 02

XS (Display Expanded Memory Status) Retour en haut

Affiche des informations sur l'etat de la memoire paginee.

Pour utiliser la memoire paginee, vous devez avoir installe un gestionnaire de memoire conforme a la version 4.0 de la norme de gestion de la memoire paginee LIM EMS (Lotus/Intel/Microsoft Expanded Memory Specification).

Syntaxe

XS (sans parametre)

Remarques

Les informations affichees sont au format:

XX pages allouees au descripteur XX

Page physique XX = Segment de cadre XX

XX pages EMS allouees sur un total de XX

XX descripteurs EMS alloues sur un total de XX

Exemple

Pour afficher des informations sur la memoire paginee, tapez: xs. Debug affiche des informations dans le format:

0000 pages allouees au descripteur 0000

0002 pages allouees au descripteur 0001

Page physique 00 = Segment de cadre C000

Page physique 01 = Segment de cadre C400

Page physique 02 = Segment de cadre C800

Page physique 03 = Segment de cadre CC00

2 pages EMS allouees sur un total de 80

2 descripteurs EMS alloues sur un total de FF

Retour en haut

Retour au sommaire de la rubrique

Retour au debut


La commande DEBUG de DOS

Objectifs du laboratoire

Se familiariser avec les concepts d'adresse, de registres et de pseudo-instructions de l'assembleur Intel à l'aide de la commande DEBUG.

La commande DEBUG de DOS permet de lancer le programme DEBUG.EXE qui met au point des fichiers exécutables. Notez bien que dans le DEBUG, toutes les valeurs entrées sont par défaut hexadécimales.

Note:

Vous trouverez dans ce document quatre (4) séries de questions auxquelles vous devrez répondre--ce que vous pourrez faire par équipe de deux (2) maximum. Vous devrez remettre vos résultats clairement identifiés au professeur à la fin du cours.

Vous devrez aussi montrer au professeur l'exécution du programme AFFICHE.COM pour avoir vos points.

Attention de ne pas prendre ce petit laboratoire à la légère, et de ne pas recopier à l'aveuglette le code que vous y trouverez. Comprendre ce que vous faites maintenant vous sauvera beaucoup de maux de tête dès la semaine prochaine.

Amusez vous-bien!

Étapes à réaliser

Étape 1: création d'un fichier qui affiche un message à l'écran

À l'invité du DOS, lancez le programme DEBUG.EXE

À l'invité du programme DEBUG, tapez A 100 pour commencer l'assemblage.

Votre écran ressemblera à celui-ci[1]:

C:\WINDOWS>debug

-A 100   ;on se place à l'adresse 100h dans un segment de mémoire,
         ;le programme doit toujours commencer à l'adresse 100h
XXXX:0100

Le XXXX représente l'adresse du segment de mémoire qu'on utilise et 0100 représente l'offset dans ce segment.

Note: Pour chaque ordinateur l'adresse du segment XXXX peut être différente. Prenez soin d'utiliser la bonne adresse pour vos fins, et ne recopiez pas le code de ce document à l'aveuglette!

Tapez le code suivant[2]:

MOV AH,09                        ; place 09h dans le registre AH
MOV DX,0109                      ; place l'adresse 0109h dans le registre DX. Le registre DX
                                 ; doit contenir l'adresse de la chaîne de caractères à afficher
                                 ; à l'écran
INT 21                           ; appel d'interruption 21h de DOS qui, accompagnée de
                                 ; la fonction 9h permettant d'afficher une chaîne
                                 ; de caractères à l'écran
INT 20                           ; l'interruption 20h de DOS permet la terminaison
                                 ; du programme
DB 'Bonjour tout le monde',D,24  ; la chaîne à afficher, le caractère D (13 en hexadécimale
                                 ; qui représente le Carriage Return) et le caractère 24h
                                 ; (qui correspond au code ASCII du ;caractère '$')
                                 ; pour terminer la chaîne de caractères à afficher

À la fin, appuyez sur Enter pour revenir à l'invité du programme DEBUG. Nous sommes prêts à exécuter notre programme. Tapez la lettre G pour exécuter...

Votre écran ressemblera au suivant:

C:\WINDOWS>debug

-A 100
1773:0100 MOV AH,9
1773:0102 MOV DX,0109
1773:0105 INT 21
1773:0107 INT 20
1773:0109 DB 'Bonjour tout le monde',D,24
1773:0120
-G

Bonjour tout le monde

Le programme s'est terminé normalement

Pour enregistrer le fichier, à l'invité de la commande DEBUG tapez sur N et donnez le nom de fichier (bonjour.com):

-N bonjour.com    ;le fichier assemblé sera enregistré sous le nom 'bonjour.com'
-RCX              ;entre le nombre d'octets à enregistrer dans le registre CX
CX 0000           ;affiche que le registre CX contient 0 (pour l'instant)
:120              ;le nombre d'octets à entrer dans le CX, cette valeur correspond
                  ;à la dernière adresse affichée lors de l'assemblage
-W                ;pour écrire le code fraîchement assemblé dans le fichier 'Bonjour.com'
-Q                ;pour quitter le programme DEBUG

Votre écran ressemblera à celui-ci:

-N bonjour.com
-RCX
CX 0000
:120
-W

Écriture de 00120 octets

-Q

Vous pouvez maintenant exécuter votre petit programme en l'appelant à l'invité du DOS comme ceci:

C:\WINDOWS>BONJOUR

Étape 2: Trace d'exécution d'un code avec la vérification du contenu de registres

À l'invité du DOS, lancez le programme DEBUG.EXE

À l'invité du programme DEBUG tapez A 100 pour commencer l'assemblage.

MOV AX,FFFF       ;place la valeur FFFF dans le registre AX
MOV BX,FFFF       ;place la valeur FFFF dans le registre BX
ADD AX,BX         ;additionne les valeurs AX et BX et place le résultat dans AX
MOV AX,0001       ;place la valeur 0001 dans le registre AX
DEC AX            ;décrément la valeur dans le registre AX
INC AX            ;incrément la valeur dans le registre AX
SUB AX,0002       ;soustrait la valeur 2h de registre AX et place le résultat dans AX
MOV AH,70         ;place la valeur 70h dans le registre AH
MOV BH,50         ;place la valeur 50h dans le registre BH
ADD AH,BH         ;additionne la valeur dans AH à la valeur dans BH et place le résultat dans AH

Votre écran ressemblera à celui-ci:

C:\WINDOWS>DEBUG

-A 100
1773:0100 MOV AX,FFFF
1773:0103 MOV BX,FFFF
1773:0106 ADD AX,BX
1773:0108 MOV AX,0001
1773:010B DEC AX
1773:010C INC AX
1773:010D SUB AX,0002
1773:0110 MOV AH,70
1773:0112 MOV BH,50
1773:0114 ADD AH,BH
1773:0116

Pour tracer l'exécution de votre petit code, tapez la lettre T et observez le contenu des registres (la première fois, tapez la lettre R pour afficher le contenu de registres avant l'exécution de la première instruction):

-R
AX=0000  BX=0000  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=0103   NV UP EI PL NZ NA PO NC
1773:0103 BBFFFF        MOV     AX,FFFF

-T
AX=FFFF  BX=0000  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=0103   NV UP EI PL NZ NA PO NC
1773:0103 BBFFFF        MOV     BX,FFFF

Après la première instruction MOV AX,FFFF, le contenu de registre AX est FFFF, les autres registres "tout usage" sont à 0.

Remarquez que les registres de segment (DS, ES, CS et SS) contiennent tous la même valeur: 1773 dans ce cas (rappel: cette valeur peut être différente pour chaque ordinateur). Cela signifie que tout le programme au complet n'occupe qu'un segment, ce qui est toujours le cas pour des programmes qui ont l'extension .COM.

Le pointeur d'instructions IP pointe à l'adresse de l'instruction suivante à exécuter: 103.

Observez le contenu des indicateurs...

NV | indique qu'il n'y a pas de débordement (No Overflow, OF = 0)
PL | indique que la dernière opération résulte en valeur est positive (SF = 0)
NZ | indique que le résultat de la dernière opération est différent de zéro (No Zero, ZF = 0)
NC | indique qu'il n'y a pas de retenu (No Carry, CF = 0)

Continuons de suivre la trace d'exécution de notre code:

-T
AX=FFFF  BX=FFFF  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=0106   NV UP EI PL NZ NA PO NC
1773:0106 01D8          ADD     AX,BX

-T
AX=FFFE  BX=FFFF  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=0108   NV UP EI NG NZ AC PO CY
1773:0108 B80100        MOV     AX,0001

Série de questions #1

  • Quelle est la valeur du registre accumulateur?
  • Quelle est la valeur du pointeur d'instruction?
  • Quel changement observez-vous dans les indicateurs?
  • Pouvez-vous donner une explication?

    • Continuons de suivre la trace d'exécution de notre code:

-T
AX=0001  BX=FFFF  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=010B   NV UP EI NG NZ AC PO CY
1773:010B 48            DEC     AX

-T
AX=0000  BX=FFFF  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=010C   NV UP EI PL ZR NA PE CY
1773:010C 40            INC     AX

    Série de questions #2

  • Quelle est la valeur du registre accumulateur?
  • Quelle est la valeur du pointeur d'instruction?
  • Quel changement observez-vous dans les indicateurs?
  • Pouvez-vous donner une explication?

    • Continuons de suivre la trace d'exécution de notre code:

-T
AX=0001  BX=FFFF  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=010D   NV UP EI PL NZ NA PO CY
1773:010D 2D0200        SUB     AX,0002

-T
AX=FFFF  BX=FFFF  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=0110   NV UP EI NG NZ AC PE CY
1773:0110 B470          MOV     AH,70

    Série de questions #3

  • Quelle est la valeur du registre accumulateur?
  • Quelle est la valeur du pointeur d'instruction?
  • Quel changement observez-vous dans les indicateurs?
  • Pouvez-vous donner une explication?

    • Continuons de suivre la trace d'exécution de notre code:

-T
AX=70FF  BX=FFFF  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=0112   NV UP EI NG NZ AC PE CY
1773:0112 B750          MOV     BH,50

-T
AX=70FF  BX=50FF  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=0114   NV UP EI NG NZ AC PE CY
1773:0114 00FC          ADD     AH,BH

-T
AX=C0FF  BX=50FF  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1773  ES=1773  SS=1773  CS=1773  IP=0116   OV UP EI NG NZ NA PE NC
1773:0116 6E            DB      6E

    Série de questions #4

  • Quelle est la valeur du registre accumulateur?
  • Quelle est la valeur du pointeur d'instruction?
  • Quel changement observez-vous dans les indicateurs?
  • Pouvez-vous donner une explication?

    • Étape 3: ouverture d'une deuxième page d'écran avec l'affichage d'un caractère avec son attribut

    À l'invité du DOS, lancez le programme DEBUG.EXE suivi du nom du fichier que vous allez créer (affiche.com)

    Tapez A 100 pour commencer l'assemblage.

C:\WINDOWS>DEBUG AFFICHE.COM

Fichier introuvable

-A 100

    Les instructions suivantes permettent d'ouvrir la deuxième page d'écran. En mode VGA on peut utiliser jusqu'à 8 pages d'écran numérotées de 0 à 7) et la page 0 est celle affichée par défaut.

MOV     AH,05     ; place la fonction 5 dans le registre AH
MOV     AL,01     ; place le numéro de la page à afficher dans le registre AL
INT     10        ; appelle l'interruption 10h de BIOS qui, combinée à la
                  ; fonction 5, permettant d'afficher la page demandée

    Les instructions suivantes permettent de positionner le curseur au milieu de l'écran.

MOV     AH,02  ; place la fonction 2 dans le registre AH
MOV     BH,01  ; place la valeur 1 dans le registre BH (numéro de page à afficher)
MOV     DH,0C  ; place la valeur 12 dans le registre DH (12ième ligne)
MOV     DL,27  ; place la valeur 39 dans le registre DL (39ième colonne)
INT     10     ; appelle l'interruption 10h de BIOS qui, combinée à la
               ; fonction 2, permet de positionner le curseur à la position demandée

    Les instructions suivantes permettent d'imprimer le caractère A en Cyan 16 fois à l'écran

MOV     AH,09     ; place la fonction 9 dans le registre AH
MOV     AL,41     ; place le caractère A dans le registre AH (41h est le code ASCII de
                  ; la lettre A)
MOV     BH,01     ; place la valeur 1 dans le registre BH (affiche à la page 1)
MOV     BL,03     ; place la valeur 3 dans le registre BL (attribut de caractère Cyan[3])
MOV     CX,0010   ; place la valeur 16 dans le registre CX (le nombre de fois que
                  ; le caractère sera affiché
INT     10        ; appelle l'interruption 10h de BIOS qui, combinée à la
                  ; fonction 9, permet d'afficher  le caractère avec son attribut

    Les instructions suivantes permettent d'attendre qu'on appuie sur une touche (pour continuer)

MOV     AH,06   ;place la fonction 6 dans le registre AH
MOV     AL,00   ;place la valeur 0 dans le registre AL
MOV     DL,FF   ;place la valeur FF dans le registre DL
INT     21      ;appelle l'interruption 21h de DOS qui, combinée à la
                ;fonction 6, permet de lire le caractère au clavier sans écho.
                ;La valeur du caractère lu est stockée dans AL.
CMP     AL,00   ;Tant que le contenu de AL est à 0,
JZ      011D    ;on effectue la lecture (011D est l'adresse de la première instruction de
                ;la boucle qui correspond à MOV AH,06

    Les instructions suivantes permettent de revenir à l'écran par défaut (0)

MOV     AH,05
MOV     AL,00     ;écran numéro 0
INT     10
INT     20        ;fin de programme

    Maintenant:

  • Enregistrez le fichier.
  • Exécutez le fichier en l'appelant à l'invité du DOS

    • ANNEXE

    Les codes d'attributs et les couleurs de caractères correspondantes:

    01h | bleu | 02h | vert | 03h | cyan
04h | rouge | 05h | magenta | 06h | brun
07h | blanc | 08h | gris | 09h | bleu clair
0ah | vert clair | 0bh | cyan clair | 0ch | rouge clair
0dh | magenta clair | 0eh | jaune | 0fh | blanc intense

    [1] Attention: il est possible que DEBUG.EXE soit dans le répertoire C:\WINDOWS\COMMAND\ plutôt que dans C:\WINDOWS\.

    [2] Vous n'avez pas à recopier les commentaires; ils sont là seulement dans le but de vous guider.

    [3] Pour connaître d'autres codes d'attribut, référez-vous à la fin de ce document.


Initiation à l’assembleur X86

TD N°3 : Initiation à l’assembleur X86

Utilisation de l’outil DEBUG

  1. L’objectif de cette séance de travaux dirigés n’est pas la programmation en langage assembleur, mais une familiarisation par la mise en pratique des notions qui ont a été vues en cours concernant la représentation des données et l’architecture de l’unité centrale d’un ordinateur. Nous nous bornerons à écrire et à exécuter quelques instructions en mode interactif sous le contrôle de l’outil DEBUG qui s’exécute sous Windows dans une fenêtre DOS .

  2. Vous avez à votre disposition :

    1. un rapide descriptif de l’outil DEBUG et de ses principales commandes; lisez l’introduction, et ce qui concerne les commandes :
      • R (register) qui donne le contenu des principaux registres du processeur,
      • D (Dump) qui donne le contenu d’un block d’adresses mémoire,
      • E (Enter) qui permet d’initialiser le contenu d’adresses mémoire,
      • A (Assemble) qui permet d’écrire des instruction en assembleur (avec une syntaxe parfois particulière à l’outil DEBUG),
      • U (Unassemble) qui permet de retrouver une instruction assembleur à partie d’une instruction en code machine (hexadécimal),
      • G (Go) qui permet de lancer l’exéution d’un programme,
      • T (Trace) qui permet d’exécuter les instruction pas à pas.

        2. une table décrivant les instructions X86 les plus courantes ; lisez plus particulièrement ce qui concerne les instructions que vous aurez à utiliser :
          MOV, ADD, SUB, MUL, DIV, NOT, NEG, JMP, INT

        3. une table donnant la correspondance entre les codes mnémoniques des instructions et leur valeur en langage machine (code hexadécimal).

        4. une table des codes ASCII.

  3. Travail à réaliser :
    1. Complément à 2
      Nous allons réaliser en pratique l’opération prise en exemple dans le cours pour illustrer la mise en œuvre du complément à 2.
      Utilisez la commande Display Register (R) pour afficher le contenu des différents registres.
      Utilisez ensuite l’instruction Move (MOV) pour charger la valeur décimale 73 dans la partie basse de l’accumulateur (AL).
      Pour cela vous passerez en mode Assemblage (A), puis vous entrerez l’instruction : MOV AL,49
      Puis vous entrerez la commande trace (T) qui provoquera l’exécution de cette instruction et affichera le contenu des registres. Vous pourrez constater que le contenu de AL est bien devenu 49.
      En procédant d’une façon analogue, transférer le contenu de AL à l’adresse mémoire 200 hexadécimal.
      Afficher le contenu de cette adresse mémoire grâce à la commande D (Dump) et vérifiez qu’elle contient bien 49 hexadécimal.
      Toujours en procédant de la même manière, chargez 0 dans AL, puis ôtez lui la valeur contenu à l’adresse 200 hexadécimale. Affichez le contenu de AL.
      Que constatez vous ?

      Réalisez la même opération qu’à la question précédente en travaillant uniquement sur AL avec une valeur immédiate et en utilisant l’instruction NEG.

    2. Complément à 1
      De la même façon qu’à la question précédente réaliser le complément à 1 de la valeur décimale 73 en remplaçant la soustraction par une fonction logique que vous devez déterminer.
      Puis comme précédemment réaliser la même fonction en travaillant uniquement sur AL avec une valeur immédiate et en utilisant la fonction NOT.

    3. Visualisation du contenu de la mémoire.
      Utilisez la commande Enter (E) pour initialiser les adresses mémoires 210 à 21F avec les valeurs hexadécimales suivantes :
      49 55 50 2D 4D 49 41 47 45 20 20 20 20 20 20 26
      Affichez le contenu de cette série d’adresses avec la commande DUMP (D) .
      Que constatez-vous ?
      Maintenant chargez en utilisant une suite de commandes move (MOV) avec donnée immédiate les adresses mémoire 220 à 22F avec les codes ASCII qui correspondent à votre nom (la recherche de ces codes a été faites lors de la séance de travaux dirigés précédente).
      Affichez le contenu de cette série d’adresses avec la commande DUMP (D) .
      Que constatez-vous ?

    4. Affichage d’un caractère à l’écran
      Nous allons maintenant afficher à l’écran les mêmes informations en utilisant une routine d’interruption du BIOS.
      Pour cela nous allons tout d’abord afficher une simple lettre à l’écran pour mettre en œuvre le principe de fonctionnement d’une routine d’ interruption, en l’occurrence l’interrupt 21
      Cet routine est capable de réaliser de nombreuses fonctions. Elle lit le contenu du registre AH pour savoir quelle opération elle doit réaliser. Dans notre exemple il faudra mettre la valeur 2 dans le registre AH pour lui signifier qu’elle doit afficher à l’écran le caractère correspondant au code ASCII contenu dans le registre DL. Une fois ces opération réaliser on lancera l’exécution de la routine d’interruption grâce à l’instruction : INT 21
      Chargez le registre DL avec le code ASCII du caractère que vous voulez affichez, puis chargez 2 dans AH et lancer l’interrupt 21.
      Important : quand vous aurez entré ces instructions avec la commande A, n’exécutez pas l’instruction INT avec la trace sinon vous allez tracer toutes les instructions de la routine d’interrupt (et elles peuvent être nombreuses). Utilisez la commande GO (G). Mais attention le programme ne s’arrêtera que si vous rendez le contrôle au DOS, ce qui se fait aussi en utilisant l’interrupt 21, mais cette fois en mettant 4C dans AH et 00 dans AL

    5. Affichage d’une chaîne de caractères à l’écran
      Pour indiquer à la routine d’interruption 21 qu’elle doit afficher à l’écran une chaîne de caractère, il faut cette fois mettre la valeur 9 dans AH (au lieu de 2 pour un simple caractère).
      L’adresse du début de la chaîne de caractères est donnée par :
      - l’adresse du segment dans lequel elle se trouve. (dans notre cas le contenu
      du registre CS) que l’on doit charger dans le registre DS (Data Segment).
      NB : on ne peut charger DS qu’à partir d’un registre. Il faudra donc d’abord
      charger la valeur de CS par exemple dans AX, puis transférer AX dans DS.
      - l’offset par rapport à cette adresse que l’on doit charger dans le registre DX
      La fin de la chaîne de caractère doit être indiquée par la présence d’un caractère $ qui doit obligatoirement finir cette chaîne, sinon la routine d’interruption affiche tous les caractères jusqu’à la fin du segment ou jusqu’à ce qu’elle rencontre un octet contenant le code du caractère .
      En vous aidant de ces informations, affichez à l’écran le contenu des adresses hexadécimales commençant en 0210.
      Vous pouvez ensuite afficher votre nom qui se trouve à l’adresse 220, mais n’oubliez pas de lui ajouter un caractère $ à la fin.


MS-DOS DEBUG Program

Sponsoring website: Emergency Boot CD
Vu dans ... http://thestarman.pcministry.com/asm/debug/debug.htm

A Guide to DEBUG
( The Microsoft® DEBUG.EXE Program )
Copyright©2004,2007,2009,2017 by Daniel B. Sedory

This page may be freely copied for PERSONAL use ONLY !
( It may NOT be used for ANY other purpose unless you have
first contacted and received permission from the author ! )

  • A History of DEBUG
    • Beginnings
    • Changes in DEBUG
    • Under Windows™ 9x/Me
    • Under Windows™ NT/2000/XP and later
    • Summary
    • But PCE even runs under Windows 10!
    • The best DEBUG 'Clone' (also for 32-bit code!)

  • The Limitations of DEBUG
    • Important Initialization Values for DEBUG
    • Using DEBUG with .EXE Files

  • Special Memory Locations in MS-DEBUG

  • DEBUG's own Dynamic Stack Area

  • Have you found a 'bug' in DEBUG?

  • Important Notes for using DEBUG

  • All the Commands -- Listed Alphabetically
    • Quick Links to Command Help

    PAGE TWO:
  • How to Use the COMMANDS
    • Meaning of Command Parameters

    • Detailed Help on each Command (begins here)
      • An Example Program for you to Enter, Run and Edit under DEBUG.
      • A Note about Win 9x's 'IHC' string in Diskette Boot Sectors

  • Appendix
    • The 8086 CPU Registers
    • The FLAGS Register
    • The Segment:Offset Addressing method used by DEBUG

A History of MS-DEBUG

Beginnings

In 1980, Tim Paterson began working on a 16-bit OS for the 8086 S-100 Bus card he had designed for SCP (Seattle Computer Products) the previous year. To help in getting QDOS (later called 86-DOS) to work correctly, Tim created a debugger in a ROM chip; the code for that ROM version was released into the Public Domain. Later, Tim adapted the code to run as a .COM program under QDOS, and also added the ability to disassemble 8086 machine code. In the meantime, Microsoft® had been busy purchasing the rights to sell Tim's QDOS to IBM® for their 'secret' PC project. Tim was then hired by Microsoft as the primary author of their first OS. When he completed his work on IBM's Personal Computer™ DOS 1.00 in 1981, his DEBUG.COM utility was included with it. All the functionality that Tim put into DEBUG is still there and little has been added to it (the major exception being the Assemble command; added under DOS 2.0).
[ Thanks go to Tim Paterson himself for reviewing this perspective on DEBUG's beginnings. ]

Changes in MS-DEBUG

With the release of DOS 2.0, DEBUG gained the ability to assemble instructions directly into machine code (the A command). This is one of the most important commands for many of its users. Though lacking much of the functionality of a stand-alone Assembler, e.g., all Jumps must be to hexadecimal addresses (no labels can be used), many useful .COM programs have been assembled with this command. Under DOS 3.0, the P (Proceed) command was added, so DEBUG could quickly execute subroutines; at the same time, it became possible to attempt stepping through Interrupts with the T (Trace) command. When DOS made EMS (Expanded Memory) functions available under DOS 4.0, the four commands xa, xd, xm and xs were also added to DEBUG. It appears they were rarely, if ever used though, even by programmers. For most of us, the only noticeable change in DEBUG was the addition of the help command (type a '?' while inside DEBUG) under DOS 5.0; when all DOS commands finally got the /? command-line switch.

DEBUG's code went through a number of changes (and 'bug fixes' too) over the years! Some of these internal changes were related to DOS system calls and screen output, then there was the change in file type from a .COM to an .EXE program under DOS 5.0. But in spite of all those changes and others which followed, DEBUG has never had an official revision since 2.40 (those digits have been embedded inside all versions of DEBUG since DOS 3.0). We can only guess about the real reasons that Microsoft® never updated DEBUG to handle instructions beyond those of the Intel® 8086/8087/8088 processors. Microsoft® did create their own Assembler (MASM), 'C' compiler and Debugger (CodeView ); which you could use too, if you were willing to pay extra, so that could have been one of their reasons. Rather than using MASM and CodeView, many opted for the less expensive Borland® assembler (TASM) and Turbo™ Debugger when they appeared, or some other commercial product. However, users and students alike can still learn a great deal about Assembly language by using DEBUG.

DEBUG under Windows® 9x/Me

The internal structure of these Windows® versions of DEBUG appear much different than any previous DOS forms; at least at first glance. Though it seems a great deal has changed, you'll still find the phrase "Vers 2.40" but in a different location. Windows® itself went through a lot of changes during this period, such as being able to handle a new file system, FAT32, and larger drives. But without access to its source code, we can't be sure if there were any major differences in DEBUG. The changes might be due to something as simple as just reorganizing the error messages in the source code and/or using a new Assembler/Linker.

DEBUG had always been an effective tool in the hands of any Batch programmer, but some time after the introduction of Windows™ 95 and especially with Windows™ 98, that effectiveness was diminished when its I/O commands became unreliable! Whether due to a 'bug' in DEBUG itself or in Windows®, the fact is that I/O commands under Windows™ 9x/Me cannot be relied upon for direct access to a hard drive! If you run our ATA drive ID script under Win9x/Me, the data you get back is as interesting as it is disturbing: It appears that every other byte is still correct! So, one has to wonder what the cause of this problem might be.

DEBUG under Windows® NT/2000/XP/2003

The DEBUG program included with Windows® NT/2000/XP/2003 appears to operate the same as it did under DOS 5.0, but with two major exceptions:

1) DEBUG is no longer allowed to load from or write to any logical HDD sectors; only named files can still be read from or written to under an NT-type OS. It can, however, still access diskette sectors in the A:\ or B:\ drives with the L and W commands, but only if those diskettes contain a file system the host OS (2000, XP, 2003) can recognize!
(See the L command in the Tutorial section for more information.)

[ Note: DEBUG has never been able to directly access areas of an HDD outside of its drive volumes; such as an Extended partition table or even the MBR sector! However, DEBUG can be used to access such data by programming it to run INT13 commands or using a script file under DOS (e.g., our old CopyMBR script). Note again, that no Windows version after Win98/ME will allow INT13 access! ]

2) The I and O commands are essentially useless, since the program's interface with the rest of the system is only being emulated under these versions of Windows® rather than having any direct access to the hardware. This was already true to varying degrees under previous versions of Windows®.

This may surprise you: We purposely mentioned the DOS 5.0 version of DEBUG here, since the DEBUG.EXE file included with Windows® XP (and every other version of the NT OS series) is exactly the same program file created for MS-DOS 5.0 (md5sum = c17afa0aad78c621f818dd6729572c48). DEBUG was only one of a small handful of DOS 5.0 programs that didn't require any changes to run under an NT operating system. It's almost ironic that another of those few programs is EDLIN, a line editor disliked by most DOS users. Though EDLIN was also created by Tim Paterson, he did so in just two weeks and was shocked when he heard IBM had actually included it in their PC-DOS 1.00 release! No doubt he wished it had been replaced by something better way back in 1981. It wasn't until the release of DOS 5.0, that EDLIN was effectively replaced by Microsoft's EDIT.COM program (v 1.0, 1991. Note: This first version of EDIT required QBASIC to also be present); EDLIN was, however, still retained, to be 'backwards compatible' with various 3rd-party Batch files. Though NOTEPAD or more advanced editors are available under Windows™, you can still use the 1995 standalone version of EDIT (v 2.0.026) at Command Line prompts in Windows™ XP; its menus will even respond to mouse clicks.

Summary

Though created at the beginning of the 16-bit processor era (before the 80286 existed), more recent versions of DEBUG (such as those found inside a Windows™ Me or 98SE Emergency Boot Diskette's EBD.CAB file) are still useful to PC techs for direct access to certain memory locations on present-day systems (an Intel® Pentium™ IV, for example). DEBUG can also be quite useful for educational purposes. And even for debugging the Assembly code that is required during the boot process: The software that checks the Partition Table on hard disks and loads OS Boot Sectors into Memory. Unfortunately, many Boot Managers and recent MBR sectors now use instructions requiring a 386 (or even 486) class CPU to function, making it difficult to use DEBUG for such a purpose. Because of the backward compatibility of most Intel® processors, and the fact that it was included with Microsoft® Windows™ XP and 2003, DEBUG has had a much longer life span than ever expected. Though the ITANIUM™ CPU was not x86-compatible, the AMD64 was. In 2005, Intel® made the so-called "x64-based" CPUs that were once again x86-compatible. So, DEBUG still continues to find some use on 64-bit computers, even my Intel® Core™ 2 Quad (4 processors in one) machine.

Final Chapter?

Yes, DEBUG and some other early DOS utilities were removed from the Command Line of the 64-bit version of Windows 7, and will never again appear in any new Microsoft OS. However, it's been possible for a few years now to run IBM PC DOS 1.x0, 2.x0 and 3.30 (possibly others) along with their versions of DEBUG in a small window under Windows XP (32-bit), Windows 7 (64-bit), 8 and even the latest update of Windows 10 (Anniversary)! Read here about the PCE (PC Emulator) program by Hampa Hug. Of course, if you wish to run the last version of MS-DEBUG from MS-DOS 6.22 or the 32-bit Debug clone we recommend below, you may need to install BOCHS, QEMU or some other emulator, or VPC, VirtualBox, VMWare Player or some other type of virtual machine software. But what's cool about PCE is that you'll be running the OS under the actual ROM BIOS code used in a physical IBM PC.

A Better (and also for 32-bit code!) DEBUG Clone!

For some years now, we've been using a 'clone' of MS-DEBUG which started as the DEBUG program for FreeDOS, but has since had even more improvements made. You can download and use this program for free; currently we recommend a page made for it on a site created by Vernon C. Brooks, who had been a Senior programmer for IBM's PC DOS 7.0, the remainder of his site deals with the history of IBM PC DOS, and is very informative (PC DOS Retro):
Enhanced DEBUG (current version, since DEC 2014, is 1.30b).

C:\DEBUG> DEBUGX
-?
assemble        A [address]
compare         C range address
dump            D[B|W|D] [range]
dump interrupt  DI interrupt [count]
dump LDT        DL selector [count]
dump MCB chain  DM
dump ext memory DX [physical_address]
enter           E address [list]
fill            F range list
go              G [=address] [breakpoints]
hex add/sub     H value1 value2
input           I[W|D] port
load file       L [address]
load sectors    L address drive sector count
move            M range address
set x86 mode    M [x] (x=0..6)
set FPU mode    MC [2|N] (2=287,N=no FPU)
name            N [[drive:][path]filename [arglist]]
output          O[W|D] port value
proceed         P [=address] [count]
proceed return  PR
quit            Q
register        R [register [value]]
MMX register    RM
FPU register    RN[R]
toggle 386 regs RX
search          S range list
trace           T [=address] [count]
trace mode      TM [0|1]
unassemble      U [range]
write file      W [address]
write sectors   W address drive sector count

prompts: '-' = real/v86-mode; '#' = protected-mode
-

The Limitations of MS-DEBUG

DEBUG was originally designed to work with .COM programs having a maximum size of only 65,280 bytes [ (64 x 1024) - 256 ] or less; how much less, depended upon the maximum number of bytes the program had to place on the Stack at the same time. The subtraction of 256 bytes is necessary since DEBUG often uses the area from offset 00 through FF hex for some internal data such as the name of the file that was loaded. Remember, true .COM programs by definition are supposed to fit inside a single Segment of memory (only 64 KiB).

Even when running MS-DEBUG under the latest Windows® OS, since it's still an old 16-bit DOS application, you can only open files whose names have been saved in the 8.3 DOS convention; i.e., up to 11 characters total, using no more than 8 DOS characters for the name and 3 for the extension.

As early as DOS 1.10, DEBUG was able to load files larger than 64 KiB. Basically, how large a file that DEBUG can safely use without error depends on the amount of available memory and the way the OS handles memory management. We'll say more about this below.

A. When DEBUG starts with no command-line parameters, it:

1) Allocates all 64 KiB of the first free Memory Segment.

2) The Segment registers, CS, DS, ES and SS are all set to the value of that 64 KiB Segment's location (CS=DS=ES=SS=Segment Location ).

3) The Instruction Pointer (IP) is set to cs:0100 and the Stack Pointer (SP) is set to ss:FFEE (under DOS 3.0 or above).

4) The registers, AX, BX, CX, DX, BP, SI and DI are cleared to zero along with the flag bits in the Flag Register; with one exception: The Interrupts Flag is set to Enable Interrupts. (See the Appendix, The 8086 CPU Registers for more information.)

B. When DEBUG starts with a filename (other than an .EXE), it:

1) Allocates at least 64 KiB of the first free Memory Segment for debugging programs or examining files specified on the command line. [Note: Ever since DOS version 1.10, DEBUG has had the ability to load (and save) files larger than 64 KiB. Just how large a file it can handle, depends upon both the OS and available memory. But before you ever consider using DEBUG to save some large file you want to edit, you should know the amount of memory it can use is limited to what's available in CONVENTIONAL MEMORY only! And remember that just because your system can debug a certain file, doesn't mean someone else's will be able to.]

2) The Segment registers, CS, DS, ES and SS are all set to the value of the first 64 KiB Segment's location (CS=DS=ES=SS=Segment Location); for a file that's larger than 64KiB, you'll have to set different segment values to access all the bytes loaded into memory beyond the first 64 KiB.

3) The Instruction Pointer (IP) is set to cs:0100 and the Stack Pointer (SP) is set to ss:FFFE (version 3.0+). [ Note: This is not the same as the ss:FFEE in A. 3) above; there's a 16 byte difference. ]

4) Most of the registers follow the same pattern as above, except for the CX and sometimes BX registers: The size of the file will be placed into the linear combination of the BX and CX registers; for files less than 64 KiB - 256 bytes, only CX is used. Example: If you can load a file of 360,247 bytes, then BX=0005 and CX=7F37 ( 57F37 hex = 360,247 ). If you load a file of exactly 65,536 bytes from a prompt, these registers will be: BX=0001, CX=0000. But due to the automatic 100h load offset, the file's last 256 bytes will have been loaded at the beginning of the next 64 KiB segment.

Remember: The Segment assigned to DEBUG, depends on the amount of memory in use, not the total memory available. So, the same DOS machine, whether it has 16 or even 4096 MiB of memory, will generally load DEBUG into the same Segment; unless a "terminate and stay resident" program is using that memory, or memory was not properly deallocated prior to running DEBUG.

Using DEBUG with .EXE Files

Any version of DEBUG from DOS 2.0 or higher, makes use of the operating system's EXEC function which means that it's possible for you to perform a limited amount of debugging on an .EXE program. However, DEBUG can never be used to save either an .EXE or a .HEX file to disk, since both of these file types contain extra data that DEBUG was never programmed to create after EXEC removed such data! It is quite possible though, to change the extension of an .EXE file, for example to .BIN, so DEBUG can be used to edit such a file, then change it back to an .EXE extension afterwards. Normally we'd recommend using a Hex editor instead, but would like to point out that DEBUG could be used with Batch files and scripts to carry out such edits automatically; taking the place of a Patch program.

One of the simplest .EXE programs you can run under DEBUG is the so-called DOS "Stub" found inside many Windows® executables. You can follow along as we examine one of these here!

Special Memory Locations in MS-DEBUG

There will always be some code and data placed within the first 256 bytes of the Segment for DEBUG's own use. And although DEBUG often functions as expected if you zero-out this area, there may be some cases where you wouldn't want to alter its contents. The code bytes are simple and always found in the same locations: The first two bytes of this area ("CD 20") are machine code for the DOS interrupt: INT 20. The bytes at offsets 50h and 51h ("CD 21") form an INT 21, and the byte "CB" at offset 52h is a RETF instruction.

When booting from an MS-DOS 6.22 upgrade install disk, this area will appear as follows (offsets 90h - FFh were all zero bytes):

A:\>debug
-d 0 8f
1787:0000  CD 20 C0 9F 00 9A EE FE-1D F0 4F 03 EB 11 8A 03   . ........O.....
1787:0010  EB 11 17 03 EB 11 38 0E-01 01 01 00 02 FF FF FF   ......8.........
1787:0020  FF FF FF FF FF FF FF FF-FF FF FF FF 5F 0F 4E 01   ............_.N.
1787:0030  AB 16 14 00 18 00 87 17-FF FF FF FF 00 00 00 00   ................
1787:0040  06 16 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
1787:0050  CD 21 CB 00 00 00 00 00-00 00 00 00 00 20 20 20   .!...........
1787:0060  20 20 20 20 20 20 20 20-00 00 00 00 00 20 20 20           .....
1787:0070  20 20 20 20 20 20 20 20-00 00 00 00 00 00 00 00           ........
1787:0080  00 0D 75 73 0D 0D 00 00-00 00 00 00 00 00 00 00   ..us............

When running DEBUG without a filename, whatever appeared on the previous command line, except for the command itself, will be displayed in the bytes at offsets 82h and following. These are often referred to as DOS switches or parameters. Thus, the "us" in the display above was from the command "keyb us" in the DOS 6.22 install disk's AUTOEXEC.BAT file. And if we had run the command "dir /w" before executing DEBUG, a "/w" would have appeared here instead. Note: Successive uses of DOS parameters are never cleared from memory, only overwritten. So, many characters of a very long parameter string will often remain intact, and as a consequence, will be copied to the bytes at offsets 82h through FFh each time DEBUG is run.

One of these data locations (the Word at offsets 36h - 37h) clearly saves the Segment assigned to DEBUG for our use. However, if we go on to load the file edit.com and dump the beginning of the Segment again, we'll find the Segment value itself has changed from 1787h to 1798h (a difference of 11h or 17 paragraphs, amounting to 256 + 16 = 272 bytes):

-n edit.com
-l
-d 0 8f
1798:0000  CD 20 C0 9F 00 9A F0 FE-1D F0 4F 03 EB 11 8A 03   . ........O.....
1798:0010  EB 11 17 03 EB 11 DA 11-01 01 01 00 02 FF FF FF   ................
1798:0020  FF FF FF FF FF FF FF FF-FF FF FF FF 75 0F E8 49   ............u..I
1798:0030  EB 11 14 00 18 00 98 17-FF FF FF FF 00 00 00 00   ................
1798:0040  06 16 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
1798:0050  CD 21 CB 00 00 00 00 00-00 00 00 00 00 45 44 49   .!...........EDI
1798:0060  54 20 20 20 20 43 4F 4D-00 00 00 00 00 20 20 20   T    COM.....
1798:0070  20 20 20 20 20 20 20 20-00 00 00 00 00 00 00 00           ........
1798:0080  09 20 45 44 49 54 2E 43-4F 4D 0D 00 00 00 00 00   . EDIT.COM......

At first, we were unsure why DEBUG was doing this, but knew it had nothing to do with the size of this program, which is only 413 bytes. Instead it's simply because this is a "program" (EDIT.COM) rather than some other type of file. DEBUG did not do this when loading much larger files of other types, but did so again when loading any of the disk's *.EXE programs. We then confirmed a similar change when loading *.COM or *.EXE programs into DEBUG on an XP machine, but the change there was larger; it added up to 91 paragraphs! After more experiments, using the SET and PATH commands, we discovered DEBUG had some need to load a copy of the DOS "environment variables" between its initial "data area" and a new 256-byte data area it creates when debugging only DOS "program" files. Even when no PATH or environmental variables exist, DEBUG still needs to create a new "data area" for *.COM or *.EXE files.

When running DEBUG in a Windows® DOS-box (under CMD.exe), dumping its first 256 bytes will almost always show the same fragmented string (shown below in white text). The characters are the remains of the Ntvdm program (which starts as soon as any 16-bit command is run) quickly reading one line at a time from the file AUTOEXEC.NT (located in the C:\WINDOWS\system32 folder), into the same area of memory where command line parameters are stored. The longest line in that file, including its trailing 0Dh (carriage return) byte, is successively overwritten by shorter lines in the file until the process results in what's copied to offsets 82h through CEh of DEBUG's Segment:

C:\>debug
-d 0 cf
0B20:0000  CD 20 FF 9F 00 9A EE FE-1D F0 4F 03 84 05 8A 03   . ........O.....
0B20:0010  84 05 17 03 84 05 25 04-01 01 01 00 02 FF FF FF   ......%.........
0B20:0020  FF FF FF FF FF FF FF FF-FF FF FF FF 28 05 4E 01   ............(.N.
0B20:0030  44 0A 14 00 18 00 20 0B-FF FF FF FF 00 00 00 00   D..... .........
0B20:0040  05 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
0B20:0050  CD 21 CB 00 00 00 00 00-00 00 00 00 00 20 20 20   .!...........
0B20:0060  20 20 20 20 20 20 20 20-00 00 00 00 00 20 20 20           .....
0B20:0070  20 20 20 20 20 20 20 20-00 00 00 00 00 00 00 00           ........
0B20:0080  00 0D 20 20 20 53 45 54-20 42 4C 41 53 54 45 52   ..   SET BLASTER
0B20:0090  3D 41 30 0D 64 64 72 65-73 73 2E 20 20 46 6F 72   =A0.ddress.  For
0B20:00A0  20 65 78 61 6D 70 6C 65-3A 0D 20 6F 6E 20 4E 54    example:. on NT
0B20:00B0  56 44 4D 2C 20 73 70 65-63 69 66 79 20 61 6E 20   VDM, specify an
0B20:00C0  69 6E 76 61 6C 69 64 0D-20 6F 6E 6C 79 2E 0D 00   invalid. only...

None of the line feeds (0Ah) at the end of each line in AUTOEXEC.NT will ever appear here, because the carriage returns (0Dh) just preceding them send the cursor to the start of the line each time they're encountered, and whatever comes before the first space character (20h) in every line does not get copied; which is why the "REM" of the last three lines doesn't appear here either.

The byte at offset 81h is always 0Dh; even if the file AUTOEXEC.NT contains a single byte of any value.

Note: If you rename or delete AUTOEXEC.NT, you will not be allowed to run DEBUG (nor any other 16-bit program; all of which must run under Ntvdm). You can, however, save a copy of AUTOEXEC.NT, then edit it to see how your changes affect what's copied into DEBUG. You may reduce its size to just a single byte. But in order to see anything other than zero bytes in offsets 82h and following, at least one space byte (20h) must be placed between a non-space byte at the beginning of a line and whatever you'd like to have displayed. If the file contains only the 3 bytes: "T", space and "S", then offsets 82h and 83h would be an "S" followed by 0Dh.

DEBUG's "Dynamic Stack" Area

This section is presently a "Work in Progress", but if you happen to see this before it's finished, can you guess what it's about?

Have you found a "Bug" in DEBUG?

Although almost all the code used by programmers performs exactly as expected; once they've eliminated their own errors in logic that is, occasionally it will produce surprising results because they didn't dig deep enough into the fine print of the user manuals for a PC's processor. Professional programmers will always test their code in as many ways as reasonably possible, but studying the processor's programming notes; especially sections pertinent to any of their tasks, should be high on their list! NOTE: If you want to be a much better hacker, the example presented here might cause you to delve into Intel's detailed notes on how their CPUs handle various instructions.

Have you ever encountered two distinct lines of Assembly instructions that DEBUG always steps through without ever stopping at the second line? The following is just one of MANY repeatable examples we could list here. Open any instance of DEBUG (DOS or Windows; any version), and copy and paste the following E (Enter) command at its (-) prompt:

e 100 8C C8 8E D0 B0 74 90 90

After entering "u 100 107" it should disassemble to:

 xxxx:0100 8CC8    MOV   AX,CS  <- Keep Stack in CS Segment.
 xxxx:0102 8ED0    MOV   SS,AX  <- The key instruction!
 xxxx:0104 B074    MOV   AL,73  <- Could be almost anything.
 xxxx:0106 90      NOP
 xxxx:0107 90      NOP

Now enter an "r" at the prompt and try to single step (t) through the code. As soon as you enter the t command at offset 0102h, you'll wind up at offset 0106h; every time! Is this some "bug" that was never dealt with? The instruction at offsets 0104h ff. could be almost anything; any 1-, 2-, 3- or even 4-byte machine code will do; we purposely picked one that would alter a register's contents (AL in this case) so you could see this instruction had indeed been executed by the CPU; without the user ever having a choice to do so.

This effect will always be observed no matter what version of DEBUG you run it under; MS-DOS 7.1, 5.0 or all the way back to the first version of DEBUG under IBM PC DOS 1.0 (no guarantee it would act the same if you ran it on an original PC though; we only have an 80486 and later for testing). However, if you expand your research to include other debugging tools, you'll soon realize the chances of every version of two or more tools having the same "bug" are... well, way too coincidental. So, why does this code affect a debugger's interrupt abilities?

If you haven't already reached for your Intel® Instruction Set Reference... What's that? You don't have one! Well, you'd better search for the keywords: Intel, IA32, Software, Instruction and at least download a PDF digital copy of the Instruction Set Reference! (Usually found as two separate files titled: Volume 2A: Instruction Set Reference, A-M and Volume 2B: Instruction Set Reference, N-Z). In my January 2006 copy under "MOV—Move", I found:

" Loading the SS register with a MOV instruction inhibits all interrupts until after the execution of the next instruction. This operation allows a stack pointer to be loaded into the ESP register with the next instruction (MOV ESP, stack-pointer value) before an interrupt occurs1." (IA-32 Intel® Architecture Software Developer’s Manual, Volume 2A: Instruction Set Reference, A-M, 253666-018, Jan 2006, "MOV—Move," page 3-584). And footnote 1. clearly states: "If a code instruction breakpoint (for debug) is placed on an instruction located immediately after a MOV SS instruction, the breakpoint may not be triggered." (page 3-585). For those who are new to how a debugger works, the "instruction breakpoint" which this refers to is not a breakpoint set by users, but rather the, let's call it, automatic breakpoint a debugger sets by itself on every single instruction users step into. So, according to these notes, what you may have thought was a "bug" in DEBUG, is in fact a processor doing what it was designed to do!

Does this mean we believe MS-DEBUG is completely "bug free"? No. In the future we'll post some examples here of real 'bugs' in DEBUG.

Before using any of the debugging (Trace, Procedure) or Register commands, you should familiarize yourself with the abbreviations for the CPU Registers that are referenced in DEBUG (See the Appendix, The 8086 CPU Registers for all the details.)

You should also know about the SEGMENT:OFFSET Addressing method used by DEBUG (and other programming utilities).

A note about where and how DEBUG is used in a computer's Memory:

Using DEBUG in a Windows® DOS-box (or CMD prompt) for the first time could easily confuse you! If you open two instances of DEBUG (one per DOS-window) and examine all the memory they can access, you might notice completely different data in many of the same memory locations! The reason is that each application under a Windows™ OS is (theoretically) given its own 4-Gigabyte "Virtual computer" sandbox to play in, and a copy of the critical data within the machine's first Megabyte of Memory is made for each running instance of DEBUG. Only under 16-bit DOS, does DEBUG actually have access to the real Memory locations in which the operating system itself is running; making it much easier to crash the whole system if an error is made. Under Windows®, the theory is that such errors should crash only the open CMD window or application that caused a problem, but not the whole computer! At least that's how Windows® is supposed to operate. From experience, it seems that Windows™ 2000/XP do a much better job at keeping control of their systems under the same circumstances that often ended in Blue Screens on the displays of earlier versions!

NOTE: If you decide to use one of the PCE emulation programs, DEBUG will exist entirely within the memory space set up by the emulator for whatever OS you happen to be running it under. We will attempt to locate and point out any inconsistencies when using PCE with these early versions of DEBUG, but would appreciate feedback from our readers as well.

Quick Links to all the Commands
(Alphabetically Listed)

We recommend reading through the entire Tutorial on DEBUG before using these quick command links.

For a reminder of all the commands available while inside of DEBUG, simply enter a question mark (?) at the DEBUG prompt; when using DOS 5.0 or later.

Click on a command here for all its details:

-?
assemble    A [address]
compare     C range address
dump        D [range]
enter       E address [list]
fill        F range list
go          G [=address] [addresses]
hex         H value1 value2
input       I port
load        L [address] [drive] [firstsector] [number]
move        M range address
name        N [pathname] [arglist]
output      O port byte
proceed     P [=address] [number]
quit        Q
register    R [register]
search      S range list
trace       T [=address] [number]
unassemble  U [range]
write       W [address] [drive] [firstsector] [number]

Page Two

Updated: December 9, 2007 (09.12.2007); February 12, 2009 (12.02.2009); August 24, 2009 (24.08.2009); April 20, 2015 (20.04.2015).
Last Update: February 18, 2017. (18.02.2017)

The Starman's Realm Assembly Page


An MS-DEBUG Tutorial

Sponsoring website: Emergency Boot CD
Vu dans ... http://thestarman.pcministry.com/asm/debug/debug.htm
    I hope you enjoy the Assembly coding examples used on this page. If you have any questions about them, use this online form to e-mail me. ( Opens in a new window. )

A Guide to DEBUG
(Page Two)
A DEBUG Tutorial
Copyright©2005,2009 by Daniel B. Sedory

This page may be freely copied for PERSONAL use ONLY !
( It may NOT be used for ANY other purpose unless you have
first contacted and received permission from the author! )

  • Each Command -- Listed Alphabetically
    • Quick Links to Command Help

  • How to Use the COMMANDS
    • Meaning of Command Parameters

    • Detailed Help on each Command (begins here)
      • An Example Program for you to Enter, Run and Edit under DEBUG.
      • A Note about Win 9x's 'IHC' string in Diskette Boot Sectors

  • Appendixes
    • The 8086 CPU Registers
    • The FLAGS Register
    • The Segment:Offset Addressing method used by DEBUG

Help on DEBUG Commands

For a reminder of all the commands (and most of the parameters) that are available while inside of DEBUG, simply enter a question mark (?) at the DEBUG prompt; when using DOS 5.0 or later. ( Note: Expanded Memory commands are rarely if ever used and will not be discussed here.)

Quick Alphabetical Links
Click on a command here for all its details:

    -?
assemble    A [address]
compare     C range address
dump        D [range]
enter       E address [list]
fill        F range list
go          G [=address] [addresses]
hex         H value1 value2   (Learn 2's Complement!)
input       I port
load        L [address] [drive] [firstsector] [number]
move        M range address
name        N [pathname] [arglist]
output      O port byte
proceed     P [=address] [number]
quit        Q . . . . . . . . . (Learn this first!)
register    R [register]
search      S range list
trace/step  T [=address] [number]
unassemble  U [range]
write       W [address] [drive] [firstsector] [number]

H o w t o u s e t h e
C O M M A N D S

P a r a m e t e r s

NOTE: Parameters listed in brackets ( [ ] ) are optional. Optional parameters usually indicate there are a number of different ways a command could be used. I've listed the meanings of all the parameters here for you:

address - Memory location specified in hexadecimal. You can use either a simple Offset all by itself (in which case, the present CS 'Code Segment' will be assumed), or you can enter the full Segment:Offset location using either all hex numbers or substituting the name of a segment register for a number. Leading zeros are not required; thus 1F all by itself would be the location 'CS:001F' ( CS meaning whatever the CS happened to be at the time you entered this ). Examples:

    100    DS:12    SS:0    198A:1234
For a detailed discussion, see: Segment:Offset notation.

range - Two hexadecimal addresses separated by a single space. They may be listed as either full Segment:Offset pairs or just an Offset alone ( in which case, the Segment is assumed to be that of the present CS or "Code Segment" ). NOTE: Some commands, such as Compare (C), may require the second address be given only as an offset.

list - A string of Hexadecimal bytes separated by a space, or ASCII data enclosed within single or double quote marks. You can list any number of bytes from a single one up whatever number fits on the line before having to press the Enter key. A single byte, such as 00 is most often used with the FILL (f) command whereas an ENTER (e) command will most likely have a string of many hex bytes or ASCII characters per line; for example:
    e 100 31 C0 B4 09 BA 50 02 CD 21 B8 4C 00 CD 21

    e 250 'This is an ASCII data string.$'

number - Remember all numbers and values used in any DEBUG command are understood as being Hexadecimal only! That includes the number of sectors in the LOAD or WRITE commands and even the number of instructions you want DEBUG to step through in the TRACE or PROCEED commands. It's all HEX all the time when using DEBUG!

A Simple DEBUG Tutorial

Details of each Command

NOTE: In the Examples below, commands which are entered by a user are shown in bold type; data displayed in response by DEBUG is in normal type. DEBUG (from MS-DOS 5.0 or later (which is true for the DEBUG version used by Windows™ XP) will display the following usage message, if you enter debug /?  at a DOS prompt:

C:\WINDOWS>debug /?
Runs Debug, a program testing and editing tool.

DEBUG [[drive:][path]filename [testfile-parameters]]

  [drive:][path]filename  Specifies the file you want to test.
  testfile-parameters     Specifies command-line information required by
                          the file you want to test.
    Quit:  Q

Immediately quits (exits) the Debug program! No questions ever asked... should be the first command you remember along with the "?" command.

Back to TOC

    Hex:  H  value1  value2
A very simple (add and subtract only) Hex calculator. Never forget that all numbers inside of DEBUG are always Hexadecimal. Enter two Hex values (no more than four digits each) and DEBUG shows first the SUM, then the DIFFERENCE of those values. Examples:
   -h aaa 531    -h fff 3      -h dbf ace
   0FDB  0579    1002  0FFC    188D  02F1
   -             -             -

Differences are always the second value subtracted from the first; AAA - 531 = 579. There are no carries past four digits.

Two's Complement arithmetic is always used in this calculator, so think of it as being limited to a maximum of plus 7FFFh (+ 32,767) or a minimum of minus 8000h (- 32,768). Positive values are represented by exactly the same digits as their numbers for 0000h through 7FFFh. A minus 7FFFh, however, is represented by the Hex digits 8001, and a minus 1h (-1) is represented by the Hex digits FFFF. Thus, the output of DEBUG after entering "h 4 fffc" would be a zero and an 8, because FFFC represents a minus 4h (-4) and 4 - (-4) = 8. Examples:

   -h 4 fffc    -h 100 123    -h 7fff 8000
   0000 0008    0223  FFDD    FFFF FFFF
   -            -             -
Note the difference between 100h and 123h; what does FFDD represent? To find the numerical value of a Two's Complement number, first invert every bit (or find its logical inverse); that would be 0022, then add 1. So, this represents a negative 23h. Both the sum and the difference of 7FFFh and 8000h are a negative 1 (or FFFF); which can be arrived at using: 7FFFh + (- 8000h) = -1. However, it's much easier to think of the sums as having nothing to do with Two's Complement notation; thus, 7FFFh + 8000h = FFFFh (32,767 + 32,768 = 65,535). This will even hold true for the differences if the second value is less than the first. But any difference which produces a negative number, must be represented in Two's Complement.

Back to TOC

   Dump:  D  [range]
          D  [address]  [length]

Displays the contents of a block of memory. The Memory locations near the beginning of Segment C000 (even under Windows 2000/XP) should display information about the kind of video card installed on your PC. The first example below shows what a Matrox video card on our system displayed.

Examples:

-d c000:0010
C000:0010 24 12 FF FF 00 00 00 00-60 00 00 00 00 20 49 42  $.......`.... IB
C000:0020 4D 20 43 4F 4D 50 41 54-49 42 4C 45 20 4D 41 54  M COMPATIBLE MAT
C000:0030 52 4F 58 2F 4D 47 41 2D-47 31 30 30 20 56 47 41  ROX/MGA-G100 VGA
C000:0040 2F 56 42 45 20 42 49 4F-53 20 28 56 31 2E 32 20  /VBE BIOS (V1.2
C000:0050 29 00 87 DB 87 DB 87 DB-87 DB 87 DB 87 DB 87 DB  )...............
C000:0060 50 43 49 52 2B 10 01 10-00 00 18 00 00 00 00 03  PCIR+...........
C000:0070 40 00 12 10 00 80 00 00-38 37 34 2D 32 00 FF FF  @.......874-2...
C000:0080 E8 26 56 8B D8 E8 C6 56-74 22 8C C8 3D 00 C0 74  .&V....Vt"..=..t
-
-d 100 130
xxxx:0100 EB 24 0D 0A 54 68 69 73-20 69 73 20 6D 79 20 66  .$..This is my f
xxxx:0110 69 72 73 74 20 44 45 42-55 47 20 70 72 6F 67 72  irst DEBUG progr
xxxx:0120 61 6D 21 0D 0A 24 B4 09-BA 02 01 CD 21 B4 00 CD  am!..$......!...
xxxx:0130 21                                               !
-
The last example above, is what you'd see after entering the code under the Assemble command. We could just as easily have used the length option with the command 'd 100 l31' (that's an 'L' in front of the "31") to produce the same results above. The following example shows only the '$'-terminated display string, which has a length of 24h bytes (remember numbers in DEBUG are always hexadecimal); so that's 36 in decimal:
-d 102 l24
xxxx:0100       0D 0A 54 68 69 73-20 69 73 20 6D 79 20 66    ..This is my f
xxxx:0110 69 72 73 74 20 44 45 42-55 47 20 70 72 6F 67 72  irst DEBUG progr
xxxx:0120 61 6D 21 0D 0A 24                                am!..$
-

Back to TOC

  Search:  S  range  list

Searches within a range of addresses for a pattern of one or more byte values given in a list. The list can be comprised of numbers or character strings enclosed by matching single or double quote marks. [ NOTE: In the examples below, if you do find the same data on your computer, the locations could easily vary from ours! ]

Examples:

-s fe00:0 ffff "BIOS"
FE00:0021
FE00:006F

-d fe00:0
FE00:0000 41 77 61 72 64 20 53 6F-66 74 77 61 72 65 49 42  Award SoftwareIB
FE00:0010 4D 20 43 4F 4D 50 41 54-49 42 4C 45 20 34 38 36  M COMPATIBLE 486
FE00:0020 20 42 49 4F 53 20 43 4F-50 59 52 49 47 48 54 20   BIOS COPYRIGHT
FE00:0030 41 77 61 72 64 20 53 6F-66 74 77 61 72 65 20 49  Award Software I
FE00:0040 6E 63 2E 6F 66 74 77 61-72 65 20 49 6E 63 2E 20  nc.oftware Inc.
FE00:0050 41 77 03 0C 04 01 01 6F-66 74 77 E9 12 14 20 43  Aw.....oftw... C
FE00:0060 1B 41 77 61 72 64 20 4D-6F 64 75 6C 61 72 20 42  .Award Modular B
FE00:0070 49 4F 53 20 76 34 2E 35-31 50 47 00 DB 32 EC 33  IOS v4.51PG..2.3

-s 0:0 dff 'A20'
0000:0C42

-d 0:c40
0000:0C40 0D 0A 41 32 30 20 68 61-72 64 77 61 72 65 20 65  ..A20 hardware e
0000:0C50 72 72 6F 72 2E 20 20 43-6F 6E 74 61 63 74 20 74  rror.  Contact t
0000:0C60 65 63 68 6E 69 63 61 6C-20 73 75 70 70 6F 72 74  echnical support
0000:0C70 20 74 6F 20 69 64 65 6E-74 69 66 79 20 74 68 65   to identify the
0000:0C80 20 70 72 6F 62 6C 65 6D-2E 0D 0A 24 1A 00 BA F6   problem...$....

-s 0:0 dff 43 4f 4d
0000:0774
0000:07C2
0000:07D4
0000:07E6

-d 0:770
0000:0770 7A 02 A6 02 43 4F 4D 31-20 20 20 20 8E 00 70 00  z...COM1    ..p.
0000:0780 C0 A0 7A 02 91 02 4C 50-54 31 20 20 20 20 A0 00  ..z...LPT1    ..
0000:0790 70 00 C0 A0 7A 02 98 02-4C 50 54 32 20 20 20 20  p...z...LPT2
0000:07A0 2D 01 70 00 C0 A0 7A 02-9F 02 4C 50 54 33 20 20  -.p...z...LPT3
0000:07B0 20 20 11 EA 27 27 3F FD-CA 00 70 00 00 80 7A 02    ..''?...p...z.
0000:07C0 AC 02 43 4F 4D 32 20 20-20 20 DC 00 70 00 00 80  ..COM2    ..p...
0000:07D0 7A 02 B2 02 43 4F 4D 33-20 20 20 20 00 00 6B 03  z...COM3    ..k.
0000:07E0 00 80 7A 02 B8 02 43 4F-4D 34 20 20 20 20 E8 D2  ..z...COM4    ..

Back to TOC

Compare:  C  range  address
Compares two blocks of memory. If there are no differences, then DEBUG simply displays another prompt (-). Here's an example of what happens when there are differences:
     -c 140 148 340
     127D:0143  30  6D  127D:0343
     127D:0146  10  63  127D:0346
     127D:0148  49  30  127D:0348
The bytes at locations 140 through 148 are being compared to those at 340 ( through 348, implied ); the bytes are displayed side by side for those which are different (with their exact locations, including the segment, on either side of them).

Back to TOC

   Fill:  F  range  list
This command can also be used to clear a whole segment of Memory as well as filling smaller areas with a continuously repeating phrase or single byte. Examples:
-f 100 12f 'BUFFER'
-d 100 12f
xxxx:0100  42 55 46 46 45 52 42 55-46 46 45 52 42 55 46 46 BUFFERBUFFERBUFF
xxxx:0110  45 52 42 55 46 46 45 52-42 55 46 46 45 52 42 55 ERBUFFERBUFFERBU
xxxx:0120  46 46 45 52 42 55 46 46-45 52 42 55 46 46 45 52 FFERBUFFERBUFFER

-f 100 ffff 0

This last example fills almost all of the assigned Segment with zero bytes (which can also be thought of as clearing the Segment). You should use this command whenever you want to be sure the bytes you'll be looking at in DEBUG's Segment are those you entered or loaded, or bytes DEBUG has changed; not soem previously used bytes from memory! If you want to examine a file from a disk in a 'clean' Segment, you'll first have to start DEBUG without any filename, clear the Segment using: f 100 ffff 0 and then finally load the file using the Name (n) and Load (L) commands in that order.

NOTE: Filling (clearing) any bytes in the area from 00h through FFh of our Segment can sometimes lead to problems; especially when file I/O is involved. DEBUG stores data for its own use in those locations, so we recommend you never overwrite bytes in that area; unless you have a reason for doing so!

Example: A student in an Assembly class was told to enter a string of commands under DEBUG, the last one being: JMP 0 which he was supposed to Trace (T) to the next command and then execute it. He was told it would be an INT 20 instruction. Well in most cases this is true, because DEBUG always sets the first two bytes of its working segment to "CD 20" for just this purpose. Let's test this out. First, open a new instance of DEBUG, then enter the following commands:

-f 100 ffff 0    [Zero-out 100 through FFFF]
-e 100 e9 fd fe  [Enters a 'JMP 0' at 100]
-u 100 102       [Check for correct entry]
xxxx:0100 E9FDFE        JMP     0000
-r
AX=0000  BX=0000  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=xxxx  ES=xxxx  SS=xxxx  CS=xxxx  IP=0100   NV UP EI PL NZ NA PO NC
xxxx:0100 E9FDFE        JMP     0000
-u 0 1
xxxx:0000 CD20          INT     20

If you don't see "INT 20" after entering "u 0 1", then restart DEBUG and try again.

-t  [The "T"(Trace) command]

AX=0000  BX=0000  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=xxxx  ES=xxxx  SS=xxxx  CS=xxxx  IP=0000   NV UP EI PL NZ NA PO NC
xxxx:0000 CD20          INT     20
-p  [Always make sure you use a "P"(Proceed) command for Interrupts!]

Program terminated normally
-q  [Quit]

Well, this never worked for those students. Why? Because the teacher had mistakenly told them to Fill the whole segment with zero bytes (f 0 ffff 0), in essence telling them to delete the very instruction he'd wanted them to execute!

Back to TOC

  Enter:  E  address  [list]
Used to enter data or instructions (as machine code) directly into Memory locations.
Example. First we'll change a single byte at location CS:FFCB from whatever it was before to D2 :
   -e ffcb d2
This next example shows that either single(') or double(") quote marks are acceptable for entering ASCII data. By allowing both forms, entry strings can be created to include either type of quote mark as data:
   -e 200 'An "ASCII-Z string" is always followed by '
   -e 22a "a zero-byte ('00h')." 0
But in order to enter more than a single line of ASCII data, the A (Assemble) command is more practical since it will calculate the next offset for you! (See that command for a Memory dump of these bytes.) Now we'll examine a string of 11 hex bytes you can enter into Memory at locations CS:0100 and following:
   -e 100 B4 09 BA 0B 01 CD 21 B4 00 CD 21
This is actually machine code for a program that will display whatever ASCII characters it finds at locations CS:010B and following, until it encounters a byte value of 24h (a $ sign). If you want to run this program, we'd recommend entering 24h at offset location 7EAh of the Segment so the program will terminate there:
   -e 7ea 24
   -g =100
And you'll soon see: "Program terminated normally" on the display screen. Why did we pick 7EAh? Because many DOS screens are set to display only 25 lines of 80 (50h) characters, and this value allows you to view the maximum number of characters possible on a single screen between the "Go," termination and prompt ("-") lines.

Here's something a bit more interesting for you to try out: It's essentially the same program, but the data includes all of the byte values from 00h through FFh; except for 24h which we placed at the end of the last line. The DEBUG prompt symbol, - , has been purposely excluded from the lines below, so you can copy and paste the whole block into a DEBUG DOS-box (Help on using DOS-Window controls is here if needed):

e 100 B4 09 BA 0B 01 CD 21 B4 00 CD 21 0D 0A 0D 0A 00 01 02
e 112 03 04 05 06 07 08 09 20 0B 0C 20 0E 0F 10 11 12 13 14
e 124 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 20 25 26
e 136 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38
e 148 39 3A 3B 3C 3D 3E 3F 0D 0A 0D 0A 40 41 42 43 44 45 46
e 15a 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58
e 16c 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A
e 17e 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C
e 190 7D 7E 7F 0D 0A 0D 0A 80 81 82 83 84 85 86 87 88 89 8A
e 1a2 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C
e 1b4 9D 9E 9F a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aA aB aC aD aE
e 1c6 aF b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 bA bB bC bD bE bF 0D
e 1d8 0A 0D 0A c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 cA cB cC cD cE
e 1ea cF d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 dA dB dC dD dE dF e0
e 1fc e1 e2 e3 e4 e5 e6 e7 e8 e9 eA eB eC eD eE eF f0 f1 f2
e 20e f3 f4 f5 f6 f7 f8 f9 fA fB fC fD fE fF 0D 0A 0D 0A 24

The bytes 0Dh and 0Ah produce a Carriage Return and Linefeed on the display, so we replaced them in the listing above by 20h; a SPACE byte. The 24h byte was moved to the end of the program with another 20h taking its place. The bytes shown above in blue (0D 0A 0D 0A) form blank lines at the beginning of the output and after every 64 bytes for a nice formatted display.

Therefore, when the program is run, we should see four separate lines of 64 characters each (a few of those being blank spaces as mentioned above), right? Well, let's find out: Start DEBUG in a DOS-Window, copy and paste the lines above into DEBUG at its prompt symbol, then enter the following command:

     g =100    ( 'g' followed by a SPACE, then '=100')

This will immediately run (see Go command) the program, displaying the output lines followed by: "Program terminated normally" [ Do not exit DEBUG, just leave the window open. We're going to show you how to 'patch' this code and save the results as a nice little console program ].

Were you surprised to find more than four spaces on the first line; starting with the very first byte? What about the fact we appear to have missing characters at the end of that line? We'll briefly explain why the characters appeared this way on your screen, but in order to create programs of your own, you'll need to study about control characters, Interrupts and what effect different BIOS and DOS video functions have on the way ASCII characters are displayed. OK, here's what happened:

First, the Zero byte also displays as a blank space here. The 07 byte may make a beep or ding sound (but does not display anything), 08 performs a BACKSPACE (erasing the 06 byte character) and 09 is a TAB -- which may jump up to eight columns to the right before reaching the next 'Tab Stop.' But since it just happens to begin in column seven, it only moves one column to the right where our program places the space we substituted for 0Ah. Lastly, for some reason, when using Function 09 of INT 21h ("Display a string of characters until a '$' sign is encountered"), the ESC character (1Bh; 27 decimal) doesn't display or do anything. So, after reaching the end of the first line, it only appeared as if many of the characters we expected to see were never displayed. In reality, the last three characters are there. It's because of the bytes 07h (displayed nothing), 08h (only backspaced over 06h), 09h (displayed nothing, but moved cursor forward one byte) and 1Bh (displayed nothing) that we saw what we did.

Enter the following two lines into DEBUG (which contain more blank-space substitutions), run the program again, and you'll see all the displayable characters output on the first line in their correct positions:

e 10F 00 01 02 03 04 05 06 20 20 20 20 0B 0C 20
e 11D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 20
All four rows will display evenly in size, including the last one. But the last character, FFh (255 decimal), just like the first, also displays as a blank space here! You can prove this by inserting another byte such as 2Eh (a period '.') after FFh. We've created the following patch which effectively moves up the remainder of the program after the FFh by one:

e 21b 2e 0d 0a 0d 0a 24

After patching and running it again, the program output should look like this:

If you want to, you can save this code as an executable program by first giving it a path and filename (such as, C:\TEMP\ASCIIDSP.COM; see Name command) and writing the bytes (see Write command) to a file like this:

   -n c:\temp\asciidsp.com
   -rcx
   CX0000
   :121       [ Program Length = 220h - 100h + 1 = 121h ]
   -w

If you check the file properties of ASCIIDSP.COM, its size should be 289 bytes.

Back to TOC

     Go:  G  [=address]  [addresses]

Go is used to run a program and set breakpoints in the program's code.

As we saw in an Example for the ENTER command, the '=address' option is used to tell DEBUG where to start executing code. If you use 'g' all by itself, execution will begin at whatever location is pointed to by the CS:IP registers.

Breakpoints are used to HALT the execution of a program we're testing at some point of interest before its termination. Up to 10, optional breakpoint addresses may be set by simply listing them on the '-g' command line; separated by spaces. DEBUG will halt execution just before any of breakpoint addresses is about to be executed, and all of them will be 'cleared' at this time as well. (If you should somehow attempt to set more than 10 breakpoints, DEBUG will respond with: bp Error.

NOTE: Breakpoints can only be set at an address containing the first byte of a valid 8088/8086 opcode. So don't be surprised if picking some arbitrary address never halts the program; especially if you're trying to DEBUG a program containing opcodes DEBUG can't understand (that's any instruction which requires a CPU above an 8088/8086)!

Using "Go" after setting breakpoints at instructions you're sure DEBUG understands is one way you can get more use out of this program; for example, when debugging the "real" code of a Master Boot Record written for an 80386 or above processor. DEBUG won't be able to disassemble, nor step through, such code, but it can pass the code for those type of instructions to the CPU for immediate execution, and still stop at the breakpoint(s) you've set for any instruction that it does understand!

CAUTION: DEBUG replaces the original instructions of any listed breakpoint addresses with CCh (an INT 3). The instructions at these locations are restored to their original bytes ONLY if one of the breakpoints is encountered. If DEBUG does not halt on any of the set breakpoint(s), they will remain enabled! So, don't ever save the code unless you're sure all breakpoints have been cleared! (Saving your work to a backup copy before ever using a breakpoint is the best way to avoid corrupting the code.)

Technical Notes: The "User" stack pointer must be valid and have 6 bytes available for this command to function correctly. An iret instruction is used to jump to the first breakpoint encountered. DEBUG sets the User Stack Pointer and pushes the user Flags, the Code Segment register and the Instruction Pointer onto the User Stack. So if the User Stack is not valid or is too small, the operation might crash.

Back to TOC

Assemble:  A  [address]

Creates machine executable code in memory beginning at CS:0100 (or the specified address) from the 8086/8088 (and 8087) Assembly Language instructions which are entered. Although no Macro instructions nor labels are recognized, you can use the pseudo-instructions 'DB' and 'DW' (so you can use the DB opcode to enter ASCII data like this: DB 'This is a string',0D,0A ); spaces after the commas would make it clearer but aren't necessary.

The 'A' command remembers the last location where any data was assembled, so successive 'A' commands (when no address is specified) will always begin at the next address in the chain of assembled instructions. This aspect of the command is similar to the Dump command which remembers the location of its last dump (if no new address is specified).

The assembly process will stop after you ENTER an empty line.

Examples:

Using the character string from our E (Enter) command above:

      -a 200
      xxxx:0200 db 'An "ASCII-Z string" is always followed by '
      xxxx:022A db "a zero-byte ('00h').", 0
      xxxx:023F
      -d 200 23e
      xxxx:0200  41 6E 20 22 41 53 43 49-49 2D 5A 20 73 74 72 69   An "ASCII-Z stri
      xxxx:0210  6E 67 22 20 69 73 20 61-6C 77 61 79 73 20 66 6F   ng" is always fo
      xxxx:0220  6C 6C 6F 77 65 64 20 62-79 20 61 20 7A 65 72 6F   llowed by a zero
      xxxx:0230  2D 62 79 74 65 20 28 27-30 30 68 27 29 2E 00      -byte ('00h')..

ENTER the characters in bold blue type; you do not need to enter the comments after the semi-colon ( ; ) symbols:

      -a 100
      xxxx:0100 jmp 126      ; Jump over data that follows:
      xxxx:0102 db 0d,0a,"This is my first DEBUG program!"
      xxxx:0123 db 0d,0a,"$"
      xxxx:0126 mov ah,9     ; Function 09 of Int 21h:
      xxxx:0128 mov dx,102   ; DS:DX -> $-terminated string.
      xxxx:012B int 21       ; Write String to STD Output.
      xxxx:012D mov ah,0     ; Function 00 of Int 21h:
      xxxx:012F int 21       ; Terminate Program.
      xxxx:0131
      -g =100

      This is my first DEBUG program!

      Program terminated normally
      -

NOTE: You can pipe simple 8086/8088 Assembly Language "scripts" into DEBUG (You can even include a semi-colon ';' followed by comments on most of its lines. For some odd reason though, these comments do not appear to be allowed on DB/DW lines!). For example, you can copy and paste the following into DEBUG (after entering an initial "a" command) and obtain the same results as above:

jmp 126      ; Jump over data that follows:
db  0d,0a,"This is my first DEBUG program!"
db  0d,0a,"$"
; End of string marker above: "$"=24h
mov ah,9     ; Function 09 of Int 21h:
mov dx,102   ; DS:DX -> $-terminated string.
int 21       ; Write String to STD Output.
mov ah,0     ; Function 00 of Int 21h:
int 21       ; Terminate Program.
  • DEBUG uses the convention of enclosing operands which refer to Memory locations in square brackets '[ ]' (as opposed to an immediate value as an operand).

  • DEBUG may require you to explicitly tell it whether or not an operand refers to a word or byte in Memory! In such cases, the data type must be stated using the prefixes 'WORD PTR' or 'BYTE PTR'

  • For all 8087 Math Coprocessor opcodes, the WAIT or FWAIT prefix must be explicitly specified.
Back to TOC

Unassemble:  U  [range]
Disassembles machine instructions into 8086 Assembly code. Without the optional [range], it uses Offset 100 as its starting point, disassembles about 32 bytes and then remembers the next byte it should start with if the command is used again. ( The word 'about' was used above, because it may be necessary to finish with an odd-number of bytes greater than 32, depending upon the last type of instruction DEBUG has to disassemble. )

NOTE: The user must decide whether the bytes that DEBUG disassembles are all 8086 instructions, just data or any newer x86 instructions (those for the 80286, 80386 on up to the lastest CPU from Intel; which are all beyond the ability of DEBUG to understand)!
   Example:

   -u 126 12F
   xxxx:0126 B409          MOV     AH,09
   xxxx:0128 BA0201        MOV     DX,0102
   xxxx:012B CD21          INT     21
   xxxx:012D B400          MOV     AH,00
   xxxx:012F CD21          INT     21
   -

Back to TOC

  Input:  I  port

The use of I/O commands while running Windows™9x/Me is just plain unreliable! This is especially true when trying to directly access hard disks! Under Win NT/2000/XP, the I/O commands are only an emulation; so don't trust them. Though the example below still works under Win2000/XP, it's most likely using some WinAPI code to show what's in the Windows clock area; not directly from an RTC chip.

Long ago (when DOS was the only OS for PCs), there were dozens of BASIC programs that used I/O commands for handling tasks through parallel and serial ports (e.g., to change the font used by a printer or values in a modem's control registers). Under real DOS, they can still be used for direct communications with keyboards or a floppy drive's control chips along with many other hardware devices.
Here's an example of how to read the hours and minutes from a computer's "real time clock" (RTC):

    -o 70 04  <-- Check the hours.
    -i 71
    18  <----- 18 hours (or 6 p.m.)
    -o 70 02  <-- Check the minutes.
    -i 71
    52  <----- 52 minutes
The first space isn't necessary under most versions of DEBUG; so you can try to get away with just "o70" and "i71" instead. Here's a page of more complex examples dealing with hard drives and the ATA commands for reading info directly from a disk controller!
Back to TOC

  Output:  O  port  byte

See comments under the Input command.

Back to TOC

   Load:
L [address] [drive] [firstsector] [number]
   or program! (See the N command for more on this)

This command will LOAD the selected number of sectors from any disk's Logical Drive under the control of MS-DOS or Windows into Memory. The address is the location in Memory the data will be copied to (use only 4 hex digits to keep it within the memory allocated to DEBUG), the drive number is mapped as: 0=A:, 1=B:, 2=C:, etc., firstsector counts from ZERO to the largest sector in the volume and finally number specifies in hexadecimal the total number of sectors that will be copied into Memory (so a floppy disk with 0 through 2,879 sectors would be: 0 through B3F in Hex).

The terms 'Volume' or 'Logical Drive' used in the definition above mean you cannot use the 'L' command to load or examine the MBR, or any other sectors outside the Primary Volumes or Logical Drive Letters assigned by DOS or Windows! For example (under Windows™ 9x/ME), if you enter the command: L 100 2 0 1 in DEBUG, instead of seeing the very first sector on that hard disk (the MBR), you'll see the first sector of the Boot Record for the Logical drive C: instead (the first partition that can accessed by a compatible MS-DOS or Windows OS). This and the following comments about diskettes, show that DEBUG has always been quite limited compared to a good disk editor or the UNIX 'dd' program.

Load can still be useful in examining Floppy Disks even under Windows™ 2000/XP, but (unfortunately), only if the disk can be read by MS-DOS or Windows. Once again, this shows how limited DEBUG is compared to any utility that can view the raw data on either a hard drive or diskette. (For those of you who wish to examine the actual contents of a hard disk under Windows™ XP, there are free disk editors, such as HxD, which allow you to do so.)

Unlike hard disks, the very first sector on a floppy disk is an OS Boot sector. Here's what you might see from a Logical disk sector and some d umps from a couple floppy disks.

Examples:

-l 100 2 0 1   [ the C: drive. ]
-d 100 10f
xxxx:0100  EB 58 90 4D 53 57 49 4E-34 2E 31 00 02 08 20 00  .X.MSWIN4.1... .
-d 280 2ff
xxxx:0280  01 27 0D 0A 49 6E 76 61-6C 69 64 20 73 79 73 74  .'..Invalid syst
xxxx:0290  65 6D 20 64 69 73 6B FF-0D 0A 44 69 73 6B 20 49  em disk...Disk I
xxxx:02A0  2F 4F 20 65 72 72 6F 72-FF 0D 0A 52 65 70 6C 61  /O error...Repla
xxxx:02B0  63 65 20 74 68 65 20 64-69 73 6B 2C 20 61 6E 64  ce the disk, and
xxxx:02C0  20 74 68 65 6E 20 70 72-65 73 73 20 61 6E 79 20  then press any
xxxx:02D0  6B 65 79 0D 0A 00 00 00-49 4F 20 20 20 20 20 20  key.....IO
xxxx:02E0  53 59 53 4D 53 44 4F 53-20 20 20 53 59 53 7E 01  SYSMSDOS   SYS~.
xxxx:02F0  00 57 49 4E 42 4F 4F 54-20 53 59 53 00 00 55 AA  .WINBOOT SYS..U.
-
-l 100 0 0 1   [ a floppy in the A: drive. ]
-d 100 13d
xxxx:0100  EB 3C 90 29 47 38 71 33-49 48 43 00 02 01 01 00  .<.)G8q3IHC.....
xxxx:0110  02 E0 00 40 0B F0 09 00-12 00 02 00 00 00 00 00  ...@............
xxxx:0120  00 00 00 00 00 00 29 40-16 D8 13 4E 4F 20 4E 41  ......)@...NO NA
xxxx:0130  4D 45 20 20 20 20 46 41-54 31 32 20 20 20        ME    FAT12
-
-l 100 0 0 1   [ a different floppy in the A: drive. ]
-d 100 13d
xxxx:0100  EB 3C 90 53 59 53 4C 49-4E 55 58 00 02 01 01 00  .<.SYSLINUX.....
xxxx:0110  02 E0 00 40 0B F0 09 00-12 00 02 00 00 00 00 00  ...@............
xxxx:0120  00 00 00 00 00 00 29 7E-CF 55 3C 20 20 20 20 20  ......)~.U<
xxxx:0130  20 20 20 20 20 20 46 41-54 31 32 20 20 20              FAT12
-
-d 2d0 2ff
xxxx:02D0  42 3B 16 1A 7C 72 03 40-31 D2 29 F1 EB A7 42 6F  B;..|r.@1.)...Bo
xxxx:02E0  6F 74 20 66 61 69 6C 65-64 0D 0A 00 00 00 00 4C  ot failed......L
xxxx:02F0  44 4C 49 4E 55 58 20 53-59 53 F4 3C 82 3A 55 AA  DLINUX SYS.<.:U.

The Linux Boot disk above (note the word: SYSLINUX) is the kind formatted as an MS-DOS diskette and not with a true Linux file system (such as ext2 or ext3). If it had been formatted with some other kind of file system, or had a faulty boot sector, then MS-DEBUG would not be able to read it! Instead you'd see that old "General failure reading drive A / Abort, Retry, Fail?" error message! And when you had finally cleared away that error message, you'd be greeted by DEBUG's "Disk error reading drive A" error message. This makes DEBUG almost worthless as far as trying to fix an error in a floppy disk's boot sector! However, if you keep a binary copy of a good floppy disk Boot Sector somewhere, you could use DEBUG to overwrite whatever's on a faulty floppy disk's first sector (see Write command). But if you really need to see what's in such a Boot sector (i.e., what is keeping DEBUG from recognizing it as valid), you'll need to use a disk editor such as Symantec's Norton DiskEdit (in Physical disk Mode only).

NOTE: Just because a floppy disk can't be read by DOS or opened in DEBUG does NOT necessarily mean it's defective. It might simply have been formatted with a file system it cannot recognize (such as Linux's ext2) and could easily boot-up on its own; a very good reason for labeling your disks! (CAUTION: Never try booting your system with a disk you're not 100% sure of; unless you disconnect all hard disks and don't have any flash BIOS, since it might contain a nasty boot virus! )
[ Many floppy disks have the letters IHC in their OEM ID field. What kind of OEM Name is that? None. Someone at Microsoft decided that this was where they'd place a new pseudo-random type of identification to make sure that any information cached by 'Windows 9x' from one disk wouldn't be mixed up with info from a different one if you swapped disks. The whole string begins with five pseudo-random hex bytes, and always ends with the characters IHC. All floppy diskettes that are not write-protected will have any original OEM ID overwritten. Once Windows has written this string, it will remain the same for any future disk reads or writes. However, performing even a quick format under Windows, will change the five hex bytes every time.
Some have concluded the characters 'IHC' are the first three letters of the word "Chicago" in reverse order, since Chicago was the 'code name' for Windows™ 95 before it was ever released (it would have appeared as ' OGACIHC' on the hypothetical disk). Although certainly a possibility, I have no proof of that. Due to our interest in some very old Greek Manuscripts, we still can't help but see the 3 characters 'IHC' as an Iota, Eta and old style Sigma since this combination of letters was often used as an abbreviation for the Greek word "IHSUS" (Jesus). Just another of many coincidences in our lives.
REMEMBER: If you really want to preserve all of the contents of an important diskette, you can't even perform a simple Directory read under a Windows OS, UNLESS it is 'write-protected' and you know the drive's write-protect system is functioning correctly! ]

Back to TOC

   Move:  M  range  address
This command could be called COPY (not Move), since it only copies all the bytes from within the specified range to a new address.

Examples:
  1)   -m 7c00 7dff 600
Copies all 512 (200h) of the bytes between Offsets 7C00 and 7DFF (inclusive) to Offset 0600 and following.
  2)   -m 100 2ff 200

This second example shows it's very easy to overwrite much of the same source area you're copying from when using the Move command. However, DEBUG must store all the source bytes in Memory before writing them; otherwise, this example would cause a problem when overwriting an area it hadn't copied data from yet, if it were copying only one byte at a time from that source area! The example above copies all 512 bytes of offsets 100h through 2FFh (inclusive) to Offsets 0200h and following; overwriting the last 256 (2FF-200+1 hex) bytes of the source in the process. This is also true under real 16-bit DOS.

Note: If your Move command produces a situation where offset FFFFh has already been written to yet there's still more data to write, you may experience unexpected results! Remember, DEBUG is technically assigned to only one 64 KiB Segment. So, the data will wrap around to the beginning of the Segment, possibly overwriting some of the source bytes you told it to copy from! But other symptoms may occur as well, since the first area to be overwritten after wrapping around (00h through FFh) is sometimes used by DEBUG to keep track of itself. So do the math whenever copying bytes to a higher location in Memory, to be sure you don't run out of room at the end of the Segment.

Copying bytes to a lower location in the Segment is quite simple though; DEBUG could actually copy just one byte at a time in that direction and never overwrite a source byte before it was already copied.

Back to TOC

   Name:  N  [pathname]  [arglist]

This command can be used to load files into DEBUG's Memory after you have started the program, but it's main function is to create a new file under control of the Operating System which DEBUG can WRITE data to.
Normally, when you want to 'debug' a file, you'd start DEBUG with a command like this: C:\WINDOWS>debug test.com . But it's also possible to load a file into DEBUG's Memory from within DEBUG itself by using the 'N' command and then the 'L' command (with no parameters) like this:
    -n c:\temp\test.com
    -l
which will load the file test.com into DEBUG's Memory starting at location CS:0100 (you cannot specify any other location when using the L command like this!).

The 'N' command makes it quite easy to save data or an Assembly program created in DEBUG to a file on your hard drive! For example, the following commands (shown in bold blue type) will create a 68-byte file called DOSWINOK.COM in the C:\TEMP folder; even when running DEBUG in a DOS-window. The file names, however, are still limited to DOS's eight characters plus three for the extension (often called an 8.3 filename):

  -n c:\temp\doswinok.com
  -a 100
  cs:0100 jmp 138
  cs:0102 db 0d,0a,"It's OK to run this "
  cs:0118 db "program under DOS or Windows!"
  cs:0135 db 0d,0a,24
  cs:0138 mov dx,102
  cs:013B mov ah,9
  cs:013D int 21
  cs:013F mov ax,4c01
  cs:0142 int 21
  cs:0144   [Press ENTER key.]
  -rcx
  CX 0000
  :44
  -w
  Writing 00044 bytes   [ 68 bytes in decimal ]
  -q

How did we arrive at the value of "44" to put into the CX register for writing that many bytes to our new file? Well, if you assemble a program in DEBUG using the Assemble command just as we did above, you can subtract the location that we started our programming on from the location DEBUG displays on the line you stopped the assembly by pressing the ENTER key. This will save you a bit of thought by not having to remember when you subtract the first byte location (of the first instruction) from the last byte location (of the last instruction), both bytes being inclusive, that you must then add a 1 to the result to obtain the total number of bytes in your program. Or, putting this in terms of the digits involved: (144h - 100h ) = (143h - 100h + 1) = 44 hex = 68 decimal.

NOTE: Unlike other programs listed on this page, this last one used the Function 4Ch instead of Function 00 of Interrupt 21h to terminate its execution. This is the preferred termination function for most DOS programs, because it can send a "Return Code" (an ERRORLEVEL value; of whatever is in the AL register), and will also close all open files and free all memory belonging to the process. However, when you use this function to terminate a program you run under DEBUG, it has a tendency to terminate DEBUG itself; thus the reason we rarely use it here!

Homework: Follow the steps above to Assemble and save this program under DEBUG, then use DEBUG to debug it! Use the P(roceed) command to step through most of the instructions, since this will keep you from accidentally stepping into an INT(errupt) instruction! If you ever do use the T(race) command on an INT, you'll end up inside nests of BIOS routines which often crashes DEBUG!

Back to TOC

Register:  R  [register]
Entering ' r ' all by itself will display all of the 8086 register's contents and the next instruction which the IP register points to in both machine code and an unassembled (Assembly Language) form. For example, if you start DEBUG in a Windows 95B DOS-box with the command line:
>debug c:\windows\command\choice.com
 and then enter an ' r ' at the first DEBUG prompt, DEBUG will display someting similar to this:
AX=0000  BX=0000  CX=1437  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=0ED8  ES=0ED8  SS=0ED8  CS=0ED8  IP=0100   NV UP EI PL NZ NA PO NC
0ED8:0100 E90E01        JMP     0211
For an explanation of the names of the registers (AX, BX, CX, etc. and the Flag symbols: NV UP EI PL NZ NA PO NC), see the Appendix (The 8086 CPU Registers). The last line shows the next CPU instruction (actually the first in this case) to be executed, begins at memory location 100 hex (the offset) in Segment ED8 hex (0ED8:0100) and the Hex bytes E90E01 represent the actual binary machine code of the CPU instruction (JMP 0211 in Assembly language) that would be executed by DEBUG if you entered a Trace (t) or Proceed (p) command.

If you enter the ' r ' followed by the abbreviation for an 8086 register, such as: ' rcx ', then DEBUG will display only the contents of that register followed by a line with a colon symbol (:) on which you can enter a hex number to change the contents of that register. If you simply press the ENTER key, the contents remain the same. For example:

    -rcx
    CX 0100
    :273
    -

means the Register command was used to change the contents of the CX register from 0100 to 0273. The command rcx could also be used again to verify the change had indeed taken place.

NOTE: The register command only functions with 16-bit registers; including the Flags register, so entering an 8-bit or an invalid register name results in: br Error.

Typing the letter f (for Flags) after the r: rf, causes DEBUG to display all the FLAG register bits and its 'prompt' on the same line. This prompt allows you to change all, or none, of the individual flag bits. For example, here's how you would display the flags and change only the Zero and Carry flag bits from being cleared (a 0 bit) to being set (a 1 bit) and check them without making any further changes:

   -rf
   NV UP EI PL NZ NA PO NC  -zr cy
   -rf
   NV UP EI PL ZR NA PO CY  -
   -
As you can see above the Zero and Carry flags were changed from NZ and NC (both cleared) to ZR and CY (both set). See our Appendix: The FLAGS Register for an explanation of all the Flag abbreviations.

Any order, with or without spaces, is acceptable after the 'prompt' (such as: -zrupcy).
  • Attempting to enter an incorrect abbreviation for a flag value (even with one or more correct names) results in a: bf Error and no flags are changed.
  • Attempting to enter abbreviations for both clearing and setting the same flag, at the same prompt, will cause DEBUG to display the: df Error.
Back to TOC

   Trace:  T  [=address]  [number]
The T command is used to trace (step through) CPU instructions one at a time. If you enter the T command by itself, it will normally step through only ONE instruction beginning at the location specified by your CS:IP registers, halt program execution and then display all the CPU registers plus an unassembled version of the next instruction to be executed; this is the 'default' mode of the TRACE command. Say, however, you wanted DEBUG to trace and execute seven instructions beginning at address CS:0205; to do so, you would enter:

    -t =205 7

Remember that the value for the number of instructions to execute must be given in hexadecimal just as all other values used in DEUBG. (Since the T command uses the "hardware trace mode" of the CPU, it's possible to step through instructions in a ROM - Read Only Memory - chip; or step into BIOS code which has been shadowed in read-only portions of Memory for decades now.) NOTE: If you find yourself stuck inside a long LOOP or REPeat string instruction, enter a P (Proceed) command and it will complete the operation and move to the next instruction.

Back to TOC

 Proceed:  P  [=address]  [number]
Proceed acts exactly the same as Debug's T (Trace) command for most instruction types; with these notable exceptions: Proceed will immediately execute all instructions (rather than stepping through each one) inside any subroutine CALL, LOOP, REPeat string instruction or any software INTerrupt. You can still step into an INT or execute all the code contained in a subroutine if you need to, but with the Proceed (P) command you are not required to do so.
This means Proceed will probably be the command of choice for most of debugging tasks, with Trace only being used to step through an unfamiliar subroutine or check the logic of the first few iterations of long REP string instructions or LOOPs. And it's a must use command when it comes to Interrupts!
Back to TOC

Write:
W [address] [drive] [firstsector] [number]
W A R N I N G
Do NOT experiment with the W - write command in DEBUG. It can be used effectively to create new files on your hard drive, but only if you use it properly. Trying to write directly to a sector on a hard disk would very RARELY be considered proper use of this command!
    Trying to write directly to a hard disk using sector numbers will most likely result in loss of data or even a non-booting system! (Although Windows XP and later prevent direct Sector writes to a hard disk, they are still allowed to floppy drive media, i.e., drive letters A: or B:)
The WRITE (W) command is often used to save a program to your hard disk from within DEBUG. But the only safe way to do so, especially under Windows, is by allowing the OS to decide where to physically create that file on the disk. This is done by first using the Name (N) command to set up an optional path and filename for the new file (or to overwrite one that already exists). DEBUG will automatically begin saving program or data bytes from Offset 0100 of the 64 KiB Segment the OS allocated for it. The only other requirement is to set the size of the file you wish to write by placing the total number of bytes in the combined BX and CX registers* before executing the WRITE command. The Register command is used to change the value in the CX register in the following example from our MyMBR Batch/Debug Script Program.

EXAMPLE:
After creating and running a small program inside of DEBUG which copies the Master Boot Record (MBR) to Offset 0000h through 01FFh, these DEBUG commands save the MBR to a file on the hard disk:
   -n mymbr.bin
   -rcx
   CX 0001
   :200
   -w 0
   Writing 00200 bytes   [ 512 bytes in decimal ]
   -

The BX register had already been set to zero by a previous instruction, so the CX register was simply set to 200 and the WRITE command executed with an address of 0 (if no address is used, the Write command starts saving bytes at Offset 100).

The WRITE command can, however, be used in a relatively safe manner with Floppy disks. For example, you could use the Load (L) command:
    l 7c00 0 0 1
to load the first sector of an MS-DOS or Windows floppy disk into DEBUG's memory at location 7C00, change some of the code and/or messages (if you know how to do so) and then use the 'W' command:
    w 7c00 0 0 1
to write the changes back to the floppy disk's first sector.

___________________
*Although the BX and CX registers are often referenced in books on Assembly as BX:CX when they discuss this write command, note that these registers are not being used like Segment:Offset pairs in this case! They are a true combination of higher and lower bytes which form a 'double word' for a theoretical total of about 4 GB (FFFF FFFFh = 4,294,967,295 bytes) that could be written to a file! Whether or not this is true of all versions of DEBUG, under DOS 7.1, we've been able to load image files of several hundred KiB and write the whole file to a new location!
For example, if you load a 360 KiB image file into DEBUG at a DOS prompt, then check the registers, BX will equal 0005 and CX will contain A000. The major problem here though is the fact DEBUG uses CONVENTIONAL MEMORY, so trying to load a file greater than about 400 KiB is bound to elicit an "Insufficient Memory" error!
[ click here to go back to text above ]

Back to TOC

N O T E :
You may have missed this page on: The Windows™ DOS Stub Program. And there are more examples of many DEBUG commands on these x86 tutorial pages:
FIRE.com: Some MS-DEBUG Tricks and The EICAR Standard AV Test Program .

Updated: September 28, 2007. (28.09.2007)
Previous Update: August 23, 2010. (23.08.2010)
Last Update: September 19, 2010. (19.09.2010)


DEBUG MS-DOS

     This  tutorial is made to present an overview of the DEBUG.COM program for
the IBM PC.   This utility can be extremely useful, when used correctly.  It is
almost  a  must for Assembler Language programmers,  and can  also  provide  an
insight  into  the operation of the machine at the bit level.   It has  several
nice features, including the ability to display and change any of the registers
in the IBMPC, start and stop program execution at any time, change the program,
and  look  at diskettes,  sector by sector.   DEBUG works at the  machine  code
level,  but it does also have the ability to disassemble machine code,  and (at
dos 2.0), assemble instructions directly into machine code.

     The  procedure for starting DEBUG and command syntax will not  be  covered
here,  as they are well documented in the DOS manual.   What we will do is show
some examples of the various commands and the response which is expected.  Note
that the segment registers will probably not be exactly what is shown.  This is
normal, and should be expected.

     For  the examples,  I will be using the demo program CLOCK.COM in the  XA4
database.   For  those of you with the IBM assembler (MASM),  the source can be
down loaded.   If you do not have the assembler, or have another assembler, the
file  CLOCK.HEX has been up loaded.   It can be converted to a .COM file  using
any of the existing HEX conversion programs on the SIG.  See the file CLOCK.DOC
for more information.

                                STARTING DEBUG

     There are two ways to start DEBUG with a file.  Both ways produce the same
results, and either can be used.

     In the Command Line:  A>debug clock.com

     Separate from the command line:  A>debug
                                      -n clock.com
                                      -l

     With either method,  you will get the DEBUG prompt of a hyphen (-).  DEBUG
has loaded your program and is ready to run.   The description of each instruc-
tion will assume this as a starting point,  unless otherwise mentioned.   If at
any time you get different results,  check your procedure carefully.   If it is
correct,  please leave me a message.   I have tried to check everything,  but I
have been known to make a mistake or two (anyway).

     If you do have problems,  you can enter the command Q (Quit) any time  you
have the DEBUG prompt (-).  This should return you to the DOS prompt.

                                RUNNING DEBUG

                               DISPLAY COMMANDS

 Register command

     The first thing we should look at are the registers,  using the R command.
If  you type in an R with no parameters,  the registers should be displayed  as
so:

AX=0000  BX=0000  CX=0446  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=6897  ES=6897  SS=6897  CS=6897  IP=0100   NV UP DI PL NZ NA PE NC
6897:0100 E96B01        JMP     026E

     CX  contains  the length of the file (0446h or 1094d).   If the file  were
larger  than 64K,  BX would contain the high order of the size.   This is  very
important to remember when using the Write command,  as this is the size of the
file to be written.   Remember,  once the file is in memory,  DEBUG has no idea
how large the file is,  or if you may have added to it.   The amount of data to
be written will be taken from the BX and CX registers.

     If  we want to change one of the registers,  we enter R and  the  register
name.  Let's place 1234 (hexadecimal) in the AX register:

     -R AX          R and AX register
     AX 0000        Debug responds with register and contents
     : 1234         : is the prompt for entering new contents.  We respond 1234
     -              Debug is waiting for the next command.

     Now if we display the registers, we see the following:

AX=1234  BX=0000  CX=0446  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=6897  ES=6897  SS=6897  CS=6897  IP=0100   NV UP DI PL NZ NA PE NC
6897:0100 E96B01        JMP     026E

Note that nothing has changed,  with the exception of the AX register.  The new
value has been placed in it,  as we requested.  One note.  The Register command
can only be used for 16 bit registers (AX,  BX,  etc.).  It cannot change the 8
bit registers (AH,  AL,  BH,  etc.).  To change just AH, for instance, you must
enter the the data in the AX register, with your new AH and the old AL values.

Dump command

     One of the other main features of DEBUG is the ability to display areas of
storage.   Unless you are real good at reading 8088 machine language,  the Dump
command is mostly used to display data (text,  flags,  etc.).  To display code,
the Unassemble command below is a better choice.   If we enter the Dump command
at this time,  DEBUG will default to the start of the program.   It uses the DS
register as it's default,  and,  since this is a .COM file,  begins at DS:0100.
It will by default display 80h (128d) bytes of data, or the length you specify.
The  next  execution of the Dump command will display the following 80h  bytes,
and so on.   For example, the first execution of D will display DS:0100 for 80h
bytes,  the next one DS:0180 for 80h bytes,  etc.   Of course, absolute segment
and  segment register overrides can be used,  but only hex numbers can be  used
for the offset.  That is, D DS:BX is invalid.

     With our program loaded, if we enter the Dump command, we will see this:

6897:0100  E9 6B 01 43 4C 4F 43 4B-2E 41 53 4D 43 6F 70 79   ik.CLOCK.ASMCopy
6897:0110  72 69 67 68 74 20 28 43-29 20 31 39 38 33 4A 65   right (C) 1983Je
6897:0120  72 72 79 20 44 2E 20 53-74 75 63 6B 6C 65 50 75   rry D. StucklePu
6897:0130  62 6C 69 63 20 64 6F 6D-61 69 6E 20 73 6F 66 74   blic domain soft
6897:0140  77 61 72 65 00 00 00 00-00 00 00 00 00 00 00 00   ware............
6897:0150  00 00 00 00 00 00 00 00-00 24 00 00 00 00 00 00   .........$......
6897:0160  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
6897:0170  00 00 00 00 00 00 00 00-00 00 00 00 44 4F 53 20   ............DOS

     Notice that the output from the Dump command is divided into three  parts.
On the left, we have the address of the first byte on the line.  This is in the
format Segment:Offset.

     Next  comes the  hex data at that location.   Debug will always start  the
second line at a 16 byte boundary; that is, if you entered D 109, you would get
7  bytes of information on the first line (109-10F),  and the second line would
start at 110.   The last line of data would have the remaining 9 bytes of data,
so 80h bytes are still displayed.

     The third area is the ASCII representation of the data.  Only the standard
ASCII  character set is displayed.   Special characters for the IBMPC  are  not
displayed;  rather periods (.) are shown in their place.   This makes searching
for plain text much easier to do.

     Dump can be used to display up to 64K bytes of data, with one restriction:
It cannot cross a segment boundary.   That is,  D 0100 l f000 is valid (display
DS:0100  to  DS:F0FF),  but  D 9000 l 8000 is not (8000h +9000h  =  11000h  and
crosses a segment boundary).

     Since  64K  is 10000h and cannot fit into four hex characters,  Dump  uses
0000 to indicate 64K.  To display a complete segment, enter D 0 l 0.  This will
display the total 64K segment.

     If,  at  any time you want to suspend the display  of  data,  Cntl-NumLock
works as usual.   If you want to terminate the display, Cntl-Break will stop it
and return you to the DEBUG prompt.

Search

     Search  is  used  to find the occurrence of a specific byte or  series  of
bytes  within a segment.   The address parameters are the same as for the  Dump
command, so we will not duplicate them here.  However, we also need the data to
be searched for.   This data can be entered as either hexadecimal or  character
data.   Hexadecimal  data is entered as bytes,  with a space or a comma as  the
separator.   Character  data is enclosed by single or double quotes.   Hex  and
character data can be mixed in the same request,  i.e. S 0 l 100 12 34 'abc' 56
is  valid,  and requests a search from DS:0000 through DS:00FF for the sequence
of 12h 34h a b c 56h,  in that order.  Upper case characters are different than
lower  case  characters,  and a match will not be found if the  case  does  not
match.   For  instance,  'ABC'  is not the same as 'abc' or 'Abc' or any  other
combination of upper and lower case characters.  However, 'ABC' is identical to
"ABC", since the single and double quotes are separators only.

     An example is looking for the string 'Sat'.  Here's what would happen:

     -S 0 l 0 'Sat'
     6897:0235
     -
Again,  the  actual segment would be different in your system,  but the  offset
should be the same.   If we then displayed the data,  we would find the  string
'Saturday'  at this location.   We could also search on 'turda',  or any  other
combination  of characters in the string.   If we wanted to find every place we
did an Int 21h (machine code for Int is CD), we would do the following:

     -S 0 l 0 cd 21
     6897:0050
     6897:0274
     6897:027F
     6897:028B
     6897:02AD
     6897:02B4
     6897:0332
     6897:0345
     6897:034C
     6897:043A
     6897:0467
     6897:047A
     6897:0513
     6897:0526
     6897:0537
     6897:0544
     -

DEBUG found the hex data CD 21 at the above locations.  This does not mean that
all these addresses are INT 21's, only that that data was there.  It could (and
most likely is) an instruction,  but it could also be an address, the last part
of a JMP instruction,  etc.  You will have to manually inspect the code at that
area to make sure it is an INT 21.   (You don't expect the machine to do every-
thing, do you?).

Compare command

     Along  the  same lines of Dump and Search commands,  we have  the  Compare
command.   Compare  will take two blocks of memory and compare them,  byte  for
byte.  If the two addresses do not contain the same information, both addresses
are displayed,  with their respective data bytes.   As an example, we will com-
pare DS:0100 with DS:0200 for a length of 8.

     -d 0100 l 8 0200
     6897:0100  E9  65  6897:0200
     6897:0101  6B  70  6897:0201
     6897:0102  01  74  6897:0202
     6897:0103  43  65  6897:0203
     6897:0104  4C  6D  6897:0204
     6897:0105  4F  62  6897:0205
     6897:0106  43  65  6897:0206
     6897:0107  4B  72  6897:0207

     None of the eight bytes compared,  so we got output for each byte.   If we
had gotten a match on any of the bytes, DEBUG would have skipped that byte.  if
all of the locations requested matched,  DEBUG would have simply responded with
another prompt.   No other message is displayed.   This is useful for comparing
two blocks of data from a file,  or a program with the BIOS ROM.   Otherwise, I
have not found a great amount of use for it.

Unassemble command

     For  debugging,  one of the main commands you will use is  the  Unassemble
command.   This  command will take machine code and convert it to instructions.
Addressing is the same as for previous commands with one exception:   Since  we
are  now  working with code (the previous commands are mainly  for  data),  the
default register is the CS register.  In a .COM program, this makes very little
difference,  unless  you reset the DS register yourself.   However,  in a  .EXE
file,  it  can make a lot of difference,  as the CS and DS registers are set to
different values.

     Unassemble data can lead to some interesting results.   For  instance,  in
our example,  CS:IP is set to 6897:0100.   If we look at the program,  we see a
JMP as the first instruction,  followed by data.   If we just enter U,  we will
start at CS:IP (6897:0100) and start unassembling data.   What we will get is a
good instruction, followed by more or less nonsense.  For instance:

     -U
     6897:0100 E96B01        JMP     026E
     6897:0103 43            INC     BX
     6897:0104 4C            DEC     SP
     6897:0105 4F            DEC     DI
     6897:0106 43            INC     BX
     6897:0107 4B            DEC     BX

And so on,  through 6897:011D.   We know the INC BX, DEC SP, etc. are not valid
instructions,  but  DEBUG doesn't,  so we do have to look at the  code.   After
working with DEBUG a little, you will be able to spot code versus data with the
Unassemble  command.   For now,  suffice to say that the first instruction will
take us to CS:026E and we can start from there.

     If we Unassemble CS:026E, we will find something which looks a little more
like what we expect.  We get:

     -U 26E
     6897:026E 8D167802      LEA     DX,[0278]
     6897:0272 B409          MOV     AH,09
     6897:0274 CD21          INT     21
     6897:0276 EB05          JMP     027D
     6897:0278 1B5B32        SBB     BX,[BP+DI+32]
     6897:027B 4A            DEC     DX
     6897:027C 24B4          AND     AL,B4
     6897:027E 30CD          XOR     CH,CL
     6897:0280 213C          AND     [SI],DI
     6897:0282 027D0A        ADD     BH,[DI+0A]
     6897:0285 8D167C01      LEA     DX,[017C]
     6897:0289 B409          MOV     AH,09
     6897:028B CD21          INT     21
     6897:028D CD20          INT     20

     The first few instructions look fine.   But,  after the JMP  027D,  things
start to look a little funny.  Also, note that there is no instruction starting
at 027D.   We have instructions at 027C and 027E,  but not 027D.  This is again
because DEBUG doesn't know data from instructions.  At 027C, we should (and do)
have the end of our data.   But, this also translates into a valid AND instruc-
tion,  so DEBUG will treat it as such.   If we wanted the actual instruction at
027D,  we could enter U 027D and get it,  but from here,  we don't know what it
is.   what I'm trying to say is,  DEBUG will do what ever you tell it.   If you
tell it to Unassemble data,  it will do so to the best of its ability.  So, you
have to make sure you have instructions where you think you do.

                              DATA ENTRY COMMANDS

Enter

     The  Enter command is used to place bytes of data in memory.   It has  two
modes:  Display/Modify  and  Replace.   The difference is in where the data  is
specified - in the Enter command itself, or after the prompt.

     If you enter E address alone,  you are in display/modify mode.  DEBUG will
prompt  you  one  byte at a time,  displaying the current byte  followed  by  a
period.   At this time,  you have the option of entering one or two hexadecimal
characters.   If you hit the space bar, DEBUG will not modify the current byte,
but  go on to the next byte of data.   If you go too far,  the hyphen (-)  will
back up one byte each time it is pressed.

     E 103
     6897:0103  43.41   4C.42   4F.43   43.     4B.45
     6897:0108  2E.46   41.40   53.-
     6897:0109  40.47   53.

In this example,  we entered E 103.   DEBUG responded with the address and  the
information  at  that  byte (43).   We entered the 41 and  DEBUG  automatically
showed the next byte of data (4C).  Again, we entered 42, debug came back.  The
next byte was 4F, we changed it to 43.  At 106, 43 was fine with us, so we just
hit the space bar.  DEBUG did not change the data, and went on to the following
bytes.  After entering 40 at location 109, we found we had entered a bad value.
The  hyphen  key was pressed,  and DEBUG backed up  one  byte,  displaying  the
address and current contents.  Note that it has changed from the original value
(41)  to  the value we typed in (40).   We then type in the correct  value  and
terminate by pressing the ENTER key.

     As you can see,  this can be very awkward,  especially where large amounts
of data are concerned.   Also, if you need ASCII data, you have to look up each
character  and enter its hex value.   Not easy,  to be sure.   That's where the
Replace  mod  of operation comes in handy.   Where the Display/Modify  mode  is
handy  for  changing a few bytes at various offsets,  the Replace mode  is  for
changing  several  bytes of information at one time.   Data can be  entered  in
hexadecimal or character format,  and multiple bytes can be entered at one time
without  waiting  for the prompt.   If you wanted to store the  characters  'My
name' followed by a hexadecimal 00 starting at location 103, you would enter:

     E 103 'My name' 0

As  in  the Search command,  data can be entered in character  (in  quotes)  or
hexadecimal  forms  and  can be mixed in the same command.   This is  the  most
useful way of entering large amounts of data into memory.

Fill

     The Fill command is useful for storing a lot of data of the same data.  It
differs  from  the Enter command in that the list will be  repeated  until  the
requested amount of memory is filled.  If the list is longer than the amount of
memory to be filled,  the extra items are ignored.   Like the Enter command, it
will  take hexadecimal or character data.   Unlike the Enter  command,  though,
large amounts of data can be stored without specifying every character.   As an
example, to clear 32K (8000h) of memory to 00h, you only need to enter:

     F 0 L 8000 0

Which translates into Fill, starting at DS:0000 for a Length of 32K (8000) with
00h.   If the data were entered as '1234',  the memory would be filled with the
repeating string '123412341234',  etc.   Usually,  it is better to enter  small
amounts of data with the Enter command,  because an error in the length parame-
ter of the Fill command can destroy a lot of work.  The Enter command, however,
will  only change the number of bytes actually entered,  minimizing the effects
of a parameter error.

Move

     The Move command does just what it says - it moves data around inside  the
machine.   It  takes  bytes from with the starting address and moves it to  the
ending address.   If you need to add an instruction into a program,  it can  be
used  to make room for the instruction.   Beware,  though.   Any data or labels
referenced after the move will not be in the same place.   Move can be used  to
save a part of the program in free memory while you play with the program,  and
restore  it  at any time.   It can also be used to copy ROM BIOS  into  memory,
where it can be written to a file or played with to your heart's content.   You
can then change things around in BIOS without having to worry about programming
a ROM.

     M 100 L 200 ES:100

This  will  move the data from DS:0100 to DS:02FF (Length 200) to  the  address
pointed to by ES:0100.  Later, if we want to restore the data, we can say:

     M ES:100 L 200 100

which will move the data back to its starting point.   Unless the data has been
changed while at the temporary location (ES:0100),  we will restore the data to
its original state.

Assemble

I purposely left the Assemble command to the end,  as it is the most complex of
the  data entry commands.   It will take the instructions in the assembler lan-
guage and convert them to machine code directly.   Some of the things it  can't
do,  however,  are: reference labels, set equates, use macros, or anything else
which cannot be translated to a value.  Data locations have to be referenced by
the physical memory address, segment registers, if different from the defaults,
must be specified,  and RET instructions must specify the type (NEAR or FAR) of
return to be used.   Also,  if an instruction references data but not registers
(i.e.  Mov [278],5), the Byte ptr or Word ptr overrides must be specified.  One
other  restriction:   To tell DEBUG the difference between moving 1234h into AX
and  moving  the data from location 1234 into AX,  the latter is coded  as  Mov
AX,[1234],  where the brackets indicate the reference is an addressed location.
The differences between MASM and DEBUG are as follows:

     MASM                DEBUG                    Comments

     Mov  AX,1234        Mov  AX,1234             Place 1234 into AX
     Mov  AX,L1234       Mov  AX,[1234]           Contents of add. 1234 to AX
     Mov  AX,CS:1234     CS:Mov AX,[1234]         Move from offset of CS.
     Movs Byte ptr ...   Movesb                   Move byte string
     Movs Word ptr ...   Movsw                    Move word string
     Ret                 Ret                      Near return
     Ret                 Retf                     Far return

Also,  Jmp instructions will be assembled automatically to Short,  Near, or Far
Jmps.  However, the Near and Far operands can be used to override the displace-
ment if you do need them.  Let's try a very simple routine to clear the screen.

     -A 100
     6897:0100 mov ax,600
     6897:0103 mov cx,0
     6897:0106 mov dx,184f
     6897:0109 mov bh,07
     6897:010B int 10
     6897:010D int 20
     6897:010F
     -

     We  are using BIOS interrupt 10h,  which is the video interrupt.  (If  you
would like more information on the interrupt,  there is a very good description
in  the Technical Reference Manual.)  We need to call BIOS with  AX=600,  BH=7,
CX=0, and DX=184Fh.  First we had to load the registers, which we did at in the
first  four instructions.   The statement at offset 6897:010B  actually  called
BIOS.   The INT 20 at offset 010D is for safety only.  We really don't need it,
but with it in,  the program will stop automatically.   Without the INT 20, and
if  we did not stop,  DEBUG would try and execute whatever occurs at 010F.   If
this  happens  to  be a valid program (unlikely),  we would  just  execute  the
program.   Usually,  though,  we will find it to be invalid,  and will probably
hang the system,  requiring a cntl-alt-del (maybe) or a power-off and on  again
(usually).  So, be careful and double check your work!

     Now, we need to execute the program.  To do this, enter the G command, a G
followed  by  the enter key.   If you have entered the program  correctly,  the
screen  will  clear and you will get a message "Program  terminated  normally".
(More on the Go command later).

     Again, I cannot stress the importance of checking your work when using the
Assemble  command.   The commands may assemble correctly,  but cause a  lot  of
problems.   This  is especially important for the Jmp and Call commands;  since
they  cause  an interruption in the flow of the program,  they  can  cause  the
program  to jump into the middle of an instruction,  causing VERY unpredictable
results.

                                 I/O commands

Name

The  Name  command has just one purpose - specifying the name of a  file  which
DEBUG is going to Load or Write.  It does nothing to change memory or execute a
program,  but does prepare a file control block for DEBUG to work with.  If you
are going to load a program,  you can specify any parameters on the same  line,
just  like in DOS.   One difference is,  the extension MUST be specified.   The
default is no extension.   DEBUG will load or write any file, but the full file
name must be entered.

     -n chkdsk.com /f

This statement prepares DEBUG for loading the program CHKDSK.COM passing the /f
switch to the program.   When the Load (see below) command is  executed,  DEBUG
will  load CHKDSK.COM and set up the parameter list (/f) in the program's input
area.

Load

     The Load command has two formats.  The first one will load a program which
has been specified by the Name command into storage, set the various registers,
and prepare for execution.   Any program parameters in the Name command will be
set into the Program Segment Prefix,  and the program will be ready to run.  If
the  file is a .HEX file,  it is assumed to have valid  hexadecimal  characters
representing  memory  values,  two hexadecimal characters per byte.  Files  are
loaded  starting at CS:0100 or at the address specified in the  command.    For
.COM.  .HEX and .EXE files,  the program will be loaded, the registers set, and
CS:IP  set  to the first instruction in the  program.   For  other  files,  the
registers are undetermined, but basically, the segment registers are set to the
segment of the PSP (100h bytes before the code is actually loaded),  and BX and
CX are set to the file length.  Other registers are undetermined

     -n clock.com
     -l

This  sequence  will load clock.com into memory,  set IP to the entry point  of
0100,  and CX will contain 0446, the hexadecimal size of the file.  The program
is now ready to run.

     The second form of the Load command does not use the Name command.   It is
used  to load absolute sectors from the disk (hard or soft) into  memory.   The
sector count starts with the first sector of track 0 and continuing to the  end
of the track.   The next sector is track 0,  second side (if double sided), and
continues to the end of that sector.   Then,  back to the first side,  track 1,
and so on,  until the end of the disk.   Up to 80h (128d) sectors can be loaded
at  one time.   To use,  you must specify starting address,  drive  (0=A,  1=B,
etc.), starting sector, and number of sectors to load.

     -l 100 0 10 20

This instruction tells DEBUG to load, starting at DS:0100, from drive A, sector
10h  for 20h sectors.   DEBUG can sometimes be used this way to recover part of
the  information on a damaged sector.   If you get an error,  check the  memory
location  for that data.   Often times,  part of the data has been  transferred
before  the error occurs and the remainder (especially for text files)  can  be
manually entered.   Also,  repetitive retrys will sometimes get the information
into  memory.   This can then be rewritten on the same diskette (see the  Write
command below), or copied to the same sector on another diskette.  In this way,
the data on a damaged disk can sometimes be recovered.

Write

     The  write  command is very similar to the Load command.   Both  have  two
modes of operation, and both will operate on files or absolute sectors.  As you
have  probably guessed,  the Write command is the opposite of the Load command.
Since all the parameters are the same,  we will not cover the syntax in detail.
However,  one  thing worth mentioning:  When using the file mode of  the  Write
command,  the  amount of data to be written is specified in BX and CX,  with BX
containing the high-order file size.   The start address can be specified or is
defaulted to CS:0100.   Also, files with an extension of .EXE or .HEX cannot be
written  out,  and error message to that effect will be displayed.   If you  do
need  to  change a .EXE or .HEX file,  simply rename and  load  it,  make  your
changes, save it and name it back to its original filename.

Input

     The  Input command can be used to read a byte of data from any of the  I/O
ports  in  the PC.   The port address can be either a one or two byte  address.
DEBUG will read the port, and display the contents.

     -i 3fd
     7D
     -

This is the Line input port for the first Asynchronous adapter.   Your data may
be different,  as it depends on the current status of the port.   It  indicates
the  data  in the register at the time it was read was 7Dh.   Depending on  the
port, this data may change, as the ports are not controlled by the PC.

Output

     As you can probably guess,  the Output command is the reverse of the Input
command.   You  can use the Output command to send a single byte of data  to  a
port.   Note  that certain ports can cause the system to hang (especially those
dealing with system interrupts and the keyboard),  so be careful with what  you
send where!

     -o 3fc 1
     -

Port 3FCh is the modem control register for the first asynchronous port.  Send-
ing a 01h to this port turns on the DTR (Data Terminal Ready) bit.   A 00h will
turn all the bits off.   If you have a modem which indicates this bit,  you can
watch the light flash as you turn the bit on and off.

                              EXECUTION COMMANDS

Go

     The  Go  command  is used to start program execution.   A  very  versatile
command, it can be used to start the execution at any point in the program, and
optionally  stop  at any of ten points (breakpoints) in  the  program.   If  no
breakpoints  are set (or the breakpoints are not executed),  program  execution
continues  until  termination,  in which case the message  "Program  terminated
normally" is sent.   If a breakpoint is executed,  program execution stops, the
current registers are displayed, and the DEBUG prompt is displayed.  Any of the
DEBUG commands can be executed, including the Go command to continue execution.
Note  that the Go command CANNOT be terminated by Cntl-break.   This is one  of
the few commands which cannot be interrupted while executing.

     -g =100

The  Go command without breakpoints starts program execution at the address (in
this  case  CS:0100)  in the command.   The equal sign before  the  address  is
required.   (Without the equal sign, the address is taken as a breakpoint.)  If
no starting address is specified,  program execution starts at CS:IP.   In this
case,  since  no breakpoints are specified,  CLOCK.COM will continue  execution
until the cntl-break key is pressed and the program terminates.   At this time,
you will get the message "Program terminated normally".   Note that,  after the
termination  message,  the  program should be reloaded before  being  executed.
Also,  any memory alterations (storing data,  etc.) will not be restored unless
the program is reloaded.

     -g 276 47c 528 347

This version of the control command will start the program and set  breakpoints
at  CS:276,  CS:47C,  CS:528  and  CS:347.   These correspond to  locations  in
CLOCK.COM  after the screen is cleared,  and the day,  date and time  are  dis-
played,  respectively.   The  program will stop at whichever breakpoint it hits
first.   Note  that the second and third breakpoints will only be displayed  at
two times - when the program is started and at midnight.   If you care to  stay
up (or just change the time in the computer),  and set a breakpoint at 47C,  t
will stop when the program is started, and again at midnight.

     Some notes about breakpoints. The execution stops just before the instruc-
tion is executed.  Setting a breakpoint at the current instruction address will
not execute any instructions.  DEBUG will set the breakpoint first, then try to
execute the instruction, causing another breakpoint.  Also, the breakpoints use
Interrupt  3 to stop execution.   DEBUG intercepts interrupt 3 to stop the pro-
gram execution and display the registers.   Finally,  breakpoints are not saved
between Go commands.  Any breakpoints you want will be have to be set with each
Go command.

Trace

     Along the same lines as Go is the Trace command.   The difference is that,
while Go executes a whole block of code at one time, the Trace command executes
instructions  one at a time,  displaying the registers after each  instruction.
Like the Go instruction,  execution can be started at any address.   The  start
address again must be preceeded by an equal sign.   However,  the Trace command
also has a parameter to indicate how many instructions are to be executed.

     -t =100 5

This Trace command will start at CS:100 and execute five instructions.  Without
the  address,  execution will start at the current CS:IP value and continue for
five instructions.  T alone will execute one instruction.

     When using Trace to follow a program, it is best to go around calls to DOS
and interrupts,  as some of the routines involved can be  lengthy.   Also,  DOS
cannot be Traced,  and doing so has a tendency to hang the system.   Therefore,
Trace  to  the call or interrupt and Go to the next address after the  call  or
interrupt.

                              ARITHMETIC COMMANDS

Hexarithmetic

     The  Hexarithmetic command is handy for adding and subtracting hexadecimal
numbers.  It has just two parameters - the two numbers to be added and subtrac-
ted.   DEBUG's response is the sum and difference of the numbers.   The numbers
can be one to four hexadecimal digits long.   The addition and subtraction  are
unsigned, and no carry or borrow is shown beyond the fourth (high order) digit.

     -h 5 6
     000B FFFF
     -h 5678 1234
     68AC 4444
     -

In  the  first example,  we are adding 0005 and 0006.   The sum  is  000B,  the
difference  is -1.   However,  since there is no carry,  we get FFFF.   In  the
second example, the sum of 5678 and 1234 is 68AC, and the difference is 4444.

                                    WRAPUP

     If  you give it a chance,  DEBUG can be a very useful tool for the  IBMPC.
It  is  almost a requirement for debugging assembler language programs,  as  no
nice  error  messages are produced at run time.   DEBUG does work at  the  base
machine  level,  so you need some experience to use it  effectively,  but  with
practice, it will be your most useful assembler language debugging tool.


FreeDOS command: debug

Command: debug

  DEBUG is a clone of the MS-DOS DEBUG command. Some minor things aren't
  implemented. DEBUG does not only support the 8086/8088 and 8087 registers
  and instruction sets but also the 32bit register and the instruction
  sets till PentiumPro CPU - except the MMX extensions.
  DEBUG is a relatively advanced DOS tool but has many uses. For instance,
  it can be used to:
    * find bugs in programs,
    * create programs from assembly code,
    * inspect and edit binary files.

Syntax:

  DEBUG [[drive:][path]filename [arglist]]
        [drive:][path]filename  Specifies the file you want to test. It
                                doesn't have to be an executable - it
                                could be a binary or text file.
                                Since it can be any file, the file
                                extension has to be specified.
        arglist                 Specifies command-line information
                                required by the file you want to test.
                                See Program Execution below for more
                                information on running executables.
  Once Debug is started, the Debug command prompt is displayed. Enter Q to
  quit Debug, and ? to display the list of Debug commands.

[Main menu] [top] (Syntax) [Options] [Comments] [Examples] [See also] [File]

Options:

  Q                     Quit
  ?                     Help

  Inspecting and Editing Memory:
    COMPARE:
      C range address   Compare two memory ranges, displaying the
                        differences. Only the starting address of the
                        second range is required, Debug will assume the
                        second range will be the same size as the first.
                        For instance:
                          C 100,110 200
                        will compare the range 100,110 to 200,210.
    DUMP:
      D [range]         Display the contents of a memory range. If no
                        range is specified, dump will display 128 bytes,
                        starting where the last dump finished or at the
                        beginning of the loaded file.
      DM                Display the DOS memory control block (MCB) chain
                        and the current PSP.
    ENTER:
      E address [list]  Write over part of memory, beginning at address,
                        with the list of bytes. To enter a string of text
                        characters, enclose the text in double quotes.
                        For instance:
                          E 100 "Hello!"
    FILL:
      F range list      Write over a memory range with a repeating list
                        of bytes. For instance, to set a memory range to
                        zero, you might put
                          E 100,110 0
                        and to set a memory range to "brumbrum":
                          E 100,107 "brum"
    MOVE:
      M sourcerange destaddress
                        Copies the range sourcerange into another range
                        that begins at destaddress.
    SEARCH:
      S range list      Searches for all occurences of the list of bytes
                        in the specified range. The starting address of
                        each occurence is displayed.

  Calculating Numbers in Hexadecimal:
    HEX
      H value1 value2   Displays two numbers, the first being
                        value1+value2, the second being value1-value2.
                        H can handle dword values.

  Loading and Saving Files:
    LOAD PROGRAM OR FILE:
      L [address]       Load program.
      L address drive sector count
                        Load sectors.
    SET NAME:
      N [[drive:][path]progname [arglist]]
                        Set the name for the current file/program.
    WRITE PROGRAM OR FILE:
      W [address]       Write the current program/file to disk. Use N to
                        change the name if you do not wish to overwrite
                        the original file.
      W address drive sector count
                        Write sectors.

  Assembling and Disassembling:
    ASSEMBLE:
      A [address]       Assemble.
    UNASSEMBLE:
      U [range]         Unassemble.
    80x86 MODE:
      M[setting]        The current processor (for the purposes of
                        assembler and disassembler warnings, above) can
                        be declared to be something else via the following
                        commands:
                          m0  Declare current processor to be an 8088
                          m1  Declare 80186
                          m2  Declare 286
                          m3  Declare 386
                          m4  Declare 486
                          m5  Declare Pentium
                          m6  Declare Pentium Pro
      MC                Declare the existence of a math coprocessor.
      MNC               Declare the absence of a math coprocessor.
      MC2               Declare that the math coprocessor is a 287
                        (only valid if the current processor is a 386)
      M?                Print currently declared processor type

  Program Execution:
    GO:
      G [=address] [breakpoints]
                        Begin execution of the loaded program or the
                        machine code at the specified address.
                        Execution is suspended at the breakpoint
                        addresses, allowing you to inspect memory etc.
    PROCEED:
      P [=address] [count]
                        Proceed with execution in steps.
    REGISTER:
      R [register [value]]
                        Display all CPU registers, or display/set a
                        particular register. Accepts 32-bit register
                        names (for cpu 80386+).
      RN                Displays FPU register status (currently for cpu
                        80386+ only). Just the raw hex values of the
                        registers are displayed, though.
      RX                Switches among 8086 and 80386 register display
                        (for cpu 80386+).
    TRACE:
      T [=address] [count]
                        Trace
      TM [0|1]          Set trace command mode, 0=process INTs (default),
                        1=single-step INTs (the MS-DOS Debug compatible
                        mode).

  Port I/O:
    INPUT:
      I port            Input from a port
      IW port           Input from a WORD port
      ID port           Input from a DWORD port
                        DWORD values require a 80386+ cpu.
    OUTPUT:
      O port value      Output to a port
      OW port value     Output to a WORD port
      OD port value     Output to a DWORD port.
                        DWORD values require a 80386+ cpu.

  Expanded memory (EMS) commands (only if an EMM v4.0 is present):
    XA count            Allocate an 'empty' EMS handle with zero pages.
    XD handle           Deallocate.
    XM logical-page physical-page handle
                        Map memory.
    XR handle count     Reallocate an EMS handle.
    XS                  Show status.
    X?                  Show help about X commands.

[Main menu] [top] [Syntax] (Options) [Comments] [Examples] [See also] [File]

Comments:

  DEBUG uses hexadecimal (base 16). Memory addresses are displayed in
  segment:offset form, e.g. 0D00:0100. The last four hexadecimal digits,
  which specify the offset from the segment, can go from 0000 to FFFF,
  a range of 64 Kilobytes.
  Executables with the .com extenstion have all their data and code in
  one segment. Hence for .com files, you can usually just write the offset
  when specifying memory addresses in DEBUG commands. DEBUG will assume
  the program segment. The same is also true of text/binary files that
  are < 64K long.
  To specify a memory range, seperate two addresses with a comma.
  Loading of .HEX files is not implemented in DEBUG.
  The assembler and disassembler support all publicly documented
  instructions for Intel chips through the Pentium Pro (P6), except for
  the MMX instructions. The assembler and disassembler inform you if
  any instruction is inappropriate for the current processor.
  FPU Opcodes are supported.

[Main menu] [top] [Syntax] [Options] (Comments) [Examples] [See also] [File]

Examples:

  Creating a backup of the bootsector (sector number 0 with 512 bytes
  =0x200) of drive C: (number 2) to the file BOOT_C.BIN on diskette A:
    A:
    cd \
    echo L 100 2 0 1 >BOOT_C.TXT
    echo RCX         >>BOOT_C.TXT
    echo 200         >>BOOT_C.TXT
    N BOOT_C.BIN     >>BOOT_C.TXT
    W                >>BOOT_C.TXT
    Q                >>BOOT_C.TXT
    DEBUG <BOOT_C.TXT

  Looking with DEBUG into the RAM of the graphics card to find some special
  characters such as year to identify the card:
    DEBUG
    S C000:0000,07FF "BIOS"
    S C000:0,7FF     "(C)"
    S c000:0,7ff     "199"
    s c000:0,7ff     "200"
    D C000:0000,100
    Q

[Main menu] [top] [Syntax] [Options] [Comments] (Examples) [See also] [File]

See also:

  emm386
  jemm386
  undelete

[Main menu] [top] [Syntax] [Options] [Comments] [Examples] (See also) [File]

File:

  Please read this command's lsm file also.
  You will find the updated version (internet) here and
  the version described in this manual page here.
  The lsm file contains information about the name of the programmer,
  the download site, and some other command related information.

[Main menu] [top] [Syntax] [Options] [Comments] [Examples] [See also] (File)

  Copyright © 2003 Robert Platt, updated 2008 by W. Spiegl.

  This file is derived from the FreeDOS Spec Command HOWTO.
  See the file H2Cpying for copying conditions.