Lokowanie zmiennych lokalnych na stosie

0

Na stosie alokowane są zmienne lokalne, parametry funkcji, wskaźnik do miejsca w kodzie skąd została wywołana funkcja. O ile to ostatnie rozumiem, bo wywołanie "zagnieżdżonych" funkcji na prawdę tworzy stos wskaźników. Stos na który się wkłada coś a zdejmuje w odwrotnej kolejności. Ale jak zmienne lokalne i parametry metod mogą być kładzione na stos? Jeśli zadeklaruję sobie zmienne

int a;
int b;
int c;

To zostaną odłożone na stos w kolejności a, b, c.
c będzie na wierzchołku, więc nie będę mógł skorzystać z b.

Czy może to jest tak że dla każdej zmiennej lokalnej tworzy się jej własny stos na którym są zapisywane wartości "zasłaniane" przez jakieś bloki kodu?

2

Funkcjonuje pojęcie ramki (frame) stosu. Przy wejściu do funkcji tworzona jest ramka stosu, a przy wyjściu jest zdejmowana. Ramka zawiera zmienne lokalne dla aktualnego wywołania funkcji, parametry, adres powrotny. Dodatkowo ramka na samej górze (czyli ramka dla aktualnego wywołania aktualnej funkcji) może być rozciągana lub zmniejszana.

Konwencja w x86 jest zwykle taka, że rejestr EBP wskazuje na początek ramki stosu, a ESP na szczyt stosu w ogóle (czyli różnica między nimi jest aktualnym rozmiarem aktualnej ramki stosu). Ramka stosu jest normalnym obszarem pamięci po którym można indeksować tak jak po np tablicy.

2

Zasadniczo @Wibowit napisał prawdę i tylko prawdę, ale nie wiem czy potrzebne było takie zagłębianie się w szczegóły.

Otóż to co napisałeś byłoby sensowne, tylko trochę zbyt dosłownie podchodzisz do pojęcia stosu.

Stos to pewna abstrakcja, tak naprawdę możesz to sobie wyobrazić tak:

byte stack[1000] // stos (z jakimś ograniczeniem wielkości - stąd się StackOverflowy biorą (tak naprawdę to nie takie proste, bo nie od razu cała pamięć jest zajmowana, ale to już nieważne)).
int stackptr; // pokazuje na ostatni element na stosie

Kiedy jakaś funkcja (powiedzmy A) jest wywoływana, wie że przed stackptr są jej argumenty a później jakieś śmieci (tzn. zmienne i argumenty poprzednich funkcji)

[śmieci] [śmieci] [argument] <<stackptr>>

I teraz, jeśli chcesz wywołać jakąś funkcję B, wrzucasz na stos te parametry:

stack[stackptr++] = 1 // wrzuć pierwszy parametr - 1
stack[stackptr++] = 2 // wrzuć drugi parametr - 2
stack[stackptr++] = 3 // wrzuć trzeci parametr - 3

[śmieci] [śmieci] [argument] [1] [2] [3] <<stackptr>>

I skaczesz do kodu wywoływanej funkcji.
Czyli teraz jesteś w funkcji B.

Dla tej wywołanej funkcji, znowu liczą się tylko jej argumenty a wszystko wcześniej traktuje jako śmieci:
[śmieci] [śmieci] [śmieci] [1] [2] [3] <<stackptr>>

Jesli chcesz użyć parametru, używasz odpowiedniego przesunięcia od stackptr:

stack[stackptr] // ostatni parametr
stack[stackptr - 1] // przedostatni parametr
stack[stackptr - 2] // trzeci od końca parametr

A zmienne lokalne gdzie?
Jeśli funkcja chce mieć jakieś (np. 4) zmienne lokalne, rezerwuje sobie miejsce, o tak:

stackptr += 4

[śmieci] [śmieci] [argument] [1] [2] [3] [nic] [nic] [nic] [nic] <<stackptr>>

I co to daje? Że teraz ma wolne i nieużywane przez nic cztery miejsca na stosie. Jeśli wywoła ona jakąś jeszcze kolejną funkcję C, nie będzie ona ruszała tych czterech wolnych miejsc (bo dla niej to śmieci). No i właśnie dzięki temu może sobie spokojnie po nich pisać.

