Virtuális gép, Assembler, távirati stílusban [II. rész, a dolgok veleje]

Ebben a részben helyre tesszük a dolgokat (lesz mit) és kissé bemozgatjuk a megszerzett tudást. A processzorok bámulatos egyszerűségére is fény derül. Akik  kitartanak és figyelnek, azok számára pedig:

Összeáll a kép.

   A korábbi rész úgy lett befejezve, hogy megalkottuk a VM-ünk processzorának utasításkészletét. Ettől nem kaptunk sérvet, hiszen az egész mindössze hét utasításból áll. Ezeket az utasításokat most gyorsan újra vesszük, pontosítva azok - eddig csak körvonalazott - szerepét. Egy új utasítással is kiegészül a sor.  Kódot is fogunk fejteni, ráadásul vissza!  A történet vége pedig egy rendkívüli dologgal kecsegtet.  

   Akkor, szaladjunk is végig az utasításokon.

Ezek az utasítások, hiába vannak kevesen, csoportokba rendezhetők.
 - Az első ilyen csoport az aritmetikai-logikai (ALU) utasítások halmaza. Ezek nálunk az összeadás, a kivonás és a komparálás. Mnemonikjaik:  ADD/SUB/CMP.
- A második csoportba tartoznak az adatmozgató utasítások. Ezek a STORE és a LOAD. Mnemonikjaik szerint STA és LDA.
- A harmadik halmaz az ugró utasításoké. Ide tartozik a JUMP és a JUMP IF ZERO, mnemonik: JMP és JZR
- Végül itt árválkodik ez a se hal, se hús RETURN. Ennek mnemonikja a RET lesz.

    Az alábbi képen látható, hogy az utasítások milyen szigorú keretek közé vannak szorítva. Az ALU utasítások mindig az ACCUMULATOR tartalmát és valamelyik memóriarekesz tartalmát használják fel a művelet elvégzéséhez, legyen az összeadás, kivonás vagy két szám összehasonlítása. Ezeknek a műveleteknek az eredményét is mindig, mindig, mindig az ACCU fogja tartalmazni, alkalmanként felülírva ezzel a korábbi tartalmat.
    Az adatmozgatás nagyon egyszerű, az STA a STORE ACCU (tárold az accu (tartalmat)) szavak összevonása. A gyakorlatban ez adatmásolás az ACCU felől valamelyik memóriarekeszbe. Az LDA pedig ennek ellentettje. Itt a memória egyik rekeszének tartalma kerül be az ACCU-ba, szóval, csak az adat iránya fordul meg.
    Az ugró utasítások egyike a JMP. Ez a VM-ünk IP regiszterét írja felül, annak a memória rekesznek a tartalmával (MEM[n]) ami mögötte áll a forráskódban. Kb. így:

JMP 08;  

    Ha a memóriatartalom a nyolcadik rekeszben mondjuk 24, akkor az IP regiszter is a 24-et fogja tartalmazni és ennek megfelelően a 24-es rekesz tartalma kerül végrehajtás alá a processzorban.    

    Ez a JMP utasítás nem függ semmiféle feltételtől. minden esetben végrehajtódik ha rá kerül a sor.
    Nem úgy a másik, a JZR, ami csak akkor fogja felülírni az IP regisztert, ha valamilyen feltétel teljesül. Erről később lesz még szó.
    A RET utasításról már ismeretes, hogy csak az alprogramok végét jelzi. Hogy tudja a processzor, mikor kell visszatérnie a főprogramba. Lényegében, a RET feldolgozása esetén egy rejtett JMP utasítás hajtódik végre, aminek a paramétere (az ugrás címe) a stack-ben tárolódik. Ez a stack a memória nulladik címe, mint azt már tudjuk. a RET utasítás tulajdonképpen egy JMP MEM[00], rövidebben JMP 00; Ami arra a címre ugrik, amit a MEM[00]-án talál.

