Pourquoi les pointeurs de fonction et les pointeurs de données sont-ils incompatibles en C / C ++?

130

J'ai lu que la conversion d'un pointeur de fonction en un pointeur de données et vice versa fonctionne sur la plupart des plates-formes mais n'est pas garantie de fonctionner. pourquoi est-ce le cas? Les deux ne devraient-ils pas être simplement des adresses dans la mémoire principale et donc être compatibles?

gexicide
la source
16
Indéfini dans le standard C, défini dans POSIX. Attention à la différence.
éphémère
Je suis un peu nouveau dans ce domaine, mais n'êtes-vous pas censé faire le casting sur le côté droit du "="? Il me semble que le problème est que vous attribuez un pointeur vide. Mais je vois que la page de manuel fait cela, donc j'espère que quelqu'un pourra m'éduquer. Je vois des exemples sur le réseau de personnes qui lancent la valeur de retour de dlsym, par exemple ici: daniweb.com/forums/thread62561.html
JasonWoof
9
Notez ce que POSIX dit dans la section sur les types de données : §2.12.3 Types de pointeurs. Tous les types de pointeur de fonction doivent avoir la même représentation que le pointeur de type void. La conversion d'un pointeur de fonction en void *ne doit pas modifier la représentation. Une void *valeur résultant d'une telle conversion peut être reconvertie dans le type de pointeur de fonction d'origine, à l'aide d'un cast explicite, sans perte d'informations. Remarque : la norme ISO C ne l'exige pas, mais elle est requise pour la conformité POSIX.
Jonathan Leffler
2
c'est la question dans la section A PROPOS de ce site .. :) :) Rendez-vous question ici
ZooZ
1
@KeithThompson: le monde change - et POSIX aussi. Ce que j'ai écrit en 2012 ne s'applique plus en 2018. Le standard POSIX a changé le verbiage. Il est maintenant associé à dlsym()- notez la fin de la section «Utilisation de l'application» où il est dit: Notez que la conversion d'un void *pointeur en un pointeur de fonction comme dans: fptr = (int (*)(int))dlsym(handle, "my_function"); n'est pas définie par la norme ISO C. Cette norme requiert que cette conversion fonctionne correctement sur les implémentations conformes.
Jonathan Leffler

Réponses:

171

Une architecture n'a pas à stocker le code et les données dans la même mémoire. Avec une architecture Harvard, le code et les données sont stockés dans une mémoire complètement différente. La plupart des architectures sont des architectures Von Neumann avec du code et des données dans la même mémoire, mais C ne se limite pas à certains types d'architectures si possible.

Dirk Holsopple
la source
15
En outre, même si le code et les données sont stockés au même endroit dans le matériel physique, les logiciels et l'accès à la mémoire empêchent souvent d'exécuter les données sous forme de code sans "approbation" du système d'exploitation. DEP et autres.
Michael Graczyk
15
Au moins aussi important que d'avoir des espaces d'adressage différents (peut-être plus important) est que les pointeurs de fonction peuvent avoir une représentation différente de celle des pointeurs de données.
Michael Burr
14
Il n'est même pas nécessaire d'avoir une architecture Harvard pour avoir des pointeurs de code et de données utilisant des espaces d'adressage différents - l'ancien modèle de mémoire DOS "Small" le faisait (près des pointeurs avec CS != DS).
caf
1
même les processeurs modernes auraient du mal avec un tel mélange car les instructions et le cache de données sont généralement traités séparément, même lorsque le système d'exploitation vous permet d'écrire du code quelque part.
PypeBros du
3
@EricJ. Jusqu'à ce que vous appeliez VirtualProtect, ce qui vous permet de marquer des régions de données comme exécutables.
Dietrich Epp
37

Certains ordinateurs ont (avaient) des espaces d'adressage séparés pour le code et les données. Sur un tel matériel, cela ne fonctionne tout simplement pas.

