Zrozumienie ramki stosu wywołania funkcji w C / C ++?

19

Próbuję zrozumieć, w jaki sposób budowane są ramki stosu i które zmienne (parametry) są wypychane do stosu w jakiej kolejności? Niektóre wyniki wyszukiwania wykazały, że kompilator C / C ++ decyduje na podstawie operacji wykonywanych w ramach funkcji. Na przykład, jeśli funkcja miała po prostu zwiększyć przekazaną wartość int o 1 (podobnie jak operator ++) i zwrócić ją, umieściłby wszystkie parametry funkcji i zmiennych lokalnych w rejestrach.

Zastanawiam się, które rejestry są używane do zwracania lub przekazywania parametrów wartości. Jak zwracane są referencje? Jak kompilator wybiera między eax, ebx, ecx i edx?

Co muszę wiedzieć, aby zrozumieć, w jaki sposób rejestry, odwołania do stosu i stosu są używane, budowane i niszczone podczas wywoływania funkcji?

Gana
źródło
jest to raczej trudne do odczytania (ściana tekstu). Czy mógłbyś edytować swój post w lepszym kształcie?
komar
1
To pytanie wydaje mi się dość szerokie. Czy to nie będzie bardzo specyficzne dla platformy?
Kazark,
Pytanie zostało również zadane na SO: stackoverflow.com/questions/16088040/…
Wayne Conrad
Zobacz także moją odpowiedź na temat SO
Basile Starynkevitch,

Odpowiedzi:

11

Oprócz tego, co powiedział Dirk, ważnym zastosowaniem ramek stosu jest zapisywanie poprzednich wartości rejestrów, aby można je było przywrócić po wywołaniu funkcji. Tak więc nawet na procesorach, w których rejestry są używane do przekazywania parametrów, zwracania wartości i zapisywania adresu zwrotnego, wartości tych rejestrów są zapisywane na stosie przed wywołaniem funkcji, aby można je było przywrócić po wywołaniu. Dzięki temu jedna funkcja może wywoływać inną bez nadpisywania własnych parametrów lub zapomnienia własnego adresu zwrotnego.

Wywołanie funkcji B z funkcji A w typowym „ogólnym” systemie może obejmować następujące kroki:

  • funkcja A:
    • wciśnij spację dla wartości zwracanej
    • parametry push
    • naciśnij adres zwrotny
  • przejdź do funkcji B
  • funkcja B:
    • naciśnij adres poprzedniej ramki stosu
    • wypychaj wartości rejestrów używanych przez tę funkcję (aby można je było przywrócić)
    • push miejsca na zmienne lokalne
    • wykonaj niezbędne obliczenia
    • przywrócić rejestry
    • przywróć poprzednią ramkę stosu
    • zapisz wynik funkcji
    • przeskocz na adres zwrotny
  • funkcja A:
    • pop parametry
    • pop wartość powrotu

Nie jest to w żaden sposób jedyny sposób, w jaki mogą zadziałać wywołania funkcji (i mogę mieć krok lub dwa zepsute), ale powinien dać ci wyobrażenie o tym, jak stos jest używany, aby procesor obsługiwał zagnieżdżone wywołania funkcji.

Caleb
źródło
Co dokładnie oznacza tutaj „push”? Nie mam pojęcia, co z tym zrobić.
Tomáš Zato - Przywróć Monikę
2
@ TomášZato pushi popsą to dwie podstawowe operacje na stosie. Stos jest strukturą „kto pierwszy, ten pierwszy”, podobnie jak stos książek. Kiedy ty pushkładziesz nowy obiekt na stosie; kiedy popbierzesz przedmiot ze szczytu stosu. Nie możesz wstawiać ani usuwać obiektów na środku, możesz operować tylko na górze stosu. Możesz przeczytać więcej o stosach ogólnie, a stosie programów w szczególności na Wikipedii.
Caleb
11

Zależy to od zastosowanej konwencji wywoływania. Ktokolwiek zdefiniuje konwencję wywoływania, może podjąć taką decyzję, jak chce.

W najpopularniejszej konwencji wywoływania na x86 rejestry nie są używane do przekazywania parametrów; parametry są wypychane na stos, zaczynając od parametru znajdującego się najdalej po prawej stronie. Zwracana wartość jest umieszczana w eax i może używać edx, jeśli potrzebuje dodatkowej przestrzeni. Referencje i wskaźniki są zwracane w postaci adresu w eax.

Dirk Holsopple
źródło
5

Jeśli rozumiesz stos bardzo dobrze, zrozumiesz, jak działa pamięć w programie, a jeśli zrozumiesz, jak pamięć działa w programie, zrozumiesz, jak przechowuje funkcje w programie, a jeśli zrozumiesz, jak przechowuje funkcje w programie, zrozumiesz, jak działa funkcja rekurencyjna, a jeśli zrozumiesz, jak działa funkcja rekurencyjna, zrozumiesz, jak działa kompilator, a jeśli zrozumiesz, jak działa kompilator, twój umysł będzie działał jako kompilator i bardzo łatwo debugujesz dowolny program

Pozwól mi wyjaśnić, jak działa stos:

Najpierw musisz wiedzieć, jak przechowywać funkcje w stosie:

Sterty przechowują wartości dynamicznego przydziału pamięci. Wartości automatycznej alokacji i usuwania magazynu stosu.

wprowadź opis zdjęcia tutaj

Rozumiemy na przykładzie:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(4)

Teraz zrozum części tego programu:

wprowadź opis zdjęcia tutaj

Zobaczmy teraz, co to jest stos, a jakie części stosu:

wprowadź opis zdjęcia tutaj

Przydział stosu:

Pamiętaj o jednej rzeczy, jeśli jakakolwiek funkcja zostanie „zwrócona” bez względu na to, że załadowała wszystkie lokalne zmienne lub cokolwiek, co natychmiast zwróci ze stosu, zmieni ramkę stosu. Oznacza to, że gdy jakakolwiek funkcja rekurencyjna uzyska warunek podstawowy, a my ustawimy return po warunku podstawowym, więc warunek podstawowy nie będzie czekał na załadowanie zmiennych lokalnych, które znajdują się w „innej” części programu, natychmiast zwróci bieżącą ramkę ze stosu, a teraz, jeśli jedna ramka powrót następnej klatki jest w rekordzie aktywacji. Zobacz to w praktyce:

wprowadź opis zdjęcia tutaj

Zwolnienie bloku:

Tak więc teraz, gdy funkcja znajdzie instrukcję return, usuwa bieżącą ramkę ze stosu.

podczas powrotu ze stosu wartość zostanie zwrócona w odwrotnej kolejności, w jakiej zostały przydzielone w stosie.

wprowadź opis zdjęcia tutaj

To bardzo krótki opis i jeśli chcesz dowiedzieć się więcej o stosie i podwójnej rekurencji, przeczytaj dwa posty na tym blogu:

Więcej informacji o stosie krok po kroku

Więcej informacji o Podwójnej rekurencji krok po kroku ze stosem

użytkownik5904928
źródło
3

To, czego szukasz, nazywa się Application Binary Interface - ABI.

Dla każdego kompilatora istnieje specyfikacja określająca ABI.

Każda platforma zwykle określa i ABI w celu obsługi interoperacyjności między kompilatorami. Na przykład, Konwencje wywoływania x86 określają typowe konwencje wywoływania dla x86 i x86-64. Spodziewałbym się jednak bardziej oficjalnego dokumentu niż wikipedia.

Bill Door
źródło