istruction set

    Végül rátérek az új utasításra, a SUB-ra. Ez a kivonást valósítja meg. Azért lett a többi közé beinplantálva ez is, mert egyszerűséget igértem és félek, az originális út, ami a kettes komplemens-en keresztül vezet, sokaknak kedvét szegte volna. Ezért a kivonás nem lesz összeadás, hanem marad ami volt, subtraction.  De azt ne feledjük, hogy szorzás és osztás továbbra sincs! Ezt a két fogalmat csak állástalan matektanárok találták ki a munkanélküli hivatalban, bosszúból, időszámításunk előtt harminckettőben.

 Az alábbi ábrán látható a VM egy végletekig lebutított, mindössze 64 Byte memóriával ellátott verziója. Ebben a memóriatartalomtól kezdve, egészen az ACCU és IP regiszterek tartalmáig, minden hexadecimális formában kerül megjelenítésre.
  Egy-egy hexa memóriacím úgy követhető le, hogy vesszük annak a sornak az elején álló hex számot (kivastagított, fekete) és amellé odaírjuk (fejben) annak az oszlopnak a tetején álló, szintén hexa számot amit ott találunk. Pl alább a 02h címen öt (05h) található  (kék színnel jelezve), a 15h címen (piros) pedig 3Fh. Ez a tudás (már akinek eddig nem volt meg) nem sokára jól fog jönni.

 

hexa

 

Az OP-kódok

Utasításaink már vannak, na de, hogy lesz ezekből futtatható program? Úgy, hogy az assembler sorról sorra értelmezi és lefordítja a forráskódot. Azt, amiben az utasításaink szerepelni fognak. Na de mire fordítja le? Ez itt a kérdés.
A válasz: Minden utasítás kapni fog egy számot. Ez a szám lesz az utasítás úgynevezett OP-kódja.
Ezeket a számokat én most hexadecimális formában fogom bemutatni (erre van is jó okom ).
Az ADD utasításunk az AD OP-kódot kapja, a SUB lesz az AA, az LDA az AB, az STA a BA, a JMP EA, a JZR E0, a CMP a C0, a RET pedig FF. Itt van erről egy táblázat. Van a táblában, ami egyelőre piros színnel takarva van, mert ipari titokká minősítettem, de idővel arra az egyre is fény derül. Aki előbb kitalálja, hogy mi lett pirossal  letakarva, az kap egy fél vödör gyémántot, amit átvehet Kuala Lumpurban, a kikötő legszebb kocsmájában.     

op-kódok

    Az ACCU regiszterről már elég sok szó esett. Az IP regiszterről annál kevesebb. Ennek vajon mi lehet a célja, a szerepe?
