Stos i funkcje - zagmatwanie w książce

0

Wydawało mi się, iż rozumiem filozofię stosu do chwili,gdy zacząłem czytać akapit Stos i funkcje w książce c++ dla każdego. Autorzy tak to pogmatwali, że nie wiem. Chyba 20 raz to już dzisiaj czytam.

"1. Zwiększany jest adres we wskaźniku instrukcji i wskazuje on instrukcję następną po tej, która wywołuje funkcję. Ten adres jest następnie umieszczany na stosie; stanowi adres powrotu z funkcji."

Nie rozumiem. Czy zwiększenie adresu we wskaźniku instrukcji oznacza "zdjęcie talerza" ze stosu? ...czy dołożenie go? Bo zgodnie z wykonywanym kodem programu, kod wykonywany jest przecież od góry w dół wers po wersie.

"2. Na stosie jest tworzone miejsce dla zadeklarowanego typu wartości zwracanej przez funkcję.
Gdy zwracany typ jest zadeklarowany jako int, w przypadku systemu z dwubajtowymi
liczbami całkowitymi, na stos są odkładane dwa kolejne bajty, ale nie jest w nich umieszczana
żadna wartość („odpady”, które się w nich dotąd znajdowały, pozostają tam nadal)."

No za tym jeszcze jako tako nadążam, ale...

"3. Do wskaźnika instrukcji jest ładowany adres wywoływanej funkcji (ten adres jest zawarty w kodzie aktualnie wykonywanej instrukcji wywołania funkcji), dzięki czemu następna wykonywana instrukcja będzie już instrukcją funkcji."

O co chodzi z tym adresem wywoływanej funkcji? Wydawało mi się, iż adresem funkcji jest jej nazwa np funkcja(){}. To "{}" jest przecież nazywane ciałem funkcji. No to o jaki adres chodzi,bo się już pogubiłem. Czy oni przez adres mają na myśli właśnie nazwę funkcji?

"4. Odczytywany jest adres bieżącego szczytu stosu, następnie zostaje on umieszczony w specjalnym wskaźniku nazywanym ramką stosu (ang. stack frame). Wszystko, co zostanie umieszczone na stosie od tego momentu, jest uważane za „lokalne” dla funkcji."

Pogubiłem się. Co jest gdzie? Adres bieżącego szczytu stosu? Ok. Ale oni wcześniej pisali, iż to,co jest nad stosem, to odpady a nie rzeczy "lokalne". A może coś mi się pomyliło;-) No cóż, takie mam wrażenie;-)

"5. Na stosie umieszczane są argumenty funkcji.
6. Wykonywana jest instrukcja wskazywana przez wskaźnik instrukcji (następuje wykonanie pierwszej instrukcji w funkcji).
7. W trakcie ich definiowania, lokalne zmienne zostają umieszczane na stosie."

Myślę, że jeśli załapię i poukładam tamte rzeczy, za którymi nie nadążam, to ostatnie trzy punkty nie będą trudne do zrozumienia. Chodzi też o to, że mam kłopot ze stworzeniem w głowie poukładanej logiki miedzy tymi wszystkimi punktami. Pozostaje też pytanie czy znajdzie się tu osoba, która znajdzie czas i ochotę w miarę krótko mi to jakoś pomóc zrozumieć i poukładać.

No cóż, z góry dzięki;-)

0
  1. Zwiększenie wartości NIE MA NIC wspólnego ze stosem. Masz coś takiego jak IP - instruction pointer. Wyobraź sobie że pokazuje on "linijkę" w kodzie która jest aktualnie wykonywana. Jak wykonasz linijkę to zwiększasz ten adres o 1, tak żeby pokazywał na następną linijkę. Jak wywołujesz funkcję to następuje "skok" do zupełnie innej linijki, prawda? Więc musimy zapamiętać gdzie byliśmy przed wywołaniem funkcji (a dokładniej gdzie powinniśmy się znaleźć po wywołaniu funkcji, stąd to zwiększenie licznika o 1).
  2. Mylisz pojęcia związane z C/C++ z pojęciami związanymi z niskopoziomowym opisem wywołania funkcji na poziomie języka asemblera. Cały ten opis jest pisany z perspektywy asemblera a nie C! Tutaj chodzi po prostu o to że odczytywany jest z kodu źródlowego adres pod jakim znajduje się w pamięci funkcja i pod ten adres następuje skok.
  3. Ech. Na stos można dokładać. Jeśli wywołałeś funkcję to na stosie już cośtam było i chciałbyś żeby po wyjściu z funkcji stos wyglądal samo jak przed jej wywołaniem (i żeby zmienne lokalne funkcji zostały "zapomniane"). W efekcie musisz zapamiętać gdzie stos miał szczyt przed wywołaniem funkcji. Zauważ ze wewnatrz funkcji szczyt stosu się przesunie, w miarę dodawania zmiennych lokalnych. Dlatego też po wyjściu z funkcji chcemy ten szczyt przywrócić do poziomu sprzed wywołania funkcji (i nasze zmienne lokalne staną się, jak to ująłeś, "odpadami").
  4. Argumenty funkcji to zwykłe zmienne lokalne, wiec ladują na stosie.
  5. Patrz punkt 1
  6. Nie wiem co miałbym tu opisać ;]
0
Shalom napisał(a)
  1. Zwiększenie wartości NIE MA NIC wspólnego ze stosem. Masz coś takiego jak IP - instruction pointer. Wyobraź sobie że pokazuje on "linijkę" w kodzie która jest aktualnie wykonywana. Jak wykonasz linijkę to zwiększasz ten adres o 1, tak żeby pokazywał na następną linijkę. Jak wywołujesz funkcję to następuje "skok" do zupełnie innej linijki, prawda? Więc musimy zapamiętać gdzie byliśmy przed wywołaniem funkcji (a dokładniej gdzie powinniśmy się znaleźć po wywołaniu funkcji, stąd to zwiększenie licznika o 1).

Od strony edytora, w którym piszę kod,sprawa wygląda dość łatwo i tutaj wszystko rozumiem. Chyba...tzn. jeśli mam np linię 100 i dodaje do niej 1 (linia wywoływania funkcji), to "komputer" wtedy wie, że 101 to adres wywołania w kodzie tej funkcji a linia 102 (czyli znowu +1), to adres tuż po zakończeniu wywoływania tej funkcji. Tutaj akurat jest wszystko dla mnie jasne jak słońce, całe szczęście...

Shalom napisał(a)
  1. Mylisz pojęcia związane z C/C++ z pojęciami związanymi z niskopoziomowym opisem wywołania funkcji na poziomie języka asemblera. Cały ten opis jest pisany z perspektywy asemblera a nie C!

No właśnie pewnie dlatego mi się wszytko kiełbasiło;-))

Shalom napisał(a)

Tutaj chodzi po prostu o to że odczytywany jest z kodu źródlowego adres pod jakim znajduje się w pamięci funkcja i pod ten adres następuje skok.

No dobra, ale!! Czy ten adres jest "widoczny" w kodzie źródłowym? Czym on jest tak z perspektywy "ludzkich oczu" ? Czy to jest ten numer linii? (mówię oczywiście o tym co widać z poziomu c++ a nie asemblera). Bo tu mi się w głowie zrobił właśnie kociokwik.

Shalom napisał(a)
  1. Ech. Na stos można dokładać. Jeśli wywołałeś funkcję to na stosie już cośtam było i chciałbyś żeby po wyjściu z funkcji stos wyglądal samo jak przed jej wywołaniem (i żeby zmienne lokalne funkcji zostały "zapomniane"). W efekcie musisz zapamiętać gdzie stos miał szczyt przed wywołaniem funkcji. Zauważ ze wewnatrz funkcji szczyt stosu się przesunie, w miarę dodawania zmiennych lokalnych. Dlatego też po wyjściu z funkcji chcemy ten szczyt przywrócić do poziomu sprzed wywołania funkcji (i nasze zmienne lokalne staną się, jak to ująłeś, "odpadami").

