Szukaj…


Wprowadzenie

W C niektóre wyrażenia dają niezdefiniowane zachowanie . Standard wyraźnie nie określa, jak powinien się zachowywać kompilator, jeśli napotka takie wyrażenie. W rezultacie kompilator może robić wszystko, co uzna za stosowne, i może dawać użyteczne wyniki, nieoczekiwane wyniki, a nawet awarię.

Kod wywołujący UB może działać zgodnie z przeznaczeniem w określonym systemie z określonym kompilatorem, ale prawdopodobnie nie będzie działał w innym systemie lub z innym kompilatorem, wersją kompilatora lub ustawieniami kompilatora.

Uwagi

Co to jest niezdefiniowane zachowanie (UB)?

Niezdefiniowane zachowanie jest terminem używanym w standardzie C. Norma C11 (ISO / IEC 9899: 2011) definiuje pojęcie zachowania niezdefiniowanego jako

zachowanie, po zastosowaniu niezbywalnej lub błędnej konstrukcji programu lub błędnych danych, dla których niniejszy standard międzynarodowy nie nakłada żadnych wymagań

Co się stanie, jeśli w moim kodzie jest UB?

Oto wyniki, które mogą się zdarzyć z powodu niezdefiniowanego zachowania zgodnie ze standardem:

UWAGA Możliwe niezdefiniowane zachowanie rozciąga się od całkowitego zignorowania sytuacji z nieprzewidywalnymi rezultatami, zachowania podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z wydaniem komunikatu diagnostycznego lub bez niego), aż do zakończenia tłumaczenia lub wykonania (za pomocą wydanie komunikatu diagnostycznego).

Poniższy cytat jest często używany do opisania (choć mniej formalnie) rezultatów wynikających z niezdefiniowanego zachowania:

„Gdy kompilator napotka [dany niezdefiniowany konstrukt], legalne jest, aby demony wyleciały z twojego nosa” (implikacja jest taka, że kompilator może wybrać dowolnie dziwaczny sposób interpretacji kodu bez naruszania standardu ANSI C)

Dlaczego istnieje UB?

Jeśli jest tak źle, dlaczego po prostu go nie zdefiniowali lub nie zdefiniowali pod względem implementacji?

Niezdefiniowane zachowanie daje więcej możliwości optymalizacji; Kompilator może słusznie założyć, że żaden kod nie zawiera nieokreślonego zachowania, co pozwala mu uniknąć kontroli w czasie wykonywania i przeprowadzić optymalizacje, których poprawność byłaby kosztowna lub niemożliwa do udowodnienia w inny sposób.

Dlaczego UB jest trudny do wyśledzenia?

Istnieją co najmniej dwa powody, dla których niezdefiniowane zachowanie powoduje błędy, które są trudne do wykrycia:

  • Kompilator nie jest zobowiązany do - i generalnie nie może niezawodnie - ostrzegać przed niezdefiniowanym zachowaniem. W rzeczywistości wymaganie tego byłoby sprzeczne z przyczyną istnienia nieokreślonego zachowania.
  • Nieprzewidywalne wyniki mogą nie rozpocząć się w dokładnym punkcie operacji, w której występuje konstrukcja, której zachowanie jest niezdefiniowane; Niezdefiniowane zachowanie zabarwia całe wykonanie, a jego efekty mogą wystąpić w dowolnym momencie: w trakcie, po, a nawet przed niezdefiniowanym konstruktem.

Zastanów się nad dereferencją wskaźnika zerowego: kompilator nie jest wymagany do diagnozowania dereferencji wskaźnika zerowego, a nawet nie może, ponieważ w czasie wykonywania dowolny wskaźnik przekazany do funkcji lub w zmiennej globalnej może być pusty. A gdy wystąpi dereferencja wskaźnika zerowego, standard nie nakazuje awarii programu. Zamiast tego program może ulec awarii wcześniej, później lub wcale; może nawet zachowywać się tak, jakby wskaźnik zerowy wskazywał na prawidłowy obiekt, i zachowywać się całkowicie normalnie, tylko w przypadku awarii.

W przypadku dereferencji wskaźnika zerowego język C różni się od języków zarządzanych, takich jak Java lub C #, w których zdefiniowane jest zachowanie dereferencji wskaźnika zerowego: wyjątek jest NullPointerException dokładnie w tym samym czasie ( NullPointerException w Javie, NullReferenceException w C #) , więc osoby pochodzące z Javy lub C # mogą błędnie sądzić, że w takim przypadku program C musi ulec awarii, z wydaniem komunikatu diagnostycznego lub bez niego .

Dodatkowe informacje

Istnieje kilka takich sytuacji, które należy wyraźnie rozróżnić:

  • Jawnie niezdefiniowane zachowanie, czyli tam, gdzie standard C wyraźnie mówi, że jesteś poza limitem.
  • Niejawnie nieokreślone zachowanie, w którym po prostu nie ma tekstu w standardzie, który przewiduje zachowanie dla sytuacji, w której wprowadziłeś swój program.

