Perché i puntatori a funzione e puntatori a dati non sono compatibili in C / C ++?


130

Ho letto che convertire un puntatore a funzione in un puntatore dati e viceversa funziona sulla maggior parte delle piattaforme ma non è garantito che funzioni. Perché è così? Entrambi non dovrebbero essere semplicemente indirizzi nella memoria principale e quindi essere compatibili?


16
Non definito nello standard C, definito in POSIX. Ricorda la differenza.
effimero

Sono un po 'nuovo in questo, ma non dovresti fare il cast sul lato destro del "="? Mi sembra che il problema sia che stai assegnando a un puntatore vuoto. Ma vedo che la pagina man lo fa, quindi spero che qualcuno possa educarmi. Vedo esempi sulla "rete di persone che genera il valore di ritorno da dlsym, ad esempio qui: daniweb.com/forums/thread62561.html
JasonWoof

9
Nota cosa dice POSIX nella sezione Tipi di dati : §2.12.3 Tipi di puntatori. Tutti i tipi di puntatori a funzione devono avere la stessa rappresentazione del puntatore a void. La conversione di un puntatore a funzione void *non deve alterare la rappresentazione. Un void *valore risultante da tale conversione può essere riconvertito nel tipo di puntatore a funzione originale, usando un cast esplicito, senza perdita di informazioni. Nota : lo standard ISO C non lo richiede, ma è necessario per la conformità POSIX.
Jonathan Leffler,

2
questa è la domanda nella sezione CHI SIAMO di questo sito .. :) :) Vedi la tua domanda qui
ZooZ

1
@KeithThompson: il mondo cambia - e anche POSIX. Quello che ho scritto nel 2012 non si applica più nel 2018. Lo standard POSIX ha cambiato la verbosità. Ora è associato a dlsym()- nota la fine della sezione "Uso dell'applicazione" dove dice: Nota che la conversione da un void *puntatore a un puntatore a funzione come in: fptr = (int (*)(int))dlsym(handle, "my_function"); non è definita dallo standard ISO C. Questo standard richiede che questa conversione funzioni correttamente su implementazioni conformi.
Jonathan Leffler,

Risposte:


171

Un'architettura non deve archiviare codice e dati nella stessa memoria. Con un'architettura di Harvard, codice e dati sono archiviati in una memoria completamente diversa. La maggior parte delle architetture sono architetture di Von Neumann con codice e dati nella stessa memoria, ma C non si limita a determinati tipi di architetture, se possibile.


15
Inoltre, anche se il codice e i dati sono archiviati nello stesso posto nell'hardware fisico, l'accesso al software e alla memoria spesso impedisce l'esecuzione dei dati come codice senza "approvazione" del sistema operativo. DEP e simili.
Michael Graczyk,

15
Almeno importante quanto avere spazi di indirizzi diversi (forse più importante) è che i puntatori a funzione possono avere una rappresentazione diversa rispetto ai puntatori a dati.
Michael Burr,

14
Non è nemmeno necessario disporre di un'architettura di Harvard per avere puntatori di codice e dati che utilizzano spazi di indirizzi diversi: il vecchio modello di memoria DOS "Small" ha fatto ciò (vicino a puntatori con CS != DS).
Caf

1
anche i processori moderni avrebbero difficoltà con tale miscela in quanto le istruzioni e la cache dei dati sono in genere gestite separatamente, anche quando il sistema operativo consente di scrivere codice da qualche parte.
PypeBros,

3
@EricJ. Fino alla chiamata VirtualProtect, che consente di contrassegnare le aree di dati come eseguibili.
Dietrich Epp,

37

Alcuni computer hanno (avevano) spazi di indirizzi separati per codice e dati. Su tale hardware non funziona.

Il linguaggio è progettato non solo per le attuali applicazioni desktop, ma per consentirne l'implementazione su un ampio set di hardware.


Sembra che il comitato del linguaggio C non abbia mai voluto void*essere un puntatore per funzionare, volevano solo un puntatore generico agli oggetti.

La logica C99 dice:

6.3.2.3 I puntatori
C sono stati ora implementati su una vasta gamma di architetture. Mentre alcune di queste architetture presentano puntatori uniformi delle dimensioni di un tipo intero, il codice al massimo portatile non può assumere alcuna corrispondenza necessaria tra diversi tipi di puntatore e tipi interi. In alcune implementazioni, i puntatori possono persino essere più larghi di qualsiasi tipo intero.

L'uso di void*("puntatore a void") come tipo di puntatore oggetto generico è un'invenzione del comitato C89. L'adozione di questo tipo è stata stimolata dal desiderio di specificare argomenti prototipo di funzione che convertano silenziosamente puntatori arbitrari (come in fread) o si lamentano se il tipo di argomento non corrisponde esattamente (come in strcmp). Non si dice nulla sui puntatori alle funzioni, che potrebbero essere incompatibili con i puntatori agli oggetti e / o numeri interi.

Nota Non si dice nulla sui puntatori alle funzioni nell'ultimo paragrafo. Potrebbero essere diversi dagli altri suggerimenti, e il comitato ne è consapevole.


Lo standard potrebbe renderli compatibili senza fare confusione con questo semplicemente rendendo i tipi di dati della stessa dimensione e garantendo che l'assegnazione a uno e poi indietro comporterà lo stesso valore. Lo fanno con void *, che è l'unico tipo di puntatore compatibile con tutto.
Edward Strange,

15
@CrazyEddie Non è possibile assegnare un puntatore a una funzione a void *.
ouah,

4
Potrei sbagliarmi nel vuoto * accettando i puntatori a funzione, ma il punto rimane. I bit sono bit. Lo standard potrebbe richiedere che le dimensioni dei diversi tipi siano in grado di accogliere i dati l'uno dall'altro e che l'assegnazione possa essere garantita anche se vengono utilizzati in diversi segmenti di memoria. Il motivo per cui esiste questa incompatibilità è che questo NON è garantito dallo standard e quindi i dati possono essere persi nell'assegnazione.
Edward Strange,

5
Ma richiederebbe sizeof(void*) == sizeof( void(*)() )spreco di spazio nel caso in cui i puntatori a funzione e puntatori a dati abbiano dimensioni diverse. Questo era un caso comune negli anni '80, quando fu scritto il primo standard C.
Robᵩ

8
@RichardChambers: i diversi spazi degli indirizzi possono anche avere diverse larghezze degli indirizzi , come un Atmel AVR che utilizza 16 bit per le istruzioni e 8 bit per i dati; in tal caso, sarebbe difficile convertire da dati (8 bit) a puntatori a funzione (16 bit) e viceversa. C dovrebbe essere facile da implementare; parte di quella facilità deriva dal lasciare incompatibili tra loro dati e puntatori di istruzioni.
John Bode,

30

Per coloro che ricordano MS-DOS, Windows 3.1 e precedenti, la risposta è abbastanza semplice. Tutti utilizzati per supportare diversi modelli di memoria, con diverse combinazioni di caratteristiche per puntatori di codice e dati.

Ad esempio, per il modello Compact (codice piccolo, dati di grandi dimensioni):

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

e viceversa nel modello Medium (codice grande, dati piccoli):

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

In questo caso non avevi spazio di archiviazione separato per codice e data ma non riuscivi ancora a convertire tra i due puntatori (a meno di usare modificatori __near e __far non standard).

Inoltre, non c'è alcuna garanzia che anche se i puntatori hanno le stesse dimensioni, indicano la stessa cosa: nel modello di memoria piccola DOS, sia il codice che i dati usati vicino ai puntatori, ma hanno indicato segmenti diversi. Quindi la conversione di un puntatore a funzione in un puntatore dati non ti darebbe un puntatore che non avesse alcuna relazione con la funzione, e quindi non era utile per tale conversione.


Ri: "la conversione di un puntatore a funzione in un puntatore dati non ti darebbe un puntatore che non avesse alcuna relazione con la funzione, e quindi non era utile per tale conversione": questo non segue del tutto. La conversione di int*in a void*ti dà un puntatore con il quale non puoi davvero fare nulla, ma è comunque utile essere in grado di eseguire la conversione. (Questo perché void*può memorizzare qualsiasi puntatore a oggetto, quindi può essere utilizzato per algoritmi generici che non hanno bisogno di sapere quale tipo trattengono. La stessa cosa potrebbe essere utile anche per i puntatori a funzione, se fosse consentito.)
ruakh

4
@ruakh: Nel caso di conversione int *in void *, void *è garantito che punti almeno allo stesso oggetto dell'originale int *, quindi è utile per algoritmi generici che accedono all'oggetto puntato, come int n; memcpy(&n, src, sizeof n);. Nel caso in cui la conversione di un puntatore a una funzione void *non produca un puntatore che punta alla funzione, non è utile per tali algoritmi - l'unica cosa che potresti fare è convertire di void *nuovo il puntatore a una funzione, quindi potresti bene basta usare un puntatore uniona void *e contenente una funzione.
Caf

@caf: abbastanza giusto. Grazie per la segnalazione. E del resto, anche se l' void* ha fatto il punto alla funzione, suppongo che sarebbe stata una cattiva idea per le persone a passare a memcpy. :-P
ruakh,

Copiato dall'alto: nota cosa dice POSIX in Tipi di dati : §2.12.3 Tipi di puntatore. Tutti i tipi di puntatori a funzione devono avere la stessa rappresentazione del puntatore a void. La conversione di un puntatore a funzione void *non deve alterare la rappresentazione. Un void *valore risultante da tale conversione può essere riconvertito nel tipo di puntatore a funzione originale, usando un cast esplicito, senza perdita di informazioni. Nota : lo standard ISO C non lo richiede, ma è necessario per la conformità POSIX.
Jonathan Leffler,

@caf Se dovesse essere passato a qualche callback che conosce il tipo corretto, sono interessato solo alla sicurezza di andata e ritorno, non a qualsiasi altra relazione che potrebbero avere quei valori convertiti.
Deduplicatore,

23

I puntatori da annullare dovrebbero essere in grado di ospitare un puntatore a qualsiasi tipo di dati, ma non necessariamente un puntatore a una funzione. Alcuni sistemi hanno requisiti diversi per i puntatori alle funzioni rispetto ai puntatori ai dati (ad esempio, ci sono DSP con indirizzamento diverso per dati vs. codice, modello medio su MS-DOS utilizzava puntatori a 32 bit per il codice ma solo puntatori a 16 bit per i dati) .


1
ma allora la funzione dlsym () non dovrebbe restituire qualcosa di diverso da un vuoto *. Voglio dire, se il vuoto * non è abbastanza grande per il puntatore a funzione, non siamo già sfocati?
Manav,

1
@Knickerkicker: Sì, probabilmente. Se la memoria serve, il tipo di ritorno da dlsym è stato discusso a lungo, probabilmente 9 o 10 anni fa, nell'elenco e-mail di OpenGroup. D'altro canto, non ricordo cosa ne sia venuto fuori.
Jerry Coffin,

1
hai ragione. Questo sembra un riassunto abbastanza carino (sebbene obsoleto) del tuo punto.
Manav,


2
@LegoStormtroopr: Interessante come 21 persone sono d'accordo con l' idea di up-voto, ma solo circa 3 hanno effettivamente fatto. :-)
Jerry Coffin

13

Oltre a quanto già detto qui, è interessante guardare POSIX dlsym():

Lo standard ISO C non richiede che i puntatori alle funzioni possano essere trasmessi avanti e indietro ai puntatori ai dati. In effetti, lo standard ISO C non richiede che un oggetto di tipo void * possa contenere un puntatore a una funzione. Le implementazioni che supportano l'estensione XSI, tuttavia, richiedono che un oggetto di tipo void * possa contenere un puntatore a una funzione. Tuttavia, il risultato della conversione di un puntatore in una funzione in un puntatore in un altro tipo di dati (tranne void *) non è ancora definito. Si noti che i compilatori conformi allo standard ISO C sono tenuti a generare un avviso se si tenta una conversione da un puntatore void * a un puntatore a funzione come in:

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

A causa del problema notato qui, una versione futura può aggiungere una nuova funzione per restituire i puntatori a funzione oppure l'interfaccia attuale può essere deprecata a favore di due nuove funzioni: una che restituisce puntatori di dati e l'altra che restituisce puntatori di funzioni.


ciò significa che l'utilizzo di dlsym per ottenere l'indirizzo di una funzione non è attualmente sicuro? Esiste attualmente un modo sicuro per farlo?
gexicide,

4
Significa che attualmente POSIX richiede da una piattaforma ABI che sia i puntatori di funzioni che quelli di dati possano essere trasmessi e trasferiti in modo sicuro void*.
Maxim Egorushkin,

@gexicide Significa che le implementazioni conformi a POSIX hanno esteso il linguaggio, dando un significato definito dall'implementazione a ciò che è un comportamento indefinito per lo standard stesso. È anche elencato come una delle estensioni comuni allo standard C99, sezione J.5.7 Lanci del puntatore a funzione.
David Hammen,

1
@DavidHammen Non è un'estensione della lingua, piuttosto un nuovo requisito aggiuntivo. C non richiede void*di essere compatibile con un puntatore a funzione, mentre POSIX lo fa.
Maxim Egorushkin,

9

C ++ 11 ha una soluzione al disadattamento di vecchia data tra C / C ++ e POSIX per quanto riguarda dlsym(). È possibile utilizzare reinterpret_castper convertire un puntatore a funzione da / a un puntatore dati purché l'implementazione supporti questa funzionalità.

Dallo standard, 5.2.10 para. 8, "la conversione di un puntatore a funzione in un tipo di puntatore oggetto o viceversa è supportata in modo condizionale". 1.3.5 definisce "il supporto condizionato" come un "costrutto di programma che non è richiesto un supporto per l'implementazione".


Uno può, ma non dovrebbe. Un compilatore conforme deve generare un avviso per quello (che a sua volta dovrebbe innescare un errore, cfr -Werror.). Una soluzione migliore (e non UB) consiste nel recuperare un puntatore all'oggetto restituito da dlsym(ovvero void**) e convertirlo in un puntatore in puntatore a funzione . Ancora definito dall'implementazione ma non causa più un avviso / errore .
Konrad Rudolph,

3
@KonradRudolph: non sono d'accordo. La formulazione "supportata condizionalmente" è stata scritta appositamente per consentire dlsyme GetProcAddresscompilare senza preavviso.
Salterio

@MSalters Cosa intendi con "disaccordo"? O ho ragione o torto. La documentazione di dlsym afferma esplicitamente che "i compilatori conformi allo standard ISO C sono tenuti a generare un avviso se si tenta di convertire un puntatore void * in un puntatore a funzione". Questo non lascia molto spazio alla speculazione. E GCC (con -pedantic) fa avvertire. Ancora una volta, nessuna speculazione possibile.
Konrad Rudolph,

1
Seguito: ora penso di aver capito. Non è UB. È definito dall'implementazione. Non sono ancora sicuro se l'avviso debba essere generato o meno - probabilmente no. Oh bene.
Konrad Rudolph,

2
@KonradRudolph: non sono d'accordo con il tuo "non dovrei", che è un'opinione. La risposta menzionava specificamente C ++ 11 e io ero un membro del C ++ CWG al momento in cui il problema era stato risolto. C99 ha infatti una diversa formulazione, supportata in modo condizionale è un'invenzione C ++.
Salterio,

7

A seconda dell'architettura di destinazione, codice e dati possono essere archiviati in aree di memoria sostanzialmente incompatibili e fisicamente distinte.


"fisicamente distinto" capisco, ma puoi approfondire la distinzione "fondamentalmente incompatibile". Come ho detto nella domanda, non è un puntatore vuoto ritenuto grande come qualsiasi tipo di puntatore - o è una presunzione errata da parte mia.
Manav,

@KnickerKicker: void *è abbastanza grande da contenere qualsiasi puntatore di dati, ma non necessariamente qualsiasi puntatore a funzione.
effimero

1
ritorno al futuro: P
SSpoke il

5

undefined non significa necessariamente non permesso, può significare che l'implementatore del compilatore ha più libertà di farlo come vuole.

Ad esempio, potrebbe non essere possibile su alcune architetture: undefined consente loro di avere ancora una libreria 'C' conforme anche se non è possibile farlo.


5

Un'altra soluzione:

Supponendo che POSIX garantisca che i puntatori di funzione e dati abbiano le stesse dimensioni e rappresentazione (non riesco a trovare il testo per questo, ma l'esempio OP citato suggerisce che almeno intendono soddisfare questo requisito), dovrebbe funzionare quanto segue:

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

Questo evita di violare le regole di alias passando attraverso la char []rappresentazione, che è autorizzata ad alias tutti i tipi.

Ancora un altro approccio:

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

Ma consiglierei l' memcpyapproccio se vuoi assolutamente il 100% corretto C.


5

Possono essere di tipi diversi con requisiti di spazio diversi. L'assegnazione a uno può dividere in modo irreversibile il valore del puntatore in modo che l'assegnazione restituisca risultati diversi.

Credo che possano essere di diversi tipi perché lo standard non vuole limitare possibili implementazioni che risparmiano spazio quando non è necessario o quando le dimensioni potrebbero far sì che la CPU debba fare una cazzata extra per usarlo, ecc ...


3

L'unica soluzione veramente portatile non è quella di utilizzare le dlsymfunzioni, ma invece dlsymdi ottenere un puntatore a dati che contengono puntatori di funzioni. Ad esempio, nella tua libreria:

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

e quindi nella tua applicazione:

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

Per inciso, questa è comunque una buona pratica di progettazione e semplifica il supporto sia del caricamento dinamico tramite dlopene del collegamento statico di tutti i moduli su sistemi che non supportano il collegamento dinamico, sia in cui l'utente / integratore di sistema non desidera utilizzare il collegamento dinamico.


2
Bello! Anche se concordo sul fatto che questo sembra più mantenibile, non è ancora ovvio (per me) come martellare il collegamento statico sopra questo. Puoi elaborare?
Manav,

2
Se ogni modulo ha la sua foo_modulestruttura (con nomi univoci), puoi semplicemente creare un file aggiuntivo con un array di struct { const char *module_name; const struct module *module_funcs; }e una semplice funzione per cercare in questa tabella il modulo che vuoi "caricare" e restituire il puntatore giusto, quindi usa questo al posto di dlopene dlsym.
R .. GitHub smette di aiutare ICE il

@R .. Vero, ma aggiunge costi di manutenzione dovendo mantenere la struttura del modulo.
user877329,

3

Un esempio moderno di dove i puntatori a funzione possono differire in dimensioni da puntatori di dati: puntatori a funzioni membro della classe C ++

Citato direttamente da 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(); };

Ora ci sono due possibili thispuntatori.

Un puntatore a una funzione membro di Base1può essere utilizzato come puntatore a una funzione membro di Derived, poiché entrambi utilizzano lo stesso this puntatore. Ma un puntatore a una funzione membro di Base2non può essere usato così com'è come un puntatore a una funzione membro di Derived, poiché il this puntatore deve essere regolato.

Esistono molti modi per risolverlo. Ecco come il compilatore di Visual Studio decide di gestirlo:

Un puntatore a una funzione membro di una classe ereditata moltiplicata è in realtà una struttura.

[Address of function]
[Adjustor]

La dimensione di una funzione da puntatore a membro di una classe che utilizza l'ereditarietà multipla è la dimensione di un puntatore più la dimensione di a size_t.

tl; dr: quando si utilizza l'ereditarietà multipla, un puntatore a una funzione membro può (in base al compilatore, alla versione, all'architettura, ecc.) essere effettivamente archiviato come

struct { 
    void * func;
    size_t offset;
}

che è ovviamente più grande di a void *.


2

Sulla maggior parte delle architetture, i puntatori a tutti i normali tipi di dati hanno la stessa rappresentazione, quindi il cast tra tipi di puntatori di dati non è consentito.

Tuttavia, è concepibile che i puntatori a funzione potrebbero richiedere una rappresentazione diversa, forse sono più grandi di altri puntatori. Se void * potesse contenere puntatori a funzioni, ciò significherebbe che la rappresentazione di void * dovrebbe avere dimensioni maggiori. E tutti i cast di puntatori di dati verso / da void * dovrebbero eseguire questa copia aggiuntiva.

Come qualcuno ha detto, se hai bisogno di questo puoi ottenerlo usando un sindacato. Ma la maggior parte degli usi di void * sono solo per i dati, quindi sarebbe oneroso aumentare tutto il loro uso della memoria nel caso in cui sia necessario memorizzare un puntatore a funzione.


-1

So che questo non è stato commentato dal 2012, ma ho pensato che sarebbe stato utile aggiungere che io faccio sapere un'architettura che ha molto puntatori incompatibili per i dati e le funzioni in quanto una chiamata su quella architettura controlli privilegio e trasporta informazioni extra. Nessuna quantità di casting sarà di aiuto. È il mulino .


Questa risposta è sbagliata Ad esempio, è possibile convertire un puntatore a funzione in un puntatore dati e leggere da esso (se si dispone delle autorizzazioni per leggere da quell'indirizzo, come al solito). Il risultato ha tanto senso quanto ad esempio su x86.
Manuel Jacob,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.