l'assembleur  [livre #4061]

par  pierre maurette



Assembleurs
intégrés

Les cas où le recours à un assembleur autonome, utilisé seul, est indispensable ou simplement représente la solution la plus productive et/ou la plus efficace sont devenus rares. Nous pouvons songer aux pilotes de périphériques ou à certains programmes aux buts non avouables.

Beaucoup de petits utilitaires peuvent s'écrire en assembleur, entre autres méthodes possibles. Le programme faire_boot.exe du chapitre précédent en est un bon exemple : il peut se traiter par n'importe quel langage de haut niveau permettant d'accéder aux fonctions du BIOS. À titre d’exemple, nous pouvons citer la fonction biosdisk()  en Turbo C. Mais la programmation est pour beaucoup d'entre nous un plaisir et traiter ce petit problème 100 % par l'assembleur est une approche viable, efficace et élégante.

Les facilités offertes par des systèmes d'exploitation comme les divers avatars de Windows et ses innombrables fonctions sont toutefois accessibles à des programmes écrits en assembleur.Vous avez donc la possibilité, le plus souvent aidé par une collection de squelettes d'applications, des bibliothèques de fonctions et la puissance d'un macro-assembleur comme MASM ou TASM, d'écrire des applications Windows efficaces en assembleur. Néanmoins, de nombreuses applications sur cette plate-forme valent en grande partie ce que vaut leur interface utilisateur. Et là, rien ne vaut un environnement gérant spécifiquement la fabrication de ces interfaces.

Donc, dans nombre de domaines, le développement en assembleur pur apparaîtra vite humainement inabordable, même si techniquement envisageable.

Plus fréquent sera alors l'usage de l'assembleur pour la fabrication de fonctions, au sens large, qui seront exploitées par des programmes écrits en langage de haut niveau, ce langage pouvant lui-même être présenté dans un RAD (développement rapide d’applications). Nous aborderons un peu plus loin, dans ce chapitre, la présentation de Delphi. Ces fonctions pourront également se présenter sous la forme d'une librairie statique ou dynamique.

Même sans parler de librairie, nous pouvons mixer dans un même projet des sources  .cpp ou  .c et des sources  .asm . Nous pouvons élaborer des DLL qui seront utilisables dans nos développements en Visual Basic. Nous sommes encore dans le cadre de l'assembleur autonome, puisque c'est par rapport à lui seul que sera écrit le code source, et qu'il sera fait appel à ml.exe , tasm32.exe ou autre pour traiter le  .asm . Un environnement permettant de tels mélanges permet d'ailleurs généralement le développement en assembleur pur.

Tout autre est l'assemblage en ligne , ou in line , ou encore intégré .

6.1 Les assembleurs intégrés

Un assembleur en ligne, ou assembleur intégré, est une fonctionnalité d'un LHN (langage de haut niveau) permettant d'inclure dans le code source des séquences écrites en assembleur. Ce bloc de code assembleur est tout à fait intégré au langage et en particulier connaît les données du module auquel il appartient, dans le respect bien entendu des règles de visibilité de ces données.

Pour beaucoup de langages, l'assembleur intégré est simplement une instruction : asm , _asm , __asm . Le paramètre de cette instruction est un texte correspondant à une instruction assembleur :

// syntaxe C++ Builder par exemple
asm mov eax,varX
asm add eax,varY
asm mov varY,eax

Ces lignes (ou cette ligne, rien n'oblige à ce qu'il y en ait plusieurs) sont insérées au sein d'un bloc d'instructions normales du langage évolué.

Très souvent, le paramètre peut être un texte définissant un bloc d'instructions assembleur :

// syntaxe VC++ par exemple
_asm {
  mov eax,varX
  add eax,varY
  mov varY,eax}

Ou en Delphi :

// syntaxe Delphi
asm
  mov eax,varX
  add eax,varY
  mov varY,eax
end;

Sous Delphi, même une simple instruction est considérée comme un bloc et exige un end; final :

// syntaxe Delphi
asm mov eax,varX end;
asm add eax,varY end;
asm mov varY,eax end;

Dans tous les cas, notre exemple fait l'équivalent de varY <- varX + varY . Le signe  <- est un symbole arbitraire pour l'affectation, le  = de C/C++, le  := de Pascal.

Certains langages vont un peu plus loin en proposant la possibilité d'écrire des procédures entièrement en assembleur et traitées comme telles par le compilateur :

function Somme(paramX,paramY:integer):integer;register;assembler;
asm
  mov eax,paramX
  add eax,paramY
  mov Result,eax
end;

Nous sommes sous Delphi. Les mots register et assembler sont inutiles, nous le verrons. Cette fonction est réellement en assembleur, à la différence de :

function Somme(paramX,paramY:integer):integer;
begin
  asm
    mov eax,paramX
    add eax,paramY
    mov Result,eax
  end;
end;

Cette dernière est une fonction normale incluant du code assembleur en ligne. Si nous connaissons les conventions d'appels de fonctions, nous pouvons radicalement simplifier la fonction pur assembleur :

function Somme(paramX,paramY:integer):integer;
asm add eax, edx end;

et la fonction en faux pur assembleur, qui ne fonctionne plus :

function Somme(paramX,paramY:integer):integer;
begin
 asm add eax, edx end;
end;

Cette fonction renvoie paramY , et non pas l'addition. Il y a donc bien des fonctions Delphi ne contenant que des instructions assembleur et des fonctions pur assembleur sous Delphi, et elles sont différentes.

Attention

C'est généralement le compilateur qui assemble

Si vous possédez C++Builder Professionnel ou Entreprise, plus généralement si vous possédez un outil de développement qui fournit un assembleur, tasm32.exe dans le dossier  ..\bin pour C++Builder Professionnel ou ml.exe pour Visual Studio, ne croyez pas qu'il est utilisé par l'assembleur en ligne. Vous pouvez être induit en erreur par l'avertissement :

W8002 Redémarrage de la compilation en utilisant l'assembleur

qui suit la compilation. bcc32.exe est autosuffisant sur ce plan. tasm32.exe sera utilisé pour des unités  .asm , que vous assembleriez au sein du même projet, mais pas par l'assembleur intégré. Vous pouvez vous en assurer en l'effaçant ou en le renommant temporairement. Donc sa mise à jour n'aura aucune influence sur l'assemblage en ligne.

Il faut bien distinguer une fonction écrite à 100 % en assembleur, au sein d’une unité en LHN, de la même fonction dans un fichier  .asm , qui sera assemblée par un assembleur autonome, le fichier  .obj étant ensuite lié au reste du projet. Nous sommes juste à la frontière entre intégré et autonome. La syntaxe sera différente. Un avantage important du module assemblé séparément se situe dans la possibilité de réutilisation. Les autres points plaident en faveur de l’assemblage intégré : performances un peu meilleures par simplification du processus d’appel, pas ou moins de problèmes de formats de fichiers objets ou de conventions d’appels et gestion de projet plus facile.

L’expression assembleur intégré désigne toutes les possibilités d'un LHN de traiter du code assembleur. Assembleur en ligne sera réservé à l'insertion de blocs asm {....} ou asm .... end; , voire de asm suivi d'une simple instruction dans le flux du code C, C++, Pascal ou Pascal Objet. Ces instructions pourront être des mnémoniques à assembler, ou du code hexadécimal à insérer, et être traitées éventuellement comme des macros.

Remarque

L'assembleur intégré et la norme en C/C++

Bien qu’il existe une forme ANSI de cette instruction, sa nature même l’empêche d’être réellement normalisée.

Chez Microsoft, dans les environnements Visual C++, version .NET comprise, les mots clés __asm  et _emit sont qualifiés de Microsoft Specific . __asm ( _asm  est accepté également) est suivi d’une instruction ou d’un bloc d’instructions. _emit  est une pseudo-instruction qui remplace la directive DB et sert donc à placer un simple octet dans le cours du code.

Chez Borland, le mot clé est tout simplement asm  en C/C+++ et Delphi et il précède une simple instruction ou un bloc { ... } ou begin ... end , mais _asm et __asm sont acceptés en C/C++.

Mieux qu'une explication ou un tableau, vous trouverez sur le CD-Rom trois petits projets nommés "syntaxe", en Delphi 6, C++Builder 6 et VC++ 6, dont la seule qualité est de se compiler et de présenter un raccourci de la syntaxe habituelle de l'assembleur intégré dans ces trois environnements. Leur utilité est peut-être de vous faire éviter quelques hésitations et, donc, de gagner du temps. Attention, alors que la puissance est là, que les instructions les plus récentes sont généralement traitées, la documentation n’est pas le point fort de nos trois environnements, quant à l’assembleur en ligne. Celle de C++Builder aboutit à quelques pages sur le sujet, mais elles font partie de la référence de... Pascal Objet, puisque C++Builder compile ce langage. Nous consacrerons un peu plus loin un paragraphe à l’aide dans Delphi 6. La documentation sur Visual Studio est importante et touffue, mais au prix d'une certaine errance dans le MSDN.

Sous Delphi, il y a le minuscule projet syntaxe , dont voici de larges extraits :

type
  // définition de deux types structure
  TypePoint = 
record
    x, y: Longint;
  end; // taille  8 octets
  TypeBiPoint = record
    p1, p2: TypePoint;
  end; // taille 16 octets
var
  u16_0: Word;
  u32_0: Longword;
  i: Integer;
  TBP: array[0..3] of TypeBiPoint;
 
procedure TForm1.Btn1Click(Sender: TObject);
begin
u16_0 := 12;
// une instruction isolée
asm sar u16_0, 2 end;
 
// un bloc de code assembleur
asm
 mov u16_0, 12            // décimal
 mov word ptr[u16_0], 12  // décimal
 mov byte ptr[u16_0], 12  // décimal
 mov u16_0, $12           // hexadécimal
 mov u16_0, 12h           // hexadécimal
 mov u16_0, 0F2h          // hexadécimal, doit débuter par un chiffre
 mov u16_0, 10110010b     //binaire
 
 mov [TBP + 16].TypeBiPoint.p1.x, 171
 mov [TBP + 24].TypeBiPoint.p1.x, 225
 lea edi, TBP
 mov (TypeBiPoint ptr[edi]).p1.x, 24
 //mov (TypeBiPoint [edi]).p1.x, 25
 //mov TypeBiPoint [edi].p1.x, 26
 //mov [edi].TypeBiPoint.p1.x, 27
end; //asm
MemoSortie.Lines.Add(IntToStr(TBP[0].p1.x));
MemoSortie.Lines.Add(IntToStr(TBP[1].p1.x));
MemoSortie.Lines.Add(IntToStr(TBP[1].p2.x));
end; //procedure
 
procedure TForm1.Btn2Click(Sender: TObject);
// Cette procédure (100% inutile) est 100% assembleur
asm
 mov eax, Sender
 mov u32_0, eax
end;
Sortie du programme Syntaxe sous Delphi 6 et C++Builder
figure 6.01 Sortie du programme Syntaxe sous Delphi 6 et C++Builder

 

Voici une grande partie du source du même projet sous C++Builder 6 :

void __fastcall TForm1::Btn1Click(TObject *Sender)
{
struct TypePoint{unsigned int x, y;};
struct TypeBiPoint{TypePoint p1, p2;};
TypeBiPoint TBP[4];
unsigned int u16_0;
 
u16_0 = 12;
 
asm shl u16_0, 2;
 
asm {
 mov u16_0, 12            // décimal
 mov word ptr[u16_0], 12  // décimal
 mov byte ptr[u16_0], 12  // décimal
 mov u16_0, 0x12          // hexadécimal
 mov u16_0, 0F2A3h        // hexadécimal, doit débuter par un chiffre
 mov u16_0, 10110010b     //binaire
 
 lea edi, TBP
 mov [edi + 16].p1.x, 171
 mov [edi + 24].p1.x, 225
 
 mov [edi].p1.x, 24
 mov [edi].p1.x, 25
 mov [edi].p1.x, 26
 mov [edi].p1.x, 27
 }
MemoSortie->Lines->Add(IntToStr(TBP[0].p1.x));
MemoSortie->Lines->Add(IntToStr(TBP[1].p1.x));
MemoSortie->Lines->Add(IntToStr(TBP[1].p2.x));
}
//-------------------------------------------------------------------
void __fastcall TForm1::Btn2Click(TObject *Sender)
{
asm
 {
 call JePionce
 mov edx, eax
 }
}

 

Sous Visual C++ 7 (.NET) et VC++ 6, strictement identiques pour ce programme, une petite application console :

int main(int argc, char* argv[])
{
 
struct TypePoint{unsigned int x, y;};  // taille  8 octets
 
struct TypeBiPoint{TypePoint p1, p2;}; // taille 16 octets
 
unsigned int u16_0;
 
// tableau de structures
TypeBiPoint TBP[4];
 
u16_0 = 12;
 
__asm shl u16_0, 2;
 
__asm {
 mov u16_0, 12            // décimal
 mov word ptr[u16_0], 12  // décimal
 mov byte ptr[u16_0], 12  // décimal
 mov u16_0, 12h           // hexadécimal
 mov u16_0, 0F2A3h        // hexadécimal, doit débuter par un chiffre
 mov u16_0, 10110010b     // binaire
 
 mov [TBP].p1.x, 33
 mov [TBP + 16].p1.x, 77
}
 
// pour vérifier la taille des structures
printf("%u\n", sizeof(TypePoint));
printf("%u\n", sizeof(TypeBiPoint));
 
printf("%u\n", TBP[1].p1.x);
printf("%u\n", TBP[0].p1.x);
printf("%X\n", u16_0);    
printf("Hello World!\n");
return 0;
}

Remarque

Indentez !

Vous pouvez constater que les listings proposés sont peu ou pas indentés. Les contraintes de mise en page en sont responsables. En temps normal, n’hésitez pas à indenter vos blocs et, au besoin, à commenter les symboles  } ou  end; pour faciliter le repérage, par une référence au bloc qu'il clôt. Idéalement, il faudrait saisir le mot clé de fin de bloc en même temps que celui de début, avec commentaire. Selon l’adage : "Faites ce que je dis et ne faites pas ce que je fais."

Ces trois exemples sont simplement là pour un premier contact avec la syntaxe de l’assembleur intégré. Mais adresser, comme ils le font, des éléments dans un tableau de structures comprenant des structures n’est pas simplissime. Ils y arrivent tous, mais avec d’étonnantes différences de syntaxe, en particulier entre Delphi et C++Builder du même éditeur. Faisons une remarque, de façon un peu prématurée, mais l’exemple est là. Les compilateurs C++ prennent parfois des initiatives de transtypage courageuses, voire dangereuses. Cela est particulièrement vrai en programmation objet et dépend, bien entendu, beaucoup de l’expertise du programmeur. Vérifiez donc souvent, par de simples tests ou l’utilisation d’un débogueur, que les données traitées sont bien celles que vous visez.

6.2 Les avantages de l'assembleur intégré

