Virtuální stroj pro Universal Scripting Language |  |
Virtální stroj, jak už jsem říkal, je jakýsi virtuální počítač který dokáže vykonávat skripty napsané v jazyce k tomu určeném. Virtální stroj stejně jako klasický procesor rozumí omezené sadě instrukcí, které jsou většinou dosti jednoduché a pracují s malým množstvím operandů (čísla, řetězce...). Programy se pak skládají z takových jednoduchých instrukcí jako je sečtení dvou operandů, odčítání nebo nějaké přesuny dat. Virtální stroj provádí stejně jednoduché akce. Když ale chcete provádět nějaké početní operace, je potřeba data která k výpočtu potřebujete někam uložit. V procesoru jsou k tomuto účelu registry, kterých je omezené množství a ne do všech můžete uložit co chcete. Zajistit aby se každý operand ukládal do registru není pro kompiler zrovna jednoduchá záležitost, takže z toho si asi příklad brát nebudeme. My chceme mít náš počítač více přátelštější. Bude to přeci jenom program který se bude tvářit jako počítač. Můžeme místo registrů použít paměť a emulovat si tam registrů co potřebujeme. Bude to něco jako pole registrů. Něco takového v hardwaru existuje například v monolitických (jednoúčelových) počítačích, které mají malé ale rychlé paměti a jednotlivé paměťové buňky představují registry. Než si ale vysvětlíme jak bude virtální stroj pracovat, je nutné seznámit se s assemblerem kterému bude virtální stroj rozumět. V tabulce je vždy uvedena instrukce, její bajtový kód (jen číslo přiřazené instrukci v rozsahu od 0 do 255) a popis její funkce. ### Podporované instrukce
Instrukce: | Bajtový kód | Co to dělá:
nop | 0 | I když to asi nebude dávat smysl, tahle instrukce nedělá nic :)
mul | 1 | Vezme dva operandy z vrcholu zásobníku a vynásobí je. Výsledek uloží na vrchol zásobníku.
neg | 2 | Změní znaménko operandu na vrcholu zásobníku.
mod | 3 | Vezme dva operandy z vrcholu zásobníku a vypočítá zbytek po celočíseleném dělení. Výsledek uloží na vrchol zásobníku.
sub | 4 | Vezme dva operandy z vrcholu zásobníku a odečte je. Výsledek uloží na vrchol zásobníku.
div | 5 | Vezme dva operandy z vrcholu zásobníku a vydělí je. Výsledek uloží na vrchol zásobníku.
add | 6 | Vezme dva operandy z vrcholu zásobníku a sečte je. Výsledek uloží na vrchol zásobníku.
lgoto <adresa> | 7 | Nepodmíněný skok. Provede skok na adresu <adresa> a to v každém případě.
ifeq <adresa> | 8 | Podmíněný skok. Provede skok na adresu <adresa> pokud hodnota na vrcholu zásobníku je 0.
ifne <adresa> | 9 | Podmíněný skok. Provede skok na adresu <adresa> pokud hodnota na vrcholu zásobníku není 0.
if_cmpeq <adresa> | 10 | Podmíněný skok. Provede skok na adresu <adresa> pokud levý operand = pravý operand.
if_cmpgt <adresa> | 11 | Podmíněný skok. Provede skok na adresu <adresa> pokud levý operand > pravý operand.
if_cmplt <adresa> | 12 | Podmíněný skok. Provede skok na adresu <adresa> pokud levý operand < pravý operand.
if_cmple <adresa> | 13 | Podmíněný skok. Provede skok na adresu <adresa> pokud levý operand <= pravý operand.
if_cmpge <adresa> | 14 | Podmíněný skok. Provede skok na adresu <adresa> pokud levý operand >= pravý operand.
if_cmpne <adresa> | 15 | Podmíněný skok. Provede skok na adresu <adresa> pokud levý operand != pravý operand.
nreturn | 16 | Návrat z funkce. Nevrací hodnotu, jen se postará o to aby byl zásobník ve stejném stavu jako před voláním funkce.
load <index> | 17 | Uloží proměnnou na vrchol zásobníku. <index;> je číslo (index) proměnné.
store <index> | 18 | Hodnotu z vrcholu zásobníku uloží do proměnné. <index> je číslo (index) proměnné.
ldc_int <index> | 19 | Načte na vrchol zásobníku celočíselnou hodnotu z tabulky konstant. <index> je číslo řádku v tabulce konstant.
ldc_string <index> | 20 | Načte na vrchol zásobníku řetězcovou konstantu z tabulky konstant. <index> je číslo řádku v tabulce konstant.
ecall <index> <argumenty> | 21 | Zavolá externí funkci. <index> je číslo řádku v tabulce konstant, kde je uloženo jméno funkce. Jméno funkce se použije ke zjištení dalších informací o funkci. <argumenty> je počet argumenů s kterými je funkce volána.
ldc_double <index> | 22 | Načte na vrchol zásobníku desetinné číslo z tabulky konstant. <index> je číslo řádku v tabulce konstant.
dup | 23 | Vezme vrchol zásobníku a uloží jej do zásobníku ještě jednou. Hodnota co byla v zásobníku tam teď bude dvakrát za sebou.
pop | 24 | Sníží vrchol zásobníku o 1.
lcall <index> | 25 | Provede skok na lokální funkci. <index> je číslo řádku v tabulce konstant, kde je uloženo jméno funkce. Jméno funkce se použije ke zjištení dalších informací o funkci.
vreturn | 26 | Provede návrat z funkce. Vrací hodnotu, která bude uložena na vrcholu zásobníku.
shl | 27 | Vezme dva operandy z vrcholu zásobníku a provede bitový posun vlevo. Výsledek uloží nan vrchol zásobníku.
shr | 28 | Vezme dva operandy z vrcholu zásobníku a provede bitový posun vpravo. Výsledek uloží nan vrchol zásobníku.
inc | 29 | Zvýší hodnotu proměnné o 1.
dec | 30 | Sníží hodnotu proměnné o 1.
and | 31 | Vezme dva operandy z vrcholu zásobníku a vypočítá logický součin. Výsledek uloží na vrchol zásobníku.
or | 32 | Vezme dva operandy z vrcholu zásobníku a vypočítá logický součet. Výsledek uloží na vrchol zásobníku.
### Virtuální stroj Nyní se podíváme na součásti virtálního stroje a poté si na příkladu vysvětlíme jak to funguje. Už jsem psal, že náš virtuální stroj bude pracovat se zásobníkem a ne s registry. V zásobníku budou uloženy proměnné a budou se tam také ukládat výsledky výpočtů. Tomuto zásobníku budeme říkat Execution Stack (Zásobník pro výpočty). Pak budeme potřebovat ještě jeden zásobník. Jeho funkce souvisí s voláním funkcí. Ve zdrojovém kódu se jmenuje Activation Stack, ale lepší název by byl Call Stack. Do tohoto zásobníku se budou ukládat informace o tom, v jakém stavu se nachází funkce před voláním další funkce, aby se do tohoto stavu po vykonání funkce mohla vrátit. Pokud někdo víte jak fungují procesory, můžete vidět podobu s Task State Segmentem (Segment stavu procesu), který se používá při přepínání úloh. Něco podobného bude představovat položka v zásobníku. Virtuální stroj bude muset obsahovat ukazatele na potřebné zdroje k vykonávání skriptu, jako je ukazatel na kódový segment, ukazatel na tabulku symbolů, na tabulku funkcí. Pak také nějaké pomocné proměnné jako PC - Program Counter, SP - Stack Pointer a BSP - Base Stack Pointer. Program Counter neboli čítač instrukcí ukazuje do kódového segmentu. Představuje posunutí od začátku kódového segmentu (offset), kde najdeme instrukci kterou máme zrovna provést. Stack Pointer - ukazatel vrcholu zásobníku ukazuje do zásobníku na místo kde je uložena poslední hodnota. Base Stack Pointer ukazuje na začátek zásobníku. Těžko se to popisuje, proto doufám že na obrázku to bude jasnější, protože to není nic složitého. Nenechte se zaskočit tím, že v obrázku nejsou všechny věci o kterých jsem se zmiňoval, jako například ukazatel na zdroje. Pro nás je nyní důležité jen to co je na obrázku, tj. Execution Stack, Call Stack, SP, BSP, PC a kodový segment pro pochopení zpracování kódu. Nyní je jistě vidět že PC ukazuje na nějakou instrukci v kódovém segmentu. V obrázku je to voleno náhodně. Neoznačil jsem naschvál čtvrtou instrukci od shora. Stejně tak místa na které ukazují SP a BSP jsou zvolena náhodně. Jediné co je důležité, je, že SP se nemůže dostat pod BSP. BSP musí mít vždy vyšší nebo stejnou hodnotu jako SP. ### Execution Stack Zásobník pro výpočty budeme používat jak pro uložení proměnných, tak také jak už název napovídá pro ukládání výsledků výpočtů a předávání parametrů funkcím. Vysvětlím to na příkladu hlavní funkce. Ve funkci budeme používat několik proměnných, pro které kompiler vypočítá jejich indexy jako čísla od nuly až kolik máme proměnných. Budeme-li například mít tři proměnné, budou mít indexy 0,1 a 2. Tyto proměnné budou uloženy do zásobníku a první z nich bude začínat na místě kam ukazuje BSP. SP bude na začátku funkce ukazovat na poslední proměnnou. Protože SP je nejdřív inkrementován a pak je teprve na místo kam ukazuje uložena hodnota, bude první hodnota kterou do zásobníku nahrajeme uložena hned za poslední proměnnou. SP tedy ukazuje na vrchol zásobníku, na místo kam můžeme ukládat hodnoty potřebné k výpočtům nebo parametry funkcí. Snad to opět objasní obrázek :) Obrázek výše zachycuje situaci funkce o které jsem mluvil. Funkce má tři proměnné, které jsou uloženy od začátku zásobníku. Nebyla dosud provedena žádná početní operace a tak vrchol zásobníku ukazuje na poslední proměnnou. Pak ale přijde volání nějaké jiné funkce, lokální, která má čtyři parametry. Tyto parametry jsou uloženy na zásobník a SP se změní. Ukazuje na poslední parametr. Virtuální stroj provede skok do těla funkce a začne ji vykonávat. Protože musí platit že BSP bude vždy ukazovat na první proměnnou funkce a za proměnné počítáme i parametry funkce, musí být pro novou funkci BSP jiné než pro starou. Jak je vidět, BSP se změní tak že bude ukazovat na první parametr funkce. SP se v tomto případě nezměnilo, protože nová funkce má jen čtyři parametry a pracuje jen s těmito parametry a nemá žádné jiné proměnné. ### Volání funkcí Už víme jak pracuje Execution Stack. Při volání funkcí budeme potřebovat ještě Call Stack. Při volání funkce se většinou mění obsahy všch ukazatelů které používáme (SP, BSP, PC). Když je ale změníme, nebudeme schopni vrátit se po skončení funkce do stejného stavu v jakém jsme byli před voláním funkce. Jak bychom věděli jaký byl obsah SP, BSP a PC? Proto potřebujeme aby každá funkce měla tyto ukazatele svoje. To vyřešíme pomocí zásobníku pro volání funkcí, jehož každá položka bude struktura obsahující všechny tři ukazatele. Když zavoláme funkci, prostě se přesuneme v zásobníku na další strukturu a budeme používat nové ukazatele. Staré hodnoty zůstanou uloženy v zásobníku níže. Až funkce skončí, vrátíme se v zásobníku zpět a budeme používat zase staré ukazatele s hodnotami které tam byly před voláním funkce. Jen PC se zvýší abychom nebyli na stejné instrukci jako předtím, protože to bylo volání funkce. Kdybychom se nepřesunuli na další instrukci, volali bychom funkci pořád dokola. Na obrázku je vidět co se děje na zásobníku při volání funkce. To že je po návratu z funkce PC = 29 je způsobeno tím, že skutečná velikost instrukce pro volání funkce j 5 bajtů. Jeden bajt je kód instrukce a zbylé čtyři představují index do tabulky konstant.