Jak to możliwe? Czy pamięć lokalnej zmiennej nie jest niedostępna poza jej funkcją?
Wynajmujesz pokój hotelowy. Wkładasz książkę do górnej szuflady stolika nocnego i idziesz spać. Sprawdzasz następnego dnia rano, ale „zapominasz” o oddaniu klucza. Kradniesz klucz!
Tydzień później wrócisz do hotelu, nie zameldujesz się, przekradniesz do swojego starego pokoju ze skradzionym kluczem i zajrzysz do szuflady. Twoja książka wciąż tam jest. Zadziwiający!
Jak to możliwe? Czy zawartość szuflady pokoju hotelowego jest niedostępna, jeśli pokój nie został wynajęty?
Oczywiście ten scenariusz może się zdarzyć w prawdziwym świecie, nie ma problemu. Nie ma tajemniczej siły, która powoduje, że książka znika, gdy nie masz już pozwolenia na przebywanie w pokoju. Nie ma też tajemniczej siły, która uniemożliwia ci wejście do pokoju ze skradzionym kluczem.
Kierownictwo hotelu nie jest zobowiązane do usunięcia książki. Nie zawarliście z nimi umowy, która mówi, że jeśli zostawicie coś za sobą, zniszczą to za was. Jeśli nielegalnie ponownie wejdziesz do pokoju ze skradzionym kluczem, aby go odzyskać, personel ochrony hotelowej nie musi cię przyłapać. Nie zawarłeś z nimi umowy, która mówi „jeśli spróbuję wślizgnąć się z powrotem do mojej pokój później, musisz mnie zatrzymać. " Przeciwnie, podpisałeś z nimi umowę, która brzmiała: „Obiecuję, że nie wrócę później do mojego pokoju”, umowę, którą złamałeś .
W tej sytuacji wszystko może się zdarzyć . Książka może tam być - masz szczęście. Może być tam czyjaś książka, a twoja może być w hotelowym piecu. Ktoś może być przy wejściu i rozrywać książkę na strzępy. Hotel mógł całkowicie usunąć stół i zarezerwować i zastąpić go szafą. Cały hotel może zostać właśnie zburzony i zastąpiony stadionem piłkarskim, a umrzesz podczas eksplozji.
Nie wiesz, co się stanie; kiedy wyrejestrowany z hotelu i ukradł klucz do nielegalnego wykorzystania później, zrezygnował z prawa do życia w przewidywalnym, bezpiecznym świecie, ponieważ pan zdecydował się złamać zasady systemu.
C ++ nie jest bezpiecznym językiem . Pozwoli to z radością złamać zasady systemu. Jeśli spróbujesz zrobić coś nielegalnego i głupiego, jak powrót do pokoju, w którym nie masz uprawnień, i szperanie w biurku, którego może już tam nie być, C ++ cię nie powstrzyma. Bezpieczniejsze języki niż C ++ rozwiązują ten problem, ograniczając twoją moc - na przykład poprzez znacznie ściślejszą kontrolę nad klawiszami.
AKTUALIZACJA
O Boże, ta odpowiedź zyskuje wiele uwagi. (Nie jestem pewien, dlaczego - uważałem to za „zabawną” małą analogię, ale cokolwiek.)
Pomyślałem, że może to być niemądre, aby zaktualizować to trochę za pomocą kilku technicznych myśli.
Kompilatory zajmują się generowaniem kodu, który zarządza przechowywaniem danych przetwarzanych przez ten program. Istnieje wiele różnych sposobów generowania kodu do zarządzania pamięcią, ale z czasem zakorzeniły się dwie podstawowe techniki.
Pierwszym jest posiadanie pewnego rodzaju „długowiecznego” obszaru pamięci, w którym „czas życia” każdego bajtu w pamięci - to znaczy okres, w którym jest on prawidłowo powiązany z jakąś zmienną programu - nie może być łatwo przewidzieć z wyprzedzeniem czasu. Kompilator generuje wywołania do „menedżera sterty”, który wie, jak dynamicznie przydzielać pamięć, gdy jest potrzebna, i odzyskiwać ją, gdy nie jest już potrzebna.
Druga metoda polega na utworzeniu „krótkotrwałego” obszaru przechowywania, w którym czas życia każdego bajtu jest dobrze znany. Tutaj wcielenia są wzorowane na „zagnieżdżaniu się”. Najdłużej żyjące z tych zmiennych krótkotrwałych zostaną przydzielone przed innymi zmiennymi krótkotrwałymi i zostaną uwolnione na końcu. Zmienne o krótszym czasie życia zostaną przydzielone po zmiennych najdłużej żyjących i zostaną przed nimi uwolnione. Czas życia tych zmiennych o krótszym czasie życia jest „zagnieżdżony” w czasie życia zmiennych o dłuższym okresie życia.
Zmienne lokalne są zgodne z tym ostatnim wzorcem; po wprowadzeniu metody jej lokalne zmienne ożywają. Kiedy ta metoda wywołuje inną metodę, lokalne zmienne nowej metody ożywają. Będą martwe, zanim zmienne lokalne pierwszej metody staną się martwe. Względną kolejność początków i zakończeń czasów życia magazynów związanych ze zmiennymi lokalnymi można ustalić z wyprzedzeniem.
Z tego powodu zmienne lokalne są zwykle generowane jako pamięć w strukturze danych „stosu”, ponieważ stos ma właściwość polegającą na tym, że pierwszą rzeczą, która zostanie na niego narzucona, będzie ostatnią rzeczą, która odpadnie.
To tak, jakby hotel postanowił wynajmować pokoje tylko sekwencyjnie i nie możesz się wymeldować, dopóki wszyscy z numerem pokoju wyższym niż wymeldowałeś się.
Pomyślmy więc o stosie. W wielu systemach operacyjnych otrzymujesz jeden stos na wątek, a stos ten ma przydzielony określony rozmiar. Kiedy wywołujesz metodę, rzeczy są wypychane na stos. Jeśli następnie przekażesz wskaźnik do stosu z powrotem z metody, tak jak robi to oryginalny plakat, jest to tylko wskaźnik do środka całkowicie poprawnego bloku pamięci o milionach bajtów. W naszej analogii wymeldowujesz się z hotelu; kiedy to zrobisz, właśnie wymeldowałeś się z zajmowanego pokoju o najwyższym numerze. Jeśli nikt inny nie zamelduje się za tobą i wrócisz nielegalnie do pokoju, wszystkie twoje rzeczy będą tam nadal w tym hotelu .
Używamy stosów do tymczasowych sklepów, ponieważ są naprawdę tanie i łatwe. Implementacja C ++ nie jest wymagana do używania stosu do przechowywania lokalnych; przydałby się stos. Nie robi tego, ponieważ spowolniłoby to program.
Implementacja C ++ nie jest wymagana, aby pozostawić śmieci pozostawione na stosie nietknięte, abyś mógł później po nie wrócić; kompilator generuje kod, który wraca do zera wszystko w „pokoju”, który właśnie opuściłeś, jest całkowicie legalne. Nie dzieje się tak, ponieważ znowu byłoby to drogie.
Implementacja C ++ nie jest wymagana, aby zapewnić, że gdy stos logicznie się kurczy, adresy, które były prawidłowe, nadal są mapowane do pamięci. Implementacja może powiedzieć systemowi operacyjnemu, że „skończyliśmy już używać tej strony stosu. Dopóki nie powiem inaczej, wydaj wyjątek, który niszczy proces, jeśli ktoś dotknie poprzednio prawidłowej strony stosu”. Ponownie wdrożenia tego nie robią, ponieważ jest powolne i niepotrzebne.
Zamiast tego implementacje pozwalają popełniać błędy i uciec od tego. Większość czasu. Aż pewnego dnia coś naprawdę okropnego pójdzie nie tak i proces wybuchnie.
To jest problematyczne. Istnieje wiele zasad i bardzo łatwo jest je przypadkowo złamać. Z pewnością mam wiele razy. Co gorsza, problem często pojawia się tylko wtedy, gdy pamięć zostanie wykryta jako zepsuta miliardy nanosekund po zdarzeniu się korupcji, kiedy bardzo trudno jest ustalić, kto go pomieszał.
Bardziej bezpieczne dla pamięci języki rozwiązują ten problem, ograniczając swoją moc. W „normalnym” języku C # po prostu nie ma sposobu, aby wziąć adres lokalny i zwrócić go lub zachować na później. Możesz wziąć adres lokalny, ale język jest sprytnie zaprojektowany, tak że nie można go używać po zakończeniu lokalnych okresów. Aby pobrać adres lokalny i przekazać go z powrotem, musisz ustawić kompilator w specjalnym „niebezpiecznym” trybie i umieścić słowo „niebezpieczne” w swoim programie, aby zwrócić uwagę na fakt, że prawdopodobnie robisz coś niebezpiecznego, co może łamać zasady.
Do dalszego czytania:
address of local variable ‘a’ returned
; pokazy valgrindInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr