15. Programok fordítása és végrehajtása
Programok fordítása és végrehajtása. (Fordítás és interpretálás, bytecode. Előfordító, fordító, szerkesztő. A make. Fordítási egység, könyvtárak. Szintaktikus és szemantikus szabályok. Statikus és dinamikus típusellenőrzés. Párhuzamos programozás.)
1. Fordítás és interpretálás, bytecode Fordítás: A forráskódot a fordítóprogram tárgyprogrammá alakítja. A tárgyprogramokból a szerkesztés során futtatható állomány jön létre. - Fordítási idő: amikor a fordító dolgozik - Futási idő: amikor a program fut Interpretálás: A forráskódot az interpreter értelmezi és azonnal végrehajtja. - Fordítási és futási idő nem különül el A fordítás: − gyorsabb végrehajtás − a forrás alaposabb ellenőrzése − minden platformra külön le kell fordítani − C, C++, Ada, Haskell Az interpretálás: − rugalmasabb (pl. utasítások fordítási időben történő összeállítása) − minden platformon azonnal futtatható (ahol megtalálható az interpreter) − Perl, Php, Javascript Fordítás és interpretálás egymásra építve (pl. Java): 1 A forráskód fordítása bájtkódra. (bytecode) 2 A bájtkód interpretálása virtuális géppel. Virtuális gép: olyan gép szoftveres megvalósítása, amelynek a bájtkód a gépi kódja. − viszonylag gyors végrehajtás (lassabb a natív gépi kódnál) − platformfüggetlenség (ahol megtalálható hozzá a virtuális gép; pl Java) − Java, C#
2. Előfordító, fordító, szerkesztő A fordítás során (C-ben, C++-ban) a fordítás lépései sorrendben az előfordítás, fordítás és a linkelés. Ekkor a forráskódból a gép számára értelmezhető gépi kód keletkezik. Az egyes lépések nem különülnek el feltétlenül egymástól (nyelvtől, fordítóprogramtól függően), tehát például az előfordító nem generál le egy különálló fájlt, hanem a kimenetét azonnal átadja a fordítónak.
2.1. Előfordító (C, C++) Az előfordító (preprocessor) különböző szöveges változtatásokat hajt végre a forráskódon, előkészíti azt a tényleges fordításra. Feladatai: − Header fájlok beszúrása. − A forrásfájlban fizikailag több sorban elhelyezkedő forráskód logikailag egy sorba történő csoportosítása (ha szükséges). − A kommentek helyettesítése whitespace karakterekkel. − Az előfordítónak a programozó által megadott feladatok végrehajtása (szimbólumok behelyettesítése, feltételes fordítás, makrók, stb.). Az előfordítónak szóló utasítások első sora a kettőskereszt (#, hashmark) karakterrel kezdődik és alapesetben a sor végén véget is ér, de a \ jellel semlegesíthetjük a sorvége jelet. Ezek az utasítások (direktívák) a következők lehetnek: #define
<érték> A kódban kicseréli az azonosító előfordulásait az értékre (kivéve, ha kommentben, stringben, vagy más változó részeként találja meg). Ha nem írunk értéket utána, akkor semmit nem cserél ki semmivel, azonban definiálva marad és hivatkozhatunk rá az #ifdef, #ifndef direktívákkal.
#undef Egy definiált azonosítót szüntet meg. Innentől arra már nem hivatkozhatunk. #if, #elif, #else, #endif: Ha az #if után nem-nulla érték található, akkor az #if-et követő sorok kerülnek a fordítandó kódba. Minden #if-et egy #endif kell zárjon, közéjük tetszőleges számú #elif és utolsónak maximum egy #else kerülhet. #ifdef, #ifndef: Ellenőrizni lehet, hogy definiálva van-e egy bizonyos azonosító, és attól függően végrehajtani valamit. (Tipikus felhasználási lehetőség, hogy a header fileok csak egyszer legyenek bemásolva.) #include: Filebeillesztés. A file nevét macskakörmök, vagy kacsacsőrök közé lehet elhelyezni: #include : a megadott elérési utakon keresi a file-t. #include ”filenév”: először a abban a könyvtárban keresi a file-t, ahol az a file van, ahova akarunk beilleszteni, majd azokban a könyvtárakban, ahova ezt a file-t illesztettük be, majd az előre megadott elérési utakon. #line: Több olyan segédprogram is létezik, amely valamilyen speciális nyelven megírt programot C forrásprogrammá alakít át. A #line direktíva segítségével elérhető, hogy a C fordító program ne a C forrásszövegben jelezze a hiba sorszámát, hanem az eredeti speciális nyelven megírt forrásban. #line kezdősorszám ”filenév” #error: Hibaüzenetet generál. Ha egy #error sorhoz ér a fordító, akkor a fordítás abbamarad és a megadott hibaüzenetet jeleníti meg. Pl.: #if !defined(__cplusplus) #error C++ compiler required. #endif #pragma: Gép és operációs rendszer függő parancsok, valamint fordítónként más. Pl.: #pragma warning(disable:4786) A hosszú változó nevek miatt kiírt sok idegesítő figyelmeztetést iktatja ki.
2.2. Fordító A fordítás során történik meg a forráskód (a precompiler kimenete) szintaktikus és szemantikus elemzése, majd a tárgykód generálása. A fordító végrehajthat kódoptimalizációt is. Ez történhet a szemantikusan elemzett programon, vagy a tárgykódon is (kódgenerálás után). Célja a futási sebesség növelése, méret csökkentése (például felesleges utasítások megszüntetésével, ciklusok kigörgetésével).
2.3. Szerkesztő (linker) Feladata a forrásfájlokból külön lefordított object fájlokat egyetlen futtatható állománnyá összeszerkeszteni. Részletes leírását lásd a fordítási egységek résznél!
3. Make A make utility célja egy nagy program fordításának automatizálása. A make , a specifikáció alapján meghatározza, hogy mely részeket kell újrafordítani, és meghívja a megfelelô parancsokat. A make használatához először létre kell hozni a makefile -t, ami leírja a fájlok közötti függőségeket, és a fájlok frissítésére szolgáló parancsokat. Egy programban a végrehajtható fájl rendszerint az object fájloktól függ, amelyek pedig a forrás fájloktól függenek. A makefile-ba írhatunk: kommenteket: #-al kezdődő sorok változó definíciókat: gyakorlatilag makró helyettesítés történik változó definiálás: VALTOZO = ERTEK hivatkozás a változó értékére: $(VALTOZO) • explicit szabályokat: ez határozza meg, hogy mikor és hogyan kell lefordítani egy vagy több állományt. A szabályok három részből állnak. A cél állományból, a függőségi listából (azok az állományok, amelyektől a cél függ) és a a parancsból (fordító utasítás). A parancsot végrehajtja, ha a célállomány nem létezik vagy a függőségi listában van olyan állomány, amelyet később módosítottak, mint a cél állományt, azaz szükséges az újrafordítás. Alakja: Célok: függőségi lista; Parancs TABParancs A parancs vagy a függőségi listával van egy sorba ';'-el elválasztva, vagy új sorba, de ekkor TAB-al kezdődnie. • implicit szabályok: egy olyan állomány, amely szerepel valamelyik szabály feltételei között, de nem szerepel szabály céljaként, megpróbál alapértelmezett szabályt találni rá. (pl.: valami.o állományt a valami.cpp állományból lehet lefordítani) • direktívákat: az előfordítási direktívákhoz hasonlóak. Pl.: include, ifeq (feltétels fordítás) stb. • •
A make program meghívása: $ make Ekkor alapértelmezésként az akt. könyvtárban lévő Makefile nevű fájlt hajtja végre. Másik makefile használata: $ make -f masik_makefile Egy példa a makefile-ra: # Makefile # A proba nevű futtatható állomány és közbülső object fájlokra vonatkozó szabályok proba: main.o mo.o seged.o; g++ -o proba main.o seged.o mo.o main.o: seged.h seged.cpp mo.h mo.cpp; g++ -c -o main.o main.cpp mo.o: mo.h mo.cpp seged.h seged.cpp; g++ -c -o mo.o mo.cpp seged.o: seged.h seged.cpp; g++ -c -o seged.o seged.cpp
4. Fordítási egység, könyvtárak 4.1. Fordítási egység Fordítási egységnek nevezzük az egy forrásba tett és emiatt egyszerre fordított függvények és változók körét. A fájl (az egyes fájlrendszerekben) a tárolás és fordítás hagyományos egysége. Egy teljes programot rendszerint lehetetlen egy fájlban tárolni, már csak azért sem, mert a szabványos könyvtárak és az operációs rendszer forráskódja általában nem szerepel a program forrásában. A valóságos méretű alkalmazásokban az sem kényelmes és célszerű, ha a felhasználó saját kódját egyetlen fájl tárolja. A program elrendezési módja segíthet kihangsúlyozni a program logikai felépítését, segítheti az olvasót a program megértésében és segíthet abban is, hogy a fordítóprogram kikényszerítse ezt a logikai szerkezetet. Amikor a fordítási egység a fájl, akkor a teljes fájlt újra kell fordítani, ha (bármilyen kis) változtatást hajtottak végre rajta, vagy egy másik fájlon, amelytől az előző függ. Az újrafordításra használt idő még egy közepes méretű program esetében is jelentősen csökkenthető, ha a programot megfelelő méretű fájlokra bontjuk. A felhasználó a fordítóprogramnak egy forrásfájlt (source file) ad át. Ezután a fájl előfordítása történik: azaz végrehajtódik a makrófeldolgozás, az #include utasítások pedig beépítik a fejállományokat. Az előfeldolgozás eredményét fordítási egységnek (translation unit) hívják. A fordítóprogram valójában csak ezekkel dolgozik és a C++ szabályai is ezek formáját írják le. Ahhoz, hogy a programozó lehetővé tegye az elkülönített fordítást, olyan deklarációkat kell megadnia, amelyek biztosítják mindazt az információt, ami ahhoz szükséges, hogy a fordítási egységet a program többi részétől elkülönítve lehessen elemezni. A több fordítási egységből álló programok deklarációinak ugyanúgy következetesnek kell lenniük, mint az egyetlen forrásfájlból álló programokénak. A rendszerünkben vannak olyan eszközök, amelyek segítenek ezt biztosítani; nevezetesen a szerkesztőprogram (linker), amely számos következetlenséget képes észrevenni. Ez az a program, ami összekapcsolja a külön fordított részeket. A teljes összeszerkesztést el lehet végezni a program futása előtt. Emellett lehetőség van arra is, hogy később új kódot adjunk a programhoz (dinamikus szerkesztés). A program fizikai szerkezetén általában a forrásfájlokba szervezett programot értik. Szerkesztés fajtái: - statikus: az object fájlokat fordítási időbe összeszerkesztjük a könyvtárakkal - dinamikus, load-time: fordítási időben úgynevezett import könyvtárakat használunk, ezek a könyvtárak csak DLL-ekre vonatkozó hivatkozásokat tartalmaznak, amiket majd az operációs rendszer a program betöltésekor kapcsol hozzá a futtatható fájlhoz. Ha valamelyik hivatkozott DLL hiányzik, a programot nem lehet betölteni! - Dinamikus, run-time: fordítási időben a programban a könyvtárak betöltésére és az eljárások címeinek lekérdezésére vonatkozó rendszerhívások kerülnek a programba. A könyvtárak betöltése futás közben történik, amikor szükség van az adott könyvtárra. Ezzel a megoldással lehetőség van arra, hogy a program a neki megfelelő verziójú könyvtárat megkeresse, vagy például a program indításkor ellenőrizze, hogy van-e egyáltalán ilyen könyvtár.
4.2. Könyvtárak A programkönyvtárakat olyan alprogram-, modul-, osztály-, illetve adattípus-gyűjteménynek tekintjük, amelyek egy jól körülhatárolható szolgáltatáscsoportot megvalósító programkódot
tartalmaznak, és egységes felületet (interfészt) biztosítanak a felhasználó programozók számára. Mivel ezeket a programkódokat más, nagyobb programokba beépítve használják, fontos, hogy rendelkezzenek a következő tulajdonságokkal: helyesség (pontosan megoldja a feladatot), hatékonyság (lehetőleg gyors futási idő és kevés memória igény, bár e kettő egymás ellen dolgoznak), megbízhatóság (rossz bementi paraméterek esetén a lehető legkisebb kárt, problémát okozza), kiterjeszthetőség (továbbfejlesztése, módosítása egyszerű legyen), újrafelhasználhatóság (minél általánosabb legyen, hogy minél több feladat megoldására használható legyen), jól dokumentáltság.
5. Szintaktikus és szemantikus szabályok.
HIÁNY!!!!! 6. Statikus és dinamikus típusellenőrzés. A típusellenőrzés az az eljárás, ami vagy fordítási időben (statikus ellenőrzés) vagy végrehajtási időben (dinamikus ellenőrzés) ellenőrzi típuskényszerítés szabályait, és szükség esetén végrehajtja a előírt művelet(ek)et. A statikus ellenőrzés elsődlegesen a fordítóprogram feladata. Ha a nyelv kikényszeríti a típushoz tartózó szabályok végrehajtását (ez általában a típuskonverziók végrehajtását jelenti, lehetőleg információ vesztés nélkül), akkor a nyelv erősen típusos, ellenkező esetben gyengén típusos. Statikus: a kifejezésekhez fordítási időben a szemantikus elemzés rendel típust •
az ellenőrzések fordítási időben történnek
•
futás közben csak az értékeket kell tárolni
•
futás közben „nem történhet baj”
•
előny: biztonságosabb
•
pl.: Ada, C++, Haskell ...
Dinamikus: a típusellenőrzés futási időben történik •
futás közben az értékek mellett típusinformációt is kell tárolni
•
minden utasítás végrehajtása előtt ellenőrizni kell a típusokat
•
típushiba esetén futási idejű hiba keletkezik
•
előny: hajlékonyabb
•
pl.: Lisp, Erlang ...
Bizonyos feladatokhoz használni kell a dinamikus típusellenőrzés technikáit: •
objektumorientált nyelvekben a dinamikus kötés
•
Java instanceof operátora
Néhány statikus típus kezelő rendszert használó nyelv létrehoz egy "hátsó ajtót" a nyelvben, hogy lehetőség legyen olyan kódot írni, amelyen nem történik statikus típusellenőrzés – castolás.
7. Párhuzamos programozás
HIÁNYOS!!! Párhuzamos programozás Ada-ban Taszkok A taszkok párhuzamosan végrehajtott egységek. Egy taszkot egy logikai processzor hajt végre. Az egyes logikai processzorok egymástól függetlenek, párhuzamosan futtatják a taszkokat. A taszkok szinkronizálása a taszkok randevúival történik, ami egy hívást kiadó és egy fogadó taszk között zajlik le. Pl.:
task Hello is Entry üzenet(s:String := ”Hello World!”); End Hello; task body Hello is begin accept üzenet(s:String := ”Hello World!”) do put_line(s); end üzenet; end Hello;
Védett egységek A védett objektumok olyan adatokat tartalmaznak, melyekhez a taszkok csak védett műveletek segítségével férhetnek hozzá. A védett adatokat a protected típus private részében helyezzük el. Három fajta védett műveletet írhatunk: védett belépési pontot, védett eljárást és védett függvényt. A védett belépési pont megegyezik egy taszk őrfeltétellel ellátott belépési pontjával. Ha az őrfeltétel igaz, akkor a hívó taszk végrehajtja a belépési ponthoz tartozó törzset, különben a hívás bekerül a belépési pont várakozási sorába, amíg az őrfeltétel igazzá nem válik. A védett eljárásnak írási-olvasási joga van a private-ben meghatározott változókra. Amikor egy taszk egy védett eljárást hív meg, semmilyen más taszk nem férhet hozzá a védett változókhoz. A védett függvénynek csak olvasási joga van, ezért a védett függvényt egyszerre több taszk is meghívhatja. A belépési pont abban különbözik a védett eljárástól, hogy várakozási sora van, illetve őrfeltételt kell hozzárendelni. Pl.:
protected type Erőforrás is Entry Lefoglal; Procedure Elenged;
Private Foglalt : Boolean := False; End Errőforrás; protected body Erőforrás is Entry Lefoglal when Foglalt=False is Begin … End Kiir; End Erőforrás;
Randevú Egy entry hívásnál, ha a hívott taszk entryhez tartozó accept utasítás végrehajtásánál tart, végrehajtódik az accept utasítás törzse és a hívó taszk felfüggesztődik. Ezáltal valósul meg a két taszk között a randevú. Az accept törzs végrehajtása után mindkét taszk párhuzamosan fut tovább.
Select A select utasítás megkönnyíti a randevút, mely az accept delay és terminate utasításokkal együtt lehetővé teszi, hogy egy taszk egy adott ponton többféle hívást várjon és fogadjon, ugyanakkor a hívásokat szabályozni is lehet a segítségével. A select utasítás biztosíthatja azt is, hogy egy hívott taszk automatikusan lezáruljon, valamint azt is lehetővé teszi, hogy egy taszk folytassa tevékenységét, ha egy bizonyos idő elteltével nem jön létre a randevú.