Sissejuhatus

Arvuti on (äärmiselt abstraheeritult) kast, mis on jagatud ruudukesteks (mälupesadeks ). Iga ruuduke võib :

- sisaldada musta palli (bit "0");
- sisaldada valge palli (bit "1");
- olla tühi.

Arvutamine (programmi käivitamine/täitmine) koosneb sammudest/instruktsioonidest, kus igal sammul:

- kirjutatakse midagi ("0" või "1") mingisse ruudukesse (mälupesasse) - kuid konkreetset mälupesa saab näidata vaid siis, kui on kokku lepitud mingi viis ruudukeste/mälupesade adresseerimiseks; järgnevas oletame (lihtsustatult), et mälupesad on aderesseeritud järjestikuste arvudega (näiteks) kaheksandsüsteemis, kuid reaalsed arvutid võivad kasutada ka näiteks heksadetsimaalsüsteemi (hex);
- loetakse midagi ("0" või "1") mingist ruudukest (mälupesast). Kuid loetu on tarvis kuidagi salvestada, sellepärast on arvutis mingi (lõplik, väike) arv registreid - mälupesad, mis on vahetulemuste ja operatsioonide argumentide salvestamiseks ( kuna need peavad olema väga kiired, siis tavaliselt väga väike arv);
- tehakse mingi (aritmeetiline, loogiline) operatsioon (binaarne - kahe argumendiga, unaarne - ühe argumendiga; kuna operatsioon salvestatakse samades mälupesades, peab olema fikseeritud mingi mälupesade hulk, kus fikseeritud 0/1-de jada kirjeldab operatsiooni, s.t. operatsioonidel on kahendkood); siin on mitmeid võimalusi sõltuvalt argumentide ja tulemuse adresseerimise võimalustest:
- operandid (mõlemad mälupesadest), tulemus - mälupesasse (kolmeaadressiline süsteem - keerukas ja kallis);
- operandid (mõlemad) mälupesadest, tulemus - fikseeritud registrisse või
- üks operand - mälupesast, teine ja tulemus - fikseeritud registritest (üheaadressiline süsteem).

Operatsiooni sooritamine, selle tulemuse salvestamine ja järgmisena täidetava instruktsiooni valik võib sõltuda mingitest väärtustest (mis on salvestatud mälupesades või registrites), seega on tarvis ka tingimuslauset: if (mälupesa/registri väärtus) then (operatsioon). Kõige lihtsamal juhul "operatsioon" on suunamine järgmisena täidetava käsu märgendile (s.t. tingimuslause üldkuju oleks if true then goto märgend, seega kogu "if-käsk" oleks kahe argumendiga (if tingimus then goto märgend1 else goto märgend2 - kolmeljaadressiline.

Kuid sõltumata sellest, milline on operatsioonide kirjeldamise süntaks, on arvuti vaid lõplik automaat, mille olekute arv on 2**(mälupesade+registrite arv) - arvuti olek (igal hetkel) on määratud tema mälupesade ja registrite sisuga.

Operatsiooni (automaadi oleku muutus) salvestatakse arvutis bitijadana, kus on kirjas operandide aadressid, operatsiooni kood ja tulemuse aadress, seega (lihtsustatult) käsk võiks olla selline:

001010100011

- võta arvud pesadest 001 ja 010, liida need (kood 100) ja salvesta tulemus pesas 011 - see oleks binaarne masinkood kolmeaadressilises süsteemis.

Arvutis salvestatakse bitid kaheksakohaliste rühmadena, baitidena; ühe baidi (8 bitti ehk 28 erinevat võimalust) sisu on lihtsam kirjutada kahe kuueteistkümnendsüsteemi (hexa) arvuga, seega tegelikult esitatakse masinkoodi programm sellisena:

   B0 13 CD 10 33 C0 BF B0 01 B9 00 7D F3 AB BA C8
03 EE 42 FE C9 80 FB 3C 73 05 80 C3 04 EB 08 80
FF 3C 73 03 80 C7 04 8A C3 EE 8A C7 EE 32 C0 EE
E2 E3 B1 C8 81 06 AC 01 E9 62 80 06 AC 01 62 81

...

Selline esitusviis on äärmiselt inimvaenulik - inimesed teevad väga palju vigu selliste jadade käsitlemisel. Esimese sammuna koodi "inimlikustamisel" oli nn sümboolsete nimede kasutuselevõtt, näit liitmist hakati koodi asemel tähistama sõnaga add, korrutamist - sõnaga mult, väärtuse liigutamist ühest pesast teise - sõnaga move jne. Selliste lühendite ja muutujate tähistuste (identifikaatorite) kasutuselevõttuga sündis assemblerkeel. Assembler on vaid masinkoodi bitijadade (mehhaaniline) asendus operatsioonide ja muutujate nimedega, kuid inimesele juba palju lihtsamini arusaadavam ja lihtsam.

Järk-järgult lisandusid programmeelimiskeeltesse üha keerukamad konstruktsioonid (if-then-else, tsüklid, moodulid, objektid jne jne), kuid kõik kaasaegsete programmeerimiskeelte (nn "kõrgtaseme keelte") konstruktsioonid tuleb lõppude-lõpuks teisendada masinkoodi - see on ainuke, millest protsessor "aru saab".

Kuigi tavaliselt nn "lõpptarbija programme" ei kirjutata assembler-is, vaid mõnes kõrgtaseme keeles, kuid süsteemiprogrammeerimises (näiteks operatsioonisüsteemide programmeerimisel, kus on väga oluline programmi kiirus) kasutatakse assemblerkeeli väga sageli. Windows operatsioonisüsteemi programmides on kasutatud kümneid assemblerkeeli (Gas, NASM, FASM, SpAsm, TASM jne, neist tuntuim on kahtlemata Masm32 - Microsoft Assembler). Mõned assemblerkeeled sarnanevad juba "tavaliste" kõrgtaseme keeltega ja nende süntaks võib olla isegi lihtsam kui C-s või Java-s.

Pisa matemaatik Leonardo Fibonacci (Filius Bonacci - Bonaccio poeg) avastas arvrea 0, 1, 1, 2, 3, 5, 8, ... (iga järgmine on kahe eelmise summa) aastal 1202, kui ta juurdles küülikute kiire paljunemise probleemi üle: "Kui meil on üks paar vastsündinud külikuid ja iga täiskasvanud (s.t. ühe kuu vanune) paar "taastoodab" uue paari, siis mitu paari on meil 6, 12, 20... kuu pärast?" (lihtsustamiseks oletame, et küülikud on surematud). Hiljem on leitud, et sama arvrida esineb väga erinevates kohtades: päevalilleseemnete arv spiraalis, paljude lillede õielehtede arv, kuid ka arvutivõrkude optimiseerimisel esinevad Fibonacci arvud. Järgnevas on näitena HLA-s ( High Level Assembler, Randall Hyde poolt algselt programmeerimise õpetamiseks loodud assemblerkeel, vabalt mahalaaditav) kirjutatud programm, mis arvutab esimesed nelikümmend Fibonacci arvu ja trükib neist välja viimased viis.

Masinkoodi ja assembleri kasutamise peamine eesmärk on saada kiirem programm - selline, mis võimalikult effektiivselt kasutaks protsessori tsükleid ja minimiseeriks infovahetusi protsessori ja RAM-mälu vahel. Masinkoodis või assembleris kirjutatud programmid on ka kümneid kordi väiksemad kui kõrgkeeles kirjutatud programmid, sest nad kasutavad "otse" protsessori võimalusi. Järgnev programm kasutab Intel-i x86-protsessori registrid (protsessori oma, sisemine mälu) eax, ebx, ecx, edx on mis võimaldavad kiiresti (RAM-mälu poole pöördumata) arvutusi sooritada, sellepärast on see palju kiirem kui ükskõik millises kõrgkeeles kirjutatud programm, sest siin välditakse täielikult von-Neumann-i arvuti "pudelikael" - protsessori ja RAM-mälu vahelised infovahetused - need on palju aeglasemad kui protsessori sees toimivad arvutused. C- ja Java-keelt tundvale peaks järgnev tekst olema täiesti arusaadav - registris eax hoitakse kogu aeg viimast Fibonacci arvu, registris ebx - eelviimast; kuna järgmise Fibonacci arvu leidmise käsk

add(ebx, eax)

(see on samaväärne "tavalise" kõrgkeele käsuga eax = ebx + eax ) "kirjutab üle" viimase eax väärtuse (seda on tarvis järgmise Fibonacci arvu leidmiseks), varutakse eax väärtus ka registris ecx; tulemuse väljatrükk muutub registriväärtuste kasutamise tõttu veidi pikemaks, sest HLA "tavaline" väljastamiskäsk stdout.put väljastab registriväärtused heksadetsimaalarvudena, sellepärast peab need väljastama käsuga stdout.puti32, mis väljastab 32-bitised registriväärtused kümnendsüsteemi arvudena. Fibonacci rida kasvab väga kiiresti, sellepärast peab selle liikmete arvutamisel teadma, mitme bitiga kasutatud programmeerimiskeeles täisarve esitatakse: pärast 23-ndat Fibonacci arvu ei ole 16 bitti enam piisav (tekib ületäitumine), pärast 46-ndat ei rahulda enam 32-bitine esitus ja pärast 92-st (vähem kui kaheksa aastat küülikuid...) tuleb ületäitumine isegi 64-bitise esituse korral. HLA kasutab 32-bitist esitust, kuid mõnedes programmeerimiskeeltes (Common Lisp, maple, mathematica, O'Caml, Scheme, Python) võimaldavad kasutada tõkestamata esitamistäpsusega arve - vajaduse korral varutakse arvudele rohkem mälu; suvalise pikkusega esitust realiseerivad teegid BigFloat on olemas mitmele keelele (Perl, Ruby).
program fib;
#include( "stdlib.hhf" );

begin fib;
// initsialiseerimine:
mov( 1, ebx );
mov( 1, eax );
mov( 1, ecx );
mov( 2, edx );
// tsykkel ylejäänud väärtuste arvutamiseks:
while( edx <= 40 ) do
add( ebx, eax );
mov( ecx, ebx ) ;
//nihutame muutujaid:
mov( eax, ecx);
//trykime viis viimast väärtust:
if(edx > 35) then
stdout.puti32(edx);
stdout.put( "-s Fibonacci arv on ");
stdout.puti32(eax);
stdout.put( nl );
endif;
// suurendame tsykliloendajat ja lõpetame tsykli ja programmi:
add( 1, edx );
endwhile;
end fib;

Kui see programm on salvestatud (näteks) failis fibo.fla, saame programmi transleerimisel käsuga (Windows-i DOS-aknas; HLA keele translaator teisendab sisendteksti Masm32-failiks ja käivitab seejärel Masm32-translaatori)

hla fibo.fla

kävituskõlbliku .exe-faili fibo.exe, mis selle käivitamisel (käsuga fibo - .exe-failile pole laiendit tarvis anda) väljastab Fibonacci arvud:
   36-s Fibonacci arv on 24157817
37-s Fibonacci arv on 39088169
38-s Fibonacci arv on 63245986
39-s Fibonacci arv on 102334155
40-s Fibonacci arv on 165580141

Ülalesitatud kõrgtaseme assembler-is kasutatakse juba mitmeid "päris" kõrgtaseme programmeerimiskeelte omadusi (if-suunamist, while-tsüklit), kuid selles ei ole võimalik näiteks aritmeetiliste avaldiste kasutamine (HLA-s on siiski lubatud käskude substitutsioon). Ülaltoodud programmis kasutatatud käsul add peab olema alati kaks argumenti ja tulemus salvestatakse teises (seega kirjutatakse üle viimase Fibonacci arvu väärtus, sellepärast tuligi kasutada abimuutujat ecx viimase Fibonacci arvu ajutiseks salvestamiseks); kui oleks tarvis arvutada näiteks avaldis a*a + b*b, peab selle lahutama elementaarseteks kahe argumendiga operatsioonideks add, mult jne, s.t. tavalises matemaatilises kirjaviisis kasutatavad avaldised ja juhtimiskonstruktsioonid peab kõik "lahti harutama" assembler-i fikseeritud formaadiga elementaaroperatsioonideks. Käesoleva kursuse eesmärk ongi näidata, kuidas (näiteks) kõrgtaseme keele käsk

IF A OR (B AND C) THEN X := W + Y + Z
teisendatakse (automaatselt) mingisse väga lihtsasse keeelde, kas assemblerkoodiks või näiteks nn kolmeaadressiliseks koodiks:
100 IF A GOTO 106
101 GOTO 102
102 IF B GOTO 104
103 GOTO 108
104 IF C GOTO 106
105 GOTO 108
106 T1 := W + Y
107 T2 := T1 + Z
108 X := T2

Kolmeaadressiline kood on üks paljudest nn vahekujudest - selles ei ole enam muutuva süntaksiga osasid (nagu on kõrgtaseme keeles näiteks aritmeetilised avaldised) ja see on juba piisavalt lihtne, et seda lihtsalt ja kiiresti saaks tõlkida konkreetse protsessori või näiteks ülalvaadeldud kõrgtaseme assembleri HLA koodiks. Samasugune vahekuju on ka näiteks Java transleerimisel esimese etapi tulemus - baitkood.

Osutub, et kogu probleem (translaator) on stringide teisendamine (nii lähtetekst kui ka tulemus on märgijadad, s.t. stringid!) ja seda on võimalik (etappide kaupa) nii kirjeldada, et kogu teisenduse programm (s.t. translaator) genereeritakse sellest kirjeldusest automaatselt.

Kuid sellisest formaalsest translaatorite genereerimise süsteemi kirjeldamiseks ja sellest arusaamiseks on tarvis arusaamist ja vilumust sümbolijadade hulkade (keelte) teisendamise formalismidest - abstraktsetest automaatidest ja grammatikatest.


Ülesandeid:
1. Ylesande tekst

Kysimused, probleemid: ©2004-2013-2009 Jaak Henno