Czy można uzyskać dostęp do pamięci zmiennej lokalnej poza jej zakresem?

1028

Mam następujący kod.

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

Kod działa tylko bez wyjątków czasu wykonywania!

Wynik był 58

Jak to możliwe? Czy pamięć lokalnej zmiennej nie jest niedostępna poza jej funkcją?

nieznanych
źródło
14
to nawet się nie skompiluje; jeśli naprawisz nieformalną firmę, gcc nadal będzie ostrzegać address of local variable ‘a’ returned; pokazy valgrindInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
patrz
76
@Serge: W młodości pracowałem kiedyś nad jakimś trudnym kodem zerowym, który działał w systemie operacyjnym Netware, i polegał na sprytnym poruszaniu się po wskaźniku stosu w sposób nie do końca usankcjonowany przez system operacyjny. Wiedziałbym, kiedy popełniłem błąd, ponieważ często stos nakładałby się na pamięć ekranu i mogłem po prostu oglądać bajty zapisywane bezpośrednio na ekranie. W dzisiejszych czasach nie można tego uniknąć.
Eric Lippert,
23
lol. Musiałem przeczytać pytanie i kilka odpowiedzi, zanim nawet zrozumiałem, gdzie jest problem. Czy to właściwie pytanie o zakres dostępu zmiennej? Nie używasz nawet „a” poza swoją funkcją. I to wszystko. Rzucanie się na niektóre odwołania do pamięci to zupełnie inny temat niż zmienny zakres.
erikbwork
10
Odpowiedź dupe nie oznacza pytania dupe. Wiele pytań dupe, które ludzie tutaj zaproponowali, są zupełnie innymi pytaniami, które odnoszą się do tego samego podstawowego objawu ... ale pytający zna sposób, aby wiedzieć, że powinni pozostać otwarci. Zamknąłem starszą wersję i połączyłem ją w to pytanie, które powinno pozostać otwarte, ponieważ ma bardzo dobrą odpowiedź.
Joel Spolsky
16
@Joel: Jeśli odpowiedź jest dobra, należy ją połączyć ze starszymi pytaniami , z których jest to duplikat, a nie odwrotnie. To pytanie jest zresztą duplikatem innych pytań zaproponowanych tutaj, a następnie niektórych (nawet jeśli niektóre z proponowanych są bardziej odpowiednie niż inne). Zauważ, że myślę, że odpowiedź Erica jest dobra. (W rzeczywistości oflagowałem to pytanie, aby połączyć odpowiedzi w jedno ze starszych pytań, aby uratować starsze pytania.)
sbi

Odpowiedzi:

4798

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:

Eric Lippert
źródło
55
@muntoo: Niestety system operacyjny nie wydaje syreny ostrzegawczej przed anulowaniem lub zwolnieniem strony pamięci wirtualnej. Jeśli chowasz się z tą pamięcią, gdy już jej nie posiadasz, system operacyjny ma pełne prawo do anulowania całego procesu po dotknięciu zwolnionej strony. Bum!
Eric Lippert,
82
@Kyle: Robią to tylko bezpieczne hotele. Niebezpieczne hotele uzyskują wymierne zyski, nie tracąc czasu na klucze programowe.
Alexander Torstling
497
@cyberguijarro: To, że C ++ nie jest bezpieczne dla pamięci, jest po prostu faktem. To nic nie „walić”. Gdybym powiedział na przykład: „C ++ to okropny miszmasz o niedokładnie określonych, zbyt skomplikowanych funkcjach, ułożonych na kruchym, niebezpiecznym modelu pamięci i jestem wdzięczny za każdy dzień, w którym nie pracuję już dla własnego zdrowia psychicznego”, to byłoby zawstydzające C ++. Wskazanie, że nie jest bezpieczne dla pamięci, wyjaśnia, dlaczego oryginalny plakat widzi ten problem; odpowiada na pytanie, a nie redaguje.
Eric Lippert,
49
Ściśle mówiąc, analogia powinna wspomnieć, że recepcjonista w hotelu był bardzo szczęśliwy, że zabrałeś ze sobą klucz. „Och, nie masz nic przeciwko, jeśli wezmę ten klucz ze sobą?” „Śmiało. Dlaczego miałbym się tym przejmować? Pracuję tylko tutaj”. Nie stanie się to nielegalne, dopóki nie spróbujesz go użyć.
philsquared
139
Proszę przynajmniej rozważyć napisanie książki pewnego dnia. Kupiłbym go, nawet jeśli byłby to tylko zbiór poprawionych i rozszerzonych postów na blogu, i jestem pewien, że zrobiłoby to wiele osób. Ale książka z oryginalnymi przemyśleniami na temat różnych zagadnień związanych z programowaniem byłaby świetną lekturą. Wiem, że trudno jest znaleźć na to czas, ale zastanów się nad jego napisaniem.
Dyppl,
275

