[assembly] wskaźnik stosu a instrukcja push, ramka stosu [32 bits]

0

Cześć, przerabiam kurs asma na youtube i nie do końca rozumiem działania pewnych instrukcji, a mianowicie:

mov ebp, esp
sub esp, 8
lea eax, [epb-4] 
push eax

pierwsza linia wrzuca do ebp adres wierzchołka stosu
druga przesuwa wskaźnik stosu o 8
trzecia wrzuca do eax adres ebp-4
czwarta wrzuca na stos eax

I teraz nie kumam tego względem czego instrukcja push wrzuca na stos zawartość rejestru eax
skoro został przesunięty wskaźnik stosu to nie jest tak, że kolejna instrukcja push powinna wrzucić zawartość eax w miejsce, na które wskazuje esp?

Czy dla programu wskaźnik esp nie jest w ogóle istotny i ma jakby własny wskaźnik stosu niedostępny dla programisty?

I kolejne pojęcie "ramka stosu".
Jesli wrzucam na stos:

db "%i %i", 0

to funkcja scanf domyśla się, że musi odczytać jeszcze dwie kolejne wartości ze stosu, które są w dwóch osobnych komórkach pamięci? czyli esp-4 i esp-8?
Czy ramka stosu obejmuje w tym przypadku 3 komórki pamięci?

3

(...) skoro został przesunięty wskaźnik stosu to nie jest tak, że kolejna instrukcja push powinna wrzucić zawartość eax w miejsce, na które wskazuje esp (...)

Najpierw dekrementowany jest rejestr esp, a następnie wrzucane są dane pod adres, który zawiera ten rejestr.

Czy dla programu wskaźnik esp nie jest w ogóle istotny i ma jakby własny wskaźnik stosu niedostępny dla programisty? (...)

Tego pytania trochę nie rozumiem. Dla programu rejestr esp jest o tyle istotny, że wskazuje na szczyt stosu i możesz względem niego do tego stosu mieć dostęp. Niemniej jednak adresowanie stosu po esp jest raczej nieporęczne, bo musiałbyś śledzić wartość tego esp, gdyż instrukcje push/pop sobie go modyfikują. Po to wrzuca się rejestr esp do rejestru ebp, żeby móc wygodnie działać na stosie wykorzystując ebp jako base address.

(...) to funkcja scanf domyśla się, że musi odczytać jeszcze dwie kolejne wartości ze stosu, które są w dwóch osobnych komórkach pamięci? czyli esp-4 i esp-8? (...)

scanf i ogólnie funkcje wykorzystujące format stringi działają tak:

...
#############
BP <== ebp
#############
RET
#############
"%i %i" (char *)
#############
integer
#############
integer
#############

Funkcje wykorzystujące format stringi parsują sobie ten format i ściągają ze stosu kolejne wartości pasujące do danego formatu. W przypadku tym co podałeś scanf ściągnie 2 inty, które leżą na stosie pod format stringiem. Ona sobie je ściągnie raczej przy użyciu [ebp-12] i [ebp-16], nie esp-4 i esp-8.

3

Najpierw dwie sprawy organizacyjne:

  1. Po co uczysz się assemblera x86? Jeśli na studia to rozumiem (tylko szkoda, że mamuty was uczą), jeśli nie, lepiej naucz się x64, bo to jest współczesny assembler na PC.
  2. Używaj debuggera, w każdym debuggerze możesz sobie podejrzeć stan rejestrów i pamięci, i jechać instrukcja po instrukcji. Poza stanem CFLAGS raczej nie powinno tam być czegoś czego nie wywnioskowałbyś ze zmian, które widzisz.

Teraz tak, myślę, że traktujesz ten kod zbyt niskopoziomowo. Generalnie masz stos. To po prostu wydzielony kawałek pamięci, rejestr ESP wskazuje na wierzchni element stosu, nie wiadomo natomiast gdzie się stos zaczyna (przynajmniej architektura nic o tym nie mówi, są sposoby, żeby to ogarnąć). Każdy koljny element jest wrzucany na stos o jedną komórkę (tutaj 4 bajty, choć to też zależy od rozmiaru argumentu) wcześniej. Tak więc push eax znaczy właściwie tyle co

mov [esp], eax
sub esp, 4

Tyle tylko, że to co tu jest robione używa stosu wedle konwencji wywołania funkcji. Świadczy o tym pierwsza linia mov ebp, esp, jest to kod idiomatyczny (choć zwykle zaraz potem jest push esp). Robi się ze względu na ramkę stosu. W momencie wywołania funkcji, na wierzchu stosu (adres esp) masz adres powrotu, który został tam umieszczony automatycznie przez instrukcję call. Pod nim masz argumenty funkcji (esp+4 pierwszy parametr, esp+8 to drugi itd. Jest to pewne uproszczenie, ale na tą chwilę wystarczy).

