Programme d'étude sur le C++ bas niveau n° 4 : plus de Pile

Alex Darby

Traducteur

ram-0000

L'objectif de cette série d'articles d'Alex Darby sur la programmation « bas-niveau » est de permettre aux développeurs ayant déjà des connaissances de la programmation C++ de mieux comprendre comment leurs programmes sont exécutés en pratique. Ce quatrième article donne les détails de fonctionnement de la Pile lorsque l'on passe plus d'un paramètre à une fonction.

Retrouver l'ensemble des articles de cette série sur la page d'index.

N'hésitez pas à commenter cet article ! Commentez

- Titre : Programme d'étude sur le C++ bas niveau n° 4 : plus de Pile

- Auteur : Alex Darby

- Traducteur : ram-0000

- Parution : 14 janvier 2013

- Mise à jour : 26 juin 2020

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

Sommaire

- I. Introduction

- II. Plus d'un paramètre à une fonction

- III. L'appel de SumOf()

- IV. Accéder aux paramètres

- V. En résumé

- VI. La prochaine fois

- VII. Remerciements

I - Introduction

Bienvenue dans ce quatrième article « Programme d'étude sur le C++ bas niveau, plus de Pile » !

L'article précédent était un mammouth et il m'a pris beaucoup de temps donc, cet article sera beaucoup plus court et, par conséquent, couvrira moins de choses. Nous allons voir plus spécifiquement ce qui arrive lorsque l'on passe plus d'un paramètre avec un code en assembleur x86 non optimisé utilisant la convention d'appel stdcall.

Cet article suppose que vous avez lu le précédent article au sujet de la Pile ou savez déjà comment fonctionne la Pile avec un assembleur simple x86.

Si vous avez manqué ces articles, voici les liens :

- Programme d'étude sur le C++ bas niveau ;

- Programme d'étude sur le C++ bas niveau n° 2 : les types de données ;

- Programme d'étude sur le C++ bas niveau n° 3 : la Pile.

J'ai également trouvé un excellent article sur l'ABI IBM PowerPC qui explique en détail (et au niveau assembleur) comment la Pile est utilisée avec les processeurs basés sur le PowerPC car c'est plus différent du x86 que je ne l'imaginais. Vous pouvez trouver cet article utile si vous travaillez sur les consoles de la génération actuelle et désirez comprendre comment ils utilisent la Pile, les appels de fonctions et le passage de paramètres.

II - Plus d'un paramètre à une fonction

Comme précédemment, j'utiliserai une application win32 en mode console construite avec l'option « new project » dans VS2010 avec les options par défaut. Le désassembleur que nous examinerons est généré par la configuration « debug », ce qui génère du code assembleur x86 non optimisé et simple utilisant la convention d'appel stdcall.

Le seul changement apporté est la désactivation de l'option « Basic Runtime Checks » pour générer un assembleur plus lisible (et nettement plus rapide…). Regardez l'article précédent pour voir comment faire.

Nous allons modifier le programme très simple de l'article précédent de manière à ce que la fonction appelée utilise trois paramètres.

1. int SumOf( int iParamOne, int iParamTwo, int iParamThree )

2. {

3. int iLocal = iParamOne + iParamTwo + iParamThree;

4. return iLocal;

5. }

7. int main( int argc, char** argv )

8. {

9. int iValOne   = 1;

10. int iValTwo   = 2;

11. int iValThree = 4;

12. int iResult   = SumOf( iValOne, iValTwo, iValThree );

13. return 0;

14. }

Et voici le code assembleur généré pour la fonction main() (comme précédemment, les adresses des instructions sont très probablement différentes pour vous) :

1. 7: int main( int argc, char** argv )

2. 8: {

3. 00401280  push        ebp

4. 00401281  mov         ebp,esp

5. 00401283  sub         esp,50h

6. 00401286  push        ebx

7. 00401287  push        esi

8. 00401288  push        edi

9. 9:     int iValOne        = 1;

10. 00401289  mov         dword ptr [ebp-4],1

11. 10:     int iValTwo        = 2;

12. 00401290  mov         dword ptr [ebp-8],2

13. 11:     int iValThree    = 4;

14. 00401297  mov         dword ptr [ebp-0Ch],4

15. 12:     int iResult        = SumOf( iValOne, iValTwo, iValThree );

16. 0040129E  mov         eax,dword ptr [ebp-0Ch]

17. 004012A1  push        eax

18. 004012A2  mov         ecx,dword ptr [ebp-8]

19. 004012A5  push        ecx

20. 004012A6  mov         edx,dword ptr [ebp-4]

21. 004012A9  push        edx

22. 004012AA  call        00401127

23. 004012AF  add         esp,0Ch

24. 004012B2  mov         dword ptr [ebp-10h],eax

25. 13:     return 0;

26. 004012B5  xor         eax,eax

27. 14: }

28. 004012B7  pop         edi

29. 004012B8  pop         esi

30. 004012B9  pop         ebx

31. 004012BA  mov         esp,ebp

32. 004012BC  pop         ebp

33. 004012BD  ret

III - L'appel de SumOf()

Comme nous avons vu dans l'article précédent, nous pouvons maintenant ignorer sans risque le préambule et le postambule (respectivement, le prologue et l'épilogue) de la fonction (lignes 3 à 8 et 28 à 31) qui créent et détruisent le « Stack Frame » car nous savons maintenant qu'ils ne sont pas impliqués dans le passage des paramètres à SumOf().

Un rapide coup d'œil au code assembleur qui initialise les variables locales nous indique que iValOne, iValTwo et iValThree sont respectivement stockés à l'emplacement [ebp -4], [ebp -8] et [ebp-0Ch].

