Virtuális gép, Assembler, távirati stílusban [IV. rész, a processzor]

A VM a Virtual Machine szavak rövidítése. Virtuális gép. Tehát nem megfogható vas, hanem csak egy szoftveres úton előállított masina. Amit mi építünk, az viszont elég  messze van a virtuális gép fogalmától. Mi a különbség?

    A VM egy teljesértékű gép, egy használható hardver, csak szoftveresen megvalósítva. Jobbára van kijelzője, billentyűzete, vagy legalább kezelőszervei, jellemző még, hogy vannak ilyen-olyan perifériái. Ennek a mi valaminknek meg nincs semmi ilyesmije. Mindössze egy processzor és némi memória az egész. A memória egyik rekesze ugyan ki van nevezve kimeneti portnak, de ahhoz a porthoz nem kapcsolódik semmi, bemeneti portja meg egyszerűen nincs is.

Miért nincs?

    Mert az egyszerűség volt a cél. Az adatbevitelt ki lehet váltani szoftveresen, hiszen programozottan olyan adatot juttathatunk a gépbe, amilyet akarunk. Na meg persze, amilyet a gépünk feldolgozni képes. A kimenet sem életbevágó, mert a gép teljes tartalma látható, a benne végbemenő változás követhető. A nagyon régi számítógépeknek sem volt monitoruk, olykor még konzolírógép sem, amivel az eredményt meg lehetett volna jeleníteni. Az eredmény sokszor csak a memória néhány erre kijelölt rekeszében tárolódott.  
Hát mi is valahogy így állunk, most.

Persze a lehetőség adott, hogy a portot olvasni tudó perifériát írjunk. Még olyat is írhatnánk, ami a kiolvasott értéket feldolgozza és azt mondjuk megjeleníti valamilyen formában.
    A legegyszerűbb ilyen lenne egy nyolc darabból álló LED-sor. Na de mit érnénk vele, hiszen a kimeneti port tartalmát követni tudjuk. Ha több LED-sor lenne, azok is valahogy egymásba szervezve, akkor már más lenne a helyzet, mert a kimeneti portot a következő adat felülírja, így annak korábbi rtartalma elvész. LED-csoportokból viszont lehetne egy átmeneti tárolót szervezni, amiről bármikor leolvasható lenne az utoljára kiírt néhány érték. Ha a LED-ek mondjuk hét szegmenses kijező részei lennének, akkor még jobb lenne a helyzetünk, mert hát akkor már nem csak egy-egy kósza adat, hanem komplett  adatcsoportok is megjeleníthetők lennének. Mondjuk egy-egy, csak több helyiértéken ábrázolható szám, vagy valami szó, szöveg. Netán egy egész sor.

                                              (később ki lesz egészítve)

A processzor

    Ennek a kevesebb, mint száz sornyi forrásnak a megértése nem nagy kihívás. Az egész mindössze öt függvényből (procedúrából) áll.   
A lefordított binárist egy paraméterrel kell meghívni, ami annak a memória-kép fájlnak a neve (program.bin) amit futtatni szeretnénk.

uVM.exe  program.bin  >kimenet.txt

A másik paraméter lényegében a program kimenetének fáljba irányítása. Ebben a fájlban (kimenet.txt) tárolódik el minden lépés során bekövetkező változás, az  ACCU-ra, az IP regisztrerre és a memóriára vonatkozóan, szépen, formázottan. Még a lépés sorszáma is kiírásra kerül. Nem árt tudni, hogy minden utasítás végrehajtása egy (virtuális) órajel ciklust igényel.   

Egy árván felejtett utasítás, a COMPARE

