Niedawno zacząłem uczyć się C i uczęszczam na zajęcia z C jako przedmiot. Obecnie bawię się pętlami i napotykam dziwne zachowania, których nie umiem wyjaśnić.
#include <stdio.h>
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%d \n", sizeof(array)/sizeof(int));
return 0;
}
Na moim laptopie z systemem Ubuntu 14.04 ten kod się nie psuje. Biegnie do końca. Na komputerze mojej szkoły z systemem CentOS 6.6 również działa dobrze. W systemie Windows 8.1 pętla nigdy się nie kończy.
Jeszcze bardziej dziwne jest to, że kiedy edytuję stan for
pętli do: i <= 11
kod kończy się tylko na moim laptopie z systemem Ubuntu. Nigdy nie kończy się w CentOS i Windows.
Czy ktoś może wyjaśnić, co dzieje się w pamięci i dlaczego różne systemy operacyjne obsługujące ten sam kod dają różne wyniki?
EDYCJA: Wiem, że pętla for wychodzi poza granice. Robię to celowo. Po prostu nie mogę zrozumieć, jak zachowanie może się różnić w różnych systemach operacyjnych i komputerach.
i
jest przechowywana zaraz po zakończeniuarray
i zastępujesz jąarray[10]=0;
. Może tak nie być w przypadku zoptymalizowanej wersji na tej samej platformie, która może przechowywaći
w rejestrze i nigdy nie odwoływać się do niego w pamięci.Odpowiedzi:
Właśnie odkryłeś tupanie w pamięci. Możesz przeczytać więcej na ten temat tutaj: Co to jest „stomp pamięci”?
Po przydzieleniu
int array[10],i;
zmienne te przechodzą do pamięci (w szczególności są przydzielane na stosie, który jest blokiem pamięci związanym z funkcją).array[]
ii
prawdopodobnie przylegają do siebie w pamięci. Wygląda na to, że w systemie Windows 8.1i
znajduje się naarray[10]
. Na CentOSi
znajduje się pod adresemarray[11]
. A w Ubuntu nie ma go w żadnym miejscu (może jest warray[-1]
?).Spróbuj dodać te instrukcje debugowania do swojego kodu. Powinieneś zauważyć, że przy iteracji 10 lub 11,
array[i]
punkty nai
.źródło
array[10]
zniszczy ramkę stosu. Jak może istnieć różnica między kodem z wyjściem debugującym a bez niego? Jeśli adresi
nigdy nie jest potrzebny, kompilator może się zoptymalizowaći
. do rejestru, zmieniając w ten sposób układ pamięci na stosie ...array[10]=0
. Jeśli skompilowałeś swój kod z optymalizacją na, prawdopodobnie tak się nie stanie (ponieważ C ma zasady aliasingu, które ograniczają rodzaje dostępu do pamięci, mogą potencjalnie nakładać się na inną pamięć. Jako zmienna lokalna, której nigdy nie bierzesz pod uwagę, myślę, że kompilator powinien być w stanie założyć, że nic nie aliasy. W każdym razie, spisując koniec tablicy jest niezdefiniowanym zachowaniem. Zawsze staraj się w zależności od tego unikaćBłąd leży między tymi fragmentami kodu:
Ponieważ
array
ma tylko 10 elementów, w ostatniej iteracjiarray[10] = 0;
występuje przepełnienie bufora. Przepełnienia buforów są NIEZDEFINIOWANE ZACHOWANIA , co oznacza, że mogą sformatować dysk twardy lub spowodować, że demony wylecą z nosa.Dość często zdarza się, że wszystkie zmienne stosu są układane obok siebie. Jeśli
i
znajduje się tam, gdziearray[10]
zapisuje, UB zresetuje sięi
do0
, co prowadzi do pętli niezakończonej.Aby naprawić, zmień warunek pętli na
i < 10
.źródło
rm -rf /
nawet jeśli nie jesteś rootem, a nie „formatuje” cały dysk oczywiście, ale wciąż niszczy wszystkie twoje dane. Auć.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ćint
w pamięci o odpowiednim rozmiarze, która leży bezpośrednio po niejarray
w 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ą:i
. Pętla nigdy się nie kończy, ponieważi
uruchamia się ponownie od 0.array
znajduje się na końcu strony pamięci wirtualnej, a następna strona nie jest mapowana.W systemie Windows zaobserwowano, że kompilator postanowił umieścić zmienną
i
bezpośrednio po tablicy w pamięci, więcarray[10] = 0
ostatecznie przypisał jąi
. W Ubuntu i CentOS kompilator nie umieściłi
tam. 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ę
-S
tworzenia 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:Tutaj zmienna
i
znajduje 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łi
tuż przed tablicą; nadpisujesz,i
jeśli zdarzy ci się pisaćarray[-1]
. Jeśli zmieniszarray[i]=0
naarray[9-i]=0
, dostaniesz nieskończoną pętlę na tej konkretnej platformie z tymi konkretnymi opcjami kompilatora.Teraz skompilujmy Twój program
gcc -O1
.To krócej! Kompilator nie tylko odmówił przydzielenia lokalizacji stosu
i
- jest on zawsze przechowywany w rejestrzeebx
- ale nie zadał sobie trudu, aby przydzielić pamięćarray
lub 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 -O0
lubgcc -O1
nie). Utwórz plik źródłowyuse_array.c
zawierającyi zmień kod źródłowy na
Połącz z
Tym razem kod asemblera wygląda następująco:
Teraz tablica jest na stosie, 44 bajty od góry. Co
i
? Nigdzie się nie pojawia! Ale licznik pętli jest przechowywany w rejestrzerbx
. To nie jest dokładniei
, ale adresarray[i]
. Kompilator zdecydował, że skoro wartośći
nigdy 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]
naarray[9-i]
) lub nawet zmiana czegoś pozornie niezwiązanego (dodanie wywołaniause_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.źródło
W przeciwieństwie do Javy, C nie wykonuje sprawdzania granic tablic, tzn. Nie
ArrayIndexOutOfBoundsException
, zadanie upewnienia się, że indeks tablicy jest prawidłowy, pozostaje w gestii programisty. Robienie tego celowo prowadzi do nieokreślonego zachowania, wszystko może się zdarzyć.W przypadku tablicy:
indeksy są poprawne tylko w zakresie
0
od9
. Próbujesz jednak:dostęp
array[10]
tutaj, zmień warunek nai < 10
źródło
Masz naruszenie granicy, a na platformach nie kończących się, uważam, że przypadkowo ustawiasz
i
zero na końcu pętli, aby zaczęło się od nowa.array[10]
jest nieważny; zawiera 10 elementów,array[0]
przezarray[9]
iarray[10]
jest 11. Pętla powinno być napisane, aby zatrzymać przed10
, jak następuje:Tam, gdzie
array[10]
ziemie są zdefiniowane implementacyjnie i zabawnie, na dwóch twoich platformach, ląduje na nichi
, na których te platformy najwyraźniej bezpośrednio po sobie układająarray
.i
jest ustawiony na zero, a pętla trwa wiecznie. W przypadku innych platformi
może znajdować się wcześniejarray
lubarray
może po nim mieć trochę wypełnienia.źródło
Deklarujesz,
int array[10]
że średniaarray
ma indeks0
do9
(10
liczba elementów całkowitych, które może pomieścić). Ale następująca pętlazapętli się
0
na10
oznacza11
czas. Stąd kiedyi = 10
przepełni bufor i spowoduje Niezdefiniowane zachowanie .Spróbuj tego:
lub,
źródło
Jest niezdefiniowany w
array[10]
i daje niezdefiniowane zachowanie, jak opisano wcześniej. Pomyśl o tym w ten sposób:Mam 10 produktów w koszyku spożywczym. Oni są:
0: Pudełko płatków
1: Chleb
2: Mleko
3: Ciasto
4: Jajka
5: Ciasto
6: 2 litry sody
7: Sałatka
8: Burgery
9: Lody
cart[10]
jest niezdefiniowany i może dawać wyjątek poza zakresem w niektórych kompilatorach. Ale wiele najwyraźniej nie. Pozornie jedenasty przedmiot to przedmiot, którego faktycznie nie ma w koszyku. Jedenasty przedmiot wskazuje, jak to nazywam, „przedmiot poltergeist”. Nigdy nie istniał, ale tam był.Dlaczego niektóre kompilatory dać
i
indeksarray[10]
lubarray[11]
nawetarray[-1]
z powodu wyciągu inicjalizacji / deklaracji. Niektóre kompilatory interpretują to jako:int
sarray[10]
i kolejnyint
blok. Aby to ułatwić, umieść je obok siebie”.array[10]
to nie wskazywałoi
.i
oarray[-1]
(ponieważ indeks tablicy nie może lub nie powinien być ujemny) lub alokuj go w zupełnie innym miejscu, ponieważ system operacyjny może to obsłużyć, i jest bezpieczniejszy.Niektóre kompilatory chcą, aby wszystko szło szybciej, a niektóre kompilatory wolą bezpieczeństwo. Chodzi o kontekst. Gdybym na przykład rozwijał aplikację dla starożytnego systemu operacyjnego BREW (system operacyjny podstawowego telefonu), nie miałoby to znaczenia dla bezpieczeństwa. Gdybym opracowywał dla iPhone'a 6, to mógłby działać szybko, bez względu na wszystko, więc musiałbym położyć nacisk na bezpieczeństwo. (Poważnie, czy przeczytałeś Wytyczne Apple App Store lub poczytałeś o rozwoju Swift i Swift 2.0?)
źródło
Ponieważ utworzyłeś tablicę o rozmiarze 10, warunek pętli powinien wyglądać następująco:
Obecnie próbujesz uzyskać dostęp do nieprzypisanej lokalizacji z pamięci,
array[10]
co powoduje niezdefiniowane zachowanie . Niezdefiniowane zachowanie oznacza, że Twój program będzie zachowywał się w nieokreślony sposób, dzięki czemu może dawać różne wyniki w każdym wykonaniu.źródło
Kompilator C tradycyjnie nie sprawdza granic. Możesz otrzymać błąd segmentacji w przypadku odniesienia do lokalizacji, która nie „należy” do Twojego procesu. Jednak zmienne lokalne są alokowane na stosie i zależnie od sposobu alokacji pamięci obszar tuż za tablicą (
array[10]
) może należeć do segmentu pamięci procesu. W ten sposób nie powstaje pułapka błędu segmentacji i wydaje się, że tego doświadczasz. Jak zauważyli inni, jest to niezdefiniowane zachowanie w C i twój kod może być uważany za niepoprawny. Ponieważ uczysz się języka C, lepiej jest przyzwyczaić się do sprawdzania granic w kodzie.źródło
Poza możliwością, że pamięć może być tak ułożona, że próba zapisu w celu
a[10]
nadpisaniai
, może być również możliwe, że kompilator optymalizacyjny może ustalić, że test pętli nie może zostać osiągnięty przy wartościi
większej niż dziesięć bez uprzedniego uzyskania dostępu do kodu nieistniejący element tablicya[10]
.Ponieważ próba dostępu do tego elementu byłaby nieokreślonym zachowaniem, kompilator nie miałby żadnych zobowiązań w odniesieniu do tego, co program mógłby zrobić po tym punkcie. Mówiąc dokładniej, ponieważ kompilator nie miałby obowiązku generowania kodu w celu sprawdzenia indeksu pętli w żadnym przypadku, w którym może on być większy niż dziesięć, nie miałby obowiązku generowania kodu w celu sprawdzenia go w ogóle; zamiast tego można założyć, że
<=10
test zawsze da wynik prawdziwy. Zauważ, że byłoby to prawdą, nawet gdyby kod czytał,a[10]
a nie pisał.źródło
Podczas iteracji w przeszłości
i==9
przypisujesz zero do „elementów tablicy”, które faktycznie znajdują się za tablicą , więc zastępujesz niektóre inne dane. Najprawdopodobniej nadpisujeszi
zmienną, która znajduje się poa[]
. W ten sposób po prostu resetujeszi
zmienną do zera, a tym samym restartujesz pętlę.Możesz to odkryć sam, jeśli drukujesz
i
w pętli:zamiast po prostu
Oczywiście wynik ten silnie zależy od przydziału pamięci dla zmiennych, co z kolei zależy od kompilatora i jego ustawień, więc ogólnie jest to zachowanie niezdefiniowane - dlatego wyniki na różnych komputerach lub w różnych systemach operacyjnych lub na różnych kompilatorach mogą się różnić.
źródło
błąd jest w porcji tablica [10] w / c to także adres i (int tablica [10], i;). gdy tablica [10] jest ustawiona na 0, to i byłoby 0 w / c resetuje całą pętlę i powoduje nieskończoną pętlę. będzie nieskończona pętla, jeśli tablica [10] zawiera się w przedziale od 0-10. poprawna pętla powinna być dla (i = 0; i <10; i ++) {...} int array [10], i; dla (i = 0; i <= 10; i ++) tablica [i] = 0;
źródło
Zasugeruję coś, czego nie znajdę powyżej:
Spróbuj przypisać tablicę [i] = 20;
Myślę, że to powinno zakończyć kod wszędzie. (Pod warunkiem, że trzymasz i <= 10 lub ll)
Jeśli to się uruchomi, możesz zdecydowanie zdecydować, że podane tutaj odpowiedzi są już poprawne [odpowiedź związana z tupiącą pamięcią np.]
źródło
Są tu dwie rzeczy źle. Int i to tak naprawdę element tablicy, tablica [10], jak widać na stosie. Ponieważ zezwoliłeś, aby indeksowanie faktycznie tworzyło tablicę [10] = 0, indeks pętli i nigdy nigdy nie przekroczy 10. Zrób to
for(i=0; i<10; i+=1)
.i ++ jest, jak nazwałby to K&R , „złym stylem”. Inkrementuje i o wielkość i, a nie 1. i ++ jest dla matematyki wskaźnika, a i + = 1 jest dla algebry. Chociaż zależy to od kompilatora, nie jest to dobra konwencja dotycząca przenośności.
źródło
i
jest elementem tablicy NOTana[10]
, nie ma obowiązku ani nawet sugestii, aby kompilator umieścił ją na stosie natychmiast poa[]
niej - równie dobrze może być umieszczony przed tablicą lub oddzielony dodatkową przestrzenią. Można go nawet przydzielić poza pamięć główną, na przykład w rejestrze procesora. To nieprawda, że++
dotyczy wskaźników, a nie liczb całkowitych. Całkowicie błędne jest to, że „i ++ zwiększa i o wielkość i” - przeczytaj opis operatora w definicji języka!i
- jestint
typu. Jest to liczba całkowita , a nie wskaźnik; liczbą całkowitą, używane jako indeks doarray
,.