Documente Academic
Documente Profesional
Documente Cultură
CURS
LIMBAJE FORMALE ȘI TRANSLATOARE
1. PROBLEME GENERALE
1.1. Introducere
Pentru implementarea studiilor de caz se va utiliza limbajul de programare C/C++ și în anumite părți
limbajul de asamblare I80X86.
De asemenea, algoritmii sunt prezentați în pseudo-cod, putând fi implementați apoi în orice limbaj de
programare.
În etapa de implementare, după analiza problemei și stabilirea algoritmului, acesta trebuie tradus
(implementat) într-un limbaj de programare.
Compilarea
Procesul de compilare este realizat cu ajutorul compilatorului, care translatează codul sursă în cod
obiect (cod maşină), pentru ca programul să poată fi înţeles de calculator.
În cazul limbajului C, în prima fază a compilării este invocat preprocesorul. Acesta recunoaşte şi
analizează mai întâi o serie de instrucţiuni speciale, numite directive procesor. Verifică apoi codul sursă
pentru a constata dacă acesta respectă sintaxa12 şi semantica limbajului. Dacă există erori, acestea sunt
semnalate utilizatorului.
Utilizatorul trebuie să corecteze erorile (modificând programul sursă). Abia apoi codul sursă este
translatat în cod de asamblare, iar în final, în cod maşină, binar, propriu calculatorului. Acest cod binar
1
https://ro.wikipedia.org/wiki/Sintaxa_limbajului_C
2 https://ro.wikipedia.org/wiki/C_(limbaj_de_programare)
1
LIMBAJE FORMALE SI TRANSLATOARE 2017
este numit cod obiect şi de obicei este memorat într-un alt fişier, numit fişier obiect. Fişierul obiect va
avea, de obicei, acelaşi nume cu fişierul sursă şi extensia *.obj.
Linkeditarea
După ce programul sursă a fost translatat în program obiect, el este va fi supus operaţiei de
linkeditare. Scopul fazei de linkeditare este acela de a obţine o formă finală a programului, în vederea
execuţiei acestuia. Linkeditorul “leagă” modulele obiect, rezolvă referinţele către funcţiile externe și
rutinele din biblioteci și produce cod executabil, memorat într-un alt fișier, numit fişier executabil (acelaşi
nume, extensia .exe)
Execuţia
Lansarea în execuţie constă în încărcarea programului executabil în memorie şi startarea execuţiei
sale.
Observaţii:
Mediile de programare integrate (BORLANDC, TURBOC) înglobează editorul, compilatorul,
linkeditorul şi depanatorul (utilizat în situaţiile în care apar erori la execuţie);
Dacă nu se utilizează un mediu integrat, programatorul va apela în mod explicit (în linie de
comandă) un editor de texte, compilatorul, linkeditorul. Lansarea în execuţie se va face tot din linie de
comandă.
2
LIMBAJE FORMALE SI TRANSLATOARE 2017
Oricum, pentru ca limbajele de nivel înalt să fie utilizabile, trebuie să existe unelte care să
convertească programele scrise în aceste limbaje în șiruri de biți pe care îi înțelege mașina. Pe la
începuturile limbajelor de programare s-a observat că dacă se impun constrângeri asupra sintaxei unui
limbaj de nivel înalt, procesul de translație devine unul care poate fi automatizat.
Aceasta a condus la dezvoltarea de translatoare și compilatoare – programe care acceptă (ca
date) o reprezentare textuală a unui algoritm exprimat într-un limbaj sursă, și care produce (ca ieșire
primară) o reprezentare a aceluiași algoritm într-un alt limbaj, limbajul obiect sau țintă.
Incepătorii adesea nu fac distincție între faza de compilare și cea de execuție în cadrul dezvoltării
și utilizării programelor scrise în limbaje de nivel înalt. Aceasta este explicabilă prin faptul că translația
(compilarea) este adesea invizibilă, sau invocată cu o tastă funcțională specială din cadrul unui mediu de
dezvoltare integrat (IDE), care are și alte taste funcționale rapide. Mai mult, începătorii învață
programarea fără a se face în mod clar această distincție, adesea instrucțiunile limbajului fiind explicate în
modul: ”când un computer execută o instructiune read acesta citește date de la intrare într-o variabilă”.
Această explicație ascunde câteva operații de nivel jos. Implicațiile manipularii fișierelor, conversiei
caracterelor și alocării spațiului de stocare sunt ignorate, deși acestea sunt necesare pentru a programa
calculatorul să inteleaga cuvantul read. Oricine a încercat să programeze operații de I/O în limbaj de
asamblare știe că multe dintre acestea nu sunt prea simplu de implementat.
Un translator, fiind el însuși un program, trebuie să fie scris într-un limbaj de programare, cunoscut
ca limbajul gazdă sau de implementare. La ora actuală, rar se întâlnesc translatoare dezvoltate în limbaj
mașină de la zero. Evident primele translatoare au trebuit sa fie scrise în acest mod și la începerea
dezvoltării unui translator pentru orice sistem nou trebuie revenit la limbajul mașină și la arhitectura
mașinii pentru acel sistem. Chiar și așa, translatoarele pentru mașinile noi sunt dezvoltate totuși în limbaje
de nivel înalt, utilizând adesea tehnicile de cross-compilare3 și bootstrapping4, care vor fi prezentate mai
în detaliu în capitolele următoare.
Primele translatoare importante scrise se pare ca au fost compilatoarele Fortran dezvoltate de
echipa lui Backus la IBM în 1950, deși existau atunci și uneltele pentru dezvoltarea codului mașină.
Primul compilator Fortran a fost un software creat de un programator 18 ani. Este interesant de observat
ca una dintre primele preocupari a fost să dezvolte un sistem care să poată produce cod obiect, a cărui
eficiență de execuție să se poată compara cu cea a unui expert uman. Un proces de translație automat
poate rareori să produca cod la fel de optim ca cel scris de un programator bun în limbaj mașină, și din
aceste motive până în zilele noastre componente importante din cadrul sistemelor sunt adesea scrise în
limbaj mașină, pentru a economisi timp și spațiu.
Programele translator nu sunt niciodată complet portabile (deși anumite părți ale lor pot fi) și de
obicei depind de alte programe sistem pe care le are la dispoziție utilizatorul. În particular, operațiile de
I/O și manipularea fișierelor pe sistemele moderne sunt controlate de sistemul de operare. Acesta este un
program sau o suita de programe și rutine a caror rol este să controleze execuția altor programe pentru a
realiza cel mai bine alocarea resurselor, cum ar fi imprimante, plottere, disk-uri, utilizând adesea tehnici
sofisticate cum ar fi procesarea paralelă, multiprogramarea și altele.
3 A cross compiler is a compiler capable of creating executable code for a platform other than the one on which the compiler is
running. For example, a compiler that runs on a Windows 7 PC but generates code that runs on Android smartphone is a cross
compiler. https://en.wikipedia.org/wiki/Cross_compiler
4 In general parlance, bootstrapping usually refers to a self-starting process that is supposed to proceed without external input.
In computer technology the term (usually shortened to booting) usually refers to the process of loading the basic software into the
memory of a computer after power-on or general reset, especially the operating system which will then take care of loading other
software as needed.
The term appears to have originated in the early 19th century United States (particularly in the phrase "pull oneself over a fence
by one's bootstraps"), to mean an absurdly impossible action. https://en.wikipedia.org/wiki/Bootstrapping
3
LIMBAJE FORMALE SI TRANSLATOARE 2017
Mulți ani dezvoltarea sistemelor de operare a necesitat utilizarea limbajelor de programare care au
rămas mai aproape de nivelul codului mașina, decât a limbajelor potrivite pentru programe știintifice sau
comerciale. Mai recent s-au dezvoltat mai multe limbaj de nivel înalt de succes, având ca scop principal
utilizare în proiectarea de sisteme de operare și de control în timp-real. Cel mai bun exemplu de asemenea
limbaj este limbajul C, dezvoltat inițial pentru implementarea sistemului de operare UNIX și acum utilizat
pe scară largă în toate domeniile.
4
LIMBAJE FORMALE SI TRANSLATOARE 2017
Verificarea erorilor: Fiind om, este normal ca programatorul să faca multe greșeli în procesul de
dezvoltare a unui program. Multe limbaje de nivel înalt – sau cel puțin implementarile lor – pot, și
adesea realizează, verificarea unui număr mare de erori atât în timpul compilarii cat și în timpul
execuției.
Alte aspecte ale proiectarii limbajelor care contribuie la succesul acestora sunt:
Ortogonalitate: Limbajele bune tind sa aiba un numar mic de caracteristici bine gandite care pot fi
combinate intr-un mod logic pentru a furniza blocuri de construire mai puternice. În mod ideal
aceste caracteristici nu trebuie sa influenteze intre ele și nu trebuie sa fie inconfurate de o gramada
de inconsistente, cazuri exceptionale și restrictii arbitrare.
5
LIMBAJE FORMALE SI TRANSLATOARE 2017
Majoritatea limbajelor au anumite bug-uri – de exemplu, în Pascal-ul original al lui Wirth o funcție
putea returna doar o valoare scalară, și nu una de orice tip structurat. Multe extensii la limbaje bine
puse la punct se dovedesc foarte valoroase datorită rectificarii unor asemenea neajunsuri.
Notații familiare: Majoritate computerelor sunt binare. Oamenii, pentru că au zece degete cu care
să numere și să socoteasca, pot răsufla ușurați pentru că limbajele de nivel înalt de obicei
realizează calcule în aritmetica zecimală și utilizează notatii matematice standard pentru operații.
Când sunt propuse limbaje noi, acestea adesea iau forma unor limbaj derivate sau dialecte ale celor
bine puse la punct, astfel încât programatorii să fie tentați să migreze spre limbajele noi și să se
simtă totuși stăpâni pe acestea – acesta a fost calea strabatută în dezvoltarea limbajului C++ din C,
a limbajului Java din C++ etc.
Pe lângă aspectele de mai sus, un limbaj de nivel înalt modern trebuie să indeplineasca urmatoarele
criterii suplimentare:
Definit clar: Trebuie să fie descris clar, atât pentru utilizator cât și pentru cel care scrie
compilatorul
Translatat rapid: Trebuie să permită translatarea rapidă, astfel încât să nu fie depășit timpul de
dezvoltare a programului atunci când se utilizeaza limbajul.
Modularitate: Este de dorit ca programele să poata fi dezvoltate în limbajul respectiv ca o colecție
de module compilate separat, cu mecanisme corespunzatoare pentru a asigura legătura între aceste
module.
Eficiență: Trebuie să permită generarea de cod obiect eficient.
Disponibilitatea: Trebuie să fie posibilă asigurarea de translatoare pentru toate mașinile importante
și pentru toate sistemele de operare importante.
Importanța unei descrieri sau specificări clare a limbajului nu poate fi mai accentuată. Aceasta trebuie
să se aplice, mai întâi, asupra sintaxei limbajului 0 adică, trebuie sa specifice în mod exact ce forma poate
lua un program sursa.
Trebuie să se aplice, apoi, asupra semanticii statice a limbajului – de exemplu, trebuie să fie clarificat
ce constrângeri trebuie să satisfacă entitatile de diferite tipuri utilizate, sau scopul pe care îl au diferiții
identificatori pe parcursul textului programului. În sfârșit, specificarea trebuie să se aplice și semanticii
dinamice a programelor care satisfac regulile sintactice și semantice statice – adică, trebuie să poată
prezice efectul pe care orice program scris în acel limbaj îl va avea când este executat.
Descrierea limbajelor de programare este extrem de dificil de realizat exact, în special dacă se încearcă
realizarea în limba vorbita, de exemplu engleza. Exista o tendință crescătoare de a utiliza formalismul,
care va fi prezentat mai tarziu. Metodele formale au avantajul preciziei, deoarece utilizează notații
matematice bine definite. Un dezavantaj al acestora este faptul ca nu sunt foarte concise. De exemplu,
descrierea informala a limbajului Modula-2 a fost de circa 35 de pagini, în timp ce descrierea formala
realizata de o comisie ISO are peste 700 de pagini.
Specificatiile formale au în plus avantajul ca, în principiu, și din ce în ce mai mult în practică, pot fi
utilizate pentru a ajuta la automatizarea implementarii translatoarelor pentru limbaj. Intradevar, tot mai rar
se gasesc compilatoare moderne care sa fie implementate fara ajutorul generatoarelor de compilatoare.
Acestea sunt programe care preiau ca marime de intrare o descriere formala a sintaxei și semanticii unui
limbaj de programare și produc la iesire parti mari dintr-un compilator pentru acel limbaj.
În capitolele urmatoare se vor prezenta modul de utilizare a unor asemenea generatoare de
compilatoare, dar și modul de realizare a unui compilator pornind de la zero.
6
LIMBAJE FORMALE SI TRANSLATOARE 2017
Un translator poate fi definit ca o functie a carui domeniu este un limbaj sursa și codomeniu este un limbaj
destinatie sau limbaj obiect.
Instructiuni in Instructiuni in
Translator
lim bajul sursa lim bajul destinatie
Cei care au putina experiente în translatoare stiu ca rareori se considera ca facand parte din functia
translatorului sa execute algoritmul implementat în sursa, ci doar sa modifice reprezentarea acestuia dintr-
o forma în alta. De fapt, în dezvoltarea translatoarelor sunt implicate cel putin trei limbaje: limbajul sursa
care va fi translatat, limbajul obiect sau destinatie care va fi generat și limbajul gazda care este utilizat
pentru implementarea translatorului. Daca translatia are loc în mai multe etape, ar putea exista și alte
limbaje intermediare. Majoritatea dintre acestea – limbajul gazda și chiar limbajele obiect – raman de
obicei invizibile utilizatorului limbajului sursa.
2.1. Diagrame T
O notatie utila pentru descrierea unui program de calculator, în particular un translator, utilizeaza
diagramele T. În fig.2.1. se prezinta cateva exemple.
7
LIMBAJE FORMALE SI TRANSLATOARE 2017
În continuare în cadrul acestor diagrame vom folosi notatia „cod-M” pentru cod mașina. Translatia este
reprezentata printr-o sageata pe o mașina, avand programul sursa și programul obiect în partea stanga și
repsectiv dreapta, asa cum se vede în fig.2.2.
Putem privi aceasta combinatie ca fiind o mașina abstracta (denumita uneori și mașina virtuala), a carei
scop este sa converteasca programe sursa Turbo Pascal în echivalentul lor în cod mașina 80X86.
Diagramele T au fost introduse pentru prima data de Bratman în 1961 și imbunatatite ulterior de Earley și
Sturgis în 1970, iar de atunci sunt utilizate foarte des.
8
LIMBAJE FORMALE SI TRANSLATOARE 2017
bine pentru producerea de cod la nivel de asamblare, este mult mai dificil atunci cand se incearca
refacerea codului sursa scris original intr-un limbaj de nivel înalt, cum ar fi Pascal sau C.
Multe translatoare genereaza cod pentru mașinile lor gazda. Acestea se numesc translatoare rezidente.
Altele, denumite cross-translatoare, genereaza cod pentru alte masini decat cea gazda. Cross-
translatoarele sunt adesea utilizate impreuna cu microcomputere, în special în sistemele embedded, care
pot fi ele insele prea mici pentru a permite operarea satisfacatoare a translatoarelor rezidente. Deșigur,
cross-translatia introduce probleme suplimentare legate de transferarea codului obiect de la mașina donor
la mașina care executa programul tradus, și pot conduce la intarzieri în dezvoltarea programelor și
depanare grea.
Iesirea unor translatoare este cod mașina, lasat incarcat la o locatie fixata intr-o mașina gata pentru
executie imediata. Alte translatoare, denumite translatoare incarca-și-lanseaza (load-and-go) pot chiar sa
lanseze executia acestui cod. Oricum, un mare numar de translatoare nu produc cod mașina de adresa
fixata. Acestea produc ceva asemanator și anume o forma semicompilata, simbolic binara sau
realocabila. O utilizare frecventa a acestora este în dezvoltarea de biblioteci de rutine speciale, care pot fi
formate din rutine scrise în mai multe limbaje sursa. Rutinele compilate în acest mod sunt legate printr-un
program denumit linker, care poate fi privit ca fiind cel care furnizeaza ultima etapa a unui translator
multi-etape. Limbajele care incurajeaza compilarea separata a partilor de program, cum este și limbajul
C++, depind în mod critic de existenta unor asemenea linker-e. Aceste sisteme sunt vitale pentru
dezvoltarea de proiecte software foarte mari. În schimb pentru programele mici pot parea o piedica,
deoarece trebuie avut grija de mai multe fisiere și se pierde timp pentru legarea tuturor componentelor
impreuna.
Se pot utiliza diagremele T pentru a arata interdependenta dintre translatoare, programele de incarcare etc.
De exemplu, sistemul FST Modula-2 utilizeaza un compilator și un linker, asa cum se prezinta în fig.2.3.
Exista o faza analitica, în care programul sursa este analizat pentru a determina daca satisface cerintele
sintactice și semantice impuse de limbaj. Aceasta este urmata de o faza sintetica în care este generat
codul obiect corespunzator în limbajul destinatie. Componentele translatorului care realizeaza aceste doua
faze majore se spune ca sunt cuprinse în front end și în back end al compilatorului. Partea front end este
9
LIMBAJE FORMALE SI TRANSLATOARE 2017
independenta de mașina destinatie, în timp ce partea back end depinde foarte mult de mașina destinatie. În
cadrul acestei structuri se pot recunoaste componente sau faze mai mici, ca în fig.2.4.
Procesorul de caractere (Character handler) este sectiunea care comunica cu lumea exterioara, prin
sistemul de operare, pentru a citi caracterele care formeaza textul sursa. Deoarece seturile de caractere și
manipularea fisierelor difera de la un sistem la altul, aceasta faza este adesea dependenta de mașina sau de
sistemul de operare.
Analizorul lexical sau scanner-ul reprezinta sectiunea care grupeaza caracterele din textul sursa în
grupuri care formeaza din punct de vedere logic token-urile limbajului – simboluri cum sunt
identificatorii, sirurile, constantele numerice, cuvintele cheie (de ex.: while, if), operatorii (de ex. <=) etc.
Unele dintre aceste simboluri sunt reprezentate foarte simplu la iesirea scannerului, altele trebuie sa fie
asociate cu diverse proprietati cum ar fi numele sau valoarea lor.
Analiza lexicala este uneori usoara iar alteori este grea. De exemplu, instructiunea C++:
WHILE keyword
(
A identifier name A
> operator comparison
3 constant literal value 3
* operator multiplication
10
LIMBAJE FORMALE SI TRANSLATOARE 2017
B identifier name B
)
A identifier name A
= operator assignment
A identifier name A
- operator subtraction
1 constant literal value 1
10 DO 20 I = 1 . 30
este mai inselatoare. Cei familiari cu limbajul Fortran ar putea spune ca se decodifica în:
10 label
DO keyword
20 statement label
I INTEGER identifier
= assignment operator
1 INTEGER constant literal
, separator
30 INTEGER constant literal
în timp ce cei rautaciosi ar putea sa vada instructiunea asa cum este de fapt:
10 label
DO20I REAL identifier
= assignment operator
1 . 30 REAL constant literal
Trebuie cautat cu atentie pentru a distinge punctul de virgula asteptata. Spatiile sunt irelevante în Fortran.
Programatorii trebuie sa fie intradevar rautaciosi ca sa foloseasca identificatori avand spatii care nu sunt
necesare dar sunt sugestive. În timp ce limbajele ca C++, Basic etc au fost proiectate în asa fel încât
analiza lexicala sa fie separata în mod clar fata de restul analizei, acelasi lucru evident nu este adevarat
pentru limbajul Fortran și altele care nu au cuvinte cheie (keywords) rezervate.
Analizorul sintactic sau parser-ul grupeaza token-urile produse de scanner în structuri sintactice – lucru
pe care il face prin parsing expresii și instructiuni. (Acest proces este asemanator cu modul de analiza a
unei propozitii pentru a gasi componente ca „subiect”, „predicat” etc). Adesea parser-ul este combinat cu
analizorul de constrangeri contextuale, a carui rol este sa determine ca dintr-o structura sintactica toate
componentele satisfac reguli de scop și reguli de tip în contextul structurii analizate. De exemplu, în C++
sintaxa instructiunii WHILE este descrisa ca fiind:
Este normal sa consideram o instructiune de forma de mai sus cu orice tip de Expresie ca fiind sintactica
corecta, dar aceasta instructiune nu are nici o semnificatie reala decat daca valoarea Expresiei este
11
LIMBAJE FORMALE SI TRANSLATOARE 2017
constransa (în acest caz) la un tip Boolean. Nici un program nu are nici o semnificatie pana cand este
executat dinamic. Oricum, este posibil în cazul limbajelor puternic tipizate sa se prevada din perioada
compilarii daca programele sursa nu au nici o semnificatie (adica, din punct de vedere static, inainte de a
se incerca executarea programului în mod dinamic). Semantica este un termen utilizat pentru a descrie
„semnificatia” și astfel analizorul de constrangeri este adesea denumit analizor semantic static, sau pur și
simplu analizor semantic.
Iesirea fazelor corespunzatoare analizorului sintactic și analizorului semantic este cateodata exprimata în
forma unui arbore sintactic abstract (AST=abstract syntax tree) decorat. Aceasta este o reprezentare
foarte utila, pentru ca poate fi utilizata în mod ingenios pentru a optimiza generarea codului intr-o faza
ulterioara.
Pe cand sintaxa concreta a multor limbaje de programare incorporeaza multe cuvinte cheie și token-uri,
sintaxa abstracta este destul de simpla, retinand doar acele componente ale limbajului necesare pentru a
captura continutul real și (în ultima instanta) semnificatia programului. De exemplu, în timp ce sintaxa
concreta a unei instructiuni WHILE necesita prezenta cuvantului cheie WHILE, asa cum s-a prezentat mai
sus, componentele esentiale ale instructiunii WHILE sunt pur și simplu Expresia (booleana) și
instructiunea Statement.
Atfel, instructiunea C++:
WHILE (1<P && P<9) P=P+Q;
Se reprezinta prin arborele AST din fig.2.5.
Fig.2.5. Arborele AST pentru instructiunea WHILE (1<P && P<9) P=P+Q;
Un arbore sintactic abstract independent este lipsit de cateva detalii semantice; analizorul semnatic are
sarcina de a adauga „tip” și alte informatii contextuale în nosuri (de unde provine și denumirea de arbore
„decorat”).
Deșigur, se pot construi și arbori sintactici concreti. Instructiunea C++:
WHILE (1<P && P<9) P=P+Q;
Poate fi figurata în detaliu prin arborele din fig.2.6.
12
LIMBAJE FORMALE SI TRANSLATOARE 2017
Fig.2.6. Arborele sintactic concret pentru instructiunea WHILE (1<P && P<9) P=P+Q;
Toate fazele prezentate pana acum sunt de natura analitica. Cele care urmeaza sunt mai mult sintetice.
Prima dintre acestea ar putea fi un generator de cod intermediar, care, în practica, poate fi integrat și în
fazele anterioare sau chiar omis în cazul unor translatoare foarte simple. Acesta utilizeaza structurile de
date produse de fazele anterioare pentru a genera o forma a codului, poate chiar sub forma unor schelete
sau macrouri simple de cod, sau limbaj de asamblare sau chiar cod de nivel înalt pentru procesarea de
catre un asamblor extern sau de catre un compilator separat. Diferenta de baza dintre codul intermediar și
codul mașina propriu-zis este aceea ca în cazul codului intermediar nu trebuie specificate în detaliu
informatii de tipul registrii mașina exacti care se vor folosi, adresele exacte la care se face referire etc.
Instructiunea C++ considerata ca exemplu:
WHILE (1<P && P<9) P=P+Q;
Ar putea conduce la codul intermediar echivalent:
L0 if 1<P goto L1
goto L3
L1 if P<9 goto L2
goto L3
L2 P=P+Q
goto L0
L3 continue
Sau, ar putea produce ceva de genul:
L0 T1=1<P
13
LIMBAJE FORMALE SI TRANSLATOARE 2017
T2=P<9
if T1 and T2 goto L1
goto L2
L1 P=P+Q
goto L0
L2 continue
Totul depinde daca implementarea translatoarelor utilizeaza abordarea denumita conjunctie secventiala
sau scurt-circuit pentru a manipula expresiile Booleene compuse (ca în prmul caz) sau abordarea
operatorului Boolean (al doilea caz). Limbajul C++ necesita abordarea scurt-circuit. Oricum, exista
limbaje, cum ar fi limbajul Pascal, care nu specifica ce abordare sa se utilizeze.
Poante exista suplimentar și un optimizator de cod, pentru a imbunatatii codul intermediar în favoarea
vitezei sau spatiului sau ambelor. Utilizand acelasi exemplu ca mai sus, o optimizare evidenta ar conduce
la codul echivalent urmator:
L0 if 1>=P goto L1
If P>=9 goto L1
P=P+Q
goto L0
L1 continue
Cea mai importanta faza în partea back end revine ca responsabilitate generatorului de cod. În cadrul
unui compilator real, aceasta faza preia iesirea de la faza precedenta și produce codul obiect, luand decizii
în privinta locatiilor de memorie pentru date, generarea codului pentru accesarea acestor locatii, selectia
registrilor pentru calcule intermediare și indexari etc. În mod evident aceasta este o faza care necesita
multa pricepere și atentie la detalii, daca se doreste ca produsul finit sa fie eficient. Unele translatoare trec
la o alta faza prin incorporarea unui asa numit optimizator de control în cadrul caruia se incearca
reducerea operatiilor care nu sunt necesare prin examinarea mai în detaliu a unor secvente scurte din codul
generat.
În continuare este listat codul generat de diverse compilatoare pentru instructiunea analizata anterior. Este
evident ca fazele de generare a codului pentru aceste compilatoare sunt complet diferite. Asemenea
14
LIMBAJE FORMALE SI TRANSLATOARE 2017
diferente pot avea un efect profund asupra dimensiunii programului și a vitezei de executie
Un translator în mod inevitabil utilizeaza o structura complexa de date, denumita tabela de simboluri, în
care se memoreaza numele utilizate în program și proprietatile asociate acestora, cum ar fi tipul lui,
necesitatile de stocare (în cazul variabilelor) sau valorile lor (în cazul constantelor).
Asa cum bine se stie, utilizatorii limbajelor de nivel înalt pot face multe erori în procesul de dezvoltare a
programelor indiferent cat se simple ar fi acestea. Astfel diferitele faze ale unui compilator, în special
primele faze, comunica și cu un hadler de erori sau un raportor de erori, care este invocat la detectarea
erorilor. Este de dorit ca compilarea programelor cu erori sa fie continuata, daca este posibil, astfel încât
utilizatorul sa poata corecta mai multe erori din sursa inaintea recompilarii. Aceasta ridica unele aspecte
interesante privind proiectarea tehnicilor de redresare a erorilor și de corectie a erorilor. (Vorbim de
redresarea erorilor atunci cand procesul de translatie incearca sa continue dupa deterctarea unei erori și de
corectarea erorilor sau repararea erorilor atunci cand incearca sa corecteze eroarea din contexe – de obicei
un subiect controversat, deoarece corectia poate sa fie departe de ce a gandit initial programatorul).
Detectarea erorilor din codul sursa în timpul compilarii nu trebuie confundata cu detectarea erorilor în
timpul executiei, adica atunci cand se executa codul obiect. Multe generatoare de cod sunt responsabile de
adaugarea codului de verificare a erorilor la programul obiect (pentru a verifica, de exemplu, daca indexii
pentru tablouri sunt intre limitele impuse). Acest lucru poate fi destul de rudimentar, sau poate implica
adaugarea unei cantitati considerabile de cod și structuri de date pentru a fi utilizate cu sisteme de
15
LIMBAJE FORMALE SI TRANSLATOARE 2017
depanare sofisticate. Asemenea cod auxiliar poate reduce în mod drastic eficienta unui program și unele
compiltoare permit suprimarea acestei caracteristici.
Uneori greselile din program detectate în timpul compilarii sunt denumite erori, iar cele care apar în
timpul executiei sunt denumite exceptii, dar nu exista o terminologie universal valabila pentru acestea.
Analizand fig.2.4 s-ar parea ca compilatoarele lucreaza în mod serial și ca fiecare faza comunica cu
urmatoarea prin intermediul unui limbaj intermediar potrivit, dar în practica distinctia dintre diferitele faze
adesea devine insesizabila. Mai mult, multe compilatoare sunt de fapt construite în jurul unui parser
central ca și componenta dominanta, cu o structura mai asemanatoare cu cea din fig.2.7.
16
LIMBAJE FORMALE SI TRANSLATOARE 2017
În practica, se utilizeaza foarte mult translatoarele cu doua treceri în care prima etapa este un translator de
nivel înalt care converteste programul sursa în asamblare, sau chiar în alt limbaj de nivel relativ înalt
pentru care exista deja un translator bun. Procesul de compilare ar putea fi reprezentat atunci ca în fig.2.8
– exemplul prezinta un program Modula-3 pregatit pentru a fi rulat pe o mașina care are un convertor
Modula-3 în C:
Este tot mai uzual sa se gaseasca compilatoare pentru limbaje de nivel înalt care au fost implementate în
limbajul C și care produc ele insele cod C la iesire. Succesul acestora se bazeaza pe premizele ca „toate
computerele moderne sunt echipate cu un compilator C” și „codul sursa scris în C este intradevar
portabil”. Niciuna dintre aceste premize, din pacat, nu este adevarata în intregime. Oricum, compilatoarele
scrise în acest mod sunt la fel de aproape de a-și realiza visul de portabilitate pe cat sunt și oricare dintre
cele existente în momentul de fata. Modul în care asemenea compilatoare pot fi utilizate este prezentat în
capitolul urmator.
17
LIMBAJE FORMALE SI TRANSLATOARE 2017
simularea unei masini virtuale pentru care codul intermediar este chiar codul sau mașina. Distinctia dintre
abordarile de executie cod mașina și pseudocod este prezentata pe scurt în fig.2.9.
Putea reprezenta procesul utilizat în cadrul unui compilator interpretativ ruland sub MS-DOS pentru un
limbaj jucarie cum este Clang, care va fi prezentat în capitolele urmatoare, sub forma diagramelor T din
fig.2.10.
Nu este necesara limitarea interpretoarelor la lucrul doar cu iesiri intermediare ale unui translator. Mai
general, deșigur, chiar și o mașina reala poate fi privita ca un interpretor foarte specializat – unul care
executa instructiuni la nivel mașina prin preluarea, analiza și apoi interpretarea lor una cate una. În cadrul
unei masini reale acestea toate se realizeaza prin hardware și, deci, foarte rapid. Pe aceeasi idee, se poate
observa ca se poate scrie un program care sa permita ca o mașina reala sa emuleze orice alta mașina reala,
chiar daca poate mai lent, doar prin scrierea unui interpretor – sau, cum este mai des denumit, un
emulator – pentru a doua mașina.
De exemplu, am putea dezvolta un emulator care ruleaza pe mașina Sun SPARC și o face sa para un IBM
PC (sau invers). O data ce am realizat acest lucru, suntem (în principiu) în situatia de a executa pe o
mașina Sun SPARC orice software dezvoltat pentru un IBM PC – software-ul PC devine efectiv portabil.
Notatia prin diagrame-T este usor de extins pentru a lucra cu conceptul de asemenea masini virtuale. De
exemplu, rularea lui Turbo Pascal pe o mașina Sun SPARC poate fi prezentata ca în fig.2.11.
Lucrul cu interpretoare/emulatoare este utilizat pe scara larga în proiectarea și dezvoltarea atat a noilor
masini, cat și a software-ului ce ruleaza pe acestea.
Utilizarea interpretoarelor poate avea cateva puncte favorabile, dupa cum urmeaza:
Este mult mai usor sa se genereze cod mașina ipotetic (care poate fi prelucrat profilul limbajului
sursa original) decat cod mașina real (care trebuie sa aiba de-a face cu profilul fara compromisuri
al masinii reale).
Un compilator scris pentru a produce (ca iesire) cod pseudo-mașina bine definit capabil de o
interpretare usoara pe mai multe masini poate fi facut foarte portabil, în special daca este scris intr-
un limbaj gazda care este disponibil pe scara larga (cum este ANSI C) sau chiar daca este
disponibil implementat deja în propriul sau pseudocod.
Poate fi facut mai usor „prietenos cu utilizatorul” decat se poate prin abordarea codului nativ.
Deoarece interpretorul lucreaza mai aproape de codul sursa decat o face un program translatat
complet, se pot asocia direct acestei sursa mesaje de eroare sau alte ajutoare de depanare.
O mare varietate de limbaje pot fi implementate destul de usor intr-o forma utila pe o mare
varietate de masini diferite. Aceasta se realizeaza prin producerea de cod intermediar la un
standard bine definit, pentru care ar trebui sa fie usor de implementat pe orice mașina reala un
interpretor destul de eficient.
Se dovedeste a fi util în conexiune cu cross-translatoarele, asa cum s-a mentionat anterior. Codul
produs de asemenea translatoare poate fi testat cateodata mai bine prin simularea executiei pe o
mașina donor, decat dupa transferul pe mașina tinta – intarzierile inerente în cadrul transferului de
pe o mașina pe alta pot fi echilibrate prin degradarea timpului de executie intr-o simulare
interpretativa.
În cele din urma, limbajele intermediare sunt adesea foarte compacte, permitand manipularea
programelor mari, chiar și pe masini relativ mici. Succesul masinilor UCSD Pascal și UCSD p-
System candva foarte utilizate reprezinta un exemplu privind ceea ce se poate realiza din acest
punct de vedere.
Pentru toate aceste avantaje, sistemele interpretative prezinta probleme privind timpul de executie,
deoarece executarea codului intermediar inseamna translatia virtuala în cod mașina de fiecare data cand se
realizeaza o instructiune mașina ipotetica.
Unul dintre primele cele mai cunoscute compilatoare interpretative portabile a fost cel dezvoltat la
Zurich și cunoscut sub numele de compilator Pascal-P. Acesta era furnizat intr-un kit cu trei componente:
Prima componenta era forma sursa a unui compilator Pascal, scris intr-un subset foarte complet al
limbajului, cunoscut la Pascal-P. Scopul acestui compilator era sa translateze programele sursa
Pascal-P intr-un limbaj intermediar bine-definit și bine-documentat, cunoscut ca și cod-P, care era
„codul mașina” pentru un computer ipotetic bazat pe stiva, denumit mașina P.
A doua componenta era o versiune compilata a primei – codurile P care erau produse de
compilatorul Pascal-P.
Ultima componenta a kit-ului era un interpretor al limbajului P-cod, furnizat ca un algoritm Pascal.
Interpretorul a fost folosit în primul rand ca un model de scriere a unui program similar pentru mașina
tinta, pentru a-i permite sa emuleze mașina-P ipotetica. Asa cum se va vedea în continuare, emulatoarele
sunt destul de usor de dezvoltat – chiar în limbaj de asamblare, daca este necesar. O data incarcat
interpretorul – adica, versiunea sa potrivita pentru o mașina reala locala – intr-o mașina reala, se poate
19
LIMBAJE FORMALE SI TRANSLATOARE 2017
Pana acum s-au prezentat cateva notiuni de baza privind translatoarele. Din acestea se poate observa ca
realizarea lor nu este prea usoara. Daca trebuie scris un translator complet pentru un limbaj sursa destul de
complex, sau un emulator pentru o noua mașina virtuala, sau un interpretor pentru un limbaj intermediar
de nivel jos, nu se va alege cu siguranta implementarea sa complet în limbaj de asamblare.
Din fericire rareori trebuie sa se realizeze implementarea completa în limbaj de asamblare. La ora actuala
sunt disponibile o mare varietate de translatoare, care sunt și bine documentate. O strategie destul de buna
atunci cand este necesar un translator pentru un limbaj vechi pe o mașina noua, sau pentru un limbaj nou
pe o mașina veche, sau chiar pentru un limbaj nou pe o mașina noua, este sa se utilizeze compilatoarele
existente pe ambele masini și sa se realizeze dezvolarea translatorului intr-un limbaj de nivel înalt. În acest
sens, în capitolul curent se prezinta cateva exemple.
20
LIMBAJE FORMALE SI TRANSLATOARE 2017
Deși dezvoltarea în C este mult mai usoara decat dezvoltarea în cod mașina, procesul este totuși complex.
Asa cum s-a mentionar anterior, poate fi posibila dezvoltarea unei mari parti a sursei compilatorului
utilizand unelte de generare a compilatoarelor – presupunand, deșigur, ca acestea sunt disponibile fie în
forma executabila, fie ca sursa în C care poate fi compilata usor. Partea cea mai grea a dezvoltarii este
probabil cea asociata cu partea back end, deoarece aceasta este foarte dependenta de mașina. Daca exista
acces la codul sursa al unui compilator, ca CtoM, atunci se poate utiliza avantajos acesta. Deși
compilatoarele comerciale sunt rareori disponibile în format sursa, este disponibil codul sursa pentru
multe compilatoare produse în universitati sau ca elemente ale proiectului GNU realizat sub auspiciile
fundatiei Free Software.
Portabilitatea unui compilator ca XtoC.C este aproape garantata, cu conditia ca sa fie el insusi scris în
limbajul „portabil” C. Din pacate insa limbajul C nu este complet portabil. Uneori se pierde mult timp
pentru a modifica codul sursa al lui XtoC.C inainte de a fi acceptat ca intrare pentru CtoM.M, sperand ca
cei care au scris XtoC.C au utilizat doar C standard și directive pre-procesor care permit adaptarea usoara
la alte sisteme.
21
LIMBAJE FORMALE SI TRANSLATOARE 2017
Daca se doreste de la inceput scrierea unui compilator portabil pe alte sistem, acesta este scris astfel încât
sa produca la iesire cod de nivel înalt. Adesea, implementarea original a unui limbaj este scrisa ca un
translator auto-rezident cu scopul de a produce direct cod mașina pentru sistemul gazda curent.
3.3. Bootstrapping
Se pune intrebarea evidenta – cum a fost implementat primul limbaj de nivel înalt? În asamblare? Dar
atunci cum a fost produs asamblorul pentru limbajul de asamblare?
Un asamblor complet este el insusi o piese importanta de software, deși destul de simplu în comparatie cu
un compilator pentru un limbaj de nivel înalt. Este, oricum, destul de uzual sa se defineasca un limbaj ca
un subset al unui alt limbaj, astfel încât subsetul 1 este continut în subsetul 2 care la randul sau este
continut în subsetul 3 și asa mai departe, adica:
Subsetul 1 din ASM Subsetul 2 din ASM Subsetul 3 din ASM
Astfel, se poate scrie la inceput un asamblor în cod mașina pentru subsetul 1 al limbajului de asamblare,
poate pe principiul load-and-go (adica se scrie în asamblare și apoi se translateaza manual instructiune cu
instructiune în cod mașina). Acest program în subsetul limbajului de asamblare ar putea sa faca destul de
putine lucruri pe langa conversia mnemonicelor în forma binara corespunzatoare. Apoi se poate scrie în
subsetul 1 al limbajului de asamblare un asamblor pentru subsetul 2 al limbajului de asamblare și asa mai
departe.
Acest proces, prin care un limbaj simplu este utilizat pentru a translata un program mai complicat, care în
schimb poate lucra cu un program și mai complicat și asa mai departe, este cunoscut ca bootstrapping.
În mod clar, scrierea unui compilator nu o data ci de doua ori este o operatie deloc usoara, daca limbajul
original de implementare nu este apropiat de limbajul sursa. Acest lucru este destul de uzual:
compilatoarele Oberon pot fi implementate în Modula-2; compilatoarele Modula-2, în schimb, au fost
implementate initial în Pascal (toate trei limbajele sunt destul de asemanatoare) și compilatoarele C++ au
fost implementate initial în C.
Dezvoltarea unui compilator care se autocompileaza are patru aspecte diferite în favoare. În primul rand,
constituie un test deloc usor de viabilitate al limbajului compilat. Al doilea, o data realizat, dezvoltarile
ulterioare pot fi facute fara a recurge la alte sisteme de translatoare. Al treilea, orice imbunatatiri care pot
fi facute la partea sa back-end se manifesta atat ca imbunatatiri ale codului obiect produs pentru programe
22
LIMBAJE FORMALE SI TRANSLATOARE 2017
generale cat și ca imbunatatiri ale compilatorului insusi. În ultimul rand, asigura verificarea destul de
exhaustiva a consistentei sale, deoarece daca acest compilator este utilizat pentru a-și compila propriul cod
sursa, ar trebuie, cu siguranta, sa fie capabil sa-și reproduca propriul cod obiect (fig.3.4).
Mai mult, fiind dat un compilator functional pentru un limbaj de nivel înalt este apoi foarte usor sa se
produca compilatoare pentru dialecte specializate ale acelui limbaj.
Primul stadiu al procesului de translatie implica modificarea lui PasToCDC.Pas pentru a genera cod
mașina ICL – producand astfel un cross compilator. Deoarece PasToCDC.Pas a fost scris intr-un limbaj
de nivel înalt, aceasta modificare nu a fost greu de realizat, și a rezultat astfel compilatorul PasToICL.Pas.
Deșigur acest compilator nu putea inca sa ruleze chiar pe orice mașina. A fost la inceput compilat
utilizand PasToCDC.CDC pe mașina CDC (fig.3.6.a). Aceasta a furnizat un cross compilator care putea
rula pa masini CDC, dar inca nu pe masini ICL. O compilatie ulterioara a lui PasToICL.Pas, utilizand
cross compilatorul PasToICL.CDC pe mașina CDC, a produs rezultatul final, PasToICL.ICL (fig.3.6.b).
23
LIMBAJE FORMALE SI TRANSLATOARE 2017
Produsul final (PasToICL.ICL) a fost apoi copiat pe banda magnetica pe mașina ICL și incarcat destul de
usor. Avand obtinut un sistem functional, echipa ICL a putut continua dezvoltarea sistemului în limbajul
Pascal.
Aceasta operatie de portare a fost un exemplu de ceea ce se numeste sistem jumatate de boostrap. Munca
de a transporta este realizata de fapt în intregime pe mașina donor, fara a fi necesar nici un translator pe
mașina destinatie, dar o parte foarte importanta a compilatorului original (partea back end, sau generatorul
de cod) trebuie rescrisa în cadrul procesului. În mod evident metoda este riscanta – orice defect sau
omisiune în scrierea lui PasToICL.Pas ar putea duce la un dezastru. Asemenea probleme pot fi reduse prin
minimizarea modificarilor realizate asupra compilatorului original. O alta tehnica este sa se scrie un
emulator pentru mașina destinatie care sa ruleze pe mașina donor, astfel încât compilatorul final sa poata
fi testat pe mașina donor inainte de a fi transferat pe mașina destinatie.
24
LIMBAJE FORMALE SI TRANSLATOARE 2017
Utilizatorii acestui kit au inceput prin implementarea unui interpretor pentru mașina P. Procesul de
bootstrapping a fost apoi inceput prin dezvoltarea unui compilator (PasPtoM.PasP) care sa translateze
programele sursa Pascal-P în codul mașina local. Acest compilator poate fi scris în limbaj Pascal-P,
dezvoltarea ghidandu-se dupa sursa compilatorului Pascal-P în cod-P furnizat ca parte componenta a kit-
ului. Acest nou compilator a fost apoi compilat cu compilatorul interpretativ (PasPtoP.P) din kit
(fig.3.7.a) și sursa compilatorului Pascal în cod-M a fost apoi compilata cu acest nou compilator,
interpretat inca o data de mașina P, pentru a obtine produsul final, PasPtoM.M (fig.3.7.b).
Compilatorul interpretativ de cod-P Zurich poate, și a fost, utilizat ca un sisteme de dezvoltare foarte
portabil. A fost utilizat cu rezultate remarcabile la dezvoltarea sistemului USCD Pascal, care a fost prima
incercare serioasa de implementare a Pascal-ului pe microcalculatoare. Echipa USCD Pascal a mers mai
departe asigurand cadrul pentru un intreg sistem de operare, editoare și alte utilitare – toate scrise în
Pascal, și toate compilate în codul obiect bine definit cod-P. Prin simpla asigurare a unui intrepretor
alternativ se putea muta intregul sistem pe un nou microcalculator virtual nemodificat.
25
LIMBAJE FORMALE SI TRANSLATOARE 2017
Exista, deșigur, și un alt mod în care un kit compilator interpretativ portabil poate fi utilizat. Se poate
incepe cu scrierea unui asamblor din cod-P în cod-M, probabil o sarcina relativ simpla. O data realizat
acest asamblor s-a obtinut produsul prezentat în fig.3.8.
Codurile-P pentru compilatorul de cod-P sunt apoi asamblate de acest sistem pentru a obtine un alt cross-
compilator (fig.3.9.a.) și acelasi asamblor cod-P/cod-M poate fi apoi utilizat ca parte back-end a cross-
compilatorului (fig.3.9.b).
4. EMULAREA MASINILOR
În capitolul 2 s-a prezentat modul de utilizare a emulatoarelor sau interpretoarelor ca unelte de translatare
a limbajelor de programare. În acest capitol se vor prezenta în detaliu limbaje de masini ipotetice și modul
de emulare a masinilor ipotetice pentru aceste limbaje. Calculatoarele moderne sunt printre cele mai
complexe masini proiectate vreodata de inteligenta umana. Dar nu vor intra în detalii privind hardware-ul
calculatoarelor, ci ne vor rezuma la prezentarea unor limbaje obiect destul de primitive potrivite pentru
translatoare simple.
26
LIMBAJE FORMALE SI TRANSLATOARE 2017
capacitatea unui singur octet (8 biti) sau, asa cum este în cazul majoritatii procesoarelor moderne, pot fi de
dimensiuni multipli mici ai octetilor sau cuvintelor.
Un registru intern fundamental este registrul de instructiuni (IR), prin care se trec siruri de biti (octeti)
reprezentand instructiuni fundamentale în cod mașina pe care procesorul le poate realiza. Aceste
instructiuni tind sa fie extrem de simple – gradul obisnuit de complexitate a acestor instructiuni fiind
operatii de genul „sterge un registru” sau „muta un octet dintr-un registru în altul”. Unele din aceste
instructiuni pot fi complet definite printr-un singur octet. Altele pot necesita doi sau mai multi octeti
pentru o definire completa. La aceste instructiuni multi-octet, primul octet defineste de obicei o operatie
și ceilalti reprezinta fie o valoare asupra careia se opereaza, fie adresa unei locatii de memorie în care se
gaseste valoarea cu care se opereaza.
Cele mai simple procesoare au doar cativa registrii de date care prezinta limitari privind operatiile care se
pot realiza cu continutul acestora și astfel procesoarele trebuie sa aiba posibilitatea de interfatare cu
memoria calculatorului și permit transferuri pe asa numitele linii de magistrala (bus) intre registrii interni
și numarul mult mai mare de locatii de memorie externa. Cand se transfera informatii la și de la memorie,
CPU depune pe magistrala de adrese informatiile corespunzatoare de adresa și apoi transmite sau
receptioneaza datele propriu-zise pe magistrala de date. Acest mecanism se prezinta în fig.4.1.
Memoria poate fi vazuta în mod simplist ca o matrice unidimensionala de octeti, analog cu ceea ce poate
fi descris în limbajul de nivel înalt prin declaratii de genul:
Deoarece memoria este utilizata pentru a stoca nu numai date ci și instructiuni, un alt registru intern
important al unui procesor, asa numitul contor program sau pointer de instructiuni (notat prin PC sau
IP), este utilizat pentru a pastra adresa din memorie a urmatoarei instructiuni care trebuie adusa în
registrul de instructiuni (IR) al procesorului.
Poate ar fi de ajutor daca am gandi și procesorul în limbaj de nivel înalt:
struct processor {
BYTES IR;
BYTES R1, R2, R3;
unsigned PC;
};
27
LIMBAJE FORMALE SI TRANSLATOARE 2017
processor cpu;
Functionarea masinii consta în preluarea repetata a cate unui octet din memorie (pe magistrala de date),
plasarea acestuia în registrul IR și apoi executarea operatiei pe care o reprezinta acest octet. Instructiunile
multi-octet pot necesita preluarea altor octeti suplimentari inaintea decodificarii complete a instructiunii
de catre CPU. Dupa ce instructiunea reprezentata de continutul lui IR a fost executata, valoarea lui PC se
va modifica pentru a indica urmatoarea instructiune de preluat. Acest ciclu preluare-executie poate fi
descris de urmatorul algoritm:
BEGIN
CPU.PC=ValoareInitiala ; adresa primului cod de instructiune
LOOP
CPU.IR=Mem[CPU.PC] ; preluare
Increment(CPU.PC) ; trece la urmatorul cod
Execute(CPU.IR) ; poate afecta alti registrii, memorie, PC
END
END
Valoarea registrului PC se modifica în pasi mici (deoarece instructiunile sunt de obicei stocate în memorie
una dupa alta); dar executia unor instructiuni de ramificare poate avea un efect mai dramatic. La fel și în
cazul aparitiei unor intreruperi hardware, deși nu se va insista în continuare asupra lucrului cu intreruperi
hardware.
Un program pentru o asemenea mașina consta dintr-un lung sir de octeti. Aceste valori (în format binar,
zecimal sau hexazecimal) citite nu au nici o semnificatie pentru operatorul uman. Am putea, de exemplu,
avea o sectiune de program de forma:
25 45 21 34 34 30 45
Deși nu este deloc evident, aceasta succesiune de octeti ar putea fi echivalentul instructiunii:
ValFin=ValInit*2+34
Programarea la nivel mașina este de obicei realizata prin asocierea de mnemonici operatiilor realizate de
mașina, de exemplu: HLT pentru „halt” sau ADD pentru „aduna la registru”. Codul de mai sus este mult
mai usor de inteles daca este scris dupa cum urmeaza (cu comentarii):
Programele scrise intr-un limbaj de asamblare – care mai intai trebuie asamblate inainte de a putea fi
executate – de obicei utilizeaza alte entitati cu nume, de exemplu:
28
LIMBAJE FORMALE SI TRANSLATOARE 2017
Cand se utilizeaza fragmente de cod ca acesta pentru exemplificare vom realiza comentarii aratand
echivalentul fragmentului în limbaj de nivel înalt. Comentariile se scriu dupa caracterul „;”, ceea ce
reprezinta o conventie a limbajelor de asamblare.
Suma=Termen1+Termen2
Unele masini și limbaje de asamblare asigura asemenea operatii în termenii asa numitului cod cu trei
adrese, în care o operatie – exprimata printr-o mnemonica denumita de obicei opcode – este urmata de
doi operanzi și o destinatie. În general aceasta ia forma:
de exemplu:
destinatie=operatie(operand1, operand2)
care ajuta la accentuarea ideii ca operand inseamna de fapt „o valoare”, în timp ce destinatie inseamna un
registru al procesorului sau o adresa de memorie în care se va stoca rezultatul.
În multe cazuri aceasta generalitate este restrictionata (adica, mașina sufera de neortogonalitate în
proiectare). În general este necesar ca valoarea unuia dintre operanzi sa fie valoarea aflata initial în
destinatie. Aceasta corespunde instructiunilor de nivel înalt de tipul:
Produs=Produs*Factor
și este exprimata la nivel jos prin asa numitul cod cu doua adrese de forma generala:
de exemplu:
Ca o curiozitate, merita evidentiata o conexiune evidenta intre unele operatii de atribuire în C++ și codul
cu doua adrese. În C++ atriburea de mai sus ar fi scrisa probabil sub forma:
29
LIMBAJE FORMALE SI TRANSLATOARE 2017
Produs*=Factor;
care este o sugestie pentru compilatorul C++ sa genereze cod de aceasta forma. (Poate acest exemplu ar
putea ajuta sa intelegem de ce C++ este privit de unii ca cel mai rafinat limbaj de asamblare.)
În cazul multor masini reale nici chiar codul cu doua adrese nu se regaseste la nivelul mașina. Una dintre
particulele destinatie sau operand ar putea fi restrictionata la un anumit registru mașina (celalalt ar putea fi
un registru mașina, o constanta sau o adresa mașina). Acesta este adesea numit cod cu o adresa și
jumatate și este exemplificat prin:
În final, în cazul masinilor acumulator s-ar putea sa fim restrictionati la cod cu o adresa, în care
destinatia este intotdeauna un registru mașina (cu exceptia acelor operatii care copiaza (stocheaza)
continutul unui registru mașina în memorie). În unele limbaje de asamblare asemenea instructiuni pot inca
parea de forma cu doua adrese, ca mai sus. Acestea pot fi scrise și în forme avand inclus implicit registrul
în mnemonica (opcode). De exemplu:
Deși multe dintre aceste exemple ar putea lasa impresia ca operatiile corespunzatoare la nivel mașina
necesita mai multi octeti pentru reprezentare, acest lucru nu este în mod necesar adevarat. De exemplu,
operatiile care utilizeaza doar registrii mașina, exemplificate prin:
ar putea necesita un singur octet – asa cum ar fi evident intr-un limbaj de asamblare care utilizeaza a treia
reprezentare. Asamblarea unor asemenea programe este usurata considerabil printr-o notatie simpla și
autoconsistenta a codului sursa, un subiect ce va fi discutat în capitolele urmatoare.
În cazul acelor instructiuni care chiar presupun manipularea altor valori decat cele aflate doar în registrii
mașina, sunt necesare de obicei instructiuni multi-octet. Primul octet specifica de obicei chiar operatia (și
daca este posibil registrul sau registrii implicati), în timp ce ceilalti octeti specifica celelalte valori (sau
adresele de memorie ale celorlalte valori) implicate. În asemenea instructiuni exista cateva moduri de
utilizare a octetilor auxiliari. Aceasta varietate da nastere la ceea ce se numesc moduri de adresare
pentru procesor și a caror scop este sa asigure utilizarea unei adrese efective intr-o instructiune. Moduri
exacte disponibile variaza foarte mult de la un procesor la altul și de aceea vom prezenta în continuare
doar cateva exemple reprezentative. Diversele posibilitati pot fi distinse în unele limbaje de asamblare
prin utilizarea unor mnemonici diferite pentru ceea ce ar parea la o prima privire operatii aproape identice.
În alte limbaje de asamblare distinctia poate fi realizata de diferitele forme sintactice utilizate pentru a
specifica registrii, adresele sau valorile. Se pot gasi diferite limbaje de asamblare pentru acelasi procesor.
30
LIMBAJE FORMALE SI TRANSLATOARE 2017
La adresarea inerenta operandul este implicit în chiar codul operatiei și adesea instructiunea este
continuta intr-un singur octet. De exemplu, pentru a sterge un registru mașina denumit A am putea utiliza:
Din nou subliniem ca, deși a doua forma pare a avea doua componente, nu inseamna intotdeuna ca se
utilizeaza doi octeti de cod la nivel mașina.
La adresarea imediata octetii auxiliari pentru o instructiune dau de obicei valoare reala care trebuie
combinata cu o valoare dintr-un registru. De exemplu:
În cazul acestor doua moduri de adresare utilizarea cuvantului „adresa” poate induce în eroare, deoarece
valoarea octetilor auxiliari adesea nu are nimic de-a face cu vreo adresa de memorie. În cazul modurilor
de adresare ce vor fi prezentate în continuare legatura cu adresele de memorie este mult mai evidenta.
La adresarea directa sau absoluta octetii auxiliari specifica de obicei adresa de memorie a valorii care
trebuie preluata sau combinata cu valoarea dintr-un registru, sau specifica unde se va stoca valoarea unui
registru. De exemplu:
La adresare indexata prin registru unul dintre operanzii din instructiune specifica atat o adresa cat și un
registru index, a carui valoare în momentul executiei poate fi perceputa ca fiind indexul unui sir stocat
incepand cu aceea adresa:
La adresarea indirecta prin registru unul dintre operanzii dintr-o instructiune specifica un registru a
carui valoare în momentul executiei da adresa efectiva unde se gaseste valoare operandului. Aceasta are
legatura cu conceptul de pointeri utilizati în limbajele de nivel înalt (C++). De exemplu:
31
LIMBAJE FORMALE SI TRANSLATOARE 2017
Nu toti registrii din cadrul unei masini pot fi folositi în aceste moduri. Unele masini au destul de multe
restrictii în aceasta privinta.
Unele procesoare permit variatii mari asupra modurilor de adresare indexata și indirecta. De exemplu, la
adresarea indexata cu memoria, un singur operand poate specifica doua adrese de memorie – prima da
adresa primului element al sirului și a doua da adresa unei variabile a carei valoare va fi utilizata ca index
al unui sir.
Similar, la adresare indirecta cu memoria unul dintre operanzii dintr-o instructiune specifica o adresa de
memorie la care se va gasi o valoare care formeaza adresa efectiva la care se gaseste un alt operand.
Acest mod nu este la fel de raspandit ca celelalte; acolo unde apare corespunde direct utilizarii variabilelor
pointer în limbaje care permit acest lucru. De exemplu, codul urmator:
MOV AX,@Arrow
MOV Target, AX
Sau chiar:
MOV Target,@Arrow
unde, din nou, se poate observa o corespondenta imediata intre sintaxa în C++ și cea corespunzatoare în
asamblare.
În sfarsit, la adresare relativa un operand specifica o cantitate cu care registrul contor de program curent
PC trebuie sa se incrementeze sau decrementeze pentru a se gasi nou adresa. Aceasta adresare se gaseste
la instructiunile de ramificare și mai rar în cele care transfera date intre diversi registrii și/sau locatii de
memorie.
32
LIMBAJE FORMALE SI TRANSLATOARE 2017
Arhitectura masinii
În fig.4.2. se prezinta schema bloc a acestei masini.
Programul model al acestui tip de mașina este destul de simplu – consta dintr-un numar de „variabile” (în
sensul C++), fiecare avand capacitatea de 1 octet. Unele dintre ele corespund registrilor procesorului, în
timp ce celelalte formeaza memoria RAM, care am presupus ca este de 256 de octeti, adresati de la 0 la
255. În aceasta memorie vor fi stocate atat datele cat și instructiunile programului în executie. Procesorul,
registrii sai și memoria RAM asociata pot fi descrisi prin urmatoarele declaratii în C++:
processor cpu;
bytes mem[256];
status ps;
unde s-a introdus conceptul de stare a procesorului PS ca fiind o enumerare care defineste starile în care
se poate gasi un emulator.
Setul de instructiuni
Unele operatii ale masinii sunt descrise printrun singur octet. Altele necesita doi octeti și au formatul
urmator:
Byte1 Opcode
Byte2 Address field
Setul de functii cod mașina disponibile este destul de mic. Cele marcate cu * afecteaza flag-urile P și Z și
cele marcate cu + afecteaza flag-ul C. În continuare se prezinta o descriere informala (în original) a
semanticii acestora:
Mnemonic Hex Dec Function
opcode
NOP 00h 0 Nici o operatie (se poate folosi pentru a seta breakpoint în emulator)
CLA 01h 1 Sterge acumulatorul A
CLC + 02h 2 Sterge bitul carry C
CLX 03h 3 Sterge registrul index X
CMC + 04h 4 Complementeaza bitul carry C
INC * 05h 5 Incrementeaza acumulatorul A
DEC * 06h 6 Decrementeaza acumulatorul A
INX * 07h 7 Incrementeaza registrul index X
DEX * 08h 8 Decrementeaza registrul index X
TAX 09h 9 Transfera acumulatorul A în registrul index X
INI * 0Ah 10 Incarca acumulatorul A cu un intreg citit de la intrare în zecimal
INH * 0Bh 11 Incarca acumulatorul A cu un intreg citit de la intrare în hexazecimal
INB * 0Ch 12 Incarca acumulatorul A cu un intreg citit de la intrare în binar
INA * 0Dh 13 Incarca acumulatorul A cu valoarea ASCII citita de la intrare (caracter)
OTI 0Eh 14 Scrie valoarea acumulatorului A la iesire ca un numar zecimal cu semn
OTC 0Fh 15 Scrie valoarea acumulatorului A la iesire ca un numar zecimal fara semn
OTH 10h 16 Scrie valoarea acumulatorului A la iesire ca un numar hexa fara semn
OTB 11h 17 Scrie valoarea acumulatorului A la iesire ca un numar binar fara semn
OTA 12h 18 Scrie valoarea acumulatorului A la iesire ca un singur caracter
PSH 13h 19 Decrementeaza SP și depune valoarea acumulatorului A în stiva
POP * 14h 20 Extrage din stiva în acumulatorul A și incrementeaza SP
SHL + * 15h 21 Deplaseaza acumulatorul A un bit la stanga
34
LIMBAJE FORMALE SI TRANSLATOARE 2017
Instructiunile de mai sus sunt toate de un singur octet. Urmatoarele sunt toate instructiuni de 2 octeti:
Mnemonic Hex Dec Function
opcode
LDA B * 19h 25 Incarca acumulatorul A direct cu continutul locatiei a carei adresa este
în B
LDX B * 1Ah 26 Incarca acumulatorul A cu continutul locatiei a carei adresa este în B,
indexata cu valoare din X (adresa calculata fiind B+X)
LDI B * 1Bh 27 Incarca acumulatorul A cu valoare imediata B
LSP B 1Ch 28 Incarca pointerul de stiva SP cu continutul locatiei de adresa data de B
LSI B 1Dh 29 Incarca pointerul de stiva SP cu valoare imediata B
STA B 1Eh 30 Stocheaza acumulatorul A în locatia a carei adresa este data de B
STX B 1Fh 31 Stocheaza acumulatorul A în locatia a carei adresa este data de B,
indexat cu valoarea lui X
ADD B + * 20h 32 Aduna la acumulatorul A continutul locatiei a carei adresa este data de B
ADX B + * 21h 33 Aduna la acumulatorul A continutul locatiei a carei adresa este data de
B, indexata cu valoarea lui X
ADI B + * 22h 34 Aduna valoarea imediata B la acumulatorul A
ADC B + * 23h 35 Aduna la acumulatorul A valoarea bitului carry C plus continutul
locatiei a carei adresa este data de B
ACX B + * 24h 36 Aduna la acumulatorul A valoarea bitului carry C plus continutul
locatiei a carei adresa este data de B, indexata cu valoarea lui X
ACI B + * 25h 37 Aduna valoarea imediata B + valoarea bitului carry C la acumulatorul A
SUB B + * 26h 38 Scade din acumulatorul A continutul locatiei de adresa data de B
SBX B + * 27h 39 Scade din acumulatorul A continutul locatiei de adresa data de B,
indexata cu valoare lui X
SBI B + * 28h 40 Scade valoarea imediata B din acumulatorul A
SBC B + * 29h 41 Scade din acumulatorul A valoare bitului carry C plus continutul locatiei
a carei adresa este data de B
SCX B + * 2Ah 42 Scade din acumulatorul A valoare bitului carry C plus continutul locatiei
a carei adresa este data de B, indexata cu valoare lui X
SCI B + * 2Bh 43 Scade valoarea imediata B plus valoarea bitului carry C din
acumulatorul A
CMP B + * 2Ch 44 Compara acumulatorul A cu continutul locatiei de adresa data de B
CPX B + * 2Dh 45 Compara acumulatorul A cu continutul locatiei de adresa data de B,
indexata cu valoare lui X
CPI B + * 2Eh 46 Compara acumulatorul A direct cu valoare lui B
ANA B + * 2Fh 47 AND la nivel de bit intre acumulatorul A și continutul locatiei de adresa
data de B
ANX B + * 30h 48 AND la nivel de bit intre acumulatorul A și continutul locatiei de adresa
data de B, indexata cu valoarea lui X
ANI B + * 31h 49 AND la nivel de bit intre acumulatorul A și valoarea imediata B
35
LIMBAJE FORMALE SI TRANSLATOARE 2017
Programul de proba
Se vor prezenta în continuare cateva exemple de programe scrise în limbajul de asamblare al masinii
considerate anterior pentru a intelege mai bine modul sau de functionare.
Sa consideram urmatoarea problema: se citeste un numar și apoi se numara cati biti 1 (nenuli) are acesta în
reprezentarea sa binara. Aceasta problema se rezolva conform programului urmator, în care se prezinta pe
langa instructiuni și reprezentarea hexazecimala a fiecarui octet și localizarea sa în memorie.
36
LIMBAJE FORMALE SI TRANSLATOARE 2017
37
LIMBAJE FORMALE SI TRANSLATOARE 2017
Implementarea rutinei emulator trebuie sa modeleze ciclul tipic de preluare-executie al masinii ipotetice.
Acest lucru se realizeaza usor prin executia repetata a unei instructiuni switch și se urmareste algoritmul
din sectiunea 4.1, dar trebuie prevazuta posibilitatea ca programul sa se opreasca pentru a se evita anumite
erori:
BEGIN
InitializeProgramCounter(CPU.PC);
InitializeRegisters(CPU.A, CPU.X, CPU.SP, CPU.Z, CPU.P, CPU.C);
PS := running;
REPEAT
CPU.IR := Mem[CPU.PC]; Increment(CPU.PC) (* fetch *)
CASE CPU.IR OF (* execute *)
....
END
UNTIL PS # running;
IF PS # finished THEN PostMortem END
END
class AS {
public:
AS(char *sourcename, MC *M);
// Opens source file from supplied sourcename
~AS();
// Closes source file
void assemble(bool &errors);
// Assembles source code from src file and loads bytes of code directly
// into memory. Returns errors = true if source code is corrupt
};
Utilizand aceste doua clase, sistemul load-and-go poate lua forma urmatoare:
38
LIMBAJE FORMALE SI TRANSLATOARE 2017
else
{ printf("Interpreting code ...\n");
Machine->interpret();
}
delete Machine;
}
Intr-un capitol urmator se vor prezenta în detaliu tehnicile specifice asambloarelor. Pentru moment se
observa ca pot fi scrise diverse implementari, în functie de complezitate, utilizand aceasta interfata. Cea
mai simpla dintre acestea ar necesita ca utilizatorul sa asambleze de mana programul sau și ar consta
dintr-un simplu loader:
Oricum, este destul de usor sa se scrie o implementarea alternativa a rutinei assemble care permite
sistemului sa accepte a secventa de mnemonici și campuri de adrese numerice, ca cele din exemplele
anterioare. Se prezinta în continuare un cod posibil, cu comentariile de rigoare:
39
LIMBAJE FORMALE SI TRANSLATOARE 2017
40
LIMBAJE FORMALE SI TRANSLATOARE 2017
În sectiunile urmatoare vom incerca sa dezvoltam un compilator care genereaza cod obiect pentru o
„mașina stiva” ipotetica, una care poate sa nu aiba registrii generali de date de tipul celor discutati în
studiul de caz 1, dar care functioneaza prin manipularea unui pointer de stiva și a stivei asociate. O
arhitectura ca aceasta se va vedea ca este ideala pentru evaluarea expresiilor aritmetice sau Booleene
complicate, ca și pentru implementarea limbajelor de nivel înalt care permit recursivitatea. Vom analiza
aceasta mașina în acelasi mod în care am analizat mașina cu un singur acumulator în sectiunea precedenta.
Arhitectura masinii
Comparata cu mașinile bazate pe registrii normali, aceasta ar putea parea la inceput un pic ciudata,
datorita saraciei de registrii. Ca element comun cu majoritatea masinilor vom presupune ca și aceasta
stocheaza codul și datele intr-o memorie care poate fi modelata ca un sir liniar. Elementele memoriei sunt
„cuvinte”, fiecare dintre acestea poate stoca un singur intreg – în general utilizand reprezentarea pe 16 biti
în complement fata de 2. În fig.4.4 se prezinta schema bloc a acestei masini.
Simbolurile din aceasta schema bloc se refera la urmatoarele componente ale masinii:
ALU este unitatea aritmetica și logica în care se realizeaza de fapt operatiile aritmetice și logice.
Temp este un set de registrii de 16 biti pentru stocarea rezultatelor intermediare necesare în timpul
operatiilor aritmetice și logice. Acesti registrii nu pot fi accesati în mod explicit
SP este un pointer de stiva de 16 biti, un registru care indica zona de memorie utilizata ca stiva
principala.
BP este un pointer de baza de 16 biti, un registru care indica baza unei zone de memorie din cadrul
stivei, cunoscuta ca stiva frame, care se utilizeaza pentru stocarea variabilelor.
MP este un pointer de stiva de marcaj (mark stack) de 16 biti, un registru utilizat la manipularea
apelurilor de subrutina, a carui utilitate se va vedea în capitolele urmatoare.
IR este un registru de instructiune de 16 biti, în care se mentine instructiunea curenta în curs de
executie.
PC este un contor de program de 16 biti, care contine adresa din memorie a instructiunii care
urmeaza a fi executata.
EAR este registru de adresa efectiva, care contine adresa din memorie a datelor manipulate de
instructiunea curenta.
Un program care modeleaza aceasta mașina în C++ este de forma:
41
LIMBAJE FORMALE SI TRANSLATOARE 2017
Presupunem ca incarcatorul (loader) programului va incarca în partea de jos a memoriei codul (lasand
marcajul CodeTop sa indice spre ultimul cuvant de cod). Acesta va incarca și constantele literale în
sectorul corespunzator (lasand marcajul StkTop sa indice spre extremitatea inferioara a acestei zone). Va
continua cu initializarea atat a pointerului de stiva SP cat și a pointerului de baza BP cu valoarea lui
StkTop. Prima instructiune din orice program are sarcina de a rezerva spatiul dinstiva necesar pentru
variabilele sale, prin simpla decrementare a pointerului de stiva SP cu numarul de cuvinte necesare acestor
variabile. O variabila poate fi adresata prin adunarea unui offset la registrul de baza BP. Deoarece stiva
„creste în jos” în memorie, de la adrese mari spre adrese mici, aceste offset-uri vor avea de obicei valori
negative.
Setul de instructiuni
În continuare se descrie în mod informal un set minimal de operatii pentru aceasta mașina. În capitolele
urmatoare se vor adauga mai multe operatii la acest set. Vom utiliza mnemonicele introduse aici pentru a
coda programe pentru mașina în ceea ce pare a fi un limbaj de asamblare simplu, deși cu adresele
specificate în forma absoluta.
Cateva dintre aceste operatii apartin unei categorii cunoscuta ca instructiuni cu zero adrese. Deși sunt
absolut necesari operanzi pentru operatii cum sunt adunarea și inmultirea, adresele acestora nu sunt
specificate prin parti ale instructiunii, ci sunt implicit derivate din valoarea pointerului de stiva SP. Cei doi
operanzi se presupune ca sunt rezidenti în varful stivei; intr-o descriere informala valorile lor sunt notate
42
LIMBAJE FORMALE SI TRANSLATOARE 2017
prin TOS („top of stack”) și SOS („second on stack”). O operatie binara se realizeaza prin extragerea celor
doi operanzi ai sai din stiva în registrii interni (inaccesibili) ai CPU, realizarea operatiei și apoi depunerea
rezultatului inapoi în stiva. Asemenea operatii pot fi foarte economic codificate în conditiile în care
stocarea este preluata de insusi codul program – densitatea mare a codului mașina al masinii orientate spre
stiva este un alt punct în favoarea sa în ceea ce priveste dezvoltarea translatoarelor interpretative.
ADD Extrage TOS și SOS, aduna SOS la TOS, depune suma în noul TOS
SUB Extrage TOS și SOS, scade TOS din SOS, depune rezultatul în noul TOS
MUL Extrage TOS și SOS, inmulteste SOS cu TOS, depune rezultatul în noul TOS
DVD Extrage TOS și SOS, imparte SOS la TOS, depune rezultatul în noul TOS
EQL Extrage TOS și SOS, depunde 1 în noul TOS daca SOS=TOS, 0 altfel
NEQ Extrage TOS și SOS, depunde 1 în noul TOS daca SOS#TOS, 0 altfel
GTR Extrage TOS și SOS, depunde 1 în noul TOS daca SOS>TOS, 0 altfel
LSS Extrage TOS și SOS, depunde 1 în noul TOS daca SOS<TOS, 0 altfel
LEQ Extrage TOS și SOS, depunde 1 în noul TOS daca SOS<=TOS, 0 altfel
GEQ Extrage TOS și SOS, depunde 1 în noul TOS daca SOS>=TOS, 0 altfel
NEG Neaga TOS
43
LIMBAJE FORMALE SI TRANSLATOARE 2017
Exemple de programe
Se vor prezenta cateva exemple de coduri pentru aceasta mașina pentru a ilustra modul sau de functionare.
1. Pentru a exemplifica modul de alocare a memoriei, se considera o sectiune simpla de program care
corespunde codului de nivel înalt de forma:
Imediat dupa incarcarea acestui program (și inainte de executarea istructiunii DSP), contorul program PC
ar trebui sa aiba valoarea 0, în timp ce registrul de baza BP și pointerul de stiva SP ar trebui fiecare sa aiba
valoarea 506.
2. Exemplul anterior este un exemplu destul de rudimentar. Un program ceva mai pretentios este prezentat
în continuare pentru urmatorul algoritm simplu:
BEGIN
Y=0;
REPEAT READ(X); Y=X+Y UNTIL X=0;
WRITE(’Totalul este ’, Y);
END
Acesta necesita o stiva frame de dimensiune 2, care sa contina variabilele X și Y. Codul mașina pentru
acest algoritm este:
44
LIMBAJE FORMALE SI TRANSLATOARE 2017
45
LIMBAJE FORMALE SI TRANSLATOARE 2017
bool tracing);
// Emulates action of the codelen instructions stored în mem, with
// program counter initialized to initpc, stack pointer initialized to
// initsp. data and results are used for I/O. Tracing at the code level
// may be requested
void interpret(STKMC_address codelen, STKMC_address initsp);
// Interactively opens data and results files. Then interprets the
// codelen instructions stored în mem, with stack pointer initialized
// to initsp
STKMC_opcodes opcode(char *str);
// Maps str to opcode, or to STKMC_nul if no match can be found
STKMC();
// Initializes stack machine
};
Emulatorul trebuie sa modeleze ciclul tipic de preluare-executie al unei masini reale. Acest lucru, la fel ca
în cazul precedent, este realizat usor și urmeaza un sablon aproape identic cu cel utilizat la cealalta
mașina. În continuare se prezinta cele mai importante parti ale emulatorului:
bool STKMC::inbounds(int p)
// Check that memory pointer p does not go out of bounds. This should not
// happen with correct code, but it is just as well to check
{ if (p < stackmin || p >= STKMC_memsize) ps = badmem;
return (ps == running);
}
void STKMC::stackdump(STKMC_address initsp, FILE *results, STKMC_address pcnow)
// Dump data area - useful for debugging
{ int online = 0;
fprintf(results, "\nStack dump at %4d", pcnow);
fprintf(results, " SP:%4d BP:%4d SM:%4d\n", cpu.sp, cpu.bp, stackmin);
for (int l = stackmax - 1; l >= cpu.sp; l--)
{ fprintf(results, "%7d:%5d", l, mem[l]);
online++; if (online % 6 == 0) putc(’\n’, results);
}
putc(’\n’, results);
}
void STKMC::trace(FILE *results, STKMC_address pcnow)
// Simple trace facility for run time debugging
{ fprintf(results, " PC:%4d BP:%4d SP:%4d TOS:", pcnow, cpu.bp, cpu.sp);
if (cpu.sp < STKMC_memsize)
fprintf(results, "%4d", mem[cpu.sp]);
else
fprintf(results, "????");
fprintf(results, " %s", mnemonics[cpu.ir]);
switch (cpu.ir)
{ case STKMC_adr:
46
LIMBAJE FORMALE SI TRANSLATOARE 2017
case STKMC_prs:
case STKMC_lit:
case STKMC_dsp:
case STKMC_brn:
case STKMC_bze:
fprintf(results, "%7d", mem[cpu.pc]); break;
// no default needed
}
putc(’\n’, results);
}
void STKMC::postmortem(FILE *results, STKMC_address pcnow)
// Report run time error and position
{ putc(’\n’, results);
switch (ps)
{ case badop: fprintf(results, "Illegal opcode"); break;
case nodata: fprintf(results, "No more data"); break;
case baddata: fprintf(results, "Invalid data"); break;
case divzero: fprintf(results, "Division by zero"); break;
case badmem: fprintf(results, "Memory violation"); break;
case badind: fprintf(results, "Subscript out of range"); break;
}
fprintf(results, " at %4d\n", pcnow);
}
void STKMC::emulator(STKMC_address initpc, STKMC_address codelen,
STKMC_address initsp, FILE *data, FILE *results,
bool tracing)
{ STKMC_address pcnow; // current program counter
stackmax = initsp;
stackmin = codelen;
ps = running;
cpu.sp = initsp;
cpu.bp = initsp; // initialize registers
cpu.pc = initpc; // initialize program counter
do
{ pcnow = cpu.pc;
if (unsigned(mem[cpu.pc]) > int(STKMC_nul)) ps = badop;
else
{ cpu.ir = STKMC_opcodes(mem[cpu.pc]); cpu.pc++; // fetch
if (tracing) trace(results, pcnow);
switch (cpu.ir) // execute
{ case STKMC_adr:
cpu.sp--;
if (inbounds(cpu.sp))
{ mem[cpu.sp] = cpu.bp + mem[cpu.pc]; cpu.pc++; }
break;
case STKMC_lit:
47
LIMBAJE FORMALE SI TRANSLATOARE 2017
cpu.sp--;
if (inbounds(cpu.sp)) { mem[cpu.sp] = mem[cpu.pc]; cpu.pc++; }
break;
case STKMC_dsp:
cpu.sp -= mem[cpu.pc];
if (inbounds(cpu.sp)) cpu.pc++;
break;
case STKMC_brn:
cpu.pc = mem[cpu.pc]; break;
case STKMC_bze:
cpu.sp++;
if (inbounds(cpu.sp))
{ if (mem[cpu.sp - 1] == 0) cpu.pc = mem[cpu.pc]; else cpu.pc++; }
break;
case STKMC_prs:
if (tracing) fputs(BLANKS, results);
int loop = mem[cpu.pc];
cpu.pc++;
while (inbounds(loop) && mem[loop] != 0)
{ putc(mem[loop], results); loop--; }
if (tracing) putc(’\n’, results);
break;
case STKMC_add:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] += mem[cpu.sp - 1];
break;
case STKMC_sub:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] -= mem[cpu.sp - 1];
break;
case STKMC_mul:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] *= mem[cpu.sp - 1];
break;
case STKMC_dvd:
cpu.sp++;
if (inbounds(cpu.sp))
{ if (mem[cpu.sp - 1] == 0)
ps = divzero;
else
mem[cpu.sp] /= mem[cpu.sp - 1];
}
break;
case STKMC_eql:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] == mem[cpu.sp - 1]);
48
LIMBAJE FORMALE SI TRANSLATOARE 2017
break;
case STKMC_neq:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] != mem[cpu.sp - 1]);
break;
case STKMC_lss:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] < mem[cpu.sp - 1]);
break;
case STKMC_geq:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] >= mem[cpu.sp - 1]);
break;
case STKMC_gtr:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] > mem[cpu.sp - 1]);
break;
case STKMC_leq:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] <= mem[cpu.sp - 1]);
break;
case STKMC_neg:
if (inbounds(cpu.sp)) mem[cpu.sp] = -mem[cpu.sp];
break;
case STKMC_val:
if (inbounds(cpu.sp) && inbounds(mem[cpu.sp]))
mem[cpu.sp] = mem[mem[cpu.sp]];
break;
case STKMC_sto:
cpu.sp++;
if (inbounds(cpu.sp) && inbounds(mem[cpu.sp]))
mem[mem[cpu.sp]] = mem[cpu.sp - 1];
cpu.sp++;
break;
case STKMC_ind:
if ((mem[cpu.sp + 1] < 0) || (mem[cpu.sp + 1] >= mem[cpu.sp]))
ps = badind;
else
{ cpu.sp += 2;
if (inbounds(cpu.sp)) mem[cpu.sp] -= mem[cpu.sp - 1];
}
break;
case STKMC_stk:
stackdump(initsp, results, pcnow); break;
case STKMC_hlt:
ps = finished; break;
49
LIMBAJE FORMALE SI TRANSLATOARE 2017
case STKMC_inn:
if (inbounds(cpu.sp) && inbounds(mem[cpu.sp]))
{ if (fscanf(data, "%d", &mem[mem[cpu.sp]]) == 0)
ps = baddata;
else
cpu.sp++;
}
break;
case STKMC_prn:
if (tracing) fputs(BLANKS, results);
cpu.sp++;
if (inbounds(cpu.sp)) fprintf(results, " %d", mem[cpu.sp - 1]);
if (tracing) putc(’\n’, results);
break;
case STKMC_nln:
putc(’\n’, results); break;
case STKMC_nop:
break;
default:
ps = badop; break;
}
}
} while (ps == running);
if (ps != finished) postmortem(results, pcnow);
}
Trebuie sa observam ca acest interpretor contine destul de mult cod de verificare a erorilor. Acest lucru va
diminua eficienta interpretorului, dar este cod care probabil este foarte necesar la testarea sistemului.
class STKASM {
public:
STKASM(char *sourcename, STKMC *M);
// Opens source file from supplied sourcename
~STKASM();
// Closes source file
void assemble(bool &errors, STKMC_address &codetop,
STKMC_address &stktop);
// Assembles source code from an input file and loads codetop
// words of code directly into memory mem[0 .. codetop-1],
// storing strings în the string pool at the top of memory în
// mem[stktop .. STKMC_memsize-1].
50
LIMBAJE FORMALE SI TRANSLATOARE 2017
//
// Returns
// codetop = number of instructions assembled and stored
// în mem[0] .. mem[codetop - 1]
// stktop = 1 + highest byte în memory available
// below string pool în mem[stktop] .. mem[STK_memsize-1]
// errors = true if erroneous instruction format detected
// Instruction format :
// Instruction = [Label] Opcode [AddressField] [Comment]
// Label = Integer
// Opcode = STKMC_Mnemonic
// AddressField = Integer | ’String’
// Comment = String
//
// A string AddressField may only be used with a PRS opcode
// Instructions are supplied one to a line; terminated at end of input file
};
Aceasta interfata ne va permite sa dezvoltam asambloare sofisticate fara a altera restul sistemului – numai
implementarea. În particular putem scrie foarte usor un asamblor/interpretor load-and-go, utilizand în
esenta acelasi sistem cu cel sugerat în cazul precedent.
Obiectivul acestui capitol este prezentarea principiilor de emulare a masinilor și nu preocuparea cu
problemele legate de asamblare. Daca ne limitam la asamblarea codului în care operatiile sunt notate prin
mnemonici, dar toate adresele și deplasamentele sunt scrise în forma absoluta, ca în exemplele anterioare,
atunci poate fi scris destul de usor un asamblor rudimentar. Esenta acestuia este descrisa informal printr-
un algoritm de genul:
BEGIN
CodeTop := 0;
REPEAT
SkipLabel;
IF NOT EOF(SourceFile) THEN
Extract(Mnemonic);
Convert(Mnemonic, OpCode);
Mem[CodeTop] := OpCode; Increment(CodeTop);
IF OpCode = PRS THEN
Extract(String); Store(String, Address);
Mem[CodeTop] := Address; Increment(CodeTop);
ELSIF OpCode în {ADR, LIT, DSP, BRN, BZE} THEN
Extract(Address); Mem[CodeTop] := Address; Increment(CodeTop);
END;
IgnoreComments;
END
UNTIL EOF(SourceFile)
END
51
LIMBAJE FORMALE SI TRANSLATOARE 2017
O implementare a acestuia poate fi realizata în C++. Se presupune ca intregul cod este introdus în mașina
intr-un format liber, o instructiune pe linie. Pot fi adaugate comentarii și etichete, ca în exemplele de mai
sus, dar acestea sunt ignorate de catre asamblor. Deoarece sunt necesare adrese absolute, orice etichete
sunt mai mult o pacoste decat o valoare.
52