To, co tu robisz, to po prostu czytanie i zapisywanie w pamięci, która kiedyś była adresem a. Teraz, gdy jesteś poza foo, jest to tylko wskaźnik do losowego obszaru pamięci. Tak się składa, że ​​w twoim przykładzie ten obszar pamięci istnieje i nic innego go nie używa w tej chwili. Nie niszczysz niczego, nadal z niego korzystasz i nic innego jeszcze go nie zastąpiło. Dlatego 5nadal tam jest. W prawdziwym programie pamięć ta zostałaby ponownie wykorzystana niemal natychmiast i można by to zepsuć (chociaż objawy mogą pojawić się dopiero później!)

Po powrocie z foosystemu operacyjnego informujesz system operacyjny, że nie używasz już tej pamięci i że można ją ponownie przypisać do innej funkcji. Jeśli masz szczęście i nigdy nie zostanie on ponownie przypisany, a system operacyjny nie przyłapie Cię na tym, że go użyjesz ponownie, to uciekniesz od kłamstwa. Są jednak szanse, że skończysz pisać na czymkolwiek innym, co skończy się na tym adresie.

Jeśli zastanawiasz się, dlaczego kompilator nie narzeka, to prawdopodobnie dlatego, że foozostał wyeliminowany przez optymalizację. Zazwyczaj ostrzega cię przed tego rodzaju rzeczami. C zakłada, że ​​wiesz, co robisz i technicznie nie naruszyłeś tutaj zakresu (nie ma odniesienia do asiebie poza foo), tylko reguły dostępu do pamięci, które wyzwalają raczej ostrzeżenie niż błąd.

Krótko mówiąc: to zwykle nie zadziała, ale czasami będzie przypadkiem.

Rena
źródło
151

Ponieważ przestrzeń dyskowa nie była jeszcze narzucona. Nie licz na to zachowanie.

msw
źródło
1
Człowieku, to było najdłuższe oczekiwanie na komentarz od czasu: „Czym jest prawda? - powiedział żartując Piłatowi”. Może w tej hotelowej szufladzie była Biblia Gideona. A zresztą co się z nimi stało? Zauważ, że nie są już obecni, przynajmniej w Londynie. Sądzę, że zgodnie z ustawodawstwem dotyczącym równości potrzebna byłaby biblioteka traktatów religijnych.
Rob Kent
Mógłbym przysiąc, że napisałem to dawno temu, ale wyskoczyło to niedawno i okazało się, że mojej odpowiedzi nie było. Teraz muszę wymyślić twoje aluzje powyżej, ponieważ spodziewam się, że będę rozbawiony, gdy to zrobię>. <
msw
1
Ha ha. Francis Bacon, jeden z największych eseistów brytyjskich, o którym niektórzy podejrzewają, że napisał sztuki Szekspira, ponieważ nie mogą zaakceptować tego, że dziecko ze szkoły podstawowej, syn rękawiczki, może być geniuszem. Taki jest system klasy angielskiej. Jezus powiedział: „Jestem Prawdą”. oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Rob Kent
83

Mały dodatek do wszystkich odpowiedzi:

jeśli zrobisz coś takiego:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

wyjście prawdopodobnie będzie: 7

Jest tak, ponieważ po powrocie z foo () stos jest zwalniany, a następnie ponownie wykorzystywany przez boo (). Jeśli zdemontujesz plik wykonywalny, zobaczysz go wyraźnie.

Michael
źródło
2
Prosty, ale świetny przykład do zrozumienia podstawowej teorii stosu. Wystarczy jedno dodanie testu, deklarujące „int a = 5;” w foo () jako „static int a = 5;” można wykorzystać do zrozumienia zakresu i czasu życia zmiennej statycznej.
kontrola w
15
-1 "dla będzie prawdopodobnie 7 ". Kompilator może zarejestrować boo. Może to usunąć, ponieważ jest to niepotrzebne. Istnieje duża szansa, że ​​* p nie będzie wynosić 5 , ale to nie znaczy, że istnieje jakiś szczególnie dobry powód, dla którego prawdopodobnie będzie to 7 .
Matt
2
Nazywa się to niezdefiniowanym zachowaniem!
Francis Cugler
dlaczego i jak booponownie wykorzystuje foostos? nie są stosy funkcji oddzielone od siebie, również mam śmieci, które uruchamiają ten kod w Visual Studio 2015
ampawd
1
@ampawd ma prawie rok, ale nie, „stosy funkcji” nie są od siebie oddzielone. KONTEKST ma stos. Ten kontekst używa swojego stosu do wejścia do main, następnie schodzi do foo(), istnieje, a następnie schodzi do boo(). Foo()i Boo()oba wchodzą ze wskaźnikiem stosu w tym samym miejscu. Nie jest to jednak zachowanie, na które należy polegać. Inne „rzeczy” (takie jak przerwania lub system operacyjny) mogą używać stosu między wywołaniem boo()i foo(), modyfikując jego zawartość ...
Russ Schultz
71

W C ++ możesz uzyskać dostęp do dowolnego adresu, ale to nie znaczy, że powinieneś . Adres, do którego uzyskujesz dostęp, jest już nieważny. To działa , bo nic innego nie kodowany pamięć po foo wrócił, ale może upaść w wielu okolicznościach. Spróbuj przeanalizować swój program za pomocą Valgrind lub nawet po prostu skompiluj go zoptymalizowany i zobacz ...

Charles Brunet
źródło
5
Prawdopodobnie masz na myśli, że możesz spróbować uzyskać dostęp do dowolnego adresu. Ponieważ większość dzisiejszych systemów operacyjnych nie zezwala żadnemu programowi na dostęp do żadnego adresu; istnieje mnóstwo zabezpieczeń chroniących przestrzeń adresową. Właśnie dlatego nie będzie już innego LOADLIN.EXE.
v010dya
66

Nigdy nie rzucasz wyjątku C ++, uzyskując dostęp do niepoprawnej pamięci. Podajesz tylko przykład ogólnej idei odwoływania się do dowolnej lokalizacji pamięci. Mógłbym zrobić to samo:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Tutaj po prostu traktuję 123456 jako adres podwójnego i piszę do niego. Może się zdarzyć dowolna liczba rzeczy:

  1. qmoże faktycznie być poprawnym adresem podwójnego adresu, np double p; q = &p;.
  2. q może wskazywać gdzieś wewnątrz przydzielonej pamięci, a ja po prostu nadpisuję tam 8 bajtów.
  3. q wskazuje poza przydzieloną pamięć, a menedżer pamięci systemu operacyjnego wysyła sygnał błędu segmentacji do mojego programu, powodując jego zakończenie.
  4. Wygrywasz na loterii.

Sposób, w jaki go skonfigurowałeś, jest nieco bardziej rozsądny, jeśli zwracany adres wskazuje prawidłowy obszar pamięci, ponieważ prawdopodobnie będzie on znajdował się nieco dalej na stosie, ale nadal jest to nieprawidłowa lokalizacja, do której nie można uzyskać dostępu w moda deterministyczna.

Nikt nie sprawdzi automatycznie semantycznej ważności takich adresów pamięci podczas normalnego wykonywania programu. Jednak debugger pamięci, taki jak valgrindten, z radością to zrobi, więc powinieneś uruchomić swój program i być świadkiem błędów.

Kerrek SB
źródło
9
Zamierzam teraz napisać program, który nadal uruchamia ten program, aby4) I win the lottery
Aidiakapi
28

Czy skompilowałeś swój program z włączonym optymalizatorem? foo()Funkcja ta jest bardzo prosta i może być inlined lub zastąpione w wynikowym kodzie.

Ale zgadzam się z Markiem B, że wynikające z tego zachowanie jest niezdefiniowane.

