Autor zadání | Desana Daxnerová |
---|---|
Úprava | Samuel Gorta |
Odevzdávané soubory |
cpu.h
cpu.c
|
Začátek odevzdávání | viz diskusní fórum |
Další odevzdání navíc do | 2025-04-09 24:00 |
Konec odevzdání | 2025-04-16 24:00 |
Vzorová implementace |
/home/kontr/pb071/hw03/cpu
|
Představení úkolu
Představme si jednoduchý procesor, který zpracovává aritmetické operace. Náš procesor má k dispozici paměť s instrukcemi, které zpracovává, a zásobník (stack), který se nachází na konci této paměti. Navíc může ukládat mezivýsledky do malé a rychlé paměti – registrů.
Emulátor procesoru postupně čte jednotlivé instrukce z paměti a vykonává je. Naším cílem bude napsat program, který načte soubor s instrukcemi a vytvoří takovýto emulátor, který je vykoná.
Odevzdávat budete pouze soubory cpu.h
a cpu.c
. V kostře naleznete
již naimplementovaný main.c
, který otevře vstupní soubor zadaný pomocí
argumentu programu a volá vámi dodané funkce. Předpokládaný vstupní soubor
s instrukcemi je binární, proto máte v kostře i zdrojový kód programu compiler.c
,
který můžete použít k převodu textových instrukcí (assembleru) do binární podoby.
Požadavky
NULL parametrPokud není explicitně uvedeno jinak, všechny funkce přebírající argument
ukazatelem předpokládají, že tyto ukazatele nejsou |
-
Číselné atributy ukládejte do proměnných typu
int32_t
, který najdete v hlavičkovém souborustdint.h
. -
Nevyužité atributy všech struktur udržujte na vhodně zvolené hodnotě, tj. čísle 0 nebo ukazateli
NULL
. -
Pro dvojici hlavičkového (
.h
) a implementačního (.c
) souboru platí, že pomocí.h
zveřejňujete funkce poskytované v.c
. Jinými slovy, hlavičkový soubor udává veřejné rozhraní implementačního souboru. V.c
samozřejmě můžete (a měli byste) mít jiné pomocné funkce, ty ale už do.h
nedoplňujte a označte je klíčovým slovemstatic
(viz příklad níže). -
Dodržujte požadované rozhraní hlavičkového souboru. Smí obsahovat jen struktury a funkce specifikované v zadání. Některé testy se kompilují s námi dodanými hlavičkovými soubory. Pokud byste tedy upravili hlavičky funkcí nebo přidali nějaké vlastní deklarace, kód se vůbec nemusí zkompilovat!
// Somewhere in a `.c` file...
static int helper_function(void);
...
static int helper_function(void)
{
return 42;
}
Typ struct cpu
Reprezentuje 32bitový procesor. V rámci tohoto úkolu bude implementován jako tzv. opaque struct, což znamená, že ve veřejném rozhraní (hlavičkovém souboru) se nachází pouze deklarace struktury. Její atributy jsou uvedeny až v implementačním souboru, a tak k nim nelze zvenčí přistupovat bez použití speciálních funkcí.
Náš procesor by měl obsahovat následující části:
-
celočíselné registry
A
,B
,C
,D
sloužící k ukládání mezivýpočtů, -
registr
status
typuenum cpu_status
popisující stav procesoru (viz sekce Stavové kódy), -
registr obsahující aktuální počet uložených hodnot v zásobníku,
-
registr obsahující index instrukce, která má být vykonaná v následujícím kroku výpočtu,
-
ukazatel na paměť emulovaného programu,
-
ukazatele do paměti emulovaného programu, které vymezují prostor, ve kterém se v paměti nachází zásobník.
Struktura paměti
Abychom odlišili kontext paměti programu a paměti emulátoru, budeme adresou
nazývat hodnotu proměnné typu ukazatel v jazyce C a indexem (který je číslo,
ne ukazatel) budeme nazývat adresu buněk v paměti emulovaného programu. Jedna
buňka emulované paměti má velikost jako |
Instrukce programu se nachází na začátku paměti. Zásobník se nachází na konci a plnit se bude směrem od konce paměti k začátku. Paměť mezi instrukcemi a zásobníkem (pokud nějaká existuje) musí být vynulovaná.
Indexem instrukce rozumíme počet buněk typu int32_t
, které se nachází před ní.
Například, pokud program tvoří dvě instrukce a první má jeden operand,
pak rozložení indexů bude:
-
index 0: první instrukce
-
index 1: operand první instrukce
-
index 2: druhá instrukce
Funkce na práci s procesorem
Úkol spočívá převážně v implementaci následujících funkcí. Jejich hlavičky se
musí nacházet v cpu.h
. Abyste strukturu hlavičkových souborů lépe chytili
do ruky, necháváme jejich doplnění na vás.
int32_t* cpu_create_memory(FILE *program, size_t stack_capacity, int32_t **stack_bottom);
Funkce přečte binární program pro procesor ze souboru program
(například pomocí
funkce fgetc(3)
z knihovny stdio.h
(dokumentace))
a uloží ho do bloku paměti P, kterou naalokuje. Soubor se do paměti zkopíruje až po EOF
.
P musí za instrukcemi obsahovat dostatek volného místa pro zásobník
velikosti stack_capacity
(počet int32_t
buněk, ne bajtů). Na adresu, na kterou
ukazuje stack_bottom
(tj. do *stack_bottom
) funkce uloží adresu posledního
prvku (typu int32_t
) v P, se kterým je ještě možné pracovat. Vrátí ukazatel
na začátek P, nebo NULL
při chybě. Za chybu se také považuje případ,
kdy počet bajtů ve vstupním souboru není násobkem velikosti typu int32_t
.
Velikost vstupu nemusí být dopředu známa, proto je třeba paměť dle potřeby zvětšovat. Aby nebyla alokace paměti zbytečně neefektivní, musí tato funkce alokovat paměť po blocích velikosti 4 KiB.
Soubor můžete přečíst jen jednou. Nepředpokládejte, že
program reprezentuje regulární soubor, tj. funkce ftell(3)
a fseek(3) nemusí na program dávat smysl.
|
struct cpu *cpu_create(int32_t *memory, int32_t *stack_bottom, size_t stack_capacity);
Funkce inicializuje strukturu cpu
. Parametr memory
ukazuje na paměť
emulovaného programu, který přečetla funkce cpu_create_memory()
.
Atribut stack_bottom
je parametr, který se získá voláním
cpu_create_memory(…, &stack_bottom)
. Je v pořádku, pokud není mezi instrukcemi
a zásobníkem volné místo.
Funkce vrací ukazatel na inicializovanou strukturu cpu
, který lze předat
jako parametr do následujících funkcí.
int32_t cpu_get_register(struct cpu *cpu, enum cpu_register reg);
Vrátí hodnotu registru (A
až D
).
Validitu parametru reg
ověřte pomocí makra assert
.
void cpu_set_register(struct cpu *cpu, enum cpu_register reg, int32_t value);
Nastaví registr (A
až D
) na hodnotu value
.
Validitu parametru reg
ověřte pomocí makra assert
.
enum cpu_status cpu_get_status(struct cpu *cpu);
Vrátí stavový kód procesoru.
int32_t cpu_get_stack_size(struct cpu *cpu);
Vrátí aktuální velikost zásobníku.
void cpu_destroy(struct cpu *cpu);
Uvolní zdroje procesoru a nastaví ukazatele ve struktuře na NULL
. Také vynuluje
všechny registry.
void cpu_reset(struct cpu *cpu);
Vynuluje všechny registry (včetně status
) a vyprázdní a vynuluje zásobník.
Nedealokuje žádnou paměť.
Pro cpu_step() a cpu_run() platí, že dokud je stavový kód procesoru
jiný, než CPU_OK , vrátí 0 a nedělají nic jiného.
|
int cpu_step(struct cpu *cpu);
Vykoná jednu instrukci procesoru. Pokud je úspěšná, vrátí nenulový kód, jinak vrátí 0.
long long cpu_run(struct cpu *cpu, size_t steps);
Vykoná steps
instrukcí. Vrátí -K, pokud se procesor dostal vykonáním K kroků
do chybového stavu, jinak vrátí skutečný počet vykonaných instrukcí.
Ten může být menší než steps
, pokud došlo k vykonání instrukce halt
.
Například pro následující program:
movr C 42 ; Make loop jump. loop -112 ; Jump to an invalid address.
Funkce cpu_run
nejdřív úspěšně vykoná movr
, potom úspěšně vykoná
loop
, následně se při vykonávání následující (neexistující, ale to
nevadí) "instrukce" stane chyba kvůli záporné hodnotě
instruction_pointer
– dohromady 3 kroky. Funkce tedy vrátí -3.
Instrukce
Instrukce jsou v binárním souboru, stejně jako v paměti programu, reprezentované
jako 32bitové čísla. Můžou mít (32bitové) parametry typu REG
(číslo
registu 0 (A
) až 3 (D
)), INDEX
(index intrukce) a NUM
(celé číslo).
Endianita instrukcí a operandů je little-endian.
Stavové kódy
Instrukce můžou v jistých případech nastavovat stavový kód. V kostře se nachází
enum cpu_status
s následujícími hodnotami (v pořadí):
-
CPU_OK
, -
CPU_HALTED
, -
CPU_ILLEGAL_INSTRUCTION
, -
CPU_ILLEGAL_OPERAND
, -
CPU_INVALID_ADDRESS
, -
CPU_INVALID_STACK_OPERATION
, -
CPU_DIV_BY_ZERO
, -
CPU_IO_ERROR
.
Kromě CPU_OK
a CPU_HALTED
jsou všechny ostatní stavy chybové.
Pro vykonání všech instrukcí platí:
-
Pokud je stavový kód emulátoru jiný než
CPU_OK
, instrukce se nevykoná. -
Pokud je kód instrukce neznámý, nastaví se kód
CPU_ILLEGAL_INSTRUCTION
. -
Pokud je registr instrukce neznámý, nastaví se kód
CPU_ILLEGAL_OPERAND
. -
Pokud je index instrukce v registru mimo paměť emulovaného programu, nebo ukazuje do zásobníku, nastaví se kód
CPU_INVALID_ADDRESS
. -
Pokud během vykonávání instrukce nastane chyba, index instrukce v registru se nemění.
Počáteční status procesoru je ve funkci cpu_create
inicializován na CPU_OK
.
Seznam instrukcí
Číselné hodnoty instrukcí jsou definované pořadím v tomto seznamu.
-
nop
: Nedělá nic. -
halt
: Zastaví vykonávání programu a nastaví stav procesoru naCPU_HALTED
. Funkcecpu_step()
po jejím vykonání vrátí 0. -
add REG
: Připočítá k registruA
hodnotu registruREG
. -
sub REG
: Odečte z registruA
hodnotu registruREG
. -
mul REG
: Vynásobí registrA
hodnotou registruREG
. -
div REG
: Vydělí registrA
hodnotou registruREG
. Pokud je jeho hodnota 0, instrukci nevykoná a nastaví stavový kód naCPU_DIV_BY_ZERO
. -
inc REG
: Inkrementuje registrREG
. -
dec REG
: Dekrementuje registrREG
. -
loop INDEX
: Pokud je registrC
nenulový, skočí na instrukci s indexemINDEX
, jinak neudělá nic. -
movr REG NUM
: Uloží do registruREG
čísloNUM
. -
load REG NUM
: Uloží do registruREG
číslo ze zásobníku, které se v něm nachází na indexuD
+NUM
od konce. Tedy pokud jsou registrD
iNUM
rovny nule, uloží se do registru hodnota na vrcholu zásobníku (tj. poslední vložená). Aktuální velikost zásobníku zůstává nezměněná. V případě, že jeD
+NUM
index mimo zaplněnou část zásobníku, operace se nevykoná a nastaví se stavový kódCPU_INVALID_STACK_OPERATION
. -
store REG NUM
: Funguje podobně jakoload
, ale hodnotu na zásobník ukládá, tedy hodnotu z registruREG
vloží na indexD
+NUM
od konce. Aktuální velikost zásobníku zůstává nezměněná. V případě neplatného indexu se nastaví stavový kódCPU_INVALID_STACK_OPERATION
stejně jako uload
. -
in REG
: Přečte ze vstupu 32bitové číslo (sekvenci číslic v desítkové soustavě) a uloží ho do registruREG
. Pokud byty (znaky) na vstupu nereprezentují číslo, instrukce nic neudělá a nastaví stav procesoru naCPU_IO_ERROR
. Pokud na vstupu už žádná čísla nejsou (EOF
), nastaví registrC
na 0 a doREG
uloží hodnotu -1 (a to i v případě, žeREG
jeC
). Instrukce ignoruje bílé znaky, kterými čísla na vstupu mohou být oddělená. -
get REG
: Přečte ze vstupu jeden znak (byte) a uloží jej do registruREG
. V případě, že na vstupu už žádné byty nejsou (EOF
), chová se instrukce jakoin
. -
out REG
: Vypíše hodnotu registruREG
jako číslo na standartní výstup. Výstup této instrukce je pouze sekvence znaků'0'
–'9'
(a případně'-'
u záporných hodnot). -
put REG
: Pokud je hodnota registruREG
v rozsahu 0 – 255, vypíše tuto hodnotu jako právě jeden znak na standartní výstup. Jinak neudělá nic a nastaví stavový kód naCPU_ILLEGAL_OPERAND
. -
swap REG REG
: Vymění hodnoty registrů. -
push REG
: Přidá hodnotu registruREG
na zásobník, pokud není plný. Jinak neudělá nic a nastaví stavový kódCPU_INVALID_STACK_OPERATION
. Instrukce upravuje aktuální velikost zásobníku. -
pop REG
: Pokud je na zásobníku alespoň jeden prvek, odebere jej a jeho hodnotu uloží do registruREG
. Jinak neudělá nic a nastaví stavový kódCPU_INVALID_STACK_OPERATION
. Instrukce upravuje aktuální velikost zásobníku.
Instrukce in a get v případě EOF vynulují registr C , aby při čtení
vstupu v cyklu (instrukcí loop ) způsobilo EOF ukončení cyklu.
|
Assembler
Programy pro procesor můžete napsat v textové podobě – assembleru, a ten si
následně zkompilovat pomocí kompilátoru v souboru compiler.c
z kostry
nebo pomocí referenční implementace.
|
Assembler může obsahovat kromě instrukcí i deklaraci návěstí (alfanumerický ASCII
identifikátor začínající písmenem a končící dvojtečkou, např. here:
).
Instrukce, které berou argument typu INDEX
, můžou použít místo číselné
konstanty tyto návěstí.
Na každém řádku se může nacházet nejvíce jedna instrukce, která je od svých
operandů oddělená právě jednou mezerou. Před a za ní může být libovolný počet
mezer. Komentáře začínají znakem ;
.
Do kostry jsme vám do adresáře assembler_inputs
přiložili pár
programů, kterými se můžete inspirovat při psaní vlastních.
Příklad
Assembler:
; simple homework assembly excercise
dec 1 ; Decrement register B, same as `dec B`.
loop here ; Same as loop 6.
push 0
here:
halt
Binární soubor (hodnota v prvním sloupci – index se v souboru nenachází, slouží jen pro názornější ukázku):
index 1 2 3 4 význam 0 07 00 00 00 ( 7) dec 1 01 00 00 00 (operand registr B) 2 08 00 00 00 ( 8) loop 3 06 00 00 00 ( 6) (operand index 6) 4 11 00 00 00 (17) push 5 00 00 00 00 ( 0) (operand registr A) 6 01 00 00 00 ( 1) halt
Rozhraní v příkazovém řádku
Tato část je implementovaná v kostře.
Program akceptuje dva až tři argumenty: první je run
nebo trace
, druhý,
nepovinný, je stack_capacity
, poslední je cesta k souboru s instrukcemi.
run
vykoná všechny instrukce a vypíše stav CPU, trace
vypíše stav po každé
instrukci a počká na Enter před vykonáním další.
Bonusové rozšíření
Instrukce skoků [5 bodů]
Kód související s touto rozšířenou instrukční sadou obalte příkazy preprocesoru
(každý na samostatném řádku). Pro účely testování přidejte k přepínačům překladače
(pro CMake obsah proměnné
|
Rozšiřte struct cpu
o registr result
, který bude obsahovat výsledek poslední
aritmetické operace (add
, sub
, mul
, div
, inc
, dec
) nebo operace cmp
.
Rozšiřte všechny relevantní funkce o registr result
. Tento registr bude
jako operand dostupný pod indexem 4.
Instrukční sadu rozšiřte o následující instrukce:
-
cmp REG_X REG_Y
: Do registruresult
zapíšeREG_X
-REG_Y
. Hodnoty registrůREG_X
aniREG_Y
se nezmění. -
jmp INDEX
: Skočí naINDEX
. -
jz INDEX
: Skočí naINDEX
, pokud seresult
rovná nule. -
jnz INDEX
: Skočí naINDEX
, pokudresult
není nula. -
jgt INDEX
: Skočí naINDEX
, pokud jeresult
striktně větší než nula.
Do registru result
zároveň uloží svůj výsledek všechny aritmetické operace
(např. add
do result
zkopíruje konečnou hodnotu A
, inc
do result
zkopíruje hodnotu operandu po jeho inkrementaci apod.). Ostatní instrukce
(např. load
, swap
, in
) můžou tento registr pouze číst. Při pokusu
o zápis procesor nastaví stavový kód CPU_ILLEGAL_OPERAND
.
Procedury [5 bodů]
Tento bonus povolte makrem
|
-
call INDEX
: Pokud je na zásobníku dost místa, uloží na něm index následující instrukce a skočí naINDEX
. Jinak nastaví stavový kód naCPU_INVALID_STACK_OPERATION
. -
ret
: Vybere z vrcholu zásobníku index instrukce (kterou, pokud byl program korektně napsaný, tam vložila instrukcecall
) a skočí na ni. Pokud je zásobník prázdný, nastaví stavový kódCPU_INVALID_STACK_OPERATION
.
Pokud se rozhodnete implementovat jen druhý bonus, ujistěte se, že má
instrukce call ve vašem řešení stejný kód, jaký by měla, pokud by existovaly
i instrukce cmp a jgt !
|
Poznámky
-
Program kompilujte příkazem
gcc -o hw03 main.c cpu.c -std=c99 -Wall -Wextra -Werror -pedantic
-
Vzorové řešení můžete spustit na aise:
/home/kontr/pb071/hw03/compiler /home/kontr/pb071/hw03/cpu /home/kontr/pb071/hw03/cpu-bonus
-
POZOR! Vzorové řešení vyžaduje argument!
-
Nezapomínejte na vhodnou dekompozici. Vyrobte si pomocné funkce, kde třeba.
Během psaní řešení vás to možná bude svádět k vytvoření obrovského switch .
Ten nepotěší vaše opravující a práce s ním bude zbytečně pracná. Zkuste
využít znalosti z přednášek a než začnete psát, zamyslete se nad správným
návrhem vašeho řešení.
|