Co to jest dokładnie wskaźnik podstawowy i wskaźnik stosu? Do czego wskazują?

225

Korzystając z tego przykładu pochodzącego z wikipedii, w której DrawSquare () wywołuje DrawLine (),

alternatywny tekst

(Pamiętaj, że ten diagram ma wysokie adresy u dołu i niskie adresy u góry).

Czy ktoś mógłby mi wyjaśnić, co ebpi espw tym kontekście?

Z tego, co widzę, powiedziałbym, że wskaźnik stosu zawsze wskazuje na górę stosu, a wskaźnik bazowy na początek bieżącej funkcji? Albo co?


edycja: Mam na myśli to w kontekście programów Windows

edit2: A jak to eipdziała?

edit3: Mam następujący kod z MSVC ++:

var_C= dword ptr -0Ch
var_8= dword ptr -8
var_4= dword ptr -4
hInstance= dword ptr  8
hPrevInstance= dword ptr  0Ch
lpCmdLine= dword ptr  10h
nShowCmd= dword ptr  14h

Wszystkie wydają się być dwordami, dlatego zabierają 4 bajty każdy. Widzę więc, że między hInstance a var_4 występuje 4-bajtowa przerwa. Czym oni są? Zakładam, że jest to adres zwrotny, jak widać na zdjęciu w Wikipedii?


(uwaga redaktora: usunąłem długi cytat z odpowiedzi Michaela, który nie należy do pytania, ale edytowano pytanie uzupełniające):

Wynika to z faktu, że przepływ wywołania funkcji jest następujący:

* Push parameters (hInstance, etc.)
* Call function, which pushes return address
* Push ebp
* Allocate space for locals

Moje pytanie (na koniec, mam nadzieję!) Brzmi teraz: co dokładnie dzieje się od momentu, gdy podskakuję argumenty funkcji, którą chcę wywołać do końca prologu? Chcę wiedzieć, jak ewoluują ebp, esp w tych momentach (już zrozumiałem, jak działa prolog, chcę tylko wiedzieć, co się dzieje po tym, jak wrzuciłem argumenty na stos i przed prologiem).

pochłonęło elysium
źródło
23
Jedną ważną rzeczą do zapamiętania jest to, że stos rośnie „w dół” w pamięci. Oznacza to, że aby przesunąć wskaźnik stosu w górę, zmniejszasz jego wartość.
BS
4
Jedna wskazówka, aby odróżnić działania EBP / ESP i EIP: EBP i ESP zajmują się danymi, podczas gdy EIP zajmuje się kodem.
mmmmmmmm,
2
Na twoim wykresie ebp (zwykle) jest „wskaźnikiem ramki”, szczególnie „wskaźnikiem stosu”. Pozwala to na dostęp do lokalnych przez [ebp-x] i parametry stosu przez [ebp + x] konsekwentnie, niezależnie od wskaźnika stosu (który często zmienia się w obrębie funkcji). Adresowanie można wykonać za pomocą ESP, uwalniając EBP do innych operacji - ale w ten sposób debuggery nie mogą rozpoznać stosu połączeń ani wartości miejscowych.
peterchen
4
@Ben. Nie niepewnie. Niektóre kompilatory umieszczają ramki stosu na stosie. Koncepcja rosnącego stosu jest właśnie taka, która ułatwia zrozumienie. Implementacja stosu może być dowolna (użycie losowych porcji stosu sprawia, że ​​włamania, które nadpisują części stosu są o wiele trudniejsze, ponieważ nie są one tak deterministyczne).
Martin York,
1
w dwóch słowach: wskaźnik stosu pozwala na działanie operacji push / pop (więc push i pop wie, gdzie umieścić / uzyskać dane). wskaźnik bazowy pozwala kodowi niezależnie odwoływać się do danych, które zostały wcześniej wypchnięte na stos.
tigrou

Odpowiedzi:

228