Należy również pamiętać, że w wielu miejscach zachowanie niektórych konstrukcji jest celowo niezdefiniowane przez standard C, aby pozostawić miejsce dla kompilatorów i implementatorów bibliotek na opracowanie własnych definicji. Dobrym przykładem są sygnały i procedury obsługi sygnałów, w których rozszerzenia C, takie jak standard systemu operacyjnego POSIX, definiują znacznie bardziej rozbudowane reguły. W takich przypadkach wystarczy sprawdzić dokumentację swojej platformy; standard C nie może ci nic powiedzieć.

Zauważ też, że jeśli w programie występuje niezdefiniowane zachowanie, nie oznacza to, że problematyczne jest tylko miejsce, w którym wystąpiło niezdefiniowane zachowanie, a cały program staje się bez znaczenia.

Z powodu takich obaw ważne jest (zwłaszcza, że kompilatory nie zawsze ostrzegają nas przed UB), aby osoba programująca w C była przynajmniej zaznajomiona z rodzajami rzeczy, które wyzwalają niezdefiniowane zachowanie.

Należy zauważyć, że istnieją pewne narzędzia (np. Narzędzia analizy statycznej, takie jak PC-Lint), które pomagają w wykrywaniu nieokreślonego zachowania, ale znowu nie mogą wykryć wszystkich przypadków nieokreślonego zachowania.

Dereferencje wskaźnika zerowego

Jest to przykład dereferencji wskaźnika NULL, co powoduje niezdefiniowane zachowanie.

int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */

Wskaźnik NULL jest gwarantowany przez standard C do porównywania nierówności z dowolnym wskaźnikiem do prawidłowego obiektu, a dereferencje wywołują niezdefiniowane zachowanie.

Modyfikowanie dowolnego obiektu więcej niż jeden raz między dwoma punktami sekwencji

int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;

Taki kod często prowadzi do spekulacji na temat „wynikowej wartości” i . Jednak zamiast określać wynik, standardy C określają, że ocena takiego wyrażenia powoduje niezdefiniowane zachowanie . Przed C2011 norma sformalizowała te zasady pod względem tak zwanych punktów sekwencji :

Pomiędzy poprzednim i następnym punktem sekwencji obiekt skalarny powinien mieć zmodyfikowaną wartość zapisaną co najwyżej raz poprzez ocenę wyrażenia. Ponadto wcześniejszą wartość należy odczytać tylko w celu ustalenia wartości, która ma być przechowywana.

(Norma C99, sekcja 6.5, akapit 2)

Schemat ten okazał się nieco zbyt gruboziarnisty, w wyniku czego niektóre wyrażenia wykazywały niezdefiniowane zachowanie w stosunku do C99, które prawdopodobnie nie powinno tego robić. C2011 zachowuje punkty sekwencji, ale wprowadza bardziej zróżnicowane podejście do tego obszaru w oparciu o sekwencjonowanie i związek, który nazywa „sekwencjonowaniem przed”:

Jeśli efekt uboczny na obiekcie skalarnym nie ma wpływu na inny efekt uboczny na ten sam obiekt skalarny lub obliczenia wartości z wykorzystaniem wartości tego samego obiektu skalarnego, zachowanie jest niezdefiniowane. Jeśli istnieje wiele dopuszczalnych kolejności podwyrażeń wyrażenia, zachowanie jest niezdefiniowane, jeśli taki niepowodowany efekt uboczny wystąpi w którymkolwiek z porządków.

(Norma C2011, sekcja 6.5, akapit 2)

Pełne szczegóły relacji „zsekwencjonowane przed” są zbyt długie, aby je tutaj opisać, ale uzupełniają one punkty sekwencyjne, a nie zastępują je, więc mają wpływ na określenie zachowania dla niektórych ocen, których zachowanie wcześniej było niezdefiniowane. W szczególności, jeśli między dwiema ocenami istnieje punkt sekwencyjny, to ten przed sekwencją sekwencyjnym jest „sekwencjonowany przed” kolejnym.

Poniższy przykład ma dobrze zdefiniowane zachowanie:

int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */

Poniższy przykład ma niezdefiniowane zachowanie:

int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */

Podobnie jak w przypadku każdej innej formy nieokreślonego zachowania, obserwowanie faktycznego zachowania wyrażeń naruszających reguły sekwencjonowania nie ma charakteru informacyjnego, z wyjątkiem retrospektywnego. Standard językowy nie daje podstaw, by oczekiwać, że takie obserwacje będą przewidywać nawet przyszłe zachowanie tego samego programu.

Brak instrukcji return w funkcji zwracającej wartość

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* Trying to use the (not) returned value causes UB */
  int value = foo();
  return 0;
}

Kiedy funkcja deklaruje, że zwraca wartość, musi to zrobić na każdej możliwej ścieżce kodu przez nią. Niezdefiniowane zachowanie występuje, gdy osoba dzwoniąca (która oczekuje wartości zwracanej) próbuje użyć wartości zwracanej 1 .

Zauważ, że niezdefiniowane zachowanie występuje tylko wtedy, gdy dzwoniący próbuje użyć / uzyskać dostęp do wartości z funkcji. Na przykład,

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* The value (not) returned from foo() is unused. So, this program
   * doesn't cause *undefined behaviour*. */
  foo();
  return 0;
}
C99

main() funkcja jest wyjątek od tej reguły, ponieważ jest możliwe, aby była ona rozwiązana bez instrukcji return, ponieważ Zakładana wartość zwracana 0 zostaną automatycznie stosowany w tym przypadku 2.


1 ( ISO / IEC 9899: 201x , 6.9.1 / 12)

Jeśli}, który kończy funkcję, zostanie osiągnięty, a wywołujący użyje wartości wywołania funkcji, zachowanie jest niezdefiniowane.

2 ( ISO / IEC 9899: 201x , 5.1.2.2.3 / 1)

dotarcie do} kończącego główną funkcję zwraca wartość 0.

Przepełnienie ze znakiem całkowitym

Zgodnie z paragrafem 6.5 / 5 zarówno C99, jak i C11, ocena wyrażenia powoduje niezdefiniowane zachowanie, jeśli wynik nie jest reprezentowalną wartością typu wyrażenia. W przypadku typów arytmetycznych nazywa się to przepełnieniem . Arytmetyka liczb całkowitych bez znaku nie przepełnia się, ponieważ stosuje się pkt 6.2.5 / 9, co powoduje, że każdy wynik bez znaku, który w innym przypadku byłby poza zakresem, zostałby zmniejszony do wartości w zakresie. Nie ma jednak analogicznego przepisu dla podpisanych typów liczb całkowitych; mogą one przepełniać, powodując niezdefiniowane zachowanie. Na przykład,

#include <limits.h>      /* to get INT_MAX */

int main(void) {
    int i = INT_MAX + 1; /* Overflow happens here */
    return 0;
}

Większość przypadków tego rodzaju nieokreślonego zachowania jest trudniejsza do rozpoznania lub przewidzenia. Przepełnienie może w zasadzie wynikać z dowolnej operacji dodawania, odejmowania lub mnożenia na liczbach całkowitych ze znakiem (z zastrzeżeniem zwykłych konwersji arytmetycznych), w których nie ma skutecznych ograniczeń lub relacji między operandami, aby temu zapobiec. Na przykład ta funkcja:

int square(int x) {
    return x * x;  /* overflows for some values of x */
}

jest rozsądny i robi to dobrze dla wystarczająco małych wartości argumentów, ale jego zachowanie jest niezdefiniowane dla większych wartości argumentów. Nie można ocenić na podstawie samej funkcji, czy wywołujące ją programy wykazują niezdefiniowane zachowanie. To zależy od tego, jakie argumenty przekazują.

Z drugiej strony, rozważ ten trywialny przykład arytmetyki liczb całkowitych ze znakiem przelewu:

int zero(int x) {
    return x - x;  /* Cannot overflow */
}

Zależność między operandami operatora odejmowania zapewnia, że odejmowanie nigdy się nie przelewa. Lub rozważ ten nieco bardziej praktyczny przykład:

int sizeDelta(FILE *f1, FILE *f2) {
    int count1 = 0;
    int count2 = 0;
    while (fgetc(f1) != EOF) count1++;  /* might overflow */
    while (fgetc(f2) != EOF) count2++;  /* might overflow */

    return count1 - count2; /* provided no UB to this point, will not overflow */
}

Dopóki liczniki nie przepełniają się indywidualnie, operandy ostatecznego odejmowania będą oba nieujemne. Wszystkie różnice między dowolnymi dwiema takimi wartościami są reprezentowane jako int .

Zastosowanie niezainicjowanej zmiennej

int a; 
printf("%d", a);

Zmienna a jest liczbą int z automatycznym czasem przechowywania. Przykład powyższy kod próbuje drukować wartość zmiennej niezainicjowanych ( nigdy nie został zainicjowany). a Zmienne automatyczne, które nie zostały zainicjowane, mają nieokreślone wartości; dostęp do nich może prowadzić do nieokreślonego zachowania.

Uwaga: Zmienne ze statyczną lub wątkową pamięcią lokalną, w tym zmienne globalne bez słowa kluczowego static , są inicjowane na zero lub ich wartość inicjalizowaną. Dlatego następujące są legalne.

static int b;
printf("%d", b);

Bardzo częstym błędem jest nie inicjowanie zmiennych, które służą jako liczniki do 0. Dodajesz do nich wartości, ale ponieważ wartość początkowa to śmieci, wywołasz Niezdefiniowane zachowanie , na przykład w pytaniu Kompilacja na terminalu wyświetla ostrzeżenie wskaźnika i dziwne symbole .

Przykład:

#include <stdio.h>

int main(void) {
    int i, counter;
    for(i = 0; i < 10; ++i)
        counter += i;
    printf("%d\n", counter);
    return 0;
}

Wynik:

C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
        counter += i;
        ^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
    int i, counter;
                  ^
                   = 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812