No tu mi się właśnie najbardziej kiełbasi, choć chyba nie powinno. Powiedziałeś, że na stos można dokładać, czyli tym bieżącym stosem jest konkretnie co? Instrukcje w ciele wywoływanej funkcji? A lokalne zmienne tej funkcji, rozumiem, że są tym co można "położyć" na moim stosie (na ramce stosu). Albo można o tych zmiennych lokalnych zapomnieć...i wtedy nazywa się je "odpadami". Nie rozumiem jednak co mam rozumieć przez owe "przesunięcie się szczytu". Czym jest ten szczyt i o który z nich chodzi,bo ja dostrzegam dwa: 1. "górną" część ramki stosu; 2. szczyt samych odpadów, które dołożyłem sobie na górze ramki stosu. Rozum mi podpowiada, że przesunięcie tego drugiego szczytu do góry może mieć miejsce,gdy dołożę jakąś lokalna zmienną. Choć w kodzie źródłowym mój stos nie rośnie do góry, tylko raczej "dolna granica" deklaracje zmiennych/instrukcje przesuwa się w dół. No właśnie...i tu mi się kiełbasi. No stosie dokładam a w kodzie źródłowym wszystko idzie mi w dół;-))

Reasumując;-) Muszę wiedzieć (żeby to załapać) czym w moim obrazkowym stosie jest "funkcja();" czym jest "()" a czym jest "{}". Tzn. wiem, że "funkcja();" to jest jej wywołanie, "()" jest miejscem parametrów a "{}" jest ciałem - jasna sprawa;-) Ale potrzebuję się dowiedzieć, w jakim porządku te elementy są poukładane w stosie. Tak na prawdę tylko to potrzebuję teraz wiedzieć. Bardziej dokładne rzeczy od strony asemblera będę gryzł później. Na razie muszę to sobie wyobrazić w postaci klocków lego.

0

Z poziomu C++ to niewiele w czasie wykonania programu widać, bo kod już dawno jest kodem asemblera zamienionym na kod maszynowy. Ale możesz sobie przyjąć że chodzi tutaj o numer linii w której znajduje się funkcja (bo opisując o co chodzi ze zwiększaniem wskaźnika IP też sobie tak założyliśmy, a to wcale nie jest prawda)
Stos to stos. Wygląda jak stos monet. Można wkładać na górę i zdejmować z góry (nie wolno wyciągać ze środka!) no i stos może mieć ograniczoną wysokość. Na stosie leżą DANE. W trakcie wywołania funkcji na stos wrzucasz:

  • adres powrotu
  • argumenty funkcji
    I dodatkowo zapamiętujesz sobie gdzie był szczyt stosu przez wywołaniem (czyli zapamiętujesz że od "spodu" do miejsca gdzie jest adres powrotu jest na przykład 100 pozycji). Wewnątrz funkcji na stosie lądują tez zmienne lokalne itd. A po wyjściu z funkcji przesuwamy wskaźnik szczytu stosu na naszą 100 pozycję (więc wszystko co jest "wyżej" traktujemy jako śmieci), odczytujemy adres powrotu (czyli informację o tym do której "linijki skoczyć" i skaczemy).
0
Shalom napisał(a)

Z poziomu C++ to niewiele w czasie wykonania programu widać, bo kod już dawno jest kodem asemblera zamienionym na kod maszynowy. Ale możesz sobie przyjąć że chodzi tutaj o numer linii w której znajduje się funkcja (bo opisując o co chodzi ze zwiększaniem wskaźnika IP też sobie tak założyliśmy, a to wcale nie jest prawda)
Stos to stos. Wygląda jak stos monet. Można wkładać na górę i zdejmować z góry (nie wolno wyciągać ze środka!) no i stos może mieć ograniczoną wysokość. Na stosie leżą DANE. W trakcie wywołania funkcji na stos wrzucasz:

  • adres powrotu
  • argumenty funkcji

Okej, nadążam...

Shalom napisał(a)

I dodatkowo zapamiętujesz sobie gdzie był szczyt stosu przez wywołaniem (czyli zapamiętujesz że od "spodu" do miejsca gdzie jest adres powrotu jest na przykład 100 pozycji).

Okej, jednak dla uściślenia muszę wiedzieć co masz na myśli przez te 100 pozycji? Sory za tę upierdliwość, ale muszę taki być, aby sensownie to sobie wszystko wyobrazić. Czym więc jest obszar, który się znajduje bezpośrednio pod "adresem powrótu"? Bo adres powrotu to sprawa już na szczęście dla mnie jasna. Czy Te 100 pozycji to instrukcje wewnątrz funkcji?

