Produkty Novinky Články Návody Kontakty

Nativní kompilátor

Kompilátor a další vývojové nástroje v současnosti nejsnáze získáme jako součást standardní linuxové distribuce v podobě nějakého instalačního balíčku. Je to pohodlné a rychlé, ale díky tomu často nevíme, jak je kompilátor nastaven, kde hledá hlavičkové soubory standardních systémových knihoven a kde hledá samotné knihovny. Vývojáři distribuce za nás vše sladili v jeden bezvadně fungující celek a my se prostě nemusíme o nic starat.
Tato neznalost prostředí, v kterém se jako vývojáři pohybujeme, se může projevit až v okamžiku, kdy svou aplikaci zveřejníme. Najednou zjistíme, že na počítači s jinou distribucí naše aplikace nefunguje, protože např. knihovna libc je starší než v našem počítači anebo proto, že dynamická knihovna, kterou aplikace vyžaduje, je umístěna na počítači uživatele v jiném adresáři apod.
V případě embedded zařízení bychom s touto neznalostí narazili o to pravděpodobněji. Běhové prostředí cílového zařízení se obvykle značně liší od prostředí v našem vývojářském počítači. V cílovém zařízení bývá jiný procesor, sada systémových knihoven je omezená, jejich funcionalita často také atd. Proto jako vývojáři ”embeddisti” musíme znát vlastnosti křížového kompilátoru a dalších nástrojů, jinak se můžeme dočkat nepříjemných problémů.
Abychom nezačali tak zhurta, prozkoumáme nejdříve vlastnosti nativního kompilátoru, který máme snadno a rychle k dispozici v podobě balíčku běžné linuxové distribuce, v našem případě distribuce Debain 6.0 Squeeze.

Hlavičkové soubory

Jak zjistíme, které adresáře kompilátor gcc prohledává jako výchozí, když hledá hlavičkové soubory? Jednoduše, stačí se na to zeptat preprocesoru, který kompilátor volá v prvních fázích překladu, protože preprocesor zpracovává direktivy #include.
Zadejme v shellu příkaz:
$ cpp -v
Z výstupu na obrázku 1.1 je patrné, že preprocesor hledá hlavičkové soubory v adresářích /usr/local/include a /usr/include (v tomto pořádí). Těmto adresářům budeme říkat výchozí adresáře. Ostatní adresáře uvedené ve výstupu na mém systému totiž neexistují.
Do seznamu prohledávaných adresářů můžeme ještě přidat další adresáře. Zadáme je při volání kompilátoru jako parametr za volbou -I. Tyto přidané adresáře jsou v pořadí prohledávání PŘED výchozími adresáři /usr/local/include a /usr/include. Seznam přidaných adresářů kompilátor automaticky předá preprocesoru, my se nemusíme o nic starat.
obrázek vystup-prikazu-cpp-v-x86
Obrázek 1.1 Výstup příkazu cpp -v, platforma x86
Jen pro úplnost dodejme, že hlavičkové soubory uvedené v závorkách <...> kompilátor hledá JEN v adresářích ze svého seznamu, zatímco soubory zadané v uvozovkách ”...” hledá nejprve v adresáři se zdrojovými kódy aplikace a až pak v adresářích ze svého seznamu.
Hlavičkové soubory jádraInformationHlavičkové soubory jádra, které jsou součástí linuxové distribuce na našem PC, jsou umístěny v adresáři /usr/src/linux. nejsou standardně zahrnuty do seznamu prohledávaných adresářů. Proto je v případě potřeby musíme přidat jako parametr při volání kompilátoru.

Knihovny