A kiedy funkcja B się kończy, cofa stackptr do poprzedniej pozycji
[śmieci] [śmieci] [argument] [1] [2] [3] <<stackptr>> [nic] [nic] [nic] [nic]

A jako że wszystko za stackptr jest już niepotrzebne, a argumenty dla B są już niepotrzebne, sterowanie wraca do funkcji A w takim stanie:
[śmieci] [śmieci] [argument] <<stackptr>>

I super, wszystko na stosie jest jak przed wywołaniem funkcji.

Nie wiem w sumie czy to jest czytelniejsze niż post powyżej, ale starałem się to w miarę obrazowo przedstawić...

0

Dzięki wielkie MSM za obszerne wytłumaczenie.

Tak naprawdę chodziło mi głównie o ten fragment.

msm napisał(a):

A zmienne lokalne gdzie?
Jeśli funkcja chce mieć jakieś (np. 4) zmienne lokalne, rezerwuje sobie miejsce, o tak:

stackptr += 4

[śmieci] [śmieci] [argument] [1] [2] [3] [nic] [nic] [nic] [nic] <<stackptr>>

I co to daje? Że teraz ma wolne i nieużywane przez nic cztery miejsca na stosie.

Teraz w ramach funkcji B, jak można korzystać z tych miejsc [nic] [nic] [nic] [nic]? Nie trzeba już przestrzegać reguły stosu że "grzebie" się tylko przy elemencie wierzchnim? Czyli wartości mogą się np. tak zmieniać:
...[nic] [4] [nic] [9]...
...[nic] [8] [nic] [9]...
...[7] [1] [nic] [9]...
...[7] [3] [5] [9]...
?

0

Nie trzeba już przestrzegać reguły stosu że "grzebie" się tylko przy elemencie wierzchnim?

Nie. Z punku widzenia procesora stos jest zwykłym kawałkiem pamięci i możesz mieć dowolną ilość wskaźników wskazujących na różne obszary stosu. A, że stos jest bardzo często używany, to są specjalne instrukcje procesora, operujące na jednym stosie, które przyspieszają operacje na tym stosie.

Np:
push x => stack[stackptr++] = x
pop x => x = stack[--stackptr]
itd

1

Dokładnie ;]

Jako że to wewnętrznie właśnie taka jakby tablica, komputer może sobie dowolnie grzebać wśród zmiennych lokalnych.
Wszystkie zmienne lokalne tworzą właśnie coś o czym @Wibowit wspomniał, czyli ramkę stosu - każda funkcja może grzebać w swojej ramce stosu (i dowolnie zmieniać), ale nie w innych.

Ale funkcja /nie może/ grzebać wśród zmiennych lokalnych nie należących do niej.

Jeśli chcesz na to patrzeć jak na klasyczny stos - możesz popatrzeć inaczej :].
Można zdejmować i wrzucać ramki stosu - czyli można wrzucić ramkę (wywołać kolejną funkcję) oraz zdjąć ramkę na szczycie (wyjść z funkcji). A każda funkcja może psuć tylko w ramce która jest na szczycie.

0

Ale funkcja /nie może/ grzebać wśród zmiennych lokalnych nie należących do niej.

W x86 procesor nie zabrania grzebania po innych ramkach stosu, nie zabrania też grzebania w nieużywanej części stosu. Kompilatory typowo dorzucają za to dodatkowy kod, który sprawdza czy grzebiemy tylko we własnej ramce.

Nie przypominam też sobie, by stos był jakoś specjalnie otagowanym obszarem pamięci. Nie jestem 100% pewien, ale podejrzewam, że mógłbym sobie przestawić wskaźnik wierzchołka stosu na dowolną komórkę w pamięci i nie spowodowałoby to natychmiastowo żadnego wyjątku.

Stos procesora nazywa się stosem dlatego, że mniej więcej działa jak stos, tzn zwykle mamy ramki ułożone w stos, dokładamy kolejne na górze i tylko z góry zdejmujemy.

1 użytkowników online, w tym zalogowanych: 0, gości: 1