2015-02-18 12 views
10

Przechodzę do zainteresowania zmienionym systemem znaków C99. To pytanie zostało zainspirowane przez this one.Kompatybilność ze zmienionymi typami i ich implikacje związane z bezpieczeństwem

Sprawdzając kod z tego pytania, odkryłem coś interesującego. Rozważ ten kod:

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

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

To oczywiście nie będzie (i nie jest) kompilować. Jednak ten kod:

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

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

kompiluje bez ostrzeżenia (na gcc).

Wydaje się to sugerować, że zmiennie zmodyfikowany typ tablicy jest kompatybilny z dowolnym nie zmodyfikowanym typem tablicy!

Ale to nie wszystko. Można by się spodziewać, że zmieniony typ będzie przynajmniej kłopotał się z użyciem zmiennej do ustawienia jej rozmiaru. Ale tak się nie wydaje!

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

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

Kompiluje również bez żadnego błędu.

Moje pytanie brzmi: czy to prawidłowe ustandaryzowane zachowanie?

Ponadto, jeśli zmiennie zmodyfikowany typ tablicy byłby rzeczywiście zgodny z dowolną tablicą o tych samych wymiarach, czy nie oznaczałoby to nieprzyjemnych problemów z bezpieczeństwem? Na przykład rozważ następujący kod:

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

Kompiluje i przekazuje 100, bez błędów i ostrzeżeń, nic. Jak widzę, oznacza to łatwe zapisywanie poza tablicą, nawet jeśli dokładnie sprawdzasz rozmiar tablicy poprzez sizeof, nie wykonując pojedynczego rzutowania, a nawet wszystkie ostrzeżenia są włączone! Czy może czegoś brakuje?

+0

Jeśli jeszcze tego nie zrobiłeś, spróbuj dodać -std = c99 -pedantic-errors do linii kompilacji gcc i sprawdź, czy to ma znaczenie. – jschultz410

+0

@ jschultz410: dobry pomysł, ale nie - nie robi żadnej różnicy = ( – Mints97

+0

Istnieje wiele przypadków, w których kompilator nie mógłby statycznie wywnioskować wartości c (np. - c jest wejściem ze stdin). często niemożliwe byłoby wykonanie jakiegokolwiek sensownego sprawdzania typu statycznego przy takich parametrach definicji funkcji. Wydaje się, że jeśli to zrobisz, kompilator mówi "OK, pozwolę ci przekazać cokolwiek chcesz jako d, więc o ile jego typem jest podwójnie indeksowana tablica int. Powodzenia! " – jschultz410

Odpowiedz

4

C99, sekcja 6.7.5.2 wydaje się być tam, gdzie podane są odpowiednie zasady. W szczególności

linia 6:

dwóch rodzajów macierzy jest zgodna zarówno mają kompatybilne rodzaje elementów, a jeżeli oba Specyfikatory wymiarach, i są liczbami całkowitymi wyrażeniami, oba Specyfikatory wymiary wynoszą mają taką samą stałą wartość.Jeśli dwa typy tablic są używane w kontekście, który wymaga ich zgodności, jest to niezdefiniowane zachowanie, jeśli specyfikatory o dwóch rozmiarach oceniają nierówne wartości.

Poprzednia, teraz usunięta odpowiedź również odnosiła się do linii 6. Komentarz na tę odpowiedź argumentował, że drugie zdanie było uzależnione od warunku pod koniec pierwszego, ale wydaje się to mało prawdopodobne. Przykład 3 tej części mogą wyjaśnić (fragment)

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 

że wydaje się być podobna do przykładu na pytanie, dwóch rodzajów macierzy, jedną o stałej wielkości i druga mająca odpowiedni wymiar zmienny, i muszą być zgodny. Zachowanie jest niezdefiniowane (na komentarz w przykładzie 3 i jeden rozsądny odczyt 6.7.5.2/6), gdy w środowisku wykonawczym wymiar zmienny różni się od stałego wymiaru czasu kompilacji. I czy nie jest to niezdefiniowane zachowanie, czegokolwiek byś się spodziewał? Po drugie, dlaczego postawić pytanie?

Przypuśćmy, możemy się zgodzić, że zachowanie jest niezdefiniowane, gdy wystąpi taka rozbieżność, zauważam, że kompilatory są na ogół nie wymaga się rozpoznać niezdefiniowany lub ewentualnie-niezdefiniowanej zachowań, ani do wydawania jakichkolwiek diagnostyczny w ogóle, jeśli nie rozpozna takie. Mam nadzieję, że w tym przypadku kompilator będzie mógł ostrzegać o możliwym niezdefiniowanym zachowaniu, ale musi pomyślnie skompilować kod, ponieważ jest poprawny pod względem składni i spełnia wszystkie obowiązujące ograniczenia. Zauważ, że kompilator, który może ostrzegać o takich zastosowaniach, może nie być domyślnie ustawiony jako .

+0

Dzięki, to wspaniała odpowiedź! Myślę, że masz rację, moje odczytanie tej zasady było prawdopodobnie nieprawidłowe. Byłem prawdopodobnie zbytnio przyzwyczajony do wpisywania niezgodności, ponieważ UB powoduje Numer Jeden ... Pozostawia to normę C w jasnym, ale nie gcc =) w każdym razie, ranting o tym, że nie dawanie ostrzeżeń prawdopodobnie nie przyniesie zbyt wiele dobrego ... – Mints97

