3 votes

Comment les variables variadiques sont-elles représentées sur la pile ?

Je cherchais à implémenter ma propre fonction variadique en utilisant ce code. Au lieu de cela, j'ai obtenu UB.

#include <stdio.h>

void test(int a, ...)
{
    char* arg_a = (char*)&a;
    char* arg_b = arg_a + sizeof(int);
    printf("%c", *arg_b);
}

int main(){
    test(1, 'a');
}

Alors pourquoi ce programme ne affiche-t-il pas la lettre a? N'est-il pas attendu que l'argument 1 (de test()) sera écrit dans le cadre de la pile de la fonction à une adresse basse par ex: 0000 0004 (puisque 0000 0000 sera réservé pour l'adresse de retour) puis suivi de arg_a à l'adresse supérieure après le premier argument?

Je suppose que ce résultat est dû à quelque chose de l'optimisation du compilateur, ou y a-t-il autre chose?

3voto

Zack Points 44583

N'est-il pas attendu que l'argument 1 (de test()) soit écrit dans le cadre de la pile de fonctions à une adresse basse ex : 0000 0004 (puisque 0000 0000 sera réservé pour l'adresse de retour) puis suivi de arg_a à une adresse plus élevée après le premier argument?

C'est ainsi que cela fonctionnait il y a longtemps, mais essentiellement tous les ABI pour les processeurs pleine taille (par opposition aux microcontrôleurs), définis depuis le milieu des années 1990, placent les premiers arguments dans des registres pour rendre les appels de fonctions plus rapides. Ils font cela que le callee soit variadic ou non. Vous ne pouvez pas accéder aux registres avec une arithmétique de pointeur, donc ce que vous essayez de faire est tout simplement impossible. En raison de ce changement, si vous regardez le contenu du stdarg.h fourni par n'importe quel compilateur de génération actuelle, vous verrez que va_start, va_arg et va_end sont définis à l'aide d'intrinsèques de compilateur, un peu comme

#define va_start(ap, last_named_arg) __builtin_va_start(ap, last_named_arg)
// etc

Vous avez peut-être été induit en erreur en pensant que les arguments étaient toujours placés sur la pile car la plupart des ABI x86 32 bits ont été définis à la fin des années 1980 (contemporains du 80386 et du 80486) et ils mettent effectivement tous les arguments sur la pile. La seule exception dont je me souvienne est Win32 "fastcall". Les ABI x86 64 bits, cependant, ont été définis au début des années 2000 (contemporains de l'AMD K8) et placent les arguments dans des registres.

Votre code ne fonctionnera pas de manière fiable même si vous le compilez pour du x86 32 bits (ou tout autre ancien ABI qui place tous les arguments sur la pile), car il enfreint les règles de la norme C concernant le décalage des pointeurs. Le pointeur arg_b ne pointe pas vers "ce qui se trouve dans la mémoire à côté de a", il ne pointe vers rien. (Formellement, il pointe un élément au-delà de la fin d'un tableau à un élément, car tous les objets non tableau sont traités comme le seul élément d'un tableau à un élément à des fins arithmétiques de pointeur. Vous êtes autorisé à effectuer l'arithmétique qui calcule ce pointeur, mais pas à le déréférencer.) Le déréférencement de arg_b entraîne un comportement indéfini du programme, ce qui signifie que le compilateur est autorisé à le "mauvais compiler" arbitrairement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X