esp jest, jak mówisz, szczytem stosu.

ebpjest zwykle ustawiany espna początku funkcji. Dostęp do parametrów funkcji i zmiennych lokalnych można uzyskać, odpowiednio dodając i odejmując stałe przesunięcie od ebp. Wszystkie konwencje wywoływania x86 definiują się ebpjako zachowane między wywołaniami funkcji. ebpsam wskazuje na podstawowy wskaźnik poprzedniej ramki, który umożliwia przechodzenie stosu w debuggerze i przeglądanie lokalnych zmiennych innych ramek do działania.

Większość prologów funkcji wygląda mniej więcej tak:

push ebp      ; Preserve current frame pointer
mov ebp, esp  ; Create new frame pointer pointing to current stack top
sub esp, 20   ; allocate 20 bytes worth of locals on stack.

Później w funkcji możesz mieć kod podobny (zakładając, że obie zmienne lokalne mają 4 bajty)

mov [ebp-4], eax    ; Store eax in first local
mov ebx, [ebp - 8]  ; Load ebx from second local

Optymalizacja FPO lub wskaźnika pominięcia ramki, którą możesz włączyć, faktycznie to wyeliminuje i użyje ebpjako innego rejestru i uzyska dostęp do miejscowych bezpośrednio zesp , ale utrudnia to debugowanie, ponieważ debugger nie może już bezpośrednio uzyskiwać dostępu do ramek stosów wcześniejszych wywołań funkcji.

EDYTOWAĆ:

W zaktualizowanym pytaniu brakuje dwóch wpisów w stosie:

var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
*savedFramePointer = dword ptr 0*
*return address = dword ptr 4*
hInstance = dword ptr  8h
PrevInstance = dword ptr  0C
hlpCmdLine = dword ptr  10h
nShowCmd = dword ptr  14h

Wynika to z faktu, że przepływ wywołania funkcji jest następujący:

  • Parametry push (hInstance itp.)
  • Funkcja wywołania, która popycha adres zwrotny
  • Pchać ebp
  • Przydziel miejsce dla mieszkańców
Michael
źródło
1
Dziękuję za wyjaśnienie! Ale jestem teraz trochę zmieszany. Załóżmy, że wywołuję funkcję i jestem w pierwszym wierszu jej prologu, wciąż nie wykonując z niej ani jednego wiersza. W tym momencie jaka jest wartość ebp? Czy stos ma w tym momencie coś oprócz argumentów wypychanych? Dzięki!
pożarł elysium
3
EBP nie zmienia się magicznie, więc dopóki nie utworzysz nowego EBP dla swojej funkcji, nadal będziesz mieć wartość dzwoniących. Poza argumentami na stosie będzie również przechowywany stary EIP (adres zwrotny)
MSalters
3
Niezła odpowiedź. Chociaż nie może być kompletne bez wspomnienia o tym, co jest w epilogu: instrukcje „zostaw” i „ret”.
Calmarius
2
Myślę, że ten obraz pomoże wyjaśnić pewne rzeczy na temat tego, czym jest przepływ. Pamiętaj również, że stos rośnie w dół. ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
Andrei-Niculae Petre
Czy to ja, czy w powyższym fragmencie kodu brakuje wszystkich znaków minus?
BarbaraKwarc
96

ESP jest bieżącym wskaźnikiem stosu, który zmienia się za każdym razem, gdy słowo lub adres są wypychane lub wyskakiwane na stosie. EBP jest wygodniejszym dla kompilatora sposobem śledzenia parametrów funkcji i zmiennych lokalnych niż bezpośrednie używanie ESP.

Ogólnie (i może się to różnić w zależności od kompilatora) wszystkie argumenty wywoływanej funkcji są wypychane na stos przez funkcję wywołującą (zwykle w odwrotnej kolejności, niż zadeklarowane w prototypie funkcji, ale to się zmienia) . Następnie wywoływana jest funkcja, która wypycha adres zwrotny (EIP) na stos.

