C / C ++ retournant struct par valeur sous le capot

(Cette question est spécifique à l’architecture de ma machine et aux conventions d’appel, Windows x86_64)

Je ne me souviens pas exactement de l’endroit où j’avais lu ceci, ou si je l’avais bien rappelé, mais j’avais entendu dire que lorsqu’une fonction doit renvoyer une structure ou un object par sa valeur, elle le rax dans rax (si l’object peut tenir dans la largeur de registre de 64 bits) ou se faire passer un pointeur sur l’object résultant (je devine alloué dans le cadre de stack de la fonction appelante) dans rcx , où il ferait toute l’initialisation habituelle, puis un mov rax, rcx pour le voyage de retour. C’est-à-dire quelque chose comme

 extern some_struct create_it(); // implemented in assembly 

aurait vraiment un paramètre secret comme

 extern some_struct create_it(some_struct* secret_param_pointing_to_where_i_will_be); 

Est-ce que ma mémoire m’a bien servi ou est-ce que je me trompe? Comment les objects volumineux (c’est-à-dire plus larges que la largeur du registre) sont-ils retournés par la valeur des fonctions?

Voici un simple déassembly d’un code examinant ce que vous dites

 typedef struct { int b; int c; int d; int e; int f; int g; char x; } A; A foo(int b, int c) { A myA = {b, c, 5, 6, 7, 8, 10}; return myA; } int main() { A myA = foo(5,9); return 0; } 

et voici le désassemblage de la fonction foo, et la fonction principale l’appelant

principale:

 push ebp mov ebp, esp and esp, 0FFFFFFF0h sub esp, 30h call ___main lea eax, [esp+20] ; placing the addr of myA in eax mov dword ptr [esp+8], 9 ; param passing mov dword ptr [esp+4], 5 ; param passing mov [esp], eax ; passing myA addr as a param call _foo mov eax, 0 leave retn 

foo:

 push ebp mov ebp, esp sub esp, 20h mov eax, [ebp+12] mov [ebp-28], eax mov eax, [ebp+16] mov [ebp-24], eax mov dword ptr [ebp-20], 5 mov dword ptr [ebp-16], 6 mov dword ptr [ebp-12], 7 mov dword ptr [ebp-8], 9 mov byte ptr [ebp-4], 0Ah mov eax, [ebp+8] mov edx, [ebp-28] mov [eax], edx mov edx, [ebp-24] mov [eax+4], edx mov edx, [ebp-20] mov [eax+8], edx mov edx, [ebp-16] mov [eax+0Ch], edx mov edx, [ebp-12] mov [eax+10h], edx mov edx, [ebp-8] mov [eax+14h], edx mov edx, [ebp-4] mov [eax+18h], edx mov eax, [ebp+8] leave retn 

Voyons maintenant ce qui vient de se passer. Ainsi, lorsqu’on a appelé foo, les parameters ont été transmis de la manière suivante: 9 correspondait à l’adresse la plus élevée, puis 5, l’adresse où commence le myA

 lea eax, [esp+20] ; placing the addr of myA in eax mov dword ptr [esp+8], 9 ; param passing mov dword ptr [esp+4], 5 ; param passing mov [esp], eax ; passing myA addr as a param 

Dans foo il y a un myA local stocké dans le cadre de la stack, puisque la stack est en train de descendre, l’adresse la plus basse de myA commence dans [ebp - 28] , le décalage -28 pourrait être causé par des alignements de structure, donc je devine. la taille de la structure devrait être de 28 octets ici et non de 25 comme prévu. et comme nous pouvons le voir dans foo après que le myA local de foo été créé et rempli de parameters et de valeurs immédiates, il est copié et réécrit à l’adresse de myA passée de main (c’est le sens réel du retour par valeur)

 mov eax, [ebp+8] mov edx, [ebp-28] 

[ebp + 8] est l’adresse où l’adresse de main::myA été enregistrée (l’adresse mémoire va vers le haut, donc ebp + ancien ebp (4 octets) + adresse de retour (4 octets)) à l’ensemble ebp + 8 pour atteindre le premier octet de main::myA , comme dit précédemment foo::myA est stocké dans [ebp-28] mesure que la stack descend

 mov [eax], edx 

place foo::myA.b dans l’adresse du premier membre de données de main::myA qui est main::myA.b

 mov edx, [ebp-24] mov [eax+4], edx 

place la valeur qui réside dans l’adresse de foo::myA.c dans edx, et place cette valeur dans l’adresse de main::myA.b + 4 octets, ce qui est main::myA.c

comme vous pouvez voir ce processus se répète à travers la fonction

 mov edx, [ebp-20] mov [eax+8], edx mov edx, [ebp-16] mov [eax+0Ch], edx mov edx, [ebp-12] mov [eax+10h], edx mov edx, [ebp-8] mov [eax+14h], edx mov edx, [ebp-4] mov [eax+18h], edx mov eax, [ebp+8] 

ce qui prouve fondamentalement que lors du renvoi d’une structure par val, qui ne peut pas être placé en tant que paramètre, il se produit que l’adresse de l’endroit où la valeur de retour doit résider est transmise en tant que paramètre à la fonction et à l’intérieur de la fonction appelée les valeurs de la structure renvoyée sont copiées dans l’adresse transmise en tant que paramètre …

J’espère que cet exemple vous a aidé à visualiser un peu mieux ce qui se passe sous le capot 🙂

MODIFIER

J’espère que vous avez remarqué que mon exemple utilisait l’assembleur 32 bits et je sais que vous avez posé une question à propos de x86-64, mais je ne suis actuellement pas en mesure de désassembler le code sur une machine 64 bits. J’espère donc que vous prenez mon mot à ce sujet. que le concept est exactement le même pour les 64 bits et les 32 bits et que la convention d’appel est presque la même

C’est tout à fait correct. L’appelant passe un argument supplémentaire qui correspond à l’adresse de la valeur de retour. Normalement, ce sera sur le cadre de la stack de l’appelant, mais il n’y a aucune garantie.

Les mécanismes précis sont spécifiés par la plateforme ABI, mais ce mécanisme est très courant.

Divers commentateurs ont laissé des liens utiles avec la documentation pour les conventions d’appel. Je vais donc en hisser quelques-uns dans cette réponse:

  • Article Wikipedia sur les conventions d’appel x86

  • La collection de ressources d’optimisation d’Agner Fog, y compris un résumé des conventions d’appel (lien direct vers un document PDF de 57 pages .)

  • Documentation MSDN (Microsoft Developer Network) sur les conventions d’appel .

  • StackOverflow x86 tag wiki contient de nombreux liens utiles.