Que se passe-t-il si je lance un pointeur de fonction en modifiant le nombre de parameters?

Je commence tout juste à comprendre les indicateurs de fonction en C. Pour comprendre le fonctionnement du casting des indicateurs de fonction, j’ai écrit le programme suivant. Il crée en principe un pointeur de fonction sur une fonction qui prend un paramètre, le convertit en un pointeur de fonction avec trois parameters et appelle la fonction en fournissant trois parameters. J’étais curieux de savoir ce qui arriverait:

#include  int square(int val){ return val*val; } void printit(void* ptr){ int (*fptr)(int,int,int) = (int (*)(int,int,int)) (ptr); printf("Call function with parameters 2,4,8.\n"); printf("Result: %d\n", fptr(2,4,8)); } int main(void) { printit(square); return 0; } 

Ceci comstack et fonctionne sans erreurs ni avertissements (gcc -Wall sur Linux / x86). La sortie sur mon système est:

 Call function with parameters 2,4,8. Result: 4 

Donc, apparemment, les arguments superflus sont simplement rejetés en silence.

Maintenant, j’aimerais comprendre ce qui se passe vraiment ici.

  1. En ce qui concerne la légalité: si je comprends la réponse à la conversion correcte d’ un pointeur de fonction , il s’agit simplement d’un comportement indéfini. Donc, le fait que cela fonctionne et donne un résultat raisonnable est simplement de la chance, n’est-ce pas? (ou gentillesse des rédacteurs du compilateur)
  2. Pourquoi gcc ne m’avertira-t-il pas de cela, même avec Wall? Est-ce quelque chose que le compilateur ne peut tout simplement pas détecter? Pourquoi?

Je viens de Java, où la vérification de type est beaucoup plus ssortingcte, alors ce comportement m’a un peu dérouté. Peut-être que je subis un choc culturel :-).

Les parameters supplémentaires ne sont pas ignorés. Ils sont correctement placés sur la stack, comme si l’appel était fait vers une fonction qui attend trois parameters. Cependant, comme votre fonction ne concerne qu’un paramètre, elle n’apparaît qu’en haut de la stack et ne touche pas les autres parameters.

Le fait que cet appel ait fonctionné relève de la pure chance et repose sur deux faits:

  • le type du premier paramètre est identique pour la fonction et le pointeur de conversion. Si vous modifiez la fonction pour prendre un pointeur sur chaîne et essayez d’imprimer cette chaîne, vous obtiendrez un plantage intéressant, car le code essaiera de déréférencer le pointeur pour adresser la mémoire 2.
  • la convention d’appel utilisée par défaut est que l’appelant nettoie la stack. Si vous modifiez la convention d’appel afin que l’appelé nettoie la stack, l’appelant poussera trois parameters sur la stack, puis l’appelant nettoiera (ou plutôt tentera de) un paramètre. Cela conduirait probablement à la corruption de stack.

Le compilateur ne peut absolument pas vous avertir de problèmes potentiels tels que celui-ci pour une raison simple: dans le cas général, il ne connaît pas la valeur du pointeur au moment de la compilation, il ne peut donc pas évaluer son contenu. Imaginons que le pointeur de fonction pointe vers une méthode dans une table virtuelle de classe créée au moment de l’exécution? Donc, si vous dites au compilateur qu’il s’agit d’un pointeur sur une fonction à trois parameters, le compilateur vous croira.

Si vous prenez une voiture et la lancez comme un marteau, le compilateur vous prendra au mot que la voiture est un marteau mais que cela ne transforme pas la voiture en un marteau. Le compilateur réussit peut-être à utiliser la voiture pour enfoncer un clou, mais sa réussite dépend de la mise en œuvre. C’est toujours une chose imprudente à faire.

  1. Oui, c’est un comportement indéfini – tout peut arriver, même si cela semble “fonctionner”.

  2. La conversion empêche le compilateur d’émettre un avertissement. En outre, les compilateurs ne sont pas tenus de diagnostiquer les causes possibles d’un comportement non défini. La raison en est qu’il est impossible de le faire ou que cela serait trop difficile et / ou causerait beaucoup de frais généraux.

La pire infraction de votre dissortingbution est de convertir un pointeur de données en un pointeur de fonction. C’est pire que le changement de signature car il n’y a aucune garantie que les tailles des pointeurs de fonction et des données soient identiques. Et contrairement à beaucoup de comportements théoriques non définis, celui-ci peut être rencontré à l’état sauvage, même sur des machines avancées (pas seulement sur des systèmes embarqués).

