Mam ten fragment kodu w c:
int q = 10;
int s = 5;
int a[3];
printf("Address of a: %d\n", (int)a);
printf("Address of a[1]: %d\n", (int)&a[1]);
printf("Address of a[2]: %d\n", (int)&a[2]);
printf("Address of q: %d\n", (int)&q);
printf("Address of s: %d\n", (int)&s);
Wynik to:
Address of a: 2293584
Address of a[1]: 2293588
Address of a[2]: 2293592
Address of q: 2293612
Address of s: 2293608
Widzę więc, że adresy pamięci od a
do a[2]
zwiększają się o 4 bajty każdy. Ale od q
do s
adresy pamięci zmniejszają się o 4 bajty.
Zastanawiam się nad 2 rzeczami:
- Czy stos rośnie czy maleje? (W tym przypadku wygląda na to, że oba)
- Co się dzieje między adresami pamięci
a[2]
iq
? Dlaczego jest tam duża różnica w pamięci? (20 bajtów).
Uwaga: to nie jest zadanie domowe. Jestem ciekaw, jak działa stack. Dzięki za wszelką pomoc.
Odpowiedzi:
Zachowanie stosu (rośnie lub rośnie) zależy od interfejsu binarnego aplikacji (ABI) i sposobu organizacji stosu wywołań (inaczej rekordu aktywacji).
Przez cały okres swojego życia program musi komunikować się z innymi programami, takimi jak system operacyjny. ABI określa, w jaki sposób program może komunikować się z innym programem.
Stos dla różnych architektur może rosnąć w obie strony, ale w przypadku architektury będzie spójny. Sprawdź ten link wiki. Ale o wzroście stosu decyduje ABI tej architektury.
Na przykład, jeśli weźmiesz MIPS ABI, stos wywołań jest zdefiniowany jak poniżej.
Rozważmy, że funkcja „fn1” wywołuje „fn2”. Teraz ramka stosu widziana przez 'fn2' wygląda następująco:
direction of | | growth of +---------------------------------+ stack | Parameters passed by fn1(caller)| from higher addr.| | to lower addr. | Direction of growth is opposite | | | to direction of stack growth | | +---------------------------------+ <-- SP on entry to fn2 | | Return address from fn2(callee) | V +---------------------------------+ | Callee saved registers being | | used in the callee function | +---------------------------------+ | Local variables of fn2 | |(Direction of growth of frame is | | same as direction of growth of | | stack) | +---------------------------------+ | Arguments to functions called | | by fn2 | +---------------------------------+ <- Current SP after stack frame is allocated
Teraz możesz zobaczyć, że stos rośnie w dół. Tak więc, jeśli zmienne są przydzielone do lokalnej ramki funkcji, adresy zmiennych faktycznie rosną w dół. Kompilator może zdecydować o kolejności zmiennych do alokacji pamięci. (W twoim przypadku może to być „q” lub „s”, które jest pierwszą przydzieloną pamięcią stosową. Ale generalnie kompilator dokonuje alokacji pamięci stosu zgodnie z kolejnością deklaracji zmiennych).
Ale w przypadku tablic alokacja ma tylko jeden wskaźnik, a pamięć, która ma zostać przydzielona, będzie faktycznie wskazywana przez pojedynczy wskaźnik. Pamięć musi być ciągła dla tablicy. Tak więc, chociaż stos rośnie w dół, w przypadku tablic stos rośnie.
źródło
To właściwie dwa pytania. Jedna dotyczy tego, w jaki sposób rośnie stos, gdy jedna funkcja wywołuje inną (kiedy przydzielana jest nowa ramka), a druga dotyczy tego, w jaki sposób zmienne są rozmieszczane w ramce określonej funkcji.
Żaden z nich nie jest określony w standardzie C, ale odpowiedzi są nieco inne:
f
wskaźnik ramki będzie większy czy mniejszy niżg
wskaźnik ramki? Może to przebiegać w obie strony - zależy to od konkretnego kompilatora i architektury (patrz „konwencja wywoływania”), ale zawsze jest spójne w ramach danej platformy (z kilkoma dziwacznymi wyjątkami, patrz komentarze). W dół jest bardziej powszechne; tak jest w przypadku x86, PowerPC, MIPS, SPARC, EE i Cell SPU.źródło
Kierunek wzrostu stosów zależy od architektury. To powiedziawszy, rozumiem, że tylko kilka architektur sprzętowych ma stosy, które rosną.
Kierunek wzrostu stosu jest niezależny od układu pojedynczego obiektu. Więc chociaż stos może się zmniejszyć, tablice nie będą (tj. & Array [n] zawsze będzie <& array [n + 1]);
źródło
W standardzie nie ma niczego, co w ogóle określa sposób organizacji rzeczy na stosie. W rzeczywistości można zbudować zgodny kompilator, który w ogóle nie przechowuje elementów tablicy w ciągłych elementach na stosie, pod warunkiem, że miałby spryt, aby nadal prawidłowo wykonywać arytmetykę elementów tablicy (aby wiedział, na przykład, że 1 jest 1K od [0] i może to zmienić).
Powodem, dla którego możesz uzyskiwać różne wyniki, jest to, że podczas gdy stos może się zmniejszać, aby dodać do niego „obiekty”, tablica jest pojedynczym „obiektem” i może zawierać elementy tablicy rosnącej w odwrotnej kolejności. Jednak poleganie na tym zachowaniu nie jest bezpieczne, ponieważ kierunek może się zmieniać, a zmienne mogą być zamieniane z różnych powodów, w tym między innymi:
Zobacz tutaj mój doskonały traktat o kierunku stosu :-)
W odpowiedzi na Twoje konkretne pytania:
To w ogóle nie ma znaczenia (jeśli chodzi o standard), ale skoro pytałeś, może urosnąć lub w zależności od implementacji maleć w pamięci.
Nie ma to żadnego znaczenia (jeśli chodzi o standard). Zobacz powyżej z możliwymi przyczynami.
źródło
Na x86, „alokacja” pamięci ramki stosu polega po prostu na odjęciu niezbędnej liczby bajtów od wskaźnika stosu (uważam, że inne architektury są podobne). W tym sensie wydaje mi się, że stos rośnie „w dół”, ponieważ adresy stają się stopniowo mniejsze, gdy wywołujesz głębiej w stosie (ale zawsze wyobrażam sobie, że pamięć zaczyna się od 0 w lewym górnym rogu i zwiększa się w miarę poruszania się w prawo i zawiń, więc w moim wyobrażeniu stos rośnie ...). Kolejność deklarowanych zmiennych może nie mieć żadnego wpływu na ich adresy - uważam, że standard pozwala kompilatorowi na zmianę ich kolejności, o ile nie powoduje to skutków ubocznych (ktoś proszę mnie poprawić, jeśli się mylę) . One'
Luka wokół tablicy może być jakimś wypełnieniem, ale jest dla mnie tajemnicza.
źródło
Przede wszystkim jego 8 bajtów nieużywanego miejsca w pamięci (jego nie 12, pamiętaj, że stos rośnie w dół, więc przestrzeń, która nie jest przydzielona, wynosi od 604 do 597). i dlaczego?. Ponieważ każdy typ danych zajmuje miejsce w pamięci, zaczynając od adresu podzielnego przez jego rozmiar. W naszym przypadku tablica 3 liczb całkowitych zajmuje 12 bajtów pamięci, a 604 nie jest podzielne przez 12. Tak więc pozostawia puste spacje, dopóki nie napotka adresu pamięci, który jest podzielny przez 12, to jest 596.
Więc przestrzeń pamięci przydzielona tablicy wynosi od 596 do 584. Ale alokacja tablicy jest kontynuowana, więc pierwszy element tablicy zaczyna się od adresu 584, a nie od 596.
źródło
Kompilator może alokować lokalne (automatyczne) zmienne w dowolnym miejscu w lokalnej ramce stosu, nie można z tego wiarygodnie wywnioskować kierunku wzrostu stosu. Kierunek wzrostu stosu można wywnioskować z porównania adresów zagnieżdżonych ramek stosu, tj. Porównując adres zmiennej lokalnej wewnątrz ramki stosu funkcji w porównaniu do jej wywoływanej:
#include <stdio.h> int f(int *x) { int a; return x == NULL ? f(&a) : &a - x; } int main(void) { printf("stack grows %s!\n", f(NULL) < 0 ? "down" : "up"); return 0; }
źródło
Nie sądzę, żeby to było deterministyczne. Tablica wydaje się „rosnąć”, ponieważ ta pamięć powinna być przydzielana w sposób ciągły. Jednakże, ponieważ q i s nie są ze sobą w ogóle powiązane, kompilator po prostu umieszcza je w dowolnej wolnej lokalizacji w stosie, prawdopodobnie takich, które najlepiej pasują do liczby całkowitej.
Między a [2] i q zdarzyło się, że przestrzeń wokół położenia q nie była wystarczająco duża (tj. Nie była większa niż 12 bajtów), aby przydzielić tablicę składającą się z 3 liczb całkowitych.
źródło
Mój stos wydaje się rozszerzać w kierunku adresów o niższych numerach.
Może się to różnić na innym komputerze lub nawet na moim komputerze, jeśli używam innego wywołania kompilatora. ... albo kompilator muigt nie zdecyduje się w ogóle używać stosu (wstawiaj wszystko (funkcje i zmienne, jeśli nie wziąłem ich adresu)).
$ cat stack.c #include <stdio.h> int stack(int x) { printf("level %d: x is at %p\n", x, (void*)&x); if (x == 0) return 0; return stack(x - 1); } int main(void) { stack(4); return 0; }
źródło
Stos rośnie (na x86). Jednak stos jest alokowany w jednym bloku podczas ładowania funkcji i nie masz gwarancji, w jakiej kolejności elementy znajdą się na stosie.
W tym przypadku przydzielił miejsce na stosie dla dwóch int i tablicy z trzema intami. Przydzielił również dodatkowe 12 bajtów po tablicy, więc wygląda to tak:
a [12 bajtów]
dopełnienie (?) [12 bajtów]
s [4 bajty]
q [4 bajty]
Z jakiegoś powodu Twój kompilator zdecydował, że musi przeznaczyć 32 bajty na tę funkcję, a być może więcej. Jest to nieprzejrzyste dla Ciebie jako programisty C, nie wiesz dlaczego.
Jeśli chcesz wiedzieć dlaczego, skompiluj kod do języka asemblera, uważam, że jest to -S na gcc i / S na kompilatorze MS C. Jeśli spojrzysz na instrukcje otwierające tę funkcję, zobaczysz, że stary wskaźnik stosu jest zapisywany, a następnie odejmowany jest od niego 32 (lub coś innego!). Stamtąd możesz zobaczyć, jak kod uzyskuje dostęp do tego 32-bajtowego bloku pamięci i dowiedzieć się, co robi twój kompilator. Na końcu funkcji możesz zobaczyć przywracany wskaźnik stosu.
źródło
Zależy to od systemu operacyjnego i kompilatora.
źródło
Stos rośnie. Zatem f (g (h ())), stos przydzielony dla h będzie zaczynał się od niższego adresu, a następnie g i g będą mniejsze niż f. Ale zmienne w stosie muszą być zgodne ze specyfikacją C,
http://c0x.coding-guidelines.com/6.5.8.html
1206 Jeśli wskazywane obiekty są członkami tego samego obiektu zagregowanego, wskaźniki do elementów składowych zadeklarowanych później porównują większe niż wskaźniki do elementów zadeklarowanych wcześniej w strukturze, a wskaźniki do elementów tablicy z większymi wartościami indeksu są większe niż wskaźniki do elementów tego samego tablica z niższymi wartościami indeksu.
& a [0] <& a [1], zawsze musi być prawdziwe, niezależnie od tego, jak jest przydzielone „a”
źródło
rośnie w dół, a to z powodu standardu kolejności bajtów little endian, jeśli chodzi o zestaw danych w pamięci.
Jednym ze sposobów, w jaki możesz na to spojrzeć, jest to, że stos Rośnie w górę, jeśli spojrzysz na pamięć od 0 od góry i maksymalnie od dołu.
Powodem, dla którego stos rośnie w dół, jest możliwość wyłuskiwania z perspektywy stosu lub wskaźnika bazowego.
Pamiętaj, że wyłuskiwanie dowolnego typu zwiększa się od najniższego do najwyższego adresu. Ponieważ stos rośnie w dół (adres od najwyższego do najniższego), pozwala to traktować stos jak pamięć dynamiczną.
Jest to jeden z powodów, dla których tak wiele języków programowania i skryptów używa maszyny wirtualnej opartej na stosie, a nie opartej na rejestrze.
źródło
The reason for the stack growing downward is to be able to dereference from the perspective of the stack or base pointer.
Bardzo fajne rozumowanieTo zależy od architektury. Aby sprawdzić swój własny system, użyj tego kodu z GeeksForGeeks :
// C program to check whether stack grows // downward or upward. #include<stdio.h> void fun(int *main_local_addr) { int fun_local; if (main_local_addr < &fun_local) printf("Stack grows upward\n"); else printf("Stack grows downward\n"); } int main() { // fun's local variable int main_local; fun(&main_local); return 0; }
źródło