Po wejściu do funkcji stara wartość EBP jest wypychana na stos, a EBP jest ustawiane na wartość ESP. Następnie ESP jest zmniejszany (ponieważ stos rośnie w pamięci), aby przydzielić miejsce dla lokalnych zmiennych i tymczasowych funkcji. Od tego momentu, podczas wykonywania funkcji, argumenty funkcji są umieszczane na stosie w dodatnich przesunięciach z EBP (ponieważ zostały wypchnięte przed wywołaniem funkcji), a zmienne lokalne znajdują się w ujemnych przesunięciach z EBP (ponieważ zostały przydzielone na stosie po wpisie funkcji). Dlatego EBP nazywany jest wskaźnikiem ramki , ponieważ wskazuje na środek ramki wywołania funkcji .

Po wyjściu z funkcji wystarczy ustawić ESP na wartość EBP (która zwalnia zmienne lokalne ze stosu i wyświetla wpis EBP na szczycie stosu), a następnie wyskakuje ze starej wartości EBP ze stosu, a następnie funkcja zwraca (wstawiając adres zwrotny do EIP).

Po powrocie do funkcji wywołującej, może następnie zwiększyć wartość ESP, aby usunąć argumenty funkcji, które wypchnął na stos tuż przed wywołaniem innej funkcji. W tym momencie stos powraca do tego samego stanu, w jakim był przed wywołaniem wywoływanej funkcji.

David R. Tribble
źródło
15

Masz rację. Wskaźnik stosu wskazuje na górny element stosu, a wskaźnik bazowy wskazuje na „poprzedni” szczyt stosu przed wywołaniem funkcji.

Po wywołaniu funkcji dowolna zmienna lokalna zostanie zapisana na stosie, a wskaźnik stosu zostanie zwiększony. Po powrocie z funkcji wszystkie zmienne lokalne na stosie wykraczają poza zakres. Robisz to, ustawiając wskaźnik stosu z powrotem na wskaźnik bazowy (który był „poprzednim” szczytem przed wywołaniem funkcji).

Takie przydzielanie pamięci jest bardzo , bardzo szybkie i wydajne.

Robert Cartaino
źródło
14
@Robert: Kiedy mówisz „poprzedni” szczyt stosu przed wywołaniem funkcji, ignorujesz oba parametry, które są wypychane na stos tuż przed wywołaniem funkcji i EIP wywołującego. To może mylić czytelników. Powiedzmy, że w standardowej ramce stosu EBP wskazuje to samo miejsce, w którym ESP wskazywał tuż po wejściu do funkcji.
wigy
7

EDYCJA: Aby uzyskać lepszy opis, zobacz Demontaż / Funkcje x86 i Ramki stosu w WikiBook o zestawie x86. Próbuję dodać informacje, które mogą Cię zainteresować za pomocą programu Visual Studio.

Przechowywanie EBP wywołującego jako pierwszej zmiennej lokalnej nazywa się standardową ramką stosu i może być używane do prawie wszystkich konwencji wywoływania w systemie Windows. Istnieją różnice, czy wywołujący lub odbierający zwalnia przekazane parametry i które parametry są przekazywane do rejestrów, ale są one ortogonalne w stosunku do standardowego problemu z ramką stosu.

Mówiąc o programach Windows, prawdopodobnie możesz użyć Visual Studio do skompilowania kodu C ++. Należy pamiętać, że Microsoft stosuje optymalizację o nazwie Frame Pointer Omission, która sprawia, że ​​prawie niemożliwe jest przejście stosu bez użycia biblioteki dbghlp i pliku PDB dla pliku wykonywalnego.

