Jak vytvořit program v Linuxu?

Tento text jsem napsal jako takový malý tutoriál o vytváření programů pod mým (a doufám, že i vaším) oblíbeným operačním systémem. Vytvoření nějakého programu nespočívá jen v napsání zdrojáků, zkompilování, funguje, dobře a hotovo. Program se musí napsat úhledně podle určitých pravidel, správně zkompilovat a slinkovat. A protože žádný program nelze napsat napoprvé úplně bez chyb, dojde zákonitě (Murphy) na debugování (odvšivení), tzn. musí se provést základní otestování. Pak zjistíme, že se nám nechce psát příkazy pro sestavení programu ručně a napíšeme si Makefile, ve kterém popíšeme pravidla pro sestavení programu. A protože jsme už viděli "standardní" (tzn. sestavení ve stylu configure; make; make install) balíčky z internetu, budeme chtít, aby ten náš byl taky takový - tzn. použijeme GNU nástrojů. U každého "slušného" programu by neměla chybět dokumentace. Projdeme si zde postupně celou cestu od zdrojového kódu, přes kompilaci, linkování a debugování, vytvoříme nějaký ten Makefile, pak ukážu, jak se (nejen) Makefile dělá pomocí GNU nástrojů a skončíme úhledným 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 (chraň bůh!), které (možná) provede všechny výše zmíněné kroky za vás, snad kromě toho zdrojového kódu, i když ... :-). Ta mají své výhody, např. GUI (už i dnešní programátoři se asi bez GUI neobejdou, ach jo). Na co se fakt hodí (pokud to umí), je naklikání GUI našeho programu, protože je to rychle a stejně pořád dokola - okno, menu, toolbar, atd.. Mají ale i (podstatné) nevýhody, např. než se s nimi naučíte vyrobit nějaký program, přijdete o nervy, že? Toho, kdo vytváří programy v těchto prostředích (př. Anjuta, KDevelop) (dobře mu tak!), mohu jen odkázat na článek Vývojová prostředí. No a pro ty zvídavé - hurá na to ! (a ať to stojí za to ;-)) )

Protože v Linuxu se programy píší většinou v jazyce C (popř. C++), budu se zde i já zabývat vytvořením programu v "céčku".

Obecné rady

Nejdříve bych zmínil nějaké obecné rady a doporučení při psaní programů. Cílem standardů, které je doporučují (GNU Coding Standards), je, aby byl software čitelnější, konzistentní a snáze spravovatelný (změny, opravy, instalace, ...).

V neposlední řadě je dobré opatřit váš kód dobrou licencí (nejlépe GNU GPL), aby si váš sqělý kód nepřivlastnil nějaký velký softwarový gigant a nevydělával pak na něm nekřesťanské peníze.

Samozřejmě, že tento výčet není úplný, dala by se vymyslet ještě spousta dalších užitečných doporučení, kterých je radno se držet. Pokud máte nějaké, které se vám také vyplatilo dodržovat a které má opodstatnělý důvod, můžete mi ho napsat a já ho sem přidám. To samé platí pro formátování kódu dále.

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, jaký je váš?). On s tím počítá (jinak by nebyl oblíbený, žejo) a nabízí nám pretty-print, zvýraznění syntaxe jazyka a další užitečné blbinky (které jsou ovšem velice důležité), což určitě není k zahození.

O formátování kódu se Emacs stará automaticky sám, pro správné odsazení řádku stačí zmáčknout klávesu Tab. Pro "obarvení" kódu je nejlepší napsat do konfiguračního souboru ~/.emacs tento LISPový kód:

(global-font-lock-mode t)

Zdrojový kód nelze jen tak nějak napsat. Kód musí být napsaný tak, aby jej bez obtíží dokázal přečíst i člověk, ne jen překladač. Musí mít nějakou vizuální strukturu (říká se tomu štábní kultura). Existují různá doporučení na formátování kódu, ty moje pocházejí jak jinak než z GNU Coding Standards.

