Datorteknik Föreläsning 3 Assembler, stack och subrutiner, programmeringskonventionen
Mål Att veta hur maskinkoden för ett program byggs upp Att börja programmera i assembler på riktigt Att kunna skriva och anropa subrutiner i assembler Att förstå hur stacken fungerar Att veta vad som förväntas av en subrutin som i sin tur anropar en annan
Innehåll Vilka komponenter består ett program av och hur överförs det till maskinkod? Hur måste subrutiner se ut för att kunna fungera? Vilka data måste en subrutin spara och varför? Var sparas dessa data och hur? Hur fungerar en stack? Vad är programmeringskonventioner och varför ska man följa dem?
Innehåll (forts) Hur använder jag assemblern? Vad är s.k. syntetiska instruktioner? Hur programmerar man för att rutiner i C och assembler ska fungera tillsammans? Exempel: Hur gör man en rekursiv subrutin i assembler som går att anropa från C?
Hur ser ett program ut? HUVUDPROGRAM C Subrutin 1 C Subrutin 2 assembler Subrutin 3 assembler Subrutin 21 assembler Subrutin 22 C Subrutin 31 Biblioteksrutin
Hur skapas maskinkoden? Huvudprog Subrutin Subrutin Bibliotek Kompilator Huvudprog Kompilator Assembler Huvudprog Objektkod Objektkod Objektkod Objektkod Länkare M a s k i n k o d
Enkelt program /* Programmet female */ main() { } int pnr, is_female; scanf( %d,&pnr); /* Pers.nr */ pnr=pnr/10; /* Heltalsdiv. */ /* check skriven i assembler */ is_female = check(pnr); if (is_female!= 0) printf( Kvinna\n ); else printf( Man\n ); Check ger värdet 1 i v0 om a0 är jämnt, annars 0. Följer anropskonventionerna..globl check.ent check check: andi t0, a0, 0x01 bne t0, zero, man nop addi v0, zero, 0x01 # 1 -> v0 beq zero, zero, endit nop man: xor v0, v0, v0 # Nollställ endit: jr ra.end check # En enkel subrutin!!!
Kan subrutiner se ut hur som helst? Vi kan ha mera komplexa situationer Subrutiner som anropar andra subrutiner kan ställa till problem. VARFÖR?? Vi tittar på ett exempel:
Subrutinen silly.globl silly.ent silly.globl sillier.ent sillier silly: addiu t0,zero,0 # Nollställ.. jal sillier # sparar #återhoppsadress i ra! nop sw v0,16(t0) jr ra.end silly sillier: addi t1, t1, 7 jr ra.end sillier OM EN RUTIN ANROPAR silly BLIR DET FEL - VARFÖR??? VAD BLIR ÅTERHOPPSADRESSEN NÄR VI HOPPAR TILLBAKA FRÅN silly?
Subrutinen silly - analys Subrutinen silly anropar en annan subrutin Genom jal-instruktionen i silly skrivs ra över Det är omöjligt att komma tillbaka till den rutin som anropade silly Register ra måste sparas undan någonstans i minnet. Men var?
Minnesorganisation Höga adresser Stack Stack. Växer nedåt Låga adresser Ledigt minne Dyn. data Statiska data Program Heap. Växer uppåt Variabler etc..data Programkod.text
Minnesorganisation (forts) Vi organiserar en del av minnet i form av en stack Stack är ett vedertaget begrepp Tekniken att använda en stack kan förklaras på många olika sätt
Stack (intro) Ex: Gruppuppgifter hamnar på assistentens bord Uppg 1 Uppg 3 Uppg 2 Uppg 2 Uppg 4 Så fort en ny uppgift dyker upp, hamnar den överst i högen. När den är färdigrättad plockas den bort och den översta rättas. Pilen visar den uppgift assistenten jobbar med vid varje tidpunkt!
Stacken (forts) På samma sätt kan en subrutin lägga upp data på stacken. När subrutinen (uppgiften) är klar tas data bort från stacken igen. För att krångla till det: Stacken i vår dator växer nedåt (varför?) Det som motsvarar pilen (håller reda på stackens topp) är ett register: stackpekaren (sp).
Stacken (forts) Vedertagna operationer för att hantera stacken: push: Lägger ett element på stackens topp och ändrar pekaren till detta element pop: Hämtar tillbaka ett element från stackens topp och sätter pekaren på elementet nämast under
Registeranvändning REG NAMN KOMMENTAR $0 zero alltid noll $1 at används av assemblern $2-$3 v0-v1 returvärden från funktioner $4-$7 a0-a3 argument till subrutiner $8-$15 t0-t7 temporära register $16-$23 s0-s7 får ej förändras av subrutiner $24-$25 t8-t9 temporära register $26-$27 k0-k1 används av operativsystem/avbrott $28 gp global datapekare $29 sp stackpekare $30 s8/fp ibland frame pointer $31 ra return address: återhoppsadress
Subrutiner och stacken Vilka element kan en subrutin vilja spara undan? ra: registret med återhoppsadressen (om rutinen anropar något annat) a0-a3: parametrarna till rutinen s0-s8: subrutinregistren ska sparas om de används lokala variabler plats för argument till anropad subrutin
Aktiveringsblock Register som ska sparas läggs på stacken Utrymme för lokala variabler och plats för argument allokeras på stacken Totala allokerade mängden data ska vara jämnt delbart med 8 (konvention) Det allokerade utrymmet kallas AKTIVERINGSBLOCK
Aktiveringsblock Sp Sp ra andra reg. lokala var argument
Subrutiner i C int stacktest(int x, int y) { int a,b; int level1(int x) { int a; } a=level1(x); b=level1(y); return a+b; /* Subrutinen stacktest anropar */ level1 som i sin tur anropar level2 a=level2(x+4); return a+4; } int level2(int x) { return x+1; }
Samma subrutiner i assembler BÖRJA MED level2: - Anropar inga subrutiner - Behöver inte spara register om bara t0-t9 används.globl level2.ent level2 level2: addi t0,a0,1 or v0,t0,zero jr ra.end level2 ENKELT... int level1(int x) { } int a; a=level2(x+4); return a+4; int level2(int x) { return x+1; }
Samma subrutiner i assembler FORTSÄTT MED level1: PROLOG - allokera aktiveringsblocket! - level1 anropar en subrutin - level2 - Vi måste spara undan register som kan förändras: ra, s0, s1, a0 - Allokera plats för en lokal variabel samt plats för argument till subrutin som vi kallar på. - Totalt 6*4=24 bytes. Jämnt delbart med 8 - OK.globl level1.ent level1 level1: subu sp,sp,24 # Plats på stacken sw ra,20(sp) # Återhoppsadressen sw s0,16(sp) # Reg s0 sw s1,12(sp) # Reg s1 sw a0,8(sp) # Argumentet # Lokal variabel på 4(sp) # Plats för argument a0 på 0(sp)
Samma subrutiner i assembler level1: SUBRUTINENS FUNKTION... addi a0,a0,4 # Räkna fram rätt argument jal level2 # Hoppa till subrutinen nop sw v0,4(sp) # Spara resultatet i lokal variabel lw s0,4(sp) # Hämta igen (lite dumt, men...) nop addi s1,s0,4 # Beräkna a+4 or v0,s1,zero # Flytta s1 till v0...
Samma subrutiner i assembler level1: EPILOG (ÅTERSTÄLL REGISTER OCH ÅTERLÄMNA AKTIVERINGSBLOCK)... # Och så är det dags att återställa.end lw a0,8(sp) lw s1,12(sp) lw s0,16(sp) lw ra,20(sp) addiu sp,sp,24 # Återställ stacken jr ra level1
Hur skriver man en subrutin? - Deklarera subrutinen - Allokera aktiveringsblocket - Spara register på stacken - Gör det som ska göras i rutinen - Återställ registren - Återlämna aktiveringsblock - Hoppa tillbaka
Assemblerdirektiv Vanligaste assemblerdirektiven:.globl : Gör symbolen känd globalt.ent : Start på subrutin.text : Text- (program) segment (instruktioner).data : Datasegment.end : Slut på subrutin
Assemblerdirektiv Assemblerdirektiv för data.word X : Deklarera ett 32 bits ord, värde X.byte Y : Samma fast byte.float Z : Deklarera flyttal.ascii sträng : Deklarera en sträng.space M : Allokera utrymme M bytes
Syntetiska instruktioner MIPS är en RISC-processor med begränsat antal instruktioner, tex add, andi osv. Vissa instruktioner är enkla att programmera med, men finns ej i instruktionsuppsättningen. Ex neg, not och b. Assemblern översätter dessa till kombinationer av existerande instruktioner
Syntetiska instruktioner (forts) Ex: neg rd, rs tar talet i rs och byter tecken, lägger i rd ersätts av sub rd, zero, rs b label (ovillkorligt hopp) ersätts av beq zero, zero, label not rd, rs inverterar bitarna i rs ersätts av nor rd, rs, zero
Rekursiv subrutin i assembler Antag att vi vill skriva en rekursiv subrutin i assembler. Ex: Rutin som summerar de första n udda talen (det 4:e udda talet är 7 = 2*4-1) Vi börjar med att skriva den i C
Rekursiv subrutin EN REKURSIV C-FUNKTION int myfun(int n) { int temp; if (n==0) { temp = 0; } else { temp = 2*n-1 + myfun(n-1); } return temp; } VI MÅSTE FÖLJA PROGRAMMERINGSKONVENTIONEN!
Rekursiv subrutin - Rutinen kommer att använda s0, s1 och måste spara ra, a0,s0,s1 - I aktiveringsblocket ska en lokal variabel (temp) och plats för argument till anropad funktion finnas.globl myfun.ent myfun # PROLOG myfun: subu sp,sp,24 # 6 hela ord reserveras sw ra,20(sp) # spara ra (returadress) på stacken sw s0, 16(sp) # spara s0 sw s1, 12(sp) # spara s1 sw a0, 8(sp) # argumentet # På 4(sp) finns temp och på 0(sp) # finns platsen för argument
Rekursiv subrutin... # Subrutinens egentliga funktion beq a0,zero,endit # Hoppa ut om argumentet är noll nop add s0, a0,a0 # 2*n i s0 addi s0,s0,-1 # 2*n-1 i s0 addi a0,a0,-1 # Minska argumentet med 1 jal myfun # Kallar på sig själv nop add s1,s0,v0 # 2*n-1 + returvärdet sw s1,4(sp) # Spara i lokal variabel beq zero,zero, ret # Hoppa ovillkorligt nop endit: sw zero,4(sp) # Lägg värdet noll i lokal variabel
Rekursiv subrutin # EPILOG - ÅTERSTÄLL REGISTER, ÅTERLÄMNA AKTIVERINGSBLOCK OCH HOPPA TILLBAKA ret: lw v0,4(sp) # Lägg lokala var i returregistret lw a0,8(sp) lw s1,12(sp) lw s0,16(sp) lw ra,20(sp) addu sp,sp,24 jr ra # Här börjar epilogen # Återlämna aktiveringsblock # Hoppa tillbaka.end myfun
Sammanfattning Vi har fått en försmak av hur det är att programmera subrutiner i assembler Assemblerprogrammering är svårt!!!! Det är lätt att göra fel. Programmeringskonventionen och stacken är viktiga begrepp!