Tyle tylko, że bardzo byłoby to uciążliwe gdybyś chciał używać stosu w funkcji, bo musiałbyś w każdym momencie programu wiedzieć ile masz elementów na stosie, dlatego jest konwencja z rejestrem ebp, zgodnie z którą środek ramki stosu trzymamy w ebp i używamy stosu jak chcemy. Wspomniana instrukcja push ebp jest konieczna żeby utrzymywać tę konwencję przy kolejnych wywołaniach funkcji wewnątrz funkcji. Nie wiem jak to dokładnie jest na x86, ale na x64 jest to opcjonalne (ale kompilatory i tak to robią, jeśli nie masz włączonych optymalizacji), a ebp jest takim samym rejestrem jak inne.

W drugiej linijce zmniejszasz esp o 8, co znów jest konwencjonalne i prawdopodobnie oznacza, że alokujesz miejsce na zmienne lokalne. Skoro masz wywołać scanf to jest to zapewne bufor, co potwierdza linijka następna, wrzucasz adres ebp-4 czyli 4 bajty na bufor (pierwszy int) i 4 bajty na drugiego inta – to będzie ebp-8. Dalej idą parametry, ebp-12=&(ebp-4), ebp-16=&(ebp-8), ebp-20=&("%i %i\0"). Potem wywoływane jest call i wrzuca pod ebp-24 adres powrotu.

jak byś chciał się tego porządnie nauczyć to polecam się. :)

0

Po pierwsze dziękuję za pomoc Panowie :)

Czy dla programu wskaźnik esp nie jest w ogóle istotny i ma jakby własny wskaźnik stosu niedostępny dla programisty? (...)

Błądziłem, ale w końcu zaczyna mi coś świtać. Nie rozumiałem tego po co eax przechowuje adres innej komórki pamięci a nie samą zmienną.
Dotarło do mnie, że scanf jako argumentu spodziewa się adresu, który przechowuje eax a nie samej zawartości eax. Dlatego pogubiłem się w tym co jest na stosie. A tak swoją drogą to skąd asembler wie, że coś jest adresem a nie zmienną?

elwis napisał(a):

Najpierw dwie sprawy organizacyjne:

  1. Po co uczysz się assemblera x86? Jeśli na studia to rozumiem (tylko szkoda, że mamuty was uczą), jeśli nie, lepiej naucz się x64, bo to jest współczesny assembler na PC.
  2. Używaj debuggera, w każdym debuggerze możesz sobie podejrzeć stan rejestrów i pamięci, i jechać instrukcja po instrukcji. Poza stanem CFLAGS raczej nie powinno tam być czegoś czego nie wywnioskowałbyś ze zmian, które widzisz.
  1. Długo szukałem materiałów na temat assemblera, a książki, które miałem w ręce opisują nawet jeszcze 16-bitowy. Była jakaś pozycja o x64, ale odradzali ją na forach. Pomyślałem, że warto nauczyć się podstaw x86, a później łatwiej będzie się przesiąść. W związku z tym, że jedyny godny kurs godny polecenia jaki znalazłem dotyczy x86 wybór padł na 32bity. Mówię o polskiej wersji językowej, bo na pewno jeśli chodzi o język angielski by się coś znalazło, ale mój angielski jest za słaby, żeby zrozumieć tekst zawarty w książce ze szczegółami.
  2. Tak, dzięki za rade. Myślę, że zaprzyjaźnię się z debuggerem.
elwis napisał(a):

jak byś chciał się tego porządnie nauczyć to polecam się

Chętnie bym skorzystał, ale chyba dzieli nas spora odległość, gdyż mieszkam w Łodzi.
Zawsze chciałem mieć guru jeśli chodzi o tematykę komputerową ;)

2

A tak swoją drogą to skąd asembler wie, że coś jest adresem a nie zmienną?

Coz... Assembler nic nie wie. Wiedziec powinien programista.
Tak jak w C masz:
void foo(bar* buzz) {
No to wiesz, ze buzz to adres a zeby dostac sie do wartosci to trzeba dokonac dereferencji.

2

A tak swoją drogą to skąd asembler wie, że coś jest adresem a nie zmienną?

Nie wie :) Stąd wynikają różne ciekawe metody eksploitacji oprogramowania w stylu type confusion, czy chociażby klasyczny stack buffer overflow -> możesz przepełnić jakiś bufor na stosie, a tym samym zacząć nadpisywać ramkę stosu i możesz w ten sposób nadpisać przechowywany na stosie adres powrotu z funkcji na dowolną inną wartość. W efekcie program pyta cię o imie i czyta stringa, ale w praktyce kawałek tego stringa nadpisze pointer i pozwoli ci skoczyć w dowolne miejsce i zacząć wykonywać kod który się tam znajduje :)

0
haracz napisał(a):