Pokud je náš kód použitelný i na jiných systémech než Linux, věci specifické pro Linux můžeme ohraničit takto:

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

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

Standardní přípona pro céčkovský zdroják je .c, pro C++ zdroják používám .cc a pro hlavičkový soubor .h.

Příklad

Příkladem v celém textu bude velmi jednoduchý program, který napíše převrácenou hodnotu čísla zadaného jako parametr. Program nemá téměř vůbec žádné kontroly proti chybám, což je záměrné (viz debugování). Zdrojové texty:

soubor "rec.h":

#ifndef __REC_H__
#define __REC_H__

double
reciprocal(int);

#endif

soubor "rec.c":

#include "rec.h"

double
reciprocal(int i)
{
  if (i != 0) return 1.0/i; else return i;
}

soubor "priklad.c":

#include <stdio.h>
#include <stdlib.h>
#include "rec.h"

int
main(int argc, char *argv[])
{
  int i;

  i = atoi(argv[1]);
  printf("Převrácená hodnota %i je %g\n", i, reciprocal(i));
  return 0;
}

Symbol __REC_H__ se definuje z jednoho prostého důvodu - aby se hlavičkový soubor nevkládal vícekrát do toho samého zdrojáku. To se může stát pokud se hlavičkový soubor a.h vkládá do našeho zdrojáku a ještě do jiného hlavičkového souboru b.h a tento se také vkládá do našeho zdrojáku. Pak tu máme dvojí vložení a.h, přímo a nepřímo přes b.h. Proto se používá tento "trik", aby k vícenásobnému vkládání nedocházelo.

Nikdo nemůže nosit všechny jména a deklarace funkcí a typů v hlavě a proto existuje programátorská dokumentace, přímo v distribuci. Systémová volání jsou v sekci 2 manuálových stránek, funkce standardních knihoven v sekci 3. Všechny výskyty funkce nebo příkazu name v manuálových stránkách lze získat pomocí whatis name a pokud neznáme přesně jméno funkce, můžeme použít vyhledávání man -k keyword nebo apropos keyword. Nepostradatelné je samozřejmě info libc, ve kterém lze najít každou standardní funkci. K prohlížení info nemusíme opouštět náš Emacs, M-x info nebo C-h i. Pokud dokumentace nestačí nebo je zastaralá (to spíš), hlavičkové soubory najdeme v adresáři /usr/include.

Kompilace a linkování

Máme tedy zdrojový text programu, teď jej musíme přeložit (zkompilovat). Na to je kompilátor pro náš zvolený jazyk, který přeloží zdrojový text do objektového souboru (*.o). Pro jazyk C je to program gcc (GNU project C and C++ Compiler), pro C++ jeho nadstavba g++. Tento program nám zároveň poslouží i jako linker, viz dále.

Kompilátor gcc 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é info gcc 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, defaultně source.o pro objektový soubor zdrojáku source a a.out pro binárku
-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) nejdříve 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 -- nevypisovat žádné chyby (zásadně nedoporučuju !)
-W, -Wneco nebo -Wall -- vypisuje chyby v kódu (některé, v souvislosti s neco - viz man, 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ň 2 je pro obyčejné programy plně postačující (pro jádro taky))
-ansi -- zapne ANSI standard, místo asm, inline a typeof musí být __asm__, __inline__ a __typeof__, definuje __STRICT_ANSI__
-pedantic -- odmítne zkompilovat soubor, který není správně ANSI C a byla použita volba -ansi

Zkompilujeme naše dva zdrojáky:

$ gcc -Wall -ansi -pedantic -c rec.c
$ gcc -Wall -ansi -pedantic -c priklad.c

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

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 (pokud se linkuje dynamicky) do výsledného spustitelného souboru, který je pro Linux dnes už standardně ELF formátu.

