ARCHITEKTURA SYSTEMÓW KOMPUTEROWYCH: Instrukcja do laboratorium 4, (2x2h) Opracowanie i prowadzenie: dr inż. Ignacy Pardyka, Uniwersytet Jana Kochanowskiego w Kielcach Temat: Architektura MIPS: wywołanie funkcji, stos, złożone struktury danych symulator: MARS 1. Wstęp W ramach niniejszego laboratorium, najpierw poznajemy mechanizm wywoływania funkcji, stosowany w architekturze MIPS. Aby zrozumieć ten mechanizm, musimy poznać i zrozumieć pojęcie ramki stosu. Szczególne miejsce, w programowaniu komputerów, zajmują algorytmy rekurencyjne, dlatego ważne jest by rozumieć, w jaki sposób należy prawidłowo organizować program, w którym występują rekurencyjne wywołania funkcji. W języku asemblera stos odgrywa ważną rolę. Poznamy też zasadę przechowywania i przetwarzania złożonych struktur danych. 2. Instrukcje wywołania funkcji i powrotu 2.1. W architekturze MIPS, na liście instrukcji widzimy dwie instrukcje dotyczące funkcji: a) instrukcję jal, która określa operację przekazania sterowania do funkcji (ze śladem); instrukcja ta nakazuje, aby procesor, zanim przekaże sterowanie do wskazanej funkcji (wykonując operację skoku), zapamiętał w rejestrze $ra adres instrukcji, występującej w programie tuż za instrukcją jal (czyli adres powrotu z funkcji do miejsca wywołania). b) instrukcję jr, określającą operację powrotu z funkcji do programu, który ją wywołał; instrukcja ta, faktycznie, nakazuje procesorowi wykonanie operacji skoku bezwarunkowego pod adres, który aktualnie jest w rejestrze $ra (return address). 2.2. Zadanie 1 a) Wprowadzić następujący program string1: string2:.data.asciiz "Hello world!\n".asciiz "Drugi napis\n".text main: la $a0, string1 drukuj pierwszy łańcuch jal function teraz wywołać funkcję function() bez argumentów. stosujemy instrukcję jal, która nakazuje zapamietać w $ra adres instrukcji występującej tuż za jal, a następnie wykonać skok do pierwszej instrukcji w funkcji $a0 zawiera wskaźnik do pierwszego z łańcuchów więc drukować go li $v0, 10 exit Ta funkcja jest wywoływana z main(). Drukuje łańcuch i zwraca sterowanie do wywołującego. function: la $a0, string2 drukuj łańcuch powrót do instrukcji wskazywanej przez $ra str. 1/8
b) Przeprowadzić asemblację i wykonać program krokowo. Proszę zaobserwować wykonanie operacji wskazywanej w instrukcji jal: jaki adres jest zapamietany w $ra, jaki jest adres początkowy funkcji? Zaobserwować zakończenie funkcji: jaki jest adres w $ra, jaki jest adres instrukcji, która będzie realizowana po instrukcji? 3. Organizacja funkcji 3.1. Pisząc (w języku asemblera) funkcje i programy, w których występują wywołania funkcji, należy przestrzegać pewnych reguł: a) Wywołujący funkcję powinien mieć możliwość przekazania jej listy argumentów (kolejność może być istotna). W ciele funkcji, dostęp do argumentów powinien być taki, jak do zmiennych lokalnych. b) Funkcja powinna mieć możliwość tworzenia własnych zmiennych. c) Wywołujący funkcję (ang. caller) i wywołana funkcja (ang. callee) muszą przestrzegać uzgodnień odnośnie do korzystania z rejestrów procesora, tak aby nie było obaw, że mogą wystąpić kolizje, prowadzące do utraty danych przechowywanych w rejestrach. d) Funkcja musi mieć możliwość zwrócenia (wywołującemu) wartości, będącej rezultatem jej działania. e) Jeśli planujemy korzystać z funkcji do implementacji algorytmów rekurencyjnych, to trzeba tak napisać funkcję, aby funkcja mogła wywołać samą siebie bez ograniczenia głębokości zagnieżdżenia. 3.2. Rejestry używane do przekazania argumentów i wartości zwracanych a) W architekturze MIPS, standardowo, do przekazania argumentów do funkcji, stosuje się rejestry $a0, $a1, $a2, $a3, $a4 jeśli trzeba przekazać więcej argumentów, to należy je przekazać poprzez pamięć (stos). b) Rezultat powinien być, standardowo, zwracany w rejestrach $v0, $v1 (wartość typu doubleword jest zwracana w parze rejestrów). 3.3. Podział odpowiedzialności za stan rejestrów CPU a) odpowiedzialność wywołującego: przed wywołaniem funkcji, należy zachować (w pamięci) stan tych spośród rejestrów $t0 do $t7, których zawartość jest istotna, i chcemy mieć pewność, że nie zostanie zmieniona przez funkcję; analogicznie, należy zadbać o stan rejestrów $a0 do $a3, oraz $v0, $v1. b) odpowiedzialność wywoływanego: funkcja musi, przede wszystkim, zachować stan (w pamięci) tych spośród rejestrów $s0.. $s7, $fp, $ra, które mają być używane przez tę funkcję, w celu przechowywania w nich nowych wartości. 3.4. Stos a) Stos jest strukturą LIFO (ang. last in first out) zorganizowaną w pamięci, wskazywaną przez rejestr $sp (stack pointer). Dane, które trzeba przechować na stosie, odkłada się na wierzchołek (rejestr $sp zawiera adres danych leżących na wierzchołku stosu). W architekturze MIPS, stos rośnie w kierunku adresów malejących, więc przed odłożeniem na stos, należy dekrementować rejestr $sp (aby nie nadpisać danych leżących na wierzchołku). str. 2/8
b) Stos jest używany przez wywołującego funkcję do zachowania zawartości rejestrów (w ramach swoich odpowiedzialności za stan rejestrów), a także do przekazania argumentów do funkcji (gdy jest ich więcej niż cztery). c) Stos jest używany przez funkcję do przechowania zawartości rejestrów (w ramach swoich odpowiedzialności za stan rejestrów), a także do przechowywania swoich zmiennych lokalnych. d) Operacja odkładania na stos (ang. push), np. subu $sp, $sp, 4 sw $t1, ($sp) dekrementacja $sp zapamiętanie zawartości rejestru $t1 na stosie e) Operacja zdejmowania ze stosu (ang. pop), np. lw $t3, ($sp) addu $sp, $sp, 4 pobranie słowa wskazywanego przez $sp aktualizacja wskaźnika wierzchołka stosu 3.5. Ramka stosu a) Funkcja tworzy na stosie obszar zwany ramką stosu (ang. stack frame), gdzie dla swoich potrzeb przechowuje: argumenty przekazane przez wywołującego adres powrotny (adres ten jest zawarty w rejestrze $ra, i byłby zniszczony, gdyby w ciele funkcji nastąpiło wywołanie innej funkcji) zawartość rejestrów, które ma zachować w ramach swojej odpowiedzialności za stan rejestrów zmienne lokalne (deklarowane w funkcji) zawartość rejestrów, które ma zachować wywołujący jeśli funkcja wywołuje funkcję. b) Przykładowa organizacja podstawowej, 24-bajtowej, ramki stosu (stos rośnie w kierunku adresów malejących; z lewej strony pokazano offset względem wierzchołka wskazywanego przez $sp): c) Przykład fragmentu prologu funkcji (początkowe instrukcje dotyczące ramki): function: subu $sp, $sp, 24 miejsce w ramce stosu dla 24 bajtów sw $ra, 20($sp) zachować $ra gdy funkcja ma wywoływać funkcję d) Przykład fragmentu epilogu funkcji (końcowe instrukcje): lw $ra, 20(sp) addu $sp, $sp, 24 pobranie adresu powrotnego niszczenie ramki stosu powrót z funkcji str. 3/8
3.6. Zadanie 2 a) Wprowadzić następujący program string1: string2:.data.asciiz "Hello world!\n".asciiz "Drugi tekst\n".text main: subu $sp, $sp, 24 program główny, main(), sam jest funkcją sw $ra, 20($sp) więc najpierw trzeba utworzyć ramkę stosu la $a0, string1 drukować pierwszy łańcuch sw $a0, 0($sp) zachować w ramce stan rejestru $a0 jal function wywołać funkcję o nazwie function lw $a0, 0($sp) odtworzyć rejestr $a0 li $v0, 10 powtórnie drukować pierwszy łańcuch exit (tutaj nie ma potrzeby niszczyc ramki) function: subu $sp, $sp, 24 utworzyć ramkę stosu sw $ra, 20($sp) la $a0, string2 lw $ra, 20($sp) addu $sp, $sp, 24 drukować drugi łańcuch zrekonstruować adres powrotny zniszczyć ramkę stosu powrót do wywołującego b) Wykonać program krokowo; prześledzić operacje na stosie, zmieniający się stan stosu, zrozumiec i zapamiętać, jak działa stos, jak utworzyć ramkę stosu, jak należy organizować funkcje. 4. Zmienne lokalne 4.1. Zmienne lokalne funkcji przechowuje się na stosie, w ramce stosu. Zmienne te mają więc charakter dynamiczny, gdyż w odróżnieniu od zmiennych statycznych (które są przechowywane w segmencie.data), zmienne lokalne nie zajmują pamięci przez cały czas wykonywania programu: istnieją (i można w nich przechowywać wartości) od chwili utworzenia ramki do czasu zniszczenia ramki stosu. a) W prologu funkcji, zmienne lokalne tworzy się następująco: należy zwiększyć rozmiar ramki stosu tak, aby utworzyć miejsce do przechowania w niej wszystkich zmiennych lokalnych należy zdecydować, gdzie w ramce stosu mają rezydować poszczególne zmienne (jaki jest ich adres względem początku ramki stosu). b) Do adresowania zmiennych lokalnych można wykorzystać rejestr $fp (wskaźnika ramki stosu), do którego można wpisać adres początkowy ramki (oczywiście, przed tą operacją, należy zachować na stosie zastaną zawartość rejestru $fp, a w epilogu funkcji zrekonstruować $fp). str. 4/8
c) Przykładowa, rozszerzona, ramka stosu z miejscem na zachowanie stanu rejestrów i zmienne lokalne: 4.2. Zadanie 3 a) Wprowadzić następujący program.data prompt:.asciiz "Prosze napisac jakis tekst (jedna linia): " resultstr:.asciiz "Najczesciej wystepujacy znak w tym tekscie, to " resultchar:.asciiz "X".text main: drukuj komunikat (polecenie) la $a0, prompt jal mostfreqchar sb $v0, resultchar la $a0, resultstr la $a0, resultchar li $v0, 10 wyznacz najczęściej występujący znak zapisz ten znak w resultchar drukuj resultstr drukuj znak najczęściej występujący exit Funkcja mostfreqchar nie ma argumentów, wczytuje linię tekstu do tablicy znaków, umieszczonej na stosie, a następnie wyznacza najczęściej występujący znak. char mfc; znak najczęściej występujący (rezultat) char ch; znak, którego wystąpienia są zliczane int cnt; licznik występowania bieżącego znaku int mfccnt = 0; aktualnie największa liczba wystapień for (int iptr = pointer_do line[0]; znak_wskazywany_przez iptr!= NUL; iptr++) { ch = znak_wskazywany_przez iptr; zacząć zliczać przypadki występowania tego znaku cnt = 0; start licznika for (int jptr = pointer_do line[0]; znak_wskazywany_przez jptr!= NUL; jptr++) { if (znak_wskazywany_przez jptr == ch) cnt++; zwiększyć licznik występowania } if (cnt > mfccnt) { ten znak wystąpił, jak dotąd, najczęściej mfccnt = cnt; zaktualizować licznik mfccnt mfc = ch; zapamiętać ten znak, występujący jak dotąd, najczęściej } } return(mfc); str. 5/8
Algorytm jest mało efektywny, ale tutaj nie jest to istotne, bo liczba znaków w linii jest niewielka wykorzystywane rejestry: int iptr => $a0 int jptr => $a1 int cnt => $a2 int ch => $a3 int mfc => $v0, wartość zwracana int mfccnt => $v1 char line[200] => 24($sp), rozmiar 200 bajtów pointer to line[0] => $t1 char at jptr => $t0 mostfreqchar: subu $sp, $sp, 224 miejsce na ramkę stosu sw $ra, 20($sp) li $v1, 0 mfccnt = 0 add $t1, $sp, 24 zapisać w $t1 wskaźnik do tablicy line move $a0, $t1 argument: $a0 <= $t1 li $v0, 8 usługa: read_string li $a1, 200 maksymalna liczba znaków w linii: 200 move $a0, $t1 iptr = adres line[0] iloop: lb $a3, ($a0) ch = znak_wskazywany_przez iptr beqz $a3, endiloop przerwać gdy ch jest NUL li $a2, 0 cnt=0 move $a1, $t1 jptr = adres line[0] jloop: lb $t0, ($a1) jch = znak_wskazywany_przez jptr beqz $t0, endjloop przerwać, gdy jch jest NUL bne $t0, $a3, jincr skok, gdy ch!= jch addi $a2, $a2, 1 kolejne wystąpienie, więc cnt++ jincr: addi $a1, $a1, 1 jptr++ b jloop zapętlić dla nowej wartości jptr endjloop: bgt $v1, $a2, iincr if mfccnt > cnt, pominąć aktualizację mfccnt move $v1, $a2 aktualizacja: mfccnt = cnt move $v0, $a3 aktualizacja: mfc = ch iincr: addi $a0, $a0, 1 iptr++ b iloop zapętlić dla nowej wartości iptr endiloop: endoffunc: lw $ra, 20($sp) odtworzenie adresu powrotnego addu $sp, $sp, 224 zniszczenie ramki stosu powrót z funkcji b) Program ten przechowuje w ramce stosu tablicę znaków, jako zmienną lokalną. W języku C, odpowiadałoby to następującej deklaracji funkcji: char mostfreqchar(void) { char line[200]; lokalna tablica 200 znaków... } c) Przeanalizować wprowadzony program, starając się zrozumieć algorytm i metodę jego implementacji w języku asemblera, a następnie program poddać asemblacji i wykonać. d) Wykonując krokowo, zaobserwować stan stosu tuż przed wywołaniem funkcji, podczas realizacji prologu, a następnie podczas realizacji epilogu funkcji. Odnaleźć, na stosie, tablicę wprowadzanych znaków linii. W sprawozdaniu opisać spostrzeżenia i przedstawić wnioski. str. 6/8
5. Rekurencja 5.1. Funkcje rekurencyjne muszą być tak konstruowane, aby mogły wywoływać same siebie, dlatego należy: a) w ramce stosu zachować stan rejestrów, zgodnie z odpowiedzialnością wywołującego b) w ramce stosu zachować stan rejestrów, zgodnie z odpowiedzialnością wywoływanego c) w ramce stosu przechowywać zmienne lokalne 5.2. Przykładowa funkcja rekurencyjna wyznaczająca silnię a) zapis w pseudokodzie int fact(int X) { int result; if (X == 1) return(1); result = X * fact(x-1); return(x); } b) Zmienna lokalna X jest jednocześnie argumentem funkcji, więc należy ją zachować w ramce stosu dla potrzeb wykonania mnożenia przez zwrócony rezultat. 5.3. Zadanie 4 a) Wprowadzić kod programu.text main: subu $sp, $sp, 24 utworzyć standardową ramkę sw $ra, 20($sp) li $a0, 6 jal factorial move $a0, $v0 li $v0, 1 li $v0, 10 wywołać factorial(6) rezultat jest w $v0, wydrukować go exit factorial: subu $sp, $sp, 24 utworzyć standardową ramkę 24-bajtową sw $ra, 20($sp) zachować adres powrotny bo będzie wywołanie rekurencyjne. sw $a0, 0($sp) zachować argument wywołania, bo będzie potrzebny do mnożenia. bgt $a0, 1, notbasecase if arg > 1, not the base case, skip basecase: li $v0, 1 przypadek bazowy: fact(1) = 1 b factreturn zwrócić rezultat. notbasecase: subi $a0, $a0, 1 przypadek niebazowy: argument funkcji zmniejszyć o 1 jal factorial i wywołać funkcję factorial rezultat jest w $v0 lw $a0, 0,($sp) pobrać oryginalny argument z ramki stosu mulo $v0, $a0, $v0 result = argument * factorial(argument - 1) factreturn: lw $ra, 20($sp) addu $sp, $sp, 24 odtworzyć adres powrotny zniszczyć ramkę wrócić do wywołującego (rezultat w $v0) b) Przeanalizować program, podać asemblacji i wykonać. c) Wykonać program krokowo, analizując stan stosu, ramki dla kolejnych wywołań rekurencyjnych, kolejne operacje mulo. str. 7/8
d) Przyjmując powyższy program za wzorzec, samodzielnie napisać program, służący do rekurencyjnego wyznaczania wartości elementów ciągu Fibonacciego: 1. Fib(1) = 1 2. Fib(2) = 1 3. Fib(X) = Fib(X-1) + Fib(X-2). e) Program poddać asemblacji i przetestować. Rezultaty pracy umieścić i opisać w sprawozdaniu. 5.4. Zadanie 5 a) W programie z zadania 4 wprowadzić zmianę organizacji dostępu do danych zawartych w ramce stosu, polegającą na zastosowaniu rejestru $fp, jako wskaźnika ramki stosu (zawartość rejestru $fp, w ciele funkcji, nie może podlegać zmianom, należy zastosować tryb adresowania pośredniego z przesunięciem, ang. indirect). b) Program przetestować za pomocą pracy krokowej. W sprawozdaniu zamieścić progam i opisać rezultaty przeprowadzonych testów. str. 8/8