Než se pustíme do pátrání, jak a kde kompilátor hledá knihovny, musíme si říci něco málo o dělení knihoven a o tom, jak kompilátor interpretuje názvy knihoven.
Nejprve se zaměříme na dělení knihoven. Knihovny dělíme na statické a dynamickéInformationRozdíly mezi statickým a dynamickým linkováním jsou detailně vysvětleny například zde: http://cs.wikipedia.org/wiki/Knihovna_(programování). Statické knihovny jsou ty, jejichž symboly (funkce, proměnné) linker připojuje k aplikaci už během procesu linkování, stávají se tak její nedílnou součástí. Nepříjemným důsledkem je ovšem vyšší spotřeba operační paměti, protože kód statické knihovny je v paměti tolikrát, kolikrát je daná aplikace spuštěna. Výhodou naopak je, že nenalezené symboly ohlásí linker jako chybu linkování, díky čemuž se o chybějící knihovně nebo o nekompatibilní verzi knihovny dozvíme okamžitě.
Naproti tomu, dynamické knihovny, resp. jejich symboly, jsou k aplikaci přilinkovány až v okamžiku jejího spuštění. Při linkování se totiž k aplikaci připojí pouze tabulka odkazů na potřebné symboly. Během spouštění aplikace pak tzv. dynamický linker/zavaděč tuto tabulku prohledá, zavede do paměti odpovídající knihovnu (pokud tam již není) a propojí ji s aplikací. Tento mechanismus umožňuje sdílet jednu knihovnu více spuštěnými aplikacemi, takže daná knihovna může být v paměti zavedenena jen jednou. Nevýhoda je, že o chybějících či nekompatibilních knihovnách se dozvíme až při startu aplikace (aplikace se nespustí) a také, že ke spuštění aplikací potřebujeme speciální knihovnu ld.so, resp. linux-ld.so - dynamický linker/zavaděč.
Obecně se mnohem častěji používají dynamické knihovny. Důvodem je úspora operační paměti a snadná aktualizace knihoven. V případě dynamických knihoven znamená jejich aktualizace jen náhradu starších knihoven v operačním systému za novější (pozor na zpětnou kompatibilitu). Ovšem v případě statických knihoven by aktualizace znamenala nové sestavení všech dotčených aplikací.
A nyní se můžeme podívat na to, jak kompilátor intepretuje názvy knihoven. Volbou -lmojeknihovna řekneme kompilátoru (a tím i linkeru), že k výslednému spustitelnému souboru má přilinkovat knihovnu mojeknihovnaInformationKnihovna se nikdy nepřilinkuje k aplikaci celá, ale jen ta její část (např. funkce), kterou aplikace potřebuje.. Kompilátor ovšem bude hledat soubor s názvem libmojeknihovna.so (v případě dynamické knihovny) nebo libmojeknihovna.a (v případě statické knihovny). Standardně se proto název souboru knihovny skládá z jména knihovny, předpony lib a přípony .so nebo .a.
Výchozí cesty ke knihovnám
Preprocesoru jsme se ptali, kde hledat hlavičkové soubory. Koho se však budeme ptát v souvislosti s knihovnami? Ano, správně. Tuto otázku položíme linkeru, neboť linkování knihoven a objektových souborů dohromady je jeho práce.
V manuálu linkeru ld se dočteme, že seznam výchozích adesářů, kde linker hledá knihovny, je ovlivněn emulačním módem a někdy také konfigurací linkeru. Tato informace nám zjevně ale moc světla do našeho pátrání nevnese.
Při podrobnějším zkoumání však nakonec dojdeme k závěru, že linker hledá knihovny nejprve v adresářích, které se zadávají jako parametr pomocí volby -L, a až pak v adresářích určených příkazem SEARCH_DIR v použitém linkovacím skriptu. Ale my přece žádný linkovací skript nepoužíváme, říkáte si. Ale ano používáme - ten výchozí.
Než se podíváme, které adresáře definuje výchozí linkovací skript, tak si řekněme něco o linkovacím skriptu samotném. Každý linker potřebuje ke své činnosti tzv. linkovací skript, který předepisuje jak má linker slučovat jednotlivé objektové soubory a kam má v paměti umisťovat proměnné a funkce. Implicitně linker využívá skript, který je v něm zakompilován (zabudován). Tento skript se nazývá výchozím linkovacím skriptem. Sice jej nemůžeme změnit (je nedílnou součástí linkeru), nicméně v adresáři /usr/lib/ldscripts nalezneme jeho kopie (liší se nastavením linkeru). Vybranou kopii si můžeme zkopírovat a upravit podle svých představ. Vlastní linkovací skript zadáme linkeru jako parametr za volbu -T.
Nyní se vraťme zpět k výchozímu linkovacím skriptu, resp. k adresářům jaké tento skript definuje:
$ ld --verbose | grep SEARCH_DIR | tr -s ’ :’ \\012
Z výstupu na obrázku 1.2 je jasné, že jsou to prakticky jen adresáře /usr/local/lib, /lib a /usr/lib (v tomto pořadí). Ostatní adresáře na mém systému neexistují.
obrázek vystup-prikazu-ld-verbose-x86
Obrázek 1.2 Výstup příkazu ld --verbose, platforma x86
Nyní bychom mohli usnout na vavřínech - vždyť známe seznam výchozích adresářů, kde linker hledá knihovny. Omyl! Věc se bohužel komplikuje tím, že standardně nevoláme linker přímo, ale volá jej za nás skrytě kompilátor (pokud mu nepřikážeme jinak). A v tom je zakopaný pes - kompilátor gcc má totiž svůj vlastní seznam výchozích adresářů, který předává linkeru jako parametr. Ve skutečnosti tedy linker hledá knihovny nejdříve v adresářích, které zadáme při volání kompilátoru explicitně jako parametr s volbou -L. Pak je hledá ve výchozích adresářích kompilátoru. A úplně nakonec je hledá v adresářích definovaných výchozím linkovacím skriptem.
Dobrá, ale jak tedy zjistíme, které adresáře má kompilátor nastaveny jako výchozí pro hledání knihoven? Naštěstí docela snadno:
$ gcc -print-search-dirs | tr -s ’:’ \\012
Výsledek je vidět na obrázku 1.3. Z něj vyplývá, že výchozí adresáře při hledání knihoven jsou adresáře /lib, /usr/lib a nakonec /usr/local/lib, který je specifikován až v linkovacím skriptu. Došlo tedy ke prohození pořadí adresářů /lib a /usr/lib.
obrázek vystup-prikazu-gcc-print-search-dirs-x86
Obrázek 1.3 Výstup příkazu gcc -print-search-dirs, platforma x86
Link-time a run-time cesty ke knihovnám
V předchozím textu jsme zjistili, které adresáře linker prohledává, když během sestavování aplikace hledá nějakou knihovnu. O cestách k těmto adresářům pak mluvíme jako o link-time cestách. Tyto cesty se uplatňují, ať linkujeme knihovny staticky nebo dynamicky.
Kromě toho lze linkeru poručit, aby do aplikace zapsal i tzv. run-time cesty. To jsou cesty k adresářům, v kterých dynamický linker hledá dynamické knihovny během spouštění aplikace. Jsou prohledávány před systémovými cestami (obsah souboru /etc/ld.so.conf, resp. cache /etc/ld.so.cache) a výchozími cestami (adresáře /lib a /usr/lib) k dynamicky linkovaným knihovnám. Pro určení run-time cest se používá parametr -rpath, který kompilátor předává linkeru:
gcc -Wl,-rpath=/cesta_k_mym_knihovnam
Nastavení run-time cest může být nezbytné, pokud nemáme při instalaci aplikace dostatečná práva na změnu nastavení systému. Tzn. že buď nemůžeme přidat potřebnou dynamickou knihovnu do výchozích adresářů /lib a /usr/lib nebo nemůžeme modifikovat soubor /etc/ld.so.conf, resp. přegenerovat cache v souboru /etc/ld.so.cache, a přidat do něj vlastní cestu k naší dynamické knihovně.