Az IP regiszter indexelni tudja a memória teljes területét. Ha az IP tartalma mondjuk 03, akkor az IP a memória harmadik rekeszére mutat. Innen kell a processzornak kiolvasni és végrehajtani azt az utasítást, amit ott, azon a memóriacímen talál. Mindig onnan olvas a CPU, ahova az IP regiszter éppen mutat és ez egy nagyon komoly fegyvertény. Az írás végén ki is derül majd, hogy miért.

    Azt tudjuk, hogy a programok szegmensei a memóriában helyezkednek el. A Stack-, az adat- és a kódszegmens. Ebből ami érdekes most, az a kódszegmens, ugyanis ez tartalmazza a futtatható programunkat. A mi demó VM-ünkben ennek a szegmensnek a helye a 10h memória-címtől kezdődik (több memó esetén a (MEMO_SIZE DIV 4 címtől), egészen addig a címig ameddig tart maga a kód a méretét tekintve. Ez utóbbi ugye változó, ahogy a programok hossza is az. Szóval, a programunk kódja a 10h (ez decimálisan a 16) címen kezdődik.   
    Amikor a memóriában egy program végrehajtásra kerül, akkor a kódszegmensének legelső címe (ez a 10h) bekerül az IP regiszterbe. Innen olvassa ki a processzor azt az OP-kódot, amit a memóriában ezen a címen (MEM[10h]) talál és ezt hajtja végre. Amikor végzett, akkor az IP regiszter tartalma automatikusan több [!] lesz, így már egy másik memóriarekeszre mutat, amit a processzor szintén kiolvas, értelmezi és végrehajtja az ott talált OP-kódot és így tovább, amíg a program végére nem ér.
   Persze az IP regiszter nem mindig ugyanannyival növekszik. Hogy éppen mennyivel, az attól függ, hogy a processzor milyen utasítást hajtott végre. Ha például összeadást, akkor kettővel nő, legalábbis nálunk, mert annak két operandusa van és abból az egyik már ott van az ACCU-ban. Az ADD utasítás utáni címen van a második és a következő lesz az újabb utasítás.
Tehát, ha az ADD utasításon áll az IP, legyen ez a 10h akkor a következő az ADD paramétere, amit hozzá kell adni az ACCU-hoz, ez a 11h és a következő utasítás, amire az IP-nek mutatnia kell, a 12h.
Ha mondjuk a NOT utasítást hajtaná végre (ilyen nálunk éppen nincs (még)), akkor eggyel kellene nőnie csak, mert invertálná az ACCU tartalmát és kész, nem kéne neki paraméter. Nézzük, miről is beszélek.

Alább a nagy képen  egy apró programocska látható, már betöltve a VM-be, futásra készen.
A nulladik memória rekesz a Stack-é, az elsőtől a tizennegyedikig a DATA rész (világosbarna), a tizenötödik pedig a kimeneti port. 
A többi a kódszegmens (valami bilikék szinű). Az éppen futtatandó kód kb. rózsaszín.

Az IP most a 10h memóriacímre mutat, az következik a végrehajtásban. Ha megnézzük a 10h címet (rózsaszín), látható, hogy a rekesz az AD OP-kódot tartalmazza. Ha megnézzük a jobboldalon felül található táblázatot, az opkód oszlopból ki is kereshetjük, mellette balra pedig a hozzá tartozó utasítás mnemonikja szerepel. Az AD utasítás az ADD, a mellette levő (11h) pedig a memória első rekeszére utal.
Az utasítás tehát forráskód szintjén így néz ki:

ADD 01;

    A következő a 12h címen található BA opkód, ami az STA utasításnak feleltethető meg. Emelletti (13h) címen a 0F található. Az utasítás tehát ez: 

STA 0F;

    Azaz, az ACCU tartalmát tárold el a memória 0Fh (decimálisan a tizenötödik) címén.

    A következő utasítás a 14h-n található és az EAh opkód mögötti JMP-ot dekódoljuk, az utasítás paramétere itt is a következő címről derül ki. Ez a memória 3Fh címén lévő szintén 00h lesz. 

Tehát az utasítás a paraméterrel együtt:

JMP 3F;

A (majdnem) teljes program ennyi:

ADD 01;
STA 0F;
JMP 3F;

    Ez a progi, működését tekintve, valójában egy összeadás akart volna lenni, csak az én kapkodásom következtében lemaradt a másik ADD utasítás ami a 02-edik memóriacímen lévő értéket (5) adta volna hozzá az ACCU beli háromhoz. Csak hát, a kimenetet én legeneráltattam a VM-mel, ki is szineztem szépen, mert nem vettem észre a másik ADD hiányát. Újra pedig már nem volt kedvem nekiállni. Szóval, a másik ADD lemaradt, ezután kellett volna következnie a portra kiírásnak (ami megvolt) és végül egy olyan memóriahelyre ugrásnak (ez is megtörtént), ami biztosan nulla tartalmú. Ugyanis ilyenkor, ha az IP olyan címre mutat, ami 00h-t foglal magába, a VM processzora megáll. 

Na de a következő program már komolyabb lesz.
Abban lesz IF (tehát szelekció) és lesz ciklus (azaz iteráció) is, pedig nem lesz sokkal  hosszabb ennél.    

 

additio

 

FOLYT. KÖV.

Hozzászólások

>> Ha az IP tartalma mondjuk 03, akkor az IP a memória harmadik rekeszére mutat

nem

uj poliverzum van kialakuloban! \o/ johet majd a furor-port is!

Ha van 'RET', akkor lehetne 'CALL' vagy 'GOSUB' is.

De az első nem teszi a stack-re az IP-t.

Már hogy ne tenné???

Hát én írom. Oda teszi, ahova akarom. 

Ez egyébként is egy minimalista, demonstrációs célzattal készített valami.
Ennek megfelelően minimális a processzor tudása, a memória mérete és az assembler is. 

 

barki aki barmit hozzaszol azt ignoralod erdemben, es csak kirohogod, es amugy is jobban tudod,...

Azért nem bárki, de ha azt a csoportot nézem, akikre te gondolsz, akiknek te is tagja vagy, akkor igen. Benneteket tényleg csak kiröhögni lehet.
Hogy miért? Azért, mert én nagyobb részben tudom, hogy ti mire gondoltok, de mellette tudom azt is, hogy én mire gondolok. Ti meg csak fújjátok a magatokét. 

Ti a saját ismereteitek fogjai vagytok, és szomorú, de olyasmit vártok el, próbáltok számon kérni, amihez a saját megszokásotokon túl nincs semmiféle jogalapotok. Sajnos még addig sem vagytok képesek eljutni, hogy legalább ezt felismerjétek.

Itt a legutóbbi, ez a call.
A processzorok és utasításkészletek tervezői figyelemmel vannak a múltban kialakult konvenciókra, nyilván jó érdekük fűződik ehhez. Hadd ne soroljam. Azonban a processzorok, utasításkészletek tervezőit nem köti törvény, megtehetnék, hogy a konvencióknak nem rendelik alá magukat és azt csinálnak, amit akarnak, mert a saját szempontjaik fontosabbak nekik. 

Na, pont ezt teszem most én is, ezt voltam szíves (pontosabban voltam kénytelen) hangsúlyozni már a fél éves preambulumban is. Nem vagyok köteles azt tenni, amit más tett eddig, ha ugyanazt a mnemonikot használom, ha mást. Nem vagyok köteles úgy írni, élni, tenni ahogy más elvárja, saját vaskalapossága okán. Gondolom  ha több nem, ez legalább befér a fejedbe, de ha akarod, szivesen le is rajzolom neked, napocskával, kiskacsával. 

Lehetnek, sőt, vannak egyéb, a ti elvárásaitokon túlmutató szempontok is. Vannak esetek, amikor a kevesebb több, vannak esetek, amikor a cél érdekében jobb, kifejezetten hasznosabb a valóság egy részét eltakarni. Ezt is próbáljátok meg felfogni, megérteni. 

 

Ez mind rendben van, de vannak célszerű dolgok. Nevezetesen bármit megtehetsz, akár mul is lehet az összeadásod mnemonikja, csak erősen szívni fog, aki használni akarja. A konvenciók nem mindig ellenségek. Arra valók, hogy rövidebb legyen a tanulási görbe, elegendő legyen az új dolgokat megtanulni. Ez olyannyira így van, hogy 32 bites MCU-ra írtam C-ben úgy programot, hogy a regiszter nevét nem néztem meg katalógusban, de mivel nagyon szigorú, következetes volt a perifériákat vezérlő regiszterek elnevezése, ezért ki tudtam találni a nevét.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

de vannak célszerű dolgok.

Persze, csak hogy mi a célszerű, azt inkább hadd döntsem el én.

akár mul is lehet az összeadásod mnemonikja, csak erősen szívni fog, aki használni akarja.

Ettől én elég messze vagyok.

A konvenciók nem mindig ellenségek.

A konvenciók a ti ellenségeitek, nem az enyém.  Én nem szembe megyek a konvenciókkal, csak addig alkalmazom, amíg nem hat a céjaim, érdekeim ellen. 

Itt van rá a példa:

"In 8085 Instruction set, STA is a mnemonic that stands for STore Accumulator contents in memory. In this instruction,Accumulator8-bit content will be stored to a memory location

In 8085 Instruction set, LDA is a mnemonic that stands for LoaD Accumulator with the contents from memory.

Compare (register or memory) with accumulator (CMP R/M) – This is a 1-byte instruction. It compares the data byte in the register or memor "

a RET utasítás tulajdonképpen egy JMP MEM[00], rövidebben JMP 00; Ami arra a címre ugrik, amit a MEM[00]-án talál.

Egy byte-os stack? Akkor az egyszintű meghívásokat is fog jelenteni, azaz, ha egy JMP (ha jól értem, ez valójában JSR?) után jön egy másik, akkor az felül fogja írni az előző letárolt címet és mind a két RET ugyanoda fog visszatérni: a második JMP mögé és egy gyönyörű végtelen "ciklust" fogsz kapni.

Pl.:

egyik:	valamit
	csinal
	itt
	ez
	JMP masik
	az
	ize
	RET

masik:	ez
	is
	csinal
	valamit
	mittudomen
	RET



main:	kod
	meg
	meg
	tobb
	kod
	JMP egyik
	meg
	meg
	egy
	kis
	kod
	stb

Itt a main-en belül meghívódik az egyik szubrutin, ekkor a nullás címre kiíródik a meghívás után következő utasítás (itt: "meg") címe, majd már a szubrutinban meghívódik a masik szubrutin és ekkor a nullás címen az előző visszatérési érték felülíródik az itteni meghívás utáni utasítás (itt: "az") címével. Ezután a masik szubrutin lefut, a végén RET kiolvassa a nullás címről az utoljára tárolt címet és folytatja az egyik szubrutinban a JMP után, viszont, amikor eléri ennek a szubrutinnak a végét, akkor a RET ugyanazt a címet fogja még egyszer kiolvasni és nem a main-ba ugrik vissza, hanem még egyszer az egyik szubrutinba, a JMP mögé...és aztán megint és megint és megint; az "az" és "ize" utasításokat fogja ismételgetni a világ végéig, vagy a resetig. Hogy fogsz így egy szintnél mélyebb szubrutinhívásokat csinálni? Szükséged lenne egy veremregiszterre, amit a JMP és a JZR mindig léptet, az mindegy, milyen irányba és az is mindegy, hogy kiírás előtt, vagy után, de mindig pont fordítva, mint ahogy a RET csinálja. Tehát, ha a JMP azt csinálja, hogy

MEM[SP++] = IP;
IP = ADDRESS;

akkor a RET-nek azt kellene, hogy

IP = MEM[--SP];

És akkor ez nem lesz gond.

Ez valoban igy van, az 1 meretu "stack" miatt nem mennek az egymasba agyazott hivasok. Ezt mar korabban talan leirta, hogy nem is cel, a cel az, hogy megmutassa hogy mukodne egy ilyen gep. Van egy rakas tervezesi dontes, amit meghozott a sajat szaja ize szerint - es persze aszerint, hogy ez egy virtualis gep, es nem is cel a fizikai hardware. Ha cel lenne, akkor kapasbol nem ilyen ertekeket kapnanak az utasitasai, hanem valami gep altal is konnyen dekodolhatot (de az ADh-t konnyebb megjegyezni, hogy az az ADD).. meg persze eleg lenne neki kevesebb bit is (mint pl. a PIC 6+8 bites architekturaja).

Meg MOV parancsa sincs szerencsetlennek, a flag-eket sem eri el, stb. Ha szelesebb architekturat valasztott volna, akkor lehetnenek felteteles utasitasai (ARM), barrel shifter (szinten ARM), szep, konnyen dekodolhato, szimmetrikus opcode-ok. De nem ez volt a cel, hanem egy egyszeru architektura letrehozasa es bemutatasa. Ha utanacsinalod, neked lehet tobb szintu stacked, stack pointered, tobb RAM-od, stb.. (viszont az osszes ilyen bonyolitana, ami az erthetoseg rovasara menne)

When you tear out a man's tongue, you are not proving him a liar, you're only telling the world that you fear what he might say. -George R.R. Martin

Talán a régi ARM-ok is ilyenek voltak?

Az uj ARM-ok is ilyenek: van egy "branch with link" (CALL helyett) es sima "indirect branch" (RET helyett). A tobbit oldd meg szoftveresen. Oke, a POP {pc} az segit meg, merthogy a Thumb utasitaskeszletben csak PUSH {lr} van, POP {lr} nincs. Meg a megszakitaskezeles is kicsit trukkosebb emiatt. 

Tökéletesen igazad van. Viszont sajnálom, hogy nem olvastad az első részt, mert akkor ettől a munkától, amit a hsz-edben kifejtettél, megkímélhetted volna magadat.

Idézem saját soraimat, az első rész végéről:

"Talán az is sokak számára nyilvánvaló már, hogy a megírandó programjainkban a szubrutinokat, procedúrákat, függvényeket nem ágyazhatjuk egymásba, hiszen ahhoz ez a picike, egy byte-os stack - most még - nem elég. A programjainkat tehát úgy fogjuk szervezni, hogy minden szubrutin hívás után, jó kisfiúk módjára, szépen visszatérünk a főprogramba és onnan hívjuk meg a következő szubrutinunkat (ha egyáltalán szükség lesz erre). Máskülönben a visszatérési értékünket a második hívás csúnyán felülírná, ami nem tenne jót a közérzetünknek."

Szerkesztve: 2021. 06. 26., szo – 02:09

Na végre, hogy én is hozzátehetek valami hihetetlen életbevágó infót a témához: a szorzást nem i.e. 32-ben, hanem - több tudós is így sejti ma már - cirka 18-20 ezer évvel Krisztus születése előtt "találták" fel: https://en.wikipedia.org/wiki/Ishango_bone

Aki kíváncsi is rá: pár éve Brüsszelben láttam ezt a "valamit" :)

Amúgy nekem továbbra is tetszik az irományod Pixel5, a rosszindulatú trollokat meg csak hagyd figyelmen kívül (nem tudom erre képes vagy-e :D ).

a rosszindulatú trollokat meg csak hagyd figyelmen kívül (nem tudom erre képes vagy-e :D

Abszolút hidegen hagynának, ha nem okoznának kárt a ténykedésükkel. Sajnos biztos vagyok benne, hogy sikerül elbizonytalanítaniuk néhány érdeklődőt.

Egyébként, örülök, hogy elégedett vagy. Köszönöm. :)
 

Biztatás képen, hogy a pozitív oldal is meghallgattassék, ez a sorozat nagyon tetszik, várom a következő részt! :)