Programmer en assembleur n'est pas une activité honteuse, bien au contraire, et utiliser un peu de code machine dans un programme en LHN est pour certains un plaisir qui se justifie lui-même. Les avantages pédagogiques à en retirer sont intéressants.

Nous trouvons ensuite les avantages du développement multilangage, que l'assemblage en ligne partage avec la programmation multimodule. L'intérêt du code assembleur est, dans ce cas, de deux ordres bien différents :

  Certaines opérations ne trouvent pas leur équivalent dans le LHN, notamment celles qui n'ont de sens que dans le cadre d'une famille donnée de processeurs. Si la fonctionnalité n'est pas encapsulée dans une fonction du système d'exploitation, l'usage de l'assembleur est alors la façon normale de travailler. Dans cette catégorie, se trouvent les instructions  CPUID et RDTSC .

  L'optimisation de certaines parties du code. Attention toutefois, une programmation correcte en langage de haut niveau, en typant correctement ses variables, en les déclarant là où elles doivent l’être, conduit le compilateur Pascal ou C/C++ à un code d'une bonne efficacité. Il serait plutôt utile de tracer le code assembleur généré (fenêtre CPU) pour nous familiariser avec un assembleur optimisé, du moins dans un premier temps. Il reste que l'optimisation existe, par exemple par l'utilisation des technologies MMX, 3DNow!, SSE/SSE2, par l'utilisation des flags de retenue ou de dépassement inaccessibles en LHN, etc. Selon le langage, selon le niveau de gamme de votre environnement de développement, vous aurez plus ou moins d'options de compilation à votre disposition : plus ces options seront rares, plus vous pourrez trouver avantage à l'assembleur. Les traitements sur les très grands entiers sont à la mode, certainement à cause des algorithmes de cryptage clé privée/clé publique. Jusqu'à 32 ou 64 bits, le compilateur va générer un code difficile à optimiser, si notre source n'est pas trop médiocre. Au-delà, il faudra fabriquer soi-même ses entiers et l'assembleur possède un énorme avantage : il a accès au registre de flags, donc à la retenue.

Avec votre moteur de recherche favori, vous pouvez vous amuser à faire votre propre enquête – informelle – sur les domaines de prédilection du langage assembleur, en ligne ou pas.

Vous y trouverez largement en tête l'identification du processeur et la détermination de sa vitesse. Si vous suivez la démarche inverse, c'est-à-dire une recherche sur ces deux thèmes, vous constaterez qu'il y a toujours quelques lignes d'assembleur dans les résultats. Ceci explique cela.

Vous trouverez ensuite des solutions d'optimisation sur de grands tableaux, graphismes ou son par exemple, souvent à l'aide d'instructions évoluées.

Enfin, dans la nébuleuse de la protection et du cryptage, il existe un certain nombre de travaux sur les grands entiers. Nous trouvons malheureusement trop de gens qui espèrent résoudre à l'aide de quelques lignes d'assembleur des problèmes sur lesquels les mathématiciens se dessèchent depuis des siècles, dans le domaine des nombres premiers.

Voici en vrac quelques avantages, parfois inattendus, à tirer d'un environnement de développement muni de l'assemblage en ligne et, idéalement, d'un bon débogueur avec fenêtre CPU.

