Czy stos rośnie w górę czy w dół?

89

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 ado a[2]zwiększają się o 4 bajty każdy. Ale od qdo sadresy pamięci zmniejszają się o 4 bajty.

Zastanawiam się nad 2 rzeczami:

  1. Czy stos rośnie czy maleje? (W tym przypadku wygląda na to, że oba)
  2. Co się dzieje między adresami pamięci a[2]i q? 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.

Spikatrix
źródło
Kolejność jest dowolna. Luka jest prawdopodobnie do przechowywania wyniku pośredniego, takiego jak & q lub & s - spójrz na demontaż i przekonaj się sam.
Tom Leys
Zgadzam się, przeczytaj kod montażowy. Jeśli zadajesz tego rodzaju pytania, czas nauczyć się czytać.
Per Johansson
Prostsza odpowiedź na wersję zestawu: stackoverflow.com/questions/664744/ ...
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:

74

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.

Ganesh Gopalasubramanian
źródło
5
Dodatkowo, jeśli chcesz sprawdzić, czy stos rośnie w górę lub w dół. Zadeklaruj zmienną lokalną w funkcji głównej. Wydrukuj adres zmiennej. Wywołaj inną funkcję z main. Zadeklaruj zmienną lokalną w funkcji. Wydrukuj jego adres. Na podstawie wydrukowanych adresów możemy powiedzieć, że stos rośnie lub maleje.
Ganesh Gopalasubramanian
dzięki Ganesh, mam małe pytanie: na rysunku u narysowanym, w trzecim bloku, miałeś na myśli "calleR zapisany rejestr używany w CALLER" ponieważ kiedy f1 wywołuje f2, musimy zapisać adres f1 (który jest adresem zwrotnym dla rejestrów f2) i f1 (calleR), a nie rejestrów f2 (callee). Dobrze?
CSawy
44

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:

  • W jaki sposób stos rośnie, gdy przydzielana jest nowa ramka - jeśli funkcja f () wywoła funkcję g (), to zrobi f wskaźnik ramki będzie większy czy mniejszy niż gwskaź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.
  • W jaki sposób zmienne lokalne funkcji są rozmieszczone w ramce stosu? Jest to nieokreślone i całkowicie nieprzewidywalne; kompilator może dowolnie układać swoje zmienne lokalne, jednak chce uzyskać najbardziej efektywny wynik.
Crashworks
źródło
7
„zawsze jest spójne w ramach danej platformy” - brak gwarancji. Widziałem platformę bez pamięci wirtualnej, na której stos był rozszerzany dynamicznie. Nowe bloki stosu były w efekcie malsocowane, co oznacza, że ​​przez chwilę schodziłeś „w dół” o jeden blok stosu, a potem nagle „na boki” do innego. „W bok” może oznaczać większy lub mniejszy adres, całkowicie zależnie od szczęścia w losowaniu.
Steve Jessop
2
Aby uzyskać dodatkowe szczegóły do ​​punktu 2 - kompilator może być w stanie zdecydować, że zmienna nigdy nie musi być w pamięci (utrzymywanie jej w rejestrze przez cały okres istnienia zmiennej) i / lub jeśli czas życia dwóch lub więcej zmiennych tego nie robi Jeśli zachodzą na siebie, kompilator może zdecydować o użyciu tej samej pamięci dla więcej niż jednej zmiennej.
Michael Burr
2
Myślę, że S / 390 (IBM zSeries) ma ABI, w którym ramki wywołań są połączone zamiast rosnąć na stosie.
ephemient
2
Poprawne na S / 390. Wywołanie to „BALR”, oddział i rejestr linków. Wartość zwracana jest umieszczana w rejestrze, a nie umieszczana na stosie. Funkcja powrotu jest odgałęzieniem do zawartości tego rejestru. W miarę pogłębiania się stosu, miejsce w stercie jest przydzielane i są one ze sobą powiązane. W tym miejscu odpowiednik „/ bin / true” MVS otrzymuje swoją nazwę: „IEFBR14”. Pierwsza wersja miała pojedynczą instrukcję: „BR 14”, która rozgałęziała się na zawartość rejestru 14, który zawierał adres zwrotny.
janm
1
Niektóre kompilatory na procesorach PIC wykonują analizę całego programu i przydzielają stałe lokalizacje dla zmiennych automatycznych każdej funkcji; rzeczywisty stos jest malutki i nie jest dostępny z poziomu oprogramowania; dotyczy tylko adresów zwrotnych.
janm
13

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 Samuel Klatchko
źródło
4

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:

  • optymalizacja.
  • wyrównanie.
  • kaprysy osoby, część zarządzająca stosem kompilatora.

Zobacz tutaj mój doskonały traktat o kierunku stosu :-)

