Techniquement, comment fonctionnent les fonctions variadiques? Comment fonctionne printf?

Je sais que je peux utiliser va_arg pour écrire mes propres fonctions variadiques, mais comment les fonctions variadiques fonctionnent-elles sous le capot, c’est-à-dire au niveau des instructions d’assemblage?

Par exemple, comment est-il possible que printf prenne un nombre variable d’arguments?


* Aucune règle sans exception. Il n’y a pas de langage C / C ++, cependant, cette question peut être répondue pour les deux

* Note: Réponse à l’origine donnée à Comment la fonction printf peut-elle prendre des parameters variables en nombre tout en les sortant? , mais il semble que cela ne s’appliquait pas à l’intervenant

Le standard C et C ++ n’a aucune exigence quant à son fonctionnement. Un compilateur conforme peut bien décider d’émettre des listes chaînées, std::stack ou même de la poussière de poney magique (selon Xeo) sous le capot.

Toutefois, il est généralement implémenté comme suit, même si les transformations telles que l’inclusion ou la transmission d’arguments dans les registres de l’UC peuvent ne rien laisser de ce code.

Veuillez également noter que cette réponse décrit spécifiquement une stack croissante dans les visuels ci-dessous; de plus, cette réponse est une simplification juste pour démontrer le schéma (veuillez consulter https://en.wikipedia.org/wiki/Stack_frame ).

Comment appeler une fonction avec un nombre d’arguments non fixé

Cela est possible car l’architecture de la machine sous-jacente a une soi-disant “stack” pour chaque thread. La stack est utilisée pour passer des arguments aux fonctions. Par exemple, lorsque vous avez:

 foobar("%d%d%d", 3,2,1); 

Ensuite, cela comstack en un code assembleur comme celui-ci (exemplaire et schématique, le code réel peut avoir un aspect différent); notez que les arguments sont passés de droite à gauche:

 push 1 push 2 push 3 push "%d%d%d" call foobar 

Ces opérations push remplissent la stack:

  [] // empty stack ------------------------------- push 1: [1] ------------------------------- push 2: [1] [2] ------------------------------- push 3: [1] [2] [3] // there is now 1, 2, 3 in the stack ------------------------------- push "%d%d%d":[1] [2] [3] ["%d%d%d"] ------------------------------- call foobar ... // foobar uses the same stack! 

L’élément de stack inférieur est appelé “Top of Stack”, souvent abrégé “TOS”.

La fonction foobar accède maintenant à la stack, en commençant par le TOS, c’est-à-dire la chaîne de format, qui, comme vous vous en souvenez, a été poussée en dernier. Imagine stack est votre pointeur de stack, stack[0] est la valeur du TOS, la stack[1] est au-dessus du TOS, et ainsi de suite:

 format_ssortingng <- stack[0] 

... puis parsing la chaîne de format. Lors de l’parsing, il reconnaît les mots-clés %d et, pour chacun, charge une valeur supplémentaire de la stack:

 format_ssortingng <- stack[0] offset <- 1 while (parsing): token = tokenize_one_more(format_string) if (needs_integer (token)): value <- stack[offset] offset = offset + 1 ... 

Ceci est bien sûr un pseudo-code très incomplet qui montre comment la fonction doit s'appuyer sur les arguments transmis pour savoir combien elle doit charger et retirer de la stack.

Sécurité

Ce recours aux arguments fournis par l'utilisateur est également l'un des plus grands problèmes de sécurité présents (voir https://cwe.mitre.org/top25/ ). Les utilisateurs peuvent facilement utiliser une fonction variadique à tort, soit parce qu'ils n'ont pas lu la documentation, soit qu'ils ont oublié d'ajuster la chaîne de format ou la liste d'arguments, ou parce qu'ils sont tout simplement mauvais, etc. Voir aussi Format Ssortingng Attack .

C Implémentation

En C et C ++, les fonctions va_list sont utilisées avec l'interface va_list . Alors que le fait de pousser sur la stack est insortingnsèque à ces langages ( dans K + RC, vous pouvez même déclarer une fonction sans indiquer ses arguments , mais l'appeler toujours avec un nombre quelconque d'arguments), la lecture d'une telle liste d'arguments inconnue est interfacée à travers les va_... -... -macros et va_list -type, qui abstrait fondamentalement l’access de bas niveau au stack-frame.

Les fonctions Variadic sont définies par la norme, avec très peu de ressortingctions explicites. Voici un exemple, extrait de cplusplus.com.

 /* va_start example */ #include  /* printf */ #include  /* va_list, va_start, va_arg, va_end */ void PrintFloats (int n, ...) { int i; double val; printf ("Printing floats:"); va_list vl; va_start(vl,n); for (i=0;i 

Les hypothèses sont grosso modo comme suit.

  1. Il doit y avoir (au moins un) premier argument, fixe, nommé. Le ... ne fait en fait rien, sauf dire au compilateur de faire la bonne chose.
  2. Les arguments fixes fournissent des informations sur le nombre d’arguments variadiques, par un mécanisme non spécifié.
  3. À partir de l'argument fixe, la macro va_start peut renvoyer un object permettant de récupérer des arguments. Le type est va_list .
  4. À partir de l'object va_list , il est possible pour va_arg d'itérer chaque argument va_arg et de le forcer à entrer dans un type compatible.
  5. Quelque chose de bizarre pourrait être arrivé dans va_start donc va_end fait bien les choses.

Dans la situation basée sur la stack la plus courante, la liste va_list est simplement un pointeur sur les arguments va_arg sur la stack et va_arg incrémente le pointeur, le lance et le va_arg - va_arg sur une valeur. Alors va_start initialise ce pointeur par une arithmétique simple (et une connaissance interne) et va_end ne fait rien. Il n'y a pas de langage d'assemblage étrange, juste quelques informations sur la position de la stack. Lisez les macros dans les en-têtes standard pour savoir ce que c'est.

Certains compilateurs (MSVC) nécessiteront une séquence d'appel spécifique, l'appelant libérant la stack plutôt que l'appelé.

Les fonctions comme printf fonctionnent exactement comme ceci. L'argument fixe est une chaîne de format qui permet de calculer le nombre d'arguments.

Des fonctions comme vsprintf transmettent l'object va_list tant que type d'argument normal.

Si vous avez besoin de plus ou moins de détails, veuillez append à la question.