10
votes

Que se passe-t-il si je jette un pointeur de fonction, modifiant le nombre de paramètres

Je commence juste à envelopper ma tête autour des pointeurs de la fonction en C. Pour comprendre la manière dont le casting des pointes de fonction fonctionne, j'ai écrit le programme suivant. Il crée essentiellement un pointeur de fonction sur une fonction qui prend un paramètre, la jette à un pointeur de fonction avec trois paramètres et appelle la fonction, fournissant trois paramètres. J'étais curieux de savoir ce qui se passerait:

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


1 commentaires

Si tel est déjà un choc de culture pour vous, attendez que vous découvriez que dans les points de vue de la C ++ aux fonctions des membres peut être plus grand que le vide *, bien qu'ils ne portent pas le ce pointeur avec eux ...


8 Réponses :


1
votes
  1. adressément je ne sais pas avec certitude, mais vous ne voulez certainement pas profiter du comportement si c'est la chance ou si son compilateur est spécifique.

  2. Cela ne mérite pas un avertissement parce que la distribution est explicite. En casting, vous informez le compilateur que vous connaissez mieux. En particulier, vous jetez un void * et, en tant que tel, vous dites "Prenez l'adresse représentée par ce pointeur et en rendez la même chose que cet autre pointeur" - La distribution informe simplement Le compilateur que vous êtes sûr de ce qui est à l'adresse cible est, en fait, identique. Bien que ici, nous savons que c'est incorrect.


0 commentaires

3
votes
  1. Oui, c'est un comportement indéfini - tout pourrait arriver, y compris celui-ci apparaissant à "travailler".

  2. La distribution empêche le compilateur de délivrer un avertissement. En outre, les compilateurs ne sont pas obligés de diagnostiquer des causes possibles. La raison en est que son impossible de le faire ou que cela serait trop difficile et / ou causer beaucoup de frais généraux.


0 commentaires

1
votes

Je devrais rafraîchir ma mémoire de la disposition binaire de la convention d'appel C à un moment donné, mais je suis sûr que c'est ce qui se passe:

  • 1: ce n'est pas une chance pure. La convention C appelant C est bien définie et des données supplémentaires sur la pile ne sont pas un facteur pour le site d'appels, bien qu'il puisse être écrasé par la callee puisque la Callee ne le sait pas.
  • 2: Un casting "dur", utilisant des parenthèses, dit au compilateur que vous savez ce que vous faites. Étant donné que toutes les données nécessaires sont dans une unité de compilation, le compilateur pourrait être suffisamment intelligent pour déterminer que cela est clairement illégal, mais le (s) concepteur (s) de C ne s'est pas concentré sur le cas d'accrochage d'angle une incorrecture vérifiable. Mettez simplement, le compilateur fait confiance que vous savez ce que vous faites (peut-être sous imprudemment dans le cas de nombreux programmeurs C / C ++!)

3 commentaires

C'est une chance pure. Le nombre de paramètres n'est pas la seule chose importante, le type de taille et de sémantique des paramètres passés et attendus est également important. La coulée de F1 (char *) à F2 (int, int int) par exemple causerait des problèmes. Ou la coulée de F1 (MyAtyLargesTrit) à F2 (Char).


Je ne préconise pas ce code en général, mais ce n'est pas une chance pure ce qui s'est passé. La convention d'appel «C» (__cdecl in MS C ++) garantit que le premier paramètre d'un appel de méthode sera toujours le premier trouvé par le Calleee, quel que soit le nombre de paramètres ultérieurs. Le comportement est complètement prévisible et reproductible, qui est le contraire de "chance" dans mon esprit.


Pour être claire, voici ce que la pile d'appels, à partir de l'adresse ESP Register, on dirait au début de la fonction Square () Appel de fonctionnement par ce code 1er 4 octets: adresse de retour de l'appel; 2ème 4 octets: valeur entière de '2'; 3ème 4 octets: valeur entière de '4'; 4ème 4 octets: valeur entière de '8'; La fonction "Square ()" ne connaît que les 8 premiers octets après ESP, mais ces 8 premiers octets sont exactement les données nécessaires pour fonctionner correctement. C'est un comportement totalement sûr et reproductible, mais il est assez confus que cela soit difficile à recommander.



0
votes

Pour répondre à vos questions:

  1. Pure Luck - Vous pouvez facilement piétiner la pile et écraser le pointeur de retour au prochain code d'exécution. Puisque vous avez spécifié le pointeur de fonction avec 3 paramètres et appelé le pointeur de fonction, les deux paramètres restants ont été «supprimés» et, par conséquent, le comportement n'est pas défini. Imaginez que si le 2e ou le 3ème paramètre contenait une instruction binaire et évoquée de la pile de procédures d'appel ....

  2. Il n'y a pas d'avertissement que vous utilisiez un "code> Void * pointeur et la jeter. C'est un code assez légitime dans les yeux du compilateur, même si vous avez explicitement spécifié -wall interrupteur. Le compilateur suppose que vous savez ce que vous faites! c'est le secret.

    J'espère que cela aide, Meilleures salutations, Tom.


2 commentaires

Ils devraient construire un compilateur qui sait mieux que de faire confiance aux humains. :-)


@Françu: C'est la nature de C, vous fournissez une syntaxe qui a l'air légitime, le compilateur l'acceptera volontiers ... Regardez ioccc.org pour voir ce que je veux dire.



12
votes

Si vous prenez une voiture et que vous le jetez comme un marteau, le compilateur vous mènera à votre mot que la voiture est un marteau, mais cela ne tourne pas la voiture en marteau. Le compilateur peut réussir à utiliser la voiture pour conduire un clou, mais c'est une bonne fortune dépendante. C'est toujours une chose imprudente à faire.