Vous pouvez rencontrer facilement des pointeurs de tailles différentes sur les plates-formes intégrées. Il existe même des processeurs où les pointeurs de données et de fonction traitent différentes choses (la RAM pour l’un, la ROM pour l’autre), l’architecture dite de Harvard. Sur x86 en mode réel, vous pouvez avoir 16 bits et 32 ​​bits mélangés. Watcom-C avait un mode spécial pour l’extension DOS où les pointeurs de données avaient une largeur de 48 bits. Surtout avec C, il faut savoir que tout n’est pas POSIX, car C est peut-être le seul langage disponible sur du matériel exotique.

Certains compilateurs autorisent des modèles de mémoire mixte où le code est garanti d’une taille inférieure à 32 bits et où les données sont adressables avec des pointeurs 64 bits, ou inversement.

Edit: Conclusion, ne jamais transtyper un pointeur de données sur un pointeur de fonction.

Le comportement est défini par la convention d’appel. Si vous utilisez une convention d’appel dans laquelle l’appelant insère et emstack la stack, cela fonctionnerait très bien dans ce cas car cela signifierait simplement qu’il y a quelques octets supplémentaires sur la stack pendant l’appel. Je n’ai pas gcc à scope de la main pour le moment, mais avec le compilateur Microsoft, ce code:

 int ( __cdecl * fptr)(int,int,int) = (int (__cdecl * ) (int,int,int)) (ptr); 

L’assembly suivant est généré pour l’appel:

 push 8 push 4 push 2 call dword ptr [ebp-4] add esp,0Ch 

Notez les 12 octets (0Ch) ajoutés à la stack après l’appel. Après cela, la stack est correcte (en supposant que l’appelé est __cdecl dans ce cas, il ne tente pas de nettoyer également la stack). Mais avec le code suivant:

 int ( __stdcall * fptr)(int,int,int) = (int (__stdcall * ) (int,int,int)) (ptr); 

Le add esp,0Ch n’est pas généré dans l’assemblage. Si l’appelé est __cdecl dans ce cas, la stack serait corrompue.

  1. Certes, je ne le sais pas avec certitude, mais vous ne voulez certainement pas tirer parti de ce comportement s’il a de la chance ou s’il est spécifique au compilateur.

  2. Cela ne mérite pas un avertissement, car la dissortingbution est explicite. En faisant un casting, vous informez le compilateur que vous connaissez mieux. En particulier, vous lancez un void* , et en tant que tel vous dites “prenez l’adresse représentée par ce pointeur et faites-la identique à cet autre pointeur” – la dissortingbution informe simplement le compilateur que vous êtes sûr ce qui est à l’adresse cible est, en fait, le même. Bien qu’ici, nous soaps que c’est incorrect.

Je devrais me rafraîchir la mémoire sur la structure binary de la convention d’appel C à un moment donné, mais je suis à peu près sûr que c’est ce qui se passe:

  • 1: Ce n’est pas de la pure chance. La convention d’appel C est bien définie et les données supplémentaires sur la stack ne sont pas un facteur pour le site d’appel, bien qu’elles puissent être écrasées par l’appelé car celui-ci ne le sait pas.
  • 2: Un casting “dur”, entre parenthèses, indique au compilateur que vous savez ce que vous faites. Étant donné que toutes les données nécessaires sont réunies dans une unité de compilation, le compilateur pourrait être suffisamment intelligent pour comprendre que cela est clairement illégal, mais le ou les concepteurs de C ne se sont pas concentrés sur la détection de l’inexactitude vérifiable. En termes simples, le compilateur dit que vous savez ce que vous faites (peut-être mal à propos dans le cas de nombreux programmeurs C / C ++!)

Pour répondre à vos questions:

  1. De la chance pure – vous pouvez facilement piétiner la stack et écraser le pointeur de retour sur le code en cours d’exécution. Etant donné que vous avez spécifié le pointeur de fonction avec 3 parameters et que vous l’avez appelé, les deux parameters restants ont été «ignorés» et, par conséquent, le comportement n’est pas défini. Imaginez si ce deuxième ou troisième paramètre contenait une instruction binary et sortait de la stack de procédures d’appel ….

  2. Il n’y a pas d’avertissement, car vous utilisiez un pointeur void * et le diffusiez. C’est un code tout à fait légitime aux yeux du compilateur, même si vous avez explicitement spécifié le commutateur -Wall . Le compilateur suppose que vous savez ce que vous faites! C’est le secret.

J’espère que cela vous aidera, Cordialement, Tom.