Question

Je commence tout juste à comprendre les pointeurs de fonction en C.Pour comprendre comment fonctionne la conversion des pointeurs de fonction, j'ai écrit le programme suivant.Il crée essentiellement un pointeur de fonction vers une fonction qui prend un paramètre, le convertit en un pointeur de fonction avec trois paramètres et appelle la fonction en fournissant trois paramètres.J'étais curieux de savoir ce qui allait se passer :

#include <stdio.h>

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;
}

Celui-ci se compile et s'exécute sans erreurs ni avertissements (gcc -Wall sous Linux / x86).Le résultat sur mon système est :

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

Apparemment, les arguments superflus sont simplement écartés en silence.

Maintenant, j'aimerais comprendre ce qui se passe réellement ici.

  1. Concernant la légalité :Si je comprends la réponse à Caster un pointeur de fonction vers un autre type correctement, il s’agit simplement d’un comportement indéfini.Donc le fait que cela fonctionne et produise un résultat raisonnable n’est que de la pure chance, n’est-ce pas ?(ou gentillesse de la part des auteurs du compilateur)
  2. Pourquoi gcc ne m'en avertit-il pas, 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 stricte, donc ce comportement m'a un peu dérouté.Peut-être que je vis un choc culturel :-).

Était-ce utile?

La solution

Les paramètres supplémentaires ne sont pas mis au rebut. Ils sont bien placés sur la pile, comme si l'appel est fait à une fonction qui attend trois paramètres. Cependant, étant donné que votre fonction se soucie d'un seul paramètre, il semble que dans la partie supérieure de la pile et ne touche pas les autres paramètres.

Le fait que cet appel a travaillé est la chance pure, sur la base des deux faits:

  • le type du premier paramètre est le même pour la fonction et le pointeur de la coulée. Si vous changez la fonction de prendre un pointeur vers la chaîne et essayer d'imprimer cette chaîne, vous obtiendrez un accident agréable, car le code va essayer de pointeur de déréférencement pour adresser la mémoire 2.
  • la convention d'appel utilisé par défaut est pour l'appelant pour le nettoyage de la pile. Si vous modifiez la convention d'appel, de sorte que le nettoie la callee pile, vous finirez avec l'appelant en poussant trois paramètres sur la pile, puis le nettoyage callee (ou plutôt tenter de) un paramètre. Cela conduirait probablement corruption de la pile.

Il n'y a aucun moyen le compilateur peut vous avertir des 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 au moment de la compilation, il ne peut pas évaluer ce qu'il pointe vers . Imaginez que les points de pointeur de fonction à une méthode dans une table virtuelle de classe qui est créée lors de l'exécution? Ainsi, il vous dites au compilateur est un pointeur vers une fonction avec trois paramètres, le compilateur vous croira.

Autres conseils

Si vous prenez une voiture et jeté comme un marteau le compilateur vous prendre au mot que la voiture est un marteau, mais cela ne tourne pas la voiture dans un marteau. Le compilateur peut être réussi à utiliser la voiture pour enfoncer un clou, mais qui dépend de l'implémentation bonne fortune. Il est encore une chose sage à faire.

  1. Oui, il est un comportement non défini - tout peut arriver, y compris ce qui apparaît à "travailler"

  2. .
  3. La distribution empêche le compilateur de délivrer un avertissement. , Les compilateurs sont également en aucune exigence pour diagnostiquer les causes possibles un comportement non défini. La raison est que soit il est impossible de le faire, ou que cela ne serait pas trop difficile et / ou faire de frais généraux.

La pire infraction de votre casting 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 pointeurs de données soient égales.Et contrairement à beaucoup de théorique comportements indéfinis, celui-ci peut être rencontré dans la nature, même sur des machines avancées (pas seulement sur les systèmes embarqués).

Vous pouvez facilement rencontrer des pointeurs de différentes tailles sur les plates-formes intégrées.Il existe même des processeurs dans lesquels les pointeurs de données et les pointeurs de fonctions adressent des choses différentes (RAM pour l'un, ROM pour l'autre), ce qu'on appelle l'architecture Harvard.Sur x86 en mode réel, vous pouvez mélanger 16 bits et 32 ​​​​bits.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 pourrait être le seul langage disponible sur du matériel exotique.

Certains compilateurs autorisent des modèles de mémoire mixtes dans lesquels le code est garanti dans une taille de 32 bits et les données sont adressables avec des pointeurs de 64 bits, ou l'inverse.

Modifier: Conclusion, ne lancez jamais un pointeur de données vers un pointeur de fonction.

Le comportement est défini par la convention d'appel. Si vous utilisez une convention d'appel où l'appelant pousse et saute la pile, il fonctionnerait très bien dans ce cas, car il serait tout simplement dire il y a un peu d'octets supplémentaires sur la pile pendant l'appel. Je n'ai pas gcc à portée de main au moment, mais avec le compilateur Microsoft, ce code:

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

L'ensemble suivant est généré pour l'appel:

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

Remarque les 12 octets (0CH) ajoutés à la pile après l'appel. Après cela, la pile est très bien (en supposant que le callee est __cdecl dans ce cas, il ne cherche pas à nettoyer aussi la pile). Mais avec le code suivant:

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

Le add esp,0Ch est pas généré dans l'assemblage. Si le __cdecl est appelé dans ce cas, la pile serait corrompu.

  1. Admitedly Je ne sais pas pour vous, mais vous ne voulez certainement pas tirer profit du comportement si c'est la chance ou si elle est

    spécifique compilateur.
  2. Il ne mérite pas un avertissement parce que le casting est explicite. Par coulée, vous informer le compilateur que vous savez mieux. vous jetant un void*, et en tant que telle que vous dites « prendre l'adresse représentée par ce pointeur, et faire la même chose que cet autre pointeur » En particulier, - la distribution informe simplement le compilateur que vous êtes sûr de ce que est à l'adresse cible est, en fait, la même chose. Bien ici, nous savons que ce incorrect.

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

  • 1: Ce n'est pas la chance pure. La convention d'appel C est bien défini, et des données supplémentaires sur la pile ne sont pas un facteur pour le site d'appel, même si elle peut être écrasée par le car le callee ne sait pas appelé à ce sujet.
  • 2: Un casting, en utilisant entre parenthèses « dur », est dit au compilateur que vous savez ce que vous faites. Étant donné que toutes les données nécessaires est dans une unité de compilation, le compilateur pourrait être assez intelligent pour comprendre que cela est clairement illégal, mais le concepteur C (s) ne se concentre pas sur la capture cas d'angle incorrection vérifiable. Plus simplement, les fiducies de compilateur que vous savez ce que vous faites (peut-être imprudemment dans le cas d'un grand nombre C / C ++ programmeurs!)

Pour répondre à vos questions:

  1. chance Pure - vous pouvez facilement piétiner la pile et remplacer le pointeur de retour au code suivant l'exécution. Depuis que vous avez spécifié le pointeur de fonction avec 3 paramètres, et invoqua le pointeur de fonction, les deux autres paramètres ont été « mis au rebut » et, par conséquent, le comportement est indéfini. Imaginez si ce 2e ou 3e paramètre contient une instruction binaire, et a sauté de la pile de la procédure d'appel ....

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

Hope this helps, Meilleures salutations, Tom.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top