W odpowiedzi na Twoje konkretne pytania:

  1. Czy stos rośnie czy maleje?
    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.
  2. Co się dzieje pomiędzy adresami pamięci [2] i q? Dlaczego jest tam duża różnica w pamięci? (20 bajtów)?
    Nie ma to żadnego znaczenia (jeśli chodzi o standard). Zobacz powyżej z możliwymi przyczynami.
paxdiablo
źródło
Widziałem, jak powiązałeś, że większość architektur procesorów przyjmuje sposób „zmniejszania się”. Czy wiesz, czy jest z tego jakaś korzyść?
Baiyan Huang
Nie mam pojęcia, naprawdę. Jest możliwe, że ktoś pomyślał, że kod idzie w górę od 0, więc stos powinien iść w dół od wysokiej pamięci, aby zminimalizować możliwość przecięcia. Ale niektóre procesory zaczynają uruchamiać kod w lokalizacjach niezerowych, więc może tak nie być. Jak w przypadku większości rzeczy, może zostało to zrobione w ten sposób po prostu dlatego, że był to pierwszy sposób, w jaki ktoś to pomyślał :-)
paxdiablo
@lzprgmr: Istnieją pewne niewielkie zalety wykonywania pewnych rodzajów alokacji sterty w porządku rosnącym. W przeszłości stos i sterta znajdowały się po przeciwnych stronach wspólnej przestrzeni adresowej. Zakładając, że łączne użycie statycznej + sterty + stosu nie przekracza dostępnej pamięci, nie trzeba się martwić o to, ile dokładnie pamięci stosu używa program.
supercat
3

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.

rmeador
źródło
1
W rzeczywistości wiem, że kompilator może zmienić ich kolejność, ponieważ w ogóle nie może ich przydzielać. Może po prostu umieścić je w rejestrach i nie wykorzystywać w ogóle miejsca na stosie.
rmeador
Nie może umieścić ich w rejestrach, jeśli odniesiesz się do ich adresów.
floren
słuszna uwaga, nie brałem tego pod uwagę. ale to wciąż wystarcza jako dowód, że kompilator może zmienić ich kolejność, ponieważ wiemy, że może to zrobić przynajmniej przez jakiś czas :)
rmeador
1

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.

Prateek Khurana
źródło
1

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;
}
matja
źródło
5
Jestem prawie pewien, że odejmowanie wskaźników do różnych obiektów stosu jest niezdefiniowanym zachowaniem - wskaźniki, które nie są częścią tego samego obiektu, nie są porównywalne. Oczywiście nie ulegnie awarii na żadnej „normalnej” architekturze.
Steve Jessop
@SteveJessop Czy jest jakiś sposób, abyśmy mogli to naprawić, aby programowo uzyskać kierunek stosu?
xxks-kkk
@ xxks-kkk: w zasadzie nie, ponieważ implementacja C nie musi mieć "kierunku stosu". Na przykład nie naruszyłoby to konwencji wywoływania, w której blok stosu jest przydzielany z góry, a następnie używana jest jakaś pseudolosowa procedura alokacji pamięci wewnętrznej do przeskakiwania wewnątrz niego. W praktyce działa tak, jak opisuje to Matja.
Steve Jessop
0

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.

javanix
źródło
jeśli tak, dlaczego q, s, a nie mają pamięci ciągłej? (Np .: Adres q: 2293612 Adres s: 2293608 Adres a: 2293604)
Widzę „lukę” między s a a
Ponieważ s i a nie zostały przydzielone razem - jedyne wskaźniki, które muszą być ciągłe, to te w tablicy. Inną pamięć można przydzielić w dowolnym miejscu.
javanix
0

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;
}
$ / usr / bin / gcc -Wall -Wextra -std = c89 -pedantyczny stos.c
$ ./a.out
poziom 4: x jest pod adresem 0x7fff7781190c
poziom 3: x jest na 0x7fff778118ec
poziom 2: x jest na 0x7fff778118cc
poziom 1: x jest pod adresem 0x7fff778118ac
poziom 0: x jest na 0x7fff7781188c
pmg
źródło
0

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.

Aric TenEyck
źródło
0

Zależy to od systemu operacyjnego i kompilatora.

David R. Tribble
źródło
Nie wiem, dlaczego moja odpowiedź została odrzucona. To naprawdę zależy od systemu operacyjnego i kompilatora. W niektórych systemach stos rośnie w dół, ale w innych rośnie w górę. W niektórych systemach nie ma rzeczywistego stosu ramek typu push-down, a raczej jest on symulowany z zarezerwowanym obszarem pamięci lub zestawem rejestrów.
David R Tribble
3
Prawdopodobnie dlatego, że twierdzenia jednozdaniowe nie są dobrymi odpowiedziami.
Wyścigi lekkości na orbicie
0

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”

user1187902
źródło
Na większości maszyn stos rośnie w dół - z wyjątkiem tych, w których rośnie w górę.
Jonathan Leffler
0

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.

Nergal
ź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 rozumowanie
user3405291
0

To 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; 
} 
kurdtpage
źródło