To pominięcie wskaźnika ramki oznacza, że ​​kompilator nie przechowuje starego EBP w standardowym miejscu i używa rejestru EBP do czegoś innego, dlatego masz trudności ze znalezieniem EIP dzwoniącego, nie wiedząc, ile miejsca potrzebują zmienne lokalne dla danej funkcji. Oczywiście Microsoft zapewnia interfejs API, który umożliwia wykonywanie spacerów po stosach nawet w tym przypadku, ale wyszukiwanie bazy danych tabeli symboli w plikach PDB w niektórych przypadkach jest zbyt długie.

Aby uniknąć FPO w swoich jednostkach kompilacyjnych, musisz unikać używania / O2 lub jawnie dodać / Oy- do flag kompilacji C ++ w swoich projektach. Prawdopodobnie łączysz się ze środowiskiem uruchomieniowym C lub C ++, które używa FPO w konfiguracji wydania, więc będziesz miał trudności z wykonaniem spacerów po stosie bez dbghlp.dll.

peruka
źródło
Nie rozumiem, jak EIP jest przechowywany na stosie. Czy nie powinien to być rejestr? Jak rejestr może znajdować się na stosie? Dzięki!
pożarł elysium
EIP dzwoniącego jest wypychany na stos przez samą instrukcję CALL. Instrukcja RET po prostu pobiera górę stosu i umieszcza ją w EIP. Jeśli masz przepełnienia bufora, ten fakt może być wykorzystany do przeskoczenia do kodu użytkownika z uprzywilejowanego wątku.
wigy
@devouredelysium Zawartość (lub wartość ) rejestru EIP jest umieszczana (lub kopiowana) na stosie, a nie sam rejestr.
BarbaraKwarc,
@BarbaraKwarc Dzięki za wartościowe wejście. Nie widziałem, czego brakuje OP w mojej odpowiedzi. Rzeczywiście, rejestry pozostają tam, gdzie są, tylko ich wartość jest wysyłana do pamięci RAM z procesora. W trybie amd64 staje się to nieco bardziej złożone, ale pozostaw to do innego pytania.
wigy
Co z tym amd64? Jestem ciekawy.
BarbaraKwarc
6

Po pierwsze, wskaźnik stosu wskazuje na spód stosu, ponieważ stosy x86 budują od wysokich wartości adresu do niższych wartości adresu. Wskaźnik stosu jest punktem, w którym następne wywołanie push (lub wywołanie) umieści następną wartość. Jego działanie jest równoważne z instrukcją C / C ++:

 // push eax
 --*esp = eax
 // pop eax
 eax = *esp++;

 // a function call, in this case, the caller must clean up the function parameters
 move eax,some value
 push eax
 call some address  // this pushes the next value of the instruction pointer onto the
                    // stack and changes the instruction pointer to "some address"
 add esp,4 // remove eax from the stack

 // a function
 push ebp // save the old stack frame
 move ebp, esp
 ... // do stuff
 pop ebp  // restore the old stack frame
 ret

Wskaźnik podstawowy znajduje się u góry bieżącej ramki. ebp ogólnie wskazuje na twój adres zwrotny. ebp + 4 punkty do pierwszego parametru twojej funkcji (lub tej wartości metody klasowej). ebp-4 wskazuje na pierwszą zmienną lokalną twojej funkcji, zwykle starą wartość ebp, abyś mógł przywrócić poprzedni wskaźnik ramki.

