2015-02-18 20 views
10

Sto attraversando un aumento di interesse nel sistema di tipi variati di C99. Questa domanda è stata ispirata da this one.Compatibilità dei tipi modificati e le relative implicazioni sulla sicurezza

Controllando il codice da questa domanda, ho scoperto qualcosa di interessante. Considerate questo codice:

int myFunc(int, int, int, int[][100]); 

int myFunc(int a, int b, int c, int d[][200]) { 
    /* Some code here... */ 
} 

Questo ovviamente non lo farà (e non) la compilazione. Tuttavia, questo codice:

int myFunc(int, int, int, int[][100]); 

int myFunc(int a, int b, int c, int d[][c]) { 
    /* Some code here... */ 
} 

compila senza nemmeno un avviso (su gcc).

Ciò sembra implicare che un tipo di matrice variabilmente modificato sia compatibile con qualsiasi tipo di matrice non variabile in modo variabile!

Ma non è tutto. Ci si aspetterebbe che un tipo variabilmente modificato si preoccupasse almeno di quale variabile è usata per impostare le sue dimensioni. Ma non sembra farlo!

int myFunc(int, int b, int, int[][b]); 

int myFunc(int a, int b, int c, int d[][c]) { 
    return 0; 
} 

Compilare anche senza errori.

Quindi, la mia domanda è: è questo comportamento standard corretto?

Inoltre, se un tipo di matrice variabilmente modificato sarebbe realmente compatibile con qualsiasi array che abbia le stesse dimensioni, non significherebbe problemi di sicurezza brutti? Ad esempio, prendere in considerazione il seguente codice:

int myFunc(int a, int b, int c, int d[][c]) { 
    printf("%d\n", sizeof(*d)/sizeof((*d)[0])); 
    return 0; 
} 

int main(){ 
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
    myFunc(0, 0, 100, &arr); 

    return 0; 
} 

Compilazioni e uscite 100, nessun errore o avviso, nulla. Per come la vedo io, ciò significa facile scrittura di array out-of-bounds anche se si sta controllando rigorosamente la dimensione dell'array via sizeof, non facendo un singolo cast e si hanno anche tutti gli avvisi attivati! O mi sta sfuggendo qualcosa?

+0

Se non lo hai già fatto, prova ad aggiungere -std = c99 -pedantic-errors alla tua riga di compilazione gcc e vedere se questo fa alcuna differenza. – jschultz410

+0

@ jschultz410: buona idea, ma no-non fa alcuna differenza = ( – Mints97

+0

Ci sono molti casi in cui sarebbe impossibile per il compilatore dedurre staticamente il valore di c (ad esempio, c è input da stdin). sarebbe spesso impossibile eseguire qualsiasi tipo di controllo statico di tipo significativo sui parametri di tale definizione di funzione.Sembra che se si esegue questa operazione, il compilatore sta dicendo "OK, ti permetterò di passare tutto ciò che vuoi come d, quindi per quanto il suo tipo sia una matrice di indici doppiamente indicizzata. Buona fortuna! " – jschultz410

risposta

4

C99, sezione 6.7.5.2 sembra essere il luogo in cui vengono fornite le norme pertinenti. In particolare,

Linea 6:

Per due tipi di matrice siano compatibili, sia deve avere tipi di elementi compatibili, e se entrambe specificatori di formato sono presenti, e sono interi espressioni costanti, allora entrambe specificatori formato è hanno lo stesso valore costante.Se i due tipi di array vengono utilizzati in un contesto che richiede che siano compatibili, è un comportamento non definito se i due identificatori di dimensione valutano valori non uguali.

Un precedente, risposta ora eliminati anche fatto riferimento linea 6. Commento quella risposta ha sostenuto che la seconda frase è stata subordinata alla condizione, alla fine del primo, ma che sembra una lettura improbabile. Esempio 3 di tale sezione può chiarire (estratto):

int c[n][n][6][m]; 
int (*r)[n][n][n+1]; 
r=c; // compatible, but defined behavior only if 
     // n == 6 and m == n+1 

Ciò sembra paragonabile all'esempio in questione: due tipi di matrice, uno avente una dimensione costante e l'altro avente una dimensione variabile corrispondente, e richiesto di essere compatibile. Il comportamento non è definito (per commento nell'esempio 3 e una lettura ragionevole di 6.7.5.2/6) quando in fase di esecuzione la dimensione variabile differisce dalla dimensione della costante di compilazione. E non è un comportamento indefinito cosa ti aspetteresti comunque? Altrimenti perché sollevare la domanda?

Supponiamo di essere d'accordo sul fatto che il comportamento non è definito quando si verifica tale discrepanza, osservo che i compilatori non sono generalmente tenuti a riconoscere un comportamento indefinito o non definito, né a rilasciare alcun tipo di diagnosi se lo riconoscono. Spero in questo caso che il compilatore sia in grado di mettere in guardia sul comportamento possibilmente non definito, ma deve compilare correttamente il codice perché è sintatticamente corretto e soddisfa tutti i vincoli applicabili. Si noti che un compilatore in grado di avvertire su tali usi potrebbe non farlo per impostazione predefinita.

+0

Grazie, è un'ottima risposta! Penso che tu abbia ragione, la mia lettura di questa regola era probabilmente errata. Probabilmente ero troppo abituato a digitare l'incompatibilità essendo UB causa Number One ... Ciò lascia lo standard C in chiaro, ma non gcc =) in ogni caso, farneticare su di esso senza dare avvertimenti probabilmente non farà molto bene ... – Mints97

+0

E se toccava a me dichiarare esplicitamente i tipi di VM incompatibili con tutto solo per essere sicuri =) ha senso, IMO. – Mints97

+1

Ma i @Mint, tipi di VM devono essere compatibili almeno con se stessi, altrimenti sarebbero inutilizzabili. È comunque possibile avere un comportamento non definito con tipi di macchine virtuali che hanno esattamente lo stesso dichiaratore, tuttavia, quando in fase di runtime le dimensioni variabili differiscono. Considerato questo problema, cosa si ottiene rendendo i tipi di macchine virtuali automaticamente incompatibili con tutto il resto?Questo è un altro esempio di C che fornisce al programmatore potenti strumenti, con cui può provocare danni potenti. C non è per weenies. (Non implica nulla lì.) –

-1
#include <stdio.h> 

void foo(int c, char d[][c]) 
{ 
    fprintf(stdout, "c = %d; d = %p; d + 1 = %p\n", c, d, d + 1); 
} 

int main() 
{ 
    char x[2][4]; 
    char y[3][16]; 
    char (*z)[4] = y; /* Warning: incompatible types */ 

    foo(4, x); 
    foo(16, y); 
    foo(16, x);  /* We are lying about x. What can/should the compiler/code do? */ 
    foo(4, y);   /* We are lying about y. What can/should the compiler/code do? */ 

    return 0; 
} 

Uscite:

c = 4; d = 0x7fff5b295b70; d + 1 = 0x7fff5b295b74 
c = 16; d = 0x7fff5b295b40; d + 1 = 0x7fff5b295b50 
c = 16; d = 0x7fff5b295b70; d + 1 = 0x7fff5b295b80 
c = 4; d = 0x7fff5b295b40; d + 1 = 0x7fff5b295b44 

Così, foo() fa in modo dinamico a capire come fino ad avanzare d basa su c, il tuo codice dimostra anche.

Tuttavia, è spesso impossibile per il compilatore determinare staticamente se/quando si chiama foo() in modo errato. Sembra che se lo fai, allora il compilatore sta dicendo "OK, ti permetterò di passare tutto ciò che vuoi come d, purché il suo tipo sia una matrice di caratteri doppiamente indicizzata. Le operazioni sul puntatore d saranno determinate c. Buona fortuna! "

Cioè, sì, il compilatore spesso non può eseguire il controllo di tipo statico su questi tipi di parametri e quindi lo standard quasi certamente non obbliga i compilatori a catturare tutti i casi in cui è possibile determinare staticamente un'incompatibilità di tipo.

+0

Sì, come 'sizeof (* d)' viene calcolato usando 'c', come è s nella mia domanda =) – Mints97

+0

"Tuttavia, è tipicamente impossibile per il compilatore determinare se/quando si chiama foo() in modo errato". Questo è esattamente il problema. Perché mi è persino permesso di chiamare 'foo' se questo elimina completamente il controllo di tipo statico? Inoltre, è possibile utilizzare il controllo dinamico dei tipi. Un'implementazione VLA dovrebbe mantenere le sue dimensioni come una qualche forma di metadata comunque per chiamate 'sizeof' dinamiche, quindi perché non, ad esempio, segfault o andare su UB se questa dimensione non corrisponde alla dimensione del tipo del parametro che è stato passato come? – Mints97

+2

@ Mints97: C non richiede il controllo dinamico dei tipi. Se la dimensione non corrisponde, il comportamento non è definito; * fa *, come dici tu, "go UB". Un comportamento indefinito non significa che il tuo programma si bloccherà. Significa che il comportamento non è definito. Questo include spesso il programma che sembra "funzionare". –