W tym, co powinno być ostatnim uruchomieniem pętli, piszesz array[10], ale w tablicy jest tylko 10 elementów, ponumerowanych od 0 do 9. Specyfikacja języka C mówi, że jest to „niezdefiniowane zachowanie”. W praktyce oznacza to, że twój program będzie próbował zapisać intw pamięci o odpowiednim rozmiarze, która leży bezpośrednio po niej arrayw pamięci. To, co się dzieje, zależy od tego, co tam właściwie leży, a to zależy nie tylko od systemu operacyjnego, ale bardziej od kompilatora, opcji kompilatora (takich jak ustawienia optymalizacji), architektury procesora, otaczającego kodu itp. Może nawet różnić się w zależności od wykonania, np. z powodu randomizacji przestrzeni adresowej (prawdopodobnie nie w tym przykładzie zabawki, ale dzieje się tak w prawdziwym życiu). Niektóre możliwości obejmują:
- Lokalizacja nie była używana. Pętla kończy się normalnie.
- Lokalizacja została użyta do czegoś, co miało wartość 0. Pętla kończy się normalnie.
- Lokalizacja zawierała adres zwrotny funkcji. Pętla kończy się normalnie, ale następnie program ulega awarii, ponieważ próbuje przejść do adresu 0.
- Lokalizacja zawiera zmienną
i. Pętla nigdy się nie kończy, ponieważ iuruchamia się ponownie od 0.
- Lokalizacja zawiera inną zmienną. Pętla kończy się normalnie, ale zdarzają się „interesujące” rzeczy.
- Lokalizacja jest nieprawidłowym adresem pamięci, np. Ponieważ
arrayznajduje się na końcu strony pamięci wirtualnej, a następna strona nie jest mapowana.
- Demony wylatują z twojego nosa . Na szczęście większość komputerów nie ma wymaganego sprzętu.
W systemie Windows zaobserwowano, że kompilator postanowił umieścić zmienną ibezpośrednio po tablicy w pamięci, więc array[10] = 0ostatecznie przypisał ją i. W Ubuntu i CentOS kompilator nie umieścił itam. Prawie wszystkie implementacje języka C grupują zmienne lokalne w pamięci na stosie pamięci , z jednym głównym wyjątkiem: niektóre zmienne lokalne można całkowicie umieścić w rejestrach . Nawet jeśli zmienna znajduje się na stosie, kolejność zmiennych jest określana przez kompilator i może zależeć nie tylko od kolejności w pliku źródłowym, ale także od ich typów (aby uniknąć marnowania pamięci na ograniczenia wyrównania, które pozostawią dziury) , na ich nazwy, na niektóre wartości skrótu używane w wewnętrznej strukturze danych kompilatora itp.
Jeśli chcesz dowiedzieć się, co zrobił twój kompilator, możesz mu powiedzieć, żeby pokazał ci kod asemblera. Aha i naucz się rozszyfrowywać asembler (to łatwiejsze niż pisanie). W GCC (i niektórych innych kompilatorach, szczególnie w świecie Unixowym), podaj opcję -Stworzenia kodu asemblera zamiast pliku binarnego. Na przykład, oto fragment asemblera dla kompilacji pętli z GCC na amd64 z opcją optymalizacji -O0(bez optymalizacji), z komentarzami dodanymi ręcznie:
.L3:
movl -52(%rbp), %eax ; load i to register eax
cltq
movl $0, -48(%rbp,%rax,4) ; set array[i] to 0
movl $.LC0, %edi
call puts ; printf of a constant string was optimized to puts
addl $1, -52(%rbp) ; add 1 to i
.L2:
cmpl $10, -52(%rbp) ; compare i to 10
jle .L3
Tutaj zmienna iznajduje się 52 bajty poniżej górnej części stosu, podczas gdy tablica zaczyna 48 bajtów poniżej górnej części stosu. Tak się składa, że ten kompilator umieścił ituż przed tablicą; nadpisujesz, ijeśli zdarzy ci się pisać array[-1]. Jeśli zmienisz array[i]=0na array[9-i]=0, dostaniesz nieskończoną pętlę na tej konkretnej platformie z tymi konkretnymi opcjami kompilatora.
Teraz skompilujmy Twój program gcc -O1.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
To krócej! Kompilator nie tylko odmówił przydzielenia lokalizacji stosu i- jest on zawsze przechowywany w rejestrze ebx- ale nie zadał sobie trudu, aby przydzielić pamięć arraylub wygenerować kod do ustawienia swoich elementów, ponieważ zauważył, że żaden z elementów są kiedykolwiek używane.
Aby ten przykład był bardziej wymowny, upewnijmy się, że przypisania tablic są wykonywane przez dostarczenie kompilatorowi czegoś, czego nie jest w stanie zoptymalizować. Prostym sposobem na to jest użycie tablicy z innego pliku - z powodu osobnej kompilacji kompilator nie wie, co dzieje się w innym pliku (chyba że zoptymalizuje się w czasie łącza, który nie, gcc -O0lub gcc -O1nie). Utwórz plik źródłowy use_array.czawierający
void use_array(int *array) {}
i zmień kod źródłowy na
#include <stdio.h>
void use_array(int *array);
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%zd \n", sizeof(array)/sizeof(int));
use_array(array);
return 0;
}
Połącz z
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
Tym razem kod asemblera wygląda następująco:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
Teraz tablica jest na stosie, 44 bajty od góry. Co i? Nigdzie się nie pojawia! Ale licznik pętli jest przechowywany w rejestrze rbx. To nie jest dokładnie i, ale adres array[i]. Kompilator zdecydował, że skoro wartość inigdy nie była używana bezpośrednio, nie było sensu wykonywać arytmetyki, aby obliczyć, gdzie przechowywać 0 podczas każdego przebiegu pętli. Zamiast tego ten adres jest zmienną pętli, a arytmetyka określająca granice została wykonana częściowo w czasie kompilacji (pomnóż 11 iteracji przez 4 bajty na element tablicy, aby uzyskać 44), a częściowo w czasie wykonywania, ale raz na zawsze, zanim pętla się rozpocznie ( wykonaj odejmowanie, aby uzyskać wartość początkową).
Nawet na tym bardzo prostym przykładzie widzieliśmy, jak zmiana opcji kompilatora (włączenie optymalizacji) lub zmiana czegoś drobnego ( array[i]na array[9-i]) lub nawet zmiana czegoś pozornie niezwiązanego (dodanie wywołania use_array) może mieć znaczący wpływ na to, co program wykonywalny wygenerował przez kompilator. Optymalizacje kompilatora mogą zrobić wiele rzeczy, które mogą wydawać się nieintuicyjne w programach wywołujących niezdefiniowane zachowanie . Dlatego niezdefiniowane zachowanie jest całkowicie niezdefiniowane. Nawet jeśli nieco odbiegasz od ścieżek, w rzeczywistych programach może być bardzo trudno zrozumieć związek między tym, co robi kod, a tym, co powinien był zrobić, nawet dla doświadczonych programistów.