Dlaczego wciąż zwiększamy stos do tyłu?

46

Podczas kompilowania kodu C i patrzenia na asembler wszystkie stosy rosną do tyłu w następujący sposób:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)- Czy to oznacza, że ​​wskaźnik bazowy lub wskaźnik stosu przesuwają się w dół po adresach pamięci zamiast w górę? Dlaczego?

Zmieniłem $5, -4(%rbp)się $5, +4(%rbp), opracowano i uruchomiono kod i nie było żadnych błędów. Dlaczego więc wciąż musimy cofać się w stosie pamięci?

alex
źródło
2
Zauważ, że w -4(%rbp)ogóle nie przesuwa on wskaźnika bazowego i że +4(%rbp)to nie mogło działać.
Margaret Bloom
14
dlaczego wciąż musimy cofać się ” - jak myślisz, jaka byłaby korzyść z pójścia do przodu? Ostatecznie to nie ma znaczenia, wystarczy wybrać jeden.
Bergi
31
„dlaczego zwiększamy stos do tyłu?” - ponieważ jeśli nie zrobimy tego, ktoś inny zapyta, dlaczego mallocrośnie sterty do tyłu
slebetman
2
@MargaretBloom: Najwyraźniej na platformie OP, kod startowy CRT nie ma znaczenia, czy mainblokuje jego RBP. To z pewnością możliwe. (I tak, pisanie 4(%rbp)opierałoby się na zapisanej wartości RBP). Brawo, ten główny nigdy tak nie jest mov %rsp, %rbp, więc dostęp do pamięci jest względny w stosunku do RBP dzwoniącego , jeśli tak właśnie testował OP !!! Jeśli zostało to faktycznie skopiowane z danych wyjściowych kompilatora, niektóre instrukcje zostały pominięte!
Peter Cordes
1
Wydaje mi się, że „do tyłu” lub „do przodu” (lub „w dół” i „w górę”) zależy od twojego punktu widzenia. Jeśli schemat został zapamiętany jako kolumna z niskimi adresami u góry, wówczas powiększanie stosu poprzez zmniejszenie wskaźnika stosu byłoby analogiczne do stosu fizycznego.
jamesdlin

Odpowiedzi:

86

Czy to oznacza, że ​​wskaźnik podstawowy lub wskaźnik stosu przesuwają się w dół po adresach pamięci zamiast w górę? Dlaczego?

Tak, pushinstrukcje zmniejszają wskaźnik stosu i zapisują na stosie, podczas gdy popczynność odwrotna odczytuje ze stosu i zwiększa wskaźnik stosu.

Jest to nieco historyczne, ponieważ dla maszyn z ograniczoną pamięcią stos został umieszczony wysoko i wyrósł w dół, podczas gdy stos został umieszczony nisko i wyrósł w górę. Jest tylko jedna luka „wolnej pamięci” - pomiędzy stertą i stosem, i ta luka jest wspólna, każda z nich może urosnąć do luki według indywidualnych potrzeb. W ten sposób programowi zabraknie pamięci tylko wtedy, gdy stos i sterty zderzą się, nie pozostawiając wolnej pamięci. 

Jeśli zarówno stos, jak i sterty rosną w tym samym kierunku, wówczas istnieją dwie luki, a stos nie może tak naprawdę wyrosnąć w szczelinę sterty (na odwrót również problematyczne).

Początkowo procesory nie miały dedykowanych instrukcji obsługi stosu. Ponieważ jednak obsługa sprzętu została dodana do sprzętu, wzrósł on w dół, a procesory nadal podążają za tym wzorem.

Można argumentować, że na 64-bitowej maszynie jest wystarczająca przestrzeń adresowa, aby pozwolić na wiele luk - i jako dowód, wiele luk koniecznie ma miejsce, gdy proces ma wiele wątków. Chociaż nie jest to wystarczająca motywacja do zmiany sytuacji, ponieważ w przypadku systemów z wieloma przerwami kierunek wzrostu jest prawdopodobnie arbitralny, więc tradycja / zgodność przechyla skalę.


Trzeba by zmienić CPU instrukcje obsługi stosu w celu zmiany kierunku stosu, albo zrezygnować z wykorzystaniem dedykowanych popychanie i popping instrukcje (np push, pop, call, ret, inne).

Zauważ, że architektura zestawu instrukcji MIPS nie ma dedykowanego push& pop, więc praktyczne jest powiększanie stosu w dowolnym kierunku - nadal możesz chcieć mieć układ pamięci o pojedynczej przerwie dla procesu z jednym wątkiem, ale możesz zwiększyć stos i górę stosu ku dołowi. Jednak jeśli to zrobisz, niektóre kody C varargs mogą wymagać dostosowania w źródle lub w przekazywaniu parametrów pod maską.

