Compilatoarele Microsoft Cl.exe Si Gcc Linux

Cuprins

1. Introducere

2. Concepte generale

2.1. Structura compilatoarelor

2.2. Fazele compilarii

3. Analiza sintactica

4. Analiza semantica

5. Compilatoarele Microsoft cl.exe si Linux GCC

5.1. compilatorul Microsoft cl.exe

5.2. compilatorul Linux GCC

1.Introducere

Limbajul de programare este un set bine definit de expresii si reguli valide de formulare a instructiunilor pentru un computer. Limbajul are definite un set de reguli sintactice si semantice. El da posibilitatea programatorului sa specifice în mod exact și amanuntit actiunile pe care trebuie să le execute calculatorul, în ce ordine și cu ce date. Computerul insa nu poate folosi codul scris in limbajul de programare. Acesta trebuie tradus intr-o forma care poate fi executata de computer. Softul care se ocupa de aceasta traducere este compilatorul.

Compilatorul este un program care traduce codul unui program scris dintr-un limbaj de programare "sursa" intr-un alt limbaj de calculator, numit limbaj "tinta". Sursa originala se numeste de obicei cod sursa iar rezultatul cod obiect.

Un rol important al compilatorului este de a semnala erorile din codul sursa pe care le detecteaza in timpul compilarii. Pe langa compilator, mai poate fi nevoie si de alte programe pentru a crea un program executabil. Programul sursa poate fi impartit in mai multe module retinute in fisiere diferite. Operatia de a lua codul sursa este uneori facuta de un program separat numit preprocesor. [6]

Preprocesorul poate dezvolta scurtaturi, numite macro-uri, in declaratiile limbajului sursa. Programul sursa modificat este apoi trimis unui compilator. Acesta poate produce un program asamblor la iesire, deoarece limbajul asamblor este mai usor de produs ca iesire si mai usor de verificat. Limbajul asamblor este apoi procesat de un program numit asamblor, program ce produce cod masina relocabil la iesire. Programele mari sunt adesea compilate pe parti, astfel codul masina relocabil poate necesita conectarea cu fisiere obiect relocabile sau fisiere librarii in codul care ruleaza propriu zis pe masina. Editorul de legaturi se ocupa de adresele de memorie externa, unde codul dintr-un fisier poate referii o locatie din alt fisier. Apoi incarcatorul uneste toate fisierele obiect executabile, in memorie, pentru executie. [6]

2.1 Structura unui compilator

Pana in acest punct am tratat compilatorul ca un singur element ce mapeaza un program sursa intr-un program tinta, echivalent semantic. Daca exploram insa acest element singular, observam de fapt ca exista doua parti ale acestei mapari: analiza si sinteza.

Partea de analiza imparte programul sursa in parti continue si impune o structura gramaticala asupra lor. Apoi foloseste aceasta structura pentru a creea o reprezentare intermediara a programului sursa. Daca partea de analiza detecteaza ca programul sursa este fie format inadecvat din punct de vedere sintactic, fie semantic inadecvat, atunci trebuie sa furnizeze un mesaj de informare despre programul sursa si sa retina acest mesaj intr-o structura de date numita table de simboluri. Partea de analiza este adesea numita partea din fata a compilatorului, partea din spate fiind partea de sinteza.

Partea de sinteza construieste programul tinta dorit din reprezentarea intermediara si informatiile din tabelul de simboluri.

Unele compilatoare au o faza de optimizare intependenta da masina intre partile de analiza si sinteza. Scopul acestei optimizari este de a efectua transformari asupra reprezentarii intermediare, astfel incat partea de sinteza sa produca un program tinta mai bun. [2]

2.2 Fazele compilarii

Structura generala a compilatorului este :

Preprocesorul efectueaza macro-substitutii, eliminarea comentariilor din cod, etc.

Pasul doi este componenta principala din compilator si de asemenea cel mai complex pas. In urma executiei se verifica codul pentru corectitudine formala, apoi este tradus intr-o forma intermediara. Fara acest pas nu poate avea loc procesul de compilare. El cuprinde urmatoarele 5 etape: analiza lexicala, sintactica, semantica, gestiunea tabelei de simboluri si generarea de cod intermediar. Aceste etape nu sunt neaparat proceduri sau functii distincte, ci reprezinta rezultatul obtinut prin cooperarea unui ansamblu de functii.

Următorii trei pași realizează prelucrări asupra programului în cod intermediar în scopul îmbunătățirii performanțelor acestuia (optimizarea) și generează programul in limbaj de asamblare sau cod mașină. [1]

2.2.1 Analiza lexicala

Analiza lexicala se ocupa de traducerea codului programului intr-o forma care sa permita prelucrare facila din partea celorlalte componente. Textul primit la intrare este analizat si segmentat in unitati lexicale cunoscute din care se extrag "atomii" lexicali. Un atom lexical poate lua mai multe forme, de exemplu un cuvant rezervat al limbajului de programare,o valoare numerica sau un sir de caractere. In cadrul proiectarii unui compilator trebuie stabilit forma atomilor lexicali. Un exemplu ar putea fi modul de interpretare al operatiei de comparatie (<, ==, >, <=, >=) existand doua cazuri: fie se interpreteaza fiecare operator sau se considera un atom lexical unic corespunzator operatiei de comparare. In cazul interpretarii fiecarui operator generarea codului este mult mai simpla, dar prezenta unui numar mare de atomi lexicali pute mari complexitatea analizei sintactice. De obicei operatorii cu acelasi grad de prioritate si asociativitate vor fi grupati impreuna.

Analizatorul lexical are rolul de traducere a segmentelor de cod in atomi lexicali. Atomul lexical este reprezentat printr-un cod numeric cu ajutorul caruia se identifica clasa acestuia si de asemenea atributele specifice clasei. Analizatorul lexical interactioneaza in mod normal cu restul compilatorului printr-o interfata simpla: cand analizatorul sintactic are nevoie de un atom lexical se apeleaza analizator lexical si se preia rezultatul. [1]

2.2.2 Analiza sintactica

Analiza sintactica sparge codul sursa in mai multe componente "gramaticale", rezultand astfel un arbore care sa ilustreze structura acestuia. Consideram expresia A*B + C*D . Aceasta expresie poate fi descrisa de urmatorul arbore sintactic :

Se poate observa in arbore ca au fost evidentiate componentele fundamentale ale expresiei si relatiile dintre acestea. Pentru a obtine o reprezentare a structurii expresiei dupa unitatile sintactice atunci este necesara reprezentarea intr-un arbore de derivare (parse tree). In imaginea de mai jos avem un exemplu pentru aceasta forma.

Analizatorul sintactic are rolul de generare a arborilor de derivare pentru sirurile de atomi lexicali pentru a obtine o descriere a relatiei ierarhice intre propozitia analizata (radacina arborelui) si atomii lexicali din care este formata propozitia ( frunzele arborelui). In urma analizei sintactice se construieste o structura de date de tip arbore ( cu pointer si inregistrari ) sau se sintetizeaza informatiile din care se poate face constructia arborelui. [1]

2.2.3 Analiza semantica

Analiza semantica este de obicei incorporata in analiza sintactica. Rolul ei consta in verificarea semantica a structurilor validate in urma analizei sintactice. De obicei verificarile realizate in analiza semantica implica tipurile constructiilor. In urma analizei semantice arborele de derivare este completat cu informatiile care vor permite generarea de cod intermediar.

Parsarea doar verifica daca programul consta in token-uri aranjate intr-o combinatie sintactica valida.

Pentru ca un program sa fie valid din punct de vedere semantic, toate variabilele, functiile, clasele, etc. trebuie definite in mod adecvat, expresiile si variabilele trebuie folosite in moduri in care se respecta sistemul tipurilor, controlul de acces trebuie respectat, si asa mai departe. Analiza semantica este penultima faza a partii din fata a compilatorului si ultima sansa a compilatorului de a semnala greselile din programe.

O mare parte din analiza semantica consta in urmarirea declararilor variabilelor/functiilor/tipurilor si verificarea tipurilor. In multe limbaje, identificatorii trebuie declarati inainte de folosire. Cand compilatorul intalneste o noua declarative, retine tipul informatiei atribuite acelui identificator. Apoi, pe masura ce continua examinarea programului ramas, verifica ca tipul identificatorului este respectat in termeni de operatii efectuate cu acesta.

Parametrii unei functii trebuie sa respecte argumentele unei apelari de functie atat in numar cat si in tip. Limbajul poate cere ca identificatorii sa fie unici, interzicand astfel ca doua declaratii globale sa imparta acelasi nume. Acestea sunt exemple de verificari ce au loc in timpul fazei de analiza semantica.

Vom incepe cu un set de definitii de baza pentru a exemplifica aceasta faza.

Un tip este un set de valori si un set de operatii posibile pe acele valori. Exista trei categorii de tipuri in majoritatea limbajelor de programare:

Tipuri de baza: int, float, double, char, bool, etc.: acestea sunt tipuri primitive puse la dispozitie direct de hardware. Exista si posibilitatea de variante definite de user pentru tipuri de baza (spre exemplu enums in C).

Tipurile compuse: matrici, pointeri, inregistrari, structure, uniuni, clase si asa mai departe. Aceste tipuri sunt construite ca agregari ale tipurilor de baza si a tipurilor compuse simple.

Tipuri complexe: liste, stive, cozi, arbori, campuri, tabele, etc. un limbaj poate sau nu sa aiba support pentru aceste tipuri abstracte de nivel inalt.

Figura ilustreaza succesiunea etapelor constructiei arborelui de derivare.

Compilatorul Microsoft Cl.exe si compilatoarele Gcc din Linux

Compilatorul Microsoft Cl.exe (Command-Line Interpreter (CL)) :

In mod uzual programatorul redirectioneaza compilarea fisierelor sursa prin rularea CL.EXE. Acest mic executabil determina ce module substantiale de compilare trebuie executate, in ce ordine si in ce scop. Acest lucru este realizat in mare parte prin indrumari date prin linie de comanda.

Sintaxa

Compilatorul primeste primele indicatii de la user in format text, in principal prin linie de comanda dar si din fisiere si variabile de mediu. Acest text este parsat in token-uri, ce sunt uzual referite ca tokenuri de linie de comanda chiar si cand provin din alte surse. Tokenurile sunt separate de spatii in general, dar parsarea liniei de comanda are cateva cazuri speciale. Un sumar rapid este acesta : spatiile sunt permise intr-un token daca sunt inchise de ghilimele, suportul pentru ghilimele are consecinte la interpretarea backslash ; tokenurile ce contin caractere wildcard (* si ?) pot fi supuse extinderii wildcard.

Tipuri

Fiecare directiva de linie de comanda consta in unul sau mai multe tokenuri intregi. Sunt recunoscute trei categorii : o optiune incepe cu o cratima sau forward slash. Pentru unele optiuni, unul sau mai multe tokenuri de linie de comanda ce urmeaza pot continua optiunea inceputa ( ca si argumente, in loc de noi directive) ; altfel un token de linie de comanda ce incepe cu @ numeste un fisier de comanda, al carui text aduce mai multe tokenuri de linie de comanda ; orice alt token numeste un fisier de intrare

Surse

Compilatorul gaseste directivele unei linii de comanda din urmatoarele surse in aceasta ordine : valoare variabilei de mediu CL, linia de comanda, valoare variabilei de mediu _CL_ ; linia de comanda cum am descris mai sus, este doar ce poate fi furnizat de user. De asemenea trebuie sa luam la cunostiinta faptul ca CL poate atrage dupa sine doua contributii codate hard. Una precede comenzile furnizate de user si seteaza optiuni initiale pe care userul este liber sa le rescrie. A doua se aplica dupa comenzile furnizate de user si seteaza optiunile obligatorii care au fost neglijate de user. [3]

Parsare liniei de comanda

Parsarea liniei de comanda in tokenuri variaza putin fata de sursa textului. Linia de comanda este defapt parsata de libraria CRT (C Run-Time), in mod specific de functia _getmainargs din MSVCR70.DLL, in concordanta cu regulile de parsare a argumentelor pentru functia main a unui program ale limbajului C specific Microsoft. Contrubutiile liniei de comanda din date de variabilele de mediu si fisierele de comanda sunt parsate de catre compilator , ce actioneaza similar dar nu identic cu CRT. Diferenta majora este aceea ca extinderile wildcard sunt o caracteristica CRT si se aplica doar tokenurilor din linia de comanda propriu zisa.

Spatiul Alb

In general, textul liniei de comanda este divizat in tokenuri la fiecare aparitie a spatiului alb. In linia de comanda propriu zisa, caracterul spatiu alb este specific spatiului si tabului. Pentru spatiul alb din variabilele de mediu si fisierele de comanda, spatiul alb este inteles in sensul functiei CRT _ismbcspace.

Ghilimele si backslah

Pentru a permite spatile albe intr-un token parsat, exista posibilitatea descrisa anterior in capitolui Sintaxa. Parsare trateaza atat ghilimelele cat si backslash ca fiind caractere speciale. Unde ghilimelele sunt precedate de un numar impar de backslah, avem ghilimele literale. Ce trece mai departe in token este cate un backslash pentru fiecare pereche de backslash din text, plus ghilimele. Unde ghilimelele sunt precedate de un numar par de backslash, avem cazul non-literal. Ce trece mai departe din nou sunt backslah pentru fiecare pereche, dar ghilimelele sunt excluse pentru a semnala ca pana la urmatoarele ghilimele non-literale, spatiile albe nu determina tokenul ci in schimb fac parte din acesta. [3]

Optiunile CL.EXE

Fiecare optiune incepe cu un caracter switch, ce poate fi un forward slash sau cratima. Practica standard in aceste note este folosirea forward slash pentru toate referintele catre CL, lasand sa se inteleaga ca (,) cratima este echivalenta daca nu exista alte specificatii.

Unele optiuni CL ce incep cu aceeasi litera sau litere dupa siwtch pot fi combinate intr-un token de linie de comanda, avand caracterul switch si literele comune date doar o data. Spre exemulu, secventa de optiuni /ob2/og/oi/ot/oy poate fi condensata in /ob2gity. Astfel de optiuni combinate sunt surtaturi conveniante. CL, desface combinatia pentru a recupera optiunile individuale, ce sunt astfel tratate ca si cum ar fi fost date individual. Doar optiunile individuale vor fi discutate mai departe.

Sintaxa

Unele optiuni au o negatie formata prin anexarea unei cratime. In mod uzual, optiunile obisnuite activeaza anumite caracteristici iar negarea le dezactiveaza. Spre exemplu, /GR indica compilatorului sa emita Run-Time Type Inofrmation, pec and /GR- indica sa nu faca acest lucru.

O serie de optiuni permit sau cer un cuvant cheie (keyword). Spre exemplu /clr si /Zc. Prima poate fi data ca atare sau ca /clr :noAssembly. A doua este invalida fara cuvantul cheie (forScope sau wchar_t).

Majuscule

In general, optiunile CL si cuvintele lor cheie sunt case-sensitive. Spre exemplu, /GR si /Gr sunt doua optiuni total diferite (enable Run-Time Type Information si a doua pentru a seta o conventie de apel). Exceptiile sunt foarte rare. Acestea se aplica optiunilor /clr sau /help.

Argumente

Unele optiuni accepta sau necesita argumente. Pentru toate mai putin /D, /link, /Tc, /To, /Tp si /U parsare unuia sau mai multor tokenuri de linie de comanda intr-o singura optiune si argumentele sale are o implementare comuna.

Urmatoarele optiuni admit un argument [3] :

/AI, /B1, /B1_5, /B2, /BK, /Bk, /Bl, /Bp1, /Bp2, /Bpl, /Bpx, /Bx, /bC, /bS, /d1, /d1_5, /d2, /F,

/FA, /Fa, /Fb, /Fc, /Fd, /Fe, /FI, /Fl, /Fm, /Fo, /FP, /Fp, /FR, /Fr, /Fs, /FU, /f, /GE, /Gp, /Gs, /Gt,

/H, /I, /il, /MP, /nl, /OV, /o, /pc, /V, /vd, /W, /w1, /w2, /w3, /w4, /wd, /we, /wo, /Yc, /Yl, /Yu,

/YX, /ZB, /Zm, /Zp and /ZX.

Argument in acelasi token :

Daca o optiune nu reprezinta intergul token al liniei de comanda, atunci argumentul poate fi doar ce a mai ramas din respectivul token. Argumentul incepe imediat dupa definirea caracterelor optiunii, incluzand cazul in care argumentul incepe cu un spatiu alb.

cl test.cpp /Fatest.asm

optiunea /Fa are argumentul ’test.asm‘. CL trebuie sa compileze fisierul sursa ‘test.asm’ si sa creeze un fisier de listare in limbaj asamblor numit ‘test.asm’ ca un produs secundar. Daca ‘test.asm’ este numele dorit pentru fisierul de listare, atunci nu trebuie sa avem spatiu alb dupa /Fa, nici macar intre ghilimele : comanda

cl test.cpp « /Fa test.asm »

numeste fisierul de listare ‘ test.asm’, cu un spatiu inainte. Fara ghilimele :

cl test.cpp /Fa test.asm

optiunea /Fa si ‘test.asm’ sunt doua tokenuri separate in linia de comanda.

Argument in urmatorul token :

Pentru unele optiuni, daca optiunea reprezinta intregul token de linie de comanda, argumentul poate sa fie intreg urmatorul token. Astfel optiunea consta in doua tokenuri. Spre exemplu, in comanda :

cl test.cpp /Fi test.h

optiunea /Fi are ‘test.h’ ca si argument, chiar daca cele doua sunt in doua tokenuri separate. Spatiul alb poate fi omis fara a schimba interpretarea, dar poate afecta lizibilitatea.

Optiunile ce se aplica sunt [3]:

/AI, /B1, /B1_5, /B2, /Bl, /Bp1, /Bp2, /Bpl, /Bpx, /Bx, /bC,

/bS, /d1, /d1_5, /d2, /F, /FI, /FU, /f, /H, /I, /il, /MP, /nl, /o,

/pc, /V, /W, /w1, /w2, /w3, /w4, /wd, /we si /wo

Compilatorul GCC

GCC este un system de compilare produs de GNU Project ce suporta variate limbaje de programare. GCC este o component cheie a GNU si a fost adoptat ca si standard de majoritatea sistemelor de operare Unix.

GCC este adesea ales pentru dezvoltarea software-ului menit sa ruleze pe o larga gama de hardware si/sau sisteme de operare. Compilatoarele specific sistemelor furnizate de hardware sau SO pot varia considerabil, complicand atat codul sursa cat si scripturile. Cu GCC, marea parte a compilatorului este aceeasi pe toate platformele, deci doar codul ce utilizeaza unelte specifice platformelor trebuiesc rescrise pentru fiecare system.

Design

Interfata externa este standard in general pentru un compilator UNIX. Userul foloseste un program driver numit gcc, ce interpreteaza argumentele comenzilor, decide ce compilatoare de limbaj sa foloseasca pentru fiecare fisier de intrare, ruleaza asamblorul pe iesire acestora si apoi ruleaza editorul de legaturi pentru a produce executabile binare complete.

Fiecare dintre compilatoarele de limbaj este un program separate ce citeste codul sursa si returneaza la iesire codul masina. Toate au o structura interna comuna. Partea din fata parseaza codul si produce arbolele sintactic.

GCC a fost scris in principal in C cu exceptia unelor parti pentru partea din fata Ada. Distributia include librarii standard pentru Ada, C++ si Java ale caror coduri sunt scrise in aceste limbaje. Pe unele platform, distrubutia include si o librarie runtime de nivel jos, libgcc, scrisa intr-o combinatie de C independent de masina si cod masina specific procesorului, dezvoltat in principiu pentru a se ocupa de operatiile artmetice ce procesorul tinta nu le poate executa direct.

Fiecare parte din fata foloseste un parser pentru a produce arborele sintactic a unui fisier sursa dat. Datorita aceste abstractizari, fisierele sursa in diferite limbaje pot fi procesate de aceeasi parte din spate. GCC a folosit la inceput parsere LARL, generate cu Bison, dar treptat a trecut la parsere recursive scrise manual. Pana de curand, reprezentarea arborelui nu era independent de procesorul tinta.

Arborii erau oarecum diferiti pentru diferite limbaje (discutand strict de partea din fata). Acest lucru a fost simplificat prin introducerea arborilor GENERIC si GIMPLE. Limbajele C, C++, Java produc arbori GENERIC direct in partea din spate. Alte limbaje au insa reprezentari intermediare diferite dupa parsare ce sunt convertite in GENERIC.

GENERIC este un limbaj de reprezentare intermediara folosit ca parte de mijloc in timp ce compileaza codul sursa in executabile binare. Un subset, numit GIMPLE, este tintit de catre toate partile din fata ale GCC. Etapa de mijloc a GCC realizeaza toata analiza de cod si optimizarea, lucrand independent de limbajul compilat si de arhitectura sistemului, incepand de la reprezentarea GENERIC si extinzand-o la RTL (Register Transfer Language).

In transformarea codului sursa in GIMPLE, expresi complexe sunt divizate intr-un cod cu trei adrese folosind variabile temporare.

Optimizarea poate aparea in oricare din fazele de compilare, dar marea majoritate a optimizarilor au loc dupa analiza sintactica si semantica a partii din fata si inainte de generarea codului in partea din spate, astfel numele de parte de mijloc.

Setul exact de optimizari GCC variaza de la distributie la distributie pe masura ce se dezvolta, dar include algoritmii standard, cum ar fi optimizare de bucle, de salturi, eliminarea subexpresiilor, intruducerea programarii si asa mai departe. Optimizarile RTL sunt de importanta mai mica cu aditia optimizarilor SSA asupra arborilor GIMPLE, deoarece optimizarile RTL au un scop mult mai limitat si contin mult mai putina informatie de nivel inalt.

Unele dintre aceste optimizari realizare la acest nivel include eliminarea codului mort, eliminarea partial a redundantei, numerotarea variabilelor globale si inlocuirea scalar a agregatelor. Optimizarile pentru vectori si matrici sunt de asemenea effectuate in aceasta etapa cum ar fi vectorizarea automata sau paralelizarea automata. Este posibila si optimizarea orientate pe profil. [4]

Bibliografie

[1] Limbaje formale si automate – Irina Athanasiu

[2] https://en.wikipedia.org/wiki/Compiler

[3] http://www.geoffchappell.com/studies/msvc/cl/cl/syntax.htm?tx=26,30

[4]https://en.wikibooks.org/wiki/GNU_C_Compiler_Internals/GNU_C_Compiler_Architecture

[5] Compilers – Principles, Techniques & Tools, Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman

Similar Posts