Compilator

Versiunea actuală a paginii nu a fost încă examinată de colaboratori experimentați și poate diferi semnificativ de versiunea revizuită la 6 decembrie 2021; verificările necesită 6 modificări .

Un compilator este un program care traduce textul scris într- un limbaj de programare într-un set de coduri de mașină [1] [2] [3] .

Funcționalități de bază și terminologie

Compilare  - asamblare a programului, inclusiv:

  1. traducerea tuturor modulelor de program scrise într-unul sau mai multe limbaje sursă de programare de nivel înalt și/sau limbaj de asamblare în module de program echivalente într-un limbaj de nivel scăzut apropiat de codul mașină ( cod absolut , modul obiect , uneori limbaj de asamblare ) [2] ] [3] [4] sau direct în limbaj mașină sau alt limbaj de comandă binar de nivel scăzut;
  2. asamblarea ulterioară a programului mașină executabilă, inclusiv inserarea în program a codului tuturor funcțiilor importate din bibliotecile statice și/sau generarea codului solicitării către OS pentru a încărca bibliotecile dinamice din care programul va apela funcții.

Dacă compilatorul generează un program în limbaj mașină executabil, atunci un astfel de program este executat direct de o mașină programabilă fizică (de exemplu, un computer). În alte cazuri, programul mașinii executabile este executat de mașina virtuală corespunzătoare .

Intrarea compilatorului este:

  1. în faza de traducere: codul sursă al programului, care este o descriere a algoritmului sau programului într-un limbaj de programare specific domeniului ;
  2. la faza de legare: fișiere de coduri obiect ale modulelor de program generate în faza de traducere, precum și fișiere de coduri obiect ale bibliotecilor statice și date despre bibliotecile dinamice utilizate.

Ieșirea compilatorului este o descriere echivalentă a algoritmului într-un limbaj orientat către mașină (cod obiect [5] , bytecode ).

Compilare  - pentru a asambla un program de mașină, inclusiv:

  1. traducere dintr-o limbă specifică domeniului într-o limbă specifică mașinii [3] ,
  2. legarea unui program executabil orientat pe mașină de la modulele obiect generate în faza de traducere - module care conțin părți din codul programului pe codul programului orientat către mașină.

Destul de des, compilatorii din limbi de nivel înalt realizează doar traducerea codului sursă, încredințând în același timp legătura unui linker extern, un linker care reprezintă un program independent numit de compilator ca subrutină externă. Drept urmare, compilatorul este considerat de mulți un fel de traducător, ceea ce este incorect...

Tipuri de compilatoare

De asemenea, toate compilatoarele pot fi împărțite condiționat în două grupuri:

Tipuri de compilare

Tipuri de compilare [2] :

Structura compilatorului

Procesul de compilare constă din următorii pași:

  1. Traducerea programului - traducerea tuturor sau numai a modulelor modificate ale programului sursă.
  2. Conectarea unui program orientat către mașină.

Implementările structurale ale compilatorului pot fi după cum urmează:

  1. Atât compilatorul, cât și linkerul pot fi incluse în compilator ca programe executabile.
  2. Compilatorul în sine efectuează doar traducerea programului compilat, în timp ce legarea programului este efectuată de un program separat de linker, numit de compilator. Aproape toate compilatoarele moderne sunt construite conform acestei scheme.
  3. Un pachet software care include traducători din diferite limbaje de programare și linkere.

Conform primei scheme, au fost construite primele compilatoare - pentru compilatoarele moderne, o astfel de schemă de construcție nu este caracteristică.

Conform celei de-a doua scheme, sunt construite toate compilatoarele din limbaje de nivel înalt, fără excepție. Orice astfel de compilator în sine efectuează doar traducerea și apoi apelează linkerul ca subrutină externă, care leagă programul orientat către mașină. O astfel de schemă de construcție permite cu ușurință compilatorului să lucreze în modul traducător din limbajul de programare corespunzător. Această împrejurare servește adesea drept motiv pentru a considera compilatorul ca un fel de traducător, ceea ce este în mod natural greșit - toți compilatorii moderni de acest tip încă efectuează legături, deși prin intermediul linkerului extern numit de compilator, în timp ce compilatorul însuși nu apelează niciodată. linkerul extern. Dar aceeași împrejurare permite compilatorului dintr-un limbaj de programare aflat în faza de legătură să includă în programul scris într-un limbaj de programare funcții-subrutine din cele deja traduse de compilatorul/compilatorul corespunzător, scrise într-un alt limbaj de programare. Deci, puteți introduce funcții scrise în Pascal sau Fortran într-un program C/C++ . În mod similar, și invers, funcțiile scrise în C/C++ pot fi inserate într-un program Pascal sau, respectiv, Fortran. Acest lucru ar fi imposibil fără suportul multor compilatoare moderne pentru generarea de cod pentru apelarea procedurilor (funcțiilor) în conformitate cu convențiile altor limbaje de programare. De exemplu, compilatoarele moderne din limbajul Pascal, pe lângă organizarea apelurilor de procedură/funcție în standardul Pascal însuși, sprijină organizarea unui apel de procedură/funcție în conformitate cu convențiile limbajului C/C++. (De exemplu, pentru ca o procedură/funcție scrisă în Pascal să funcționeze cu parametrii de intrare la nivel de cod mașină în conformitate cu convențiile limbajului C/C++, declarația de declarare a unei astfel de proceduri Pascal/funcție Pascal trebuie să conțină cuvântul cheie cdecl .)


În cele din urmă, conform celei de-a treia scheme, sunt construite compilatoare, care sunt sisteme întregi care includ traducători din diferite limbaje de programare și linkere. De asemenea, orice astfel de compilator poate folosi ca traducător orice compilator capabil de traducător dintr-un anumit limbaj de nivel înalt. Desigur, un astfel de compilator poate compila un program ale cărui părți diferite ale textului sursă sunt scrise în diferite limbaje de programare. Adesea, astfel de compilatoare sunt controlate de un interpret încorporat al unuia sau altuia limbaj de comandă. Un exemplu izbitor de astfel de compilatoare este punerea la dispoziție a compilatorului pe toate sistemele UNIX (în special, pe Linux) .

Traducerea programului ca parte integrantă a compilației include:

  1. Analiza lexicala . În această etapă, secvența de caractere din fișierul sursă este convertită într-o secvență de jetoane.
  2. Analiza sintactică (gramaticală) . Secvența de jetoane este convertită într-un arbore de analiză.
  3. Analiza semantică . În această fază, arborele de analiză este procesat pentru a-și stabili semantica (sensul) - de exemplu, identificatorii de legare la declarațiile lor, tipurile de date, verificări de compatibilitate, tipuri de expresii etc. Rezultatul este de obicei numit „reprezentare intermediară/cod”, și poate fi un arbore de analiză augmentat, un arbore nou, un set de instrucțiuni abstracte sau altceva convenabil pentru procesare ulterioară.
  4. Optimizare . Construcțiile redundante sunt eliminate și codul este simplificat, păstrându-și în același timp sensul. Optimizarea poate fi la diferite niveluri și etape - de exemplu, peste codul intermediar sau peste codul final al mașinii.
  5. Generarea codului . Din reprezentarea intermediară se generează codul în limbajul țintă orientat către mașină.

Generarea codului

Generarea codului de mașină

Majoritatea compilatoarelor traduc un program dintr -un limbaj de programare de nivel înalt în cod de mașină care poate fi executat direct de un procesor fizic . De regulă, acest cod se concentrează și pe execuția în mediul unui anumit sistem de operare , deoarece folosește capabilitățile oferite de acesta ( apeluri de sistem , biblioteci de funcții). Arhitectura (set de software și hardware) pentru care este compilat (asamblat) un program orientat către mașină se numește mașina țintă .

Rezultatul compilării - un modul de program executabil - are cea mai mare performanță posibilă, dar este legat de un anumit sistem de operare (familie de sisteme de operare sau subfamilie) și procesor (familia de procesoare) și nu va funcționa pe altele.

Fiecare mașină țintă ( IBM , Apple , Sun , Elbrus etc.) și fiecare sistem de operare sau familie de sisteme de operare care rulează pe mașina țintă necesită scrierea propriului compilator. Există, de asemenea, așa-numitele cross-compilatoare care permit, pe o mașină și în mediul unui sistem de operare, să genereze cod destinat execuției pe o altă mașină țintă și/sau în mediul altui OS. În plus, compilatoarele pot optimiza codul pentru diferite modele din aceeași familie de procesoare (prin sprijinirea caracteristicilor specifice modelului sau a extensiilor setului de instrucțiuni). De exemplu, codul compilat pentru procesoarele din familia Pentium poate lua în considerare caracteristicile paralelizării instrucțiunilor și poate folosi extensiile lor specifice - MMX , SSE etc.

Unii compilatori traduc un program dintr-un limbaj de nivel înalt nu direct în codul mașinii, ci în limbaj de asamblare . (Exemplu: PureBasic , traducerea codului BASIC în asamblator FASM .) Acest lucru se face pentru a simplifica partea de generare a codului a compilatorului și pentru a crește portabilitatea acestuia (sarcina de a genera codul final și de a-l conecta la platforma țintă necesară este mutată la asamblator ), sau pentru a putea controla și corecta rezultatul compilației (inclusiv optimizarea manuală) de către programator.

Generare bytecode

Rezultatul muncii compilatorului poate fi un program într-un limbaj special creat de comenzi de cod binar de nivel scăzut, executat de o mașină virtuală . Un astfel de limbaj se numește pseudocod sau bytecode . De regulă, nu este codul de mașină al oricărui computer, iar programele de pe acesta pot fi executate pe diferite arhitecturi, unde există o mașină virtuală corespunzătoare, dar în unele cazuri sunt create platforme hardware care execută direct pseudocodul oricărui limbaj. . De exemplu, pseudocodul limbajului Java se numește bytecode Java și rulează în Java Virtual Machine , iar specificația procesorului picoJava a fost creată pentru executarea sa directă . Pentru .NET Framework , pseudocodul se numește Common Intermediate Language (CIL), iar timpul de execuție se numește Common Language Runtime (CLR).

Unele implementări ale limbajelor interpretate la nivel înalt (cum ar fi Perl) folosesc bytecode pentru a optimiza execuția: pașii scumpi de analiză și conversie a textului programului în bytecode se fac o dată la încărcare, apoi codul corespunzător poate fi reutilizat fără recompilare.

Compilare dinamică

Datorită necesității de interpretare, bytecode rulează mult mai lent decât codul de mașină cu funcționalitate comparabilă, dar este mai portabil (nu depinde de sistemul de operare și modelul procesorului). Pentru a accelera execuția bytecode, se folosește compilarea dinamică , atunci când mașina virtuală traduce pseudocodul în cod mașină imediat înainte de prima execuție (și când codul este accesat în mod repetat, versiunea deja compilată este executată).

Cel mai popular tip de compilare dinamică este JIT . O altă variantă este compilarea incrementală .

Codul CIL este, de asemenea, compilat pentru codul mașinii țintă de către compilatorul JIT, în timp ce bibliotecile .NET Framework sunt precompilate.

Traducerea bytecode în codul mașinii

Traducerea bytecode în codul mașină de către un traducător special de bytecode, așa cum s-a menționat mai sus, este o fază integrală a compilației dinamice. Dar traducerea bytecode este utilă și pentru pur și simplu convertirea unui program bytecode într-un program echivalent în limbaj mașină. Poate fi tradus în codul mașinii ca bytecode pre-compilat. Dar și traducerea bytecode în codul mașină poate fi efectuată de compilatorul bytecode imediat după compilarea bytecode. Aproape întotdeauna în acest din urmă caz, traducerea bytecode este efectuată de un traducător extern numit de compilatorul bytecode.

Decompilare

Există programe care rezolvă problema inversă - traducerea unui program dintr-un limbaj de nivel scăzut într-unul de nivel înalt. Acest proces se numește decompilare, iar astfel de programe se numesc decompilatoare . Dar, din moment ce compilarea este un proces cu pierderi, în general nu este posibil să restabiliți exact codul sursă în, de exemplu, C++. Programele în bytecodes sunt decompilate mai eficient - de exemplu, există un decompilator destul de fiabil pentru Flash . O variație a decompilării este dezasamblarea codului de mașină în codul limbajului de asamblare, care aproape întotdeauna se execută în siguranță (în acest caz, complexitatea poate fi un cod cu auto-modificare sau un cod în care codul real și datele nu sunt separate). Acest lucru se datorează faptului că există o corespondență aproape unu-la-unu între codurile de instrucțiuni ale mașinii și instrucțiunile de asamblare.

Compilare separată

Compilare separată ( ing.  compilare separată ) - traducerea părților programului separat cu combinația lor ulterioară de către linker într-un singur modul de încărcare [2] .

Din punct de vedere istoric, o caracteristică a compilatorului, reflectată în numele său ( eng.  compilare  - asamblare, compus), a fost că producea atât traducere , cât și legături, în timp ce compilatorul putea genera imediat codul mașinii . Cu toate acestea, mai târziu, odată cu creșterea complexității și dimensiunii programelor (și în creșterea timpului petrecut pentru recompilare), a devenit necesară separarea programelor în părți și izolarea bibliotecilor care pot fi compilate independent unele de altele. În procesul de traducere a unui program, compilatorul însuși, sau un compilator numit de compilator, generează un modul obiect care conține informații suplimentare, care este apoi - în procesul de conectare a părților într-un modul executabil - folosit pentru a lega și rezolva referințe între părți ale programului. De asemenea, compilarea separată vă permite să scrieți diferite părți ale codului sursă al unui program în diferite limbaje de programare.

Apariția compilației separate și alocarea legăturilor ca etapă separată a avut loc mult mai târziu decât crearea compilatoarelor. În acest sens, în locul termenului „compilator”, termenul „traducător” este uneori folosit ca sinonim al acestuia: fie în literatura veche, fie atunci când doresc să sublinieze capacitatea acestuia de a traduce un program în codul mașinii (și invers, folosesc termenul „compilator” pentru a sublinia capacitatea de a asambla din mai multe fișiere unul). Doar că utilizarea termenilor „compilator” și „traducător” în acest context este incorectă. Chiar dacă compilatorul realizează însuși traducerea programului, delegând legătura programului de linker extern invocat, un astfel de compilator nu poate fi considerat un fel de traducător - traducătorul realizează traducerea programului sursă și nimic mai mult. Și cu siguranță nu traducătorii sunt compilatori ca utilitarul make system compilator găsit pe toate sistemele UNIX.

Utilitarul make în sine  este un prim exemplu de implementare destul de reușită a compilației separate. Funcționarea utilitarului make este controlată de un script în limbajul de intrare interpretat de utilitar, cunoscut sub numele de makefile , conținut în fișierul text de intrare specificat la rularea utilitarului. Utilitarul în sine nu realizează nicio traducere sau legătură - de facto , utilitarul make funcționează ca un manager de proces de compilator, organizând compilarea programului în conformitate cu scriptul specificat. În special, în timpul compilării programului țintă, utilitarul make apelează compilatoare din limbaje de programare care traduc diferite părți ale programului sursă în cod obiect, iar după aceea se numește unul sau altul linker care leagă programul sau biblioteca finală executabilă. modul de program. În același timp, diferite părți ale programului, aranjate ca fișiere text sursă separate, pot fi scrise atât în ​​același limbaj de programare, cât și în limbaje de programare diferite. În timpul recompilării programului, sunt traduse doar fișierele părți modificate ale codului sursă al programului, drept urmare durata recompilării programului este redusă semnificativ (uneori cu un ordin de mărime).

Istorie

În zorii dezvoltării computerelor, primii compilatori (traducători) au fost numiți „programe de programare” [6] (întrucât în ​​acel moment doar codul de mașină era considerat program, iar un „program de programare” era capabil să facă cod de mașină din text uman, adică programarea unui computer ).

Note

  1. GOST 19781-83 // Informatică. Terminologie: Manual de referință. Numărul 1 / Referent Ph.D. tehnologie. Științe Yu. P. Selivanov. - M . : Editura de standarde, 1989. - 168 p. - 55.000 de exemplare.  — ISBN 5-7050-0155-X . ; vezi și GOST 19781-90
  2. 1 2 3 4 Pershikov, 1991 .
  3. 1 2 3 Calculatoare .
  4. Borkovsky A. B. Dicționar englez-rus de programare și informatică (cu interpretări). - M . : Limba rusă, 1990. - 335 p. - 50.050 de exemplare (suplimentare).  — ISBN 5-200-01169-3 .
  5. Dictionary of Computing Systems = Dictionary of Computing / Ed. V. Illingworth şi alţii: Per. din engleza. A. K. Belotsky și alții; Ed. E. K. Maslovsky. - M . : Mashinostroenie, 1990. - 560 p. - 70.000 de exemplare (suplimentare).  - ISBN 5-217-00617-X (URSS), ISBN 0-19-853913-4 (Marea Britanie).
  6. N. A. Krinitsky, G. A. Mironov, G. D. Frolov. Programare / Ed. M. R. Shura-Bura . - M . : Editura de stat de literatură fizică și matematică, 1963.

Literatură

Link -uri