jmucchiello
źródło
2
Nie, ESP nie wskazuje na spód stosu. Schemat adresowania pamięci nie ma z tym nic wspólnego. Nie ma znaczenia, czy stos rośnie do niższych czy wyższych adresów. „Góra” stosu jest zawsze tam, gdzie następna wartość zostanie przekazana (umieszczona na górze stosu), lub, w innych architekturach, gdzie ostatnia przekazana wartość została umieszczona i gdzie aktualnie się znajduje. Dlatego ESP zawsze wskazuje na górę stosu.
BarbaraKwarc,
1
Z drugiej strony, spód lub podstawa stosu to miejsce, w którym została umieszczona pierwsza (lub najstarsza ) wartość, a następnie objęta nowszymi wartościami. Stąd nazwa „wskaźnik bazowy” dla EBP: miała wskazywać na podstawę (lub dół) bieżącego lokalnego stosu podprogramu.
BarbaraKwarc,
Barbara, w Intel x86, stos jest UPSIDE DOWN. Wierzchołek stosu zawiera pierwszy przedmiot wepchnięty na stos, a każdy następny element jest popychany PONIŻEJ górnego przedmiotu. Na dole stosu znajdują się nowe przedmioty. Programy są umieszczane w pamięci od 1k i rosną do nieskończoności. Stos zaczyna się od nieskończoności, realistycznie maks. Pamięci minus ROM i rośnie w kierunku 0. ESP wskazuje na adres, którego wartość jest mniejsza niż pierwszy adres przekazany.
jmucchiello,
1

Dawno już nie programowałem w Asemblerze, ale ten link może być przydatny ...

Procesor ma kolekcję rejestrów, które służą do przechowywania danych. Niektóre z nich są wartościami bezpośrednimi, podczas gdy inne wskazują na obszar w pamięci RAM. Rejestry są zwykle używane do określonych czynności, a każdy operand w zestawie wymaga określonej ilości danych w określonych rejestrach.

Wskaźnik stosu jest najczęściej używany podczas wywoływania innych procedur. W nowoczesnych kompilatorach wiązka danych zostanie najpierw zrzucona na stos, a następnie adres zwrotny, dzięki czemu system będzie wiedział, dokąd zwrócić, gdy otrzyma polecenie zwrotu. Wskaźnik stosu wskaże następną lokalizację, w której nowe dane mogą zostać przekazane do stosu, gdzie pozostaną, dopóki nie zostaną ponownie wyświetlone.

Rejestry podstawowe lub rejestry segmentowe wskazują tylko przestrzeń adresową dużej ilości danych. W połączeniu z drugim regresem wskaźnik Base podzieli pamięć na ogromne bloki, podczas gdy drugi rejestr wskaże element w tym bloku. Wskaźniki bazowe wskazują zatem na bazę bloków danych.

Pamiętaj, że asembler jest bardzo specyficzny dla procesora. Strona, do której linkuję, zawiera informacje o różnych typach procesorów.

Wim ten Brink
źródło
Rejestry segmentów są osobne na x86 - są to gs, cs, ss i chyba, że ​​piszesz oprogramowanie do zarządzania pamięcią, nigdy ich nie dotykasz.
Michael
ds jest również rejestrem segmentowym, aw czasach MS-DOS i 16-bitowego kodu zdecydowanie trzeba od czasu do czasu zmieniać te rejestry segmentowe, ponieważ nigdy nie mogą wskazywać na więcej niż 64 KB pamięci RAM. Jednak DOS może uzyskać dostęp do pamięci do 1 MB, ponieważ wykorzystuje 20-bitowe wskaźniki adresu. Później mamy systemy 32-bitowe, niektóre z 36-bitowymi rejestrami adresów, a teraz rejestry 64-bitowe. W dzisiejszych czasach tak naprawdę nie trzeba już zmieniać tych rejestrów segmentów.
Wim ten Brink
Żaden współczesny system operacyjny nie używa 386 segmentów
Ana Betts
@Paul: ŹLE! ŹLE! ŹLE! Segmenty 16-bitowe są zastępowane segmentami 32-bitowymi. W trybie chronionym pozwala to na wirtualizację pamięci, zasadniczo umożliwiając procesorowi mapowanie adresów fizycznych na logiczne. Jednak w Twojej aplikacji wszystko wydaje się płaskie, ponieważ system operacyjny zwirtualizował pamięć. Jądro działa w trybie chronionym, umożliwiając aplikacjom działanie w modelu z płaską pamięcią. Zobacz także en.wikipedia.org/wiki/Protected_mode
Wim ten Brink
@ Workshop ALex: To jest technika. Wszystkie nowoczesne systemy operacyjne ustawiają wszystkie segmenty na [0, FFFFFFFF]. To się tak naprawdę nie liczy. A jeśli przeczytasz połączoną stronę, zobaczysz, że wszystkie fantazyjne rzeczy są wykonywane ze stron, które są znacznie bardziej szczegółowe niż segmenty.
MSalters
-4