Le langage est conçu non seulement pour les applications de bureau actuelles, mais pour lui permettre d'être implémenté sur un grand nombre de matériels.


Il semble que le comité du langage C n'ait jamais voulu void*être un pointeur vers une fonction, il voulait juste un pointeur générique vers des objets.

La justification du C99 dit:

6.3.2.3 Les pointeurs
C ont maintenant été implémentés sur une large gamme d'architectures. Alors que certaines de ces architectures comportent des pointeurs uniformes qui ont la taille d'un certain type entier, le code portable au maximum ne peut pas supposer la correspondance nécessaire entre les différents types de pointeurs et les types entiers. Sur certaines implémentations, les pointeurs peuvent même être plus larges que n'importe quel type entier.

L'utilisation de void*(«pointeur vers void») comme type de pointeur d'objet générique est une invention du Comité C89. L'adoption de ce type a été stimulée par le désir de spécifier des arguments de prototype de fonction qui convertissent discrètement des pointeurs arbitraires (comme dans fread) ou se plaignent si le type d'argument ne correspond pas exactement (comme dans strcmp). Rien n'est dit sur les pointeurs vers des fonctions, qui peuvent être incommensurables avec les pointeurs d'objet et / ou les entiers.

Remarque Rien n'est dit sur les pointeurs vers des fonctions dans le dernier paragraphe. Ils peuvent être différents des autres indicateurs, et le comité en est conscient.

Bo Persson
la source
La norme pourrait les rendre compatibles sans déranger cela en rendant simplement les types de données de la même taille et en garantissant que leur attribution à un puis à nouveau aboutira à la même valeur. Ils le font avec void *, qui est le seul type de pointeur compatible avec tout.
Edward Strange
15
@CrazyEddie Vous ne pouvez pas affecter un pointeur de fonction à un void *.
ouah
4
Je pourrais me tromper en acceptant les pointeurs de fonction void *, mais le point demeure. Les bits sont des bits. La norme pourrait exiger que la taille des différents types puisse accueillir les données les unes des autres et l'affectation serait garantie de fonctionner même si elles sont utilisées dans des segments de mémoire différents. La raison pour laquelle cette incompatibilité existe est que cela n'est PAS garanti par la norme et que des données peuvent donc être perdues dans l'affectation.
Edward Strange
5
Mais exiger sizeof(void*) == sizeof( void(*)() )gaspillerait de l'espace dans le cas où les pointeurs de fonction et les pointeurs de données sont de tailles différentes. C'était un cas courant dans les années 80, lorsque la première norme C a été écrite.
Robᵩ
8
@RichardChambers: Les différents espaces d'adressage peuvent également avoir des largeurs d' adresse différentes , comme un AVR Atmel qui utilise 16 bits pour les instructions et 8 bits pour les données; dans ce cas, il serait difficile de convertir des pointeurs de données (8 bits) en pointeurs de fonction (16 bits) et inversement. C est censé être facile à implémenter; une partie de cette facilité vient du fait que les données et les pointeurs d'instructions sont incompatibles les uns avec les autres.
John Bode
30

Pour ceux qui se souviennent de MS-DOS, de Windows 3.1 et des versions antérieures, la réponse est assez simple. Tous ces éléments étaient utilisés pour prendre en charge plusieurs modèles de mémoire différents, avec différentes combinaisons de caractéristiques pour les pointeurs de code et de données.

Ainsi par exemple pour le modèle Compact (petit code, grandes données):

sizeof(void *) > sizeof(void(*)())

et inversement dans le modèle Medium (grand code, petites données):

sizeof(void *) < sizeof(void(*)())

Dans ce cas, vous n'aviez pas de stockage séparé pour le code et la date, mais vous ne pouviez toujours pas convertir entre les deux pointeurs (à moins d'utiliser des modificateurs __near et __far non standard).