Az utasítások közül ez az egy, úgy érzem nem lett elég világosan körülírva. A COMPARE (mnemonikja CMP) dolga, hogy két számot hasonlítson össze.  Az egyik szám az ACCU tartalom, a másikat az utasítás paramétere határozza meg, ami egy memóriacímre mutat. Annak tartalmát veti össze az ACCU-éval. Két eset lehetséges:

  • - Ha a két szám nem egyezik, akkor nem történik semmi, csak az IP regiszter lép át a következő utasításra (ha nem ér véget a program). 
  • - Ha viszont egyezést talál a COMPARE, akkor az ACCU tartalmát - de csak azt - se szó, se beszéd,  kinullázza.

    A CMP utasítás lényegében kiegészítő utasítása a mi feltételes JUMP-unknak, a JZR-nek. A CMP elvégzi az összehasonlítást és ha egyezés van, akkor nullázza az ACCU-t, a JZR (Jump If Zero) pedig ugrik, hiszen az ACCU nulla, más esetben pedig nem csinál semmit. Ez az utasítás-páros alkalmas ciklusszervezésre és elágazások készítésére, feltételvizsgálatra is. 

Így lehetett elérni, hogy flag-ek használatára ne legyen szükség.

Most jöjjenek a forrás eszenciáját magyarázó telegramok:

[074] A főprogram a memória-kép fájl nevével - mint paraméterrel - meghívja a [064] LoadToMem() procedúrát. Ezzel a memóriába töltődik a futtatandó programunk.
[075] ha a program paraméter nélkül hívódik meg, akkor egy figyelmeztető üzenetet ír ki a konzolra és kilép, hisyen nincs mit futtasson.

Tételezzük fel, hogy a betöltés sikerült. A következő lépés:

[077] a főprogramból meghívódik egy [059] Executor ('végrehajtó') nevű procedúra. Ez annyit tesz csak, hogy inicializálja az IP regisztert (IP := INITIP), a running nevű boolean tipusú változónak pedig true értéket ad. Ezután egy while cikluson belül meghívódik a [038] OneStep procedúra.  
[040] Az IP tartalmát megkapja az ExIP változó, aminek tipusa byte.
[041] Az utasítások végrehajtása egy CASE szerkezetbe van ágyazva. Ebben kerül kiválasztásra az az utasítás OP-kód, amire az IP regiszter tartalma aktuálisan mutat (MEM[IP] címen). Az utasításokat külön nem magyaráznám, elég egyértelműek. Az utasítás végrehajtása után a [017] DrawScreen procedúra kerül meghívásra.
A DrawScreen tevékenysége elég hangsúlyos. Ez a kódrész hivatott kiírni a processzor (vagy VM) állapotát a standard kimenetre, amit mi már a program meghívásakor egy kimeneti fájlba ('kimenet.txt') irányítottunk.
 
A rövidke kódot hibakezelés nemigen tarkítja. Ezt a majdani felhasználónak kell pótolnia (ha akarja), igénye szerint. Alapban így is használható az assembler is és a processzor is (nálam a kettő egyben van). Csak arra kell vigyázni, hogy ne írásvédett adathordozóra irányítsuk a kimenetet, vagy arra, hogy legyen elég hely az adathordozón arra az esetre is, ha végtelen ciklusba keveredne a programunk.

Ennek kellemetlen következményei lehetnének, így beépítettem egy féket. A lefutott ciklusokat számolja egy cycle nevű számláló. Ennek értékét minden utasításvégrehajtás után összehasonlíttatjuk a CMAX nevű konstanssal. [057] és ha cycle értéke eléri CMAX-ét, akkor a végrehajtás leáll. A  visszatér a főprogramba és kilép. 

Még egyetlen veszélyforrásra kell figyelemmel lenni. Az assembler nem vizsgálja, ha egy olyan memóriacímre történik hivatkozás, ami nem is létezik. Tehát ha a 224-edik cím az JMP utasítás paramétere, de a jelen állapothoz hasonlóan csak 64 byte memória érhető el a VM számára, akkor gond lehet. Nos, a hibát akkor kell lekezelni, amikor az megjelenik.  Ezért már az assembler feldolgozójában meg kell ejteni egy viszgálatot, hogy az utasítás address paramétere nem nagyobb-e a MEMMAX  konstansnál.  

