Jak vytvořit program v Linuxu?

V tomto článku se podíváme na vytváření programů pod naším oblíbeným operačním systémem. Projdeme si cestu od zdrojového kódu, přes kompilaci, linkování a debugování, nastíním zde základy vytváření souboru Makefile a skončíme balíčkem pro koncového uživatele. Jak jste už mohli vytušit, nebudu zde rozebírat žádné vývojové prostředí ala M$ Visual Studio, které provede všechny výše zmíněné kroky za vás (snad kromě toho zdrojového kódu, i když ... :-)), a které má své výhody, ale i nevýhody(!). Toho, kdo vytváří programy v těchto prostředích (př. gIde, KDevelop) a nemá zájem poodhalit jejich tajemnou práci při tvorbě programu, mohu odkázat na článek Vývojová prostředí. No a pro ty zvídavé - jdeme na to!

Zdrojový kód

Jak všichni dobře víme, zdrojový kód je jen obyčejný text (i když někdy pro obyčejného smrtelníka dosti nečitelný :-)), a můžeme jej tedy psát ve svém oblíbeném textovém editoru (můj je Emacs). On s tím počítá a nabízí nám zvýraznění syntaxe jazyka, pretty-print a další užitečné věci, což myslím není k zahození.

Pokud je náš kód použitelný i na jiných systémech než Linux, věci specifické pro Linux uzavřeme takto (v jazyce C):

#ifdef __linux__
/* ... nas Linuxovy kod ... */
#endif

Symbol __linux__ je jeden ze symbolů, které definuje kompilátor automaticky při každé kompilaci.

Příklad

Příkladem v celém článku bude velmi jednoduchý program, který vypíše dvě řádky textu. Funkce na vypis textu bude v knihovně. Zdrojové texty:

soubor "libtext.h":

void napis(char *);

soubor "libtext.c":

#include < stdio.h >
#include "libtext.h"

void napis(char *text)
{
  printf(text);
}

soubor "priklad.c":

#include "libtext.h"

int main()
{
  napis("Hello World!\n");
  napis("Hello again!\n");
  return 0;
}

Kompilace

Máme tedy zdrojový text programu, teď jej musíme přeložit (zkompilovat). Na to je kompilátor pro ten náš zvolený jazyk, který přeloží zdrojový text do objektového souboru (*.o). Já se zde jen krátce zmíním o kompilátoru projazyk C (C++) (a linkeru zároveň, viz. dále), o programu gcc (nebo jeho nadstavbě g++) (GNU C and C++ Compiler).

Kompilátor gcc (nebo egcs, záleží na implementaci (Ehm, v novejsich verzich je to jedno a to same - chicky)) je velmi komplexní a propracovaný nástroj. Snad jako každý dobrý kompilátor má spoustu (a ještě mnohem víc) různých přepínačů, voleb a nastavení. Samozřejmě je zde nebudu všechny jeden po druhém popisovat, to by bylo nošení dříví do lesa, od toho je tu nepostradatelné gcc info a manuálová stránka. Snad jen ty nejdůležitější:

-c -- jen kompilace, ne linkování
-o file -- výstupní soubor se bude jmenovat file
-Dmacro nebo -Dmacro=defn -- definuje makro nebo symbol (př. __linux__) jako 1 nebo defn, stejně jako #define ve zdrojáku
-Umacro - zruší definici makra nebo symbolu, stejně jako #undef ve zdrojáku
-Idir -- hledá hlavičkové soubory (*.h) i v adresáři dir (defaultně se hledá jen v /usr/include)
-llibrary -- při linkování použije knihovnu jmémem liblibrary.[a|so]
-Ldir -- hledá knihovny i v adresáři dir
-static -- linkuje staticky
-shared -- výsledkem linkování je sdílená knihovna
-W nebo -Wall -- vypisuje chyby v kódu (některé nebo všechny)
-g nebo -glevel -- generuje informace pro debugování úrovně level (defaultně 2, 1-3)
-O nebo -Olevel -- úroveň optimalizace výsledného kódu (defaultně 1, 0-3, řekl bych, že úroveň 1 je pro obyčejné programy plně postačující)