Powyższe zasady dotyczą również wskaźników. Na przykład następujące powoduje niezdefiniowane zachowanie

int main(void)
{
    int *p;
    p++; // Trying to increment an uninitialized pointer.
}

Zauważ, że powyższy kod sam w sobie nie może powodować błędu lub błędu segmentacji, ale próba późniejszego usunięcia tego wskaźnika spowodowałaby niezdefiniowane zachowanie.

Odsyłanie wskaźnika do zmiennej poza jego okresem użytkowania

int* foo(int bar)
{
    int baz = 6;
    baz += bar;
    return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage   
   * duration (local variables), thus the returned pointer is not valid! */

int main (void)
{
    int* p;

    p = foo(5);  /* (2) this expression's behavior is undefined */
    *p = *p - 6; /* (3) Undefined behaviour here */

    return 0;
}

Niektóre kompilatory pomagają to wskazać. Na przykład gcc ostrzega za pomocą:

warning: function returns address of local variable [-Wreturn-local-addr]

i clang ostrzega:

warning: address of stack memory associated with local variable 'baz' returned 
[-Wreturn-stack-address]

dla powyższego kodu. Ale kompilatory mogą nie być w stanie pomóc w złożonym kodzie.

(1) Zwracanie odniesienia do zmiennej zadeklarowanej jako static jest zdefiniowanym zachowaniem, ponieważ zmienna nie jest niszczona po opuszczeniu bieżącego zakresu.

(2) Zgodnie z ISO / IEC 9899: 2011 6.2.4 § 2 „Wartość wskaźnika staje się nieokreślona, gdy obiekt, na który wskazuje, osiąga koniec swojego życia”.

(3) Odwołanie wskaźnika zwróconego przez funkcję foo jest niezdefiniowanym zachowaniem, ponieważ pamięć, do której się odwołuje, ma nieokreśloną wartość.

Dzielenie przez zero

int x = 0;
int y = 5 / x;  /* integer division */

lub

double x = 0.0;
double y = 5.0 / x;  /* floating point division */

lub

int x = 0;
int y = 5 % x;  /* modulo operation */

Dla drugiej linii w każdym przykładzie, gdzie wartość drugiego argumentu (x) wynosi zero, zachowanie jest niezdefiniowane.

Zauważ, że większość implementacji matematyki zmiennoprzecinkowej będzie zgodna ze standardem (np. IEEE 754), w którym to przypadku operacje takie jak dzielenie przez zero będą miały spójne wyniki (np. INFINITY ), nawet jeśli standard C mówi, że operacja jest niezdefiniowana.

Dostęp do pamięci poza przydzieloną porcją

Wskaźnik do elementu pamięci zawierającego n elementów może być wyłuskany tylko wtedy, gdy znajduje się w memory zakresu i memory + (n - 1) . Odsunięcie wskaźnika poza ten zakres powoduje niezdefiniowane zachowanie. Jako przykład rozważ następujący kod:

int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */

Trzeci wiersz ma dostęp do czwartego elementu w tablicy, która ma tylko 3 elementy, co prowadzi do nieokreślonego zachowania. Podobnie zachowanie drugiego wiersza w poniższym fragmencie kodu również nie jest dobrze zdefiniowane:

int array[3];
array[3] = 0;

Zauważ, że wskazanie ostatniego elementu tablicy nie jest niezdefiniowanym zachowaniem ( beyond_array = array + 3 jest tutaj dobrze zdefiniowanym), ale jest dereferencją ( *beyond_array zachowanie *beyond_array jest niezdefiniowane). Ta reguła dotyczy także dynamicznie alokowanej pamięci (takiej jak bufory tworzone przez malloc ).

Kopiowanie nakładającej się pamięci

Wiele różnych standardowych funkcji bibliotecznych ma wśród swoich efektów kopiowanie sekwencji bajtów z jednego regionu pamięci do drugiego. Większość tych funkcji ma niezdefiniowane zachowanie, gdy regiony źródłowy i docelowy nakładają się.

Na przykład to ...

#include <string.h> /* for memcpy() */

char str[19] = "This is an example";
memcpy(str + 7, str, 10);

... próbuje skopiować 10 bajtów, w których obszary pamięci źródłowej i docelowej nakładają się na trzy bajty. Aby wizualizować:

               overlapping area
               |
               _ _
              |   |
              v   v
T h i s   i s   a n   e x a m p l e \0
^             ^
|             |
|             destination
|
source

Z powodu nakładania się wynikowe zachowanie jest niezdefiniowane.

Wśród standardowych funkcji biblioteki z tego rodzaju ograniczeniami są memcpy() , strcpy() , strcat() , sprintf() i sscanf() . Standard mówi o tych i kilku innych funkcjach:

Jeśli kopiowanie odbywa się między nakładającymi się obiektami, zachowanie jest niezdefiniowane.

Funkcja memmove() jest głównym wyjątkiem od tej reguły. Jego definicja określa, że funkcja zachowuje się tak, jakby dane źródłowe zostały najpierw skopiowane do tymczasowego bufora, a następnie zapisane na adres docelowy. Nie ma wyjątku dla nakładających się regionów źródłowych i docelowych, ani żadnej potrzeby takiego, więc memmove() ma dobrze zdefiniowane zachowanie w takich przypadkach.

To rozróżnienie odzwierciedla efektywność vs. kompromis ogólności. Kopiowanie, takie jak te funkcje, wykonuje się zwykle między rozłącznymi regionami pamięci i często w czasie opracowywania można dowiedzieć się, czy określone wystąpienie kopiowania pamięci będzie należało do tej kategorii. Zakładając, że brak nakładania się daje względnie bardziej wydajne implementacje, które nie dają niezawodnie poprawnych wyników, gdy założenie nie ma zastosowania. Większość funkcji biblioteki C jest dozwolonych w bardziej wydajnych implementacjach, a funkcja memmove() wypełnia luki, obsługując przypadki, w których źródło i miejsce docelowe mogą się pokrywać. Aby jednak uzyskać poprawny efekt we wszystkich przypadkach, musi wykonać dodatkowe testy i / lub zastosować stosunkowo mniej wydajne wdrożenie.

Odczytywanie niezainicjowanego obiektu, który nie jest zabezpieczony pamięcią

C11

Czytanie obiektu spowoduje niezdefiniowane zachowanie, jeśli obiekt ma wartość 1 :

  • niezainicjowany
  • zdefiniowane z automatycznym czasem przechowywania
  • jego adres nigdy nie jest brany

Zmienna a w poniższym przykładzie spełnia wszystkie te warunki:

void Function( void )
{
    int a;
    int b = a;
} 

1 (Cytat z: ISO: IEC 9899: 201X 6.3.2.1 Wartości, tablice i desygnatory funkcji 2)
Jeśli wartość określa obiekt o automatycznym czasie przechowywania, który mógł zostać zadeklarowany za pomocą klasy pamięci rejestru (nigdy nie został pobrany jego adres), a obiekt ten nie został zainicjowany (nie został zadeklarowany za pomocą inicjatora i nie zostało do niego przypisane żadne użycie przed użyciem) ), zachowanie jest niezdefiniowane.

