Nagy Gergely C/C++ programozás UNIX környezetben Tartalomjegyzék 1. Ismerkedés a UNIX terminállal 2 1.1. Fontos parancsok....................................... 2 1.2. Egyszerű szövegszerkesztő program............................. 3 2. Egyszerű, C/C++ nyelvű projektek fejlesztése és tesztelése 3 2.1. A make program használata................................. 4 Speciális célállományok.................................... 4 A make vezérlő karakterei.................................. 5 A gcc make támogatása a függőségek felderítésében.................... 5 2.2. Hibakeresés a programokban................................. 6 A gdb indítása és a program lefuttatása........................... 6 Töréspontok elhelyezése, soronkénti futtatás........................ 6 Kifejezések kiíratása..................................... 7
1. Ismerkedés a UNIX terminállal 1.1. Fontos parancsok echo Szöveg kiírása a képernyőre. echo "Hello" -e Értelmezi a speciális vezérlő-karaktereket. echo -e "\thello vilag!\n" cat Kiírja egy fájl tartalmát a standard kimenetre. cat a.txt Argumentumok nélkül indítva a standard bemenetet másolja ctrl-z érkézéséig. cd Katalógus (könyvtár, mappa) váltás (change directory). cd alma pwd Az aktuális katalógus (print working directory). rm Fájlok/könyvtárak törlése (remove directory). -r Rekurzív működés így lehet könyvtárakat és a tartalmukat törölni. mkdir Könyvtár létrehozása. mv Fájl mozgatása. mv ezt.txt ide/ mv ezt.txt ide/ilyen_neven.txt cp Fájl másolása használata hasonló az mv-éhez. more Fájlok képernyőre bontott kiírása. space következő képernyő b következő képernyő enter következő sor q kilépés chmod Hozzáférési jogok megváltoztatása. u user g group o other + hozzáadás - elvétel r olvasás w írás x futtatás chmod ugo+x script.sh ln a b Link létrehozása (hard link, a-ra mutat b) 2
-s szimbolikus link df Csatolt kötetek (meghajtók) listázása. -h A méretek emberi kiírása (SI mértékegységgel, nem bájtokban). du Az aktuális katalógus által foglalt lemezterület (itt is van -h kapcsoló). 1.2. Egyszerű szövegszerkesztő program Szinte minden UNIX/Linux rendszeren megtalálható a pico (vagy GNU változata: nano). Kódszerkesztéshez érdemes -i kapcsolóval indítani ilyenkor a automatikus behúzás üzemmódba kerül, ami azt jelenti, hogy egy új sort a felette lévő alatt kezd, tehát a sor elején található szóközöket és tabulátorokat lemásolja automatikusan. A legfontosabb gyorsbillentyűk megtalálhatóak a képernyő alján szerkesztés közben. A ctrl-o hatására menti az aktuális fájlt, a ctrl-x lenyomásával kiléphetünk a programból. 2. Egyszerű, C/C++ nyelvű projektek fejlesztése és tesztelése Egy forrásállományból álló program lefordítása a GNU C fordítóval: gcc -o futtathato program.c Látható, hogy a -o kapcsoló után kell megadni a kimeneti fájl nevét, utána pedig a bementi fájl(oka)t. Ha sikeres a fordítás, akkor az aktuális kötetbe kerül a futtathato nevű program. Mivel az UNIX rendszerek alatt az aktuális könyvtár nem része az alapértelmezett elérési útvonalnak, ezért eléréssel kell megadni a fájlt, hogy futtathassuk. Ennek legegyszerűbb módja (ha egy könyvtárban vagyunk vele):./futtathato Összetettebb projektek esetén a definíciókat tartalmazó fájlokat (a c fájlok) előbb tárgykodú állományokká kell fordítanunk, majd ezeket össze kell szerkesztenünk. Tegyük fel, hogy projektünk a következő három állományból áll: seged.h, seged.c, program.c. Ekkor a fordítás menete a következő: gcc -c -o seged.o seged.c gcc -c -o program.o program.c gcc -o futtathato seged.o program.o A -c kapcsoló azt mondja meg a fordítónak, hogy csak tárgykodú állományt készítsen, az összeszerkesztési (linking) lépést hagyja ki. Ha C++ nyelven szeretnénk dolgozni, akkor mindent ugyanígy kell végeznünk, csak a GNU fordító csomagnak egy másik elemét, a g++-t kell használnunk. Tehát a hello.cpp nevű fájlt fordítása így történik: g++ -o futtathato hello.cpp 3
2.1. A make program használata A make segítségével automatizálhatjuk a fordítást. Ezzel egyrészt megkíméljük magunkat a fordítási parancsok begépelésétől, másrészt kihasználhatjuk azt, hogy a make ismeri a fájlok függőségeit és minden esetben csak azokat a fordítási utasításokat végzi el, amelyekre ténylegesen szükség van. Például, ha egy fájlt nem módosítottunk a legutóbbi fordítás óta, akkor őt nem fordítja újra. Ennek akkor láthatjuk hasznát, amikor egy nagy projekttel dolgozunk, ahol egy teljes újrafordítás percekig, akár órákig is tarthat. A make meghívásához szükség van egy konfigurációs fájlra, ami leírja a projektben található függéseket (mely állományoknak kell készen lenniük egy adott állomány előállításához) és az ezek feloldásához szükséges műveleteket. Ennek a fájlnak az alapértelmezett neve: Makefile. Egy Makefile az alábbi felépítésű elemekből áll: célfájl: függőségek listája utasítás(ok) Nagyon fontos, hogy az utasítás(ok)nak beljebb kell kezdődniük, ugyanis innen tudja a make, hogy egy utasításblokk meddig tart. Például az előző pontban látott projekthez az alábbi Makefile tartozhat: futtathato: seged.o program.o gcc -o futtathato seged.o program.o seged.o: seged.c gcc -c -o seged.o seged.c program.o: program.c gcc -c -o program.o program.c A fenti fájl szövegesen azt jelenti, hogy a futtathato nevű program előállításához szükség van, hogy a seged.o és a program.o fájlok friss verziói rendelkezésre álljanak. Ha ez teljesül, akkor a gcc -o futtathato seged.o program.o sorral tudjuk őt előállítani. A seged.o fájl a seged.c fájltól függ és a gcc -c -o seged.o seged.c sorral áll elő. A program.o fájlnál hasonló a helyzet. Amennyiben a projektet egyszer már fordítottuk és megváltoztatjuk a seged.c tartalmát, akkor a make észreveszi, hogy a seged.c módosítási ideje későbbi, mint a seged.o-é, ezért a seged.o-t újra fogni generálni. Ekkor viszont a seged.o újabbá válik, mint a futtathato, tehát belőle is új példány készül. Speciális célállományok A make mindig az elő célállománnyal kezd. Ezen úgy lehet módosítani, hogy a neve után írjuk futtatáskor az előállítani kívánt fájl nevét. Lehetőség van arra is, hogy álcélokat, ún. phony-kat hozzunk létre, amelyek nem jelentenek ténylegesen létrehozandó fájlt, ugyanakkor a függőségek figyelembevételével végrehajtanak utasításokat. Például, ha több futtathatót is létre szeretnénk hozni, akkor létrehozhatunk egy all nevű célt: 4
.PHONY: all futtathato1:... futtathato2:... all: futtathato1 futtathato2 Jelen esetben nem is kell megadni utasítást, hiszen célunk csupán annyi, hogy frissüljön futtathato1 és futtahato2, amelyeknek az előállítását már korábban leírtuk. A fenti esetben a make-et így hívhatjuk: make all Szokásos phony még a clean:.phony: clean clean: rm *.o futtathato Itt nincs függőség, vagyis mindenképpen végrehajtódik és kitörli az összes tárgykódú fájlt és a futtathatót (hívása: make clean). A make vezérlő karakterei Sok feladat automatizálására lehetőséget ad a make. Így például kiterjesztés alapján adhatunk általános parancsot bizonyos műveletekre. Például azt, hogy minden c fájlból készüljön tárgykódú fájl (o kiterjesztés), így mondhatjuk meg: %.o: %.c gcc -c $< -o $@ Ez azt jelenti, hogy ha szükség van egy fájlra (egy más szabály megköveteli), amelynek o a kiterjesztése, akkor azt az azonos nevű c fájlból kell előállítani a megadott módon. A $@ egy vezérlő karakter, ami a make bejegyzés célállományának a nevére hivatkozik. A $< a függőségek közül az elsőt jelenti. A gcc make támogatása a függőségek felderítésében A fenti make feltételekkel egy baj van: a fejléc állományoktól nem függenek a tárgykódú fájlok. Ezzel az a probléma, hogy ha egy deklarációt megváltoztatunk, nem fordul le újból az adott kód, csak akkor, ha a defincíción is módosítunk. A beszerkesztett fájlokat minden make bejegyzéshez felsorolni meglehetősen fáradságos, ezért léteznek eszközök, amelyek segítenek ebben. Többek közt maga a fordító is támogatja ezt. Ha az alábbi módon hívjuk a fordítót: gcc program.c -M akkor ki fogja írni, hogy milyen függőségei vannak a program.c-nek, méghozzá pontosan make formátumban. Ha -MM kapcsolót használunk, akkor csak azokat a függőségeket sorolja fel, amelyek a saját projektünkből fakadnak, a rendszerfüggőségeket nem (azok ugyanis ritkán változnak fejlesztés közben). 5
2.2. Hibakeresés a programokban A hibakereséshez (debuggolás) több segédprogram is elterjedten használt a UNIX/Linux rendszereken. Az alábbiakban a gdb-ről lesz néhány szó. A gdb segítségével egy programot soronként lehet futtatni közben kiírva a változók (illetve tetszőleges C kifejezések) értékét. Bemenete a programunk futtatható állománya. Ha egy program nyers, gépi kódú verziójával dolgoznánk, akkor elvesznének számunkra a változó-, és függvénynevek, és általában azt sem tudnánk, hogy meddig tart egy C sor. Ezeket az információkat mind el kell tárolni a futtaható állományban ahhoz, hogy azt egy hibakereső program fel tudja dolgozni és a programozó számára hasznos információkat tudjon adni a futásközbeni viselkedésről. Ugyanakkor, ha ezek az információk mindig belekerülnének a futtatható állományokba, akkor azok nagyon nagyok lennének feleslegesen, ezért egy speciális kapcsolóval (-g) kell a fordítót futtatni annak érdekében, hogy az ún. debug információk bekerüljenek a bináris állományba. A gdb indítása és a program lefuttatása A gdb futtatásakor meg kell adnunk a programunk binárisának a nevét (a futtatható fájlt): gdb futtathato Ekkor elindul a program, kapunk egy belső promptot (általában ezt: (gdb)), ahová utasításokat írhatunk a gdb-nek. A programunkat futtatni a run paranccsal tudjuk, ami után megadhatjuk a programunknak szánt parancssori paramétereket is. A legtöbb gyakran használt parancsnak a gdb-ben van rövid neve is. A run parancsé: r. Tehát, ha a programunkat a "szia" sztringgel felparaméterezve szeretnénk elindítani, akkor a kiadandó parancs: (gdb) r futtathato szia Ezzel még túl sokra nem megyünk, hiszen így csak lefut a program, megjelenik a képernyőn minden, amit az a szabványos kimenetre küldött és a végén kapunk egy üzenetet. Ha nem történt hiba, akkor ezt: Program exited normally. Azért már ennek is tud haszna lenni hibás esetekben, ugyanis ha a program valamiért hibát okoz (pl. segmentation fault), akkor a backtrace (röviden: bt) utasítás segítségével kiírattathatjuk az összes stack frame-et, vagyis a stack (verem) teljes tartalmát. Így pontosan meg tudjuk állapítani, hogy mely függvényben történt a hiba és milyen függvényhívások során jutottunk el oda. A gdb-ből kilépni a quit (röviden: q) paranccsal tudunk. Töréspontok elhelyezése, soronkénti futtatás A lépésenként való futtatáshoz el kell helyeznünk egy megállási pontot (töréspont, breakpoint) a programban, vagyis meg kell mondanuk, hogy meddig fusson szabadon és hol álljon meg. A megállástól kezdve pedig lépésenként futtathatjuk a kódot, vagy újból továbbindíthatjuk, hogy végigfusson, vagy beleakadjon a következő töréspontba (ami akár ugyanaz is lehet, amennyiben a program visszatér az adott pontra). Töréspontot a break (röviden: b) paranccsal tudunk elhelyezni. Ennek több formája is létezik: break main.c:37 A megadott fájl megadott sorában áll meg. A fájl neve elhagyható. break valami.c:fuggveny A megadott függvény hívásakor áll meg. A fájlnév itt is elhagyható. 6
break +offset Az aktuális ponttól offset sorral később áll meg (létezik negatív előjeles változata is). Érdemes a debuggolás kezdetekor legalább egy töréspontot elhelyezni, majd elindítani a futást. A gdb ekkor kiírja a következő programsort megadva annak számát, illetve amikor egy új fájlba váltunk, akkor a fájl nevét is. Lépdelni kétféleképpen tudunk: 1. a függvényhívásokba belelépve: ami azt jelenti, hogy ha az adott sorban történik egy függvényhívás, akkor beugrunk az adott függévybe és azt is soronként futtatjuk erre a step (röviden: s) parancs használható; 2. a függvényhívásokon átlépve: aminek során a függvényhívásokat egy lépésben végrehajtja a gdb, nem kell végiglépdelnünk az adott függvény sorain ehhez a next (röviden: n) parancsot kell használnunk. Kifejezések kiíratása A futtatás során bármikor lekérdezhetjük tetszőleges C kifejezések értékét feltéve, hogy az azokban szereplő összes változó létezik/látható az aktuális programblokkban. Az érték lekérdezése a print (röviden: p) paranccsal történik. Például az alábbi programrészletben int i; for (i = 0; i < 4; ++i) { int j = i + 6; printf("%d", j); } puts("kívül vagyok a cikluson."); ha épp a printf függvény során állunk, akkor lekérdezhetjük az i és a j változó értékét is. Ugyanezt a puts sorában állva a j-vel kapcsolatban már nem tehetjük meg. Ahogy említettük, nem csak egyszerű változóértékek, hanem kifejezések is lekérdezhetőek: (gdb) p i+5 (gdb) p *egy_pointer (gdb) p struct_mutato->mezo_nev 7