V Linuxu představuje linker program ld (GNU linker, info o něm najdete v info ld 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.

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.

Slinkujeme náš příklad:

$ gcc -o priklad priklad.o rec.o
Teď ho můžeme spustit:
$ ./priklad 8
Převrácená hodnota 8 je 0.125

gcc automaticky linkuje i standardní C knihovnu, která obsahuje implementaci funkcí printf a atoi.

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 od něj víc nedozvíme (ale dozvíme - z 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!). Informace pro debugování nemusíme kompilovat do celého programu, jen do těch částí, které chceme debugovat. Tyto informace obsahují věci jako popis funkcí a globálních proměnných (level 1), popis lokálních proměnných, která adresa kódu je na kterém řádku ve zdrojáku (level 2 - defaultní), definice maker atd. (level 3).

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

break [line | function] -- nastaví breakpoint (přerušení) na řádku line nebo začátku funkce function
clear [line | function] -- smaže breakpoint na řádku line nebo začátku funkce function
print expr -- zobrazí hodnotu výrazu expr
run [arglist] -- spustí program (s parametry arglist)
kill -- ukončí debugovaný program
continue -- pokračuje v programu (př. po breakpointu)
next -- vykoná další řádek kódu programu, nevstupuje do funkcí
step -- jako next, ale vstupuje do funkcí
backtrace -- zobrazí zásobník programu, tj. volání jednotlivých funkcí
info neco -- zobrazí informace o neco (adresa, registry, breakpointy, funkce, řádek, proměnné, zdrojáky) v programu
help [name] -- nápověda (k příkazu name)
quit -- ukončení gdb

Podtržená písmena jsou zkratky pro celý příkaz.
Spustíme debugger se jménem programu jako parametrem:

gdb priklad
gdb je interaktivní program, před námi se tedy objeví prompt (gdb). Program spustíme příkazem run:
(gdb) run
Starting program: priklad 

Program received signal SIGSEGV, Segmentation fault.
0x4004cbc9 in __strtol_internal () from /lib/libc.so.6
Vidíme, že program skončil s chybou SIGSEGV ve funkci __strtol_internal, která je ovšem ve standardní knihovně. Prozkoumáme tedy zásobník programu:
(gdb) bt
#0  0x4004cbc9 in __strtol_internal () from /lib/libc.so.6
#1  0x4004a661 in atoi () from /lib/libc.so.6
#2  0x8048447 in main (argc=1, argv=0xbffffa14) at priklad.c:10
Funkce main volala funkci atoi bez parametru. Protože funkce atoi převádí řetězec na číslo, je to jako by dostala v parametru NULL. Na volání funkce atoi se můžeme podívat podrobněji:
(gdb) up 2
#2  0x8048447 in main (argc=1, argv=0xbffffa14) at priklad.c:10
10         i = atoi(argv[1]);
Zdroják priklad.c je dostupný, funkce je volána na řádku 10. Podíváme se na obsah jejího parametru:
(gdb) print argv[1]
$1 = 0x0
... a problém je jasně vidět, parametr je 0x0, tedy NULL.
Pokud chceme program krokovat, nastavíme breakpoint (místo přerušení), odkud chceme krokovat:
(gdb) break main
Breakpoint 1 at 0x8048436: file priklad.c, line 9.
Toto nastaví breakpoint na začátek funkce main. Teď program znova spustíme (s parametrem):
(gdb) run 8
Starting program: priklad 8

Breakpoint 1, main (argc=2, argv=0xbffffa14) at priklad.c:10
10         i = atoi(argv[1]);
... a vidíme, že se zastavil na breakpointu. Nyní krokujeme pomocí next (provede celou funkci) nebo step (vstupuje do funkcí):
(gdb) next
11        printf("Převrácená hodnota %i je %g\n", i, reciprocal(i));
(gdb) step
reciprocal (i=8) at rec.c:6
6         if (i != 0) return 1.0/i; else return i;
(gdb) next
7       }
(gdb) next
Převrácená hodnota 8 je 0.125
main (argc=2, argv=0xbffffa14) at priklad.c:12
12        return 0;
(gdb)