Wyścig danych

C11

C11 wprowadził obsługę wielu wątków wykonywania, co daje możliwość wyścigów danych. Program zawiera wyścig danych, jeśli do dostępnego do niego obiektu prowadzą 1 różne dwa wątki, w którym co najmniej jeden dostęp jest nieatomowy, co najmniej jeden modyfikuje obiekt, a semantyka programu nie zapewnia, że oba dostępu nie mogą się pokrywać tymczasowo. 2 Należy pamiętać, że faktyczna współbieżność dostępu nie jest warunkiem wyścigu danych; Wyścigi danych obejmują szerszą klasę problemów wynikających z (dozwolonych) niespójności w poglądach pamięci różnych wątków.

Rozważ ten przykład:

#include <threads.h>

int a = 0;

int Function( void* ignore )
{
    a = 1;

    return 0;
}

int main( void )
{
    thrd_t id;
    thrd_create( &id , Function , NULL );

    int b = a;

    thrd_join( id , NULL );
}

Główny wątek wywołuje thrd_create celu uruchomienia nowej funkcji uruchamiania wątku Function . Drugi wątek modyfikuje a , a główny wątek czyta a . Żaden z tych dostępów nie jest atomowy, a dwa wątki nic nie robią ani pojedynczo, ani wspólnie, aby zapewnić, że się nie nakładają, więc następuje wyścig danych.

Wśród sposobów, w jakie ten program może uniknąć wyścigu danych, są

  • główny wątek może wykonywać swoje lektury przed rozpoczęciem innego wątku; a
  • główny wątek może wykonywać swoje lektury po zapewnieniu przez a thrd_join że druga zakończona;
  • wątki mogłyby zsynchronizować dostęp przez muteks, z których każdy blokuje ten muteks przed dostępem do a a następnie odblokowuje go.

Jako opcja mutex demonstruje, unikając wyścigu danych nie wymaga zapewnienia porządku specyficznej operacji, takich jak nitki dziecięcej modyfikującego zanim główny wątek czyta; a wystarczy (aby uniknąć wyścigu danych), aby zapewnić, że dla danego wykonania jeden dostęp nastąpi przed drugim.


1 Modyfikowanie lub czytanie obiektu.

2 (Cytat z ISO: IEC 9889: 201x, sekcja 5.1.2.4 „Wykonania wielowątkowe i wyścigi danych”)
Wykonywanie programu zawiera wyścig danych, jeśli zawiera on dwie sprzeczne akcje w różnych wątkach, z których co najmniej jeden nie jest atomowy i żadne z nich nie występuje wcześniej. Każda taka wyścig danych powoduje niezdefiniowane zachowanie.

Odczytaj wartość wskaźnika, który został zwolniony

Nawet samo odczytywanie wartości wskaźnika, który został zwolniony (tj. Bez próby wyłuskiwania wskaźnika), jest niezdefiniowanym zachowaniem (UB), np.

char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{

}

