Funkcje i stos

Funkcje i stos

Funkcja to nazwany blok instrukcji, którego możesz użyć więcej niż raz. Skaczesz do niej, wykonuje swoje zadanie i zwraca sterowanie do miejsca, z którego ją wywołałeś. Dzięki temu możesz napisać fragment logiki raz i używać go z wielu miejsc, zamiast powtarzać te same linie w całym programie.

Użyj , aby wywołać funkcję: skacze do etykiety i zapisuje miejsce powrotu w ra. Funkcja kończy się , który skacze z powrotem pod adres w ra. i to pseudoinstrukcje: wygodne nazwy, które asembler rozwija do rzeczywistych instrukcji, i .

Argumenty są umieszczane w a0, a1 i kolejnych rejestrach. Wartość zwracana jest umieszczana w a0. Ten układ to konwencja wywołań, wspólne porozumienie, które pozwala różnym fragmentom kodu współpracować ze sobą.

Funkcja square()
li a0, 6 # argument
call square # wywołaj, a ra pamięta, gdzie wrócić
li a7, 1 # square zostawia wynik w a0
ecall
li a7, 10
ecall
square:
mul a0, a0, a0 # a0 = a0 * a0
ret # wraca do ra

Prześledź wywołanie. umieszcza argument 6 w a0. skacze do etykiety square i zapisuje adres powrotu w ra. Wewnątrz funkcji oblicza 6 * 6 = 36 i pozostawia wynik w a0, rejestrze używanym do wartości zwracanych. skacze z powrotem do instrukcji zaraz po wywołaniu, gdzie i wypisują 36, a następnie program kończy działanie.

Wywołanie funkcji i zwrot
caller
call A
...
A
addi sp, sp, -4
sw ra, 0(sp)
call B
lw ra, 0(sp)
addi sp, sp, 4
ret
B
mul a0, a0, a0
ret
STACK
sp →
0x7ffffff0
0x7fffffec

Obiekt wywołujący zamierza wywołać A.

Obserwuj, jak podświetlona linia wykonuje całą podróż w obie strony. caller wykonuje , co automatycznie zapisuje punkt powrotu do caller w rejestrze ra. Zanim A może wykonać , napotyka problem: instrukcja zaraz nadpisze ra nowym adresem powrotu, niszcząc ten, którego A potrzebuje, aby wrócić do domu. Aby to naprawić, A robi miejsce na stosie () i zapisuje tam oryginalny ra (). Teraz można bezpiecznie wykonać . B wykonuje się i wraca zwykłym , lądując z powrotem wewnątrz A. Wreszcie A przywraca oryginalny ra ze stosu (), porządkuje wskaźnik stosu (), a jego własny używa tej przywróconej wartości, aby pomyślnie przeskoczyć z powrotem do caller. Gdyby A pominął ten krok, instrukcja trwale nadpisałaby ra, a końcowy w A przeskoczyłby w złe miejsce zamiast wrócić do domu.

Stos

Istnieje tylko kilka rejestrów, więc funkcje wywołujące inne funkcje potrzebują miejsca tymczasowego. Stos to pamięć, którą pożyczasz za pomocą wskaźnika stosu sp: odejmij, aby zwolnić miejsce, zapisz to, co musisz zachować, i przywróć to przed .

Stos to struktura Last-In, First-Out (LIFO): ostatnio zapisana wartość jest pierwszą, którą odczytujesz. W RISC-V stos rośnie w dół przez pamięć. Odjęcie od sp rezerwuje nowe miejsce, którego można bezpiecznie użyć. Gdy odczytujesz wartości z powrotem, musisz to robić w odwrotnej kolejności niż je zapisałeś, inaczej przepływ wykonania programu zostanie uszkodzony.

Zapisywanie i przywracanie wartości ra podczas wywołania
addi sp, sp, -4 # robi miejsce na jedno słowo
sw ra, 0(sp) # zapisuje adres powrotu
# ... wywołaj tu inne funkcje ...
lw ra, 0(sp) # przywraca go
addi sp, sp, 4 # zwalnia miejsce
ret

Każde / nadpisuje ra, więc jeśli twoja funkcja wywołuje inną funkcję, wewnętrzne wywołanie niszczy twój adres powrotu i twoje własne skacze w złe miejsce. Aby temu zapobiec, rezerwuje 4 bajty na stosie, a zapisuje tam ra. Po zakończeniu wewnętrznych wywołań przywraca adres powrotu, a oddaje tę przestrzeń przed .

⚠
Jeśli cokolwiek wywołałeś, przywróć ra przed i zawsze przywracaj sp do wartości, którą miał na początku. Stos, który nie zgadza się ze swoim stanem początkowym, jest zwykłą przyczyną awarii programów w asemblerze.