Certaines instructions peuvent manquer dans votre langage, ou vous pouvez avoir des difficultés à les trouver dans la documentation : par exemple, les instructions bit à bit. Delphi dispose de  shr et  shl , mais pas de  sar (bien qu'il l'utilise pour la multiplication entière, une forme d'optimisation primitive classique). L'arithmétique BCD (Binary Coded Decimal ou décimal codé en binaire) n'est pas souvent gérée par le langage.

Il arrive que certaines opérations soient simplement plus faciles ou plus lisibles codées en assembleur. Supposons que nous désirions transporter du texte dans des mots de 32 bits. Cet exemple n'est pas irréaliste, une zone de texte se transfèrera plus rapidement découpée en mots de 32 bits (alignés correctement) qu'en octets. Nous voulons à un moment donné initialiser la variable  LW par les lettres de HOU! . Les pointeurs le permettent, mais n'est-il pas plus simple, plus lisible d'écrire :

asm
 mov byte ptr [LW + 0], 'H'
 mov byte ptr [LW + 1], 'O'
 mov byte ptr [LW + 2], 'U'
 mov byte ptr [LW + 3], '!'
end; //asm

Si votre langage n'accepte pas les entrées de constantes directement en binaire, ce qui semble fréquent, bien que très utile, pour les masques par exemple, il suffit de taper (Delphi) :

asm
 mov variable_byte, 01001101b 
end
;

L'assembleur permet de pointer à l'intérieur d'une variable, plus simplement qu'une manipulation laborieuse de pointeurs. Par exemple ( f3 est un Extenso, donc un flottant sur 80 bits) :

asm
 
and byte ptr[f3+9], 01111111b 
end
;
f3 := abs(f3);

La première ligne force à 0 le bit 7 de l'octet 9 de f3 , donc son bit de signe. Nous avons ainsi obtenu la valeur absolue de f3 en une instruction, d'une façon parfaitement viable sur des processeurs récents. Si nous regardons le code machine généré par Delphi pour la ligne suivante, nous voyons qu'il fait appel à une artillerie plus lourde. C++Builder fait encore plus long.

En généralisant l’exemple précédent, l’assembleur vous permettra de passer outre les contraintes imposées, à juste titre, par le langage de haut niveau. Le LHN refuse de compiler des affectations ne respectant pas le typage, avec plus (Delphi) ou moins (C/C++) de rigueur. Il va, en revanche, dans un bloc asm , compiler tout ce qui est syntaxiquement compilable. Comme un cast , comme un transtypage forcé, le mot clé asm affirme la volonté du programmeur de faire des bêtises. C’est à l’exécution que les problèmes vont surgir… peut-être.

La pratique d'un peu d'assembleur, ou tout simplement l'observation de la fenêtre CPU du débogueur, vous fera faire rapidement des progrès en programmation en langage de haut niveau comme en assembleur.

L'assembleur intégré est d’un usage très agréable : vous pouvez bâtir votre application en C++ ou en Pascal, puis peu à peu introduire le code assembleur. Si votre ambition vise à développer des fonctions, ou même des applications en assembleur pur, vous pouvez utiliser Delphi comme une plate-forme de test, voire d'apprentissage.

Surtout si vous disposez déjà sur votre machine, pour d'autres utilisations, d'un environnement comme Visual Studio, Delphi ou C++Builder, que donc vous maîtrisez cet environnement, il sera très facile de tester une séquence de quelques instructions assembleur pour, par exemple, lever un doute sur leur fonctionnement, et ce, quel que soit l'environnement final de codage. Un bon exemple est la rédaction des chapitres sur les jeux d'instructions.

C'est là qu'apparaît un défaut de l'assembleur intégré : le mode de fonctionnement est imposé par l'environnement, c'est généralement un modèle flat 32 bits qui est mis à votre disposition. Donc, certains tests ne seront pas accessibles dans ce contexte. Dans les environnements modernes que nous évoquons, pas de mode DOS. Mais rien n'interdit de tester l'assembleur intégré des anciennes versions de Turbo C ou Turbo Pascal, sous DOS.

Le langage de haut niveau, ou mieux le RAD, permet l'accès à des concepts comme la programmation objet et permet au minimum de structurer et de sécuriser plus efficacement son application. Il est ainsi possible de bénéficier, au sein d'un même projet, de diagrammes UML pour la gestion d'une base de photographies et de code assembleur pour leur traitement.

Enfin, si vous programmez en professionnel, ou simplement avec des normes de qualité et des obligations de résultats, soyez très prudent : tout ce que l’environnement de développement met en place pour favoriser la stabilité d’une application, typage, profilage, warnings, peut être rendu inopérant par quelques lignes d’assembleur. Par la hiérarchie, il vous sera plus facilement reproché l’utilisation de l’assembleur que le contraire.

6.3 Le marché – Delphi 6

Tous les compilateurs sérieux, anciens ou récents, et notamment C/C++, compilent du code assembleur en ligne. De plus, au moins pour les versions les plus évoluées, les environnements de développement bâtis autour de ces outils intègrent la possibilité de traiter des modules en pur assembleur.

Le tout est de choisir en fonction de son budget et de ses convictions. Le monde du logiciel libre, en environnements Linux et même Windows, recèle d’intéressantes ressources. Notez que, si vous développez pour votre plaisir, l’investissement peut être très faible, à coups de versions Education, de démos limitées dans le temps, voire de produits gratuits ou gratuitement améliorés.

Notre ambition est de développer sur PC sous Windows. Il serait peut-être dommage, dans le choix de ce système complet, de nous priver des possibilités offertes par un RAD. Combien de temps sommes-nous prêts à consacrer à une interface Windows soignée pour notre application ?

Visual C++ 6 et 7, de Visual Studio 6 et Visual Studio .NET, ainsi que tous les produits Borland, permettent d'assembler en ligne. Parmi eux, il y a C++ Compiler 5.5 (gratuit), mais c'est un produit en ligne de commande, le contraire d’un RAD, et surtout les deux EDI majeurs, C++Builder et Delphi.

Dans sa version 6 Personnel, pour un usage non commercial, Delphi est librement téléchargeable ; il est de plus présent sur le CD-Rom accompagnant cet ouvrage. À ce sujet, précisons que cette fourniture, ainsi que celle de Turbo Debugger et de C++ 5.5 Compiler, n'est qu'une facilité destinée à remplacer le téléchargement des 140 Mo de l'objet et qu'un enregistrement par internet est nécessaire pour l’installer et l’utiliser. Tous renseignements à ce sujet, y compris des liens précis, figurent sur le CD-Rom, sur la page internet de l'auteur et surtout sur le site www.developpez.com . Bien entendu, ces produits sont mis à disposition gratuitement par Borland sans support aucun de leur part.

Cette version gratuite est toujours d'actualité en juin 2003. En revanche, vous pouvez acquérir la version 7, voire 8 qui est imminente. Vous pouvez également télécharger ou même commander par e-mail pour un envoi par la poste, la version d'évaluation de Delphi 7, dans une version supérieure à la Personnelle. La durée de l'évaluation est suffisante pour un apprentissage et les explications qui suivent s'appliquent sans grandes différences. Vous aurez ainsi accès à une fenêtre FPU, permettant de visualiser également les registres MMX.

La fenêtre FPU
figure 6.02 La fenêtre FPU

C’est donc Delphi 6 Personnel que nous choisirons prioritairement pour nos essais. Delphi est un RAD, fondé sur un excellent langage Pascal Objet. La portabilité vers Linux est maintenant assurée, au travers de Kylix. Faire du code assembleur au sein d’un langage comme Pascal présente bizarrement des avantages par rapport à des environnements bâtis sur C/C++. Plus fortement typé, en fait plus rigoureux que C++, il tirera plus davantage de l’assembleur et la frontière sera plus clairement marquée.

La norme de Pascal Objet reconnaît l'assembleur et Delphi le traite richement, connaissant la notion de fonction ou procédure en assembleur. L'assembleur intégré à Delphi s'appelle BASM (Built in ASseMbler) ; ce détail vous facilitera les recherches sur la toile.

Si vous souhaitez utiliser C++Builder, c'est que vous maîtrisez à priori mieux la syntaxe du C++, ou du moins mieux que celle du Pascal Objet de Delphi. Dans ces conditions, vous n’aurez aucune difficulté à reprendre à votre compte ce qui est dit à propos de Delphi 6 et à l’appliquer à C++Builder. Le plus souvent possible, des exemples seront donnés sur le CD-Rom sur la base de C++Builder Entreprise.

Nous allons maintenant décrire de façon assez détaillée un squelette d'application, pour commencer à tester l'assemblage en ligne. Comme pour tous les exemples fondés sur Delphi, le projet vierge, avant saisie du code assembleur, se trouve sur le CD-Rom.

6.3.1 Prise en main de Delphi 6 Personnel

Le but n'est pas ici de découvrir la richesse et les subtilités de Delphi 6, mais c'est un apprentissage personnel qu'il serait dommage de négliger si vous installez le produit. Un tutorial est proposé dans l'aide, des ressources sont indiquées sur le CD-Rom, entre autres trois fichiers  .pdf , dont une prise en main du produit.

Nous allons créer pas à pas une application destinée à recevoir du code assembleur. Nous adoptons le niveau d'explication minimal pour permettre d'arriver au résultat, même pour un premier contact avec Delphi. C’est donc une approche pragmatique.

Comme souvent en environnement Windows, plusieurs voies sont possibles pour la même action : menu, barre d'outils, clic droit pour ouvrir un menu contextuel, double-clic sur un objet, raccourci clavier. Nous n'en donnons qu'un seul. Face à une interrogation, n'hésitez pas à tester le clic droit sur l'objet concerné, avant même d'aller dans l'aide.

Si vous connaissez Delphi, ou si vous avez déjà mené votre apprentissage, vous pouvez charger le projet skel_d6 à partir du CD-Rom, après copie sur le disque dur, et passer à la suite, c'est-à-dire aux remarques sur l'aide.

À la fin de cette phase, votre écran devra ressembler à celui figurant sur l’illustration (nous sommes sous Windows XP).

Application skel_d6
figure 6.03 Application skel_d6

L'espace de travail est constitué de plusieurs fenêtres libres sur le Bureau. Cela présente le petit inconvénient de laisser la possibilité de cliquer sur n’importe quoi se trouvant sur le Bureau, derrière Delphi. Il ne faut donc pas hésiter à réduire les applications non utilisées dans la barre des tâches.

Nous n’avons gardé ouvertes que les fenêtres absolument nécessaires. Procédons à un bref inventaire de ce Bureau.

En bas à droite, la fiche principale  de l'application, ici nommée Squelette, et plus généralement, la fiche de l'unité en cours d'élaboration.

En partie cachée par cette fiche, l' éditeur de code . En bas de cette fenêtre, une fenêtre affiche les messages. Utilisez la touche   F12  pour basculer entre code et fiche, plus généralement pour faire monter l'un ou l'autre au premier plan. Nous verrons plus loin que l'éditeur de code s'ouvre automatiquement au bon endroit, avec génération automatique d'un squelette de procédure à compléter, par double-clic sur un élément de la fiche.

Attention

Laissez travailler l'environnement le plus possible

En particulier, vous allez, à un moment ou à un autre, fatalement double-cliquer sur un élément, puis décider de ne pas traiter l'événement associé. Si vous avez saisi du code, effacez-le, et strictement lui seul. Ne touchez pas aux  begin et  end; générés par l'environnement et n'effacez surtout pas l'ensemble de la procédure. Puis lancez une compilation par   Ctrl  +  F9  . Delphi constate que la procédure est vide, l'efface et efface également tout ce qui lui était lié (pour info, la déclaration de la procédure en tant que méthode de classe). Dans le cadre de notre travail sur l'assembleur, nous éviterons les manipulations plus profondes du code.

À gauche, l' inspecteur d'objets . Il affiche et permet d'éditer les propriétés d'un élément de la fiche principale, qui sera sélectionné dans la fiche ou dans la liste déroulante en haut de l'inspecteur d'objets. C'est la touche   F11  qui permet de faire apparaître ou monter au premier plan cette fenêtre, dont l'utilité est permanente.

Enfin, tout en haut de notre capture d'écran, se trouve le tableau de bord de Delphi. Globalement, du classique pas trop dense, avec quelques goodies, que vous découvrirez progressivement.

Nous remarquons une série de neuf onglets, de Standard à ActiveX  : la palette des composants , comportant dans cette version neuf pages . Nous éludons toute considération sur la nature exacte de ces composants. Ils font partie de la VCL, bibliothèque de composants visuels, même si tous ne sont pas visibles à l'exécution. Nous en utiliserons trois, Edit , Button et Memo , tous de la page Standard , et qui encapsulent des contrôles Windows. Les propriétés de ces contrôles sont modifiables à la conception, via l'inspecteur d'objets, puis de façon simple à l'exécution.

Delphi travaille sur un seul projet. Néanmoins, vous pouvez parfaitement ouvrir plusieurs fois Delphi simultanément, c'est-à-dire lancer plusieurs instances. Pour cela, il faut le lancer directement, par son raccourci. Si vous double-cliquez sur un fichier de projet  .dpr , celui-ci va remplacer un éventuel projet déjà ouvert et non pas s'ouvrir dans une nouvelle fenêtre Delphi. Si vous souhaitez consulter le projet terminé, tout en élaborant votre version, utilisez l'interface du CD-Rom pour l'ouvrir, mais faites-le dès maintenant. Attention, les projets Delphi peuvent s'ouvrir à partir du CD-Rom, mais toute autre opération que la consultation risque bien évidemment de générer des messages d'erreurs, puisqu'il y aura tentative d'écriture sur le disque, dans le fichier du projet.

Lançons donc Delphi par son raccourci, puis par Fichier/Nouveau/Application . Un projet vierge s’affiche.

Immédiatement, activez Fichier/Enregistrer le projet sous... La boîte de dialogue standard qui apparaît permet la création d'un dossier vide, si vous ne l'aviez pas préparé, par exemple E:\skel_d6 . Pensez à entrer dans ce dossier avant de confirmer la sauvegarde. Prenez l'habitude dès maintenant de ne pas vous contenter des noms de fichiers proposés par défaut. Delphi n'autorisant pas une unité à porter le même nom que le projet, attribuons par exemple les noms skel_main.pas pour le source Pascal de la fiche principale et skel_d6.dpr pour le projet.

Un peu de ménage : vous pouvez fermer les fenêtres inutiles, par exemple la fenêtre Vue arborescente des objets qui prend plus de place sur le Bureau qu'elle n'apporte de confort dans une application aussi simple. Vous pouvez donner à votre Bureau l'allure de la capture de l'écran final. Expérimentez la touche   F12  . Survolez les menus, en particulier Outils/Options d'environnement et Outils/Options de l'éditeur... où vous pouvez librement modifier certaines options de confort, comme l'enregistrement, l'ancrage, le docking si vous le trouvez crispant, etc. L'auto-enregistrement du projet avant exécution est une excellente précaution. Pour un petit projet comme celui-ci, où la reconstruction est très rapide, vous pourrez ainsi exécuter, après chaque modification, ce qui aura pour effet annexe de sauvegarder.

Vous pouvez lancer tout de suite Exécuter/Exécuter (ou appuyer sur la   F9  ). Votre application va rapidement se construire, puis se lancer. Vous êtes devant une fenêtre vide, que vous pouvez redimensionner et déplacer. Utilisez la croix en haut à droite pour la fermer. À l'aide de l'Explorateur de Windows, vous constatez qu'un fichier . exe a été créé. Une précision importante : si skel_d6.exe a une taille avoisinant 365 Ko, il est autonome, c'est-à-dire qu'il va s’exécuter indépendamment de la présence de Delphi sur la machine. S'il est plutôt de 15 Ko, il nécessitera la présence de bibliothèques installées, ce qui est le cas si Delphi est lui-même installé sur la machine. Pour paramétrer ce point, activez Projet/Options.../Paquets  ; cochez la case Construire avec les paquets d'exécution . Avec un outil d'inspection comme Aperçu rapide, vous constatez que la version 15 Ko importe des fonctions de vcl60.bpl et rtl60.bpl , dont la taille est à peu près de 1 300 et 630 Ko. Cela pour vous aider à faire fonctionner un programme sur une autre machine sans vous obliger à lire immédiatement le chapitre de l'aide consacré au déploiement des applications. Pour notre propos actuel, avec ou sans paquets d'exécution, c'est sans importance.

Dans l'inspecteur d'objets, vérifiez que c'est bien l'onglet Propriétés qui est sélectionné. L'objet sélectionné ne peut, pour l'instant, être que Form1 , la fiche principale. Vous pouvez choisir chacune des propriétés de  Form1 , obtenir sa description en appuyant sur   F1  et, au besoin, la modifier. Contentons-nous de modifier la propriété Caption de "Form1" en "Squelette". L'effet est immédiatement visible. Cette propriété se modifie par simple édition. D'autres moyens de modification sont proposés, pour d'autres types de propriétés. Étudiez la rubrique Font . Quand elle est sélectionnée, apparaît à droite un bouton garni de trois points qui, généralement, lance un éditeur de propriété. Ici, c'est la boîte de dialogue Windows de choix de police.

Icône points de suspension

Mais cette propriété peut également être éditée sous-propriété par sous-propriété, en cliquant sur le signe  + à sa gauche. La sous-propriété Style peut, elle-même, être détaillée. Une fois déployées, les sous-propriétés peuvent être repliées par le même bouton, qui représente maintenant un signe  - .

Icône  +

Icône  -

Pour placer un composant sur la fiche, il suffit, après avoir sélectionné la bonne page de la palette, de cliquer sur son icône, puis à peu près au bon endroit sur la fiche. Une variante permet de placer plusieurs composants de même type, sans faire la navette entre la palette et la fiche : maintenez la touche   Maj  appuyée pour sélectionner le composant, placez-en le nombre souhaité sur la fiche, puis enfin désélectionnez en cliquant sur la flèche (première icône sur chaque page de la palette).

Placez donc deux composants Edit , deux composants Button et un composant Memo sur la fiche, à peu près dans la bonne disposition.

La fiche avant, puis après finitions
figure 6.04 La fiche avant, puis après finitions

Commencez par positionner et dimensionner les composants. Dans notre cas, l'alignement sur la grille, 8 pixels par 8 pixels (par défaut), est tout à fait adapté.

Dans Outils/Options d'environnement/Concepteur , vous pouvez configurer le concepteur de fiches, en particulier la grille d'alignement.

Nous n'insisterons pas sur les manipulations graphiques sur les composants à l'aide de la souris ; elles sont suffisamment intuitives. Pour sélectionner plusieurs composants, soit vous les entourez d'une zone avec la souris, soit vous les sélectionnez/désélectionnez un à un en cliquant dessus tout en maintenant la touche   Maj  enfoncée.

Dans le menu Voir/Palette d'alignement , sélectionnez la palette et tapez   F1  pour obtenir de l'aide. Vous pouvez vous amuser à appliquer la palette d'alignement aux quatre composants Edit , après les avoir tous sélectionnés. Mais c'est inutile, répétons-le. La grille suffit.

Chaque composant déplaçable possède les propriétés Left et Top (positions horizontale et verticale par rapport au composant propriétaire, ici la fiche). Chaque composant redimensionnable possède les propriétés Width et Height (largeur et hauteur). Ces propriétés sont éditables dans l'inspecteur d'objets, et pour plusieurs composants sélectionnés simultanément si besoin est. C'est parfois la méthode la plus efficace pour aligner ou répartir subtilement une série d'objets.

Remarque

Avantage aux anglophones

Borland traduit ses produits en français, sans grand délai avec la version US. Mais les propriétés Width, Height, Top et Left, si elles font partie de l'environnement de développement, sont également des propriétés d'objets, donc susceptibles d'apparaître dans le code source. Comme les noms de fonctions dans beaucoup de langages, elles ne peuvent donc être traduites. Les anglophones sont ainsi favorisés. Il est impossible de traduire les mots clés ( ShowHint ), mais il arrive souvent que la notion sous-jacente soit traduite en français dans la documentation ( Afficher un conseil ) ; l’effort est louable, mais le rapport entre la notion et le mot clé disparaît dans le processus. Double avantage pour les anglophones donc. Mais vous devez vous résigner face à cette insoutenable injustice !

Considérant les composants placés et positionnés, la fiche et l'inspecteur d'objets simultanément visibles, sélectionnez le bouton, soit en cliquant dessus dans la fiche, soit dans la liste déroulante en haut de l'inspecteur d'objets. Nous souhaitons modifier son nom (propriété Name) et son libellé (propriété Caption). Ces deux modifications s‘effectuent par simple saisie. Ce bouton devant lancer une action indéterminée pour l'instant, affublez-le du libellé Action 1 . Faites de même pour le second bouton.

Adoptez dès maintenant une attitude homogène quand au nommage des composants. Rien ne nous oblige à modifier les noms, mais vous savez que, quel que soit le langage, un programme présentant de bons noms sera en grande partie autodocumenté. Nous choisissons de composer le nom d'un composant par quelques lettres désignant son type, suivies de son rôle. L'exemple de skel_d6 n'est pas excellent, puisqu'il s'agit d'un cadre à usages multiples. Pour les boutons Button1 et Button2 , choisissez BtnAction1 et BtnAction2 et modifiez dans ce sens leur propriété Name. Faites de même avec Edit1 et Edit2 , que vous renommez EdSaisie1 et EdSaisie2 , puis avec Memo1 qui se transforme en MemoSortie .

Dans le même ordre d'idée, évitez d'utiliser le fait que Delphi soit insensible à la casse (majuscule/minuscule) pour faire n'importe quoi : imposez-vous des règles, de préférence en accord avec la documentation, la littérature Windows, un autre langage, etc.

La fiche, ou unité, dans laquelle se trouve le composant, son parent, n'apparaît pas dans son nom. Plusieurs EdSaisie1 pourront donc coexister dans le projet. C'est un choix, mais la syntaxe objet appliquée par Delphi fait que, pour se référer à un composant à partir d'une fiche qui n'est pas la sienne, c'est la forme NomFiche . NomComposant qui prévaut. En assembleur, un tel choix serait dangereux, voire impossible.

Nous allions justement oublier le plus important, la fiche principale, qui s'appelle  Form1 . Renommez-la skel_mainForm , qui est le nom de l'unité, augmenté de Form pour Fiche. Pour vous convaincre de l'utilité de ce détail, activez Voir/Gestionnaire de projet , ou composant  Ctrl  +  Alt  +  F11  , et choisissez Projet/Options.../Fiches avant et après la modification. Et imaginez le résultat avec 10 fiches de 10 composants, ce qui reste encore un projet de taille modeste.

Il est important de nommer chaque composant le plus tôt possible. En effet, l'EDI Delphi gère parfaitement les changements de noms dans le code qu'il a lui-même généré, mais vous devrez modifier le vôtre à la main. Néanmoins, cette opération étant grandement facilitée par l'éditeur (fonction rechercher/remplacer), si un changement tardif de nom améliore la lisibilité, faites-le sans hésiter.

Le texte dans les deux boîtes d'édition n'est pas satisfaisant. Décidons de le remplacer par une chaîne vide. Sélectionnons les deux composants. L'inspecteur d'objets reste opérationnel. En fait, nous pouvons constater que certaines propriétés, comme Name, ne sont plus exposées, puisque affecter le même nom à quatre composants n'a pas de sens. Pour la propriété Text en revanche, aucun problème. Effaçons son contenu ; le texte disparaît des deux boîtes en même temps.

Pour le contrôle MemoSortie , nous désirons effacer le texte qu'il contient, changer la couleur du fond, changer la police de caractères et, enfin, ajouter une barre de défilement verticale, pour être certains de pouvoir consulter entièrementl'historique des sorties.

Occupons-nous du fond et de la police avant d'effacer le texte. Ce sera plus visuel que le contraire.

Propriété Color, choisissez clBlack dans la liste déroulante. Puis propriété Font, déroulez les sous-propriétés par le signe  + , sélectionnez un blanc cassé (adapté pour tout) comme clCream dans Color, une police non proportionnelle comme Courier New dans Name, et enfin Size à 10 si la police semble trop petite. Attention, Color, Size et Name sont bien ici des sous-propriétés de Color.

Pour éditer le texte, il faut modifier la propriété Lines. Pour cela, cliquez sur les trois points en regard de celle-ci. La suite est évidente. Qu’est-ce que cette propriété Lines ? L’inspecteur de propriétés nous apprend que, comme Font est du type TFont , Lines est du type TStrings , c’est-à-dire une collection de chaînes de caractères. Appuyez sur   F1  au besoin. Battons le fer tant qu’il est chaud et analysons rapidement la ligne suivante, courante en programmation objet :

MemoSortie.Lines.Add(IntToStr(var));

Un objet est un type de structure incluant des propriétés qui peuvent être des variables ou d’autres objets, et des fonctions, également appelées méthodes. MemoSortie désigne un objet boîte de type TMemo . Le point désigne la propriété Lines de type TSrings , une collection de chaînes de caractères, comme nous l’avons vu. Le point suivant permet d’invoquer la méthode Add de l’objet TStrings , qui ajoute une ligne à la collection. En C++, le point est remplacé par  -> .

Nous venons de mettre un doigt dans le code. Notre squelette d’application est maintenant prêt à en recevoir. Voyons d’abord à quoi ressemble le code de l’unité, avant toute saisie.

État initial de la fenêtre de code.
figure 6.05 État initial de la fenêtre de code.

Remarquez les deux zones principales, interface et implémentation, cette dernière partie étant pour l’instant vide. Dans la première, la clause uses inclut les bibliothèques utilisées dans l’unité. type définit une classe, Tskel_mainForm , qui est en gros la fiche de notre unité. Pour parler OOP (ou POO, Programmation Orientée Objet), cette classe est dérivée de la classe de base TForm (une fiche vide) à laquelle sont ajoutés cinq objets. Dans la zone var, la seule variable déclarée est justement skel_mainForm , une instance de Tskel_mainForm , c’est-à-dire la fiche de l’application. C’est dans cette zone que nous pourrons saisir des variables globales, du moins au niveau de l’unité.

Nous voulons maintenant coder le comportement suivant : à l’appui sur le bouton Action 1, les valeurs saisies dans les deux champs sont additionnées et le résultat sorti sur MemoSortie . Double-cliquez sur le bouton. Les lignes suivantes apparaissent :

procedure Tskel_mainForm.BtnAction1Click(Sender: TObject);
begin
 
end;

Le curseur est positionné entre le begin et le end; , prêt pour la saisie. Nous constatons également que la ligne suivante a été ajoutée dans la définition de Tskel_mainForm  :

procedure BtnAction1Click(Sender: TObject);

En POO, nous dirons que cette procédure est une méthode de Tskel_mainForm .

Le contenu de EdSaisie1 et EdSaisie2 est une chaîne de caractères. Nous allons le convertir en entier. Nous avons besoin de quelques variables, que nous déclarons localement avant le begin . Ce qui donne :

procedure Tskel_mainForm.BtnAction1Click(Sender: TObject);
var
  int1, int2, int3: Longint;
begin
int1 := StrToInt(EdSaisie1.Text);
int2 := StrToInt(EdSaisie2.Text);
int3 := int1 + int2;
MemoSortie.Lines.Add(EdSaisie1.Text +
                     ' + ' +
                     EdSaisie2.Text +
                     ' = ' +
                     IntToStr(int3) )
end;

Pour ce programme d'essai, il vous appartient de faire en sorte que cette conversion soit possible. Dans le cas contraire, une exception sera générée. Essayez justement de lancer le programme et de cliquer sur le bouton, sans remplir les boîtes de saisie. Vous voyez apparaître.

Oups !
figure 6.06 Oups !

Cliquez sur OK, puis immédiatement :

Ouf !
figure 6.07 Ouf !

Notez bien la combinaison   Ctrl  +  F2  , puis testez à nouveau le programme, en saisissant cette fois-ci des valeurs possibles pour des entiers Longint (32 bits signés). Modifiez enfin le code pour en arriver à :

procedure Tskel_mainForm.BtnAction1Click(Sender: TObject);
var
  i1, i2, i3: Longint;
begin
 //try
   i1 := StrToInt(EdSaisie1.Text);
   i2 := StrToInt(EdSaisie2.Text);
   asm
     mov eax, i1
     add eax, i2
     mov i3, eax
   end; //asm
 
   MemoSortie.Lines.Add(EdSaisie1.Text +
                      ' + ' +
                      EdSaisie2.Text +
                      ' = ' +
                      IntToStr(i3) )
 
 //except
 //  ShowMessage('Entiers uniquement...');
 //  EdSaisie1.Text := '12';
 //  EdSaisie2.Text := '34';
 //end;
end;

Nous traitons les exceptions, de façon imparfaite, puisque nous supposons à priori que l’exception sera une erreur de conversion. Le débogueur intégré traite lui-même l’exception en priorité, masquant notre propre traitement. Pour tester notre code, il faudra donc soit lancer directement l’exécutable depuis Windows, soit désactiver le débogueur intégré ( Outils/Options du débogueur ). Nous constatons que, dans tous les cas, il vaut mieux se passer de cette amélioration, à moins de le faire complètement. D’où la mise en commentaire. N‘oubliez pas de revalider si nécessaire le débogueur intégré.

Profitons-en pour placer les deux valeurs 12 et 26 (ou toute autre valeur entière valide) dans les boîtes de saisie, à la conception, pour ne pas avoir à les saisir à chaque test.

Nous introduisons quand même un peu de code assembleur. Les noms de variables ont changé, de int1 à int3 en i1 à i3 . Simple précaution, puisqu’il n’est pas impossible que int3 soit un mot clé de l’assembleur (c'est le cas). Il faut absolument éviter des noms de variables comme ax , ebx , cl , ch , etc. Non, etc ., vous pouvez l’utiliser sans danger. L’erreur avec ch , pour un caractère, est fréquente.

À titre d’exercice et pour conclure cette prise en main, essayons de saisir le bout de code suivant

Dans la zone des variables dites globales, complétez pour obtenir :

var
  skel_mainForm: Tskel_mainForm;
  G1, G2: Longint;

Puis, par un double-clic sur le bouton libellé Action 2 :

procedure Tskel_mainForm.BtnAction2Click(Sender: TObject);
var
  L1, L2: Longint;
begin
  L1 := 2;
  L1 := 3;
  L2 := 4;
  L1 := 4;
  L1 := 2;
  G1 := L1 + L2;
  L1 := 0;
  G2 := 4;
  G2 := 4;
end;

Lancez le programme, qui ne doit pas être trop long. Enfin, lisez la remarque suivante.

Remarque

Comportement du compilateur

Les compilateurs, notamment Delphi, ne compilent que ce qui est nécessaire.

Dans une procédure, sont déclarées des variables locales, qui meurent à la fin de celle-ci. Elles sont créées sur la pile et ensuite oubliées, s’il s’agit de variables simples. Pour des variables complexes, il est possible qu’elles soient déclarées sur le tas, mais la mémoire correspondante devra être libérée avant la fin de la procédure. Toutes les autres variables sont considérées par la procédure comme globales.

Nous sommes dans un environnement multitâche. Le programme peut être interrompu, entre deux instructions, voire au milieu d’une instruction dans certaines configurations. Il existe à ce sujet des stratégies de protection ; mais, ce que nous devons retenir, c’est que, dans un programme Delphi, les valeurs des variables ne sont pas garanties. Elles sont considérées comme volatiles.

Nous n’avons fait qu’aborder ces sujets, mais nous pouvons déduire de ce qui précède quelques règles permettant de prévoir le comportement du compilateur :

*           Une variable locale est considérée comme utilisée si elle sort de la procédure, pour affichage ou impression par exemple, en fait si elle est transmise à une autre procédure. Elle est également considérée comme utilisée si elle participe à la modification d’une variable globale.

*           Le compilateur connaît le moment à partir duquel une variable locale cesse d’être utilisée.

Muni de ces renseignements, vous devriez être en mesure de prévoir, dans le listing précédent cette remarque, quelles lignes seront compilées.

La réponse se trouve dans les points.

Après construction du projet
figure 6.08 Après construction du projet

Ces points dans la marge, ou gouttière, indiquent les lignes qui génèrent effectivement, après compilation et liaison, du code exécutable. Ce sont les endroits susceptibles de recevoir des points d’arrêt valides, ce que nous verrons à propos du débogueur. Remarquons que, si begin ne produit rien d’exécutable, end; génère au moins un RET .

6.3.2 L'aide

La qualité d'un produit de développement est constituée en partie de celle de son aide en ligne. Celle fournie par Borland est tout à fait correcte, mais présente certaines particularités, dont l'éditeur n'a d'ailleurs pas l'exclusivité. En un mot, les aides en ligne sont souvent plus riches que leurs interfaces le laissent supposer.

Dans Delphi 6, dans le menu Aide , vous trouvez Aide Delphi et Outils Delphi . La seconde est peu intéressante pour nous puisque, mis à part l'Éditeur d'images et OpenHelp, elle concerne des produits non distribués avec cette version.

Lancez donc l'aide en ligne par Aide/Aide Delphi . Allez sous l'onglet Rechercher , ce qui a pour effet, lors d'une première utilisation, de lancer l'Assistant Création de recherche. Choisissez n'importe quelle option, indifféremment, puis lancez la recherche. L’aide est maintenant opérationnelle. Nous allons nous livrer à une petite expérience.

Soit la procédure IntToStr() , qui convertit un entier en chaîne de caractères. Dans Index , rien. Dans Rechercher , une dizaine d'entrées sont trouvées, mais qui ne sont qu'une référence à la fonction, aucune ne la décrivant. Cette fonction, issue de la VCL, la bibliothèque qui fait de Delphi ce qu'il est, ne serait donc pas expliquée ? Que nenni. Dans l'éditeur de code, positionnez-vous sur IntToStr , puis appuyez sur   F1  . Bingo ! Nous voici sur la fiche descriptive de la fonction, où nous apprenons, outre sa syntaxe d'appel, qu'elle fait bien partie de la VCL, catégorie Routines de formatage numérique. Vous y trouverez parfois des liens vers les autres procédures de la famille, et un exemple d'implémentation. Nous aurions le même comportement avec d'autres mots clés aussi importants que New() par exemple.

Conclusion :

  Si vous connaissez exactement le mot clé, par exemple lors de l'étude d'un projet de démo, ou pour (re)vérifier une syntaxe d'appel, utilisez   F1  . Vous pouvez saisir le mot clé n'importe où dans un fichier de l'éditeur de code ; vous pouvez même ouvrir un fichier texte à cet effet par le menu Fichier/Nouveau/Autre où vous choisirez l'icône Texte .

  Vous pouvez également utiliser   F1  sur une fonction de la famille que vous connaîtriez, puis continuer par liens successifs.

  Dans tous les autres cas, vous trouverez votre bonheur dans le Sommaire de l'aide , rubrique Référence de la bibliothèque de composants visuels , sous-rubrique à choisir en fonction de vos besoins du moment. Le mot "visuel" ne doit absolument pas vous émouvoir, la VCL regroupe des routines aussi peu visuelles que New() , SetLength() , et beaucoup d'autres.

  L'autre rubrique intéressante est Référence Pascal Objet . Vous y trouverez des renseignements utiles sur les données manipulées, ainsi que la structure des programmes Pascal Objet. Lisez, bien évidemment, Code assembleur en ligne , la dernière sous-rubrique. L'ensemble de la rubrique fait l'objet du fichier oplg.pdf (voir CD-Rom), plus facile à imprimer et plus agréable à parcourir.

D'autres conseils pour obtenir de l'aide sont fournis sur le CD-Rom. Citons en particulier le fichier tasm.hlp , fourni avec C++Builder 6 Pro et au-dessus, en version d'essai éventuellement, ou les bcbxtool.hlp , aides de quelques outils en ligne de commande.

Puisque nous sommes dans l’aide, positionnez le curseur sur Longint , puis appuyez sur   F1  .

Une page précieuse
figure 6.09 Une page précieuse

Les types génériques   dépendent de l’implémentation, de la technologie du moment. Ils sont à utiliser pour favoriser les performances. Mais seuls les types fondamentaux   sont bien adaptés à une programmation en assembleur, puisque leur taille est définitivement fixée.

6.3.3 Débogueurs et fenêtres CPU

La fenêtre CPU du débogueur de Delphi est un outil remarquable dans une optique assembleur. En plus des services habituels d'un débogueur, elle possède au moins deux usages d'un très grand intérêt :

  Pour récupérer des données à l'entrée d'une procédure Delphi, pour exécuter une fonction de Windows, par exemple, la documentation vous manquera parfois, ou alors elle sera muette. Un petit coup de débogueur vous fournira le renseignement. Vous vous apercevrez que cette méthode est très souvent plus rapide que la voie documentaire normale, même si celle-ci existe.

  Il est toujours intéressant de regarder le code assembleur généré par Delphi ou tout autre bon compilateur. Le seul danger est de renoncer à optimiser. Plus sérieusement, le code assembleur généré est une base nécessaire à l'optimisation. À l'abord d'une technique nouvelle pour vous, la programmation de la FPU par exemple, l'observation du code dans la fenêtre CPU vous fera généralement gagner du temps : vous programmez en Pascal une multiplication de réels, vous regarderez comment elle est traitée, et enfin vous approfondirez dans la documentation Intel à partir des instructions relevées.

Dans le cadre de cette prise en main et pour de simples raisons de volume, nous ne présenterons que les possibilités de l’outil par rapport à l’assembleur. Nous laisserons donc de côté ses précieuses possibilités dans le cadre de la recherche de bogues pour nous pencher prioritairement sur l’observation du code dans les fenêtres CPU et FPU.

Attention

Delphi 6 Personnel ne propose pas de fenêtre FPU

Tout comme C++Builder Personnel d’ailleurs. Pour illustrer cette fenêtre du débogueur, nous utiliserons une version d'évaluation de C++Builder 6 Entreprise ou de Delphi 7 Studio Architecte. Si vous souhaitez tester cette possibilité, vous devrez installer une version Pro ou supérieure de Delphi ou de C++Builder. Les versions d'évaluation de ces produits conviennent parfaitement. La période d'essai de 60 jours est généreuse. Peut-être même, à l'issue de ce délai, serez-vous devenu suffisamment rodé à la pratique du débogueur pour faire en sorte que cette durée soit prolongée. Mais cela serait tout à fait malhonnête. Plus sérieusement, vous pouvez utiliser Turbo Debugger 32, réellement gratuit et présent sur le CD-Rom.

Observons d’abord le menu Exécuter , dans C++Builder et dans Delphi.

Les menus Exécuter
figure 6.10 Les menus Exécuter

Les menus sont représentés ici, alors qu'aucun programme ne s'exécute ; d'où les nombreuses rubriques en grisé. Notez les raccourcis clavier qui vous intéressent, au moins   F8  . Nous allons observer le code à partir d'un point d'arrêt, que nous placerons simplement à l'aide de la souris dans le code source : un clic dans la gouttière pour le placer, un second pour l'ôter. Ces points sont des points d'arrêt simples, sans condition ni actions associées.

Quelques points d'arrêt
figure 6.11 Quelques points d'arrêt

Cette capture a été faite alors que cinq BP (points d'arrêt) ont été placés et le programme reconstruit ou lancé sans être passé par aucun d'eux. Deux BP sont acceptés, trois sont refusés. Si nous enlevons maintenant ces arrêts, nous constatons que ceux qui étaient validés et eux seuls laissent place à un point dans la marge, signifiant que la ligne concernée génère du code. Analysons rapidement les trois refus :

  Pour le premier, il s’agit d’une déclaration de variables ; la ligne s’adresse donc au compilateur et ne génère pas directement de code.

  Pour le troisième, il y a encore moins de raisons de générer du code.

  Pour le deuxième, c’est un tantinet plus subtil : la variable  i4 n’est pas affichée comme l’est  i3 , c’est une variable locale, dont la durée de vie est celle du passage du programme dans la procédure. Le compilateur la néglige, en nous en avertissant ([Conseil], au bas de la capture). Nous avons déjà évoqué ce comportement minimaliste du compilateur. Plaisanterie mise à part, laissez-le exprimer ses conseils, comptez les points bleus et interrogez-vous sur l’opportunité d’un source sans objet.

Placez un point d’arrêt dans le code final de la prise en main de Delphi, l’addition du contenu des deux boîtes de saisie (12 et 26), sur la ligne :

  i1 := StrToInt(EdSaisie1.Text);

Lancez le programme par Exécuter/Exécuter ou   F9  , puis cliquez sur Action 1. Après avoir appuyé sur   F8  , qui vous fait avancer à la ligne de code exécutable suivante, promenez le pointeur de la souris un peu partout dans la fenêtre d’édition, ce qui fait apparaître des bulles d’information. Jouez également avec le menu Exécuter . Le BP arrête le programme avant l’exécution de la ligne marquée. Puisque vous avez exécuté cette ligne par   F8  , la capture suivante représente la situation juste après, avant que i2 ne soit affectée. Bien entendu, il est impossible d’avoir toutes les bulles en même temps.

Réalisé avec trucages !
figure 6.12 Réalisé avec trucages !

Pour l’instant, c’est du traçage classique de variables, sans rapport avec l’assembleur. Tout au plus avons-nous constaté que EAX était à 0 au moment de l’arrêt, et que le   F8  l’a fait passer à 12. La fonction de conversion renverrait dans EAX ?

Pour avoir une idée des possibilités mises à notre disposition par M. Borland, il nous faut aller dans Voir/Fenêtres de Débogage , ce qui donne pour C++Builder 6 Entreprise et Delphi 6.

Les menus Voir/Fenêtre de débogage
figure 6.13 Les menus Voir/Fenêtre de débogage

La fenêtre CPU nous tente.  Ctrl  +  Alt  +  C  , et nous y voilà.

La fenêtre CPU
figure 6.14 La fenêtre CPU

Nous sommes arrivés dans la fenêtre CPU sur l’instruction machine lea edx,[ebp-$14] , c’est-à-dire avant son exécution, puis nous avons appuyé deux fois sur   F8  . La flèche dans la gouttière montre l’emplacement actuel du programme ; elle pointe sur la prochaine instruction à exécuter.

L’autre flèche, sur la droite, indique que le programme va sauter, ici à cause du call . Elle sera surtout utile pour les instructions de saut conditionnel.

Les instructions du source Delphi sont indiquées en gras ; nous voyons qu’à chacune correspond une ou plusieurs instructions machine.  F8   exécute chaque ligne assembleur,  F7   sautant de plus à l’intérieur des call .  Maj  +  F7   exécute le code machine jusqu’à la prochaine ligne du source Delphi. Expérimentez.

Un point d’arrêt mis dans l’éditeur de code sur une ligne Delphi est visible, dans la fenêtre CPU, sur la première instruction machine correspondant à cette ligne. Et vice versa. En revanche, il est possible de placer un BP sur une autre ligne assembleur ; il est même possible d’en poser à l’extérieur du programme, dans la fonction de classe TControl.GetText par exemple. À ce moment-là, elle ne sera pas rappelée dans l’éditeur de code.

La ligne en surbrillance située 3 lignes sous la flèche indique un simple curseur, une ligne à laquelle vont s’appliquer certaines actions.

Ce qui précède concerne la zone de code. Il existe quatre autres zones :

  La zone des registres ;

  La zone des flags ;

  La zone mémoire, sous la zone de code ;

  La zone de la pile, en bas à droite.

Dans chacune de ces zones, une ligne est en surbrillance et désigne donc, à la façon d’un curseur, un élément particulier. Voyons ce que nous proposent les menus contextuels (clic droit).

Les menus contextuels de la fenêtre CPU
figure 6.15 Les menus contextuels de la fenêtre CPU

Dense, n’est-ce pas ? Nous retrouvons systématiquement l'option Changer de thread... , qui permet d'accéder aux autres threads du même programme, ce qui est très utile en débogage.

Dans les zones registres et flags , les éléments de menu permettent de changer la valeur du registre ou du flag mis en surbrillance et n’appellent pas de commentaire. Les valeurs ayant changé lors de la dernière instruction exécutée sont en rouge. Si vous positionnez le curseur de la souris sur un flag, son intitulé, en français, apparaît dans une info-bulle.

Vous trouvez dans les trois autres zones le choix Aller à l’adresse  Ctrl  +  G  . Cela permet la visualisation de l’adresse saisie et positionne le curseur dessus, dans le segment correspondant à la zone, code, données ou pile. Attention, les adresses seront certainement saisies en hexadécimal ; elles devront être précédées du caractère  $ . Toute expression de type Base-Offset-Déplacement est acceptée, ainsi que les indirections par les crochets  [] .

Dans les zones mémoire , les éléments de menus se comprennent sans problème. L'option Suivre permet de se rendre, soit dans le code, soit dans la zone de données, à l'adresse en surbrillance. Remarquez également les nombreux formats de visualisation offerts, qui sont fort pratiques déjà pour se familiariser avec la notion de little endian.

La zone de pile représente les données de celle-ci, donc dans le segment SS, les adresses en ordre décroissant. Le sommet de la pile est pointé par une flèche. Comparez son adresse effective à la valeur de ESP. Les captures d'écrans sont trompeuses. Dès qu'une instruction modifiant ESP, comme un PUSH , aura été exécutée, la flèche sera placée en bas de la zone, permettant ainsi de voir au-dessus les valeurs empilées, l'usage de l'ascenseur restant possible.

Si vous avez installé le débogueur TD32 (consultez le CD-Rom et installez-le au besoin), reprenez le projet, déplacez-le éventuellement pour respecter les dossiers et noms de fichiers en 8.3 et allez dans le menu Projet/Options/Lieur .

Propriétés du Projet/Options du lieur
figure 6.16 Propriétés du Projet/Options du lieur

Validez Informations de débogage TD32 . Vous pouvez également valider une option dans la rubrique Fichiers map . Et ne manquez pas de cliquer sur Aide . OK, puis Projet/Construire skel_d6 , si tel est le nom de votre projet. Vous pouvez maintenant lancer TD32 et charger skel_d6.exe pour le tracer. Mais vous pouvez faire mieux.

Dans le menu Outils/Configurer les outils , cliquez sur Ajouter .

Paramétrage de l'outil TD32
figure 6.17 Paramétrage de l'outil TD32

Il est possible d’ajouter des options devant $EXENAME . Nous pouvons créer un autre outil, TD32 Config, mais ce serait de la gourmandise.

C'est ici qu'il est préférable d'être en 8.3. Les débogueurs sont des programmes spéciaux. Le nôtre désactive le Num Lock en permanence. Ils mettent en jeu les registres de DEBUG, en cas de débogage hardware. Cela est vrai pour tous, TD, TD32, CodeView. N'en faites pas fonctionner plusieurs en même temps. Désactivez éventuellement le débogueur intégré de Delphi (nous avons vu comment procéder un peu plus haut) en essayant de ne pas oublier de le revalider.

Il est préférable (dans tous les cas) de cocher les options d'auto-enregistrement, afin que les sources soient à jour avec l'exécutable. Il est raisonnable de penser que Delphi le fait quand l'option TD32 est cochée, mais rien n’est garanti.

Remarque

Utilisez $TDW

En fait, nous avons configuré cet outil TD32 manuellement dans le but de voir une fois la configuration d'un outil de A à Z. Il existe une macro $TDW, la dernière au bas de la liste, qui, placée seule dans Paramètres , permet de ne pas renseigner Répertoire de travail et de ne pas se préoccuper de paramétrer le lieur ni de la sauvegarde automatique.

Il ne vous reste plus, projet ouvert, qu'à cliquer sur Outils/TD32 . Le résultat est presque parfait.

Presque car, d'abord, les points d'arrêt posés dans Delphi ne semblent pas pouvoir être transmis à TD32. Ceux posés dans TD32 se retrouvent au lancement suivant, sauf si le projet a été reconstruit, ce qui est en général le cas.

Ensuite, le débogueur s'ouvre sur le module projet, skel_d6.pas ici, qui est accessible sous Delphi par Projet/Voir le source . La première chose à faire (et semble-t-il à refaire à chaque lancement) est de changer de module, pour poser des points d'arrêt dans le source skel_main.pas du module du même nom. Pour cela, activez View/Modules , accessible également grâce à un clic droit ou en appuyant sur   F3  . Vous disposez de macros pour des tâches répétitives.

Parcourez les menus et l'aide, malheureusement en anglais, pour vous rendre compte de toutes les possibilités de cet excellent outil. La fenêtre coprocesseur arithmétique est, bien entendu, présente. Au vu de la richesse des options, nous regrettons la taille imposée de l'affichage. Le passage en Plein écran amène du confort, mais pas des pixels. Pour en arriver sur un point d'arrêt, dans une de nos fonctions, et sur une vue CPU.

TD32 en pleine action
figure 6.18 TD32 en pleine action

Nous voyons là les fenêtres CPU, source avec un BP et FPU, inutiles. Et une bonne surprise : la fenêtre CPU est, comme celle de Delphi, mais également comme celles de toutes les versions de TD, composée des cinq mêmes zones. Un clic droit dans chacune de ces zones produit également le même effet, l’ouverture d’un menu contextuel. Et ces menus sont les mêmes, à peu près, selon la version. Nous pouvons donc reprendre le commentaire précédent sur ces menus, dans la présentation du débogueur de Delphi.

Deux petites différences toutefois, d'ordre ergonomique, sans parler de la langue :

  Pour obtenir le menu contextuel dans une fenêtre donnée, il faut d'abord y sélectionner une ligne par un clic gauche, pour obtenir ensuite le menu par un clic droit. C'est logique, puisque le menu s'applique à une ligne et non à une fenêtre, mais déroutant après Delphi.

  Une fois le menu contextuel à l'écran, vous pouvez le parcourir en maintenant appuyé le bouton gauche de la souris : une phrase d'aide apparaît tout en bas de la fenêtre TD32.

6.4 BASM, l'assembleur de Delphi

Delphi intègre efficacement son assembleur intégré. C'est-à-dire qu'il fait plus qu'assembler brutalement vos instructions ; il s'intéresse à votre code, par exemple quand il établit les dépendances entre variables pour déterminer ce qui doit être compilé et ce qui peut ne pas l'être. Mais il ne faut pas lui en demander trop. Il est préférable, dans le code assembleur, d'utiliser les noms de variables explicitement, ce qui lui facilitera la tâche et nous évitera des erreurs. Reprenez le code de test (à la fin de la prise en main de Delphi 6) :

1  L1 := 2;
2  L1 := 3;
3  L2 := 4;
4  L1 := 4;
5  L1 := 2;
6  G1 := L1 + L2;
7  L1 := 0;
8  G2 := 4;
9  G2 := 4;

Rappelons que les lignes 3 et 5 étaient compilées, mais pas 1, 2 et 4, considérées par Delphi comme inutiles. Remplacez la ligne de l'addition par :

asm
 mov edx, L1
 add edx, L2
 mov G1, edx
end;

Les lignes 1 à 5 sont toutes compilées. Delphi a vu que L1 et L2 modifiaient la variable globale  G1 . Et il n'a pas lésiné. Proposons-lui :

asm
 push edx
 mov edx, L1
 add edx, L2
 pop edx
end; 

Il ne lésine pas là non plus : il compile tout ce qui précède, malgré l'inutilité flagrante de ce bout de code. Tout ce qui est dans un bloc asm...end; est assemblé. Il semble également que toutes les variables présentes dans un tel bloc prennent le même statut que des variables extérieures, volatiles.

Delphi aide également BASM en s’occupant en grande partie de la gestion des registres avant de lui donner la main et quand lui-même la reprend.

Nous allons, dans les trois paragraphes suivants, regrouper ce que nous avons pu glaner quant à la syntaxe de BASM, ainsi que les conventions d'appels des procédures et de fonctions en assembleur.

6.4.1 Syntaxe

Le mot clé asm

Les instructions assembleur en ligne seront placées entre les mots clés asm et end;  :

asm xor ecx, ecx end;

S'il y a plusieurs instructions assembleur dans le bloc, elles pourront être séparées soit par des points-virgules  ; , soit par de simples sauts de ligne :

asm
 cpuid ; mov var1, eax 
end
;

ou :

asm
 push ebx
 mov u16_0, valeur1
 mov u32_0, 0
 mov ax, test
 pop ebx
end
;

Dans une procédure (ou une fonction), si le mot clé asm remplace le mot clé begin , la procédure devient une procédure assembleur :

function
 IdentCPU_EBX():Longword;
Assembler
;
asm
 cpuid
 mov Result, ebx
end
;

Le mot clé Assembler , tout comme d'ailleurs Inline , est accepté par le compilateur pour des raisons de compatibilité, mais ne joue plus aucun rôle aujourd'hui.

Jeu d'instructions

Le jeu d'instructions reconnu par BASM dans les versions 6 et 7 de Delphi couvre toutes les instructions des processeurs jusqu'aux Pentium 4 et Athlon XP. Ce n'est pas le cas des versions antérieures. Il n'y a pas nécessairement synchronisation entre le jeu d'instructions reconnu par BASM, celui utilisé par le compilateur et celui reconnu par le désassembleur du débogueur. Pas plus qu'avec les indications de l'aide en ligne.

C'est malheureusement parmi ces instructions manquantes que figurent préférentiellement celles que nous aimerions bien utiliser. Les possesseurs d'anciennes versions peuvent souvent s'en sortir en codant à la main, en hexadécimal, à partir de la documentation des constructeurs AMD et Intel. C'est une solution très courante pour CPUID, RDTSC et autres opcodes sans arguments. Dans d'autres cas, quand il y a des arguments à fournir, c'est plus compliqué.

La syntaxe générale de BASM est théoriquement un sous-ensemble de celle de TASM, donc de MASM, avec utilisation de spécificités Delphi. Les nuances apportées par ces spécificités sont telles qu'il est préférable de mémoriser la syntaxe de BASM indépendamment de celle de TASM.

Commentaires

Les commentaires respectent la syntaxe du Pascal de Delphi :

{ Ceci est un commentaire
qui continue et peut durer tout un bloc} 
// Commentaire jusqu'à la fin de la ligne
(* Comme pour les parenthèses*)

Les instructions ne doivent pas être coupées par des commentaires :

{commentaire savant}  lea eax, L1
lea eax, dword ptr[L1] {commentaire savant}
mov eax, G1 {comment
aire savant} mov eax, dword ptr[G1]

Ces quatre lignes se compilent. À éviter malgré tout, à part la deuxième.

Labels

Il existe en Delphi des étiquettes, des labels, devant comme les variables être déclarés en début de bloc. Ces labels sont les compagnons de l'instruction goto . Autant dire que leur utilisation est rarissime. Ils sont positionnables dans des blocs assembleur. Il est préférable d'utiliser des labels locaux, dont la portée est limitée au bloc asm...end; . Ils n'ont pas à être déclarés préalablement et doivent commencer par le signe  @ . Les bonnes raisons d'utiliser autre chose que des labels locaux sont rares.

Sauts

Delphi optimise les sauts sans nécessiter de directive particulière (le . JUMPS  de TASM est implicite). Donc, il choisit au mieux le type de JMP et remplace si nécessaire un saut conditionnel Jcc[cible] par la séquence JNcc[justapré]; JMP[cible] . Ncc représente la condition inverse de cc .

Directives de réservation mémoire – variables

Coder une instruction non implémentée est le cas le plus fréquent d'utilisation des directives de réservation mémoire  db , dw , dd , ainsi que dq  depuis Delphi 6, qui réservent 1, 2, 3 et 4 octets en mémoire. Tous réservent plusieurs mots de même taille, en les séparant par des virgules. Par exemple dw 0, 0, 0 réserve 6 octets (3 mots de 16 bits). De plus, db accepte des chaînes à la place d’une liste d’octets. Nous en parlerons un peu plus loin.

Avec l'assembleur intégré, cette réservation se fait dans le segment de code. S'il s'agit d'une donnée, son utilisation est bien compliquée et pour un avantage douteux. L'exemple suivant fonctionne :

  jmp @suite
@var:
  dw $0000
@suite:
  mov word ptr [@var], ax

En réalité, les variables, simples ou complexes, seront créées en Delphi par la directive var . Écrire un bloc  var en début de procédure assembleur ne remet pas en cause son statut 100 % assembleur. Les variables de ce bloc sont des variables locales à la procédure, créées dans le cadre de pile. Celles créées au niveau module sont des variables globales, situées dans le tas (heap), c’est-à-dire dans le segment de données.

Mots réservés

Quand le compilateur trouve un mot qui se trouve dans sa liste de mots réservés, il ne cherche pas plus loin. Donc, n’attribuez surtout pas à une variable un nom parmi la liste suivante :

AH      BX      DI      EBX     ESP     OFFSET  SP
AL      BYTE    DL      ECX     FS      OR      SS
AND     CH      DS      EDI     GS      PTR     ST
AX      CL      DWORD   EDX     HIGH    QWORD   TBYTE
BH      CS      DX      EIP     LOW     SHL     TYPE
BL      CX      EAX     ES      MOD     SHR     WORD
BP      DH      EBP     ESI     NOT     SI      XOR

Cette liste est directement copiée depuis l'aide de Delphi. Certains de ces mots sont de vrais pièges, en particulier ceux à deux lettres, ch par exemple, qui convient si bien pour un caractère. Or, un caractère mesure un octet, tout comme CL. Donc, là où vous allez mettre la variable  cl , le registre CL convient également, du point de vue syntaxique. La compilation s’effectue ainsi sans réagir ; le bogue n’est dès lors pas immédiat à détecter. Si vous devez absolument utiliser un mot réservé, dans le cas d'ajout de code assembleur à un code Delphi préexistant par exemple, préfixez-le de l'ampersand : &cl .

Saisie des constantes numériques

La taille maximale d'une constante numérique est celle d'un mot de 32 bits signé ou non signé.

Par défaut, la saisie est interprétée en décimal. Il est également possible de saisir :

  En octal : suffixe  O ou  o , comme dans : 7247O . Entrée directe en octal impossible en Pascal. Maximum 37777777777o .

  En binaire : suffixe  B ou  b , comme dans  01111011b . Il n'est pas possible d'insérer des séparateurs de quartets, espaces ou autres, ce qui serait plus pratique : 0111 1011b n'est pas accepté. Maximum 32 caractères. Les entrées directes en binaire ne sont pas acceptées en Pascal, même si, dans une déclaration de constante, x: Longint = 111b; se compile, mais x prend la valeur décimale 111. Sans doute un bogue !

  En hexadécimal, suffixe  H ou  h , ou alors préfixe  $ . De plus, la règle veut qu'une constante numérique débute par un chiffre ou le signe  $ . D'où : $1234 et $FFFF mais 1234h et 0FFFFh . Seule la forme avec  $ est acceptée en Pascal. Maximum : 8 caractères.

  En chaîne de caractères, maximum 4 caractères. La chaîne est, si nécessaire, complétée par des caractères  NULL à gauche, donc par des  0 .

Essayons d'être logiques dans nos choix ; nous éviterons ainsi des commentaires :

and ax, 010000000000b , est-ce utile de préciser qu'il s'agit d'un masque sur le bit 14 ?

mov al, 'a' est suffisant, mieux que mov al, 61h // code ASCII du a dans AL .

mov  al, "\0" fait penser à un caractère de fin de chaîne. Lisez la remarque un peu plus loin.

Pour ce dernier exemple, si vous chassez le cycle, vous préférerez peut-être un xor al, al  //fin de chaîne dans AL , encore que, sur un Pentium, cela n’est pas crucial, peut-être même plus lent...

Saisie des chaînes de caractères

La saisie sous BASM de chaînes de caractères ne sera pas fréquente. Nous avons vu que les vraies chaînes de caractères seront déclarées sous Delphi, qui demande des apostrophes et le  # pour introduire un caractère de contrôle. Par exemple, STR := 'Nestor'#32'Burma'; affectera "Nestor Burma" à STR, 32 étant le code de l'espace. Les guillemets sont refusés.

L'aide en ligne de Delphi affirme que, en assembleur, les apostrophes et les guillemets sont acceptés de la même façon. Cela n'est pas tout à fait vrai. Testez ce code, en plaçant un point d'arrêt sur le JMP :

jmp @1
db "Nestor\0Burma"
db "Nestor", "\0", "Burma"
db 'Nestor\0Burma'
db 'Nestor',0,'Burma'
@1:
mov al, "\0"

Une fois sur le BP, après avoir constaté que le mov al, "\0" devient un mov al, 0 , observez dans la zone mémoire le contenu de la zone de code à partir du premier db , sans oublier le  $ devant l'adresse.

La mémoire réservée
figure 6.19 La mémoire réservée

Tout se passe comme si les chaînes entre guillemets étaient d'abord interprétées, puis le résultat mis en mémoire. De la même façon, nous constaterions que le \n est interprété 0Ah (Line feed, à la ligne) entre guillemets, et comme \n entre apostrophes. Il faudra donc être prudent ; mais rappelons que la déclaration de chaînes sous BASM n'a logiquement pas lieu d'être. Nous utiliserons plutôt les constantes caractères. Mais qu'en est-il de la légitimité d'un "\0" ou d'un "\n"  ?

Les constantes de Delphi

Il y a deux façons de déclarer des constantes, avec et sans type :

const
   NbreCylindres = 80 ;
   PisteParCylindre = 18;
   y: Longint = 55;
   z: Longint = 12;

Les deux premières constantes n’ont pas de type. Leur rôle est en gros celui d’une macro : simple remplacement dans les expressions du nom de la variable par sa valeur, comme si celle-ci était saisie au clavier.

Les deux autres, y  et  z , sont de vraies variables, ici de type Longint, mais dont la modification est interdite. Une option de compilation permet d’ailleurs leur modification. Elles deviennent alors de simples variables initialisées. Elles sont compilées comme des variables, qu’elles sont. Leur valeur n’est pas supposée connue au moment de la compilation.

Expressions interdites :

mov eax, y + z //comme pour des variables
mov ax, y //pas le bon type
z := 44 ; //z est quand même déclaré const

Expressions autorisées :

mov eax, NbreCylindres * PisteParCylindre // = mov eax, 1440
mov ax, NbreCylindres * PisteParCylindre // même chose, pas de type

La séquence suivante fonctionne très bien :

mov eax, z
shl eax, 2
mov z, eax

z vaut maintenant 48, valeur qui peut, bien sûr, être utilisée par la suite. Pour une constante…

6.4.2 Les classes et les types d'opérandes – opérateurs

Cette partie traite des opérandes d'une instruction assembleur. La notion de mode d'adressage est supposée connue. C'est le problème du rapport entre ces opérandes et les variables (ou constantes) Delphi qui nous intéresse ici.

Un opérande peut être Registre , Référence mémoire ou Valeur immédiate (opérande source dans ce dernier cas), dans des tailles de 8, 16 ou 32 bits. Les opérandes Registre n'appellent pas de commentaire particulier.

Pour les deux autres, nous allons utiliser des valeurs numériques et des noms de variables et de constantes Delphi. Le problème est de savoir si une expression est plausible et, si oui, comment elle sera interprétée par Delphi. Pour le résoudre, il faut avant tout avoir une idée claire de ce que connaît le compilateur. Il existe en gros trois catégories de valeurs numériques :

  Les valeurs absolues, qui sont absolument connues à la compilation. Sur ces valeurs, les opérateurs s'appliquent sans restriction, puisque le résultat remplacera l'expression pour former une valeur immédiate.

  Les valeurs absolument inconnues au moment de la compilation : ce sont les contenus des registres et des emplacements mémoire.

  Les valeurs relogeables, qui seront connues à l'issue du travail du lieur. Par exemple l'adresse d'une procédure, utilisée dans un CALL ou un JMP , sera donnée par le lieur. Cette caractéristique entraîne des restrictions quant à l'utilisation des opérateurs.

C'est une bonne intuition de la logique de compilation et de lien qui permet de déterminer ce qu'il est possible d'attendre d'une expression donnée. La pratique et l'utilisation du débogueur à des fins de vérifications permettront de s'en sortir ; ce ne sera peut-être pas immédiat. Nous allons passer en revue les opérateurs les plus importants, puis étudier quelques exemples.

L'aide Delphi vous donnera un ordre de priorité des opérateurs, utilisez plutôt les parenthèses en cas de doute. Le compilateur vous en sera reconnaissant et ne générera pas un octet de plus, même si le source est moins compact. De plus, il aura ainsi plus de chances de produire le code que vous espérez.

Le type d'une expression en assembleur est tout simplement sa taille en octets, exprimée par un des mots suivants, à utiliser dans les expressions de transtypage :

1    BYTE
2    WORD
4    DWORD
8    QWORD
10   TBYTE

Le type d'une valeur immédiate est 0.

 

Les opérateurs de BASM

Opérateur(s)

Description

AND , OR , XOR , NOT , SHR , SHL

Opérateurs logiques bit à bit. Ne s'appliquent qu'à une ou deux valeurs immédiates absolues, pour obtenir une valeur immédiate absolue.

HIGH , LOW

Renvoie l'octet constitué des 8 bits de poids respectivement fort et faible d'un mot de 16 bits, valeur immédiate absolue.

* , / , MOD , + , -

Multiplication, division entière et reste de la division entière (modulo), addition, soustraction. Valeurs immédiates absolues en entrée pour obtenir une valeur immédiate absolue. + et -  peuvent simplement précéder une valeur, sans effet pour le  + , inversion de signe pour le  - .

+ , -

Une des expressions peut être une valeur relogeable, la première dans le cas du  - .

(....)

Pour fabriquer une sous-expression et forcer un ordre de calcul. Ne pas craindre d'en abuser.

&

L'identificateur qui suit est interprété comme un symbole utilisateur, même si c'est également un mot clé. Voir section Mots réservés .

[....]

Référence mémoire. À l'intérieur du crochet se trouve une expression correspondant à une référence mémoire, pouvant contenir des registres (modes d'adressage) ou des constantes.

OFFSET

Valeur du déplacement de l'expression qui suit. C'est une valeur immédiate.

PTR

Opérateur de transtypage. Précédé d'un type et suivi d'une référence mémoire, il désigne l'objet d'un certain type à cette adresse mémoire.

TYPE

Renvoie la taille en octets d'une expression.

:

Surcharge de segment. Permet de spécifier un autre segment que celui par défaut.

.

Sélecteur de membre de structure. Conduit le compilateur à additionner l'adresse de la structure et le déplacement du membre dans la structure.

Appliquons ces renseignements à quelques exemples, tirés de l'aide de Delphi. Pour tous les exemples, nous supposons :

const
  konst = 12;
var
  var1 : Integer;

 

  mov eax,konst donne : mov eax, 12 , donc 12 dans EAX.

  mov eax, var1 donne : mov eax,[adresse de var1] , donc valeur de DS:adresse de var1 dans EAX. La syntaxe mov eax, [var1] est identique.

  mov eax,[konst] donne : mov eax,[12] , donc valeur à l'adresse DS:12 dans EAX.

  mov eax, OFFSET var1 donne : mov eax, adresse de var1 , donc déplacement de l'emplacement de var1 dans EAX.

Dans les deuxième et quatrième cas, adresse de var1 est une constante (valeur immédiate) calculée à la compilation.

OFFSET et [] sont deux opérateurs complémentaires. Donc :

  mov eax,konst est identique à mov eax,OFFSET [konst] .

  mov eax,var1 est identique à mov eax,[OFFSET var1] .

Identique signifie que le même code machine sera généré.

La logique permet de prévoir ce qui sera refusé :

  mov ax, var1 (non concordance des types).

et ce qui sera accepté :

  mov eax, [12] (12 n'est pas typé, donc EAX impose son DWORD).

  mov ax, WORD PTR [var1] (il faut transtyper var1 vers le type WORD).

Deux autres syntaxes pour un transtypage :

mov ax, WORD(var1) et mov ax, var1.WORD .

Vous pouvez maintenant revoir l'exemple de code donné en tout début du chapitre intitulé Assembleurs autonomes . Il devrait être un peu plus clair.

Enfin, Delphi interprète bien entendu tous les modes d'adressage. Ainsi :

mov eax, [konst+ ebx + edx]

sera un adressage de type déplacement + base + index.

BASM accède aux entités Delphi généralement sous la forme d'un pointeur 32 bits, accompagné éventuellement d'un déplacement de champ dans le cas d'entités structurées.

Utilisation des registres

Quand du code assembleur est simplement inséré en ligne dans un flux du programme en Pascal, la seule précaution à prendre est de ne pas modifier les registres EDI, ESI, ESP, EBP et EBX. En cas d'utilisation, il faut les sauver et les restaurer par des PUSH et POP en début et fin du bloc de code.

Les registres de segment CS, SS, ES, FS, GS ne doivent tout simplement pas être manipulés.

Les registres EAX, ECX et EDX sont librement utilisables. Ils seront souvent suffisants pour de courtes insertions.

Au début d'un bloc asm , EBP pointe vers le cadre de pile actuel, ESP vers le haut de la pile, SS et DS sont normalement les segments de pile et de données. Aucun autre registre n'a de valeur garantie.

Il n'est pas courant de sauvegarder le registre EFLAGS. Nous avons néanmoins constaté un dysfonctionnement dans les fonctions d'affichage de Delphi quand elles suivent, même d'assez loin, une modification en assembleur du flag de direction DF.

6.4.3 Fonctions et procédures

Une procédure accomplit une tâche, en prenant ou pas des paramètres. Une fonction est une procédure qui renvoie une valeur. Étudier les fonctions revient donc à étudier également les procédures. Nous utiliserons ce dernier mot pour désigner génériquement les deux types. L'interface d'une procédure est définie par sa déclaration, ou prototype.

Écrire des procédures entièrement en assembleur est de loin plus efficace que de parsemer son code de petits blocs asm ... end; . Et nous avons la chance que ce soit accepté par Delphi. La contrainte consiste à devoir gérer nous-mêmes un certain nombre de points un peu rébarbatifs.

Avant de voir un par un ces points, écrivons (voir CD-Rom) une petite fonction :

function essai(valeur1:Word; valeur2:Longword):Longword;Register;Assembler;
var
 localvar:Longword;
asm
 push ebx
 mov  ebx, valeur2
 mov  localvar, 12
 mov  u32_0, 24
 add  ebx, localvar
 add  ebx, u32_0
 mov  Result, ebx
 pop  ebx
end;

Cette fonction ignore son paramètre  valeur1 et additionne 36 au paramètre valeur2 avant de renvoyer le résultat de cette addition. Nous testerons cette fonction par :

MemoSortie.Lines.Add(IntToStr(essai(0,100)));

Le résultat est 136. Rassurant. Observons la fenêtre CPU après avoir posé un point d'arrêt sur le push ebx  :

valeur2 est le second paramètre passé, la convention est register , il est donc passé dans EDX. mov  ebx, valeur2 donne donc :

0044FFDB 89D3             mov ebx,edx

localvar est une variable locale, dans la pile. mov  localvar, 12 se traduit par :

0044FFDD C745FC0C000000   mov [ebp-$04],$0000000c

u32_0 est une variable externe à la fonction (globale), elle est quelque part dans le segment de données, mov  u32_0, 24 va donc faire :

0044FFE4 C705DC3B45001800 mov [u32_0],$00000018

Nous pouvons vérifier que u32_0 est bien à l'offset 00453BDC dans le segment de données.

Le résultat doit être renvoyé dans EAX. Mais Delphi crée d'abord une variable locale pour accueillir Result , puis en fin de code transfère cette variable dans EAX :

(mov  Result, ebx)
0044FFF7 895DF8           mov [ebp-$08],ebx
(pop  ebx)
0044FFFA 5B               pop ebx
0044FFFB 8B45F8           mov eax,[ebp-$08]

Bien entendu, l'utilisation de Result n'a rien d'obligatoire, et nous aurions très bien pu pousser directement le résultat dans EAX.

Puisque nous sommes dans la fenêtre CPU, profitons-en pour noter le code ajouté par Delphi avant (prologue) et après (épilogue) la fonction, que nous pourrions qualifier de code de maintenance du cadre de pile :

push ebp
mov  ebp, esp
add  esp, -$08
...
mov  eax, [ebp-$08]
pop  ecx
pop  ecx
pop  ebp
ret

Les deux pop ecx (en fait, des pop nimportequoi ) ont le même effet ici que mov esp, ebp . Ils rétablissent le pointeur de pile en fonction des paramètres passés. C'est ce que signifie : la maintenance (ou nettoyage) de la pile est assurée par la procédure, dans les conventions d'appel (voir un peu plus loin).

Comme nous sommes curieux, nous englobons le bloc asm ... end; dans un bloc begin ... end; , pour que notre fonction ne soit plus 100 % assembleur et nous relevons les différences :

push ebp
mov  ebp, esp
add  esp, -$0C
mov [ebp-$04], edx
lea eax, [ebp-$0C]
...
mov  eax, [eax]
mov  esp, ebp
pop  ebp
ret

Il transfère les paramètres depuis les registres vers la pile, en fait donc des variables locales. C'est d'ailleurs de cette façon qu'il faut voir un paramètre de procédure vu de la procédure, en Pascal ou en C/C++ : une variable locale. C'est une bonne méthode, puisqu'elle permet d'accéder aux paramètres sans ambiguïté par leur nom. La méthode que nous avions utilisée est dangereuse : valeur2 sera toujours remplacé mot pour mot par EDX, et le risque est grand d'en avoir écrasé la valeur depuis le début de la procédure. Remarquez que le compilateur analyse suffisamment pour constater que valeur1 n'est pas utilisé et n'en fait pas une variable locale. Une autre variable locale est créée pour Result , et de plus, son adresse est sauvée dans EAX, dont le compilateur a noté la non-utilisation dans le module. Le couple lea eax, [ebp-$0C] , mov  eax, [eax] semble superflu ici. Vous pouvez utiliser EAX, EDX, etc. et voir comment réagit Delphi.

 

Cadres de pile

Soit une procédure dans laquelle nous déclarons des variables locales et les utilisons pour être certains que le compilateur les crée :

var
  T_essai: Array[1..100] of Byte;
asm
  ..
  mov byte ptr T_essai[51], al
  ..
end; //asm

 

Mettons un point d'arrêt vers le début de la procédure. Que soient ou non cochées les lignes Cadres de pile et Optimisation dans les options du compilateur, nous trouvons, avant notre propre code, le passage suivant :

push ebp
mov ebp, esp
add esp, -$64

( $64 font 100). Et en fin, inséré avant le RET  :

mov esp, ebp
pop ebp

Nous avons suffisamment présenté la notion de cadres de pile ; reportez-vous-y si ce code n'est pas clair. Visual C++ utilise LEAVE , pour le même résultat.

L'utilisation des registres suit les mêmes règles que pour les blocs asm  : préservation de ESP et EBP, dans le cas où le cadre de pile n'est pas créé par Delphi, de EBX, EDI et ESI. Les registres de segment ne doivent pas être modifiés du tout.

Penser à préserver également la FPU : ne pas réduire la précision, par exemple, sans la restaurer ensuite.

Passage de paramètres

Un des problèmes sera de passer les paramètres et de fournir un résultat. Delphi propose diverses conventions d'appel.

Conventions d'appel sous Delphi

Convention

Paramètres passés par

Sens

Nettoyage

Usage

register

Registre (3)

De gauche à droite

Procédure

Général (efficace)

pascal

Pile

De gauche à droite

Procédure

Obsolète (compatibilité)

cdecl

Pile

De droite à gauche

Appelant

C/C++ et autres

stdcall

Pile

De droite à gauche

Procédure

API Windows

safecall

Pile

De droite à gauche

Procédure

API Windows

Nous utiliserons essentiellement register , qui est la convention par défaut. Bien entendu, tout ne sera pas toujours passé par les registres. Seuls trois registres (EAX, ECX et EDX) sont utilisés. Si une fonction est une méthode de classe, EAX est un pointeur sur l'instance (self) ; il restera donc deux registres disponibles.

Attention : si un paramètre sur 8 ou 16 bits est passé par registre, il en utilise un en entier, mais les bits restants ne sont pas connus, pas mis à 0 par exemple.

Avant de bien s’imprégner de la psychologie de Delphi, il est bon, comme dans beaucoup d'autres cas, de tester à l'aide du débogueur. À ce sujet, un nom de paramètre sera toujours compilé comme s'il était le registre dans lequel il est arrivé et ce jusqu'à la fin de la procédure. Très dangereux, réserver l'utilisation des noms de paramètres au passage par la pile. En revanche, si vous exploitez temporairement ce nom, un coup d'œil au code désassemblé peut confirmer immédiatement le registre utilisé.

Résumé presque complet
figure 6.20 Résumé presque complet

Dans le cas des conventions Gauche vers Droite ( register et pascal ) et si les paramètres sont passés par la pile, l'ordre des paramètres est le suivant : ils sont empilés dans l'ordre normal de lecture du prototype de la procédure. Et vice-versa pour les conventions d'appel de type Droite vers Gauche.

Le comportement des variables locales est sur bien des points, par rapport à la pile, comparable à celui des paramètres. Rappelons encore une fois que le traitement de la pile par EBP est expliqué ailleurs dans cet ouvrage.

La contrainte de l'utilisation des registres sera de sauvegarder les données en début de procédure.

Delphi passe ses paramètres par valeur ou par référence. Dans le premier cas, la valeur sera copiée dans le registre ou sur la pile. Dans le second, c'est son adresse.

Retour du résultat

Vous pouvez bien entendu passer en tant que paramètre un pointeur vers un résultat attendu, voire plus souvent, comme les API Windows, un pointeur sur une structure. Mais vous pouvez également renvoyer directement un résultat, et un seul. Le principal avantage est syntaxique : le nom de la fonction sera alors traité dans les formules comme une variable du même type que le résultat (c'est une R-Value ).

Rappelons que sous Delphi, en Pascal, une variable Result  est créée automatiquement dans chaque fonction et que ce mot peut être remplacé par celui de la fonction :

function essai(a, b, c, d: Integer): Integer;
begin
  Result := a + b + c + d;
  essai  := a + b + c + d;
end;

Les deux lignes sont équivalentes. Cette variable n'existe pas réellement.

Au niveau assembleur, le résultat est renvoyé dans AL, AX et EAX, selon sa taille de 8, 16 out 32 bits. Plus EDX:EAX pour un int64 . Même les pointeurs, objets, classes, références de classes et pointeurs de procédures sont renvoyés dans EAX.

Les types dépendant de la FPU sont renvoyés dans SP(0).

Tout pourrait fonctionner de cette façon, mais… Mais il en va différemment pour tout ce qui ressemble à un tableau et ne rentre pas dans 32 bits : chaînes diverses, tableaux, structures, variants.

Voyons la fonction IntToBin qui sera employée à nouveau un peu plus tard dans ce chapitre, qui renvoie une chaîne de caractères. Si celle-ci était une variable ordinaire, elle aurait été déclarée, utilisée puis détruite dans l'appelant. Il est donc assez logique de faire réserver une chaîne par l'appelant, et qu'il en envoie les clés à la fonction.

Cette clé s'appelle Result et constitue un paramètre supplémentaire passé par l'appelant vers la fonction. C'est un pointeur vers une mémoire où l'appelant s'attend à trouver… un pointeur vers l'objet retourné.

Attention

En LHN, un pointeur est une variable

En langage de haut niveau, un pointeur est une variable . Plus exactement, un pointeur typé est une variable de type PTYPE, pointeur sur type  TYPE .

Donc, déclarer PCh comme pointeur sur chaîne consiste à réserver une mémoire de la taille d’une adresse, qui pourra contenir l’adresse d’une chaîne. À la déclaration, cette variable pointeur n’est pas initialisée, ce qui représente un danger. Il est courant de lui affecter la valeur particulière NULL .

Initialiser un pointeur consiste, comme pour toute variable, à lui affecter une valeur : ici, l’adresse valide d’une chaîne. Les fonctions qui allouent de la mémoire renvoient le plus souvent une adresse.

Deux pointeurs différents peuvent pointer vers le même objet, puisque deux variables différentes peuvent avoir la même valeur.

Dans PTYPE, TYPE peut être lui-même un pointeur. Un PPChar sera un pointeur vers un PChar , lui-même pointeur vers un Char . Un PPChar sera initialisé quand le PChar sera créé, mais pas nécessairement initialisé lui-même.

En assembleur, un pointeur est synonyme d'adresse complète ( SEGMENT:OFFSET ) et tient sur 4 ou 6 octets.

Procédons à une simulation. Une fonction F, qui prend deux paramètres a et b entiers, doit renvoyer un objet string . Au début du film, l'appelant MAIN s'apprête à invoquer F pour obtenir une string :

MAIN déclare (en cachette) une variable pointeur de type Pstring PstrA , sans l’initialiser, ou mieux en l’initialisant à NULL .

MAIN récupère l’adresse de cette variable PstrA , en fait un paquet avec a et b et envoie le tout vers F. PstrA est un paramètre caché, une initiative du compilateur.

F demande au système un objet string , d’une taille adéquate.

Le système obtempère et fournit en retour à F l’adresse de cet objet string .

F va consulter le paramètre caché, y trouve une adresse. Il utilise cette adresse pour sauver l’adresse de l’objet string . Il fait tout cela très simplement, par un :

mov dword ptr param_caché, adresse_de_la_string.

F accomplit son travail sur la_string , à l’aide certainement de a et b, puis rend la main à MAIN .

MAIN retrouve PstrA initialisé à l’adresse de la chaîne fournie par F. Il en est en quelque sorte propriétaire.

Pointeurs dans MAIN et F
figure 6.21 Pointeurs dans MAIN et F [the .swf]

Cette démarche est conforme à la notion de niveau lexical vue au chapitre sur la pile et les sous-programmes. Elle évite qu'une donnée créée entièrement dans F ne survive à la fonction. C'est MAIN qui initie la création de la string , mais il délègue une partie du travail restant à F. Nous sommes là dans une logique de langage de haut niveau ; c'est bien ce que nous souhaitons : intégrer notre code assembleur dans Delphi.

Voici une partie du code qui correspond exactement à cette opération :

mov  esi,ecx // Récupération de l'adresse de Result
.
.
.
mov eax, edx    // Longueur
// Allocation mémoire, pointeur sur la nouvelle AnsiString dans EAX
call System.@NewAnsiString
mov [esi], eax  // Adresse de la chaîne en sortie

 

6.5 Application : une fonction IntToBin

Nous souhaitons implémenter une fonction IntToBin équivalente à IntToHex . Voyons par   F1  la définition et le prototype de IntToHex .

Aide de IntToHex
figure 6.22 Aide de IntToHex

La fonction comporte deux versions. Elle est surchargée (mot clé overload ), le compilateur choisira la version en fonction de l’argument. À l’utilisation, ce sera transparent pour le programmeur, qui utilisera la même syntaxe pour tout entier du Byte à l’ Int64 . Oublions pour l’instant les Int64 .

Le but est de proposer une fonction qui s’utilise exactement comme IntToHex . D’où son prototype :

function
 IntToBin(Value: Integer; Nibbles: Integer = 8): 
string
;

Le paramètre Nibbles est le même que le Digits de IntToHex  ; il représente un nombre de paquets de 4 bits. Nous avons ajouté une valeur par défaut, 8, qui sera utilisée si ce second paramètre est omis.

Si nous invoquons IntToHex avec un Byte (8 bits) en premier paramètre, le compilateur transtypera sans problème. Il le fera également, sans que nous ayons à nous en occuper, pour IntToBin . C’est pour cela qu’il est un tout petit peu plus compliqué d’implémenter une taille par défaut de Nibbles égale à la taille de l’opérande réel. Il suffit en fait d’écrire une version (overload) par taille d’opérande, chacune faisant le transtypage et affectant la bonne valeur à Nibbles avant d’invoquer la version 32 bits :

function
 IntToBin(Value: Byte; Nibbles: Integer = 2): 
string
; 
overload
;
begin
Result := IntToBin(Integer(Value), Nibbles);
end
;

Il faudra une surcharge par type, signé et non signé.

Le cœur du problème reste l’écriture de la version Integer . Avant d’aller plus loin dans l’analyse, voyons comment nous allons transformer notre entier en chaîne représentant sa valeur binaire. Celle-ci est initialisée par des caractères ‘0’ (code ASCII 30h , pas valeur 0). Par décalages successifs (voir l’instruction de décalage SHR), nous lisons les bits un par un et positionnons éventuellement le caractère correspondant à ‘1’ , code ASCII 31h .

// eax contient l’adresse du premier octet de la chaîne
mov edx, Value
mov ecx, Nibbles
shl ecx, 2     // nombres de bits (Nibbles * 4)
@1:
shr edx, 1
adc byte ptr[eax + ecx -1], 0
dec ecx
jnz @1

SHR décale les bits en faisant tomber à chaque fois le poids faible dans le CF (Carry Flag). Et justement, ADC effectue une addition comme ADD, mais y ajoutant la valeur du CF. C’est bien entendu prévu au départ pour gérer une retenue. Le résultat est qu’il sera ajouté 1 au caractère pointé par EAX + ECX - 1 si CF est à 1. Or, le code du 0 ( 30h ) plus 1 donne 31h , soit le code du caractère 1.

Le tout est de bien appliquer cette opération au bon caractère. Nous devons décrémenter ECX juste avant le JNZ, puisqu’il positionne le CF comme le SHR. D’où le -1. Nous pouvons gagner un peu en performance (?) au détriment d’un point de lisibilité :

// eax contient l’adresse du premier octet de la chaîne
dec eax
mov edx, Value
mov ecx, Nibbles
shl ecx, 2     // nombres de bits (Nibbles * 4)
@1:
shr edx, 1
adc byte ptr[eax + ecx], 0
dec ecx
jnz @1

Nous avons parlé de perte de lisibilité, parce qu’une fois décrémenté, EAX ne signifie plus rien. Il y a donc un danger, en cas de modification du code, de la confondre avec l’adresse du début de la chaîne.

Rappelons que nous nous sommes imposés le prototype de la fonction, qui doit renvoyer une chaîne. Pas question donc de lui passer cette chaîne par adresse. Nous verrons un peu plus loin que c'est malgré tout ce que fait Delphi.

En Delphi, les types chaîne sont multiples  : ShortString et AnsiString en particulier. string est un type générique, qui est transformé à la compilation en un type ou un autre. Avec AnsiString , Delphi alloue dynamiquement la mémoire. En C++Builder, AnsiString est une classe. Pour cette première application, nous ne savons pas coder ceci entièrement en assembleur :

function IntToBin(Value:Integer; Nibbles:Integer = 8):string;overload;
var
S: AnsiString;
i: Integer;
begin
if Nibbles < 2 then Nibbles := 2;
if Nibbles > 8 then Nibbles := 8;
S := '0000';
for i := 2 to Nibbles do S := '0000' + S;
asm
lea eax, S     // pointeur vers pointeur vers S dans eax
mov eax, [eax] // adresse de S dans eax
dec eax        // attention: eax ne contient plus l'adresse de S
mov edx, Value
mov ecx, Nibbles
shl ecx, 2     // multiplication: nombres de bits = (Nibbles * 4)
@1:
shr edx, 1
adc byte ptr[eax + ecx], 0
dec ecx
jnz @1
end; //asm
IntToBin := S;// renvoie la chaîne
end; //IntToBin

C’est la ligne du for..to qui alloue dynamiquement de la mémoire et qui nous pose problème pour coder entièrement la fonction en assembleur.

Le code d’essai, dans le code de traitement du bouton Action 1, est par exemple :

procedure Tskel_mainForm.BtnAction1Click(Sender: TObject);
var
b: Byte;
begin
  b := $84;
  MemoSortie.Lines.Add(IntToBin(b, 8));
  MemoSortie.Lines.Add(IntToBin(b, 4));
  MemoSortie.Lines.Add(IntToBin($740F740F));
end;

Testez, en faisant varier les valeurs et le type de  b . À l'aide du débogueur, vous devez sans trop de peine retrouver toutes les variables dans la pile et/ou les registres.

Examinons, en mettant un BP sur la ligne S = ‘0000’ ; , le code généré par Delphi.

La boucle for..to dans le débogueur
figure 6.23 La boucle for..to dans le débogueur

La chaîne est d’abord fabriquée par  @LStrLAsg , puis par  @LStrCat3 .

La première assigne une chaîne constante à S. Soyons plus précis : à l'appel de la fonction, EAX pointe vers une mémoire contenant l'adresse de la chaîne. C'est le contenu de cette mémoire qui va être modifié, pour contenir l'adresse d'une chaîne initialisée à '0000' .

La seconde concatène deux chaînes, S et une constante, résultat dans S.

Tout coder en assembleur ne semble effectivement pas insurmontable, pour peu que nous puissions utiliser les bonnes fonctions. En effet, c'est à nous de créer une chaîne, à une nuance importante près, que nous allons voir.

Notre première idée est de cloner le désassemblage de la version mixte. L'aide étant muette sur  @LStrLAsg et  @LStrCat3 (oui, nous avons ôté les  @ pour les recherches !) et n'ayant aucune autre documentation sur le sujet à notre disposition, nous étudions les paramètres d'appel des deux fonctions, en jouant du débogueur.

Cette méthode est dangereuse, nous cherchons donc sur notre disque quels fichiers peuvent contenir les mots  LStrLAsg et  LStrCat3 . Nous arrivons sur le fichier source system.pas , malheureusement d'origine C++Builder (voir le CD-Rom pour se procurer ce fichier). Ce sera notre seul fichier d'aide pour un grand nombre de fonctions. Par bonheur, il est sèchement mais suffisamment commenté. Voici une des fonctions, vue dans notre éditeur de texte favori.

Source de LStrLAsg
figure 6.24 Source de LStrLAsg

La partie active de la fonction est la ligne XCHG EDX, [EAX] . Mais le reste est passionnant, c'est la gestion d'un compteur de référence sur la source. Voir à ce sujet le commentaire des instructions XCHG et LOCK . Le fichier system.pas est d'une lecture très enrichissante, en particulier pour l'étude de BASM.

Cette démarche de recherche d'informations est classique.

LStrLAsg et LStrCat3 font donc partie de l'unité System . Pour les utiliser, il faudra adopter la syntaxe :

call system.@LStrLAsg
call system.@LStrCat3

 

Nous avons écrit la version finale, dans une fonction en assembleur pur, en ajoutant la possibilité de grouper les bits par paquets de 4. Par curiosité, nous avons mesuré la vitesse des différentes versions.

Nombre de ces solutions chronométrées sont fournies sur le CD-Rom. Des lignes mises en commentaires montrent les essais successifs. Puisque le code de chronométrage est déjà en place, si vous commencez ici votre apprentissage, c'est certainement une très bonne idée de partir de cette base et de l'améliorer ; c'est parfaitement faisable, et dans de bonnes proportions.

Pour avoir un élément de comparaison, nous avons écrit une version Delphi :

function IntToBinD(Valeur: Integer; Nibbles: Integer = 8): string;
var
  S: string;
  i: Integer;
begin
  if Nibbles < 2 then Nibbles := 2;
  if Nibbles > 8 then Nibbles := 8;
  S := '0000';
  for i := 2 to Nibbles do S := '0000 ' + S;
  for i := Nibbles * 4 downto 1 do begin
    if Valeur mod 2 = 1 then S[i + ((i - 1) div 4)] := '1';
    Valeur := Valeur shr 1;
  end;
  Result := S;
end;

Outre le fait qu'elle soit relativement illisible, cette fonction est caractérisée par le fait que ses performances dépendent beaucoup du nombre de nibbles à traiter : de 6 000 cycles pour 8, nous passons à 2 200 pour 2  nibbles .

Cette quasi-proportionnalité nous a fait penser que la majorité du temps était passé dans les fonctions système, donc dans la fabrication de la chaîne initiale S = '0000 ....... 0000' . La fonction Delphi est donc certainement elle-même améliorable, sans passer par l'assembleur.

Voici la dernière mouture de la version assembleur :

function IntToBinA1(Valeur:Integer; Nibbles:Integer = 8):string;register;overload;
asm
// Sauvegarde registres
push edi
push esi
push ebx
 
push eax     // Valeur à afficher
mov  esi,ecx // Récupération de l'adresse de Result
 
{ Correction de Nibbles
  if Nibbles < 2 then Nibbles := 2;
  if Nibbles > 8 then Nibbles := 8;}
mov ebx, 2
cmp edx, ebx
cmovl edx, ebx
shl ebx, 2
cmp edx, ebx
cmovg edx, ebx            
 
{ Calcul de la longueur = (Nibbles * 5) - 1}
mov ecx, edx
shl edx, 2   // fois 4
add edx, ecx // plus 1 fois
dec edx      // moins 1
push edx
 
mov eax, edx    // Longueur
call System.@NewAnsiString
mov [esi], eax  // Adresse de la chaîne en sortie
 
dec eax  //!! eax ne contient plus l'adresse de la chaîne
 
pop ecx  // Récup Longueur
pop edx  // Récup Valeur à afficher
 
mov ebx, 4
@boucle2:
mov byte ptr[eax + ecx], '0'
shr edx, 1
adc byte ptr[eax + ecx],  0
dec ecx
jz @finalisation
dec ebx
jnz @boucle2
mov byte ptr[eax + ecx], ' '
dec ecx
mov ebx, 4
jmp @boucle2
 
 
@finalisation:
pop ebx
pop esi
pop edi
end;

En plus des tests de rapidité, nous avons testé à l'aide de la procédure :

procedure Tskel_mainForm.BtnAction2Click(Sender: TObject);
var
  b: Integer;
begin
  b := $84;
  MemoSortie.Lines.Add(IntToBinA1(b, 2));
  MemoSortie.Lines.Add(IntToBinA1($740F740F));
  MemoSortie.Lines.Add(IntToBinA1($F40F, 4));
end;

Ce qui donne à l’écran.

Test des fonctions
figure 6.25 Test des fonctions

Le point clé du gain en rapidité est qu'il n'y a qu'une seule utilisation de fonctions système. Nous arrivons à une performance intéressante, de l’ordre de 500 cycles.

Une optimisation supplémentaire serait certainement possible, mais presque inutile, le temps étant maintenant largement passé en appels de fonctions.

Étudions rapidement ce listing. La procédure est encadrée de 3  PUSH et 3  POP de préservation de registres. Après ces  PUSH , un quatrième pour préserver temporairement le paramètre Valeur , qui est transmis dans EAX.

À ce sujet, push Valeur aurait fonctionné, mais il est impératif de s'en abstenir. En effet, quoi qu'il advienne de EAX, Valeur désignera toujours ce registre pour la durée de la procédure ; donc le nom de variable est inutilisable. Cette syntaxe peut néanmoins avoir une utilisation détournée, en cas de doute sur la façon dont une variable est transmise : il suffit de placer un push nom_variable et un point d'arrêt, et le tour est joué.

La ligne suivante sauve le paramètre Result , transmis dans ECX.

À l'entrée de la fonction, ECX pointe vers l'adresse de Result . Nous sauvons cette valeur dans ESI, dans un premier temps pour pouvoir utiliser ECX.

Nous corrigeons ensuite le paramètre Nibbles , en utilisant des CMOVcc . Le shl ebx, 2 fait simplement passer EBX de 2 à 8. Ce paramètre corrigé sert ensuite pour calculer la longueur de la chaîne ( Nibbles * 5) - 1 . Nous empilons le résultat et notons notre sommet de pile sur un bout de papier : Nibbles au-dessus, ensuite Valeur , et enfin 3 valeurs à dépiler, EBX, ESI et EDI dans cet ordre.

Ce type de stockage sur la pile au petit bonheur doit être de très faible amplitude, c’est-à-dire que la récupération doit intervenir très rapidement, sous peine d’erreurs. Méfions-nous en particulier des cas où le POP correspondant à un PUSH se trouverait dans une branche conditionnelle du programme. Mais nous sommes là dans des méthodes qui nous sont personnelles, des choix d’organisation que nous devons faire seuls.

Nous invoquons ensuite la fonction NewAnsiString de l’unité système. Elle prend une longueur en paramètre dans EAX et renvoie, toujours dans EAX, une chaîne, par son adresse.

Delphi nous transmet Result dans ECX (sauvé dans ESI). Result contient une adresse. À cette adresse, [Result] , doit se trouver celle de la chaîne résultat. D’où cette ligne capitale :

mov [esi], eax  // Communication de l'adresse de la chaîne

Nous venons de mettre l’adresse d’une chaîne à l’endroit pointé par Result .

C’était la dernière difficulté. Ensuite, sur la base de l’adresse de la chaîne, de sa longueur, d’un compteur annexe par 4 pour placer les espaces séparateurs, la chaîne sera fabriquée, dans une de ces boucles dix fois plus longues à détailler qu’à écrire. Il n’est pas impossible que l’auteur de ces quelques lignes ait procédé par quelques essais, plus que par une belle analyse préalable. Méthode qu’il ne peut en aucun cas conseiller. Mais bon…

6.6 Autres environnements

Pour les environnements C++Builder et Visual Studio 6, le principe reste le même. La grande différence se situe au niveau de la possibilité, somme toute rare, qu'a Delphi de traiter une fonction ou une procédure entièrement en assembleur.

Visual Studio .NET (ou plutôt VC++ 7) se comporte strictement comme Visual Studio 6.0 sur le plan de l'assembleur, moyennant une précision importante. Sans entrer dans les détails de la définition du code managé dans Visual Studio .NET, il suffit de savoir que ce mode, à l’opposé du code natif, compile vers un code intermédiaire MSIL (Microsoft Intermediate Language). Ce code sera ensuite compilé à la volée vers du code machine spécifique au processeur par un compilateur JIT (Just In Time). C’est à cette couche que sera déléguée en particulier la gestion de la mémoire. Ceci dans le but d’autoriser une compatibilité interlangage, voire inter-plate-forme. Dans ces conditions, il est clair que mode managé et assembleur ne seront pas amis. Effectivement, le mot clé __asm force la compilation en mode natif.

Dans ces conditions, l’assembleur n’intéresse Visual Studio .NET que dans le cadre de VC++ en mode natif, cadre dans lequel il n’y a pas de différences significatives avec Visual C++ 6.0. C’est d’ailleurs indifféremment sous .NET et 6.0 que nous avons mené nos tests.

Ajoutons que le mot assembly désigne une fonctionnalité fondamentale de cet environnement qui n’a rien à voir avec le langage assembleur. Cette précision est utile pour les recherches dans l'aide ou le MSDN en ligne.

Vous trouverez dans la suite de l'ouvrage et sur le CD-Rom quelques exemples de codes sous les environnements Visual Studio et C++Builder.

Ce n'est pas une règle, mais un état de fait, presque culturel : la façon habituelle de traiter l'assembleur est différente dans les trois produits :

  Avec Delphi, BASM est utilisé à fond de ses possibilités, c'est une composante presque nécessaire du langage.

  Le programmeur Visual C++ va plus opter pour la compilation de fonctions assembleur à l'aide de ml.exe . Ce qui n'interdira pas d'utiliser quelques __asm pour une ligne ou deux de temps en temps.

  Sous C++Builder, la démarche sera entre les deux, mais sans l'ensemble des facilités offertes par BASM. En fait, le choix pourra dépendre de l'acquis du programmeur et du code déjà à sa disposition. Nous nous imaginons très bien intégrer peu à peu du code en ligne, puis en final compiler un module indépendant rassemblant ces fonctionnalités à l'aide de TASM32.

Mais ce ne sont que des tendances. Chacun est libre de sa façon de travailler. Pour terminer, signalons que si l'assembleur est lié à la machine cible, une bonne routine doit toujours pouvoir être portée d'un environnement de développement vers un autre sur la même plate-forme.

Envoyer un message à l'auteur

se connecter pour pouvoir écrire

Ce site est créé et maintenu (laborieusement) par Pierre Maurette
hébergé par 1 & 1