+0

A jeśli zależało mi tylko na tym, że jawnie zadeklarowałbym, że typy VM są niekompatybilne ze wszystkim tylko po to, by być pewnym =) To ma sens, IMO. – Mints97

+1

Ale @Mints, typy VM muszą być kompatybilne przynajmniej ze sobą, w przeciwnym razie byłyby bezużyteczne. Nadal można zachować niezdefiniowane zachowanie w przypadku maszyn wirtualnych z dokładnie tymi samymi deklaratorami, jednak w czasie wykonywania zmienne wymiary różnią się. Biorąc pod uwagę tę kwestię, co można zyskać, czyniąc typy maszyn wirtualnych automatycznie niekompatybilnymi ze wszystkim innym?Jest to kolejny przykład C, który daje programistom potężne narzędzia, dzięki którym może siać potężne obrażenia. C nie jest dla łasuchów. (Nic nie sugeruje.) –

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

Wyjścia:

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 

Więc foo() jest dynamicznie dowiedzieć się, jak daleko, aby przejść d oparty na C, jak Twój kod demonstruje również.

Jednak często niemożliwe jest, aby kompilator statycznie ustalił, czy/kiedy niepoprawnie wywołuje funkcję foo(). Wydaje się, że jeśli to zrobisz, kompilator mówi "OK, pozwolę ci przekazać cokolwiek chcesz jako d, o ile jego typ jest podwójnie indeksowaną tablicą znaków. Operacje na wskaźniku d zostaną określone przez c. Powodzenia! "

To znaczy, tak, kompilator często nie może wykonywać sprawdzania typu statycznego na tego typu parametrach, więc standard prawie na pewno nie nakazuje kompilatorom wychwycenia wszystkich przypadków, w których możliwe jest statyczne określenie niezgodności typów.

+0

Tak, ponieważ 'sizeof (* d)' jest obliczane przy użyciu 'c', jak to jest s hown in my question =) – Mints97

+0

"Jednak kompilator zwykle nie jest w stanie określić, czy/kiedy niepoprawnie wywołasz funkcję foo()". To jest właśnie problem. Dlaczego mogę nawet nazywać 'foo', jeśli to całkowicie zabija statyczne sprawdzanie typu? Można również zastosować dynamiczne sprawdzanie typu. Implementacja VLA musiałaby mimo to zachować swój rozmiar jako pewną formę metadanych dla dynamicznych wywołań 'sizeof', więc dlaczego nie, powiedzmy, segfault lub przejdź do UB, jeśli ten rozmiar nie jest zgodny z rozmiarem typu parametru, który został przekazany tak jak? – Mints97

+2

@ Mints97: C nie wymaga sprawdzania typu dynamicznego. Jeśli rozmiar nie jest zgodny, zachowanie jest niezdefiniowane; to * robi *, jak mówisz, "idź UB". Niezdefiniowane zachowanie nie oznacza, że ​​twój program się zawiesi. Oznacza to, że zachowanie jest niezdefiniowane. To często obejmuje program wyświetlający się jako "działa". –

Powiązane problemy