gastush
źródło
To mój zakład. Optymalizator zrzucił wywołanie funkcji.
Erik Aronesty
9
To nie jest konieczne. Ponieważ żadna nowa funkcja nie jest wywoływana po foo (), ramka lokalnego stosu funkcji po prostu nie jest jeszcze nadpisana. Dodaj kolejne wywołanie funkcji po foo (), a 5zostanie zmienione ...
Tomas
Uruchomiłem program z GCC 4.8, zamieniając cout na printf (i włączając stdio). Słusznie ostrzega „ostrzeżenie: adres zmiennej lokalnej„ a ”zwrócił [-Wreturn-local-addr]”. Wyjścia 58 bez optymalizacji i 08 z -O3. O dziwo P ma adres, mimo że jego wartość wynosi 0. Oczekiwałem NULL (0) jako adresu.
kevinf
22

Twój problem nie ma nic wspólnego z zakresem . W pokazanym kodzie funkcja mainnie widzi nazw w funkcji foo, więc nie możesz uzyskać dostępu aw foo bezpośrednio z nazwą na zewnątrz foo.

Problemem jest to, że program nie sygnalizuje błędu podczas odwoływania się do nielegalnej pamięci. Wynika to z faktu, że standardy C ++ nie określają bardzo wyraźnej granicy między pamięcią nielegalną a pamięcią legalną. Odwoływanie się do czegoś w wyskakującym stosie czasami powoduje błąd, a czasem nie. To zależy. Nie licz na to zachowanie. Załóż, że zawsze spowoduje błąd podczas programowania, ale załóż, że nigdy nie zasygnalizuje błędu podczas debugowania.

Chang Peng
źródło
Pamiętam starą kopię Turbo C Programming dla IBM , którą kiedyś bawiłem się, kiedy szczegółowo opisywano, jak bezpośrednio manipulować pamięcią graficzną i układem pamięci wideo w trybie tekstowym IBM. Oczywiście system, w którym działał kod, jasno zdefiniował znaczenie zapisu na te adresy, więc dopóki nie martwiłeś się o możliwość przenoszenia na inne systemy, wszystko było w porządku. IIRC, wskaźniki do unieważnienia były częstym tematem w tej książce.
CVn
@Michael Kjörling: Jasne! Ludzie lubią od czasu do czasu wykonywać brudne prace;)
Chang Peng
17

Zwracasz tylko adres pamięci, jest to dozwolone, ale prawdopodobnie błąd.

Tak, jeśli spróbujesz wyrejestrować ten adres pamięci, będziesz mieć niezdefiniowane zachowanie.

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}
Brian R. Bondy
źródło
Nie zgadzam się: istnieje problem przed cout. *awskazuje na nieprzydzieloną (uwolnioną) pamięć. Nawet jeśli go nie odrzucisz, nadal jest niebezpieczny (i prawdopodobnie fałszywy).
ereOn
@ereOn: Wyjaśniłem więcej, co mam na myśli przez problem, ale nie, nie jest to niebezpieczne pod względem prawidłowego kodu c ++. Ale jest to niebezpieczne z punktu widzenia prawdopodobieństwa, że ​​użytkownik popełnił błąd i zrobi coś złego. Być może na przykład próbujesz zobaczyć, jak rośnie stos, i zależy ci tylko na wartości adresu i nigdy go nie odrzucisz.
Brian R. Bondy
17

To klasyczne, nieokreślone zachowanie , o którym dyskutowano tutaj dwa dni temu - poszukaj trochę w witrynie. Krótko mówiąc, miałeś szczęście, ale wszystko mogło się zdarzyć, a Twój kod uniemożliwia dostęp do pamięci.

Kerrek SB
źródło
17

To zachowanie jest niezdefiniowane, jak zauważył Alex - w rzeczywistości większość kompilatorów ostrzega przed zrobieniem tego, ponieważ jest to łatwy sposób na awarie.

Na przykład tego rodzaju upiorne zachowanie, które możesz się spodziewać , spróbuj tego przykładu:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

Wyświetla to „y = 123”, ale wyniki mogą się różnić (naprawdę!). Twój wskaźnik blokuje inne niepowiązane zmienne lokalne.

AHelps
źródło
17

