l'assembleur  [livre #4061]

par  pierre maurette


MASM
/
MASM32

Pour détailler l'utilisation d'un assembleur autonome, nous avons décidé de nous appuyer sur MASM, officiellement Microsoft Macro-Assembleur , dans ses versions ml.exe  6.15.8803 et 7.00.9254. Pour les tests, la première a parfois été renommée ml615.exe , la seconde ml7.exe . c'est sous ce nom qu'ils peuvent apparaître de temps en temps dans les divers fichiers du CD-Rom.

Pourquoi MASM, plutôt que TASM, MASM32, NASM, Paradigm ? Parce que ses règles de langage et de syntaxe sont une référence de fait. Nous avons déjà abordé ce point.

TASM peut fonctionner en mode de compatibilité MASM, en plus du mode IDEAL qui lui est propre. Paradigm est un clone de TASM.

MASM32 est un kit bâti autour de MASM, en version 6.14 au moment de la rédaction de ces lignes.

Les produits libres comme NASM non seulement sont accompagnés de leur propre documentation, mais celle-ci utilise souvent MASM comme base d'explication, ou alors un chapitre est consacré à la conversion de sources MASM vers la nouvelle syntaxe. Ces derniers points s'appliquent également au mode IDEAL de TASM, présenté généralement à partir de ses améliorations, bien réelles, par rapport à MASM.

Nous venons à nouveau d'évoquer des défauts pour MASM. Nous n'y reviendrons pas, ils n'empêchent en rien une programmation efficace. Il s'agit essentiellement d'un certain laxisme de l'assembleur quand il doit interpréter les constructions d'un code source. C'est la raison pour laquelle nous insisterons particulièrement, lourdement même, sur l'utilisation qui devrait au début être systématique des fichiers listing  .lst .

Nous avons bâti cette présentation sur une série de chapitres thématiques, qui sont bien autre chose qu'une référence des commandes. D'un autre côté, il n'est pas possible, ou alors pour faire un exercice de style, de concevoir une série de chapitres dans laquelle seul le contenu déjà vu serait nécessaire à chaque étape de la progression.

Comme pour le microprocesseur avec tel ou tel document d'Intel, une référence existe, d'origine Microsoft et en langue anglaise. Il est possible de se la procurer sur internet.

Les thèmes traités couvrent une grande partie des besoins d'un programmeur. Il y aura fatalement des manques, des sujets sur pour lesquels vous serez renvoyés à la documentation externe. À une couverture moyenne de l'ensemble, c’est-à-dire au clonage de la documentation officielle, nous avons préféré traiter de façon détaillée le principal, en mêlant concepts généraux utiles hors MASM, et même hors assembleur, référence du langage, méthodologie d'apprentissage à base de tripatouillage, de je_teste_tout_et_je_mets_le_bout_de_code_de_côté .

Ainsi, les diverses parties de ce chapitre resteront disponibles en tant que référence, mais cette bibliothèque de petits programmes jouera un rôle de complément.

Au sujet des exemples, vous en trouverez sur le CD-Rom, classés par chapitre, et qui parfois ne sont pas annoncés dans le texte. À l’inverse, tel ou tel bout de code précis, de trois ou cinq lignes, pourra ne pas se trouver sur le fichier du CD-Rom. À cela, une raison : tous ont été testés, un certain ménage aura été fait à l'issue de la rédaction des chapitres, mais les essais se font souvent par petites modifications, mise en commentaire, etc. Donc, si quelques lignes manquent de temps en temps, il vous suffira de les saisir ou même de simplement modifier un exemple similaire.

Avant de se lancer dans l'étude d'un sujet, il est important de s'assurer que l'assemblage se déroule correctement sur le fichier initial, dans une configuration confortable dont voici un exemple tout simple mais efficace : code source, fichiers batch, fichiers listing ouverts dans un bon éditeur, une ou deux fenêtres Invite de commandes prêtes à assembler et tester, lancer DEBUG, etc.

Commençons par le commencement, installation et configuration.

10.1 Installer et configurer MASM et MASM32

MASM désigne non pas un programme, mais un ensemble d'outils, bâti autour essentiellement du ou des assembleurs ml.exe et du ou des lieurs link.exe . Depuis, semble-t-il, une version 6.1x, MASM n'est plus disponible à la vente en tant que produit complet, accompagné de sa documentation. Cet assembleur n'est plus considéré comme un produit de développement commercial, mais plus comme un outil nécessaire, entretenu de façon minimale mais suffisante. Cet état de fait présente l'avantage de rendre le produit librement accessible, mais complique notre tâche, puisqu'il faut partir à la chasse aux éléments et ensuite les installer, c’est-à-dire le plus souvent les recopier dans des dossiers correctement choisis.

La principale source d'approvisionnement était jusqu'à récemment les divers DDK (Drivers Development Kit), en accès libre sur le site de Microsoft. La politique de l'éditeur concernant ces kits a changé et nombre de liens fournis sur internet sont devenus obsolètes. Signalons à ce sujet que les auteurs de ces pages de liens préfèrent souvent conserver ces liens morts en toute connaissance de cause. Les divers SDK/DDK existent toujours, mais les modalités pour se les procurer ont changé.

Des liens précis ou des liens vers des pages de liens sont disponibles sur le CD-Rom. De plus, un moteur de recherche comme Google fournit également de bons renseignements.

Si vous hésitez sur une version à installer, il est fortement conseillé de télécharger (MASM32v8.ZIP, environ 3 Mo) et d'installer l'ensemble MASM32. Vous aurez ainsi tout ce qu'il faut pour travailler immédiatement, y compris de la documentation et les fichiers  .lib et .inc nécessaires. Nous y reviendrons dans le chapitre consacré à la programmation Windows.

Dans MASM32 version 8, la version de l'assembleur ml.exe est la 6.14.8444, celle du lieur link.exe la 5.12.8078. Il existe deux versions plus récentes de l'assembleur, très proches de cette 6.14 : une 6.15.8803, incluse dans le Visual Studio Processor Pack, toujours intéressant à télécharger, ne serait-ce que pour la documentation et la 7.0 de Visual Studio .NET. Mais, répétons-le, MASM32 v. 8 est tout à fait suffisant. Ces deux versions sont également plus récentes que celles fournies avec MASM d'origine Microsoft.

Il existe deux situations pour lesquelles vous pouvez avoir besoin d'autres programmes que ceux fournis avec le kit MASM32, situations qu'il ne faut pas confondre :

  Pour générer un exécutable DOS 16 bits, il faut utiliser le lieur segmenté link.exe (dernière version 5.60.339) ou éventuellement tout autre lieur DOS compatible.

  Le problème est différent pour l'assembleur ml.exe  : à partir de la version 6.12, il s'agit d'une application console 32 bits. Attention, ceci n'a rien à voir avec le code objet généré. La dernière version tournant en environnement DOS 16 bits est la 6.11d. Les situations débouchant aujourd'hui sur ce besoin sont certainement rares.

Nous verrons comment intégrer les outils nécessaires pour assembler du code 16 bits à partir d'une installation MASM32, en fin de chapitre.

Dans les versions antérieures à 6.x de MASM, l’assembleur portait le nom de masm.exe . Dans le seul but de pouvoir réutiliser les makefiles des anciens projets, les versions 6.x de MASM sont fournies avec un exécutable masm.exe , qui n’est qu’un simple traducteur (MASM Compatibility Driver Version 6.11), qui appelle ml.exe en adaptant les paramètres de la ligne de commandes.

Reste le problème de la documentation. Cet ouvrage fournit une réponse, espérons-le suffisante. Se posera néanmoins le problème des versions plus récentes. Les dernières vendues étaient accompagnées d'un ensemble de quatre ouvrages, dont il circule des versions .doc ou .pdf certainement peu légales. Depuis, peu de choses ont évolué, au plan de la syntaxe et de la philosophie du produit. Il suffit de vérifier les points de détail qui peuvent avoir changé.

La première chose à faire est de consulter l’aide en ligne de commandes, en saisissant par exemple ml /? ou ml /? >h.txt pour récupérer la sortie dans un fichier texte. Nous obtenons ainsi un résultat strictement identique pour les versions 6.15 et 7.0.

Ensuite, tout dépend de la façon dont la nouvelle version a été approvisionnée. Pour la 7.0 de Visual Studio .NET, l’aide en ligne contient une rubrique Microsoft Macro Assembler Reference . Pour la 6.15, le Visual Studio Processor Pack inclut un ficher MasmRef.doc . Les deux décrivent, de la même façon brute mais exhaustive, options en ligne de commandes, messages d’erreur, directives, opérateurs et symboles, pour ml.exe et éventuellement pour tel ou tel utilitaire. Les principales différences visibles sur la version 7.0 concernent les directives de type de processeur, ce qui n’a rien d’étonnant.

Si vous ne passez pas par une procédure d'installation, vous pourrez vous inspirer de ce qui suit, ainsi que de ce qui a été écrit sur le sujet au chapitre sur les assembleurs autonomes, pour procéder manuellement. Il n'est pas nécessaire de chercher à tout prix à innover et la structure de dossiers habituelle conviendra : créez un dossier nommé MASM par exemple, ou DEV si vous centralisez les outils de plusieurs langages pour partager ceux, nombreux, qui sont communs. Ensuite, créez une série de sous-dossiers, qui peuvent être BIN (les exécutables de base), INCLUDE (pour les .inc , mais aussi les .h s'il y a lieu), LIB , et tout ceux qui vous semblent utiles pour structurer la chose, DOC , TOOLS ou OUTILS , SOURCES (pour les sources des .lib ), etc.

Il est tout à fait pertinent de partir d'une installation existante (Visual Studio, C++Builder, C++ 5.50) et d’ajouter simplement les fichiers nécessaires dans les dossiers adéquats. Le dernier nommé, C++ 5.50, un compilateur C++ en lignes de commande gratuit et présent sur le CD-Rom, serait une bonne solution, mais plus dans le cas d'assembleurs de la famille TASM.

Nous allons maintenant décrire deux installations, celle de MASM 6.11 et celle de MASM32.

10.1.1 Une installation de MASM 6.11

Bien que non distribué depuis maintenant un temps certain, il n'est pas impossible, le produit ayant été relativement répandu à une certaine époque, que vous ayez la chance d'en posséder encore une copie complète. Les bennes à ordures de certains organismes de formation sont un bon territoire de chasse, histoire vécue ! Seul problème dans ce cas, trouver une des dernières versions 6.1x, contenant 5 disquettes 3"1/2 1.44MB encore lisibles. Si vous avez la chance d'être dans cette situation, faites immédiatement une copie des cinq disquettes de distribution.

Nous allons installer ensemble MASM 6.11 à partir de cette copie sur disque dur, puis appliquer un patch de mise à jour vers la version 6.13. Celui-ci a été chargé sur internet, il s’appelle ml613 . Quoi qu’il en soit, c’est dans cette arborescence d’installation que nous copierons les divers composants de MASM, au fur et à mesure de l’apparition de nouvelles versions. Pour cette raison, nous allons nommer notre dossier d’installation tout simplement MASM .

Si vous ne possédez pas cette distribution telle quelle, vous pouvez vous inspirer de ce qui suit pour créer une arborescence analogue et y copier les fichiers utiles.

L’installation sera décrite sous XP, qui est l’environnement le plus éloigné de l’environnement cible de ce package. Qui peut le plus peut le moins, tout doit fonctionner de la même façon sous d’autres versions de Windows. L'installation a d'ailleurs été faite dans une partition partagée, MASM sera utilisé à partir de Windows 2000 et Windows 98 sans problème.

Ce fait est logique, puisqu'en fait d’installation, il s’agit plutôt d’une copie après décompression, et c’est bien ce que nous recherchons.

Le dossier MASM occupera aux alentours de 8 Mo. Il est bien clair que, au moment de faire des choix, la place occupée sur le disque ne sera pas un critère déterminant, alors que la volonté de ne pas perturber son système d’exploitation en sera un.

Considérons le contenu de la première disquette d’installation dans …\\masm6\disque1 et lançons par un double-clic setup.exe . Laissons passer l’écran de bienvenue et nous arrivons sur l’écran figurant sur l’illustration.

Début d’installation
figure 10.01 Début d’installation

Nous allons bien entendu appuyer sur  Entrée  pour continuer l’installation avec choix, non sans avoir remarqué, à l’avant-dernière ligne et à la précédente, la possibilité de consulter la liste des fichiers du package et celle d’extraire chacun d’eux individuellement, ce qui pourra être utile ultérieurement.

Nous passons par une série de choix ; nous avons opté pour les suivants.

Étapes de l’installation
figure 10.02 Étapes de l’installation

Ce qui est ici appelé Windows est la version 3. Les choix que nous avons faits installent un maximum de fichiers.

PWB est l’atelier intégré, qui permet d’accéder à l’aide tout en éditant, compilant et testant les programmes. Il est préférable de l’installer même sans envisager de l’utiliser.

N’installez pas de driver de souris, car XP ou 98 s’occupe de ce point. masm.exe étant proposé pour des raisons de compatibilité, le copier ne peut pas nuire. Enfin, nous installons les fichiers d’aide et les exemples.

Nous choisissons d’installer sur  C: , ensuite nous éditons le dossier de masm611 en masm , pour le premier dossier proposé, bin . Cette modification n’est à faire que pour ce dossier, les autres suivront. L’installation se poursuit ; il suffit de confirmer tous les choix puis de quitter.

Il reste à installer éventuellement le patch vers 6.11d ou 6.13. Un patch contient généralement une nouvelle version de ml.exe , plus éventuellement de nouveaux fichiers textes et utilitaires. Si vous récupérez ml.exe d’une autre façon, il faut copier au minimum ce fichier, ainsi que celui de messages d’erreurs ml.err . MasmRef.doc est également utile.

Pour des raisons de gestion de la mémoire étendue, la version 6.13 nécessite au minimum Windows NT ou 95 pour fonctionner, ce qui ne devrait pas poser de problème, sauf si vous tenez à booter en DOS vrai pour développer. Mais, dans ce cas, il aurait été préférable de faire l’installation sous DOS. Précisons que cette limitation n’empêche pas 6.13 de générer des applications DOS. Pour être exact, la 6.13 va s’exécuter dans une fenêtre DOS de Windows à partir de 95.

Le fichier ml613.exe n’est qu’un fichier compressé auto-décompactable. Décompactons-le donc dans un dossier ml613 par exemple et commençons la mise à jour. Nous menons cette opération sous Windows, et non en ligne de commandes.

Il est conseillé dans le readme.txt de sauvegarder quelques fichiers ( ml.exe , ml.err , h2inc.exe , h2inc.err et win.inc ). Cela est facultatif, puisque nous savons les récupérer à partir de la 6.11.

Copions patch.exe , patch.rtd et patch.rtp dans c:\masm\ , puis lançons patch.exe , en fenêtre DOS si nous souhaitons pouvoir lire les résultats. Nous pouvons effacer les trois fichiers patch copiés. Il reste à copier h2inc.exe et h2inc.err dans le dossier bin et win.inc dans le dossier include .

Vous pouvez renommer readme.txt en readme613.txt , puis le copier dans la racine du dossier d’installation ( c:\masm\ dans notre cas). Copiez également les autres fichiers textes, en remplacement de ceux existant.

Avant de faire le ménage, pensez à recopier la documentation dont vous disposez dans le dossier d’installation ou à tout autre endroit où vous avez pour excellente habitude de la centraliser. Dans ce dernier cas, renommez-le de façon explicite, Docs_masm_6 par exemple.

Vous pouvez maintenant effacer les dossiers \\masm6 et ml613 . Cette installation n’a modifié aucun fichier système, ni la base de registre bien entendu. Vous pouvez éventuellement zipper et archiver le dossier c:\masm complet, à une étape où tout fonctionne correctement. Un peu plus tard serait le mieux.

Si le programme d’installation n’a pas modifié le système, c’est parce qu’il utilise une autre très bonne méthode pour arriver au même résultat. Il a fabriqué un certain nombre de fichiers ; libre à nous de les utiliser. Ces fichiers se trouvent dans le dossier binr , à l’exception de tools.pre , qui est dans init .

Le fichier new-vars.bat , prévu pour modifier autoexec.bat , contient :

SET PATH=C:\MASM\BIN;C:\MASM\BINR;%PATH%
SET LIB=C:\MASM\LIB
SET INCLUDE=C:\MASM\INCLUDE
SET INIT=C:\MASM\INIT
SET HELPFILES=C:\MASM\HELP\*.HLP
SET ASMEX=C:\MASM\SAMPLES
SET TMP=D:\WINDOWS\TEMP

Nous allons l’utiliser pour modifier non pas notre autoexec.bat (nous sommes sous XP, et de plus nous n’aimons pas ça), mais un fichier masmxp.bat , comme suit :

PATH = %SystemRoot%\system32
 
SET PATH=C:\MASM\BIN;C:\MASM\BINR;%PATH%
SET LIB=C:\MASM\LIB
SET INCLUDE=C:\MASM\INCLUDE
SET INIT=C:\MASM\INIT
SET HELPFILES=C:\MASM\HELP\*.HLP
SET ASMEX=C:\MASM\SAMPLES
SET TMP=D:\WINDOWS\TEMP
 
%SystemRoot%\system32\cmd.exe

Élaborez dès maintenant le fichier masm.bat , adapté à vos dossiers et à votre version de Windows.

Il sera ensuite possible de créer des raccourcis vers ce fichier, pour par exemple lancer la session en mode Plein écran et de le modifier légèrement pour lancer directement l’atelier : il suffit de remplacer la dernière ligne par pwb ou par qh , pour lancer l’aide, une fois celle-ci configurée.

new-conf.sys comporte des lignes à ajouter éventuellement à un fichier config.sys . Il ne nous intéresse pas dans notre configuration.

new-sys.ini sert éventuellement à modifier le fichier system.ini . Gardons-le au frais pour l’instant. Il est utile si nous souhaitons exploiter cvw , une version Windows de CodeView, mais qui semble ne pas vouloir fonctionner sur les versions actuelles.

Le fichier tools.pre , dans le dossier init , sera simplement renommé tools.ini . Cela va permettre aux utilitaires d’environnement de retrouver les fichiers nécessaires à leur fonctionnement. Après avoir ouvert une fenêtre par le batch fabriqué au paragraphe précédent, testez qh , avant et après le renommage de tools.pre .

Vous pouvez maintenant découvrir qh et pwb . avant d’aborder la première application, essayez par exemple la séquence suivante :

Lancez masm.bat , puis qh . Cliquez sur <Contents> , puis sur <Assembly> dans le rectangle Languages . Repérez le rectangle System Resources pour un usage ultérieur, puis cliquez sur <BIOS Calls> , <13h-Direct Disk Services> et enfin 02h Read Sectors .

La fonction Read Sector dans Quick Help
figure 10.03 La fonction Read Sector dans Quick Help

Une fois les fonctions BIOS repérées, vous pouvez revenir quelques étapes en arrière, pour explorer les fonctions  <MS‑DOS Calls> , à la recherche des fonctions de gestions de fichiers.

Si vous disposez de cette aide, elle sera utile. En revanche, il est certain que l’utilisation de pwb risque de nous apporter des déboires, à cause de sa vétusté, si nous utilisons une version de Windows à partir de la 95. Il est préférable d'utiliser, pour un plus grand confort, un éditeur indépendant et surtout exploiter les possibilités de multitâche de Windows.

10.1.2 Présentation et installation de MASM32

Rappelons que l'ensemble MASM32 ne permet pas tel quelle la programmation DOS 16 bits. C'est d'ailleurs pratiquement son seul inconvénient majeur. Nous proposerons un bricolage fonctionnel à la fin de cette rapide prise en main.

C'est par contre une solution extrêmement confortable, parce que complète en une seule installation, pour aborder la programmation Windows. Sans être exhaustif et sans affirmer non plus qu'il n'existe pas de solution concurrente, voyons quelques goodies de MASM32 :

  Ensemble complet et cohérent pour faire du développement 32 bits, jusqu'à l'éditeur de code.

  Intégration des bibliothèques et fichiers d'en-tête nécessaires pour du développement Windows immédiat.

  L'environnement permet de fonctionner en syntaxe MASM pure.

  Installation efficace, peu contraignante et évolutive, nous allons y revenir.

  Bibliothèques propres à la distribution fort utiles, fournies avec code source.

  Une grande quantité de documentation et tutoriaux inclus en un seul téléchargement.

  Produit suffisamment populaire pour générer une bonne activité sur internet.

  Embryon d'activité et de documentation en langue française.

L'auteur a découvert le projet MASM32 sur internet, n'y est absolument pas impliqué et donc n'a aucune possibilité d'en garantir la pérennité.

Les liens pour télécharger MASM32 sont fournis sur le CD-Rom. Une recherche Google sur ce mot clé tout simplement les fournit parmi les quelques premières réponses. Au moment de la rédaction de ces lignes, il est possible de télécharger les packages compressés : masm32v7.zip (environ 5,2 Mo) et masm32v8.exe (environ 3,1 Mo).

L'installation ne pose pas de problème particulier. Pour la version 7, install.exe peut très bien être lancé directement à partir du désarchiveur (WinRar ou WinZip). Pour la version 8, il suffit de lancer masm32v8.exe . En revanche, bien qu'il soit possible d'accéder au contenu masm32v8.exe à l'aide d'un désarchiveur, il ne faut pas tenter une installation personnalisée par ce moyen, un grand nombre d'éléments comme les librairies étant construits lors de l'installation.

Le seul choix proposé est la partition d'installation.

Installation de MASM32
figure 10.04 Installation de MASM32

Bien que le processus semble bloqué aux premiers instants, l'installation est assez rapide sur une machine récente. Le facteur global de décompression est assez impressionnant.

Donc, tout s'installe dans un dossier masm32 , à la racine de la partition choisie. Le nom du répertoire est imposé. Nous allons comprendre pourquoi. En cas d'installation simultanée de deux versions, 7 et 8 par exemple, il faudra donc choisir de le faire sur deux partitions différentes.

Rien ne s'installe en dehors de ce dossier, pas de raccourcis, la base de registre n'est pas modifiée, ni les variables d'environnement. Donc, MASM32 qui vient d'être installé ne doit pas interférer avec des installations antérieures d'autres environnements de développement dont certains programmes peuvent porter le même nom.

Il en découle qu'il n'existe pas de processus spécifique de désinstallation, il suffit d'effacer le dossier masm32 , après bien entendu avoir sauvegardé son travail personnel.

Autre conséquence, dans le cas où vous auriez plusieurs systèmes d'exploitation installés sur votre machine et le désir légitime de tester vos programmes sous ces divers OS, une seule installation suffira. Vous devriez même pouvoir le faire sur une partition qui ne porterait pas la même lettre dans tous les systèmes. Mais ce dernier point n'a pas été testé, les partitions aux noms incertains apportant souvent d'autres ennuis. Il reste une possibilité (improbable) que le processus d'installation code en dur le nom de la partition d'installation dans certains fichiers.

Enfin, sous réserve de ce dernier point, il serait possible de faire une archive du dossier masm32 juste après l'installation. Mais pour quel gain ?

Vous voilà face à une foule d'outils, d'exemples et de documentations diverses. Nous n'utiliserons pas de façon habituelle l'éditeur fourni avec cet environnement, et il vous appartiendra de le découvrir par vous-même si vous souhaitez l'adopter. Néanmoins, pour ceux qui sont fâchés avec la langue anglaise, nous allons ensemble effectuer une prise en main de niveau zéro. Ceci va également nous permettre de découvrir quelques fichiers qui nous seront utiles et qui semblent en première approche sous-documentés.

Nous considérons que MASM32 a été installé dans la partition C: . Avec l'Explorateur, allez dans C:\MASM32\EXAMPLE1\LISTBOX . Vous pouvez par curiosité double-cliquer sur listbox.exe . Ensuite, effacez les fichiers listbox.exe et listbox.obj .

Maintenant, dans C:\MASM32\ , vous pouvez lancer qeditor.exe . Vous aurez avantage, si vous pensez utiliser ce programme de façon habituelle, à créer un raccourci vers lui et le déposer par exemple sur le Bureau.

Par le menu File / Open , ouvrez le fichier C:\MASM32\EXAMPLE1\LISTBOX\listbox.asm .

QEditor
figure 10.05 QEditor

Vous pouvez constater que cet éditeur est de type SDI (Single Document Interface), c’est-à-dire qu'il ne peut ouvrir qu'un seul document. Pour travailler sur plusieurs documents simultanément, il faut ouvrir plusieurs instances de QEditor, ce qui peut être obtenu par File / New Instance ou tout simplement en le lançant plusieurs fois.

Étudiez le menu Project .

Le menu Project
figure 10.06 Le menu Project

Run Program ne fait rien, pas même de message d'erreur ou d'avertissement. Essayez Assemble & Link . Une fenêtre de type DOS apparaît.

Construction de LISTBOX.exe
figure 10.07 Construction de LISTBOX.exe

Les fichiers listbox.obj et listbox.exe ont été créés dans le dossier C:\MASM32\EXAMPLE1\LISTBOX . Nous pouvons maintenant relancer la commande Project / Run Program . Et le programme tourne effectivement.

Le programme LISTBOX
figure 10.08 Le programme LISTBOX

Voilà, vous pouvez maintenant vous lancer, si vous le souhaitez, à la découverte des menus Tools , Templates , Script et Help , ce dernier étant dans un premier temps le plus enrichissant. Réservé aux anglophones, rappelons-le encore.

Ce qui nous intéresse d'urgence, c'est la façon dont fonctionne le menu Project . Bizarrement, dans Help / Quick Editor Help , même en utilisant la fonction Rechercher , l'aide semble muette sur le sujet. Aide-toi, le ciel t'aidera. Une étude des fichiers  .ini ou une recherche des fichiers contenant par exemple Assemble ASM file , nous amène rapidement, dans le même dossier que qeditor.exe , à menus.ini , dont voici un extrait :

[&Project]
Compile &Resource File,\MASM32\BIN\Bres.bat {b}
&Assemble ASM file,\MASM32\BIN\Assmbl.bat {b}
-
&Link OBJ File,\MASM32\BIN\Lnk.bat {b}
Assemble && Link,\MASM32\BIN\Build.bat {b}
&Build All,\MASM32\BIN\Bldall.bat {b}
Run &Makeit.bat,makeit.bat
-
Console Link &OBJ File,\MASM32\BIN\Lnkc.bat {b}
&Console Assemble && Link,\MASM32\BIN\Buildc.bat {b}
Console Build &All,\MASM32\BIN\Bldallc.bat {b}
-
&Run Program,{b}.exe

L'interprétation en est immédiate. Le caractère  & précède la lettre de raccourci de l'élément de menu, exactement comme dans les fichiers de scripts de ressources. Le caractère  - seul sur une ligne de menu devient un séparateur. Enfin, {b}  est remplacé par le nom du fichier en cours d'édition, sans extension.

Il est ainsi facile d’ajouter ou de modifier un élément du menu Project , ainsi que Tools , Templates , Script et Help .

À l’inverse, et notre but était bien là, nous savons maintenant où trouver une série de fichiers batch, que nous allons modifier pour réutilisation dans l'environnement de notre choix. C’est une dizaine de fichiers .bat du dossier C:\MASM32\BIN .

Ces fichiers sont très simples à interpréter. Le principe de la modification sera de changer les références du type \masm32\bin\ml /c /coff "%1.asm" en ?:\masm32\bin\ml /c /coff "%1.asm" , où ?  est la lettre de votre partition d'installation. Il en est de même pour les fichiers batch particuliers de certains exemples. De la même façon, selon l'utilisation que vous ferez de MASM32 et de ces nombreux et excellents exemples, vous pourrez être amenés à modifier certaines lignes dans le code source :

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
 
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

en (par exemple) :

include c:\masm32\include\windows.inc
include c:\masm32\include\user32.inc
include c:\masm32\include\kernel32.inc
 
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib

Vous trouverez un grand nombre d'utilitaires, parfois avec code source en assembleur. Sans code source, mais tout à fait utile : dumppe.exe , utilisé par les fonctions Tools / Dis‑assemble EXE file et Tools / Dis‑assemble DLL de QEditor.

Les lignes qui suivent et terminent cette rapide découverte de MASM32 s'adressent à ceux parmi les lecteurs qui, séduits par MASM32, chercheraient toutefois à tester quelques programmes DOS, pour un effort minimal, en termes de téléchargement et d'installation.

Nous avons vu, en introduisant MASM, que l'outil nécessaire et souvent manquant pour obtenir des exécutables DOS 16 bits est le lieur segmenté. Chez Microsoft, nous avons cité la version 5.60.339 de link.exe , téléchargeable sous forme d'une archive lnk563.exe (voir CD-Rom). Nous avons extrait link.exe de cette archive et l'avons copié sous le nom link560.exe , dans le dossier C:\MASM32\BIN .

Ensuite, nous avons ajouté trois lignes au fichier menus.ini  :

...
Console Build &All,\MASM32\BIN\Bldallc.bat {b}
-
&DOS Construit,\MASM32\BIN\builallD.bat {b}
DOS Assemble,\MASM32\BIN\assmblD.bat {b}
DOS Lien,\MASM32\BIN\lnkD.bat {b}
-
&Run Program,{b}.exe
...

Ceci modifie le menu Project .

Menus supplémentaires
figure 10.09 Menus supplémentaires

Il nous reste à fabriquer et à sauver les fichiers batch dans le dossier C:\MASM32\BIN . Ceci nous donne :

@echo off
 
if exist "%1.obj" del "%1.obj"
if exist "%1.exe" del "%1.exe"
if exist "%1.lst" del "%1.lst"
if exist "%1.map" del "%1.map"
 
\masm32\bin\ml /Fl /Sa /Fm /Bl"\masm32\bin\link560.exe" %1.asm > \masm32\bin\asmbl.txt
 
\masm32\thegun.exe \masm32\bin\asmbl.txt
LEGENDE=Le fichier builallD.bat

Remarquez ici l'option /Bl NomFichier , qui permet de modifier le nom du lieur.

 

@echo off
 
if exist "%1.obj" del "%1.obj"
if exist "%1.exe" del "%1.exe"
if exist "%1.lst" del "%1.lst"
if exist "%1.map" del "%1.map"
 
\masm32\bin\ml /Fl /Sa /Fm /c %1.asm > \masm32\bin\asmbl.txt
 
\masm32\thegun.exe \masm32\bin\asmbl.txt
LEGENDE=Le fichier ASSMBLD.bat

 

 

@echo off
 
if exist "%1.exe" del "%1.exe"
 
\masm32\bin\link560.exe %1.obj;> \masm32\bin\lnk.txt
 
\masm32\thegun.exe \masm32\bin\lnk.txt
LEGENDE=Le fichier LNKD.bat

 

Pour un usage plus soutenu, ces trois fichiers demandent certainement à être améliorés ou personnalisés. Sous cette forme, ils fonctionnent et permettent de construire un petit programme d'essai, dans le dossier Test_DOS .

 

Les chapitres qui suivent dans cette troisième partie sont consacrés à l'étude du langage propre de MASM, c’est-à-dire de tout ce qui est syntaxe, méthodes de travail, notions, à l'exclusion de ce qui concerne directement le microprocesseur et son jeu d'instructions. De nombreuses notions vues à cette occasion dépasseront le seul cadre de cet assembleur particulier.

10.2 Le langage du macro-assembleur MASM

Comme pour tout langage informatique, le programmeur en assembleur doit obéir à un certain nombre de règles de syntaxe pour obtenir le résultat attendu.

Nous allons dans la suite de ce gros chapitre passer en revue des éléments syntaxiques de MASM, à l'exception notable des instructions du microprocesseur elles-mêmes, c’est-à-dire les couples mnémonique + opérande(s), longuement présentés par ailleurs. Nous détaillerons ces éléments syntaxiques de façon inégale, selon l'idée que nous nous faisons des difficultés de compréhension qu'ils peuvent susciter. Une grande partie de ces sujets ont une portée qui dépasse la simple utilisation de l'assembleur MASM. Vous trouverez sur le CD-Rom suffisamment de liens pour compléter au besoin votre documentation. Un domaine saute aux yeux pour ne pas avoir été abordé : la conduite d'un projet multifichier, voire multilangage. Ceci pour des raisons de place essentiellement. De plus, les autres langages de programmation sont très présents dans d'autres parties du livre, et la programmation d'une DLL, une forme de bibliothèque, et proposée dans un des derniers chapitres. Vous trouverez un exemple basique de ce type de projet sur le CD-Rom.

Un point sur lequel nous reviendrons souvent : la lecture de ces chapitres, comme par ailleurs celle de l'ensemble de l'ouvrage, ne peut pas être linéaire. Déjà, les options de la ligne de commandes décrites dès ce chapitre n'ont de sens qu'une fois connues nombre de notions développées plus tard. Vous aurez souvent besoin, en cours d'apprentissage, de consulter parallèlement plusieurs chapitres.

Nous allons maintenant voir la façon d'utiliser ml.exe , c’est-à-dire la liste des options de la ligne de commandes, ainsi que celles des lieurs. Puis nous jetterons un œil sur un listing pour en identifier les éléments importants. Enfin, nous présenterons une série de directives, options, commandes, opérateurs, etc. qui n'ont pas trouvé place dans les sous-chapitres thématiques qui suivent ou qui sont mieux regroupés ici en un seul endroit. Cette dernière partie n'a pas à être lue. Pas plus que la liste des options de la ligne de commandes ou que l'annuaire téléphonique. Il suffit de la parcourir pour savoir quoi y trouver.

10.2.1 Options de la ligne de commandes

Des essais sont sur le CD-Rom dans le dossier MASM_general . En fait d’essais, il s’agit de fichiers  .bat , .asm et autres qu’il vous appartiendra de modifier pour tester les options qui vous sembleront nébuleuses ou imprécises. Un exemple ? Pour tester les options /Fe et /Fo , nous pouvons tester successivement :

ml7 /omf /Fl /Sa squelet.asm
ml7 /omf /Fl /Sa /Fo objet /Fe execut squelet.asm
ml7 /omf /Fl /Sa /Fo objet.obj /Fe execut.exe squelet.asm
ml7 /omf /Fl /Sa /Fo objet.ooo /Fe execut.eee squelet.asm

Nous en concluons :

  Par défaut, MASM génère des fichiers portant le nom du source  .asm et les extensions .obj et .exe .

  Si un autre nom est donné sans extension, l’extension par défaut est ajoutée.

  Si une extension, celle par défaut ou non, est donnée, elle est conservée. L’extension par défaut n’est alors pas ajoutée (pas de fichier objet.obj.obj , ou execut.eee.exe ).

  Accessoirement, nous constatons que ces options concernent autant le lieur que l’assembleur et que les échanges d’informations se font correctement entre les deux.

Cette façon de travailler par expérimentation et de proposer des exemples à modifier est un choix constant dans cette partie du livre. Cette méthode est parfois presque indispensable :

ml615 /omf /Fl /Sa  squelet.asm
ml615 /Fl  /Sa  squelet.asm
ml7   /omf /Fl /Sa  squelet.asm
ml7   /Fl /Sa  squelet.asm

Les trois premières lignes fonctionnent, la quatrième génère le message d’erreur suivant :

Assembling: squelet.asm
squelet.asm(21) : error A2006: undefined symbol : DGROUP
squelet.asm(30) : error A2074: cannot access label through segment registers
squelet.asm(40) : error A2074: cannot access label through segment registers
squelet.asm(53) : warning A4023: with /coff switch, leading underscore required for start address : debut

Ce message est typique d’une erreur dans le format du fichier objet. Or, rien en première lecture de la documentation n’indique de différence sur ce plan entre les versions 6.15 et 7.0.

La grammaire BNF

La documentation Microsoft, qu'elle soit celle propre à MASM ou celle du MSDN proposée en ligne ou sur CD-Rom, fait largement appel à la grammaire BNF (Backus-Naur Form) pour décrire les symboles, directives, opérateurs de MASM. Nous nous en sommes inspirés, mais de très loin.

S'en inspirer pour simplifier, c'est bien, la respecter conduit à apprendre un nouveau langage. De plus, nous avons des contraintes typographiques et un léger désir de franciser.

Se réfugier derrière une telle modélisation conduit à ne pas expliquer. Si vous avez l'occasion de feuilleter les quelques pages où toute la syntaxe de MASM est décrite de cette façon, certainement exacte, vous comprendrez...

Retenons-en les quelques conventions suivantes : les doubles crochets [[ Param ]] indiquent une option, un paramètre non obligatoire. Généralement, l'italique comme Param indique un paramètre dont les définitions est à compléter, non terminal selon le vocabulaire BNF. La barre verticale | indique un choix exclusif, un choix inclusif (plusieurs options peuvent être choisies simultanément) étant noté plutôt [[ Param [[ Param ]] ... Un choix par défaut est repéré par (déf.).

 

Bon, ceci étant vu, revenons à notre ligne de commandes. ml.exe est appelé à partir de l'interpréteur de commande avec généralement une série d'options. Certaines de ces options concernent spécifiquement le lieur. Une particularité de l'assembleur de MASM, ml.exe , est d'invoquer par défaut le lieur link.exe en lui transmettant ces options, ainsi que celles qu'il aura élaborées en fonction du travail demandé.

Beaucoup de ces options, ou commutateurs, de la ligne de commandes sont précisées, annulées, complétées, par des options apparaissant dans le fil du code source.

La syntaxe générale d'appel est :

ML [ [ Options ]    NomFichier [ [ [ [ Options ] NomFichier ] ]... [ [ / l ink OptionsLieur ] ]

Les options de la ligne sont sensibles à la casse, c’est-à-dire qu’il est fait une différence entre majuscules et minuscules. Quand un paramètre, textuel ou numérique, suit un commutateur, un espace entre les deux n'est pas toujours accepté.

Les options ou commutateurs de la ligne de commandes de ML 7.0

Option

Effet

/AT

Autorise le support du modèle mémoire TINY et la génération de messages d'erreur en cas de violation du format .com . Ne remplace pas la directive .MODEL TINY .

/Bl nomfichier

Le lieur nomfichier sera appelé à la place de link.exe .

/c

Assemble sans lier.

/coff

Les fichiers objets sont au format COFF (Common Object File Format). Conseil : ne pas compter sur l'option par défaut qui semble varier selon les versions, toujours préciser /coff ou /omf .

/Cp

Conserve la casse (MAJUSCULES /minuscules) des identificateurs utilisateur dans les tables.

/Cu

Option par défaut. Convertit les identificateurs utilisateur en MAJUSCULES.

/Cx

Conserve la casse (MAJUSCULES /minuscules) des symboles externes et publics.

/D symbole [= val ]

Définit la macro symbole . Si val n'est pas renseigné, symbole est simplement défini sans valeur. Si val contient des espaces, il faut utiliser des guillemets. Utile pour l'assemblage conditionnel.

/EP

Envoie à STDOUT (l’écran) le listing sortie du préprocesseur passe 1.

/F hexnum

Positionne la taille de la pile à hexnum octets. Équivalent à /link /STACK : xxxx . Valeur en hexadécimal.

/Fe nomfichier

Nom du fichier exécutable, si différent de celui du fichier source. Le . exe est optionnel, il sera rajouté si absent.

/Fl [[ nomfichier ]]

Génère un fichier listing. Possibilité de donner un nom au fichier. Par défaut, pas de génération. Prioritaire sur toute directive.

/Fm [[ nomfichier ]]

Fait générer un fichier .map par le lieur.

/Fo nomfichier

Nom du fichier objet, si différent de celui du fichier source. Le .obj est optionnel, il sera ajouté si absent.

/Fpi

Génère des liens vers des fonctions émulées pour l’arithmétique en virgule flottante (uniquement multilangage).

/Fr [[ nomfichier ]]

Génère un fichier .sbr (à destination d’un inspecteur de symboles).

/FR [[ nomfichier ]]

Génère un fichier .sbr étendu (à destination d’un inspecteur de symboles).

/Gc

Spécifie l’usage des conventions d’appel FORTRAN (ou Pascal) Idem que OPTION LANGUAGE:PASCAL .

/Gd

Spécifie l’usage des conventions d’appel C. Idem que OPTION LANGUAGE:C .

/H nombre

Restreint le nombre de caractères significatifs des noms de symboles externes à nombre . Par défaut : 31.

/help

Lance QuickHelp sur ML si installé.

/I chemin

Détermine un chemin de recherche pour les fichiers d’en-tête. Il peut y avoir jusqu’à dix /I chemin différents dans la même ligne de commandes.

/nologo

Supprime les messages s’il n’y a pas d’erreur.

/omf

Les fichiers objets sont au format OMF (Object Module File Format). Conseil : ne pas compter sur l'option par défaut qui semble varier selons les versions, toujours préciser /coff ou /omf .

/Sa

Liste toutes les informations dans le fichier listing, si /Fl est positionné bien entendu.

/Sc

Ajoute les durées d'instruction dans le fichier listing.

/Sf

Ajoute en début de fichier listing la sortie préprocesseur passe 1.

/Sg

Liste également les instructions générées par l’assembleur dans le fichier listing.

/Sl nbCaract

Définit la largeur de ligne du listing par nbCaract , de 60 à 255 caractères, ou 0 (défaut). Voir directive PAGE.

/Sn

Dévalide l’inclusion de la table des symboles dans la génération du listing.

/Sp nbLignes

Définit la taille de la page du listing par nbLignes , de 10 à 255 lignes, ou 0 (défaut). Voir directive PAGE.

/Ss texte

Passe le sous-titre du listing dans texte . Voir directive SUBTITLE.

/St texte

Passe le titre du listing dans texte . Voir directive TITLE.

/Sx

Force à inclure dans le fichier listing le code non assemblé pour cause d’assemblage conditionnel.

/Ta nomfichier

Assemble un fichier source dont le nom ne se termine pas par .asm .

/w

Équivalent à /W0

/W niveau

Positionne le niveau d'avertissements par niveau (0, 1, 2, ou 3).

/WX

Renvoie un code d'erreur en cas d'avertissement(s).

/Zd

Insère les informations de numéro de ligne dans le fichier objet.

/Zf

Rend publics tous les symboles.

/Zi

Insère des informations de débogage CodeView dans le fichier objet.

/Zm

Force l'option M510 pour une compatibilité maximale avec MASM 5.1.

/Zp [ alignement ]

Aligne les structures sur des adresses multiples de alignement octets. alignement peut prendre les valeurs 1, 2 ou 4.

/Zs

Ne fait qu'une vérification syntaxique, sans rien générer.

/?

Affiche l'aide (cette liste de commandes).

 

Les options ou commutateurs de la ligne de commandes de ML 7.0

 

L'assembleur de MASM est le fichier ml.exe depuis la version 6. Nous avons déjà signalé que masm.exe existe encore, mais il ne s'agit que d'un utilitaire de compatibilité ascendante : c'est un programme interface qui prend en entrée des arguments compatibles avec l'ancienne version de masm.exe , puis appelle ml.exe avec des arguments adaptés. Ceci permet de réutiliser d'anciens makefiles.

ml.exe utilise si elles existent les variables d’environnement INCLUDE , TMP et ML . INCLUDE spécifie, comme la directive /I , le chemin de recherche des fichiers d’en-têtes, TMP un chemin pour des fichiers temporaires et ML les options par défaut de la ligne de commandes. Par exemple, la ligne :

ml7   /Fl /Sa  squelet.asm

pourra être remplacée par :

SET ML=/omf /Fl /Sa /Fr 
…
ml7 squelet.asm

Les options saisies directement sur la ligne de commandes deviennent prioritaires sur celles de ML.

Si vous utilisez l'option /c pour assembler sans lier, ou simplement les transmettre via /link   Options , il faut avoir une idée des options, nombreuses, du lieur. Il y a plusieurs lieurs, selon le travail demandé, nous parlons de lieur segmenté pour les applications 16 bits et de lieur incrémental pour les applications 32 bits. Le terme incrémental n'a rien à voir avec la segmentation, c’est-à-dire qu'un lieur pourrait fort bien être segmenté et incrémental. Ce terme correspond à la possibilité de création d'un fichier .ilk dans le but de ne pas tout refaire à chaque édition de liens et ainsi d'être plus rapide.

Ces lieurs ne sont pas spécifiques à l'assembleur. Pour le lieur incrémental, la documentation est à rechercher dans le MSDN (ou l'aide de Visual Studio) comme étant celle du lieur de Visual C++. Nous donnerons simplement les listes brutes d'options de trois versions du lieur.

Pour la dernière version du lieur 16 bits :

Microsoft (R) Segmented Executable Linker  Version 5.60.339 Dec  5 1994
Copyright (C) Microsoft Corp 1984-1993.  All rights reserved.
 
Usage:
 
LINK
LINK @<response file>
LINK <objs>,<exefile>,<mapfile>,<libs>,<deffile>
 
Valid options are:
  /?                             /ALIGNMENT
  /BATCH                         /CODEVIEW
  /CPARMAXALLOC                  /DOSSEG
  /DSALLOCATE                    /DYNAMIC
  /EXEPACK                       /FARCALLTRANSLATION
  /HELP                          /HIGH
  /INFORMATION                   /LINENUMBERS
  /MAP                           /NODEFAULTLIBRARYSEARCH
  /NOEXTDICTIONARY               /NOFARCALLTRANSLATION
  /NOGROUPASSOCIATION            /NOIGNORECASE
  /NOLOGO                        /NONULLSDOSSEG
  /NOPACKCODE                    /NOPACKFUNCTIONS
  /NOFREEMEM                     /OLDOVERLAY
  /ONERROR                       /OVERLAYINTERRUPT
  /PACKCODE                      /PACKDATA
  /PACKFUNCTIONS                 /PAUSE
  /PCODE                         /PMTYPE
  /QUICKLIBRARY                  /SEGMENTS
  /STACK                         /TINY
  /WARNFIXUP

 

Pour la version contenue dans MASM32 :

Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.
 
usage: LINK [options] [files] [@commandfile]
 
   options:
 
      /ALIGN:#
      /BASE:{address|@filename,key}
      /COMMENT:comment
      /DEBUG
      /DEBUGTYPE:{CV|COFF}
      /DEF:filename
      /DEFAULTLIB:library
      /DLL
      /DRIVER[:{UPONLY|WDM}]
      /ENTRY:symbol
      /EXETYPE:DYNAMIC
      /EXPORT:symbol
      /FIXED[:NO]
      /FORCE[:{MULTIPLE|UNRESOLVED}]
      /GPSIZE:#
      /HEAP:reserve[,commit]
      /IMPLIB:filename
      /INCLUDE:symbol
      /INCREMENTAL:{YES|NO}
      /LARGEADDRESSAWARE[:NO]
      /LIBPATH:dir
      /MACHINE:{ALPHA|ARM|IX86|MIPS|MIPS16|MIPSR41XX|PPC|SH3|SH4}
      /MAP[:filename]
      /MAPINFO:{EXPORTS|FIXUPS|LINES}
      /MERGE:from=to
      /NODEFAULTLIB[:library]
      /NOENTRY
      /NOLOGO
      /OPT:{ICF[,iterations]|NOICF|NOREF|NOWIN98|REF|WIN98}
      /ORDER:@filename
      /OUT:filename
      /PDB:{filename|NONE}
      /PDBTYPE:{CON[SOLIDATE]|SEPT[YPES]}
      /PROFILE
      /RELEASE
      /SECTION:name,[E][R][W][S][D][K][L][P][X]
      /STACK:reserve[,commit]
      /STUB:filename
      /SUBSYSTEM:{NATIVE|WINDOWS|CONSOLE|WINDOWSCE|POSIX}[,#[.##]]
      /SWAPRUN:{CD|NET}
      /VERBOSE[:LIB]
      /VERSION:#[.#]
      /VXD
      /WARN[:warninglevel]
      /WINDOWSCE:{CONVERT|EMULATION}
      /WS:AGGRESSIVE

 

Et enfin, la dernière version, celle Visual Studio .NET :

Microsoft (R) Incremental Linker Version 7.00.9466
Copyright (C) Microsoft Corporation.  All rights reserved.
 
 usage: LINK [options] [files] [@commandfile]
 
   options:
 
      /ALIGN:#
      /ALLOWBIND[:NO]
      /ASSEMBLYMODULE:filename
      /ASSEMBLYRESOURCE:filename
      /BASE:{address|@filename,key}
      /DEBUG
      /DEF:filename
      /DEFAULTLIB:library
      /DELAY:{NOBIND|UNLOAD}
      /DELAYLOAD:dll
      /DLL
      /DRIVER[:{UPONLY|WDM}]
      /ENTRY:symbol
      /EXETYPE:DYNAMIC
      /EXPORT:symbol
      /FIXED[:NO]
      /FORCE[:{MULTIPLE|UNRESOLVED}]
      /HEAP:reserve[,commit]
      /IDLOUT:filename
      /IGNOREIDL
      /IMPLIB:filename
      /INCLUDE:symbol
      /INCREMENTAL[:NO]
      /LARGEADDRESSAWARE[:NO]
      /LIBPATH:dir
      /LTCG[:{NOSTATUS|PGINSTRUMENT|PGOPTIMIZE|STATUS}]
             (PGINSTRUMENT and PGOPTIMIZE are only available for IA64)
      /MACHINE:{AM33|ARM|IA64|M32R|MIPS|MIPS16|MIPSFPU|MIPSFPU16|MIPSR41XX|
                PPC|PPCFP|SH3|SH3DSP|SH4|SH5|THUMB|TRICORE|X86}
      /MAP[:filename]
      /MAPINFO:{EXPORTS|LINES}
      /MERGE:from=to
      /MIDL:@commandfile
      /NOASSEMBLY
      /NODEFAULTLIB[:library]
      /NOENTRY
      /NOLOGO
      /OPT:{ICF[=iterations]|NOICF|NOREF|NOWIN98|REF|WIN98}
      /ORDER:@filename
      /OUT:filename
      /PDB:filename
      /PDBSTRIPPED:filename
      /PGD:filename
      /RELEASE
      /SECTION:name,[E][R][W][S][D][K][L][P][X][,ALIGN=#]
      /STACK:reserve[,commit]
      /STUB:filename
      /SUBSYSTEM:{CONSOLE|EFI_APPLICATION|EFI_BOOT_SERVICE_DRIVER|
                  EFI_ROM|EFI_RUNTIME_DRIVER|NATIVE|POSIX|WINDOWS|
                  WINDOWSCE}[,#[.##]]
      /SWAPRUN:{CD|NET}
      /TLBOUT:filename
      /TSAWARE[:NO]
      /TLBID:#
      /VERBOSE[:LIB]
      /VERSION:#[.#]
      /VXD
      /WINDOWSCE:{CONVERT|EMULATION}
      /WS:AGGRESSIVE

Souvent, en cas de petit souci suite à un message d'erreur, les noms suffiront pour résoudre le problème par tâtonnements. Sinon, il faudra chercher dans la documentation, par exemple en utilisant ces noms comme clés de recherche.

 

10.2.2 Le code source, généralités

À l'exception des symboles prédéfinis et de ceux définis par l'utilisateur, MASM n'est pas sensible à la casse, c’est-à-dire qu'il ne fait pas la différence entre majuscules et minuscules. Il n'est pas bon de trop profiter de ce faux avantage et bien préférable de se fixer des contraintes. Nommez les mnémoniques et registres toujours de la même façon dans un projet donné ou sur l'ensemble de votre œuvre, donnez un nom à chaque variable ou étiquette selon une règle de nommage qui peut être personnelle mais qui inclut la casse et référencez ces éléments toujours de la même façon. Pas de code de ce genre :

label1: mov eax, 4
        MOV EBX, EAX
        anD ebx, 4
        JZ LABEL1
        jmp Label1

Ceci est particulièrement important si vous programmez plusieurs langages.

À l’inverse, mais pour les mêmes raisons, lors de l'utilisation d'un compilateur sensible à la casse, évitez d'utiliser cette caractéristique pour faire la différence entre deux identificateurs.

Regardons le listing simpliste suivant :

 1 ;--------------------------------------------------;
 2 ;Programme hello "squelet"
 3 ;Pierre Maurette pour Assembleur / Micro Application
 4 ;Sète, le 3/03/2003
 5 ;--------------------------------------------------;
 6 lowbyte EQU     low
 7 ;--------------------------------------------------;
 8         .286
 9         .MODEL SMALL
10 ;--------------------------------------------------;
11         .STACK 500
12 ;--------------------------------------------------;
13 const1  EQU     513+6
14 ;--------------------------------------------------;
15         .DATA
16 bijor   DB      "Bonjour, e-monde !",0Dh, 0Ah, "$"
17 IFDEF DEBUG
18 testdb  DB      "Mode DEBUG",0Dh, 0Ah, "$"
19 ENDIF
20 str1    DB      " ",0Dh,0Ah, "$"
21 var1    SBYTE   7
22 var2    WORD    2
23 ;--------------------------------------------------;
24         .CODE
25 debut:   mov ax, @data  ;initialisation data segment
26          mov ds, ax
27 ;--------------------------------------------------;
28          ; Message de bienvenue
29          mov ah, 09h
30          mov dx, offset bijor
31          int 21h
32 ;--------------------------------------------------;
33          ;Code d'essai
34          IFDEF DEBUG
35            ;.ERRDEF DEBUG
36            mov ah, 09h
37            mov dx, offset testdb
38            int 21h
39          ENDIF
40          mov al, var1
41          mov al, byte ptr var2
42          mov al, lowbyte const1
43          .IF SBYTE PTR al > 9
44          mov al, 3 * 3
45          .ENDIF
46          cmp al, 0
47          jnl suite
48          mov al, 0
49 suite:   add al, '0'
50          mov str1, al
51          mov ah, 09h
52          mov dx, offset str1
53          int 21h
54 ;--------------------------------------------------;
55          ; attente touche
56          mov ah, 00h
57          int 16h
58 
59          ;et fin du programme
60          mov AH, 4Ch             ;fin du programme
61          int 21h                 ;et retour au DOS
62 
63          END debut
64 ;--------------------------------------------------;

Ce petit programme exécutable sous DOS sera assemblé et lié par une suite de commandes regroupées dans un ficher batch :

echo off
PATH = %SystemRoot%\system32
SET PATH=C:\MASM\BIN;C:\MASM\BINR;%PATH%
SET LIB=C:\MASM\LIB
SET INCLUDE=C:\MASM\INCLUDE
 
del *.obj
del *.exe
del *.lst
del *.map
del listing.txt
 
REM ml7 /? >ml7.txt
REM ml615 /? >ml615.txt
 
REM ml7 /Zi /Fl"listing.txt" /Sf /Sg /Fm /omf %1.asm
ml615 /Zi /Fl"%1.txt" /Fm /Sg /Sf /omf %2 %1.asm
REM ml615 /Zi /Fl /Sa /Fm /omf %1.asm
REM ml615 /Zi /Fl /Sa /Fm %1.asm
REM dir
pause

Les lignes débutant par SET  servent à positionner temporairement, c’est-à-dire pour la session ouverte par le fichier batch, des variables d'environnement. Pour le reste, nous pouvons retrouver dans le tableau les commutateurs de la ligne de commandes utilisés.

À partir de ces listings, nous allons prendre un premier contact avec le vocabulaire MASM. Il est difficile d'être totalement précis dans la catégorisation des éléments du langage, la notion de directive par exemple s'applique à des éléments fonctionnellement très variés. Il faut faire avec, et ce n'est pas un vrai problème. Sauf éventuellement pour rédiger un texte à visée pédagogique. Cette syntaxe est basée sur cinq grandes catégories d'éléments :

Les instructions , que nous reconnaissons facilement à partir de la ligne 25. Elles nous intéresseront dans cette partie de l'ouvrage quand, comme à la ligne 30, elles mettent en œuvre pour l'écriture de l'opérande des spécificités de MASM. Pour le reste, elles sont explorées particulièrement dans le chapitre Le jeu d'instructions .

Des directives , éléments propres à MASM, qui permettent de passer des ordres à l'assembleur. En réalité, les directives peuvent être définies comme ce qui n'est pas classé dans les autres catégories. Certaines produisent du code, comme à la ligne 43 le .IF . D'autres se contentent de transmettre une information à l'assembleur, comme le .286 de la ligne 8 ou le IFDEF de la ligne 34.

Les attributs sont également de types relativement variés. Globalement, ce sont les paramètres des directives. Nous trouvons des directives avec attribut aux lignes 9, 11, 13 et bien d'autres.

Les opérateurs , qui sont utilisés dans les expressions, qui peuvent par exemple calculer un attribut. Nous en voyons aux lignes 13 (+) et 43 (>) par exemple.

Des symboles prédéfinis , par lesquels MASM permet au programme d'accéder à certaines valeurs : à la ligne 25, @data est un symbole qui remplace (nous y reviendrons en temps utile) la valeur du sélecteur de segment des données.

 

Il est habituel, depuis l'origine de la littérature sur l'assembleur, de présenter la ligne d'instructions standard de la façon suivante :

[[Label:]] [[Mnémonique]] [[Opérandes]] [[;Commentaires]]

C'est bien joli, mais dans le listing proposé, seule la ligne 25 correspond à ce schéma. Nous allons donc aborder les éléments syntaxiques par familles et même ensuite par thèmes.

Mais auparavant, quelques remarques fondamentales pour mieux situer les éléments de ce code source dans le processus de fabrication de l'exécutable. Nous avons dit qu'il est difficile d'avoir une attaque rigoureuse de la syntaxe de MASM. Il est vrai qu'un label, ou étiquette, et un nom de variable, c'est pareil et ce n'est pas non plus la même chose. Il y a par contre deux points sur lesquels il est possible de catégoriser :

  Différencier ce qui est connu dès l'assemblage et ce qui ne le sera qu'à l'exécution. Ce point fondamental sera vu en détail à plusieurs occasions ultérieurement.

  Dans le code source, différencier les lignes qui génèrent de l'octet en mémoire de celles qui n'en produisent pas. Ce point sera plus clair à partir du sous-chapitre sur les fichiers listing, mais intéressons-nous-y dès maintenant.

Quelles lignes produisent de l'occupation mémoire ? 16, 18, 20 à 22, 25, 26, 29 à 31, 36 à 38, 41 à 53 (y compris 43 et 45), 56, 57,60 et 61.

Les lignes 18, et 36 à 38 produisent bien des données et du code, si l'assembleur les traite. Nous y reviendrons un peu plus loin. Quand à la ligne 11, elle crée une pile, donc de la mémoire, c'est un cas un peu particulier.

Aux lignes 15 et 24, nous trouvons les directives .DATA  et .CODE . Elles correspondent au début de deux zones de mémoire, des segments logiques. Un compteur démarre à cet endroit, un compteur de position, un pour chaque segment. Il est incrémenté d'un nombre variable d'unités à chaque ligne qui génère de la mémoire. La valeur dans ce compteur est très proche d'un offset.

Qu'est-ce qu'un label ou étiquette en bon français ? Un objet, une constante nommée, qui prend la valeur du compteur de position de l'endroit où il est placé dans le code source. C'est en première approche très peu différent d'un nom de variable. Ces constantes, noms de variables, de labels, mais également de segments sont utilisables par les instructions du programme pour accéder à la mémoire. C'est ainsi qu'il est possible d'écrire à la ligne 40 mov al, var1 , ou à la ligne 47 jnl suite . Que fait l'assembleur à l'arrivée sur la ligne 40 ? quand il était passé par la ligne 21, il avait noté dans une table qu'il existait dorénavant un symbole var1 , qui correspondait à un offset donné dans le segment débuté par .DATA , disons le segment pointé par DS pour simplifier. Il va donc utiliser sa documentation Intel pour fabriquer les trois octets de l'instruction, A0 pour l'opcode, plus les deux octets de l'offset. C'est en réalité un peu plus sioux que ça, mais nous pouvons en rester à cette image. Il génère vraiment trois octets. En fait, il connaît ces adresses à un décalage près.

Puisque nous en sommes au travail de l'assembleur, détaillons une séance complète d'assemblage, en sachant que beaucoup de notions nous sont peut-être encore inconnues :

Dans un premier tour de piste, il évalue les conditions d'assemblage conditionnel et nettoie le source de l'inutile.

Il développe ce qui doit l'être (macros, equates). Ici, il remplace par exemple à la ligne 42 lowbyte par low , comme il lui a été demandé à la ligne 6. Fin de la passe 1.

Dans le même ordre d'idée, il calcule ce qu'il doit, par exemple à la ligne 44 : 3 x 3 = 9 (bravo).

Il code les instructions qu'il sait coder : la ligne 46, par exemple.

Il remplace les offsets qu'il ne connaît pas entièrement par l'offset interne, celui correspondant plus ou moins au compteur de position.

Il copie dans le fichier objet les segments et leurs attributs.

Il copie dans le fichier objet des marqueurs pour repérer ce qui est à corriger en fonction des adresses réelles des segments (c'est bien cela qui manquait).

Il fabrique le fichier listing si demandé.

Il fabrique une ligne de commandes pour le lieur, en particulier il lui indique les librairies à lier et l'appelle.

10 Il se recouche, parce qu'il a bien travaillé et qu'il est très fatigué.

Les deux ou trois premières phases sont le fait d'une couche particulière, correspondant parfois dans d'autres langages à un programme indépendant : le préprocesseur. Nous n'avons pas parlé ici des possibles inclusions de fichiers, .inc en général : ces inclusions ne changent rien, les inclusions créent un fichier source développé, c'est tout.

 

Ensuite le lieur :

Arrange les segments au mieux, en fonction des instructions de l'assembleur, il résout donc toutes les adresses à une seule inconnue près : l'adresse réelle du début de ce bloc.

Il corrige les offsets marqués.

Il copie les informations sur les adresses à modifier (remplir par une valeur correcte) dans l'en-tête du fichier .exe sous forme d'une table de relogement (ou relocation).

Il fabrique et sauve ce fichier exécutable.

 

Et enfin le chargeur (loader) du système d'exploitation :

Il fabrique dans la mémoire un en-tête (PSP), qui par exemple contient les paramètres de la ligne de commandes, des informations pour le retour en fin d'exécution, etc.

Il alloue de la mémoire au programme, et là, il connaît enfin toutes les adresses.

Il charge le programme en mémoire.

Il corrige les adresses qui doivent l'être (celles figurant dans la table créée par le lieur).

Il initialise les registres de segment et lance le programme.

Nous allons commencer à voir quelques points précis du langage, à commencer par ce qui est le plus présent dans le listing exemple : les commentaires.

10.2.3 Commentaires

Un commentaire correspond à tout texte qui sera oublié par l'assembleur ou le compilateur. Il sera éliminé par le préprocesseur, ou la phase de préassemblage pendant laquelle sont également développées les macros.

La façon habituelle de placer des commentaires sous MASM est le point-virgule. Tout ce qui suit ce signe sur une ligne est ignoré à l'assemblage, ce qui permet à la fois de faire des blocs en commentaire :

;-------------------------------
; Bloc en commentaire
;-------------------------------

et de commenter brièvement une ligne de code utile :

xor eax, eax ;mettre EAX à 0

 

Il existe une autre façon de définir un bloc de commentaires, la directive COMMENT . Le premier caractère hors l'espace qui suit cette directive, impérativement sur la même ligne, définit un délimiteur. Tout ce qui se trouve avant la prochaine occurrence de ce délimiteur, plus la fin de la ligne contenant cette occurrence, est ignoré à l'assemblage :

COMMENT @
Ceci est un commentaire
multiligne
TEXTEQU 
    call    tempo
    in      al,status_8042
    test    al,DataDispo_8042
    jnz     purge_8042
@ ceci est encore en commentaire

Cette seconde méthode est peu utilisée. Son avantage est de pouvoir facilement mettre en commentaire (voir un peu plus loin) un bloc entier de code source. Le délimiteur n'est formé que d'un seul caractère. Il est défini pour chaque utilisation de la directive COMMENT , ce qui permet normalement de toujours trouver un caractère adéquat, parmi les caractères rares ou accentués. Le danger est de ne pas voir immédiatement qu'une zone est en commentaire. Les éditeurs, même dotés de la coloration syntaxique, ne connaissent pas cette directive. Une tentative pour l'ajouter sur UltraEdit32 a pour l'instant échoué.

Usage des commentaires

Un usage bien connu du commentaire est de renseigner le lecteur du source sur le fonctionnement du code. Le lecteur est très souvent le programmeur lui-même. Il est toujours difficile d'imaginer qu'un détail qui semble aujourd'hui si évident nous fera perdre beaucoup de temps quelques jours plus tard. Et pourtant...

Toutefois, le commentaire ligne à ligne n'est pas nécessairement recommandable. Hors contexte pédagogique particulier, tout lecteur d'un listing assembleur sait que mov eax, 12 charge le registre EAX par la valeur 12 sur 32 bits, soit 0000000Ch .

Un choix judicieux de noms de variables ou de constantes symboliques est bien préférable :

EcrirePort_8042  EQU 0d1h
command_8042     EQU 64h
...
mov     al, EcrirePort_8042
out     command_8042, al

Les deux premières lignes pourront avec avantage se trouver dans un fichier d'include, par exemple ports.inc ou _8042.inc .

Cette séquence sera à l'assemblage strictement équivalente et beaucoup plus agréable à lire que :

mov     al, 0d1h ;0d1h:  commande "écrire dans 8042"
out     64h,  al ; 64h:  registre de commande du 8042

Il est bien plus efficace de séparer le code source en blocs, procédures ou simples regroupements fonctionnels, et de faire précéder chacun de ces blocs d'une brève explication, en cas de nécessité.

Tout fichier source devrait débuter par un en-tête. À vous de le définir une fois pour toutes : but du programme ou liste des fonctions exposées, nom du programmeur, date et/ou numéro de version, etc. En assembleur, vous n'aurez que très rarement à votre disposition un gestionnaire de projet et encore moins de version ou de travail en équipe.

Il peut être très utile de mentionner systématiquement en tête d'un code testé avec succès les conditions exactes de ce test :

;-----------------------------------------------------------
;  testé le 14 juillet 1789 
;  ml /c /coff /Cp /Fl %1.asm [ml.exe v 6.14.8444]
;  link /SUBSYSTEM:CONSOLE %1.obj [link.exe v 5.12.8078]
;-----------------------------------------------------------

Une dernière utilisation du commentaire est une alternative légère à l'assemblage conditionnel. Nous parlerons de mise en commentaire d'une ligne ou d'un bloc dans certaines circonstances, voire de décommenter une ligne.

mov ax, 0 ;décommenter si Pentium
;mov ax, 1 ;décommenter si 486 et antérieurs

C'est une méthode moyennement rationnelle mais bien utile en situation de débogage ou d'apprentissage. Voilà pour les commentaires.

Nous avons vu dans le listing une directive IFDEF qui ne produisait pas de code. Son utilisation peut être comparée à la mise en commentaire que nous venons de voir, de façon plus puissante. Voyons ce qu'il en est de l'assemblage conditionnel.

 

10.2.4 Directives conditionnelles du préprocesseur

C'est le nom le moins mauvais que nous avons trouvé pour désigner deux familles de directives qui modifient l'assemblage, alors que d'autres, tout aussi conditionnellement, génèrent du code. Elles concernent généralement des projets d'une certaine envergure, mais peuvent nous aider également au quotidien.

Assemblage conditionnel

La première famille de directives, semblable à ce qui est proposé par les langages évolués, consiste à assembler ou non des portions de code selon une condition, l'état d'une variable ou le fait qu'une constante soit ou non définie par exemple.

La structure de base, que nous retrouverons dans toutes les instructions de type IF en assembleur (et en C/C++) est la suivante :

IF Expression1

Bloc assemblé si Expression1 est vrai (non nul).

[[ ELSEIF Expression2

Bloc assemblé si Expression1 est faux (nul) et Expression2 est vrai (non nul)]].

[[ ELSE

Bloc assemblé dans les cas où Expression1 et Expression2 sont faux (nul)]].

ENDIF

Tout est dans la définition, ajoutons simplement que les blocs ELSEIF et ELSE sont optionnels, que les blocs ELSEIF peuvent être multiples, que le bloc ELSE ne peut être qu'unique et placé entre le dernier bloc ELSEIF s'il existe et le ENDIF qui est nécessaire.

Les expressions sont des expressions qui renvoient la valeur vrai ou faux, ou une valeur numérique entière, 0 étant équivalent à faux, toute autre valeur à vrai.

Il existe des variantes du IF et du ELSEIF qui ne modifient pas le fonctionnement de base. En voici la liste :

Conditions des directives d'assemblage conditionnel

Directive

Assemblage du bloc si et seulement si

IF Expression

Expression est vrai (non nul).

IFE Expression

Expression est faux (nul).

IFDEF Nom

Nom a été défini.

IFDNEF Nom

Nom n'est pas ou plus défini.

IFB Param (1)

Param est vierge (blank), n'a pas été transmis.

IFNB Param (1)

Param n'est pas vierge, a été transmis.

IFIDN[I] Param1 , Param2 (1)

Param1  =  Param2 (I : comparaison insensible à la casse).

IFDIF[I] Param1 , Param2 (1)

Param1 différent de Param2 (I : comparaison insensible à la casse).

IF1

Uniquement pendant la passe 1.

ELSEIF1

Uniquement pendant la passe 1.

IF2

Uniquement pendant la passe 2.

ELSEIF2

Uniquement pendant la passe 2.

(1) réservé aux macros.

L'assemblage conditionnel va être utilisé pour tenir compte d'un certain nombre de choix ou d'options, matériel installé, type de microprocesseur, voire système d'exploitation cible, et n'assembler que ce qui est nécessaire. Certes, cette façon de faire ne facilitera pas la lecture du code source. Nous ne connaissons pas d'éditeur de texte qui interprète ces informations et qui pourrait masquer automatiquement ce qui n'est pas assemblé. L'avantage est de n'avoir à maintenir qu'une seule version de ce source.

Une utilisation classique est d'assembler certains blocs uniquement en phase de débogage, dans le but de tester le passage du programme à tel ou tel endroit ou pour afficher des variables. Dans les langages évolués, s'ils proposent des options prédéterminées débogage et final , une macro DEBUG est automatiquement définie. C'est ce que nous pouvons faire manuellement, soit en tête du fichier source, soit en transmettant l'information par la commande /D de la ligne de commandes. Voyons ce que nous avons fait sur l'exemple du CD-Rom :

Nous utilisons un fichier de commandes build.bat , qui prend le nom du programme à construire en premier argument, représenté par %1 . Ajoutons %2 pour le second argument :

ml615 /Zi /Fl"%1.txt" /Fm /Sg /Sf /omf %2 %1.asm

Il faudra construire first.asm en saisissant build first /DDEBUG . Dans le fichier source, nous trouvons :

.DATA
...
IFDEF DEBUG
  testdb  db  "Mode DEBUG",0Dh, 0Ah, "$"
ENDIF
...
.CODE
...
IFDEF DEBUG
  mov ah, 09h
  mov dx, offset testdb
  int 21h
ENDIF

Remarquons que l'assemblage conditionnel s'applique également aux données : il est inutile de créer la chaîne testdb dans la version finale.

Génération conditionnelle d'erreurs

Toujours à des fins de débogage, il est possible de générer des erreurs à l'assemblage à certaines conditions. La génération d'erreur par les directives de la famille .ERR est très proche des conditions du paragraphe précédent. Il s'agit de générer une erreur sous diverses conditions, à l'exception de .ERR qui la provoquera toujours, et qui doit donc être placée dans un bloc conditionnellement assemblé, sinon ça n'a pas de sens. Voici la liste de ces directives, très proche effectivement de la liste des directives de la famille IF  :

Directives de génération conditionnelle d'erreurs

Directive

Erreur générée si

.ERR

Toujours.

.ERRE Expression

Expression est faux (nul).

.ERRNZ Expression

Expression est vrai (non nul).

.ERRDEF Nom

Nom a été défini.

.ERRNDEF Nom

Nom n'est pas ou plus défini.

ERRB Param (1)

Param est vierge ( blank ), n'a pas été transmis.

ERRNB Param (1)

Param n'est pas vierge, a été transmis.

ERRIDN[I] Param1 , Param2 (1)

Param1  =  Param2 (I : comparaison insensible à la casse).

ERRDIF[I] Param1 , Param2 (1)

Param1 différent de Param2 (I : comparaison insensible à la casse).

ERR1

Pendant la passe 1.

ERR2

Pendant la passe 2.

(1) réservé aux macros.

Rappelons que c'est bien à l'assemblage que se produira l'erreur provoquée. Modifions notre code :

IFDEF DEBUG
  .ERRDEF DEBUG
  mov ah, 09h
  mov dx, offset testdb
  int 21h
ENDIF

Le message suivant s'affiche, stoppant le processus d'assemblage :

first.asm(38) : error A2056: forced error : symbol defined : DEBUG

 

10.2.5 La directive OPTION

Cette directive concerne les comportements généraux de l’assembleur. Elle peut venir masquer l’effet d’un commutateur de la ligne de commandes.

Prenons l’exemple de la compatibilité 5.10. Cette option indique à MASM d’assembler un source en respectant le maximum de compatibilité avec la version 5.10 et est utile pour tenter de récupérer des projets anciens. Par défaut, cette compatibilité n’est pas respectée. Si vous saisissez /Zm dans la ligne de commandes de MASM, la compatibilité 5.10 sera activée. L’effet est le même que la ligne OPTION M510 dans le programme. Si maintenant vous incorporez dans votre programme OPTION NOM510 , que vous ayez ou non saisi /Zm , la compatibilité 5.10 sera désactivée.

Donc, pour un certain nombre de comportements, la hiérarchie est la suivante, par ordre de priorité croissante :

Comportement par défaut.

Comportement lié à une variable d’environnement.

Comportement dicté par la ligne de commandes, s’il existe un commutateur adéquat.

Comportement dicté par la directive OPTION dans le code source ou toute autre directive susceptible de modifier ce comportement.

En plus, la modification du comportement par OPTION n’est pas globale, mais court à partir de la ligne validant le comportement jusqu’à la ligne validant le comportement inverse, si elle existe, et cela plusieurs fois si nécessaire pour définir des zones. En particulier, jusqu’à la première ligne définissant un comportement, c’est celui défini par la ligne de commandes, ou la valeur par défaut qui prévaudra.

Chaque option est soit en couple de type actif/inactif, soit suivie d’un attribut définissant le comportement. Voici la liste de ces options, sachant que pour beaucoup, le concept qui se trouve derrière est développé plus loin. Nous avons choisi de ne pas vraiment traiter de la compatibilité avec la version 5.10.

 

Options de compatibilité avec la version 5.1

CASEMAP : Type

Si Type vaut NONE, l'assembleur est sensible à la casse (majuscule-minuscule) des identificateurs en interne, et ceux-ci sont exportés dans les fichiers .obj tel que déclarés par EXTERNDEF , PUBLIC ou COMM .

Si Type vaut NOTPUBLIC (déf.), l'assembleur est insensible à la casse (majuscule-minuscule) des identificateurs en interne, mais ceux-ci sont exportés dans les fichiers .obj tel que déclarés par EXTERNDEF , PUBLIC ou COMM .

Si Type vaut ALL, l'assembleur est insensible à la casse (majuscule-minuscule) des identificateurs en interne et ceux-ci sont exportés dans les fichiers .obj convertis en majuscules.

M510 | NOM510 (déf.)

M510 recherche la compatibilité MASM 5.10 et pour cela positionne NOSCOPED , OLDMACROS , DOTNAME , OLDSTRUCTS et EXPR16 (options non défaut).

DOTNAME | NODOTNAME (déf.)

DOTNAME autorise l'utilisation d'un point comme premier caractère d'un nom de variable, structure, union, membre ou macro. Normalement interdit ( NODOTNAME )

SCOPED (déf.) | NOSCOPED

SCOPED garantit que les labels à l'intérieur d'une procédure sont locaux à cette procédure.

OLDMACROS | NOOLDMACROS (déf.)

OLDMACRO met en œuvre le traitement des macros à la 5.1 .

OLDSTRUCTS | NOOLDSTRUCTS (déf.)

OLDSTRUCTS permet la compatibilité 5.1 pour le traitement des membres de structures.

SETIF2  : TRUE | FALSE

Si SETIF2 est à TRUE , .ERR2 , IF2 et ELSEIF2 sont évaluées à chaque passe. Si SETIF2 est à FALSE , .ERR2 , IF2 et ELSEIF2 ne sont pas évaluées. Par défaut, si SETIF2 n'est pas spécifié .ERR2 , IF2 et ELSEIF2 génèrent une erreur.

 

Options touchant les procédures

(voir le sous-chapitre traitant ce sujet).

LANGUAGE : Type

Type spécifie parmi C, PASCAL, FORTRAN, BASIC, SYSCALL ou STDCALL la convention de langage par défaut utilisée avec PROC , EXTERN et PUBLIC . Surcharge la convention déterminée par .MODEL (mais est prévue pour être utilisée en l'absence de cette directive).

PROLOGUE : NomMacro

Définit NomMacro comme la macro remplaçant le code de prologue standard.

EPILOGUE : NomMacro

Définit NomMacro comme la macro remplaçant le code d'épilogue standard.

PROC : Visibilité

Définit la visibilité par défaut parmi PUBLIC , EXPORT ou PRIVATE .

 

Options diverses

EMULATOR | NOEMULATOR (déf.)

EMULATOR demande à MASM. LJMP (déf.) | NOLJMP

Autorise la génération automatique de code d'émulation de saut conditionnel long.

EXPR16 | EXPR32 (déf.)

Spécifie la taille des mots des expressions. Ne peut plus être changé.

NOKEYWORD < ListeMotsClés >

Permet de faire perdre aux mots de la liste leur statut de mot réservé, et donc de pouvoir les utiliser comme identifiant. Saisir sans virgule : NOKEYWORD<PROC OFFSET> .

NOSIGNEXTEND

Surcharge l'extension de signe par défaut des instructions AND , OR , XOR . Implanté pour une compatibilité avec processeurs NEC.

OFFSET : TypeOffset

TypeOffset peut être GROUP (déf.), SEGMENT ou FLAT . Il détermine le point de référence pour l'opérateur OFFSET .

READONLY | NOREADONLY (déf.)

Autorise ou non la vérification par l'assembleur qu'aucune instruction n'écrit dans un segment de code.

SEGMENT : TailleSegment

TailleSegment parmi USE16 , USE32 et FLAT permet de fixer la taille de segment par défaut, ainsi que la taille d'adresse pour les symboles externes définis hors de tout segment.

10.2.6 Opérateurs

MASM supporte, pour éléborer ses expressions numériques (entières) et logiques, une série d'opérateurs dont certains sont intuitifs, d'autres plus spécifiques.

Tout d'abord, les opérateurs arithmétiques de base, plus deux version spéciales de l'opérateur d'addition :

  +  Addition

  - Soustraction

  * Multiplication

  / Division entière

  MOD  Modulo, ou reste de la division entière

  []  Remplace le + , utilisé dans les expressions d'offsets

  .  Remplace le +, additionne à l'adresse d'une structure ou union le déplacement d'un champ

 

Nous trouvons ensuite six opérateurs bitwise, bit à bit :

  AND   ET bit à bit

  OR    OU INCLUSIF bit à bit

  XOR    OU EXCLUSIF bit à bit

  NOT    INVERSION de chaque bit

  SHL    DÉCALAGE VERS LA GAUCHE de tous les bits, d'une unité

  SHR    DÉCALAGE VERS LA DROITE de tous les bits, d'une unité

 

Nous verrons dans la partie Directives de contrôle du flux de programme les opérateurs logiques spécifiques aux directives .IF, .WHILE et .REPEAT. Pour les autres directives conditionnelles, nous disposons des opérateurs relationnels suivants :

  EQ  Égal.

  NE  Différent.

  GT  Plus grand.

  GE   Plus grand ou égal.

  LT  Plus petit.

  LE  Plus petit ou égal.

 

Quatre opérateurs, selon la terminologie Microsoft, concernent les segments :

  OFFSET  Renvoie l'offset de l'argument.

  LROFFSET  Variante de OFFSET, valeur résolue par le loader.

  :  Surcharge de registre de segment.

  SEG  Renvoie la valeur du segment de l'argument.

Par exemple, OFFSET var1 et LROFFSET var1 renvoient l'offset de var1 dans DS ou DGROUP, SEG var1 renvoie généralement le contenu de DS, et ES:var1 permet d'accéder à une donnée dans le segment ES.

 

Enfin, citons parmi les plus utiles :

  HIGH  et LOW  Renvoient le BYTE respectivement de poids fort et de poids faible de l'argument.

  HIGHWORD  et LOWWORD  Renvoient le WORD respectivement de poids fort et de poids faible de l'argument.

  TYPE  Renvoie le type de l'argument, en octets (4 pour un DWORD).

  SIZE  et SIZEOF  Renvoient le nombre d'octets dans l'argument, une variable ou un type.

  LENGTH  et LENGTHOF  Renvoient le nombre d'éléments dans une variable complexe.

D'autres sont présentés dans les différentes parties de ce chapitre. Pour le restant, il reste la documentation.

 

10.3 Génération et lecture des listings

Placer physiquement un article sur les listings au tout début d’un parcours d’apprentissage peut sembler anachronique. D’abord, il a déjà été signalé qu’un tel parcours linéaire n’était pas possible et que l'ordre importait peu. Ensuite, lors de la découverte de notions nouvelles comme les données ou les macros, la consultation d’un listing est souvent plus enrichissante que le test du programme, même au sein d’un débogueur.

En réalité, la compilation d’un source avec génération de listing, sans édition de lien, est tout à fait suffisante dans beaucoup de cas pour accompagner la lecture de la documentation. Nous aurons donc besoin de produire et de lire un fichier listing dès le premier sujet que nous aborderons.

À l’inverse, l’interprétation des listings aborde des notions qui devront être admises en première lecture.

Pour désigner un listing, nous utiliserons le mot listing, issu de l’anglais listing. Plus sérieusement, dans un dictionnaire déjà un peu ancien, nous avons relevé la définition du mot listing (ou listage) : document produit par l’imprimante d’un ordinateur. Effectivement, le listing est historiquement orienté imprimante. Il s’agissait d’un document important dans la gestion de projet, voire contractuellement d’un élément de la livraison finale.

Pour cette présentation, nous nous baserons sur ml.exe version 7.0 avec tests sur 6.15 en cas de doute.

Vous trouverez quelques fichiers évoqués dans les lignes qui suivent dans un dossier Listings du CD-Rom.

10.3.1 Génération d’un listing

Deux familles d’actions influent sur la génération d’un fichier listing :

  Les paramètres de la ligne de commandes à l’appel de ml.exe .

  Une série de directives au sein du code source.

Les paramètres de la ligne de commandes qui intéressent la génération d’un fichier de listing sont /Fl ainsi que tous les  /S? . Ajoutons /EP qui envoie un listing passe 1 à l’écran. Tous sont décrits dans le tableau récapitulatif des commutateurs de la ligne de commandes. Par défaut, c’est-à-dire quelles que soient les directives dans le source et si le commutateur /Fl n’est pas envoyé à ml.exe , aucun fichier listing ne sera généré.

Pour lire le tableau, il faut bien voir que certaines de ces directives vont par couples, comme par exemple  .LIST/.NOLIST . Ces couples fonctionnent en flip-flop, pour déterminer des zones de listing dans laquelle le comportement sera déterminé.

Directives de gestion des listings

Directive

Effet

.LISTALL

Équivalent à .LIST + .LISTIF + .LISTMACROALL .

.LIST

Positionnée par défaut. Démarre le listing des instructions.

.NOLIST

Interdit le listing des instructions.

.XLIST

Autre forme de .NOLIST

.LISTIF

Force le listing des instructions non assemblées en cas d’assemblage conditionnel.

.TFCOND

Autre forme de .LISTIF

.NOLISTIF

Stoppe l’effet de .LISTIF . Les instructions non assemblées ne sont plus listées.

.SFCOND

Autre forme de .NOLISTIF

.NOCREF  [nom[,nom..]]

Interdit le listing des noms de symboles dans la table. Possibilité de limiter cette interdiction à certains symboles dont les noms sont donnés.

.XCREF

Autre forme de .NOCREF

.CREF

Stoppe l’effet de .NOCREF (ou .XCREF ).

.LISTMACROALL

Force le listing complet de macros développées.

.LALL

Autre forme de .LISTMACROALL .

.LISTMACRO

Positionnée par défaut. Liste les instructions des macros développées qui génèrent code ou données.

.XALL

Autre forme de .LISTMACRO

.NOLISTMACRO

Interdit le listing des macros développées.

.SALL

Autre forme de .NOLISTMACRO

PAGE

Génère un saut de page.

PAGE [[nl][nc]]

Positionne les dimensions de la page à nl lignes de nc caractères.

PAGE +

Changement de section. Le N° de page repasse à 1.

TITLE  texte

Fixe le titre du listing.

SUBTITLE  texte

Fixe le sous-titre du listing.

SUBTTL  texte

Autre forme de .SUBTITLE texte.

 

Il est visible que certains comportements sont commandés à la fois par un commutateur de la ligne de commandes et par une directive. Il est donc possible que surviennent des ordres contradictoires. Voici quelques règles de priorité applicables :

  Sans commutateur /Fl , aucun listing ne sera généré.

  Le commutateur /Sa (avec /Fl , répétons-le) rend inopérantes les directives du code source visant à interdire ou limiter le listing.

  Dans le code source, .NOLIST masque les autres directives, comme .LISTMACROALL .

  Les commutateurs /Sx , /Ss , /Sp , /Sl positionnent les comportements à une valeur différente de celle par défaut. Ensuite, les directives correspondantes reprennent au besoin la main.

Bien qu’il ne s’agisse pas à proprement parler de listing, citons ici la directive ECHO (ou %OUT , c’est équivalent). Elle permet d’envoyer du texte sur STDOUT , donc à priori l’écran. Ce peut être utilisé par exemple pour signaler qu’un processus d’assemblage risque d’être long, et à signaler au fur et à mesure différentes étapes de l’assemblage. Un exemple, avec une macro texte prédéfinie :

%ECHO Programme &@FileName
 ECHO par l'excellent Julius Caius Caesar
 ECHO Début de l'assemblage ...

 

Manipulons maintenant pour mettre un peu en pratique. Avec un fichier test.asm ne contenant pour l’instant aucune directive listing, ajoutez /Fl dans la ligne de commandes, par exemple en modifiant un fichier build.bat . Vérifiez que le commutateur de ligne de commandes /Sa n’est pas actif, ni aucun commutateur de type  /S? . Un fichier listing portant le nom test.lst est créé. Remplaçons le commutateur par /Fllisting.txt ou /Fl"listing.txt" (attention, pas d’espace devant le nom du fichier). Le fichier listing créé porte maintenant le nom listing.txt .

Nous analyserons les fichiers listing un peu plus loin. Si vous y jetez un coup d’œil et si vous avez un include test.inc dans votre source, vous constatez que ce fichier est logiquement listé au moment de son inclusion.

Dans le fichier source, rajoutez la ligne include win.inc , où un autre gros fichier d’en-tête selon votre installation. Le fichier listing prend de l’embonpoint et perd par la même occasion de la lisibilité. La raison est claire, mais le phénomène deviendra gênant dans les applications Windows. Modifions :

.NOLIST
include win.inc
.LIST

Le début du listing devient plus lisible, mais la taille demeure importante, à cause de la taille des symboles. Les symboles issus de notre code sont de plus noyés dans ceux issus de win.inc . Modifions à nouveau :

.NOCREF
.NOLIST
include win.inc
.LIST
.CREF

Maintenant, tous nos symboles sont listés, mais plus ceux en provenance de win.inc .

Une dernière manipulation, avant de passer aux listes de commutateurs et de directives. Nous avons dans notre code d’essai une zone assemblée sous condition :

;UTILE EQU UTILE
…
IFDEF UTILE                  
         ; Partie "utile" (!!)
;        mov ax, 69
         mov ax, 60 + (3 * 3)
         cmp ax, bx
         jne @F
         
         mov ah, 09h
         mov dx, offset egal
         int 21h                 
ENDIF

En jouant sur la mise ou non en commentaire de la première ligne, nous constatons que n’est listé dans listing.txt que ce qui est effectivement assemblé. Or, dans le cadre d’un projet ou deux parties du code ne sont jamais assemblées ensemble, il sera parfois gênant de ne pouvoir produire un listing plus complet. Nous pouvons utiliser les directives .LISTIF (ou . LISTALL ) et .NOLISTIF pour modifier cet état de fait dans tout le source ou sur certaines zones. Nous verrons alors le code source des parties non assemblées, mais évidemment pas le code objet généré. Le commutateur /Sx (et /Sa ) produit un effet similaire sur l ‘ensemble du source.

 

10.3.2 Contenu du fichier listing

Vous pouvez utiliser pour cette découverte du fichier listing à la fois celui généré par nos manipulations précédentes ( test.lst ou listing.txt ) et le fichier sk_win.lst issu d’une application de cet ouvrage et recopié sur le CD-Rom dans le dossier Listings du présent chapitre. Vous aurez ainsi à disposition une application DOS 16 bits et une autre en 32 bits, sous Windows.

En cas d’erreur à l’assemblage, le fichier listing est généré autant que faire se peut. Vous trouverez un message d’erreur au-dessous de l’instruction fautive :

                     mov var1, var2
test.asm(59) : error A2070: invalid instruction operands

 

Supposons que tout s’est bien passé. Si nous avons positionné les options adéquates, nous avons trois grandes zones dans le fichier listing généré :

La sortie du préprocesseur, ou passe 1. C’est le travail de première lecture de l’assembleur, sans génération de code. Les fichiers inclus sont parcourus, les macros développées. Cette partie alourdit inutilement le listing. Il est généralement inutile de la demander par le commutateur /Sf .

La passe 2 (et finale) du travail du compilateur. C’est la génération du code proprement dite, listée encore de façon chronologique.

Une partie synthétique : les diverses tables regroupant les macros, les diverses structures définies, segments, groupes et symboles.

La section génération du code

Attention, le fichier listing est généré avec des tabulations. Il est préférable de configurer la taille de la tabulation à 8 voire plus dans votre éditeur. Nous n’avons pas ici respecté cette valeur pour des raisons de mise en page. Cette valeur de 8 est par contre trop importante pour la saisie de code profondément indenté, une valeur de 2 à 4 étant préférable.

Dans le listing vous retrouvez bien entendu tout votre code source, commentaires compris, au fur et à mesure qu’il est parcouru par l’assembleur, et en fonction des options et directives de listing. Ce source occupe la droite de la ligne d’écran. Les quatre premières colonnes, à gauche, sont réservées au code généré. Entre les deux groupes de colonnes peuvent se trouver certains symboles. Par exemple  C indique des lignes, quel que soit leur contenu, hors du fichier principal, donc dans un fichier inclus :

            include resrc1.inc
               C 
               C option expr32
               C option casemap:none
               C 
               C ; Begin of file resrc1.h
 = 00000001          C IDI_ICON     EQU      1t
 = 00000001          C IDOK         EQU      1t
 = 00000002          C IDM_MENU     EQU      2t

 

Voyons le contenu des colonnes de gauche, code généré. Tout d’abord, le cas particulier : pour les equates, ou plus généralement les constantes symboliques manifestes, nous trouvons à gauche la valeur affectée, pour laquelle il n’y a bien entendu pas d’affectation mémoire, précédée du signe  =  :

= blibli            VALT1     TEXTEQU <blibli>
= 45                VALT2     TEXTEQU %(3*15)
= @FileName         VALT3     EQU     @FileName
= 0000              NULL      EQU     0
= 004E              VALEUR    =       78
= TEST              VALT31    TEXTEQU @FileName
= blibli            VALT4     CATSTR  VALT1 VALT2 VALT3
= MaClasse->Taille  VALT5     TEXTEQU <MaClasse-!>Taille>
= "Bonjour"         VALT6     EQU     "Bonjour"
= "Aurevoir"        VALT6     EQU     "Aurevoir"

 

Dans le cas général d’une ligne générant de la mémoire, code ou données, la première colonne est toujours une adresse, plus précisément un offset. Il est noté en hexadécimal, donc sur 4 signes dans un modèle 16 bits et 8 dans un modèle 32 bits. Par exemple, la première occurrence d’une directive de segment indique toujours 0000. Ensuite, l’assembleur gère cet offset :

0000                    .DATA
                        ORG 10h
0010 00000000           VARX    dword ?
...
; Code
…
0014                    .DATA
0014 000C               var1    dw   12

 

Nous voyons dans cet exemple que, dans le cas d’une déclaration, la mémoire réservée et initialisée est indiquée à la suite de l’offset. Il en est de même pour les chaînes de caractères. Les réservations mémoire par la directive DUP  font appel à une présentation particulière :

0040   0020 [     Table1 byte  32 dup (0FFh)
        FF
       ]
0060   0040 [     Table2 dword 64 dup (?)
        00000000
       ]

Il faut bien lire dans les colonnes, par exemple 0040 0020[FF] , pour 32 octets initialisés à 255 à l’offset 64.

Pour une ligne de code ordinaire, contenant une instruction, après l’offset, nous trouvons optionnellement (commutateur /Sc ) une indication du nombre de cycles nécessaires à l’instruction. Bien entendu cette donnée n’est pas mesurée, mais prise dans une table et dépend du processeur déclaré. Donc, cette indication est sur un processeur moderne peu utile. Ensuite, c’est le code généré pour l’instruction, s’il est connu à l’assemblage :

0023   4   B8 0045      mov ax, 60 + (3 * 3)

Cette instruction est complètement définie : offset 0023h , 4 cycles sur un 8086, B8h opcode du MOV immédiat, 0045h soit 69, l’assembleur ayant calculé 60 + (3* 3) . Il n’y a pas d’espace entre 00 et 45, ce n’est pas innocent. C’est un WORD, si c’est une image de la mémoire que nous voulons représenter, c’est B8 45 00 qu’il faut écrire (little endian).

Si le code n’est entièrement déterminé à l’assemblage, mais sera résolu par le lieur, la lettre  R indique ce caractère relogeable :

002C   4   BA 002A R    mov dx, offset egal

Dans ce cas, BA est l’opcode correct. 002A n’est pas l’offset de la variable egal . C’est l’offset de la variable dans la zone de données à laquelle elle appartient. Cette valeur sera calculée, avec l’ensemble des valeurs relogeables, par le lieur. Le mot segment est normalement utilisé ici pour désigner les zones de données, il entraîne de graves ambiguïtés.

De la même façon, la lettre  E indique une donnée externe, ici une adresse, qui sera également résolue par le lieur :

000001F7   7m  E8 00000000 E  *  call InitCommonControls

L’astérisque  * qui apparaît dans cette ligne indique du code généré par MASM. Comme ici par la directive conditionnelle .IF  :

                     .IF(ax == bx)
 0026  3B C3         *      cmp    ax, bx
 0028  75 07         *      jne    @C0001
 002A  B4 09                  mov ah, 09h
 002C  BA 0016 R              mov dx, offset egal
 002 F  CD 21                  int 21h    
                     .ENDIF
 0031          *@C0001:

La directive INVOKE génère beaucoup de code de ce type.

Enfin, un entier de 1 à 9 ou un signe  + dans la colonne centrale indique un niveau d’imbrication dans les développements de macros. Voici un exemple, extrait de la partie passe 1 du listing (le rôle de la macro MINMAXLIST est décrit par ailleurs) :

                 C MINMAXLIST    MACRO param1, param2, args
                 C    p1 = 0
                 C    FOR p2, <args>
                 C           IF p2 GT p1
                 C               p1 = p2
                 C           ENDIF
                 C    ENDM
                 C    param1 EQU p1
                 C 
                 C    p1 = 7FFFFFFFh
                 C    FOR p2, <args>
                 C           IF p2 LT p1
                 C               p1 = p2
                 C           ENDIF
                 C    ENDM
                 C    param2 EQU p1           
                 C       
                 C    ENDM
                 C        
                 C MINMAXLIST   WM_MAXI, WM_MINI, <1,2,3>
                1C      ??0000 = 0
                2C                      ??0000 = 1
                2C                      ??0000 = 2
                2C                      ??0000 = 3
                1C      ??0000 = 7FFFFFFFh
                2C                      ??0000 = 1

 

La section des tables

Cette section comporte au maximum sept tables :

Macros ;

Structures et unions ;

Structures de champs de bits (records) ;

Types ;

Segments et groupes ;

Procédures et fonctions ;

Symboles.

Seules les 5 et 7 sont de tous les projets, les trois dernières si nous considérons qu’un main() ne fait pas de mal. Les éléments de ces tables, à l’exception de la table des segments et groupes, sont listés par ordre alphabétique.

La table des macros se contente de les lister, en précisant toutefois s’il s’agit d’une procédure, qui ne renvoie rien, ou d’une fonction, qui donc renvoie quelque chose :

Macros:
 
                N a m e                 Type
 
ArgCount . . . . . . . . . . . . . . .  Func
MINMAXLIST . . . . . . . . . . . . . .  Proc
MMOV . . . . . . . . . . . . . . . . .  Proc

 

La table des structures et unions indique pour chacune d’entre elles son nom et sa taille en octets, suivi de la liste des éléments avec indication pour chacun de leur offset par rapport au début de la structure et du type. Cette table peut être très utile à imprimer, pour du travail à partir de fichiers .inc mal connus :

Structures and Unions:
 
                N a m e                  Size
                                         Offset      Type
 
ABCFLOAT . . . . . . . . . . . . . . .   0000000C
  abcfA  . . . . . . . . . . . . . . .   00000000    DWord
  abcfB  . . . . . . . . . . . . . . .   00000004    DWord
  abcfC  . . . . . . . . . . . . . . .   00000008    DWord
CPINFOEXA  . . . . . . . . . . . . . .   0000011C
  MaxCharSize  . . . . . . . . . . . .   00000000    DWord
  DefaultChar  . . . . . . . . . . . .   00000004    Byte
  LeadByte . . . . . . . . . . . . . .   00000006    Byte
  UnicodeDefaultChar . . . . . . . . .   00000012    Word
  CodePage . . . . . . . . . . . . . .   00000014    DWord
  CodePageName . . . . . . . . . . . .   00000018    Byte

 

Pour chaque structure de champs de bits ou record, Width donne la taille en bits de la structure complète, # fields le nombre de champs dans le record.

Pour chaque champ, Shift donne l’offset en bits du LSB (bit de poids faible) du champ par rapport au LSB du record. Width est le nombre de bits du champ, Mask donne la valeur maximale du champ, Initial est la valeur initiale du champ s’il est initialisé :

Records:
 
        N a m e           Width         # fields
                          Shift         Width         Mask   Initial
 
FPOProlog  . . . . . . .  00000010      00000006
  cbFrame  . . . . . .    0000000E      00000002      C000     ?
  reserved . . . . . .    0000000D      00000001      2000     ?
  fUseBP . . . . . . .    0000000C      00000001      1000     ?
  fHasSEH  . . . . . .    0000000B      00000001      0800     ?
  cbRegs . . . . . . .    00000008      00000003      0700     ?
  cbProlog . . . . . .    00000000      00000008      00FF     ?
ImportRec  . . . . . .    00000010      00000003
  Reserved . . . . . .    00000005      0000000B      FFE0     ?
  NameType . . . . . .    00000002      00000003      001C     ?
  Type2  . . . . . . .    00000000      00000002      0003     ?

 

La table des types liste les types définis par TYPEDEF . Size est la taille en octets du type, Attr le type de base de la définition :

Types:
 
       N a m e             Size         Attr
 
HWINSTA  . . . . . . .     00000004     DWord 
HWND . . . . . . . . .     00000004     DWord 
INT64  . . . . . . . .     00000008     QWord  

 

La liste des groupes et segments n’appelle pas de commentaire particulier. Voici un exemple pour un programme Windows :

Segments and Groups:
 
          N a m e         Size    Length   Align  Combine Class
 
CONST  . . . . . . . . . .32 Bit  0000008B DWord  Public  'CONST'  ReadOnly
FLAT . . . . . . . . . . .GROUP
_BSS . . . . . . . . . . .32 Bit  000001C0 DWord  Public  'BSS'  
_DATA  . . . . . . . . . .32 Bit  00000289 DWord  Public  'DATA'  
_TEXT  . . . . . . . . . .32 Bit  0000078A DWord  Public  'CODE'  

 

Et pour un programme DOS :

Segments and Groups:
 
          N a m e         Size   Length  Align   Combine  Class
 
DGROUP . . . . . . . . . .GROUP
_DATA  . . . . . . . . . .16 Bit  0177   Word     Public  'DATA'  
STACK  . . . . . . . . . .16 Bit  0100   Para     Stack   'STACK'    
_TEXT  . . . . . . . . . .16 Bit  0035   Word     Public  'CODE'

 

La table des procédures et fonctions liste toutes les fonctions et procédures référencées par le programme, donc y compris les externes.

Procedures,  parameters and locals:
 
                N a m e                 Type     Value    Attr
 
Main . . . . . . . . . . . . . . . . .    P Near     0000      _TEXT    Length= 0035 Public
 
 
 
 
Procedures,  parameters and locals:
 
                N a m e                 Type     Value    Attr
 
AProposProc  . . . . . . . . . . . . .    P Near     00000528 _TEXT    Length= 0000004E Public STDCALL
  hDlg . . . . . . . . . . . . . . . .    DWord     bp + 00000008
  uMsg . . . . . . . . . . . . . . . .    DWord     bp + 0000000C
  wParam . . . . . . . . . . . . . . .    DWord     bp + 00000010
  lParam . . . . . . . . . . . . . . .    DWord     bp + 00000014
AbortDoc . . . . . . . . . . . . . . .    P Near     00000000 FLAT    Length= 00000000 External STDCALL
AbortPath  . . . . . . . . . . . . . .    P Near     00000000 FLAT    Length= 00000000 External STDCALL
ActivateKeyboardLayout . . . . . . . .    P Near     00000000 FLAT    Length= 00000000 External STDCALL
AddAtomA . . . . . . . . . . . . . . .    P Near     00000000 FLAT    Length= 00000000 External STDCALL
AddAtomW . . . . . . . . . . . . . . .    P Near     00000000 FLAT    Length= 00000000 External STDCALL

 

La table des symboles regroupe ce qui reste. Donc en particulier mélange les variables, les labels (étiquettes) et les constantes symboliques, définies généralement par les equates ou par l’assembleur lui-même. Ces dernières sont de type Number ou Text , qui justement ne sont pas réellement des types. Pour les variables, c’est un vrai nom de type qui apparaît dans cette colonne Type .

La colonne Value représente pour les constantes symboliques vraiment la valeur du symbole. Pour les variables et les labels, cette valeur est en réalité l’offset du symbole par rapport au début du segment qui le contient.

Symbols:
 
          N a m e          Type      Value    Attr
 
??0000 . . . . . . . . . . Number    0001h
@CodeSize  . . . . . . . . Number    0000h
@DataSize  . . . . . . . . Number    0000h
@Interface . . . . . . . . Number    0000h
@Model . . . . . . . . . . Number    0002h
@code  . . . . . . . . . . Text      _TEXT
@data  . . . . . . .  . .  Text      DGROUP
@fardata?  . . . . . . . . Text      FAR_BSS
@fardata . . . . . .  . .  Text      FAR_DATA
@stack . . . . . . .  . .  Text      DGROUP
NULL . . . . . . . . . . . Number    0000h
SC . . . . . . . . . . . . Byte      0160     _DATA
Table1 . . . . . . .  . .  Byte      0040     _DATA
Table2 . . . . . . . . . . DWord     0060     _DATA
UTILE  . . . . . . .  . .  Text      UTILE
VARX . . . . . . . . . .   DWord     0010     _DATA
WM_MAXI  . . . . . . . . . Number    0003h
WM_MINI  . . . . . .  . .  Number    0001h
bijor2 . . . . . . . . . . Byte      0161     _DATA
bijor  . . . . . . .  . .  Byte      0014     _DATA
debut  . . . . . . .  . .  L Near    0000     _TEXT
egal . . . . . . . .  . .  Byte      002A     _DATA
nomfichier . . . . .  . .  Byte      0031     _DATA
var1 . . . . . . . .  . .  Word      003C     _DATA
var2 . . . . . . . . . . . Word      003E     _DATA

 

10.4 Les données

Nous abordons ici une série de sections traitant de façon très générale des données. C'est un domaine central, qui fait intervenir un grand nombre de notions, parmi lesquelles nous rencontrons des mots comme constante , donnée , variable , type , tableau , structure , pointeur , et bien d’autres.

Ces objets ont un point commun, ils concernent tous la mémoire, plus précisément le positionnement de l'objet dans la mémoire, les valeurs que cette case ou zone mémoire est susceptible de prendre, ainsi que l’interprétation qu’il est possible d’en donner.

Tous ? en réalité, pas tout à fait. Une catégorie de données n’est connue que du programmeur et de l’assembleur/compilateur pendant la phase de construction du programme, à l’issue de laquelle elles n’existent plus. Certaines ne concernent que la cuisine interne d’assemblage, nous les verrons par ailleurs, dans le courant de la présentation de MASM. Mais d’autres ressemblent à ce que nous appelons, souvent à tort, des constantes , par opposition aux variables. Nous en parlerons, en insistant un peu sur la notion importante de connu à l'assemblage .

Les constantes appellent la notion de variables simples , sujet du sous-chapitre suivant. Une donnée ou variable simple est très grossièrement un nombre, occupant en mémoire une zone de 1 à 10 octets, voire plus aujourd’hui, cette zone possédant une adresse unique. Nous évacuerons pour l’occasion ce qui concerne la notation des valeurs numériques, les bases, les opérateurs, etc. Les notions de données simples à laquelle il faut accéder, d’adresse donc, nous amènera vers les pointeurs . La mise en collection, série, tableau de données simples débouchera sur les données structurées . Pointeurs et données structurées seront les titres des troisième et quatrième sous-chapitres de cette série. Toutefois, ces sujets ne sont pas indépendants, puisque les pointeurs sur structures seront évoqués avant les structures. Il faudra, une fois de plus, pratiquer la lecture ping-pong.

Avant d’aborder ces quatre sujets spécifiquement dans une optique assembleur, il n’est peut-être pas inutile d’aborder de façon plus intuitive les notions pas si évidentes de variables, type et visibilité.

Très schématiquement, dans un langage évolué, une variable est une case mémoire affublée d'un nom ou identifiant , unique dans un espace de nommage donné, et généralement d'un type . La mémoire est allouée pendant la durée de vie de la variable, et normalement libérée ensuite. De la mémoire est dite libérée, ou libre, quand le système d'exploitation la considère comme telle et qu'elle devient disponible pour répondre à de nouvelles demandes.

Nous utilisons un langage évolué, en l’espèce du C, pour illustrer nos propos :

#define varconst 13
 
long essai1(void)
{
  long ess_dword = varconst;
  return ess_dword;
}
 
long essai2(void)
{
  const long var = 12;
  //var ++;
  long ess_dword = var + 1;
  return ess_dword;
}

Un long est, dans le langage utilisé ici, un entier signé sur 32 bits.

Nous avons deux fonctions, essai1() et essai2() qui ne prennent pas d’argument, c’est le sens de void , et qui renvoient un long .

ess_dword est le nom, ou identifiant, d’une variable de type long , locale à la fonction essai1() . Le nom n’est ici connu que du compilateur. Cette variable va avoir une durée de vie très brève, le temps que dure l’appel à essai1() . Le nom ess_dword peut être utilisé plusieurs fois dans le même module, sans qu’il ne s’agisse de la même variable, pourvu que ce soit dans des procédures, fonctions ou blocs différents. C’est ainsi que le même nom, et non la même variable, est utilisé dans essai1() et essai2() . Ce qui n’est pas nécessairement une bonne idée. Nous verrons que MASM est moins souple sur ce plan. Remarquons que les deux variables ess_dword pourraient coexister, même dans un système monotâche, il suffirait pour cela que essai1() appelle essai2() , voire s'appelle elle-même de façon récursive. La visibilité du nom n’est pas liée à la coexistence.

Généralement, une variable locale est créée dans le cadre de pile de la fonction, zone mémoire qui est libérée à la fin de l'exécution de celle-ci. Cette notion est précisée dans un chapitre consacré spécifiquement à la pile et aux sous-programmes, intitulé Pile, cadres de pile et sous-programmes .

Le projet ( !) qui contient cet inénarrable extrait de code est sur le CD-Rom, dossier cppVariables . Vous pouvez y consulter le fichier vars.asm , vous constaterez que le compilateur, ayant noté que les trois fonctions renvoient la valeur constante 13, n’a créé aucune variable, se contentant d’un mov eax, 13 . Qu’il a néanmoins encadré :

push ebp
mov  ebp, esp
mov  eax, 13
pop  ebp
ret

Peut-être a-t-il ses raisons ?

Le modificateur const devant var pourrait nous faire croire que nous avons défini une constante. Perdu. Nous avons seulement signifié au compilateur que nous désirions que ne soit pas compilé du code qui conduirait à modifier la valeur de var . C'est un service que nous demandons au compilateur, une contrainte que nous nous imposons, pour tenter de détecter d'éventuelles erreurs de programmation. Si nous ôtons le symbole de commentaire  // devant var++; , qui incrémente var d’une unité, nous aurons une erreur.

Pourquoi utiliser des variables comme constantes ? Tout d'abord, ce n'est pas plus cher. Que 1560334032094 soit une variable non modifiable ou une constante vraie ne change rien au fait qu'il faudra que la valeur se promène avec le fichier exécutable (et les fichiers annexes). Mais le modificateur const est surtout utilisé dans les prototypes (= définitions) de fonctions. Il est possible ainsi de déclarer tel ou tel paramètre non modifiable par la fonction. Ce point ne concerne bien entendu pas les paramètres passés par valeur, mais ceux qui le sont par référence, de façon explicite ou implicite. Il est même possible, dans le cas d'un pointeur, de le déclarer constant, et/ou de déclarer constante la variable pointée.

Par contre, varconst défini à la première ligne est une véritable constante, ou mieux une substitution de texte, souvent nommée constante symbolique ou manifeste. La valeur 13 n'est connue que du compilateur qui, à chaque fois qu'en première lecture il rencontrera varconst , le remplacera par 13. Pour vous en convaincre, il suffit de remplacer la première ligne par :

#define varconst frigidaire

Cette ligne ne générera pas d'erreur. C'est à la ligne :

long ess_dword = varconst;

que le compilateur commencera à s'inquiéter : symbole "frigidaire" non défini .

L'identifiant varconst , remplacé mot pour mot par 13, n'est pas typé et pourra sans problème initialiser un long , comme un unsigned char ou un double (réel en virgule flottante). Elle sera valide à tous les endroits où saisir 13 aurait été accepté. D'autres langages que C proposent, entre ces constantes symboliques et les fausses constantes/vraies variables, de vraies constantes typées. C'est le cas du Pascal Objet de Delphi. Précisons maintenant cette notion de type .

Nous nous positionnons en langage machine, plus qu'en assembleur qui est déjà un langage plus ou moins évolué. Nous pouvons sans problème effectuer physiquement des transferts entre des variables (au sens case mémoire) ou des registres de même taille. Nous pourrions même envisager des conversions logiques, sans utiliser d'instruction spécifique. Pour transférer BX (16 bits) dans EAX (32 bits), sans erreur arithmétique, il suffit de faire :

mov  eax, 0   ;ou xor eax, eax
mov  ax, bx

Le transfert inverse EBX dans AX ne peut se faire sans risques que dans le cas où nous avons de bonnes raisons de savoir que les 16 bits forts de EBX sont à 0.

Au chapitre de la numération, nous avons abordé un problème relativement troublant. Une case mémoire ou un registre n'a pas physiquement de signe. Ce sont 8, 16 ou 32 bits allumés ou éteints, point final. Simplement, nous pouvons interpréter cette valeur comme un entier naturel (de 0 à 255 pour 8 bits), ou comme un entier signé en complément à 2 (-128 à +127), ou autre chose éventuellement. Les instructions vont fonctionner sur cette case mémoire sans savoir comment nous l'interprétons. En revanche, certaines instructions n'auront de sens que pour la bonne interprétation. Par exemple :

mov var, 131 ; 10000011 en binaire
neg var      ; 01111101 en binaire

neg inverse le signe de l'opérande et n'a de sens que si nous n'interprétons cet opérande comme étant signé. Nous attendons -131 comme résultat. Mais nous avons fait une erreur, en ne remarquant pas que +131 dépassait la capacité d'un 8 bits signé. Rien ne va nous signaler cette erreur, et le résultat sera +125, que nous l’interprétions en signé ou en non signé. L'explication est que 131 interprété en non signé à la même représentation binaire que -125 interprété en signé.

Pour en revenir aux langages évolués, et donc peut-être aux macro-assembleurs comme MASM, l'idée est donc de demander à l'assembleur ou au compilateur de détecter dans la mesure du possible ces erreurs. Nous allons créer des types, par exemple entier 8 bits signé et entier 8 bits non signé. Les affectations entre deux données de types différents ou par une valeur immédiate qui dépasse l'étendue du type seront soit refusées, soit signalées. MASM ne va pas bien loin dans ce sens. C/C++ est un peu mieux, mais malgré tout décevant.

Prenons ce bout de code en C++ :

signed   long s;
unsigned int u;
char t;
...
t = s + u;

En paramétrant correctement, nous aurons au mieux l'avertissement La conversion peut perdre des chiffres significatifs . C'est un minimum. Il suffit de modifier la dernière ligne pour que l'avertissement disparaisse :

t = (char)(s + u);

Ceci signifie un forçage de type , ou transtypage , ou cast . Ça n'améliore pas vraiment la conversion, mais ça signifie que le programmeur sait ce qu'il fait.

Maintenant, supposons que dans une application de gestion d'une population, une variable v_age contient l'âge d'un individu, une autre v_ poids contient son poids. Il est logique de coder ces deux variables sur 8 bits non signé. Moyennant quoi, en C :

unsigned char v_age, v_poids, x, y, z;
...
x = v_age;
y = v_poids;
z = x + y;
x = y;

Tout va bien se compiler, le microprocesseur est content, et le programmeur un peu moins. En effet, il aimerait bien que son outil lui signale qu'il additionne des kilos et des années, puis qu'il affecte un âge à un poids. Un langage sera dit fortement typé s'il propose un nombre important de types différents, et surtout est rigoureux dans les vérifications de concordance de type. Il faut bien être conscient que les contraintes amenées par le typage fort sont une qualité d'un compilateur. Il est toujours possible de passer outre ces contraintes par le transtypage. Nous aurons l'occasion d'en reparler au chapitre des pointeurs.

Il est possible de distinguer les types fondamentaux et les types génériques . Les premiers sont définis par leur taille de façon définitive : un entier 16 bits signé. Un type générique peut voir sa taille varier avec les versions du compilateur, dans le but de coller à la taille des registres. Ceci concerne par exemple les entiers d'usage général, comme les compteurs de boucle. Le type générique Integer de Delphi, dans les versions 6 et 7, fait 32 bits. Ce gadget, toujours simulable par une macro, serait peu utile en assembleur.

Voilà, nous sommes assez loin de l'assembleur, et si tout n'est pas clair, ce n'est pas très grave. Nous possédons maintenant suffisamment de vocabulaire pour nous attaquer à l’application de ces diverses notions à l’assembleur MASM.

 

10.5 Constantes

Nous nous pencherons d'abord sur la notion même de constante, et plus particulièrement sur le point de savoir ce qui est connu au moment de l'assemblage et ce qui ne l'est pas. Il y a une différence essentielle entre les deux catégories, et le sujet vaut qu'il lui soit consacré quelques lignes, et même plus. Nous passerons ensuite à une partie plus technique, avec les equates de MASM.

10.5.1 Notion de constante

Vous trouverez sur le CD-Rom des listings plus complets que les extraits proposés dans les lignes qui suivent, sous le dossier Constantes . L’utilité de ces programmes est faible, simplement tester la compilabilité de certaines syntaxes. Il est surtout intéressant de consulter simultanément le fichier listing test.lst .

Pour ne pas multiplier à l’infini le nombre de listings, de nombreuses lignes sont mises en commentaire. Il s’agit bien entendu de pouvoir tester diverses possibilités. Dans le morceau suivant, la partie dite utile ne sert qu’à comparer le contenu de BX et celui de AX, et à afficher Égal en cas d’égalité :

 

   VAL1   =        23
   VALEUR =        69
;  VALEUR EQU      69
;  VALEUR TEXTEQU <69>
 
.DATA
bijor       db      "Bienvenue dans test",0Dh, 0Ah,"$"
egal        db      "Egal",0Dh, 0Ah,"$"
 
 
;JUMPS    ; Décommenter pour TASM
 
.CODE
 
debut:   mov ax, @data ; initialisation du data segment
         mov ds, ax
         mov es, ax
         
 
         ; Message de bienvenue
         mov ah, 09h
         mov dx, offset bijor
         int 21h
 
         ; Partie “utile” (!!)
;        mov ax, 69
         mov ax, 60 + (3 * 3)
;        mov bx, VALEUR
         mov bx, VAL1 * 3    
         cmp ax, bx
         jne @F
         
         mov ah, 09h
         mov dx, offset egal
         int 21h         
@@:
         mov AH, 4Ch             ;fin du programme
         int 21h                 ;et retour au DOS
         END debut 

Dans l’instruction mov ax, 69 , le nombre 69 est-il une constante ? Pas exactement, puisque cette valeur 69 ou 45h fait partie du code exécutable en tant que valeur immédiate. D’un autre côté, à l’endroit où nous avons saisi 69, l’assembleur va accepter tout ce qu’il est capable de transformer en valeur numérique déterminée sans ambiguïté au moment de l’assemblage, qui pourrait être nommé une constante numérique manifeste .

Les endroits où MASM va accepter une constante de ce type dépassent largement les opérandes immédiats du code. Il semble par exemple clair que dans la définition d’une telle constante, d’autres constantes, reliées par des opérateurs de calcul (+, -, *, /, etc.) peuvent très bien remplacer une valeur numérique. Et c’est effectivement le cas.

Anticipons sur le sous-chapitre suivant. Nous verrons que, quand nous déclarons une variable par une directive comme dw , nous pouvons ou non l’initialiser. Il est évident maintenant qu’une constante numérique manifeste conviendra aussi bien qu’une valeur immédiate pour cette initialisation.

Il est inutile de mémoriser énormément de choses. Une fois la logique de fonctionnement bien assimilée, il est relativement facile de savoir ce qu’il est possible de saisir et à quel endroit. En fait, un point nodal de la compréhension est dans la connaissance de ce qui est connu au moment de l’assemblage, et de ce qui est connu uniquement à l’exécution. Un exemple très simple :

VALEUR = 69
…
.data
…
VALUE  dw 69
…
mov ax, VALEUR
mov ax, VALUE
mov ax, [VALUE]         
mov ax, word ptr VALUE
mov ax, word ptr [VALUE]

Il suffit de jeter un regard sur le fichier test.lst  :

0026  B8 0045          mov ax, VALEUR
0029  A1 0026 R        mov ax, VALUE

Les quatre dernières lignes génèrent le même code, c’est une particularité syntaxique de MASM, qui n’est pas le sujet immédiatement. Ce qui importe, c’est que les opcodes sont différents : mov ax, VALEUR génère simplement un adressage immédiat ( 0045h  = 69), exactement comme si la ligne avait été mov ax, 69 . Alors que mov ax, VALUE nous donne un adressage mémoire, un offset dans le segment DS. La lettre R indique que la variable est relogeable, c’est-à-dire située dans une table résolue par le lieur, à l’aide de la valeur 0026h . Si le contenu de la variable VALUE est inconnu à l’assemblage, nous pouvons par contre considérer que nous connaissons son offset. Nous allons voir pourquoi.

La notion de connu à l’assemblage demande à être nuancée. Eh oui, en assembleur, c’est souvent presque très simple.

Nous savons que le processus qui part du code source pour aboutir non pas au fichier  .exe mais à l’exécutable installé en mémoire est complexe et fait intervenir trois outils principaux : l’assembleur, le lieur et le système d’exploitation par l’intermédiaire du loader. Nous savons également que tout n’est pas résolu à l’issue de la première phase, loin de là, et qu’il reste des cases vierges, des tables. Nous savons surtout que sous DOS, le contenu des registres de segments sera connu uniquement au lancement du programme. L’exemple le plus troublant est donc la ligne :

debut:   mov ax, @data ; initialisation du data segment

Le .lst nous donne :

0000  B8 ---- R     debut:   mov ax, @data ; initialisation du data segment

Nous retrouvons le R de relogeable, mais l’opcode B8h est bien celui d’un MOV immédiat. Ce dernier point ne laisse place à aucune ambiguïté sur la nature de @data . Selon les documentations, il est fait état de constante ou de variable. En fait, c’est une valeur 16 bits laissée en blanc jusqu’au lancement. Mais au moment où commencera à tourner le programme, toutes ses occurrences auront été remplacées par une même valeur. Donc, elle a indubitablement pour nous au moment de l’écriture du code la dimension d’une constante manifeste imposée.

Vous pouvez lancer une session type DOS dans un dossier contenant le \Données\Constantes\test.exe du CD-Rom et ensuite saisir debug test.exe . Il suffit de taper une ou deux fois u (nassemble) pour voir les valeurs réelles remplaçant les valeurs relogeables.

Avant d’entamer un long pèlerinage dans la syntaxe, récapitulons :

  Nous considérons comme inconnu à l’assemblage (ou variable) le contenu des mémoires et des registres d’une façon générale. Attention, une variable initialisée à une valeur et dont le programmeur sait qu’elle ne changera pas conserve dans notre optique son statut de variable.

  Dans ce qui est connu à l’assemblage, il y a d’abord les vraies constantes que le programmeur déclare ou définit. Elles n’ont qu’un rôle temporaire de remplacement.

  Nous avons enfin le connu sans être connu : par exemple l’adresse effective de la variable VALUE . Nous ne connaissons numériquement ni son segment, ni son offset, mais nous pouvons écrire :

mov bx, @data
mov cx, offset VALUE 

Ces deux lignes se résolvent en des adressages immédiats, donc @data et offset VALUE sont bien des constantes.

Attention

La taille des constantes n’est pas vérifiée

Si une constante, issue d’un equate par exemple, avec ou sans intervention d’opérateurs de calcul, est plus grande que le maximum de la valeur immédiate qu’elle remplace, aucune erreur n’est générée, et la constante est simplement tronquée :

ZERO  =  00FF0000h
mov ax, ZERO

s’assemble sans problème, mais en :

003F  B8 0000         mov ax, PASZERO

Pas glop pas glop…

10.5.2 Les equates

Le but des equates est de créer un symbole défini par le programmeur et de lui affecter une valeur, au sens large. Le symbole sera ensuite utilisé dans le code source en lieu et place de la valeur. Le remplacement sera effectué en début de phase d’assemblage, de façon plus ou moins mécanique. Le rapport avec le #define de C/C++ est clair, mais l’analogie imparfaite.

Les equates sont les trois directives = , EQU et TEXTEQU . Nous avons donc d’une façon générale :

Symbole    =    Valeur
Symbole   EQU   Valeur
Symbole TEXTEQU Valeur

Valeur peut être de type numérique, chaîne de caractères (string), ou textuel, mais pas de la même façon pour les trois equates.

Les types numériques sont entier, réel (ou virgule flottante) et BCD (Binary Coded Decimal). Seuls les entiers ont un sens particulier par rapport aux equates.

Les strings peuvent être considérées comme un type numérique particulier, puisqu’il s’agit de tableaux d’octets, donc de véritables données.

Le type texte ou textuel est particulier, en fait c’est une forme de macro. Le terme macro de substitution conviendrait bien. Elles partagent d’ailleurs leur syntaxe avec les macros. Le texte situé entre les crochets < et >  est substitué tel quel au nom de la macro.

Donner un nom français aux trois types numériques, au type chaîne et au type textuel pose un vrai problème. Même les noms anglais, qui sont bien entendu ceux que vous trouverez le plus souvent, varient selon la source et même la page dans la source. Disons au moins que, pour les deux derniers, nous avons d’un côté chaîne (de caractères), string, qui sont des données, et de l’autre côté texte, textuel, text, textual, text macro, qui sont des macros. Nous utiliserons macro texte .

De nombreux symboles prédéfinis sont simplement des macros textes. Par exemple, @FileName est une macro texte qui, pour un fichier TEST.asm serait virtuellement définie en interne par :

@FileName TEXTEQU <TEST>

 

Symbole  =  Expression.

Assigne à Symbole la valeur numérique de Expression. Ce n’est pas explicitement indiqué dans la référence actuelle, mais cette valeur ne peut être qu’une valeur entière , éventuellement calculée, à partir de valeurs numériques ou d’autres constantes symboliques.

Symbole peut être redéfini plusieurs fois au cours du code. Cette redéfinition est utile pour des valeurs de boucles ou de dimensions de tableaux, pour ne pas chercher un nouveau nom à chaque fois. Une structure relativement souple à utiliser pourrait ressembler à :

TailleTable = 64
ValInit     = 0
…
.DATA
Table1 byte TailleTable dup (?)
…
.CODE
mov cx, TailleTable
mov bx, 0
@@:
mov Table1[bx], ValInit
inc bx
loop @B
…
TailleTable = 32
ValInit     = 0FFh
.DATA
Table2 byte TailleTable dup (?)
.CODE
mov cx, TailleTable
mov bx, 0
@@:
mov Table2[bx], ValInit
inc bx
loop @B

 

 

Symbole TEXTEQU <ExpressionTexte>.

Symbole TEXTEQU AutreMacro.

Symbole TEXTEQU %(Calcul numérique entier).

Cet equate ne définit que des macros textes.

Dans le premier cas, le texte entre <  et >  est lettre à lettre assigné à Symbole. Ce peut être des expressions lourdes à gérer. Par exemple, pour des variables passées par la pile, l’expression suivante sera pratique :

Param TEXTEQU <[BP+4]>

Dans le deuxième cas, AutreMacro sera une autre macro texte, une fonction macro ou un symbole prédéfini renvoyant du texte, etc. Par exemple :

VALT3 TEXTEQU @FileName

Le troisième cas permet de remplacer une constante entière par le texte représentant sa valeur calculée.

VALT2     TEXTEQU %(3*15)

Est équivalent à :

VALT2     TEXTEQU <45>

Il existe un grand nombre de possibilités autour des macros textes, que vous découvrirez peu à peu avec d’autres sujets, comme les chaînes de caractères et les macros. Certaines sont superflues, il est préférable de travailler avec une petite boîte à outils que vous connaissez bien et de consacrer vos efforts à la programmation. Voyons ou récapitulons simplement quatre opérateurs de base, vus avec plus de précision dans le contexte général des macros :

<…>  : est le délimiteur de texte.

!  : opérateur de caractère littéral. Il fait perdre au caractère qu'il précède son rôle symbolique s’il en a un, pour le transformer en simple caractère. Si par exemple nous voulions faire une macro texte pour remplacer le texte MaClasse->Taille , nous aurions un petit problème. Il suffit de faire :

VALT5     TEXTEQU <MaClasse-!>Taille>

%  : est l’opérateur d’expansion. Nous en avons vu l’effet, qui est de convertir des nombres en texte. Placé en début de ligne, il a l’effet particulier de forcer à développer les macros qui se trouvent sur le reste de la ligne et qui pourraient ne pas l’être parce que, par exemple, entre guillemets.

&  : est l’opérateur de substitution. Il identifie de façon certaine un élément en cas d’ambiguïté.

Les symboles définis par TEXTEQU peuvent être redéfinis, du moins par TEXTEQU et EQU . Pour  = , cela dépend de la valeur affectée par TEXTEQU .

 

Symbole EQU Expression

Le comportement de EQU est un peu compliqué, voire d’un certain niveau d’imprévisibilité. Si Expression est encadrée de <  et >  elle se comporte comme TEXTEQU dans les mêmes conditions. Si Expression peut s’évaluer comme un entier, EQU se comporte comme  = . C’est-à-dire que Symbole représente un nombre. C’est par exemple le cas de EQU ‘A’ , qui se transforme en 0041h . Dans les autres cas, Symbole est considéré comme du texte. Mais Expression aura éventuellement été modifiée, si une valeur réelle peut être détectée.

Dans la mesure du possible, il vaut mieux utiliser au maximum  = et TEXTEQU pour éviter les ambiguïtés.

Les possibilités de redéfinition d’un symbole, par la même directive ou par une autre, sont parfois imprévisibles. De plus, cette possibilité dépend, dans le cas de EQU , du fait que la valeur est ou non un entier.

Ce que nous devons retenir, c’est qu’un symbole défini par un EQU et à valeur entière n’est redéfinissable en aucune façon. Le but est certainement de pouvoir ainsi protéger les symboles définis dans les fichiers .inc par la directive EQU .

Les possibilités de saisie des constantes numériques, particulièrement dans d'autres bases que le système décimal, seront vues à la rubrique suivante, celle des variables.

10.6 Variables simples

L'idée de variable, qui n'est pas immédiate, a été présentée avec les généralités sur les données. Une dernière précision : écrivons une affectation, X = A , X := A ou même X <- A . Vous entendrez parfois parler de left-value pour X (et plus rarement de right-value pour A). Une left-value doit pouvoir recevoir une valeur. En d'autres termes, version assembleur, c'est de la mémoire. C'est une variable. Une right-value doit pouvoir être résolue en une valeur immédiate. Ce peut être une constante, mais également une variable, mais dans son rôle de valeur actuelle.

Pour créer des variables et des constantes stockées en mémoire (pour nous des variables), généralement dans un segment de données, l'assembleur utilise des directives d'allocation ou réservation, de mémoire, et les affuble ensuite d'une étiquette ou label. Physiquement, il s'agit bien d'un label, c’est-à-dire d'un offset. Mais c'est également ce qu'il est coutumier d'appeler le nom de la variable. En assembleur, la définition ou déclaration (je dis que Var1 est une variable entière non signée sur 8 bits) et la réservation mémoire (je dis que cet octet désigné par Var1 est situé ici) sont généralement simultanées, de la forme :

NomVariable DIRECTIVE Initialiseur

Il y a au moins une exception à la simultanéité déclaration–allocation : la directive LABEL qui permet justement de définir une étiquette ou label à la position courante de l'offset (ou compteur de position) et de lui affecter simplement un type, simple ou pointeur :

NomLabel LABEL  TypeQualifié

NomLabel LABEL [[NEAR | FAR | PROC]] PTR [[ TypeQualifié ]]

Pour tout dire, nous avons cherché à quoi ce truc pouvait bien servir. Sans avoir testé si c'est possible autrement, c'est une solution propre et simple pour situer plusieurs labels, éventuellement de types différents, au même endroit en mémoire. Par exemple, il pourrait être pratique de pouvoir initialiser une zone par des données textes nécessaires en début d'exécution, puis réutiliser la même zone comme tampon d'entrée organisé en WORD. Nous aurions :

bouffeur LABEL WORD
mess1    szSTR \
"Je suis le Ténébreux, - le Veuf, - l'Inconsolé,", 13,10,
"Le Prince d'Aquitaine à la Tour abolie :", 13,10,
"Ma seule Etoile est morte, - et mon luth constellé", 13,10,
"Porte le Soleil noir de la Mélancolie. ", 13,10,0
ORG 256

szSTR est un TYPEDEF BYTE , comme nous le verrons un peu plus loin. Le ORG 256 est une option, c'est une des méthodes pour sur-réserver de la mémoire en début de segment. Donc, si mess1 ne sert qu'à afficher un message de bienvenue, la zone sera ensuite utilisée comme un tampon de 128 WORD, soit 256 octets.

Les directives de déclaration simple sont réparties en deux groupes. Les directives de réservation  DB , DW , DD , DF , DQ  et DT  réservent simplement 1, 2, 4, 6, 8 et 10 octets en mémoire, sans grande précision de type. Elles sont les plus anciennes et leur usage ne devrait pas être recherché. D'autres directives, BYTE , WORD , etc. (voir tableau) remplissent le même rôle, mais en renseignant mieux sur le type de la variable, pour une meilleure lisibilité et les rares vérifications de types qu’effectue l'assembleur. Pour être plus précis, les versions antérieures à 6.0 de MASM distinguaient la réservation (DB) et la déclaration de type (BYTE, SBYTE). La seconde catégorie suffit aujourd'hui pour déclarer et réserver. Voyons, dans un premier temps, ce qui concerne le type de la donnée déclarée.

 

10.6.1 Types de base, types qualifiés

Dans le tableau suivant sont données les douze directives décrivant chaque type de base. Pour chacune est donnée également la directive non typée correspondante ainsi qu'une brève description.

Les types de base de données entières et réelles

Type de base

Directive ancienne

Description

BYTE

DB

Mot non signé sur 8 bits (octet). Usages très variés.

SBYTE

DB

Mot signé sur 8 bits (octet). Valeur numérique.

WORD

DW

Mot non signé sur 16 bits. Usages très variés.

SWORD

DW

Mot signé sur 16 bits. Valeur numérique.

DWORD

DD

Mot non signé sur 32 bits. Usages très variés.

SDWORD

DD

Mot signé sur 32 bits. Valeur numérique.

FWORD

DF

FAR WORD, sur 48 bits, en général un pointeur de format SEGMENT16:DEPLACEMENT32 .

QWORD

DQ

Mot sur 64 bits, ou 8 octets. Usage possible : FPU.

TBYTE

DT

Mot sur 80 bits, ou 10 octets. Usage possible : FPU, dont BCD.

REAL4

DD

Nombre réel 32 bits (short).

REAL8

DQ

Nombre réel 64 bits (long).

REAL10

DT

Nombre réel 80 bits (extended).

 

Nous n’entrons pas dans la complexe, et pour nous peu utile, théorie sur les types qualifiés , issue de la documentation Microsoft et à base de grammaire BNF. Contentons-nous de voir ces douze types de base comme les douze premiers types qualifiés. D'autres types qualifiés s'en déduisent, de fil en aiguille, par la directive TYPEDEF , selon le schéma :

NouveauTypeQualifié TYPEDEF TypeQualifié

TYPEDEF peut également créer des types pointeur (voir ce à sujet le sous-chapitre qui leur est réservé) :

NouveauTypeQualifié TYPEDEF [[ distance ]] PTR TypeQualifié

Cette création de nouveaux types est parfois nécessaire, pour que la directive INVOKE joue le rôle attendu par exemple. Il est également agréable de pouvoir se mettre en conformité avec un langage de haut niveau ou avec ses propres habitudes, par la directive TYPEDEF .

Les exemples de ce chapitre et des suivants se font à partir d'une base MASM32, en mode Console sous Windows, FLAT,  .386 . Nous pourrons ainsi au besoin utiliser les fonctions de la bibliothèque. Le dossier pour les variables simples est Données\variables simples . La majorité de ces exemples consisteront à vérifier la compatibilité, en s'aidant au besoin du fichier listing. D'ailleurs, tous les exemple donnés ne sont pas destinés à être exécutés. En voici un échantillon :

szSTR    TYPEDEF BYTE
CHAR     TYPEDEF BYTE
extended TYPEDEF REAL10
EXTENDED TYPEDEF REAL8
 
TQ1      TYPEDEF BYTE
TQ2      TYPEDEF TQ1
PTQ      TYPEDEF PTR TQ2

TQ1, TQ2 et PTQ sont maintenant des types qualifiés. Ils sont tous dérivés à partir de la graine BYTE. Utilisons quelques-uns de ces types pour des déclarations :

message     szSTR    "Je reserve un peu de place",0
a_la_ligne  szSTR    13,10,0
dix_octet1  extended 12.4
dix_octet2  DT 12.4
dix_octet3  EXTENDED 12.4
un_caract   CHAR     'X'

Ce qui va être généré pour les flottants est intéressant, nous le verrons dans les sections suivantes. Contentons-nous pour l'instant de constater que la définition de types dérivés est sensible à la casse. Pour confirmer l'ensemble de ces points, voyons dans le fichier listing, table des symboles, rubrique des types :

EXTENDED . .  00000008     QWord
NPTQ . . . .  00000004     FarPTR Byte
PTQ  . . . .  00000004     FarPTR Byte
TQ1  . . . .  00000001     Byte
TQ2  . . . .  00000001     Byte
extended . .  0000000A     TWord
szSTR  . . .  00000001     Byte

Il est également intéressant de voir (sur le CD-Rom) la liste des variables. Notez à l'occasion dans le code source l'utilisation quasi impérative des directives .NOCREF et .CREF encadrant les inclusions de fichiers d'en-tête.

10.6.2 Réservation et initialisation

Nous avons pratiquement vu la base de la réservation, ou allocation, de mémoire qui crée une variable, sous la forme :

NomVar TypeQualifié Valeur

Par exemple :

message     szSTR    "Je reserve un peu de place",0
a_la_ligne  szSTR    13,10,0
dix_octet1  extended 12.4
dix_octet2  DT 12.4
dix_octet3  EXTENDED 12.4
un_caract   CHAR     'X'
un_octet    BYTE 04h
un_mot      WORD ?
un_grosmot  DWORD 12*sizeof PTQ

Dans le cadre des variables simples, nous nous limitons à une seule valeur d'initialisation. Valeur est toute expression se résolvant en une constante numérique compatible avec l'affectation. Donc, la syntaxe utilisée pour initialiser une variable numérique est celle de la constante numérique correspondante. Ce sera l'objet des sections suivantes. Le  ? laisse l'assembleur initialiser la variable à sa guise, c’est-à-dire ne pas initialiser : la variable aura une valeur aléatoire correspondant à l'état de la mémoire allouée, ou une valeur choisie par l'assembleur, à priori 0, mais il est préférable de ne pas compter là-dessus : pour initialiser à 0, autant le faire explicitement.

Voyons dans le fichier listing le résultat de ces déclarations-réservations-initialisations. D'abord, le positionnement en mémoire, un offset par rapport au segment de données :

a_la_ligne . .  . . . . Byte   0000001B _DATA
debut  . . . .  . . . . L Near 00000000 _TEXT Public STDCALL
dix_octet1 . .  . . . . TWord  0000001E _DATA
dix_octet2 . .  . . . . TWord  00000028 _DATA
dix_octet3 . .  . . . . QWord  00000032 _DATA
essdword . . .  . . . . DWord  0000003B _DATA
message  . . .  . . . . Byte   00000000 _DATA
un_caract  . .  . . . . Byte   0000003A _DATA
un_grosmot . .  . . . . DWord  00000042 _DATA
un_mot . . . .  . . . . Word   00000040 _DATA
un_octet . . .  . . . . Byte   0000003F _DATA

Voyons maintenant les valeurs initiales données par MASM :

00000000             .data
00000000 4A 65 20 72 65 message szSTR "Message",0
     73 65 72 76 65
     20 75 6E 20 70
     65 75 20 64 65
     20 70 6C 61 63
     65 00
0000001B 0D 0A 00       a_la_ligne  szSTR    13,10,0
0000001E                dix_octet1  extended 12.4
     4002C666666666666666
00000028                dix_octet2  DT 12.4
     4002C666666666666666
00000032                dix_octet3  EXTENDED 12.4
     4028CCCCCCCCCCCD
0000003A 58             un_caract   CHAR     'X'
0000003B 00000000       essdword    DWORD    0
0000003F 04             un_octet    BYTE     04h
00000040 0000           un_mot      WORD     ?
00000042 00000030       un_grosmot  DWORD    12*sizeof PTQ

un_grosmot est initialisé à 12 fois la taille d'un pointeur PTQ, 4 octets, c’est-à-dire 48 ou 30h . un_mot est initialisé à 0, c'est logique puisque nous sommes au sein d'un segment de données initialisées.

 

10.6.3 Les nombres entiers

Pour initialiser une variable, mais également pour définir une constante, qu'il s'agisse d'un equate ou de la saisie d'une valeur immédiate d'opérande, il est nécessaire de savoir exprimer un nombre entier. Ce nombre sera utilisé seul ou au sein d'une expression comportant des opérateurs, des constantes déjà définies, d'autres nombres.

Par défaut, MASM comprend les nombres exprimés naturellement en décimal. Il vérifie la taille de la donnée pour les variables et les constantes numériques, mais se préoccupe peu du signe :

      un_octet    BYTE    255
      ;un_octet2   DB       256
      un_mot      SWORD   60000
      essai1      EQU     500000
      un_mot2     WORD    -1
      ;un_mot3     WORD    70000

Les lignes 2 et 6 sont rejetées, la constante étant trop grande. En revanche, 60000 est accepté pour un SWORD, alors qu'une telle variable devrait être limitée à 32767. La valeur -1 est également valide pour initialiser un WORD. Cette valeur est souvent utilisée en tant que valeur non signée maximale, FFh pour un BYTE, FFFFh pour un WORD, etc.

Rappelons que nous avons constaté qu'à l'inverse MASM ne vérifiait pas toujours la taille d'une constante immédiate dans le code, par exemple dans MOV AX, 100000 qui est accepté, la constante étant tronquée.

Nous constatons donc que le base de numération utilisée par défaut est la base 10. Nous pouvons en changer par la directive .RADIX   Expression . Il suffit de donner à Expression une valeur entière comprise entre 2 et 16 pour que la base par défaut soit modifiée. Il est possible d'utiliser plusieurs fois cette directive au cours du programme. Testons, en modifiant nos lignes précédentes :

      .RADIX 16
      un_octet    BYTE    255
      .RADIX 10

Naturellement, nous obtenons un message d'erreur, 255h valant 597. Changeons 255 en FF. A nouveau un message d'erreur. Cette fois-ci, MASM ne reconnaît pas FF comme une valeur numérique, mais comme un symbole non défini. La raison est simple : toute constante entière doit débuter par un chiffre compris entre 0 et 9. Puisque nous utiliserons rarement de base entre 11 et 15, la base 16 et la seule concernée par ce détail. Il suffit d'ajouter un 0 devant le nombre. Changeons don FF en 0FF et tout rentre dans l'ordre.

Comment faire pour saisir un nombre dans une base qui n'est pas la base par défaut ? Le cas général est celui dans lequel ne voulons pas changer de base par défaut à tout bout de champ et malgré tout utiliser selon le type de donnée les bases 2 et 16.

Il faut pour cela utiliser un suffixe indiquant la base. Pour la base 16, ce suffixe est h . Pour la base 2, c'est y ou b . Pour la base 8, ou octal, c'est o ou q . Enfin, pour le décimal, la base 10, c'est t ou d .

Pourquoi des choix, alors qu'un seul suffixe par base aurait convenu ? Les lettres naturelles étaient h , b , o et d . Mais si la base par défaut est la base 16, b et d sont ambigus, puisqu'il s'agit également de chiffres hexadécimaux. Quant à o , il ressemble trop au zéro. 0F1h, 1234h, 00100011d, 45789t sont des valeurs hexadécimales (deux fois), binaire et décimale valides.

Terminons par une petite curiosité : dans quelle base exprimer l'argument de la directive .RADIX ? Il est préférable de s'en tenir aux valeurs 2, 10, 16 et plus rarement 8, exprimées en décimal, qui sont toujours acceptées. Mais vous pourrez constater que .RADIX 10h (pour passer en base 16) fonctionne, ainsi que .RADIX 0A (pour passer en base 10), sans que la base actuelle n'entre en considération. 

 

10.6.4 Les nombres réels

Nous sommes supposés connaître la FPU, non pas nécessairement son fonctionnement mais les types de données qu'elle traite. Pour ce qui est des nombres réels, ils sont de trois types, codés sur 32, 64 et 80 bits, soit 4, 8 et 10 octets. Ils correspondent respectivement aux directives de réservation DD , DQ et DT , et aux types REAL4, REAL8 et REAL10. Vous pouvez utiliser un TYPEDEF pour utiliser des noms de type que vous connaissez, issus du C/C++, par exemple Single, Double et Extended.

Les types de réels et leur étendue

Type

Étendue

Chiffres significatifs

Octets

REAL4, Single

1.5 x 10  –45 ... 3.4 x 10  38

7-8

4

REAL8, Double

5.0 x 10  –324 ... 1.7 x 10  308

15-16

8

REAL10, Extended

3.6 x 10  –4951 ... 1.1 x 10  4932

19-20

10

 

La forme générale de saisie en base décimale d'une valeur en virgule flottante est la suivante :

[[+ | –]] Entier [[ Fraction ]][[E[[+ | –]] Exposant ]]

Fraction est un nombre réel positif compris entre 0 (compris) et 1 (exclus), qui donc commence par un point décimal. Attention, la forme générale n'est pas très claire : le point décimal est nécessaire, même suivi de 0 ou de rien. Les saisies suivantes sont valides :

fvar1  REAL8  1.545
fvar2  REAL8  -1.545E0
fvar3  REAL8  1.
fvar4  REAL8  1.E-10

En revanche, les notations suivantes sont refusées :

fvar5  REAL8  -1.545E1.2
fvar6  REAL8  4
fvar7  REAL8  1E-10

Il est également possible, bien que nettement moins prisé, d'initialiser un réel directement en hexadécimal, en le postfixant de la lettre r (ou R) :

fvarh1 REAL4   3F800000r
fvarh2 REAL8   3FF0000000000000r
fvarh3 REAL10  3FFF8000000000000000r

Ces trois valeurs valent... 1, ou plus exactement 1.0, et il est impossible d'utiliser le signe négatif -.

Une fois initialisées, ces variables s'utilisent facilement :

fvar2  REAL8  -1.545E0
fvar3  REAL8  3.
...
fld  fvar2
fmul fvar3
fstp fvar2
invoke FloatToStr,fvar2, ADDR message
invoke StdOut,ADDR message

 

10.6.5 Les nombres BCD

Les nombres BCD non compactés n'ont pas besoin de techniques d'initialisation particulière, il suffit d'utiliser la directive BYTE :

;choix 1
bcd_nc BYTE 0,0,0,5,1,0,0,0
;choix 2
bcd_nc BYTE 0,0,0,1,5,0,0,0

Cet exemple initialise un QWORD à 15000, deux choix sont possibles selon la façon de mener les calculs.

Le cas des BCD compactés de la FPU est différent. Rappelons que ces nombres contiennent au plus 18 chiffres (compactés sur 9 octets d'un registre FPU) et d'un octet de signe. Pour obtenir facilement un BCD compacté au format de la FPU, il faut initialiser une variable TBYTE par une constante décimale. Il faut donc soit que la base 10 soit par défaut, soit utiliser le suffixe t :

pos1 TBYTE  1234567890t
neg1 TBYTE -1234567890t

Le fichier listing donne les résultats interprétés comme du décimal mais traduits en hexadécimal : 000000000000499602D2 et FFFFFFFFFFFFB669FD2E. Mais les octets en mémoire sont bien :

00 00 00 00 00 12 34 56 78 90h et 80 00 00 00 00 12 34 56 78 90h .

 

10.7 Pointeurs

Dans tous les langages où ce type existe, un pointeur n'est rien d'autre qu'une variable simple un peu particulière. Il est donc pertinent de consacrer un sous-chapitre spécifique au sujet et de le situer entre les variables simples et les données complexes ou structurées.

Nous allons consacrer plus de lignes à présenter la notion générale de pointeur qu'à développer la façon dont MASM les traite. Il se trouve qu'il les traite un peu par-dessus la jambe, et que ce n'est pas une raison pour partir sur de mauvaises bases. Nous allons donc étudier ce que doit être un pointeur, puis l'approche que nous pouvons en avoir avec MASM.

10.7.1 Notion de pointeur

Commençons par une brève présentation générale, à base de langage C++ et Pascal, de la notion de pointeur. Si cette introduction nous paraît claire, nous aurons ensuite le plaisir de constater que les choses sont très simples en assembleur, celui-ci permettant de mieux "voir le moteur" et un peu de déception de voir que la notion de pointeur en tant que type y est moins nette.

Si vous avez des rapports inamicaux avec les pointeurs, collez sur le bord de votre moniteur un Post-it sur lequel vous aurez écrit :

UN POINTEUR EST UNE VARIABLE – UN POINTEUR N'EST PAS UNE ADRESSE

Comme toute variable, vu du compilateur ou assembleur, un pointeur est un nom , ou identifiant , un type , et éventuellement une valeur . Vu de la machine, c'est une place réservée quelque part en mémoire, cette zone étant ou non initialisée , c’est-à-dire remplie ou non d'une valeur déterminée. Ce dernier point étant ici très important et source d'erreurs fréquentes.

Un "unsigned int" désigne une variable de type unsigned int . De la même façon, le terme "un pointeur sur unsigned int" désigne une variable de type pointeur sur unsigned int . En première approche, à chaque type Chose connu peut correspondre un type dérivé, le type pointeur sur Chose . Cette définition est parfaitement récursive, et à chaque fois qu'un type pointeur est créé, il est possible d'en dériver un type pointeur sur pointeur, etc.

D'une façon générale, un pointeur sur Chose contient ce qu'il faut pour accéder à un objet de type Chose. C'est effectivement une adresse, mais peu importe en première approche. Certains disent qu'un pointeur sur Chose regarde un objet de type Chose. Chose est le plus souvent un type simple ou un type structuré, mais il peut également s'agir d'une fonction ou procédure, en fait de tout type d'objet réservant de la place en mémoire. Nous verrons un exemple de pointeur sur fonction en assembleur, quand nous parlerons des procédures et de la directive INVOKE .

Quand il est dit qu'un pointeur n'est pas une adresse, c'est essentiellement dans un but pédagogique. Dans le cas de l'assembleur, comme de C++ et de Pascal, une variable pointeur initialisée contient une adresse. Dans tous les langages, il est préférable d'écrire qu'un pointeur P est initialisé par l'adresse de la variable A. En d'autres termes, si toute variable possède une adresse à certains moments de l'exécution, elle n'a pas toujours, loin s'en faut, de pointeur qui la regarde.

À chaque type Chose (y compris les pointeurs) correspond donc un type pointeur sur Chose associé. Que Chose soit un type prédéfini ou défini par l'utilisateur, la syntaxe pour désigner le type pointeur sur Chose (référencement) est Chose* en C++ et ^Chose en Pascal. Ainsi, en C++ :

unsigned long *   PMonUL;
unsigned long ** PPMonUL;

déclare que PMonUL est une variable de type pointeur sur unsigned long et PPMonUL une variable de type pointeur sur pointeur sur unsigned long.

La valeur contenue dans un pointeur pointant vers une variable est l'adresse de cette variable en mémoire. Un pointeur vers une donnée complexe, comme une chaîne, un tableau, une structure, pointe vers son premier élément. Ainsi, il est commun de désigner une chaîne de caractères à zéro final par un pointeur vers son premier caractère.

Voyons un petit programme de test en C++Builder et observons les variables en mémoire à l'occasion d'un point d'arrêt. Vous trouverez quelques exemples en C++, Delphi et assembleur sur le CD-Rom, dans le dossier pointeurs .

typedef char Chose;
int Var;
char * chaine = NULL;
int * PVar = &Var;
unsigned long      MonUL = 445;
unsigned long *   PMonUL = (unsigned long *)0;// NULL   )
unsigned long ** PPMonUL = NULL;
Chose* PChose;
PMonUL = (unsigned long *) &chaine;
PMonUL = &MonUL;
chaine = (char*)malloc(50);
for(int i = 0; i < 50; i++) chaine[i] = 'A' + i;
*PVar = 12;
//Point d'arrêt, photo de la mémoire
free(chaine);
chaine = NULL;
//chaine[5] = *PChose;

Relevons les variables à l'aide de la fonction Exécuter / Inspecter… de l'EDI.

Les variables du programme de test en mémoire
figure 10.10 Les variables du programme de test en mémoire [the .swf]

Toutes les variables, pointeurs compris, étant locales, elles sont créées sur la pile. En revanche, la fonction malloc(50) demande au système d'exploitation de réserver 50 octets sur le tas et renvoie un pointeur sur void vers cette zone. Transtypée en pointeur sur char, cette valeur, ici 00BC6C10 , va nourrir la variable chaine .

La variable PPMonUL a été correctement initialisée à NULL (voir plus bas). PChose n'a pas subi le même traitement, dans notre cas il contient 00000005. Si nous dé-commentons la dernière ligne, syntaxiquement correcte, nous obtenons à l'exécution.

Oups !
figure 10.11 Oups !

Déréférencer  un pointeur consiste à désigner la donnée pointée. Si PMaChose est une variable de type pointeur sur…, nous avons vu que la syntaxe de ce déréférencement est *PMaChose en C++ et ^PMaChose en Pascal. Ainsi en C++, *PMonUL est accepté dans le code source au même titre que tout nom de variable de type unsigned long.

Un pointeur simplement déclaré mais non initialisé contient n'importe quoi. Le déréférencer ne provoque rien de prévisible ni d'utile, ou plutôt très souvent un plantage par génération d'une exception de Windows. Cette exception est en réalité un moindre mal, puisque l'erreur du programmeur est immédiatement visible. Pour être certain que cette exception se produira, il est bon d'affecter à tout pointeur non initialisé une valeur particulière, NULL en C++, nil en Pascal. Ces valeurs correspondent numériquement à 0, transtypé vers le type pointeur adéquat. Il est ainsi possible de coder :

if(pointeur != NULL){
  //Utiliser pointeur;
  }

Si par exemple vous écrivez un BIOS ou travaillez sur une carte à microcontrôleur, vous pouvez initialiser directement une valeur numérique immédiate. En programmation plus quotidienne, vous pouvez utiliser pour cette initialisation l'adresse d'une variable, soit du type ad hoc comme dans PMonUL = &MonUL; soit avec transtypage : PMonUL = (unsigned long *) &chaine; . L'affectation définitive n'est bien entendu réalisée qu'à l'issue du processus compilation-édition de liens-loader.

Un autre moyen est d'utiliser le pointeur que renvoient certaines fonctions allouant de la mémoire, comme l'opérateur new ou la fonction malloc() en C++, la procédure New() en Delphi, et bien d'autres : chaine = (char*)malloc(50); . C'est la base de la gestion des données dynamiques.

Il existe un type de pointeur dit générique, non typé, qui pointe quelque part mais sur aucun objet précisé. C'est void* en C++, Pointer en Pascal. Il doit maintenant être clair que ces pointeurs ne peuvent être déréférencés sans être transtypés. Le pointeur générique est, en langage évolué, l'objet qui se rapproche le plus d'une adresse. En assembleur, c'est une adresse.

C'est par l'intermédiaire de ces pointeurs génériques et le transtypage (ou cast ) que vous accéderez aux joies des grosses bêtises dont les langages de haut niveau tentent de vous protéger.

Certains langages permettent de spécifier si un paramètre de fonction sera passé à celle-ci par valeur ou par adresse. Mais peu importe, imaginons nous en tenir au passage par valeur. Dire qu'une fonction prend un pointeur sur Chose en paramètre signifie qu'elle recevra la valeur d'une variable de type pointeur sur Chose. Si cette variable pointeur est correctement initialisée par l'adresse d'une variable de type Chose, cela reviendra à un passage de paramètre de type Chose par adresse.

Les buts de cette façon de faire peuvent être par exemple la récupération de résultats ou le passage facile à la fonction de grandes structures de données.

Voyons un exemple en C++. D'abord, une fonction :

void Bricole(const char * cle, int * VersBoite)
{
* VersBoite = (int)* cle;
//*cle = 'B';// ne compile pas à cause de const
}

Et dans le programme principal :

1 char Maison = 'A';
2 int Boite;
3 char * CleMaison;
4 int * OuvreBoite = &Boite;
5 CleMaison = &Maison;
6 Bricole(CleMaison, OuvreBoite);
7 Bricole(&Maison, &Boite);

Quelques indications de syntaxe C++ : & est l'opérateur adresse. const permet de demander au compilateur de ne pas modifier la variable pointée par cle , et non cle lui-même.

Fonctionnement du programme principal ligne par ligne :

Déclaration d'une variable de type char Maison , initialisée à la valeur 'A', soit 65.

Déclaration d'une variable de type int Boite , non initialisée.

Déclaration d'une variable de type pointeur sur char CleMaison , non initialisée.

Déclaration d'une variable de type pointeur sur int OuvreBoite , initialisée par l'adresse de Boite .

Affectation de l'adresse de Maison au pointeur CleMaison .

Appel de la fonction Bricole() .

Appel de la fonction Bricole() .

Nous pourrions imaginer au moment de l'appel à Bricole() une phrase :

"Bricole, voici un moyen d'accéder à Maison et un autre à Boite. Maison contient une lettre, Boite ne contient rien d'intéressant. Va dans Maison, regarde la lettre qui s'y trouve sans la changer, et dépose une copie de cette lettre dans Boite. Ne me renvoie pas de message particulier, je constaterai en regardant dans Boite que tu as bien travaillé."

À la ligne 2, la variable Boite est simplement déclarée. Sa valeur est donc aléatoire. Ce qui n'empêche pas, à la ligne 4, d'initialiser le pointeur OuvreBoite par l'adresse de Boite .

La ligne 7 fonctionnera correctement, non pas parce qu'un pointeur est une adresse, mais bien parce que le C++ sait fabriquer sans ambiguïté des variables temporaires de type pointeur sur char ou pointeur sur int à partir de ces adresses. Il serait bon que cette nuance formelle soit comprise plutôt qu'admise. Tout comme l'ensemble de cet aparté, qui doit maintenant cesser.

10.7.2 Les pointeurs sous MASM

En assembleur MASM, un pointeur reste une variable, mais avec quelques nuances avec les langages évolués. En assembleur, du moins en MASM, la notion de type est faible, la notion d'adresse forte.

Il n'est pas choquant de dire qu'une variable est un pointeur sans préciser sur quoi elle pointe. Ceci vient du fait que la notion de type est une surcouche de MASM, traitée par des instructions comme INVOKE ou TYPEDEF .

Par contre, dire qu'une variable est un pointeur ou une adresse ne suffit pas. Nous savons qu'une adresse, qui permet d'accéder à une donnée ou une instruction en mémoire, est formée par la connaissance d'un segment, sur 16 bits, et d'un déplacement, sur 16 ou 32 bits, selon le mode. Mais le changement de segment est rare, voire inexistant dans certains programmes. Les instructions mettant en jeu des adresses ont toujours une version qui se contente du déplacement comme adresse, le segment par défaut étant connu. Il y a donc deux catégories, ou distances , de pointeurs ou adresse : NEAR  (proche) et FAR  (lointain).

NEAR exprime une adresse simplement par un déplacement, donc sur 16 ou 32 bits, selon la valeur de l'attribut de taille d'adresse.

FAR exprime une adresse par à la fois le segment et le déplacement, souvent écrits sous la forme SEG:OFFSET . La taille d'un pointeur FAR est donc de 32 (16:16) ou 48 (16:32) bits, là aussi selon la valeur de l'attribut de taille d'adresse.

Donc, rien n'empêche de considérer n'importe quelle valeur de 16, 32 ou 48 bits comme une adresse, n'importe quelle zone mémoire qui contient une ces valeurs comme un pointeur.

Depuis les versions 6.0 de MASM, les choses se sont améliorées, avec les directives TYPEDEF et ASSUME .

TYPEDEF permet de définir un type de pointeur, par la syntaxe suivante :

NomType TYPEDEF [[ distance ]] PTR TypeQualifié

NomType est le nom du type défini, distance est soit NEAR, soit FAR, et TypeQualifié est un type prédéfini de MASM, comme BYTE, SWORD, REAL10, ou alors un type qualifié, issu par un ou plusieurs TYPEDEF d'un type prédéfini.

PBYTE   TYPEDEF PTR BYTE
NPBYTE  TYPEDEF NEAR PTR BYTE
FPBYTE  TYPEDEF FAR PTR BYTE
PWORD   TYPEDEF PTR WORD
NPWORD  TYPEDEF NEAR PTR WORD
FPWORD  TYPEDEF FAR PTR WORD
PPBYTE  TYPEDEF PTR PBYTE
 
PVOID TYPEDEF PTR
 
ADR STRUCT
  rue    BYTE 20 DUP (?)
  numero WORD ?
ADR ENDS
 
PADR TYPEDEF PTR ADR

Tout ces exemples sont valides, nous y voyons un pointeur sur BYTE, un pointeur NEAR sur BYTE, un pointeur FAR sur BYTE, un pointeur sur WORD, un pointeur NEAR sur WORD, un pointeur FAR sur WORD, un pointeur sur pointeur sur BYTE, un pointeur générique ou pointeur void. Une structure de type ADR est ensuite créée, suivie de la définition d'un pointeur sur ADR. C'est ce que confirme le fichier listing :

Structures and Unions:
 
N a m e                           Size
                                  Offset    Type
 
ADR  . . . . . . . . . . . . . .  0016
  rue  . . . . . . . . . . . . .  0000      Byte
  numero . . . . . . . . . . . .  0014      Word
 
 
Types:
 
N a m e                           Size   Attr
 
FPBYTE . . . . . . . . . . . . .  0004   FarPTR Byte
FPWORD . . . . . . . . . . . . .  0004   FarPTR Word
NPBYTE . . . . . . . . . . . . .  0002   PTR Byte
NPWORD . . . . . . . . . . . . .  0002   PTR Word
PADR . . . . . . . . . . . . . .  0002   PTR ADR
PBYTE  . . . . . . . . . . . . .  0002   PTR Byte
PPBYTE . . . . . . . . . . . . .  0002   PTR PTR Byte
PVOID  . . . . . . . . . . . . .  0002   PTR
PWORD  . . . . . . . . . . . . .  0002   PTR Word

Il s'agit là de définitions libres. Néanmoins, ces noms reviennent souvent, comme PBYTE, NPWORD, P NomStructure .

La distance du pointeur, si elle n'est pas spécifiée, ainsi que la taille du pointeur (de l'offset) dépend de la directive .MODEL , de la taille du segment. Le défaut est NEAR. Pour forcer à une distance et une taille déterminée, il faut utiliser NEAR16 (16), NEAR32 (32), FAR16 (16:16) et FAR32 (16:32).

Nous savons donc créer des types de pointeurs. Ils peuvent s'employer maintenant comme tout type qualifié. Rappelons encore que le contrôle de type est très souple en assembleur MASM. Il nous reste encore à créer de pointeurs, c’est-à-dire des variables ou cases mémoire de la taille du type, susceptibles de contenir une adresse :

var1     sword -12
var2     word   14
pbEss1   PBYTE ?
pbEss2   PBYTE var1
pbEss3   PBYTE 0
pwEss    PWORD var2
Chez     ADR <>
PChez    PADR Chez

Nous voyons que ces variables pointeur peuvent être créées non initialisées, initialisées par l'adresse d'une variable de même type, par l'adresse d'une variable d'un autre type sans message d'alerte, par une valeur immédiate.

L'instruction inc [bx] génère le message :

ptr.asm(46) : error A2023: instruction operand must have size

Effectivement, inc peut s'appliquer à diverses tailles d'opérande, et rien ne permet d'indiquer quelle est la taille de la donnée pointée par BX qui doit être incrémenté.

Par contre, mov ax, [bx] est accepté, puisque AX donne la taille de l'opérande. Il faut donc préciser cette taille, par exemple par inc WORD PTR [bx] , ce qui signifie BX considéré comme un pointeur sur WORD, le temps de cette instruction. Il est possible de faire cette supposition pour toute la durée entre deux directives ASSUME  :

ASSUME bx: PTR WORD
mov bx, offset var2
inc [bx]
ASSUME bx:NOTHING

Cette séquence a pour effet d'incrémenter var2 . ASSUME dit à MASM de supposer un certain type pour le registre, ici un type pointeur sur WORD, pour BX. ASSUME reg:NOTHING annule le ASSUME précédent.

Nous verrons d'autres possibilités de ASSUME avec les directives de segment simplifiées. Par exemple, ASSUME bx:ERROR protége BX en signalant son utilisation par un message d'erreur.

 

10.8 Données structurées

Même si certaines instructions, comme celles de type chaîne, sont clairement orientées vers un type particulier de données structurées, il faut bien voir qu'en langage machine le programmeur est totalement maître de la structure des données complexes qu'il désire manipuler. Il en est de même en C/C++, mais dans un cadre solide proposé par le compilateur.

Un macro-assembleur comme MASM, au travers de ses directives de réservation mémoire, permet de faciliter la création de données en général, donc des données structurées. De plus, des directives spécifiques sont offertes pour déclarer plus facilement les tableaux, chaînes de caractères, structures et unions et champs de bits. Ce sont ces quatre sujets que nous allons successivement aborder.

 

10.8.1 Les tableaux

Les données simples sont déjà en quelque sorte des tableaux, puisqu'il est possible d'accéder facilement à chacun de leurs octets, à l'aide de la syntaxe  donnee[n] . Testons ce point dans un petit programme :

;------------------------------------------------
byte2hex PROTO :BYTE, :DWORD
szSTR    TYPEDEF BYTE
;------------------------------------------------
.DATA
message     LABEL    szSTR
ORG 64
a_la_ligne  szSTR    13,10,0
essdword    DWORD    0
;------------------------------------------------
.CODE
debut:
  cls
  
  PetitTablo TEXTEQU <BYTE PTR essdword>
  
  mov PetitTablo[0], 0Ah
  mov PetitTablo[1], 1Bh
  mov PetitTablo[2], 2Ch
  mov PetitTablo[3], 3Dh
  
  mov ebx, 0
  .WHILE(EBX <= 3)
    invoke byte2hex,PetitTablo[ebx], ADDR message
    invoke StdOut,ADDR message
    invoke StdOut,ADDR a_la_ligne
    inc ebx
  .ENDW
  
  invoke ExitProcess, 0
END debut
;------------------------------------------------

Notez la façon de réserver un tampon pour les fonctions d'impression, à l'aide des directives LABEL et ORG . À vrai dire, c'est en recherchant des utilisations possibles de ces directives (en dehors du ORG 100h des fichiers .com et utilisations similaires) que nous en sommes arrivés à cette forme.

byte2hex est une procédure inspirée de dw2hex de MASM32 (voir le source complet sur le CD-Rom).

Après assemblage et édition de liens, lançons l’exécutable dans une fenêtre DOS.

Le résultat est conforme à nos attentes
figure 10.12 Le résultat est conforme à nos attentes

 

Venons-en aux vrais tableaux de MASM. Au départ est la notion de tableau de dimension 1, qui est une collection indicée d’éléments. Par tablo[n] , nous accédons en langage de haut niveau à l’élément d’ordre n du tableau tablo . Selon les langages, n va de 0 à nmax , de 1 à nmax ou court de nmin à nmax , pour ncompte valeurs possibles et donc éléments différents de tableau.

Signalons tout de suite que la syntaxe sous-jacente à MASM, qu'elle soit ou non exprimée clairement, et sauf exception, est celle de C/C++, n éléments indicés de 0 à n-1, avec une différence fondamentale et source de nombreuses erreurs : quelle que soit la déclaration du tableau, l'indice exprime des octets et non des éléments . Ainsi, tablo[n] permettra en assembleur d'accéder au (n+1) e  octet du tableau, et donc ni au n e  octet, ni au (n+1) e  élément, si la taille de l'élément est différente de l'octet. Ceci est à mettre en parallèle avec les expressions d'adressage indexé.

En langage évolué existe la notion de tableau de dimension N. Chaque élément est référencé par la donnée de N indices. Par exemple, un élément de tableau de dimension 3 pourrait s’écrire tablo[n][m][p] (notation du C/C++), pour un nombre d’éléments égal à ncompte * mcompte * pcompte . Un tableau de dimension N n’est jamais qu’un tableau de dimension 1 de tableaux de dimension N-1, et ainsi de suite de façon récursive. De plus, en langage de haut niveau, il est possible de définir des tableaux de pratiquement n’importe quoi. Ce n'est pas non plus impossible en assembleur, où nous savons par exemple gérer des tableaux de pointeurs.

Les chaînes de caractères peuvent être tout simplement des tableaux de caractères (octets non signés) de dimension 1 ; mais, de plus en plus, ce sont des instances de classe, donc des entités beaucoup plus complexes. Notons toutefois que, généralement, une des propriétés de cette classe est un pointeur sur un tableau de caractères.

En assembleur, MASM en particulier, les possibilités sont beaucoup plus restreintes. Un tableau est une collection (dimension 1) de données de même type, donc de même taille. Ce type est un type qualifié, c’est-à-dire un des types simples définis par le langage et tout type explicitement dérivé.

Il est possible, c’est même très fréquent, de travailler avec des tableaux de pointeurs. Nous pourrons, par leur intermédiaire, construire et parcourir des tableaux de dimension N, mais MASM ne les connaît pas en tant que tels.

Une chaîne de caractères est un tableau de caractères, donc en quelque sorte de BYTE. L'utilisation de ce symbole plutôt à réserver à des valeurs numériques n'est pas très heureuse. Nous verrons comment y remédier.

Un tableau est déclaré (et sa mémoire réservée) en donnant son nom (une étiquette), suivi d’un type et d’une liste de données. Les données peuvent être remplacées par des  ? , auquel cas la mémoire est réservée, mais sa valeur laissée telle quelle :

tablo1  word 12, ?, 0

Nous aurons souvent à initialiser des tableaux sur plusieurs lignes, pour des raisons soit de taille, soit de structure. Il faut alors terminer chaque ligne, sauf la dernière, par une virgule :

tablo2  word 1, 2, 3,
             4, 5, 6,
             7, 8, 9

 

Cette déclaration initialise un tableau de 9 éléments de 16 bits, mais pourrait très bien évoquer une matrice 3 x 3 dans l’esprit du programmeur.

Cette syntaxe définit bien un tableau tablo2 de 9 éléments ; MASM en connaît par exemple la taille, comme nous le verrons un peu plus bas. En revanche, voyons la syntaxe suivante :

tablo3  word 1, 2, 3
        word 4, 5, 6
        word 7, 8, 9

Elle permet d’initialiser et d’utiliser tablo3 comme un tableau de 9 mots de 16 bits, puisque MASM ne vérifie pas les limites d’indices (C/C++ ne le fait pas davantage). En revanche, MASM verra tablo3 comme un tableau de 3 éléments.

En effet, MASM connaît les tableaux en tant qu’entités. Comment le vérifier ? Au travers des opérateurs LENGTHOF , SIZEOF  et TYPE , qui retournent respectivement le nombre d’éléments du tableau, le nombre d’octets occupés par ces éléments et le type (taille en octets) de l’élément de base. Nous devons donc avoir la relation : SIZEOF = LENGTHOF * TYPE . Ces valeurs sont des constantes symboliques connues à la compilation ; ce sont donc des valeurs immédiates vues du programme.

Avant de réaliser quelques essais sur ces tableaux, nous allons créer localement une macro qui sera immédiatement rentabilisée :

petrus_affiche MACRO valeur
    invoke dwtoa, valeur , ADDR message
    invoke StdOut,ADDR message
    invoke StdOut,ADDR a_la_ligne
ENDM

Pourquoi ce nom étrange ? Quand nous travaillons ici avec quelques fonctions de la distribution MASM32, avec des includes et des bibliothèques dont nous ne sommes pas les auteurs. Il est donc toujours possible que les noms de macros et de procédures se télescopent. Nous avons en effet une tendance bien excusable à toujours attribuer à peu près les mêmes noms. Il est donc courant d’identifier sa production par un préfixe improbable. L'auteur fait plutôt dans le petrus .

 

;------------------------------------------------
    .DATA
 
      tablo1  WORD 12, ?,  0
 
      tablo2  WORD 1,  2,  3,
                   4,  5,  6,
                   7,  8,  9
 
      tablo3  WORD 1,  2,  3
              WORD 4,  5,  6
              WORD 7,  8,  9
;------------------------------------------------
    .CODE
debut:
    cls
 
    xor eax, eax
    mov ax, tablo3[3*WORD]
    petrus_affiche eax
    mov ax, tablo3[4*sizeof WORD]
    petrus_affiche eax
    mov ax, tablo3[5*TYPE tablo3]
    petrus_affiche eax
    petrus_affiche LENGTHOF tablo2
    petrus_affiche SIZEOF tablo2
    petrus_affiche TYPE tablo2
    petrus_affiche LENGTHOF tablo3
    petrus_affiche SIZEOF tablo3
    petrus_affiche TYPE tablo3
 
    invoke ExitProcess, 0
END debut
;------------------------------------------------

 

Pour le résultat suivant.

Les résultats
figure 10.13 Les résultats

Nous constatons que tablo3 est effectivement vu comme un tableau de 3 éléments de 2 octets, pour un total de 6 octets, en termes de SIZEOF , LENGTHOF et TYPE . Il faudra donc être soigneux dans la déclaration, d’autant plus que les trois premières lignes montrent qu'il est tout à fait possible pour le reste de travailler avec tablo3 comme avec un tableau de 9 éléments.

L’opérateur DUP  peut être utilisé pour initialiser un tableau. Sa syntaxe générale est :

nombre  DUP liste

qui réserve en mémoire nombre séquences de liste , cette dernière pouvant être n’importe quoi, du simple octet à une liste d’autres  DUP .

Table_transcodage BYTE 256 DUP(FFh) déclare et initialise à 255 (ou -1) une table de 256 octets.

Nous avons déjà évoqué le fait que tablo[n] pointe vers le (n+1) e  octet du tableau, par exemple tablo[0] vers le premier octet, et dans ce cas, le premier élément. Donc, pour accéder à un élément particulier connu à la compilation, il faut utiliser TYPE. Par exemple tablo[n * TYPE tablo] ou tablo[n * sizeof WORD] (s'il s'agit d'un tableau de WORD, le sizeof devant un identificateur de type étant optionnel) pointe vers le (n+1) e  élément.

À l'exécution, il faudra faire le même type de calcul et mettre en œuvre les possibilités offertes par les modes d'adressage.

Une autre façon de référencer est la syntaxe tablo+n . Les opérateurs  + et [] sont à ce niveau équivalents. Cette possibilité fait partie des zones de flou du langage. Vous pourriez réserver les crochets aux tableaux réellement déclarés en tant que tels, le signe  + aux accès sauvages, par exemple à un octet dans un DWORD. Il est de bon ton, dans le cas d'utilisation du  + de conserver la valeur 0 :

  PetitTablo TEXTEQU <BYTE PTR essdword>
 
  mov PetitTablo + 0, 0Ah
  mov PetitTablo + 1, 1Bh
  mov PetitTablo + 2, 2Ch
 
  mov PetitTablo,     0Ah
  mov PetitTablo + 1, 1Bh
  mov PetitTablo + 2, 2Ch

Le premier bloc est plus joli, mais surtout plus explicite. Les deux fonctionnent.

Avant de manipuler autour de ces dernières informations, replongeons dans le petrus_  :

petrus_affiche16 MACRO valeur
    xor eax, eax
    mov ax, valeur
    petrus_affiche eax
ENDM

Le but est simplement d'afficher les valeurs 16 bits. Nous avons ajouté au code les lignes suivantes :

     tablo4  TBYTE 256 DUP(112233445566778899AAh)
     .
     .
     .
     petrus_affiche16 tablo2[6]
     petrus_affiche16 tablo3[6]
     petrus_affiche16 tablo2+6
     petrus_affiche16 tablo3+6
     petrus_affiche16 tablo2[3 * TYPE tablo2]
     petrus_affiche16 tablo3[3 * TYPE tablo3]
 
     petrus_affiche LENGTHOF tablo4
     petrus_affiche SIZEOF tablo4
     petrus_affiche TYPE tablo4

Les six premières lignes produisent la valeur 4, qui est le quatrième (indice 3) élément des deux tableaux. Les trois dernières fournissent respectivement 256, 2560 et 10. Tout est donc normal, conforme à nos attentes.

Nous avons enfin ajouté un code de test de l'instruction BOUND  (voir le chapitre Le jeu d'instructions ), qui permet de détecter les limites d'un tableau.

.DATA
limites_tablo1 WORD  4, 12
...
.CODE
...
mov  limites_tablo1 + 0,  4
mov  limites_tablo1 + 2, 12
mov  ax, 13
bound ax, limites_tablo1

Le couple de WORD limites_tablo1 peut être initialisé une fois pour toutes ou être modifié à l'exécution. En l'état, tout se passe bien. Remplacez le 10 et vérifiez, à 4 ou 12 tout va bien ; à 3 ou 13, boum ! une exception ( #BR, n°05 ) est levée et le programme est arrêté.

Gasp !
figure 10.14 Gasp !

 

10.8.2 Les chaînes de caractères

Les chaînes de caractères sont d'abord des tableaux de BYTE et tout ce qui vient d'être écrit sur les tableaux peut leur être appliqué. Leur spécificité vient essentiellement de la déclaration et de l'idée que le programmeur s'en fait.

Il peut être utile d'utiliser le TYPEDEF  pour adapter le vocabulaire de MASM aux spécificités des chaînes de caractères :

CHAR    TYPEDEF BYTE
szSTR   TYPEDEF BYTE
DOS_STR TYPEDEF BYTE
PBYTE   TYPEDEF BYTE PTR
PCHAR   TYPEDEF BYTE PTR

Ces types permettent de clarifier le listing, en distinguant les caractères, les chaînes à 0 final et les chaînes à $ final (fonctions du DOS). Mais attention, ce sont bien pour les trois premiers des types BYTE, rien n'est vérifié. Une autre habitude possible est de réserver la directive  DB (ou db ) aux chaînes de caractères. La définition de PBYTE est standard : si vous incluez des en-têtes d'origine extérieure, Windows par exemple, ce type y sera souvent déjà défini. Vous pouvez toujours faire un petrus_PBYTE ...

Les chaînes de caractères peuvent être saisies entre guillemets, simple ou doubles. Pour saisir des guillemets faisant partie de la chaîne, il suffit de les doubler ou de mettre les doubles dans une chaîne encadrée par des simples, et vice versa.

message     db       "Je reserve un peu de place",0
a_la_ligne  db       13,10,0
ch01      DWORD "abcd"
ch02      WORD  "ef"
essai1    BYTE "A l'aide"
essai2    BYTE 'A l''aide'
essai3    BYTE "Il a crié: ""Help!"", et il lui fut répondu:""Est-ce le moment d'apprendre l'anglais ?"""

Les variables de types autres que BYTE ne peuvent initialiser en chaîne qu'une seule donnée. Les quatre derniers exemples ne prévoient pas le caractère de fin de chaîne. Celui-ci n'est jamais ajouté par l'assembleur. Il est souvent utile d'avoir des tableaux de pointeurs sur chaînes :

PBYTE TYPEDEF PTR BYTE
.
.
tabptrch  PBYTE essai1, essai2, essai3
.
.
petrus_affiche LENGTHOF tabptrch
petrus_affiche TYPE tabptrch
petrus_affiche SIZEOF tabptrch

Le résultat est 3, 4, 12. Logique : 3 pointeurs, donc 3 éléments de 32 bits, au total 12 octets.

Il existe une famille d'instructions qu'il est coutumier d'appeler les instructions chaînes, composée de MOVS , STOS , CMPS , LODS , SCAS , en liaison avec les préfixes de répétition REP , REPZ/REPE et REPNZ/REPNE .

Une rubrique leur est consacrée au chapitre intitulé Le jeu d'instructions , nous n'y reviendrons pas, si ce n'est pour remarquer :

  Qu'il serait préférable de les appeler fonctions tableau.

  Que leur pertinence est parfois douteuse sur la génération Pentium. Il est en effet souvent plus efficace en terme de vitesse de bricoler son algorithme à la main, avec des INC , DEC et autres JZ .

10.8.3 Structures et unions

Indépendamment de l'assembleur, une structure (struct en C, record en Pascal) est le regroupement dans une même entité de divers éléments, appelés des champs, de taille et de type pouvant différer. Une structure peut être un champ d'une autre structure. Généralement, il est possible de confondre le nom de la structure et l'adresse de son début. À partir de là, les divers champs sont à un déplacement connu par rapport à cette adresse. Ces champs sont accessibles par une syntaxe du type :

nom_structure.nom_champ

Vous trouverez également :

adresse_structure->nom_champ

Une union est une structure dont tous les champs sont situés à l'adresse de la structure. Ils se recouvrent donc. Nous pourrons ainsi accéder à la même donnée sous différents types. Ce point sera utile dans un langage typé.

Représentons une structure et une union composées chacune d'un mot de 16 bits et de deux de 8 bits.

Structure et union
figure 10.15 Structure et union [the .swf]

L'ordre des données est important dans la structure, indifférent dans le cas de l'union. Si la valeur dans l'union est de taille suffisamment faible, elle peut être lue dans les trois champs. La convention little endian (voir ce terme) permet cette particularité, ce qui confirme le fait que cette convention n'est pas si à l'envers que certains veulent le dire.

En assembleur (MASM), comme en programmation évoluée voire objet, la définition d'une variable structure ou union se fera en deux temps : la définition du type, puis, à partir de ce dernier, la création d'une ou de plusieurs variables, ou instances du type. Remarquons que nous sommes, sur ce point et sur celui de la syntaxe, proches du C.

La syntaxe générale de déclaration est :

Nom { STRUCT ou UNION } [[ valeur d'alignement ]] [[, NONUNIQUE ]]

déclarations des champs

Nom ENDS

La directive STRUC peut être utilisée indifféremment à la place de STRUCT . Le paramètre optionnel valeur d'alignement peut prendre les valeurs 1, 2 ou 4. S'il est absent, ou égal à 1, les champs sont placés dans la structure à la suite les uns des autres, en optimisant donc l'occupation de la mémoire. S'il vaut 2, des zéros seront ajoutés avant les champs mesurant un nombre impair d'octets, pour que tous les champs soient alignés sur des adresses paires. S'il vaut 4, la même opération sera effectuée, pour aboutir à un alignement sur des adresses multiples de 4. Cette dernière valeur est une bonne précaution pour améliorer les performances en architecture 32 bits.

Les noms de champs, selon les versions de MASM, doivent ou non être uniques dans le programme. Ils doivent de toute façon l'être dans une structure et l'ensemble des structures imbriquées.

La déclaration de type ne générant pas de code pourra être placée soit dans un fichier  .inc , soit dans une zone réservée à ce type de déclarations, avec les macros locales et autres TYPEDEF . Cette déclaration pourra ressembler à :

STR1  STRUCT 4
    var1_1 BYTE  0
    tab1_1 DWORD 10 DUP(0)
    var2_1 WORD  0FFFFh
STR1 ENDS

Des valeurs sont données aux champs. Ce sont les valeurs d'initialisation par défaut, qui seront appliquées dans certaines conditions au moment de la création des variables.

Nous devons ensuite créer des instances de cette structure. La syntaxe générale répond à :

[[ Nom ]] TypeStruct < [[ Initialiseur [[, Initialiseur ]]...]] >

[[ Nom ]] TypeStruct { [[ Initialiseur [[, Initialiseur ]]...]] }

[[ Nom ]] TypeStruct N DUP ({ [[ Initialiseur [[, Initialiseur ]]...]] })

TypeStruct est le nom de la structure, celui de la classe que nous venons de définir, par exemple STR1 . Nom est le nom que nous voulons donner à l'instance, à la variable structure (ou union) bâtie sur le modèle TypeStruct . Les Initialiseur sont optionnels, ils surchargent les valeurs par défaut, s'il y en a.

Essayons de coder tout cela. Nous avons défini un type UNION et un type STRUCT , puis déclaré un certain nombre de variables sur ces modèles. Cet exemple couvre une grande partie des problèmes syntaxiques qui peuvent se présenter :

STR1  STRUCT 4
    var1_1 BYTE  0
    tab1_1 DWORD 10 DUP(0)
    var2_1 WORD  0FFFFh
STR1 ENDS
 
UNI1  UNION
    var_8  BYTE  7 
    var_16 WORD  7
    var_32 DWORD 7
UNI1 ENDS
 
CMPLX STRUCT 4
    st1  STR1 <>
    st2  STR1 <>
    un1  UNI1 <>
    nbre BYTE 0
CMPLX ENDS
 
;-----------------------------------------------------------
    .data
      message     db       "Je reserve un peu de place",0
      a_la_ligne  db       13,10,0
      varst1      STR1 <>
      varst2      STR1 {}
      varst3      STR1 <1, 10 DUP(14)>
      varst4      STR1 {12}
      vartabst    STR1 4 DUP(<>)
      varun1      UNI1 <>
      varun2      UNI1 {}
      varun3      UNI1 <44h>
      vartabun    UNI1 2 DUP({0})
      ;varcmplx    CMPLX <varst1, varst2, varun1, 5>
      varcmplx    CMPLX <>             
 
;-----------------------------------------------------------

 

À la déclaration de varun3, la ligne varun3      UNI1 <5544h> aurait été refusée par l'assembleur. Il faut que la valeur d'initialisation d'une union entre dans la taille de son premier champ décrit (et non pas du plus petit).

Face à des listes d'initialisation incomplètes, l'assembleur les utilise en commençant tout simplement par le début. La ligne varst3      STR1 <1, 10 DUP(14)> aurait aussi bien pu être varst3      STR1 <1, 4DUP(14)> . L'assembleur aurait initialisé les 4 premiers DWORD du DUP à 14, les autres à la valeur par défaut 0.

Nous avons également défini un type CMPLX , pour voir les effets des structures imbriquées, et instancié ce type par varcmplx .

Nous allons en terminer avec structures et unions par du code :

     petrus_affiche LENGTHOF varst1
     petrus_affiche SIZEOF varst1
     petrus_affiche TYPE varst1
     
     petrus_affiche LENGTHOF vartabst
     petrus_affiche SIZEOF vartabst
     petrus_affiche TYPE vartabst

Les résultats sont 1, 48, 48, 4, 192, 48. C'est logique, varst1 est un seul objet de taille, donc de type, 48. vartabst est un tableau de 4 de ces objets, d'où la taille en octets. Le type reste 48. Si nous retirons le 4 servant à aligner STR1 , les structures sont plus compactes et les résultats deviennent : 1, 43, 43, 4, 172, 43. Logique, tout cela.

 

     mov eax, varst3.tab1_1[0]
     petrus_affiche eax
     
     mov vartabst[2 * TYPE STR1].tab1_1[4 * TYPE DWORD], 122 
     
     mov eax, vartabst[2 * TYPE STR1].tab1_1[4 * TYPE DWORD]
     petrus_affiche eax
     
     mov eax, varcmplx.un1.var_32
     petrus_affiche eax  

Les résultats sont 14, 122 et 7. Il y a peu à dire sur la syntaxe : il suffit de bien se représenter les structures et unions, ainsi que les tableaux. Ce qui est intéressant, c'est de constater qu'accéder à un champ d'un élément du tableau vartabst par vartabst+n est devenu pratiquement impossible. Et pourtant, l'assembleur le fait, puisque tout est effectivement connu à la compilation. Pour en arriver au même résultat, nous devrions calculer la taille de tous les éléments, en tenant compte de l'alignement, et compter les octets. Nous sommes là dans un domaine où l'apport d'un bon macro-assembleur est indiscutable.

 

10.8.4 Structures de champs de bits

Certains titres font froid dans le dos. Celui-ci est dû à un petit piège de vocabulaire : nous appelons champ de bits ce que Microsoft nomme RECORD dans sa documentation. Or, ce terme, qui devrait être traduit par enregistrement, désigne en Pascal, une structure (struct) en C/C++. Le record de MASM ressemble en réalité plus à un champ de bits ou bitfield. C'est ce que nous avons trouvé de moins calamiteux pour traduire ce record de MASM, par trop ambigu. Et qui plus est, il s'agit bien de structures dont les entiers doivent être vus comme des champs de bits (bitfield).

Pour sentir intuitivement ce qu'est un champ de bits, vous pouvez vous reporter à la présentation des opérations sur les bits, où est évoquée la façon dont sont codés le secteur et le cylindre de début (et de fin) de partition dans la table des partitions d'un disque. Vous y trouverez ce schéma.

Codage des secteur et cylindre de début de partition
figure 10.16 Codage des secteur et cylindre de début de partition [the .swf]

Mais l’analogie s'arrête rapidement, puisque justement dans cet exemple les bits ne sont pas vraiment répartis de façon contiguë.

Nous trouvons encore ce concept dans le codage des couleurs. Les informations de couleurs concernant chaque point représentent des données de largeurs non multiples de 8. Par exemple, 4 valeurs sur 8 bits dans 3 octets.

Comme les structures et les unions, une structure de champ de bits est d'abord déclarée par la directive RECORD avant instanciations. C'est le nombre de bits à réserver qui est indiqué :

NomBitField RECORD  Champ [[, Champ ]]...

Chaque Champ   possède la structure suivante :

NomChamp : Largeur [[= Expression ]]

Expression est une valeur d'initialisation optionnelle.

Déclarons :

BF1 RECORD R1:2=0, R2:4=1 , R3:2=2, R4:4=3
BF2 RECORD Q1:2, Q2:4

BF1 demande 12 bits, BF2 6. Il faudra donc respectivement 2 octets et 1 octet. Vérifions :

petrus_affiche TYPE BF1
petrus_affiche TYPE BF2

Les résultats, 2 et 1, sont conformes. Nous avons donné des valeurs par défaut pour BF1, pas pour BF2. Nous pouvons maintenant déclarer des instances, non initialisées :

ess_bf1     BF1      <>
ess_bf2     BF2      {}

ou initialisées :

ess_bf1     BF1      <0,0,0,0>
ess_bf2     BF2      {3,4}

Attention, il faut que la valeur du champ entre dans le nombre de bits réservés. Pour l'instant, nous ne pouvons pas faire grand-chose avec cette structure :

petrus_affiche16 BF1
petrus_affiche16 ess_bf1

La première ligne, saisie par erreur, donne 4095, soit 12 bits à 1. Nous avons vérifié avec BF2, ce n'est pas une règle. La seconde ligne répond par 0.

Il reste donc à découvrir deux opérateurs spécifiques : WIDTH  et MASK . Commençons par WIDTH  :

petrus_affiche WIDTH R1   
petrus_affiche WIDTH R2
petrus_affiche WIDTH R3
petrus_affiche WIDTH R4
petrus_affiche WIDTH Q1
petrus_affiche WIDTH Q2

Le résultat donne 2, 4, 2, 4, 2, 4, ce que nous savions déjà. Bon, au MASK :

petrus_affiche16 MASK R4
petrus_affiche16 NOT MASK R3

Le résultat est 15 et 65487, ce qui donne en binaire 0000 0000 0000 1111 et 1111 1111 1100 1111. Nous avons bien les masques correspondant aux champs. C'est mieux, mais bon...

Et c'est tout. Nous pourrons avec cela effectuer quelques opérations, à l'aide d'instructions logiques, mais nous sommes loin d'un compilateur. Pour terminer, tentons de placer la valeur 6 dans R2 et de la relire.

petrus_affiche16 ess_bf1
 
mov ax,  6
shl ax, (WIDTH R3 + WIDTH R4)
and ess_bf1, NOT MASK R2
or  ess_bf1, ax
 
petrus_affiche16 ess_bf1

Les résultats sont 1092, 01 00 01 00 0100 (nous avions changé les initialisations) et 1412, 01 01 10 00 0100. R2 est bien passé de 1 à 6, le reste n'ayant pas changé.

La lecture, dans BX, est encore plus simple :

mov bx, ess_bf1
and bx, MASK R2
shr bx, (WIDTH R3 + WIDTH R4)
 
petrus_affiche16 bx

6 ! Pair, rouge et manque. Les structures de champs de bits ont fait leur œuvre.

Un assembleur peut apporter une aide dans la rédaction et la maintenance du code source, mais il ne peut pas inventer d'instructions. Or, sur le plan de la manipulation un peu évoluée des bits, la descendance du 8086 n'est pas particulièrement outillée. Certains microcontrôleurs font certainement mieux, au moins pour les instructions.

10.9 La mémoire : comprendre et utiliser les segments

Nous abordons une partie un peu austère. À n’attaquer qu’avec un minimum d’envie. Il serait d’autant plus dommage de se forcer qu’il est possible, à partir d’une paire de squelettes d’applications, l’un en mode 16 bits segmenté, l’autre en mode 32 bits linéaire, de bien progresser tout en s’amusant. Ou s’amuser tout en progressant.

Attention, peut-être le titre de la partie est-il trompeur : la notion de segment dans l’architecture x86 est supposée connue, à la fois en mode Réel et en modeProtégé, ce n'est pas cette notion qui sera présentée à nouveau ici. Rappel, sans pour autant évoquer tous les cas :

  En mode Réel, vous travaillez avec des segments de 64 Ko, donc vous manipulez un offset sur 16 bits, qui est combiné avec un registre de segment (CS, DS, ES, SS), sur 16 bits également. Et le résultat n’est pas une adresse physique sur 32 bits, comme si le couple DX:AX formait un faux registre 32 bits, mais bien une adresse sur 20 bits. De plus, les segments de taille fixe peuvent très bien se recouvrir partiellement ; si vous avez besoin d'une zone de données ou de pile de 16 Ko, le segment qui va l'accueillir fera toujours 64 Ko et rien sauf les précautions du programmeur ne pourra empêcher à l'exécution d'écrire dans les 48 Ko non utilisés. Enfin, sous DOS, rien ne protège la mémoire d'un programme d'un accès par un autre processus. Tout ceci est déjà connu.

  En mode Protégé, notre offset s’exprime sur 32 bits, ce qui nous donne des segments d’un peu plus de 4 Go. Nous accédons si nous le souhaitons à toute la mémoire de façon linéaire. La mémoire est mise à notre disposition par le système, au travers du gestionnaire de mémoire virtuelle. Les registres de segment contiennent des sélecteurs, mais généralement cela nous est parfaitement égal. Nous pourrions même adresser de la mémoire sans même nous préoccuper de savoir si elle existe. Nous sommes en environnement multitâche, et chaque segment d'une application est protégé d'un accès par d'autres applications et même protégé de tel ou tel type d'accès par l'application elle-même. En revanche, au contraire de ce qui se passait sous DOS, nous ne savons absolument pas où sera un segment donné au moment de l'exécution, ni même s’il sera en mémoire. Tout cela, pour le programmeur d’applicatifs.

Le mode Réel est moyennement complexe, mais surtout il nous faut prendre en compte cette complexité. Le mode Linéaire nous semble plus simple, parce qu’un système d'exploitation nous y chaperonne lourdement. Bien entendu, en mettant les mains dans le cambouis, les choses deviennent vite velues, plus certainement que pour le mode Réel. Mais programmer des applications ordinaires en mode Protégé est bien plus simple que le faire en mode Réel.

Aussi étonnant que le fait puisse apparaître, le même mot segment désigne pour MASM deux choses différentes, mais dans le même domaine. La documentation de MASM affirme que le contexte permet de lever le doute à coup sûr. Il est permis d’avoir des doutes, cette ambiguïté est certainement gênante. Particulièrement en situation d’apprentissage. Le sujet de la segmentation dans l’architecture IA était suffisamment confus pour ne pas en rajouter.

10.9.1 Segments physiques

Ce sont ceux que nous connaissons, définis par l’architecture du processeur. Dans tous les modes de fonctionnement, un segment est l'ensemble de la mémoire accessible pour une valeur donnée d’un registre de segment. Cette définition reste valable aussi bien avec les segments du mode Réel qu'avec les sélecteurs du mode Protégé.

Quand DOS charge un programme en mémoire, avant de lui donner la main pour l’exécution, il positionne les registres CS et SS (code et pile) à la valeur correcte. Pour les autres segments, c’est au début un peu troublant. Par une collaboration entre l’assembleur, le lieur et le loader de DOS, vous pouvez accéder par avance à la valeur, par exemple, du (ou des) segment de données affecté au programme. C’est pour cela que vous commencerez souvent un programme DOS de la façon suivante :

mov ax, @data ; initialisation du data segment
mov ds, ax
mov es, ax

Par quelle magie, au moment où ce code s’exécute, @data (sans parler de sa syntaxe) contient-il une adresse de segment valide ? À l'issue de l’assemblage, le programme se retrouve dans un fichier  .obj qui contient le code utile, mais également de façon compréhensible pour le lieur une foule d’informations. Parmi celles-ci, la mémoire dont le programme aura besoin pour tourner, et une liste d’éléments à modifier en fonction de l’adresse réelle qui sera affectée à cette mémoire. Le lieur fait son boulot, résout une partie des problèmes (ceux concernant les autres modules à lier par exemple), mais ne connaît toujours pas cette satanée adresse réelle. Et pour cause, elle ne sera certainement pas la même à chaque lancement du programme. Donc, si le lieur, comme l'assembleur, se débrouille tout seul avec les offsets, il n'a aucun accès à la valeur réelle du contenu des registres de segments.

Il peut réduire le problème à un niveau d’incertitude minimal, par exemple l’adresse réelle de CS, le reste s’en déduisant. Il va par exemple demander, sur indications de l'assembleur, 32 Ko pour le code, suivis de 16 Ko de données et de 16 Ko pour la pile. Si chacune de ces trois zones est un segment, ils auront respectivement comme adresse de base CS, CS + 32 Ko, CS + 48 Ko. Ces adresses de base sont les valeurs de CS, DS et SS (ou les valeurs pointées par les sélecteurs correspondants, peu importe ici). Remarquons que, comme nous l'avons rappelé un peu plus haut, en mode Réel, ce sera au programmeur de veiller par exemple à ce que le code ne dépasse pas les 32 Ko, le segment de code recouvrant les deux autres.

Le lieur va donc résoudre les indéterminations en décidant que CS vaut 0000h , refaire la liste des inconnues, fabriquer un en-tête (MZ header) compréhensible par le loader de DOS, coller cela devant le code objet et les données, et enfin sauver l’exécutable. Quand le loader va récupérer le bébé, il pourra facilement consulter la liste des éléments relogeables et leur ajouter la valeur réelle de CS. Si les choses ne se passent pas exactement de cette façon, cela y ressemble.

Le travail de l’assembleur, du lieur et du loader n’est pas tout simple. En revanche, celui du programmeur n’est pas bien compliqué : il lui suffit d’oublier tout et de se convaincre qu’il connaît déjà les adresses des segments. Et en mode Protégé 32 bits, les segments physiques lui causeront encore moins de soucis. Le plus embêtant par rapport à ce sujet est peut-être la gestion de grosses structures de données monoblocs dépassant la taille d’un segment en mode Réel.

10.9.2 Segments logiques

Il est toujours possible de se réserver un gros bloc de mémoire, d’initialiser CS en bas de ce bloc, SS en haut et de placer ses données quelque part vers le milieu, à l’endroit où code et pile ont le moins de chances de se rencontrer. C’est à peu près suffisant pour un  .com . Mais MASM nous propose beaucoup mieux.

Nous pouvons définir des zones de mémoire, aussi nombreuses que souhaité ou presque. À chacune de ces zones, nous associons des propriétés ou attributs, pour définir l’alignement sur le DWORD par ALIGN ou l’interdiction d’écriture par READONLY . Ces zones s’appellent… des segments. Disons que quand il faudra être clair, il sera préférable de les nommer segments logiques .

La première propriété de chaque segment logique est certainement le nom qui lui est donné. Grâce à lui, nous pouvons commencer par saisir des données dans un segment (logique !) DATA1, puis du code dans CODE, puis encore des données en rouvrant DATA1, puis la suite du code dans CODE, etc. Précisons tout de suite que le fait de contenir des données ou du code ne fait pas partie des attributs de chaque segment. Nous pourrons donc gérer le contenu des segments logiques avec une grande souplesse, ainsi que la présentation du code source, ce qui est capital.

Au moment de l’assemblage va se déclencher une vaste cuisine, au cours de laquelle les segments logiques vont être regroupés au mieux, dans le respect des attributs déclarés, puis répartis dans un certain nombre de segments physiques. Tout ce travail se fera automatiquement, avec le programmeur comme chef d’orchestre plus ou moins actif.

La définition des segments logiques peut se faire entièrement par le programmeur, segment par segment. Mais il existe des directives qui permettent, en choisissant simplement un modèle mémoire, de créer automatiquement les segments logiques, ainsi que quelques autres paramètres. La gamme des modèles proposés couvre tous les besoins courants. C’est la déclaration simplifiée des segments.

Si vous venez juste de découvrir l’assembleur et, comme tout le monde ou presque, avez choisi de commencer par des exemples, vous avez peut-être assemblé quelques lignes qui ressemblent à :

.MODEL SMALL
.STACK 100h
 
.DATA
bijor db "Bonjour, monde !",0Dh, 0Ah,"$"
 
.CODE
debut:
  
  ; Initialisation du data segment
  mov ax, @data 
  mov ds, ax
  
  ; Message de bienvenue
  mov ah, 09h
  mov dx, offset bijor
  int 21h
  
  
  ;fin du programme et retour au DOS
  mov AH, 4Ch            
  int 21h                 
END debut

Peut-être avez-vous pensé que .STACK  100h créait la pile, .DATA  le segment de données et .CODE  le segment de code. En réalité, la ligne la plus importante est la première .MODEL  SMALL qui définit le modèle mémoire. Elle crée un groupe de trois segments, STACK , _DATA  et _ TEXT , et quelques bricoles de plus. Il suffit d’utiliser les autres directives de segment ( .DATA , . CODE ...) sans indiquer de propriétés, pour que ces segments par défaut soient utilisés. C’est bien ce que montre le fichier listing :

seg.asm                         Symbols 2 - 1
 
 
Segments and Groups:
 
        N a m e     Size    Length Align    Combine Class
 
DGROUP . . . . . . .GROUP
_DATA  . . . . . . .16 Bit  0013   Word     Public  'DATA'  
STACK  . . . . . . .16 Bit  0100   Para     Stack   'STACK'  
_TEXT  . . . . . . .16 Bit  0010   Word     Public  'CODE'  
 
 
Symbols:
 
        N a m e     Type    Value    Attr
 
@CodeSize  . . . . .Number  0000h    
@DataSize  . . . . .Number  0000h    
@Interface . . . . .Number  0000h    
@Model . . . . . . .Number  0002h    
@code  . . . . . . .Text    _TEXT
@data  . . . . . . .Text    DGROUP
@fardata?  . . . . .Text    FAR_BSS
@fardata . . . . . .Text    FAR_DATA
@stack . . . . . . .Text    DGROUP
bijor  . . . . . . .Byte    0000     _DATA   
debut  . . . . . . .L Near  0000     _TEXT   
 
      0 Warnings
      0 Errors

Nous voyons que, outre le groupe de trois segments, il a été créé un certain nombre de symboles. Nous en verrons la signification plus loin dans ce chapitre.

Nous allons détailler les éléments d’une déclaration manuelle des segments. Il n’est pas certain que vous utilisiez un jour cette technique, mais elle est à la base des modèles utilisés pour les déclarations simplifiées, sur lesquelles nous reviendrons.

Auparavant, quelques mots sur les directives de processeurs et coprocesseurs.

10.9.3 Directives de processeurs et coprocesseurs

Si nous traitons ici de ces directives, c’est parce qu’elles interfèrent avec les modèles mémoire et les attributs de segments. Il n’est pas possible, par exemple, de définir un segment en 32 bits avec un 8086. Plus subtil : pour que les attributs par défaut de taille d'opérande et d'adresse liés à un segment soient à 32 bits, il faut qu'un processeur .386 ou postérieur soit déclaré. Les directives SEGMENT et .MODEL auront un comportement différent selon que le processeur est déclaré avant ou après la directive. En bref, si nous voulons travailler en 32 bits, déclarer le processeur avant toute autre déclaration.

Les directives listées sont issues de la documentation .NET, donc la version 7.0 de l’assembleur. .XMM et .686P qui doivent être les plus récentes sont reconnues par la 6.15.8803, donc toutes les autres également.

Si un programme est adapté à un processeur, il tournera à priori sur les modèles plus récents. Néanmoins, MASM considère qu’il assemble pour un processeur donné, et non pas pour une famille.

Ceci a pour conséquence de :

  Générer des erreurs pour les instructions invalides pour ce processeur.

  Générer des erreurs pour les paramétrages invalides avec ce processeur, attributs de segments par exemple.

  Dans le même ordre d’idées, adapter les paramétrages par défaut au processeur.

  Modifier le code généré par MASM.

  Adapter les indications de timing dans les listings.

Par défaut, MASM est en mode 8086, c’est-à-dire qu’il reconnaît les instructions et modes de fonctionnement d’un 8086 associé à un coprocesseur 8087. Les directives de processeur sont :

.8086, .186, .286, .286P, .386, .386P, .486, .486P, .586, .586P, .686, .686P

À partir du 286, il existe deux directives pour chaque génération. Celle suffixée d’un P accepte toutes les instructions du modèle, l’autre rejette les instructions privilégiées.

Le 586 est le Pentium de base, le 686 le Pentium Pro.

En mode 8086 et 186, les instructions du 8087 sont acceptées, de même que celles du 287 avec les 286 et 286P et celles du 387 avec les 386 et 386P. Dans ces conditions, l’utilisation des .8087 , .287  et .387  doit être relativement rare. En revanche, l’usage de .NO87 , qui inhibe l’assemblage des instructions FPU, peut se révéler utile.

Enfin, .K3D , .MMX , .XMM  autorisent respectivement l’assemblage des instructions des jeux 3DNow!, MMX et SSE/SSE2. La logique est respectée, en l’absence de ces directives, les instructions sont rejetées.

10.9.4 Déclaration complète des segments

Cette partie est un peu lourde. Il ne devrait pas y avoir de problème, en première lecture, à passer directement au suivant, traitant des directives de déclarations simplifiées. Sauf indication contraire, dans ce chapitre, nous utiliserons le terme segment pour désigner les segments logiques de MASM.

10.9.5 La directive SEGMENT … ENDS

Nous avons modifié le petit programme précédent, dans l’optique de ne pas utiliser la déclaration simplifiée des segments par la directive .MODEL et ainsi d’étudier de plus près la mécanique des segments :

.8086
 
PROG SEGMENT WORD PUBLIC 'CODE'
PROG ENDS
 
DATOS SEGMENT WORD PUBLIC 'DATA'
DATOS ENDS
@DATOS = seg DATOS
 
PILE SEGMENT PARA STACK 'STACK'
p    byte 100h dup (?)
PILE ENDS
 
DATOS SEGMENT
bijor db "Bonjour, monde !",0Dh, 0Ah,"$"
DATOS ENDS
 
PROG SEGMENT
debut:
  ; Initialisation du data segment
  mov ax, @DATOS
  mov ds, ax
 
  ; Message de bienvenue
  mov ah, 09h
  mov dx, offset bijor
  int 21h
 
 
  ;fin du programme et retour au DOS
  mov AH, 4Ch
  int 21h
PROG ENDS
END debut

Ce programme, ou plutôt la comparaison avec la première version, appelle un commentaire. La directive .MODEL simplifiait le travail. Mais avec une petite bibliothèque de squelettes et du copier-coller, ce n’est pas bien compliqué d’utiliser les déclarations complètes des segments. L’avantage des déclarations simplifiées est d’alléger le code source. Sans les facilités offertes par un macro-assembleur, comme .MODEL ou INVOKE , le moindre code source deviendrait très vite illisible. Il faut bien se convaincre que le principal intérêt des directives de haut niveau de MASM (et de tout macro-assembleur) est de rendre le code lisible et donc maintenable. À un certain niveau, illisibilité et infaisabilité tendent à se confondre.

Dans ce programme, la directive SEGMENT est utilisée d’abord trois fois, dont deux fois à blanc, pour les segments PROG et DATOS. La raison en est que l’ordre dans lequel les noms de segments font leur première apparition dans le code source peut avoir son importance comme nous le verrons. En procédant ainsi, nous maîtrisons cet ordre sans contrainte sur la rédaction du reste du code.

L’equate @DATOS = seg DATOS n’est pas réellement nécessaire ici, puisque mov ax, seg DATOS fonctionne très bien.

Le fichier listing :

seg2.asm                     Symbols 2 - 1
 
 
Segments and Groups:
 
        N a m e     Size    Length Align    Combine Class
 
DATOS  . . . . . . .16 Bit  0013   Word     Public  'DATA'  
PILE . . . . . . . .16 Bit  0100   Para     Stack   'STACK'  
PROG . . . . . . . .16 Bit  0010   Word     Public  'CODE'  
 
 
Symbols:
 
        N a m e     Type    Value  Attr
 
@DATOS . . . . . . .Number  0000h    
bijor  . . . . . . .Byte    0000   DATOS  
debut  . . . . . . .L Near  0000   PROG   
p  . . . . . . . . .Byte    0000   PILE   
 
      0 Warnings
      0 Errors

 

Tout commence donc par la directive SEGMENT , de syntaxe :

Nom   SEGMENT  [[READONLY]][[ align ]][[ combine ]][[ use ]][[ 'class' ]]

Instructions et données

Nom   ENDS

Tous les paramètres sont syntaxiquement facultatifs, en ce sens qu’ils ont une valeur par défaut, mais ils sont très importants.

Nom est une étiquette qui est accolée au segment. À l’intérieur d’un module, si plusieurs segments portent le même nom, ils sont traités comme un seul segment. Ils doivent avoir les mêmes paramètres, le plus simple étant de ne saisir ces paramètres qu’une seule fois. Les segments peuvent être imbriqués, même dans eux-mêmes. En réalité, un segment ouvert une première fois, éventuellement à blanc, est refermé puis ouvert et refermé à nouveau, selon les besoins de l’écriture du code. Il est visible que l’offset ou compteur de position évolue d’un tronçon à l’autre de façon continue.

Puisque nous évoquons le compteur de position, présentons la directive ORG , dont nous avons vu un exemple d'utilisation à propos du modèle TINY. En effet, les fichiers exécutables de type  .com sont de gros utilisateurs de cette directive. Sa syntaxe est :

ORG valeur

valeur est une valeur numérique immédiate, ou toute expression pouvant être ramenée par l'assembleur à une telle valeur. Elle doit impérativement se trouver au sein d'un bloc segment. Quand l'assembleur la rencontre, il force l'offset courant, ou compteur de position, à valeur . C'est ainsi qu'un  .com va pouvoir démarrer en 100h .

READONLY , si présent, indique à l’assembleur de signaler par une erreur tout code qui écrirait dans le segment.

Le paramètre align  est soit absent, soit un des mots BYTE, WORD, DWORD, PARA, PAGE, correspondant respectivement à 1, 2, 4, 16 ou 256 octets. Si le paramètre est absent, c’est PARA qui est pris en compte. L’assembleur s’adressera au lieur, qui traitera avec le loader, pour que le premier octet du segment soit sur la première adresse disponible divisible par le paramètre. La valeur par défaut de PARA/16 octets est logique, puisque les segments physiques sont obligatoirement alignés de cette façon. D’autre part, si le segment logique est porté par un segment physique et que l’alignement est sur BYTE, WORD ou DWORD, il faudra que le segment physique commence sous le segment précédent et que l’offset ne démarre pas à 0. Mais ceci est une affaire de lieur et de loader.

À partir du moment où nous maîtrisons l’alignement du début du segment logique, il sera possible de contrôler l’alignement des divers éléments à l’intérieur de ce segment. Et l’alignement des données est un point particulièrement important aujourd’hui.

Le paramètre combine  est un des mots PUBLIC, STACK, COMMON, MEMORY, AT address , PRIVATE. S’il est omis, c’est PRIVATE qui est traité par défaut. Ce paramètre agit sur le comportement du lieur, quant à la façon de traiter des segments de même nom issus de différents modules.

PUBLIC ou MEMORY : concatène tous les segments de même nom pour faire un seul segment. Correct pour les données et le code.

STACK : concatène tous les segments de ce type du même nom, pour transmettre au DOS une seule pile à initialiser. Voir COMMON.

COMMON : transforme les segments de même nom en un seul segment de la taille du plus grand d'entre eux, quand ils ne sont pas de même taille. Pour STACK et COMMON, le lieur voit deux fois le début de la zone, une fois pour chaque module. Donc, s’il y avait des données initialisées, ce serait le dernier traité qui gagnerait, l’autre perdrait ses données. Donc, pas de données initialisées dans des segments de paramètre combine STACK ou COMMON.

AT memory  : réservé au mode Réel. Ne peut contenir ni données ni code. Permet de coder pour une ressource mémoire particulière. Il s'agit tout simplement de déclarer une zone dont l'adresse est connue de façon absolue, un tampon d'écran par exemple.

use est un des mots USE16, USE32 ou FLAT. Permet de déterminer la taille de mot par défaut, opérande et adresse, à 16 ou 32 bits, sur le segment. Voir à ce sujet la section sur les directives de processeur en début de chapitre.

'class' permet de créer des catégories. Des segments de même nom ne seront pas agrégés si leur classe est différente. D’un autre côté, le lieur fait en sorte de rapprocher les segments de même classe. Les noms de classes ne sont pas imposés. Toutefois, les noms ‘CODE’, ‘DATA’, ‘COST’ et ‘STACK’ sont classiques. De plus, nommer ‘KIKI’ le segment de code génère un warning. Il est donc préférable de rester classique.

10.9.6 La directive GROUP

Elle répond à la syntaxe :

nom GROUP   segment [[, segment ]]...

Un groupe est un ensemble de segments homogènes, c’est-à-dire tout 16 bits ou tout 32 bits. En mode 16 bits, la taille totale des segments d'un groupe ne doit pas dépasser 64 Ko, 4 Go en mode 32 bits. L'idée est bien entendu de placer l'ensemble des segments logiques d'un groupe dans le même segment physique. Ainsi, nous bénéficierons de la facilité offerte par de multiples segments, de données en particulier, tout en profitant d'un adressage NEAR , la valeur de DS ne changeant pas d'un segment logique à l'autre.

Il est possible de définir un groupe en plusieurs fois :

MonGroupe GROUP Seg1, Seg2
…
MonGroupe GROUP Seg3

Le groupe MonGroupe est alors constitué des segments Seg1 , Seg2 et Seg3 .

Si nous reprenons le premier exemple de ce chapitre, basé sur le modèle SMALL, nous constatons qu'un groupe DGROUP est créé, comportant les segments _DATA et STACK . Nous verrons que de nombreux modèles créent un groupe de données de ce nom. Nous constatons que les constantes symboliques @data et @stack sont initialisées à la même valeur, DGROUP , qui est le sélecteur (ici l'adresse x 16) du segment physique unique.

Nous complèterons notre exemple de déclaration complète de segments par la déclaration d’un groupe. Le code est à la fin du paragraphe suivant, puisque nous utiliserons également la directive ASSUME .

10.9.7 La directive ASSUME et les registres de segments

Le verbe anglais to assume signifie supposer, considérer que. Il existe deux directives ASSUME , qui toutes deux demandent à l’assembleur de supposer quelque chose. L’une s’adresse aux registres généraux et elle est décrite avec les pointeurs. L’autre s’occupe du contenu des registres de segments, c’est celle-ci qui nous intéresse ici.

L’assembleur sait quel registre de segment associer par défaut à chaque instruction référençant la mémoire. Par exemple SS pour un PUSH ou un POP , CS pour un CALL ou un JMP , DS pour un MOV , mais ce peut être aussi ES, FS, GS en le précisant. Il est de la responsabilité du programmeur d’affecter une valeur à tout ou partie de ces registres :

mov ax, @data
mov ds, ax
mov es, ax

Les registres DS et ES ont été initialisés par une valeur de segment physique.

Maintenant, voyons ce qui se passe du côté de l’assembleur, à l’aide d’un extrait de code :

DATOS SEGMENT
bijor db "Bonjour, monde !",0Dh, 0Ah,"$"
Ecrit db 09h
DATOS ENDS
 
PROG SEGMENT
debut:
  ; Initialisation du data segment
  mov ax, @DATOS
  mov ds, ax
  mov ax, 0
  mov es, ax
 
  ; Message de bienvenue
  mov ah, Ecrit
  mov dx, offset bijor
  int 21h

L’assembleur va générer un message d’erreur :

seg3.asm(37) : error A2074: cannot access label through segment registers

Il considère qu’il n’a pas les éléments pour savoir si la base du segment physique contenant Ecrit doit être cherchée dans DS, ES, etc. Il suffirait d’écrire mov ah, ds:Ecrit pour qu’il soit content. Il serait aussi satisfait de mov ah, es:Ecrit , ou même mov ah, cs:Ecrit , alors qu’une petite analyse du code montre qu’il s’agit certainement d’une erreur, mais cela ne le regarde pas. Voici le code généré par les trois versions :

0012      8A 26 0013 R   mov ah, ds:Ecrit
0012  26: 8A 26 0013 R   mov ah, es:Ecrit
0012  2E: 8A 26 0013 R   mov ah, cs:Ecrit

Nous notons, dans les deux cas ES et CS, un préfixe de surcharge de segment par défaut.

Avec ASSUME DS : DATOS , plus de problème. Par cette directive, le programmeur affirme que le contenu de DS pointe vers le segment logique DATOS , il demande à l’assembleur de supposer que DS contient @DATOS (ou offset DATOS ). Nous pouvons sur cet exemple juger l’assembleur timoré. Mais analyser le code source n’est pas de son ressort, par contre il lui appartient de signaler par une alerte ou une erreur tout code ambigu. Même si nous pensons que l’assembleur pourrait supposer par défaut le registre par défaut, DS, il ne le fait pas et c’est comme cela que nous devons travailler.

Il est possible de définir un ou plusieurs registres de segments, séparés par des virgules, dans une directive ASSUME . Une telle directive sur un segment annule la précédente définition :

ASSUME NomSegment : expression [, NomSegment : expression ]...

expression est toute expression pouvant se résoudre en une base de segment :

  Un nom de segment : ASSUME ds:DATOS .

  Un nom de groupe : ASSUME ds:MonDGROUP .

  Le segment d’une variable : ASSUME ds:SEG Ecrit .

  Un equate se traduisant par un nom de segment.

  Un mot clé ERROR , NOTHING ou FLAT .

ERROR  protège un registre de segment en générant une erreur s’il est utilisé. NOTHING  dé-ASSUME le registre (annule l’effet de ASSUME ). ASSUME NOTHING fait la même chose pour tous les registres. FLAT  désigne le segment unique du modèle du même nom.

Voici, comme promis, une évolution de notre exemple, avec utilisation de GROUP et ASSUME  :

.8086
 
MonDGROUP GROUP DATOS, PILE
MonDGROUP GROUP DATO2
 
@DATOS = seg MonDGROUP
 
ASSUME cs:PROG, ss:MonDGROUP, ds:MonDGROUP, es:ERROR
ASSUME ds:SEG Ecrit ;inutile
 
PROG SEGMENT WORD PUBLIC 'CODE'
essai db "Essai",0Dh, 0Ah,"$"
PROG ENDS
 
DATOS SEGMENT WORD PUBLIC 'DATA'
DATOS ENDS
 
 
PILE SEGMENT PARA STACK 'STACK'
p    byte 100h dup (?)
PILE ENDS
 
DATOS SEGMENT
bijor db "Bonjour, monde !",0Dh, 0Ah,"$"
DATOS ENDS
 
DATO2 SEGMENT WORD PUBLIC 'TEMPO'
Ecrit db 09h
DATO2 ENDS
 
PROG SEGMENT
debut:
  ; Initialisation du data segment
  mov ax, @DATOS
  mov ds, ax
  mov ax, 0
  mov es, ax
 
  ; Message de bienvenue
  mov ah, Ecrit
  mov dx, offset bijor
  int 21h
 
  ;fin du programme et retour au DOS
  mov ah, 4Ch
  int 21h
PROG ENDS
END debut

Signalons à nouveau que CS et SS seront initialisés par le loader. Par contre, il faut initialiser DS (et ES si utilisé, ainsi que FS et GS sur un processeur supérieur), la directive ASSUME n’y change rien.

10.9.8 Maîtrise de l’ordre des segments

L’ordre d’implantation des segments en mémoire est un problème qui a toutes les chances de ne jamais se poser. D’autant plus que si les segments ont été correctement conçus, les classes bien choisies, le résultat ne sera pas calamiteux. Mais pour le cas où nous aurions une contrainte très particulière, comme un fichier  .com où le code doit commencer à l’offset 100h , voyons ce que MASM nous propose.

Par défaut, les segments apparaissent dans les fichiers objets dans l’ordre de leur apparition dans le code source. Le lieur traite les divers fichiers objets dans l’ordre de la ligne de commandes. Notons que cet ordre est déterminé par ml.exe si nous le laissons appeler lui-même le lieur.

L’ordre dans lequel les segments apparaissent dans l’exécutable est contrôlé par trois directives :

  .SEQ correspond à l’ordre par défaut de l’apparition dans le code source.

  .ALPHA arrange les segments dans le module par ordre alphabétique. Répond à un besoin de compatibilité avec un archaïque assembleur IBM.

  .DOSSEG , la seule à connaître, arrange les segments dans l’ordre standard des langages Microsoft (convention MS-DOS). Cet ordre est le suivant :

  Segments de code (nom de classe CODE).

  Segments hors de DGROUP.

  Segment de données initialisées de DGROUP.

  Segment de données non initialisées de DGROUP (nom de classe BSS).

  Segment de pile de DGROUP (nom de classe STACK).

La directive .DOSSEG est à l’origine de problèmes. Elle ne doit généralement pas être utilisée en liaison avec d’autres langages.

.DOSSEG  est une directive de l’assembleur. Mais des informations sont passées au lieur afin qu’il respecte l’ordre tel que nous venons de le décrire.

La meilleure (la plus simple) solution pour contrôler au mieux l’ordre des segments en mémoire à l’issue du travail du lieur est peut-être la stratégie suivante : déclarer, éventuellement à blanc, tous les segments, dans l’ordre souhaité, dans le premier module, par exemple dans un fichier d’include. Les segments appartenant à d’autres modules resteront vides dans le premier module, seul le fait que le nom soit commun importe. Il faudra veiller à ce que ce premier module figure effectivement en première position dans la liste des fichiers objets à lier.

 

10.9.9 Déclaration simplifiée des segments

Nous venons de voir que la définition d’une cartographie des segments, dans chaque module qui plus est, est une opération relativement complexe, qui grève la lisibilité du code source et est certainement cause potentielle d’erreurs. Or, pensons-nous réellement que nos besoins dans ce domaine sont originaux ? Non, en fait, un certain nombre de configurations standard vont répondre à l’immense majorité de nos besoins. C’est ce que va nous offrir la notion de modèle, basé sur la directive .MODEL .

Avantage supplémentaire non négligeable, les modèles standard vont faciliter la liaison de nos modules avec d’autres écrits en langage évolué, puisque justement ils sont standard.

Un exemple d’utilisation très simple du modèle SMALL a été donné en introduction de la présentation des déclarations complètes de segments.

Au départ nous trouvons toujours la directive .MODEL .

10.9.10 La directive .MODEL

Cette directive a sur le module de grandes conséquences. Elle est unique dans le module. Si vous utilisez deux fois la directive .MODEL , une simple alerte sera générée et la seconde directive simplement ignorée. Prudence, donc :

            .MODEL SMALL
            .STACK 100h
            .MODEL LARGE
seg1.asm(3) : warning A4011: multiple .MODEL directives found : .MODEL ignored

Dans ce cas, le modèle reste SMALL.

La directive .MODEL est nécessairement préalable à l’utilisation des autres directives de déclaration simplifiée de segments. Elle sera donc placée près du début de module, sans en être obligatoirement la première directive. En fait, certaines directives antérieures à .MODEL peuvent en modifier les options par défaut. La syntaxe de MASM étant suffisamment compliquée, il est sans doute préférable de débuter par .MODEL , puis de positionner les options particulières importantes, sans trop compter sur leur valeur par défaut.

Donc, .MODEL affecte l’ensemble du module, sur les points suivants :

  Modèle mémoire.

  Conventions d‘appel.

  Système d’exploitation (pour nous, DOS ou Windows).

  Type de pile.

De plus, elle crée des segments et groupes, en leur donnant des noms qu’il faudra ou non connaître.

La syntaxe de la directive .MODEL est :

.MODEL ModèleMémoire [[, Langage ]] [[, OptionPile ]]

Seul ModèleMémoire est obligatoire. C’est un modèle choisi dans la liste TINY, SMALL, COMPACT, MEDIUM, LARGE, HUGE, ou FLAT.

Langage détermine en réalité les conventions d’appel et de nom des procédures et symboles publics. Le choix est à faire parmi C, BASIC, FORTRAN, PASCAL, SYSCALL et STDCALL.

OptionPile détermine la localisation du segment de pile : NEARSTACK pour une pile dans le même segment physique que les données, dans DGROUP, SS = DS. FARSTACK crée une pile autonome, avec une valeur propre de SS.

Les sept modèles mémoire ne sont pas spécifiques à l’assembleur, ils sont utilisés dans d’autres langages de haut niveau comme le C. En voici les caractéristiques de base :

Attributs généraux des modèles mémoire

Modèle

Code

Données

OS

Données et code

@Model

TINY

NEAR

NEAR

MS-DOS

Même segment

1

SMALL

NEAR

NEAR

MS-DOS, Windows

Segments séparés

2

COMPACT

FAR

NEAR

MS-DOS, Windows

Segments séparés

3

MEDIUM

NEAR

FAR

MS-DOS, Windows

Segments séparés

4

LARGE

FAR

FAR

MS-DOS, Windows

Segments séparés

5

HUGE

FAR

FAR

MS-DOS, Windows

Segments séparés

6

FLAT

NEAR

NEAR

Windows

Même segment

7

Les valeurs NEAR ou FAR pour le code et les données sont les valeurs par défaut. @Model est une constante symbolique créée par la directive .MODEL qui permet d’avoir accès à l’exécution au modèle mémoire.

Les modèles TINY et FLAT se distinguent des cinq autres, qui sont relativement proches. Ces deux modèles ont en commun de ne pas être segmentés ou de ne proposer qu'un seul segment, ce qui est la même chose.

Le modèle TINY (tout petit) ne tourne que sous MS-DOS. Tout est dans un seul segment, donc tient dans 64 Ko. Les registres -CS, SS et DS contiendront donc la même valeur. Les adressages sont nécessairement NEAR pour le code et les données. Il est possible d'utiliser des données  FAR , mais elles seront allouées dynamiquement à l'exécution, par Allocate Memory, fonction 48h du DOS. DOS renvoie alors le segment dans AX. Ce modèle est celui des .com de MS-DOS. La directive .MODEL TINY envoie l'information au lieur, le paramètre /AT n'est donc pas nécessaire.

.MODEL TINY
.STACK 10h
 
.CODE
ORG 100h
debut:
  ; Message de bienvenue
  mov ah, 09h
  mov dx, offset bijor
  int 21h
  ;fin du programme et retour au DOS
  mov AH, 4Ch
  int 21h
 
bijor db "Bonjour, monde !",0Dh, 0Ah,"$"
 
END debut

Le seul point important est la directive ORG  100h . En effet, l'exécutable .com sera placé en mémoire à l'offset  100h par rapport au segment unique, nous avons déjà abordé ce point. En l'absence de cette directive, offset bijor pointera 100h  octets trop en avant dans la mémoire.

Le modèle  FLAT est le plus simple de tous. Il n'y a qu'un seul segment de 4 Go et c'est le loader du système d'exploitation qui initialise CS, SS, DS et ES par le sélecteur de ce segment. Ces quatre segments forment le groupe  FLAT . La seule raison de modifier le contenu de ces registres est de mêler 16 et 32 bits dans la même application, mais nous sortons alors du cadre strict du modèle. Attention, le mode 16 ou 32 bits par défaut dépend de la déclaration du type de processeur. Voir à ce sujet la section sur les directives de processeur en début de chapitre.

Les cinq modèles SMALL, COMPACT, MEDIUM, LARGE et HUGE sont issus des modèles mémoire traditionnels sous MS-DOS. Le modèle SMALL propose un seul segment de code et un seul segment de données. LARGE plusieurs segments de code et plusieurs segments de données. MEDIUM plusieurs segments de code et un seul segment de données. Pour COMPACT, c'est le contraire, plusieurs segments de données et un seul segment de code. Enfin, HUGE (hénaurme) est une autre forme de LARGE. La différence est surtout dans l'esprit, HUGE est destiné à l'utilisation de structures de données dont la taille dépasse celle d'un segment. Mais la gestion de ces structures incombe au programmeur, donc, c'est tout simplement du LARGE.

Les conventions de langage STDCALL, BASIC, FORTRAN et PASCAL sont équivalentes, SYSCALL et C également sur les conventions d'appel mais différentes sur les conventions de nommage. Les conventions d'appel sont vues ailleurs dans cet ouvrage. Le langage peut être défini avec la directive .MODEL , mais également par OPTION LANGUAGE . Il doit de toute façon l'être avant d'utiliser PROC, INVOKE, EXTERN, PUBLIC, PROTO dont il modifie le comportement. Le programme peut retrouver la convention de langage en cours au travers de la constante symbolique @Interface .

NEARSTACK indique que la pile sera placée dans DGROUP, donc que CS et SS contiendront la même valeur. L'initialisation de SS:SP sera soit faite par le programmeur, soit par la directive .STARTUP (voir plus loin dans ce chapitre). À l’inverse, FARSTACK prévoit un segment physique particulier pour la pile.

La constante symbolique @stack (macro texte) renvoie le nom du segment de pile, c’est-à-dire DGROUP pour une NEARSTACK et STACK pour une FARSTACK. Ce nom peut-être utilisé comme @data, de la façon suivante : mov ax, @stack .

 

10.9.11 Création des segments de code, pile et données

Pour les modèles TINY et FLAT, nous en savons presque assez pour programmer. Par contre, il nous faut étudier les segments mis à notre disposition par les autres modèles, comment initialiser les registres correspondants si nécessaire, etc. C’est donc à ces cinq modèles que s’applique ce qui suit.

Segment de pile

Il est parfois bon d'enfoncer le clou : .STACK est une directive de segment simplifié, qui n'a donc de sens qu'après les définitions d'un modèle par .MODEL . Dans ces conditions, la syntaxe est :

.STACK [[ taille ]]

Si taille n'est pas indiqué, la taille de la pile est fixée par défaut à 1 Ko. Si vous créez plusieurs fois la pile à l'aide de cette directive, une seule pile sera allouée, de la taille du plus grand des paramètres taille .

Voir, au sujet du nom et de la distance du segment de pile, la fin de la partie sur .MODEL , le paramètre FARSTACK/NEARSTACK.

C'est tout ce qu'il y a à faire pour initialiser la pile.

Segments de données

La définition des segments de données est plus complexe et laisse place à plus de choix. Sauf s'il s'agit d'un seul segment.

Les noms de segments sont en conformité avec les normes en vigueur pour les langages de haut niveau, typiquement ceux de Microsoft. Cette compatibilité est la principale raison de la présence de certains noms. Les directives mises en jeu sont .DATA , .DATA? , .CONST , .FARDATA  et .FARDATA? . Ces directives correspondent à des types de segments particuliers, ces associations ne sont pas toujours absolument obligatoires, mais il serait stupide d'utiliser les directives de création simplifiée des segments et de ne pas jouer le jeu.

Remarque

Données initialisées, données non initialisées

Il n'est pas obligatoire non plus de distinguer données initialisées et données non initialisées. Néanmoins, c'est une bonne habitude. Nous pouvons imaginer qu'elles sont traitées de façon différente, par le lieur en particulier. Un segment de données initialisées qui au moment du chargement ne sont rien d'autre que des constantes devra être véhiculé en entier dans l'exécutable (ou un fichier annexe, peu importe), puis à un moment ou à un autre copié en mémoire. Par contre, un segment de données non initialisées n'est rien d'autre qu'une allocation de mémoire. La seule chose véhiculée par l'exécutable, c'est la définition d'une requête, soit quelques octets, quelle que soit la taille de la zone. Il est donc clair que mélanger des données initialisées et des données non initialisées n'est pas toujours optimal. Sauf bonne raison. Remarque : si vous avez besoin d'une zone de 1 Mo initialisée à 0 ou toute autre valeur fixe, il sera préférable de déclarer cette zone comme non initialisée, puis d'écrire une demi-douzaine de lignes de code pour l'initialisation. Une autre méthode serait de demander dans le code au système d'allouer le méga-octet de mémoire à l'aide de la fonction appropriée. Mais vous perdriez le confort donné par un vrai segment de données, étiquettes, noms et types des variables, etc.

MASM crée systématiquement, pour les modèles autres que FLAT où il crée un groupe FLAT , un groupe DGROUP , pour accueillir dans le même segment physique des données et la pile.

Pour créer des segments de données NEAR , vous utilisez la directive à tout faire .DATA , pour créer le segment logique _DATA contenant les données ordinaires du programme. Il est possible de créer, dans le même groupe, les segments logiques _BSS par .DATA? , destiné aux données non initialisées (initialisées à 0 par .STARTUP en option langage C), et CONST par .CONST , particulièrement destiné aux constantes. Il y a donc un choix entre l’usage de ces trois directives et l’utilisation organisée de la seule .DATA . Dans tous les cas, tout se passe dans la zone de 64 Ko de DGROUP (nous ne sommes pas en modèle FLAT ).

Pour créer des segments de données FAR , il faut utiliser la directive .FARDATA  [[ NomSegment ]] pour créer un segment nommé soit NomSegment , soit si aucun nom n’est donné FAR_DATA . De la même façon, un segment de données  FAR non initialisé pourra être créé par .FARDATA?  [[ NomSegment ]], et sera nommé soit NomSegment , soit FAR_BSS .

@data   renvoie le nom du groupe du segment de donnée par défaut, donc DGROUP , ou FLAT pour le modèle FLAT. Le nom du groupe désigne également l’adresse de base du segment dans mov ax, @data , déjà vu plusieurs fois. De la même façon, @fardata renvoie le nom du segment défini par .FARDATA et @fardata? celui du segment défini par .FARDATA? .

@DataSize  renvoie le modèle mémoire, 0 pour TINY, SMALL, MEDIUM et FLAT, 1 pour COMPACT et LARGE, et 2 pour HUGE. @WordSize renvoie la taille en octets de l’attribut de taille du segment (2 pour 16 bits, 4 pour 32 bits). Enfin, @CurSeg renvoie le nom du segment courant.

Segments de code

La directive .CODE  indique le début d’un segment de code. La syntaxe est .CODE [[ NomSegment ]]. Il est ainsi facile de changer le nom par défaut et de créer très simplement plusieurs segments de code logiques.

Ce segment continue jusqu’à la directive de segment suivante, .DATA par exemple, qui ferme le segment de code. Bien entendu, .CODE peut être réutilisée pour rouvrir et continuer le segment de code. C’est la même chose pour tous les segments. La directive END ne fait que fermer les segments encore ouverts, généralement un seul.

Dans les modèles SMALL , COMPACT et TINY , même s’il est créé plusieurs segments de code, ils sont de type NEAR (adresses sur deux octets) et sont combinés dans un segment nommé _TEXT .

Pour les modèles MEDIUM , LARGE et HUGE , l’adressage sera de type FAR . Chaque module aura son segment de code, nommé NomModule _TEXT, à moins qu’un nom ne soit donné à la suite de la directive .CODE , auquel cas ce nom remplacera NomModule .

@CodeSize renvoie le modèle mémoire, 0 pour TINY, SMALL, MEDIUM et FLAT, 1 pour COMPACT, LARGE et HUGE.

Tableaux récapitulatifs

Utiliser les directives de segments simplifiées a pour but de... simplifier la vie du programmeur. Dans ces conditions, la connaissance des seuls mots clés .CODE , .DATA , .FARDATA? , etc. est suffisante le plus souvent. Mais s'il s'agit de travailler à partir d'un modèle et de lui ajouter des segments, pour travailler par exemple dans un contexte mixte 16 et 32 bits, il faudra connaître un peu plus de ce qui se cache sous le capot.

Nous avons, dans cette optique, rassemblé ici une série de tableaux permettant de retrouver rapidement le nom et les attributs des segments créés par défaut par la directive .MODEL dans ses différents modèles mémoire.

 

Segments par défaut pour le modèle TINY

Directive

Nom

Alignement

Combine

Classe

Groupe

.CODE

_TEXT

WORD

PUBLIC

'CODE'

DGROUP

.FARDATA

FAR_DATA

PARA

PRIVATE

'FAR_DATA'

 

.FARDATA?

FAR_BSS

PARA

PRIVATE

'FAR_BSS'

 

.DATA

_DATA

WORD

PUBLIC

'DATA'

DGROUP

.CONST

CONST

WORD

PUBLIC

'CONST'

DGROUP

.DATA?

_BSS

WORD

PUBLIC

'BSS'

DGROUP

 

Segments par défaut pour le modèle SMALL

Directive

Nom

Alignement

Combine

Classe

Groupe

.CODE

_TEXT

WORD

PUBLIC

'CODE'

 

.FARDATA

FAR_DATA

PARA

PRIVATE

'FAR_DATA'

 

.FARDATA?

FAR_BSS

PARA

PRIVATE

'FAR_BSS'

 

.DATA

_DATA

WORD

PUBLIC

'DATA'

DGROUP

.CONST

CONST

WORD

PUBLIC

'CONST'

DGROUP

.DATA?

_BSS

WORD

PUBLIC

'BSS'

DGROUP

.STACK

STACK

PARA

STACK

'STACK'

DGROUP *

* : si NEARSTACK ou par défaut, pas si FARSTACK.

 

Segments par défaut pour le modèle MEDIUM

Directive

Nom

Alignement

Combine

Classe

Groupe

.CODE

Module_TEXT

WORD

PUBLIC

'CODE'

 

.FARDATA

FAR_DATA

PARA

PRIVATE

'FAR_DATA'

 

.FARDATA?

FAR_BSS

PARA

PRIVATE

'FAR_BSS'

 

.DATA

_DATA

WORD

PUBLIC

'DATA'

DGROUP

.CONST

CONST

WORD

PUBLIC

'CONST'

DGROUP

.DATA?

_BSS

WORD

PUBLIC

'BSS'

DGROUP

.STACK

STACK

PARA

STACK

'STACK'

DGROUP *

* : si NEARSTACK ou par défaut, pas si FARSTACK.

 

Segments par défaut pour le modèle COMPACT

Directive

Nom

Alignement

Combine

Classe

Groupe

.CODE

_TEXT

WORD

PUBLIC

'CODE'

 

.FARDATA

FAR_DATA

PARA

PRIVATE

'FAR_DATA'

 

.FARDATA?

FAR_BSS

PARA

PRIVATE

'FAR_BSS'

 

.DATA

_DATA

WORD

PUBLIC

'DATA'

DGROUP

.CONST

CONST

WORD

PUBLIC

'CONST'

DGROUP

.DATA?

_BSS

WORD

PUBLIC

'BSS'

DGROUP

.STACK

STACK

PARA

STACK

'STACK'

DGROUP *

* : si NEARSTACK ou par défaut, pas si FARSTACK.

 

Segments par défaut pour les modèles LARGE et HUGE

Directive

Nom

Alignement

Combine

Classe

Groupe

.CODE

Module_TEXT

WORD

PUBLIC

'CODE'

 

.FARDATA

FAR_DATA

PARA

PRIVATE

'FAR_DATA'

 

.FARDATA?

FAR_BSS

PARA

PRIVATE

'FAR_BSS'

 

.DATA

_DATA

WORD

PUBLIC

'DATA'

DGROUP

.CONST

CONST

WORD

PUBLIC

'CONST'

DGROUP

.DATA?

_BSS

WORD

PUBLIC

'BSS'

DGROUP

.STACK

STACK

PARA

STACK

'STACK'

DGROUP *

* : si NEARSTACK ou par défaut, pas si FARSTACK.

 

Segments par défaut pour le modèle FLAT

Directive

Nom

Alignement

Combine

Classe

Groupe

.CODE

_TEXT

DWORD

PUBLIC

'CODE'

FLAT

.FARDATA

_DATA

DWORD

PUBLIC

'DATA'

FLAT

.FARDATA?

_BSS

DWORD

PUBLIC

'BSS'

FLAT

.DATA

_DATA

DWORD

PUBLIC

'DATA'

FLAT

.CONST

CONST

DWORD

PUBLIC

'CONST'

FLAT

.DATA?

_BSS

DWORD

PUBLIC

'BSS'

FLAT

.STACK

STACK

DWORD

PUBLIC

'STACK'

FLAT

 

Le groupe FLAT du modèle du même nom apparaît effectivement dans les listings, mais n'est pas vraiment documenté. Il y est parfois fait allusion sous l'appellation de supergroupe.

Quand la directive .MODEL est traitée par l'assembleur, une directive ASSUME (virtuelle ou cachée) est exécutée. Voici les valeurs de ces ASSUME pour les différents modèles :

Directives ASSUME cachées

Modèle

CS

DS

SS

TINY

DGROUP

DGROUP

DGROUP *

SMALL & COMPACT

_TEXT

DGROUP

DGROUP *

MEDIUM , LARGE & HUGE

Module_TEXT

DGROUP

DGROUP *

* : si NEARSTACK

 

10.9.12 Code de démarrage et code de fin : .STARTUP et .EXIT

Les directives .STARTUP  et .EXIT  génèrent le code de démarrage et de fin d’une application, ce n’est pas une surprise. .STARTUP se place uniquement au début du module principal, .EXIT à la fin du même module. C’est tout. De plus, ces directives ne sont pas utilisables dans les programmes 32 bits modèle FLAT.

Attention, ces deux directives génèrent du code. Il ne s’agit pas uniquement de marqueurs de début et de fin de programme, bien qu’elles volent une partie de ce rôle au couple debut : .... END debut .

Sans ces directives, la structure d’un programme principal écrit dans les règles de l’art est :

.CODE
debut :
;code d’initialisation
.
.
.
;code de clôture
END debut

Le fait que debut se trouve après la directive END le désigne comme marqueur de début de programme (et non son nom !). Les codes d’initialisation et de clôture sont du tonneau du programmeur. Il s’agira par exemple d’initialiser DS à la bonne valeur.

Avec les deux directives de démarrage et de fin, la structure devient :

.CODE
.
.STARTUP
.
.
.
.EXIT
END

.STARTUP détermine le début de l’exécution. Il n’est pas obligatoire que la directive débute le segment de code. .EXIT marque la fin de l’exécution, alors que END , qui n’a plus besoin d’étiquette puisque le point d’entrée est connu, est simplement la fin syntaxique du code source.

Nous avons fait quelques essais (CD-Rom, dossier Startup ). C'est à partir du résultat de ces essais que nous allons voir ce que font exactement ces deux directives.

Premier point, .EXIT accepte un paramètre, uniquement une valeur immédiate. Cette valeur est la valeur de retour du programme, dans AX avant d'appeler la fonction EXIT ( 4Ch ) du DOS. Pour toutes les configurations, les fonctions de sortie, correspondant à .EXIT 1 , est :

             .EXIT 1
 0030  B8 4C01    *  mov    ax, 04C01h
 0033  CD 21      *  int    021h

C'est le code de fin dont nous avions l'habitude, avec parfois mov ah, 4ch si pas de valeur de retour, ce qui n'est pas effectivement une bonne habitude.

Nous avons vérifié, n'ayant rien trouvé à ce sujet dans la documentation (mal cherché, sans doute), que la directive .STARTUP ne peut exister qu'à un seul exemplaire dans le programme, alors qu'il est possible de multiplier les .EXIT , ce qui tombe sous le sens.

Le code généré par .STARTUP dépend à la fois du processeur et de la distance de la pile, FARSTACK ou NEARSTACK.

FARSTACK et .8086 ou .586 :

           .STARTUP
0000           *@Startup:
0000  BA ---- R *   mov    dx, DGROUP
0003  8E DA     *   mov    ds, dx

Nous connaissons déjà ce code, en remplaçant parfois DGROUP par @data.

NEARSTACK et .8086 :

              .STARTUP
0000           *@Startup:
0000  BA ---- R *   mov    dx, DGROUP
0003  8E DA     *   mov    ds, dx
0005  8C D3     *   mov    bx, ss
0007  2B DA     *   sub    bx, dx
0009  D1 E3     *   shl    bx, 001h
000B  D1 E3     *   shl    bx, 001h
000D  D1 E3     *   shl    bx, 001h
000F  D1 E3     *   shl    bx, 001h
0011  FA        *   cli
0012  8E D2     *   mov    ss, dx
0014  03 E3     *   add    sp, bx
0016  FB        *   sti

Avec le 8086, l'instruction shl reg, imm n'existait pas. Il est donc fait appel quatre fois à shl bx, 1 . Dans le dernier bloc, les registres SS et SP sont modifiés. Or, le traitement de l'interruption utilisait la pile, et que ce serait-il passé si une interruption survenait pendant mov ss, dx  ? L'instruction se terminerait, puis l'interruption serait prise en compte, SS étant modifié sans que SP ne le soit. Donc SS:SP  pointe vers n'importe quoi. Donc, MASM masque les interruptions par CLI pour la durée de ces deux instructions, avant de les autoriser à nouveau par STI.

NEARSTACK et .586 :

              .STARTUP
0000           *@Startup:
0000  B8 ---- R *   mov    ax, DGROUP
0003  8E D8     *   mov    ds, ax
0005  8C D3     *   mov    bx, ss
0007  2B D8     *   sub    bx, ax
0009  C1 E3 04  *   shl    bx, 004h
000C  8E D0     *   mov    ss, ax
000E  03 E3     *   add    sp, bx

Il n'est plus nécessaire (et généralement plus possible) depuis le 286 et le mode Protégé de masquer les interruptions. Et en vrai mode Réel ? Eh bien, si le but est de travailler en vrai mode Réel, que rien (ou si peu) ne distingue d'un 8086, il faut utiliser la directive .8086 . Pour le shl bx, 4, no comment.

Comment ça marche ? Les deux premières instructions transfèrent DGROUP dans DS, rien de sorcier, nous avons longuement évoqué le sujet, @data à la place de DGROUP, mais nous savons maintenant que c'est la même chose.

Le reste est un peu plus futé. Au lancement, le programme reçoit du loader une pile toute neuve, sous la forme d'une valeur dans SS, et il faut bien le supposer, de la place au-dessous pour que la pile puisse pousser. Mais nous, en NEARSTACK, nous souhaitons que SS soit égal à DGROUP, ce qui est fait à l'avant-dernière ligne. Attention, SS comme DGROUP sont des contenus de registres de segments, donc pas exactement des adresses. Pour obtenir l'adresse de base correspondante, il faut multiplier par 16, soit ajouter 4 bits à 0 à droite.

Pour utiliser cette pile, il faut initialiser SP pour que la pile pointe sur le sommet de la pile fournie par le loader : SS:0000h .

Pile neuve
figure 10.17 Pile neuve [the .swf]

Il est clair que la représentation est approximative, à cause de l'ambiguïté signalée entre adresse et contenu de segment. Mais le dessin dit bien ce qu'il veut dire. Nous voyons qu'une fois SS initialisé par DGROUP, il faut que SP (stack pointer) contienne la différence en octets entre la base de DGROUP et la base de l'ancien segment de pile, pointé par SS initial : 0000h . Il est clair également que multiplier DGROUP et SS initial par 16 puis faire la différence donne le même résultat que faire la différence puis la multiplication.

 

10.10 Macros

D'une façon très générale et dans tous les langages où cette notion existe, une macro est une simple possibilité de substitution automatique dans le code source d'une chaîne par une autre. La macro de base pourrait presque être simulée par un traitement de texte comme Word.

Ce remplacement est effectué pendant une phase préalable à la compilation ou à l'exécution dans le cas d'un langage interprété. Cette phase est purement mécanique : inventaire des mots remplaçables, puis recherche et remplacement dans le code source des occurrences de ces mots. Il est vraisemblable que, dans la même phase, les commentaires soient éliminés du code source temporaire. Logiquement, les remplacements ne seront pas effectués dans les chaînes de caractères, ni dans les commentaires. Il y aura donc dans cette phase une première analyse syntaxique du code source.

Pour fixer les idées, nous pourrions vouloir franciser un hypothétique langage de la façon suivante :

imprime DEFINI_COMME print
...
imprime("Bonjour, le monde.")

Nous avons défini la macro imprime . Ce code sera exécuté exactement comme : print("Bonjour, le monde.") . Disons plutôt qu'il sera d'abord transformé ainsi, puis compilé ou exécuté.

Dans certains langages, cette phase appelée précompilation, généralement transparente peut être isolée, gérée par un programme nommé préprocesseur. C'est par exemple cpp32.exe en C++ Borland. Des options peuvent également être passées au compilateur pour le préprocesseur intégré, pour par exemple demander la génération d'un listing intermédiaire après précompilation.

En C/C++ et bien entendu sous MASM, les possibilités de la précompilation vont aller bien au-delà de ce simple remplacement mot pour mot. D'abord pour traiter des macros multilignes et ensuite pour pouvoir passer des paramètres à la macro. En C/C++, certaines syntaxes sont des macros et non des fonctions comme certains le pensent à tort.

Prenons le cas de max(x, y) . Cette macro renverra la plus grande des deux variables x et y, pour tous les types où cette comparaison a un sens. Ces variables peuvent être remplacées par des expressions : max(10, a + (2 * b)) . Pour en arriver à cette souplesse, max() est définie dans stdlib.h  :

#ifndef max
#define max(__a,__b)    (((__a) > (__b)) ? (__a) : (__b))
#endif

Même chose pour min() . Les parenthèses sont nécessaires pour éviter un piège classique des macros : il faut toujours bien voir que les arguments seront remplacés lettre pour lettre et qu'il est souhaitable que ces macros puissent accepter en argument des expressions complexes, comportant fonctions et autres macros.

Dans ces conditions, les macros deviendront réellement un outil puissant, qui dans le cas de l'assembleur transfigure le produit. Les utilisateurs ayant fait le choix de l'assembleur tiennent généralement à maîtriser intégralement le code objet. Les macros permettent d'améliorer le confort du langage en respectant ce souhait. Certaines directives se comportent comme des macros. Dans le cas de vos propres macros, des macros exogènes et des directives produisant du code, il est bon de vérifier le code généré, surtout en cas de problème.

Nous avons choisi de traiter les equates, et donc la directive TEXTEQU qui définit une macro texte, dans la rubrique des constantes.

 

10.10.1 Macro procédures simples

Une macro procédure est une méthode pour remplacer une série d'instructions par un simple nom. Le terme macro remplacera celui de macro procédure quand il n'y aura pas d'ambiguïté.

Pour créer une macro il est fait appel aux directives MACRO et ENDM selon la syntaxe :

NomMacro MACRO

(Série d'instructions)

ENDM

Dans le code source, le mot NomMacro sera remplacé par tout ce qui est entre MACRO et ENDM . La définition de la macro doit être vue par l'assembleur avant son utilisation. Pour cette raison et afin de ne pas polluer le code source, une très bonne idée consiste à placer les définitions dans un fichier include, mac1.inc dans les exemples du CD-Rom (dossier MACROS ). Il suffira ensuite de placer un include mac1.inc en début de fichier source.

Il est courant d’utiliser, d'une application à l'autre, un bouquet de macros plus ou moins personnelles. Il est alors intéressant de les regrouper dans un fichier MesMacros.inc , séparé d'un éventuel Appli.inc spécifique de l'application.

En l'absence de dispositif plus élaboré, il est courant d’employer des macros pour remplacer les trois ou quatre instructions constituant un appel à une fonction du DOS ou du BIOS. Prenons tout de suite un exemple des plus classiques :

bip MACRO
mov ah, 2 ;; Fonction Print Char
mov dl, 7 ;; (bell)
int 21h   ;  Appel au DOS
ENDM

Si maintenant nous saisissons bip dans notre programme, ça fera bip . Plus intéressant, nous avons pris soin de générer un fichier listing toutes options, dont voici un extrait :

0000               .CODE
                    ORG 100h
0100                debut:
                        bip
0100  B4 02      1  mov ah, 2
0102  B2 07      1  mov dl, 7
0104  CD 21      1  int 21h   ;  Appel au DOS

Le chiffre 1 indique un niveau d'imbrication macro de 1, c’est-à-dire le plus faible, une simple invocation de la macro.

Le double point-virgule ;;  définit un commentaire de macro. Il ne sera pas développé avec la macro, ce que nous montre le listing, qui nous montre également qu'un simple commentaire est développé. Il arrive que ce développement des commentaires pose problème. Donc, sauf raison particulière, utilisons les  ;; .

Cela semble très pratique, très lisible. attention toutefois, cette facilité cache un piège : ce simple mot bip cache trois instructions assembleur. Imaginons que, d'humeur mélomane, nous codions :

mov ax, var1
bip
cmp ax, 'A'
je UnA

Le piège est ici évident, mais le risque d'oublier que bip modifie des registres existe réellement. Il peut être confortable, si quelques microsecondes importent peu, de bétonner un peu la macro :

bip MACRO
pusha
mov ah, 2 ;; Fonction Print Char
mov dl, 7 ;; (bell)
int 21h   ;  Appel au DOS
popa
ENDM

Cette méthode est tentante pour appeler les fonctions du DOS et du BIOS. Prenons une sortie de caractère à l'écran. Sans macro, ça se code :

PetrusWriteChar = 2
...
mov ah, PetrusWriteChar 
mov dl, 'B' 
int 21h

Le problème d'une macro pour ce travail, c'est le contenu de DL, qui est le code ASCII du caractère à afficher. Nous ne pouvons quand même pas écrire une macro AffichA , une autre AffichB , et ainsi de suite. Nous pourrions tenter :

mov dx, 'B'
AfficheCar

Ce n'est pas très élégant, nous pouvons faire mieux. Écrire quelque chose comme une vraie procédure : AfficheCar('B') , par exemple.

10.10.2 Macro procédures avec argument(s)

Le problème semble à la portée du préprocesseur : s'il sait remplacer un nom de macro par son développement, il doit être capable de remplacer dans le corps de la macro un marqueur particulier par une valeur, texte à priori, le paramètre. C'est bien ce qui se produit. Nouvelle syntaxe :

NomMacro MACRO ListeParams

(Série d'instructions)

ENDM

ListeParams sera une série de symboles, séparés par des virgules s'ils sont plusieurs. Ces symboles vont apparaître dans le développement de la macro. La macro sera toujours invoquée par son nom, mais suivi par une série du même nombre de valeurs que de paramètres, séparées également par des virgules. Le préprocesseur remplacera tout d'abord les symboles par les valeurs dans le corps de la macro, puis placera la macro modifiée dans le code source.

Prenons un exemple : nous ne savons pas faire en une seule instruction assembleur mov var1, var2 . Soit la macro :

MMOV  MACRO param1, param2, param3
      mov         param2, param3
      mov         param1, param2
      ENDM

Elle sera appelée par mmov var1, eax, var2 . La même macro permettra mmov var1, al, var2 si var1 et var2 sont des octets. Aucune vérification n'est effectuée, et s'il y a erreur, c'est le code développé qui la signalera. Si nous trouvons plus élégant d'écrire mmov var1, var2 , nous pouvons modifier la macro ainsi :

mmov  MACRO param1, param2
      mov         ax, param3
      mov         param1, ax
      ENDM

Petit problème, tout est fait pour nous faire oublier que AX est modifié dans la macro. De plus, son utilisation est limitée à des mots de 16 bits. Pour l'instant, nous ne savons pas corriger ce dernier défaut. Nous pouvons au moins changer le nom. Une version utilisable pourrait être :

mmov16  MACRO param1, param2
        push ax
        mov         ax, param3
        mov         param1, ax
        pop ax
        ENDM

L'utilisation (évitons de parler d'appel pour les macros) se fera de la façon suivante :

var1  WORD 12
...
mmov16  var1, 12

Il ne faut jamais oublier, lors de l'utilisation de macros, qu'elles effectuent leur travail, une simple substitution, au moment de l'assemblage et même avant l'assemblage. Il est malheureusement difficile d'écrire des macros totalement transparentes : nous avons vu qu'elles pouvaient modifier des registres, il existe également des contraintes sur les arguments. Dans la macro MMOV16 , ces contraintes ne sont pas des pièges : le fait que param1 doive être une left value est évident dans la saisie de l'invocation de la macro. De plus, cette macro n'a de sens que si les deux paramètres sont des variables.

Si le nombre d'arguments passés est insuffisant, les manquants seront remplacés par des chaînes vides. Les paramètres sont affectés à partir du premier (à gauche), donc s'il y a un ou des manquants ce sera toujours le ou les derniers. Pas de vérification au niveau de la macro, mais l'erreur probable se produira dans le code généré. Il faut, de plus, bien garder à l'esprit qu'il ne se fait que du remplacement texte pour texte, pour la macro et pour les paramètres.

Cette absence de contrôle d'erreur est gênante. C'est la raison d'être de l'attribut REQ . Il indique simplement à l'assembleur de générer une erreur si le paramètre est manquant :

MMOV MACRO parm1, param2:REQ

Il est inutile d'utiliser l'attribut plusieurs fois : pour que param1 soit manquant, il faut que param2 le soit aussi, donc un seul REQ suffit.

Il est également possible de définir une valeur par défaut à utiliser, au lieu de la chaîne vide, quand un paramètre est manquant. Pour cela, il faut spécifier cette valeur entre  <> . Récrivons le MMOV  :

MMOV  MACRO param1:REQ, param2:REQ, tampon:<ax>
      mov         tampon, param2
      mov         param1, tampon
      ENDM

Si nous invoquons la macro avec seulement deux paramètres, la valeur par défaut est utilisée par le préprocesseur :

                     mmov var1, var2
010E  A1 0134 R  1   mov         ax, var2
0111  A3 0132 R  1   mov         var1, ax

Il existe pour traiter les paramètres manquants les directives IFB , IFNB , .ERRB et .ERRNB , déjà vues avec l'assemblage conditionnel.

test MACRO param
     IFB<param>
       mov al, 4
     ENDIF
     IFNB<param>
       mov al,param
     ENDIF
     ENDM

Cette façon de faire revient à donner à param la valeur 4 par défaut. Les possibilités sont bien sûr plus étendues que ce simple exemple.

10.10.3 Symboles déclarés locaux

Dans une macro procédure, nous pouvons utiliser toutes les possibilités du langage, en particulier créer des identificateurs ou symboles, d'étiquettes ou de variables par exemple. Un inconvénient est qu'ils sont cachés dans la macro, mais quand la macro est développée, ils font partie du code du module. Donc, le nom des identificateurs peut très bien être en conflit avec un identificateur de même nom. Pour éviter cet inconvénient, il est possible d'utiliser la directive LOCAL pour en limiter la visibilité au corps de la macro :

AFFBOOL MACRO val
      LOCAL vrai, faux, suite, fin
      jmp suite
vrai db "VRAI",0Dh, 0Ah,"$"
faux db "FAUX",0Dh, 0Ah,"$"
suite:
      mov dx, offset faux
      cmp val, 0
      je  fin
      mov dx, offset vrai
fin:
      mov ah, 09h
      int 21h
      ENDM

Cette macro est inadaptée à un usage réel, à chaque utilisation elle réserve deux chaînes de caractères. De plus, elle ne fonctionne que parce que nous sommes dans le cadre d'un modèle TINY, fichier généré  .com .

f possible d'utiliser, dans le code ou dans d'autres macros, des identificateurs aux noms aussi courant que vrai, faux, suite et fin. Mais surtout, il est possible de recourir à la macro plus d'une fois, ce qui est la moindre des choses. Si les identificateurs n'étaient pas déclarés locaux, la seconde utilisation de la macro dans un même module entrerait en conflit avec la première. Au lieu de cela, l'assembleur génère à chaque développement de la macro des identificateurs uniques à la place de ceux déclarés locaux. Le préprocesseur prépare le travail de l'assembleur :

010E  B8 0000                           mov ax, 0
                                      AFFBOOL ax
0111  EB 0E                1            jmp ??0002
0113 56 52 41 49 0D 0A 24  1      ??0000 db "VRAI",0Dh, 0Ah,"$"
011A 46 41 55 58 0D 0A 24  1      ??0001 db "FAUX",0Dh, 0Ah,"$"
0121                       1      ??0002:
0121  BA 011A R            1            mov dx, offset ??0001
0124  83 F8 00             1            cmp ax, 0
0127  74 03                1            je  ??0003
0129  BA 0113 R            1            mov dx, offset ??0000
012C                       1      ??0003:
012C  B4 09                1            mov ah, 09h
012E  CD 21                1            int 21h

L’emploi des symboles locaux permet d'éviter facilement l'utilisation des labels anonymes (@@:, @F et @B), qui est possible mais tout à fait déconseillée : s'ils sont utilisés, vous finirez toujours par placer la macro entre un @@: et un @F ou un @B du code principal.

Il faut retenir qu'il ne s'agit pas de variable locales, mais d'une simple protection contre la déclaration multiple d'un identifiant. Pour nous en convaincre, il suffit de reprendre notre exemple, idiot mais utile, et de mettre en commentaire la ligne LOCAL vrai, faux, suite, fin , tout se passera bien. Si ensuite nous ajoutons une seconde utilisation de la macro, par AFFBOOL ax , nous obtenons une volée d'erreurs à l'assemblage :

mac1.asm(18) : error A2005: symbol redefinition : vrai
 AFFBOOL(3): Macro Called From
  mac1.asm(18): Main Line Code

 

10.10.4 Traitement des arguments

Les paramètres d'une macro peuvent faire penser à ceux passer à une procédure. Les différences sont pourtant importantes.

Un paramètre de procédure est typé et surtout possède une adresse en mémoire, dans la pile. C'est donc une valeur qui sera initialisée par le programme appelant et pourra évoluer pendant le déroulement de la procédure.

Un paramètre de macro n'est que l'indication d'endroits où sera inséré un texte. Un paramètre de macro doit donc être résolu par l'assembleur, être une constante texte au moment de l'assemblage. De plus, cette constante texte devra respecter les contraintes que vous respectez quand vous codez : dans la macro AFFBOOL , le paramètre val est utilisé dans cmp val, 0 , c’est-à-dire qu'il doit au moins respecter la contrainte d'être une left value. var1 sera accepté, mais une valeur immédiate sera refusée. Par contre, mmov var1, (@Interface + 2)* 12 passera très bien, puisqu’une valeur immédiate est possible en second paramètre de MMOV et @Interface est une valeur immédiate, ici 0.

Les paramètres sont donc, au sens large, un type particulier de macro texte. Ils peuvent donc être des expressions comportant des macros textes, prédéfinies ou non, ainsi que des macros fonctions, que nous verrons un peu plus loin, mais dont nous pouvons imaginer qu'il s'agit de macro procédures renvoyant quelque chose. Se pose alors la question de l'ordre dans lequel le préprocesseur va développer ces éléments :

Il développe les paramètres de macros procédures en les remplaçant par le texte de l'argument.

Il développe les macros textes contenues dans l'expression des paramètres.

Il fait de même pour les macros fonctions contenues dans le texte résultant.

Il est clair que certains cas de figure se mordent la queue et ne pourront être résolus.

MASM offre quatre opérateurs spécifiques aux macros, utiles dans l'écriture des macros textes ou des arguments des macro procédures au moment de leur utilisation.

 

Les quatre opérateurs de macros

Opérateur

Nom

Action

<>

Délimiteur de texte

Encadre une chaîne de texte.

!

Opérateur de caractère littéral

Force à considérer le caractère suivant comme un caractère, même s'il peut avoir un autre sens.

%

Opérateur d'expansion

Force l'expansion de macros.

&

Opérateur de substitution

Permet d'identifier un paramètre ou une macro texte en cas d'ambiguïté.

 

< Texte est le délimiteur de texte. Il permet de définir les macros textes à l'aide de TEXTEQU . Il permet également d’invoquer une macro avec un texte en argument qui pourrait être interprété différemment :

TUTU  MACRO param1, param2
      LOCAL suite, message, txt1, txt2
      jmp suite
txt1  db "&param1",0Dh, 0Ah,"$" 
IFNB<param2>
  txt2  db "&param2",0Dh, 0Ah,"$"      
ENDIF
suite:
      mov dx, offset txt1
      mov ah, 09h
      int 21h         
      IFNB<param2>
        mov dx, offset txt2
        mov ah, 09h
        int 21h      
      ENDIF
      ENDM

Testons cette macro avec les deux lignes suivantes :

  TUTU  adieu veau, vache, cochon  
  TUTU <adieu veau, vache, cochon>

Le résultat est :

adieu veau
vache
adieu veau, vache, cochon

La première forme se développe en trois arguments, les crochets forcent dans la seconde à un seul.

 

!  est l'opérateur de caractère littéral. Il fait perdre au caractère qu'il précède son rôle symbolique, s’il en a un, pour le transformer en simple caractère :

TUTUTEST TEXTEQU <Si A !>= 100>
...
TUTU  TUTUTEST
TUTU %TUTUTEST

Le sortie est :

TUTUTEST
Si A >= 100

Sans le  ! dans le TEXTEQU , l'assembleur signale une erreur et stoppe l'assemblage. En effet, il termine le texte après le premier  >  et se retrouve avec  = 100> dont il ne sait que faire.

 

%  est l’opérateur d’expansion. Nous venons d'en voir un effet, qui est de forcer au développement de la macro texte TUTUTEST . Dans le même ordre d'idée, cet opérateur force le calcul d'expressions numériques. Nous allons faire une série de tests, à partir d'une macro d'exemple.

TOTO MACRO param
     LOCAL suite, s1, s2, message, egalite     
     jmp suite
message  db "&param",0Dh, 0Ah,"$"
egalite  db "c'est egal!",0Dh, 0Ah,"$"
suite:
     mov ax, 5        
     cmp ax, param
     jne s1
     mov dx, offset egalite
     mov ah, 09h
     int 21h
     jmp s2
s1:     
     mov dx, offset message
     mov ah, 09h
     int 21h            
s2:
     ENDM

Si param est évalué à 7, cette macro affichera c'est egal! , sinon, elle affichera cet argument sous forme de texte. Voici une première liste de tests avec le résultat en commentaire :

trois    EQU 3
trois_ch TEXTEQU <3>
  
  mov  cx, 7       
  TOTO cx           ;c'est egal!
  inc  cx
  TOTO cx           ;cx 
  TOTO 7            ;c'est egal!
  TOTO 3 + 4        ;c'est egal!
  TOTO trois + 4    ;c'est egal!
  TOTO trois_ch + 4 ;c'est egal!

La macro accepte effectivement :

  Soit un texte qui sera interprété par l'assembleur, en passe 2, comme un nom de registre, et pourra assembler cmp ax, cx .

  Soit un texte qui pourra être développé, puis calculé jusqu'à une valeur numérique.

Et ceci sans précaution particulière. Ce n'est pas le cas de TEXTEQU , comme nous allons le voir maintenant. La macro TOTO ne sert ici qu'à afficher le contenu des macros textes élaborées :

T1 TEXTEQU <5>
  TOTO %T1          ;5
T2 TEXTEQU <3 + 2>
  TOTO %T2          ;3 + 2
T3 TEXTEQU <trois + 2>
  TOTO %T3          ;trois + 2
T4 TEXTEQU <trois_ch + 2>
  TOTO %T4          ;3 + 2
 
S1 TEXTEQU %5
  TOTO %S1          ;5
S2 TEXTEQU %3 + 2
  TOTO %S2          ;5
S3 TEXTEQU %trois + 2
  TOTO %S3          ;5
S4 TEXTEQU %trois_ch + 2
  TOTO %S4          ;5

Dans les quatre premiers exemples, rien n'est changé entre les  <> , sauf pour T4, où une macro texte à l'intérieur d'une autre est développée. Étonnant au premier abord, en comparaison avec T3 où l'equate numérique n'est pas traduit, mais logique en fin de compte.

Les quatre derniers exemples démontrent le rôle de %  : forcer à l'évaluation, puis transformer le résultat en texte. C'est ce qui explique que 5 sans  <> n'est pas accepté, mais que %5 l'est.

L'opérateur  % rend le comportement de TEXTEQU semblable à celui du traitement des arguments de macros.

Cet opérateur a un rôle particulier quand il est placé en première position sur la ligne de code : il indique à l'assembleur qu'il doit développer toutes les macros de cette ligne. Nous en avions vu un exemple, à la rubrique des listings :

%ECHO Programme &@FileName

 

&  est l’opérateur de substitution. Il identifie de façon certaine un élément en cas d’ambiguïté. Voici un exemple d’utilisation des deux opérateurs  % et  & , le but étant d’écrire à l’écran le nom de notre programme de test :

% nomfichier  db  "&@FileName",0Dh, 0Ah,"$"
…
mov ah, 09h
mov dx, offset nomfichier
int 21h

Tel quel, MAC1 s’affiche. En enlevant  % , c’est &@FileName . En enlevant  & ou les deux, c’est @FileName . Ce n’est pas nécessairement entièrement clair, du moins pour le rôle de  & . Mais nous ne connaissons peut-être pas tout de la structure interne de @FileName . Disons que le  & force au développement de @FileName , alors qu'il est entre guillemets. Et le  % force alors au développement de &@FileName . Enfin, ça fonctionne…

D'une façon plus générale, l'opérateur  & force l'assembleur à considérer comme un nom de paramètre les lettres situées à sa suite, jusqu'au  & ou au séparateur suivant. Un séparateur peut être un espace, un caractère de tabulation, des guillemets, virgules, etc. Il existe plusieurs façons d'obtenir le même effet à l'aide de cet opérateur, par exemple en le plaçant après le nom du paramètre. Quelques exemples, sachant que les substitutions sont faites de la gauche vers la droite :

si param vaut var1 , param&7 est transformé en var17 .

Si param1 vaut var1 et param2 vaut 7  :

  &param1&&param2& est transformé en var17 .

  &param1&param2 est transformé en var1param2 .

  param1&&param2 est transformé en var17 .

10.10.5 Les boucles REPEAT, WHILE et FOR dans les macros

Il est possible de définir des boucles au sein de macros. Cette technique ne doit pas être confondue avec les directives ressemblantes vues à la rubrique Sauts et boucles , qui elles génèrent un code particulier qui sera ensuite exécuté.

Les directives de boucle traitées ici sont REPEAT , WHILE , FOR et FORC . Les noms qu'elles portaient dans d'anciennes versions de MASM sont toujours reconnus. Ce sont : REPT pour REPEAT , IRP pour FOR et IRPC pour FORC .

Ce sont des macros particulières, fournies par MASM, qui permettent de générer plusieurs fois le même code durant la passe 1. Étant des macros, et si elles ne sont pas utilisées au sein d'une macro utilisateur, cas rare à priori, elles devraient être terminées par ENDM .

De plus, le nombre de boucles étant déterminé durant la passe 1, il faut être très prudent dans l'utilisation des offsets de variables ou autres valeurs susceptibles d'être ensuite modifiées par l'assembleur :

REPEAT (OFFSET var2 - OFFSET var2)

est à éviter.

Les boucles REPEAT

REPEAT reproduit la même séquence un nombre défini de fois. Sa syntaxe est :

REPEAT  Expression

Instructions

ENDM

Expression est une expression évaluable en un entier. Un exemple qui doit être placé directement dans la zone des données :

alphabet LABEL BYTE
 
letter = 'A'
REPEAT 26
BYTE letter 
letter = letter + 1
ENDM

Ainsi sera créée la chaîne alphabet  = "ABCD...XYZ" .

 

Les boules WHILE

WHILE est semblable à REPEAT , mais la boucle sera effectuée selon le résultat d'un test :

WHILE  TestExpression

Instructions

ENDM

TestExpression sera évaluée, rappelons-le à l'assemblage, et la génération des Instructions cessera dès qu'elle sera fausse, c’est-à-dire évaluée à 0. Nous pouvons l'utiliser pour récrire l'exemple précédent :

alphabet2 LABEL BYTE
 
lettre = 'A'
WHILE (lettre LE 'Z')
BYTE lettre 
lettre = lettre + 1
ENDM

 

La boucle FOR

À partir de la directive FOR , nous pouvons parcourir, à l'assemblage, une boucle en appliquant à chaque itération une valeur d'un paramètre extraite d'une liste. La syntaxe est :

FOR  Paramètre , < ListeArgument >

Instructions

ENDM

ListeArgument est une liste d'arguments, entre  <> et séparés par des virgules. Instructions sera effectué autant de fois que ListeArgument contient d'éléments, en affectant chacune de ces valeurs à Paramètre .

L'exemple est encore inspiré du précédent. Il crée le sous-alphabet des signes (code ASCII) représentant les 10 chiffres de la numération décimale :

alphachiffres LABEL BYTE
 
FOR param, <0,1,2,3,4,5,6,7,8,9>
lettre = '0' + param
BYTE lettre
ENDM

 

La boucle FORC

La directive FORC est semblable à la directive FOR , mais la liste d'arguments est remplacée par un texte dont le préprocesseur va extraire les caractères un par un pour obtenir les arguments. Sa syntaxe est :

FORC  Paramètre , < Texte >

Instructions

ENDM

Voici l'exemple précédent, écrit en utilisant FORC :

alphachiffres2 LABEL BYTE
 
FORC param, <0123456789>
lettre = '0' + param
BYTE lettre
ENDM

 

Les directives EXITM, GOTO et les labels de macro

Utilisée sans argument, la directive EXITM  termine, non pas l'exécution, mais l'expansion, le développement d'un macro. Il est important, lors d'un travail sur les macros, de se remémorer de temps en temps cette vérité : une macro se développe à l'assemblage, elle ne s'exécute pas. La nuance est importante avec la directive GOTO , qui transfère l'expansion vers une étiquette, Ce macrolabel doit être défini en étant précédé de : .

alphachiffres3 LABEL BYTE
 
FORC param, <0123456789ABCDEF>
lettre = '0' + param
IF(lettre EQ '5')
  GOTO macrolabel
ENDIF
BYTE lettre
:macrolabel
IF(lettre EQ '9')
  EXITM
ENDIF
ENDM

Cetta macro crée le même sous‑alphabet que précédemment (chiffres de 0 à 9) mais privé cette fois‑ci du chiffre 5.

10.10.6 Macros fonctions

Par la directive EXITM<> , une macro procédure devient une macro fonction. L'argument de la macro EXITM est une texte. Par ce texte, la macro devient l'équivalent d'une macro texte, mais variable.

 

10.11 Sauts et boucles

Nous ne traiterons ici que de ce qui est spécifique à MASM, en reportant l'étude des sous-programmes ou procédures (instructions CALL , RET , voire INT et RTI ) à d'autres chapitres. Les instructions assembleur permettant de gérer des ruptures de flux de programme et des boucles sont vues au chapitre sur le jeu d'instructions et sont supposées connues. Si ce n'est pas le cas, il suffit de procéder à une lecture en parallèle.

Ces instructions sont le saut inconditionnel JMP , la famille des sauts conditionnels Jcc , à la partie Flux du programme , ainsi que les LOOP et LOOPcc situés à la partie Chaînes et tableaux .

Si vous souhaitez expérimenter en lisant ce chapitre, ce qui une fois de plus est une bonne idée, vous trouverez du code 16 et 32 bits sur le CD-Rom, dossier jumps . À vous de modifier ces projets pour tester tel ou tel point. Pour constater le résultat, le mieux sera souvent de consulter simplement le fichier listing.

10.11.1 Labels anonymes

Les sauts et boucles font un abondant usage de labels (étiquettes pour les puristes). À ce sujet, une facilité vous est offerte, les labels anonymes . Un listing assembleur est vite truffé de sauts très locaux, par exemple pour simuler les if...else . Il est vrai que les directives de décisions et de boucles, que nous allons voir un peu plus loin dans ce chapitre, en diminuent considérablement le nombre. Dans un espace de visibilité donné, les labels doivent être uniques, bien entendu, et nous sommes vite lassés des suite1, suite15, boucle7, etc. Pour répondre à ce problème, un label particulier, @@, peut être dupliqué à souhait en tant que déclaration de label, sous la forme habituelle @@:. Nous disposons parallèlement des deux pseudo-labels, @F et @B, utilisables uniquement comme opérandes adresse. @F (forward) pointe vers le plus proche @@ suivant, @B (back) vers le plus proche @@ précédent. Normalement, la prudence voudrait que leur usage soit réservé à des situations telles que les @F ou @B et le @@: correspondant apparaissent au même coup d'œil.

Labels anonymes
figure 10.18 Labels anonymes [the .swf]

10.11.2 Codage des instructions de saut

Les instructions de saut, c’est-à-dire les JMP , Jcc et éventuellement LOOPcc , existent en plusieurs déclinaisons, selon la distance maximale à laquelle la cible peut se trouver par rapport à l'instruction de saut. Ce point pose quelques petits problèmes de saisie et d'interprétation sous MASM. C'est ce que nous allons voir maintenant.

La distance des sauts

Rappelons la façon dont est exprimée l'adresse d'arrivée, ou cible, dans ces instructions de saut, donc la notion de distance :

  Un adressage court, exprimé par la distance sur 8 bits signés (-128 à +127) par rapport à l'instruction suivant l'instruction de saut. C'est le saut SHORT .

  Un adressage proche, sans changement de segment, exprimé sur 16 bits ou 32 bits selon l'attribut de taille d'opérande. Ce saut peut être relatif, comme le précédent, ou absolu, par la donnée de l'offset de la cible. Dans ce dernier cas, il faut encore considérer l'absolu immédiat, avec donnée directe de l'offset, et l'absolu indirect, l'offset étant dans une mémoire ou un registre. C'est le saut NEAR , qui peut se décliner en NEAR16 et NEAR32 .

  Un adressage lointain, quand le segment change au cours du saut. Toujours absolu immédiat ou absolu indirect. C'est le saut FAR , donc également FAR16 et FAR32 .

Nous utiliserons les mots anglais, qui présentent l'avantage de correspondre aux directives de MASM. Ces modes ne sont pas tous utilisables avec toutes les instructions de saut :

  Le JMP est déclinable en SHORT ,  NEAR16  et NEAR32  (en relatif et absolu indirect), FAR16  et FAR32  (en absolu immédiat et absolu indirect).

  Les Jcc n'ont au départ existé qu'en SHORT . À partir du 386, en SHORT , NEAR16 et NEAR32 , tous relatifs.

  Les LOOP et LOOPcc , uniquement en SHORT .

En général, vous utiliserez des labels pour coder les sauts et MASM prendra en charge les choix de distance et de type d'opérande. Mais il reste un ou deux points un peu ambigus.

Les sauts directs

Dans les micro-projets de tests, pour éloigner une partie du code d'une autre, nous pouvons utiliser simplement une ligne comme :

bidon byte 2048 dup (?)    

Donc, MASM optimise les sauts en choisissant le plus court. Dans la mesure où il a le choix, c’est-à-dire pour le JMP et pour les Jcc si vous déclarez un processeur égal ou postérieur au 386. Si nous forçons une distance égale ou supérieure à cette distance minimale, il s'y conformera. Si cette distance est trop petite, une erreur sera générée. Un seul exemple suffira.

Soit un JMP donc la cible est à moins de 127 octets de distance, donc à la portée d'un SHORT . Voici ce que donnent diverses syntaxes, relevées dans le fichier listing :

002A  EB F3              jmp Avorte
002A  E9 FFF2            jmp NEAR PTR Avorte
002A  EA ---- 001F R     jmp FAR PTR Avorte
002A  EB F3              jmp @code:Avorte
002A  EB F3              jmp @code:OFFSET Avorte

Attention, il s'agit bien de plusieurs expériences et non d'un seul extrait d'un fichier listing. La première ligne génère du SHORT , la deuxième du NEAR16 et la troisième du FAR16 . Si ce n'est déjà vu, les tirets  ---- et la lettre  R indiquent une valeur relogeable, donc que l'adresse du segment de code sera résolue par le lieur.

Les deux dernières lignes montrent que même en codant CS:Adresse , MASM optimise et génère du SHORT .

Dernière série d'expériences : nous éloignons la cible, hors de portée de SHORT , mais à portée de NEAR  :

jmp SHORT PTR Avorte

génère bien une erreur. Pour le reste :

082D  E9 F7F2            jmp Avorte
082D  E9 F7F2            jmp NEAR PTR Avorte
082D  EA ---- 0022 R     jmp FAR PTR Avorte
082D  E9 F7F2            jmp @code:OFFSET Avorte

Les commentaires sont les mêmes que précédemment. Remarquez que les lignes 1, 2 et 4 représentent un adressage relatif, la 3 un adressage absolu immédiat. C'est MASM qui s'est chargé des calculs.

Les sauts indirects

Nous avons vu les adressages relatif et absolu immédiat. Il nous reste l'adressage absolu indirect ou plus simplement adressage indirect. C'est une méthode très utilisée par Windows pour adresser des fonctions au travers de tables. Si VAR1 est un DWORD :

mov VAR1, OFFSET suite
jmp [VAR1]

Ce code fonctionne. À partir du moment où la taille de la donnée pointée n'est pas précisée, MASM interprète en fonction du modèle de mémoire, ici FLAT. En fait, les lignes suivantes fonctionnent également :

jmp VAR1
jmp DWORD PTR [VAR1]

Il est normal que les trois syntaxes fonctionnent, puisque MASM génère le même code objet :

0000084F  FF 25 0000005D R     jmp DWORD PTR [VAR1]

0000005D est l'offset de VAR1 dans son segment (au sens de MASM). Le  R de relogeable indique que cette adresse sera en final résolue par le lieur.

Les sauts conditionnels SHORT

La différence entre les sauts NEAR et FAR est fondamentale, en général le programmeur sait s'il change ou non de segment. Il en va tout autrement des sauts SHORT , auxquels sont limitées les instructions Jcc avant le 386, donc en modèle SMALL, et LOOP / LOOPcc .

Pour les LOOP , ce n'est pas trop gênant, cette instruction étant à priori nettement orientée petites boucles, voire chaînes de caractères ou tableaux. Pour les Jcc , c'est réellement plus embêtant. 128 octets sont vite atteints, et cela risque de survenir lors d'une correction du code.

MASM a donc décidé de s'en occuper. À l'assemblage, il calcule la distance du saut pour coder le saut relatif, ce qui est normal. Si la valeur trouvée est supérieure à 127 ou inférieure à -128, de lui-même comme un grand, il simule un Jcc NEAR , de la façon suivante :

Soit à coder :

je la_bas

Si la_bas est trop loin, alors il code :

jne @F
jmp la_bas
@@:

Vérifions :

         mov cx, 12
@2:
         dec cx
         jmp @F
bidon    byte   2048 dup (?)
@@:
         cmp cx, 0
         jnz @2

Regardons dans le fichier listing :

083E  74 03 E9 F7F4           jnz @2

Bizarrement, malgré le commutateur de ligne de commandes  /Sg qui fait lister le code généré par l'assembleur, nous n'obtenons que le code hexadécimal. Il est néanmoins facile à interpréter : 74 03 est un JZ de 03 octets de distance, c’est-à-dire juste après le JMP qui suit. Donc, E9 est un JMP relatif. F7F4 , ça fait -2060. En comptant sur le listing, nous trouvons effectivement 12 octets de code entre l'adresse qui suit le JMP et @2, ce qui, ajouté aux 2048 réservés, donne bien 2060.

Comme il fallait s'y attendre, les règles concernant le forçage de la distance des sauts JMP s'appliquent à Jcc  :

         mov cx, 12
@3:
         dec cx
         cmp cx, 0
         jnz @3
 
         mov cx, 12
@4:
         dec cx
         cmp cx, 0
         jnz NEAR PTR @4

Le code généré est :

084A  75 FA              jnz @3
...
0853  74 03 E9 FFF7      jnz NEAR PTR @4

Les instructions JCXZ et JECXZ (saut si CX ou ECX est nul) ne possèdent pas d'instruction complémentaire, c’est-à-dire que ni JCXNZ ni JECXNZ n'existent. Ces instructions ne sont donc pas transformées en version NEAR ou FAR , pas plus que ne le sont les LOOP et LOOPcc .

Si vous positionnez le niveau de warning à 3 par le commutateur ligne de commandes  /W3 , vous obtenez les messages :

Assembling: tests.asm
tests.asm(65) : warning A6003: conditional jump lengthened : by 1929 byte(s) 
tests.asm(77) : warning A6003: conditional jump lengthened : by -122 byte(s)

Si vous souhaitez traiter le problème vous-même, il suffit de modifier le comportement de MASM à l’aide de la directive OPTION NOLJMP . Si la cible du saut est hors de portée, MASM générera une erreur et le programmeur pourra réagir en conséquence :

Assembling: tests.asm
tests.asm(67) : error A2075: jump destination too far : by 1929 byte(s)

OPTION LJMP revient au comportement par défaut.

10.11.3 Directives de contrôle du flux de programme

Nous allons étudier le comportement de deux groupes de directives, qui influent toutes sur le déroulement du programme. Ce sont :

  Les directives de choix : .IF , .ELSE , .ELSEIF , .ENDIF .

  Les directives de boucles : .WHILE , .ENDW , .REPEAT , .UNTIL , .UNTILCXZ , .BREAK , .CONTINUE .

Ces directives génèrent du code objet, il ne faut en aucun cas les confondre avec les directives d’assemblage conditionnel.

Avec INVOKE , elles ont un effet considérable sur la facilité d’écriture et la lisibilité du code source. Leur influence dépasse l’idée de confort pour jouer pratiquement au niveau de la faisabilité.

La base de la syntaxe est :

.IF condition
   ;Code effectué si condition est vraie
.ENDIF

Par exemple, extrait du fichier listing :

                     .IF (ax == 112)
 085F  83 F8 70      *  cmp    ax, 070h
 0862  75 03         *  jne    @C0003
 0864  B9 0001          mov cx, 1
                     .ENDIF

Les astérisques  * marquent les lignes générées par l’assembleur. Ici, condition est (ax == 112) . Les parenthèses ne sont pas nécessaires. MASM peut aller assez loin dans la complexité de la condition. Nous allons donc étudier ce point, avant de revenir sur les structures de choix et de boucle.

Condition de choix et de boucle

Il s’agit d’une expression parfois complexe qui peut s’évaluer à l’exécution à VRAI ou FAUX. Cette expression peut comporter des registres et des variables. Les opérations arithmétiques sur ces registres ou variables ne sont pas possibles. Par exemple :

((ax > 0) && (var1 == bx))

La syntaxe est très proche de celle du langage C. Ainsi, 0 est évalué comme FAUX, toute valeur non nulle comme VRAI. Il n’est donc pas nécessaire d’utiliser un opérateur de comparaison : la condition (var1) sera à FAUX si var1 est nulle, à VRAI dans tous les autres cas. Donc, puisque  ! est l’opérateur de négation logique (nous le verrons plus loin), la condition  !variable est équivalente à (variable == 0) . En d'autres termes, les deux lignes suivantes sont équivalentes :

.IF(ax==0)
.IF !ax

Les mots clés TRUE et FALSE ne sont pas définis dans MASM. Ils le sont dans les fichiers d'en-tête Windows, respectivement à 1 et 0. C'est le 1 de TRUE qui peut poser problème, cette valeur étant définie à -1 en d'autres circonstances. De plus, en C ou sous Windows, ces mots désignent des constantes numériques, ici ce sont des entités logiques.

Même si vous voulez absolument utiliser TRUE et FALSE , il n'est pas bon d'écrire :

.IF(ax==TRUE)

À moins que vous ayez vous-même défini ces valeurs, de la façon suivante :

FALSE EQU 0
TRUE  EQU !FALSE

Avec la certitude d'entrer un jour en conflit avec une autre déclaration dans un fichier d'en-tête. Le mieux est de s'abstenir et de remplacer FALSE par 0 et TRUE par !0 (ou 1) pour écrire des conditions toujours fausses ou toujours vraies. Ou alors profitons pour une fois de notre francophonie, pour écrire :

FAUX  TEXTEQU 0
VRAI  TEXTEQU !FAUX ;ou !0

Et hop...

Remarque

Le signe  = , le mot égale

Le langage courant fait une regrettable confusion entre l’affectation et l’expression de l’égalité. Dire A égale 12 sans rien de plus signifie soit qu’il a été constaté que la valeur de A est 12 (égalité), soit, dans l’idée d’une ligne de code, que la valeur 12 est affectée à A (affectation). Ce sont deux concepts totalement différents. Dire Si A égale 12 procède d’une comparaison entre A et 12. Dans cette dernière acception, (A égale 12) est une expression numérique, qui peut prendre la valeur VRAI ou la valeur FAUX. Il est donc important de bien distinguer l’opérateur d’affectation et l’opérateur de comparaison.

*           En C, l’opérateur d’affectation est le  = , l’opérateur de comparaison  == .

*           En Pascal, l’opérateur d’affectation est le  := , l’opérateur de comparaison  = .

En C, écrire c = (a==12) est légal. c prendra la valeur 0 si a est différent de 12, 1 si a vaut 12.

Les opérateurs utilisables pour élaborer notre condition sont d'abord les opérateurs de comparaison, entre valeurs numériques :

  ==  égal à.

  !=  différent de.

  >   supérieur à.

  >=   supérieur ou égal à.

  <  inférieur à.

  <=  inférieur ou égal à.

  &   test de bits.

 

L'usage des six premiers opérateurs est facile à deviner et deviendra évident après quelques exemples.

&  correspond à l'instruction machine TEST . C'est un ET bit à bit, comme dans l'instruction machine AND , mais à blanc, sans modifier les opérandes, simplement pour positionner les flags SF, ZF et PF. C'est ZF qui est le plus intéressant.

Ces opérateurs comparent à peu près tout, variables, registres, valeurs immédiates, en suivant les règles des instructions sous-jacentes, à savoir CMP et TEST. Il faut respecter l'ordre des deux opérandes et les comparaisons variable/variable ne sont pas possibles.

Soit .IF(var & mask) . Si un seul bit de mask est à 1, la condition teste ce bit dans var . (var & 00001000b) est validé si le bit 3 de var est à 1. Si plusieurs bits de mask sont à un, la condition est vraie, si un au moins des bits correspondants de var est à 1.

Il est possible d'utiliser ZERO? , CARRY? , OVERFLOW? , SIGN? , PARITY?  qui transforment les flags ZF, CF, OF, SF et PF directement en opérandes de condition. Par exemple .IF(SIGN?) est équivalent à .IF(SF == 1) , expression correcte bien sûr. Attention, s'ils ne sont pas seuls dans la condition, la valeur du flag est celle du moment où l'expression est calculée. Voici un exemple en anticipant un peu sur la suite du chapitre :

0890  83 FB 11      cmp bx, 17
                     .IF(ax == 12) && (ZERO?)
 0893  83 F8 0C   *    cmp    ax, 00Ch
 0896  75 05      *    jne    @C000E
 0898  75 03      *    jne    @C000E
 089A  B9 0001                   mov cx, 1
                     .ENDIF
 089D             *@C000E:

Nous voyons que le ZERO? ne concerne par cmp bx, 17 , mais le test cmp ax, 00Ch .

Au chapitre sur le jeu d'instructions, il est expliqué qu'il existe deux familles d'instructions Jcc de tests d'inégalité : l'une donne des résultats corrects en non signé (JB, JA...), l'autre en signé (JL, JG...). En l'absence de tout renseignement, MASM générera par défaut des comparaisons non signées. Dès qu'un des membres de la comparaison est identifié comme signé, la comparaison se fait en signé. Cette identification peut se faire à partir de la déclaration (SBYTE, SWORD, etc.) ou directement à l'aide de PTR : .IF(SBYTE PTR [di] >= al) . En voici un exemple :

0058 FFF4          var1    sword   -12
005A 000E          var2    word    14
...
                        .IF(var1 >= 12)
089D  83 3E 0058 R 0C *    cmp    var1, 00Ch
08A2  7C 03           *    jl     @C0011
08A4  B9 0001              mov cx, 1
                        .ENDIF
08A7              *@C0011:
 
                        .IF(var2 >= 12)
08A7  83 3E 005A R 0C *    cmp    var2, 00Ch
08AC  72 03           *    jb     @C0013
08AE  B9 0001              mov cx, 1
                        .ENDIF
08B1              *@C0013:
 
                        .IF(SBYTE PTR [di] >= al)
08B1  38 05           *    cmp    sbyte ptr [di], al
08B3  7C 03           *    jl     @C0015
08B5  B9 0001              mov cx, 1
                        .ENDIF
08B8              *@C0015:

À l'aide des opérateurs précédents, il est possible d'établir des conditions unitaires, comme (CARRY?) , (ax >= bx) , sans oublier la valeur sans opérateur (eax) dont la non-nullité est testée. Les trois opérateurs logiques suivants vont permettre de combiner ces briques élémentaires pour élaborer des conditions plus complexes :

  !  négation logique.

  &&  ET logique.

  ||  OU logique.

Ces opérateurs sont donnés dans l'ordre de priorité décroissante ( !  plus prioritaire que &&  plus prioritaire que  || ). Il est préférable de mettre des parenthèses.

Les expressions sont évaluées de gauche à droite, et en ne calculant pas les expressions inutiles. Ceci traduit le fait que l'analyse, par exemple, abandonne une chaîne de  && (ET) dès qu'un élément est faux, ou une chaîne de || (OU) dès qu'un élément est vrai.

Pourquoi cette dernière précision, qui semble évidente ? Certains compilateurs proposent une option Évaluation booléenne complète qui force à tout calculer même si le résultat est connu. Comme raison, nous pourrions penser que, dans les tests, figure la lecture d'un port d'entrée de périphérique et que la lecture de ce registre enlève automatiquement cette donnée du tampon d'entrée. La programmation en assembleur ayant parfois à traiter les E/S, ne pas en tenir compte est une cause possible de bug vicieux. Terminons par un exemple, encore une fois extrait du fichier listing :

                           .IF ((ax >= 112)\                 
                            && ((dx < 12)||(dx > var1))\
                            && (cx == 0))||(var2 == 14)
0865  83 F8 70        *    cmp    ax, 070h
0868  72 0F           *    jb     @C0007
086A  83 FA 0C        *    cmp    dx, 00Ch
086D  72 06           *    jb     @C0008
086F  3B 16 0058 R    *    cmp    dx, var1
0873  7E 04           *    jle    @C0007
0875                  * @C0008:
0875  0B C9           *    or  cx, cx
0877  74 07           *    je     @C0006
0879                  * @C0007:
0879  83 3E 005A R 0E *    cmp    var2, 00Eh
087E  75 03           *    jne    @C0005
0880                  * @C0006:
0880  B9 0001              mov cx, 1
.ENDIF
0883                  * @C0005:

Traduisons la condition.

Il faut que les trois conditions partielles suivantes soient simultanément vraies :

AX plus grand ou égal à 112.

DX plus petit que 12 ou plus grand que var1 .

CX nul ou var2 vaut 14.

Une condition interminable doit nous mettre la puce à l'oreille et nous amener à vérifier si des simplifications ne sont pas possibles. Il arrive que cette simplification soit majeure et qu'en fin de compte, la condition se traduise par VRAI (ou FAUX) !

D'un autre côté, il n'est pas toujours pertinent de simplifier les conditions. En effet, certaines normes industrielles imposent une redondance de précautions : quelle que soit son analyse, le programmeur doit conditionner certaines opérations, généralement les sorties de commandes vers des actionneurs, à des tests, d'entrées de capteurs typiquement. Les tests doivent être effectués immédiatement avant l'action. Ceci n'empêche pas d'autres redondances, encore plus impératives, directement sur les commandes physiques. En d'autres termes, le programmeur doit se considérer lui-même comme l'élément le moins fiable de la chaîne de traitement, le maillon faible.

Si vous travaillez sur des conditions complexes, vous pouvez structurer votre travail en créant des variables booléennes de la façon suivante :

.IF condition_compliquée1
mov varBool1, 1
.ELSE
mov varBool1, 0
.ENDIF

Si vous créez plusieurs variables de ce type, vous pouvez ensuite faire :

.IF Condition(varBool1, varBool2, etc.)
mov varBoolTresCompliquée, 1
.ELSE
mov varBoolTresCompliquée, 0
.ENDIF

Et ainsi de suite... L'utilisation de ces variables pourra être de la forme :

.IF varBoolxxx
;faire quelque chose
.ENDIF

Attention toutefois, les éléments de varBoolxxx ne sont pas recalculés au moment du test, cette solution n'est donc pas très adaptée aux boucles.

Maintenant que nous savons rédiger des conditions, il nous reste à les utiliser dans des structures à base de directives de choix, ou décisions, et de boucles.

Directives de décisions

Ce sont des blocs bâtis entre un .IF (condition) et un .ENDIF  :

.IF(condition)
    ; instructions effectuées si condition est vérifiée
.ENDIF 

Les parenthèses autour de condition ne sont pas nécessaires.

Le fonctionnement est évident : si condition est vérifiée, le code continue à la première instruction suivant la ligne du .IF(condition) et la suite dépend du code situé à cet endroit, .ENDIF étant ignoré, un peu comme un label. Si condition n'est pas vérifiée, le code est transféré à l'instruction suivant le .ENDIF .

Il est possible d’ajouter un ou plusieurs .ELSEIF (conditionN) entre le .IF(condition) et le .ENDIF  :

.IF(condition)
    ; instructions bloc
.ELSEIF(condition1)
    ; instructions bloc 1
.ELSEIF(condition2)
    ; instructions bloc 2
......
.ENDIF

Le fonctionnement est le suivant : condition est testée.

Si elle est vérifiée, le code continue à la première instruction suivant la ligne du .IF(condition) , et si le code arrive au .ELSEIF suivant, c’est-à-dire à la fin du bloc, le code est transféré directement à l'instruction suivant le .ENDIF . Si condition n'est pas vérifiée, condition1 est testée.

Si elle est vérifiée, le code continue à la première instruction suivant la ligne du .ELSEIF(condition1) , et si le code arrive au .ELSEIF suivant, c’est-à-dire à la fin du bloc, le code est transféré directement à l'instruction suivant le .ENDIF . Si condition1 n'est pas vérifiée, condition2 est testée.

Et ainsi de suite...

Aucune vérification de cohérence n'est faite pour détecter par exemple du code inatteignable, comme c'est souvent le cas dans les langages évolués. Vous pouvez très bien avoir deux fois la même condition. La première condition résolue à VRAI est traitée, les suivantes sont oubliées.

Donc, cette structure n'a pas, bien entendu, la finesse d'une instruction CASE de C/C++ ou Pascal, mais moyennant une certaine discipline nous pourrons arriver à un résultat comparable.

Nous n'avons pas évoqué le .ELSE . C'est un .ELSEIF particulier. Si le dernier .ELSEIF avant le .ENDIF est associé à la condition VRAI, c’est-à-dire .ELSEIF !0 par exemple, alors si aucune condition précédente n'a été vérifiée, le code sera dirigé vers le bloc associé à ce dernier .ELSEIF . Il s'agit donc du traitement par défaut. Nous venons de décrire le .ELSE . Cette directive traite les cas non traités ni par le .IF initial, ni par aucun .ELSEIF . Il ne peut y avoir de .ELSEIF entre le .ELSE et le .ENDIF . Il ne serait, de toute façon, jamais atteint.

Qu'il comporte ou non un ou des .ELSEIF , un .ELSE ou pas, un bloc de décision commence toujours par un .IF et se termine toujours par un .ENDIF . Voyons un exemple classique de bloc de ce type :

.IF (var2 < 12)
  mov cx, 1
.ELSEIF(var2 == 2)
  mov cx, 2
.ELSEIF(var2 == 4)
  mov cx, 3
.ELSE
  mov cx, 0
  .IF(var2 == 3)
    mov cx, 3
  .ELSE
    mov cx, 4 
  .ENDIF
.ENDIF

Nous remarquons des directives .IF , .ELSE et .ENDIF entre le .ELSE et le .ENDIF final. L'explication est qu'un bloc complet, c'est-à-dire ouvert par un .IF et fermé par un .ENDIF , devient une entité qui peut se placer à tout endroit où peut se trouver une instruction. Il est important de bien indenter son code source dans ces situations d'imbrication.

Il est facile de confirmer le fonctionnement de ce bloc sur l'extrait du fichier listing. Ceci permet éventuellement de lever tel ou tel doute sur le comportement :

                        .IF(var2 < 12)
0917  83 3E 005C R 0C *        cmp    var2, 00Ch
091C  73 05           *        jae    @C0029
091E  B9 0001                  mov    cx, 1
                        .ELSEIF(var2 == 2)
0921  EB 2A           *        jmp    @C002B
0923                  *@C0029:
0923  83 3E 005C R 02 *        cmp    var2, 002h
0928  75 05           *        jne    @C002C
092A  B9 0002                  mov    cx, 2
                        .ELSEIF(var2 == 4)
092D  EB 1E           *        jmp    @C002E
092F                  *@C002C:
092F  83 3E 005C R 04 *        cmp    var2, 004h
0934  75 05           *        jne    @C002F
0936  B9 0003                  mov    cx, 3
                        .ELSEIF !0
0939  EB 12           *        jmp    @C0031
093B                  *@C002F:
093B  B9 0000                  mov    cx, 0
                            .IF(var2 == 3)
093E  83 3E 005C R 03     *        cmp    var2, 003h
0943  75 05               *        jne    @C0034
0945  B9 0003                      mov    cx, 3
                            .ELSE
0948  EB 03               *        jmp    @C0036
094A                      *@C0034:
094A  B9 0004                      mov    cx, 4
                            .ENDIF
                        .ENDIF
094D              *@C0036:
094D              *@C0031:
094D              *@C002E:
094D              *@C002B:

 

Directives de boucles

MASM propose les directives .WHILE , .ENDW , .REPEAT , .UNTIL , .UNTILCXZ , pour deux types principaux de boucles, auxquelles il faut ajouter .BREAK  et .CONTINUE , directives de rupture prématurée de la boucle. C'est moins riche que dans les langages évolués, mais suffisant.

Dans les deux types de boucles, un bloc de code est effectué, selon le test d'une condition. Remarquons qu'il faut à priori que les termes de la condition soient modifiés dans le bloc de code, sauf dans des cas particuliers qui existent.

Boucles .WHILE ... .ENDW

Une boucle de type WHILE obéit au schéma général :

.WHILE condition
;Bloc d'instructions
.ENDW

condition est d'abord testée, si FAUX le programme continue à l'instruction suivant le .ENDW , si VRAI le Bloc d'instructions est exécuté. Arrivé à .ENDW , condition est à nouveau testée et ainsi de suite. Le fonctionnement avec des .IF et .ENDIF pourrait être :

.IF condition
;Bloc d'instructions
JMP (.IF condition)
.ENDIF

En réalité, nous allons voir que le code assembleur est différent, mais le résultat est le même. La caractéristique de cette boucle est que Bloc d'instructions peut ne jamais être exécuté. Il ne le sera jamais avec condition à FAUX.

Testons une copie d'une chaîne dans une autre :

.DATA
chaine1 BYTE "Fin de la boucle WHILE",0Dh, 0Ah,"$"
chaine2 BYTE 50 DUP (?)
 
.CODE
mov bx, 0
.WHILE (chaine1[bx] != '$')
  mov al, chaine1[bx]
  mov chaine2[bx], al
  inc bx
.ENDW
 
mov chaine2[bx], '$'
 
mov ah, 09h
mov dx, offset  chaine2
int 21h

La boucle ne sera jamais parcourue avec le caractère  $ , qui pourrait donc manquer en fin de la chaîne cible. C'est ce qui explique la ligne mov chaine2[bx], '$' . Il aurait été possible d'initialiser BX à ‑1 et de remonter inc bx en début de bloc.

Les trois dernières lignes affichent chaine2 .

Voyons ce que MASM génère :

095F  BB 0000           mov bx, 0
                        .WHILE (chaine1[bx] != '$')
0962  EB 09           *     jmp    @C003D
0964                  *@C003E:
0964  8A 87 005E R          mov al, chaine1[bx]
0968  88 87 007F R          mov chaine2[bx], al
096C  43                    inc bx
                        .ENDW
096D                  *@C003D:
096D  80 BF 005E R 24 * cmp    chaine1 + [bx], '$'
0972  75 F0           * jne    @C003E
 
0974  C6 87 007F R 24   mov chaine2[bx], '$'

Remarquons que le test est reporté en fin de boucle. Après le JMP initial, la structure de code assembleur en est allégée.

Comme pour le .IF ... .ENDIF , le bloc situé entre un .WHILE et le .ENDW correspondant devient en quelque sorte autonome et peut syntaxiquement être placé partout où pourrait être placée une instruction. Anticipons pour dire qu'il en est de même des blocs .REPEAT ... .UNTIL et .REPEAT ... .UNTILCXZ .

Boucles .REPEAT ... .UNTIL et .REPEAT ... .UNTILCXZ

Le schéma général est :

.REPEAT
;Bloc d'instructions
.UNTIL condition

Le programme effectue Bloc d'instructions , teste condition , si VRAI reprend Bloc d'instructions et si FAUX continue à la suite. Soit, avec des .IF et .ENDIF  :

boucle:
;Bloc d'instructions
.IF condition
JMP boucle
.ENDIF

La différence avec la précédente est que Bloc d'instructions est toujours effectué au moins une fois. Reprenons l'exemple précédent :

.DATA
ch1 BYTE "Fin de la boucle REPEAT",0Dh, 0Ah,"$"
ch2 BYTE 50 DUP (?)
 
.CODE
mov bx, -1
.REPEAT
  inc bx
  mov al, ch1[bx]
  mov ch2[bx], al
.UNTIL (ch1[bx] == '$')
 
mov ah, 09h
mov dx, offset  ch2
int 21h

et ce que génère MASM :

097B  BB FFFF           mov bx, -1
                        .REPEAT
097E                  *@C0040:
097E  43                    inc bx
097F  8A 87 00A9 R          mov al, ch1[bx]
0983  88 87 00C3 R          mov ch2[bx], al
                        .UNTIL (ch1[bx] == '$')
0987  80 BF 00A9 R 24 * cmp    ch1 + [bx], '$'
098C  75 F0           * jne    @C0040

 

Les boucles de type .REPEAT ... .UNTIL , comme les .WHILE ... .ENDW , génèrent des Jcc  :

.REPEAT
  xor ax, ax
.UNTIL((ax >= 112) &&\
      ((dx < 12)||(dx > var1)) &&\
      (cx == 0))||(var2 == 14)

Ce code génère :

                       .REPEAT
0942                  *@C0037:
0942  33 C0             xor ax, ax
                       .UNTIL((ax >= 112) &&\
                           ((dx < 12)||(dx > var1)) &&\
                           (cx == 0))||(var2 == 14)
0944  83 F8 70        *        cmp    ax, 070h
0947  72 0F           *        jb     @C0039
0949  83 FA 0C        *        cmp    dx, 00Ch
094C  72 06           *        jb     @C003A
094E  3B 16 005A R    *        cmp    dx, var1
0952  7E 04           *        jle    @C0039
0954                  *@C003A:
0954  0B C9           *        or  cx, cx
0956  74 07           *        je     @C0038
0958                  *@C0039:
0958  83 3E 005C R 0E *        cmp    var2, 00Eh
095D  75 E3           *        jne    @C0037
095F                  *@C0038:

Ce code est généré de la même façon que dans le cas de la directive .IF . Il en est autrement des directives .UNTILCXZ [condition] .

Dans ce cas, la fin de la boucle intervient quand CX vaut 0 ou quand condition , optionnelle, est à VRAI. Testons :

mov cx, 14
.REPEAT
  xor ax, ax
.UNTILCXZ((ax >= 112) &&\
         ((dx < 12)||(dx > var1)) &&\
         (cx == 0))||(var2 == 14)

Oups ! Erreur à l'assemblage :

tests.asm(172) : error A2062: expression too complex for .UNTILCXZ

Simplifions la condition en (ax == 112) , tout s'assemble et génère le code :

0942  B9 000E      mov cx, 14
                   .REPEAT
0945             *@C0037:
0945  33 C0           xor ax, ax
                   .UNTILCXZ (ax == 112)
0947  83 F8 70   * cmp    ax, 070h
094A  E0 F9      * loopne @C0037

Tout s'explique : .UNTILCXZ génère des LOOP et LOOPcc , la condition optionnelle est donc réduite à un seul test d'égalité, à base de  == ou de != et donc sans && ni || . Le  ! est autorisé devant le test.

Directives .BREAK et .CONTINUE

Ces directives ont le même usage que leur équivalent en C/C++.

.BREAK n'est pas seulement le titre d'un beau film, c'est également une directive qui force la sortie de la boucle, c’est-à-dire qui envoie le code à l'instruction suivant le .ENDW , .UNTIL ou .UNTILCXZ . En cas de boucles imbriquées, on ne sort que de la boucle la plus interne.

.CONTINUE expédie le flux de code directement au test de la condition de la boule la plus interne, pour abréger ainsi une itération.

Ces deux directives sont souvent conditionnées à un test. Une facilité d'écriture est offerte :

.IF condition
  .BREAK
.ENDIF

peut être remplacé par :

.BREAK .IF condition

Il en est de même pour .CONTINUE .IF condition .

Pour tester ces deux directives, voyons ces quelques lignes qui lisent un caractère au clavier sans écho à l'écran, l'affichent si c'est une lettre minuscule, ceci en boucle jusqu'à ce que la touche   Entrée  soit enfoncée :

.WHILE VRAI ;boucle infinie
  ;lecture du clavier sans echo écran
  mov ah, 08h
  int 21h
  .BREAK .IF al == 0Dh
  .CONTINUE .IF (al < 'a') || (al > 'z')
  ;sortie écran du caractère
  mov dl, al
  mov ah, 02h
  int 21h
.ENDW

Et voyons le code fabriqué par MASM :

                    .WHILE VRAI ;boucle infinie
0982  EB 16        *        jmp    @C003D
0984               *@C003E:
0984  B4 08                 mov ah, 08h
0986  CD 21                 int 21h
                            .BREAK .IF al == 0Dh
0988  3C 0D        *        cmp    al, 00Dh
098A  74 10        *        je     @C003F
                            .CONTINUE .IF (al < 'a') || (al > 'z')
098C  3C 61        *        cmp    al, 'a'
098E  72 0A        *        jb     @C003D
0990  3C 7A        *        cmp    al, 'z'
0992  77 06        *        ja     @C003D
0994  8A D0                 mov dl, al
0996  B4 02                 mov ah, 02h
0998  CD 21                 int 21h
                   .ENDW
099A               *@C003D:
099A  EB E8        *        jmp    @C003E
099C               *@C003F:

 

10.12 Procédures

L'assembleur partage la notion de procédure avec les langages évolués : il s'agit d'un bloc de code autonome destiné à effectuer une tâche particulière. À l'appel d'une procédure, il est souvent possible de lui passer un ou plusieurs paramètres. Si elle renvoie une valeur, il est courant de parler alors de fonction . Le terme de sous-programme (subroutine) est parfois employé.

Très généralement, un programme, appelé programme appelant ou programme principal dans ce contexte mais qui peut être lui-même une procédure, appelle une procédure par une instruction comme CALL adresse . La procédure se termine par une instruction de retour, comme RET ou return , qui renvoie le programme à l'instruction suivant le CALL .

Une caractéristique intéressante des procédures est de pouvoir être nommées et ensuite appelées sous leur nom. Dans les langages évolués, ce nom remplace à la fois le CALL ou son équivalent et l'adresse de la procédure. Dans le cas d'une fonction, le nom remplace souvent syntaxiquement la valeur retournée, directement dans les expressions. Tout cela permet l'écriture d'un code lisible et compact :

var1 = tangente(sinus(var2) / complement(var3))

Cette écriture est plus lisible que :

[peut-être déclaration de tempo1, tempo2 et tempo3]
tempo1 = complement(var3)
tempo2 = sinus(var2)
tempo3 = tempo2 / tempo1
var1   = tangente(tempo3)
[peut-être libération  de tempo1, tempo2 et tempo3]

C'est pourtant ce que génère le compilateur.

Il existe des procédures avec argument, ColoreEcran(CL_BLACK) , des procédures sans argument, Bip() , des fonctions avec argument, a = Log(b) , et des fonctions sans argument, time = Now() .

Les raisons d'utiliser procédures et fonctions sont multiples. À l’origine, l'argument majeur était de n'écrire qu'une fois un code utilisé à de multiples reprises, d'où gain de place en mémoire. Normalement, il n'y a pas de raison d'écrire une procédure qui n'est appelée qu'une seule fois. Néanmoins, si cela facilite la clarté du source, nous pourrons souvent accepter les quelques octets et nanosecondes perdues dans le CALL et le RET . Donc, les procédures améliorent la structuration du code. De plus, elles peuvent se situer dans des modules spécifiques, voire dans des librairies réutilisables. C'est ainsi qu'un système d'exploitation peut se mettre à la disposition des programmes applicatifs : Windows propose ses très nombreuses fonctions dans des DLL, alors que DOS utilise une méthode légèrement différente, les interruptions logicielles.

Le langage machine propose les instructions CALL et RET , ainsi que ENTER et LEAVE . Nous sommes loin des langages de haut niveau. Un simple var1 = func(var2, var3) nécessite :

push var3
push var2
call [adresse de fonct]
mov var1, eax

Et encore supposons-nous fonct bien écrite : elle assure elle-même la maintenance de la pile, permettant au programme appelant de ne pas avoir à coder les deux  POP qui devraient correspondre aux deux  PUSH . Nous avons également supposé que fonct renvoie le résultat dans EAX et attend deux paramètres, var2 et var3 , situés dans cet ordre presque au sommet de la pile. Presque, puisque ce sommet sera occupé par l'adresse de retour du CALL , ici celle de la ligne mov var1, eax . Ces suppositions constituent une convention d'appel. Il en existe d'autres, mais elles se classent en deux grandes catégories : celles pour lesquelles les paramètres sont, comme ici, passés par la pile et celles où ils utilisent quelques registres pour cette transmission.

Nous avions débuté cette partie consacrée aux procédures en écrivant : « L'assembleur partage la notion de procédure avec les langages évolués. » Plus que la notion, nous partagerons les procédures elles-mêmes entre différents langages de haut niveau et l'assembleur. Il suffira pour cela de choisir une convention d'appel commune, souvent de respecter en assembleur la convention imposée par le langage évolué. Ce sont ces langages qui ont amené la notion de cadre de pile, ou stack frame.

Les conventions d'appel et les cadres de pile sont présentés, et même développés, à plusieurs autres endroits de l'ouvrage. Voyez l'index, et en particulier le paragraphe sur BASM dans le chapitre Assembleurs intégrés , et le chapitre Pile, cadres de pile et sous-programmes , entièrement consacré au sujet.

Pour un travail autonome en assembleur, nous pourrions travailler au petit bonheur, décidant pour chaque fonction de la façon de passer les paramètres et de récupérer un résultat. Nous n'en avons pas parlé, mais il est possible de concevoir toutes les fonctions sans paramètres ni résultats, échangeant par des variables globales, c’est-à-dire du tas (heap), soit en assembleur le segment de données. Nous perdrions beaucoup en termes de réutilisabilité, mais cela fonctionnerait. Néanmoins, il est de loin préférable de choisir une ou deux conventions et de s'y tenir. Et à choisir une convention, inutile d'innover (ce serait difficile, tout existe). Nous choisirons donc des conventions en accord avec celles existant dans les langages évolués.

Nous pouvons nous poser la question du nombre de points d'entrée et de sortie d'une procédure. Les langages évolués imposent certaines règles, certains intégristes en édictent d'autres, particulièrement sur l'unicité du point de retour. L'assembleur permettant de tout faire, gardons-nous de sodomiser le diptère, il nous appartiendra de fixer nos propres limites.

Pour les points d'entrée, il est clair que, si nous donnons un nom à une procédure, il définira un point d'entrée unique. Il reste la possibilité d'avoir un recouvrement, une partie du code en commun, dans les cas où tout reste clair : soit une fonction bibi qui prend un DWORD en paramètre, passé dans EAX. Nous souhaiterions une autre fonction, baba , la même mais qui prend un WORD dans AX en paramètre. Nous pourrions écrire :

baba:
  and eax, 0000FFFFh
bibi:
  ;traitement
  ret

Nous aurions ainsi deux fonctions pour le prix d'une, mais il ne paraît pas souhaitable pour du code maintenable et évolutif d'aller plus loin dans ce sens que cet exemple trivial. Une meilleure structure, à peine plus chère en temps et volume de code, sera :

baba:
  and eax, 0000FFFFh
  call bibi
  ret
 
bibi:
  ;traitement
  ret

Il en ira de même pour les procédures imbriquées dans une autre procédure : il semble raisonnable de choisir entre deux cas de figure, soit la procédure imbriquée est à usage exclusif de la procédure principale et elle ne pourra pas être appelée directement, soit elle est utilisable seule et elle sera sortie de la procédure principale, livrée à part en quelque sorte.

Ce ne sont que des opinions, l'avantage parfois dangereux de l'assembleur est une grande liberté laissée au programmeur. Si son programme n'est pas destiné à gérer l'arrêt d'urgence d'une centrale nucléaire en Ukraine (bug célèbre), autant profiter de cette liberté.

Pour la sortie, il ne semble pas qu'il y ait la moindre raison de se priver de la facilité offerte par de multiples points de sortie. Bien entendu, dans le cas de code de sortie lourd, il pourra être choisi de renvoyer la sortie vers un point unique.

Nous allons donc voir maintenant ce que propose le macro-assembleur MASM pour nous aider dans cette démarche. Les techniques et instructions de l'assembleur CALL , RET , RET n , ENTER n , LEAVE , les notions de FAR et NEAR sont supposées connues. Voir éventuellement le chapitre qui leur est dédié, déjà mentionné.

Les exemples, ou le cadre pour effectuer des manipulations, sont sur le CD-Rom dans un dossier originalement nommé Procedures .

10.12.1 La directive PROC ... ENDP simplifiée

Nous pouvons mettre en œuvre efficacement les procédures sans utilisation de directives particulières. Dans ce cas-là, le rôle de l'assembleur se limitera à la gestion du nom de la procédure, le label de son point d'entrée, pour l'élaboration de la cible de l'instruction CALL . Notre propos est ici d'utiliser les directives de MASM, à commencer par PROC ... END .

La syntaxe en est :

NomProcedure PROC[[NEAR|FAR]]

Instructions

NomProcedure ENDP

Le bloc d' Instructions constituant le corps de la procédure est totalement libre, l'utilisation basique de ces directives n'apporte, semble-t-il, pas grand-chose. Les deux séquences suivantes répondent de façon identique à l'appel call Aff_bijor  :

Aff_bijor:
       mov ah, 09h
       mov dx, offset   bijor
       int 21h
       ret

 

Aff_bijor PROC NEAR
       mov ah, 09h
       mov dx, offset   bijor
       int 21h
       ret
Aff_bijor ENDP

Ces deux blocs ont été pour l'instant placés à la fin du bloc principal, avant notre END debut . En étudiant le fichier listing, nous constatons que MASM ne génère rien de plus que ce qui est codé. Pour l'appel, avec l'indication NEAR , ou ici sans indication, le code généré est :

000D  E8 001C     call Aff_bijor
...
0033  C3          ret

En utilisant Aff_bijor PROC FAR , nous obtenons :

000D  0E E8 001C  call Aff_bijor
...
0034  CB          ret

0E est tout bêtement un PUSH CS . Le code généré est donc en réalité :

000D 0E      push cs
000E E8 001C call n Aff_bijor
...
0034 CB      retf 

Donc, l'assembleur met en accord les instructions CALL et RET générées avec la distance spécifiée. Ici, il préfère optimiser (c'est un peu sa marotte), en utilisant un CALL NEAR , mais en le précédent d'un PUSH CS , pour que le RET FAR puisse fonctionner. Vous pouvez vérifier à l'aide d'un simple coup de DEBUG (celui du DOS, pas la peine de dépenser plus).

Si vous n'utilisiez pas PROC ... ENDP , il vous faudrait gérer ces détails, en forçant les distances d'appel et de retour, par exemple :

call NEAR PTR Procedure
...
retn

Voilà pour l'utilisation basique de PROC ... ENDP .

Avant de passer à la suite, à commencer par les variables locales et le passage de paramètres, il est impératif de lire également le chapitre Pile, cadres de pile et sous-programmes , déjà évoqué. Il est séparé de celui-ci parce que la notion de cadre de pile nous intéresse également dans le cadre des langages de haut niveau et n'est donc pas spécialement liée à MASM. Ce qui y est écrit est d'ordre général et doit être relié à ce qui va être développé ici.

Situons-nous ici dans le cadre d'un simple appel de procédure, à un seul niveau, et rappelons le principe de base.

Paramètres et variables locales dans une procédure
figure 10.19 Paramètres et variables locales dans une procédure [the .swf]

En nous situant arbitrairement dans un modèle 32 bits (c'est important de le savoir pour le calcul des adresses), le principe de base est de sauver à un moment donné la valeur de (E)SP dans (E)BP, pour ensuite continuer à utiliser la pile sans restriction. Quand (E)SP varie, il sera toujours possible d'accéder à la fois aux paramètres et aux variables locales avec le même écart par rapport à (E)BP. La valeur exacte de (E)SP sauvée dans (E)BP importe peu. Telle que représentée, la différence est faite facilement entre paramètres et variables locales, mais ce n'est pas vraiment utile. Il serait possible de fixer (E)BP à la case située au-dessus de var. locale 3 . De plus, nous n'avons pas représenté la sauvegarde de (E)BP sur la pile juste avant sa modification. C'est généralement ce qui se passe.

Il est tentant d'utiliser des macros textes pour améliorer la lisibilité du code :

param2 TEXTEQU DWORD PTR [EBP + 8]

Et de même pour les variables locales.

Certains compilateurs utilisent les instructions ENTER et LEAVE pour cette gestion. De nombreux d'autres (et certainement nous-mêmes) utilisent parfois le LEAVE et beaucoup plus rarement le ENTER , instruction puissante mais réputée lente et adaptée à des procédures imbriquées. ENTER sera donc simulée le plus souvent.

10.12.2 Les variables locales, la directive LOCAL

Une variable locale à une procédure est une variable qui n'est utile que pendant l'exécution de la procédure. Dans les langages évolués, les variables locales ne sont visibles que de l'intérieur de la procédure. Elles sont généralement créées sur la pile. Dans le cas de structures de données importantes, voire dynamiques (c’est-à-dire dont la taille n'est pas nécessairement connue à la compilation ou est amenée à changer en cours de traitement), la pile ne contiendra qu'un pointeur, la structure étant créée sur le tas. Cette variable pointeur sera habituellement renvoyée par une fonction d'allocation mémoire du système d'exploitation, de type malloc() .

Situer les variables locales dans le pile est une excellente solution, puisqu’elles seront automatiquement détruites en même temps que la procédure prendra fin. Attention toutefois : si vous utilisez de la mémoire dynamique dans une procédure, ne négligez pas de libérer cette mémoire en fin de procédure par un appel de type free() . Sinon, seul le pointeur sera détruit et à chaque appel de la procédure, un peu de mémoire sera perdue. C'est une fuite de mémoire, memory leak.

En assembleur, toutes les variables locales ne méritent pas le même traitement : les variables à durée de vie très brève peuvent souvent naître et mourir dans un registre. C'est en particulier le cas des variables de boucle, qui généralement seront simplement une valeur qui va évoluer dans un registre, CX par exemple. C'est l'équivalent de la variable créée dans une boucle FOR en C++ :

for(int i = 0; i < 100; i++){}

Les variables qui nous intéresseront ici sont celles créées sur la pile. La méthode assembleur pour les créer est désormais connue, du moins dans les grandes lignes. Nous avons déjà dans ce chapitre évoqué celui où sont traités pile, cadres de pile et sous-programmes. Nous avons proposé un schéma montrant ces variables dans la pile, en compagnie des paramètres passés à la procédure.

Réserver de la place pour ces variables est simple, soit par l'instruction ENTER , soit plus souvent par une décrémentation de (E)SP ou quelques PUSH , s'il est bon d'initialiser ces variables. Récupérer la pile en fin de procédure n'est pas plus compliqué. En fait, à part le fait que c'est toujours la procédure qui se charge de la maintenance de la pile pour ses variables locales, ces dernières sont très proches de paramètres supplémentaires gérés par la procédure. La seule différence est qu'elles sont empilées après l'adresse de retour.

Pour faciliter la gestion des variables locales, MASM nous propose la directive LOCAL , de syntaxe :

LOCAL   VariableLocale [[, VariableLocale ]]...

 

Il est ainsi possible de définir d’un à plusieurs éléments VariableLocale , chacun étant la définition d'une variable locale, c’est-à-dire un nom suivi, c'est pratiquement obligatoire, d'un type qualifié : NomVar : TypeQualifié . Il est de plus possible de définir un tableau en ajoutant un nombre d'éléments de la façon suivante : NomVar [ Nombre ]: TypeQualifié . Voyons cela sur un exemple :

0048  Aff_bijor PROC NEAR USES eax edx
                LOCAL VARLOC1:BYTE, VARLOC[2]:BYTE
0048  55            *      push   bp
0049  8B EC         *      mov    bp, sp
004B  83 C4 FC      *      add    sp, 0FFFCh
004E  66| 50        *      push   eax
0050  66| 52        *      push   edx
0052  C6 46 FF 01          mov VARLOC1,   01h
0056  C6 46 FD 02          mov VARLOC[0], 02h
005A  C6 46 FE 06          mov VARLOC[1], 06h
005E  8A 66 FF             mov ah, VARLOC1
0061  02 66 FD             add ah, VARLOC[0]
0064  02 66 FE             add ah, VARLOC[1]
0067  BA 0000 R            mov dx, offset   bijor
006A  CD 21                int 21h
                           ret
006C  66| 5A        *      pop    edx
006E  66| 58        *      pop    eax
0070  8B E5         *      mov    sp, bp
0072  5D            *      pop    bp
0073  C3            *      ret    00000h
0074  Aff_bijor ENDP

Les variables locales ne servent ici qu'à faire l'équivalent d'un mov ah, 09h . Pour être bien interprété, le listing n'est pas idéal, puisqu'il faut désassembler à la main pour voir comment sont faits les accès comme mov VARLOC1, 01h . DEBUG peut nous aider, une fois de plus :

F:\CodeBook\procedures>debug proc1.exe
-u 48 73
0D3D:0048 55            PUSH    BP
0D3D:0049 8BEC          MOV     BP,SP
0D3D:004B 83C4FC        ADD     SP,-04
0D3D:004E 66            DB      66
0D3D:004F 50            PUSH    AX
0D3D:0050 66            DB      66
0D3D:0051 52            PUSH    DX
0D3D:0052 C646FF01      MOV     BYTE PTR [BP-01],01
0D3D:0056 C646FD02      MOV     BYTE PTR [BP-03],02
0D3D:005A C646FE06      MOV     BYTE PTR [BP-02],06
0D3D:005E 8A66FF        MOV     AH,[BP-01]
0D3D:0061 0266FD        ADD     AH,[BP-03]
0D3D:0064 0266FE        ADD     AH,[BP-02]
0D3D:0067 BA0800        MOV     DX,0008
0D3D:006A CD21          INT     21
0D3D:006C 66            DB      66
0D3D:006D 5A            POP     DX
0D3D:006E 66            DB      66
0D3D:006F 58            POP     AX
0D3D:0070 8BE5          MOV     SP,BP
0D3D:0072 5D            POP     BP
0D3D:0073 C3            RET

Nous pourrions également utiliser un désassembleur comme W32Dasm , un produit certainement plus intéressant dans un rôle de désassembleur pédagogique que dans celui de débogueur.

Désassemblage
figure 10.20 Désassemblage

Il suffit en cas de doute de prendre papier et crayon et de suivre l'évolution de la pile, conformément à tout ce qui a déjà été vu à ce sujet. Remarquons que FFFCh correspond à la valeur -4. Cette valeur pour réserver 3 octets s'explique par la volonté de maintenir la pile alignée sur le WORD.

Un détail important apparaît à la lecture de ce désassemblage : pour accéder aux variables locales, MASM utilise un adressage basé, pour lequel il utilise (E)BP. Ce fait réduira de façon importante les possibilités d'accès aux tableaux de variables locales : il ne restera pratiquement que l'utilisation des registres d'index (E)SI et (E)DI. Voir également à ce sujet le mot clé : VARARG , plus loin dans ce chapitre, ainsi que la présentation des modes d'adressage dans le chapitre Le jeu d'instructions .

Avant d'envisager le passage de paramètres, il serait bon de traiter de ce que MASM connaît en tant que conventions d'appel.

10.12.3 Les conventions d'appel

MASM reconnaît par un mot clé un certain nombre de langages, avec lesquels il assure la compatibilité au niveau des appels de procédures. Ces noms sont C, SYSCALL, STDCALL, BASIC, FORTRAN et PASCAL. Ce sont ceux utilisés par les directives .MODEL langage et OPTION LANGUAGE . Les conventions possibles et surtout utilisées sont en nombre relativement restreint, et avec parfois des différences dans le nom, vous retrouverez partiellement celles que nous présentions dans le chapitre Assembleurs intégrés .

Pour un usage interne, vous utiliserez la convention qui vous convient, STDCALL par exemple, voire pas de convention existante du tout. Pour un travail en liaison avec un langage évolué, vous choisirez à priori la convention propre à ce langage. Toutefois, cette contrainte n'est pas rigoureuse, les langages comme C ou Pascal permettent eux-mêmes de gérer divers types de conventions d'appel, en le spécifiant dans le prototype des fonctions, internes ou externes. Visual C++ propose même le mot clé naked , qui permet au programmeur d'écrire lui-même les prologues et épilogues de ses fonctions pour les adapter à n'importe quelle convention d'appel, du moins en termes de passage d'arguments. Il s'agit de fonctions C, mais l'assembleur en ligne sera nécessaire à l'écriture du prologue et de l'épilogue, aucune fonction C n'ayant accès à la pile.

Les conventions d'appel concernent deux domaines : le traitement des noms de symboles et le passage des arguments.

Le traitement des noms de symboles, ou conventions de nommage, décrivent la façon dont les noms des identifiants sont modifiés avant d'être inclus dans le code objet. Les modifications possibles sont l'ajout d'un caractère souligné initial (leading underscore) et le passage en majuscules. Ce point est souvent à l'origine de messages d'erreur du lieur. Le nom Aff_bijor sera inchangé dans la convention SYSCALL. Il deviendra _Aff_bijor dans les conventions C et STDCALL, et AFF_BIJOR pour BASIC, FORTRAN et PASCAL. Dans le cas de la construction d'un exécutable, ce nom se vérifie dans le  .exe , et non dans le  .obj .

Les conventions d'appel décrivent essentiellement :

  L'ordre de passage des arguments dans la pile : dans la liste d'arguments du prototype, ils sont lus pour être PUSHés de droite à gauche ou de gauche à droite.

  La préservation des registres. Il s'agit de la liste des registres susceptibles d'être modifiés par la procédure, ou plus souvent la liste de ceux qui ne doivent pas l'être. (E)AX est toujours utilisé pour renvoyer un résultat.

  La maintenance de la pile : avant l'appel, la procédure appelante fera un certain nombre de PUSH . IL faudra bien les équilibrer par des POP , un RET n , voire une modification directe de (E)SP. Cette partie de la convention s’occupe de savoir ce que fait la procédure et ce qui reste à faire pour l'appelant.

  Le traitement éventuel d'un nombre d'arguments variable. Nous verrons la directive VARARG à ce sujet. Pensez aux arguments de la ligne de commandes dans la fonction main() de C ou à la fonction printf() du même langage. Il y aura souvent un premier paramètre obligatoire donnant le nombre d'arguments passés.

Il y a une logique qui relie ces différents points, particulièrement en cas de nombre variable d'arguments : dans ce cas, il est préférable de pousser les arguments de gauche à droite. Ainsi, le premier de la liste, à priori le seul obligatoire, c’est-à-dire le nombre d'arguments compte , est placé à un endroit connu de la pile, juste en dessous de l'adresse de retour (nous utilisons les termes en dessous et sommet de la pile , sans tenir compte du fait que cette pile pousse vers le bas. Voir le chapitre Pile, cadres de pile et sous-programmes à ce sujet). Ceci reste vrai même si compte est précédé d'un certain nombre d'arguments obligatoires. Dans ces conditions, il est logique qu'il appartienne à la procédure appelante de faire la maintenance de la pile.

Prenons une fonction stats() qui fait soit la somme, soit la moyenne (selon type ) d'une liste de compte WORD. Son prototype serait, en notation ressemblant à du C :

DWORD stats(type, compte, [liste d'arguments])

Son appel en assembleur, pour 3 arguments var1 , var2 et var3 serait (en mode 16 bits) :

push var2
push var1
push var0
mov ax, 3
push ax
mov ax, TYPE_MOYENNE
push ax
call stats
add sp, 10

La dernière ligne représente la maintenance de la pile par le programme appelant.

Évolution de la pile
figure 10.21 Évolution de la pile [the .swf]

Nous avons expliqué par ailleurs que nous préférons représenter la pile à l'envers , sans tenir compte du fait que le pointeur de pile progresse en diminuant, quand ce fait n'est pas important. Cette représentation est plus cohérente avec les expressions empiler, dépiler, sommet de la pile, qui sont bien pratiques pour décrire le comportement de la pile.

Les conventions d'appel interlangage décrites ici, celles normalisées par MASM, utilisent toutes la pile pour le passage des arguments. Nous rencontrerons ou avons rencontré (Delphi, Visual Basic) des passages par les registres, qui sont efficaces au sein d'un langage donné mais difficiles à normaliser, du fait du petit nombre de registres disponibles.

La convention C :

  Un caractère souligné est ajouté au nom de symbole : MaFonction devient _MaFonction .

  Le passage des arguments se fait de droite à gauche, par la pile, comme dans l'exemple stats() . Comme dans l'exemple, le nombre d'arguments variable est autorisé, et c'est la procédure appelante qui se charge de la maintenance de la pile.

  La procédure doit renvoyer dans leur état initial les registres (E)BP, (E)SI, (E)DI, DS et SS, ainsi que le flag DF (direction flag).

 

La convention STDCALL :

  Un caractère souligné est ajouté au nom de symbole : MaFonction devient _MaFonction .

  Le passage des arguments se fait de droite à gauche, par la pile, comme dans l'exemple stats() .

  Un nombre d'arguments variable est autorisé, et dans ce cas, c'est la procédure appelante qui se charge de la maintenance de la pile. Si un nombre fixe d'arguments est déclaré dans le prototype, c'est alors la procédure elle-même qui se charge de cette tâche de nettoyage.

  La procédure doit renvoyer dans leur état initial les registres (E)BP, (E)SI, (E)DI, DS et SS. Le flag DF (direction flag) doit être éteint à l'entrée et le rester.

 

La convention SYSCALL :

  Les noms de symboles ne sont pas modifiés.

  Le passage des arguments se fait de droite à gauche, par la pile, comme dans l'exemple stats() . Comme dans l'exemple, le nombre d'arguments variable est autorisé, et c'est la procédure appelante qui se charge de la maintenance de la pile.

  La procédure doit renvoyer dans leur état initial les registres (E)BP, (E)SI, (E)DI, DS et SS, ainsi que le flag DF (direction flag).

 

Les conventions PASCAL, BASIC et FORTRAN :

  Les noms de symboles sont passés en majuscules : MaFonction devient MAFONCTION .

  Le passage d'un nombre variable d'arguments n'est pas possible.

  Les arguments sont empilés en les lisant de gauche à droite.

  La procédure doit se charger d'ajuster la pile avant de rendre la main, la procédure appelante n'a donc qu'à empiler les paramètres.

  SI, DI, BP, DS et SS en mode 16 bits, EBX, ESI, EDI, EBP, DS, SS, ES, FS et GS en mode 32 bits doivent être préservés par la procédure. Le flag DF doit être éteint à l'entrée et le rester.

10.12.4 La directive PROC ... ENDP complète et le passage de paramètres

Revenons à nos directives MASM, à la version complète de PROC  :

NomProcedure PROC  [[ Attributs ]] [[ USES ListeRegistres ]] [[, ]] [[ Paramètres ]]... ]]

Tout sauf le nom de la procédure déjà vu est optionnel, puisque nous avons utilisé la directive sans aucun paramètre, ce qui d'ailleurs n'inhibe pas toutes les possibilités dont nous venons de parler, conventions d'appel et cadres de pile. Nous allons passer en revue l'ensemble de ces mots clés, dans le désordre, puisque par exemple il vaut mieux traiter des paramètres avant les attributs.

USES  : suivi d'une liste de registres. Du code sera ajouté par MASM pour sauvegarder par des PUSH les registres de la liste (code de prologue) en début de procédure, puis les récupérer par des POP avant le RET (code d'épilogue). À la saisie, les registres sont simplement séparés par un espace. Ils doivent faire partie des registres autorisés par le processeur cible :

0040         Aff_bijor PROC NEAR USES eax edx
0040  66| 50       *   push   eax
0042  66| 52       *   push   edx
0044  B4 09            mov ah, 09h
0046  BA 0000 R        mov dx, offset         bijor
0049  CD 21            int 21h
                       ret
004B  66| 5A       *   pop    edx
004D  66| 58       *   pop    eax
004F  C3           *   ret    00000h
0050  90               nop
0051  66| 33 C0        xor eax, eax
                       ret
0054  66| 5A       *   pop    edx
0056  66| 58       *   pop    eax
0058  C3           *   ret    00000h
0059  90               nop
005A         Aff_bijor ENDP

Nous nous sommes amusés à ajouter des instructions après le RET , ainsi qu'un second RET (inaccessible, à dire vrai). Nous constatons que le code d'épilogue est logiquement ajouté avant chaque RET . Nous constatons également que, avec le USES , MASM se prépare à générer des RET n , en codant un RET mais en le désassemblant en RET 0 .

Paramètres

Paramètres  : c'est la liste optionnelle des paramètres passés par la pile à la procédure, selon un protocole dépendant des conventions d'appel. Chaque élément, séparé du précédent par une virgule, est constitué d'un nom de paramètre suivi d'un type qualifié (voir ce terme), selon la syntaxe NomParam : TypeQualifié . Si le type est absent, la taille d'opérande par défaut est appliquée, 16 ou 32 bits. Mais ne pas indiquer de type, c'est tendre le bâton pour se faire battre. Il faudra bien entendu coder les PUSH nécessaires avant l'appel de la procédure, ensuite il suffira dans la procédure de se référer aux paramètres par leurs noms. Plus précisément, MASM saura aller chercher le paramètre dans la pile. Par contre, il n'en fera pas plus dans le cas d'un pointeur, qui reste pour lui une simple valeur qu'il faudra déréférencer à la main, en deux instructions :

0074  test2 PROC v1:WORD, v2:BYTE, pvar:NEAR PTR BYTE
0074  55         *   push   bp
0075  8B EC      *   mov    bp, sp
0077  8B 5E 08       mov bx, pvar
007A  8A 0F          mov cl, BYTE PTR [bx]
007C  8A 5E 06       mov bl, v2
007F  8B 46 04       mov ax, v1
                     ret
0082  5D         *   pop    bp
0083  C2 0006    *   ret    00006h
0086  test2 ENDP

Le listing n'est pas suffisant pour interpréter le travail fait par MASM. Voyons le code désassemblé :

1CA1:0074 55            PUSH    BP
1CA1:0075 8BEC          MOV     BP,SP
1CA1:0077 8B5E08        MOV     BX,[BP+08]
1CA1:007A 8A0F          MOV     CL,[BX]
1CA1:007C 8A5E06        MOV     BL,[BP+06]
1CA1:007F 8B4604        MOV     AX,[BP+04]
1CA1:0082 5D            POP     BP
1CA1:0083 C20600        RET     0006

Les deux listings s'interprètent d'eux-mêmes. Remarquons que mov cl, BYTE PTR [bx] est redondant, la taille de la donnée étant fournie par le registre CL. Mais cette forme est peut-être plus lisible.

Nombre de paramètres variable :VARARG

Pour le dernier paramètre de la liste, le type peut être remplacé par le mot –clé :VARARG , sous la forme NomParam :VARARG , dans le cas où la convention d'appel en cours autorise un nombre de paramètres variable, c’est-à-dire C, STDCALL ou SYSCALL. Dans ce cas, un nombre variable de paramètres est supposé présent dans la pile. Il sera possible d'y accéder par la syntaxe NomParam[ n ]. Attention, comme toujours avec MASM, n  est un nombre d'octets et non de mots. Comme chaque paramètre est sauvé par un PUSH , il occupera sur la pile une taille correspondant à l'attribut de taille d'opérande, 16 ou 32 bits.

C'est le programmeur de la procédure appelante qui va décider du nombre réel de paramètres passés, voire le type de ces paramètres, en les empilant avant l'appel. Il commencera par empiler ( PUSH ) le dernier élément de la liste, puis le liste du dernier jusqu'au premier, puis les paramètres simples, en respectant le sens droite vers gauche (voir l'exemple et le schéma de la pile).

 Il est bien clair que la procédure ne connaissant pas directement le nombre d'arguments passés, il faudra que le programme appelant lui passe au besoin cette information. Deux méthodes pour cela : soit donner au dernier élément de la liste une valeur particulière qui ne peut être une valeur normale, s'il est possible d'en trouver une, soit passer le nombre d'éléments en tant que paramètre. C'est cette dernière méthode que nous avons testée :

test3 PROC NEAR C X: WORD, nombre:WORD, liste:VARARG
  xor ax, ax
  mov di, nombre
  shl di, 1
  .WHILE (di != 0)
    sub di, sizeof WORD
    add ax, liste[di]
  .ENDW
  add ax, X
  ret
test3 ENDP

Cette magnifique fonction fait la somme de tous les éléments de liste, additionne X à cette somme et renvoie le résultat dans AX.

shl di, 1 est une multiplication par deux du nombre d'éléments de la liste pour obtenir le nombre d'octets. De même, DI est décrémenté de deux à chaque itération. Rien ne nous empêcherait de passer le nombre d'octets à la place du nombre d'éléments ou alors de prendre nombre comme compteur et d'entretenir DI ou SI à part.

Représentons la pile à l'appel de la procédure en fonction des explications fournies :

État de la pile à l'entrée de la procédure test3
figure 10.22 État de la pile à l'entrée de la procédure test3 [the .swf]

Essayons de confirmer les indications de ce schéma. Le fichier listing nous donne :

0062  test3 PROC NEAR C X:WORD, nombre:WORD, liste:VARARG
0062  55       *  push bp
0063  8B EC    *  mov  bp, sp
0065  33 C0       xor  ax, ax
0067  8B 7E 06    mov  di, nombre
006A  D1 E7       shl  di,1
                  .WHILE (di != 0)
006C  EB 06    *  jmp  @C0003
006E           *@C0004:
006E  83 EF 02    sub  di, sizeof WORD
0071  03 43 08    add  ax, liste[di]
                  .ENDW
0074           *@C0003:
0074  0B FF    *  or   di, di
0076  75 F6    *  jne  @C0004
0078  03 46 04    add  ax, X
                  ret
007B  5D       *  pop  bp
007C  C3       *  ret  00000h
007D  test3 ENDP

Le désassemblage est encore nécessaire pour bien voir ce qui se passe :

1CA1:0062 55            PUSH    BP
1CA1:0063 8BEC          MOV     BP,SP
1CA1:0065 33C0          XOR     AX,AX
1CA1:0067 8B7E06        MOV     DI,[BP+06]
1CA1:006A D1E7          SHL     DI,1
1CA1:006C EB06          JMP     0074
1CA1:006E 83EF02        SUB     DI,+02
1CA1:0071 034308        ADD     AX,[BP+DI+08]
1CA1:0074 0BFF          OR      DI,DI
1CA1:0076 75F6          JNZ     006E
1CA1:0078 034604        ADD     AX,[BP+04]
1CA1:007B 5D            POP     BP
1CA1:007C C3            RET

Les valeurs du schéma sont confirmées. La ligne ADD AX,[BP+DI+08] est intéressante. Elle montre que MASM utilisant déjà (E)BP comme registre de base pour accéder à la liste avec autre chose qu'une valeur immédiate, il faut impérativement utiliser un registre d'index. Voir à ce sujet la partie concernant les modes d'adressage dans le chapitre intitulé Le jeu d'instructions .

N'oublions surtout pas que la pile doit être nettoyée par le code appelant. Voici un exemple d'appel :

push 00000001b
push 00000010b
push 00000100b
push 3
push 0D0h
;5 WORD PUSHés
call test3
add sp, (5 * sizeof WORD)

Le résultat est conforme : D7h . Pourquoi les trois valeurs de tests sont exprimées en binaire ? Pour des cas plus graves, un seul bit allumé par valeur peut permettre de voir immédiatement dans le résultat quel élément n'aurait pas été ajouté. Une ruse comme une autre.

L'utilisation de  :VARARG n'est pas si fréquente qu'il pourrait paraître. En effet, dans le cas très quotidien des chaînes de caractères, la variable est généralement un simple pointeur et la zone pointée contient sa propre marque de fin. :VARARG n'est donc pas utilisé dans ce cas.

Attributs  : ils sont au nombre de 4, tous indépendants et optionnels. Ce sont :

  La distance , NEAR ou FAR , NEAR16 ou NEAR32 , FAR16 ou FAR32 . La précision 16 ou 32 n'est utile que dans le cas d'un processeur à partir du 386, s'il existe des segments 16 et 32 bits. NEAR par défaut pour les modèles TINY, SMALL, COMPACT ou FLAT, donc FAR pour MEDIUM , LARGE ou HUGE .

  Le langage , ou convention d'appel, que respecte la procédure. Vient surcharger éventuellement celui déterminé par .MODEL langage et OPTION LANGUAGE .

  La visibilité  : PUBLIC (par défaut) indique que la procédure est disponible pour les autres modules. PRIVATE qu'elle ne l'est pas. EXPORT qu'elle est PUBLIC et que, de plus, le lieur placera son nom dans la table d'exportation.

  Une liste d'arguments prologuearg à effet sur la génération de code de prologue et d'épilogue, notions que nous allons maintenant aborder.

10.12.5 Code de prologue et d'épilogue

À partir des indications fournies à la directive PROC , nous avons vu que MASM générait un bloc de code d'entrée dans la procédure, le prologue , et un autre bloc avant la sortie, l'épilogue. En fait, nous avons constaté qu'une copie de l'épilogue était placée avant chaque instruction RET de la procédure.

Le code de prologue et d'épilogue est facile à analyser, il suffit d'étudier le listing fourni avec le commutateur de ligne de commandes  /Sg , et c'est certainement la meilleure des choses à faire. Ce code peut parfois être imprévisible, il dépend en effet des facteurs suivants :

  Variables locales.

  Paramètres passés à la procédure.

  Convention d'appel de la procédure, ou langage.

  Registres déclarés dans USES .

  Les options passées dans la liste prologuearg .

De plus, MASM optimise et adapte le code généré, mais non sa fonction, au processeur cible. Très logiquement, le code d'épilogue dépend fortement du code de prologue.

La création d'un cadre de pile, même simplifié, par la création d'un pointeur de pile fixe dans (E)BP, ne se fera que s'il y existe des paramètres ou des variables locales déclarées. Pour générer un cadre de pile même dans le cas contraire, il faut inclure FORCEFRAME dans la liste prologuearg .

Inclure LOADDS dans cette liste entraîne dans le prologue la sauvegarde de DS sur la pile, suivie de son initialisation. DS sera bien entendu restauré par un POP au moment correspondant de l'épilogue :

push ds
mov ax, DGROUP
mov ds, ax
...
pop ds

Nous avons vu qu'un épilogue est généré pour chaque instruction RET rencontrée. Ce RET sera d'ailleurs remplacé par MASM par un RET n , éventuellement RET 0 . Par contre, ce code d'épilogue ne sera généré qu'avant un RET normal. Il suffira donc d'utiliser un RETN , RETF , RET 0 (ou RET n ) pour empêcher la génération de ce code d'épilogue.

Il est possible de forcer MASM à utiliser nos propres codes de prologue et/ou d'épilogue, sous forme de macros. Pour cela :

OPTION PROLOGUE : NomMacro

OPTION EPILOGUE : NomMacro

Écrire ces macros n'a rien de simple, puisque même dans les cas où elles feraient peu de travail, elles doivent répondre à un cahier des charges épais et précis. Le but de cette démarche semble être d'adapter MASM pour travailler sur et avec de nouvelles versions de systèmes d'exploitation et de compilateur (C/C++) compagnon. Par exemple, le fichier PROLOGUE.INC inclut les macros permettant à MASM de générer les prologues et épilogues compatibles Windows et VC++ 6.0.

 

10.12.6 Les directives PROTO et INVOKE : appeler des procédures

La directive INVOKE fait partie des plus puissantes mises à notre disposition par MASM. Il suffit de parcourir le source d'une application Windows (voir le chapitre consacré à ce sujet) pour se rendre compte de la clarification du code qu'elle permet, sans parler de l'aspect sécurisation. En un mot, il s'agit de faire générer par une directive le code d'appel à une procédure, de la même façon que PROC peut permettre de générer une grande partie du code de service de la procédure.

La directive PROTO

Pour qu'une procédure soit appelée par INVOKE , il faut que ses caractéristiques soient connues à l'assemblage au moment de l'appel. Ces caractéristiques sont à la base définies par PROC , mais également par EXTERNDEF (ou EXTERN ), ou encore par un TYPEDEF .

L'ensemble de ces caractéristiques constitue, à l'instar des langages évolués, le prototype de la procédure. Que la fonction soit externe, dans une librairie, décrite après l'appel, ou même pas encore implémentée, il est possible d'assembler du code utilisant INVOKE en déclarant simplement ce prototype : c'est le rôle de la directive PROTO .

PROTO est une directive PROC à blanc , préparatoire. Elle ne nécessite pas de ENDP . Sa syntaxe est la même que celle de PROC , à l'exception des champs ListeRegistres , prologuearg et visibilité . Les types de paramètres sont nécessaires, mais leurs noms peuvent être omis. Voici un prototype valide pour une procédure déjà vue, légèrement modifiée :

test3 PROTO NEAR C :DWORD, :WORD, :WORD, :VARARG

Bien évidemment, le prototype ne doit pas être en contradiction avec la déclaration PROC .

Utiliser systématiquement une déclaration PROTO en début de code source ou dans un fichier include est une bonne habitude. Les habitués de C/C++ ne démentiront pas.

La directive INVOKE

INVOKE est donc la directive qui permet d'appeler une procédure. Sa syntaxe est :

INVOKE expression [[, arguments ]]

expression est le nom de la procédure ou parfois une référence indirecte à cette procédure. Nous reviendrons sur ce dernier point un peu plus loin.

arguments est une liste de valeurs de paramètres, séparés par des virgules. Remarquez qu'une virgule précède le premier argument de la ligne, il est fréquent de l'oublier.

Sans :VARARG , le nombre de paramètres doit correspondre à celui de ceux attendus par la procédure. Avec :VARARG , il doit être au minimum égal à celui des paramètres obligatoires de la procédure.

Le code généré par INVOKE recèle un piège, un grand nombre de paramètres étant susceptibles de le modifier. Il est donc plus que jamais utile de le vérifier à partir du fichier listing au moindre doute.

Souvenons-nous de la séquence d'appel de test3 dont nous avons écrit le prototype :

push 00000001b
push 00000010b
push 00000100b
push 3
push 00D0h
;5 WORD PUSHés         
call test3
add sp, (5 * sizeof WORD)

Remplaçons-le par un INVOKE et observons le code généré :

            INVOKE test3, 14500, 0D0h, 3, 4, 16200, 1
 0008  6A 01           * push   +00001h
 000A  68 3F48         * push   +03F48h
 000D  6A 04           * push   +00004h
 000F  6A 03           * push   +00003h
 0011  68 00D0         * push   +000D0h
 0014  66| 68 000038A4 * push   +0000038A4h
 001A  E8 004C         * call   test3
 001D  83 C4 0E        * add    sp, 0000Eh

C'est à peu de choses près le code que nous avions utilisé. Remarquons que l'ordre de passage des paramètres est géré par INVOKE et que nous devons toujours les saisir dans leur ordre naturel, celui du prototype ( PROC ou PROTO ).

Nous pouvons utiliser des valeurs immédiates exprimées en décimal ou dans n'importe quelle base. Le paramètre est converti, c’est-à-dire qu'il est mis à la taille attendue, ici l'octet. Vérification faite, 6Ah est bien l'opcode d'un PUSH imm8 . Pourquoi des +0001h , +0002h , etc., qui semblent indiquer un format SWORD (16 bits signés) ? Parce que c'est effectivement cette taille qu'occupera chaque paramètre dans la pile, sous cette forme. Le paramètre  Y (voir code source complet sur le CD-Rom) de type DWORD occupera deux WORD sur la pile, à partir d'un PUSH surchargé par 66h .

Toute syntaxe désignant une donnée plus petite ou de même taille que le paramètre attendu conviendra : MASM effectuera la conversion. Faisons un essai, donner AL comme valeur pour le paramètre  Y (DWORD) :

            INVOKE test3, al, 0D0h, 3, 4, 16200, 1

Alors, l'ancien :

 0014  66| 68 000038A4 * push   +0000038A4h

sera remplacé par :

 0014  6A 00           *  push   000h
 0016  0F B6 C0        *  movzx  ax, al
 0019  50              *  push   ax

Ceci correspond à : empiler un WORD à 0, convertir AL en AX en complétant par des 0, empiler AX, le total correspond bien à convertir AL en EAX en complétant par des 0 et à l'empiler comme DWORD.

Si var0 est un SWORD, le code généré pour :

            INVOKE test3, var0, 0D0h, 3, 4, 16200, 1

sera de la même façon :

 0014  66| 0F BF 06 0058 R *  movsx  eax, var0
 001A  66| 50              *  push   eax

Donc, un argument peut être saisi dans INVOKE , pour un paramètre de type WORD (pour l'exemple) de l'une de ces façons :

  Un registre de 8 ou 16 bits.

  Une valeur immédiate de -32768 à +65535.

  Toute variable de type BYTE, SBYTE, WORD, SWORD.

  Une expression spécifiée par l'opérateur PTR à l’un de ces types.

  Un NEAR PTR .

En résumé, MASM convertit gaillardement tout ce qui entre, sans trop se préoccuper des signes. Il faudra donc rester prudent.

Testez, toujours sur la même procédure, l'appel :

INVOKE test3, eax, cl, 3, 4, 16200, 1

Nous obtenons un message d'erreur :

proc2.asm(39) : error A2133: register value overwritten by INVOKE

Le fichier listing, qui est généré malgré l'erreur, nous fournit l'explication :

 0011  8A C1         *      mov    al, cl
 0013  32 E4         *      xor    ah, ah
 0015  50            *      push   ax
 0016  66| 50        *      push   eax
 0018  E8 004C       *      call   test3

AX est utilisé pour convertir CL vers le format WORD, ce qui détruit le contenu de EAX avant qu'il ne soit empilé. Comme souvent dans ces cas-là, MASM ne cherche pas une stratégie de remplacement et signale une erreur. C'est aussi bien comme ça, il n'est pas nécessaire d'ajouter à l'imprévisibilité.

Il est possible de passer un pointeur  FAR (en mode 16 bits) en paramètre, en déclarant un DWORD dans le prototype ou la définition. L'appel pourra se faire par INVOKE , avec la particularité que segment et offset seront séparés par un double deux-points  :

INVOKE MaProc, es::bx, var0, var1

 

Une autre méthode pour passer un pointeur  FAR fait appel à l'opérateur ADDR . Imaginons une procédure de traitement de chaîne de caractères. La question se posera parfois de lui passer en argument l'adresse d'une chaîne, ce qui sera aisée si elle est NEAR , mais plus embêtant sous forme de pointeur  FAR . La solution commence par un TYPEDEF très couramment utilisé :

PBYTE TYPEDEF FAR PTR BYTE

Ensuite, le prototype ou la définition de la procédure :

proch PROC NEAR C fpChaine:PBYTE
;traitement sur la chaine
ret
proch ENDP

Enfin, l'appel par INVOKE et le code généré par MASM :

                   INVOKE proch, ADDR resultat
 0008  1E         *      push   ds
 0009  68 0049 R  *      push   OFFSET DGROUP:resultat
 000C  E8 007B    *      call   proch
 000F  83 C4 04   *      add    sp, 00004h

resultat est une chaîne définie de façon habituelle dans le segment .DATA  :

 resultat  DB "Chaine test", 00h

 

Les langages comme le C/C++ permettent de travailler facilement (enfin...) sur des tableaux de fonctions, la fonction réellement appelée étant déterminée à l'exécution. L'assembleur permet de résoudre ce genre de situation par des instructions comme CALL NEAR PTR [BX+SI] . Signalons que, dans tous les cas de figure, il faut que toutes les fonctions susceptibles d'être appelées aient des prototypes au minimum compatibles.

Malheureusement, INVOKE n'est pas tout à fait adaptée à ce genre de sport. Testons les propositions de la documentation. Au départ, nous voulons accéder à l'une de ces deux fonctions, de même prototype (puisque le nom du paramètre ne fait pas partie du prototype) :

testA PROTO NEAR C varA:WORD
testB PROTO NEAR C varB:WORD

Ensuite, par deux TYPEDEF , nous faisons un type de ce prototype, et un autre, pointeur sur le précédent :

FUNCPROTO TYPEDEF PROTO NEAR varAB:WORD
FUNCPTR   TYPEDEF PTR FUNCPROTO

Ensuite, nous créons une table de (deux) pointeurs de type FUNCPTR , initialisés vers nos deux procédures  testA et testB  :

.DATA
pFonction FUNCPTR OFFSET testA, OFFSET testB

Les deux entrées dans cette table sont aux index 0 ( testA ) et 2 ( testB ), puisque, rappelons-le, l'index indique un déplacement en octets.

Il suffit (sic) maintenant de placer la base de la table dans BX, l'index dans SI et d'utiliser INVOKE  :

mov bx, OFFSET pFonction
mov si, 2
INVOKE FUNCPTR PTR [bx + si], var0

La démarche évoque indubitablement le langage C. Le code généré par INVOKE est d'une grande simplicité :

       INVOKE FUNCPTR PTR [bx + si], var0
 000E  FF 36 0076 R  *  push   var0
 0012  FF 10         *  call   word  ptr [bx+si]

Pour être complet, la documentation propose une autre forme, sur la base de la directive ASSUME  :

ASSUME bx:FUNCPTR         
mov bx, OFFSET pFonction
mov si, 0
INVOKE [bx + si], var0

Le code généré est pratiquement le même, et c'est bien normal :

                        ASSUME bx:FUNCPTR
 0008  BB 0000 R        mov bx, OFFSET pFonction
 000B  BE 0000          mov si, 0
       INVOKE [bx + si], var0
 000E  FF 36 0076 R  *  push   var0
 0012  FF 10         *  call   near  ptr [bx+si]

Ces exemples sont sur le CD-Rom, avec les fonctions testA et testB contenant trois lignes de code permettant de les identifier.

Envoyer un message à l'auteur

se connecter pour pouvoir écrire

Ce site est créé et maintenu (laborieusement) par Pierre Maurette
hébergé par 1 & 1