LTY, TITE
Kari Jyrkinen
010730001 Languages, Compilers and Interpreters
Harjoitustyö, kevät 2003
C-kääntäjä
Tehtävän yleinen kuvaus
Kirjoita C-kääntäjä, joka kääntää yksinkertaisia C-kielisiä ohjelmia
ajettavaksi konekieliseksi ohjelmaksi. Vain osa C-kääntäjän ominaisuuksista
tarvitsee toteuttaa. Yleensä pyritään noudattamaan tiukkaa ANSI C -määrittelyä,
paitsi silloin, kun halutaan yksinkertaistaa kääntäjän toimintoja. Jos
jotain ei ole määritelty, voitte olettaa määrittelyn noudattavan ANSI
C -standardia - tai varmistaa asian minulta. Hyviä lähdeteoksia ovat mm.
kirjat Brian W. Kerningham, Dennis M. Richie: The C programming language
(ANSI C) tai Jukka Korpela, Timo Larmela: C-ohjelmointikieli.
Kääntäjä toteutetaan flexiä ja bisonia hyväksi käyttäen.
Parserin tulee tuottaa TAC-välikoodia, joka sitten muunnetaan
assembleriksi ja GNU:n as-kääntäjällä ajettavaksi
ohjelmaksi. Kääntäjän tulee tuottaa ELF-määrityksen mukaista koodia,
jota voidaan ajaa IA-32 -arkkitehtuurin prosessoreilla (Intel x86-arkkitehtuurin
prosessoreilla). Käännettävän tiedoston nimi pitää päättyä .c-lyhenteeseen
(esim. testi.c) ja tiedostosta tuotetaan vastaavan niminen ajettava
ohjelma ilman tuota .c-lyhennettä (testi). Assembler-koodi
tallennetaan vastaavaan .s-päätteiseen tiedostoon (testi.s).
as-assemblerin kutsuminen C-koodissa käy näppärästi vaikkapa
system-funktion avulla. Kääntäjän käyttö pitää siis onnistua tähän
tapaan:
$ ./kaantajannimi testi.c
$ ./testi
Kielen rakenne
Käännettävät ohjelmat koostuvat vain pääfunktiosta main. Funktion
määrityksiä ja -kutsuja ei siis tarvitse toteuttaa. Kaikki muuttujat
ovat globaaleja, ja ne voidaan määrittää joko ennen main-funktiota tai
heti funktion alussa. Esikäsittelijän komentoja (#include jne.)
ei tarvitse myöskään määrittää, sillä ainoa käytettävä C-funktio, printf,
toteutetaan määrittelemällä se varatuksi sanaksi. Pääohjelman paluuarvo
on kokonaislukutyyppinen ja main-funktio ei ota yhtään argumenttia. Tyhjien
sulkujen sijaan vaadimme void-sanan. Return-lause ei ole välttämätön, vaan
ohjelman suoritus voi loppua myös koodin loputtua. Ohjelman rakenne on siis
tällainen:
[muuttujanmäärittelyjä]
int main (void)
{
[muuttujanmäärittelyjä]
[ohjelmalauseita]
}
Hakasulkeissa olevat osat voivat myös puuttua.
Kommentit ja 'white space'
C-kommentit pitää toteuttaa. Sisäkkäisiä kommentteja ei sallita.
Välilyönnit, tabulaattorit ja rivinvaihdot ovat ylimääräisiä merkkejä,
jotka voidaan jättää huomiotta lekserissä. Varattuja sanoja, muuttujia
ja numeroita pitää kuitenkin yleensä erottaa vähintään yksi välilyönti.
"Jos symbolia, joka alkaa kirjaimella tai numerolla, seuraava symboli
alkaa myös kirjaimella tai numerolla, täytyy niiden välissä olla vähintään
yksi tyhjä väli." (Korpela & Larmela)
Muuttujat
Muuttuja pitää olla määritelty, ennen kuin sitä voidaan käyttää,
muuten tulostetaan virheilmoitus käännösvaiheessa. Muuttujan tyyppejä
tarvitsee toteuttaa kolme: int, long ja char. Kaikki
nämä tyypit ovat kokonaislukuarvoisia, joten niiden tyyppisiä muuttujia
voidaan käyttää hyvin samalla tavoin; char-muuttujaan voidaan siis tallettaa
niin merkin ASCII-koodi kuin lukuarvokin. Käytän testiohjelmissa long- ja
char-tyyppisiä muuttujia ainoastaan asetuslauseiden vasemmalla puolella ja
tulostuslauseissa, joten mm. tyyppimuunnoksista ei tarvitse huolehtia. Tosin
riittävän pienillä luvuilla eri tyyppisten muuttujien käyttö sekaisin tuskin
aiheuttaisi mitään ongelmia, luulen ma :)
Sallittuja muuttujan nimiä ovat merkkijonot, jotka alkavat kirjaimella
tai alaviivalla. Näitä voi seurata kirjain, numero tai alaviiva. Rajoitetaan
muuttujien pituus korkeintaan kahdeksaan merkkiin. Isot ja pienet kirjaimet
ovat eri merkkejä, mutta tämä onkin oletus flexissä. Varattu sana ei
voi olla muuttujan nimenä. Itse muuttujan määrittely koostuu muuttujan
tyypistä, yhdestä tai useammasta pilkulla erotetusta muuttujan nimestä
sekä lauseen päättävästä puolipisteestä. Muuttujanmäärittelyvaiheessa
ei tarvitse pystyä alustamaan muuttujien arvoja. Standardin mukaan alustamattoman
globaalin kokonaislukumuuttujan arvo on nolla, jos muuta arvoa ei ole määritelty.
Tämä alustus hoituu näppärästi assembler-vaiheessa.
Kokonaisluku- ja merkkiliteraalit
Kokonaislukuliteraaleilla tarkoitetaan tavallisia kokonaislukuja,
joita käytetään laskutoimituksissa, sijoituksissa ja vertailuissa. Merkkiliteraali
merkitään 'x' ja tarkoittaa hipsuissa esiintyvän merkin ASCII-koodia
eli lukua. Niinpä sallittuja ovat mm. seuraavat operaatiot:
merkkimuuttuja = 'Z';
inttimuuttuja = 3 * 'g' + 'A';
Toisin sanoen, hipsuissa olevat merkit voidaan palauttaa merkin
ASCII-koodina parserin puolelle.
Lauseet ja lausekkeet
Toteutettavat lauseet ovat muuttujan määrittelyjen lisäksi
sijoituslause, tulostuslause, if-lause, for-lause, while-lause, return-lause
sekä tyhjä lause. Kaikki lauseet päättyvät puolipisteeseen - paitsi
siinä tapauksessa, että lauseen loppuosa koostuu ohjelmalohkosta. Toki
tällöinkin lause voi päättyä puolipisteeseen, koska tyhjä lause on mahdollinen,
mutta puolipistettä ei vaadita. C-kielessä lauseke voi olla muun muassa
vertailulauseke tai laskutoimitus - erottakaamme nämä kuitenkin toisistaan:
jatkossa lauseke tarkoittaa laskutoimitusta ja tarvittaessa puhutaan vertailulausekkeesta
tai ehdosta. Vertailulauseita on tarpeen käyttää vain if, for ja while-lauseissa,
ja vaatikaamme myös niitten käyttöä näissä tapauksissa. Pelkkä laskutoimitus
voi olla myös lause, kun perään lisätään puolipilkku. Tässä tosin on järkeä
ainoastaan silloin, kun laskutoimituksen osana on dekrementointi tai inkrementointi.
Näitä operaatioita voi käyttää osana laskutoimitusta tai erillisinä lauseina,
koska lausekkeesta saadaan lause puolipisteellä. Inkrementointi ja dekrementointi
riittää toteuttaa postfix-operaationa (muuttuja++), jolloin laskutoimitus
itsessään tehdään vanhalla arvolla. Lauseiden hieman C-kieltä rajatumpi
syntaksi on seuraava:
muuttuja = lauseke ;
printf("merkkijono"[, arg1[, arg2[,...]]]) ;
if (ehto) lause1 [else
lause2] ;
for (lauseke1; ehto; lauseke2) lause ;
while (ehto) lause ;
return lauseke ;
lauseke ;
;
Sijoituslauseessa sijoitetaan muuttujaan muuttuja, luku tai
laskutoimituksen tulos. C-kielessä on mahdollista kirjoittaa lause a=b=c;,
mutta tätä ei ole tarpeen määritellä. Tulostuslauseessa annetaan
parametrina vähintään merkkijono, jossa voi olla C:ssä käytettyjä kentänmäärittelyjä
(esim. %d) sekä erikoismerkkejä (esim. \n). Merkkijonon
sisällöstä ei tarvitse välittää, koska se annetaan vastaavan C-funktion
käsiteltäväksi. Parametreina voidaan antaa myös muuttujia tai lukuja,
jos halutaan. Periaatteessa parametreja voi olla määrittelemätön määrä,
mutta jos ette halua kikkailla linkitetyllä listalla, riittää mahdollistaa
enintään kolmen argumentin käyttö. Argumenttien tyyppejä ei tarvitse tarkastaa
vastaamaan kentänmäärittelyjä, koska C-kääntäjäkään ei tee tätä. Tarpeen
on vain tallettaa merkkijono sellaisenaan sekä mahdolliset argumentit.
If, for ja while-lauseissa lauseet voivat olla myös lohkoja.
Lohko ympäröidään aaltosulkeilla ja sen sisältö koostuu lausejonosta,
siis myös nämä ohjausrakenteet ja lohkot ovat sallittuja. Toki lause tai
lohko voi olla pelkkä tyhjä lause, jolloin ei tehdä mitään. If-lauseen
else-osa ei ole pakollinen. Lauseiden ehdot ovat muotoa lauseke vertailuoperaattori
lauseke ja vertailuoperaattoreita ovat >, >=, <, <=, ==
ja !=. Ehto voi olla siis vaikkapa muotoa muuttuja + 1 != 5 * i2.
For-lauseessa lauseke voi olla laskutoimitus, sijoitus, tulostus tai
tyhjä - voit tosin kelpuuttaa tällä paikalla myös minkä tahansa aikaisemmin
määritellyn C-lauseen, jos haluat. While-lauseen avulla kirjoitettuna for-lauseen
toiminta on seuraava:
lauseke1;
while (ehto)
{
lause;
lauseke2;
}
Return-lause palauttaa laskutoimituksen tuloksen, muuttujan
arvon tai luvun - lauseke voi myös puuttua. Tyhjä lause koostuu pelkästä
puolipisteestä, joka ei tee mitään. Laskutoimitukset voivat koostua yhteen-,
vähennys-, kerto- ja jakolaskuista. Termejä voidaan ryhmitellä suluilla.
Unaarinen minus on mahdollinen; unaarista plussaa ei C-kielessä ole. Operaattoreiden
sidontajärjestys on seuraava:
() vasemmalta oikealle
++, --, unarinen minus oikealta vasemmalle
*, / vasemmalta oikealle
+, - vasemmalta oikealle
Vertailuoperaattoreille ei sidontajärjestystä tarvitse määritellä, koska
sallimme vain yhden vertailun per lause.
Varatut sanat
Varattuja sanoja ovat char, else, for, if, int, long, return,
void ja while. Myös funktioiden nimiä printf ja main
voidaan pitää varattujen sanojen kaltaisina. Varattuja sanoja ei voi eikä
tarvitse voida käyttää muuttujan niminä.
Esimerkkikoodi
Seuraava koodinpätkä pitäisi mennä kääntäjästäsi lävitse. Kääntäjän
ohjelmointivaiheessa ei kannattane käyttää koko koodia kerralla testaukseen,
vaan vain koodin osia.
/* Esimerkkiohjelma */
int a;
long _x2, z;
int main (void)
{
int i, j;
char nimi6;
nimi6 = 'x';
_x2 = 42;
i = _x2++*5 + 10/2;
printf("i:n arvo on: %d\n", i);
if (a != 0)
printf("a ei ole 0\n");
else
{
printf("a on 0\n");
a++;
}
for(printf("Asetuslause voi olla mitä tahansa\n"); i>0; i--); /* Silmukan lause on tyhjä */
while(i<20)
{
i++;
while(i<20)
{
i++; /* Yksikin lause voi muodostaa myös lohkon */
}
}
i--;
return i;
}
Symbolitaulu ja TAC-välikoodin generointi
Tarvitset symbolitaulun, johon talletat määritellyt muuttujat ja niiden
tyypit. Myös tulostuksessa käytettävät merkkijonot on talletettava jonnekin.
Symbolitauluksi riittää staattinen taulukko, mutta toki hash-taulukkoa
tai linkitettyä listaa voi myös käyttää. Näistä jälkimmäistä käytetään
sinänsä harvemmin kääntäjissä, koska staattinen taulukko on nopeampi. Symbolitauluun
ei tarvitse tallettaa muuttujien arvoja, koska ne määritelleen vasta ajonaikaisesti.
Eri tyyppisille parametreille on toki mahdollista luoda myös omat symbolitaulunsa.
TAC-komento ja komennon parametrit kannattaa tallettaa tietueeseen,
jotka talletetaan linkitettyyn listaan. Tietue voi näyttää vaikkapa tällaiselta:
struct tac
{
int operation;
struct tacarg a;
struct tacarg b;
struct tacarg c;
}
Argumentit voivat olla tietuita tai yhtä hyvin osoittimia symbolitauluun.
Jos käytät erillisiä argumenttitietueita, tietueen rakenne voi olla jotain
tämän tyylistä:
struct tacarg
{
int argumentype;
union
{
int number; /* Kokanaislukuliteraareille */
struct symtab *symbol; /* Muuttujille */
char *label; /* Labeleille */
} argument;
}
Huomaa, että kaikki toiminnallisuus esitetään TAC-välikoodina. Seuraavien
TAC-käskyjen pitäisi riittää välikoodin generointiin:
- Binääriset laskutoimitukset:
a := b + c (TAC_ADD,a,b,c)
a := b - c (TAC_SUB,a,b,c)
a := b * c (TAC_MUL,a,b,c)
a := b / c (TAC_DIV,a,b,c)
- Unaarinen minus:
a := -b (TAC_UM,a,b,NULL)
- Sijoitus:
a := b (TAC_AS,a,b,NULL)
- Label:
label a (TAC_LBL,a,NULL,NULL)
- Ehdoton hyppy:
goto a (TAC_JMP,a,NULL,NULL)
- Ehdollinen hyppy:
if a less b goto c (TAC_JL,a,b,c)
if a greater b goto c (TAC_JG,a,b,c)
if a less or equal b goto c (TAC_JLE,a,b,c)
if a greater of equal b goto c (TAC_JGE,a,b,c)
if a equal b goto c (TAC_JE,a,b,c)
if a not equal b goto c (TAC_JNE,a,b,c)
- Tulostus:
arg a (TAC_ARG,a,NULL,NULL)
arg b (TAC_ARG,b,NULL,NULL)
...
call printf (TAC_PR,NULL,NULL,NULL)
Tarvitset myös apumuuttujia, jotka pitää niin ikään lisätä symbolitauluun,
sekä labeleita hyppyjä varten. Apumuuttujat ja labelit kannattaa aloittaa
alaviivalla, jolloin on pienempi mahdollisuus sille, että ne sekaantuvat
ohjelmakoodin muuttujan nimien kanssa.
Virheiden käsittely
Jos ja kun ohjelmasta löytyy virhe, tulostetaan virherivin numero ja
yritetään jatkaa koodin jäsentämistä. Viiden virheen jälkeen lopetetaan
syötteen jäsentäminen kokonaan. Assembleria ei tietenkään enää tarvitse
eikä saa generoida, jos ohjelmasta on löytynyt yksikin virhe.
Assemblerin generointi
Assembler generoidaan muuten samannimiseen tiedostoon kuin itse koodi,
mutta päätteenä käytetään .c:n sijaan .s:ää. Älkää poistako
tätä aputiedostoa kääntämisen loputtua, sillä saatan tarvita sitä harkkatyönne
testaukseen. Perusasiat assemblerista on käyty lävitse harjoituksissa 12,
joten niitä en tähän kirjaa. Muuten pyrin antamaan kaiken tarvittavan tiedon
tässä, mutta lisätietoa löytyy mm. seuraavien linkkien takaa:
Assemblerin kääntäminen ja linkittäminen ajettavaksi ohjelmaksi käy näppärimmin
gcc-kääntäjän avulla, joka kutsuu as-assembleria ja linkittää tarvittavat
kirjastot mukaan; eli siis tyyliin gcc -o ohjelma ohjelma.s.
Jos haluatte kääntää ohjelman as:n avulla (as -o ohjelma.o ohjelma.s),
pitää linkkeriä kutsua seuraavilla optioilla: ld -m elf_i386 -dynamic-linker
/lib/ld-linux.so.2 /usr/lib/crt1.o /usr/lib/crti.o -lc ohjelma.o
/usr/lib/crtn.o -o ohjelma.
Intellin komentokanta on laaja, mutta meille riittää muutaman käskyn käyttö.
Tietotyyppejä ja rekistereitä ei tarvita myöskään monta. Operoidaan oletuksena
32-bittisillä operandeilla. Täppäpä muutava huomioitava asia:
- Tarvitset kaksi osiota assembler-ohjelmaasi: data- ja text-osion.
Data-osiossa määrittelet käytettävät muuttujat, niiden tyypit ja alkuarvot.
Tietotyypeistä meille riittävät 32-bittiset tietotyypit .int (char
ja int -tyyppisille muuttujille) ja .long (long-tyyppisille muuttujille)
sekä .asciz tai .string merkkijonoille. (Itseasiassa
.int ja .long ovat synonyymejä, mutta käytä molempia ohjeen
mukaan. Halutetessasi voit toki käyttää tyyppiä .byte merkkimuuttujille, mutta
tällöin pitää ottaa huomioon, että tietotyyppi on 8-bittinen.) Käytännössä
siis tulostat symbolitaulusi sisällön tähän osioon. Text-osioon kirjotetaan
itse assembler-koodi ja labelit hyppyjä varten. Suoritus etenee rivi riviltä,
jos hypyt eivät sitä muuta. Osien järjestyksellä ei ole merkitystä.
- Seuraavat komennot riittävät meille: add, call, cdq, cmp, dec,
idiv, imul, inc, jl, jg, jle, jge, je, jne, jmp, mov, neg, pop, push, ret
ja sub - komennoista tarkemmin hieman myöhemmin. Yleensä komentojen
loppuun tulee vielä l, koska operandit ovat 32-bittisiä - eli esim.
addl. Toisaalta as pystyy päättelemään operandin koon useimmissa
tapauksissa ja muulloin oletus on 32 bittiä, joten pääte ei ole aivan välttämätön.
Operandeja voi olla 0, 1, 2 tai 3, yleensä 1 tai 2. Kahden operandin tapauksessa
ensimmäinen on lähde ja toinen kohde - jos tavaat Intellin manuaalia, niin
täällä Intellin syntaksissa järjestys on toisin päin! Operandina on tarpeen
käyttää rekistereitä, muuttujia sekä numerovakioita.
- 32-bittisiä yleisrekistereitä ovat %eax, %ebx, %ecx, %edx, %esi,
%edi, %ebp ja %esp. Kannattanee käyttää lähinnä neljää ensimmäistä,
sillä ainakin %ebp ja %esp -rekistereitä käytetään pinon hallinnointiin,
eikä niiden arvoja parane hävittää - toki voit ottaa ne ennen käyttöä talteen
ja palauttaa käytön jälkeen, jos välttämättä haluat myös noita rekistereitä
käyttää. Käytä tiedon pitempiaikaiseen säilytykseen muuttujia, älä rekistereitä,
koska osa komennoista ja erityisesti C-funktiot käyttävät myös näitä rekistereitä,
etkä voi olettaa, että rekisterin sisältö säilyy muuttumattomana, vaikka
itse et sisältöä muuttaisikaan.
- Muuttujat (tai ehkä oikeammin nimetyt muistipaikan osoitteet tjsp.)
pitää määritellä data-osiossa, jotta niitä voitaisiin käyttää. Muuttujiin
viitataan pelkällä muuttujan nimellä, ei siis $muuttuja. Dollarimerkki
muuttujan edessä aiheuttaa muuttujan osoitteen välittämisen parametriksi.
Tämä on tarpeen ainoastaan printf-komennon yhteydessä, jolloin komennolle
välitetään tietenkin osoitteet merkkijonoihin, eikä itse merkkijonoja.
- Kokonaislukuja voidaan käyttää suoraan parametrina, kunhan niiden
eteen lisätään dollarin merkki, eli vaikkapa $123.
- Kun käytetään C-kirjastoa hyväksi, pitäisi muistaa muutama asia:
jokaisen funktion alussa pitäisi laittaa (parametri)pinon arvot talteen
ja lopussa palauttaa tiedot, jotta funktiota kutsuneen ohjelman/funktion
tiedot eivät häviä. Eli koodin ensimmäiset ja poistumiskomentoa lukuunottamatta viimeiset rivit pitäisi olla:
pushl %ebp
movl %esp, %ebp
pushl %ebx
pushl %esi
pushl %edi
-------------------------------------
popl %edi
popl %esi
popl %ebx
movl %ebp, %esp
popl %ebp
/* Poistuminen ret-komennolla tai C:n exit-funktiolla -
ret-komennon käyttö edellyttää pino-osoittimen päivitystä ohjelman aikana */
Pinoa tarvitaan lähinnä printf-funktiota käytettäessä. Funktion
parametrit laitetaan pinoon ennen funktion kutsumista. Funktio ei poista
parametreja pinosta, vaan sinun pitäisi tehdä se: jokaista parametria kohti
pitäisi %esp-rekisterin arvoa kasvattaa 4:llä. (Jokainen parametri
on 32 bittiä eli 4 tavua.) Pienillä testiohjelmilla pino tuskin loppuu,
vaikka et tätä tekisikään, mutta ohjelmasi (main-funktiosi) ei ole
sen jälkeen yhteensopiva C:n muiden funktioiden kanssa. Ohjelmasta poistumiseen
joudut käyttämään tällöin exit-funktiota, et voi tehdä sitä 'siististi'
ret-komennolla, koska pino ei ole ajantasalla.
Komentojen syntaksi
Seuraavassa assembler-komentojen toiminta on käyty lyhyesti lävitse, erityisesti
parametrien käyttö. imm32 tarkoittaa kokonaislukua (32-bit), r32
rekisteriä (32-bit) ja r/m32 joko rekisteriä tai muuttujaa, muistipaikkaa
(32-bit). Aina voi tietenkin pelata varman päälle ja siirtää kaikki operandit
ensin rekistereihin ja sitten suorittaa itse operaatio.
Add - laskee yhteen operandit ja tallettaa tuloksen jälkimmäiseen
operandiin
addl imm32, r/m32
addl r32, r/m32
addl r/m32, r32
Call - kutsuu parametrina saamaansa funktiota (tai järjestelmäkutsua)
call printf
call exit
Cdq - needed with idiv, to change doubleword to quadword
cdq
Cmp - kahden operandin vertaus; vertaustulos talletetaan statusrekisteriin.
Tämän komennon jälkeen voidaan kutsua ehdollisia hyppyjä, joka käy tarkistamassa
hypyn ehdon totuuden statusrekisteristä.
cmpl imm32, r/m32
cmpl r32, r/m32
cmpl r/m32, r32
Dec - parametrin vähentäminen yhdellä ts. dekrementointi
decl r/m32
Idiv - merkin säilyttävä jakolasku. Jaettava talletetaan rekisteriin
%eax ja kutsutaan komentoa cdq, jolloin saadaan 'sign-extend'
rekisteriin %edx. Jakaja annetaan parametrina. Rekisteriin %eax
palautetaan jakolaskun tulos (ja rekisteriin %edx jakojäännös).
idivl r/m32
Imul - merkin säilyttävä kertolasku. Parametrit kerrotaan keskenään
ja tulos tallennetaan jälkimmäiseen rekisteriin.
imull r/m32, r32
imull imm32, r32
Inc - parametrin kasvattaminen yhdellä ts. inkrementointi
incl r/m32
Jl, jg, jle, jge, je, jne - ehdolliset hypyt. Ehdon paikkaansapitävyys
tarkastetaan edellä kutsutun cmp-komennon perusteella.
jCC label
Jmp - ehdoton hyppy
jmp label
Mov - arvojen siirto muistin ja rekistereiden väillä sekä kokonaisluvun
talletus muuttujaan tai rekisteriin
movl r32, r/m32
movl r/m32, r32
movl imm32, r/m32
Neg - negaatio
negl r/m32
Pop - arvojen haku pinosta
popl r/m32
Push - arvojen laitto pinoon
pushl r/m32
pushl imm32
Ret - paluu kutsuvaan funktioon tai proseduuriin; paluuarvon voimme
unohtaa, vaikka se synteksissa onkin mahdollinen
ret
Sub - vähentää ensimmäisen operandin toisesta operandista ja tallettaa
tuloksen toiseen operandiin
subl imm32, r/m32
subl r32, r/m32
subl r/m32, r32
TAC:ista assembleria
TAC:in muuntaminen assembleriksi on varsin suoraviivaista - tässäpä muutama
esimerkki:
(TAC_ADD,a,b,c):
fprintf(f,"\t movl %s, %%eax\n", getNameOrValue(c));
fprintf(f,"\t addl %s, %%eax\n", getNameOrValue(b));
fprintf(f,"\t movl %%eax, %s\n", getNameOrValue(a));
(TAC_DIV,a,b,c)
fprintf(f,"\t movl %s, %%eax\n", getNameOrValue(b));
fprintf(f,"\t cdq);
fprintf(f,"\t movl %s, %%ecx\n", getNameOrValue(c));
fprintf(f,"\t idiv %%ecx\n");
fprintf(f,"\t movl %%eax, %s\n", getNameOrValue(a));
(TAC_JLE,a,b,c)
fprintf(f,"\t movl %s, %%eax\n", getNameOrValue(b));
fprintf(f,"\t cmpl %s, %%eax\n", getNameOrValue(a));
fprintf(f,"\t jle %s", getNameOrValue(c));
Muut TAC-käskyt menevät samaan tyyliin. Printf-funktion käytöstä
kannattaa katsoa esimerkkejä harjoituksista.
Vihjeitä
Työ kannattaa jakaa osiin. Kirjoita ensin lekseri, joka jakaa syötteen
oikein osiin. Lisää sitten vähitellen sääntöjä parseriin, kunnes se hyväksyy
määritellyn kielen. Lisää toiminnallisuus, muuttujien lisääminen symbolitauluun
ja kolmen osoitteen koodin generointi. Generoi lopuksi assembler.