Le code assembleur responsable de l'appel de la fonction et de la récupération de la valeur du retour est celui-ci :

1. 12:     int iResult        = SumOf( iValOne, iValTwo, iValThree );

2. 0040129E  mov         eax,dword ptr [ebp-0Ch]

3. 004012A1  push        eax

4. 004012A2  mov         ecx,dword ptr [ebp-8]

5. 004012A5  push        ecx

6. 004012A6  mov         edx,dword ptr [ebp-4]

7. 004012A9  push        edx

8. 004012AA  call        00401127

9. 004012AF  add         esp,0Ch

10. 004012B2  mov         dword ptr [ebp-10h],eax

Comme avec un seul argument, les copies des paramètres de la fonction sont empilées (push) sur la Pile - notez bien qu'ils sont empilés dans l'ordre inverse par rapport à la liste des paramètres attendus dans le code C++.

La dernière chose à noter, c'est que juste après l'instruction call de la ligne 22 (c'est-à-dire immédiatement avant que l'assembleur pour SumOf () ne soit exécuté), la copie de iValOne qui a été empilée sur la Pile à la ligne 21 est maintenant à [esp +4] parce que l'instruction call empile aussi l'adresse de retour de la fonction sur la Pile.

Juste au cas où, voici à quoi ressemble la Pile immédiatement après que la ligne 22 a été exécutée mais avant que tout code de la fonction SumOf() ne soit exécuté :

La Pile après la ligne 22 mais avant le début de SumOf()

IV - Accéder aux paramètres

Voici le code assembleur de la fonction SumOf() :

1. 1: int SumOf( int iParamOne, int iParamTwo, int iParamThree )

2. 2: {

3. 00401250  push        ebp

4. 00401251  mov         ebp,esp

5. 00401253  sub         esp,44h

6. 00401256  push        ebx

7. 00401257  push        esi

8. 00401258  push        edi

9. 3:     int iLocal = iParamOne + iParamTwo + iParamThree;

10. 00401259  mov         eax,dword ptr [ebp+8]

11. 0040125C  add         eax,dword ptr [ebp+0Ch]

12. 0040125F  add         eax,dword ptr [ebp+10h]

13. 00401262  mov         dword ptr [ebp-4],eax

14. 4:     return iLocal;

15. 00401265  mov         eax,dword ptr [ebp-4]

16. 5: }

17. 00401268  pop         edi

18. 00401269  pop         esi

19. 0040126A  pop         ebx

20. 0040126B  mov         esp,ebp

21. 0040126D  pop         ebp

22. 0040126E  ret

Nous pouvons voir que le prologue de la fonction empile ebp, ce qui décale esp de quatre octets supplémentaires, puis copie esp dans ebp - si bien qu'à la ligne 4, la valeur de iValOne est maintenant à [ebp+8].

Voici le nouvel état de la Pile juste après le prologue de la fonction (c'est-à-dire après la ligne 8) :

État de la pile juste après le prologue de SumOf()

En regardant les lignes 10 à 12, nous pouvons voir que le code accède aux paramètres comme ceci :

- iParamOne (iValOne) à partir de [ebp+8] ;

- iParamTwo (iValTwo) à partir de [ebp+0Ch] ;

- iParamThree (iValThree) à partir de [ebp+10h].

Ce qui, sans surprise, est exactement là où la fonction main() avait empilé les valeurs sur la Pile juste après le prologue de la fonction main().

Maintenant, nous comprenons pourquoi les paramètres de la fonction sont empilés par la fonction main() sur la Pile dans l'ordre inverse - parce que les fonctions appelées s'attendent à trouver ces paramètres dans le bon ordre à partir de [ebp+8] en incrémentant le décalage à partir de ebp pour chacun des paramètres.

Comme auparavant, la valeur de retour (iLocal, stockée à [ebp -4]) est copiée dans eax avant le code de l'épilogue de la fonction de manière à le retourner à la fonction main() et comme nous savons comment l'épilogue et le retour fonctionnent depuis l'article précédent nous savons tout sur les appels stdcall avec plusieurs paramètres. Quelle joie !

V - En résumé

Nous avons vu en détail comment est utilisée la Pile pour appeler des fonctions stdcall simples et non optimisées par le compilateur en assembleur x86. Cela devrait vous suffire pour aller trainer dans la fenêtre de désassemblage avec une assez bonne idée de quelles parties du code assembleur de chaque fonction sont le plus susceptibles d'être pertinentes.

Pour plus d'information et pour vous montrer les différences d'utilisation de la Pile (bien que basiquement, cela soit le même principe), voici un lien sur la quatrième partie d'une série d'articles du site IBM qui traite de l'assembleur du PowerPC et en particulier l'ABI 64bits : Assembly language for Power Architecture, Part 4: Function calls and the PowerPC 64-bit ABI.

Selon toute vraisemblance, vous aurez besoin de lire les trois premiers articles pour donner un sens au quatrième, mais ce quatrième article est l'endroit où se trouvent la plupart des informations juteuses.

VI - La prochaine fois

La prochaine fois, je vais examiner la convention d'appel thiscall pour x86 utilisée par les fonctions membres C++ (quand le pointeur this est passé dans ecx) et nous regarderons aussi comment la convention d'appel fastcall utilise la Pile, son nom semble excitant.

Au fait, Joyeux Noël !

VII - Remerciements

Cet article est une traduction autorisée de « C / C++ Low Level Curriculum Part 4: More Stack » par Alex Darby.

Je tiens à remercier ClaudeLELOUP pour la relecture orthographique de cette traduction.