Pracuję z kompilatorem układu DSP, który celowo generuje kod, który uzyskuje dostęp do końca tablicy poza kodem C, co nie!
Wynika to z faktu, że pętle są tak skonstruowane, że koniec iteracji poprzedza niektóre dane do następnej iteracji. Tak więc dane wstępnie wybrane na końcu ostatniej iteracji nigdy nie są faktycznie używane.
Pisanie takiego kodu C wywołuje niezdefiniowane zachowanie, ale jest to tylko formalność z dokumentu standardowego, który dotyczy samego siebie z maksymalną przenośnością.
Częściej nie, program, który uzyskuje dostęp poza granicami, nie jest sprytnie zoptymalizowany. To jest po prostu błąd. Kod pobiera pewną wartość śmieci i, w przeciwieństwie do zoptymalizowanych pętli wyżej wspomnianego kompilatora, kod następnie wykorzystuje tę wartość w kolejnych obliczeniach, powodując w ten sposób ich uszkodzenie.
Warto wychwytywać takie błędy, dlatego warto sprawić, by zachowanie było niezdefiniowane tylko z tego tylko powodu: aby czas działania mógł wygenerować komunikat diagnostyczny, taki jak „przepełnienie tablicy w linii 42 main.c”.
W systemach z pamięcią wirtualną może się zdarzyć, że tablica zostanie przydzielona w taki sposób, że następujący adres znajduje się w niezmapowanym obszarze pamięci wirtualnej. Dostęp wtedy bombarduje program.
Nawiasem mówiąc, zauważmy, że w C możemy utworzyć wskaźnik, który znajduje się za końcem tablicy. I ten wskaźnik musi być większy niż jakikolwiek wskaźnik do wnętrza tablicy. Oznacza to, że implementacja C nie może umieścić tablicy bezpośrednio na końcu pamięci, gdzie adres plus będzie zawijał się i wyglądałby na mniejszy niż inne adresy w tablicy.
Niemniej jednak dostęp do niezainicjowanych lub poza granicami wartości jest czasem ważną techniką optymalizacji, nawet jeśli nie jest maksymalnie przenośna. Jest to na przykład powód, dla którego narzędzie Valgrind nie zgłasza dostępu do niezainicjowanych danych, gdy te dostępy się zdarzają, ale tylko wtedy, gdy wartość jest później wykorzystywana w jakiś sposób, który mógłby wpłynąć na wynik programu. Otrzymujesz komunikat diagnostyczny, taki jak „gałąź warunkowa w xxx: nnn zależy od niezainicjowanej wartości” i czasami może być trudno wyśledzić, skąd pochodzi. Gdyby wszystkie takie dostępy zostały natychmiast uwięzione, pojawiłoby się wiele fałszywych alarmów wynikających z kodu zoptymalizowanego przez kompilator, a także kodu zoptymalizowanego ręcznie.
Mówiąc o tym, pracowałem z jakimś kodekiem od dostawcy, który dawał te błędy, gdy był przenoszony do Linuksa i działał pod Valgrind. Ale sprzedawca przekonał mnie, że tylko kilka bitówużyta wartość faktycznie pochodzi z niezainicjowanej pamięci, a logika ostrożnie uniknęła tych bitów. Użyto tylko dobrych bitów wartości, a Valgrind nie ma możliwości wyśledzenia pojedynczego bitu. Niezainicjowany materiał powstał po przeczytaniu słowa za końcem strumienia bitów zakodowanych danych, ale kod wie, ile bitów znajduje się w strumieniu i nie zużyje więcej bitów, niż jest w rzeczywistości. Ponieważ dostęp poza końcem tablicy strumienia bitów nie powoduje żadnej szkody w architekturze DSP (po tablicy nie ma pamięci wirtualnej, nie ma portów mapowanych w pamięci, a adres się nie zawija), jest to ważna technika optymalizacji.
„Niezdefiniowane zachowanie” nie znaczy tak naprawdę wiele, ponieważ według ISO C, po prostu włączenie nagłówka, który nie jest zdefiniowany w standardzie C, lub wywołanie funkcji, która nie jest zdefiniowana w samym programie lub standardzie C, są przykładami niezdefiniowanymi zachowanie. Niezdefiniowane zachowanie nie oznacza „niezdefiniowane przez nikogo na planecie”, tylko „niezdefiniowane przez normę ISO C”. Ale oczywiście czasami nieokreślone zachowanie nie jest absolutnie przez nikogo zdefiniowane.