Pro jiné jazyky se použije kompilátor toho jazyka.

Linkování

Objektové soubory (*.o) musíme složit dohromady (slinkovat) pomocí linkeru. Linker tyto objektové soubory složí spolu s knihovnami (pokud se linkuje staticky) nebo jen vyřeší jejich závislosti (pokus se linkuje dynamicky) do výsledného spustitelného souboru (pro Linux nejčastěji ELF formátu).

V Linuxu představuje linker program ld (GNU linker, info o něm najdete v ld info a manuálové stránce), ale některé kompilátory provádějí i linkování (často voláním programu ld).

Linkovat můžeme buď staticky (u gcc parametr -static) nebo dynamicky (u gcc defaultně). V případě statického linkování se použijí statické knihovny (*.a) a kousky kódu z knihovny se vloží do výsledného souboru, který tudíž narůstá. U dynamického linkování se použijí sdílené knihovny (*.so pro ELF) a do výsledného souboru se jen uloží informace, že se mají natáhnout spolu s programem. Pokud kompilátor nemůže linkovat dynamicky (knihovny nejsou tam, kde mají být, nebo jsou nečitelné), linkuje staticky.

Pro zjištění, jaké sdílené knihovny program používá, existuje program ldd, pro výpis všech symbolů z objektového souboru (a tudíž i z knihovny) pak program nm.

Doporučený formát pro linkování (vytvoření) sdílené knihovny je:

gcc -Wall -shared -Wl,-soname,libneco.so.major -o libneco.so.major.minor

(major a minor jsou čísla verze knihovny :-)). Při linkování programu se knihovny hledají v adresářích uvedených v proměnné prostředí LIBRARY_PATH, ale nejdříve v těch u volby -L. Při spuštění programu se knihovny hledají v adresářích uvedených v proměnné prostředí LD_LIBRARY_PATH a pak v souboru /etc/ld.so.conf. Více se o sdílených knihovnách a explicitní práci s nimi dočtete v Sdílené knihovny.

Příklad

Nejdříve musíme sestavit knihovnu libtext.so (sdílená):

gcc -Wall -shared -o libtext.so libtext.c

a teď program "priklad" (dynamicky linkovaný):

gcc -Wall -o priklad priklad.c -L. -ltext

(-L. říká, že knihovna libtext.so se má hledat i v aktuálním adresáři).

Doporučuji používat parametr kompilátoru -Wall (i když u tohoto příkladu by se žádná chybová hlášení neměla vyskytnout).

Pokud program spustíte, vypíše se však místo očekávaného textu chybová hláška:

./priklad: chyba při natahování sdílených knihovan: libtext.so: nelze otevřít sdílený objektový soubor: Soubor nebo adresář neexistuje

Vše funguje perfektně tak, jak má, že?! Víme přece, kde se hledají knihovny - a v adresáři s programem určitě ne (doufám, že tento příklad nikdo nezkouší jako root v adresáři se standardními knihovnami !!!). Soubor /etc/ld.so.conf měnit nebudeme (protože ani nemůžeme - nejsme root) a proto do proměnné prostředí LD_LIBRARY_PATH přidáme adresář, ve kterém máme knihovnu libtext.so (př. aktuální adresář, pro interpret bash je to příkaz):

export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:."

a program teď skutečně funguje.

Debugování

Jistě každý, kdo už nějaký program vytvořil (pro jakýkoliv OS), ví, že eliminace varovných hlášení kompilátoru (warnings) jako veškeré odstranění chyb nestačí. V kódu se totiž mohou vyskytnout chyby, které se projeví až při běhu programu (runtime errors). Bohužel těchto chyb se dá nadělat mnohem víc, než těch, co odhalí kompilátor. Jak ale chybu odhalit, když program spadne a už se o něj víc nedozvíme (snad kromě toho "smetí" v core souboru :-))? Odpověď je snadná - musíme program debugovat, tj. spustit ho v jiném programu, který jeho chybu zachytí, v debuggeru.