Zwróć uwagę na wszystkie ostrzeżenia. Nie tylko rozwiązuj błędy.
GCC pokazuje to Ostrzeżenie

ostrzeżenie: zwrócony adres zmiennej lokalnej „a”

To jest moc C ++. Powinieneś dbać o pamięć. Z -Werrorflagą to ostrzeżenie staje się błędem i teraz musisz go debugować.

sam
źródło
16

Działa, ponieważ stos nie został zmieniony (jeszcze) od czasu umieszczenia tam. Wywołaj kilka innych funkcji (które również wywołują inne funkcje) przed aponownym uzyskaniem dostępu i prawdopodobnie nie będziesz już miał tyle szczęścia ... ;-)

Adrian Grigore
źródło
15

W rzeczywistości wywołałeś niezdefiniowane zachowanie.

Zwrócenie adresu tymczasowego dzieła, ale ponieważ tymczasowe zostaną zniszczone na końcu funkcji, wyniki dostępu do nich będą niezdefiniowane.

Więc nie zmodyfikowałeś, aale raczej miejsce w pamięci, w którym akiedyś był. Ta różnica jest bardzo podobna do różnicy między awarią a awarią.

Alexander Gessler
źródło
13

W typowych implementacjach kompilatora można myśleć o kodzie jako o „wypisaniu wartości bloku pamięci z adresem, który był wcześniej zajęty przez”. Ponadto, jeśli dodasz nowe wywołanie funkcji do funkcji składającej się z lokalnego int, istnieje duża szansa, że ​​wartość a(lub adres pamięci, którya zmieni się który wskazywał). Dzieje się tak, ponieważ stos zostanie zastąpiony nową ramką zawierającą różne dane.

Jest to jednak niezdefiniowane zachowanie i nie powinieneś na nim polegać!

larsmoa
źródło
3
„wypisz wartość bloku pamięci z adresem, który był kiedyś zajęty przez” nie jest całkiem poprawne. Brzmi to tak, jakby jego kod miał jakieś dobrze zdefiniowane znaczenie, co nie jest prawdą. Masz jednak rację, że prawdopodobnie tak właśnie większość kompilatorów to zaimplementuje.
Brennan Vincent
@BrennanVincent: Podczas gdy pamięć była zajęta a, wskaźnik posiadał adres a. Chociaż Standard nie wymaga, aby implementacje definiowały zachowanie adresów po zakończeniu okresu ich istnienia, rozpoznaje również, że na niektórych platformach UB jest przetwarzany w udokumentowany sposób charakterystyczny dla środowiska. Chociaż adres zmiennej lokalnej nie będzie generalnie przydatny po przekroczeniu zakresu, niektóre inne rodzaje adresów mogą nadal mieć znaczenie po upływie czasu ich odpowiednich obiektów docelowych.
supercat
@BrennanVincent: Na przykład, chociaż Standard może nie wymagać, aby implementacje pozwalały na reallocporównanie przekazanego wskaźnika z wartością zwracaną, ani nie pozwalały na dostosowywanie wskaźników w starym bloku, aby wskazywał na nowy, niektóre implementacje to robią , a kod wykorzystujący taką funkcję może być bardziej wydajny niż kod, który musi unikać jakichkolwiek działań - nawet porównań - obejmujących wskaźniki przydzielonej alokacji realloc.
supercat
13

Może, ponieważ ajest to zmienna przydzielona tymczasowo na czas życia jej zakresu ( foofunkcji). Po powrocie zfoo pamięci pamięć jest wolna i może zostać nadpisana.

To, co robisz, jest określane jako niezdefiniowane zachowanie . Nie można przewidzieć wyniku.

littleadv
źródło
11

Rzeczy z poprawnym (?) Wyjściem konsoli mogą się radykalnie zmienić, jeśli użyjesz :: printf, ale nie cout. Możesz bawić się debuggerem w ramach poniższego kodu (testowane na x86, 32-bit, MSVisual Studio):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}
Mykoła
źródło
4

Po powrocie z funkcji wszystkie identyfikatory są niszczone zamiast wartości przechowywanych w pamięci i nie możemy zlokalizować wartości bez identyfikatora, ale ta lokalizacja nadal zawiera wartość przechowywaną przez poprzednią funkcję.