Edytować Tak, w większości jest to złe. Opisuje coś zupełnie innego na wypadek, gdyby ktoś był zainteresowany :)

Tak, wskaźnik stosu wskazuje na górę stosu (czy jest to pierwsza pusta lokalizacja stosu, czy ostatnia pełna, której nie jestem pewien). Wskaźnik bazowy wskazuje lokalizację pamięci wykonywanej instrukcji. Jest to na poziomie kodów operacyjnych - najbardziej podstawowej instrukcji, jaką można uzyskać na komputerze. Każdy kod operacji i jego parametry są przechowywane w miejscu w pamięci. Jedna linia C, C ++ lub C # może zostać przetłumaczona na jeden kod operacji lub sekwencję dwóch lub więcej, w zależności od stopnia złożoności. Są one kolejno zapisywane w pamięci programu i wykonywane. W normalnych okolicznościach wskaźnik bazowy jest zwiększany o jedną instrukcję. W celu sterowania programem (GOTO, IF itp.) Można go wielokrotnie zwiększać lub po prostu zastępować kolejnym adresem pamięci.

W tym kontekście funkcje są przechowywane w pamięci programu pod określonym adresem. Kiedy funkcja jest wywoływana, pewne informacje są wypychane na stos, który pozwala programowi znaleźć ją z powrotem do miejsca, z którego funkcja została wywołana, a także parametry funkcji, następnie adres funkcji z pamięci programu jest przekazywany do wskaźnik bazowy. W następnym cyklu zegarowym komputer rozpoczyna wykonywanie instrukcji od tego adresu pamięci. W pewnym momencie POWRÓT do miejsca w pamięci PO instrukcji, która wywołała funkcję i kontynuuje od tego momentu.

Stephen Friederichs
źródło
Mam trochę problemów ze zrozumieniem, co to jest ebp. Jeśli mamy 10 linii kodu MASM, to znaczy, że kiedy zaczynamy korzystać z tych linii, ebp zawsze będzie rosło?
pożarł elysium
1
@Devoured - Nie. To nie jest prawda. eip będzie rosło.
Michael
Masz na myśli, że to, co powiedziałem, jest słuszne, ale nie dla EBP, ale dla IEP, prawda?
pożarł elysium
2
Tak. EIP jest wskaźnikiem instrukcji i jest domyślnie modyfikowany po wykonaniu każdej instrukcji.
Michael
2
O mój Boże. Myślę o innym wskaźniku. Chyba umyję mózg.
Stephen Friederichs
-8

esp oznacza „Extended Stack Pointer” ..... ebp dla „Something Base Pointer” .... i eip dla „Something Instruction Pointer” ...... Wskaźnik stosu wskazuje adres przesunięcia segmentu stosu . Wskaźnik bazowy wskazuje adres przesunięcia dodatkowego segmentu. Wskaźnik instrukcji wskazuje adres przesunięcia segmentu kodu. Teraz, jeśli chodzi o segmenty ... są to małe działy o wielkości 64 KB w obszarze pamięci procesorów ... Ten proces jest znany jako segmentacja pamięci. Mam nadzieję, że ten post był pomocny.

Adarsha Kharel
źródło
3
Jest to stare pytanie, jednak sp oznacza wskaźnik stosu, bp oznacza wskaźnik bazowy, a ip wskaźnik instrukcji. Litera „e” na początku mówi po prostu, że jest to wskaźnik 32-bitowy.
Hyden,
1
Segmentacja nie ma tutaj znaczenia.
BarbaraKwarc,