Debugger potřebuje, aby v programu byly určité informace, informace pro debugování. Tyto informace přidáme do programu při kompilaci. Pro gcc je to volba -g (-glevel), a navíc nesmí být zapnuta volba -fomit-frame-pointer (může být zapnuta volbami optimalizace!). Informacemi pro debugování nemusíme kompilovat do celého programu, je do těch částí, které chceme debugovat.

U Linuxu je nejběžnějším debuggerem program gdb (The GNU Debugger), nebo jeho Xový interface xxgdb. Jako parametr se mu zadá program, který chceme debugovat, a pak se ovládá příkazy. Zde uvedu jen ty nejdůležitější (více viz. (xx)gdb info a manuálová stránka):

break function -- nastaví breakpoint (přerušení) na funkci function
run [arglist] -- spustí program (s parametry arglist)
print expr -- zobrazí hodnotu výrazu expr
c -- pokračuje v programu (př. po breakpointu)
next -- vykoná další řádek kódu programu, nevstupuje do funkcí
step -- jako next, ale vstupuje do funkcí
help [name] -- nápověda (k příkazu name)
quit -- ukončení gdb

Další velmi užitečný program je strace. Tento program vám vypíše různé informace o všech systémových voláních, která váš program volá, a signálech, které program dostane. Zvláště pri některých "nepochopitelných" skrytých chybách je to spásný a nepostradatelný nástroj na jejich odhalení! Program se mu zadá jako parametr, ostatní parametry viz. strace info nebo manuálová stránka.

Makefile

Tato kapitola je velmi stručným úvodem do vytváření souboru Makefile, zájemce můžu opět odkázat na sqělé make info a manuálovou stránku.

Pro jednoduché programy (jako náš příklad) stačí na kompilaci a slinkování jeden řádek v shellu. Ale pro rozsáhlejší projekty, kdy je zdrojový kód ve více souborech, by bylo psaní příkazu pro každý soubor velmi neefektivní a zdlouhavé. Samozřejmě to za nás může udělat skript, ale brzy zjistíme, že to stále není ono (už jen proto, že takhle to nikdo nedělá :-)). Pro kompilaci a linkování malých i velkých (př. jádro) programů nebo knihovan se prostě používá pouze a jedině Makefile.

Makefile je obyčejný textový soubor, ve kterém jsou popsány závislosti mezi soubory a způsob kompilace a linkování. Tento soubor používá program make, který jej po spuštění hledá jako soubor Makefile, makefile nebo GNUmakefile.

Příklad

Soubor "Makefile":

# Program "priklad", 2000
#

CFLAGS = -Wall

priklad: priklad.c libtext.h libtext.so
	$(CC) $(CFLAGS) -o $@ $< -L. -ltext

libtext.so: libtext.c libtext.h
	$(CC) $(CFLAGS) -shared -o $@ $<

clean:
	rm -f libtext.so priklad

První dva řádky jsou komentáře.

Nejdříve se nastaví proměnná CFLAGS. Proměnné se definují jako jmeno = hodnota a volají se jako $(jmeno) (bez závorek, pokud je jméno jednopísmenné).

Pak se určí první závislost - na čem je závislý samotný program: na zdrojových souborech priklad.c, libtext.h a knihovně libtext.so, která se musí teprve vytvořit. Závislosti se píší ve tvaru cile: soubory_na_kterych_jsou_cile_zavisle, kde cile jsou jsou soubory, které se mají vytvořit, oddělené mezerou, a za dvojtečkou jsou soubory, které jsou k tomu potřeba.

Další řádek je příkaz, kterým se vytvoří program "priklad" a vykoná se jen, pokud bude některý ze souborů priklad.c, libtext.h nebo libtext.so novější než soubor priklad. POZOR: Příkaz v Makefile musí začínat tabulátorem (mezery make nezbaští)!!! make každý příkaz vypisuje. Pokud nechcete, aby se příkaz nepsal na výstup, napište před něj zavináč @.

Další řádky určují závislost a způsob vytvoření knihovny libtext.so.

Pokud potřebujete něco, co má být na jednom řádku, rozdělit do více řádků, musí být na konci všech řádků (kromě posledního) zpětné lomítko \.

V praxi (a v tomto příkladě také) se často používají již předdefinované proměnné, např.:

CC -- kompilátor, defaultně cc
MAKE -- program make; tato proměnná se používá k rekurzivnímu volání make
$ -- znak $
@ -- cíle v určení závislosti, tj. všechno před dvojtečkou
< -- první soubor za dvojtečkou
^ -- všechny soubory za dvojtečkou (oddělené mezerou, každý jedenkrát)

a dále se obvykle nastavují např. tyto (defaultně jsou nastavené na ""):

CFLAGS -- volby kompilátoru
LDFLAGS -- volby linkeru
TARGETS -- konečné cíle, většinou jméno programu

Nyní můžete spustit program make. Ten najde soubor Makefile a vezme si první cíl (priklad). Soubor libtext.so neexistuje, takže make přejde na určení jeho závislosti, vytvoří jej a nakonec už může provést cíl priklad. Pokud spustíte make s parametrem priklad, stane se totéž. Pokud jej však spustíte s parametrem libtext.so, vytvoří se jen knihovna. Při spuštění bez parametru si make totiž vezme ten první cíl, parametrem můžeme určit, jaký cíl má provést. Pokud za dvojtečkou není uveden žádný soubor, přikaz se provede vždy. Klasickým příkladem je "čistka" (vymazání souborů vzniklých kompilací a linkováním):

clean:
	rm -f priklad.o libtext.so priklad

Program make má samozřejmě také nějaké parametry, zde jen ty nejdůležitější:

-C dir -- ještě před čtením Makefile se přepne se do adresáře dir; používá se při rekurzivním volání make
-f file -- místo Makefile zpracovává soubor file
-j jobs -- počet úloh (příkazů) prováděných paralelně; pokud jobs není uvedeno, počet není omezen
-n -- jen vypisuje příkazy, které by vykonal, ale nevykonává je
-s -- nevypisuje příkazy

Zabalení

Teď, kdy už všechno máme hotové, můžeme zdrojové texty a Makefile (a vše ostatní, př. datové soubory, dokumentaci, ...) zabalit do balíčku pro uživatele. Standardně jsou to archivy tar.gz (tgz). Balíček vytvoříme takto:

tar cfz balicek.tar.gz adresar_s_programem

Tento balíček můžeme dát např. na internet. (Více se o balení pod Linuxem dozvíte v článku Balení a rozbalování pod Linuxem.)

Příklad

Vytvoření balíčku:

tar cfz priklad.tar.gz priklad

Uživatelská část

V této části se podíváme na uživatelovo počínání s naším programem. Ten si např. stáhne náš program (balíček) z internetu a zkopíruje do aresáře, kde jej chce mít. Teď musí balíček rozbalit:

tar xfz balicek.tar.gz 

Pak přejde do adresáře s programem a zkompiluje ho:

make

Nakonec je někdy potřeba udělat ještě nějaké další věci, které jak který program potřebuje (př. pro náš příklad - nastavit proměnnou prostředí LD_LIBRARY_PATH).

Jak je vidět, uživatel má nesrovnatelně jednodušší práci než my - vývojáři (nebo chcete-li programátoři) - a je to tak určitě DOBŘE. Co se dá dělat - tak už to na tom světě chodí! :-).

Závěr

Tento článek měl problematiku vytváření programů pod Linuxem jen nastínit, pro zájemce je tu nepostradatelné info a manuálové stránky (nejen zmíněných programů).
Řekl bych, že pro toho, kdo před přečtením tohoto článku vůbec nevěděl, jak na to, by už neměl být problém nějaký ten prográmek vytvořit.

Teď už tedy víte, jak vytvořit program v našem oblíbeném operačním systému a proto vám můžu s radostí popřát spoustu vynikajících programů !



--- JOHNY.5 ---