Mindezt FÖLÖSLEGES megtenni, ha az assemblert és a VM-et mindjárt 256 Byte memóriával kezdjük el használni.   Ehhez a BASE konstanst kell átírni az assemblerben és a VM-ben is.  
 

64 byte memória esetén három ciklusonként bő egy kilobyte tárterületet emészt fel a kimeneti ('kimenet.txt') fájl. Végtelen ciklus esetén körülbelül hat MByte-ot. a CMAX értéke 16k.

                                               (később ki lesz egészítve)

Alább a szöveg folytatódik.

uVM

 

Néhány dolog.

   Az alábbi képen egy három utasításból álló programocska forrása, valamint a VM által, a futtatás során generált kimeneti file tartalma (világoskék háttéren).

   A kimenet első táblázatán jól látható, hogy az ACCU tartalma már 02. Ez azért van, mert a táblázatok mindig az utasítás végrehajtása utáni állapotot mutatják.   

   Gondolom, az sokak számára nyilvánvalóvá vált, hogy az utasítások után álló paraméterek, nem direkt feldolgozható értékek, hanem memóriacímek és a processzor az ezeken a címeken található értékekkel operál, tehát adja hozzá, vonja ki az ACCU tartalmából vagy hasonlítja össze vele.

Az ugró utasítások NEM így működnek.  Tehát, a JMP 20 a MEM[20]-ra ugrik.

A táblázatokban minden hexadecimális formában van megjelenítve. Egy kivétel van, ez a jobb oldalon kilógó sorszám ami a már lefutott utasításokat számlálja.
       
Az utolsó táblázat azt az állapotot mutatja, amikor a futás már meg lett szakítva, mert a vezérlés olyan memóriacímre mutatott, aminek a tartalma 00 volt. Ez a képen látszik is:

Az IP reg. a 16h memóriacímre mutat.

Ez az utolsó állapot már nem része a programnak, aki akarja, el is távolíthatja. Ehhez a 057-edik sorban található DrawScreen procedúrát feltételesen kell meghívni. Így:

If running then DrawScreen;

 

 

szimpla adder program

 

Count Up

Hozzászólások

Egy apró észrevétel: A CMP alapú ugrásod működése hogyan lesz implementálva? A virtális géped hajtja végre CMP, JZR kombinációt vagy a kódban lesz a felhasználó által implementálva CMP és JZR sorként? Ha jól értelmezem, a JZR akkor ugrik ha az ACCU értéke 0, ez viszont az általad vázolt kódban implementált CMP+JZR megoldás esetében azt eredményezi, hogy amennyiben nulla az ACCU értéke és azt hasonlítod össze, és hamis az összehasonlítás eredménye, akkor is végre fog hajtódni a JZR urgás. Ez esetben jobb lenne, ha CMP az akku értékét egyezés esetén nullázná, nem egyezés esetén más bitmintára állítaná. A másik megoldás, hogy  nem egyezés esetén a CMP eggyel növeli a IP értékét, ami miatt a CMP végrehajtása után nem nem a JZR-re fog az IP mutatni, hanem eggyel utána. Ez arra is jó, hogy a CMPd nem csak feltételes ugrást tudjon végrehajtani, hanem más feltételes műveletet is. 

"Maradt még 2 kB-om. Teszek bele egy TCP-IP stacket és egy bootlogót. "

Flag-ek bevezetése már az első procikban is szokásos megoldás volt.
  - zero flag
  - carry flag
Ezáltal lehetett megvalósítani a JE, JG, JGE és inverzeit, ahol ezek a fenti 2 flag állapota alapján ágaztak el.

Egy másik érdekesség: főleg olcsó mikrovezérlőknél volt olyan, hogy CMP_EQ_skip és hasonló utasítás, amelynél
   CMP_EQ_skip   memcim   ; Accu összehasonlítása memcím tartalolmával
   CALL akárhova                 ; ha nem volt skip, de jöhetett ide pl. INC mem2  is vagy bármi 1 utasítás
   ... további utasítások        ; innen folytattuk

Tehát egyetlen következő utasítást volt képes skip-elni ez a spéci CMP+skip kombó.