0 commentaires

15
votes

Les paramètres supplémentaires ne sont pas jetés. Ils sont correctement placés sur la pile, comme si l'appel est effectué sur une fonction qui attend trois paramètres. Cependant, étant donné que votre fonction se soucie d'un seul paramètre, il ne regarde que sur le dessus de la pile et ne touche pas les autres paramètres.

Le fait que cet appel a fonctionné est une chance pure, basée sur les deux faits:

  • Le type du premier paramètre est identique pour la fonction et le pointeur moulé. Si vous modifiez la fonction pour prendre un pointeur sur la chaîne et essayer d'imprimer cette chaîne, vous obtiendrez un joli crash, car le code essaiera du pointeur de déséroférence pour adresser la mémoire 2.
  • La convention d'appel utilisée par défaut est pour l'appelant pour nettoyer la pile. Si vous modifiez la convention d'appel, de sorte que la callee nettoie la pile, vous vous retrouverez avec l'appelant en poussant trois paramètres sur la pile, puis la callee nettoyage (ou plutôt tenter de) un paramètre. Cela conduirait probablement à empiler la corruption.

    Il n'y a aucun moyen que le compilateur ne peut vous avertir de problèmes potentiels comme celui-ci pour une raison simple - dans le cas général, il ne connaît pas la valeur du pointeur à la compilation, il ne peut donc pas évaluer ce qu'il pointe de . Imaginez que le pointeur de fonction pointe sur une méthode dans une table virtuelle de classe créée au moment de l'exécution? Donc, vous dites au compilateur, il s'agit d'un pointeur d'une fonction avec trois paramètres, le compilateur vous croira.


1 commentaires

@Jasonc Yep, exactement ce que j'ai dit à ma deuxième balle. :-)



3
votes

La pire infraction de votre distribution est de lancer un pointeur de données sur un pointeur de fonction. C'est pire que le changement de signature car il n'ya aucune garantie que les tailles des pointeurs de fonction et le pointeur de données sont égales. Et contrairement à beaucoup de comportements théoriques indéfinis , celui-ci peut être rencontré dans la nature, même sur des machines avancées (non seulement sur des systèmes embarqués).

Vous pouvez rencontrer facilement des pointeurs de taille différents sur des plates-formes intégrées. Il existe même des processeurs où les pointeurs de données et le pointeur de fonction traitent de différentes choses (RAM pour une, ROM pour l'autre), la soi-disant architecture 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 DOS Extender où les pointeurs de données étaient de 48 bits de large. Surtout avec C, il faut savoir que tout n'est pas POSIX, car c pourrait être la seule langue disponible sur le matériel exotique.

Certains compilateurs permettent des modèles de mémoire mélangés dans lesquels le code est garanti à la taille de 32 bits et que les données sont adressables avec des pointeurs 64 bits, ou l'inverse.

Edit: Conclusion, ne jette jamais un pointeur de données à un pointeur de fonction.


6 commentaires

+1 Intéressant, je ne savais pas à propos de ce pointeur de données de distinction <-> Pointeur de fonction. Avez-vous une référence pratique pour en savoir plus? En outre, quel serait l'équivalent de vide * être pour les pointeurs de la fonction? Ou n'y a-t-il pas de pointeur de fonction "générique", comme Void * pour les pointeurs de données?


Standard C ne garantit pas le "code> tailleOf (vide *) == Tailleof (void (*) (void)) '; Heureusement, Posix fait.


Section 6.3.2.3 "Pointants" dit: "Un pointeur sur une fonction d'un type peut être converti en un pointeur sur une fonction d'un autre type et de retour à nouveau; le résultat doit comparer égal au pointeur d'origine. Si un pointeur converti est utilisé. Si un pointeur converti est utilisé. Si un pointeur converti est utilisé. Si un pointeur converti est utilisé. Pour appeler une fonction dont le type n'est pas compatible avec le type pointu, le comportement est indéfini. " Notez que la question invoque explicitement un comportement non défini - méfiez-vous des démons nasaux. Un paragraphe similaire (plus tôt) sur les pointeurs vers des objets. À aucun moment, la norme C ne dit que les pointeurs vers des fonctions peuvent être convertis en des pointeurs vers des objets ou inversement.


Vous pouvez rencontrer des pointeurs de taille différents facilement sur des plates-formes embarquées. Il existe même des processeurs où les pointeurs de données et le pointeur de fonction traitent de différentes choses (RAM pour une, ROM pour l'autre), la soi-disant architecture 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 DOS Extender où les pointeurs de données étaient de 48 bits de large. Surtout avec C, il faut savoir que tout n'est pas POSIX, car c pourrait être la seule langue disponible sur le matériel exotique.


Quant à un pointeur de fonction de vide générique, il n'y en a pas. La chose la plus approchée serait int (*) () . Notez qu'il n'y a pas de void dans la liste des paramètres, comme cela indiquerait que la fonction ne prend aucun paramètre, avec () il ne dit que c'est un nombre inconnu de paramètre. C'est une différence sémantique entre C et C ++.


@Tristopia: Bonjour, j'ai pris la liberté de modifier votre commentaire utile dans la réponse. J'espère que ça ne vous dérange pas.



2
votes

Le comportement est défini par la convention appelante. Si vous utilisez une convention appelante où l'appelant pousse et apparaît la pile, cela fonctionnerait bien dans ce cas car il serait juste de dire qu'il y a quelques octets supplémentaires sur la pile pendant l'appel. Je n'ai pas de GCC Handy pour le moment, mais avec le compilateur Microsoft, ce code:

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


1 commentaires

+1 Merci pour les informations sur les conventions d'appel. Je ne savais pas qu'il y a plusieurs conventions.