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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
int SumOf( int iParamOne, int iParamTwo, int iParamThree )
{
int iLocal = iParamOne + iParamTwo + iParamThree;
return iLocal;
}
int main( int argc, char** argv )
{
int iValOne = 1;
int iValTwo = 2;
int iValThree = 4;
int iResult = SumOf( iValOne, iValTwo, iValThree );
return 0;
}
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) :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
7: int main( int argc, char** argv )
8: {
00401280 push ebp
00401281 mov ebp,esp
00401283 sub esp,50h
00401286 push ebx
00401287 push esi
00401288 push edi
9: int iValOne = 1;
00401289 mov dword ptr [ebp-4],1
10: int iValTwo = 2;
00401290 mov dword ptr [ebp-8],2
11: int iValThree = 4;
00401297 mov dword ptr [ebp-0Ch],4
12: int iResult = SumOf( iValOne, iValTwo, iValThree );
0040129E mov eax,dword ptr [ebp-0Ch]
004012A1 push eax
004012A2 mov ecx,dword ptr [ebp-8]
004012A5 push ecx
004012A6 mov edx,dword ptr [ebp-4]
004012A9 push edx
004012AA call 00401127
004012AF add esp,0Ch
004012B2 mov dword ptr [ebp-10h],eax
13: return 0;
004012B5 xor eax,eax
14: }
004012B7 pop edi
004012B8 pop esi
004012B9 pop ebx
004012BA mov esp,ebp
004012BC pop ebp
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 :
16.
17.
18.
19.
20.
21.
22.
23.
24.
12: int iResult = SumOf( iValOne, iValTwo, iValThree );
0040129E mov eax,dword ptr [ebp-0Ch]
004012A1 push eax
004012A2 mov ecx,dword ptr [ebp-8]
004012A5 push ecx
004012A6 mov edx,dword ptr [ebp-4]
004012A9 push edx
004012AA call 00401127
004012AF add esp,0Ch
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é :
IV. Accéder aux paramètres▲
Voici le code assembleur de la fonction SumOf() :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
1: int SumOf( int iParamOne, int iParamTwo, int iParamThree )
2: {
00401250 push ebp
00401251 mov ebp,esp
00401253 sub esp,44h
00401256 push ebx
00401257 push esi
00401258 push edi
3: int iLocal = iParamOne + iParamTwo + iParamThree;
00401259 mov eax,dword ptr [ebp+8]
0040125C add eax,dword ptr [ebp+0Ch]
0040125F add eax,dword ptr [ebp+10h]
00401262 mov dword ptr [ebp-4],eax
4: return iLocal;
00401265 mov eax,dword ptr [ebp-4]
5: }
00401268 pop edi
00401269 pop esi
0040126A pop ebx
0040126B mov esp,ebp
0040126D pop ebp
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) :
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.





