Je veux savoir comment passer des arguments à des fonctions en C fonctionne. Où les valeurs sont-elles stockées et comment et sont-elles récupérées? Comment fonctionne le passage d’arguments variadiques? Aussi, puisque c’est lié: quid des valeurs de retour?
J’ai une connaissance de base des registres de CPU et de l’assembleur, mais pas assez pour bien comprendre l’ASM que GCC crache de travers. Quelques exemples simples commentés seraient très appréciés.
Considérant ce code:
int foo (int a, int b) { return a + b; } int main (void) { foo(3, 5); return 0; }
La gcc foo.c -S
avec gcc foo.c -S
donne la sortie de l’assemblage:
foo: pushl %ebp movl %esp, %ebp movl 12(%ebp), %eax movl 8(%ebp), %edx leal (%edx,%eax), %eax popl %ebp ret main: pushl %ebp movl %esp, %ebp subl $8, %esp movl $5, 4(%esp) movl $3, (%esp) call foo movl $0, %eax leave ret
Donc, fondamentalement, l’appelant (dans ce cas main
) affecte d’abord 8 octets sur la stack pour prendre en charge les deux arguments, puis place les deux arguments sur la stack aux décalages correspondants ( 4
et 0
), puis l’instruction d’ call
est émise et transfère le contrôle à la routine foo
. La routine foo
lit ses arguments à partir des décalages correspondants sur la stack, les restaure et place sa valeur de retour dans le registre eax
afin qu’il soit disponible pour l’appelant.
Cela est spécifique à la plate-forme et fait partie de la “ABI”. En fait, certains compilateurs vous permettent même de choisir entre différentes conventions.
Microsoft Visual Studio, par exemple, propose la convention d’appel __fastcall, qui utilise des registres. Les autres plates-formes ou conventions d’appel utilisent exclusivement la stack.
Les arguments variadiques fonctionnent de manière très similaire: ils sont transmis via des registres ou une stack. Dans le cas des registres, ils sont généralement classés par ordre croissant, en fonction du type. Si vous avez quelque chose comme (int a, int b, float c, int d), un ABI PowerPC peut mettre a
dans r3, b
dans r4, d
dans r5 et c
dans fp1 (j’ai oublié où les registres float commencent, mais vous avoir l’idée).
Les valeurs de retour, encore une fois, fonctionnent de la même manière.
Malheureusement, je n’ai pas beaucoup d’exemples, la plupart de mon assemblage est en PowerPC et tout ce que vous voyez dans l’assembly est le code qui passe directement à r3, r4, r5 et le placement de la valeur de retour dans r3 également.
Vos questions sont plus que quiconque pourrait raisonnablement essayer de répondre dans un message SO, sans mentionner que sa mise en œuvre est également définie.
Cependant, si vous êtes intéressé par la réponse x86, je vous suggère de regarder cette conférence de Stanford CS107 intitulée Programmation de paradigmes dans laquelle toutes les réponses aux questions que vous avez posées seront expliquées en détail (et de manière assez eloquent) lors des 6 à 8 premières conférences. .
Cela dépend de votre compilateur, de l’architecture cible et du système d’exploitation pour lequel vous comstackz, et de la compatibilité de votre compilateur avec des extensions non standard modifiant la convention d’appel. Mais il y a des points communs.
La convention d’appel C est généralement établie par le fournisseur du système d’exploitation, car il doit décider de la convention utilisée par les bibliothèques système.
Les conventions d’appel des CPU les plus récentes (telles que ARM ou PowerPC) sont généralement définies par le fournisseur de la CPU et compatibles entre différents systèmes d’exploitation. x86 est une exception à cela: différents systèmes utilisent différentes conventions d’appel. Auparavant, il existait beaucoup plus de conventions d’appel pour les 8086 16 bits et 80386 32 bits que pour x86_64 (bien que cela ne soit pas égal à un). Les programmes Windows 32 bits x86 utilisent parfois plusieurs conventions d’appel au sein du même programme.
Quelques observations:
STDCALL
, à l’origine FAR PASCAL
) de la convention d’appel «C» pour la même plate-forme et prend également en charge les conventions FORTRAN
et FASTCALL
. Tous les quatre viennent dans les variantes NEAR
et FAR
sur les systèmes d’exploitation 16 bits. Presque tous les programmes Windows utilisent donc au moins deux conventions différentes dans le même programme. FASTCALL
sous MS-DOS et Windows. printf("%d\n", x);
le compilateur va pousser x
, puis la chaîne de format, puis l’adresse de retour, sur la stack. Cela garantit que le premier argument est à un décalage connu du pointeur de stack et que
dispose des informations nécessaires pour fonctionner. STDCALL
cela s’appelait la convention PASCAL
sous MS-DOS et STDCALL
sous la convention STDCALL
sous Windows. Il ne peut pas supporter les fonctions variadiques. ( https://en.wikibooks.org/wiki/X86_Disassembly/Calling_Conventions ) -fomit-frame-pointer
( -fomit-frame-pointer
). Vous pouvez faire en sorte que des compilateurs croisés émettent du code en utilisant différentes conventions d’appel et les comparer à l’aide de commutateurs tels que -S -target
(on clang
).
Fondamentalement, C passe les arguments en les poussant sur la stack. Pour les types de pointeur, le pointeur est placé sur la stack.
Une des choses à propos de C est que l’appelant restaure la stack plutôt que la fonction appelée. De cette façon, le nombre d’arguments peut varier et la fonction appelée n’a pas besoin de savoir à l’avance combien d’arguments seront transmis.
Les valeurs renvoyées sont renvoyées dans le registre AX ou leurs variations.