Comment fonctionne l’argument en passant?

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:

  • Linux pour x86_64 est un exemple de système d’exploitation prenant en charge simultanément plusieurs ABI avec différentes conventions d’appel, dont certaines suivent les mêmes conventions que les autres systèmes d’exploitation pour la même architecture. Celui-ci peut héberger trois principales ABI différentes (i386, x32 et x86_64), dont deux sont identiques aux autres systèmes d’exploitation pour le même processeur et plusieurs variantes.
  • Une exception à la règle selon laquelle une convention d’appel système est utilisée pour tout est les versions 16 et 32 ​​bits de MS Windows, qui ont hérité d’une partie de la prolifération de conventions d’appel de MS-DOS. L’API Windows C utilise une convention d’appel différente ( 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.
  • Les architectures comportant de nombreux registres, y compris RISC classique et presque toutes les normes ISA modernes, utilisent plusieurs de ces registres pour transmettre et renvoyer des arguments de fonction.
  • Les architectures avec peu ou pas de registres à usage général transmettent souvent des arguments à la stack, pointés par un pointeur de stack. Les architectures CISC ont souvent des instructions pour appeler et retourner, qui stockent l’adresse de retour sur la stack. (Les architectures RISC stockent généralement l’adresse de retour dans un “registre de liaison”, que l’appelé peut sauvegarder / restaurer manuellement s’il ne s’agit pas d’une fonction feuille.)
  • Une variante courante consiste pour les appels de fin de ligne, fonctions dont la valeur de retour est également la valeur de retour de l’appelant, pour passer à la fonction suivante (afin qu’elle retourne à notre fonction parent) au lieu de l’appeler puis de la retourner après son retour. Placer les arguments au bon endroit doit rendre compte de l’adresse de retour déjà présente dans la stack, où une instruction d’appel la placerait. Cela est particulièrement vrai des appels de type queue-récursif, qui ont exactement le même cadre de stack à chaque appel. Un appel final récursif est généralement équivalent à une boucle: mettez à jour quelques registres modifiés, puis revenez au point d’entrée. Ils n’ont pas besoin de créer un nouveau cadre de stack ni d’avoir leur propre adresse de retour: vous pouvez simplement mettre à jour le cadre de stack de l’appelant et utiliser son adresse de retour comme correspondant à l’appel final. c’est-à-dire que la récursion de queue optimise facilement une boucle.
  • Certaines architectures ne disposant que de quelques registres ont néanmoins défini une convention d’appel alternative permettant de transmettre un ou deux arguments dans les registres. C’était FASTCALL sous MS-DOS et Windows.
  • Quelques anciennes ISA, telles que SPARC, disposaient d’une banque spéciale de registres «fenêtrés», de sorte que chaque fonction disposait de sa propre banque de registres d’entrée et de sortie. Lorsqu’elle effectuait un appel de fonction, les sorties de l’appelant devenaient les entrées de l’appelé. l’inverse quand il est temps de retourner une valeur. Les conceptions superscalaires modernes tiennent compte de ces problèmes.
  • Quelques très anciennes architectures utilisaient du code auto-modificateur dans leurs conventions d’appel. La première édition de The Art of Computer Programming a suivi ce modèle pour son langage abstrait. Cela ne fonctionne plus sur la plupart des processeurs modernes, qui ont des caches d’instruction.
  • Quelques autres très anciennes architectures n’avaient pas de stack et ne pouvaient généralement plus appeler la même fonction et y entrer jusqu’à ce qu’elle revienne.
  • Une fonction avec beaucoup d’arguments met presque toujours la plupart d’entre eux dans la stack.
  • Les fonctions C qui mettent des arguments sur la stack doivent presque les pousser dans l’ordre inverse et laisser l’appelant nettoyer la stack. La fonction appelée peut même ne pas savoir exactement combien d’arguments sont sur la stack! Autrement dit, si vous appelez 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.
  • La plupart des autres langages, et donc certains systèmes d’exploitation pris en charge par les compilateurs C, procèdent de manière inverse: les arguments sont poussés de gauche à droite. La fonction appelée nettoie généralement son propre cadre de stack. 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 )
  • Fortran et quelques autres langues ont historiquement passé tous les arguments par référence, ce qui traduit C en arguments de pointeur. Les compilateurs pouvant avoir besoin d’interfaces avec ces autres langues prennent souvent en charge ces conventions d’appels étrangers.
  • Parce qu’une source majeure de bogues était de «détruire la stack», de nombreux compilateurs ont maintenant un moyen d’append des valeurs canariennes (qui, comme un canari dans une mine de charbon, vous avertissent que quelque chose de dangereux se produit si quelque chose leur arrive) et d’autres des moyens de détection lorsque le code altère le cadre de la stack.
  • Une autre forme de variation entre différentes plates-formes consiste à déterminer si le cadre de stack contiendra toutes les informations nécessaires à un suivi par le débogueur ou le gestionnaire d’exceptions, ou si ces informations figureront dans des métadonnées séparées (ou ne seront pas du tout présentes), ce qui permettra de simplifier le prolog de la fonction. / -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.