Zatem tutaj funkcja foo()zwraca adres ai ajest niszczona po zwróceniu adresu. I możesz uzyskać dostęp do zmodyfikowanej wartości za pośrednictwem tego zwróconego adresu.

Weźmy przykład z prawdziwego świata:

Załóżmy, że mężczyzna ukrywa pieniądze w miejscu i informuje o miejscu. Po pewnym czasie umiera mężczyzna, który powiedział ci, gdzie są pieniądze. Ale nadal masz dostęp do tych ukrytych pieniędzy.

Ghulam Moinul Quadir
źródło
3

Jest to „brudny” sposób używania adresów pamięci. Kiedy zwracasz adres (wskaźnik), nie wiesz, czy należy on do lokalnego zasięgu funkcji. To tylko adres. Teraz, gdy wywołałeś funkcję „foo”, ten adres (lokalizacja pamięci) „a” został już tam przydzielony w (bezpiecznej, przynajmniej na razie) adresowalnej pamięci twojej aplikacji (procesu). Po zwróceniu funkcji „foo” adres „a” można uznać za „brudny”, ale jest tam, nie jest czyszczony ani zaburzony / modyfikowany przez wyrażenia w innej części programu (przynajmniej w tym konkretnym przypadku). Kompilator AC / C ++ nie powstrzymuje cię przed takim „brudnym” dostępem (może cię jednak ostrzec, jeśli ci na tym zależy).

Ayub
źródło
1

Twój kod jest bardzo ryzykowny. Tworzysz zmienną lokalną (która jest uważana za zniszczoną po zakończeniu funkcji) i zwracasz adres pamięci tej zmiennej po jej zniszczeniu.

Oznacza to, że adres pamięci może być prawidłowy lub nie, a kod będzie podatny na możliwe problemy z adresem pamięci (na przykład błąd segmentacji).

Oznacza to, że robisz coś bardzo złego, ponieważ przekazujesz adres pamięci do wskaźnika, który wcale nie jest godny zaufania.

Zamiast tego rozważ ten przykład i przetestuj go:

int * foo()
{
   int *x = new int;
   *x = 5;
   return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; //better to put a new-line in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

W przeciwieństwie do twojego przykładu, w tym przykładzie jesteś:

  • przydzielanie pamięci dla int do funkcji lokalnej
  • ten adres pamięci jest nadal ważny także po wygaśnięciu funkcji (nie jest przez nikogo usuwany)
  • adres pamięci jest wiarygodny (ten blok pamięci nie jest uważany za wolny, więc nie zostanie zastąpiony, dopóki nie zostanie usunięty)
  • adres pamięci należy usunąć, gdy nie jest używany. (zobacz usunięcie na końcu programu)
Nobun
źródło
Czy dodałeś coś, czego nie obejmują już istniejące odpowiedzi? I proszę nie używać surowych wskaźników / new.
Wyścigi lekkości na orbicie
1
Pytający użył surowych wskaźników. Zrobiłem przykład, który odzwierciedlał dokładnie ten przykład, który zrobił, aby pozwolić mu zobaczyć różnicę między nieufnym wskaźnikiem a zaufanym wskaźnikiem. Właściwie jest inna odpowiedź podobna do mojej, ale używa strcpy, który, IMHO, może być mniej czytelny dla początkującego programisty niż mój przykład, który używa nowego.
Nobun
Nie korzystali new. Uczysz ich używania new. Ale nie powinieneś używać new.
Wyścigi lekkości na orbicie
Więc Twoim zdaniem lepiej jest przekazać adres do zmiennej lokalnej, która jest zniszczona w funkcji niż faktycznie alokuje pamięć? To nie ma sensu. Zrozumienie koncepcji przydzielania i zwalniania pamięci jest ważne, imho, głównie jeśli pytasz o wskaźniki (pytający nie używał nowych, ale używał wskaźników).
Nobun
Kiedy to powiedziałem? Nie, lepiej jest użyć inteligentnych wskaźników, aby poprawnie wskazać własność zasobu, do którego istnieje odwołanie. Nie używaj neww 2019 r. (Chyba że piszesz kod biblioteki) i nie ucz nowych użytkowników, aby to robili! Twoje zdrowie.
Wyścigi lekkości na orbicie