Cytując ISO / IEC 9899: 2011 , sekcja 6.2.4 § 2:

[…] Wartość wskaźnika staje się nieokreślona, gdy obiekt, na który wskazuje (lub po prostu przeszłość), osiąga koniec swojego życia.

Użycie nieokreślonej pamięci do czegokolwiek, w tym pozornie nieszkodliwe porównanie lub arytmetyka, może mieć niezdefiniowane zachowanie, jeśli wartość może być reprezentacją pułapki dla typu.

Zmodyfikuj literał ciąg

W tym przykładzie kodu wskaźnik char p jest inicjowany na adres literału łańcucha. Próba modyfikacji literału łańcuchowego ma niezdefiniowane zachowanie.

char *p = "hello world";
p[0] = 'H'; // Undefined behavior

Jednak modyfikacji zmienny tablicę char bezpośrednio lub poprzez wskaźnik naturalnie nie jest niezdefiniowane zachowanie, nawet jeśli jego inicjator jest ciągiem znaków. Wszystko w porządku:

char a[] = "hello, world";
char *p = a;

a[0] = 'H';
p[7] = 'W';

Wynika to z faktu, że literał łańcucha jest skutecznie kopiowany do tablicy za każdym razem, gdy tablica jest inicjowana (raz dla zmiennych o czasie trwania statycznym, za każdym razem, gdy tablica jest tworzona dla zmiennych o czasie trwania automatycznym lub czasie trwania wątku - zmienne o przydzielonym czasie trwania nie są inicjowane), i dobrze jest zmodyfikować zawartość tablicy.

Dwa razy zwalnianie pamięci

Dwukrotne zwolnienie pamięci jest niezdefiniowanym zachowaniem, np

int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);

Cytat ze standardu (7.20.3.2. Bezpłatna funkcja C99):

W przeciwnym razie, jeśli argument nie pasuje do wskaźnika wcześniej zwróconego przez funkcję calloc, malloc lub realloc, lub jeśli przestrzeń została zwolniona przez wywołanie free lub realloc, zachowanie jest niezdefiniowane.

Używanie niepoprawnego specyfikatora formatu w printf

Użycie niepoprawnego specyfikatora formatu w pierwszym argumencie printf wywołuje niezdefiniowane zachowanie. Na przykład poniższy kod wywołuje niezdefiniowane zachowanie:

long z = 'B';
printf("%c\n", z);

Oto inny przykład

printf("%f\n",0);

Powyżej linii kodu jest niezdefiniowane zachowanie. %f oczekuje podwójnie. Jednak 0 jest typu int .

Zauważ, że twój kompilator zwykle może pomóc uniknąć takich przypadków, jeśli włączysz odpowiednie flagi podczas kompilacji ( -Wformat w clang i gcc ). Z ostatniego przykładu:

warning: format specifies type 'double' but the argument has type
      'int' [-Wformat]
    printf("%f\n",0);
            ~~    ^
            %d

Konwersja między typami wskaźników daje niepoprawnie wyrównany wynik

Następujące zachowanie może mieć niezdefiniowane zachowanie z powodu nieprawidłowego wyrównania wskaźnika:

 char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
 uint32_t *intptr = (uint32_t*)(memory_block + 1);  /* possible undefined behavior */
 uint32_t mvalue = *intptr;

Niezdefiniowane zachowanie występuje, gdy wskaźnik jest konwertowany. Zgodnie z C11, jeśli konwersja między dwoma typami wskaźników daje wynik, który jest niepoprawnie wyrównany (6.3.2.3), zachowanie jest niezdefiniowane . W tym przypadku uint32_t może wymagać na przykład wyrównania 2 lub 4.

z drugiej strony calloc jest wymagane do zwrócenia wskaźnika, który jest odpowiednio wyrównany dla dowolnego typu obiektu; dlatego memory_block jest odpowiednio wyrównany, aby zawierał uint32_t w początkowej części. Następnie w systemie, w którym uint32_t wymagało wyrównania 2 lub 4, memory_block + 1 będzie nieparzystym adresem, a zatem nie będzie poprawnie wyrównany.

Zauważ, że standard C żąda, aby już operacja rzutowania była niezdefiniowana. Jest to narzucone, ponieważ na platformach, na których adresy są segmentowane, adres bajtu memory_block + 1 może nawet nie mieć właściwej reprezentacji jako wskaźnik liczby całkowitej.

Rzucanie char * na wskaźniki na inne typy bez względu na wymagania dotyczące wyrównania jest czasami niepoprawnie używane do dekodowania spakowanych struktur, takich jak nagłówki plików lub pakiety sieciowe.

Za pomocą memcpy można uniknąć niezdefiniowanego zachowania wynikającego z nieprawidłowo wyrównanej konwersji wskaźnika:

memcpy(&mvalue, memory_block + 1, sizeof mvalue);

Tutaj nie następuje konwersja wskaźnika do uint32_t* a bajty są kopiowane jeden po drugim.

Ta operacja kopiowania w naszym przykładzie prowadzi tylko do prawidłowej wartości mvalue ponieważ:

  • Użyliśmy calloc , więc bajty zostały poprawnie zainicjowane. W naszym przypadku wszystkie bajty mają wartość 0 , ale wystarczyłaby każda inna właściwa inicjalizacja.
  • uint32_t jest dokładnym typem szerokości i nie ma bitów wypełniających
  • Każdy dowolny wzór bitowy jest prawidłową reprezentacją dowolnego typu niepodpisanego.

Dodawanie lub odejmowanie wskaźnika nie jest odpowiednio ograniczone

Poniższy kod ma niezdefiniowane zachowanie:

char buffer[6] = "hello";
char *ptr1 = buffer - 1;  /* undefined behavior */
char *ptr2 = buffer + 5;  /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char *ptr4 = buffer + 7;  /* undefined behavior */

Zgodnie z C11, jeśli dodanie lub odjęcie wskaźnika do obiektu tablicy lub tuż poza nim, a liczba całkowita da wynik, który nie wskazuje na ten sam obiekt tablicy lub tuż za nim, zachowanie jest niezdefiniowane (6.5.6 ).

Dodatkowo, jest naturalnie niezdefiniowane zachowanie, aby wyłuskać wskaźnik, który wskazuje tuż poza tablicą:

char buffer[6] = "hello";
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char value = *ptr3;       /* undefined behavior */

Modyfikacja stałej zmiennej za pomocą wskaźnika

int main (void)
{
    const int foo_readonly = 10;
    int *foo_ptr;

    foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
    *foo_ptr = 20; /* This is undefined behavior */

    return 0;
}

Cytując ISO / IEC 9899: 201x , sekcja 6.7.3 §2:

Jeśli podjęta zostanie próba zmodyfikowania obiektu zdefiniowanego typem const-kwalifikowanym poprzez użycie wartości o typie non-const, zachowanie jest niezdefiniowane. [...]


(1) W GCC może to warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers] wyświetlenie następującego ostrzeżenia: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]

Przekazywanie wskaźnika zerowego do konwersji printf% s

Konwersja %s printf stwierdza, że odpowiedni argument jest wskaźnikiem do początkowego elementu tablicy typu znaków . Wskaźnik zerowy nie wskazuje początkowego elementu żadnej tablicy typu znaków, dlatego zachowanie następujących elementów jest niezdefiniowane:

char *foo = NULL;
printf("%s", foo); /* undefined behavior */

Niezdefiniowane zachowanie nie zawsze oznacza jednak awarię programu - niektóre systemy podejmują kroki w celu uniknięcia awarii, która normalnie ma miejsce, gdy wskaźnik zerowy zostanie odwołany. Na przykład Glibc jest znany z drukowania

(null)

dla powyższego kodu. Jednak dodaj (tylko) nową linię do ciągu formatu, a otrzymasz awarię:

char *foo = 0;
printf("%s\n", foo); /* undefined behavior */

W tym przypadku dzieje się tak, ponieważ GCC ma optymalizację, która zmienia printf("%s\n", argument); do wywołania puts z puts(argument) , a puts w Glibc nie obsługuje wskaźników zerowych. Wszystkie te zachowania są zgodne ze standardami.

Zauważ, że wskaźnik zerowy różni się od pustego ciągu . Zatem poniższe informacje są prawidłowe i nie mają niezdefiniowanego zachowania. Po prostu wydrukuje nowy wiersz :

char *foo = "";
printf("%s\n", foo);

Niespójne powiązanie identyfikatorów

extern int var;
static int var; /* Undefined behaviour */

C11, § 6.2.2, 7 mówi:

Jeśli w jednostce tłumaczeniowej pojawia się ten sam identyfikator z połączeniem zarówno wewnętrznym, jak i zewnętrznym, zachowanie nie jest określone.

Zauważ, że jeśli wcześniejsza deklaracja identyfikatora jest widoczna, będzie miała powiązanie z wcześniejszą deklaracją. C11, § 6.2.2, 4 zezwala na:

W przypadku identyfikatora zadeklarowanego z zewnętrznym specyfikatorem klasy pamięci w zakresie, w którym widoczna jest wcześniejsza deklaracja tego identyfikatora, 31) jeżeli w poprzedniej deklaracji określono powiązanie wewnętrzne lub zewnętrzne, powiązanie identyfikatora w późniejszej deklaracji jest takie samo jak powiązanie określone w poprzedniej deklaracji. Jeśli nie jest widoczna żadna wcześniejsza deklaracja lub jeśli w poprzedniej deklaracji nie ma powiązania, to identyfikator ma powiązanie zewnętrzne.

/* 1. This is NOT undefined */
static int var;
extern int var; 


/* 2. This is NOT undefined */
static int var;
static int var; 

/* 3. This is NOT undefined */
extern int var;
extern int var; 

Używanie fflush w strumieniu wejściowym

Standardy POSIX i C wyraźnie stwierdzają, że użycie fflush w strumieniu wejściowym jest niezdefiniowanym zachowaniem. fflush jest zdefiniowany tylko dla strumieni wyjściowych.

