Mikroarkitektur Niels Olof Bouvin Institut for Datalogi Aarhus Universitet 1
Level 1: Mikrokode niveauet Level 5 Problem-oriented language level Translation (compiler) Level 4 Assembly language level Translation (assembler) Level 3 Operating system machine level Partial interpretation (operating system) Level 2 Instruction set architecture level Interpretation (microprogram) or direct execution Level 1 Micro-architecture level Hardware Level 0 Digital logic level 2
Oversigt Mic-1 s mikroarkitektur Introduktion til IJVM 3
Maskinniveauet Central Processing Unit (CPU) Control Unit Den generelle von Neumann maskine Arithmetic Logic Unit (ALU) Registers Input/Output devices Moderne CPUer ser alle ud som denne Vort eksempel er Mic-1 Main Memory Disk Printer simplere end (langt) de fleste CPUer, men stadig en god illustration Bus I dag skal vi se på Mic-1 s arkitektur og IJVM næste gang ser vi på, hvorledes IJVM kan realiseres ved hjælp af mikroprogrammering på Mic-1 4
IJVM instruktionerne og arbejdet bag dem Afvikles i en uendelig løkke bestående af fetch / decode / execute Mere specifikt: 1. Fetch the next instruction from memory into the instruction register 2. Change the program counter to point to the following instruction 3. Decode the type of instruction just fetched 4. If the instruction uses a word in memory, determine where it is 5. Fetch the word, if needed, into a CPU register 6. Execute the instruction 7. Go to step 1 to begin executing the next instruction 5
Execute? A + B A Registers Involverer som regel den aritmetisk logiske enhed (ALU en) Operander må typisk hentes ind fra lageret før ALU en kan operere på dem i forbindelse med udførsel af instruktionen Central Processing Unit (CPU) Control Unit Arithmetic Logic Unit (ALU) Registers Main Memory B A ALU A + B Input/Output devices Disk Printer B ALU Input registers ALU input bus ALU output register Bus 6
Registre Kan lagre et antal bits afhængig af registerbredden (typisk 8-64 bit) Central Processing Unit (CPU) Control Unit Kontrolinformation program counter instruktionsregister stack pointer stack frame pointer Arithmetic Logic Unit (ALU) Registers Main Memory Input/Output devices Disk Printer Operander (data) Bus Ikke alle registre er tilgængelige for ISA-programmøren ingen er tilgængelig via IJVM 7
Hukommelse En byte er 8 bit Et ord (word) afhænger som regel af maskinens registerbredde (oftest 4 eller 8 bytes) Hukommelse bruges til programmet (i form af absolut maskinkode) og data (operander) Organiseret som en sekvens af celler (angivet med adresse 0,,N-1), som gemmer et vist antal bits afhængig af den pågældende maskinarkitektur 8
Datadelen af Mic-1: Registre og busser MAR Memory Address Register. Adressen på et word i lageret (32-bit) MDR Memory Data Register. Indeholder word udpeget af MAR (32-bit) PC Program Counter. Adressen på næste instruktion (32-bit) MBR Memory Buffer Register. Til bytes fra method area (8- eller 32-bit) TOS Top of Stack. Kopi af det øverste element på stakken OPC Opcode Register. Temporær register. H A B C input register til ALU. Input fra C bus fra H til ALU bus fra registrene til ALU bus fra ALU til registrene 9
Kontrol af Mic-1 s Aritmetiske Logiske Enhed ALUen styres med 6 input bits Disse kan kombineres efter behag her er de typiske kombinationer (F 0 og F 1 vælger funktionen) N Z 1, hvis output fra ALU er negativt 1, hvis output fra ALU er 0 Shifteren kan to ting SLL8 (Shift Left Logical 8): Skubber bits én byte til venstre (fylder ud med nuller) SRA1 (Shift Right Arithmetic 1): Skubber bits (med fortegnsforlængelse) 1 bit til højre (ækvivalent til heltalsdivision med 2) 10
Timing is everything Hvad sker der i løbet af et taktslag (clock cycle)? Elektriske signaler tager tid Digital logik tager tid, før det stabiliserer sig w, x, y, z angiver tidsrum til de enkelte undertrin A 11
Timing af PC=PC+1 1 1 110101 00 A 12
Timing af PC=PC+1 1 1 110101 00 A 13
Timing af PC=PC+1 1 1 110101 00 A 14
Timing af PC=PC+1 1 1 110101 00 A 15
Timing af PC=PC+1 1 1 110101 00 A 16
Mikroarkitekturen i al sin gru og skønhed dette diagram kan realiseres i hardware Kontrolenheden styrer fetch/ decode/execute cyklen Control store MPC indeholder mikroprogrammet PC for mikroprogrammet MIR den aktuelle mikroinstruktion Registre Beregningsenhed Mic-1 A bus Kontrolenhed 17
Hukommelsestilgang fra Mic-1 Stack Constant Pool Method Area }4.294.967.296 bytes 1.073.741.824 words PC/MBR bruges til at tilgå Method Area med byteadresser MAR/MDR bruges til at tilgå Constant Pool og Stak med wordadresser Mic-1 s hukommelse tilgås faktisk med byteadresser derfor er det nødvendigt at shifte MAR 2 bit til venstre (dvs.gange med 4) Cykel k Cykel k+1 Cykel k+2 MAR/PC sættes MBR/MDR kan læses Hukommelsen & bussen arbejder Timing: Hvis PC/MAR sættes i cycle k, kan MBR/MDR læses i cycle k+2 18
Fra MBR til B-bus: Unsigned og signed MBR har to kontrolsignaler, fordi den 8-bits værdi, som den kan indeholde kan sendes ud på B-bussen (der er 32-bit bred) på to måder: Unsigned praktisk, når byteværdien f.eks. skal sættes sammen med en anden unsigned byte til en 16-bits værdi, eller bruges til opslag i et indeks. Eksempel: iload 1 de første 24 bits sættes til 0 Signed bruges til værdier mellem -128 og 127. Eksempel: bipush 42 de første 24 bits sættes til kopier af den mest betydende bit af de 8 bits (altså 00000000 00000000 00000000 eller 11111111 11111111 11111111). Dette kaldes sign extension 19
H registret: Venstre input til ALU Der er kun to busser forbundet til registrene, B og C til input til hhv. output fra ALU og shifter Mic-1 er et basalt design, så der er ingen tredje bus, som registrene kunne sende input ud på A bus Men en ALU har behov for to input til mange operationer! Registret H tjener som fast inputsregister til ALUen den kan naturligvis slås fra, om nødvendigt (ENA sættes til 0) Det betyder, at det kan være nødvendigt at sende input over B bussen igennem ALUen over C bussen til H, for dernæst at foretage den operation med ALUen, som man ønsker sig ikke optimalt, men simpelt. Mic-2 (beskrevet i Chapter 4.4) har tre busser 20
Mikroinstruktionsformatet CPUen kontrolleres ved at sætte de rigtige kontrolsignaler Kontrolinstruktionerne til CPUen (mikroprogrammet) ligger i Control Store Formatet afhænger af hvor mange signaler, der skal til for at kontrollere CPUen. På Mic-1 bliver det 24 kontrol bits, 9 adresse bits og 3 JAM bits vi kan skrive til flere registre simultant på C-bussen (hvis vi vil), men vi kan kún kopiere ét register til B-bussen af gangen 21
NEXT_ADDRESS JAM ALU Mikroinstruktionsformatet En mikroinstruktion indeholder altid adressen på næste instruktion (512 words 9 bit er nok) bruges til at modificere NEXT_ADDRESS, f.eks. ved branching og goto ALU og shifter håndteres her under ét C-bus vs. B-bus adressering det kan være nødvendigt at udpege flere registre som destination, derfor en bit/et signal per register der skal kun bruges ét register som input (foruden H) til ALU, så 9 registre kan nemt enkodes i 4 bit Memory fetch bruges til PC/MBR; read/write bruges til MDR/MAR B-bus registre 0 = MBR 5 = LV 1 = PC 6 = CPP 2 = MBR 7 = TOS 3 = MBRU 8 = OPC 4 = SP 9-15 er ubrugte Micro address NEXT_ADDRESS 9 JAM ALU C-bus Memory JMPC JAMN JAMZ SLL8 SRA1 F₀ F₁ ENA ENB INVA INC H OPC TOS CPP LV SP PC MDR MAR WRITE READ FETCH 3 8 9 3 B-bus B-bus 4 22
PC=PC+1 som mikroinstruktion 1 1????????? 0 0 0 0 0 1 1 0 1 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 B-bus registre 0 = MBR 5 = LV 1 = PC 6 = CPP 2 = MBR 7 = TOS 3 = MBRU 8 = OPC 4 = SP 9-15 er ubrugte 110101 00 Mikroinstruktioner leverer signaler til styre beregningsenheden, samt registrenes adgang til busserne og hukommelsen Mikroinstruktionerne hentes fra kontrollageret, der er helt adskilt fra det sædvanlige lager Den næste mikroinstruktion står enten i NEXT_ADDRESS eller udvælges 23
Udvælgelse af næste mikroinstruktion Ofte kan man blot vælge instruktionen angivet i NEXT_ADDRESS den sædvanlige udførsel af mikroinstruktioner bemærk, at mikroinstruktionerne ikke nødvendigvis ligger sekventielt Men hvordan håndteres goto og branches? 24
Input til udvælgelse af næste mikroinstruktion N / Z resultatet fra ALU er negativt eller nul MBR JAMN / JAMZ angiver om N/Z skal bruges ved udvælgelse af næste mikroinstruktion Addr MBR 9 bit fra den aktuelle mikroinstruktion i MIR 8 bit fra MBR registret JMPC angiver om MBR skal bruges ved udvælgelse af næste mikroinstruktion 25
Beregning af næste mikroinstruktion: JAMZ og JAMN Hvis JAM er 000, så brug NEXT_ADDRESS overført til MPC MBR Hvis JAMN eller JAMZ er 1, sættes den højeste bit i MPC efter denne formel MPC[8] = (JAMZ AND Z) OR (JAMN AND N) OR NEXT_ADDRESS[8] Dvs., at hvis der er et negativ (eller nul) resultat fra ALU (og hvis vi bruger det), så lægges 100000000₂ (256 10 el. 100 16 ) til NEXT_ADDRESS der skal altså hoppes 100₁₆ (2⁸) mikroinstruktioner frem i mikroprogrammet: 26
Beregning af næste mikroinstruktion: JMPC Hvis JAM er 000, så brug NEXT_ADDRESS overført til MPC MBR Hvis JMPC er 1, sættes MPC til MPC = MBR OR NEXT_ADDRESS her vil NEXT_ADDRESS enten være 000₁₆ eller 100₁₆ Dvs., at MPC derefter er 0MBR eller 1MBR, alt efter om den højeste bit i NEXT_ADDRESS var sat Dette er superpraktisk, for det betyder, at vi kan hoppe til (mikro)adresser defineret ved opcodes dette er en virkelig elegant løsning på decoding af instruktioner 27
Mic-1 En simpel mikroarkitektur men med alt hvad man behøver Mic-1 er blot selve arkitekturen, ikke et instruktionssæt (ISA) Men man kan bygge et instruktionssæt ovenpå Mic-1 28
Oversigt Mic-1 s mikroarkitektur Introduktion til IJVM 29
Level 2: Maskinniveau (ISA) Level 5 Problem-oriented language level Translation (compiler) Level 4 Assembly language level Translation (assembler) Level 3 Operating system machine level Partial interpretation (operating system) Level 2 Instruction set architecture level Interpretation (microprogram) or direct execution Level 1 Micro-architecture level Hardware Level 0 Digital logic level 30
Maskinniveauet! Machine Language: You try to shoot yourself in the foot only to discover you must first reinvent the gun, gunpowder, the bullet, and your foot. 31
Maskinkodeinstruktionsformat og -repræsentation Højniveausprog k = 6-i-j; Symbolsk maskinkode bipush 6 iload 1 isub iload 2 isub istore 3 Absolut maskinkode 0x10 0x06 0x15 0x01 0x64 0x15 0x02 0x64 0x36 0x03 Den absolutte maskinkode er her opskrevet i heksadecimal for letlæselighed, men ligger som binære tal i hukommelsen 32
Hvorfor skrive maskinkode? Alle andre sprog er abstraktioner, der skjuler egenskaber hos CPUen oftest med god grund men ikke altid! Hvis man har behov for at skrive en compiler (det kommer I til senere i dovs) tale med hardware på laveste niveau (f.eks. skrive en devicedriver) få absolut maksimal performance (i tidskritiske applikationer) have den mindst mulige kode (på meget små enheder) så er maskinkode sandsynligvis et godt valg til nogle dele af ens program Hvis man har behov for at forstå, hvordan computere faktisk fungerer så er maskinekode ikke til at komme udenom 33
IJVM s ISA En kraftigt reduceret udgave af Java Virtual Machine Kun simple integeroperationer Ingen objektorientering Ingen synlige registre alt foregår på stakken 34
Register CPUer Langt den mest udbredte løsning Data og mellemregninger gemmes i registre lokalt på CPUen meget, meget hurtigt (registrene sidder direkte sammen med ALU etc) begrænset plads der kan kun være relativt få registre man bliver derfor nødt til jonglere en del med de registre, der er til rådighed instruktioner kan blive komplicerede i adressering ( register-register, register-hukommelse, ) historisk betingede omstændigheder kan begrænse antallet af registre (f.eks. x86-familien) eller reservere særlige registre til særlige formål (igen er x86-familien et godt eksempel) Når udregningerne er udført (eller vi har behov for registrene til andre, vigtigere formål), gemmes data i hukommelsen og det tager lang tid, ligesom det tager lang tid at hente data fra hukommelsen 35
Stak-baserede CPUer I stedet for at bruge registre, gemmes data og mellemregninger på en stak naturligvis er der registre, men de er skjulte for ISA-niveauet og opefter Fleksibelt: Stakken kan blive større og mindre efter behov Stakken kan være langt større end et begrænset antal registre En elegant løsning, der er let at programmere til intet behov for at jonglere med registre instruktioner kan være særdeles uniforme Men: Hvis stakken ligger i den almindelige hukommelse, så bliver der langt fra CPU til data hvilket er årsagen til, at det nu kun sjældent implementeres i hardware 36
Regning på en stak: Omvendt Polsk Notation RPN (Reversed Polish Notation) efter Jan Łukasiewicz Eliminerer behovet for parenteser og precedensregler Infix RPN A + B A B + A + B C B C A + A B + C D A B C D + A (B + C) D B??????????????????????????? C + A D ((A + B) C + D)/(E + F + G) A??????????????????????????? B + C D + E F + G + / Findes i programmeringssprog som PostScript og Forth, samt lommeregnere fra Hewlett-Packard 37
JVM ordreformat IJVM bruger en delmængde heraf 38
IJVM ISA: Stakoperationer bipush A ( -- A) Putter A (der er en signed byte) på stakken dup (A -- A, A) Putter det øverste element på stakken en gang til pop (A -- ) Fjerner det øverste element på stakken swap (A, B -- B, A) Bytter om på de to øverste elementer på stakken 39
IJVM ISA: Beregninger iadd (A, B -- A+B) Fjerner de to øverste ord på stakken, og putter summen af dem på stakken isub (A, B -- A-B) Fjerner de to øverste ord på stakken, og putter differencen mellem dem på stakken iand (A, B -- A&&B) Fjerner de to øverste ord på stakken, og putter logisk AND af dem på stakken ior (A, B -- A B) Fjerner de to øverste ord på stakken, og putter logisk OR af dem på stakken 40
IJVM ISA: Sammenligninger og branches goto offset Lægger offset (16-bit, signed) til PC ifeq offset (A -- ) Fjerner det øverste ord på stakken, og hvis det er nul lægger offset til PC iflt offset (A -- ) Fjerner det øverste ord på stakken, og hvis det er mindre end nul lægger offset til PC if_icmpeq offset (A, B -- ) Fjerner de to øverste ord på stakken, og hvis de er ens lægger offset til PC 41
Et simpelt program add.j.method main bipush 7 bipush 6 iadd ireturn ijvm-asm add.j add.bc ijvm add.bc add.bc main index: 0 method area: 10 bytes 00 01 00 00 10 07 10 06 60 ac constant pool: 1 words 00000000 IJVM Trace of add.bc stack = 0, 1, 5 bipush 7 [10 07] stack = 7, 0, 1, 5 bipush 6 [10 06] stack = 6, 7, 0, 1, 5 iadd [60] stack = 13, 0, 1, 5 ireturn [ac] stack = 13 return value: 13 42
Method area? Stack? Constant pool? IJVMs hukommelse (4 GB (2 32 bytes)) opdelt i tre områder: Constant pool, SP stack og metode area. Disse bestyres af fire 32-bit registre: Program Counter (PC) Udpeger aktuel instruktion (byte) i method area Constant Pool Pointer (CPP) Udpeger bunden (word) af constant pool i lageret Stack Pointer (SP) Udpeger toppen (word) af stakken Local Variable Frame Pointer (LV) Udpeger bunden (word) af det aktuelle stakafsnit Constant Pool CPP Current Operand Stack 3 Local Variable Frame 3 Local Variable Frame 2 Local Variable Frame 1 LV Method Area PC 32 bit 32 bit 8 bit 43
Stakken Bruges altså til beregninger og mellemresultater Samt til udførsel af metodekald 44
Metodekald Hvert kald har behov at få et nyt sæt argumenter Hvert kald har behov for sine egne lokale variabler Når kaldet afsluttes, skal verden genoprettes, så vi kommer tilbage til det rigtige sted i koden, og så vi har de rigtige argumenter/variabler klar for det pågældende kald Så hvordan gør vi dét? /* find_min.c */ #include <stdio.h> #include <stdlib.h> int min(int a, int b) { int r; } if (a >= b) { r = b; } else { r = a; } return r; int main(int argc, char *argv[]) { int a = atoi(argv[1]); // første tal på cmdlinjen int b = atoi(argv[2]); // andet tal på cmdlinjen printf("%d\n", min(a, b)); // } 45
IJVM ISA: Metodekald og lokale variabler invokevirtual disp kalder en metode disp angivet med et 2 bytes (16 bit) indeks i Constant Pool. Etablerer en ny stack frame med ny LV og SP, og gemmer de gamle værdier af LV, SP og PC så de kan reetableres senere ireturn returnerer fra et metodekald med det øverste word på stakken som resultat. Nedlægger den nuværende stack frame og genopretter den foregående, herunder de gamle PC, LV og SP registre iload n kopierer det n te (n er en byte, talt fra 1) word fra Local Variable Frame og putter det øverst på stakken istore n kopierer det øverste element fra stakken og gemmer det som det n te (n er en byte, talt fra 1) word i Local Variable Frame 46
invokevirtual De gamle værdier gemt Arbitrær værdi (place holder) callee's Med denne pointer kan verden genskabes (a) Inden kald til invokevirtual, efter parametre er puttet på stakken (b) Efter invokevirtual er udført 47
ireturn Callee's (a) Inden kald til ireturn (b) Efter ireturn er udført 48
find_min.j.method main // int main.args 3 // ( int a, int b ).define a = 1.define b = 2 // { bipush 88 // Push some object reference iload a iload b /* find_min.c */ #include <stdio.h> #include <stdlib.h> int min(int a, int b) { int r; } if (a >= b) { r = b; } else { r = a; } return r; invokevirtual min ireturn int main(int argc, char *argv[]) { int a = atoi(argv[1]); // fã rste tal pã cmdlinjen int b = atoi(argv[2]); // andet tal pã cmdlinjen printf("%d\n", min(a, b)); // } // return min (a,b); // }.method min // int min.args 3 // ( int a, int b ){.define a = 1.define b = 2.locals 1 // int r;.define r = 3 iload a // if ( a >= b ) iload b isub // stack = a - b,... ; a - b < 0 => a < b iflt else iload b // r = b; istore r goto end_if else: // else iload a // r = a; istore r end_if: iload r // return r; ireturn // } 49
find_min.bc main index: 0 method area: 40 bytes 00 03 00 00 10 58 15 01 15 02 b6 00 01 ac 00 03 00 01 15 01 15 02 64 9b 00 0a 15 02 36 03 a7 00 07 15 01 36 03 15 03 ac constant pool: 2 words 00000000 0000000e Genereret med kommandoen ijvm-asm find_min.j find_min.bc 50
Antal argumenter for main Antal argumenter for min Start adresse for main, udpeget af main index: 0 find_min.bc main index: 0 method area: 40 bytes 0 00 03 2 00 00 4 10 58 bipush 88 (88d = 0x58) 6 15 01 iload 1 8 15 02 iload 2 10 b6 00 01 invokevirtual 1 13 ac ireturn 14 00 03 16 00 01 18 15 01 iload 1 20 15 02 iload 2 22 64 isub 23 9b 00 0a iflt 10 (10d = 0x0a) 26 15 02 iload 2 28 36 03 istore 3 30 a7 00 07 goto 7 33 15 01 iload 1 35 36 03 istore 3 37 15 03 iload 3 39 ac ireturn constant pool: 2 words 00000000 0000000e Antal lokale variabler for main Antal lokale variabler for min Start adresse for min (14d = 0x0e) 51
Kørsel af ijvm find_min.bc 9 8 IJVM Trace of find_min.bc stack = 0, 1, 8, 9, 15 bipush 88 [10 58] stack = 88, 0, 1, 8, 9, 15 iload 1 [15 01] stack = 9, 88, 0, 1, 8, 9, 15 iload 2 [15 02] stack = 8, 9, 88, 0, 1, 8, 9, 15 invokevirtual 1 [b6 00 01] stack = 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 iload 1 [15 01] stack = 9, 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 iload 2 [15 02] stack = 8, 9, 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 isub [64] stack = 1, 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 iflt 10 [9b 00 0a] stack = 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 iload 2 [15 02] stack = 8, 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 istore 3 [36 03] stack = 12, 13, 8, 8, 9, 21, 0, 1, 8, 9, 15 goto 7 [a7 00 07] stack = 12, 13, 8, 8, 9, 21, 0, 1, 8, 9, 15 iload 3 [15 03] stack = 8, 12, 13, 8, 8, 9, 21, 0, 1, 8, 9, 15 ireturn [ac] stack = 8, 0, 1, 8, 9, 15 ireturn [ac] stack = 8 return value: 8 Decimal Heksadecimal Decimal (Sådan er det bare) 52
Kørsel af ijvm find_min.bc 9 8 IJVM Trace of find_min.bc Argumenter for main stack = 0, 1, 8, 9, 15 4 bipush 88 [10 58] stack = 88, 0, 1, 8, 9, 15 6 iload 1 [15 01] stack = 9, 88, 0, 1, 8, 9, 15 8 iload 2 [15 02] stack = 8, 9, 88, 0, 1, 8, 9, 15 10 invokevirtual 1 [b6 00 01] stack = 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 18 iload 1 [15 01] stack = 9, 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 20 iload 2 [15 02] stack = 8, 9, 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 22 isub [64] stack = 1, 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 23 iflt 10 [9b 00 0a] stack = 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 26 iload 2 [15 02] stack = 8, 12, 13, 0, 8, 9, 21, 0, 1, 8, 9, 15 28 istore 3 [36 03] stack = 12, 13, 8, 8, 9, 21, 0, 1, 8, 9, 15 30 goto 7 [a7 00 07] stack = 12, 13, 8, 8, 9, 21, 0, 1, 8, 9, 15 37 iload 3 [15 03] stack = 8, 12, 13, 8, 8, 9, 21, 0, 1, 8, 9, 15 39 ireturn [ac] stack = 8, 0, 1, 8, 9, 15 13 ireturn [ac] stack = 8 return value: 8 OBJREF Overskrives straks med link pointer Argumenter til min Lokal variabel for min Link pointer overskrives med return værdien 53
Opsummering Mic-1: Mikroarkitekturen Registre, busser, ALU Mikroinstruktioner IJVM maskinen Registre: PC, SP, LV, CPP Lagermodel: method area, constant pool, stakken Metodekald og stakafsnit, parametre og lokale variabler IJVM simulatoren Bytecode kode Kørsel med stack trace 54
Ugeopgave 2 55