A hw. flagek és azok státuszregiszterbe rendezése mögött leginkább a spórolás állt indokban, mert több azonos típusú eredményt adó művelet eredményét lehet vele becsatornázni más műveletek felé és ezáltal lehet csökkenteni a duplikációkat a processzoron belül. Kevesebb kapuval, több utasítást lehetett implementálni.

Igen, klasszikus PIC-es megoldások. Viszont OP azt írja, hogy nem akar flageket használni, így nem marad más, csakis a IP ugratása, vagy a bitminta használata.

"Maradt még 2 kB-om. Teszek bele egy TCP-IP stacket és egy bootlogót. "

Köszönöm az észrevételedet, ami értékes is és alapos is.

Pontosan úgy van, ahogy mondod. Abban az esetben, ha az ACCU vmilyen okból eleve nullát tartalmaz, a komparálás művelete hamis eredményre vezet, minek okán a köv. JZR utasítás, bár nem biztos, hogy kéne neki, de mindenképpen lefut. Ezzel pedig a program nem kivánt időben eltérül és ez baj.
Amikor ezt az egészet összehoztam, az egyszerűség volt a fő cél. Minél több valamit kell elsajátítania valakinek rövid idő alatt, annál kisebb az esély rá, hogy eredményes  is lesz. Azt hiszem, elég jól kitapintható, hogy én ezen cél érdekében, amivel csak lehetett, spóroltam, Kihagytam a valós, több szintű stacket is, és a flag-eket is. Bár az ok ezekre a döntésekre többes. 

Erre a problémára, amire te most rávilágítasz, nem találtam olyan megoldást, ami tetszett volna. Te most kettőt javasolsz. Ebből az egyik, az IP regiszter értékének a JZR utasítás mögé állítása nekem is eszembe jutott, csak azért nem voltam ezzel megelégedve, mert úgy gondoltam, hogy az aki a programot majd használja, és a saját programjának futását nyomköveti, nem fogja tudni, hogy az IP  miért mutat máshova mint kellene. Ezért végül úgy döntöttem, hogy inkább térüljön el a program, de a mellékelt user guide-ban felhívom az eltérülés lehetőségének veszélyére a figyelmét. Ez talán nem a legjobb megoldás, sőt, biztos, hogy nem a legjobb, mert így megint a felhasználó, az ő figyelme, körültekintése van terhelve. De úgy véltem, mégis ez a kisebbik rossz. Most nem állok bele abba, hogy ezt a döntésemet részletesen indokoljam.  
A másik megoldásodat, annak esetleges hatását, externáliáit kicsit jobban végig kell gondolnom, mielőtt alkalmaznám. De azt mindenesetre nagyon köszönöm, hogy vetted a fáradságot és nem csak a hibalehetőségre hívtad fel a figyelmemet, de a probléma elhárításának módjaira is rámutattál.

Mégegyszer, köszönöm. 

Nincs mit.

Amúgy ez egy elég érdekes architektúra szervezési probléma. Az összehasonlító műveleteknél tipikusan az elv, hogy a művelet nem változtathatja semelyik adatot sem. Ennek egyik oka, hogy ciklikusan végrehajtott összehasonlításnál nem szükséges az akkumulátor értékét újra és újra betölteni. A hátulütője viszont, hogy hosszú ciklusoknál, külső behatásra változhat az akkumulátor értéke.

A destrultív összehasonlító műveleteknél. Pl. kivonás és nulla esetén ugrás vagy összeadás és nulla esetén ugrás. lehetőség van hibaellenőrzésre és a ciklikus műveleteknél kisebb az esély az esetleges akkumulátor hibának a megjelenésére. (Abba, most ne menjünk bele, hogy robosztussági szempontból az adatbetöltés vagy a ciklikus regiszterhasználat számít-e magasabb kockázatúnak.) Hardware szempontból ez azért előnyösebb, mert kevesebb kapu kell hozzá.

"Maradt még 2 kB-om. Teszek bele egy TCP-IP stacket és egy bootlogót. "