Błądziłem, ale w końcu zaczyna mi coś świtać. Nie rozumiałem tego po co eax przechowuje adres innej komórki pamięci a nie samą zmienną.
Dotarło do mnie, że scanf jako argumentu spodziewa się adresu, który przechowuje eax a nie samej zawartości eax. Dlatego pogubiłem się w tym co jest na stosie. A tak swoją drogą to skąd asembler wie, że coś jest adresem a nie zmienną?

Najprościej mówiąc, instrukcje assemblera nijak nie mają się do wartości w pamięci i rejestrach (ewentualnie możesz mieć stałą). Kiedy piszesz db "foo", 0 to znaczy tylko tyle, że zamiast wpisywać w danym miejscu kod instrukcji, ma po prostu wstawić "foo\0". Natomiast przedtem masz deklarację etykiety, która działa jak każda inna. Wszędzie gdzie wpiszesz nazwę etykiety będzie wstawiony jej adres, innej opcji nie ma.

  1. Długo szukałem materiałów na temat assemblera, a książki, które miałem w ręce opisują nawet jeszcze 16-bitowy. Była jakaś pozycja o x64, ale odradzali ją na forach […]

To ma sens, na szczęście x86 jest dość podobny od x64. Jest więcej rejestrów, trochę inaczej się wywołuje funkcje i jest nowy sposób adrsowania.
Tu jest całkiem dobry opis: https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/x64-architecture . Tylko tyle, że na Linuksie jest chyba inna konwencja wywoływania funkcji i warto się zapoznać z instrukcją syscall dla wywołań systemowych. Architerktury 16-bitowej nie tykaj, szkoda zdrowia. ;)

Chętnie bym skorzystał, ale chyba dzieli nas spora odległość, gdyż mieszkam w Łodzi.
Zawsze chciałem mieć guru jeśli chodzi o tematykę komputerową ;)

To chyba nie jest problem, w XXI w mamy internet, udostępnianie ekranu i kamerki internetowe xD Napisz na priv jak coś.

2
haracz napisał(a):

Cześć, przerabiam kurs asma na youtube i nie do końca rozumiem działania pewnych instrukcji, a mianowicie:

mov ebp, esp
sub esp, 8
lea eax, [epb-4] 
push eax

Zacznijmy od tego, że tu się dzieją w zasadzie dwie logiczne rzeczy:

mov ebp, esp
sub esp, 8

To jest tworzenie typowej "ramki stosu". Mamy tu zaalokowanie 8 bajtów na stosie na zmienne lokalne. Do ich adresowania będzie używany rejestr ebp. Jeśli uznamy że te 8 bajtów będą zajęte przez dwa inty, to te dwie zmienne będą miały adresy ebp-4 i ebp-8.

lea eax, [epb-4] 
push eax

Tu mamy w jakimś (nieznanym nam na podstawie tego fragmentu) celu pushnięcie na stos adresu tej pierwszej zmiennej lokalnej, czyli wartości ebp-4.
Jest to równoważne takiemu zapisowi (gdyby był on prawidłowy):

mov eax, ebp-4
push eax

Instrukcja mov nie pozwala nam na takie wykonywanie działań w ramach jednej instrukcji, ale pozwala na to lea (udając że robi coś więcej, ale tak naprawdę to taki mov na sterydach).

Moglibyśmy więc też zrobić:

mov eax, ebp
sub eax, 4
push eax

i efekt będzie taki sam (z dokładnością do ustawianych flag, których teraz nie chce mi się sprawdzać).

0

A jak to jest z tworzeniem ramki stosu dla funkcji i wrzucenie na stos adresu starej ramki stosu?

push ebp
mov ebp, esp

Rejestr ebp zawsze przy wywołaniu funkcji posiada adres starej ramki stosu?
Jak to wygląda, gdy wywoływana funkcja jest pierwszą funkcją wywołaną w programie?
Co wtedy zawiera ebp?

0
haracz napisał(a):

A jak to jest z tworzeniem ramki stosu dla funkcji i wrzucenie na stos adresu starej ramki stosu?

push ebp
mov ebp, esp

Rejestr ebp zawsze przy wywołaniu funkcji posiada adres starej ramki stosu? Czy musimy sami o to zadbać?
Jak to wygląda, gdy wywoływana funkcja jest pierwszą funkcją wywołaną w programie?
Co wtedy zawiera ebp?

1

Adres powrotu nie jest adresem ramki stosu a adresem do kolejnej instrukcji (kodu), ktora ma sie wykonac kiedy funkcja zakonczy dzialanie.

Pierwsza funkcja w programie ma zapewne (zgaduje, szczerze mowiac nie wiem) adres powrotu ustawiony na cos pokroju systemowej funkcji exit, ktora jeszcze posprzata po procesie. Mozna to sprawdzic debuggerem jesli bardzo chcesz wiedziec.

0

Z kursu wynika, że adres starej ramki stosu to co innego niż adres powrotu. Adres starej ramki ma znajdować się tuż za adresem powrotu.

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