Shalom napisał(a)

Wewnątrz funkcji na stosie lądują tez zmienne lokalne itd. A po wyjściu z funkcji przesuwamy wskaźnik szczytu stosu na naszą 100 pozycję (więc wszystko co jest "wyżej" traktujemy jako śmieci), odczytujemy adres powrotu (czyli informację o tym do której "linijki skoczyć" i skaczemy).

Czyli to znaczy, że poprawnie ciebie zrozumiałem?...te 100 pozycji to ciąg instrukcji, który w stosie znajduje się pod adresem powrotu i argumentami funkcji. Gdy działanie instrukcji dobiegło końca a argumenty nie są potrzebne, to samoistnie stają się odpadami i wskaźnik instrukcji na stosie wraca na naszą pozycję nr sto. Błagam! Powiedz, że załapałem!;-)

0

Te przykładowe 100 pozycji o których pisałem to po prostu zmienne lokalne funkcji z której wywoływaliśmy naszą funkcję (czyli na przykład zmienne lokalne funkcji main()). Na stosie nie ma instrukcji, tylko DANE! To co napisałes na końcu jest ok.

0
Shalom napisał(a)

Te przykładowe 100 pozycji o których pisałem to po prostu zmienne lokalne funkcji z której wywoływaliśmy naszą funkcję (czyli na przykład zmienne lokalne funkcji main()). Na stosie nie ma instrukcji, tylko DANE! To co napisałes na końcu jest ok.

Zaraz,ale ja na końcu napisałem, że bredzę;-)) Świetnie...

Ale dobra, okej, czuję, że powoli dochodzę do sedna. Jeszcze trochę i myślę, że temat będzie zamknięty.
Czyli idąc od dołu mam tak: 100 pozycji (czyt. argumety funkcji) a na tych 100 pozycjach jest adres powrotu (mam nadzieję...). Natomiast to, co jest na samej górze tzn. nad adresem powrotu, to są odpady. Nie mogę sobie jednak wyobrazić tych argumentów tzn. tych 100 pozycji danych. Po prostu wydawało mi się, że dane jako takie są powiązane ze zmiennymi lokalnymi. Jeśli więc tak jest, to wyobrażałem sobie, że dane także stają się odpadami.

0

W ogóle nie rozumiem o czym teraz piszesz. Weź do ręki książkę do asemblera i przeczytaj jak wygląda wykonanie programu w asemblerze. Bo inaczej na prawdę trudno ci będzie cokolwiek wyjaśnić.
Te przykładowe 100 pozycji o których pisałem też staną się śmieciami jak tylko wyskoczymy z funkcji w której były deklarowane. Adresów powrotu na stosie może być wiele, bo przecież możesz z funkcji wywoływać inne funkcje, które wywołują inne funkcje. Wygląda to tak:
W C/C++ mamy kontrukcję:

void funkcja2(int funkcja2_x){
  int funkcja2_y;
  return;
}
void funkcja1(int funkcja1_x){
  int funkcja1_y;
  funkcja2(10);
  return;
}

int main(){
  int mainx;
  int mainy;
  funkcja1(5);
  return 0;
}

A stos w chwili kiedy jesteśmy w funkcji 2, przed instrukcją return wygląda tak:

  • wolne miejsce na stosie
  • aktualny wierzchołek stosu
  • zmienne lokalne funkcji2:
    • funkcja2_y
    • funkcja2_x
  • adres powrotu z funkcji2
  • zmienne lokalne funkcji1:
    • funkcja1_y
    • funkcja1_x
  • adres powrotu z funkcji 1
  • zmienne lokalne main:
    • main_y
    • main_x

W następnym kroku, czyli po wykonaniu return z funkcji2 stos zmieni się na taki:

  • wolne miejsce na stosie
  • aktualny wierzchołek stosu
  • zmienne lokalne funkcji1:
    • funkcja1_y
    • funkcja1_x
  • adres powrotu z funkcji 1
  • zmienne lokalne main:
    • main_y
    • main_x

Czyli jak widać to co było wyżej stało sie "wolnym miejscem"

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