De plus, il n'y a aucune garantie que même si les pointeurs sont de la même taille, ils pointent vers la même chose - dans le modèle de mémoire DOS Small, le code et les données sont utilisés près des pointeurs, mais ils pointent vers des segments différents. Ainsi, la conversion d'un pointeur de fonction en pointeur de données ne vous donnerait pas un pointeur ayant une quelconque relation avec la fonction, et par conséquent, une telle conversion n'était pas utile.

Tomek
la source
Re: "convertir un pointeur de fonction en pointeur de données ne vous donnerait pas un pointeur qui ait une quelconque relation avec la fonction, et donc il n'y avait aucune utilité pour une telle conversion": Cela ne suit pas entièrement. La conversion d'un an int*en un void*vous donne un pointeur avec lequel vous ne pouvez vraiment rien faire, mais il est toujours utile de pouvoir effectuer la conversion. (C'est parce que void*peut stocker n'importe quel pointeur d'objet, donc peut être utilisé pour des algorithmes génériques qui n'ont pas besoin de savoir quel type ils contiennent. La même chose pourrait être utile pour les pointeurs de fonction aussi, si elle était autorisée.)
ruakh
4
@ruakh: Dans le cas de la conversion de int *vers void *, il void *est garanti qu'il pointe au moins vers le même objet que l'original int *- cela est donc utile pour les algorithmes génériques qui accèdent à l'objet pointé, comme int n; memcpy(&n, src, sizeof n);. Dans le cas où la conversion d'un pointeur de fonction en a void *ne donne pas un pointeur pointant sur la fonction, ce n'est pas utile pour de tels algorithmes - la seule chose que vous pouvez faire est de reconvertir le void *retour en pointeur de fonction, vous pouvez donc Eh bien, utilisez simplement un pointeur unioncontenant une void *fonction et.
caf
@caf: Très bien. Merci d'avoir fait remarquer cela. Et d'ailleurs, même si le void* fait pointer vers la fonction, je suppose que ce serait une mauvaise idée pour les gens de la transmettre memcpy. :-P
ruakh
Copié ci-dessus: Notez ce que POSIX dit dans Types de données : §2.12.3 Types de pointeurs. Tous les types de pointeur de fonction doivent avoir la même représentation que le pointeur de type void. La conversion d'un pointeur de fonction en void *ne doit pas modifier la représentation. Une void *valeur résultant d'une telle conversion peut être reconvertie dans le type de pointeur de fonction d'origine, à l'aide d'un cast explicite, sans perte d'informations. Remarque : la norme ISO C ne l'exige pas, mais elle est requise pour la conformité POSIX.
Jonathan Leffler du
@caf Si elle doit juste être transmise à un rappel qui connaît le type approprié, je ne suis intéressé que par la sécurité aller-retour, pas par toute autre relation que ces valeurs converties pourraient avoir.
Deduplicator
23

Les pointeurs vers void sont censés pouvoir accueillir un pointeur vers n'importe quel type de données - mais pas nécessairement un pointeur vers une fonction. Certains systèmes ont des exigences différentes pour les pointeurs vers des fonctions et les pointeurs vers des données (par exemple, il existe des DSP avec un adressage différent pour les données par rapport au code, le modèle moyen sur MS-DOS utilise des pointeurs 32 bits pour le code mais uniquement des pointeurs 16 bits pour les données) .

Jerry Coffin
la source
1
mais alors la fonction dlsym () ne devrait pas renvoyer autre chose qu'un void *. Je veux dire, si le vide * n'est pas assez grand pour le pointeur de fonction, ne sommes-nous pas déjà fubared?
Manav
1
@Knickerkicker: Oui, probablement. Si ma mémoire est bonne, le type de retour de dlsym a été longuement discuté, il y a probablement 9 ou 10 ans, sur la liste de diffusion d'OpenGroup. Par contre, je ne me souviens pas de ce qui (le cas échéant) en est sorti.
Jerry Coffin
1
vous avez raison. Cela semble un résumé assez agréable (bien que dépassé) de votre point.
Manav
2
@LegoStormtroopr: Il est intéressant de voir comment 21 personnes sont d'accord avec l' idée du vote ascendant, mais seulement environ 3 l'ont fait. :-)
Jerry Coffin
13