(W rzeczywistości, ponieważ nie ma dedykowanej obsługi stosu w MIPS, możemy użyć przyrostu wstępnego lub końcowego lub zmniejszenia wstępnego lub końcowego w celu wypychania na stos, pod warunkiem, że użyjemy dokładnej odwrotności do zrzucenia stosu, a także zakładając, że system operacyjny przestrzega wybranego modelu wykorzystania stosu. Rzeczywiście, w niektórych systemach wbudowanych i niektórych systemach edukacyjnych stos MIPS jest zwiększany).

Erik Eidt
źródło
32
To nie jest tylko pushi popna większości architektur, ale także o wiele ważniejsze przerwaniami manipulacja, call, ret, i co tam jeszcze jest upieczone w interakcji ze stosu.
Deduplicator
3
ARM może mieć wszystkie cztery smaki stosu.
Margaret Bloom
14
Co do tego, co jest warte, nie sądzę, że „kierunek wzrostu jest arbitralny” w tym sensie, że którykolwiek wybór jest równie dobry. Odrastanie ma właściwość polegającą na przepełnieniu końca bufora, który blokuje wcześniejsze ramki stosu, w tym zapisane adresy zwrotne. Dorastanie ma właściwość polegającą na przepełnieniu końca bufora, który zapycha tylko pamięć w tej samej lub późniejszej pamięci (jeśli bufor nie jest najnowszy, mogą być późniejsze) ramka wywołania, a być może nawet tylko nieużywane miejsce (wszystkie przyjmujące wartownik strona po stosie). Dlatego z punktu widzenia bezpieczeństwa dorastanie wydaje się bardzo preferowane
R ..
6
@R ..: dorastanie nie eliminuje exploitów polegających na przepełnieniu bufora, ponieważ wrażliwe funkcje zwykle nie są funkcjami typu liść: wywołują inne funkcje, umieszczając adres zwrotny nad buforem. Funkcje liści, które otrzymują wskaźnik od osoby dzwoniącej, mogą stać się podatne na zastąpienie własnego adresu zwrotnego. np. jeśli funkcja alokuje bufor na stosie i przekazuje go do gets(), lub wykonuje takie, strcpy()które nie jest wstawiane, wówczas zwrot w tych funkcjach biblioteki użyje zastąpionego adresu zwrotnego. Obecnie z rosnącymi stosami, kiedy wraca ich rozmówca.
Peter Cordes
5
@PeterCordes: Rzeczywiście w moim komentarzu zauważyłem, że ramki stosu tego samego poziomu lub nowsze niż przepełniony bufor są nadal potencjalnie możliwe do zablokowania, ale to o wiele mniej. W przypadku, gdy funkcja clobbering jest funkcją liścia wywoływaną bezpośrednio przez funkcję, której bufor jest (np. strcpy), Na łuku, w którym adres zwrotny jest przechowywany w rejestrze, chyba że trzeba go rozlać, nie ma dostępu do clobber powrotu adres.
R ..
8

W twoim systemie stos zaczyna się od adresu o wysokiej pamięci i „rośnie” w dół do adresów o niskiej pamięci. (istnieje również symetryczny przypadek od niskiego do wysokiego)

A ponieważ zmieniłeś z -4 i +4 i działało, to nie znaczy, że jest poprawne. Układ pamięci uruchomionego programu jest bardziej złożony i zależy od wielu innych czynników, które mogą przyczynić się do tego, że nie od razu zawiesiłeś się na tym niezwykle prostym programie.

nadir
źródło
1

Wskaźnik stosu wskazuje granicę między przydzieloną i nieprzydzieloną pamięcią stosu. Zwiększenie go w dół oznacza, że ​​wskazuje on początek pierwszej struktury w przydzielonej przestrzeni stosu, a inne przydzielone elementy następują pod większymi adresami. Posiadanie wskaźników wskazujących na początek przydzielonych struktur jest znacznie częstsze niż na odwrót.

Obecnie w wielu systemach istnieje osobny rejestr ramek stosu , który można nieco niezawodnie rozwinąć w celu ustalenia łańcucha połączeń, z lokalną zmienną pamięcią. Sposób, w jaki rejestr ramki stosu jest skonfigurowany w niektórych architekturach, oznacza, że ​​kończy się wskazywaniem za lokalną pamięcią zmiennych, w przeciwieństwie do wskaźnika stosu przed nią. Zatem użycie tego rejestru ramki stosu wymaga ujemnego indeksowania.

Zauważ, że ramki stosu i ich indeksowanie są bocznym aspektem skompilowanych języków komputerowych, więc to generator kodu kompilatora ma do czynienia z „nienaturalnością”, a nie słabym programistą języka asemblera.

Więc chociaż istnieją dobre historyczne powody, aby wybierać stosy, aby rosły w dół (a niektóre z nich zostały zachowane, jeśli programujesz w języku asemblera i nie zawracasz sobie głowy ustawieniem odpowiedniej ramki stosu), stały się one mniej widoczne.


źródło
2
„Obecnie w wielu systemach istnieje osobny rejestr ramek stosu”, który opóźnia się. Bogatsze formaty informacji debugowania w dużej mierze wyeliminowały obecnie potrzebę stosowania wskaźników klatek.
Peter Green