Jelikož Emacs umí vše :-), můžeme program debugovat pomocí gdb přímo v něm. Debugger v Emacsu spustíme pomocí M-x gdb. Pokud zastavíme na breakpointu, Emacs nám ukáže celý zdrojový soubor a nastaví se na odpovídající řádek. Na druhou stranu ale vlastní program gdb zobrazuje méně informací při krokování.

Když program skončí s fatální chybou (např. na signál SIGSEGV), vygeneruje se soubor core, což je soubor s obsahem paměti procesu těsně před ukončením. Tento soubor není tak úplně k ničemu, protože jej lze využít k debugování programu a zjistit tak, jak program běžel (které funkce byly volány a v jakém pořadí, hodnoty proměnných) a hlavně kde skončil (s tou chybou). Stačí gdb spustit ještě se jménem core souboru za jménem programu a debugovat stejně jako bez core souboru.

Pozorný čtenář si jistě všiml, že gdb lze spustit také s pid za jménem programu. Jaké je to pid? No přece pid běžícího programu, který chceme debugovat. Ano, gdb umí debugovat i běžící program. Manipulace s programem v gdb se samozřejmě projeví i na běžícím programu.

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. Program se mu zadá jako parametr, ostatní parametry viz. info strace 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é info make 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 knihoven 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. Makefile má samozřejmě svoji předepsanou strukturu a formátování, se kterým spolehlivě pomůže náš oblíbený editor Emacs.

Soubor "Makefile":

# Program "priklad", 2001
#

CFLAGS = -Wall -ansi -pedantic

priklad: priklad.o rec.o
	$(CC) $(CFLAGS) -o $@ $^

priklad.o: rec.h priklad.c

rec.o: rec.h rec.c

clean:
	rm -f *.o 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é). Proměnné je možné také definovat už na příkazovém řádku při spuštění programu make, př. make CFLAGS=-O2.

Pak se určí první závislost - na čem je závislý samotný program: na objektových souborech priklad.o a rec.o, 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.o nebo rec.o 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 psal na výstup, napište před něj zavináč @. Příkazů samozřejmě může být víc, každý na jednom řádku a uvozen tabulátorem.

Další řádky určují závislost a způsob vytvoření objektových souborů. Možná se divíte, proč u těchto závislostí chybí příkaz, kterým se mají cíle vytvořit. Jak se tedy cíle vytvoří? Program make umí řešit nějaké základní jednoduché závislosti, např. že ze zdrojáku .c se má zkompilovat objektový soubor .o, proto tyto základní příkazy nemusíme psát. Pro náš případ si make sám vygeneroval příkaz $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $^. Pokud vás zajímají ostatní implicitní pravidla, seznamte se s info make.

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 -- C kompilátor, defaultně cc, což je ve většině distribucí symbolický link na gcc
CXX -- CXX kompilátor, defaultně g++
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 C kompilátoru
CXXFLAGS -- volby C++ 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). Objektové soubory neexistují, takže make postupně přejde na určení jejich závislosti, vytvoří je 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 rec.o, vytvoří se jen tento objektový soubor. 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 *.o 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 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 czf 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.)

Vytvoření balíčku (vše je v adresáři priklad):

tar czf 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. Teď musí balíček rozbalit:

tar xzf balicek.tar.gz 

Pak přejde do adresáře se zdrojáky 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, např. když se sestaví sdílená knihovna, kterou program potřebuje, ale nenainstaluje se do standardních adresářů pro sdílené knihovny, je potřeba před spuštěním programu např. nastavit proměnnou prostředí LD_LIBRARY_PATH.

A může program spustit:

./priklad 10

Jak je vidět, uživatel má nesrovnatelně jednodušší práci než my - programátoři.

Závěr

Tento text 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ů !



Jan Outrata
outrataj@phoenix.inf.upol.cz