#include <stdio.h>

int main()
{
    int i;
    char input[4096];

    scanf("%i", &i);
    fflush(stdin); // <-- undefined behavior
    gets(input);

    return 0;
}

Nie ma standardowego sposobu na odrzucenie nieprzeczytanych znaków ze strumienia wejściowego. Z drugiej strony, niektóre implementacje używają fflush do czyszczenia bufora stdin . Microsoft określa zachowanie fflush w strumieniu wejściowym: Jeśli strumień jest otwarty na wejście, fflush usuwa zawartość bufora. Zgodnie z POSIX.1-2008 zachowanie fflush jest niezdefiniowane, chyba że plik wejściowy jest widoczny.

Zobacz Korzystanie z fflush(stdin) aby uzyskać więcej szczegółowych informacji.

Przesunięcie bitów przy użyciu zliczeń ujemnych lub poza szerokością typu

Jeśli wartość zliczania przesunięcia jest wartością ujemną, wówczas zarówno operacje przesunięcia w lewo, jak i przesunięcie w prawo są niezdefiniowane 1 :

int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */

Jeśli lewe przesunięcie jest wykonywane na wartości ujemnej , jest niezdefiniowane:

int x = -5 << 3; /* undefined */

Jeśli lewe przesunięcie jest wykonywane na wartości dodatniej, a wynik wartości matematycznej nie jest reprezentowalny w typie, jest niezdefiniowany 1 :

/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit 
 * in an int. So, this is undefined. */
       
int x = 5 << 72;

Zauważ, że przesunięcie w prawo na wartości ujemnej (.eg -5 >> 3 ) nie jest niezdefiniowane, ale zdefiniowane w implementacji .


1 Cytując ISO / IEC 9899: 201x , sekcja 6.5.7:

Jeśli wartość prawego operandu jest ujemna lub jest większa lub równa szerokości promowanego lewego operandu, zachowanie jest niezdefiniowane.

Modyfikowanie ciągu zwracanego przez funkcje getenv, strerror i setlocale

Modyfikacja ciągów zwracanych przez standardowe funkcje getenv() , strerror() i setlocale() jest niezdefiniowana. W związku z tym implementacje mogą używać przechowywania statycznego dla tych ciągów.

Funkcja getenv (), C11, § 7.22.4.7, 4 , mówi:

Funkcja getenv zwraca wskaźnik do łańcucha powiązanego z dopasowanym elementem listy. Wskazany ciąg nie powinien być modyfikowany przez program, ale może zostać zastąpiony przez kolejne wywołanie funkcji getenv.

Funkcja strerror (), C11, § 7.23.6.3, 4 mówi:

Funkcja strerror zwraca wskaźnik do łańcucha, którego zawartość jest specyficzna dla danego regionu. Wskazana tablica nie powinna być modyfikowana przez program, ale może zostać zastąpiona przez kolejne wywołanie funkcji strerror.

Funkcja setlocale (), C11, §7.11.1.1, 8 mówi:

Wskaźnik do łańcucha zwracany przez funkcję setlocale jest taki, że kolejne wywołanie z tą wartością ciągu i powiązaną z nią kategorią przywróci tę część ustawień regionalnych programu. Wskazany ciąg nie powinien być modyfikowany przez program, ale może zostać zastąpiony przez kolejne wywołanie funkcji setlocale.

Podobnie funkcja localeconv() zwraca wskaźnik do struct lconv którego nie należy modyfikować.

Funkcja localeconv (), C11, §7.11.2.1, 8 mówi:

Funkcja localeconv zwraca wskaźnik do wypełnionego obiektu. Struktura wskazywana przez wartość zwracaną nie może być modyfikowana przez program, ale może zostać zastąpiona przez kolejne wywołanie funkcji localeconv.

Zwracanie z funkcji zadeklarowanej za pomocą specyfikatora funkcji „_Noreturn” lub „noreturn”

C11

Specyfikator funkcji _Noreturn został wprowadzony w C11. Nagłówek <stdnoreturn.h> dostarcza makro noreturn który rozszerza się _Noreturn . Zatem użycie _Noreturn lub noreturn z <stdnoreturn.h> jest w porządku i równoważne.

Funkcja zadeklarowana za pomocą _Noreturn (lub noreturn ) nie może wrócić do swojego programu wywołującego. Jeśli taka funkcja nie wraca do swojego rozmówcy, zachowanie jest niezdefiniowane.

W poniższym przykładzie func() jest zadeklarowany ze specyfikatorem noreturn ale wraca do swojego programu wywołującego.

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void func(void);

void func(void)
{
    printf("In func()...\n");
} /* Undefined behavior as func() returns */

int main(void)
{
    func();
    return 0;
}

gcc i clang generują ostrzeżenia dla powyższego programu:

$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
 }
 ^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^

Przykład użycia noreturn który ma dobrze zdefiniowane zachowanie:

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void my_exit(void);

/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
    printf("Exiting...\n");
    exit(0);
}

int main(void)
{
    my_exit();
    return 0;
}


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow