Domanda

Sto solo cominciando ad avvolgere la mia testa intorno puntatori a funzione in C. Per capire come casting di puntatori a funzione lavori, ho scritto il seguente programma. Si crea in pratica un puntatore a funzione a una funzione che prende un parametro, l'inserisce in un puntatore a funzione con tre parametri, e chiama la funzione, la fornitura di tre parametri. Ero curioso di ciò che sarebbe accaduto:

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

Questa compilato ed eseguito senza errori o avvisi (-Wall gcc su Linux / x86). L'uscita sul mio sistema è il seguente:

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

Quindi, a quanto pare gli argomenti superflui vengono semplicemente scartate silenziosamente.

Ora mi piacerebbe capire cosa sta realmente accadendo qui.

  1. Per quanto riguarda la legalità: Se ho capito la risposta a Casting un puntatore a funzione a un altro tipo correttamente, questo è semplicemente non definito comportamento. Quindi il fatto che questo funziona e produce un risultato ragionevole è solo pura fortuna, giusto? (O simpatia da parte degli scrittori del compilatore)
  2. Perché il GCC non mi avvertire di questo, anche con Wall? E 'questo qualcosa che il compilatore semplicemente non può rilevare? Perché?

Vengo da Java, dove typechecking è molto più rigorosa, in modo da questo comportamento mi ha confuso un po '. Forse sto vivendo uno shock culturale: -).

È stato utile?

Soluzione

I parametri aggiuntivi non vengono scartati. Essi sono correttamente posizionati in pila, come se la chiamata viene effettuata ad una funzione che richiede tre parametri. Tuttavia, dal momento che la funzione si preoccupa solo di un parametro, si guarda solo in cima alla pila e non tocca gli altri parametri.

Il fatto che questa chiamata ha funzionato è pura fortuna, sulla base dei due fatti:

  • il tipo del primo parametro è la stessa per la funzione e il puntatore cast. Se si cambia la funzione di prendere un puntatore a corda e cercare di stampare la stringa, si otterrà un arresto piacevole, dal momento che il codice cercherà di puntatore dereference per indirizzare la memoria 2.
  • la convenzione di chiamata utilizzato per impostazione predefinita è per il chiamante per ripulire lo stack. Se si modifica la convenzione di chiamata, in modo che il chiamato pulisce lo stack, si finirà con il chiamante spingendo tre parametri sullo stack e quindi il chiamato pulizia (o meglio tentare di) un parametro. Ciò probabilmente portare a corruzione dello stack.

Non c'è modo in cui il compilatore può segnalare problemi potenziali come questo per un motivo molto semplice - nel caso generale, non si conosce il valore del puntatore al momento della compilazione, quindi non può valutare ciò a cui punta . Immaginate che i punti di puntatore a funzione a un metodo in una tabella virtuale di classe che si crea in fase di esecuzione? Così, si dice al compilatore è un puntatore ad una funzione con tre parametri, il compilatore ti crederà.

Altri suggerimenti

Se si prende una macchina e lanci come un martello il compilatore prendere in parola che l'auto è un martello, ma questo non girare la macchina in un martello. Il compilatore può avere successo nel usando la macchina per guidare un chiodo, ma che dipende dall'implementazione fortuna. E 'ancora una cosa poco saggia da fare.

  1. Sì, è un comportamento indefinito - tutto può succedere, includendolo appare a "lavorare"

  2. .
  3. Il cast impedisce al compilatore di emettere un avvertimento. Inoltre, i compilatori non hanno alcun obbligo di diagnosticare possibili cause comportamento non definito. La ragione di questo è che o la sua impossibilità di farlo, o che così facendo sarebbe troppo difficile e / o causare a molto in alto.

La peggiore offesa del cast è di lanciare un puntatore di dati ad un puntatore a funzione. E 'peggio del cambiamento firma perché non v'è alcuna garanzia che le dimensioni dei puntatori a funzione e puntatore di dati sono uguali. E contrariamente a un sacco di teorica comportamenti indefiniti, questo si possono incontrare in natura, anche su macchine avanzate (non solo sui sistemi embedded).

È possibile riscontrare diversi puntatori taglia facilmente su piattaforme embedded. Ci sono anche i processori in cui i puntatori di dati e puntatore a funzione affrontano cose diverse (RAM per uno, ROM per l'altro), la cosiddetta architettura Harvard. Su x86 in modalità reale si possono avere 16 bit e 32 bit misti. Watcom-C aveva un modo speciale per extender DOS in cui i puntatori ai dati erano larghi 48 bit. Soprattutto con C si dovrebbe sapere che non tutto è POSIX, come C potrebbe essere l'unica lingua disponibile su hardware esotico.

Alcuni compilatori consentono modelli di memoria misti in cui il codice è garantito essere entro 32 bit e dimensioni dei dati è indirizzabile con puntatori a 64 bit, oppure il contrario.

Modifica:. Conclusione, mai gettato un puntatore di dati ad un puntatore a funzione

Il comportamento è definito dal convenzione di chiamata. Se si utilizza una convenzione di chiamata in cui il chiamante spinge e si apre lo stack, allora avrebbe funzionato bene in questo caso, dal momento che sarebbe solo significa che ci sono un extra pochi byte nello stack durante la chiamata. Non ho gcc a portata di mano in questo momento, ma con la Microsoft compilatore, questo codice:

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

Il seguente assemblaggio viene generato per la chiamata:

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

Nota 12 byte (0Ch) aggiunta alla pila dopo la chiamata. Dopo questo, lo stack è soddisfacente (supponendo che il chiamato è __cdecl in questo caso in modo non tenta di pulire anche lo stack). Ma con il seguente codice:

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

Il add esp,0Ch non viene generato nel complesso. Se il chiamato è __cdecl in questo caso, lo stack sarebbe danneggiato.

  1. Admitedly non lo so per certo, ma sicuramente non voglio approfittare del comportamento se si tratta di fortuna o se è specifica del compilatore.

  2. E non merita un avvertimento perché il cast è esplicito. Con la fusione, si sta informando il compilatore che si conosce meglio. In particolare, si sta lanciando un void*, e come tale si sta dicendo "prendere l'indirizzo rappresentato da questo puntatore, e rendere lo stesso di questo altro puntatore" - il cast informa semplicemente il compilatore che siete sicuri che cosa è all'indirizzo di destinazione è, infatti, lo stesso. Anche se qui, sappiamo che non è corretto.

dovrei rinfrescarmi la memoria del layout binario della convenzione di chiamata ad un certo punto C, ma sono abbastanza sicuro che questo è ciò che sta accadendo:

  • 1: Non è pura fortuna. La convenzione di chiamata C è ben definito, e dati aggiuntivi nello stack non è un fattore per il sito di chiamata, anche se può essere sovrascritto dal chiamato dal il chiamato non sa su di esso.
  • 2: Un cast "duro", usando le parentesi, sta dicendo al compilatore che si sa cosa si sta facendo. Dal momento che tutti i dati necessari è in un'unità di compilazione, il compilatore potrebbe essere abbastanza intelligente per capire che questo è chiaramente illegale, ma designer (s) di C non si è concentrato sulla cattura caso d'angolo scorrettezza verificabile. In parole povere, i trust compilatore che si sa cosa si sta facendo (forse incautamente nel caso di molti programmatori C / C ++!)

Per rispondere alle vostre domande:

  1. fortuna Pure - si potrebbe facilmente calpestare la pila e sovrascrivere il puntatore ritorno al codice in esecuzione successiva. Poiché è stato specificato il puntatore funzione con 3 parametri, e invocato il puntatore alla funzione, i restanti due parametri sono stati 'scartati' e quindi, il comportamento è indefinito. Immaginate se questo 2 ° o 3 ° parametro conteneva un'istruzione binaria, e estratto dallo stack di chiamata di procedura ....

  2. Non v'è alcun avviso come si stesse utilizzando un puntatore void * e il cast. Questo è piuttosto un codice di legittima agli occhi del compilatore, anche se è stato specificato in modo esplicito interruttore -Wall. Il compilatore presuppone che sai quello che stai facendo! Questo è il segreto.

Spero che questo aiuti, I migliori saluti, Tom.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top