I. Introduction▲
I-A. Comment utiliser ce manuel▲
Si vous voulez savoir pourquoi j'ai écrit GoAsm, connaître les aspects juridiques et les conditions de licence relatives à ce produit, la suite de cette introduction vous est destinée.
Si vous voulez un aperçu de certaines des caractéristiques de GoAsm, alors cliquez ici.
Si vous débutez et que vous voulez apprendre comment faire un programme Windows simple, cliquez ici.
Si vous souhaitez voir quelques exemples de code GoAsm pour plateforme 32 bits, activez les liens suivants :
- HelloWorld1.asm : programme de console Windows 32 bits (voir également ici) ;
- HelloWorld2.asm : programme pour Windows GDI 32 bits dessinant une ellipse dans une fenêtre ;
- HelloWorld3.asm : version plus élaborée de HelloWorld2.asm avec usage intensif de trames de pile, de structures, de variables locales, INVOKE et de définitions (macros) ;
- HelloDialog.asm : dialogue utilisant la fonction DialogBoxIndirectParam avec création de contrôles par modèle interne (tables décrivant les contrôles).
Si vous souhaitez voir quelques exemples de code GoAsm pour plateforme 64 bits, activez les liens suivants :
- Hello64World1.asm : programme de console Windows 64 bits ;
- Hello64World2.asm : programme Windows 64 bits dessinant une ellipse dans une fenêtre ;
- Hello64World3.asm : programme Windows dessinant une ellipse dans une fenêtre avec commutation de compilation sur plateforme 32 ou 64 bits.
Les programmes Unicode ainsi que certains aspects de programmation afférents ont été intégrés dans un document séparé qui constitue le volume 2.
Si vous voulez en savoir plus sur les lignes directrices qui structurent GoAsm, alors cliquez ici.
Si vous êtes simplement intéressé par la façon d'utiliser GoAsm, alors cliquez ici pour acquérir les bases de cet assembleur, ici pour en connaître ses fonctionnalités avancées ou ici pour découvrir les points divers - mais néanmoins importants - le concernant.
Cliquez enfin ici si vous désirez tout connaître sur la programmation en 64 bits permise par cet assembleur.
Si vous débutez en assembleur
Bienvenue aux joies de la programmation assembleur ! Écrivez des programmes de travail rapides et compacts. L'assembleur fonctionne très bien avec Windows. Et, s'il est vrai que nous sommes en présence d'un langage de bas niveau, il n'en demeure pas moins que l'API Windows (Applications Programming Interface) lui adjoint des fonctionnalités de très haut niveau. Les deux sont parfaitement compatibles aussi bien en 64 bits qu'en 32 bits. Ce document vous aidera à appréhender la programmation en assembleur. Consultez plus particulièrement, dans votre parcours initiatique, le chapitre III et les annexes. On lira enfin avec le plus grand intérêt les tutoriels qui n'auraient pas fait l'objet de traduction et qui figurent sur le site http://www.godevtool.com/.
I-B. En quoi un nouvel assembleur est-il nécessaire ?▲
Il existe un certain nombre d'assembleurs sur le marché tels que le très populaire MASM de Microsoft, NASM (issu d'une équipe dirigée à l'origine par Simon Tatham et Julian Hall), TASM de Borland et enfin, A386 de Eric Isaacson. De mon point de vue, aucun de ces assembleurs ne peut être considéré comme parfait dans le cadre de la programmation Windows. Certains ont même des défauts gênants. En écrivant GoAsm, je me suis efforcé de construire un assembleur qui produise toujours un code de taille minimale avec une syntaxe claire et évidente, qui n'impose que de faibles exigences au niveau du script source et propose des extensions pour aider à la programmation en Win32 et Win64. Cela m'a également donné l'occasion d'écrire l'éditeur de liens GoLink, qui est finement réglé pour travailler avec GoAsm.
D'autres que moi ont également essayé d'engager une démarche similaire, notamment René Tournois, qui a écrit le fabricant d'exécutables Spasm (maintenant appelé RosAsm), et Tomasz Grysztar avec son assembleur flat (FASM).
I-C. Versions et mises à jour▲
Mon intention est de préserver GoAsm de tout bogue connu. Donc, je travaille habituellement sur des corrections de bogues dès que je les découvre (à moins d'être en vacances). Je produis généralement un correctif à destination de ceux qui signalent des bogues en leur envoyant (ou en postant) une copie de GoAsm avec un numéro de version affecté d'une lettre suffixe. Les bogues relativement mineurs sont généralement traités de cette façon et puis donnent finalement lieu à la publication d'une mise à jour formelle. Ces mises à jour peuvent être obtenues à partir de mon site Web à l'adresse http://www.godevtool.com/. Un bogue grave peut entraîner la publication immédiate d'une mise à jour de GoAsm. Je travaille également à son amélioration de temps en temps : cela se traduit par la mise à disposition d'une version bêta de GoAsm qui est disponible pour tests. Ces versions d'essai sont souvent également disponibles à partir de mon site web. C'est n'est seulement qu'à l'issue de ces tests et des éventuelles modifications induites que les versions bêta se transforment en mise à jour officielle.
I-D. Forum de discussion▲
Il existe un forum consacré à l'assembleur GoAsm et ses outils à l'intérieur du forum MASM géré par Hutch. Vous pouvez y exprimer vos idées sur les outils « Go », me poser des questions ou faire de même avec d'autres utilisateurs et vérifier les mises à jour. Le forum est aussi l'occasion pour moi de vous consulter sur les améliorations à apporter à GoAsm et aux autres outils « Go ».
I-E. Environnements de Développement Intégré (IDE)▲
Les IDE sont des éditeurs qui vous aident à utiliser la syntaxe de programmation correcte, puis à exécuter les outils de développement en vue de créer les fichiers de sortie. En voici quelques-uns :
- Easy Code pour GoAsm : excellent IDE de Visual Assembler écrit par Ramon Sala.
Téléchargez ECGo.zip sur le site http://www.godevtool.com/ - incluant par ailleurs les versions les plus récentes des outils « Go » et des fichiers d'inclusion pour l'utilisation de Easy Code - 792K.
Les tutoriels de Bill Aitken pour l'utilisation de GoAsm et de l'IDE.
- RadAsm : excellent IDE de Visual Assembler pour Windows conçu par Ketil Olsen.
Vous pouvez aller sur Donkey's stable pour Radasm, les fichiers d'inclusion, les macros, des exemples et projets GoAsm.
- NaGoa : Visual Assembler (utilisant GoRC seulement).
I-F. Aspects juridiques▲
I-F-1. Copyright▲
GoAsm est couvert par le Copyright © Jeremy Gordon 2001-2016 [MrDuck Software] - all rights reserved.
I-F-2. GoAsm - licence et distribution▲
Vous pouvez utiliser GoAsm à toutes fins, y compris des programmes commerciaux. Vous pouvez le redistribuer librement (mais sans contrepartie financière, ni l'utilisation avec un programme ou tout autre matériau pour lequel l'utilisateur est invité à payer). Vous n'êtes pas habilité à masquer ou à contester mes droits d'auteur.
I-F-3. Avertissement▲
J'ai fait tous les efforts possibles pour faire en sorte que GoAsm et son programme d'accompagnement AdaptAsm soient au point, mais vous les utilisez entièrement à vos risques. Je ne peux accepter la moindre responsabilité concernant leur fonctionnement, le travail produit, ni les conséquences d'erreurs entachant éventuellement ce manuel.
I-G. Remerciements▲
Je dois des remerciements particuliers à Wayne J. Radburn, de Gatineau, au Québec, qui a entrepris et conduit avec succès tout récemment un ensemble d'améliorations et de corrections de bogues dans les versions les plus récentes de GoAsm (0,57 à 0,61). Je voudrais également remercier Edgar Hansen de Kelowna, en Colombie-Britannique, Canada (« Donkey ») pour son soutien continu et ses encouragements à Wayne et moi-même et, d'une manière générale, remercier tous les utilisateurs de GoAsm. Nous sommes trois, désormais, à détenir le code source de GoAsm et de GoLink, et cela contribuera à garantir l'avenir du projet « Go ». Je suis également très reconnaissant à toutes ces autres personnes qui m'ont encouragé à écrire ces programmes et m'ont éclairé par d'utiles commentaires, des rapports et des conseils avisés. Je me dois de citer, en particulier :
Leland M. George de West Virginia, Daniel Fazekas de Budapest, Greg Heller du Congo (« Bushpilot »), René Tournois de Louisville, Meuse, France (« Betov »), Ramon Sala de Barcelone, Espagne, Bryant Keller de Cartersville, Géorgie, Emmanuel Zacharakis (Manos), et Brian Warburton de Weybridge, au Royaume-Uni.
Merci aussi pour le soutien, les suggestions et les rapports de bogues de grv, Jeff Aguilon, Jonne Ahner, Thomas Hartinger, Martyn Joyce, Kazó Csaba, Dmitry Ilyin, Patrick Ruiz, et de tous les contributeurs du forum GoAsm et outils associés, ainsi que d'autres forums que j'aurais omis de mentionner ici.
II. Les concepts de GoAsm▲
II-A. Les caractéristiques de GoAsm en bref▲
- GoAsm est un assembleur 32 bits pour les processeurs 86 et Pentium et un assembleur 64 bits pour les processeurs AMD64 et EM64T.
- GoAsm produit un fichier objet dans le format Portable Executable COFF approprié pour un éditeur de liens tel que GoLink ou ALINK. Le format COFF est de loin supérieur à l'OMF (Module Object Format) produit par certains assembleurs plus anciens parce que, dans le format OMF, la taille des fichiers objet est limitée à environ 55K. Dans tout projet d'envergure, vous vous situez au-delà de cette limite.
- GoAsm fonctionne seulement en mode flat. Cela signifie qu'il n'y a pas de segmentation du code et des données. Cela rend le script source beaucoup plus propre et plus facile à écrire. Fondamentalement, dans GoAsm, vous pouvez déclarer la section, puis commencer à coder. En programmation 32 bits, vous pouvez utiliser les registres 32 bits pour y entreposer et manipuler des données (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP) et aussi leurs subdivisions 8 bits et 16 bits (AL, AH, BL, BH, CL, CH, DL, DH et AX, BX, CX, DX, SI, DI, BP et SP). Vous pouvez adresser des données de toute taille dans la mémoire, mais, dans la mesure où GoAsm fonctionne uniquement en mode flat vous ne pouvez utiliser que des adresses 32 bits pour ce faire. Il en résulte que vous ne pouvez pas utiliser, par exemple, des instructions telles que ADC W [BX], 6 ou MOV AX, [SI] pour traiter des données en mémoire ; vous devez impérativement leur préférer ADC W [EBX], 6 ou MOV AX, [ESI].
- GoAsm a un certain nombre de fonctionnalités pour vous aider à écrire des programmes Unicode ou pour utiliser le même script source pour des programmes Unicode et ANSI. GoAsm peut lire les fichiers Unicode (UTF-16 ou format UTF-8) et peut recevoir ses commandes et produire sa sortie en Unicode. Voir le support Unicode pour un aperçu et l'écriture de programmes Unicode pour plus de détails.
- GoAsm fonctionne également comme assembleur 64 bits. Bien que le code exécutable en 64 bits soit tout à fait différent, le code source est très similaire et se révèle tout aussi facile à écrire. Vous pouvez même, à partir d'un même code source, produire des exécutables en 32 ou 64 bits à l'aide d'un commutateur approprié. Voir le chapitre 6 relatif à l'écriture de programmes 64 bits pour plus de détails.
- GoAsm prend en charge tous les mnémoniques standard (autres que ceux utilisés uniquement pour la programmation 16 bits), les instructions en virgule flottante x87, MMX, 3DNow! (avec les extensions), les instructions SSE, SSE2, SSE3 et SSSE4, ainsi que AES (cryptage selon l'algorithme de Rijndael), ADX, et quelques autres instructions nouvelles diverses. Voir à ce sujet mnémoniques pris en charge et syntaxe des registres FPU, MMX et XMM.
- J'ai essayé de prendre le meilleur de la syntaxe des assembleurs utilisée en général et du C, de mon point de vue. Voir la section syntaxe et compatibilité avec d'autres assembleurs pour plus de détails.
- Plus de flexibilité et de simplicité sont obtenues en renonçant au contrôle du type de variable ou des paramètres d'API. Voir mon explication pour cette décision.
- Tous les labels (sauf ceux réutilisables) sont supposés être global et public, en ce sens qu'ils sont accessibles à d'autres fichiers source (via l'éditeur de liens). Ceci est très simplement réalisé en utilisant un flag dans le fichier objet et évite la nécessité de déclarer de tel ou tel label en GLOBAL et PUBLIC. D'où une grande économie de temps et d'effort au bénéfice du programmeur ! (Les labels réutilisables dans le cadre des sauts de code courts sont traités différemment).
- Pour des raisons de certitude et de clarté dans votre script, les crochets sont obligatoires pour l'écriture et la lecture de mémoire. Voir mon explication de ce choix.
- GoAsm fournit un moyen très simple de mettre en pile (PUSH) le pointeur d'une chaîne terminée par un zéro pour les appels d'API. Vous pouvez également pousser en pile le pointeur vers des données brutes ordinaires. Voir plus d'informations à ce sujet.
- Vous pouvez également charger les pointeurs chaînes terminées par 0 et des pointeurs vers les données brutes dans les registres de la même manière. Par exemple MOV EAX, ADDR 'Bonjour'. Voir le paragraphe IV.K sur ce point.
- Contrairement à MASM, GoAsm ne renverse pas l'ordre de mémorisation des caractères de valeurs immédiates. Par exemple, la saisie de MOV EAX, 'The ' à destination de GoAsm doit s'écrire MOV EAX, ' ehT' avec MASM. La première syntaxe offre une bien meilleure lisibilité du code source. Elle est également plus cohérente avec les chaînes de caractères d'octets encadrées de guillemets, qui sont toujours chargées caractère par caractère. En théorie, il est discutable qu'un assembleur inverse l'ordre de mémorisation. La raison en est que le processeur, du fait de sa structure, inverse l'ordre des octets chargés dans un registre lorsque ceux-ci proviennent de la mémoire, cette inversion se produisant naturellement dans le sens registre vers mémoire. On voit bien que ces deux inversions s'annulent dans les faits, d'où l'idée qu'il serait imprudent que l'assembleur ne casse cette logique au détriment de la lisibilité du code source. NASM avait pris le contrepied sur cette question et GoAsm s'est rallié à ce choix. Penchez-vous sur cette pratique respectivement pour le code et pour les données. Voir aussi le mécanisme de mémorisation inversée.
- GoAsm offre un système flexible et pratique en matière de labels de code. Je crois que cela est extrêmement important. GoAsm offre trois possibilités : des labels uniques (« globaux »), labels réutilisables de portée locale ainsi que des labels réutilisables de portée non limitée.
- À ce système de label de GoAsm, s'ajoute un système de notation très explicite pour les sauts de code courts et longs.
- Les appels de type « C » sont disponibles en utilisant INVOKE.
- GoAsm vous permet d'appeler une fonction dans une bibliothèque de code statique et d'en charger le code et les données directement dans le fichier de sortie au moment de l'assemblage.
- GoAsm et le linker GoLink sont les seuls fournissant le moyen d'appeler une fonction dans un autre exécutable directement par ordinal, en utilisant une syntaxe simple telle que, par exemple : CALL MyDll:6. Et vous pouvez importer des pointeurs de données sans aucune formalité.
- GoAsm vous permet de spécifier les EXPORTS dans votre fichier source. Vous pouvez utiliser le nom seulement, spécifiez une valeur ordinale, et vous assurer qu'aucun nom n'apparaît dans l'exécutable final si vous le souhaitez.
- Lors de l'édition des liens des fichiers objet GoAsm, GoLink est en mesure d'identifier les labels de code et de donnée qui n'auraient pas été utilisés ou référencés. En utilisant cette fonctionnalité, vous pouvez facilement repérer ces déclarations de données et zones de code redondants dans votre programme.
- Les instructions PUSH, POP, ARG, INC et DEC peuvent respectivement être répétées sur plusieurs opérandes successifs en séparant ces derniers par des virgules.
- L'utilisation de FLAGS comme opérande de PUSH, POP et ARG, et INVOKE et USES.
- Sauvegarde et restauration automatiques des registres et des flags à l'aide de l'instruction USES.
- Pour simplifier les procédures de callback de Windows, GoAsm fournit une structure automatisée de trame de pile utilisant FRAME … ENDF. Des sous-routines peuvent partager les données stockées sur la pile en utilisant USEDATA … ENDU. Les données locales peuvent être déclarées dynamiquement sur une base de message spécifique.
- Simplification de la forme de l'indicateur de type. Par exemple, D au lieu de DWORD PTR.
- GoAsm fournit un support complet pour les structures et unions. Les membres de structures d'unions sont traités comme des labels de plein droit afin qu'ils puissent être traités en utilisant un point et apparaissent comme des symboles de débogage.
- GoAsm fournit également un support complet pour les equates, les macros et les définitions y compris les définitions de portée limitée.
- GoAsm fournit un support complet pour les inclusions de fichiers (include), et vous pouvez également charger un fichier directement dans une section GoAsm en utilisant INCBIN.
- Au lieu d'utiliser INCBIN vous pouvez charger des blocs de données déclarés dans le fichier source lui-même en utilisant DATABLOCK.
- GoAsm est sensible à la casse des caractères (majuscules/minuscules) dans le cas des noms de labels et des noms de définition. Cette fonctionnalité n'est pas escamotable car, permettre l'utilisation de labels de casse mixte peut induire de la confusion. Dans toutes les autres situations GoAsm n'est pas sensible à la casse. Par exemple, vous pouvez écrire indifféremment MOV [ESI], EAX, #INCLUDE, #IF, #DEFINE, STRUCT, DB, ou mov [ESI], eax, #include, #if, #define, struct, db.
- GoAsm ne supporte pas les commandes de run-time « if ». MASM vous permet de tester les conditions et/ou des instructions de répétition dans une boucle utilisant les commandes .IF / .ELSE / .ELSEIF / .ENDIF et .WHILE / .BREAK. Celles-ci testent les conditions à l'exécution. L'assembleur A386 offre la forme #IF pour cette commande, laquelle teste les flags au moment de l'exécution. Cependant, dans MASM, les séries de commandes IF / ELSE / ELSEIF / ENDIF / IFDEF sont utilisées au moment du processus d'assemblage pour structurer le code objet en fonction des paramètres testés. A386 utilise la forme #IF pour ce faire, tandis que NASM lui préfère %IF. La syntaxe « C » est #IF pour les tests de compilation. Pour ma part, ayant utilisé toutes ces syntaxes pour les tests d'exécution qui sont si semblables à la syntaxe établie tout en signifiant quelque chose de complètement différent, j'affirme que c'est une excellente recette pour un désastre assuré. Pour le moment, j'ai décidé de ne pas soutenir toute forme de tests d'exécution ou en boucle. Je suis prêt à reconsidérer cette décision radicale si quelqu'un peut suggérer une syntaxe appropriée. Pour le moment les utilisateurs GoAsm devront donc se contenter des classiques CMP, TEST, LOOP et autres mnémoniques de saut conditionnel. Pour autant, GoAsm soutient pleinement l'assemblage conditionnel au moment de la compilation utilisant les commandes de type « C » #if / #else / #elseif / # endif, etc.
- GoAsm n'impose pas que l'adresse de départ de votre programme soit un nom réservé comme dans l'assembleur A386, bien que cette possibilité soit néanmoins offerte avec le mot réservé START. De plus, vous ne devez pas définir un label puis la directive END pour définir le point d'entrée (comme dans MASM). Avec GoAsm vous utilisez tout simplement un label et indiquez à l'éditeur de liens ce qu'est ce label. Ou, si vous utilisez GoLink, START est présumé en l'absence d'indication contraire. Consulter également sur ce point la section relative à l'utilisation de GoAsm avec différents linkers.
- Vous pouvez essayer GoAsm sur vos scripts source existants. Pour vous soulager de quelques travaux fastidieux pour adapter ces fichiers initialement destinés à d'autres assembleurs, j'ai écrit le programme AdaptAsm.exe, qui effectue ce travail pour vous (sans écraser, pour autant, le fichier d'origine !).
- Et si vous désirez connaître précisément la rapidité d'exécution de GoAsm lors de l'assemblage de vos fichiers ou de parties d'entre eux, vous disposez pour cela de la directive GOASM_REPORTTIME.
II-B. Syntaxe et compatibilité avec d'autres assembleurs▲
La syntaxe acceptable pour l'assembleur est d'une importance capitale pour tout programmeur en assembleur. Elle varie selon les assembleurs. GoAsm ne crée pas de code 16 bits et fonctionne uniquement en mode « flat » (absence de segments). Pour cette raison, sa syntaxe est très simple. J'ai choisi ce que je considère être la meilleure syntaxe avec, pour principal objectif, la clarté et la cohérence. Vous pouvez être en désaccord avec moi sur ce point. Si oui, je serais intéressé par vos points de vue.
Lors de l'écriture initiale de GoAsm, j'ai réfléchi à la possibilité de construire une syntaxe entièrement compatible avec celle d'autres assembleurs, mais j'ai dû rapidement y renoncer en raison d'écarts trop importants susceptibles de se traduire par d'importantes incohérences. J'ai également renoncé à rendre GoAsm entièrement compatible avec un quelconque autre assembleur.
Vous reconnaîtrez la syntaxe d'autres assembleurs. Lorsque cela était possible, j'ai essayé de rester proche de ce que je considère être la meilleure syntaxe de l'assembleur d'usage général. Vous reconnaîtrez également certaines syntaxes empruntées à la programmation en C. J'ai suivi principalement la syntaxe « C préprocesseur » lorsqu'il me semblait inutile de procéder autrement. Cela rend également l'utilisation du préprocesseur commandes de GoAsm totalement compatible avec mon compilateur de ressources GoRC.
II-C. Pourquoi GoAsm ne vérifie pas les types et paramètres▲
Après réflexion, j'ai décidé que GoAsm ne devait pas vérifier les types ou les paramètres. Ceci, dans le but de réduire substantiellement la taille du script source et d'ajouter à sa flexibilité et à sa lisibilité. Je conclus que même vérifier sommairement le type dans la programmation assembleur pour Windows n'est pas du tout essentiel, et génère plus d'inconvénients que d'avantages.
Permettez-moi de m'en expliquer ici.
Dans la vérification de type, l'assembleur doit s'assurer que les références aux zones de mémoire sont faites avec la bonne taille et le bon type de données en fonction de l'usage qui doit être fait de ces zones de mémoire. Ce résultat est obtenu grâce à un processus en deux étapes. Premièrement, lorsque la zone de mémoire est déclarée, le programmeur doit lui allouer un certain « type ». Ensuite, lorsque la zone de mémoire est utilisée, le programmeur a encore pour tâche d'indiquer le type de la mémoire appelée à être utilisée. S'il y a discordance, l'assembleur ou le compilateur afficheront une erreur.t
Certains assembleurs, comme NASM, ne font aucune vérification de type. D'autres, comme A386, ne font que des vérifications sommaires sur les types BYTE, WORD, DWORD, QWORD et TWORD. MASM et TASM, comme C, vous permettent de spécifier vos propres types en utilisant TYPEDEF puis en assurent la vérification.
La vérification de paramètre s'assure que le nombre correct de paramètres est passé à une API et en contrôle individuellement le type. La plupart des assembleurs ne vérifient pas les paramètres, mais MASM permet de le faire si le pseudomnémonique INVOKE est utilisé.
Les requis pour parvenir à une vérification parfaite de type et de paramètre correspondant au niveau du compilateur C sont énormes. Il suffit de regarder l'en-tête d'un programme Windows et de voir les longues listes de différents types alloués aux différentes structures et les paramètres d'API. Sans compter évidemment les efforts du programmeur qui sont nécessaires dans le script source pour veiller à ce qu'aucune erreur ne soit renvoyée par l'assembleur ou le compilateur.
Pour toutes ces raisons, j'ai décidé de suivre l'exemple de NASM sans même proposer la vérification élémentaire de type implémentée dans A386. J'ai utilisé ce dernier de nombreuses années et j'ai apprécié sa syntaxe propre, mais j'ai plutôt ressenti sa vérification sommaire de type comme un obstacle lors de la programmation sous Windows. Ceci, parce que l'on est souvent confronté à des situations où il est nécessaire d'écrire ou de lire des données en utilisant une taille différente de celle utilisée pour les déclarer en premier lieu.
J'ai également renoncé au contrôle de paramètre, estimant qu'il complique inutilement les choses. Il exige d'énormes listes d'API et des paramètres qui doivent être fournis à l'assembleur ou au compilateur afin qu'ils puissent vérifier que ceux-ci correspondent aux besoins de l'API. Oubliez-en ne serait-ce qu'un seul et votre programme ne compile pas. Considérons l'exemple suivant :
PUSH
40h
, EDX
, EAX
, [hwnd]
CALL
MessageBoxA
Voici un appel d'API qui met en œuvre quatre paramètres. Vous seriez tenté d'attendre de l'assembleur qu'il en contrôle le nombre et qu'il vous alerte en cas d'erreur de votre part sur ce point. Mais vous n'avez pas besoin de cet avertissement car votre programme sera tout simplement planté si tel est le cas. Alors, pourquoi effectuer un tel test dans la mesure où il n'y a rien de sournois ici, en tout cas rien qui ne puisse être appréhendé au stade de l'expérimentation ? Au-delà du nombre de paramètres on peut également s'interroger sur la nécessité de tester le type de chacun d'eux. En effet, quel est l'intérêt d'un tel contrôle dans la mesure où tous les paramètres à destination des API sont de type DWord (avec une ou deux exceptions sur des milliers) ? Donc, le risque de taille erronée des données à destination d'une API est quasiment nul.
Je conviens qu'il peut être possible d'envoyer le mauvais type de données à une API. Par exemple, vous pourriez envoyer une constante là où il devrait y avoir un handle ou le contenu d'une adresse mémoire en lieu et place d'un pointeur vers une adresse mémoire. Cependant, l'API ne fonctionnera tout simplement pas dans ce cas - et, encore une fois, il n'y a rien qui ne puisse être remarqué au stade de l'expérimentation.
L'abolition du contrôle de paramètres et de type ne libère pas seulement l'assembleur de beaucoup de travail, le rendant plus rapide en fonctionnement ; elle épargne aussi au programmeur les interrogations qui accompagnent inévitablement la manipulation d'en-tête et d'inclusion de fichiers. Enfin, elle garantit une plus grande fluidité dans l'adressage mémoire, car les notifications d'erreur vous seront épargnées si, d'aventure, vous voulez utiliser des données selon une taille qui ne correspond pas à celle qui a été préalablement déclarée. Donc, en GoAsm même si lParam a été déclarée comme une valeur DWord,
MOV
[lParam], AL
est encore permis. Et si LOGFONT est une structure simple de DWords, GoAsm se satisfait pleinement, par exemple, de
MOV
B[LOGFONT+
14h
], 1
que vous pouvez utiliser pour définir une police en italique.
En m'exemptant du contrôle de type et de paramètres, j'ai été en mesure d'abolir également EXTRN. GoAsm n'a pas besoin de connaître le type de symboles qui sont déclarés en dehors du fichier source (c'est-à-dire découverts pendant la phase d'édition de liens). J'espère que vous conviendrez que cela vous épargnera beaucoup de travail acharné et l'angoisse d'avoir à ajouter ces EXTRN dans les programmes liés.
La contrepartie de la suppression du contrôle du type et des paramètres est que vous devez indiquer à GoAsm la taille des données à exploiter, dans les cas où celle-ci n'est pas implicite.
Par exemple, MOV [MemThing], 23h est-il incorrect. Pour charger 23h en tant qu'octet en MemThing vous devez coder MOV B[MemThing], 23h (équivalent à MOV Byte Ptr [MemThing], 23h avec MASM). Ceci est parce GoAsm ne saura pas au moment de l'assemblage si la valeur 23h doit être chargée en tant qu'octet, mot ou DWord, formats qui sont tous acceptés par l'instruction MOV.
À certains égards, l'exigence d'un indicateur de type (lorsque celui-ci n'est pas évident) est utile. Ceci, ne serait-ce que parce que vous pouvez voir immédiatement sur le libellé de l'instruction elle-même la taille de mémoire affectée par son action. Vous n'avez pas à vous reporter à une déclaration de données antérieure pour rechercher son type pour déterminer ce que l'instruction fera. Par exemple :
MOV
B[MemByte], 23h
; réconfortant de voir que cela se limite à une opération sur un octet
FLD Q[NUMBER] ; utile de savoir que c'est nombre réel Qword chargé en double précision
INC
B[COUNT] ; essentiel de savoir que ce comptage est limité à 256
Un autre avantage découlant de l'absence de vérification de tout paramètre est qu'il n'y a pas besoin que GoAsm mette en relief les noms des appels vers d'autres modules ou ceux destinés à l'importation. Lors de l'utilisation GoLink, c'est un avantage considérable puisqu'il n'y a pas besoin de fichiers LIB à l'étape d'édition de liens. Mais cela signifie aussi que les fichiers objet GoAsm seront différents de ceux fabriqués par un compilateur C ou MASM, parce que ces fichiers contiennent des symboles qui seront mis en relief tandis que GoAsm ne fera pas rien de tel. Depuis sa version 0.26.10, GoLink est cependant en mesure d'accepter des fichiers objet des deux ensembles d'outils précités et de les lier aux fichiers objet GoAsm (il suffit d'utiliser le commutateur de GoLink /mix - voir l'aide de GoLink).
II-D. Pourquoi GoAsm utilise les crochets pour l'écriture et la lecture de la mémoire▲
Les programmeurs en assembleur ont longtemps débattu sur l'usage de crochets dans le libellé de l'adressage mémoire. L'argument dominant est que, puisque vous devez utiliser des crochets lorsque l'adresse est contenue dans un registre - par exemple MOV EAX, [EBX] -, alors vous devez également utiliser des crochets lorsque l'adresse est matérialisée par un label - par exemple MOV EAX, [lParam]. Évidemment, j'ai suivi ce débat avec intérêt. MASM et A386 se sont abstenus de trancher, de sorte que les deux instructions qui suivent font exactement la même chose :
MOV
EAX
, lParam
MOV
EAX
, [lParam]
Cependant, A386 différencie les labels suivis ou non de deux points d'où il résulte que la remarque qui précède est vraie si lParam a été déclaré par :
lParam DD
0
et fausse si lParam a été déclaré par :
lParam
:
DD
0
Dans ce dernier cas, MOV EAX, lParam, toujours selon l'assembleur A386, agirait à l'identique de MOV EAX, OFFSET lParam. Très déroutant !
NASM a fait le grand saut en en faisant une condition pour tout adressage mémoire libellé entre crochets. Toutefois, preuve que le débat reste indéterminé, MOV EAX, lParam y demeure permis. Dans cet assembleur, cette formulation équivaut au MOV EAX, OFFSET lParam utilisé par d'autres assembleurs.
Donc, quand on regarde le code assembleur, sans connaître la syntaxe de l'assembleur concerné, on ne peut jamais être vraiment sûr de ce que MOV EAX, lParam fait. La même instruction peut faire deux choses totalement différentes selon l'assembleur utilisé.
Le TASM de Borland, lorsqu'il passe en mode « Idéal », proscrit complètement MOV EAX, lParam et permet seulement :
MOV
EAX
, [lParam]
ou
MOV
EAX
, OFFSET
lParam
J'approuve cette approche. L'objectif principal, ici, est de s'assurer que le codage est sans ambiguïté. Pour cette raison, j'ai décidé que GoAsm devait être strict sur cette question. Par conséquent, dans GoAsm :
MOV
EBX
, wParam
est complètement interdit, à moins que wParam ne soit un mot défini. Afin d'obtenir l'offset dans GoAsm, vous devez utiliser :
MOV
EBX
, ADDR
wParam
ou, si vous préférez :
MOV
EBX
, OFFSET
wParam
qui signifie la même chose.
Si vous souhaitez adresser la mémoire dans GoAsm, vous devez donc utiliser la syntaxe
MOV
EBX
, [wParam]
II-E. Mnémoniques supportés par GoAsm▲
II-E-1. Qu'est-ce qu'un « mnémonique » ?▲
Un mnémonique est une instruction sous forme de texte que vous utilisez dans votre script source assembleur. GoAsm assemble ces mnémoniques et les convertit en codes opération (opcodes) que le processeur exécute. Ces codes opération sont parfois appelés codes machine. Les mnémoniques sont recommandés par les fabricants de processeurs. Ils sont destinés à transmettre sous forme abrégée et aussi précisément que possible ce que l'instruction fait. Bien qu'il existe maintenant plus de 550 mnémoniques, un programmeur en assembleur n'en utilise seulement que 20 ou 30 régulièrement. Voir une proposition de liste des mnémoniques les plus couramment utilisés dans l'annexe consacrée aux débutants en assembleur.
Pour des raisons de portabilité des scripts source et de cohérence en prévision d'éventuelles mises à jour, tous les assembleurs reconnaissent normalement les mnémoniques au niveau où ils se rejoignent dans la fonction d'assemblage. Pour autant, le processeur ignore les mnémoniques et ne fonctionne que dans le code de la machine lui-même. Les programmeurs non familiers de l'assembleur n'utilisent jamais les mnémoniques. Un compilateur travaillant uniquement en « C », par exemple, produit encore du code machine, mais il ne fonctionne pas avec les mnémoniques en tant que tels (sauf basculé en mode assembleur en ligne).
II-E-2. Quels mnémoniques sont pris en charge par GoAsm ?▲
GoAsm prend en charge tous les mnémoniques correspondant aux instructions à usage général, y compris les instructions x87 en virgule flottante, les instructions MMX, 3DNow! (avec les extensions), SSE, SSE2, SSE3 et SSSE4, ainsi que AES, ADX, et quelques autres nouvelles instructions. GoAsm prend en charge les instructions pseudo CMP qui peuvent être utilisées avec les registres XMM.
GoAsm ne supporte pas certains mnémoniques qui sont utilisés uniquement pour la programmation 16 bits. C'est le cas de IBTS, IRETW, JCXZ, RETF et XBTS.
Enfin, GoAsm ne supporte pas les mnémoniques qui nécessitent des opérandes supplémentaires, et les cas où il existe des mnémoniques plus faciles à utiliser. Entrent dans cette catégorie :
CMPS
; utiliser CMPSB ou CMPSD
INS
; utiliser INSB ou INSD
LODS
; utiliser LODSB ou LODSD
MOVS
; utiliser MOVSB ou MOVSD
OUTS
; utiliser OUTSB ou OUTSD
SCAS
; utiliser SCASB ou SCASD
STOS
; utiliser STOSB ou STOSD
XLAT
; utiliser XLATB
III. Débuter sur GoAsm▲
III-A. Construction d'un fichier ASM▲
Le fichier ASM est un fichier que vous créez et éditez en utilisant un éditeur de texte ordinaire, comme Paws que vous pouvez télécharger à partir de mon site web, www.GoDevTool.com, ou de programmes courants comme Notepad (Bloc-Notes) ou Wordpad qui sont livrés avec Windows. Si vous utilisez ce dernier, vous devez vous assurer que vous enregistrez le fichier dans un format qui n'ajoute pas de caractères de contrôle ou de formatage autres que l'habituelle fin de ligne (retour chariot et saut de ligne). Ceci, parce GoAsm ne s'intéresse qu'au texte brut. Vous pouvez vous prémunir contre ces caractères non désirés en sauvegardant le fichier comme document « texte ». Si vous n'adjoignez pas une extension au nom de fichier (l'extension désigne les caractères après le « point »), alors l'éditeur peut lui attribuer automatiquement une extension « .txt ». Cependant, rien ne vous empêche de la changer en renommant le fichier (vous pouvez exécuter cette opération sur l'Explorateur Windows en pratiquant un clic droit sur le nom et en sélectionnant la fonction « Renommer »).
Il se peut que vous ne puissiez visualiser l'extension du fichier sur votre ordinateur. Il s'agit, en ce cas, d'une question de paramétrage de l'Explorateur Windows. Pour ce faire, sélectionnez l'élément de menu « Affichage », « Options », « Modifiez les options des dossiers et de recherche » puis sur l'onglet « Affichage » et, enfin, veillez à ce que la case « Masquer les extensions des fichiers dont le type est connu » soit décochée. La procédure peut différer légèrement selon la version de Windows.
Il est de tradition chez les programmeurs d'attribuer à leurs scripts source une extension qui correspond au langage dans lequel il est écrit. Par exemple, vous pourriez avoir un fichier assembleur appelé « myprog.asm ». De la même manière, vous trouverez généralement le code source écrit en langage C avec l'extension « .c » ou « .cpp » (pour « C ++ »), « .pas » pour Pascal et ainsi de suite. Cependant, ces extensions sont totalement neutres d'un point de vue strictement informatique. GoAsm accepte ainsi les fichiers de toute extension, de même que ceux qui en sont dépourvus.
Le fichier .asm contient vos instructions pour le processeur en mots et nombres. Celles-ci sont converties en code exécutable successivement par l'assembleur puis par l'éditeur de liens. C'est ce code qui sera reconnu et exécuté par le processeur. On dit donc que le fichier .asm contient votre « code source » ou votre « script source ».
III-B. Insérer du code et des données▲
À titre d'exemple, examinons le code et les données d'un simple programme Windows 32 bits qui écrit « Hello World (from GoAsm) » dans la fenêtre MS-DOS de l'invite de commande. Voici comment s'écrit le fichier asm :
DATA SECTION
;
KEEP DD
0
; variable temporaire
;
CODE SECTION
;
START
:
PUSH
-
11
; STD_OUTPUT_HANDLE
CALL
GetStdHandle ; récupère, en EAX, le handle du buffer de l'écran actif
PUSH
0
, ADDR
KEEP ; KEEP reçoit la sortie de l'API WriteFile
PUSH
24
, 'Hello World (from GoAsm)'
; 24 = longueur de la chaîne
PUSH
EAX
; handle correspondant au buffer de l'écran actif
CALL
WriteFile
XOR
EAX
, EAX
; retourne EAX = 0 comme recommandé par Windows
RET
Notez que tout ce qui est après un point-virgule est ignoré jusqu'à la fin de la ligne, de sorte que vous pouvez insérer des commentaires à partir de ce signe.
Voir la rubrique opérateurs pour d'autres formes de commentaire. Lire le paragraphe Qualité des descriptions et commentaires dans l'Annexe K sur l'importance des commentaires en programmation.
La première ligne de ce fichier ouvre la section de données par DATA SECTION. On consultera la rubrique sections - déclaration et utilisation en ce qui concerne l'importance des sections et comment les utiliser.
Dans cette section, nous déclarons une zone de données de quatre octets (DD signifie un « DWord » ou « double-mot » qui est de quatre octets) et, comme cette zone va être sollicitée dans le programme, nous l'identifions avec le nom « KEEP » et l'initialisons à zéro. En d'autres termes, nous avons créé la variable 32 bits « KEEP ». Lire à cet égard la section déclaration de données pour des explications détaillées sur ce point.
Nous ouvrons ensuite la section de code avec le label « START », qui indique au processeur où commencer l'exécution des instructions du programme. C'est ce qu'on appelle habituellement le « point d'entrée ». Voir la rubrique code et point d'entrée pour de plus amples explications, et notamment sur les variantes admises en matière de point d'entrée.
L'instruction suivante PUSH -11 met la valeur décimale -11 sur la pile en préalable à l'appel de l'API Windows GetStdHandle sur la ligne suivante. Il s'agit là du seul paramètre exigé par cette API. La valeur -11 précise ici que la recherche du handle du buffer d'écran est requise. Ce handle est fourni dans le registre EAX en sortie d'API. Lire l'annexe comprendre la pile pour une explication du fonctionnement de la pile et de l'instruction PUSH. La rubrique comprendre les nombres finis, négatifs, signés et en complément à 2 explique, en détail, ce que l'on entend précisément par « valeur décimale -11 ». Enfin, les débutants en Windows liront avec intérêt l'annexe pour les débutants en Windows qui introduit le fonctionnement des API.
L'instruction d'exécution qui suit PUSH -11 transfère l'exécution à l'API GetStdHandle et, à son retour, le registre EAX se retrouve chargé avec la valeur de handle recherchée. L'exécution se poursuit sur la ligne suivante. Lire, sur ce point, la rubrique transfert de l'exécution à une procédure.
Après ce premier appel d'API, on trouve cinq PUSH successifs. Notez que les deux premiers utilisent une syntaxe spéciale où un seul PUSH permet d'en réaliser plusieurs, les opérandes étant mis à la suite les uns des autres et séparés par une virgule. Ici, il s'agit d'une commodité d'écriture permise par GoAsm et n'ayant rien à voir avec les instructions processeur. Lire à ce sujet la section instructions répétées pour en savoir plus. Ces PUSH constituent les paramètres à passer à l'API WriteFile. Ces paramètres sont, dans l'ordre : zéro, puis l'adresse de la variable KEEP, puis le nombre 24 décimal qui est la longueur de la chaîne (les mots entre guillemets), puis un pointeur vers de début de cette même chaîne et, enfin, le contenu du registre EAX chargé avec la valeur du handle donnée en retour du précédent appel d'API.
En retour de l'appel d'API CALL WriteFile, la valeur zéro est mise dans le registre EAX utilisant l'instruction XOR EAX, EAX. C'est la même chose que MOV EAX,0 mais produit moins d'octets de code. Voir à ce sujet l'annexe quelques conseils et astuces de programmation.
Enfin RET termine le programme en retournant à l'appelant (dans ce cas, Windows lui-même). Voir l'annexe pour les débutants en Windows.
III-C. Assemblage du fichier avec GoAsm▲
Après avoir écrit comme il convient le code et les données de votre fichier ASM, vous êtes maintenant prêt à finaliser votre programme. Cela se fait en deux temps. Vous devez tout d'abord assembler votre fichier puis le lier. Pour ce faire, vous devez ouvrir une fenêtre MS-DOS(1) (invite de commande). Dans ce cas, vous utilisez la ligne de commande :
GoAsm /fo HelloWorld.obj filename
où filename est le nom de votre fichier asm. Voir la section démarrage de GoAsm pour savoir comment utiliser la ligne de commande de GoAsm.
GoAsm produit un fichier « objet » contenant votre code et les données. Ce fichier reçoit l'extension « .obj » et se présente dans un format adapté à l'éditeur de liens. Voir plus d'informations sur le fichier objet.
III-D. Lien du fichier objet pour créer le programme EXE▲
L'étape finale est de « lier » votre programme pour créer l'exécutable final. Vous pouvez utiliser le programme GoLink complémentaire à GoAsm pour ce faire. Dès lors, la ligne de commande se présente ainsi :
GoLink /console helloworld.obj kernel32.dll
Ajoutez le commutateur « -debug coff » si vous envisagez d'examiner le programme dans le débogueur.
Notez que les appels GetStdHandle et WriteFile s'adressent à KERNEL32.DLL, ce qui explique que le nom de cette DLL apparaisse dans la ligne de commande de GoLink. Voir pour plus d'informations à propos des DLL. Voir la rubrique utilisation de GoAsm avec divers linkers si vous souhaitez procéder à l'édition des liens autrement qu'avec GoLink. Consulter l'aide de GoLink pour connaître les autres options de cet éditeur.
Dans la ligne qui précède, GoLink crée le fichier HelloWorld.exe. Vous pouvez ensuite exécuter ce programme à partir de la fenêtre MS-DOS (invite de commande). Tapez « HelloWorld » et appuyez sur Entrée. Vous verrez la chaîne que vous avez envoyée à l'API WriteFile s'écrire dans la console.
Revenons maintenant sur les lignes de votre script source.
Dans un premier temps, vous avez demandé à Windows le handle de la fenêtre de la console, lequel a été renvoyé par l'API GetStdHandle qui l'a recherché puis stocké dans le registre EAX. Dans un second temps, ce handle et la chaîne à écrire ont été passés à WriteFile. Exprimé autrement, vous avez invité Windows à écrire la chaîne spécifiée dans la console. L'information quant à la manière exacte d'utiliser les API et leur passer les paramètres appropriés est disponible auprès de Microsoft depuis le site MSDN (chercher « Platform SDK »). Enfin il est utile de lire la section consacrée aux suggestions sur la façon d'organiser votre travail de programmation.
IV. Éléments de base de GoAsm▲
IV-A. Démarrage de GoAsm▲
La syntaxe de la ligne de commande est :
GoAsm [command line switches] filename[.ext]
Où
- filename est le nom du fichier source ;
- [command line switches] donne l'emplacement des éventuels commutateurs de la ligne de commande de GoAsm décrits ci-après.
IV-A-1. Commutateur de ligne de commande▲
|
beep sur erreur. |
|
place systématiquement le fichier de sortie dans le répertoire courant. |
|
définit un mot (par exemple /d WINVER=0x400). |
|
fichier de sortie vide autorisé. |
|
spécifie le fichier sortie avec son chemin d'accès. Par exemple /fo asm\myprog.obj. |
|
conserve le soulignement d'en-tête dans les appels externes « C » de la bibliothèque. |
|
aide (affiche les possibilités présentes de la ligne de commande). |
|
crée un fichier contenant le listing d'assemblage. |
|
enrichissement pour mslinker. |
|
pas de messages d'erreur. |
|
pas de messages d'information. |
|
aucun message d'avertissement. |
|
aucun message de sortie quel qu'il soit. |
|
partage des fichiers d'en-tête (les fichiers d'en-tête peuvent être ouverts par d'autres programmes lors de l'assemblage). |
|
assemblage pour processeurs AMD64 ou IA-64. |
|
source assembleur 64 bits en mode de compatibilité 32 bits. |
Si la spécification du nom du fichier d'entrée ne comporte pas d'extension, GoAsm cherche le fichier sans aucune extension. Si ce fichier est introuvable en tant que tel, GoAsm recherche à nouveau le même fichier agrémenté d'une extension .asm.
Si aucun chemin d'accès ne précède le nom du fichier d'entrée, ce dernier est supposé être localisé dans le répertoire courant.
Si aucun nom de fichier n'est précisé pour la création du fichier objet, celui-ci est créé avec le même nom que le fichier d'entrée et affecté de la terminaison .obj. Par exemple MyAsm.asm va créer un fichier appelé MyAsm.obj.
Le répertoire qui reçoit le fichier de sortie est :
- le chemin d'accès spécifié si /fo est utilisé, ou si on ne le mentionne pas :
- le répertoire courant si /c est spécifié, ou si on n'utilise pas ce commutateur :
- le chemin d'accès associé au fichier d'entrée, ou si aucun répertoire n'est donné :
- le répertoire courant.
Si aucune extension n'est précisée pour le fichier de sortie, .obj est créée par défaut. Le fichier de listing d'assemblage emploie le même nom que le fichier de sortie, mais avec l'extension .lst et il est créé, par ailleurs, dans le même répertoire que celui-ci.
IV-B. Sections - déclaration et utilisation▲
IV-B-1. Pourquoi les sections sont nécessaires▲
Vous devez déclarer une section avant que vous ne commenciez à coder. La raison en est que le processeur a besoin de connaître les attributs des instructions qui lui sont adressées. Notez également que le système Windows se repose sur ces attributs pour identifier les parties de votre code. Les attributs les plus courants sont la lecture seule (ne peut pas recevoir d'écriture), la lecture-écriture (peut recevoir une écriture) et l'exécution (instructions de code). En interne, les processeurs traitent l'instruction de la manière la plus appropriée et la plus rapide en rapport avec l'attribut. Par exemple, les instructions de code utilisent le code cache du processeur, le matériau non constitutif de code est assimilé à des données et peut être pris en charge par le cache de données.
Lorsque vous déclarez une section dans votre script source, GoAsm définit automatiquement l'attribut de la section. Une fois ceci fait, vous pouvez commencer à écrire le code ou les données dans votre programme.
IV-B-2. Comment déclarer une section▲
En programmation Windows, nous sommes intéressés par seulement quatre types de sections : le code, les données, les constantes, et les données non initialisées. Vous déclarez le code, les données ou les sections de constantes comme suit :
CODE SECTION
DATA SECTION
CONST SECTION
; ou
CONSTANT SECTION
Les mots CODE, DATA, CONST et CONSTANT sont réservés à la déclaration des sections et une erreur sera signalée si ces mots sont utilisés ailleurs dans votre source.
GoAsm permet également de raccourcir les formes de déclaration de section comme suit :
CODE
DATA
CONST
Vous pouvez également utiliser
.CODE
.DATA
.CONST
si vous le souhaitez.
GoAsm ajoute automatiquement les attributs en fonction du processeur et de Windows. Une section de code reçoit les attributs lecture, exécution, code. Une section de données est dotée des attributs lecture, écriture, données initialisées. Une section const reçoit les attributs lecture, données initialisées (vous ne pourriez pas écrire dans une section const). Les données non initialisées possèdent les attributs lecture, écriture, données non initialisées.
À défaut d'ajouter l'attribut SHARED, vous ne pouvez faire fi de ces attributs de votre propre initiative. Ceci est inutile car Windows s'arroge le contrôle total sur les attributs de la section lorsqu'elle est chargée et exécutée. Par exemple, même si vous donnez à une section de code l'attribut d'écriture, Windows ne vous permettra pas d'écrire dedans. De la même manière, Windows ne vous permettra pas d'exécuter du code dans une section de données. Vous pouvez néanmoins déroger à ce comportement en appelant l'API VirtualProtect au moment de l'exécution.
Dans GoAsm vous pouvez inclure des données en lecture seule (read-only) dans une section de code, même s'il peut en résulter une réduction des performances.
Déclarer une section positionne implicitement certains commutateurs dans GoAsm qui affectent la syntaxe et le codage. Les règles sont les suivantes.
- Tous les labels d'une section de code doivent impérativement se terminer par deux points. Cela permet à GoAsm de distinguer un label de ce qui n'en est pas un, de veiller à ce que les mnémoniques et les directives mal orthographiés soient toujours signalés comme une erreur.
- Les labels réutilisables ne sont autorisés que dans une section de code. Si vous les utilisez dans une section de données, ils seront considérés comme des labels uniques et intégrés à ce titre dans la table des symboles.
- GoAsm signalera une erreur si une instruction tente d'écrire dans une section const. La section const est destinée aux données et chaînes initialisées qui ne sont pas appelées à recevoir une écriture.
IV-B-3. Section de données non initialisées▲
Si vous déclarez des données non initialisées, GoAsm constitue une section spécifique de données non initialisées dans le fichier-objet. Elle sera nommée « .bss » en considération d'autres outils. GoAsm lui attribue d'office ce nom que vous ne pouvez pas modifier parce que certains linkers s'attendent précisément à le trouver. Avec la plupart des linkers, y compris GoLink, la section .bss ne trouve pas sa place dans l'exe final. Au lieu de cela, elle est fusionnée avec une section d'attribut lecture/écriture dans le fichier exe. Les attributs de la section de données non initialisées sont la lecture, l'écriture, les données non initialisées.
L'avantage de déclarer des données non initialisées, plutôt que des données initialisées, est que l'exécutable est plus petit. Ceci, parce que l'exécutable se borne à spécifier la quantité de données non initialisées à réserver sans leur attribuer la moindre valeur. Les buffers de toute sorte sont souvent constitués ainsi. Voir la section déclaration des données non initialisées ordinaires.
IV-B-4. Morcellement des sections▲
Rien n'oblige le programmeur à écrire de grandes sections indivisibles. Par exemple, il est tout à fait envisageable d'avoir une portion de données suivie d'une portion de code, elle-même suivie par une nouvelle portion de données et ainsi de suite, sous réserve de prendre soin d'en déclarer la nature à chaque fois avec un des mots-clés suivants :
CODE SECTION
DATA SECTION
CONST SECTION
ou leur forme abrégée, le cas échéant. Vous pouvez le faire aussi souvent que vous le souhaitez au travers de votre script source. GoAsm et l'éditeur de liens se chargent de concaténer toutes les instructions destinées à chaque section.
Voir aussi sections - gestion avancée sur les dénominations de sections, les sections partagées, les sections de commande, et les considérations liées à l'alignement de la section.
IV-C. Déclaration des données▲
IV-C-1. Qu'est-ce qu'une « donnée » ?▲
D'une certaine manière toutes les instructions transmises à un processeur sont des « données ». Mais les programmeurs en assembleur utilisent ce mot pour désigner une information qui est, soit fixe, soit susceptible d'être modifiée au moment de l'exécution et qui ne peut pas être exécutée en tant qu'instruction de processeur. Les données peuvent être classées en quatre catégories.
- Les données en lecture seule - read-only - spécifiées en tant que telles à la phase d'assemblage (lorsque le programme est compilé) et qui sont conservées dans la section const qui a un attribut de lecture seule. On parle alors de « données initialisées » parce que leur contenu est fixé dans le script source. Au moment de l'exécution, ces données peuvent être lues, mais on ne peut écrire dessus et modifier ainsi leur contenu. Dans votre script source, vous devez leur attribuer des labels de sorte qu'elles puissent être référencées facilement.
- Les données fixées au moment de l'assemblage et localisées dans la section de données du fichier exécutable. Encore une fois, le contenu des données sera fixé dans votre script source mais, au moment de l'exécution, elles pourront être lues ou modifiées en utilisant les labels de donnée correspondants.
- Les données non fixées au moment de l'assemblage, mais qui sont localisées dans une zone qui leur est réservée. Il s'agit des « données non initialisées » et seule leur taille est recensée dans le fichier exécutable. Dans votre script source assembleur vous spécifiez la quantité de données devant être réservées. Vous pouvez leur attribuer des labels, mais vous ne pouvez pas initialiser leur contenu. L'avantage de ce type de données est qu'elles ne prennent pas de place dans l'exécutable. Au moment du chargement, les données sont seulement localisées et ne reçoivent pas de contenu donné à ce stade. Au moment de l'exécution, les données de ce type peuvent être lues ou écrites de la même manière que leurs homologues de la section de données.
- Les données établies au moment de l'exécution, soit par le programme lui-même, soit par le système. Ces données ne sont pas établies au moment de la phase d'assemblage de votre script source. Elles le sont, en réalité, par le système d'exploitation lorsque votre code est exécuté.
IV-C-2. Déclaration des données numériques initialisées▲
GoAsm se conforme à la syntaxe assembleur traditionnelle pour déclarer des données dans votre script source.
Dans une section data ou cons, un label ne doit pas être terminé par deux points. Dans une section de code cela est nécessaire, pour favoriser l'identification des erreurs de syntaxe. Quelques exemples (utilisant une section de données) :
HELLO1 DB
0
; 1 octet avec le label "HELLO1" fixé à zéro
DB
0
; le second octet fixé à zéro
HELLO2 DW
34h
; 2 octets (soit un mot) fixé à la valeur 34h
HELLO3 DD
12345678h
; 4 octets (un dword) fixés à la valeur 12345678h
HELLO4 DD
12345678D
; 4 octets (un dword) fixés à la valeur décimale 12345678
HELLO5 DD
1.1 ; 4 octets (un dword) fixés à la valeur du nombre réel 1.1
HELLO6 DQ
0.0 ; 8 octets (un qword) fixés à la valeur du nombre réel 0.0
HELLO7 DQ
123456789ABCDEFh
; 8 octets (un qword) fixés à la valeur 123456789ABCDEFh
HELLO8 DQ
1234567890123456
; 8 octets (un qword) fixés à la valeur décimale 1234567890123456
HELLO9 DT
1.1E0 ; 10 octets (un tword) fixés à la valeur du nombre réel 1.1
HELLOA DT
123456789ABCDEFh
; 10 octets (un tword) fixés à la valeur 123456789ABCDEFh
Notez que DB, DW, DD et DQ acceptent les nombres aussi bien dans le format décimal qu'hexadécimal ; DD, DQ et DT acceptent également les nombres réels.
Voir les sections déclaration des nombres réels, chargement direct de l'exposant et de la mantisse et chargement d'un fichier avec INCBIN.
IV-C-3. Déclaration de plusieurs données sur une même ligne▲
Une virgule après un initialiseur signifie qu'un autre initialiseur est attendu afin de déclarer d'autres données. La syntaxe est la suivante :
Label
DB
0
, 0
, 0
, 0
; 4 octets fixés à zéro
DW
33h
, 44h
, 55h
, 66h
; 4 mots initialisés
DD
33h
, 44h
, 55h
, 66h
; 4 dwords initialisés
DD
1.1, 2.2 ; 2 DD de nombres réels
DQ
1.1, 2.2 ; 2 DQ de nombres réels
DQ
3333h
, 4444h
; 2 DQ de nombres hexa
DT
1.1, 2.2 ; 2 DT de nombres réels
DT
5555h
, 6666h
; 2 DT de nombres hexa
IV-C-4. Déclaration de données non initialisées ordinaires▲
GoAsm rejoint ici la syntaxe traditionnelle des assembleurs mais, à l'instar de A386, il ne nécessite pas de section non initialisée (la section .bss) pour déclarer ce type de variable. Au lieu de cela, un simple point d'interrogation garantit que la donnée est considérée comme non initialisée. Quelques exemples (dans les sections texte data ou const) :
HELLO1 DB
? ; 1 octet avec le label "HELLO1" enregistré comme non initialisé
HELLO2 DW
? ; 2 octets (word)
HELLO3 DD
? ; 4 octets (dword)
HELLO4 DQ
? ; 8 octets (qword)
HELLO5 DT
? ; 10 octets (tword)
Les données non initialisées orphelines ne sont pas permises : vous ne pouvez pas mélanger les données initialisées et non initialisées, à défaut de quoi vous provoquez une erreur :
DATA6 DD
5
DUP
0
DB
? ; déclaration non autorisée
DB
0
En revanche, l'écriture qui suit est parfaitement correcte :
DATA6 DD
5
DUP
? ; 5 dwords pour le client
DB
? ; un octet pour avoir le plat principal
DB
? ; et un octet pour avoir les sauces
Ceci vous permet de séparer les zones de données non initialisées de sorte que chaque zone séparée puisse avoir son propre commentaire.
Les données non initialisées ne peuvent pas être déclarées tant qu'une section n'a pas été ouverte. Vous pouvez déclarer des données non initialisées au sein de la section de code, mais les labels doivent se terminer par deux points comme il est de règle pour la section de code, par exemple :
HELLO1
:
DB
? ; 1 octet avec le label "HELLO1" enregistré comme non initialisé
HELLO2
:
DW
? ; 2 octets (un word)
IV-C-5. Déclaration de données dupliquées (DUP)▲
GoAsm utilise la syntaxe DUP bien connue, mais ne nécessite pas d'initialiseur entre parenthèses. Quelques exemples (dans la section de données) :
HELLO1 DB
2
DUP
0
; 2 octets avec le label "HELLO1" tous les deux initialisés à zéro
HELLO1A DB
800h
DUP
? ; 2K de buffer de données non initialisées
HELLO2 DW
2
DUP
0
; 4 octets tous fixés à zéro
HELLO3 DD
2
DUP
? ; 8 octets dans la section non initialisée
HELLO4 DD
2
DUP
1.1 ; nombre réel 1.1 dans dword répété 1 fois
HELLO5 DQ
2
DUP
1.1 ; nombre réel 1.1 dans qword répété 1 fois
HELLO6 DQ
2
DUP
333h
; qword répété 1 fois
HELLO7 DT
2
DUP
1.1 ; nombre réel 1.1 dans tword répété 1 fois
HELLO8 DT
2
DUP
444h
; tword répété 1 fois
Vous pouvez utiliser DUP pour déclarer une donnée globalement, puis en initialiser individuellement chaque élément :
HELLO300 DB
3
DUP
<
23
, 24
, 25
>
; déclare 3 octets et les initialise respectivement à 23, 24, 25
qui fait la même chose que :
HELLO300 DB
23
, 24
, 25
; déclare 3 octets et les initialise respectivement à 23, 24, 25
Bien qu'il puisse sembler inutile d'y recourir, la syntaxe rend plus facile l'initialisation d'un membre d'une structure si celui-ci contient l'opérateur DUP Voir la section initialisation de membres de structure avec déclarations de données DUP.
IV-C-6. Initialisation utilisant des caractères en lieu et place de leurs codes ASCII▲
Au lieu de devoir initialiser des caractères par le biais de leur code ASCII, vous pouvez obtenir plus directement ce résultat en vous bornant à déclarer les caractères entre guillemets. Par exemple :
Letters DB
'a'
; au lieu de DB 61h
DW
'xy'
; au lieu de DW 7978h
Sample DD
'form'
; au lieu de DD 6D726F66h
ZooDay DQ
'Saturday'
; au lieu de DQ 7961647275746153h
Exception faite du cas de l'insertion de chaînes Unicode, GoAsm ne réalise pas de conversion du caractère, de sorte que la valeur réelle insérée dans le fichier objet dépendra du jeu de caractères courant au moment de l'assemblage.
GoAsm ne mémorise pas le mot et les déclarations de chaîne Word et DWord ci-dessus en utilisant le stockage inverse. Il rejoint en cela la pratique de NASM qui a pris le contrepied de MASM en la matière. Cela signifie que l'octet de poids faible dans DW 'xy' est 'x' et que 'y' est l'octet de poids fort. De même dans DD 'form' correspondant au label Sample, l'octet de plus faible poids est 'f' et ainsi de suite jusqu'à l'octet de plus fort poids qui est 'm'. L'avantage de cette configuration est de vous permettre, par exemple, de transférer très simplement cette chaîne au moyen du codage ci-dessous :
MOV
EDI
, ADDR
BUFFER
MOV
EAX
, [Sample]
STOSD
qui insère dans le buffer la chaîne 'form'.
Les octets non initialisés reçoivent la valeur zéro. Par exemple :
DW
'a'
; le premier octet est 'a', le second est nul
DD
'ab'
; 'a' puis 'b' puis 2 octets à zéro
On peut répéter les initialisations de valeur de caractère, par exemple :
DD
3
DUP
"Hi"
Cela insère 'H' puis 'i' puis deux zéros, cette opération étant répétée à trois reprises.
IV-C-7. Déclaration de chaînes▲
Les chaînes peuvent être entre guillemets simples ou doubles. En voici quelques exemples d'utilisation :
String1 DB
'Ceci est une chaîne'
DB
'Ceci est une chaîne avec des guillemets "internes"'
String2 DB
"Une chaîne entre guillemets"
DB
"J'apprécie le contenu de la chaîne"
String3 DB
'"Une chaîne elle-même entre guillemets"'
DB
"'Une chaîne elle-même entre guillemets simples'"
DB
"'Une chaîne avec ses propres guillemets simples'"
String4 DB
"""Une chaîne avec ses propres guillemets simples et doubles"""
DB
'''Une chaîne elle-même avec ses guillemets "internes"'''
Dans String4 chaque couple de deux guillemets consécutifs constitue un guillemet qui est partie intégrante de la chaîne en tant que caractère affichable. Cette convention vaut uniquement pour les guillemets des deux extrémités de la chaîne qu'ils encadrent (contrairement à GoRC, qui agit également à l'intérieur de la chaîne).
IV-C-8. Déclaration d'une chaîne sur plusieurs lignes▲
Une virgule après une chaîne signifie qu'un autre initialiseur est attendu lequel peut déclarer, soit des données complémentaires, soit une autre chaîne ainsi qu'on peut le voir dans les exemples suivants :
String1 DB
'Ceci est une chaîne avec terminateur null'
, 0
DB
'Première chaîne'
,0
,'Et une autre chaîne'
, 0
String2 DB
22h
, "Une chaîne avec ses propres guillemets doubles"
, 22h
Les valeurs ASCII que vous pouvez utiliser ici, si le vous souhaitez, sont : 22h pour les guillemets doubles et 27h pour les apostrophes.
IV-C-9. Chaînes plus longues▲
Dans le cas d'une chaîne plus longue, il est possible de la scinder par faute de place sur la ligne et d'en reporter le contenu résiduel à la ligne suivante tout en faisant précéder ce contenu de l'opérateur DB. Par exemple :
LongString1 DB
'Son premier programme semblait très prometteur'
DB
'jusqu'
à ce qu'il ne fonctionne pour la première fois'
, 0
LongString2 DB
'Son erreur fondamentale:'
, 0Dh
, 0Ah
DB
'il ne l'
a pas testé en cours de développement', 0
Les valeurs ASCII 0Dh et 0Ah sont respectivement le retour chariot et le saut de ligne. Ils sont utilisés pour commencer une nouvelle ligne lorsque l'ensemble de la chaîne est affiché sur l'écran.
IV-C-10. Chaînes Unicode▲
En programmation Windows, vous avez parfois besoin de déclarer des chaînes Unicode dans les sections data ou const, par exemple dans un modèle de dialogue. Il y a plusieurs façons de procéder dans GoAsm qui sont décrites en détail dans le chapitre « Écriture de programmes Unicode » du volume 2. En bref, vous pouvez utiliser l'une ou l'autre des méthodes suivantes.
- Reposez-vous sur le format Unicode de base du script source (GoAsm peut lire les fichiers Unicode UTF-16 et UTF-8) ;
- Utilisez le symbole L suivi d'une apostrophe utilisé en programmation C, par exemple :
DB
L'Bonjour comment vas-tu?'
- Déclarez la séquence Unicode en utilisant DUS :
DUS 'Je suis une chaîne Unicode avec une nouvelle ligne et le terminateur null'
, 0Dh
, 0Ah
, 0
Voir aussi la section « Prépositionnement utilisant la directive STRINGS » dans le volume 2.
IV-C-11. Insertion de blocs de données par DATABLOCK▲
Pour les blocs de données volumineux risquant d'encombrer inutilement le fichier source, il existe une alternative consistant à utiliser INCBIN pour charger le contenu ou une partie du contenu du fichier contenant ces données. Sinon, vous pouvez utiliser DATABLOCK_BEGIN et DATABLOCK_END s'il s'avère plus approprié de faire figurer explicitement le bloc de données dans le fichier source lui-même.
La syntaxe d'un DATABLOCK est la suivante :
MyBlockData DATABLOCK_BEGIN ;comment
.
. les
données sont insérées ici
.
DATABLOCK_END
Ici tout le matériau positionné entre DATABLOCK_BEGIN et DATABLOCK_END est inséré dans le fichier de sortie de l'assembleur, et vous pouvez ensuite adresser les données en utilisant le label MyBlockData.
GoAsm considère que ces données commencent immédiatement après la fin de la ligne contenant DATABLOCK_BEGIN et s'achèvent à la fin de la ligne précédant immédiatement celle contenant DATABLOCK_END.
Les données sont insérées à l'état brut, c'est-à-dire qu'aucune conversion n'est effectuée. Cela signifie que les caractères qui ne peuvent pas être affichés dans un éditeur ordinaire tel que les espaces ou les tabulations, par exemple, seront également chargés. Cela signifie aussi que le format des données et des caractères qui peuvent être utilisés dans les données ne sont limités que par l'éditeur que vous utilisez pour écrire votre code source.
IV-C-12. Initialisation utilisant les adresses de labels▲
Il est fréquent que vous ayez besoin de charger un DWord avec l'adresse d'un label, de telle sorte qu'après traitement par l'assembleur puis le linker, ledit DWord contienne un pointeur vers ce label. Le label peut être, soit un label de donnée, soit un label de code. Par exemple :
MS1 DB
'Première chaîne à utiliser'
, 0
MS2 DB
'Deuxième chaîne à utiliser'
, 0
Strings DD
MS1, MS2 ; Strings contient l'adresse de MS1 et MS2
Alors, si vous souhaitez utiliser la chaîne MS2, il vous est possible d'écrire MOV ESI, [Strings+4] au lieu de MOV ESI, ADDR MS2.
En généralisant, tous les tableaux peuvent être créés en utilisant cette méthode et adressés au moyen du multiplicateur de registre d'index * (scale), par exemple :
MOV
ESI
, [Strings +
EAX
*
4
]
Ici, le registre EAX reçoit l'index de la chaîne à utiliser. Lorsque EAX est nul, ESI reçoit l'adresse du label de la première chaîne ; lorsque EAX = 1, ESI reçoit l'adresse du label de la première chaîne et ainsi de suite s'il y a plus de chaînes.
Voici un exemple en utilisant des labels de code :
PROCEDURE_TO_CALL DD
FIRSTPROC, SECONDPROC
MOV
ESI
, ADDR
PROCEDURE_TO_CALL ; adresse de la liste de procédures dans ESI
MOV
ESI
, [ESI
+
EAX
*
4
] ; adresse du label de la procédure recherchée dans ESI
CALL
[ESI
] ; appel de la procédure
IV-D. Code et point d'entrée▲
IV-D-1. Qu'est-ce que le « code » ?▲
Le code est constitué des instructions contenues dans une section nommée « Code », qui a les attributs code et execute. Concrètement, vous indiquez au processeur laquelle des instructions de code doit être exécutée. Le processeur lit les instructions octet par octet et les exécute. Chaque octet de code exécutable est appelé un opcode.
IV-D-2. Que fait le « point d'entrée » ?▲
Dans un exécutable ordinaire (fichier .exe), le point d'entrée caractérise l'adresse où l'exécution commence immédiatement après le chargement. Dans une DLL (fichier .dll), cela désigne l'adresse où l'exécution prend place pendant le processus de chargement.
IV-D-3. Comment est contrôlée l'exécution ?▲
Une fois l'exécution commencée et le point d'entrée atteint, votre programme prend le contrôle de l'exécution et va se poursuivre à partir de cette adresse. Assez souvent, bien que cela ne soit pas une obligation, la première instruction au point d'entrée consiste en un CALL, un saut conditionnel ou inconditionnel à destination de procédures écrites plus avant ou d'API.
IV-D-4. Comment établir un point d'entrée ?▲
De ce qui précède on peut voir que, sauf si votre script source est constitué uniquement de données, il est essentiel de fournir un point d'entrée à votre programme. Dans GoAsm, ceci est réalisé très simplement en attribuant un label au point d'entrée puis en indiquant au linker que ledit label est le point d'entrée du programme. On peut également utiliser le label START - suivi de deux points - mot réservé de GoAsm définissant explicitement le point d'entrée et reconnu comme tel par l'éditeur de liens.
Les exemples qui suivent proposent deux syntaxes possibles du commutateur à mettre sur la ligne de commande de GoLink si l'on décide de ne pas utiliser START et de spécifier un label de point d'entrée distinct, par exemple, entry :
-entry STARTINGADDRESS
/entry STARTINGADDRESS
Si vous utilisez ALINK seule la première méthode fonctionne.
L'intérêt du label réservé START est d'éviter de devoir donner au linker une directive spécifique désignant le point d'entrée. GoLink suppose en effet que celui-ci est constitué par label réservé START sauf avis contraire et lorsqu'il est présent. Voici comment spécifier START dans votre script source pour désigner le point d'entrée du programme :
START
:
Cela peut être en majuscules, en minuscules ou en une combinaison des deux.
Nous venons de voir ce qu'il en est en ce qui concerne GoLink. Les linkers concurrents abordent cette question de différentes manières.
Si vous utilisez le MS linker, vous devez faire précéder votre label par un caractère de soulignement. Votre label du point d'entrée devient donc _START: dans votre script source. Ensuite, vous devez positionner l'une ou l'autre de ces deux instructions sur la ligne de commande de l'éditeur de liens (sans le caractère de soulignement) :
-ENTRY START
/ENTRY START
On constate ici que le MS linker est conçu pour fonctionner avec un compilateur C qui fera précéder les labels globaux d'un caractère de soulignement. Donc, l'éditeur de liens cherche l'étiquette _START, plutôt que START. Les programmeurs en assembleur ont dû s'accommoder de ces bizarreries dans les outils Windows pendant de nombreuses années, mais maintenant nous avons notre indépendance !
Voir aussi la section utilisation de GoAsm avec différents linkers.
IV-E. Labels uniques, réutilisables et à portée paramétrable▲
IV-E-1. Qu'est-ce qu'un « label » ?▲
Un label est un nom que vous attribuez à un emplacement particulier dans les données ou dans le code dans la perspective de pouvoir y accéder simplement. Il a la même fonction qu'un signet. Cela vous permet de vous référer à cet emplacement et d'y accéder en utilisant un nom. Un label de donnée se réfère aux données ; un label de code fait référence à un code exécutable. Un symbole est un label qui apparaît dans la table des symboles du fichier objet et qui peut donc être vu par le débogueur si une version de débogage de l'exécutable est constituée.
IV-E-2. Labels uniques▲
Un label unique correspond au cas général d'un label qui ne peut être utilisé qu'une seule fois dans votre script source et dans les fichiers objet liés. Il est dit de portée « globale », c'est-à-dire qu'au moment de l'édition des liens, il peut être accessible à d'autres fichiers objet. Généralement, il est d'usage de choisir un nom qui distingue la fonction de donnée de la fonction de code, par exemple « NAME_LIST » ou « CALCULATE_RESULT ». Si vous avez paramétré votre linker pour fournir une sortie de débogage, tous les labels uniques seront mis dans la liste des symboles et transmis au débogueur. Dans GoAsm vous établissez un label unique comme suit :
NAMEOFLABEL
:
Cela ne produit aucun code, mais fixe un signet appelé NAMEOFLABEL au point des données ou du code où il apparaît. Si vous êtes dans une section de données, les deux points ne sont pas obligatoires. Il en va de même si un label donne le nom d'une trame de pile automatisée. Par conséquent, les lignes suivantes créent toutes des labels uniques :
;(dans la section de données)
HELLO DB
0
; label HELLO
BYE
:
DB
0
; label BYE
MEAGAIN ; label MEAGAIN
;(dans la section de code)
RICE
:
; label RICE
PEAS
:
FRAME ; label PEAS
BEANS FRAME ; label BEANS
Vous pouvez voir à partir de cela que tout mot qui n'est pas réputé être une directive, un mnémonique, une déclaration ou initialisation de données, ou un mot réservé de GoAsm sera considéré comme un label. GoAsm attend deux points après un label de section de code. Ceci parce qu'il y a de nombreux mots qui doivent être utilisés dans une section de code et que, s'ils sont mal orthographiés, il est important qu'une erreur soit déclarée plutôt que le mot soit interprété à tort comme un label.
IV-E-3. Labels réutilisables▲
Parfois, vous avez besoin d'apposer des labels sur des parties de votre script source avec des noms que vous avez déjà utilisés auparavant. GoAsm offre deux niveaux de labels réutilisables qui peuvent être employés dans une section de code :
- labels réutilisables de portée locale commençant par un point ;
- labels réutilisables de portée non limitée composés de chiffres ou d'un caractère suivi de chiffres.
La portée d'un label définit d'où il peut être consulté en utilisant son propre nom non modifié. Regardons de plus près ces deux types labels réutilisables.
IV-E-4. Labels réutilisables de portée locale▲
Ces types de labels, d'une syntaxe particulière et donc reconnaissables à ce titre, sont créés en utilisant un point suivi d'un label, par exemple :
.looptop ; label looptop
.fin ; label fin
La limite de la portée de ces labels spéciaux est balisée par les labels de code uniques présents dans le script source. En d'autres termes, le label peut être sauté à condition qu'il n'y ait pas de label unique sur le chemin. Ainsi, :
JZ
>
.fin
CALCULATE
:
.fin
RET
Ici l'instruction de saut JZ ne trouvera pas .fin parce que le label CALCULATE est un label de code unique placé sur le chemin.
Si vous voulez sauter par-dessus un label de code unique pour atteindre un label réutilisable de portée locale, vous pouvez utiliser un autre label de code unique ou un label réutilisable non délimité comme destination du saut. Il vous est également possible, quoique de manière plus marginale, d'utiliser le label de portée locale dans une trame de pile automatisée. Voir, à ce sujet, labels réutilisables à portée définie dans les trames de pile automatisées.
Les labels réutilisables de portée locale sont envoyés au débogueur comme des symboles avec leur « propriétaire ». Par conséquent le symbole envoyé au débogueur dans l'exemple ci-dessus est CALCULATE.fin, et une autre façon de sauter par-dessus le label unique serait d'écrire JZ >CALCULATE.fin.
IV-E-5. Labels réutilisables de portée non limitée▲
Vous rencontrerez souvent dans votre code des sauts ou des boucles d'amplitude faible pour lesquels le choix d'un nom de label mûrement réfléchi n'apporte aucune plus-value à la compréhension du listing. Pour ceux-ci vous pouvez utiliser un label dont le nom ne sera pas transmis au débogueur en tant que symbole. Il est utile, par ailleurs, lors du débogage de limiter la table de symboles aux noms les plus importants dans votre code. Ces labels sont constitués soit uniquement de chiffres, soit d'un caractère suivi d'un ou plusieurs chiffres. Vous pouvez également utiliser une variante avec un point décimal qui facilite l'ajout de nouveaux labels locaux au code existant. Le label lui-même doit toujours se terminer par deux points. Voici des exemples de syntaxe de labels réutilisables non délimités :
L1
:
24
:
24.6
:
Vous pouvez même utiliser deux points tous seuls pour ces destinations de saut de très faible portée dans votre code.
IV-F. Sauts vers des labels : sauts de code courts et longs▲
Il existe plusieurs instructions de saut. Certaines ne vont agir que si les flags sont dans un état particulier. On les appelle « instructions de saut conditionnel ». D'autres, telles que l'instruction JMP, sauteront toujours à la destination spécifiée indépendamment de l'état des flags. On trouve également les instructions de boucle et leur variante conditionnelle qui s'interrompt si ECX = 0. Enfin, il y a l'instruction CALL qui effectue un saut puis un retour au terme de la procédure appelée. Toutes ces instructions ont besoin d'un label précisant leur destination.
IV-F-1. Les indicateurs de direction▲
Afin de rendre votre script source plus lisible, GoAsm propose des indicateurs de direction pour préciser la direction du saut. L'indicateur de direction « retour » est facultative. Par exemple, en utilisant des labels réutilisables à portée locale :
JZ
>
.fin ; sauter en avant à .fin
JMP
>
.exit
; sauter en avant à .exit
LOOP
.looptop ; boucle arrière vers .looptop
LOOP
<
.looptop ; boucle arrière vers .looptop (forme alternative)
Voici un exemple en utilisant des labels non délimités :
JZ
>
L10 ; saut en avant à L10
JNC
L3 ; saut en arrière à L3
JNC
L3 ; saut en arrière à L3 (forme alternative)
JMP
100
; saut en l'arrière à 100
IV-F-2. Sauts vers des labels uniques▲
Ceux-ci sont traités différemment, selon que le saut est effectué ou non en utilisant un mnémonique de saut conditionnel.
IV-F-3. Sauts conditionnels à des labels uniques▲
Vous pouvez coder des sauts conditionnels à des labels uniques de la même manière que vous le feriez pour des sauts à des labels de portée locale ou non délimitée. En d'autres termes, utilisez l'indicateur vers l'avant « > » si le saut est plus avant dans le script source. En option, vous pouvez utiliser l'indicateur vers l'arrière « < » pour signifier que le saut est à un lieu en amont dans le script source, ou vous pouvez l'omettre. Fondamentalement, GoAsm vous permettra de ne pas sauter d'un fichier en utilisant un saut conditionnel. Ainsi, au lieu de coder :
JZ
EXTERNALLABEL
vous pourriez écrire
JNZ
>
JMP
EXTERNALLABEL
Ceci, pour faciliter la vérification des erreurs. GoAsm suppose qu'un saut conditionnel est censé aboutir à un endroit à l'intérieur du script source existant.
IV-F-4. Sauts inconditionnels à destination de labels uniques▲
Vous pouvez utiliser un indicateur de direction pour ces sauts si vous le souhaitez, mais vous n'y êtes pas contraint. L'indicateur de direction ne fera que dire à GoAsm de rechercher le label dans le script source. GoAsm ne dira pas au linker de chercher le label dans d'autres scripts source. Si vous n'utilisez pas d'indicateur de direction, GoAsm va néanmoins trouver le label s'il existe dans le script source, mais, si tel n'est pas le cas, il va dire au linker de le rechercher dans d'autres scripts source. Par exemple :
JMP
LABEL
; cherche le label dans tous les scripts source
JMP
<
INTERNALLABEL1 ; ne cherche le label qu'en amont dans le script source
JM >
INTERNALLABEL2 ; ne cherche le label qu'en aval dans le script source
IV-F-5. Sauts vers les deux points▲
La ponctuation consistant en deux points isolés est traitée comme un label non délimité et peut être utilisée pour vos sauts les moins significatifs, par exemple :
CALL
PROCESS
LOOPZ
<
ou
CMP
EAX
,EDX
JZ
>
CALL
PROCESS
:
RET
IV-F-6. L'intérêt de sauts longs ou courts▲
Un saut court utilise un mécanisme de déplacement relatif qui tient seulement sur deux octets. Il invite le processeur à revenir en arrière ou à aller vers l'avant avec une amplitude de +127 octets ou -128 octets par rapport à la position courante. L'amplitude du saut est contenue dans le deuxième octet de l'opcode, ce qui en explique la limitation aux valeurs précédemment indiquées.
Pour surmonter cette contrainte, il existe une variante de cette instruction contenant six octets. Il s'agit de la forme longue de l'instruction de saut relatif.
L'utilisation de sauts courts non seulement resserre votre code mais en augmente également la vitesse d'exécution parce que le processeur doit lire et exécuter moins d'octets. Cette considération pourra être déterminante dans les structures d'instructions en boucle qui sont exécutées plusieurs fois.
IV-F-7. Comment forcer GoAsm à coder un saut long▲
Utilisez soit l'opérateur LONG, soit << ou >>. Par exemple :
JZ
>>
.fin ; long saut avant vers .fin
JZ
LONG >
.fin ; long saut avant vers .fin (variante)
JC
<<
A1
; long saut arrière vers A1
JC
LONG A1
; long saut arrière vers A1 (variante)
JC
LONG <
A1
; long saut arrière vers A1 (variante)
Notez qu'il n'y a aucune forme longue de l'instruction LOOP et de ses variantes, ni de JECXZ. Si vous avez besoin d'un saut long de ces instructions utiliser à la place :
DEC
ECX
JNZ
LONG L2 ; saut long remplaçant LOOP
OR
ECX
, ECX
; test de ECX = 0
JZ
LONG >
L44 ; saut long remplaçant JECXZ
IV-F-8. Principes de codage des sauts longs ou courts▲
GoAsm essaie toujours de générer le plus petit code possible, en cohérence avec le fait qu'il s'agit d'un assembleur en une passe. Voici les règles observées :
- GoAsm codera toujours un saut long si c'est spécifié (pour les instructions qui admettent cette possibilité) ;
- pour les sauts en amont vers des labels uniques et portée locale, GoAsm codera automatiquement un saut court si c'est possible, sinon, à défaut, un saut long ;
- pour les sauts en amont vers des labels non délimités, GoAsm codera un saut court ;
- pour les sauts en aval vers des labels non délimités et aussi pour des labels de portée locale, GoAsm codera un saut court ;
- pour les sauts en aval vers des labels uniques, GoAsm va coder un saut long.
GoAsm affichera une erreur si un court saut est spécifié, mais ne peut être atteint. Ceci, pour vous assurer que vous n'avez pas commis d'erreur dans votre script source. Par exemple, vous pourriez avoir codé un saut court tout en oubliant d'ajouter la destination du saut à votre script source.
IV-G. Accès aux labels▲
IV-G-1. Obtention de l'adresse d'un label (ADDR et OFFSET)▲
Les opérateurs ADDR et OFFSET permettent d'obtenir l'adresse d'un label. Dans l'exécutable final sous Windows, ils fournissent la distance du label par rapport au début de la section, ainsi que la position de la section dans la mémoire virtuelle. En d'autres termes, c'est l'adresse du label en mémoire lorsque l'exécutable est chargé et s'exécute.
Voici des exemples d'utilisation de labels uniques :
MOV
ESI
, ADDR
Process_dabs ; ESI = adresse de code du label Process_dabs
MOV
ESI
, ADDR
Hello2 ; ESI = adresse de la chaîne avec le label Hello2
MOV
ESI
, ADDR
HelloX+
10h
; ESI = adresse 16 octets au-delà du label HelloX
Exemple utilisant un label réutilisable de portée locale :
MOV
ESI
, ADDR
CALCULATE.fin ; ESI = adresse de code du label .fin
; dans la procédure CALCULATE
Exemple utilisant une structure formelle :
MOV
ESI
, ADDR
Lv1.pszText ; ESI = adresse du membre psztext dans
; la structure formelle Lv1
Pour le code 64 bits, notez qu'un PUSH, ARG, ou MOV vers la mémoire d'un ADDR ou d'un OFFSET concernant un label non local (les labels locaux sont gérés différemment) fera usage du registre R11 et profitera de l'adressage relatif RIP plus court de l'instruction LEA de la manière suivante :
LEA
R11, ADDR
Non_Local_Label
PUSH
R11
LEA
R11, ADDR
Non_Local_Label
MOV
[MEMORY64], R11
Ce sera également le cas avec INVOKE en passant les arguments avec ADDR, qui comprend également l'utilisation de pointeurs vers une chaîne ou une donnée brute (ex. 'Bonjour' ou <'H', 'i', 0>).
IV-G-2. Lecture de données à partir de l'emplacement pointé par un label▲
La lecture des données à partir de l'endroit pointé par un label est tout à fait différente de l'action consistant à obtenir l'adresse du même label. Ici vous lisez la valeur de données dans le domaine de la mémoire concernée. Cela doit être fait à l'aide des crochets. Exemples :
MOV
ESI
, ADDR
Hello1 ; ESI = adresse du label Hello1
MOV
EAX
, [ESI
] ; EAX = valeur mémoire à l'emplacement du label Hello1
ou, ce qui revient au même :
MOV
EAX
, [Hello1] ; EAX = valeur mémoire à l'emplacement du label Hello1
IV-G-3. Écriture à l'emplacement pointé par un label▲
Ici, vous provoquez une écriture de donnée comme suit :
MOV
ESI
, ADDR
Hello1 ; ESI = adresse du label Hello1
MOV
[ESI
], EAX
; contenu de EAX dans la mémoire dword correspondant au label Hello1
ou ce qui revient au même :
MOV
[Hello1], EAX
; contenu de EAX dans la mémoire dword correspondant au label Hello1
IV-G-4. Lecture et écriture sur des labels utilisant un déplacement▲
Supposons que vous ayez une structure simple de données déclarée comme suit :
PARAM_DATA DD
0
; +0h
DD
0
; +4h
DD
55h
; +8h
DD
0
; +0Ch
DD
0
; +10h
Vous pouvez utiliser le label pour lire et écrire dans une partie particulière de la structure en utilisant une valeur de déplacement conformément à l'exemple suivant :
MOV
ESI
, ADDR
PARAM_DATA ; ESI = offset de PARAM_DATA
MOV
EAX
, [ESI
+
8h
] ; EAX = lecture du troisième DWORD de PARAM_DATA
MOV
[ESI
+
8h
], EDX
; on remplace ce 3e DWORD par le contenu de EDX
ou ce qui fait la même chose :
MOV
EAX
, [PARAM_DATA+
8h
] ; EAX = lecture du troisième DWORD de PARAM_DATA
MOV
[PARAM_DATA+
8h
], EDX
; on remplace ce 3e DWORD par le contenu de EDX
La valeur de déplacement peut prendre toute valeur jusqu'à 0FFFFFFFFh. Elle peut être positive ou négative. Les éléments non numériques doivent être séparés par le signe plus.
Voir la section structures - différents types et utilisation.
IV-G-5. Lecture et écriture sur des labels utilisant l'indexation▲
Supposons que vous ayez 16 DWords de données déclarés comme suit :
PARAM_DATA DD
10h
DUP
0
Vous pouvez utiliser l'indexation (scaling) en complément du registre d'index comme suit :
MOV
ESI
, ADDR
PARAM_DATA
MOV
EAX
, [ESI
+
ECX
*
4
] ; EAX = lecture du dword pointé par ESI et indexé par ECX
MOV
[ESI
+
ECX
*
4
], EDX
; insertion de EDX en remplacement du dword lu précédemment
ou, ce qui revient au même :
MOV
EAX
, [PARAM_DATA+
ECX
*
4
] ; EAX = lecture du dword pointé par ESI et indexé par ECX
MOV
[PARAM_DATA+
ECX
*
4
], EDX
; insertion de EDX en remplacement du dword lu précédemment
Vous pouvez utiliser une indexation de 0, 2, 4 ou 8. Les instructions qui suivent sont toutes valides :
MOVZX
EAX
, B[PARAM_DATA+
ECX
] ; octet pointé par [PARAM_DATA+ECX] avec ext. à 0 sur EAX
MOVZX
EAX
, W[PARAM_DATA+
ECX
*
2
] ; mot pointé par [PARAM_DATA+ECX*2] avec ext. à 0 sur EAX
MOV
Q[PARAM_DATA+
ECX
*
8
], EDX
; insertion EDX dans Qword pointé par [PARAM_DATA+ECX*8]
Les lettres B, W et Q précédant le crochet ouvrant sont des abréviations spécifiques à GoAsm décrivant respectivement les modificateurs de type Byte Ptr, Word Ptr et Qword Ptr.
Les éléments non numériques doivent être séparés par le signe plus.
Dans le codage 32 bits, seuls les registres 32 bits à usage général peuvent être utilisés comme registre d'index - EAX, EBX, ECX, EDI, EDX, ESI ou EBP. Vous ne pouvez pas utiliser ESP en tant que registre d'index.
En codage 64 bits, vous pouvez utiliser les registres 32 bits à usage général ou les nouveaux registres en mode d'adressage 32 bits (R8D à R15D). Vous pouvez également utiliser les extensions 64 bits des registres à usage général - RAX, RBX, RCX, RDI, RDX, RSI, ou RBP -, et les nouveaux registres 64 bits R8 à R15. Vous ne pouvez pas utiliser RSP en tant que registre d'index.
Notez que les instructions ci-dessus qui utilisent PARAM_DATA et l'indexation n'utilisent pas l'adressage relatif RIP, de sorte que la base de l'image doit être bien en dessous 7FFFFFFFh.
IV-G-6. Lecture et écriture sur des labels utilisant indexation et déplacement▲
Supposons que vous ayez 24 DWords de données déclarés comme suit, où le dernier DWord dans chaque cas détient le résultat requis :
PARAM_DATA DD
19h
, 0
, 0
, 22222h
DD
1Ah
, 0
, 0
, 44444h
DD
1Bh
, 0
, 0
, 66666h
DD
1Ch
, 0
, 0
, 88888h
DD
1Dh
, 0
, 0
, 0AAAAAh
DD
1Eh
, 0
, 0
, 0CCCCCh
Alors, vous pouvez utiliser une indexation (scaling) et un déplacement de la manière suivante :
MOV
ESI
, ADDR
PARAM_DATA ; adresse de départ de la table de Dwords
CMP
EAX
, [ESI
+
ECX
*
4
] ; voir si EAX = dword pointé par cette table
JNZ
>
L2 ; non
MOV
EDX
, [ESI
+
ECX
*
4
+
0Ch
] ; oui, alors on récupère le résultat dans EDX
ou, plus directement, et ce qui revient au même :
CMP
EAX
, [PARAM_DATA+
ECX
*
4
] ; voir si EAX = dword pointé par cette table
JNZ
>
L2 ; non
MOV
EDX
, [PARAM_DATA+
ECX
*
4
+
0Ch
] ; oui, alors on récupère le résultat dans EDX
Vous devez utiliser l'indexation au moyen des seules valeurs 0, 2, 4 ou 8. La valeur de déplacement peut être toute valeur allant jusqu'à 0FFFFFFFFh. Dans votre script source, cette valeur peut être positive ou négative. Les éléments non numériques doivent être séparés par le signe plus.
Dans le codage 32 bits, seuls les registres 32 bits à usage général peuvent être utilisés comme registres d'index - EAX, EBX, ECX, EDI, EDX, ESI ou EBP. Vous ne pouvez pas utiliser ESP en tant que registre d'index.
En codage 64 bits, vous pouvez utiliser les registres 32 bits à usage général ou les nouveaux registres en mode d'adressage 32 bits (R8D à R15D). Vous pouvez également utiliser les extensions 64 bits des registres à usage général - RAX, RBX, RCX, RDI, RDX, RSI, ou RBP - ainsi que les nouveaux registres 64 bits R8 à R15. Vous ne pouvez pas utiliser RSP en tant que registre d'index.
Notez que les instructions ci-dessus qui utilisent PARAM_DATA et l'indexation n'utilisent pas l'adressage relatif RIP, de sorte que la base de l'image doit être bien en dessous 7FFFFFFFh.
IV-H. Appel (ou Saut) à des procédures▲
IV-H-1. Qu'est-ce qu'une « procédure » ?▲
Une procédure est une série d'instructions de code avec un label auquel l'exécution peut être transférée. Les procédures peuvent également être nommées « fonction », « routine » ou « sous-programme ». Voici un exemple de procédure courte :
PROCESS_HASH
:
; label permettant d'atteindre la procédure
XOR
EAX
, EAX
MOV
EDX
, ESI
CALL
PH23
MOV
EDX
, 866h
; retour de la procédure avec EDX = 866h
RET
IV-H-2. Transfert de l'exécution à une procédure▲
Habituellement, l'exécution est transférée à la procédure par l'utilisation de l'instruction CALL. Celle-ci impose tout d'abord au processeur de pousser sur la pile (PUSH) la position dans le code juste après l'instruction CALL, puis de poursuivre l'exécution dans la procédure appelée. À la fin de la procédure, on trouve une instruction RET (abrégé de RETURN) qui invite le processeur à retirer de la pile (POP) la position dans le code immédiatement après le CALL mémorisée précédemment, à placer cette valeur dans le pointeur d'instructions EIP, puis à reprendre l'exécution à partir de ce point.
Exceptionnellement l'exécution peut être transférée à la procédure par l'utilisation de l'instruction JMP. À la fin de la procédure, on peut également rencontrer une autre instruction JMP, comme dans l'exemple qui suit :
PROCESS_HASH
:
XOR
EAX
, EAX
MOV
EDX
, ESI
CALL
PH23 ; transfère l'exécution à la procédure PH23 et retourne après celle-ci
MOV
EDX
, 866h
; retour de la procédure avec EDX = 866h
JMP
>
SOMEWHERE_ELSE
START
:
; point d'entrée de l'exécution
JMP
PROCESS_HASH
IV-H-3. Syntaxe de CALL et JMP en direction d'une procédure▲
La manière habituelle de faire un CALL ou un JMP en direction d'une procédure est d'utiliser le label de code marquant le début de la procédure. Par exemple :
CALL
PROCESS_HASH
JMP
PROCESS_HASH
Parfois, l'adresse de la procédure destination peut être conservée en mémoire, pointée par un label ou un registre ou même localisée à un endroit connu dans la mémoire ainsi que l'illustrent les différents exemples qui suivent :
CALL
[PROCADDRESS]
CALL
[PROCTABLE+
20h
]
CALL
[ESI
]
CALL
[ESI
+
EDX
]
JMP
[4000000h
]
Il peut arriver enfin que l'adresse de la procédure destination soit détenue par un registre, auquel cas la syntaxe du CALL ou du JMP peut prendre la forme suivante :
CALL
EAX
JMP
EDI
IV-H-4. Syntaxes plus complexes du CALL et du JMP▲
Nous espérons que vous n'aurez jamais à utiliser l'une des formes qui suivent, mais GoAsm les autorise néanmoins (en utilisant soit CALL, soit JMP) :
#define Hello PROCESS_HASH
CALL
Hello ; traité comme un CALL à PROCESS_HASH
CALL
100h
; traité comme un CALL à une adresse relative
CALL
[HELLO3+
ECX
+
EDX
*
4
]
CALL
[HELLO3+
ECX
+
EDX
*
4
+
9000h
]
CALL
$$ ; CALL au départ du début de la section courante
CALL
$+
20h
; CALL 20h octets plus loin que la position courante
IV-H-5. CALL et JMP vers des procédures en dehors du fichier objet ou de la section▲
Certains assembleurs vous obligent à signaler dans votre script source au moyen de la directive EXTRN que la destination d'un appel est quelque part en dehors du fichier objet. Ils imposent également que la même destination soit marquée comme GLOBAL ou PUBLIC. GoAsm vous dispense de ces subtilités de syntaxe en considérant que, si la destination d'un l'appel se révèle introuvable lors de l'assemblage, elle est supposée relever d'un appel externe. Aussi, tous les labels qui ne sont pas locaux ou qui ont des noms réutilisables sont supposés être « globaux ». GoAsm fonctionne de la même manière lorsqu'un appel ou un saut doit être effectué à une section de code avec un autre nom.
Donc, si vous voulez appeler une procédure dans un autre script source (lequel produira un autre fichier objet), appelez-la simplement de la manière habituelle. De même, si vous avez une procédure dans un autre exécutable (généralement une DLL), vous pouvez procéder de même.
Par exemple, supposons que vous ayez écrit My.Dll incluant un algorithme de calcul que vous souhaitez utiliser avec le label CALCULATE. On pourrait l'appeler comme suit :
CALL
CALCULATE
Dans votre liste des DLL que vous donnerez à GoLink, vous mentionnerez My.Dll. GoLink cherchera d'abord le label de code CALCULATE dans les fichiers objet, mais regardera ensuite dans les DLL spécifiées. La plupart des autres linkers regardent dans les fichiers de bibliothèque (fichiers .lib) pour les fonctions qu'ils contiennent, ce qui signifie que vous avez à constituer un fichier lib. De toute façon, si l'on s'en tient à la syntaxe GoAsm, vous n'avez plus rien à faire dans votre script source. Si l'éditeur de liens ne trouve pas la destination de l'appel, une erreur sera affichée.
Cette forme de l'appel est un appel relatif utilisant l'opcode E8.
Vous pouvez également utiliser cette forme :
CALL
[CALCULATE]
Pour ce type d'appel, GoAsm utilise les opcodes FF15. Il s'agit d'un appel à une adresse absolue. Dans l'assembleur 32 bits, c'est un appel à une adresse de 32 bits, mais dans l'assembleur 64 bits, c'est un appel à une adresse de 64 bits.
Voir aussi :
IV-I. Appel des API Windows 32 et 64 bits▲
L'appel des API Windows (qui résident dans les DLL système de Windows) est très simple dans le cas où aucun paramètre n'est requis. Par exemple dans Windows 32 bits, vous pouvez écrire :
CALL
GetModuleHandle
ou sa variante plus évoluée, qui peut être utilisée soit pour Windows 32 bits ou 64 bits :
INVOKE
GetModuleHandle
Il n'y a rien d'autre à mettre dans le script source. Dans la mesure où la fonction appelée réside en dehors de l'exécutable que vous élaborez, il revient à l'éditeur de liens de trouver la DLL qui contient la procédure GetModuleHandle et il va y enregistrer le nom de la DLL à cet effet. GoLink effectuera les recherches nécessaires au moyen de la liste des DLL que vous fournissez.
La plupart des API Windows, cependant, attendent des paramètres (souvent désignés également comme « arguments ») lorsqu'elles sont appelées. Il incombe au programmeur de s'assurer que ces paramètres sont envoyés à l'API correctement. Ils contiennent les informations, ou des pointeurs vers des informations, qui indiquent à l'API ce qu'elle doit faire. Parfois, ils contiennent des adresses de zone mémoire où l'API doit insérer des informations.
La manière de communiquer les paramètres à l'API varie selon que vous assembliez en Windows 32 ou 64 bits. Chaque système d'exploitation utilise en effet des conventions d'appels spécifiques qui affectent la façon dont les paramètres sont envoyés et utilisés. Windows 32 bits utilise la convention d'appel standard (STDCALL) et Windows 64 bits utilise la convention d'appel qualifiée de « rapide » (FASTCALL).
GoAsm propose, dans ce but, les opérateurs ARG et INVOKE, qui peuvent être utilisés indifféremment sur les plateformes 32 ou 64 bits. L'assembleur génère le code approprié selon la convention d'appel utilisée. Si vous écrivez pour 32 bits avec aucune velléité de transposition en 64 bits, vous pouvez utiliser PUSH et CALL pour la transmission des paramètres, mais si vous voulez garantir la portabilité de votre code vers Windows 64 bits ultérieurement, vous devrez les remplacer par ARG et INVOKE. Dans les codes source 32 et 64 bits vous êtes libre d'utiliser CALL pour appeler des procédures dans vos propres exécutables, à moins que vous ne leur envoyiez les paramètres selon l'une des conventions d'appel suivantes.
- Dans la convention d'appel STDCALL utilisée dans Windows 32 bits, tous les paramètres sont mis sur la pile par l'appelant, et le pointeur de pile (ESP) est déplacé vers le haut des paramètres sur la pile. Ensuite, l'API est appelée. Celle-ci utilise les paramètres sur la pile et avant de revenir, restaure cette dernière à l'équilibre en déplaçant le pointeur de la pile à la position qu'il avait avant la mise sur la pile du premier paramètre.
- Dans la convention d'appel FASTCALL utilisée dans Windows 64 bits, les quatre premiers paramètres sont chargés successivement dans les registres RCX, RDX, R8 et R9 au lieu d'être mis sur la pile. Cependant, les éventuels paramètres suivants sont mis sur la pile. L'appelant doit faire en sorte que le pointeur de la pile (dans ce cas RSP) soit déplacé vers le haut des paramètres comme d'habitude, incluant notamment les quatre premiers paramètres qui sont détenus dans des registres (ce qui revient à autoriser l'API de les récupérer sur la pile comme s'ils avaient été mis là en premier lieu). Une autre différence est que l'API ne restaure pas la pile à l'équilibre avant de revenir de l'appel. Cette particularité facilite les choses pour les quelques API qui ne disposent pas d'un nombre fixe de paramètres.
Si vous désirez que le même script source puisse indifféremment être assemblé en 32 ou 64 bits, il est indispensable que vous envoyiez les paramètres à l'aide de ARG puis que vous appeliez l'API en utilisant INVOKE. Un exemple simple en montre le principe :
ARG 40h
, RDX, RAX, [hwnd]
INVOKE
MessageBoxA
Lors de l'assemblage 32 bits, ARG agit de manière identique à PUSH, et INVOKE produit le même effet que CALL. GoAsm accepte une instruction PUSH d'un registre à usage général 64 bits, et donc PUSH RDX est traité de la même manière que PUSH EDX au nombre de bits près, bien évidemment. Par conséquent, l'appel ci-dessus fonctionne sur les deux plateformes mais reçoit une traduction différenciée selon le cas :
; Plateforme 32 bits
PUSH
40h
PUSH
EDX
PUSH
EAX
PUSH
[hwnd]
CALL
MessageBoxA
; Plateforme 64 bits
MOV
R9, 40h
MOV
R8, RDX
MOV
RDX, RAX
MOV
RCX, [hwnd]
SUB
RSP, 20h
CALL
MessageBoxA
ADD
RSP, 20h
L'assemblage 64 bits produit, comme on peut le voir, un résultat radicalement différent à partir du même code.
Voir le chapitre programmation en 64 bits pour plus de détails.
IV-I-1. Appel des API Windows - Utilisation de INVOKE▲
Il est important évidemment d'envoyer les paramètres à l'API dans le bon ordre. INVOKE vous aide à le faire en vous permettant de mettre les paramètres après le nom de l'API comme en C. Cela est appréciable aussi lorsque vous travaillez avec la documentation de Windows qui décrit toujours les paramètres des API en utilisant la syntaxe du C. Par exemple, voici comment l'API MessageBox y est décrite :
int
MessageBox
(
HWND hwnd, // handle of owner window
LPCTSTR lpText, // address of text in message box
LPCTSTR lpCaption, // address of title of message box
UINT uType // style of message box
);
Avec INVOKE, vous pouvez respecter le même ordre, par exemple :
INVOKE
MessageBoxA, [hwnd], EAX
, EDX
, 40h
qui équivaut à :
ARG 40h
, RDX, RAX, [hwnd]
INVOKE
MessageBoxA
Notez que ARG (comme PUSH) lit les paramètres d'une façon, tandis que les paramètres après INVOKE sont lus dans l'autre sens.
INVOKE et ses nombreux paramètres peuvent être écrits sur plusieurs lignes en utilisant le caractère de continuation :
INVOKE
CreateWindowExA, WS_EX_OVERLAPPEDWINDOW, ADDR
szClassName, \
ADDR
szWindowName,\
WS_OVERLAPPEDWINDOW+
THING,\
100
, 16
, 400
, 0
, 0
, 0
, [hInstance], 0
Puisque GoAsm considère les paramètres de INVOKE à partir de la fin, les erreurs vers la fin seront trouvées en premier.
Lors de l'utilisation de INVOKE, si vous souhaitez ranger vos paramètres dans un mot défini, alors GoAsm les récupèrera toujours dans le bon ordre. Par exemple :
z_function_params=
3
,2
,1
INVOKE
z_function, z_function_params
produit le même code que :
ARG 1
,2
,3
INVOKE
z_function
IV-I-2. Appel des API Windows - Versions ANSI et Unicode▲
Les API Windows qui gèrent des entrées ou sorties de caractères (généralement sous forme de chaînes de caractères) ont généralement deux versions différentes : une version ANSI et une version Unicode. La version ANSI gère les chaînes en ANSI, standard selon lequel un seul octet de valeur 0 à 255 représente un caractère unique basé sur le jeu de caractères courant. Ces caractères sont parfois appelés caractères « multioctets ». De manière on ne peut plus simple, le nom de la version ANSI de l'API se termine par un « A » comme dans l'exemple de l'API CreateWindowEx utilisée ci-dessus. La version Unicode gère les chaînes en format Unicode et utilise à cet effet deux octets par caractère sur la base de la table standard des caractères Unicode. Ceux-ci sont parfois qualifiés de caractères « larges » (wide). La version Unicode de l'API se terminera logiquement par un « W » à la fin de son nom.
Dans votre script source, vous devez spécifier les API que vous souhaitez appeler en ajoutant un « A » ou un « W » à la fin du nom de l'API, selon le cas. Lorsque vous soumettez votre fichier objet au linker et que ce dernier aura été incapable de trouver l'API dans un autre exécutable (ou dans les fichiers .lib si vous n'utilisez pas GoLink), c'est probablement parce que vous aurez oublié d'ajouter le « A » ou le « W ». Vous pourriez également avoir omis de fournir à GoLink le nom de la DLL détenant l'API (ou les fichiers .lib appropriés si vous n'utilisez pas GoLink).
Si vous voulez automatiser le bon appel d'API « A » ou « W », les règles suivantes doivent être observées :
- le programme est en version ANSI sauf spécification contraire ;
- la version Unicode peut être obtenue, soit en mettant le commutateur /d sur la ligne de commande de GoAsm, soit en écrivant la ligne #define UNICODE au début du script source.
Voir le chapitre relatif à l'écriture de programmes Unicode dans le volume 2 pour de plus amples informations sur le sujet.
Il n'y a aucune différence entre l'assemblage 32 et 64 bits à cet égard pour la simple raison que Windows 64 bits a des versions ANSI et Unicode des API tout comme Windows 32 bits.
IV-J. Les pointeurs de chaînes et de données avec PUSH et ARG▲
IV-J-1. Pointeurs sur des chaînes terminées par un zéro▲
GoAsm permet une extension de PUSH ou ARG qui est très utile en programmation sous Windows. Souvent, dans ce cas, vous êtes dans la situation d'envoyer à une API un paramètre qui est un pointeur vers une chaîne se terminant par zéro. Supposons, par exemple, l'appel d'API suivant (en 32 bits) :
MBTITLE DB
'Hello'
, 0
MBMESSAGE DB
'Click OK'
, 0
PUSH
40h
, ADDR
MBTITLE, ADDR
MBMESSAGE, [hwnd]
CALL
MessageBoxA
Pour simplifier cette syntaxe, GoAsm permet d'utiliser PUSH ou ARG comme suit tout en garantissant un résultat identique :
PUSH
40h
, 'Hello'
, 'Click OK'
, [hwnd]
CALL
MessageBoxA
ou, si vous écrivez un script source permettant la compatibilité entre les plateformes 32 et 64 bits :
ARG 40h
, 'Hello'
, 'Click OK'
, [hwnd]
INVOKE
MessageBoxA
et, si vous préférez envoyer les paramètres par INVOKE :
INVOKE
MessageBoxA, [hwnd], 'Click OK'
, 'Hello'
, 40h
Vous pouvez enfin utiliser cette même facilité avec des chaînes Unicode comme suit :
ARG 40h
, L'Hello'
, L'Click OK'
, [hwnd]
INVOKE
MessageBoxW
ou
INVOKE
MessageBoxW, [hwnd], L'Click OK'
, L'Hello'
, 40h
Lorsque vous utilisez l'une de ces formes, la chaîne sera toujours terminée par zéro. Il se trouve en effet que GoAsm place la chaîne dans la section const s'il y en a une (la section data s'il y en a une, sinon, à défaut, dans la section de code) et y ajoute un terminateur null. Puis, GoAsm crée l'instruction correcte et lui donne un pointeur vers la chaîne. Aucun symbole n'est produit à des fins de débogage.
En assemblage 64 bits, GoAsm garantit que les chaînes Unicode sont alignées sur la limite de mot requise par le système.
Notez que ceci est similaire à PUSH ADDR et fera usage du registre R11 tout en tirant avantage de la taille réduite de l'adressage relatif RIP de l'instruction LEA.
IV-J-2. Mise en pile de pointeurs dans des données brutes▲
Vous pouvez faire la même chose que précédemment avec les données brutes ordinaires (en octets) en utilisant les opérateurs < et >. Par exemple :
PUSH
<
23
, 24
, 25
>
; mise en pile d'un pointeur des octets 23,24,25
ou
PUSH
<
23
, 6
DUP
20h
, 23
>
; mise en pile d'un pointeur des octets 23,6 espaces, puis 23
ou
PUSH
<
'Hi'
, 0Dh
, 0Ah
, 'There'
, 0
>
; mise en pile d'un pointeur sur la chaîne terminée
; par un zéro sur 2 lignes (RC+LF au milieu)
On peut également utiliser les opérateurs < et > de cette manière avec ARG et après INVOKE. Dans ce cas de figure, GoAsm place la déclaration des données entre les opérateurs < et > dans la section const s'il y en a une (ou la section data s'il y en a une, sinon à défaut, dans la section de code). Puis GoAsm crée l'instruction appropriée et calcule un pointeur vers les données. Aucun symbole n'est constitué à des fins de débogage.
Notez que lorsque vous utilisez les opérateurs < et > de cette manière, aucun terminateur Null n'est ajouté aux chaînes.
Lors de l'assemblage 64 bits, GoAsm garantit que les données sont alignées sur une limite de mot rendue nécessaire par le système si les données contiennent des chaînes Unicode.
IV-K. Mémorisation de pointeurs de chaîne et données dans des registres▲
Vous pouvez également déclarer des chaînes terminées par un zéro et des données tout en établissant simultanément leurs pointeurs dans des registres à l'aide de la syntaxe suivante :
MOV
EAX
, ADDR
'This is a string'
MOV
EAX
, ADDR
<
'String'
,0Dh
,0Ah
>
Lorsque GoAsm traite ce type de code, il constitue une chaîne terminée par un zéro - sans que ce dernier ne soit spécifié - ou les données entre les opérateurs < et > dans la section const. s'il y en a une (ou la section data si elle existe, sinon, dans la section code). Puis, GoAsm fournit le pointeur des données ainsi créées à l'instruction. Aucun symbole n'est produit à des fins de débogage.
Notez que lorsque vous utilisez les opérateurs < et >, aucun terminateur Null n'est rajouté aux chaînes.
Notez également comment ceci diffère de la syntaxe relative au chargement de caractères immédiats dans un registre. Cette différence réside dans l'utilisation de l'opérateur ADDR.
Tout ceci fonctionne de la même façon en programmation 64 bits, sauf que GoAsm garantit qu'une chaîne ou une donnée Unicode sont alignées WORD dans la mémoire ainsi que le requiert le système.
Notez que ceci est similaire à PUSH ADDR et fera usage du registre R11 tout en tirant avantage de la taille réduite de l'adressage relatif RIP de l'instruction LEA.
IV-L. Utilisation de caractères immédiats dans le code▲
GoAsm ne renverse pas l'ordre des Words et DWords mémorisés sous forme de caractères immédiats comme MASM le pratique. Ainsi, GoAsm utilise-t-il le format suivant :
MOV
AL
, '1'
MOV
AX
, '12'
; considéré comme des octets - 1 en AL, 2 en AH
MOV
EAX
, 'ABCD'
; considéré comme des octets - A en premier, puis B puis C puis D (AL)
Cela facilite l'ajout des chaînes courtes à la mémoire. Par exemple, pour ajouter l'extension .fil à un nom de fichier stocké en mémoire, vous pouvez coder :
MOV
[EDI
], '.fil'
;ou
MOV
EAX
, '.fil'
MOV
[EDI
], EAX
et non pas :
MOV
[EDI
], 'lif.'
;ou
MOV
EAX
, 'lif.'
MOV
[EDI
], EAX
L'instruction CMP (comparaison) travaille de la même manière :
CMP
AL
, '1'
CMP
EAX
, 'ABCD'
CMP
[EDI
], '.fil'
Cela ne change pas l'ordre inverse habituel dans les opérandes qui ne seraient pas entre guillemets. Ainsi lorsque vous souhaitez ajouter un retour chariot, puis un saut de ligne au texte que vous souhaitez réutiliser :
MOV
AX
, 0A0Dh
STOSW
Ici, le retour chariot (0Dh) qui est en AL, est chargé le premier en mémoire, puis le saut de ligne (0Ah) dans AH est chargé en mémoire d'adresse immédiatement supérieure.
Si la chaîne est plus courte que le type du registre ou de la mémoire, des zéros absolus de comblement sont ajoutés automatiquement :
MOV
EAX
, 'ABC'
; code A, puis B, puis C, puis zéro
Lors de l'écriture du code source pour les programmes Unicode vous pouvez garantir que les caractères immédiats sont Unicode ou, si nécessaire, basculés d'ANSI en Unicode. Voir, à ce sujet, les sections « Utilisation de la chaîne correcte dans les valeurs immédiates sous guillemets » et « commutation des chaînes sous guillemets et immédiates » dans le chapitre du volume 2 consacré à l'Unicode.
En programmation 64 bits, vous pouvez utiliser les registres 64 bits pour y stocker des chaînes immédiates de 8 caractères de long, par exemple :
MOV
RAX, 'Saturday'
Cependant, l'instruction CMP n'admet pas les valeurs immédiates supérieures à 32 bits, de sorte que par exemple
CMP
RAX, 'Saturday'
affiche une erreur. En mode 64 bits, seules sont autorisées les comparaisons entre registre et mémoire et entre deux registres sur cette variante de l'instruction CMP.
IV-M. Indicateurs de Type▲
IV-M-1. Intérêt et syntaxe▲
Penchons-nous sur l'instruction
MOV
[ESI
], 20h
Elle consiste à stocker le nombre 20h à l'adresse mémoire spécifiée par le contenu du registre ESI. Mais une question importante apparaît immédiatement. Le nombre doit-il être chargé comme un octet, un mot, un DWord ou tout autre type de donnée ? De la réponse dépend le nombre d'octets de mémoire - à partir de l'adresse fournie par ESI - qui doivent être modifiés. Tous les assembleurs exigent donc à cet effet un indicateur de type pour lever l'ambiguïté mais avec une syntaxe sensiblement différente ainsi que le montrent les exemples suivants (en utilisant DWORD comme exemple) :
MOV
DWORD
PTR
[ESI
], 20h
; MASM
MOV
DWORD
[ESI
], 20h
; NASM
MOV
D[ESI
], 20h
; A386
GoAsm utilise la syntaxe A386 qui nécessite beaucoup moins de frappe de sorte que les indicateurs de type rencontrés sont :
- B signifie Byte (1 octet) ;
- W signifie Word (2 octets) ;
- D signifie DWord (4 octets) ;
- Q signifie QWord (8 octets) ;
- T signifie TWord (10 octets).
Vous pouvez également utiliser ces deux indicateurs de type commutable :
- S est un indicateur dont la taille varie selon les caractères utilisés : 1 (octet) en ANSI et 2 (octets) en Unicode.
MOV
S[EDI
], 0
; insère un simple zéro si ANSI, un double zéro si Unicode.
Voir la section « Utilisation de l'indicateur de type commutable pour Unicode/ANSI » dans le chapitre du volume 2 consacré à l'Unicode.
- P est un indicateur dont la taille varie selon le format 32/64 bits (par défaut, 4 pour 32 bits, 8 pour 64 bits) :
MOV
P[RDI], 0
; 0 sur qword à RDI si 64 bits, 0 sur dword à EDI si 32 bits
Voir la section utilisation de l'indicateur de type commutable pour 32/64 bits.
IV-M-2. L'indicateur de type est également requis pour les références mémoire nominatives▲
Comme NASM, GoAsm ne vérifie pas les types, de sorte qu'il ne peut connaître la taille d'une opération telle que celle-ci :
INC
[COUNT]
Ici, GoAsm ignore tout de la taille de COUNT qui peut être indifféremment un octet, un mot (Word), un double-mot (DWord) ou un quadruple mot (QWord) et ceci, bien que la variable ait été préalablement déclarée. Par conséquent, vous devez impérativement préciser le type de l'opération, par exemple :
INC
B[COUNT]
Bien que cette obligation impose un peu plus de travail au programmeur, force est de reconnaître que le script source s'en trouve plus facile à lire et à comprendre, puisque vous pouvez immédiatement voir la taille de l'opération de l'instruction, plutôt que de devoir explorer l'ensemble du programme pour vous enquérir de la taille avec laquelle COUNT a été déclarée. Du reste, l'utilisation d'indications de type tenant en une seule lettre compense le côté fastidieux induit par la répétition de ces indications.
IV-M-3. Quelles instructions requièrent un indicateur de type ?▲
Généralement toutes les instructions où la taille de l'opération n'est pas implicite sont concernées. Certains des exemples qui suivent utilisent des références de mémoire nominatives alors que d'autres préfèrent des références de mémoire soutenues par des registres :
AND
B[MAINFLAG], 0FEh
ADC
W[EAX
], 66h
ADD
D[MEM_AREA], 66h
BT
D[EBX
], 31D
CMP
D[HELLOWORD], 0Dh
DEC
D[ECX
]
DIV
B[HELLO]
INC
D[EDX
]
MOV
B[MEM_AREA], 23h
MOVSX
EDX
, B[EDI
]
MUL
B[HELLO]
NEG
W[ESI
]
NOT
D[HELLO3]
OR
B[MAINFLAG], 1h
SETZ
B[BYTETEST]
SHL
W[IAMAWORD], 23h
SHL
D[IAMADWORD], CL
SUB
D[EBP
+
10h
], 20D
TEST
B[ESP
+
4h
], 1h
XOR
D[IMAWORD], 11111111h
Et, en programmation 64 bits, vous pouvez également voir, par exemple :
ADC
W[RAX], 66h
BT
D[R12], 31D
INC
Q[RDX]
NEG
W[R15D]
IV-M-4. Les instructions qui ne requièrent pas d'indicateur de type▲
Entrent dans cette catégorie les opérations dont la taille de l'opération est évidente, du fait de l'utilisation d'un registre, par exemple :
AND
[MAINFLAG], CL
CMP
[HELLOWORD], EDI
MOV
[IAMABYTE], AL
MOV
[IAMADWORD], ESI
OR
[MAINFLAG], BH
XCHG
CL
, [ESI
]
De fait, aucune des instructions MMX, XMM ou 3DNow! ne nécessite un indicateur de type. Il en va de même pour plusieurs des instructions en virgule flottante x87. Les autres peuvent prendre plus d'une taille d'opérande. Il y a aussi plusieurs instructions qui n'acceptent qu'une taille d'opérande, de sorte qu'avec celles-ci, l'indicateur de type n'est pas requis. Il en va Ainsi, des instructions CALL, JMP, PUSH et POP qui ne supportent qu'un DWord (en 32 bits). Voir cependant la section relative aux demi-opérations de pile concernant l'utilisation de PUSHW et POPW. Enfin, certaines des instructions les moins courantes n'ont pas besoin d'un indicateur de type, par exemple ARPL, BOUND, BSF, BSR, CMOV (sous toutes ses formes), CMPXCHG et CMPXCHG8B.
IV-N. Instructions répétées▲
Les instructions répétées sont possibles pour PUSH, POP, INC, DEC, et bien sûr lors de la déclaration des données, par exemple :
PUSH
0
, 23h
, [hwnd], ADDR
lParam, EAX
POP
EAX
, [EBP
+
2Ch
], [hwnd]
DEC
ECX
, EDX
, [COUNT]
INC
[EBP
+
10h
], EDI
DB
23h
, 24h
, 25h
Les instructions ici sont toujours assemblées de gauche à droite.
IV-O. Nombres et arithmétique▲
IV-O-1. Nombres▲
La plupart des assembleurs utilisent la syntaxe suivante pour les nombres :
66ABCDEh
; nombre hexadécimal
34567789
; nombre décimal
1100011B
; nombre binaire
1.0 ; nombre réel
1.0E0 ; nombre réel (autre forme)
GoAsm accepte ces nombres, mais prend également en charge les formats suivants :
9999999D
; nombre décimal
0x456789
; nombre hexadécimal
Un nombre hexadécimal qui commence par une lettre (A à F caractérisant, par convention, les valeurs décimales de 10 à 15) doit impérativement être précédé d'un zéro, par exemple :
0A789ABCDh
ou
0xA789ABCD
IV-O-2. Arithmétique ▲
GoAsm peut effectuer des opérations arithmétiques limitées à l'intérieur des déclarations de données, du quantitatif de DUP, des définitions, lors de la déclaration de définitions, lors de l'utilisation des définitions, et dans des opérandes des instructions de code. Vous n'êtes pas autorisé à utiliser le signe de multiplication (astérisque) à l'intérieur de crochets autrement que lors de l'utilisation d'un registre d'index.
Soyez prudent en utilisant les opérateurs logiques OR, AND et NOT puisque ceux-ci sont à la fois des mnémoniques processeur et des directives de l'assembleur. Bien que GoAsm sache les distinguer dans leurs deux acceptions, il vous est possible de recourir à une écriture distinctive en utilisant, pour les directives, les symboles | pour OR, & pour AND, et ! pour NOT.
Les opérations arithmétiques entre parenthèses sont effectuées en premier lieu, sinon les calculs sont effectués strictement de gauche à droite. Voici quelques exemples :
DB
2
*
3
DB
(2
+
30h
)/
(2
+
1
)
DD
(2000h
+
40h
-
20h
)/
2
DD
SIZEOF
HELLO/
2
DD
444444h
&
226222h
DB
20h
/
2
DUP
44h
DB
6
+
2
DUP
0
#define globule (2
*
3
)/
2
DB
globule
DD
globule|
100h
DD
2D00h
>>
8
DQ
2D00h
<<
48
MOV
EAX
, globule|
100h
MOV
EAX
, SIZEOF
HELLO*
2
MOV
EAX
, ADDR
HELLO+
10h
MOV
EAX
, 0x68
+
0x69
-
0x70
MOV
EAX
, [MemName+
0x68
+
0x69
-
0x70
]
MOV
EAX
, [ESI
*
4
+
45000h
]
MOV
EAX
, [ESI
*
4
+
SIZEOF
HELLO/
2
]
MOV
EAX
, 8
+
8
*
2
; résultat = 32
MOV
EAX
, 8
+
(8
*
2
) ; résultat = 24
Le quotient des divisions, lorsque cette opération est utilisée, est arrondi selon la règle de l'entier le plus proche ainsi que le montrent les exemples suivants :
MOV
EAX
, 32
/
3
; met 11 dans EAX
MOV
EAX
, 31
/
3
; met 10 dans EAX
MOV
EAX
, 10
/
4
; met 3 dans EAX
GoAsm considère que les multiplications et divisions effectuées dans ce cadre le sont en utilisant des nombres non signés. MUL et DIV sont utilisées lors de la compilation et non leurs homologues signés IMUL et IDIV.
IV-O-3. Déclaration des nombres réels▲
Les nombres réels sont des nombres qui ont la capacité de représenter une valeur inférieure à 1. GoAsm attend des nombres réels présents dans le script source qu'ils prennent la forme d'un nombre à virgule flottante, techniquement composé de plusieurs chiffres et d'un point parmi ceux-ci conformément à la notation anglo-saxonne qui prévaut ici. Le point doit être représenté par le caractère correspondant au code ASCII 2Eh et peut se situer n'importe où pourvu qu'il soit unique et positionné entre deux chiffres consécutifs. Le nombre réel peut, si nécessaire, être assorti d'un exposant décimal signé à la fin du nombre (en utilisant l'indicateur d'exposant « e » ou « E » conformément à la norme IEEE relative aux nombres en virgule flottante). Les registres x87 en virgule flottante du processeur peuvent accepter des nombres réels dans les résolutions 32, 64 ou 80 bits. Le 3DNow! et les instructions SSE travaillent avec des nombres réels 32 bits et les instructions SSE2 utilisent des nombres réels 64 bits.
Parfois, ces types sont nommés :
- 32 bits simple-précision ;
- 64 bits double-précision ;
- 80 bits double-précision étendue.
Donc, les nombres réels peuvent être déclarés comme DWords (32 bits), QWords (64 bits) ou TWords (80 bits). Voici quelques exemples de déclarations de données en nombres réels :
DD
1.6789E3
DQ
1.6789E3
DT
1.6789E3
DD
3
DUP
7.6789E-
2
DQ
678.27896435E3
DT
1.2
Vous pouvez également déclarer directement le nombre PI soit en tant que TWord, QWord ou DWord comme suit :
DD
PI ; pi comme un dword
DQ
PI ; pi comme un qword
DT
PI ; pi comme un tword
GoAsm tente d'obtenir une précision maximale dans la fourniture de PI en écrivant une valeur connue directement dans la mantisse.
Vous pouvez également déclarer un nombre réel comme suit :
PUSH
1.1
MOV
EAX
, 1.1
Ces deux formes utilisent un format 32 bits pour le nombre réel. La première place ce nombre sur la pile et la seconde le place dans le registre spécifié.
IV-O-4. Précision de conversion de GoAsm▲
GoAsm utilise des algorithmes spéciaux pour garantir une précision optimale lors du chargement de la déclaration de nombre réel en tant que donnée. Dans le cas de la déclaration d'un TWord (80 bits) le calcul est effectué si nécessaire sur un maximum de 92 bits, avant d'être arrondi puis inséré dans la mantisse 64 bits. La conversion en un QWord (64 bits) est effectuée en utilisant la précision maximale disponible (53 bits de mantisse) avec un arrondi à la valeur la plus proche. La conversion en un DWord (32 bits) est effectuée en utilisant la précision maximale disponible (24 bits de mantisse) avec un arrondi à la valeur la plus proche.
IV-O-5. Chargement direct de l'exposant et de la mantisse▲
Au lieu d'utiliser des nombres réels pour charger les registres en virgule flottante, vous pouvez déclarer un TWord et charger la mantisse et l'exposant directement en utilisant l'instruction FLD. Pour ce faire, vous aurez besoin de connaître les valeurs de mantisse et d'exposant à charger (ceux-ci peuvent être soit calculés, soit trouvés et vérifiés en utilisant l'une des fonctions de simulation FPU de GoBug). Supposons, par exemple, que vous vouliez une représentation du nombre π aussi précise que possible et vous savez, par ailleurs, que cette représentation consiste en un exposant de +0002 et une mantisse de +C90FDAA22168C235h. Vous pouvez donc déclarer ce nombre par :
DIRECT_PI DT
4000C90FDAA22168C235h
et le charger en utilisant :
FLD T[DIRECT_PI]
Considérons maintenant la figure ci-après qui montre la structure d'un registre de données x87. Le bit le plus significatif - bit 79 - dans la déclaration de tword qui précède est le bit de signe indiquant que le nombre réel est positif ou négatif. Ici, le nombre est positif puisque ce bit est à zéro. Le reste des quatre premiers chiffres hexadécimaux contient l'exposant qui est donc représenté sur 15 bits. Pour mieux faire tenir les valeurs d'exposant positives et négatives sur ce format, la valeur nulle est fixée à 3FFEh, ce qui autorise dès lors les exposants à évoluer entre -3FEEh et +4001h sans risquer d'atteindre le bit le plus significatif - le bit 79 évoqué plus haut. Le reste des chiffres hexadécimaux - 0 à 62 inclus - contient la mantisse.
Il est beaucoup plus difficile de charger la mantisse et l'exposant directement à partir de données déclarées en DWord et QWord. Ceci, parce que la répartition entre mantisse et exposant dans ce format numérique s'affranchit partiellement de la modularité 4 bits, base de la notation hexadécimale. Il résulte de cette structure particulière une certaine difficulté à chiffrer les nombres hexadécimaux à déclarer, outre le fait que l'exposant, pour ne rien simplifier, subit un codage qui accentue cette opacité.
IV-P. Caractères dans GoAsm▲
IV-P-1. Chaînes de caractères▲
Dans votre script source, vous serez souvent amené à représenter des caractères sous différentes formes. Par exemple :
Mess DB
'I am a string of characters'
,0
PUSH
'This is supposed to be a carat ^'
MOV
EAX
, '£$|@'
Il faut se demander quelles valeurs réelles sont chargées par GoAsm à l'issue du traitement de ces instructions. Au moment de l'assemblage GoAsm consulte votre script de source en utilisant les tables de caractères Windows, puis le lit caractère par caractère. En d'autres termes, GoAsm se voit attribuer la valeur des caractères dans le script source par Windows. Lorsque GoAsm charge dans le fichier objet les chaînes de caractères décrites ci-dessus, il charge la même valeur de caractère qui lui est donnée par Windows. Dans le cas de conversion de chaînes ANSI en chaînes Unicode, ces dernières sont passés d'abord par l'API MultiByteToWideChar. Cela signifie que la valeur donnée à GoAsm par Windows correspondra au jeu de caractères courant (page de code). En conséquence, vous devez vous assurer que le jeu de caractères utilisé dans l'ordinateur qui supporte GoAsm est le jeu de caractères pour lequel votre programme est conçu pour fonctionner.
Si vous utilisez un script source qui est dans un format Unicode (UTF-8 ou UTF-16), alors la question de la page de code disparaît. Les caractères corrects sont donnés par leur valeur Unicode.
IV-P-2. Caractères spécifiés directement▲
Parfois, vous aurez à spécifier des caractères par leur valeur réelle dans la table courante pour pouvoir faire face à d'éventuelles variations de jeux de caractères. C'est par exemple le cas pour le symbole représentant l'opérateur logique OR par une barre verticale, lequel peut prendre deux valeurs de code ASCII :
CMP
AL
, 124D
; est-ce que ce code est un OR dans ce jeu de caractères ?
JZ
>
L4 ; oui
CMP
AL
, 221D
; est-ce que ce code est un OR dans cet autre jeu de caractères ?
JZ
>
L4 ; oui
Cette formulation vous permet d'autoriser une possible variation du propre jeu de caractères de l'utilisateur. Si nécessaire, vous pouvez organiser votre code de telle sorte qu'il teste le jeu de caractères de l'utilisateur au moment de l'exécution, et qu'il teste les caractères ou utilise les chaînes corrigées en conséquence. Vous pouvez également tester la langue de la machine de l'utilisateur et fournir des chaînes dans la langue correcte. Les API de ressources offrent une solution à cet égard et cela peut être fait automatiquement - voir le manuel du compilateur de ressources GoRC dans le volume 2.
IV-Q. Opérateurs▲
Voici certains opérateurs qui peuvent être utilisés dans le script source et qui ont une signification particulière pour GoAsm.
, | L'instruction n'est pas terminée, continuer. |
|
Ligne de commentaire - ignorer tout jusqu'à la fin de la ligne. |
|
Commentaire continu - ignore ce qui est entre les astérisques. |
|
L'instruction ou la donnée se poursuivent sur la ligne suivante. |
|
Le nombre est négatif. |
|
Inverse le nombre (comme NOT). |
|
Inverse le nombre. |
|
Idem. |
|
Signe plus. |
|
Signe moins. |
|
Symbole de multiplication. |
|
Symbole de division. |
|
OR bit à bit. |
|
OR bit à bit. |
|
AND bit à bit. |
|
AND bit à bit. |
|
Décalage à gauche d'un bit sur le nombre spécifié. |
|
Décalage à droite d'un bit sur le nombre spécifié. |
|
Exécute le calcul prioritairement entre les parenthèses. |
## dans une définition a une signification spéciale. Voir la section utilisation du double-dièse dans les définitions.
V. Fonctionnalités avancées▲
V-A. Structures - différents types et utilisation▲
V-A-1. Qu'est-ce qu'une « structure » ?▲
Les structures sont des zones de données de taille fixe qui contiennent des données réparties dans divers composants (éléments de structure). Elles peuvent aller de dispositions très souples à d'autres, très formalisées, avec même des structures au sein d'autres structures (structures imbriquées). Elles peuvent être des zones de données résultant d'une déclaration de données ordinaire ou de modèles STRUCT. Les structures sont très importantes en programmation Windows et GoAsm en supporte tous les types.
Voir aussi le paragraphe suivant concernant les unions.
V-A-2. Utilisation de structures simples en programmation Windows▲
Considérons la structure LV_COLUMN qui est utilisée pour organiser les colonnes dans un contrôle ListView. Le code suivant envoie le message LVM_INSERTCOLUMN (valeur 101Bh) au contrôle ListView pour constituer une nouvelle colonne avec le numéro d'index correspondant dans EAX. Les détails des colonnes sont contenus dans la structure LV_COLUMN. Voici comment ils pourraient être utilisés en code 32 bits :
PUSH
ADDR
LV_COLUMN, EAX
, 101Bh
, hListView
CALL
SendMessageA ; insère la colonne indexée par EAX
Regardons maintenant de plus près la structure LV_COLUMN. Dans le fichier d'en-tête de Windows Commctrl.h (version pre- Win_IE 300) qui contient des informations sur la structure, elle est décrite comme une structure de six DWords. En un sens, donc, la structure peut être considérée comme une succession de six DWords qui peuvent être déclarés très simplement comme suit :
LV_COLUMN DD
6
DUP
0
Cependant, dans l'information de Windows, il apparaît que chacun des six doubles-mots a un nom qui donne une idée de ce pour quoi il est utilisé, ce qui est utile. De plus, le premier DWord s'avère être un masque qui identifie lesquels des éléments suivants de la structure sont valides. Ce masque est important car une version ultérieure de la structure révèle, par exemple, la présence de deux autres membres, ce qui impose de facto un masque différent. Donc, il pourrait être préférable de déclarer une structure de données de ce genre de telle sorte que le masque puisse être initialisé avec une valeur, et que vous puissiez voir les noms dans votre script source :
LV_COLUMN
DD
0Fh
; +0h mask
DD
2h
; +4h fmt=LVCFMT_CENTER=2
DD
0
; +8h cx
DD
0
; +0Ch pszText
DD
0
; +10h cchTextMax
DD
0
; +14h iSubItem
Ici, remarquons que, tandis que nous déclarions la structure de données, nous en avons profité pour initialiser deux des membres avec des valeurs qui ne changeront pas et que nous avons inclus dans les commentaires l'offset des différents membres, leurs noms ainsi que d'autres informations.
V-A-3. Lecture et écriture dans une structure simple▲
Il est très facile de lire et d'écrire sur la structure simple décrite précédemment :
MOV
EDI
, ADDR
LV_COLUMN
MOV
ESI
, ADDR
ColumnText ; récupère en ESI la colonne de texte à utiliser
MOV
[EDI
+
0Ch
], ESI
; et la communique au membre approprié de la structure
MOV
D[EDI
+
8h
], 50D
; et établit la largeur à 50 pixels
ou, vous pouvez utiliser :
MOV
ESI
, ADDR
ColumnText ; récupère en ESI la colonne de texte à utiliser
MOV
[LV_COLUMN+
0Ch
], ESI
; et la communique au membre approprié de la structure
MOV
D[LV_COLUMN+
8h
], 50D
; et établit la largeur à 50 pixels
V-A-4. Structures plus formelles utilisant STRUCT▲
Certains programmeurs préfèrent être plus formels dans l'utilisation des structures en utilisant notamment un modèle de structure. Cela se fait en deux étapes. La première consiste à élaborer un modèle en utilisant STRUCT et en lui donnant un nom. À ce stade, les données ne sont pas encore déclarées.
Voici un exemple de modèle de structure portant le nom LV_COLUMN :
LV_COLUMN STRUCT
mask DD
0Fh
; mask
fmt DD
2h
; LVCFMT_CENTER=2
cx
DD
0
pszText DD
0
cchTextMax DD
0
iSubItem DD
0
ENDS
Deux commentaires ont été ajoutés ici pour faciliter la compréhension de l'initialisation de deux membres de la structure. Noter que ENDS (littéralement END STRUCT) marque la fin du modèle. Vous pouvez également marquer cette fin en lui donnant le nom de la structure suivi de ENDS comme ci-dessous :
LV_COLUMN ENDS
La seconde étape consiste ensuite à utiliser le modèle. Vous y parvenez en le nommant et en faisant précéder ce nom d'un label, par exemple :
Lv1 LV_COLUMN
À ce stade, vous venez de déclarer six doubles-mots en utilisant le modèle de la structure LV_COLUMN et vous avez donné, à la déclaration de la structure, le label Lv1.
V-A-5. Les symboles créés par les structures formelles▲
Dans GoAsm, des symboles sont créés pour le label de la structure elle-même et également pour chaque membre nommé de la structure. Ceux-ci peuvent ensuite être référencés directement et transmis aussi comme tels au débogueur.
Ainsi :
RECT STRUCT
left DD
top DD
right DD
bottom DD
ENDS
rc RECT
crée les symboles :
- rc
- rc.left
- rc.top
- rc.right
- rc.bottom
V-A-6. Lecture et écriture sur la structure formelle▲
L'utilisation de la structure formelle vous permet d'être plus explicite dans votre script source lors de la lecture et l'écriture dans la structure. Par exemple :
MOV
ESI
, ADDR
ColumnText ; récupère en ESI la colonne de texte à utiliser,
MOV
[Lv1.pszText], ESI
; ... la communique au membre approprié de la structure
MOV
D[Lv1.cx], 50D
; ... et établit la largeur à 50 pixels
ou même
MOV
ESI
, ADDR
ColumnText ; récupère en ESI la colonne de texte à utiliser
MOV
EDX
, ADDR
Lv1.pszText ; récupère en EDX l'adresse du membre psztext
MOV
[EDX
], ESI
; et charge le texte à utiliser
MOV
EDX
, ADDR
Lv1.cx ; récupère en EDX l'adresse du membre cx
MOV
D[EDX
], 50D
; et établit la largeur à 50 pixels
Mais, rien ne vous empêche de recourir à une écriture plus classique qui produit, pour autant, le même résultat :
MOV
ESI
, ADDR
ColumnText ; récupère en ESI la colonne de texte à utiliser,
MOV
[Lv1+
0Ch
], ESI
; ... la communique au membre approprié de la structure
MOV
D[Lv1+
8h
], 50D
; ... et établit la largeur à 50 pixels
Bien qu'elle se révèle plus complexe à mettre en œuvre, la première méthode vous permet de suivre votre code dans le débogueur symbolique. Les symboles de la structure y apparaîtront en entier, c'est-à-dire en associant label de structure et nom du membre, ce qui procure un gain de lisibilité évident. En effet, GoAsm crée des symboles pour tous les membres de la structure et transmet ceux-ci au linker. Il me semble que cette possibilité est spécifique à GoAsm et qu'elle est absente des autres assembleurs.
V-A-7. Récupération de l'offset des membres d'une même structure▲
Il peut arriver que vous ayez à connaître l'offset d'un membre au sein d'une structure. Il vous suffit pour cela de vous référer au nom de la structure auquel vous ajoutez un point puis le nom du membre concerné. Par exemple, si :
POINT STRUCT
left DD
0
right DD
0
ENDS
alors, l'instruction MOV EBX, POINT.right charge la valeur 4 dans EBX, qui est la distance de l'élément par rapport au début de la structure.
Cette façon de récupérer un offset est parfois utile pour obtenir des informations envoyées par Windows dans une structure. À titre d'exemple, la procédure de callback OFNHookProc reçoit de Windows de l'information dans un message de WM_NOTIFY. Le paramètre lParam contient un pointeur vers une structure OFNOTIFY. Il s'agit d'une structure imbriquée de la forme suivante :
OFNOTIFY STRUCT
hdr NMHDR
lpOFN DD
pszFile DD
ENDS
dans laquelle la structure NMHDR est :
NMHDR STRUCT
hwndFrom DD
idFrom DD
code DD
ENDS
Donc, au sein de votre procédure de fenêtre, vous pouvez obtenir la valeur du membre idFrom dans le NMHDR (identifiant du contrôle d'envoi du message) comme suit :
MOV
ESI
, [EBP
+
14h
] ; récupère en ESI le pointeur de la structure OFNOTIFY
MOV
EAX
, [ESI
+
OFNOTIFY.hdr.idFrom]
MOV
EDX
, [ESI
+
OFNOTIFY.pszFile]
En fait, il advient ici que OFNOTIFY.hdr.idFrom contient la valeur 4 ; NOTIFY.pszFile contient la valeur 10h. Ce sont leurs offsets corrects par rapport au début de la structure OFNOTIFY. Bien sûr, les structures concernées doivent être connues de GoAsm. Cela se fait en incluant les modèles de structure dans le script source assembleur, quelque part en amont dans le fichier.
V-A-8. Redéfinition de l'initialisation de la structure▲
Supposons que vous ayez une structure appelée RECT comme suit :
RECT STRUCT
left DD
10
top DD
10
right DD
120
bottom DD
90
ENDS
Vous pouvez remplacer l'initialisation de la structure en utilisant les opérateurs < et >, { et }. Par exemple :
rc1 RECT <
0
, 20
, 120
, 300
>
réinitialise les doubles-mots dans la structure de données à 0, 20, 120 et 300 respectivement.
Vous pouvez utiliser le point d'interrogation et la virgule, ou tout simplement la virgule pour ignorer certains membres, par exemple :
rc1 RECT <
0
, ?, ?, 300
>
rc1 RECT <
0
, , , 300
>
Ici vous remplacez seulement les premier et quatrième membres de la structure.
En utilisant des accolades, vous pouvez nommer explicitement les membres que vous souhaitez réinitialiser :
rc1 RECT {left=
2
, top=
5
}
Vous pouvez même mélanger les deux méthodes :
rc1 RECT <
{left=
2
, top=
5
}, 300h
>
Lors de l'utilisation des accolades, il n'est pas nécessaire de spécifier les noms complets de symbole (dans l'exemple ci-dessus, ces noms complets seraient « rc1.left » et « rc1.top »). Au lieu de cela vous pouvez vous limiter au seul nom du membre (« left » et « top » dans notre cas). La réinitialisation est également effectuée dans les structures imbriquées. Aussi, si vous utilisez les mêmes noms pour les membres au sein d'une structure imbriquée, il est possible d'initialiser plusieurs membres à la fois en utilisant une accolade de réinitialisation.
V-A-9. Initialisation de membres de structure avec déclarations de données DUP▲
Si les membres d'une structure sont établis en utilisant l'opérateur DUP, vous pouvez procéder simplement à leur initialisation en utilisant une chaîne ou en spécifiant chaque élément au sein des délimiteurs < et > :
UP STRUCT
DB
27
DUP
0
DB
2
DUP
0
ENDS
Pent UP <
'My cat was born on 23 April'
,<
23h
,4h
>>
L'exemple qui suit décrit la structure GUID et une initialisation typique pour une COM :
GUID STRUCT
Data1 dd
?
Data2 dw
?
Data3 dw
?
Data4 db
8
dup
?
GUID ENDS
IID_IShellLink GUID <
0000214eeh
, 00000h
, 00000h
, <
0c0h
, 00h
, 00h
, 00h
, 00h
, 00h
, 00h
, 46h
>>
V-A-10. Quelques règles de syntaxe concernant STRUCT▲
Une règle importante veut que, dans la mesure où GoAsm est un assembleur à une seule passe, les modèles de structure soient impérativement écrits dans le script source avant qu'ils ne soient utilisés. En effet, l'assembleur ne peut pas connaître à l'avance la taille de la structure. GoAsm est plutôt plus tolérant avec la syntaxe de STRUCT que d'autres assembleurs. Par exemple, STRUC a la même signification que STRUCT. De même, il n'y a pas besoin de fournir des valeurs initiales du tout, et il n'y a pas d'importance à ce que les membres ne soient pas nommés, ainsi qu'en attestent les exemples suivants qui sont parfaitement valides en dépit des apparences :
RECT STRUCT
left DD
top DD
right DD
bottom DD
ENDS
et
RECT STRUCT
left DD
0
DD
2
DUP
0
bottom DD
0
ENDS
mais aussi
RECT STRUCT
DD
4
DUP
0
ENDS
sont des déclarations de structure tout aussi valides les unes que les autres. Toutefois, lorsque les membres sont nommés, ils doivent l'être sur une nouvelle ligne.
Vous pouvez réutiliser le nom de membres de la structure, pourvu que le nom de la structure soit différent, par exemple :
RECT STRUCT
left DD
0
top DD
0
right DD
0
bottom DD
0
ENDS
RECT2 STRUCT
left DD
0
top DD
0
right DD
0
bottom DD
0
ENDS
Si vous utilisez ? dans l'initialisation des membres de la structure, vous obtenez le même effet qu'avec la valeur zéro. Cela ne se traduit pas, en tout cas, par des données enregistrées comme non initialisées, comme ce serait le cas avec une déclaration de données ordinaire. De la sorte,
RECT STRUCT
left DD
?
top DD
?
right DD
?
bottom DD
?
ENDS
rc1 RECT
est parfaitement valable, mais les données vont dans la section de données avec initialisation automatique à zéro, comme si des zéros avaient été utilisés.
Dans un modèle de structure, vous pouvez mettre des données supplémentaires sur une même ligne de la manière habituelle. Voici un modèle de structure de quatre DWords utilisant cette facilité :
RECT STRUCT
lefttop DD
0
, 0
rightbottom DD
0
,0
ENDS
V-A-11. Déclarations de structure répétées▲
Ce procédé peut être utile pour créer des tableaux et des tables utilisant des modèles de structure. Par exemple :
RECT <>
, <>
, <>
, <>
crée quatre structures RECT (quatre DWords dans chaque). Dans la mesure où aucun label n'a été utilisé devant RECT, aucun symbole ne sera créé et transmis au débogueur. Dans cet exemple :
Buffer RECT <
0
,0
,10
,10
>
, <
5
,5
,20
,20
>
, <
8
,8
,30
,30
>
un tableau est constitué de trois structures RECT (quatre mots chacune) initialisées selon les valeurs indiquées. Des symboles ne seront élaborés que pour la toute première structure. Ceci afin d'éviter la duplication des noms de symboles.
Si vous voulez que les membres de la matrice aient des noms de symboles uniques, il vous est possible de procéder, par exemple, comme suit :
Buffer1 RECT <
0
, 0
, 10
, 10
>
Buffer2 RECT <
5
, 5
, 20
, 20
>
Buffer3 RECT <
8
, 8
, 30
, 30
>
ou
Buffer RECT3 <
0
, 0
, 10
, 10
, 5
, 5
, 20
, 20
, 8
, 8
, 30
, 30
>
où RECT3 est une structure de trois RECT.
Si vous n'avez pas besoin d'initialiser les structures, vous pouvez les répéter en utilisant, soit :
Buffer RECT <>
, <>
, <>
ce qui crée trois structures RECT, soit :
Buffer RECT, RECT, RECT
qui fait la même chose.
Vous pouvez également utiliser des DUP pour répéter des structures, par exemple :
ThreeRects RECT 3
DUP
<>
FiveRects RECT 5
DUP
<
23
, 24
, 25
, 26
>>
Dans le deuxième exemple, chaque RECT est initialisé à la même valeur. L'initialisation de structures dupliquées, de cette manière, ne peut seulement être faite qu'au plus haut niveau et non dans des structures imbriquées.
V-A-12. Structures imbriquées utilisant STRUCT▲
Les structures peuvent être imbriqués en utilisant une structure à l'intérieur d'une autre, de sorte que :
RECT STRUCT
left DD
0
top DD
0
right DD
0
bottom DD
0
ENDS
StructTest STRUCT
a DD
6
b RECT
c DD
7
d DD
8
ENDS
Dans ce cas, Hello StructTest crée sept DWords. Les symboles créés (et passés au débogueur) sont :
- Hello
- Hello.a
- Hello.b
- Hello.b.left
- Hello.b.top
- Hello.b.right
- Hello.b.bottom
- Hello.c
- Hello.d
et peuvent être lus ou écrits de la manière habituelle, par exemple :
MOV
D[Hello.b.left], 100h
; construction d'un rectangle commençant à 256 pixels
De même que les membres d'une structure, les structures imbriquées n'ont pas nécessairement besoin d'être nommées, de sorte que ce qui suit est parfaitement valable :
StructTest STRUCT
DD
6
RECT
c DD
7
d DD
8
ENDS
V-A-12-a. Structures imbriquées internes▲
Les structures peuvent être imbriquées en déclarant une structure au sein d'une structure, de sorte que
StructTest STRUCT
a DD
6
b STRUCT
left DD
0
top DD
0
right DD
0
bottom DD
0
ENDS
c DD
7
d DD
8
ENDS
Puis
Hello StructTest
produisent le même résultat que StructTest dans l'exemple précédent. La seule différence réside dans le fait que la structure b n'est pas disponible pour une utilisation ailleurs.
V-A-13. Outrepasser l'initialisation dans les structures imbriquées▲
Vous devez utiliser avec prudence les délimiteurs < et > pour initialiser les membres corrects de la structure imbriquée. Chaque délimiteur < permettra d'aller plus loin dans l'imbrication tandis que chaque délimiteur > permettra d'en sortir un peu plus. Au sortir d'une imbrication, une virgule est attendue après le délimiteur >. Donc, dans la structure imbriquée StructTest évoquée précédemment :
rc1 StructTest <
23
, <
10
,20
,120
,300
>
, 44
, 55
>
va non seulement initialiser la structure principale mais également son membre b imbriqué. En revanche :
rc1 StructTest <
, <
10
,20
,120
,?>
, 44
, 55
>
outrepassera seulement l'initialisation de certains membres, de même que la formulation suivante qui est rigoureusement équivalente :
rc1 StructTest <
, <
10
,20
,120
,>
, 44
, 55
>
Enfin, l'écriture qui suit n'apportera aucune modification au membre b imbriqué :
rc1 StructTest <
, , 44
, 55
>
Une bonne façon de suivre la cohérence des délimiteurs est de visualiser mentalement les membres que vous ne voulez pas modifier par un point d'interrogation. Vous pouvez même les insérer dans l'expression pour en faciliter la lecture. Par exemple le dernier exemple peut être écrit :
rc1 StructTest <
?, ?,4
4
, 55
>
V-A-14. Priorité d'outrepassement▲
Plus grand est le niveau d'outrepassement, plus grande sera sa priorité. Supposons, par exemple, que vous ayez ces modèles de structure :
RECT STRUCT
left DD
1
top DD
2
right DD
3
bottom DD
4
ENDS
StructTest STRUCT
a DD
6
b RECT <
3333h
, 4444h
, 5555h
,>
c DD
7
d DD
8
ENDS
Puis
Hello StructTest <
, <
,0Bh
,0Ch
,>
, , >
Alors, RECT sera initialisé à 3333h, 0Bh, 0Ch, 4.
Outrepasser le nom des membres en utilisant les accolades {} bénéficie d'une priorité plus élevée que de le faire en utilisant les délimiteurs < et >.
V-A-15. Utilisation de chaînes dans les structures▲
Vous pouvez utiliser des chaînes dans les structures de la même manière que vous le feriez dans une déclaration de données ordinaire, par exemple :
StringStruct STRUCT
DB
'I am a lonely string in a struct'
, 0
DB
'I will keep you company'
, 0
ENDS
V-A-16. Structures avec des chaînes : initialisation et outrepassement▲
Si une structure est déclarée avec ?, alors elle peut être de n'importe quelle taille lorsqu'elle est utilisée avec des chaînes. Par exemple :
Rect STRUCT
a DB
?
DB
0
b DB
?
DB
0
ENDS
RC1 Rect <
'Hello'
, , 'Goodbye'
>
fixera la structure Rect en des chaînes terminées par un zéro, de cinq et sept octets respectivement.
Lorsque la taille du membre d'une structure est déjà définie, par exemple :
Rect STRUCT
a DB
'Hello'
DB
0
b DB
'Goodbye'
DB
0
ENDS
alors, l'outrepassement de l'initialisation ne changera pas la taille des membres, de sorte que par exemple :
RC1 Rect <
'Goodbye'
,,'Hello'
>
se traduira par la chaîne tronquée 'Goodb' au label Rect.a et par la chaîne 'Hello' au label Rect.b, le reste de cette dernière étant comblée avec deux zéros.
L'initialisation des membres des structures établie à l'aide DUP peut également être surpassée par des chaînes. Par exemple :
UP STRUCT
DB
20
DUP
0
ENDS
Pent UP <
"Hello"
>
se traduit par la chaîne 'Hello' suivie par 15 zéros.
Voir aussi la section « Utilisation des chaînes Unicode dans les structures » du volume 2, dans le chapitre consacré à l'Unicode.
V-A-17. Assemblage conditionnel dans les structures▲
Vous pouvez utiliser l'assemblage conditionnel directement dans une structure, par exemple :
NMHDR STRUCT
hwndFrom DD
idFrom DD
code DD
ENDS
NMTTDISPINFO STRUCT
hdr NMHDR
lpszText DD
#if
STRINGS UNICODE
szText DW
80
DUP
?
#else
szText DB
80
DUP
?
#endif
hinst DD
uFlags DD
lParam DD
ENDS
DATA
Use1 NMTTDISPINFO
Use2 NMTTDISPINFO <<>
, , "Hello"
, , , , >
La deuxième utilisation de la structure va assembler la chaîne 'Hello', soit en Unicode, soit en ANSI selon le paramétrage, en conséquence, de la directive STRINGS.
Voir la section relative à l'assemblage conditionnel.
V-B. Unions▲
V-B-1. Définition▲
Les unions, comme les structures, sont des zones de données de taille fixe qui contiennent des données dans divers composants (les membres d'union). Comme pour les structures, aucune zone de données n'est effectivement créée lorsque vous déclarez un modèle d'union. Cette déclaration se fait lorsque vous utilisez le modèle. Les unions diffèrent des structures en ce que chaque membre de l'union commence à la même adresse dans la mémoire. Les unions se révèlent utiles par exemple si vous souhaitez utiliser des labels différents pour traiter la même zone de données. Le label que vous adressez au moment de l'exécution pourrait alors compter sur des éventualités telles que la version du système d'exploitation sur lequel le programme est en cours d'exécution. La taille d'une union est toujours réglée sur celle de la plus grande déclaration de données en son sein. Vous pouvez mélanger les unions avec des structures pour former des modèles complexes. Vous pouvez déclarer des unions dans les données locales et les répéter de la même manière que vous le pouvez pour les structures.
Considérons, par exemple, le modèle d'union déclaré comme suit :
Thing UNION
Cat DD
0
Dog DW
0
Rat DB
0
ENDS
Vous pouvez utiliser ce modèle en écrivant :
Hungry Thing
Ceci définit alors à part une zone de données de quatre octets (DWord). Mais, pourquoi seulement quatre octets ? Tout simplement parce que chaque membre commence à la même place et que l'on prend seulement en considération la taille du plus grand d'entre eux. La fin du modèle de l'union est marquée, au choix, par ENDS ou ENDUNION si vous préférez.
Les symboles créés par cette union sont :
- Hungry
- Hungry.Cat
- Hungry.Dog
- Hungry.Rat
Et vous pouvez adresser ces labels de la manière habituelle, par exemple :
MOV
[Hungry.Cat], EAX
MOV
AL
, [Hungry.Dog]
MOV
ESI
,[Hungry.Rat]
Ce qui, bien sûr, puisque chaque membre commence à la même place, est identique à :
MOV
[Hungry.Cat], EAX
MOV
AL
, [Hungry.Cat]
MOV
ESI
, [Hungry.Cat]]
V-B-2. Unions imbriquées▲
Vous pouvez imbriquer des unions dans des structures, des structures dans des unions ou des unions dans des unions. Par exemple
Laugh STRUCT
Balm DW
0
Ointment DB
0
ENDS
;
Zebra UNION
Tiger DD
0
Hyaena Laugh
ENDS
;
Lion STRUCT
BagPuss DB
3
DUP
0
Striped Zebra
ENDS
;
Fierce Lion
qui produit les symboles et offsets correspondants suivants dans Fierce :
|
+0 |
|
+0 |
|
+3 |
|
+3 |
|
+3 |
|
+3 |
|
+5 |
V-B-3. Unions imbriquées en interne▲
Les unions peuvent être imbriquées en les déclarant dans une structure ou une union, tel que le montre l'exemple suivant :
Lion STRUCT
BagPuss DB
3
DUP
0
Striped UNION
Tiger DD
0
Hyaena STRUCT
Balm DW
0
Ointment DB
0
ENDS
ENDS
ENDS
;
Fierce Lion
produit le même résultat que l'exemple précédent. La seule différence est que Laugh et Zebra ne sont pas disponibles pour être utilisés ailleurs.
V-B-4. Initialisation des membres d'union▲
De la même manière que dans les structures, vous pouvez utiliser les opérateurs < et > pour initialiser les unions avec des chaînes ou des valeurs numériques, par exemple :
Cat UNION
Ginger DB
Tortie DW
Grey DD
Tabby DQ
ENDS
Hungry Cat <
"a string for Ginger"
>
Anxious Cat <
, 4444h
>
; initialise le mot
Sleepy Cat <
, , 55555555h
>
; initialise le double-mot
Insistent Cat <
, , , 6666666666666666h
>
; initialise le quadruple-mot
Il est encore plus difficile, lorsque vous utilisez les unions, de suivre les opérateurs < et >. En lieu et place et à votre convenance, vous pouvez spécifier le nom du membre inclus entre les opérateurs { et }, ou vous pouvez les initialiser lors du lancement de l'exécution, par exemple :
Scaredy Cat {Ginger=
"a string for Ginger"
}
GString DB
"a string for the Grey cat"
MOV
[Scaredy.Grey], ADDR
GString ; charge un pointeur vers GString
Rappelez-vous que, dans la mesure où les membres d'unions sont au même endroit, une initialisation ultérieure peut en écraser une précédente.
Le point d'interrogation en option (?) est utile pour montrer que vous ne voulez pas effacer un remplacement précédent.
Exemple :
Laugh STRUCT
Balm DW
6666h
Ointment DB
ENDS
;
Zebra UNION
Tiger DD
88888888h
Hyaena Laugh
ENDS
;
Lion STRUCT
BagPuss DB
DB
DB
Striped Zebra
ENDS
;
Fierce Lion <
{Ointment=
0AAh
} 22h
, 33h
, 44h
, <
?,<
?,55h
>>>
qui initialise la zone de données de la manière suivante :
At Fierce.BagPuss (at offset
+
0
) 22h
,33h
,44h
Then at Fierce.Striped.Tiger (at offset
+
3
) 66h
,66h
,0AAh
,88h
Il apparaît ici que le DWord Tiger à +3 dans l'union Zebra a été initialisé à 88888888h mais qu'il a ensuite été remplacé par les valeurs de Balm et Ointment (qui étaient dans la même union). Seul le dernier octet a survécu.
V-C. Définitions : Equates, macros et #define▲
V-C-1. Quand faire quelque chose signifie quelque chose d'autre▲
Equates, macros et #defines donnent un sens à un mot. En d'autres termes, le mot est défini. Une fois défini, et à partir de ce point dans le script source, la définition est utilisée en lieu et place du mot d'origine si le contexte le permet. Si le mot tel que défini désigne :
- un nombre, alors les programmeurs en assembleur appellent la définition une « equate » parce que, traditionnellement, le mot est défini en utilisant les opérateurs EQU ou = ;
- une chaîne, alors traditionnellement, on parle d'une « equate » texte ;
- quelque chose de plus élaboré qu'un nombre ou une chaîne - une série d'instructions, par exemple - alors les programmeurs appellent cela une « macro ». Ceci parce que, selon un usage répandu, le mot est défini en utilisant l'opérateur MACRO.
Lors de l'utilisation GoAsm, pour les définitions qui peuvent être construites sur une seule ligne, vous aimeriez utiliser EQU, =, ou #define comme vous le feriez en langage C. Il suffit d'utiliser celui des trois que vous aimez le mieux. Vous pouvez utiliser le caractère de continuation (« \ ») pour permettre aux définitions de s'étendre sur plus d'une ligne, mais il est préférable d'utiliser MACRO … ENDM à la place, de manière à éviter d'éventuels problèmes de syntaxe.
Dans la mesure où GoAsm est un assembleur qui ne travaille qu'en une seule passe, vous devez vous assurer que vos définitions ne sont pas utilisées avant qu'elles ne soient déclarées dans le script source. Une fois qu'un mot a été défini, vous pouvez modifier sa définition mais GoAsm vous en avertira car une telle intervention peut ne pas être intentionnelle.
Voici quelques exemples de mise en œuvre de définitions.
V-C-2. Définitions de mots représentatifs de nombres ou chaînes (exemples de données)▲
Voici trois exemples définissant un mot comme une valeur constante. Le premier utilise =, le second utilise EQU et le troisième, #define. Ils ont tous les trois le même effet.
WS_CHILD=
40000000h
WS_CHILD EQU
40000000h
#define WS_CHILD 40000000h
Outre une constante numérique, vous pouvez utiliser une expression arithmétique, des chaînes ou même d'autres définitions lorsque vous définissez un mot. En voici quelques exemples :
SKIP_VALUE EQU
20h
|
40h
#define SKIP_VALUE 20h
|
40h
HelloText=
'Hello world'
#define HelloText "Hello world"
MANIA=
SKIP_VALUE+
WS_CHILD
Si vous ne donnez pas de valeur à l'equate, celle-ci prend la valeur 1 par défaut, qui correspond à TRUE dans la philosophie Windows. Par exemple :
NT_VERSION=
NT_VERSION EQU
#define NT_VERSION
Une fois un mot défini, vous pouvez l'utiliser dans presque toute situation où la définition est valide, par exemple :
DB
HelloText
PUSH
WS_CHILD|
WS_VISIBLE|
SS_OWNERDRAW
MOV
EAX
, WS_CHILD
MOV
EAX
, [ESI
+
SKIP_VALUE]
MOV
EAX
, MANIA+
800h
V-C-3. Définition de mots en substitution d'instructions de code▲
Voici un exemple de la façon dont vous pouvez attribuer à un mot tout ou partie de la fonctionnalité d'une instruction de code :
#define lParam [EBP
+
14h
]
Vous pouvez utiliser alors la définition ainsi créée de la manière suivante :
MOV
lParam, EAX
; identique à MOV [EBP+14h], EAX
Ici, on peut dire, d'une certaine manière, que le texte « [EBP+14h] » a tout simplement été remplacé par lparam dans le script source.
V-C-4. Utilisation d'arguments dans la définition de mots▲
Les arguments sont des valeurs qui sont données lorsque les définitions sont utilisées. Ces valeurs sont ensuite utilisées dans la définition ou la macro elles-mêmes. Ainsi :
RECTB(%
a,%
b,%
c,%
d) =
DD
%
a,%
b,%
c,%
d
Ensuite, vous pouvez déclarer quatre DWords initialisés comme spécifié dans les arguments :
rc1 RECTB (10
, 10
, 100
, 200
) ; identique à DD 10,10,100,200
Voici maintenant un autre exemple utilisant #define :
#define DBDATA(%
a,%
b) DB
%
a DUP
%
b
DBDATA(3
,'x'
) ; identique à DB 3 DUP 'x'
Il existe une règle de syntaxe importante lorsque vous utilisez des arguments dans les définitions : il ne doit pas y avoir d'espace entre le nom de la définition et l'ouverture de parenthèse qui précède l'énoncé des arguments.
Ainsi :
RECTB(%
a,%
b,%
c,%
d)
est correct, mais
RECTB (%
a,%
b,%
c,%
d)
est erroné. Cette contrainte syntaxique, déroutante à certains égards, garantit que GoAsm reconnaît les paramètres entre parenthèses en tant qu'arguments et seulement en tant que tels.
Vous devez également vous assurer que les noms des arguments sont inhabituels et ne risquent donc pas de se retrouver dans tout autre matériau utilisé dans la définition ou la macro. Cela évite que d'autres choses soient remplacées par inadvertance. D'où le signe de pourcentage utilisé dans les exemples ci-dessus.
V-C-5. Définitions réparties sur plusieurs lignes▲
La définition peut occuper plusieurs lignes pour en accroître la lisibilité et, notamment, pour vous permettre d'ajouter des commentaires. Par exemple :
#define WS_POPUP 0x80000000L
#define WS_BORDER 0x00800000L
#define WS_SYSMENU 0x00080000L
#define WS_POPUPWINDOW (WS_POPUP |
\
WS_BORDER |
\
WS_SYSMENU)
Ce dernier exemple a été pris directement sur le fichier d'en-tête de Windows Winuser.h et vous pouvez voir qu'il épouse la syntaxe typique du C. GoAsm ne peut que s'en réjouir et tolère, de surcroît, l'absence éventuelle des parenthèses :
#define WS_POPUPWINDOW WS_POPUP |
\
WS_BORDER |
\
WS_SYSMENU
Vous pouvez préférer utiliser MACRO … ENDM à la place du caractère de continuation, auquel cas l'exemple ci-dessus peut être récrit sous la forme :
WS_POPUPWINDOW MACRO
WS_POPUP |
WS_BORDER |
WS_SYSMENU
ENDM
V-C-6. Définitions multiligne : exemple de trame de pile (callback windows)▲
Ceci s'applique uniquement à la programmation 32 bits.
Vous pouvez utiliser la méthode de définition multiligne pour constituer un mot signifiant plusieurs lignes d'instructions de code. Par exemple :
OPEN_STACKFRAME(a) =
PUSH
EBP
\
MOV
EBP
, ESP
\
SUB
ESP
, a*
4
\
PUSH
EBX
, EDI
, ESI
CLOSE_STACKFRAME =
POP
ESI
, EDI
, EBX
\
MOV
ESP
, EBP
\
POP
EBP
En utilisant MACRO … ENDM, cela donne :
OPEN_STACKFRAME(a) MACRO
PUSH
EBP
MOV
EBP
, ESP
SUB
ESP
, a*
4
PUSH
EBX
, EDI
, ESI
ENDM
CLOSE_STACKFRAME MACRO
POP
ESI
, EDI
, EBX
MOV
ESP
, EBP
POP
EBP
ENDM
Dans cet exemple, le mot OPEN_STACKFRAME est défini pour élaborer une trame de pile qui pourrait typiquement être utilisée dans une procédure de fenêtres appelée par le système Windows. Il possède un argument qui détient le nombre de mots dans la trame de pile permettant d'accepter des données locales (le pointeur de pile est déplacé par ce paramètre de sorte que la pile puisse être utilisée pour stocker les données locales). Le mot CLOSE_STACKFRAME ferme, quant à lui, la trame de pile. Voici maintenant comment utiliser ces définitions. Dans la section de code :
WndProc
:
; nom de cette procédure
OPEN_STACKFRAME (6
) ; création d'un espace pour 6 dwords de données locales
;----------------- insertion du code de la procédure de fenêtre ici
CLOSE_STACKFRAME
RET
10h
; retrait de la pile des 4 paramètres envoyés par Windows
Ajoutons maintenant un peu de raffinement de sorte que la pile puisse être accessible de manière plus compréhensible :
lParam =
[EBP
+
14h
] ;
wParam =
[EBP
+
10h
] ; on se tient prêt à accéder aux paramètres qui
uMsg =
[EBP
+
0Ch
] ; sont envoyés par Windows à la procédure de fenêtre
hwnd =
[EBP
+
8h
] ;
;
hDC =
[EBP
-
4h
] ; certains noms d'éléments de données
hBrush =
[EBP
-
8h
] ; sont souvent utilisées dans des
hPen =
[EBP
-
0Ch
] ; procédures de fenêtre différentes
DATA1 =
[EBP
-
10h
] ;
DATA2 =
[EBP
-
14h
] ; espace pour plus de données locales
DATA3 =
[EBP
-
18h
] ;
À l'intérieur de la trame de pile constituée ici, les paramètres envoyés par Windows (hwnd, uMsg, wParam et lParam) seront toujours sur la pile de EBP+14h à EBP+8h. À EBP+4h, nous trouvons l'adresse de retour du CALL. En EBP nous avons la valeur précédente de EBP que nous avons poussée au moment de la constitution de la trame de pile. Puis, de EBP-4h à EBP-18h, nous avons l'espace dévolu à nos données locales qui, dans cet exemple, peuvent être accessibles en utilisant les définitions hDC, hBrush, hPen, DATA1, DATA2 et DATA3 (ou tout nom que vous voudrez bien leur attribuer). À EBP-1Ch nous avons la valeur de EBX quand ce registre a été poussé en pile lorsque la trame de cette dernière a été élaborée. De même EDI est en EBP-20h et ESI en EBP-24h. Toutes ces valeurs sont protégées tandis que la trame de pile reste ouverte (elles ne seront pas écrasées par d'autres fonctions avant la fin du callback). Pour accéder aux données dans la trame de pile, vous devez vous assurer que vous ne modifiez EBP en aucune manière (ou si vous le faites, vous le restaurez à sa valeur initiale). Vous ne devez pas accéder aux données par leur nom. Dans cet exemple, MOV EAX, [hBrush] est la même chose que MOV EAX, [EBP-8h]. Ceci est une question de style et de choix personnel. En utilisant ces méthodes, vous pouvez établir autant de données locales que vous le souhaitez, et si vous vous en tenez à une méthode fixe comme celle-ci, vous saurez toujours où sont vos données locales. Dans cet exemple, le premier DWord des données locales est toujours à EBP-4h. Soustraire 4 de cette valeur pour accéder à chaque DWord supplémentaire de données locales.
Il y a beaucoup d'autres façons de travailler avec la pile dans les callbacks. Voir, à ce sujet, les sections trames de pile pour Callback en 32 et 64 bits, et trames de pile automatisées utilisant FRAME … ENDF, LOCALS et USEDATA.
Voir aussi comprendre la pile (parties 1 et 2) dans les annexes.
V-C-7. Assemblage conditionnel dans les macros▲
Si vous utilisez l'assemblage conditionnel dans vos définitions il est recommandé d'utiliser MACRO … ENDM au lieu de la méthode des caractères de continuation mentionnée plus haut.
Voici un exemple :
STRINGS UNICODE
CODE
;
#define REPORT
;
MBMACRO(%
lpTextW) MACRO
#ifdef
REPORT
INVOKE
MessageBoxW, 0
, addr
%
lpTextW, "Report"
, 40h
#endif
ENDM
;
MBMACRO("Ce code a été assemblé !"
)
Dans le code ci-dessus, la boîte de message (Message Box) est affichée de manière classique via l'API MessageBox. On a vu précédemment que la ligne #define REPORT sans autre paramètre donne à REPORT la valeur 1 (TRUE). Cette situation autorise l'affichage. Mais si REPORT était suivi d'une valeur, alors il n'y aurait pas de message affiché. La chaîne « Ce code a été assemblé ! » est envoyée comme un argument à la macro.
Voir la section assemblage conditionnel.
V-C-8. Comptage des arguments avec ARGCOUNT▲
Ceci s'applique uniquement à la programmation 32 bits.
ARGCOUNT renvoie le nombre d'arguments donnés lorsque la définition est utilisée, ce qui peut être utilisé avec l'assemblage conditionnel dans les définitions. Par exemple, considérons la macro nommée macro26 ainsi définie :
macro26(%
a,%
b,%
c,%
d,%
e,%
f) =
#if
ARGCOUNT=
6
\
PUSH
%
f \
#endif
\
#if
ARGCOUNT >=
5
\
PUSH
%
e \
#endif
\
#if
ARGCOUNT >=
4
\
PUSH
%
d \
#endif
\
#if
ARGCOUNT >=
3
\
PUSH
%
c \
#endif
\
#if
ARGCOUNT >=
2
\
PUSH
%
b \
#endif
\
#if
ARGCOUNT >=
1
\
PUSH
%
a \
#endif
Imaginons maintenant que nous utilisions macro26 comme suit :
macro26(4
,3
,2
,1
)
Dans ce cas, ARGCOUNT prendrait la valeur 4 et le code résultant correspondrait exactement à l'instruction multiple suivante :
PUSH
1
, 2
, 3
, 4
Dans l'exemple ci-dessus, les deux premiers PUSH sont ignorés parce que ARGCOUNT est ni 6 (dans le premier test) ni supérieur ou égal à 5 (dans le second test).
Cela peut être amélioré pour établir une macro d'appel de fonction « C » où la pile est effacée après l'appel (le nombre correct d'octets est ajouté au pointeur de pile ESP après l'appel) :
macro26(%
x,%
a,%
b,%
c,%
d,%
e,%
f) =
#if
ARGCOUNT=
7
\
PUSH
%
f \
#endif
\
#if
ARGCOUNT >=
6
\
PUSH
%
e \
#endif
\
#if
ARGCOUNT >=
5
\
PUSH
%
d \
#endif
\
#if
ARGCOUNT >=
4
\
PUSH
%
c \
#endif
\
#if
ARGCOUNT >=
3
\
PUSH
%
b \
#endif
\
#if
ARGCOUNT >=
2
\
PUSH
%
a \
#endif
\
CALL
%
x \
ADD
ESP
, ARGCOUNT-
1
*
4
et ici, on peut voir une autre manière d'y arriver :
cinvoke(funcname,%
1
,%
2
,%
3
,%
4
,%
5
) =
\
#if
ARGCOUNT=
1
\
invoke
funcname \
#elif ARGCOUNT=
2
\
invoke
funcname, %
1
\
#elif ARGCOUNT=
3
\
invoke
funcname, %
1
, %
2
\
#elif ARGCOUNT=
4
\
invoke
funcname, %
1
, %
2
, %
3
\
#elif ARGCOUNT=
5
\
invoke
funcname, %
1
, %
2
, %
3
, %
4
\
#elif ARGCOUNT=
6
\
invoke
funcname, %
1
, %
2
, %
3
, %
4
, %
5
\
#endif
\
#if
ARGCOUNT>
1
\
ADD
ESP
, ARGCOUNT-
1
*
4
\
#endif
Lesquels pourraient alors être utilisés comme suit :
cinvoke(_cprintf,23
,24
,25
,26
,27
)
macro26(_cprintf,23
,24
,25
,26
,27
)
Si vous ne souhaitez utiliser le caractère de continuation, vous pouvez utiliser MACRO … ENDM en lieu et place.
V-C-9. Utilisation des doubles-dièses dans les définitions▲
Un double-dièse dans une définition a pour fonction de joindre deux éléments en supprimant tous les espaces entre les deux. Cela vous permet de créer un seul mot à partir d'un ou plusieurs composants, par exemple :
LVERS=
0030
MVERS=
0044h
VERSION=
LVERS##MVERS
;
MOV
EAX
, VERSION
Ici, VERSION est défini par le nombre 00300044h.
V-C-10. L'utilisation des définitions et ses limites▲
Certains programmeurs utilisent des définitions aussi souvent que possible. À mon avis, cela rend le script source plus difficile à lire. Aussi convient-il de déterminer dans quelle mesure quelqu'un d'autre que le concepteur pourrait être amené à parcourir le code source et à en comprendre les tenants et les aboutissants à supposer, bien évidemment, que cela soit souhaitable… Si cette personne doit souvent se référer à d'autres fichiers ou à des listes de définitions pour ce faire, alors le codage manquera, à l'évidence, de clarté. Utilisées cependant avec modération en programmation Windows, les définitions peuvent se révéler utiles pour expliciter le script source. Par exemple :
PUSH
WS_CHILD|
WS_VISIBLE|
SS_OWNERDRAW
est plus parlant que :
PUSH
5000000Dh
bien qu'un commentaire approprié puisse, en l'espèce, éclairer utilement cette instruction sans qu'il soit nécessaire d'utiliser une définition :
PUSH
5000000Dh
; WS_CHILD, WS_VISIBLE, SS_OWNERDRAW
En revanche, la formulation qui suit nuit à la clarté du code et doit donc être évitée :
#define wParam EBP
+
10h
MOV
EAX
, [wParam] ; identique à MOV EAX, [EBP+10h]
On notera en effet que la référence [wParam] fait apparaître wParam comme un label, ce qui n'est justement pas le cas ici. Mieux vaut écrire en l'occurrence :
#define wParam [EBP
+
10h
]
MOV
EAX
, wParam
Ceci est plus clair ainsi parce que, dans GoAsm, la seule chose que vous pouvez aborder de cette manière sans l'aide des crochets est une définition.
Ce qui suit est également contestable et doit être évité à tout prix :
#define GET_LPARAM MOV
EAX
, [EBP
+
14h
]
GET_LPARAM
Une meilleure pratique de programmation suggère d'écrire :
CALL
GET_LPARAM
et de réaliser correctement cet appel sous forme de fonction. Cependant lors de la manipulation de la pile, il est très difficile d'utiliser une procédure dans la mesure où CALL et RET modifient eux-mêmes la pile. Donc, dans ce cas, il peut être pratique d'utiliser une définition si la clarté du script source n'en souffre pas. Voir, à ce titre, les exemples OPEN_STACKFRAME et CLOSE_STACKFRAME proposés précédemment.
Enfin, il semble qu'il y ait peu d'intérêt à écrire :
THOUSAND =
1000D
MOV
EAX
, THOUSAND
alors qu'une formulation plus directe serait tout aussi claire :
MOV
EAX
, 1000D
À titre d'illustration et en guise de conclusion, imaginez que vous souhaitiez que votre script source soit compréhensible par des non-Anglophones. Dans ce cas, il vous serait parfaitement possible de traduire tous les mnémoniques du processeur en utilisant des equates pour parvenir à vos fins. Voir à ce sujet la section « Utilisation de mots définis dans les fichiers Unicode » dans le chapitre du volume 2 consacré à l'Unicode.
V-D. Importation : utilisation des bibliothèques run-time▲
Avec GoAsm, il est facile d'utiliser la bibliothèque run-time du C. Elle consiste en un certain nombre de fonctions contenues dans Crtdll.dll ou Msvcrt.dll (ou leurs variantes) qui se trouvent habituellement sur un ordinateur Windows dans le dossier système. L'information sur cette bibliothèque est disponible sur le site Microsoft developer site (MSDN). La principale chose à retenir lors de l'utilisation de ces fonctions est que même si vous envoyez des paramètres à la fonction au moyen de la pile, la fonction ne restaure pas celle-ci. Par conséquent, vous aurez besoin de le faire vous-même en utilisant l'instruction ADD ESP, x après l'appel, x étant le nombre d'octets utilisés pour les paramètres.
V-E. Import : données, par ordinal, par DLL spécifiques▲
V-E-1. Import de données▲
Au moment de l'exécution, les données ne peuvent être importées qu'indirectement. Cela veut dire que vous ne pouvez importer qu'un pointeur vers des données. Cependant, en utilisant ce pointeur, vous pouvez obtenir les données elles-mêmes.
Par exemple, en supposant que DATA_VALUE est un export de données dans un autre programme vous obtenez, avec GoAsm, le pointeur et la donnée comme suit :
MOV
EBX
, [DATA_VALUE] ; pointeur vers DATA_VALUE dans EBX
MOV
EAX
, [EBX
] ; on récupère la donnée dans EAX à partir de ce pointeur
De la même manière que pour l'importation des procédures d'autres programmes au moment de l'édition de liens, vous donnez au linker (GoLink, en l'occurrence) le nom de l'exécutable contenant l'import.
V-E-2. Import direct par ordinal▲
Le type d'import auquel nous sommes confrontés en utilisant la procédure CALL importera la procédure par son nom. De manière plus détaillée lorsque le chargeur Windows démarre le programme, il parcourt les DLL pour les importations requises par le programme. Il le fait en comparant les noms des exportations dll aux noms des imports du programme. Pour accélérer ce processus avec les Dll privées l'exportation et l'importation par ordinal sont parfois utilisées. Par ce biais, le chargeur peut trouver la bonne importation en utilisant un index dans une table de la DLL. Notez qu'il est imprudent de le faire dans le cas des DLL du système Windows puisque l'uniformité des nombres ordinaux des exportations n'est pas garantie selon les différentes versions de DLL.
En utilisant GoAsm et son complément GoLink, vous pouvez importer par ordinal par cette syntaxe simple :
CALL
MyDll:6
Cette procédure va appeler le numéro 6 dans MyDll.dll. Notez que l'extension « dll » est implicite si aucune extension n'est mentionnée. Supposons que vous vouliez une DLL pour appeler une fonction dans l'exécutable principal par ordinal. Dans ce cas, vous pourriez utiliser :
CALL
Main.exe:15
qui appellera la 15e fonction de Main.exe.
Les appels ordinaux utilisant la forme absolue (c'est-à-dire utilisant les opcodes FF15) emploient, quant à eux, la syntaxe qui suit :
CALL
[Main.exe:15
]
Vous n'avez pas à inclure le chemin d'accès au fichier dans le CALL. GoLink réalise une recherche étendue des fichiers spécifiés, mais s'il était toutefois nécessaire de fournir un chemin ce serait dans le cadre de l'éditeur de liens plutôt qu'au niveau du CALL du script de l'assembleur.
Évidemment, pour utiliser cette méthode d'appel d'une fonction par ordinal vous devez vous assurer que le nombre ordinal de la fonction est fixé. Voir la section export par ordinal.
Note : ce qui précède s'applique uniquement à GoLink.
Il y a une autre façon d'utiliser les ordinaux en utilisant LoadLibrary pour charger la DLL (ou retourner un handle si elle est déjà chargée), puis en appelant GetProcAddress en passant la valeur ordinale pour obtenir la valeur de la procédure à appeler. Enfin, vous appelez la procédure telle que fournie par GetProcAddress.
V-E-3. Import par des DLL spécifiques ▲
Occasionnellement, vous voudrez peut-être forcer le linker à utiliser une importation en provenance d'une DLL particulière. Vous pourriez avoir besoin de procéder de la sorte si deux DLL voire plus (désignées au linker au moment de la phase d'édition de liens) comportent des fonctions portant le même nom. Il est donc possible, dans ce cas, de forcer GoLink à établir des liens avec une DLL particulière en utilisant la syntaxe :
CALL
NameOfDll:NameOfAPI
Ceci s'applique uniquement à GoLink.
V-F. Export de procédures et de données▲
Vous pouvez faire en sorte que vos procédures et données soient disponibles pour d'autres exécutables en les exportant. Dans Windows, il est habituel que les DLL soient utilisées pour les exportations, mais parfois une DLL a besoin d'appeler une procédure ou d'utiliser les données d'un fichier Exe et, dans ce cas, ce dernier exporte également. L'export peut être fait soit au moment de l'édition de liens (vous indiquez au linker les symboles à exporter), soit au moment de l'assemblage avec GoAsm. Dans ce dernier cas, GoAsm donne alors au linker les informations d'export via la section .drectve dans le fichier objet (à noter que tous les linkers ne supportent pas cette particularité).
Il y a deux façons de déclarer les exports dans GoAsm. Vous pouvez les déclarer tous au début de votre fichier (avant qu'une quelconque section soit déclarée) ou les déclarer dans votre code source au fur et à mesure que vous avancez dans son écriture. Vous pouvez utiliser l'une ou l'autre de ces méthodes selon votre propre préférence.
Voici un exemple de déclaration de toutes les exportations avant que les sections ne soient déclarées :
EXPORTS CALCULATE, ADJUST_DATA, DATA_VALUE
Voici un exemple de déclaration d'export en cours de développement :
EXPORT
CALCULATE:
CMP
EAX
, EDX
; code pour la
JZ
>
4
; procédure
Ce code exporte le label à la procédure CALCULATE.
Si vous préférez, vous pouvez avoir les deux mots sur des lignes séparées comme, par exemple :
EXPORT
CALCULATE
:
CMP
EAX
, EDX
; code pour la
JZ
>
4
; procédure
V-F-1. Export de données▲
Les données ne peuvent être exportées qu'indirectement. Seul, un pointeur vers ces données peut être exporté. Par l'utilisation de ce pointeur, le programme d'importation peut toutefois obtenir lui-même les données.
Vous pouvez utiliser exactement la même méthode pour exporter un label de donnée que celle utilisée pour un label de code. Il n'y a pas besoin de déclarer le label comme étant de données ou de code. Il en est ainsi parce que la tâche de vérification de l'appartenance du label à une section de données ou à une section de code incombe à l'éditeur de liens. Voici un exemple d'export du label de donnée :
EXPORT
DATA_VALUE DD
0
Cette instruction exporte l'étiquette de données DATA_VALUE. Le destinataire peut obtenir la valeur de DATA_VALUE de la manière suivante :
MOV
EBX
, [DATA_VALUE] ; EBX = pointeur vers DATA_VALUE
MOV
EAX
, [EBX
] ; EAX = valeur de DATA_VALUE
V-F-2. Export par ordinal▲
Normalement, les exports sont nominatifs. Lorsque le chargeur Windows démarre le programme, il parcourt les DLL pour y chercher les importations requises par le programme. Cela se fait en confrontant les noms des exportations des DLL aux noms des importations du programme. Pour accélérer ce processus avec DLL private, l'exportation et l'importation par ordinal sont parfois utilisées. Dans ce cas, le chargeur peut trouver la bonne importation en utilisant simplement un index destiné à une table dans la DLL. Notez qu'il est imprudent de le faire dans le cas des DLL du système Windows puisque les nombres ordinaux des exportations peuvent varier selon les versions de DLL.
Pour utiliser cette méthode, il est clairement impératif que le programme d'exportation spécifie une valeur ordinale pour une exportation particulière et que l'éditeur de liens ne puisse y apporter aucune modification. Encore une fois, en utilisant GoAsm vous pouvez spécifier la valeur ordinale correcte et la passer au linker via la section .drectve (cependant, les linkers ne supportent pas tous cette pratique).
Voici comment spécifier une exportation par ordinal si les exportations sont listées avant que les sections ne soient déclarées :
EXPORTS CALCULATE:2
, DATA_VALUE:6
Ici, le linker sera chargé d'utiliser les ordinaux 2 et 6 pour les exportations. Si vous utilisez la méthode alternative de déclarer les exportations (au sein d'une section) vous pouvez utiliser par exemple :
EXPORT
:
2
CALCULATE:
ou dans le cas de données :
EXPORT
:
6
DATA_VALUE DD
0
V-F-3. Export anonyme par ordinal▲
L'export par ordinal n'empêche pas le nom de l'export d'apparaître dans l'exécutable final. Il en est ainsi parce que c'est le programme d'importation qui décide d'importer par ordinal ou par nom. Tout ce que le programme d'exportation peut faire se limite à fixer la valeur ordinale. Cependant, il peut arriver que le programmeur ne souhaite pas qu'un nom pour l'exportation apparaisse dans l'exécutable final. On peut observer de telles exportations « sans nom » dans les DLL système par exemple, probablement dans le but de masquer le travail effectué par des fonctions particulières. Il est possible de recourir à ce procédé dans GoAsm en ajoutant la déclaration NONAME à la fin de l'exportation. Par exemple :
EXPORT
:
2
:NONAME
CALCULATE
:
Ici, la valeur du label de code CALCULATE sera exportée comme nombre ordinal 2, mais le nom de l'exportation n'apparaîtra pas dans l'exécutable final. Cela signifie que si un autre programme essayait d'appeler la fonction CALCULATE, il enregistrerait un échec. La fonction peut seulement être appelée par ordinal.
V-G. Sauvegarde et restauration des flags et des registres avec USES … ENDU▲
L'instruction USES suivie d'une liste de registres provoque la mise en pile (PUSH) de ces derniers selon l'ordre dans lequel ils apparaissent dans la liste. Puis, jusqu'à ce que ENDU soit rencontré dans le script source (ou ENDUSES si vous préférez), tout RET rencontré provoquera le dépilage (POP) des mêmes registres dans l'ordre inverse. Par exemple :
ProcX
:
USES
EAX
, EBX
, ECX
; prêt à mettre les registres en pile (PUSH)
CMP
EAX
, ESI
; le premier mnémonique rencontré rend les PUSHes effectifs
;
; code de la procédure
;
.finnc
CLC
; met à zéro le flag de carry
RET
; on retire tous les registres de la pile en ordre inverse du USES (POP)
; avant d'exécuter le RET
.finc
STC
; met à 1 le flag de carry
RET
; on retire tous les registres de la pile en ordre inverse du USES (POP)
; avant d'exécuter le RET
ENDU ; on désactive cette action de POP automatique lors d'un RET
Vous pouvez également automatiquement empiler (PUSH) et dépiler (POP) les flags en utilisant l'instruction FLAGS qui est un mot réservé dans GoAsm :
USES
FLAGS
Vous ne pouvez modifier ou compléter la liste des registres à préserver à l'intérieur même de la procédure. Pour ce faire, vous devez recourir à une instruction ENDU suivie d'une liste USES actualisée. Si vous avez besoin d'un RET ne déclenchant pas une restitution automatisée des registres, vous devez utiliser RETN qui a la fonctionnalité d'un RET « normal ». Notez que RETN est une directive de l'assembleur et non une instruction processeur.
En programmation 64 bits, vous pouvez utiliser non seulement la version étendue des registres généraux (de RAX à RSP) mais aussi les nouveaux registres 64 bits (R8 à R15). Vous pouvez également utiliser les versions 32 bits des registres généraux (EAX à ESP). Ceci, parce que l'opcode du PUSH est identique, qu'il s'agisse de registres 32 ou 64 bits. Donc, si vous assemblez en 32 ou 64 bits,
USES
RAX, RBX, RCX
produira le même code que
USES
EAX
, EBX
, ECX
Cela contribue à la transportabilité de votre code entre les deux plateformes.
V-H. Trames de pile pour Callback en 32 et 64 bits▲
Voir également l'annexe comprendre la pile - partie 1 et partie 2.
V-H-1. Introduction▲
En programmation Windows, les trames de pile sont nécessaires pour les procédures de fenêtre, les procédures d'hameçonnage (hooking), le sur-classement et le sous-classement, les procédures de chargement et de déchargement de DLL et autres callbacks. Les procédures callback sont toutes appelées par Windows, en utilisant le propre thread de votre programme.
Dans GoAsm, la création et l'utilisation de trames de pile sont entièrement automatisées lorsque vous utilisez FRAME … ENDF.
Voir la section trames de pile automatisées pour savoir comment l'utiliser en pratique.
Entre Windows 32 et 64 bits, les trames de pile sont différentes et doivent donc être traitées de manière spécifique. Mais la syntaxe pour utiliser FRAME … ENDF ainsi que leurs instructions d'accompagnement telles que LOCALS et USEDATA … ENDU est la même sur les deux plateformes. Pour cette raison, lorsque vous utilisez FRAME … ENDF il est possible d'utiliser le même script source pour les deux. Voir le chapitre écriture de programmes 64 bits pour plus d'informations sur la programmation 64 bits en général.
V-H-2. Trames de pile en Windows 32 bits▲
Le travail dévolu à la trame de pile est dicté par la convention d'appel utilisée. Windows 32 bits utilise la convention d'appel standard (STDCALL). Dans ce contexte, la trame de pile dans une procédure de fenêtre doit accomplir quatre actions :
- accéder aux paramètres envoyés par Windows (qui sont justement envoyés sur la pile) ;
- rétablissement de l'équilibre de la pile avant de retourner à l'appelant ;
- allocation d'un espace sur la pile pour les données locales ;
- préservation pour Windows du contenu des registres EBX, ESI, EDI et EBP si ces derniers sont appelés à être modifiés.
Ces quatre actions sont aussi importantes les unes que les autres.
V-H-2-a. Accès aux paramètres à partir de la pile▲
Windows pousse les paramètres sur la pile avant d'appeler votre procédure de fenêtre, de la même manière que vous le faites avant d'appeler une API dans vos programmes. Lorsque Windows appelle une procédure de fenêtre, il envoie les paramètres suivants sous forme de DWords placés sur la pile (les mots utilisés ici sont ceux couramment utilisés pour les nommer) :
hwnd | handle de la fenêtre |
uMsg | identificateur de message |
wParam | donnée |
lParam | donnée |
Votre procédure de fenêtre a besoin d'accéder à ces paramètres. Une façon de le faire est de les extraire de la pile par des POP successifs en tant que données statiques, mais il est plus judicieux (et plus sûr) de conserver ces paramètres sur la pile et de les adresser directement. Cela vaut d'autant mieux que les procédures de fenêtres s'appellent elles-mêmes parfois. Cela peut sembler étrange, mais un exemple suffira à s'en convaincre.
Supposons que votre procédure ait besoin de remplir la fenêtre avec un matériau approprié au bon moment. Ceci s'appelle « peindre » la fenêtre. Cela se fait en réponse au message Windows WM_PAINT (numéro de message 0Fh). Maintenant, la bonne façon de répondre à ce message consiste tout d'abord à appeler l'API BeginPaint, puis à peindre la fenêtre, puis appeler enfin l'API EndPoint. Une des choses que BeginPaint fait est de préparer la fenêtre pour la peinture. Ce faisant, elle envoie un autre message à votre procédure de fenêtre, cette fois WM_ERASEBKGND (numéro de message 14h). Ainsi, alors que votre procédure de fenêtre est aux prises avec ce deuxième message, elle n'est pas encore revenue de l'API BeginPaint. Après que le deuxième message a été traité, BeginPaint sera de retour. Ainsi, la procédure de fenêtre est-elle récursive, ce qui revient à dire qu'elle peut revenir sur elle-même. Pour chaque message (sauf pour hwnd) les paramètres seront différents. Si ces derniers sont maintenus sur la pile, cela signifie que chaque fois que la procédure de fenêtre sera appelée les paramètres seront conservés sur une partie différente de la pile et ne pourront pas être écrasés.
Voici, à titre d'exemple, une trame de pile typique sur 32 bits :
TypicalStackFrame
:
PUSH
EBP
; sauvegarde de EBP qui va être altéré ┐ appelé
MOV
EBP
, ESP
; EBP mémorise la valeur courante du pointeur de pile ┘ "prologue"
; ; POINT "X"
; ; code pour isoler le message WM_PAINT
PUSH
ADDR
PAINTSTRUCT
PUSH
[hwnd]
CALL
BeginPaint ; API prêt à peindre la fenêtre
; ; peinture de la fenêtre et appel de l'API EndPaint
;
MOV
ESP
, EBP
; rétablissement ancienne valeur du pointeur de pile ┐ appelé
POP
EBP
; restauration de EBP │ "épilogue"
RET
10h
; retour à l'appelant en ajustant le pointeur de pile ┘
Pendant toute récursion, ESP sera changé dans la mesure où une utilisation ultérieure de la pile interviendra. En revanche, EBP sera toujours sauvegardé et restauré par cette procédure de sorte qu'il pourra toujours être invoqué pour accéder à la partie correcte de la pile afin que le message soit traité.
Cette particularité sera probablement mieux illustrée en approfondissant le fonctionnement de la pile dans l'exemple qui précède. Dans une procédure de fenêtre typique répondant au message WM_ERASEBKGND, prenons un instantané de la pile lorsque l'exécution est au point « X ». La pile va alors ressembler à ceci (j'ai passé sous silence une bonne partie de l'utilisation de la pile par souci de clarté) :
Ici, les paramètres lParam, wParam, uMsg et hwnd qui sont sur la pile de EBP+3Ch à EBP+30h sont ceux envoyés à la procédure de fenêtre sur le message WM_PAINT. Puis, à EBP+2Ch nous trouvons l'adresse de l'appelant qui envoie le message WM_PAINT (ce sera un appel en provenance d'une DLL Windows). À EBP+28h se situe la valeur de EBP enregistrée sur la première instruction d'entrée dans la procédure de fenêtre sur le message WM_PAINT. Entre EBP+24h et EBP+1Ch on trouve l'utilisation de la pile de votre procédure de fenêtre avant que vous n'appeliez l'API BeginPaint. Concrètement, ces emplacements seraient occupés par deux PUSHes, puis l'adresse de retour sur l'appel de l'API (dans votre procédure de fenêtre) mise sur la pile. Le quatrième PUSH ici pourrait être quelque chose poussé sur la pile par BeginPaint lui-même avant d'envoyer le message WM_ERASEBKGND. En pratique, il pourrait y avoir beaucoup plus d'utilisation de la pile à l'intérieur BeginPaint ici. La prochaine chose que vous voyez à EBP+14h est à nouveau lParam. Mais celui-ci est différent de son homologue situé à EBP+3Ch. Il s'agit du lParam envoyé avec WM_ERASEBKGND. Le reste des entrées à la valeur actuelle de EBP sont en rapport avec le message WM_ERASEBKGRND.
Voyons maintenant ce qui se produit au retour de BeginPaint. Puisque nous savons que BeginPaint restaurera toujours EBP à la valeur qui était la sienne à l'entrée de l'API, nous savons que EBP au retour pointera vers la pile à EBP+28h dans la trame de pile ci-dessus. À partir de ce point, vous pouvez voir que les paramètres antérieurs (ceux envoyés avec WM_PAINT) peuvent être consultés à l'aide de EBP+8h, EBP+0Ch, EBP+10h et EBP+14h comme avant. Ils ont été préservés et n'ont pas été écrasés par l'appel à BeginPaint et par la récursion dans la procédure de fenêtre.
V-H-2-b. Restauration de la pile à l'équilibre avant de retourner à l'appelant▲
C'est également le travail de la procédure de Windows que de restaurer la pile à l'équilibre avant qu'elle ne retourne à l'appelant. Cela implique de déplacer le pointeur de pile à une valeur supérieure et multiple de quatre octets (soit un double-mot) pour chaque argument envoyé (chaque PUSH de l'appelant a réduit la valeur de ESP de quatre octets afin qu'il pointe sur un emplacement plus élevé sur la pile). C'est exactement ce que Windows fait lui-même lorsque vous appelez une API. Par exemple, dans
PUSH
ADDR
PAINTSTRUCT
PUSH
[hwnd]
CALL
BeginPaint ; fait en sorte qu'on puisse être prêt à peindre la fenêtre
le pointeur de pile (ESP) a été réduit de 8 octets en raison des deux PUSHes avant l'appel de l'API BeginPaint. Mais après le retour de BeginPaint, le pointeur de la pile récupère sa valeur d'origine d'avant les deux PUSHes. Ce rétablissement est dû au fait que la pile a été incrémentée de 8 octets en sortie de BeginPaint avant de retourner à votre code.
La plupart des procédures Windows nécessitent quatre paramètres, de sorte que le pointeur de pile doit être incrémenté de 16 octets pour la restaurer. D'autres types de callback ont des nombres différents de paramètres. Le SDK de Windows donne les informations appropriées sur ceux-ci.
L'appelant peut utiliser ADD ESP, 10h pour ajouter les 16 octets nécessaires en retour d'appel, mais le plus simple est de confier ce réajustement à la procédure elle-même qui déplacera donc le pointeur de la pile avant de retourner à l'appelant en utilisant l'instruction RET suivie d'un nombre, par exemple RET 10h. Cette instruction obtient par un POP l'adresse de retour de l'appelant, incrémente le pointeur de pile (registre ESP) du nombre d'octets nécessaires - dans ce cas 16 -, puis renvoie finalement l'exécution à l'adresse de retour de l'appelant.
V-H-2-c. Création d'un espace sur la pile pour les données locales▲
Une autre tâche importante dévolue à la procédure callback est d'allouer, en cas de besoin, un espace aux données locales. Il s'agit de données qui sont conservées dans la pile pendant que l'exécution se poursuit dans la procédure callback. Elles sont perdues lorsque l'exécution quitte la procédure. Nous avons déjà vu comment les données sur la pile (sous la forme de paramètres) sont conservées en constituant une trame de pile. L'espace pour les données locales utilise le même principe. Supposons que nous ayons besoin d'espace pour trois DWords sur la pile parce que nous voulons préserver ces données cependant que la procédure de fenêtre est en récursion. Le code à utiliser pourrait être le suivant :
TypicalStackFrame
:
PUSH
EBP
; sauvegarde de EBP qui va être altéré ┐
MOV
EBP
, ESP
; EBP mémorise la valeur courante du pointeur de pile │ "prologue"
SUB
ESP
, 0Ch
; on constitue un espace pour les données locales ┘
; ; POINT "X"
;
; ; code de la procédure de fenêtre
;
MOV
ESP
, EBP
; rétablissement ancienne valeur du pointeur de pile ┐
POP
EBP
; restauration de EBP │ "épilogue"
RET
10h
; retour à l'appelant en ajustant le pointeur de pile ┘
Ici, nous avons déplacé le pointeur de pile de 12 octets, ce qui est exactement la même chose que si nous avions procédé à trois PUSH successifs. Ceci permet d'obtenir une zone de la pile qui ne peut être utilisée à d'autres fins.
En utilisant FRAME … ENDF vous pouvez créer automatiquement la trame de pile.
Voici une utilisation typique de FRAME … ENDF qui fait la même chose que TypicalStackFrame ci-dessus et fournit des noms pour les paramètres qui sont envoyés à la procédure de fenêtre et des noms pour chaque DWord de donnée locale :
WndProc FRAME hwnd, uMsg, wParam, lParam
LOCALS hDC, hInst, KEEP
; ; POINT "X"
;
; ; le code vient ici
;
RET
ENDF
La pile courante apparaît comme ceci au point « X » :
Bien sûr, si le pointeur de pile est soumis à ces déplacements, il va de soi qu'il doit être restauré en sortie. Mais cette fois, l'opération est automatique, car EBP est tout simplement restauré à sa valeur avant de retourner à l'appelant, et EBP avait du reste été enregistré avant le déplacement du pointeur de pile pour faire de la place à des données locales.
V-H-2-d. Préservation pour Windows des registres EBX, ESI, EDI et EBP▲
Enfin, il est important de rappeler que votre procédure de fenêtre se doit de préserver les registres EBX, ESI, EDI et EBP. EBP est déjà mémorisé puis restauré respectivement par les codes de prologue et d'épilogue. Quant à EBX, ESI et EDI, il est prudent de les sauvegarder puis les restaurer systématiquement même s'ils ne sont pas modifiés par votre procédure de fenêtre afin de prévenir une éventuelle modification ultérieure de votre code impactant un ou plusieurs de ces registres. Au reste, vous pouvez compter sur le fait quasi certain qu'une API Windows préservera ces registres. Cette caractéristique est particulièrement utile dans la programmation assembleur parce que vous pouvez conserver les handles utiles ainsi que d'autres valeurs dans ces registres tout en appelant des API. Vous pouvez facilement préserver EBX, ESI et EDI en recourant à l'instruction USES, ainsi que le montre l'exemple suivant :
WndProc FRAME hwnd, uMsg, wParam, lParam
USES
EBX
, ESI
, EDI
LOCALS hDC, hInst, KEEP
;
; ; le code est placé ici
;
RET
ENDF
V-H-3. Trames de piles avec Windows 64 bits ▲
Le travail qui incombe à la trame de pile est dicté par la convention d'appel utilisée. Windows 64 bits utilise la convention d'appel dite rapide (FASTCALL). Au lieu que l'appelant mette les paramètres sur la pile comme dans la convention STDCALL, les quatre premiers paramètres sont mis dans les registres RCX, RDX, R8 et R9. Les éventuels paramètres complémentaires sont mis sur la pile. Ainsi, la procédure de fenêtre doit-elle exécuter les tâches qui suivent :
- enregistrer sur la pile les paramètres envoyés dans les registres et accéder à tous les paramètres supplémentaires envoyés par Windows sur la pile ;
- fournir un espace sur la pile pour les données locales ;
- préserver pour Windows les registres dits « non volatils » dès qu'ils sont modifiés. Il s'agit de RBP, RBX, RDI, RSI, R12 à R15 et XMM6 à XMM15.
Il n'est pas nécessaire que la procédure de fenêtre restaure l'équilibre de la pile, avant de retourner à l'appelant. Ce travail est fait par l'appelant.
V-H-3-a. Enregistrement et accès aux paramètres▲
Les registres RCX, RDX, R8 et R9 sont qualifiés de « volatils » dans le sens où Windows ne garantit pas le maintien de leur intégrité au travers d'un appel d'API. Pour autant, ils accueillent les quatre premiers paramètres d'un appel d'API. Cela signifie que dès que votre procédure de fenêtre fait un appel d'API, il est possible que les paramètres soient écrasées. Pour cette raison, il est nécessaire d'en conserver trace quelque part. L'utilisation des registres non volatils pourrait être une solution, mais la documentation de Windows recommande qu'ils soient maintenus sur la pile. Apparemment, cela se gère dans les API elles-mêmes. Dans la convention d'appel FASTCALL telle que documentée et mise en œuvre dans Windows 64 bits, l'appelant est tenu de déplacer RSP négativement de 32 octets avant de faire l'appel afin de fournir un espace sur la pile pour que les paramètres soient sauvegardés. L'espace de pile ainsi réservé est appelé - qui l'eût cru ? - « espace réservé ». Chaque paramètre a son propre espace réservé connu. Évidemment, on pourrait se demander pourquoi FASTCALL a été choisi puisque les paramètres se retrouvent, de toute façon, sur la pile. Il se dit que cette situation traduirait une intention non aboutie de la part des programmeurs !
Pour faire face à l'obligation d'enregistrer les paramètres envoyés dans les registres, lorsque vous utilisez FRAME … ENDF sur une plateforme 64 bits, il faut savoir que GoAsm génère automatiquement des instructions qui prennent l'allure suivante au début de la trame de la pile :
MOV
[RSP+
8h
], RCX
MOV
[RSP+
10h
], RDX
MOV
[RSP+
18h
], R8
MOV
[RSP+
20h
], R9
PUSH
RBP
MOV
RBP, RSP
Ce code met les paramètres dans leur espace réservé sur la pile. S'il y a moins de quatre paramètres, ces instructions ne sont pas toutes émises. Notez que le cinquième paramètre, s'il existe, est déjà sur la pile à [RSP+28h], le sixième paramètre à [RSP+30h], etc. Les deux dernières instructions rétablissent RBP dans sa fonction de pointeur vers les données après l'avoir préalablement sauvegardé de telle sorte qu'il puisse être restauré ultérieurement.
Dans l'épilogue vous vous attendez à voir quelque chose comme :
LEA
RSP, [RBP]
POP
RBP
RET
L'instruction LEA est utilisée ici à la place d'un simple MOV RSP, RBP de manière à aider le gestionnaire d'exception Windows à identifier l'épilogue.
V-H-3-b. Construire un espace sur la pile pour les données locales▲
Cela fonctionne exactement de la même manière que pour une trame de pile 32 bits sauf que chaque élément de donnée locale doit être au moins de la taille d'un QWord. Ainsi, pour trois QWords de données locales, l'instruction destinée à créer la place nécessaire devrait s'écrire SUB RSP, 18h.
V-H-3-c. Préservation des registres non volatils pour Windows▲
RBP est déjà sauvegardé par le code du prologue puis restauré par celui de l'épilogue. Comme pour les registres à usage général, si les registres RBX, RDI, RSI et R12 à R15 sont modifiés par la procédure de fenêtre, ils auront besoin de voir leur valeur initiale restaurée. La meilleure façon de le faire est d'utiliser l'instruction USES qui les copie sur la pile. Les registres XMM6 à XMM15 peuvent être sauvegardés et restaurés en bloc, en utilisant les instructions du processeur FXSAVE et FXRSTOR qui n'agissent toutefois qu'au niveau de l'Unité de Calcul en virgule flottante (FPU).
Considérons maintenant l'utilisation assez classique de FRAME … ENDF qui suit :
WndProc FRAME hwnd, uMsg, wParam, lParam
USES
RBX, RSI, RDI
LOCALS hDC, BUFFER[256
]:B
; ; POINT 'X'
; ; le code s'écrit ici
;
RET
ENDF
Voici maintenant comment la trame de pile se présente en assemblage 64 bits, en utilisant les valeurs de RSP et RBP au début du code au POINT 'X' (notez que RBP affiche 32 octets de moins que RSP affichait en entrant dans la procédure : ce décalage est dû aux instructions PUSH portant successivement sur les registres RBP, RBX, RSI et RDI) :
V-I. Trames de pile automatisées utilisant FRAME … ENDF, LOCALS et USEDATA▲
V-I-1. Introduction▲
V-I-1-a. Les bases▲
FRAME … ENDF, implémenté dans GoAsm, est similaire au PROC … ENDP utilisé dans MASM tout en offrant beaucoup plus de possibilités. Les sous-programmes peuvent également utiliser les données sur la pile en utilisant USEDATA … ENDU. Et vous pouvez déclarer des données locales dynamiquement. Cela vous permet, à l'intérieur d'une procédure de fenêtre, de déclarer uniquement les données locales qui sont effectivement nécessaires à un message particulier. Vous pouvez utiliser des mots définis localement qui opèreront uniquement à l'intérieur de l'espace délimité par FRAME … ENDF ou des zones de USEDATA … ENDU qui leur sont associées.
La syntaxe est plus claire et le script source est beaucoup plus facile à comprendre, car il n'y a pas de contrôle de type ou de paramètre.
Voici une manière de procéder si vous utilisez FRAME … ENDF pour constituer une trame de pile automatisée :
WndProc FRAME hwnd, uMsg, wParam, lParam
USES
EBX
, ESI
, EDI
LOCALS hDC, BUFFER[256
]:B
;
; ; le code s'écrit ici
;
RET
ENDF
Lorsque vous utilisez FRAME … ENDF de cette manière, GoAsm crée une trame de pile à votre insu. Pour cette raison, il convient d'être un peu méfiant. Les programmeurs en assembleur aiment savoir tout ce qui se passe, et c'est d'ailleurs la raison majeure pour laquelle ils utilisent ce langage ! Nous allons donc décrire cette question en détail bien qu'il ne soit pas nécessaire d'en connaître les tenants et les aboutissants avec précision.
Si vous l'estimez nécessaire, vous pouvez passer sur ces détails et vous intéresser plutôt au fonctionnement de FRAME … ENDF dans l'exemple de programme Hello World 3 proposé en annexe.
Dans le code ci-dessus, FRAME prescrit à GoAsm de constituer une trame de pile automatisée dont ENDF marquerait la fin. Les mots après FRAME sont les paramètres. Dans notre cas, il y a quatre paramètres qui ont pour nom ceux qui sont indiqués. Il n'y a pas besoin d'ajouter quoi que ce soit d'autre puisque GoAsm connaît la taille des paramètres. En codage 32 bits, ils sont toujours en format DWord ; en codage 64 bits, ils sont toujours en format QWord.
USES désigne à GoAsm les registres qui ont besoin d'être préservés dans la trame. Ici, nous utilisons des registres 32 bits, mais en assemblage 64 bits, l'instruction se lit comme USES RBX, RSI, RDI sans avoir à modifier le code source (dans le cas de PUSH registre c'est le même opcode qui est généré pour chacune des deux plateformes).
LOCALS vous permet de déclarer un label pour les données locales dans la trame. GoAsm ajoute la taille de ces données locales et réserve l'espace à cet effet sur la pile. Lors de la déclaration des données locales, DWord est la valeur par défaut en assemblage 32 bits, QWord est la valeur par défaut en assemblage 64 bits. La valeur par défaut est utilisée si vous ne spécifiez pas une taille pour les données. Ainsi dans l'exemple, hDC est une valeur DWord. Il y a aussi une zone sur la pile appelée BUFFER (tampon). Celle-ci est de 256 octets en raison de la notation [256]:B. Au lieu de B, vous pourriez utiliser W, D, Q ou T pour, respectivement, Word, DWord, QWord ou Ten-Word. Vous pouvez également utiliser le nom d'une structure ainsi que le décrit la section utilisation des structures comme données locales dans une trame de pile.
GoAsm crée automatiquement le code de prologue comme décrit dans les sections 32 bits ou 64 bits ci-dessus. GoAsm va ajouter le code d'épilogue à chaque fois qu'il rencontre un RET dans la trame délimitée par FRAME … ENDF.
V-I-1-b. Accès aux paramètres et aux données locales à l'intérieur d'une trame automatisée▲
Dans une trame constituée de cette façon vous pouvez accéder aux paramètres envoyés à la procédure de fenêtre. Dans l'exemple suivant écrit pour Windows 32 bits, les offsets accolés à EBP qui sont générés par GoAsm sont donnés sur l'hypothèse qu'il n'y a pas de déclaration USES (le codage 64 bits est très similaire mais utilise RBP et chaque poste de pile occupe 8 octets au lieu de 4) :
PUSH
[hwnd] ; équivaut à PUSH [EBP+8h]
MOV
EAX
, [uMsg] ; équivaut à MOV EAX, [EBP+0Ch]
MOV
EBX
, ADDR
wParam ; équivaut à LEA EBX, [EBP+10h]
PUSH
ADDR
lParam ; équivaut à PUSH EBP suivi de ADD D[ESP], 14h
MOV
EBX
, [hDC] ; équivaut à MOV EBX, [EBP-10h]
MOV
EBX
, ADDR
BUFFER ; équivaut à LEA EBX, [EBP-110h]
Dans cette portion de code, on voit que GoAsm se charge de trouver sur la pile la bonne position de la variable à laquelle vous souhaitez accéder afin d'en lire ou modifier le contenu. Dès lors, votre seul souci se réduit à connaître le nom de la variable tout en faisant l'impasse sur sa position au sein de la pile. Si vous utilisez l'option /l sur la ligne de commande, vous pouvez vous rendre compte par vous-même de la réalité de ce codage en consultant le fichier-listing. Sinon, vous pouvez également l'observer en utilisant le débogueur.
Notez que l'adresse du buffer est donnée à son point le plus négatif. Il est donc correct de coder :
MOV
D[BUFFER+
10h
], 44h
si vous insérez la valeur 44h à un DWord dont l'octet le moins significatif est à 16 octets du début du buffer.
Notez que GoAsm définit la valeur de EBP après avoir poussé en pile (PUSHing) les registres mentionnés dans la déclaration USES. Cette disposition permet à l'ensemble des données locales d'être ajusté dynamiquement sur une base spécifique à un message. Mais cela signifie aussi que, si vous avez une déclaration USES dans un FRAME, l'offset des paramètres par rapport à EBP sera plus grand que le contraire. Ainsi, si vous mettez en pile (PUSH) trois registres dans une FRAME avec une déclaration USES comme suit :
USES
EBX
, EDI
, ESI
alors, EBP sera mis en pile plus loin et au-delà des paramètres avec un décalage de 12 octets. Donc, dans cet exemple, hwnd serait à [EBP+14h], Msg à [EBP+18h], wParam à [EBP+1Ch] et lParam à [EBP+20h]. Lors du codage vous ne devez pas vous soucier de la position exacte des paramètres relatifs à EBP, mais vous devez être au courant de cette particularité si vous examinez votre code dans le débogueur. Voir aussi la section ce que vous pouvez voir dans le débogueur.
V-I-1-c. Utilisation de structures comme données locales dans une trame de pile▲
Dans l'exemple qui suit, les données locales de taille adaptée à la structure RECT, préalablement déclarée dans votre script source, sont établies sur la pile.
RECT STRUCT
left DD
0
top DD
0
right DD
0
bottom DD
0
ENDS
;
WndProc FRAME hwnd, uMsg, wParam, lParam
LOCALS hDC, rc1:RECT
;
; ; le code s'écrit ici
;
RET
ENDF
Chaque élément de la structure RECT est accessible de la même façon que si elle était en données statiques. Par exemple (en utilisant encore des exemples 32 bits) :
MOV
EAX
, [rc1.right] ; équivaut à MOV EAX, [EBP-0Ch]
MOV
EAX
, [ESI
+
RECT.right] ; équivaut à MOV EAX, [ESI+8h]
MOV
EAX
, SIZEOF
RECT ; équivaut à MOV EAX, 10h
MOV
EAX
, ADDR
rc1.right ; équivaut à LEA EAX, [EBP-0Ch]
PUSH
[rc1.right] ; équivaut à PUSH [EBP-0Ch]
PUSH
ADDR
rc1.right ; équivaut à PUSH EBP suivi de ADD D[ESP], -0Ch
PUSH
ADDR
rc1 ; équivaut à PUSH EBP suivi de ADD D[ESP], -14h
V-I-2. Pratique des trames de pile automatisées▲
V-I-2-a. Quelques considérations pratiques▲
La manière par laquelle vous voudrez utiliser les facilités offertes par FRAME … ENDF sera une question de choix personnel comme nous allons le voir :
- Vous pouvez englober tout votre code de procédure de fenêtre dans une trame FRAME … ENDF. Dans ce cas, vous devrez vous assurer que les sous-routines utilisent un RET « normal » en utilisant RETN. Vous pouvez souhaiter maintenir la trame FRAME … ENDF aussi compacte que possible, mais accéder encore aux paramètres et données locales de l'extérieur. Ce sera possible en spécifiant une zone USEDATA … ENDU.
- Vous pouvez déclarer des données locales sous forme de message spécifique plutôt que pour la trame de pile dans son ensemble. Vous pouvez le faire en positionnant la déclaration LOCAL.
- Vous pouvez souhaiter combiner ces méthodes avec une table de messages pour créer une procédure de fenêtre réduite.
- Enfin, vous pouvez vouloir libérer les zones de données locales et construire ensuite de nouvelles zones de données locales avec l'instruction LOCALFREE.
V-I-2-b. Appel de procédures à l'intérieur d'une trame de pile - Usage de RETN▲
Vous pouvez avoir autant de points de retour de la procédure FRAME utilisant RET que vous le souhaitez. Chacun va produire un code d'épilogue qui sera exécuté au moment de quitter la trame de pile. Cependant, cela signifie également que, si vous avez des procédures additionnelles à l'intérieur de la trame de pile, vous devez veiller à les conclure par RETN (RET « normal ») pour éviter la création de code d'épilogue pour celles-ci. Il en résulterait en effet des dépilages inappropriés, source irrémédiable de plantage…
Seule la procédure FRAME principale peut être appelée depuis l'extérieur de celle-ci. L'appel à d'autres procédures peut entraîner des résultats imprévisibles. Ceci, parce que les procédures dans l'enveloppe FRAME … ENDF s'attendent à adresser des paramètres et des données locales en utilisant le pointeur de pile (RBP ou EBP) alors qu'il n'a pas été initialisé en cas d'appel de l'extérieur.
Voici un exemple concret :
WndProc FRAME hwnd, uMsg, wParam, lParam
USES
EBX
, ESI
, EDI
LOCAL
hDC, BUFFER[256
]:B
MOV
EAX
, [uMsg] ; récupération du message envoyé par Windows
CMP
EAX
, 0Fh
; on regarde si c'est WM_PAINT
JNZ
>
L2 ; non
CALL
WINDOW_PAINT ; on peint la fenêtre
XOR
EAX
, EAX
; renvoie zéro pour montrer que le message est traité
RET
; restauration de la pile et retour à Windows
;
L2
:
ARG [lParam], [wParam], [uMsg], [hwnd]
INVOKE
DefWindowProcA ; permet à Windows de traiter avec le message
RET
; restauration de la pile et retour à Windows
;
WINDOW_PAINT
:
; code pour peindre la fenêtre
RETN
; exécute un retour ordinaire de la procédure de peinture
;
ENDF ; stoppe toute action FRAME à partir de ce point.
V-I-2-c. Appel de procédures à l'extérieur d'une trame de pile▲
V-I-2-c-1. Utilisation de USEDATA…ENDU▲
Vous pouvez préférer conserver une trame plus compacte et ne procéder à des CALL qu'en direction de l'extérieur de celle-ci pour accroître la compacité de votre code et en améliorer ainsi la lisibilité. Vous pouvez le faire tout en conservant l'accès aux paramètres et aux données locales dans la trame en utilisant la déclaration de USEDATA suivie du nom de la trame concernée. Par exemple, le message WM_PAINT dans la trame ci-dessus pourrait appeler cette procédure :
PAINT
:
USEDATA WndProc
INVOKE
BeginPaint, [hwnd], ADDR
lpPaint ; récupère en EAX le DC à utiliser
MOV
[hDC], EAX
INVOKE
Ellipse, [hDC], [lpPaint.rcPaint.left], \
[lpPaint.rcPaint.top], \
[lpPaint.rcPaint.right], \
[lpPaint.rcPaint.bottom]
INVOKE
EndPaint, [hwnd], ADDR
lpPaint
XOR
EAX
, EAX
RET
ENDU
Nous venons de lister ici le code de la procédure PAINT qui utilise les données locales dans la FRAME appelée WndProc. Tout ce code est extérieur à l'enveloppe FRAME … ENDF.
Vous pouvez également utiliser USEDATA pour accéder aux données locales dans d'autres zones USEDATA … ENDU.
Assurez-vous que USEDATA est utilisée postérieurement dans le script source à toutes les déclarations de paramètres et de données locales sur lesquels elle repose. Ceci est dû au fait que GoAsm est un assembleur travaillant en une seule passe et qu'il a besoin, pour ce faire, de trouver la position de ces données pleinement définie.
Si une procédure appelée à partir d'une zone FRAME ou USEDATA n'a besoin d'accéder à aucun paramètre, donnée locale, ou mot définis localement, alors elle ne doit pas avoir sa propre déclaration USEDATA.
V-I-2-c-2. Points de sortie multiples ou procédure à l'intérieur d'une zone USEDATA … ENDU▲
Tout comme lors de l'utilisation de FRAME … ENDF, vous pouvez avoir autant de points de retour de la procédure USEDATA utilisant RET que vous le souhaitez. Chacun va produire le code d'épilogue approprié qui est exécuté au moment de quitter la zone USEDATA. Cependant, cela signifie également que si vous avez des procédures supplémentaires au sein de la zone USEDATA vous devez veiller à utiliser RETN (RET « normal ») pour éviter que l'assembleur ne leur crée un code d'épilogue.
Seule la première procédure USEDATA doit être appelée depuis l'extérieur de la zone USEDATA … ENDU. Ceci, parce que le code approprié pour accéder à la pile ne sera mis en place que pour cette première procédure.
V-I-3. Utilisation avancée des trames de pile automatisées▲
V-I-3-a. Déclaration de donnée locale à message spécifique▲
V-I-3-a-1. Positionnement de l'instruction LOCAL ▲
Dans les exemples exposés jusqu'ici, la zone des données locales localisée sur la pile avait été déclarée globalement pour le FRAME. Mais vous pouvez préférer mettre en place tout ou partie des données locales sur une base de message spécifique. Voici un exemple de cette façon de faire :
WndProc FRAME hwnd, uMsg, wParam, lParam
USES
EBX
, ESI
, EDI
LOCAL
hDC ; déclare hDC pour un usage de trame étendue
MOV
EAX
, [uMsg] ; récupération du message envoyé par Windows
CMP
EAX
, 0Fh
; est-ce WM_PAINT ?
JNZ
>
L2 ; non
CALL
WINDOW_PAINT ; c'est WM_PAINT, alors on peint la fenêtre
XOR
EAX
, EAX
; on retourne zéro pour signifier que le message a été traité
RET
; restauration de la pile et retour à Windows
;
L2
:
ARG [lParam], [wParam], [uMsg], [hwnd]
INVOKE
DefWindowProcA ; permet à Windows de traiter avec le message
RET
; restauration de la pile et retour à Windows
;
ENDF ; arrête toute action de FRAME à partir de ce point
;
WINDOW_PAINT
:
USEDATA WndProc ; utilise les paramètres et les données locales de WndProc
LOCAL
ps:PAINTSTRUCT ; construit des zones de données locales
LOCAL
BUFFER[1024
]:B ; spécifiquement à ce message
;
ARG ADDR
ps, [hwnd]
INVOKE
BeginPaint ; prêt à peindre la fenêtre
MOV
[hDC], EAX
; sauvegarde le contexte de périphérique dans la donnée locale hDC
; code pour peindre la fenêtre
RET
; effectue un retour ordinaire de la procédure de peinture
ENDU ; met fin à l'utilisation de la trame de données WndProc
V-I-3-b. Création d'une procédure de fenêtre réduite▲
En utilisant les méthodes décrites, vous pouvez créer une procédure de fenêtre en utilisant une table de messages. La procédure de fenêtre en tant que telle n'a pas besoin d'être plus complexe que ceci :
WndProc FRAME hwnd, uMsg, wParam, lParam
MOV
EAX
, [uMsg]
MOV
ECX
, SIZEOF
MESSAGES/
8
MOV
EDX
, OFFSET
MESSAGES
:
DEC
ECX
JS
>
.notfound
CMP
[EDX
+
ECX
*
8
], EAX
; est-ce le message correct ?
JNZ
<
; non, on va voir le suivant...
CALL
[EDX
+
ECX
*
8
+
4
] ; appel de la procédure correcte pour le message
JNC
>
.exit
.notfound
INVOKE
DefWindowProcA, [hwnd], [uMsg], [wParam], [lParam]
.exit
RET
ENDF
Quelque part dans les sections data ou const, on pourrait imaginer le tableau suivant pour les messages (dans la pratique, il y en aurait beaucoup plus que cela) :
MESSAGES DD
1h
, CREATE ; la valeur du message puis l'adresse du code
DD
2h
, DESTROY
DD
0Fh
, PAINT
NextLabel
:
Puis, dans la section de code (et après WndProc) vous pourriez avoir le code suivant pour le traitement de ces messages :
CREATE
:
USEDATA WndProc ; utiliser les données de la pile dans la trame
; de la procédure de fenêtre
USES
EBX
, EDI
, ESI
; préservation des registres pour Windows
LOCALS LocalData ; établissement de la zone de données locales requise
;
; code à exécuter sur le message WM_CREATE
;
XOR
EAX
, EAX
; retourne NC et EAX = 0 pour continuer à créer la fenêtre
RET
; restauration des registres puis RET
ENDU ; arrêt de toute action automatique et accès aux données
Dans la procédure de fenêtre réduite, DefWindowProc n'est pas appelée à moins que le message ne se trouve pas dans la table de messages ou que le code du message retourne le flag de Carry à 1. Certains messages doivent appeler DefWindowProc même s'ils sont traités par la procédure de fenêtre - Voir le SDK Windows à ce sujet.
V-I-3-c. Mots définis localement utilisant #localdef ou LOCALEQU▲
Dans une zone FRAME … ENDF vous pouvez utiliser des mots définis localement. La définition peut être effectuée soit dans la zone FRAME … ENDF elle-même, soit dans une zone USEDATA … ENDU associée.
Leur champ d'application est limité aux zones d'action de FRAME ou USEDATA. Voir la section héritage et portée d'action avec USEDATA … ENDU pour plus de détails sur la façon dont cela fonctionne en pratique.
Vous définissez ces mots locaux en utilisant #localdef (ou LOCALEQU si vous préférez - ils font la même chose).
Par exemple :
FrameProc1 FRAME Param
#localdef THING1 23h
THING2 LOCALEQU 88h
;
MOV
EAX
, THING1 ; définition locale 23h
MOV
EAX
, THING2 ; définition locale 88h
;
RET
ENDF
;
MyFunction44
:
USEDATA FrameProc1
#localdef THING3 0CCh
;
MOV
EAX
, THING1 ; la définition locale devrait être 23h
MOV
EAX
, THING2 ; la définition locale devrait être 88h
;
RET
ENDU
Dans l'exemple ci-dessus, si THING1 et THING2 sont définis globalement (en utilisant #define ou EQU), cette définition est ignorée (la définition locale est prioritaire).
#undef a une priorité de portée locale. Si le mot appelé à être indéfini se trouve sur localement, alors #undef s'applique à lui. Sinon, #undef s'appliquera à un label global.
V-I-3-d. Portée d'un label réutilisable dans les trames de pile automatisées▲
On peut accéder aux labels réutilisables commençant par un point n'importe où au sein d'une trame de pile automatisée (qui peut être établie à l'aide FRAME … ENDF). D'autres labels uniques au sein de la trame sont ignorés pour cet usage de sorte que, par exemple :
ExampleProc FRAME Param
CMP
EDX
, EAX
JZ
>
.fin
LABEL1
:
XOR
EAX
,EAX
.fin
RET
ENDF
LABEL2
:
Ici, le saut vers le label .fin fonctionnera encore malgré l'existence de LABEL1. En effet, le label .fin porte sur toute la trame et pas seulement sur le code entre LABEL1 et LABEL2. En d'autres termes, l'utilisation de FRAME … ENDF étend le champ d'application d'un label réutilisable à l'ensemble de la trame.
V-I-3-e. Héritage et portée avec USEDATA … ENDU▲
Une zone USERDATA … ENDU peut être associée soit directement avec une FRAME, soit avec une autre zone USERDATA … ENDU.
Cela permet aux utilisateurs expérimentés de sélectionner les données locales et les mots définis qu'une zone USEDATA peut utiliser.
La disposition habituelle est d'avoir, pour chaque zone de USEDATA, un enfant de FRAME :
FrameExample FRAME Param
LOCAL
LocalLabel1
#localdef CONSTANT 23h
;
RET
ENDF
;
Usedata#1
: USEDATA FrameExample
MOV
EAX
, [LocalLabel1]
MOV
EAX
, CONSTANT
MOV
EAX
, [Param]
RET
ENDU
;
Usedata#2
: USEDATA FrameExample
LOCAL
Specific
#localdef SPECIFIC_CONSTANT 444444h
MOV
EAX
, [LocalLabel1]
MOV
EAX
, CONSTANT
MOV
EAX
, [Param]
MOV
EAX
, [Specific]
MOV
EAX
, SPECIFIC_CONSTANT
RET
ENDU
Ici chaque zone USEDATA peut accéder aux paramètres de FRAME, aux données locales et aux mots définis. Notez cependant que Usedata#2 a ses propres données locales et mots définis. Ceux-là seuls peuvent être référencés dans Usedata#2. Si Usedata#1 avait essayé d'y accéder (ou le code dans FrameExample, d'ailleurs), ils n'auraient pas été trouvés.
Dans la représentation qui suit, la première zone USEDATA est l'émanation de FRAME et la seconde zone USEDATA est sa sous-émanation.
FrameExample FRAME Param
LOCAL
LocalLabel1
#localdef CONSTANT 23h
;
R ET
ENDF
;
Usedata#1
: USEDATA FrameExample
LOCAL
Specific
#localdef SPECIFIC_CONSTANT 444444h
RET
ENDU
;
Usedata#2
: USEDATA Usedata#1
MOV
EAX
, [LocalLabel1]
MOV
EAX
, CONSTANT
MOV
EAX
, [Param]
MOV
EAX
, [Specific]
MOV
EAX
, SPECIFIC_CONSTANT
RET
ENDU
Ici, bien que chaque zone USERDATA puisse accéder aux paramètres de la trame, aux données locales et aux mots définis, Usedata#2 peut également faire référence à des données locales et des mots définis localement dans Usedata#1.
V-I-3-f. Diffusion des données locales et constitution de nouvelles données locales▲
V-I-3-f-1. Utilisation de LOCALFREE▲
LOCALFREE est disponible uniquement pour les programmes 32 bits et n'est pas supporté par les modes x86 ou x64 en raison de l'information d'utilisation de la pile en prologue enregistrée pour la gestion des exceptions.
Vous pouvez utiliser LOCALFREE pour libérer les zones de données locales pour en constituer de nouvelles. Cela peut aider à économiser de la mémoire si vous utilisez beaucoup la pile. LOCALFREE rendra une donnée locale existante déclarée dans une FRAME ou une zone USEDATA … ENDU, dans laquelle elle apparaît, inaccessible à tout le code placé en aval dans votre script source. Il n'affectera pas les données locales dans d'autres zones FRAME ou USEDATA. Lorsque GoAsm rencontre LOCALFREE dans le script source, il provoque la restauration de ESP/RSP à sa valeur dans la zone FRAME ou USEDATA avant que toute donnée locale n'ait été déclarée. Vous pouvez ensuite déclarer de nouvelles données locales à l'aide de LOCAL ou LOCALS.
Utilisez LOCALFREE uniquement lorsque la pile est à l'équilibre. Ne l'utilisez pas s'il y a des PUSHes en suspens qui doivent être POPpés. En effet, le changement d'ESP/RSP effacera en pratique tout PUSH en suspens.
À la fin d'une procédure vous n'avez pas besoin d'utiliser LOCALFREE puisque la pile est restaurée automatiquement sur un RET, de toute façon.
Voici un exemple de la façon d'utiliser LOCALFREE :
CREATE
:
USEDATA WndProc ; utilisation des données de la pile dans la trame
; de la procédure de fenêtre
USES
RBX, RDI, RSI ; préservation des registres pour Windows
LOCALS BUFFER[4000
]:B ; établissement d'un grand buffer sur la pile
;
; part de code correspondant à l'exécution sur le message WM_CREATE
;
LOCALFREE ; effacement du grand buffer
LOCALS BUFFER[256
]:B ; établissement d'un buffer plus petit sur la pile
;
; part de code correspondant à l'exécution sur le message WM_CREATE
;
XOR
RAX, RAX ; retourne NC et RAX=0 pour continuer la création de la fenêtre
RET
; restauration des registres puis RET
ENDU ; met fin à toute action automatique et accès aux données
V-I-4. Considérations syntaxiques▲
V-I-4-a. Quelques points de syntaxe lors de l'utilisation de FRAME … ENDF▲
- La syntaxe de FRAME … ENDF est comme suit, avec les éventuelles variations mentionnées ci-dessous :
CodeLabel
:
FRAME Parameter List ; si des paramètres sont nécessaires
USES
Register List ; s'il est nécessaire de sauvegarder des registres
LOCAL
Local
List ; si des variables locales sont requises
;
ret
ENDF
- Une instruction FRAME doit être précédée par un label, soit immédiatement avant sur la même ligne, soit sur la ligne qui précède. Il s'agit du « nom de trame ».
- Une seule déclaration FRAME par trame est autorisée.
- Tous les paramètres doivent être immédiatement après la déclaration FRAME, séparés par des virgules. Pour poursuivre, le cas échéant, sur la ligne suivante, utiliser le caractère de continuation « \ ».
- Vous pouvez automatiquement sauvegarder et restaurer les registres à l'intérieur d'une trame avec l'instruction USES. ENDF arrête l'action de USES.
- Les sauts ne peuvent être que dans le FRAME lui-même. En effet, un FRAME a son propre code d'épilogue unique qui doit être mis en œuvre.
- Les CALLs peuvent s'adresser à l'extérieur de la trame. Si vous avez besoin d'accéder aux paramètres de la trame comme les données locales ou des mots définis, utilisez USEDATA.
- Si vous appelez une fonction dans la même trame, cette fonction doit utiliser RETN (RET « normal ») au lieu de RET. Cela empêche la génération d'un code d'épilogue au moment de quitter la fonction.
- Les données locales doivent être déclarées en utilisant une ou plusieurs instructions LOCAL. Après la première instruction de code suivante, vous ne serez pas en mesure d'utiliser LOCAL à nouveau à moins que vous n'ayez libéré les données locales existantes en utilisant l'instruction LOCALFREE.
- LOCALFREE doit être suivie d'une instruction LOCAL ou LOCALS.
- Vous ne pouvez utiliser l'instruction LOCALFREE que si la pile est à l'équilibre (pas de PUSH en suspens devant être corrigé par un POP).
- LOCALFREE n'est pas autorisé dans les modes x86 ou x64.
- Vous pouvez utiliser LOCALS à la place de LOCAL si vous préférez.
- Vous ne pouvez pas avoir une déclaration USEDATA à l'intérieur d'une trame.
- Les labels de portée locale (dont le nom commence par un point) vont travailler n'importe où dans le cadre FRAME … ENDF. Leur limite de portée est la trame et l'instruction ENDF elles-mêmes, et, en tout cas, aucun autre label qui apparaît dans la trame.
- Fermez la trame à l'aide de ENDF (ou ENDFRAME si vous préférez), et éventuellement, faites précéder cette instruction par le nom de la trame concernée.
- Dans la mesure où GoAsm est un assembleur travaillant en une seule passe, les données locales doivent être déclarées dans le script source avant d'envisager leur utilisation.
- Puisque GoAsm s'appuie sur EBP/RBP dans une trame pour accéder aux paramètres et données locales sur la pile, ne pas modifier ces registres dans la trame ou les procédures appelées par la trame à moins que cet accès ne soit pas nécessaire dans la procédure. Si EBP/RBP est changé toujours restaurer sa valeur par la suite.
- Une trame peut en appeler une autre et lui passer ainsi les paramètres sur la pile, mais, dans la mesure où EBP/RBP seront modifiés dans ce processus, les données originales de la pile ne seront pas accessibles dans la trame appelée.
V-I-4-b. Quelques points de syntaxe lors de l'utilisation de USEDATA … ENDU▲
- La syntaxe de USEDATA … ENDU est comme suit, avec les éventuelles variations mentionnées ci-dessous :
CodeLabel
:
USEDATA SourceData
USES
Register List ; si les registres ont besoin d'être sauvegardés
LOCAL
Local
List ; si des variables locales sont requises
;
ret
ENDU
- Une instruction USEDATA doit être précédée par un label, soit immédiatement avant, soit sur la ligne qui précède. Il s'agit du « nom USEDATA ».
- SourceData peut être un nom de trame ou le nom d'une procédure USEDATA.
- Si SourceData est un nom de trame, les paramètres et les données locales établies dans la trame seront accessibles dans la zone USEDATA … ENDU.
- Si SourceData est le nom d'une procédure USEDATA, alors tous les paramètres de données locales et de mots définis qui étaient accessibles au sein de cette procédure peuvent être consultés.
- Vous pouvez automatiquement sauvegarder et restaurer des registres dans une zone USEDATA avec l'instruction USES … ENDU arrête l'action de USES.
- Les sauts ne peuvent être que dans la zone USEDATA elle-même. En effet, une zone USEDATA a son propre code d'épilogue unique qui doit être mis en œuvre.
- Les CALLs peuvent s'adresser à l'extérieur de la zone de USEDATA. Si vous avez besoin d'accéder aux paramètres de données locales ou de mots définis de la zone USEDATA, constituez une autre zone USEDATA … ENDU.
- Si vous appelez une fonction dans la même zone USEDATA, cette fonction doit utiliser RETN (RET « normal ») au lieu de RET. Cela empêche la génération du code d'épilogue au moment de quitter la fonction.
- Les données locales doivent être déclarées en utilisant une ou plusieurs instructions LOCAL. Après la première instruction de code suivante, vous ne serez pas en mesure d'utiliser LOCAL à nouveau à moins que vous n'ayez libéré les données locales existantes en utilisant l'instruction LOCALFREE.
- LOCALFREE doit être suivie d'une instruction LOCAL ou LOCALS.
- Vous ne pouvez utiliser l'instruction LOCALFREE que si la pile est à l'équilibre (pas de PUSH en suspens devant être corrigé par un POP).
- LOCALFREE n'est pas autorisé dans les modes x86 ou x64.
- Vous pouvez utiliser LOCALS à la place de LOCAL si vous préférez.
- Une procédure USEDATA peut en appeler une autre sans perte de données puisque EBP/RBP n'est pas modifié.
- Les labels de portée locale (dont le nom commence par un point) fonctionnent normalement dans les zones USEDATA. Les labels de codes constituent leur frontière de portée.
- Au lieu d'utiliser ENDU pour fermer la zone USEDATA, vous pouvez utiliser ENDUSEDATA en lieu et place. En option, pour des questions de clarté du script, le nom du USEDATA peut précéder cette déclaration.
- Dans la mesure où GoAsm est un assembleur travaillant en une seule passe, les données locales doivent être déclarées dans le script source avant d'envisager leur utilisation.
- Puisque GoAsm s'appuie sur EBP/RBP dans une zone USEDATA pour accéder aux paramètres et aux données locales sur la pile, ne pas modifier ces registres dans la zone USEDATA ou les procédures appelées au sein de la zone USEDATA à moins que cet accès ne soit pas nécessaire dans la procédure. Si EBP/RBP est changé toujours restaurer sa valeur par la suite.
V-I-4-c. Ce que vous pouvez voir dans le débogueur▲
Pour établir et utiliser des trames de pile automatisées, GoAsm génère du code supplémentaire. Lorsque vous examinez votre code dans le débogueur ce code supplémentaire peut être source de confusion et gêner l'identification du code que vous recherchez. Une façon de voir ce que GoAsm a inséré est de regarder le fichier listing produit à l'issue de l'assemblage (option /l dans la ligne de commande).
Voici une brève description de quelques lignes de code supplémentaires que vous pourrez observer.
Dans les trames 32 bits (FRAME), GoAsm commence par mettre en pile EBP et les registres mentionnés par USES, puis met la valeur du pointeur de pile ESP dans EBP par MOV EBP, ESP. Sur un RET, cet ordre est inversé, de sorte que vous verrez MOV ESP, EBP suivi par un ou plusieurs POPs de registre. Un espace est constitué pour les données locales par un simple SUB ESP, x où x représente le chiffrage en octets de l'espace requis pour les données locales.
Dans des trames 64 bits (FRAME), GoAsm commence par mémoriser sur la pile les paramètres n° 1 à n° 4 à l'aide d'une instruction telle que MOV [RSP+8h], RCX comme décrit précédemment. Puis après avoir mis en pile RBP et registres prévus par USES, MOV RBP, RSP est utilisée pour allouer à RBP la capacité d'adresser la trame de pile. En sortant de FRAME, l'instruction LEA RSP, [RBP] est utilisée pour restaurer RSP prêt à effectuer un POP des registres et, finalement, un RET.
Le code tel que MOV EAX, [EBP-34h] ou LEA, [EBP-56h] ou PUSH EBP, ADD D[ESP], -60h (ou leurs équivalents utilisant des registres de 64 bits) sera généré au moment de l'accès aux données locales. Les valeurs d'offset seront positives lors de l'accès aux paramètres.
Dans les zones USEDATA, comme GoAsm ne connaît pas, au moment de l'assemblage, le nombre d'utilisations de la pile qu'il y a eu avant l'appel à la procédure USEDATA, il constitue une sorte de bouclier de 100h octets (200h octets en assemblage 64 bits) pour garantir que pareille utilisation de la pile est protégée contre les sur-écritures. Pour cette raison, le nombre de décalages utilisés lors de l'accès aux données locales peut être plus grand que prévu.
Les utilisateurs expérimentés pourront procéder comme pour ajuster la taille de l'écran. Cela peut être fait en utilisant cette syntaxe, par exemple :
USEDATA WndProc SHIELDSIZE:20h
; en assemblage 32 bits
USEDATA WndProc SHIELDSIZE:40h
; en assemblage 64 bits
Ceci restreint la protection à seulement huit valeurs poussées en pile (huit DWords en assemblage 32 bits, huit QWords en assemblage 64 bits), ce qui serait approprié si vous étiez certain qu'au moment de l'exécution il n'y aurait jamais plus de sept PUSHes et un CALL avant la déclaration de données locale dans la procédure USEDATA. Rappelez-vous que vous devez compter tous les PUSHs avant le CALL, le CALL lui-même et tous les sous-appels, ainsi que tout PUSH causé par la déclaration des utilisations dans la procédure de USEDATA. Une fois que SHIELDSIZE est définie, il reste à cette valeur pendant le reste du script source jusqu'à leur modification.
Dans les zones USEDATA, la valeur du pointeur de pile n'est pas conservée dans le registre EBP/RBP car celui-ci héberge déjà le pointeur de la pile à l'entrée dans la trame. Au lieu de cela, GoAsm maintient la valeur du pointeur de pile à un endroit convenable sur la pile. Cela se fait lorsque les premières données locales de la zone USEDATA sont déclarées. De manière à le faire en toute sécurité, GoAsm ajoute plusieurs lignes de code aboutissant à MOV EAX, [EAX-4h] (ou MOV RAX, [RAX-8h] en assemblage 64 bits). GoAsm utilise EAX/RAX au cours de ce processus, mais restaure sa valeur après coup, de sorte que vous pouvez toujours l'utiliser pour transmettre des informations à la procédure. Sur un RET vous verrez la valeur du ESP/RSP en cours de restauration en utilisant POP ESP ou POP RSP selon le cas.
LOCALFREE provoquera aussi une restauration du pointeur de pile.
Les instructions USES provoqueront des PUSHs des registres concernés avant que ESP/RSP ne soit sauvegardé et des POPs de registres après sa restauration.
V-J. Assemblage conditionnel▲
V-J-1. Qu'est-ce que l'assemblage conditionnel et pourquoi est-il utilisé ?▲
L'assemblage conditionnel vous permet de sélectionner, lors de la phase d'assemblage, la partie de votre script source que vous souhaitez voir assemblée. Cela peut être utile si, par exemple, vous voulez constituer différentes versions de votre programme à partir d'un même script source.
V-J-2. Les directives conditionnelles▲
Dans ce domaine, GoAsm utilise simplement la syntaxe du langage C qui est basée sur les directives #if, #ifdef, #else, #elif (ou #elseif) et #endif. La syntaxe de la structure de base d'une directive conditionnelle dans sa forme la plus simple est la suivante :
#if
condition
text A
#endif
Ici, si la condition est VRAIE, le texte A sera assemblé. A contrario, si la condition est FAUSSE, l'assembleur ignorera le texte A et poursuivra la compilation à partir de #endif.
Vous pouvez ajouter quelque chose à faire si la condition est FAUSSE de cette manière :
#if
condition
text A
#else
text B
#endif
Ici, si la condition est VRAIE, texte A sera assemblé, mais pas texte B.
A contrario, si la condition est FAUSSE, texte A sera ignoré et seul texte B sera assemblé.
Le #endif indique la fin de la trame conditionnelle, de sorte que tout le texte au-delà sera assemblé normalement.
La déclaration #else doit toujours précéder #endif.
Vous pouvez ajouter une condition supplémentaire à la structure :
#if
condition1
text A
#elif condition2
text B
#endif
Ici, si condition1 est VRAIE, texte A sera assemblé, texte B sera ignoré et l'assemblage se poursuivra à partir du #endif. Toutefois, si condition1 est FAUSSE, texte A ignoré jusqu'à #elif précédant le test de condition2. Si donc condition2 est VRAIE, texte B sera assemblé.
Notez, au passage, que « #elif » est identique à « #elseif ».
L'ajout du #else à la trame conditionnelle ci-dessus produit :
#if
condition1
text A
#elif condition2
text B
#else
text C
#endif
Ici, si condition1 est VRAIE, texte A sera assemblé tandis que texte B et texte C seront ignorés et l'assemblage se poursuivra à partir du #endif. Toutefois, si condition1 est FAUSSE, texte A sera ignoré jusqu'à #elif testant condition2. Si donc condition2 est VRAIE, texte B sera assemblé et texte C ignoré ; si, toutefois condition2 est FAUSSE, texte B sera ignoré jusqu'au #else qui fera que texte C sera assemblé.
Vous pouvez avoir autant de #elif (ou #elseif) que vous le souhaitez dans chaque trame conditionnelle, mais il ne peut y avoir qu'un #else par trame, et chaque #if doit avoir un #endif correspondant. Certains programmeurs imbriquent les trames conditionnelles, mais cela peut devenir très confus et ne peut constituer une bonne pratique de programmation. Si toutefois ce choix est retenu, il est recommandé que vous étiquetiez chaque #endif avec un commentaire de sorte que vous puissiez voir à quel #if il se réfère.
V-J-3. Types d'instructions #if▲
#ifdef
identifier
où identifier est un mot qui peut être défini dans le script source ou dans un fichier d'inclusion. Cette déclaration renvoie VRAI si identifier est défini et FAUX dans le cas contraire. identifier doit être un mot et pas un nombre, ni une chaîne entre guillemets.
#ifndef
identifier
comme ci-dessus, mais cette déclaration retourne FAUX si identifier est défini et VRAI s'il n'est pas défini.
#if
expression
où expression peut être un nombre, la déclaration retournant alors FAUX pour 0, et VRAI pour les valeurs non nulles.
où expression peut être un identifier qui évalue un nombre.
où expression peut être l'opérateur défini utilisé comme suit :
defined identifier
defined(identifier)
qui renvoie VRAI (1) si l'identifiant est défini, et FAUX (0) s'il n'est pas défini, de manière similaire à #ifdef.
L'opérateur ! (point d'exclamation) peut être utilisé devant ces expressions simples pour inverser le résultat de la condition, de sorte que par exemple :
#if !0 retournerait VRAI, et
#if !defined identifier retournerait FAUX (0) si identifier était défini, et VRAI (1) dans le cas contraire, de manière similaire à #ifndef.
où expression peut être plus complexe et revêtir la forme :
identifier opérateur-relationnel value
identifier doit être un mot défini ailleurs dans le fichier, dans un fichier inclus ou dans la ligne de commande. Il ne peut pas être un nombre.
L'opérateur relationnel peut être l'un des éléments suivants :
|
supérieur ou égal à |
|
inférieur ou égal à |
|
égal à |
|
égal à |
|
différent de |
|
supérieur à |
|
inférieur à |
value peut être un nombre ou un mot qui est défini ailleurs dans le fichier, dans un fichier inclus ou dans la ligne de commande, qui évalue un nombre.
où expression peut être plus complexe, combinant plusieurs expressions avec l'opérateur AND conditionnel && ou l'opérateur conditionnel OR ||:
expression1 &&
expression2
expression1 ||
expression2
où expression1 et expression2 sont l'un des types d'expressions précédents.
L'instruction && renvoie VRAI si expression1 et expression2 sont VRAIES.
Si expression1 est fausse, alors expression2 est pas évaluée.
L'instruction || renvoie VRAI si expression1 ou expression2 est VRAIE.
Si expression1 est VRAIE, alors expression2 n'est pas évaluée.
Notez que, pour les expressions multiples avec plusieurs opérateurs conditionnels, l'évaluation de la déclaration est effectuée avec un traitement simple de gauche à droite. Normalement && a préséance sur ||. Donc si vous placez ces expressions conditionnelles avec && en premier, vous devez obtenir des résultats similaires.
V-J-4. Exemples d'assemblage conditionnel▲
#define HELLO
;
#ifdef
HELLO
BSWAP
EAX
; inverse l'ordre des octets dans EAX si HELLO est défini
#endif
#if
HELLO==
3
OUTPUT DD
3h
; si HELLO est défini comme 3, déclarer le label de donnée OUTPUT à 3
#elif WINVER>=
400h
OUTPUT DD
4h
; donnée alternative data si WINVER est égal ou plus grand que 400h
#else
OUTPUT DD
5h
; donnée alternative si aucun des cas ci-dessus n'est avéré
#endif
Vous pouvez définir un mot dans la ligne de commande permettant de déclencher la prise en compte des bonnes parties de votre script source pour l'assemblage. Par exemple :
GoASM /l /d WINVER
=
401h MyProg.ASM
ou simplement
GoASM /l /d VERSIONA MyProg.ASM
signifie que le mot VERSIONA sera défini et qu'il pourra être testé par #ifdef.
Voir également :
V-K. Inclusion de fichiers - #include et INCBIN▲
L'inclusion de fichiers contenant du code assembleur ou simplement des structures et des définitions peut être une alternative intéressante pour opacifier votre script source en le rendant difficile à lire et à suivre. En effet, il faut du temps au lecteur pour se plonger dans le fichier numéro 2 pour comprendre le dossier numéro 1 et réciproquement. Néanmoins les fichiers contenant des structures et des définitions Windows sont populaires et GoAsm offre un support complet pour inclure des fichiers.
GoAsm distingue deux types de fichiers à inclure :
Type | Effet |
Fichiers avec une extension en « a » ou « A », par exemple MyInclude.asm, ou simplement MyInclude.a |
Avec ce type de fichier, au moment où l'inclusion est déclarée dans votre script source, l'assemblage est détourné dans le fichier d'inclusion. Et si vous produisez un fichier listing à l'aide de l'option /l, vous constaterez que le contenu du fichier inclus apparaît dans le listing général du programme. Ne pas utiliser ce type de fichier si votre fichier à inclure ne contient que des définitions, structures et autres. Cela va ralentir GoAsm inutilement, car il va chercher des mnémoniques et instructions assembleur dans ledit fichier. |
Fichiers sans extension en « a » ou « A », par exemple MyInclude.inc, ou simplement MyInclude |
Avec ce type de fichier, aucun assemblage n'est effectué dans le fichier d'inclusion. Seules les définitions et les structures dans ce fichier sont examinées et enregistrées. Si vous produisez un fichier listing à l'aide de l'option /l, vous constaterez que le contenu du fichier inclus n'y apparaîtra pas. Utilisez cette extension si votre fichier d'inclusion ne contient que des définitions, des structures et fonctionnalités similaires (communément appelés fichiers d'en-tête). GoAsm fera un enregistrement de tout cela dans le cas où ils seraient appelés plus tard dans le script source principal. Pour cette raison, un grand fichier d'inclusion va ralentir GoAsm. Normalement GoAsm ne permet pas d'ouvrir des fichiers à inclure qui ne seraient pas dotés d'une extension en « a » ou « A ». Une telle restriction contribue à faciliter la vérification des erreurs. Mais il vous est possible de contourner cette fonctionnalité si vous voulez permettre, par exemple, aux mêmes fichiers d'en-tête d'être disponibles dans un environnement de compilation parallèle. Pour ce faire, il vous suffit de spécifier le commutateur /sh (fichiers « share header ») dans la ligne de commande. |
V-K-1. Syntaxe pour #include▲
#include path\filename
path\filename peut être, soit :
- une chaîne entre guillemets ;
- une chaîne sans guillemets ;
- une chaîne de caractères encadrée par les symboles « < » et « > ».
GoAsm va chercher le fichier en utilisant le chemin d'accès spécifié. Si aucun chemin n'est spécifié, il va le chercher dans le répertoire courant. Si le fichier reste malgré tout introuvable, il va le chercher dans le répertoire fourni par la chaîne d'environnement INCLUDE. Vous pouvez paramétrer la chaîne d'environnement INCLUDE en utilisant la commande DOS SET dans la fenêtre MS-DOS (invite de commande), ou en appelant l'API SetEnvironmentVariable. Vous pouvez également utiliser le panneau de configuration (paramètres système avancés, variables d'environnement) si votre système d'exploitation le permet, le tout suivi d'un redémarrage.
Il peut y avoir une chaîne d'environnement différente pour chaque répertoire ou sous-répertoire. Assurez-vous que la chaîne d'environnement que vous souhaitez utiliser est dans le répertoire courant.
Vous pouvez imbriquer des #include file mais il est de bonne pratique d'éviter cela autant que possible.
V-K-2. Chargement d'un fichier avec INCBIN▲
INCBIN vous permet de charger des morceaux de matériau à partir d'un fichier directement dans une section de données ou de code sans autre traitement. Vous pouvez choisir le nombre d'octets à sauter dès le début du fichier et/ou la quantité à charger à partir du fichier. Voici des exemples de la façon d'utiliser INCBIN :
DATA SECTION
BULKDATA INCBIN
MyFile.txt ; charge l'ensemble de MyFile.txt dans la section de
; données avec le label BULKDATA
INCBIN
MyFile.txt, 100
; évite les 100 premiers octets mais charge le reste du fichier
INCBIN
MyFile.txt, 100
, 300
; évite les 100 premiers octets mais charge 300 octets
Voir également la section insertion de blocs de données.
V-L. Fusion (merging) - Utilisation de bibliothèques de code statiques (.lib)▲
V-L-1. Que sont les « bibliothèques de code statiques » ?▲
Les bibliothèques de code statiques sont des fichiers avec l'extension « .lib » contenant un ou plusieurs fichiers objet COFF. Ces fichiers objet contiennent du code et des données relatifs à des fonctions prêtes à l'emploi. Le matériau à l'intérieur du fichier de bibliothèque est au format binaire (code machine), et non du code source. Les fichiers de bibliothèque contiennent un index avec une liste des fonctions et les labels de code et de données qu'ils utilisent. Les bibliothèques de code statiques doivent être distinguées des bibliothèques liées dynamiquement (DLL) et de bibliothèques d'importation qui contiennent simplement une liste des fonctions exportées par DLL.
V-L-2. Comment utiliser une bibliothèque de code statique ?▲
Dans GoAsm, vous pouvez utiliser du code et des données prêts à l'emploi dans des bibliothèques de code statiques simplement en appelant la fonction requise dans votre script source, en mentionnant le nom de la bibliothèque contenant cette fonction. Par exemple :
CALL
zlibstat.lib:compress
Vous pouvez également utiliser des equates pour simplifier le libellé du CALL, par exemple :
LIB1=
c:\prog\libs\zlibstat.lib
CALL
LIB1:compress
En utilisant INVOKE, ces exemples deviennent :
INVOKE
zlibstat.lib:compress, [pCompHeap], ADDR
ComprSize, [pHeap], [DataSize]
INVOKE
LIB1:compress, [pCompHeap], ADDR
ComprSize, [pHeap], [DataSize]
Si votre chemin d'accès contient des espaces, vous devez mettre le chemin d'accès et le nom de fichier entre guillemets.
V-L-3. Qu'advient-il lorsque vous appelez une fonction issue d'une bibliothèque ?▲
Le codage qui précède enjoint à GoAsm de charger le code et les données associés et de les fusionner avec le fichier de sortie de GoAsm (fichier objet) lors de l'assemblage. Vous pouvez ensuite envoyer le fichier de sortie au linker de la manière habituelle, mais l'éditeur de liens n'est pas concerné du tout par le code et les données source (le fichier de bibliothèque) : l'ensemble du code et des données associées à la fonction ont déjà été chargés par GoAsm. Le seul travail supplémentaire que vous pourriez avoir besoin de faire au moment de la phase d'édition de liens est de veiller à ce que le linker soit informé de toutes les DLL supplémentaires nécessitées par les fonctions prêtes à l'emploi. Par exemple, une fonction prête à l'emploi peut appeler une API Windows dans OLEAUT32.dll. Afin d'éviter une erreur de « symbole introuvable », il est indispensable de mentionner cette DLL dans la ligne de commande de GoLink. Si vous utilisez un autre éditeur de liens, vous devez ajouter la bibliothèque d'importation pour OLEAUT32.dll à la liste des bibliothèques d'importation donnée à l'éditeur de liens.
V-L-4. Et la méthode Microsoft dans tout ça ?▲
La méthode de GoAsm concernant l'utilisation de bibliothèques de code statiques diffère très sensiblement de celle utilisée par les outils Microsoft. Le MS Linker ajoute le code et les données au moment de l'édition des liens. Si vous préférez, vous pouvez toujours utiliser cette méthode avec GoAsm. Pour ce faire vous devez charger les fichiers de sortie de GoAsm dans MS Link et demander à ce dernier de rechercher les fonctions dans la bibliothèque de code statique appropriée.
Voir la section utilisation de GoAsm avec différents linkers.
V-L-5. Comment GoAsm trouve le fichier LIB approprié▲
Si le chemin d'accès du fichier .lib n'est pas donné dans l'appel, GoAsm conduira ses recherches dans le répertoire courant et dans celui donné dans l'environnement LIB. Vous pouvez paramétrer la chaîne d'environnement LIB à l'aide de la commande DOS SET dans la fenêtre MS-DOS (invite de commande), en appelant l'API SetEnvironmentVariable ou en utilisant le panneau de configuration (paramètres système avancés, variables d'environnement) si votre système d'exploitation le permet, le tout suivi d'un redémarrage.
Il peut y avoir une chaîne d'environnement différente pour chaque répertoire ou sous-répertoire. Assurez-vous que la chaîne d'environnement que vous souhaitez utiliser est dans le répertoire courant.
Vous avez besoin de spécifier une seule fois le chemin d'accès du fichier lib dans votre script source (dans un appel à un fichier lib) et GoAsm l'utilisera automatiquement pour tous les fichiers lib spécifiés du même nom.
V-L-6. Usage de JMP au lieu de CALL/INVOKE▲
Vous pouvez effectuer un saut vers une fonction de la manière suivante :
JMP
MyLib.lib:MainProc
Cette méthode peut être employée si, par exemple, le code de la bibliothèque contient un call à l'API ExitProcess pour mettre fin au programme.
V-L-7. Visualisation du contenu d'un fichier LIB▲
Il est très utile de pouvoir identifier les fonctions disponibles dans les bibliothèques de code statiques. Il existe un certain nombre d'outils disponibles à cet effet, mais l'un des plus utiles est PEView de Wayne J. Radburn, qui vous permet de visualiser le contenu de différents fichiers y compris les fichiers lib. Vous pouvez voir à partir de cet outil que les membres individuels des bibliothèques de code statiques sont toujours des fichiers « .obj », mais que les membres individuels de bibliothèques importées sont toujours des fichiers « .dll ».
DUMPBIN est un utilitaire Microsoft qui est généralement associé à MASM et aux compilateurs C. La ligne de commande LINK -dump (utilisant donc l'éditeur de liens Microsoft) est fonctionnellement identique et fournit une liste d'options si elle est utilisée sans autres paramètres également. Il existe diverses options, mais par exemple,
DUMPBIN /LINKER MEMBER: 1
MYLIB.LIB>
MyLib.dmp
donnera (dans le fichier MyLib.dmp) des informations sur le premier membre du fichier de bibliothèque et DUMPBIN /ALL MyLib.lib donnera des informations sur tous les membres.
PEDUMP, enfin, est un utilitaire écrit par Matt Pietrek qui peut visualiser, octet par octet, le contenu d'un fichier PE (y compris un fichier lib) d'une manière ordonnée (voir également LIBDUMP du même auteur).
V-L-8. Vos propres fichiers LIB▲
L'utilitaire Microsoft LIB.EXE, qui repose sur LINK.EXE et aussi MsPDB50.dll, permet d'élaborer des fichiers de bibliothèque de code statique. Supposons que vous ayez un fichier objet appelé calculate.obj qui contient une fonction que vous souhaitez réutiliser. Il vous est possible d'en faire une bibliothèque au moyen de la syntaxe de ligne de commande suivante, par exemple :
LIB calculate.obj
Vous obtiendrez ainsi calculate.lib. Et, pour ajouter un autre fichier objet à cette même bibliothèque, vous pouvez procéder comme suit :
LIB calculate.lib added.obj
Cela va ajouter added.obj à la bibliothèque calculate.lib. Ceci est utile si vous voulez garder vos fonctions dans les bibliothèques, afin qu'elles puissent être réutilisées sans avoir à écrire à nouveau le code dans vos scripts source. Ces bibliothèques seront également utiles pour distribuer vos fonctions tout en conservant votre code source à votre seule discrétion. LIB.EXE et ses composants font partie des outils MSDN qui peuvent être téléchargés gratuitement à partir du site Microsoft MSDN (partie du SDK). Le téléchargement exact ne cesse de changer de sorte que des tâtonnements seront peut-être nécessaires pour obtenir ces fichiers. Il est fort probable également que LIB soit rangé du côté d'outils de compilation tels que VC++ ou MASM.
V-L-9. Augmentation de taille de la bibliothèque de code statique▲
L'appel d'une fonction dans une bibliothèque de code statique agrandit le fichier de sortie GoAsm au prorata du code et des données associés de la fonction. Souvent, du reste, la fonction dépend elle-même d'autres fonctions au sein du même fichier de la bibliothèque, ce qui entraîne encore plus de code et de données à charger. Vous pouvez visualiser ce qui est chargé en utilisant le commutateur /l (édition d'un fichier listing) dans la ligne de commande de GoAsm et en examinant le fichier listing résultant. Malheureusement, certains code et données peuvent être chargés tout en n'étant pas effectivement utilisés. Vous pouvez néanmoins visualiser les labels de code et de données inutilisés à l'aide du commutateur /unused de GoLink.
V-L-10. Callbacks et dépendance à l'égard des données▲
Certaines fonctions de fichiers de bibliothèque s'attendent à trouver des labels spécifiques de code et de données à l'exécutable qui doivent donc impérativement faire partie de votre script source. Par exemple la procédure de callback RegisterDialogClasses et ses variables de données associées doivent être établies dans votre script source afin d'utiliser SCRNSAVE.LIB pour faire un économiseur d'écran.
V-L-11. Cas des données sans code▲
Les bibliothèques sont destinées à donner accès à des fonctions au moment de la compilation plutôt que simplement des données. Toutefois, si vous voulez charger une bibliothèque particulière au moment de la compilation de sorte que vous puissiez accéder à ses données, vous pouvez appeler un label de code approprié dans un code inutilisé figurant dans votre script source (le label de code n'est jamais appelé). Par exemple :
CALCULATE1
:
; lignes de code ici
RET
CALL
Lib1:DUMMY ; garantit que Lib1 est chargé au moment de la compilation
CALCULATE2
:
; lignes de code ici
RET
V-L-12. Intégration du code et des données, priorités attribuées aux labels de même nom▲
Si vous constituez vos propres fichiers lib, il vous sera utile de comprendre comment le code et les données de bibliothèque sont intégrés au code et aux données résultant du script source principal. Lors de l'utilisation des bibliothèques de code, il est courant de rencontrer des noms de labels identiques, et si cela se produit lors du chargement d'une bibliothèque, GoAsm utilise seulement l'information associée au tout premier label et ignore celle relative aux autres labels de même nom.
Supposons, par exemple, que vous ayez une zone de données appelé « BUFFER » déclarée soit dans le script source principal, soit dans la bibliothèque. Il peut y avoir plusieurs fonctions dans le script source principal ou dans les différents composants de bibliothèque ou même dans d'autres bibliothèques qui pourraient utiliser BUFFER.
Alors, où BUFFER devrait-il être déclaré et ce que se passerait-il s'il l'était plus d'une fois ? Une question similaire se pose avec les fonctions. Le code pour celles-ci peut être dans le script source principal ou dans une bibliothèque. Il peut être dupliqué en plusieurs endroits. La réponse à ces questions se trouve dans les règles de priorité.
Ces règles sont les suivantes :
- Le script source principal GoAsm et tous fichiers inclus avec extension en « a » ont toujours la priorité. En d'autres termes, tout label de code ou déclaration des données dans les scripts trouveront toujours leur chemin dans le fichier de sortie GoAsm avec le code et les données auxquels ils apposent des labels ;
- Sous réserve du 1 ci-dessus, les appels formels de la bibliothèque (utilisant le format library:functionname) ont priorité dans l'ordre dans lequel ils sont appelés.
Ces règles signifient que les bibliothèques de code sont en mesure d'appeler des fonctions et d'utiliser les données dans les scripts source directement (sans aucune aide de l'éditeur de liens). Elles signifient également que tous les labels dans une bibliothèque qui a déjà été utilisée dans le script source ou dans une bibliothèque qui a déjà été appelée seront ignorés. Supposons par exemple que BUFFER soit déclaré dans le script source avec une taille de 256 octets. Si library1 le déclare à son tour à 128 octets, ce label est ignoré et BUFFER sera de 256 octets dans le fichier de sortie. Et de plus, bien que la zone de données réservée à BUFFER dans library1 soit chargée dans le fichier de sortie, le label BUFFER ne la pointera pas et s'en tiendra à la zone déclarée dans le script source. Maintenant, si un appel plus tard, Library2 déclare BUFFER à 1024 octets, encore une fois cette zone de données ira dans le fichier de sortie, mais BUFFER continuera à pointer la zone de données d'origine. La raison pour laquelle GoAsm traite ainsi les labels de même nom est double. Tout d'abord, il serait impossible pour un assembleur (ou un linker d'ailleurs) de déterminer quel label est prioritaire au vu de sa taille. En effet, dans GoAsm au moment de l'assemblage et, plus certainement, au moment de l'édition de liens, la taille d'une zone particulière pointée par un label n'est pas connue avec certitude. Ceci, parce que lesdites zones sont parfois agrandies par des zones de code et de données dépourvues de label, ou parfois d'autres labels sont utilisés comme pointeurs vers une position intermédiaire dans la zone.
Les règles ont aussi une signification vis-à-vis de l'ordre dans lequel sont disposées les données. Supposons que votre bibliothèque repose sur des données détenues dans un tampon et également sur des données débordant parfois dans une zone élargie nommée BUFFER_EXT et déclarée immédiatement après BUFFER dans la bibliothèque. Maintenant, dans l'exemple ci-dessus BUFFER ne pointerait pas en réalité vers l'endroit attendu (juste avant BUFFER_EXT). Au contraire, il pointerait vers la première déclaration de BUFFER ailleurs dans la section de données.
Au terme de cette réflexion, je suggèrerais donc que les règles suivantes soient respectées lors de la création de fichiers lib :
- Utilisez uniquement des labels de même nom dans le script source et dans la bibliothèque si ces labels sont appelés à pointer vers la même chose au même endroit, à savoir, à la première déclarée en tant que label ;
- Si des labels de donnée de même nom doivent être utilisés dans différentes fonctions dans le fichier lib, il faut savoir que la taille de la zone de données qu'ils identifient sera fixée par la première déclarée en tant que label. Aussi, ne vous attendez pas à ce que la zone de données que le label identifie soit placée dans une position particulière dans l'exécutable.
V-L-13. Utilisation de fichiers-objet uniques▲
Actuellement, GoAsm ne supporte pas d'appeler les mêmes fonctions de la bibliothèque à partir de plus d'un module (script source). Si tel est le cas, vous verrez apparaître des erreurs « Duplication de symbole » en provenance de l'éditeur de liens. Pour contourner ce problème, vous devez concentrer tous les appels à la bibliothèque sur la même fonction dans une bibliothèque en un seul script source. Dans des versions ultérieures de GoAsm et de GoLink un support pourrait être ajouté pour les appels vers la même fonction de bibliothèque de plus d'un script source en cas d'utilité avérée pour les utilisateurs.
V-M. Unicode▲
GoAsm et son programme complémentaire GoRC (compilateur de ressources) lisent tous les deux des fichiers Unicode UTF-16 et UTF-8, peuvent prendre leurs commandes en Unicode et produire leur sortie en Unicode. Cela signifie que si vous utilisez un éditeur Unicode, il est possible d'utiliser des noms de fichiers, des commentaires, des labels de code et de données, des mots définis (equates, macros et structs), des exportations et aussi des chaînes de données, tous en Unicode. GoAsm dispose d'un certain nombre de fonctionnalités pour vous aider à écrire des programmes en Unicode ou même créer une version Unicode et ANSI de votre programme source à partir d'un seul script. Voir le chapitre « écriture de programmes Unicode » du volume 2 pour plus d'informations sur tous ces sujets.
V-N. Assemblage 64 bits▲
L'utilisation du commutateur /x64 dans la ligne de commande de GoAsm bascule l'assemblage en mode 64 bits, et permet de produire un fichier objet COFF 64 bits au format PE+. GoRC (le compilateur de ressources) et GoLink (l'éditeur de liens) peuvent également travailler en 64 bits et produire des exécutables destinés à fonctionner sous Windows 64 bits pour les processeurs AMD64 et EM64T. Bien que le code exécutable 32 bits diffère sensiblement de son homologue 64 bits et dans la mesure où les principes de base utilisés dans l'écriture du code source restent les mêmes, il est possible d'utiliser le même code source pour les deux plateformes. Ce qui fait que le code existant source 32 bits peut être porté à 64 bits. AdaptAsm.exe peut même aider à effectuer cette conversion, le cas échéant.
Voir :
V-O. Mode compatible x86 (assemblage 32 bits utilisant un source 64 bits)▲
L'utilisation du commutateur /x86 dans la ligne de commande de GoAsm bascule l'assemblage en mode de compatibilité x86 et vous permet de traiter un code source utilisant les registres à usage général étendus RAX, RBX, RCX, RDX, RDI, RSI, RBP et RER. Dans ce mode, ces registres sont lus par GoAsm comme s'ils étaient EAX, EBX, ECX, EDX, EDI, ESI, EBP et ESP. Cela vous permet d'utiliser des instructions comme :
MOV
RSI, ADDR
String
MOV
[RDI], AL
MOV
RAX, [hInst]
Ces instructions et d'autres semblables vont travailler à la fois dans les modes de compatibilité x64 et x86.
En plus de cela, dans le mode de compatibilité x86 :
- « x86 » devient un mot défini et identifiable pour l'assemblage conditionnel à l'aide de #if (non sensible à la casse) ;
- ARG RAX devient PUSH EAX
- ARG ADDR WNDCLASS devient PUSH ADDR WNDCLASS ;
- ARG 800h devient PUSH 800h ;
- ARG [hInst] devient PUSH [hInst] ;
- INVOKE fonctionne comme STDCALL ;
- JCXZ instruction fonctionne comme JECXZ.
Notez que le commutateur /x86 ne doit pas être utilisé dans la ligne de commande pour assembler du code source Win32 (ne l'utiliser que pour du code source commutable 32/64 bits).
Même en mode de compatibilité x86, vous ne pouvez pas utiliser les nouveaux registres AMD64/EM64RT, R8 à R15, XMM 8 à XMM 15, ni les formats d'adressage des nouveaux registres SIL, DIL, BPL, SPL, R8W à R15W ou R8D à R15D. En effet, ils ne sont pas disponibles pour une utilisation par un exécutable 32 bits.
Tout code source incompatible avec un exécutable 32 bits doit être éliminé au moment de la phase d'assemblage au moyen des techniques d'assemblage conditionnel.
Voir appel des API Windows en 32 bits et 64 bits dans le manuel GoAsm pour plus d'informations à propos de ARG et INVOKE.
Voir le fichier Hello64World3 comme exemple de code source qui peut produire, soit une fenêtre « Hello World » à partir d'un programme simple Win32, soit son équivalent en Win64.
Voir aussi le chapitre programmation en 64 bits pour s'informer des différences entre les programmes 32 et 64 bits.
V-P. Sections - Gestion avancée▲
V-P-1. Attribuer un nom aux sections▲
Le fichier objet attend un nom pour chaque section et GoAsm le fournit par défaut. Exprimé autrement, il n'est pas nécessaire du tout de nommer les sections. Les noms par défaut utilisés par GoAsm sont « code » pour une section de code, « data » pour une section de données et « const » pour une section const (ou constante).
Vous pouvez, si vous le souhaitez, baptiser une section en faisant suivre la déclaration de la section par un nom choisi par vous, par exemple :
DATA SECTION
MySect
ou
DATA SECTION
"Hello are you well?"
Chaque section dotée d'un nom sera unique. Par conséquent, vous pouvez créer autant de sections que vous le désirez en attribuant à chacune un nom différent. En temps normal, cependant, vous aurez besoin d'une seule section de données, d'une section de code et d'une section de const. Dans les programmes plus importants vous pouvez souhaiter avoir plus d'une section. Il est possible que cela puisse rendre le débogage un peu plus facile. Bien que GoAsm vous permette d'utiliser un nom de section de plus de huit caractères, l'éditeur de liens le limitera à huit caractères lorsqu'il constituera l'exécutable.
V-P-2. Ajout de l'attribut « shared »▲
L'attribut shared (flag 10000000h), lorsqu'il est utilisé dans la section de données d'une DLL, invite le chargeur de Windows à fournir une seule copie des données contenues dans cette section à chaque exécutable utilisant la DLL. Sans ce flag, chaque exécutable obtiendrait une copie distincte des données. Cela pourrait être le moyen pour un exécutable d'envoyer des données à un autre, sans avoir à utiliser le mappage de fichiers. Pour définir cet attribut ajouter le mot « shared » juste après la déclaration du nom de section, par exemple :
DATA SECTION
"MyData"
SHARED
Noter qu'une section partagée (shared) doit avoir un nom unique attribué de la manière indiquée ci-dessus. Vous ne voudrez probablement pas utiliser l'un des noms de section par défaut, à savoir « data », « code » ou « const », puisqu'ils sont normalement réservés à des sections non partagées.
GoAsm traite toute donnée non initialisée comme initialisée à zéro dans une section partagée. Cela évite les problèmes qui pourraient se poser si une donnée non initialisée non partagée est également requise dans un module (il ne peut y avoir qu'une section de données non initialisée dans un fichier objet) ; ou encore des problèmes qui se poseraient au moment de l'édition de liens en l'absence de section appropriée à laquelle attacher ces données non initialisées partagées.
V-P-3. Ordre des sections▲
GoAsm insère les sections dans le fichier objet dans l'ordre où elles apparaissent dans le script source. Il est normalement de la responsabilité de l'éditeur de liens d'ordonner les sections dans l'exécutable final. Si l'éditeur de liens est paramétré pour suivre le même ordre que celui des fichiers objet, vous pouvez modifier l'ordre des sections dans l'exécutable final en changeant l'ordre de leur déclaration dans le script source. Dans les sections elles-mêmes, vous pouvez demander à l'éditeur de liens d'ordonner les éléments de données brutes individuelles d'une certaine manière en ajoutant le suffixe $ au nom de la section. Par exemple :
CODE SECTION
'Asm$b'
;
CODE SECTION
'Asm$a'
;
Ici, le linker veillera à ce que le code de la section intitulée Asm$a apparaisse dans l'exécutable avant celui de la section intitulée Asm$b. En fait, l'éditeur de liens regroupera le code dans une même section (appelée Asm) et dans l'ordre attendu. Le caractère situé immédiatement après le signe dollar est utilisé uniquement pour préciser l'ordre souhaité et, lorsque l'on compare les noms de section, l'éditeur de liens ne considèrera seulement que les caractères précédant le symbole dollar. Enfin, puisque, dans l'exécutable, les sections ne peuvent pas avoir des noms de plus de huit caractères vous devez limiter, dans la pratique, le nombre de caractères devant le signe dollar à 8.
V-P-4. Alignement d'une section▲
Les sections ont un alignement par défaut de 16 octets. En d'autres termes, elles commencent sur une adresse multiple de 16. Cette valeur peut être modifiée pour un fichier objet pour contrôler comment l'éditeur de liens aligne le contenu de telle section avec le contenu de telle autre section en provenance d'autres fichiers objet. Pour spécifier un alignement de section différent, ajouter la mention ALIGN value juste après la déclaration de section, où value est la taille de l'alignement requis en octets et prendre les valeurs 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, ou 8192 (ce qui correspond à la forme 2n). Cela peut être utile pour les projets avec plusieurs fichiers sources qui ont besoin de construire différents tableaux d'informations, par exemple les tables de SEH dans des projets 64 bits :
CONST SECTION
'.xdata'
ALIGN
8
;
;UNWIND_INFO
;
CONST SECTION
'.pdata'
ALIGN
4
;
;RUNTIME_FUNCTION
;
V-Q. Adaptation de fichiers source existants à GoAsm▲
À l'origine, AdaptAsm.exe a été écrit dans le but d'alléger les tâches de modification de syntaxe rendues nécessaires pour adapter des fichiers source existants issus d'autres assembleurs à la syntaxe 32 bits de GoAsm. AdaptAsm.exe peut maintenant aider à l'adaptation de la syntaxe 32 bits GoAsm (ou peut-être un autre assembleur) en syntaxe 64 bits. Pour plus de détails sur ce sujet, voir la section utilisation de AdaptAsm pour aider à convertir en format 64 bits.
AdaptAsm s'utilise à partir de la simple ligne de commande suivante :
AdaptAsm [command line switches] inputfile[.ext]
Si aucune extension du nom du fichier d'entrée n'est spécifiée, l'extension .asm est présumée.
Si aucune extension du nom du fichier de sortie n'est spécifiée, l'extension .adt est présumée.
Les commutateurs de ligne de commande sont :
|
affiche l'aide. |
|
adapte un fichier A386. |
|
adapte un fichier MASM. |
|
adapte un fichier NASM. |
|
spécifie le chemin d'accès du fichier de sortie. Ex : AdaptAsm /fo GoAsm\adapted.asm. |
|
crée un fichier de sortie avec l'extension .log. |
|
supprimer l'alerte précédant toute écriture sur le fichier d'entrée. |
|
adapte le fichier à la plateforme 64 bits. |
Vous pouvez assimiler un fichier TASM à un fichier MASM s'il est écrit dans le mode MASM. Je n'ai pas inclus de version concernant le mode idéal TASM.
AdaptAsm crée le fichier de sortie en utilisant le même nom et le même chemin d'accès que ceux du fichier d'entrée, mais avec l'extension .adt (sauf si un nom et un chemin différent sont spécifiés). Le fichier de sortie rend compte, en son début, du nombre de modifications rendues nécessaires et exécutées par la fonction d'adaptation.
AdaptAsm ne peut pas écrire dans le fichier source, sauf si le fichier de sortie porte le même nom, extension comprise. Même dans ce cas, il vous sera demandé de confirmer votre souhait d'écraser le fichier, sauf bien évidemment si le commutateur /o a été mentionné dans la ligne de commande. Je vous suggère de n'opter pour le remplacement du fichier d'origine que si vous en détenez une copie quelque part. Je n'ai pas écrit d'antidote !
Si le commutateur /l est spécifié, AdaptAsm produit un fichier de sortie de même nom et de même répertoire que le fichier de sortie mais avec l'extension .log. Cela montre les changements qui ont été faits. Les numéros de ligne fournis font référence aux numéros de lignes du fichier d'entrée.
V-Q-1. Ce que fait Ad convertit différents fichiers source▲
Pour l'effet du commutateur /x64 voir utilisation de AdaptAsm pour faciliter la conversion en programmes 64 bits | |||
Action |
fichiers utilisant |
fichiers utilisant |
fichiers utilisant |
Sauf si le mot est défini (par ex. au moyen d'un equate), les crochets sont ajoutés à toutes les références de mémoire où ils ne sont pas déjà présents, par exemple : MOV EBX, MEM_REFERENCE devient MOV EBX, [MEM_REFERENCE] mais MOV EBX, OFFSET MEM_REFERENCE est laissé seul. |
oui | oui | non |
Met les références mémoire agrémentées de crochets dans la forme correcte, par exemple : MOV DX, sHEXw[ECX*2] devient MOV DX, [sHEXw+ECX*2]. |
oui | oui | non |
Ajoute ADDR à des références mémoire NASM qui ne disposent pas des crochets. | non | non | oui |
Les indicateurs de type et les remplacements BYTE, BYTE PTR, WORD, WORD PTR, DWORD, DWORD PTR, QWORD, QWORD PTR, et TWORD, TWORD PTR sont remplacés respectivement par les raccourcis équivalents B, W, D, Q et T. | oui | oui | oui |
Renverse l'ordre de tous les messages immédiats entre guillemets afin qu'ils soient lus dans le bon sens, par exemple : MOV [ESI], 'exe.' devient MOV [ESI], '.exe' MOV EAX, 'morf' devient MOV EAX, 'from' DW 'GJ' devient DW 'JG' DD 'dcba' devient DD 'abcd'. |
oui | oui | non |
Change HELLO LABEL en HELLO:. | oui | oui | non |
Le label local @@: de MASM et les sauts correspondants @F et @B sont convertis au format GoAsm. Ils reçoivent des numéros de séquence dans le fichier et, le cas échéant, l'indicateur de sens « > » est ajouté dans les instructions de saut. | non | oui | non |
Les labels locaux de NASM précédées d'un point - par exemple « .23 » - sont convertis au format GoAsm. Le nombre reste inchangé, mais le cas échéant, l'indicateur de sens « > » est ajouté dans l'instruction de saut. | non | non | oui |
Dans les instructions de saut, les indicateurs de proximité NEAR et SHORT sont enlevés car plus du tout utilisés. | oui | oui | oui |
Remplace les registres FPU désignés seulement par un chiffre (0 à 7) par la syntaxe ST0 à ST7. Par exemple : FDIV 0, 1 devient FDIV ST0, ST1. |
oui | non | non |
Change les registres FPU exprimés sous la forme ST(0) à ST(7) en ST0 to ST7, par exemple : FDIV ST(0), ST(1) devient FDIV ST0, ST1. |
non | oui | non |
Les déclarations de données effectuées par BYTE, ACHAR, SBYTE, sont changées en DB. Les déclarations de données effectuées par WORD, SWORD, SHORTINT sont modifiées en DW. |
non | oui | non |
Les déclarations de données effectuées par BYTE, ACHAR, SBYTE, sont changées en DB. Les déclarations de données effectuées par WORD, SWORD, SHORTINT sont modifiées en DW. Les déclarations de données effectuées par DWORD, HDC, ATOM, BOOL, HDWP, HPEN, HRGN, HSTR, HWND, LONG, LPFN, UINT, HFILE, HFONT, HICON, HHOOK, HMENU, HRSRC, HTASK, LPINT, LPSTR, LPVOID, WCHAR, HACCEL, HANDLE, HBRUSH, HLOCAL, LPARAM, LPBOOL, LPCSTR, LPLONG, LPTSTR, LPVOID, LPWORD, SDWORD, WPARAM, HBITMAP, HCURSOR, HGDIOBJ, HGLOBAL, INTEGER, LONGINT, LPBYTE, LPCTSTR, LPCVOID, LPDWORD, LRESULT, POINTER, WNDPROC, COLORREF, HPALETTE, HINSTANCE, HINTERNET, HMETAFILE, HTREEITEM, HCOLORSPACE, LOCALHANDLE, GLOBALHANDLE, HENHMETAFILE sont toutes changées en DD. Les déclarations de données effectuées par QWORD et DWORDLONG sont changées en DQ. Les déclarations de données effectuées par TWORD sont changées en DT. |
non | oui | non |
La duplication de syntaxe de données TIMES utilisée dans NASM est changée par la méthode DUP de déclaration de données multiple. De même, RESB / RESW / RESD utilisés dans NASM pour réserver les données non initialisées sont remplacés par la méthode DUP? de déclaration des données non initialisées. | non | non | oui |
TEXTEQU est changé dans sa version de type « C » #define. Les equates EQU et = ne sont pas changées car supportées par GoAsm. | non | oui | non |
La directive INCLUDE est remplacée par #INCLUDE. | oui | oui | oui |
La directive %INCLUDE est remplacée par #INCLUDE. | non | non | oui |
La série de directives IF / ELSE / ELSEIF / ENDIF / IFDEF (assemblage conditionnel) est remplacée par #IF / #ELSE / #ELSEIF / #ENDIF / #IFDEF. Les directives .IF /.ELSE /.ELSEIF /.ENDIF / .IFDEF et .WHILE et .BREAK ne sont pas traitées. Elles doivent être éliminées et le process réécrit manuellement en conséquence. |
non | oui | non |
La série de directives %IF / %ELSE / %ELSEIF / %ENDIF / %IFDEF (assemblage conditionnel) est remplacée par #IF / #ELSE / #ELSEIF / #ENDIF / #IFDEF. La directive %DEFINE est remplacée par #DEFINE. |
non | non | oui |
Commentaires sur toutes les lignes commençant par EXTERNE ou EXTERN, GLOBAL, ou PUBLIC. | oui | oui | oui |
PROC est remplacé par FRAME et ENDP par ENDF. Dans le code de MASM, les paramètres et la déclaration USES sont équivalents à la syntaxe GoAsm. | oui | oui | non |
La taille des données LOCAL dans une trame de pile automatisée est remplacée par des versions plus abrégées dans GoAsm (B, W, Q, T) et D est supprimé complètement car correspondant à la valeur par défaut dans GoAsm. | non | oui | non |
Change EVEN en ALIGN. | oui | oui | oui |
Plusieurs lignes qui ne supportent pas de commentaire, par exemple NAME, TITLE, SUBTITLE, SUBTTL, PROTO lines etc. | oui | oui | oui |
Plusieurs lignes que GoAsm ne prend pas en charge sont tout simplement supprimées. Par exemple .ERR, .EXIT, .LIST, .286 etc. | oui | oui | oui |
Le mot COMMENT est remplacé par un point-virgule. | oui | oui | oui |
V-Q-2. Ce que AdaptAsm ne fait pas…▲
Malgré le traitement de votre script de source par AdaptAsm vous devrez :
- Vérifier la déclaration des sections. La syntaxe primaire de GoAsm est DATA SECTION, CODE SECTION, CONST SECTION ou CONSTANT SECTION, mais CODE (ou .CODE), DATA (ou .DATA), et CONST (ou .CONST) sont également acceptés.
- Ajouter des indicateurs de type aux instructions où ce type ne va pas de soi pour GoAsm.
- Convertir les macros à la syntaxe GoAsm (si vous avez encore besoin d'elles avec GoAsm).
- Si vous utilisez MASM, vos initialisations de structure peuvent utiliser, soit les délimiteurs <…>, soit les délimiteurs {…}. Pour GoAsm vous devez vous assurer que seuls les délimiteurs <…> sont utilisés à cet effet (les délimiteurs {…} étant réservés à l'initialisation des membres individuels comme dans TASM - voir plus).
- Si vous utilisez NASM, convertir vos modèles de structure à la syntaxe GoAsm.
- Si vous utilisez NASM, vérifier tous les sauts de labels locaux définis à l'intérieur de labels non locaux (par ex. JZ MyProc.34) et renommer si nécessaire.
- Vérifier tous les labels de la section de code qui ont deux points (AdaptAsm ne le fait pas, de peur de l'ajout inopiné de deux points à un non-label).
- Vérifiez que AdaptAsm n'a pas ajouté des crochets pour encadrer des equates, des noms de macro et de STRUC. AdaptAsm tente néanmoins d'obtenir une liste complète de ceux-ci et d'éviter de leur donner ainsi des crochets dans la mesure du possible.
- Vérifiez si tous les CALLs ou JMPs qui comporteraient des références de mémoire entre crochets (ce qui serait inhabituel). AdaptAsm n'ajouter pas de crochets pour ces instructions.
- Convertir toutes les instructions MASM REPEAT, .REPEAT, REPT, .UNTIL, .UNTILCXZ WHILE, .WHILE, .ENDW, .IF / .ELSE / .ELSEIF / .ENDIF par les instructions traditionnelles de l'assembleur CMP, TEST, LOOP et des instructions de saut.
- Vérifiez que toutes les lignes utilisant des remplacements de segment (segment overrides) sont correctement codées.
- Remplacer toute utilisation de SIZESTR par le procédé de codage utilisant l'opérateur $ (position du pointeur d'instructions courant).
- Si vous avez utilisé TYPEDEF, sa suppression et son remplacement par un script approprié sont indispensables.
- Si vous utilisez modèles de structures MASM, veiller à ce que les symboles « supérieur à » et « inférieur à » sont utilisés à la place des accolades pour une initialisation séquentielle.
- Si vous utilisez modèles de structures MASM, veiller à ce que les symboles « supérieur à » et « inférieur à » sont utilisés à la place des accolades pour une initialisation séquentielle.
- Comme il n'y a pas de priorité des opérateurs dans GoAsm, vérifier toutes les opérations arithmétiques s'appuyant sur une priorité particulière. Par exemple, MOV EAX, 8+8*2 équivaut à 32 avec GoAsm.
- Lors de la conversion PROC … ENDP en trames de pile automatisées FRAME … ENDF, GoAsm ignore l'attribut STDCALL, qui est la norme dans Windows 32 bits. Il devrait de toute façon être retiré pour Windows 64 bits qui utilise FASTCALL (GoAsm commute automatiquement entre les deux). Si vous avez utilisé d'autres attributs après PROC vous pouvez probablement les supprimer entièrement - GoAsm les laisse seuls - en les vérifiant à la main.
Bien que AdaptAsm explore les fichiers « include » afin d'obtenir des renseignements sur les equates, les macros et des modèles de structure, il n'adapte pas ces fichiers. S'il y a du code ou des données dans ces fichiers, vous devez les soumettre individuellement à AdaptAsm de la même manière que pour le script principal.
VI. Divers▲
VI-A. Instructions PUSH spéciales▲
VI-A-1. Demi-opérations de pile▲
Ne s'applique qu'à la programmation 32 bits.
Dans Win32, les données sur la pile sont gérées en DWords, et la valeur du pointeur ESP est toujours sur une limite DWord après une opération PUSH ou POP. GoAsm prend cependant en charge des demi-opérations de pile, qui consistent à effectuer des PUSH et des POP sur deux octets seulement au lieu de quatre. Lorsque vous utilisez ces instructions, vous devez effectuer un PUSH ou un POP une seconde fois pour maintenir ESP à un multiple de DWord. Pour rendre la syntaxe explicite, GoAsm requiert l'utilisation de PUSHW et POPW pour ces opérations de demi-pile. PUSH et POP ne peuvent pas être utilisés car ils effectuent systématiquement une opération de pile DWord. À titre d'exemple, les instructions de demi-pile peuvent être utilisées en réponse au message WM_LBUTTONDOWN :
MOUSEX_POS DD
0
MOUSEY_POS DD
0
PUSH
[EBP
+
14h
] ; met lParam sur la pile
POPW [MOUSEX_POS] ; extrait de la pile le mot le moins significatif
POPW [MOUSEY_POS] ; puis le mot le moins significatif
Ou lorsque vous utilisez certaines API qui reçoivent certaines données de la pile respectivement dans le mot de poids fort et celui de poids faible :
PUSH
ADDR
lpFileTime
PUSHW [wFatTime]
PUSHW [wFatDate]
CALL
DosDateTimeToFileTime
GoAsm supporte également les instructions PUSHAW, PUSHFW et POPFW, POPAW, bien que vous ne devriez normalement pas y recourir dans la mesure où GoAsm est utilisé uniquement en programmation 32 bits.
VI-A-2. Sauvegarde en pile des flags et restauration▲
Au lieu d'utiliser PUSHF et POPF pour, respectivement, sauvegarder en pile et restituer les flags (mot d'état), vous pouvez recourir, si vous préférez, à :
PUSH
FLAGS
et
POP
FLAGS
Cette fonctionnalité peut également être utilisée avec INVOKE et USES.
FLAGS est donc un mot réservé de GoAsm et ne peut être utilisé comme un label.
Voir également :
VI-B. Changements de segment▲
En programmation Windows, les remplacements de segments sont très rarement utilisés et généralement limités au registre de segment FS.
Dans GoAsm, le remplacement de segment peut se situer avant ou après le mnémonique, et revêtir les formes suivantes :
FS
OR
D[24h
], 100h
OR
FS
D[24h
], 100h
FS
MOV
[ESI
], EAX
MOV
FS
[ESI
], EAX
Pour autant, les remplacements de segments ne peuvent pas être dans une position où ils risqueraient d'être confondus avec un registre de segment, pas plus qu'ils ne peuvent d'ailleurs se situer à l'intérieur des crochets, de sorte que les configurations suivantes sont à proscrire impérativement :
PUSH
FS
[0
] ; utiliser plutôt FS PUSH [0]
POP
FS
[0
] ; utiliser plutôt FS POP [0]
MOV
[FS
:0
], EAX
; utiliser plutôt FS MOV [0], EAX
VI-C. Utilisation de l'information du script source▲
L'information du script source suivante est disponible au moment de la compilation :
@line ; ligne courante en cours d'assemblage
@filename ; nom de fichier du script source principal en cours d'assemblage
@filecur ; fichier courant en cours d'assemblage
Ces mots sont insensibles à la casse des caractères utilisés. @filecur montre le fichier suivant courant dans un include de fichier en « a », alors que @filename montre le nom du tout premier script source soumis à GoAsm au démarrage.
@line fournit un entier de 32 bits qui peut être utilisé comme suit :
MOV
EAX
, @line ; EAX récupère le numéro de ligne
PUSH
@line ; le numéro de ligne est poussé en pile
DD
@line ; le numéro de ligne est déclaré en mémoire
@filename et @filecur fournissent des pointeurs vers une chaîne contenant le nom et peuvent être utilisés comme suit :
PUSH
@filename ; pointeur vers chaîne terminée par un zéro
DB
@filename ; chaîne non terminée par un zéro
DB
@filename, 0
; chaîne terminée par un zéro
VI-D. Utilisation des compteurs d'emplacement $ et $$▲
VI-D-1. Signification des compteurs d'emplacement▲
- $position au point d'utilisation en mémoire dans l'exécutable tel que chargé par Windows ;
- $$position au début de la section en cours dans la mémoire dans l'exécutable tel que chargé par Windows.
Parce que ces deux opérateurs donnent des positions en mémoire dans l'exécutable tel que chargé par Windows, leur valeur n'est pas connue pendant la phase d'assemblage par GoAsm, ni par le linker pendant la phase d'édition des liens. Ils agissent à cet égard comme un label de code ou de données. Lorsque vous les utilisez, ils ne possèdent pas de valeur, mais ils peuvent être soustraits les uns aux autres ou à des références de mémoire au sein de la même section pour produire une valeur. En effet, leurs valeurs relatives sont connues.
VI-D-2. Utilisation des compteurs d'emplacement▲
Le compteur d'emplacement $ est utile, par exemple, pour obtenir la taille d'une chaîne :
HELLO DB
'He finally got the courage to talk to her'
, 0
LENGTHOF_HELLO DB
$-
HELLO
Notez que le compteur d'emplacement $ marque à la fois la position du label LENGTHOF_HELLO et la position de la fin de la chaîne HELLO, de sorte que la longueur de la chaîne sera obtenue par simple soustraction au compteur $ de l'emplacement du label HELLO.
Voici maintenant un exemple où l'on obtient la taille d'un tableau de DWords en utilisant une méthode de calcul légèrement différente aboutissant à ce que la taille soit contenue dans le premier DWord de la table elle-même :
MESSAGES DD
ENDOF_MESSAGES-
$
DD
MESS1, MESS2, MESS3, MESS4, MESS5
ENDOF_MESSAGES
:
qui est la même chose que :
MESSAGES DD
ENDOF_MESSAGES-
MESSAGES
DD
MESS1, MESS2, MESS3, MESS4, MESS5
ENDOF_MESSAGES
:
On voit ici que le premier DWord de la table de valeurs contient la valeur 24 puisque le premier mot est compté également. Avec un peu d'arithmétique, vous pouvez en déduire le nombre de valeurs contenues dans le tableau (ici, au nombre de cinq) :
MESSAGES DD
(ENDOF_MESSAGES-
$-
4
)/
4
DD
MESS1, MESS2, MESS3, MESS4, MESS5
ENDOF_MESSAGES
:
Dans cette instruction :
LABEL400
:
JMP
$+
20h
; poursuit l'exécution 20h octets plus loin
Le compteur d'emplacement $ reflète la position de LABEL400 qui est la même que le début de l'instruction JMP. Par conséquent, les cinq octets constitutifs du codage de l'instruction JMP elle-même (saut relatif utilisant l'opcode E9) doivent être pris en compte dans le calcul.
Voici quelques autres exemples d'utilisation dans la section de code :
CALL
$$ ; call vers le début de la section courante
MOV
EAX
, $-
$$ ; EAX = distance de l'emplacement actuel par rapport
; au début de la section courante
Voici quelques autres exemples d'utilisation de $ et $$ dans une section de données :
HELLOZ DD
$$ ; HELLOZ contient la position du début de la section
DB
100
-
($-
$$) DUP
0
; bloc de zéros de la variable HELLOZ jusqu'à l'offset 100
Lorsqu'il est utilisé dans une définition, le compteur d'emplacement se réfère à l'emplacement où la définition est utilisée plutôt qu'à celui où elle est déclarée. Par exemple, dans ce qui suit, $$ se réfère au début de la section de code dans la mesure où la définition de globule est utilisée dans la section de code :
#define globule $$+
2
+
3
CODE SECTION
MOV
EAX
, globule
VI-E. Alignement et utilisation de ALIGN▲
VI-E-1. Qu'est-ce qu'un « alignement » ?▲
Toutes les zones de la mémoire ont certaines limites et des points d'alignement, même si l'on considère les adresses virtuelles utilisées par Windows. Prenez les adresses virtuelles 400000h et 410000h où, en général, vous trouverez peut-être les sections de code et de données de votre programme chargées par Windows. Ces deux adresses peuvent être considérées comme pouvant partir à la frontière d'un mot, d'un DWord, d'un paragraphe (16 octets) ou même à la frontière d'entités de 400h (qui valent 1K) ou de 10000h (qui correspondent à 64 Ko). En revanche, les adresses 400001h et 410001h ne peuvent s'aligner sur aucun des formats précédents. Les adresses 400002h et 410002h pourront être calées sur une limite de mot. Elles seront donc qualifiées de « word aligned ». 400004h et 410004h correspondront à des limites Word et DWord ; 400010h et 410010h, à des limites Word, DWord et paragraphe. Ces dernières adresses sont donc correctement décrites comme étant alignées Word, DWord et paragraphe.
Une approche plus simple consiste à s'intéresser à la divisibilité des adresses pour en déterminer l'alignement :
Adresse | Alignement | Caractéristiques adresse hexa |
divisible par 2 | Word | le quartet de plus faible poids est pair |
divisible par 4 | DWord | le quartet de plus faible poids est divisible par 4 |
divisible par 16 | Para | le quartet de plus faible poids est nul |
En assembleur, vous pouvez imposer l'alignement des données et du code. Nous allons examiner d'abord l'alignement des données puis nous nous intéresserons brièvement à l'alignement du code.
VI-E-2. Nécessité d'un alignement des données▲
Il y a deux raisons qui militent en faveur d'un alignement de vos données :
- la première consiste à satisfaire Windows ;
- la deuxième est d'essayer d'obtenir un peu de vitesse supplémentaire dans l'exécution de vos programmes. Certaines instructions du processeur voient en effet leurs performances accrues par un judicieux alignement des données.
VI-E-2-a. Alignements imposés par Windows▲
Dans Windows 32 bits (NT/2000/XP et Vista fonctionnant comme Win32), de nombreux pointeurs de données à destination des API nécessitent un alignement DWord, et souvent, cette exigence n'est pas documentée.
Même sous Windows 9x, plusieurs pointeurs doivent être alignés DWord par exemple les structures DLGITEMTEMPLATES et DLGITEMTEMPLATESEX. De plus, le menu, la classe, le titre et les données de police d'un DLGTEMPLATE doivent être alignés Word et les structures utilisées dans les API de gestion de réseau doivent être alignées DWord.
Certains membres de structures bitmap doivent être alignés en interne. XP impose que la hauteur et la largeur des bitmaps compatibles soient toujours divisibles par quatre afin de garantir que chaque ligne soit correctement alignée.
Certaines instructions SSE et SSE2 exigent un alignement de 16 octets de la zone de mémoire à laquelle ils ont affaire. C'est le cas, par exemple, des instructions FXSAVE, FXRSTOR, MOVAPD, MOVAPS et MOVDQA.
Dans Windows 64 bits les exigences d'alignement sont encore plus strictes. Il est essentiel de veiller à ce que les membres de structure soient alignés sur leur « frontière naturelle ». Ainsi, un mot doit-il être sur une limite Word, une valeur DWord sur une limite DWord, une valeur QWord sur une limite QWord, etc. Cela ne fonctionne que si la structure elle-même est bien alignée sur la limite correcte. Fondamentalement, la structure doit être alignée sur la limite naturelle de son membre le plus important. Il est également important que la même structure se termine sur la fin de la limite naturelle de son membre le plus important en ajoutant, le cas échéant, des octets de bourrage. Toujours dans Windows 64 bits, le pointeur de pile RSP doit aussi toujours être aligné selon une modularité de 16 octets lors d'un appel d'API. Voir la section exigences en matière d'alignement dans le chapitre consacré à la programmation 64 bits.
Quelle que soit la plateforme (32 bits ou 64 bits), les résultats sont imprévisibles si l'alignement est incorrect, allant de la simple non-apparition de contrôles, à la sortie pure et simple du programme.
VI-E-2-b. Alignements améliorant la vitesse d'exécution▲
L'alignement qui permet d'obtenir une vitesse d'exécution optimale varie d'un processeur à l'autre mais généralement, il est de bon usage que les données soient alignées dans la mémoire en fonction de la taille dans laquelle elles opèrent. Par exemple un alignement DWord est plausible pour une table de DWords. En théorie, les QWords et TWords doit être alignés QWord pour une performance optimale.
VI-E-3. Réalisation d'un alignement de données correct▲
En Win32, GoAsm aligne automatiquement les structures sur une limite DWord, à la fois quand elles sont déclarées comme données locales et dans la section de données. Ainsi, vous pouvez être certain que toutes vos structures fonctionneront. Cependant, vous pouvez ajouter un alignement supplémentaire à ces structures qui sont déclarées dans la section de données en utilisant l'opérateur ALIGN. Vous pouvez également choisir d'utiliser ALIGN sur les données ordinaires (hors structures) pour obtenir la meilleure performance possible.
Un bon alignement peut généralement être réalisé automatiquement en déclarant les données par séquence de taille dans la section de données. Vous pourriez ainsi déclarer tous les QWords d'abord, puis les DWords, les mots, les octets et, enfin, les chaînes. Les TWords contenant 10 octets peuvent bouleverser cet ordonnancement. Il est donc souhaitable de les déclarer en premier puis de corriger l'alignement en utilisant ALIGN.
En Win64, GoAsm aligne automatiquement les structures et leurs membres en fonction de la limite naturelle de ces structures et de leurs membres. GoAsm bourre aussi la taille de la structure en fonction des besoins. Enfin, GoAsm aligne automatiquement le pointeur de pile en préalable à un appel d'API.
Voir le chapitre programmation en 64 bits pour plus d'information et voir comment cela fonctionne en pratique.
VI-E-4. Alignement du code ▲
Le bon alignement du code relève plus de l'impondérable et dépend véritablement du processeur exécutant le code. Rick Booth aborde le sujet dans son livre « Inner Loops » (« boucles internes ») publié par Addison Wesley.
J'ai inclus quelques tests de vitesse dans TestBug, qui montrent la différence qui peut résulter d'un bon alignement lors des opérations de lecture, d'écriture ou de comparaison du contenu de la mémoire à une valeur.
VI-E-5. Utilisation de ALIGN▲
GoAsm reconnaît l'opérateur ALIGN value, où value est la taille de l'alignement requis en octets. Dès lors, GoAsm alignera les données ou le code qui suivent à la limite correcte pour assurer l'alignement. Par exemple :
ALIGN
4
; la donnée suivante sera alignée sur un dword
ALIGN
16
; (ou ALIGN 10h) aligne ce qui suit sur un paragraphe de 16 octets
Afin de réaliser l'alignement dans une section de code, GoAsm effectue un « bourrage » d'octets au moyen d'une succession d'instructions NOP (opcode 90h) qui ont la particularité de n'exécuter aucune opération. Pour une section de données ou const, GoAsm se livre également à un « bourrage » mais utilisant ici des zéros disposés à la bonne place.
Voir aussi sections - Gestion avancée s'agissant de l'alignement de la section.
VI-F. Utilisation de SIZEOF▲
L'opérateur SIZEOF peut être utilisé comme ADDR ou OFFSET, mais au lieu de donner l'adresse d'un label de code ou de données, il donne sa taille en octets. Comme GoAsm est un assembleur travaillant en une seule passe, SIZEOF ne renvoie la valeur appropriée que pour un label qui commence et se termine avant cet opérateur dans le script source. SIZEOF peut être utilisé soit pour des données ordinaires et du code, soit pour des données locales.
VI-F-1. Utilisation de SIZEOF sur les labels de donnée▲
Voici quelques exemples illustrant la manière dont SIZEOF peut être utilisé (où Hello est un label de donnée) :
MOV
EAX
, SIZEOF
Hello
MOV
EAX
, SIZEOF
(Hello)
MOV
EAX
, [ESI
+
SIZEOF
Hello]
SUB
ESP
, SIZEOF
Hello
DD
SIZEOF
Hello
Label
DB
SIZEOF
Hello DUP
0
MOV
EAX
, SIZEOF
Hello+
4
MOV
EAX
, SIZEOF
Hello-
4
MOV
EAX
, SIZEOF
Hello/
2
MOV
EAX
, SIZEOF
Hello*
2
Lorsqu'il agit sur un label de donnée dans un ensemble de données brutes, l'opérateur SIZEOF calcule la distance entre le label mentionné et le label suivant ou la fin de la section si celle-ci s'avère plus proche, comme le montre l'exemple suivant :
Hello DB
0
DD
0
Hello2 DB
0
alors,
MOV
EAX
, SIZEOF
Hello
charge dans EAX la valeur 5.
VI-F-2. Utilisation de SIZEOF avec des chaînes▲
Vous pouvez utiliser SIZEOF avec des chaînes (en remplacement du LENGTHOF de MASM). Par exemple :
WrongB DB
'You pressed the wrong button!, '
0
alors,
MOV
EAX
, SIZEOF
WrongB
renvoie la longueur de la chaîne, terminateur null compris.
VI-F-3. Utilisation de SIZEOF sur les labels de code▲
Dans cet exemple :
START
:
XOR
EAX
, EAX
XOR
EAX
, EAX
XOR
EAX
, EAX
XOR
EAX
, EAX
LABEL
:
MOV
EAX
, SIZEOF
START
La valeur 8 sera chargée dans EAX. C'est la taille des quatre instructions XOR EAX, EAX.
VI-F-4. Utilisation de SIZEOF avec les structures▲
Vous pouvez utiliser SIZEOF avec des structures pour en retourner la taille. Par exemple, si vous avez
Rect STRUCT
left DD
top DD
right DD
bottom DD
ENDS
rc Rect
alors, les instructions
MOV
EAX
, SIZEOF
Rect
et
MOV
EAX
, SIZEOF
rc
chargent toutes les deux la valeur 16 dans EAX.
VI-F-4-a. Utilisation de SIZEOF avec des membres de structures▲
Vous pouvez utiliser SIZEOF pour retourner la taille de membres d'une structure qui est calculée dans ce cas par rapport au label suivant. Par exemple, si vous avez
Rect STRUCT
left DD
DD
right DD
bottom DD
ENDS
rc Rect
alors, les instructions
MOV
EAX
, SIZEOF
rc.left
et
MOV
EAX
, SIZEOF
Rect.left
retournent toutes les deux la valeur 8.
VI-F-5. Utilisation de SIZEOF avec des unions et des membres d'union ▲
Vous pouvez utiliser SIZEOF pour retourner la taille d'une union ou de l'un de ses membres, mais gardez à l'esprit que chaque membre retournera la même taille (c'est-à-dire la plus grande taille de membre). Ainsi :
Sleep STRUCT
DW
2222h
DB
0h
ENDS
Ness UNION
Possums DB
L'Balance'
Koalas Sleep
Devils DB
'Roar'
ENDS
Happy Ness
SizeLabel DD
SIZEOF
Happy
DD
SIZEOF
Ness
DD
SIZEOF
Happy.Possums
DD
SIZEOF
Happy.Koalas
DD
SIZEOF
Happy.Devils
Chaque DWord dans SizeLabel contient 14, qui est la taille du plus grand membre de l'union, à savoir Happy.Possums, qui contient une chaîne Unicode de 14 octets de long.
VI-F-6. Influence de l'initialisation d'une structure sur sa taille▲
Lors du processus d'obtention de la taille, les arguments qui pourraient être utilisés pour modifier la taille de la structure sont ignorés. Considérons, par exemple, la structure suivante :
StringStruct STRUCT
DB
?
ENDS
Dans cette situation, l'instruction
MOV
EAX
, SIZEOF
StringStruct
retourne une taille de 1 octet dans EAX, même si la structure avait préalablement été mise en œuvre à l'aide de :
LongString StringStruct <
'Je hais les structures'
>
qui agrandit la structure de 22 octets dans cette mise en œuvre. De même, si la longueur de la chaîne repose sur la résolution d'une définition, par exemple :
StringStruct STRUCT
DB
LONGSTRING
ENDS
alors
MOV
EAX
, SIZEOF
StringStruct
retournerait également une taille de 1 octet dans EAX, quelle que soit la valeur de LONGSTRING.
Les tailles de structure susceptibles de varier avec les définitions sont, par exemple, de la forme :
Rect STRUCT
DB
TWELVE DUP
0
ENDS
Et, dans ce cas, la taille de la structure sera effectivement modifiable.
VI-F-7. Initialisation d'une structure avec SIZEOF▲
Vous pouvez initialiser une structure avec sa propre taille ou la taille d'autre chose, par exemple :
PARAM_STRUCT STRUCT
DD
0
DD
0
DD
0
ENDS
ps1 PARAM_STRUCT <
SIZEOF
PARAM_STRUCT,,>
VI-F-8. Utilisation de SIZEOF avec des données locales▲
La taille des données locales est retournée, par exemple :
MyWndProc FRAME
LOCALS hDC, DemonFlag:B, Buffer[256
]:B, MyRect:RECT
MOV
EAX
, SIZEOF
hDC ; 4 en 32 bits, 8 en 64 bits (taille par défaut)
MOV
EAX
, SIZEOF
DemonFlag ; 1
MOV
EAX
, SIZEOF
Buffer ; 256
MOV
EAX
, SIZEOF
MyRect ; 16
RET
ENDF
La taille retournée ignore tout comblement opéré par GoAsm pour aligner les données locales correctement sur la pile (toutes les données locales sont alignées DWord en assemblage 32 bits et alignées QWord en assemblage 64 bits).
Voir aussi la section utilisation de l'instruction LOCALS.
VI-G. Utilisation des branchements prédictifs▲
VI-G-1. Qu'est-ce qu'une prédiction de branchement ?▲
Les processeurs modernes (par exemple les Pentium du haut de gamme) sont capables d'améliorer sensiblement leur vitesse d'exécution en prédisant la prochaine instruction qui sera exécutée après une instruction de saut conditionnel. Par exemple :
CMP
EDX
, EAX
; compare EDX et EAX
JZ
>
L1 ; saut au label L1 si EDX = EAX
; autres instructions
L1
:
Le processeur ne peut être certain à 100 %, à l'avance, que EDX sera égal à EAX au moment où l'instruction de comparaison est exécutée. Il pourrait prédire que cela est peu probable, auquel cas, il déciderait que les instructions situées immédiatement après le saut conditionnel doivent être exécutées à la suite. Si cette prédiction s'avère exacte, ces instructions seront donc exécutées immédiatement et sans aucune perte de temps. Dans le cas contraire, il y aurait une certaine perte de temps à passer à l'instruction correcte (juste après L1) en lieu et place. Différents algorithmes de prédiction de branchement sont utilisés par le processeur, y compris certains qui sont capables de tirer parti de leurs erreurs. Une des prédictions de base utilisées comme point de départ postule que :
- tous les sauts conditionnels avant ne pourront avoir lieu ;
- tous les sauts conditionnels arrière (boucles arrière) auront lieu.
Il est établi que, dans un code normal, les sauts conditionnels arrière seront effectifs avec une probabilité de 80 %, alors que les sauts conditionnels avant seront plus rares. Il est dit que, globalement, la prédiction par défaut est correcte dans 65 % du temps. Donc prédire si oui ou non le saut conditionnel aura lieu peut accélérer le code, en particulier sur une série de boucles de retour. En tant que programmeur en assembleur, vous pouvez produire du code rapide en tenant compte des mécanismes de prédiction par défaut du processeur que vous programmez d'autant plus finement que vous êtes en mesure de contrôler complètement votre code.
VI-G-2. Les prédicteurs de branchement 2Eh et 3Eh▲
Dans le P4 (et peut-être dans certains processeurs antérieurs), vous pouvez donner au processeur un « conseil » quant à savoir si oui ou non la branche est susceptible de se produire.
On utilise à cet effet les octets de prédiction de branchement 2EH et 3Eh qui sont insérés immédiatement avant l'instruction de saut conditionnel. Respectivement, ils signifient ce qui suit :
- 2Eh prédit que le branchement ne s'exécutera pas la plupart du temps ;
- 3Eh prédit que le branchement s'exécutera la plupart du temps.
L'utilisation du prédicteur de branchement 2Eh serait utile (par exemple), si vous aviez un branchement arrière correspondant à un saut conditionnel qui ne se produit que dans le cas d'une erreur présumée rare. Nous avons vu que le processeur, dans son mode de fonctionnement de base, prévoit normalement que le branchement arrière est considéré comme étant susceptible de se produire, ce qui ralentit le code puisque nous attendons précisément le contraire. C'est là que l'insertion du prédicteur 2Eh intervient pour neutraliser, à point nommé, cet automatisme.
Le prédicteur de branchement 3Eh serait utile (exceptionnellement) si vous souhaitez créer une boucle où le saut conditionnel serait au début d'un fragment de code et la destination du saut plus avant dans le code. Cette boucle devrait normalement fonctionner plus lentement qu'une boucle de type normal en raison de la prévision par défaut, par le processeur, que le branchement avant est peu probable, à moins que le processeur ne soit capable de tirer parti de ses erreurs de prévision initiales.
VI-G-3. Insertion de prédicteurs de branchement▲
Intel ne propose aucun mnémonique pour insérer les octets de prédiction de branchement. Vous pouvez procéder comme suit :
DB
2Eh
; ou
DB
3Eh
Si vous préférez, vous pouvez insérer les octets prédiction de branchement automatiquement. À cet effet, GoAsm utilise la syntaxe qui suit pour insérer l'octet de prédiction de branchement approprié pour le processeur P4 :
hint.nobranch ; insère 2Eh
hint.branch ; insère 3Eh
Pour en revenir au premier exemple, si EDX est normalement égal à EAX, alors ce code sera plus rapide sur le P4 en ajoutant la prédiction de branchement suivante :
CMP
EDX
, EAX
; compare EDX et EAX
hint.branch JZ
>
L1 ; effectue normalement le branchement à L1
; autres instructions
L1
:
J'ai inclus un test de vitesse dans TestBug qui démontre l'amélioration de la vitesse obtenue en utilisant ce mécanisme de prédiction de branchement. Il apparaît par exemple que le code s'exécute environ 1,5 fois plus rapidement sur un processeur par la mise en œuvre de ce procédé.
VI-H. Syntaxe des registres FPU, MMX et XMM▲
Registres | Syntaxe GoAsm |
FPU x87 (virgule flottante) | ST0, ST1, ST2, ST3, ST4, ST5, ST6 et ST7 |
Registres MMX et 3DNow! | MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7 |
Registres XMM | XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 et XMM7 |
Les huit nouveaux registres XMM des processeurs 64 bits |
XMM8, XMM9, XMM10, XMM11, XMM12, XMM13, XMM14 et XMM15 |
Lire écriture des programmes 64 bits pour connaître les autres nouveaux registres et leurs méthodes d'adressage.
VI-H-1. L'opcode « Wait » (9Bh)▲
Puisque GoAsm ne fonctionne que sur les processeurs modernes intégrant l'unité de calcul en virgule flottante, il émet l'opcode « Wait » seulement pour les instructions suivantes :
FCLEX FINIT FWAIT FSAVE |
FSETPM FSTCW FSTENV FSTSW |
VI-I. Information sur la durée d'assemblage (GOASM_REPORTTIME)▲
GoAsm peut vous indiquer le temps qu'il a fallu pour assembler les différentes parties de votre script source si vous portez le mot GOASM_REPORTTIME une ou plusieurs fois dans le texte. Le temps restitué par GOASM_REPORTTIME est :
- celui écoulé depuis la spécification GOASM_REPORTTIME précédente (ou le début de l'assemblage) ;
- celui qui est nécessaire pour atteindre la prochaine spécification GOASM_REPORTTIME (ou la fin de l'assemblage).
Ces durées excluent les phases de mise en place et de nettoyage.
GOASM_REPORTTIME est utile si vous voulez examiner différentes configurations du script source et en déduire celles qui permettront d'accélérer l'assemblage, ou pour identifier une éventuelle partie du script qui ralentirait à elle seule l'assemblage.
VI-J. Autres interruptions GoAsm▲
Vous pouvez envoyer un message à la console à l'occurrence d'un événement lors de l'assemblage (qui peut éventuellement être activé par assemblage conditionnel) en utilisant, par exemple :
GOASM_ECHO L'Assembleur a atteint de nouveaux sommets
Le matériau à écrire dans la console n'a pas à être entre guillemets, bien qu'il puisse l'être également.
Enfin, vous pouvez forcer GoAsm à arrêter l'assemblage et à en sortir par :
GOASM_EXIT
VI-K. Fichier-listing d'assemblage GoAsm▲
GoAsm produit un fichier-listing si on lui demande de le faire avec le commutateur /l dans la ligne de commande. Le fichier utilise les mêmes nom et répertoire que le fichier objet de sortie, mais il est pourvu de l'extension .lst. Il est dans le même format que celui de votre fichier source (un fichier source Unicode se traduira par un fichier-listing Unicode). Le volume 2 décrit, de manière plus précise, la programmation en Unicode.
Le listing montre, d'une part les opcodes qui ont été générés à la suite de chaque instruction sur le côté gauche de la page, d'autre part l'instruction et les commentaires sur le côté droit de la page. Si une ligne est trop longue pour être représentée, elle est purement et simplement tronquée. Dans le cas des opcodes un symbole en forme d'ellipse avertit qu'une troncature est intervenue. Les références de mémoire qui requièrent une relocalisation (c'est-à-dire une valeur sera insérée soit par GoAsm, soit par le linker) sont spécifiées entre crochets. Cependant, la relocation insérée ne sera pas affichée.
Les définitions sont présentées telles que définies ou étendues.
Les fichiers d'inclusion pourvus d'un « a » ou « A » dans l'extension sont traités en tant que partie du script source et leur contenu sera inclus dans le listing. Le contenu des fichiers inclus pourvus d'autres extensions ne seront pas inclus dans le listing.
Faites-moi savoir s'il y a quelque chose d'autre que vous aimeriez que le fichier-listing affiche ou si vous souhaitez en modifier le format.
VI-L. Messages d'erreur et d'avertissement de GoAsm▲
GoAsm donne toute l'information relative aux erreurs et alertes sur la ligne de commande. Si GoAsm constate une erreur dans le script source, il cessera l'assemblage et interrompra le traitement avec le code de retour TRUE (EAX = 1). GoAsm affiche alors dans la fenêtre MS-DOS (invite de commande) la ligne et l'origine (le script source) de l'erreur survenue, une description de l'erreur, et peut également visualiser le mot ou la ligne de texte incriminés. Si le mot ou la ligne sont définis, il peut montrer l'origine de la définition. Certaines erreurs dans les fichiers inclus « non a » (ceux qui ne sont pas considérés comme faisant partie du script source) sont tout simplement ignorées, d'autres sont présentées comme des alertes, en fonction de la nature de l'erreur.
Si vous utilisez le commutateur /b, vous entendrez également un bip sur une erreur.
Je n'ai pas opté pour la faculté offerte à l'assembleur de continuer en dépit d'une erreur, considérant que celle-ci se traduit souvent par d'autres erreurs induisant un phénomène cumulatif, le tout tendant à obscurcir en définitive la cause originelle. J'ai trouvé que cette réponse à une simple erreur est tout à fait adéquate à la lumière de la syntaxe simplifiée de GoAsm dont on espère réduire les erreurs dans votre script source de toute façon. En plus de cela, si vous écrivez votre code de manière incrémentale, j'ai bon espoir que les erreurs seront peu nombreuses.
Je me suis également prononcé contre l'idée consistant à recenser les erreurs dans un fichier séparé, car il serait légitimement fastidieux d'avoir à ouvrir un autre fichier afin d'y lire les erreurs plutôt que de lire directement cette information sur la ligne de commande. Du reste, vous pouvez parfaitement rediriger toutes les sorties GoAsm dans un fichier que vous pourrez lire plus tard en utilisant la commande DOS de redirection, par exemple :
GoAsm Test.asm >
output.fil
Une autre façon de contrôler la sortie d'erreurs et d'avertissements GoAsm est d'utiliser les commutateurs suivants sur la ligne de commande :
|
bip sur erreur. |
|
pas de messages d'erreur. |
|
pas de messages l'information. |
|
pas de messages d'avertissement. |
|
aucun message de sortie du tout. |
Un simple avertissement sera délivré si un mot a été défini plus d'une fois dans la ligne de commande ou dans le script source, mais l'assemblage sera autorisé à continuer pour autant. En effet, il serait inhabituel de définir un mot plus d'une fois et il se peut également que ce soit une erreur de programmation. Il est parfaitement possible d'annuler une définition précédente en utilisant #undef pour que le mot puisse être défini. Dans ce cas, aucun avertissement n'est donné.
Vous obtiendrez également un avertissement si :
- vous essayez d'inclure le même fichier deux fois à moins que ce ne soit le type de fichier d'inclusion qui contienne lui-même script source, dans ce cas, il y aura une erreur à la place ;
- vous essayez de déclarer plus de 1 Mo de données en double ;
- (dans certains cas) si vous utilisez un indicateur de type alors que ce n'est pas nécessaire.
Dans un fichier batch, vous pouvez capter le retour d'erreur avec ERRORLEVEL et en déduire une action. Par exemple, dans ce qui suit, le processus est interrompu s'il y a un retour d'erreur :
GoAsm MyFile.asm
IF
ERRORLEVEL 1
PAUSE
VI-M. Utilisation de GoAsm avec différents linkers▲
VI-M-1. Utilisation de GoLink▲
GoLink est un éditeur de liens libre écrit par moi et disponible à partir de mon site web www.GoDevTool.com. Il accepte les fichiers objet au format PE produits par GoAsm (ou d'autres assembleurs et compilateurs) et les fichiers RES ou OBJ issus de GoRC pour produire un fichier exécutable.
GoLink présente certains avantages par rapport aux autres linkers. En particulier il est conçu pour collaborer étroitement avec GoAsm sur les importations et les exportations, et peut rendre compte des données et labels de code redondants lors de l'édition de liens des fichiers produits par GoAsm. Mais ses principaux avantages sont qu'il fonctionne rapidement en réduisant les « bagages ». Les fichiers sont réduits au minimum. Les fichiers LIB ont été supprimés. Au lieu de cela, GoLink regarde les exécutables actuellement stockés sur l'ordinateur au moment de l'édition de liens pour obtenir ses informations sur les importations requises par l'exécutable qu'il constitue. Comme la plupart d'entre eux sont dans la mémoire de toute façon s'il s'agit de DLL système, cela peut être fait rapidement.
Voir le chapitre consacré à GoLink pour une information complète sur cet éditeur et la manière de l'utiliser (Volume 2). Notez que l'action de GoAsm puis celles de GoLink peuvent utilement être automatisées à l'aide d'un fichier batch (avec l'extension .bat) visant à créer un exécutable. En voici un exemple simple :
GoAsm MyProg.asm
GoLink MyProg.obj Kernel32.dll User32.dll
Le lancement de ce fichier batch va aboutir à la création du fichier Windows PE myprog.exe avec les importations en provenance des DLL mentionnées. L'adresse d'entrée START est implicite mais cela peut être spécifié.
Vous pouvez utiliser un fichier de commandes avec GoLink par exemple :
GoLink @command.fil
Au lieu (ou en plus) de spécifier les DLL dans la ligne de commande de GoLink ou les fichiers, vous pouvez utiliser #DYNAMICLINKFILE dans le code source GoAsm. La syntaxe est :
#dynamiclinkfile path/filename, path/filename
La virgule est facultative. L'ensemble répertoire/nom de fichier peut être entre guillemets. Un ou plusieurs ensembles répertoire/nom de fichier peuvent être spécifiés. Vous n'êtes pas obligé de fournir le chemin d'accès lors de la spécification de fichiers système car GoLink consulte automatiquement le contenu des répertoires système. Le nom du fichier doit être stipulé avec son extension qui peut être .dll, .ocx, .exe ou .drv.
Le nom de fichier est envoyé à GoLink dans la section .drectve du fichier objet créé par GoAsm. Il est utilisé pour donner des informations et des directives à l'éditeur de liens, mais n'a aucune incidence sur l'exécutable final.
Vous devez utiliser GoLink à partir de la version 26.5 pour que cela fonctionne.
Il y a plusieurs autres commutateurs et options dans la ligne de commande lors de l'utilisation GoLink. Pour plus de détails consulter le fichier d'aide Golink, GoLink.htm.
VI-M-2. Utilisation de ALINK▲
ALINK est un éditeur de liens libre de Anthony Williams. La sortie de ALINK est plutôt fournie, ce qui fait qu'il s'avère préférable de la rediriger vers un fichier que vous pourrez regarder plus tard. Je vous suggère de constituer à cet effet un fichier batch avec une extension .bat contenant les lignes suivantes :
GoAsm MyProg.asm
ALINK @Respons.fil >
link.opt
Vous exécutez ce fichier batch en saisissant son nom sur la ligne de commande dans une fenêtre MS-DOS (invite de commande) et en appuyant sur Entrée.
Respons.fil est un fichier avec les instructions à l'éditeur de liens qui pourraient être les suivantes (à titre d'exemple) :
-m ; produit un fichier map
-oPE ; produit un fichier au format PE
-o MyProg.Exe ; impose le nom du fichier de sortie
-entry START ; précise le point d'entrée
-debug ; signifie que des symboles debug doivent être constitués
kernel32.lib ;
COMCTL32.lib ; un fichier lib pour chaque API
COMDLG32.lib ; qui est appelé dans votre programme.
user32.lib ; fait usage de ALIB qui est
gdi32.lib ; un composant du package ALINK.
shell32.lib ;
MyProg.obj ; le fichier d'entrée
Vous pouvez organiser votre travail en laissant les différents fichiers nécessaires entreposés dans des dossiers différents. Dans ce cas, vous devez inclure les chemins d'accès correspondants dans les instructions données à GoAsm et à ALINK.
VI-M-3. Utilisation de l'éditeur de liens Microsoft▲
L'éditeur de liens Microsoft est disponible gratuitement dans le SDK Microsoft ou en téléchargeant le package MASM. Voir www.godevtool.com/ pour plus d'informations ou essayez différents liens vers d'autres sites. L'éditeur de liens Microsoft est plus difficile à utiliser car il prévoit que le label d'adresse de démarrage de programme et les appels externes soient « enrichis » de la manière dont ils sont émis par les compilateurs C et par MASM. L'enrichissement attendu prévoit, en l'occurrence, un caractère de soulignement précédant le label et (dans le cas d'un appel de fonction) le symbole @ suivi du nombre d'octets poussés sur la pile lorsque la fonction est appelée. Normalement GoAsm n'utilise pas d'enrichissement parce que mon objectif a été de tout simplifier autant que possible.
VI-M-3-a. Enrichissement automatique avec le commutateur /ms▲
Vous pouvez demander à GoAsm de fournir automatiquement l'enrichissement nécessaire en utilisant le commutateur /ms dans la ligne de commande (en 32 bits seulement mais cette fonction est inactivée avec l'utilisation du commutateur /x64). Cette configuration invitera GoAsm à enrichir tous les labels de code, des CALL et INVOKE.
Dans chacun de ces cas, l'enrichissement est sous cette forme :
_CodeLabel@x
où le label est déclaré comme CodeLabel et où x est le nombre d'octets utilisés par les paramètres de CodeLabel. Enrichi de cette manière, CodeLabel est disponible pour d'autres fichiers objet devant être liés par le MS linker (et peuvent donc être appelées à partir de ces autres fichiers objet). Et si CodeLabel réside dans une DLL, le MS Linker va le reconnaître en tant que tel à partir d'un fichier .lib fabriqué à partir de la DLL et qui lui est donné au moment de la phase d'édition de liens.
Au moment de l'édition des liens, le MS linker attend que la valeur de « @x » corresponde exactement entre l'appelant et l'appelé. Ceci est donc une forme limitée de contrôle de paramètre. Lorsque le commutateur /ms est utilisé, GoAsm doit donc compter le nombre de paramètres utilisés par le label de code afin d'obtenir la valeur de « @x » correcte. Pour ce faire, dans le cas des labels de FRAMEs, GoAsm compte le nombre de paramètres déclarés dans la trame et ajuste l'enrichissement en conséquence. Dans le cas d'un appel utilisant INVOKE, GoAsm compte à nouveau le nombre de paramètres utilisés.
Cependant, GoAsm ne peut pas compter le nombre de paramètres pour un appel utilisant CALL et suppose qu'il n'y en a pas. Pour cette raison, s'il y a des paramètres, vous devez impérativement utiliser INVOKE pour que l'enrichissement puisse fonctionner correctement (et non pas un PUSH xxx ordinaire suivi d'un CALL). Notez également que si vous utilisez ARG avant INVOKE, chaque argument doit posséder sa propre ligne (ce qui élimine toute représentation telle que ARG 1,2,3).
Ainsi, si vous utilisez le code suivant avec le commutateur /ms sur la ligne de commande de GoAsm :
HelloProc FRAME hwnd, arg1, arg2
INVOKE
MessageBoxA, [hwnd], 'Click OK'
, 'Hello'
, 40h
alors, GoAsm va insérer le symbole HelloProc dans le fichier objet sous la forme
_HelloProc@12
et mettre la fonction appelée dans le fichier objet sous la forme
_MessageBoxA@16
En effet, GoAsm sait que 12 octets sont sur la pile dans le cas de HelloProc et que 16 octets sont poussés sur la pile avant que MessageBoxA ne soit appelée.
À partir de la version 0.49 de GoAsm, les labels de codes ordinaires sans paramètres sont également enrichis. Ceci doit permettre que de tels labels de code puissent être reconnus à l'extérieur au moment de l'édition des liens de sorte qu'ils puissent être appelés par d'autres fichiers objet créés par des outils MS. On leur donne un nombre d'octets de paramètre nul. Cela comprend l'étiquette donnant elle-même l'adresse de départ. Donc, supposons que votre adresse de départ dans votre script source soit START: (non précédée par un caractère de soulignement). Elle se trouve maintenant enrichie sous la forme :
_START@0
et, pour procéder à une édition de liens correcte avec MS Linker, vous devrez inclure ce qui suit dans sa ligne de commande ou le fichier :
-ENTRY START@0
VI-M-3-b. Enrichissement manuel▲
Vous pouvez également utiliser le MS linker en effectuant les modifications manuelles suivantes sur votre script source :
- Assurez-vous que le label donnant l'adresse de démarrage du programme commence bien par un caractère de soulignement. Mais omettez ce caractère de soulignement lorsque vous le déclarez au MS Linker.
- Si vous avez plus d'un fichier objet à envoyer au MS Linker, et qu'il y a des fonctions dans un fichier objet appelées à partir d'un autre fichier objet, enrichir les labels correspondant à ces fonctions. Les labels de code ordinaires sans paramètres auront besoin d'un caractère de soulignement avant et de @0 à la fin du nom. Les labels de code avec des paramètres ont besoin d'un caractère de soulignement en tête et de @x après le nom où x est le nombre d'octets de paramètres (chaque paramètre comprenant quatre octets).
- Enrichissez tous les appels de l'API avec un caractère de soulignement en tête et @x après le nom, x étant le nombre d'octets poussés sur la pile lorsque l'API est appelée (chaque PUSH occupant quatre octets).
Par exemple :
PUSH
12h
CALL
_GetKeyState@4
; test si la touche Alt est pressée
L'API GetKeyState ne requiert qu'un seul paramètre, ce qui fait que quatre octets sont mis sur la pile.
Si vous faites une DLL, vous aurez besoin d'utiliser un label pour l'adresse de départ enrichie de la même manière, par exemple :
_DLLENTRY@12:
Ceci indique que le label DLLENTRY est appelé avec 12 octets poussés sur la pile, soit trois DWords.
Après avoir effectué ces changements dans votre script source, vous êtes prêt à constituer un fichier batch avec l'extension .bat pour l'assembler, puis traiter le fichier résultant dans l'éditeur de liens. En voici une rédaction possible :
GoAsm MyProg.asm
LINK @Respons.fil
Vous lancez le fichier batch en saisissant son nom en regard de l'invite de commande MS-DOS puis en validant sur Entrée. Le fichier Respons.fil pourrait contenir les lignes suivantes :
/OUT:MyProg.Exe ; donne le nom du fichier de sortie
/MAP ; produit un fichier map
/SUBSYSTEM:WINDOWS ; élabore un exécutable Windows GDI
/ENTRY:START ; correspond au _START du script source!
/DEBUG:FULL ; faire une sortie debug
/DEBUGTYPE:COFF ; faire des symboles COFF embarqués
MyProg.obj ; le fichier d'entrée
comctl32.lib ;
user32.lib ; fichiers lib pour chaque API
gdi32.lib ; qui appelle votre programme, ces
kernel32.lib ; fichiers sont livrés avec le linker
VI-M-3-c. Définitions facilitant l'interfaçage avec l'éditeur de liens Microsoft▲
Pour donner à votre code source une meilleure apparence en utilisant l'éditeur de liens Microsoft vous pouvez faire en sorte que les appels d'API soient enrichis de manière appropriée en définissant le nom de l'API pour une utilisation tout au long de votre code, par exemple :
GetKeyState=
_GetKeyState@4
CALL
GetKeyState
ou, pour appeler l'API plus directement, vous pouvez utiliser :
GetKeyState=
__imp__GetKeyState@4
CALL
[GetKeyState]
VI-M-4. Conservation de symboles de soulignement dans les appels de bibliothèque avec le commutateur /gl▲
Certains éditeurs de liens, tels que MinGW Linker, attendent au moins un symbole de soulignement pour les appels vers les fonctions C. Si dans GoAsm vous appelez une fonction dans une bibliothèque de code statique, cette bibliothèque peut faire des appels à des fonctions C. Mais GoAsm élimine normalement le symbole de soulignement dans la perspective que vous allez normalement utiliser GoLink. Mais si vous utilisez un éditeur de liens de ce type, il faut que le soulignement soit conservé. C'est précisément la fonction du commutateur /gl dans la ligne de commande.
VII. Programmation en 64 bits▲
Ce chapitre est destiné à ceux qui souhaitent écrire des programmes 64 bits pour les processeurs AMD64 et Intel IA 32/64 fonctionnant en x64 (Windows 64 bits), en utilisant GoAsm (assembleur), GoRC (compilateur de ressources) et GoLink (linker). Il peut également intéresser ceux qui écrivent des programmes en assembleur 64 bits pour Windows avec d'autres outils.
VII-A. Introduction à la programmation 64 bits▲
VII-A-1. Programmer en 64 bits, c'est simple !▲
Malgré les différences entre les processeurs 64 bits et leurs homologues 32 bits et entre les systèmes d'exploitation x64 (Win64) et Win32, l'écriture des programmes Windows 64 bits est aussi facile qu'elle l'était dans Win32 avec l'assembleur GoAsm.
En fait, vous pouvez utiliser sans hésitation le même code source pour créer un exécutable pour les deux plateformes si vous vous conformez à l'ensemble de règles décrites plus avant dans ce chapitre.
Vous pouvez également convertir un code source 32 bits existant en 64 bits sachant qu'une partie du travail nécessaire à cet effet peut être fait automatiquement en utilisant le convertisseur AdaptAsm.
VII-A-2. Différences entre les exécutables 32 bits et 64 bits▲
Bien que les exécutables 32 bits et 64 bits s'appuient sur le même format PE (Portable Executable), il existe un certain nombre de différences importantes. Leur ampleur a pour conséquence que le code 32 bits ne pourra fonctionner sur Win64 qu'en utilisant le Windows du sous-système Windows (WOW64). Cela fonctionne par l'interception des appels d'API de l'exécutable et la conversion des paramètres en accord avec les exigences de Win64. En revanche, le code 64 bits ne fonctionnera pas du tout sur les plateformes 32 bits.
L'exécutable contient un flag qui indique au système au moment du chargement s'il est en 32 ou 64 bits. Si le chargeur x64 voit un exécutable 32 bits, WOW64 se lance automatiquement. Cela signifie qu'il est impossible de faire cohabiter dans le même exécutable du code 32 et 64 bits.
Il résulte de ce qui précède que le programmeur doit choisir entre :
- faire une version de l'application (Win32) qui fonctionnera sur les deux plateformes ;
- faire deux versions de l'application, une pour Win32, l'autre pour Win64.
Pour ceux qui sont intéressés par le format de fichier PE, voici un résumé des principales différences entre les exécutables 32 bits et 64 bits.
- Le format de fichier PE pour les fichiers Win64 se nomme « PE +».
- La taille du champ d'en-tête optionnel dans l'en-tête COFF est 0F0h dans un fichier PE+ et 0E0h dans un fichier PE.
- Le « type de machine » dans l'en-tête COFF n'est pas 14CH (comme pour les processeurs x86), mais 8664h (pour le processeur AMD64).
- Le « nombre magique » au début de l'en-tête optionnel est 20Bh au lieu de 10Bh.
- La « majorsubsystemversion » dans un fichier PE+ est de 5 au lieu de 4 dans un fichier PE.
- « L'image » exécutable (le code/data tel que chargé en mémoire) d'un fichier Win64 est limitée en taille à 2 Go. En effet, les processeurs AMD64 / EM64T utilisent l'adressage relatif pour la plupart des instructions, et l'adresse relative est conservée dans un DWord. Un DWord suffit seulement à gérer une valeur relative de ± 2 Go.
- La table d'adresses d'importation (où le chargeur remplace les adresses des appels externes telles que les adresses des API dans les DLL système) est élargie à 64 bits, tout comme la table d'importation. En effet, l'adresse d'appels externes pourrait être n'importe où dans la mémoire.
- La base d'image préférée, les champs SizeofStackReserve, SizeofStackCommit, SizeofHeapReserve et SizeofHeapCommit dans l'en-tête optionnel sont élargis de 4 à 8 octets.
- L'adresse de base par défaut dans Win64 est 400000h comme dans les fichiers Win32.
- Les exécutables 64 bits qui assurent correctement en Win64 intégral une gestion d'exception complète contiennent une section .pdata gérant les tables nécessaires à cet effet.
Vous pouvez explorer les arcanes d'un fichier PE en utilisant l'utilitaire PEView de Wayne J. Radburn.
VII-A-3. Différences entre Win32 et Win64 (pour AMD64/EM64T)▲
Voici les principales différences entre Win32 et Win64 susceptibles d'intéresser le programmeur en assembleur ou Windows :
- Convention d'appel : Win32 utilise la convention STDCALL alors que Win64 utilise la convention FASTCALL.
Dans STDCALL tous les paramètres envoyés à une API sont poussés successivement sur la pile (avec un PUSH). Dans Win32 le pointeur de pile (ESP) est réduit de quatre octets à chaque PUSH. En STDCALL il est de la responsabilité de l'API de restaurer l'équilibre de la pile en sortie.
En FASTCALL, les quatre premiers paramètres sont envoyés à l'API par l'intermédiaire de registres (dans l'ordre : RCX, RDX, R8 puis R9), mais le cinquième paramètre et les suivants sont classiquement mis en pile par des PUSH. Dans Win64, le pointeur de pile (RSP) est réduit de huit octets à chaque PUSH. Contrairement à STDCALL, il n'est pas de la responsabilité de l'API de rétablir l'équilibre de la pile en fin d'exécution. Cette tâche incombe à l'appelant de l'API. Ce dernier doit également veiller à ce qu'il y ait un espace suffisant sur la pile pour que l'API puisse y stocker les paramètres passés dans les registres. En pratique, cela est obtenu en réduisant le pointeur de pile de 32 octets juste avant l'appel.
Notez que dans GoAsm tout le travail requis par la convention d'appel FASTCALL se fait automatiquement si vous utilisez INVOKE ou ARG suivie de INVOKE. Voir la section concernant le codage pour se conformer à la convention d'appel FASTCALL. L'utilisation de ARG et INVOKE est décrite en détail dans le corps du manuel GoAsm.
Notez que GoAsm ne fait pas encore ceci pour les paramètres qui doivent être envoyés dans les registres XMM (i.e. dans instructions en virgule flottante).
- Windows utilise la convention FASTCALL pour appeler les procédures de fenêtre et autres procédures callback dans votre application. Cela signifie que vos procédures de fenêtre vont récupérer les paramètres d'une manière différente sous Win64. En outre, les procédures de fenêtre n'ont plus à restaurer la pile à l'équilibre en sortie.
Notez que GoAsm mettra en œuvre ces choses automatiquement si vous utilisez FRAME … ENDF qui est décrite en détail dans le corpus du manuel GoAsm.
- Toutes les fonctions utilisant une trame de pile (y compris les procédures de fenêtres) doivent respecter certaines règles si elles souhaitent faire usage de la gestion d'exceptions. Les outils doivent également ajouter des enregistrements de trame d'exception à l'exécutable. Ces aspects seront également gérés automatiquement par les outils « Go ».
Notez cependant qu'ils ne sont pas encore disponibles à ce jour.
- Volatilité des registres. En Win32, les procédures de fenêtres et autres procédures callback doivent restaurer le contenu des registres EBP, EBX, EDI et ESI avant de retourner à l'appelant (si la valeur de ces registres est modifiée). Cette fonction est assurée par les API Windows (ces registres ne changent pas lorsque vous appelez une API). C'est pour cette raison qu'ils reçoivent le qualificatif de registres « non volatils ». Dans Win64, cette catégorie de registres est étendue à RBP, RAX, RDI, RSI, R12 à R15 et XMM6 à XMM15.
Les registres dits « volatils » sont ceux qui peuvent être modifiés par les API, et dont vous n'avez pas besoin pour sauvegarder et restaurer dans vos procédures de fenêtre et autres procédures callback. Dans Win32, les registres volatils à usage général étaient EAX, ECX et EDX. Ceux-ci sont maintenant étendus à RAX, RCX, RDX, et R8 à R11.
- Vous pourriez ne pas avoir prévu cela, mais dans l'assemblage 64 bits pour AMD64, les pointeurs de code et de données dont les adresses sont dans l'exécutable ne sont encore qu'à 32 bits. Cela rejoint le fait que l'adressage relatif RIP limite la taille de l'exécutable à 2 Go. Les pointeurs vers des adresses externes, telles que les fonctions de DLL, sont à 64 bits afin que la fonction puisse se situer n'importe où dans la mémoire. Voir, à ce sujet la section tailles des adresses de call.
- En Win64 la taille de donnée de tous les handles et pointeurs est maintenant de 64 bits au lieu de 32 bits. Voir à ce sujet la section modifications des types de données Windows.
- En Win64 il y a des exigences plus strictes pour l'alignement de la pile, des données, et pour les structures (voir la section alignement et comblement automatiques des structures et de leurs membres).
- Les API Windows ont été modifiées pour fonctionner en 64 bits. Il y a, cependant, un petit nombre de nouvelles API pour gérer les exigences supplémentaires de fonctionnement en 64 bits. En voici la liste :
- GetClassLongPtr ;
- GetWindowLongPtr ;
- SetClassLongPtr ;
- SetWindowLongPtr.
Notez que, comme dans Win32, vous pouvez écrire vos applications soit avec la version ANSI (cas le plus courant), soit avec la version Unicode des API. Voir le chapitre « Écriture des programmes Unicode » du volume 2.
VII-A-4. Différences entre les processeurs x86 et x64▲
Les principales différences résident dans l'extension de la gamme registres disponibles, des modifications apportées à quelques instructions et l'utilisation de l'adressage relatif RIP. Les notes ci-dessous font référence au processeur AMD64 en mode 64 bits. Dans ce mode ce processeur peut également faire fonctionner des exécutables 32 bits.
VII-A-5. Registres▲
L'AMD64 ajoute plusieurs nouveaux registres à ceux disponibles dans la série de processeurs x86, et propose également de nouvelles façons d'aborder les registres existants.
- Les registres à « usage général » EAX, EBX, ECX, EDX, ESI, EDI, EBP et ESP sont tous élargis à 64 bits et prennent respectivement pour nom RAX, RBX, RCX, RDX, RSI, RDI, RBP et RSP.
- Vous pouvez toujours accéder au DWord de poids faible de ces registres (i.e. les 32 bits moins significatifs) en utilisant les noms originels EAX, EBX, ECX, EDX, ESI, EDI, EBP et ESP.
- Vous pouvez toujours accéder au mot de plus faible poids de ces registres (i.e. les 16 bits moins significatifs) en utilisant les noms originels AX, BX, CX, DX, SI, DI, BP et SP.
- Vous pouvez toujours accéder au premier octet de RAX, RBX, RCX et RDX (i.e. les 8 bits les moins significatifs) en utilisant les noms existants AL, BL, CL, DL comme dans le processeur x86. De plus, il vous est désormais possible de communiquer avec l'octet de plus faible poids des registres « d'index » en utilisant SIL, DIL, BPL et SPL. SIL correspond, par exemple, aux 8 bits les moins significatifs du registre d'index RSI.
- De même, vous pouvez toujours accéder au deuxième octet (bits 8 à 15) de RAX, RBX, RCX et RDX en utilisant les noms existants AH, BH, CH, DH comme dans le processeur x86. Cependant, les opcodes pour ce faire ont été modifiés dans le processeur AMD64. Ils entrent en conflit maintenant avec les opcodes désignant la version 8 bits des registres étendus R8 à R15. Vous ne pouvez donc pas utiliser AH, BH, CH, DH et R8B à R15B dans la même instruction.
- Il y a huit nouveaux registres 64 bits (les « registres étendus ») nommés R8 et R15.
- Le DWord de plus faible poids de ces registres (i.e. les 32 bits les moins significatifs) est accessible sous la forme R8D à R15D.
- Le mot de plus faible poids de ces registres (i.e. les 16 bits les moins significatifs) est accessible sous la forme R8W à R15W.
- L'octet de plus faible poids de ces registres (i.e. les 8 bits les moins significatifs) est accessible sous la forme R8B à R15B.
- Il y a huit nouveaux registres XMM (128 bits) nommés XMM 8 à XMM 15.
- Les registres MMX 64 bits (MM0 à MM7) sont toujours disponibles. Comme dans le processeur x86, ils sont également utilisés en tant que registres à virgule flottante (ST0 à ST7) pour les instructions x87 en virgule flottante.
- Le pointeur d'instruction est maintenant dans le registre 64 bits RIP.
VII-A-6. Instructions▲
- Certaines instructions ne sont pas disponibles dans le mode 64 bits. Les opcodes sont maintenant utilisés à d'autres fins. La liste complète est fournie dans les manuels AMD et Intel et comprend AAA, AAD, AAM, AAS, DAA, DAS, PUSH CS, ainsi que les opérations PUSH et POP sur les registres de segment DS, ES et SS.
- La portée des instructions est élargie pour accueillir les nouveaux registres et les nouvelles formes d'adresse, par exemple :
MOV
RAX, immediate ; mémorise un nombre 64 bits dans un registre 64 bits
JRCXZ >
L1 ; si RCX = 0 saut vers l'avant au label L1
- Les instructions de chaîne sont maintenant agrandies pour permettre l'adressage 64 bits pour, par exemple :
LODSB
; équivaut maintenant à MOV AL, [RSI] puis INC RSI
LODSW
; équivaut maintenant à MOV AX, [RSI] puis ADD RSI, 2
LODSD
; équivaut maintenant à MOV EAX, [RSI] puis ADD RSI, 4
LODSQ ; NOUVEAU ! équivaut à MOV RAX, [RSI] puis ADD RSI, 8
CMPSB
; équivaut maintenant à CMP B[RSI], B[RDI] puis INC RSI, RDI
CMPSQ ; NOUVEAU ! équivaut à CMP Q[RSI], Q[RDI] puis ADD RSI, 8 et ADD RDI, 8
MOVSW
; équivaut maintenant à MOV W[RDI], W[RSI] puis ADD RSI, 2 et ADD RDI, 2
MOVSQ ; NOUVEAU ! équivaut à MOV Q[RDI], Q[RSI] puis ADD RSI, 8 et ADD RDI, 8
SCASD
; équivaut maintenant à CMP [RDI], EAX puis ADD RDI, 4
SCASQ ; NOUVEAU ! équivaut à CMP [RDI], RAX puis ADD RDI, 8
STOSQ ; NOUVEAU ! équivaut à MOV [RDI], RAX puis ADD RDI, 8
Les préfixes de répétition REP, REPZ et REPZ utilisent RCX en lieu et place de ECX. Les instructions de boucle LOOP, LOOPZ et LOOPNZ font de même. L'instruction de consultation de table XLAT utilise RBX plutôt que EBX.
- En dehors de ce qui précède, la seule nouvelle instruction notable utilisable par les programmeurs est MOVSXD qui peut déplacer de 32 bits de données à partir d'un registre ou de la mémoire vers un registre 64 bits, avec extension du bit de signe 31 dans tous les bits supérieurs. On note aussi un ensemble de nouvelles instructions système.
- Dans l'AMD64, chaque instruction PUSH et POP déplace le pointeur de pile de 8 octets au lieu de 4 octets comme dans le processeur x86. Cela signifie que l'instruction PUSH registre 32 bits n'est plus admise sur le processeur AMD64. Pour aider à la compatibilité du code source, GoAsm traite (par exemple) PUSH EAX comme équivalent à PUSH RAX. En mode /x86, GoAsm traite PUSH RAX comme équivalent à PUSH EAX. Ces interprétations automatiques de l'écriture initiale exigent une grande attention de la part du programmeur.
- PUSH immediate sur l'AMD64 prend une valeur immédiate limitée à 32 bits (nombre), en étend le signe du bit 31 en copiant ce dernier du bit 32 au bit 63 inclus, puis pousse en pile l'ensemble 64 bits ainsi constitué. Il n'y a en effet aucune instruction capable de pousser directement une valeur immédiate 64 bits sur la pile. Pour cette raison, l'instruction PUSH ADDR CHOSE n'est pas reconnue sur l'AMD64 (la valeur de décalage est traitée comme étant immédiate). Le problème ici est que la valeur immédiate courante de tout décalage est inconnue jusqu'au moment de l'édition de liens et qu'au moment de l'assemblage il est impossible pour l'assembleur de savoir si ce décalage est au-dessus 7FFFFFFFh et qu'il serait ainsi touché par l'extension de signe.
Par conséquent, dans GoAsm, PUSH ADDR CHOSE fait usage du registre R11 et profite de l'adressage relatif RIP plus court de LEA avec le codage suivant :
LEA
R11, [CHOSE]
PUSH
R11
- Les instructions 3DNow! sont toujours disponibles dans le AMD64. On ne sait pas si elles sont maintenant disponibles sur les processeurs compatibles avec la technologie Intel EM64T.
VII-A-7. L'adressage relatif RIP▲
Certaines instructions du processeur AMD64 qui adressent le code ou les données, utilisent l'adressage relatif RIP pour ce faire. L'adresse relative est contenue alors dans un DWord qui fait partie de l'instruction. Lorsque vous utilisez ce type d'adressage, le processeur ajoute trois valeurs :
- le contenu du DWord contenant l'adresse relative ;
- la longueur de l'instruction ;
- la valeur de RIP (le pointeur d'instruction courant) au début de l'instruction.
La valeur résultante est alors considérée comme l'adresse absolue de donnée ou de code devant être traitée par l'instruction. Dans la mesure où l'adresse relative peut être négative, il est possible d'adresser la donnée ou le code plus tôt, ou plus tard, dans la représentation de RIP. La gamme est d'environ ± 2 Go, en fonction de la taille de l'instruction. Si l'on considère que l'adressage relatif ne peut s'exercer au-delà de cette plage, cela constitue la limite pratique de taille des représentations 64 bits.
L'adressage relatif RIP agit à l'insu de l'utilisateur. Le processeur utilise ce mode si les opcodes contiennent certaines valeurs (dans l'octet ModRM, le champ Mod est égal à 00 binaire, et le champ r/m est égal à 101 binaire). Vous ne pouvez pas en prendre le contrôle, sauf en changeant le type d'instructions que vous utilisez. En général, voici les règles qui déterminent si oui ou non une instruction utilise l'adressage relative RIP.
- Les adresses dans les données ne peuvent pas utiliser l'adressage relatif RIP puisque la valeur de ce registre ne peut être connue au moment où ces adresses sont définies. Au lieu de cela, une adresse absolue pour l'insertion est calculée au moment de l'édition des liens. Ainsi, les instructions suivantes n'utilisent pas l'adressage relatif RIP, mais lui préfèrent l'adressage absolu :
MyDataLabel1 DQ
MyDataLabel3 ; adresse du label de donnée
MyDataLabel2 DQ
MyCodeLabel ; adresse du label de code
MyDataLabel3 DQ
$ ; utilisation du pointeur courant de donnée
MyDataLabel4 DD
MyDataLabel3 ; adresse du label de donnée
MyDataLabel5 DT
MyCodeLabel ; adresse du label de code
MyDataLabel6 DD
$ ; utilisation du pointeur courant de donnée
Notez que, dans la pratique, l'adresse absolue est contenue dans un DWord et non dans un QWord. Voilà pourquoi, dans les exemples qui précèdent, les adresses de donnée et de code peuvent être contenues dans une déclaration de données DWord. Cette restriction est possible parce que la taille pratique de l'image est limitée à 2 Go de toute façon en raison des restrictions imposées par l'adressage relatif RIP.
- Les offsets convertis en valeurs immédiates soit au moment de l'assemblage, soit au moment de l'édition des liens utilisent l'adressage absolu plutôt que l'adressage relatif. Par exemple, les instructions suivantes n'utilisent pas l'adressage relatif RIP, mais lui préfèrent l'adressage absolu :
MOV
RAX, ADDR
MyDataLabel3 ; adresse du label de donnée copiée dans un registre
MOV
MM0
, ADDR
MyCodeLabel ; adresse du label de code copiée dans un registre
MOV
Q[RSP], ADDR
MyDataLabel3 ; adresse du label de donnée copiée dans une adresse mémoire
MOV
Q[RSP], ADDR
MyCodeLabel ; adresse du label de code copiée dans une adresse mémoire
Cependant, GoAsm code en fait MOV RAX, ADDR MyDataLabel3 et les instructions similaires en utilisant l'instruction LEA plus courte, qui utilise l'adressage relatif RIP.
À noter également que pour un MOV à destination de la mémoire d'un ADDR, GoAsm utilise le registre R11 et tire avantage de l'adressage relatif RIP plus court de LEA avec le codage suivant :
LEA
R11, ADDR
Non_Local_Label
MOV
[Memory64], R11
- Voici des exemples d'autres instructions qui utilisent l'adressage relatif RIP :
MOV
RAX, [MyDataLabel3+
55h
] ; adresse du label de donnée
RCL
Q[MyDataLabel3], 1
; adresse du label de donnée
MOV
Q[MyDataLabel3], 20h
; adresse du label de donnée
PAVGUSB
MM3
, [MyDataLabel3] ; instruction 3DNow!
CALL
ExitProcess ; adresse du label de code (API système)
JMP
InternalCodeLabel ; adresse du label de code à l'intérieur du module
CALL
InternalCodeLabel ; adresse du label de code à l'intérieur du module
CALL
ExternalCodeLabel ; adresse du label de code à l'extérieur du module
PUSH
[MyData] ; sauvegarde du contenu d'un label de donnée
POP
[MyData] ; restauration du contenu d'un label de donnée
Notez que, dans le cas d'un appel externe, l'adresse relative pointe sur la table d'adresses d'importation (Import Address Table). Dans la mesure où cette table est maintenant étendue à 64 bits, il est possible d'appeler un label de code partout dans la mémoire.
- LEA utilise l'adressage relatif RIP. Par exemple :
LEA
RBX, MyDataLabel3 ; charge dans RBX l'adresse du label de donnée
- L'adressage relatif RIP n'est pas utilisé lorsque le label de donnée ou de code est complété par un registre d'index. Bien que cela puisse sembler étrange, la raison semble être que l'ajout d'informations sur le registre sur l'opcode signifie que le processeur ne peut plus reconnaître l'instruction en tant qu'utilisatrice de l'adressage relatif RIP (dans l'octet ModRM, le champ Mod n'est plus égal à 00 binaire, et le champ r/m n'est plus égal à 101 binaire). Cela signifie que les instructions suivantes utilisent des adresses absolues plutôt que relatives :
MOV
RAX, [ESI
+
MyData]
RCL
Q[EBX
+
MyData], 1
MOV
Q[RSI*
2
+
MyData], 44444444h
PAVGUSB
MM3
, [R12+
MyData]
LEA
RBX, MyData+
RSI
CALL
[MyCall+
RDI]
JMP
[MyJump2+
RDI]
PUSH
[MyCall+
RSI]
POP
[MyCall+
R12]
Dans la mesure où l'adressage relatif RIP n'est pas utilisé ici, la Base Image doit être inférieure à 7FFFFFFF pour que ces types d'instructions fonctionnent correctement. Ces instructions doivent être réaménagées si vous utilisez une plus grande Base Image, sauf à pratiquer l'édition de liens avec l'option /LARGEADDRESSAWARE.
Ayant à l'esprit que la taille de l'image est limitée à 2 Go par les dispositions ci-dessus, on pourrait penser que les avantages de l'adressage relatif RIP sont quelque peu limités. Le seul intérêt de cette pratique semble résider dans le fait qu'elle réduit le nombre de relocations qui devraient être effectuées par le chargeur si une DLL venait à être chargée à une adresse inattendue. Le chargeur aurait alors besoin d'ajuster toutes les adresses absolues en fonction de la base de l'image réelle, alors que des adresses relatives n'auraient pas à être modifiées car elles se réfèrent à d'autres parties de l'image virtuelle de l'exécutable. Cependant, il est de bonne pratique pour le programmeur de choisir une base d'image appropriée au moment de l'édition de liens pour éviter la nécessité de relocations dans une DLL en premier lieu. Un bon exemple de ceci est celui des DLL système. Elles ont toutes une base d'image différente qui permet d'éviter efficacement des conflits potentiels de l'image dans la mémoire qui nécessiterait la réinstallation au moment du chargement.
VII-A-8. Taille des adresses de Call▲
Lors de l'assemblage 64 bits, un simple CALL à un label de code, par exemple
CALL
CALCULATE
sera codé E8 en tant qu'appel relatif RIP utilisant un DWord pour fournir le décalage de RIP. La destination de cet appel pourrait être un label de code interne (c'est-à-dire une procédure ou une fonction au sein de l'exécutable lui-même). Ou elle pourrait être un label de code externe, comme une API dans une DLL système ou un label de code exporté par un autre EXE ou une DLL. La première destination de l'appel d'un label de code externe est la Table d'Adresses d'Importation (Import Address Table) qui fait partie de l'exécutable lui-même. Cette table est écrite par le chargeur lorsque l'exécutable commence. Par conséquent, pendant l'exécution, la table contient les adresses absolues dans la mémoire virtuelle de la destination éventuelle de l'appel. Dans un exécutable 64 bits, la table contient des valeurs 64 bits, de sorte que l'appel relatif RIP codé E8 est capable d'appeler une procédure ou une fonction partout dans la mémoire.
L'appel à une adresse mémoire identifiée par un label de donnée, contenue dans un registre ou dans un emplacement mémoire pointé par un registre est traité, en revanche, de manière différente. Il n'est pas acheminé par l'intermédiaire de la Table d'Adresses d'Importation. Cet appel doit également permettre d'autoriser la destination de l'appel partout dans la mémoire. Pour ce faire, il doit utiliser une adresse absolue 64 bits. Voici quelques exemples de ces types d'appels :
CALL
RAX
CALL
EAX
; produit le même code que CALL RAX
CALL
[Table+
8h
]
CALL
[RSI]
CALL
[ESI
] ; produit le même code que CALL [RSI]
Ici, vous devez tenir compte du fait que l'appel est dirigé vers un QWord et non vers un DWord.
Voir la section quelques pièges à éviter lors de la conversion du code source existant.
VII-B. Modifications apportées aux types de données Windows▲
Voici une liste des changements apportés aux types de données entre 32 et 64 bits.
VII-B-1. Tous les handles passent de DWORD à QWORD▲
Par exemple :
- HACCEL, HINSTANCE, HBRUSH, HBITMAP
HCOLORSPACE, HCURSOR, HDC, HFONT
HICON, HINSTANCE, HKEY, HLOCAL
HMENU, HMODULE, HPEN, HPALETTE, HWND
(+ les autres commençant par H)
Exceptions :
- HRESULT, HFILE demeurent des DWords, et HALF_PTR (voir ci-dessous).
VII-B-2. Tous les pointeurs passent de DWORD à QWORD▲
Par exemple :
- LPCSTR, LPCTSTR, LPLONG, LPSTR
(+ les autres commençant par LP)
PBOOL, PHANDLE, PHKEY, PVOID
(+ les autres commençant par P)
DWORD_PTR, ULONG_PTR, UINT_PTR
(+ les autres s'achevant par _PTR)
et LRESULT
Exceptions :
- HALF_PTR, et UHALF_PTR qui sont maintenant des DWords au lieu de Words.
POINTER_32, qui reste un pointeur 32 bits.
VII-B-3. WPARAM et LPARAM deviennent des QWORD au lieu de DWORD▲
Voici une liste des types de données qui demeurent inchangés :
|
|
VII-B-4. Utilisation de l'indicateur de type commutable▲
La modification ci-dessus du type d'une donnée peut nécessiter la modification correspondante d'un indicateur de type. La lettre P est réservée en tant qu'indicateur de type paramétrable dans toutes les situations où GoAsm pourrait s'attendre à en trouver un. Les choses peuvent se présenter de la manière suivante :
#if
x64
P =
8
#else
P =
4
#endif
P peut prendre une valeur correspondant à l'un quelconque des indicateurs de type prédéfinis tels que B, W, D, Q ou T. Concrètement, P contient le nombre d'octets du type. Dans l'exemple ci-dessus, P représente le type Q (valeur 8) ou D (valeur 4). Par conséquent, vous pouvez contrôler la taille de l'instruction avec elle, par exemple :
MOV
P[RDI], 0
; RAZ du qword à RDI si 64-bit, du dword à EDI si 32-bit
LOCAL
POINTERS[10
]:P ; fait un pointeur de buffer local de 80 octets si 64 bits,
; de 40 bits si 32 bits
VII-C. Exigences en matière d'alignement▲
Les exigences du système dans Win64 en matière d'alignement du pointeur de pile, des données et des membres de structure sont beaucoup plus strictes que dans Win32. Un mauvais alignement peut causer, au mieux une perte de performance, au pire, une exception ou une sortie inopinée du programme.
VII-C-1. Alignement de la pile▲
Le pointeur de pile (RSP) doit être aligné sur une modularité de 16 octets lors d'un appel d'API. Cependant, cet alignement est géré automatiquement par GoAsm si vous utilisez INVOKE. Voir sur ce point la section alignement automatique de la pile.
VII-C-2. Alignement des données▲
Toutes les données doivent être alignées sur une « frontière naturelle ». Donc, un octet peut être aligné sur un octet, un mot sur deux octets, un DWord sur quatre octets et un QWord sur huit octets. Un TWord devrait également être aligné sur un QWord. GoAsm procède à un alignement automatique lorsque vous déclarez des données locales (dans une zone FRAME ou USEDATA). Mais il sera nécessaire que vous organisiez vos propres déclarations de données pour garantir que les données seront correctement alignées. La meilleure façon d'y parvenir est de déclarer tout d'abord tous les QWords, puis tous les DWords, puis tous les Words et, enfin, tous les octets. Les TWords (10 octets) éventuels pourraient cependant mettre à mal l'alignement des déclarations suivantes, de sorte qu'il est recommandé de les déclarer en tout premier lieu, puis de déclarer les types inférieurs dans l'ordre décroissant (QWord, puis, DWord, etc.) en ayant soin de les faire précéder par la directive d'alignement ALIGN 8.
S'agissant des chaînes et en conformité avec les règles ci-dessus, les chaînes Unicode doivent être alignées sur deux octets, alors que les chaînes ANSI peuvent l'être sur un octet.
Lorsque des structures sont utilisées, elles doivent être alignées sur la « frontière naturelle » du plus grand membre. Tous les membres de la structure doivent également être alignés correctement, et la structure elle-même doit être comblée, le cas échéant, par des octets de bourrage de manière à ce qu'elle puisse se terminer sur une limite naturelle (le système peut être amené à écrire dans cette zone). En raison de l'importance fondamentale de cette contrainte, GoAsm aligne automatiquement les structures depuis la version 0.56 (bêta). Voir la section Alignement et comblement automatiques des structures et de leurs membres.
VII-D. Structures Windows en programmation 64 bits▲
Windows utilise souvent les structures pour envoyer et recevoir des informations lorsque l'on utilise des API. En 64 bits, ces structures sont susceptibles d'être considérablement différentes de leurs homologues 32 bits en raison de l'élargissement de nombreux types de données à 64 bits. Voir la section « Modifications apportées aux types de données Windows ».
VII-D-1. Structure WNDCLASS▲
Prenons par exemple la structure WNDCLASS qui est utilisée lorsque vous souhaitez enregistrer une classe de fenêtre :
WNDCLASS STRUCT
style DD
0
; +0 style de classe de fenêtre
DD
0
; +4 octets de comblement
lpfnWndProc DQ
0
; +8 pointeur vers la procédure de fenêtre
DD
0
; +10 nb d'octets supplémentaires à allouer après la structure
DD
0
; +14 nb d'octets supplémentaires à allouer après l'instance de fenêtre
hInstance DQ
0
; +18 handle de l'instance contenant la procédure de fenêtre
hIcon DQ
0
; +20 handle de l'icône de classe
hCursor DQ
0
; +28 handle du curseur de classe
hbrBackground DQ
0
; +30 identifie la brosse de l'arrière-plan de la classe
lpszMenuName DQ
0
; +38 pointeur vers le nom de ressource pour le menu de classe
lpszClassName DQ
0
; +40 pointeur vers la chaîne contenant le nom de la classe de fenêtre
ENDS
Un certain nombre de membres sont maintenant des QWords, alors qu'auparavant il s'agissait de DWords comme vous pouvez le voir dans la version 32 bits ci-dessous. Le style de classe à l'offset + 0h demeure un DWord, mais dans la version 64 bits, un bourrage de quatre octets est nécessaire parce que le membre suivant est un QWord. Ceci est conforme à l'exigence selon laquelle les membres d'une structure doivent être alignés sur leur frontière naturelle. Suivent, jusqu'à la fin de la structure, une série de QWords incluant trois pointeurs. Le premier (en +8h) est utilisé pour pointer la procédure Window elle-même, le second, en + 38h pointe le nom de la ressource de la classe Menu et le troisième, en +40h, pointe le nom de classe de fenêtre. Ceci en dépit du fait que les programmes 64 bits tels qu'implémentés par Win64 pour le processeur AMD64 utilisent uniquement des pointeurs 32 bits où ces pointeurs donnent les adresses des données internes. On peut en attribuer la raison au fait que les structures utilisées ici sont les mêmes que celles qui sont mises en œuvre pour la famille de processeurs Intel IA-64 (lesquels emploient, pour leur part, des pointeurs 64 bits en direction des données internes). Enfin, les handles de la structure sont également étendus à 64 bits.
WNDCLASS STRUCT
style DD
0
; +0 style de la classe de fenêtre
lpfnWndProc DD
0
; +4 pointeur vers la procédure de fenêtre (callback)
DD
0
; +8 nb d'octets supplémentaires à allouer après la structure
DD
0
; +C nb d'octets supplémentaires à allouer après l'instance de fenêtre
hInstance DD
0
; +10 handle de l'instance contenant la procédure de fenêtre
hIcon DD
0
; +14 handle de l'icône de classe
hCursor DD
0
; +18 handle du curseur de classe
hbrBackground DD
0
; +1C identifie la brosse de l'arrière-plan de la classe
lpszMenuName DD
0
; +20 pointeur vers le nom de la ressource pour le menu de classe
lpszClassName DD
0
; +24 pointeur de la chaîne contenant le nom de classe de fenêtre
ENDS
Voici un autre exemple, cette fois celui de la structure DRAWITEMSTRUCT sous ses formes 32 et 64 bits :
typedef
struct
DRAWITEMSTRUCT {
UINT CtlType; // +0
UINT CtlID; // +4
UINT itemID; // +8
UINT itemAction; // +C
UINT itemState; // +10
HWND hwndItem; // +14
HDC hDC; // +18
RECT rcItem; // +1C
ULONG_PTR itemData; // +2C
// (la taille totale de la structure est de 30h octets)
}
DRAWITEMSTRUCT;
typedef
struct
DRAWITEMSTRUCT {
UINT CtlType; // +0
UINT CtlID; // +4
UINT itemID; // +8
UINT itemAction; // +C
UINT itemState; // +10
// Dwords de bourrage
HWND hwndItem; // +18
HDC hDC; // +20
RECT rcItem; // +28
ULONG_PTR itemData; // +38
// (la taille totale de la structure est de 40h octets)
}
DRAWITEMSTRUCT;
En 64 bits, on retrouve l'exigence selon laquelle la structure est agrandie de telle sorte qu'elle se termine sur la limite naturelle de son plus grand membre. Cette contrainte est satisfaite en ajoutant le bourrage nécessaire à l'extrémité de la structure.
Dans ces mêmes conditions, PAINTSTRUCT devient, en 64 bits :
PAINTSTRUCT STRUCT
hDC DQ
0
; +0
fErase DD
0
; +8
left DD
0
; +C ┐
top DD
0
; +10 │ RECT
right DD
0
; +14 right │
bottom DD
0
; +18 bottom ┘
fRestore DD
0
; +1C
fIncUpdate DD
0
; +20
rgbReserved DB
32
DUP
0
;+24
DD
0
; +44 octets de bourrage pour obtenir une taille totale de 72 octets
ENDS
Dans la pratique, il a bien été constaté que le système écrivait dans la zone de comblement à + 44h lors de l'utilisation de PAINTSTRUCT dans certaines circonstances. Cela prouve l'importance de se conformer à ces règles (sinon vous pourriez être confronté à un écrasement inopiné des données immédiatement après la structure).
Notez que le début des structures doit également être aligné sur la limite naturelle du plus grand membre. Toutes les règles ci-dessus garantissent, par conséquent, que les QWords dans la structure sont toujours alignés selon une modularité de quatre octets.
VII-D-2. Alignement et comblement automatiques des structures et de leurs membres▲
Comme nous l'avons vu, l'alignement correct des structures et de leurs membres est crucial pour le bon fonctionnement du code 64 bits. Malheureusement, les fichiers d'en-tête de Windows contenant les définitions de structure ne contiennent pas nécessairement les directives de comblement nécessaires pour atteindre un tel alignement.
C'est pourquoi, à partir de la version 0.56 (bêta), GoAsm fait ce travail automatiquement pour vous en procédant comme suit :
- GoAsm aligne toujours la structure elle-même sur la modularité de données correcte.
- GoAsm effectue toujours un comblement, si nécessaire, pour garantir que les membres de la structure sont sur leur frontière naturelle. Ainsi, dans l'exemple de structure MSG ci-dessous, le comblement à + 0Ch peut être laissé de côté car inséré automatiquement par GoAsm.
- GoAsm ajoute toujours des octets de comblement à l'extrémité d'une structure de telle sorte que celle-ci se termine sur une frontière naturelle. Ainsi, dans l'exemple ci-dessous, le comblement à + 2Ch peut être laissé de côté car inséré automatiquement par GoAsm.
- Les symboles créés lors de l'utilisation d'une structure sont automatiquement ajustés pour satisfaire l'alignement et le comblement qui sont appliqués.
MSG STRUCT
hWnd DQ
0
; +0h
message DD
0
; +8h
DD
0
; comblement pour garantir l'alignement de la suite
wParam DQ
0
; +10h
lParam DQ
0
; +18h
time DD
0
; +20h
point DD
0
; +24h position de la souris (X)
DD
0
; +28h position de la souris (Y)
DD
0
; +2Ch comblement pour porter la taille globale de la structure à 48 octets
ENDS
Vous pouvez constater par vous-même l'alignement et le comblement que GoAsm a ajoutés à votre code source si vous spécifiez /l dans la ligne de commande de GoAsm et que vous examinez le fichier listing résultant. Vous pouvez également faire le même constat avec le débogueur.
VII-D-3. Structures - Tableau d'ensemble▲
Si vous écrivez un code source destiné aux versions 32 et 64 bits de votre programme, il sera beaucoup plus facile d'utiliser l'assemblage conditionnel pour produire les structures correctes au moment de l'assemblage. De même, au lieu de remplir les structures en utilisant des décalages, il est nettement préférable d'utiliser des noms de membres. En utilisant cette méthode, GoAsm calcule automatiquement le décalage approprié. Cette technique a été utilisée dans le fichier de démonstration Hello64World 3.
Vous pouvez utiliser l'assemblage conditionnel pour changer de banque de structures en une seule fois. Celles-ci peuvent être contenues dans des fichiers d'inclusion comprenant respectivement des structures 64 bits et 32 bits.
Dans la mesure où GoAsm aligne et comble automatiquement les structures, vous pouvez utiliser les définitions de structure 64 bits déjà disponibles dans les fichiers d'inclusion, ou vous pouvez constituer les vôtres à partir des fichiers d'en-tête Windows à l'aide de l'utilitaire xlatHinc de Wayne J. Radburn.
VII-E. Choix des registres▲
La chose principale à retenir est que tous les handles de Windows sont sur 64 bits de sorte que les API opèreront leur retour dans RAX plutôt que dans EAX.
La même chose vaut pour les pointeurs de Windows. Par exemple, vous pouvez demander de la mémoire à Windows. L'adresse de cette mémoire sera retournée alors dans RAX et non dans EAX.
Cela signifie donc que
ARG 4h
, 3000h
, EDX
, 0
INVOKE
VirtualAlloc ; réserve et engage EDX octets de mémoire en lecture/écriture
MOV
[EAX
], 66666666h
; insère un nombre au début de cette mémoire
est un mauvais codage 64 bits, alors que
ARG 4h
, 3000h
, EDX
, 0
INVOKE
VirtualAlloc ; réserve et engage EDX octets de mémoire en lecture/écriture
MOV
[RAX], 66666666h
; insère un nombre au début de cette mémoire
est correct.
Étant donné que tous les pointeurs vers des labels internes de donnée et de code sont sur 32 bits, il est possible, en théorie, d'utiliser les versions 32 bits des registres à usage général (EAX à ESP) pour tous ces pointeurs de telle sorte que, par exemple, vous pourriez utiliser MOV [ESI],AL au lieu de MOV [RSI],AL.
Cependant, je déconseille cette pratique pour les cinq raisons suivantes :
- Cela signifie que vous devez garder une trace de ceux des pointeurs qui sont internes et de ceux qui sont externes. Vous devez prévoir que les externes seront sur 64 bits.
- Vous pouvez avoir besoin de deux ensembles de procédures qui sont souvent utilisés dans votre programme, l'un utilisant des pointeurs sous forme de registres 32 bits, l'autre utilisant des pointeurs sous forme de registres 64 bits.
- Les instructions de chaîne telles que LODSB, MOVSW, STOSD, CMPSQ et SCASB utilisent RSI et RDI dans un programme 64 bits en lieu et place de ESI et EDI. Et les préfixes de répétition REP, REPZ et REPNZ utilisent RCX au lieu de ECX.
- L'utilisation des versions 32 bits de ces instructions dans les codes de programme 64 bits produit un opcode plus grand que pour la version 64 bits. En effet, dans un programme 64 bits, MOV [RSI], AL est le codage par défaut et sa conversion en MOV [ESI], AL nécessite l'octet 67h d'override.
- Vous pouvez toujours utiliser le même code source pour produire un programme sur les plateformes 32 bits et 64 bits à condition que vous utilisiez uniquement les registres à usage général, RAX à RSP. En effet, lorsque vous utilisez le commutateur /x86 avec GoAsm, ces registres sont automatiquement considérés, en lieu et place, comme EAX à ESP.
Vous pouvez automatiser les modifications nécessaires au code 32 bits pour passer au 64 bits en utilisant AdaptAsm.
Si vous avez besoin d'utiliser les registres R8 à R15, rappelez-vous que R8 à R11 sont volatils, c'est-à-dire qu'ils ne seront pas préservés par les API. Si vous utilisez les registres non volatils R12 à R15 à l'intérieur des procédures de fenêtre et de procédures callback vous devez vous assurer qu'ils sont restaurés après utilisation. Cela peut être fait en utilisant PUSH au début et POP à la fin de la procédure qui les utilise, ou en utilisant l'instruction USES.
Lors du passage des paramètres à une API par INVOKE, vous devez garder présent à l'esprit que, dans la convention d'appel FASTCALL, les paramètres sont transmis à l'API successivement par les registres RCX, RDX, R8 et R9. Par conséquent, vous ne pouvez envisager de passer des paramètres dans des registres qui seront écrasés par GoAsm (vous obtiendriez un message d'erreur si vous essayiez de le faire).
Par exemple, la formulation qui suit est erronée et affiche une erreur :
INVOKE
MessageBoxW, RDX, R8, R9, R10
Elle est dans cette situation parce que, si elle était autorisée, elle se traduirait par :
MOV
R9, R10
MOV
R8, R9
MOV
RDX, R8
MOV
RCX, RDX
où l'on peut voir en effet que le contenu des registres est écrasé avant d'être utilisé pour établir les paramètres.
Une meilleure formulation pourrait être :
INVOKE
MessageBoxW, R10, R9, R8, RDX
Qui se traduirait par :
MOV
R9, RDX
MOV
RDX, R9
MOV
RCX, R10
Notez que GoAsm n'empêche pas le codage de MOV R8, R8.
Pour terminer, voici une formulation plus probante qui ne nécessite pas de code supplémentaire pour passer les paramètres car ceux-ci sont déjà dans les registres appropriés :
INVOKE
MessageBoxW, RCX, RDX, R8, R9
Ce code se révèle donc très efficace.
Voir aussi quelques conseils pour réduire la taille de votre code, qui a des implications supplémentaires dans votre choix des registres et aussi quelques pièges à éviter lors de la conversion du code source existant.
VII-F. Extension à zéro des résultats dans les registres 64 bits▲
Il convient d'être prudent lors de l'usage conjoint de registres 32 et 64 bits parce que le processeur peut modifier le contenu de la partie haute du registre 64 bits au terme d'une opération bien que rien ne le laisse présager. En effet, lors de l'écriture de résultats dans un registre 32 bits, le processeur va étendre le résultat sur l'ensemble des 64 bits du registre, avec pour conséquence de mettre inopinément à zéro les 32 bits de plus fort poids. Ainsi, :
MOV
RAX, -
1
; charge RAX avec 0FFFFFFFF FFFFFFFFh
AND
EAX
, 0F0F0F0Fh
; (apparemment) agit seulement sur EAX
le processeur étend à zéro le résultat dans RAX. En d'autres termes, le DWord de poids fort de RAX sera mis à zéro dans cette opération alors que rien de tel n'a été explicitement demandé. D'où le résultat inattendu : RAX = 00000000 0F0F0F0Fh et non pas 0FFFFFFFF 0F0F0F0Fh comme on pouvait s'y attendre. Cela se produit indépendamment de la valeur du bit 31 de RAX et se distingue, en cela, du mécanisme d'extension de signe.
Un phénomène similaire se produit lors de l'utilisation d'autres instructions. En voici un exemple avec XOR :
MOV
RAX, -
1
; charge RAX avec 0FFFFFFFF FFFFFFFFh
XOR
EAX
, EAX
; (apparemment) met EAX à zéro
Contre toute attente, le résultat dans RAX est zéro.
Ce phénomène apparaît de la même manière avec l'instruction MOV :
MOV
RCX, 1111111111111111h
MOV
ECX
, 88888888h
Le résultat, dans ce cas, est RCX=88888888h.
Vous pouvez tirer avantage de l'extension à zéro de diverses manières. Quelques exemples en sont donnés dans la section quelques conseils pour réduire la taille de votre code. Considérez également l'exemple qui suit où la structure RECT (qui est de quatre DWords) contient des valeurs qui doivent être transmises à l'API MoveWindow en tant que QWords :
MOV
RBX, ADDR
RECT
MOV
EAX
, [EBX
] ; récupère x-pos
MOV
ECX
, [EBX
+
4
] ; récupère y-pos
MOV
EDX
, [EBX
+
8
] ; récupère right
SUB
EDX
, EAX
; récupère width
MOV
R8D, [EBX
+
0Ch
] ; récupère bottom
SUB
R8D, ECX
; récupère height
INVOKE
MoveWindow, [hWnd], RAX, RCX, RDX, R8, 0
Ici seuls des registres 32 bits sont utilisés pour extraire les informations de la structure RECT, mais nous savons que la partie haute des versions 64 bits de ces registres sera systématiquement mise à zéro.
Il est possible qu'il y ait une perte de rendement en se fondant sur le mécanisme d'extension à zéro. Une partie de la documentation suggère en effet que le processeur doit effectuer une opération supplémentaire pour mettre à zéro les bits de poids fort du registre.
VII-G. Extension de signe des résultats dans les QWords▲
Vous pouvez légitimement vous interroger sur la différence qui pourrait exister entre les instructions suivantes :
MOV
D[THING], 12345678h
MOV
Q[THING], 12345678h
Ces codes produisent, contre toute attente, des résultats différents ! La version DWord, comme vous vous y attendez, place la valeur 12345678H dans le DWord du label THING. La version QWord fait la même chose, mais force également à zéro le DWord à THING+4. Techniquement, cette instruction pratique une extension de signe sur le résultat dans le QWord situé au label THING. Selon le même principe, si le bit le plus élevé du DWord de plus faible poids avait été à 1, la version QWord de l'instruction aurait rempli THING+4 avec 0FFFFFFFFh. En d'autres termes, les valeurs de 32 bits dans ces instructions sont considérées comme des nombres signés, et écrits en mémoire en conséquence.
MOV
D[THING], 12345678h
; THING prend la valeur 12345678h (comme DWord)
MOV
Q[THING], 12345678h
; THING prend la valeur 12345678h (comme QWord)
MOV
D[THING], 82345678h
; THING prend la valeur 82345678h ie. -7DCBA988h (comme DWord)
MOV
Q[THING], 82345678h
; THING prend la valeur 0FFFFFFFF 82345678h
; c'est-à-dire -7DCBA988h (comme QWord)
La même chose se produit si vous utilisez un registre pour traiter la zone de données, par exemple :
MOV
RSI, ADDR
THING
MOV
D[RSI], 12345678h
; THING prend la valeur 12345678h (comme DWord)
MOV
Q[RSI], 12345678h
; THING prend la valeur 12345678h (comme QWord)
MOV
Q[RSI], 82345678h
; THING prend la valeur 0FFFFFFFF 82345678h
; c'est-à-dire -7DCBA988h (comme QWord)
Notez que vous ne pouvez pas mettre plus de quatre octets directement dans la mémoire en utilisant l'instruction MOV, même si vous utilisez le code 64 bits. La représentation qui suit fait donc apparaître une erreur :
MOV
Q[THING], 123456789ABCDEFh
Au lieu de cela, il convient de procéder comme suit pour atteindre ce résultat :
MOV
RAX, 123456789ABCDEFh
MOV
[THING], RAX
VII-H. Alignement automatique de la pile▲
Le pointeur de pile (RSP) doit être aligné sur une modularité de 16 octets lors d'un appel d'API. Avec certaines API cela n'a pas d'importance, mais avec d'autres, un mauvais alignement va provoquer une exception. Certaines vont gérer l'exception elles-mêmes et aligner la pile au besoin (au prix, cependant, d'une perte de performances). D'autres API (élaborées au minimum au début du x64) ne peuvent pas gérer l'exception et, à moins que vous n'exécutiez l'application sous le contrôle du débogueur, il en résultera une sortie de programme.
En raison de cette exigence, la documentation Win64 indique que vous ne pouvez appeler une API qu'à l'intérieur d'une trame de pile. En effet, il est supposé que c'est seulement à l'intérieur d'une trame de pile que l'on peut garantir qu'elle sera alignée correctement. Un appel hors trame de pile aura pour effet de désaligner la pile de 8 octets.
Cette exigence est très restrictive pour les programmeurs en assembleur, et occasionne de fortes migraines aux compilateurs. GoAsm résout ce problème en proposant l'insertion d'un codage spécial avant et après chaque appel d'API (lorsque INVOKE est utilisé) pour veiller à ce que la pile soit toujours bien alignée au moment de l'appel. Cela soulage le programmeur en assembleur et signifie que :
- Les appels vers les API (en utilisant INVOKE) peuvent être faits partout dans votre code. Ils peuvent être construits à partir de procédures appelées par d'autres procédures sans se soucier du pointeur de pile.
- PUSH et POP peuvent être utilisés de la manière habituelle pour sauvegarder et restaurer les registres, les adresses de mémoire et les contenus de mémoire sans avoir à se soucier que cela mette ou non la pile hors alignement.
- Vous pouvez utiliser le même code source à la fois pour les versions 32 bits et 64 bits de votre application (il n'y a aucune exigence pour l'alignement de la pile en 32 bits).
L'en-tête pour aligner la pile au moment de chaque appel d'API se traduit pas neuf octets supplémentaires par API, ce qui semble un faible prix à payer au regard du bénéfice escompté. A contrario, pour conserver une taille de code aussi réduite que possible, GoAsm exploite un certain nombre de possibilités pour optimiser le code, en particulier lors de l'insertion des paramètres. Voir la section optimisations effectuées par GoAsm pour plus de détails. Voir aussi codage pour obtenir un alignement automatique de la pile.
VII-I. Utilisation du même code source en 32 et 64 bits▲
Le présent manuel décrit l'utilisation d'ARG et INVOKE dans la section traitant des appels des API Windows en 32 bits et 64 bits et l'utilisation de FRAME … ENDF dans la section traitant des trames de pile pour callback en 32 et 64 -bits. Les constructions ARG/INVOKE et FRAME … ENDF de GoAsm permettent de gérer efficacement les changements de convention d'appel induits par la programmation 64 bits.
Si l'on réunit toutes ces considérations, ainsi que celles énoncées ci-dessus, il est parfaitement possible d'utiliser le même code source pour créer des exécutables pour les deux plateformes 32 bits et 64 bits.
Pour mémoire, voici les règles qui doivent être suivies pour ce faire.
- Lors des appels d'API utiliser INVOKE dans votre code au lieu de CALL.
- Pour passer les paramètres aux API utiliser ARG dans votre code au lieu de PUSH ou alors, positionner les paramètres à la fin du INVOKE (immédiatement après le nom d'API).
- Utilisez FRAME… ENDF dans votre code lors de l'utilisation de données locales (LOCAL) ou de la collecte de paramètres envoyés à une procédure de fenêtre (ou à toute autre procédure callback similaire).
- Si vous souhaitez utiliser les nouveaux registres R8-R15, XMM8-XMM 15, ou les nouveaux registres adressables en 8, 16 ou 32 octets, assurez-vous qu'ils ne sont mentionnés que dans la portion du script source strictement réservée au code 64 bits lorsque vous utilisez l'assemblage conditionnel.
- Utilisez de préférence la forme 64 bits des registres à usage général (RAX, RBP, RBX, RCX, RDX, RDI, RSI et RER) pour les pointeurs. Lorsque GoAsm procèdera en effet à un assemblage en 32 bits, il réduira automatiquement ces registres à leur configuration 32 bits.
- Si vous avez utilisé PUSHFD et POPFD pour sauvegarder et restaurer les flags, changer pour PUSHF et POPF ou PUSH FLAGS et POP FLAGS.
- Veiller à ce que les structures, les tailles de données et les indicateurs de type soient corrects pour un usage alternatif 32/64 bits, si nécessaire en recourant à l'assemblage conditionnel.
- Utiliser le commutateur /x64 dans la ligne de commande pour créer un exécutable 64 bits, et /x86 pour créer un exécutable 32 bits.
Les outils « Go » vont faire le reste…
Notez que le commutateur /x86 ne doit pas être utilisé dans la ligne de commande pour un code source Win32 (le réserver uniquement à un code source 32/64 bits commutable).
Voir le fichier Hello64World3 comme exemple de code source qui peut afficher un « Hello World » soit en Win32, soit en Win64.
VII-J. Conversion d'un code 32 bits existant en code 64 bits▲
Compte tenu des considérations qui précèdent, voici ce que vous devez faire pour convertir un code source existant 32 bits en code source 64 bits :
- Changez tous les CALLs à destination d'API en INVOKE. Ne pas changer les CALLs autres que pour des API.
- Si vous avez utilisé PUSH pour envoyer des paramètres à une API dans votre source 32 bits, changer pour ARG. En revanche, ne pas utiliser ARG en lieu et place des PUSH qui ne seraient pas dédiés à cet usage.
- Changez tous les registres 32 bits à usage général utilisés comme pointeurs (qui sont donc entre crochets) par leurs homologues 64 bits (RAX, RBP, RBX, RCX, RDX, RDI, RSI et RSP). Cela vous permettra de conserver un code plus court, et de garantir que les pointeurs vers des données externes fonctionnent correctement. Pensez aussi à utiliser uniquement RSI, RDI et RCX avec les instructions de chaîne et les préfixes de répétition utilisant un format inférieur de ces registres. Voir, à ce sujet, le paragraphe choix des registres.
- Veillez à ce que les registres qui contiennent les handles du système et d'autres valeurs fournies par le système soient changés par leurs homologues 64 bits (RAX, RBP, RBX, RCX, RDX, RDI, RSI et RER).
- Ajustez l'usage des autres registres aux besoins. En général pour une autre utilisation, les registres existants fonctionneront parfaitement bien, mais ne mélangez pas l'utilisation de registres 32 bits et 64 bits en raison du mécanisme d'extension à zéro des résultats. Il n'y a pas besoin de modifier les PUSH et POP de registres. Ces changements sont effectués automatiquement par GoAsm parce que les opcodes sont les mêmes (par exemple PUSH EAX est considéré comme étant le même que PUSH RAX et réciproquement).
- Veillez à ce que les structures, la taille des données et les indicateurs de type soient corrects dans la perspective d'une utilisation 64 bits.
- Vérifiez que vos instructions JECXZ sont modifiées en JRCXZ, le cas échéant.
- Dans la mesure où un code 64 bits a tendance à être un peu plus grand que son homologue 32 bits, vous pourriez constater, lors du réassemblage de votre code en utilisant le commutateur /x64, que certains sauts courts doivent être réorganisés car devenus hors de portée.
Fort heureusement, le programme AdaptAsm permet de réaliser automatiquement une partie de ces travaux d'adaptation.
VII-K. Utilisation de AdaptAsm.exe pour la conversion 64 bits▲
AdaptAsm est compris dans le pack GoAsm. J'ai écrit cet utilitaire à l'origine pour aider à convertir à la syntaxe GoAsm le code source créé sur d'autres assembleurs. Aujourd'hui, il est capable, outre les fonctions précitées, de convertir du code source 32 bits en code source 64 bits. Cela fonctionne aussi bien sur les scripts GoAsm que sur ceux d'autres assembleurs.
Pour plus de détails sur les autres fonctionnalités de AdaptAsm voir le paragraphe V.Q.
AdaptAsm s'utilise à partir de la simple ligne de commande suivante :
AdaptAsm [command line switches] inputfile[.ext]
Si aucune extension du nom du fichier d'entrée n'est spécifiée, l'extension .asm est présumée.
Si aucune extension du nom du fichier de sortie n'est spécifié, l'extension .adt est présumée.
Les commutateurs de la ligne de commande de AdaptAsm.exe sont :
|
affiche l'aide. |
|
adapte un fichier A386. |
|
adapte un fichier MASM. |
|
adapte un fichier NASM. |
|
spécifie le chemin d'accès du fichier de sortie. Ex : AdaptAsm /fo GoAsm\adapted.asm. |
|
crée un fichier de sortie avec l'extension .log. |
|
supprimer l'alerte précédant toute écriture sur le fichier d'entrée. |
|
adapte le fichier à la plateforme 64 bits. |
Ce que AdaptAsm fait lorsqu'il facilite l'adaptation d'un fichier en 64 bits en utilisant le commutateur /x64 |
Les CALLs en direction des API sont changés en INVOKE (les CALLs qui ne sont pas des appels d'API ne sont pas affectés). AdaptAsm réalise cette conversion en consultant la liste des API dans les fichiers « .h.txt » dans le même dossier que AdaptAsm.exe. Voir la section sur les fichiers « h.txt » pour plus d'informations. Cela fonctionne avec tous les types d'appels, même si la destination est entre crochets et même si elle dépend d'une définition (equate) ou d'un interrupteur, par exemple : CALL ExitProcess : changé en INVOKE CALL [ExitProcess] : changé en INVOKE CALL INTERNAL_PROC : inchangé CALL SendMessage : changé en INVOKE CALL SendMessageA : changé en INVOKE CALL SendMessageW : changé en INVOKE CALL SendMessage##AW : changé en INVOKE |
Change PUSH en ARG pour les paramètres envoyés à l'API. AdaptAsm y procède en comptant le nombre correct de paramètres à rebours du CALL et en le comparant avec le nombre correct de paramètres figurant dans la liste des API du fichier « h.txt » dans le même répertoire que AdaptAsm.exe. Voir le paragraphe sur les fichiers « h.txt » pour plus d'informations. Voici quelques exemples simples : PUSH EBX, 0, 1100h, [hMessTV] : PUSH est changé en ARG (et EBX changé en RBX) CALL SendMessageA : CALL est changé en INVOKE PUSH EBX, 0 : PUSH est changé en ARG (et EBX changé en RBX) PUSH 1100h : PUSH est changé en ARG PUSH [hMessTV] : PUSH est changé en ARG CALL SendMessageA : CALL est changé en INVOKE Vous avez peut-être préservé des registres au travers des appels d'API et ceux-ci ne sont pas affectés, par exemple : PUSH EAX : PUSH demeure inchangé (mais EAX est changé en RAX) PUSH EBX, 0, 1100h, [hMessTV] : PUSH est changé en ARG (et EBX est changé en RBX) CALL SendMessageA : CALL est changé en INVOKE POP EAX : POP demeure inchangé (mais EAX est changé en RAX) Toutefois, si vous avez mélangé ces deux utilisations de PUSH, AdaptAsm montrera une erreur en changeant le PUSH en ARG et en notifiant le problème dans le fichier journal : PUSH EAX, EBX, 0, 1100h, [hMessTV] : PUSH est changé en ARG (trop de paramètres) CALL SendMessageA : CALL est changé en INVOKE POP EAX : restaure le registre EAX Si AdaptAsm ne peut pas trouver tous les paramètres attendus, il affiche une erreur en changeant le CALL en INVOKE et en notifiant le problème dans le fichier journal. Par exemple : CALL INTERNAL_PROC : inchangé PUSH 0, 1100h, [hMessTV] : PUSH est changé en ARG CALL SendMessageA : CALL est changé en INVOKE (trop peu de paramètres) Cela signifie que ce genre de chose qui pourrait être fait en 32 bits, sera affiché comme une erreur par AdaptAsm (et à juste titre puisque, dans l'assembleur 64 bits, chaque CALL doit immédiatement succéder aux paramètres) : PUSH 0, EAX, 14Eh, [hComboSev]; 14Eh = CB_SETCURSEL PUSH 0, EAX, 151h, [hComboSev]; 151h = CB_SETITEMDATA CALL SendMessageA CALL SendMessageA |
Les registres à usage général 32 bits entre crochets sont remplacés par leurs homologues 64 bits afin qu'ils puissent être utilisés à la fois en assemblage 32 et 64 bits. Par exemple : MOV EAX, [EAX+EBX] : changé en MOV EAX, [RAX+RBX] MOV D[EBX*8+EBP], 8h : changé en MOV D[RBX*8+RBP], 8h CALL [EBX] : changé en CALL [RBX] INVOKE ExitProcess, [EBX] : changé en INVOKE ExitProcess, [RBX] PUSH [EBX] : changé en PUSH [RBX] ou ARG [RBX] POP [EBX] : changé en POP [RBX] |
Lorsqu'un pointeur est utilisé avec un registre à usage général 32 bits, le registre est changé par son équivalent 64 bits, par exemple : MOV EAX, ADDR THING : changé en MOV RAX, ADDR THING CMP ESI, ADDR THING : changé en CMP RSI, ADDR THING MOV EBP, OFFSET THING : changé en MOV RBP, OFFSET THING LEA EAX, THING : changé en LEA RAX, THING |
Bien que pas strictement nécessaire mais pour faire bonne mesure, les registres 32 bits à usage général après PUSH, POP et INVOKE sont changés en leur équivalent 64 bits, par exemple : PUSH EAX, EBX : changé en PUSH RAX, RBX POP EBX, EAX : changé en POP RBX, RAX INVOKE ExitProcess, EBX : changé en INVOKE ExitProcess, RBX |
Ce que AdaptAsm ne fait pas (et que vous devez faire à la main) |
AdaptAsm ne peut pas décider à votre place quel registre à utiliser dans d'autres circonstances. Ce choix vous incombe au cas par cas en vous inspirant toutefois des règles et usages recensés dans la section choix des registres qui fournit quelques indications à ce sujet. |
AdaptAsm ne garantit pas que les tailles des structures et des données sont correctes pour une utilisation en 64 bits, ni que les pointeurs vers les structures et les chaînes sont correctement alignés. |
VII-L. Les fichiers « h.txt » utilisés par AdaptAsm avec le commutateur /x64▲
Ces fichiers sont des fichiers texte contenant des listes d'API ainsi que le nombre de paramètres requis par chacune d'entre elles. AdaptAsm regarde à l'intérieur de son propre répertoire pour voir si des fichiers « h.txt » sont présents. Ces fichiers sont créés à partir de fichiers d'en-tête Microsoft en utilisant l'astucieux fichier JavaScript ApiParamCount.js, écrit par Leland M George de West Virginia, qui a bien voulu en autoriser l'usage dans le domaine public. Ce fichier js est livré avec AdaptAsm ainsi que quelques fichiers h.txt prêts à l'emploi contenant les API les plus couramment utilisées. Si votre programme utilise des API déclarées dans d'autres fichiers d'en-tête, vous pouvez constituer vos propres « h.txt » de ces fichiers en utilisant le fichier js. Il y a deux façons d'utiliser ce dernier :
- soit glisser-déplacer le fichier d'en-tête sur le fichier js (un fichier h.txt sera constitué dans le même répertoire) ;
- soit à partir de la ligne de commande en utilisant la commande suivante (par exemple) :
cscript ApiParamCount.js WinNT.h
ou
wscript ApiParamCount.js WinNT.h
qui commande le démarrage du Windows Scripting Host qui gère les handles des fichiers JavaScript en dehors de la page Web environnements.
Si vous devez télécharger le Windows Scripting Host vous pouvez l'obtenir à partir du site Microsoft.
Sinon, vous pouvez créer votre propre fichier h.txt ou modifier ceux qui existent déjà. Le format est le suivant :
- le premier nom d'API doit commencer au début du fichier et les suivants systématiquement en début de ligne ;
- de nouvelles lignes sont créées à l'aide d'un retour chariot (code ASCII décimal 13) suivi d'un saut de ligne (code ASCII décimal 10) ;
- une virgule suit immédiatement le nom de l'API ;
- le nombre de paramètres requis par l'API suit immédiatement la virgule et est exprimé par un chiffre décimal en format ASCII. Si l'API ne prend pas de paramètres le nombre est égal à zéro.
VII-M. Commutation utilisant x64 et x86 en assemblage conditionnel▲
En plus de pouvoir déclencher l'assemblage 64 ou 32 bits en spécifiant respectivement /x64 ou /x86 dans la ligne de commande de GoAsm, cet assembleur permet même à ces directives d'être testées dans l'assemblage conditionnel. Ainsi, vous pouvez écrire une procédure de fenêtre distincte pour chaque plateforme et déterminer leur activation au moyen du test suivant :
WndProcTable
:
#if
X64
MOV
EAX
, ADDR
MESSAGES ; met en EAX la liste des messages à traiter
CALL
GENERAL_WNDPROC64 ; appel du gestionnaire de message générique (version 64 bits)
#else
MOV
EDX
, ADDR
MESSAGES ; met en EDX la liste des messages à traiter
CALL
GENERAL_WNDPROC ; appel du gestionnaire de message générique (version 32 bits)
#endif
RET
Notez que les mots « x64 » et « x86 » ne sont pas sensibles à la casse.
Voici un autre exemple où l'on commute des fichiers d'inclusion porteurs de structures :
#if
X64
#include
structures64.inc
#else
#include
structures32.inc
#endif
VII-N. Quelques pièges à éviter lors de la conversion du code source existant▲
VII-N-1. Vous oubliez que les paramètres d'API sont systématiquement des QWords.▲
Votre code source existant 32 bits aura été écrit selon l'hypothèse correcte que chaque paramètre est un DWord. Par exemple :
ARG 4000h
, [SYSTEM_INFO+
4h
], [MEMORY_END]
INVOKE
VirtualFree ; libère une page de mémoire
En 32 bits, ce codage est correct parce qu'il y a un DWord à [SYSTEM_INFO +4h] (le DWord contient ici la taille des pages mémoire système (celle-ci supposant que la structure a été remplie au moyen d'un appel à l'API GetSystemInfo).
En 64 bits, ce codage est mauvais parce que la valeur à +4h est toujours un DWord, mais vous envoyez maintenant un QWord à VirtualFree et pas seulement un DWord. Cela devrait donc être codé comme suit en remplacement :
XOR
RAX, RAX ; RAX = 0
MOV
EAX
, [SYSTEM_INFO+
4h
] ; récup. de la taille de page dans les 32 bits de poids faible de RAX
ARG 4000h
, RAX, [MEMORY_END]
INVOKE
VirtualFree ; libère une page de mémoire
Notez que, dans la pratique, MOV EAX met à zéro la partie supérieure de RAX de sorte que vous pouvez supprimer la première ligne de cet exemple !
Un problème similaire se pose lors de l'interrogation du système et de la réception des informations dans les données. Votre code 32 bits existant peut bien ressembler à ceci :
ARG 0
, ADDR
SIZEOF_WORKAREA, 0
, 48
; 48 = SPI_GETWORKAREA (excluding tray)
INVOKE
SystemParametersInfoA ; récupère la taille de la zone de travail dans SIZEOF_WORKAREA
Ici, l'appel place une valeur de 32 bits dans le dword SIZEOF_WORKAREA, ce qui est correct. Cependant l'assemblage puis l'exécution du même code sur une plateforme 64 bits a pour effet d'écraser le DWord suivant en mémoire (un QWord est envoyé, pas un DWord). D'où la nécessité d'étendre SIZEOF_WORKAREA au format QWord.
VII-N-2. Vous oubliez que tous les CALLs sont maintenant avec des valeurs 64 bits.▲
Cela peut arriver facilement lors de l'utilisation de tables permettant, par exemple, d'effectuer les CALLs en direction de labels de code dûment répertoriés dans ces tables. Considérons le cas d'un tableau simple de labels suivant :
DATA
Table DD
CODELABEL, 2h
CODE
CALL
[Table]
; ou
DATA
Table DD
CODELABEL, 2h
CODE
MOV
RSI, ADDR
Table
CALL
[RSI]
Ces CALLs attendent une adresse de 64 bits qui va être constituée, contre toute attente, de l'adresse du label CODELABEL dans le DWord de poids faible et de la valeur 2 dans le Dword de poids fort. D'où une erreur plus que probable au moment de l'exécution. Cela provient évidemment de la table qui déclare des DWords au lieu de QWords. La solution pour les appels internes est donc de coder comme suit :
DATA
Table DQ
CODELABEL, 2h
CODE
CALL
[Table]
; ou
DATA
Table DD
CODELABEL, 2h
CODE
MOV
RSI, ADDR
Table
XOR
RAX, RAX
MOV
EAX
, [RSI]
CALL
RAX
Le code qui précède garantit que le DWord de poids fort de l'adresse 64 bits est bien égal à zéro. Cela fonctionne parce que tous les pointeurs vers des labels de donnée et de code interne sont à 32 bits.
VII-N-3. Vous oubliez que tous les handles de Windows sont maintenant des valeurs 64 bits.▲
Dans Win64, les handles de système sont étendus à 64 bits de sorte qu'il est imprudent de supposer qu'ils pourraient toujours tenir dans 32 bits. Cela signifie donc que :
ARG 32512
; IDC_ARROW (curseur classique en forme de flèche)
INVOKE
LoadCursorA, 0
; récupère dans EAX le handle de ce curseur
MOV
[WNDCLASS+
28h
], EAX
; et le communique à WNDCLASS
procède d'un mauvais codage 64 bits, alors que :
ARG 32512
; IDC_ARROW (curseur classique en forme de flèche)
INVOKE
LoadCursorA, 0
; récupère dans RAX le handle de ce curseur
MOV
[WNDCLASS+
28h
], RAX ; et le communique à WNDCLASS
est correct.
VII-N-4. Vous oubliez que tous les POPs sont maintenant des QWords.▲
Votre code source existant 32 bits peut effectuer des POP de DWord en mémoire. Par exemple :
DRAW_RECTANGLE
:
PUSH
[RECT], [RECT+
4
] ; sauvegarde de la largeur et de la longueur du rectangle
; code pour ajuster le rectangle
; puis le dessiner
POP
[RECT+
4
], [RECT] ; restauration de la longueur et de la largeur
; du rectangle pour un usage ultérieur
RET
En 64 bits, une structure RECT est toujours de quatre DWords comme elle l'était en 32 bits. Toutefois, le deuxième POP dans le code ci-dessus effacera le deuxième DWord dans la structure parce que le POP agit en fait sur 64 bits, et non pas 32 bits.
Il en résulte que le codage correct pour 64 bits doit s'écrire :
DRAW_RECTANGLE
:
PUSH
[RECT], [RECT+
4
] ; sauvegarde de la largeur et de la longueur du rectangle
; code pour ajuster le rectangle puis le dessiner
POP
RAX ; restauration de la longueur du rectangle pour un usage ultérieur
MOV
[RECT+
4
], EAX
; insertion d'un dword seulement
POP
RAX ; restauration de la largeur du rectangle pour un usage ultérieur
MOV
[RECT], EAX
; insertion d'un dword seulement
RET
VII-O. Assemblage et édition de liens pour produire un exécutable▲
Pour créer un fichier d'objet 64 bits avec GoAsm, utiliser la ligne de commande :
GoAsm /x64 filename
où filename est le nom de votre fichier asm écrit soit comme un fichier source 64 bits soit comme un fichier source commutable 32/64 bits. Utilisez /x86 au lieu de /x64 lorsque vous assemblez un fichier source commutable 32/64 pour en faire une version 32 bits.
Le fichier objet créé par GoAsm peut être envoyé à GoLink ou à un autre linker de la manière habituelle.
GoLink détecte automatiquement si le fichier objet est en 32 ou 64 bits et crée, selon le cas, le type de fichier exécutable approprié.
Vous ne pouvez pas mélanger des fichiers objet 32 bits et 64 bits. GoLink affichera une erreur pour toute tentative faite en ce sens.
Vous ne devez pas nécessairement faire des exécutables 64 bits sur une machine 64 bits. En effet, les noms de DLL donnés à GoLink disent simplement au linker que les DLL contiennent les API utilisées par l'application, lesquelles API tendent à être identiques entre les deux plateformes. Si votre application appelle des API spécifiques au système 64 bits cependant, cela ne fonctionne pas.
VII-P. Quelques optimisations et améliorations apportées par GoAsm▲
GoAsm vise toujours à produire, à partir de votre script source, le code le plus compact possible. Dans le cas du 64 bits, GoAsm n'a pas encore pris en considération toutes les possibilités d'optimiser le code. En effet, il subsiste encore quelques inconnues, notamment les effets sur les performances du code optimisé sur x64.
Les optimisations et améliorations effectuées automatiquement par GoAsm sont listées ici pour vous aider lorsque vous regarderez le code produit par GoAsm dans le débogueur.
Optimisations et améliorations apportées par GoAsm dans tout le code |
Aucune de celles-ci n'affecte les flags ou n'affecte négativement les performances.
|
LEA R11, ADDR Non_Local_Label PUSH R11 Voir l'explication de cette méthode. Notez que cela aura également lieu avec INVOKE lors de la mise en pile des arguments avec ADDR , qui comprend également l'utilisation de pointeurs vers une chaîne ou une donnée brute (ex. 'Hello' ou <'H','i',0>). Ceci affecte les flags.
PUSHRBP ADDD[RSP], +/-Displacement |
Optimisations et améliorations additionnelles uniquement avec INVOKE |
Celles-ci peuvent affecter les flags qui n'ont pas d'importance lors de l'appel d'API. Celles qui comptent sur l'extension à zéro peuvent nécessiter une autre opération de la part du processeur, mais il est supposé que cela n'a pas d'importance lors de l'appel d'une API. Il est plus important de réduire la taille du code.
Sélectionnez
ou Sélectionnez
|
VII-Q. Quelques conseils pour réduire la taille de votre code▲
Notez qu'il est possible certaines de ces optimisations nuisent à la performance.
- L'utilisation de registres 64 bits (RAX à RSP) en tant que pointeurs vers la mémoire (par exemple, l'instruction MOV [RSI], AL) permet de gagner un octet sur la longueur de l'opcode par rapport à la variante utilisant des registres 32 bits (par exemple MOV [ESI], AL). En effet, dans ces instructions, un octet d'override 67h est nécessaire pour la version 32 bits.
- Il en va différemment lorsque vous assignez des valeurs immédiates (nombres) à des registres. Dans ce cas, l'utilisation de registres élargis (RAX à RSP), de registres étendus (R8 à R15) ou de l'une des nouvelles méthodes adressage des registres, ajoutent au moins un octet à chaque instruction. Par exemple, MOV RAX, 23456h occupe deux octets de plus que MOV EAX, 23456h. Le contraste est encore plus saisissant en utilisant de grands nombres, et supérieurs en tout état de cause à 7FFFFFFFh, parce ceux-ci doivent être codés comme des nombres 64 bits si vous utilisez un registre 64 bits. Ainsi, le codage de MOV RAX, 80234560h occupe cinq octets de plus que celui de MOV EAX, 80234560h. Si le nombre que vous souhaitez déplacer tient dans un octet, de plus grandes économies peuvent être réalisées. Par exemple, le codage de MOV AL, 88h n'occupe que deux octets, alors que celui de MOV RAX, 88h atteint 10 octets.
- DEC et INC (avec un registre) utilisent maintenant deux opcodes, alors que dans les processeurs 86, ces instructions se contentaient d'un seul. Cela étant, Intel recommande aujourd'hui de leur préférer respectivement SUB registre, 1 et ADD registre, 1 afin d'uniformiser le traitement des flags.
En effet, les instructions INC et DEC ne modifient seulement qu'une partie des bits dans le registre des flags comparativement aux instructions ADD et SUB dont elles ne sont pourtant qu'un cas particulier. Selon Intel,(2) il peut même s'avérer particulièrement problématique dans certains cas d'en poursuivre l'usage. - En programmation 64 bits, l'instruction LEA register, Label est de 5 opcodes plus courte que l'instruction MOV register, ADDR Label tout en atteignant le même résultat. Dans un code source GoAsm, cependant, vous pouvez utiliser l'une ou l'autre de ces deux formes puisque GoAsm sélectionne automatiquement la plus courte.
- PUSH ADDR THING se code sur 9 octets, alors que si vous utilisez, en remplacement, LEA RAX, THING suivi de PUSH RAX, le codage global se réduit à 8 octets mais présente l'inconvénient de modifier le contenu du registre RAX.
- Mettre à zéro un registre en utilisant XOR. L'instruction XOR RAX, RAX occupe 3 octets, alors que l'instruction MOV RAX, 0 occupe 10 octets parce qu'elle met en œuvre une valeur immédiate de 64 bits (nombre). Notez cependant que XOR affecte les flags, ce qui n'est pas le cas de MOV.
- L'instruction XOR EAX, EAX est encore plus courte avec seulement deux octets et met à zéro l'ensemble du registre RAX en vertu du principe d'extension à zéro automatique des résultats.
- Une bonne façon de porter la valeur -1 dans un registre, est d'utiliser l'instruction OR registre, -1 qui, dans le cas d'un registre 64 bits, occupe 4 octets, soit un gain de 6 octets par rapport à MOV registre ,-1. Cependant, contrairement à MOV, l'instruction OR affecte les flags.
- Une comparaison dans la plage -80h à +7Fh n'occupe que 4 octets (par exemple, CMP RDX, -80h à CMP RDX, 7Fh), mais en dehors de cette plage, le codage s'effectue sur 7 octets. Par exemple, CMP RDX, 80h se code sur 7 octets.
- Vous pouvez toujours utiliser LEA pour réaliser certaines opérations arithmétiques intraregistre, par exemple LEA RAX, [RAX+RAX*2] qui multiplie RAX par trois. Ce code n'occupe que 4 octets.
Voir également les quelques conseils et astuces de programmation prodigués dans l'annexe K de ce document.
VII-R. Références et liens concernant la programmation 64 bits▲
VIII. Annexes▲
VIII-A. Exemples de programmes en assembleur GoAsm▲
VIII-A-1. Programme HelloWorld1.asm▲
Ce programme est dit « de console », c'est-à-dire qu'il est prévu pour fonctionner sous l'invite de commande MS-DOS. Son action se borne à afficher le message « Hello World (from GoAsm) » ainsi que le montre la copie d'écran ci-dessous (texte sur fond jaune et flèche rouge) :
;------------------------------------------------------------------
; HelloWorld1 - copyright Jeremy Gordon 2002
; SIMPLE "HELLO WORLD" WINDOWS CONSOLE PROGRAM - for GoAsm
;
; Assemblage & Édition de liens :
; GoAsm HelloWorld1 (produit un fichier PE COFF)
; GoLink /console [-debug coff] helloworld1.obj kernel32.dll
; -debug coff n'est utilisé que s'il est souhaitable d'analyser le
; programme dans le débogueur
;
; Notez que les API GetStdHandle et WriteFile relèvent de kernel32.dll
;------------------------------------------------------------------
;
DATA SECTION
;
RCKEEP DD
0
; variable à usage général
;
CODE SECTION
;
START
:
PUSH
-
11D
; STD_OUTPUT_HANDLE
CALL
GetStdHandle ; récupère en EAX le handle du buffer d'écran actif
PUSH
0
, ADDR
RCKEEP ; RCKEEP va récupérer la sortie d'API
PUSH
24D
, 'Hello World (from GoAsm)'
; 24 = longueur de la chaîne
PUSH
EAX
; handle du buffer d'écran actif
CALL
WriteFile
XOR
EAX
, EAX
; retour de la valeur zéro
RET
VIII-A-2. Programme HelloWorld2.asm▲
Le programme HelloWorld2.asm, écrit pour Windows 32 bits, permet de dessiner une ellipse dans un rectangle. La figure ci-dessous montre le résultat obtenu sur l'écran :
On trouvera, dans la même annexe, trois variantes de ce programme :
- HelloWorld3.asm, toujours en 32 bits, qui est une version plus structurée du même programme faisant usage de structures formelles, de trames automatiques de pile FRAME … ENDF, de USEDATA et USES, INVOKE, de définitions et de labels de nom réutilisables ;
- He