En plus de ce qui est déjà dit ici, il est intéressant de regarder POSIX dlsym():

La norme ISO C n'exige pas que les pointeurs vers les fonctions puissent être convertis en pointeurs vers des données. En effet, la norme ISO C n'exige pas qu'un objet de type void * puisse contenir un pointeur vers une fonction. Les implémentations prenant en charge l'extension XSI, cependant, nécessitent qu'un objet de type void * puisse contenir un pointeur vers une fonction. Le résultat de la conversion d'un pointeur vers une fonction en un pointeur vers un autre type de données (à l'exception de void *) n'est cependant toujours pas défini. Notez que les compilateurs conformes à la norme ISO C sont tenus de générer un avertissement si une conversion d'un pointeur void * en un pointeur de fonction est tentée comme dans:

 fptr = (int (*)(int))dlsym(handle, "my_function");

En raison du problème noté ici, une version future peut soit ajouter une nouvelle fonction pour renvoyer des pointeurs de fonction, soit l'interface actuelle peut être déconseillée au profit de deux nouvelles fonctions: l'une qui renvoie des pointeurs de données et l'autre qui renvoie des pointeurs de fonction.

Maxim Egorushkin
la source
cela signifie-t-il que l'utilisation de dlsym pour obtenir l'adresse d'une fonction est actuellement dangereuse? Existe-t-il actuellement un moyen sûr de le faire?
gexicide
4
Cela signifie qu'actuellement, POSIX exige d'une plate-forme ABI que les pointeurs de fonction et de données puissent être transtypés en toute sécurité void*.
Maxim Egorushkin
@gexicide Cela signifie que les implémentations qui sont conformes à POSIX ont fait une extension au langage, donnant une signification définie par l'implémentation à ce qui est un comportement indéfini par le standard lui-même. Il est même répertorié comme l'une des extensions courantes de la norme C99, section J.5.7 Casts de pointeur de fonction.
David Hammen du
1
@DavidHammen Ce n'est pas une extension du langage, mais plutôt une nouvelle exigence supplémentaire. C n'a pas besoin void*d'être compatible avec un pointeur de fonction, contrairement à POSIX.
Maxim Egorushkin
9

C ++ 11 a une solution à la discordance de longue date entre C / C ++ et POSIX en ce qui concerne dlsym(). On peut utiliser reinterpret_castpour convertir un pointeur de fonction vers / à partir d'un pointeur de données tant que l'implémentation prend en charge cette fonctionnalité.

D'après la norme, 5.2.10 par. 8, "la conversion d'un pointeur de fonction en un type de pointeur d'objet ou vice versa est conditionnellement prise en charge." 1.3.5 définit "pris en charge conditionnellement" comme une "construction de programme qu'une implémentation n'est pas tenue de prendre en charge".

David Hammen
la source
On peut, mais on ne devrait pas. Un compilateur conforme doit générer un avertissement pour cela (qui à son tour devrait déclencher une erreur, cf. -Werror). Une meilleure solution (et non-UB) est de récupérer un pointeur vers l'objet renvoyé par dlsym(ie void**) et de le convertir en un pointeur en pointeur de fonction . Toujours défini par l'implémentation mais ne provoque plus d'avertissement / d'erreur .
Konrad Rudolph du
3
@KonradRudolph: Pas d'accord. Le libellé «sous condition» a été spécifiquement écrit pour permettre dlsymet GetProcAddresscompiler sans avertissement.
MSalters le
@MSalters Que voulez-vous dire par «pas d'accord»? Soit j'ai raison, soit j'ai tort. La documentation de dlsym dit explicitement que «les compilateurs conformes à la norme ISO C doivent générer un avertissement si une conversion d'un pointeur void * en un pointeur de fonction est tentée». Cela ne laisse pas beaucoup de place à la spéculation. Et GCC (avec -pedantic) n'avertit. Encore une fois, aucune spéculation n'est possible.
Konrad Rudolph du
1
Suivi: je pense que maintenant je comprends. Ce n'est pas UB. C'est défini par l'implémentation. Je ne sais toujours pas si l'avertissement doit être généré ou non - probablement pas. Tant pis.
Konrad Rudolph du
2
@KonradRudolph: Je suis en désaccord avec votre "ne devrait pas", qui est une opinion. La réponse mentionnait spécifiquement C ++ 11, et j'étais membre du CWG C ++ au moment où le problème a été résolu. C99 a en effet un libellé différent, le support conditionnel est une invention C ++.
MSalters le
7

En fonction de l'architecture cible, le code et les données peuvent être stockés dans des zones de mémoire fondamentalement incompatibles et physiquement distinctes.

Graham Borland
la source
«physiquement distinct», je comprends, mais pouvez-vous élaborer davantage sur la distinction «fondamentalement incompatible». Comme je l'ai dit dans la question, un pointeur vide n'est-il pas censé être aussi grand que n'importe quel type de pointeur - ou est-ce une mauvaise présomption de ma part.
Manav
@KnickerKicker: void *est assez grand pour contenir n'importe quel pointeur de données, mais pas nécessairement n'importe quel pointeur de fonction.
éphémient
1
retour vers le futur: P
SSpoke
5

undefined ne signifie pas nécessairement non autorisé, cela peut signifier que l'implémenteur du compilateur a plus de liberté pour le faire comme il le souhaite.

Par exemple, cela peut ne pas être possible sur certaines architectures - undefined leur permet de conserver une bibliothèque 'C' conforme même si vous ne pouvez pas le faire.

Martin Beckett
la source
5

Une autre solution:

En supposant que POSIX garantit que les pointeurs de fonction et de données ont la même taille et la même représentation (je ne trouve pas le texte pour cela, mais l'exemple OP cité suggère qu'ils avaient au moins l' intention de faire cette exigence), ce qui suit devrait fonctionner:

double (*cosine)(double);
void *tmp;
handle = dlopen("libm.so", RTLD_LAZY);
tmp = dlsym(handle, "cos");
memcpy(&cosine, &tmp, sizeof cosine);

Cela évite de violer les règles d'aliasing en passant par la char []représentation, qui est autorisée à aliaser tous les types.

Encore une autre approche:

union {
    double (*fptr)(double);
    void *dptr;
} u;
u.dptr = dlsym(handle, "cos");
cosine = u.fptr;

Mais je recommanderais l' memcpyapproche si vous voulez absolument 100% correct C.

R .. GitHub STOP AIDING ICE
la source
5

Ils peuvent être de différents types avec des exigences d'espace différentes. L'affectation à un peut découper de manière irréversible la valeur du pointeur de sorte que l'assignation en retour aboutisse à quelque chose de différent.

Je pense qu'ils peuvent être de types différents parce que la norme ne veut pas limiter les implémentations possibles qui économisent de l'espace lorsque ce n'est pas nécessaire ou lorsque la taille pourrait obliger le processeur à faire des conneries supplémentaires pour l'utiliser, etc.

Edward étrange
la source
3

La seule solution vraiment portable consiste à ne pas utiliser dlsympour les fonctions, mais plutôt à utiliser dlsympour obtenir un pointeur vers des données contenant des pointeurs de fonction. Par exemple, dans votre bibliothèque:

struct module foo_module = {
    .create = create_func,
    .destroy = destroy_func,
    .write = write_func,
    /* ... */
};

puis dans votre application:

struct module *foo = dlsym(handle, "foo_module");
foo->create(/*...*/);
/* ... */

Incidemment, c'est de toute façon une bonne pratique de conception, et il est facile de prendre en charge à la fois le chargement dynamique via dlopenet la liaison statique de tous les modules sur des systèmes qui ne prennent pas en charge la liaison dynamique, ou lorsque l'intégrateur utilisateur / système ne souhaite pas utiliser de liaison dynamique.

R .. GitHub STOP AIDING ICE
la source
2
Agréable! Bien que je convienne que cela semble plus facile à maintenir, il n'est toujours pas évident (pour moi) comment je martèle sur les liens statiques en plus de cela. Peux-tu élaborer?
Manav
2
Si chaque module a sa propre foo_modulestructure (avec des noms uniques), vous pouvez simplement créer un fichier supplémentaire avec un tableau de struct { const char *module_name; const struct module *module_funcs; }et une fonction simple pour rechercher dans cette table le module que vous voulez "charger" et renvoyer le bon pointeur, puis utilisez ceci à la place de dlopenet dlsym.
R .. GitHub STOP HELPING ICE
@R .. C'est vrai, mais cela ajoute des coûts de maintenance en ayant à maintenir la structure du module.
user877329
3

Un exemple moderne où les pointeurs de fonction peuvent différer en taille des pointeurs de données: pointeurs de fonction membre de classe C ++

Directement cité de https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/

class Base1 { int b1; void Base1Method(); };
class Base2 { int b2; void Base2Method(); };
class Derived : public Base1, Base2 { int d; void DerivedMethod(); };

Il y a maintenant deux thispointeurs possibles .

Un pointeur vers une fonction membre de Base1peut être utilisé comme pointeur vers une fonction membre de Derived, car ils utilisent tous les deux le même this pointeur. Mais un pointeur vers une fonction membre de Base2ne peut pas être utilisé tel quel en tant que pointeur vers une fonction membre de Derived, car le this pointeur doit être ajusté.

Il existe de nombreuses façons de résoudre ce problème. Voici comment le compilateur Visual Studio décide de le gérer:

Un pointeur vers une fonction membre d'une classe à héritage multiple est en réalité une structure.

[Address of function]
[Adjustor]

La taille d'un pointeur vers une fonction membre d'une classe qui utilise l'héritage multiple est la taille d'un pointeur plus la taille d'un size_t.

tl; dr: Lors de l'utilisation de l'héritage multiple, un pointeur vers une fonction membre peut (selon le compilateur, la version, l'architecture, etc.) être stocké comme

struct { 
    void * func;
    size_t offset;
}

qui est évidemment plus grand qu'un void *.

Andrew Sun
la source
2

Sur la plupart des architectures, les pointeurs vers tous les types de données normaux ont la même représentation, donc la conversion entre les types de pointeurs de données est un no-op.

Cependant, il est concevable que les pointeurs de fonction nécessitent une représentation différente, peut-être qu'ils sont plus grands que les autres pointeurs. Si void * pouvait contenir des pointeurs de fonction, cela signifierait que la représentation de void * devrait avoir la plus grande taille. Et tous les moulages de pointeurs de données vers / depuis void * devraient effectuer cette copie supplémentaire.

Comme quelqu'un l'a mentionné, si vous en avez besoin, vous pouvez y parvenir en utilisant un syndicat. Mais la plupart des utilisations de void * sont juste pour les données, il serait donc onéreux d'augmenter toute leur utilisation de la mémoire juste au cas où un pointeur de fonction doit être stocké.

Barmar
la source
-1

Je sais que cela n'a pas été commenté depuis 2012, mais je pensais que ce serait utile d'ajouter que je ne sais une architecture qui a très pointeurs incompatibles pour les données et les fonctions depuis un appel en ce privilège vérifie l' architecture et porte des informations supplémentaires. Aucune quantité de casting n'aidera. C'est le moulin .

phorgan1
la source
Cette réponse est fausse. Vous pouvez par exemple convertir un pointeur de fonction en pointeur de données et lire à partir de celui-ci (si vous disposez des autorisations de lecture à partir de cette adresse, comme d'habitude). Le résultat a autant de sens que, par exemple, sur x86.
Manuel Jacob