Hlavní navigace

Co před námi tají /proc (9)

14. 8. 2001
Doba čtení: 6 minut

Sdílet

Teď už budeme zase pokračovat v dalších souborech adresáře /proc. Dnes je na řadě soubor ksyms. Příště se už dostaneme k písmenu L.

Opět dle tradice výpis základního adresáře /proc. Tečka před názvem znamená, že jsme daný soubor/adresář již probrali:

  .840     .devices     .kcore    modules     stat     ide/
  .842     .dma         .kmsg     mounts      swaps    irq/
  .847     .execdomains  ksyms    mtrr        uptime   net/
  .848     .filesystems  loadavg  partitions  version  nv/
  .apm     .interrupts   locks    pci         bus/     sys/
  .cmdline .iomem        meminfo  self@       driver/  sysvipc/
  .cpuinfo .ioports      misc     slabinfo    fs/      tty/

Na začátek jen malé upozornění. Před psaním tohoto článku jsem aktualizoval jádro. Původní 2.4.5 bylo nahrazeno jádrem 2.4.7 se „stejnou“ konfigurací. Je proto teoreticky možné spatřit jisté nekonzistence ve vzorových příkladech celé série článků. Pokud si toho budu vědom, upozorním vás, z obsahu informací o změnách (Changelog) však zatím není patrný žádný zásadní rozdíl.

ksyms

Obsahem souboru jsou informace o symbolech exportovaných jádrem. Jedná se o vstupní bod rutiny, dále řetězec vycházející z jejího názvu a případně název modulu, který rutinu poskytuje. Příkladem buďtež třeba následující tři části souboru:

d08d617c nvHalVideoFree_NV20    [NVdriver]
d0862874 CliMakeDeviceFifoList  [NVdriver]
d0878eb0 dacDisableCRTCSlave    [NVdriver]
d08e00c6 CH_FSCI_720x576NC      [NVdriver]
d0869258 DldGetImageOffset      [NVdriver]
....
d0825700 vfat_create_Rbe27f635  [vfat]
d08259a0 vfat_unlink_Rc285bb37  [vfat]
d0825a30 vfat_mkdir_R0fe130b5   [vfat]
d08258f0 vfat_rmdir_R12aa0637   [vfat]
....
c01081a0 enable_irq_Rfcec0987
c0108140 disable_irq_R3ce4ca6f
c01089d0 disable_irq_nosync_R27bbf221
c0108570 probe_irq_mask_R360b1afe
c0105510 kernel_thread_R7e9ebb05

Teď je vhodná chvíle říci si něco málo o fungování modulů v Linuxu. Budu se snažit být stručný, protože rozsáhlejší popis bude pravděpodobně zabírat celý jeden díl (možná ten o souboru /proc/modules). Takže, jak je asi známo, může být jádro nakonfigurováno tak, že hlavní část kódu je obsažena v základním bloku, který se nahrává a spouští po startu systému. Odpovídající soubor se často jmenuje vmlinuz a bývá umístěn v adresáři /boot, případně v kořenovém adresáři /. Umístění i jméno se ovšem mohou lišit dle distribuce, respektive dle rozvahy uživatele/správce daného systému. Ani jedno není totiž nikde pevně dáno a závisí pouze na odpovídající konfiguraci zavaděče systému (/etc/lilo.conf pro LILO).

Mohou ovšem existovat také části jádra, které potřebujeme pouze občas. Příkladem budiž ovladač pro nějaký exotičtější souborový systém nebo třeba obyčejný sériový port. V tom případě lze daný ovladač v běžném jádru vynechat a vytvořit jedno „nadupané“ jádro, které v případě potřeby nabootujeme (to si lze představit na počítači pro jednoho člověka), nebo budeme používat nadupané jádro rovnou. Problémem ale může být jednak větší spotřeba paměti a jiných systémových zdrojů (teoreticky třeba IRQ nebo DMA kanál, obojí ovšem svědčí spíše o špatně napsaném ovladači), druhý problém může nastat v okamžiku, kdy je třeba přidat nový ovladač nebo aktualizovat stávající na novou verzi. Potom je v případě monolitického jádra třeba vše překompilovat a restartovat.

Proto byly vymyšleny moduly. Jedná se o části jádra, které lze za běhu počítače (a operačního systému spolu s jinými programy) vkládat a vyndávat z jádra. Jde o princip zpřístupnění rozhraní modulu pro zbytek jádra a potažmo uživatelské aplikace (rozšiřující systémové volání).

S jejich příchodem bylo nutno doplnit do jádra další administrativu, která jednak zajišťovala vlastní zpřístupnění exportovaných symbolů (názvy funkcí a proměnných), a také bylo potřeba řešit problém s rozličnými verzemi jádra. Představte si totiž situaci, kdy zkompilujete modul pro jádro třeba 2.2.něco, a potom je tato binárka z nějakého důvodu použita k jádru 2.2.něcojiného nebo dokonce 2.4.něco (a nedej bože 1.x.x – tedy starší jádro, než bylo použito pro překlad). Během vývoje jádra se totiž může určitým způsobem měnit jeho rozhraní. Markantní to nebude ani tak u názvů (respektive deklarací) funkcí, jako spíše u změny definice datových typů jádra. Stačí přidat položku do nějaké tabulky informací v jádře (nebo ubrat či změnit typ), a binární podoba modulu se s těmito změnami nedokáže vyrovnat. Uvědomme si, jak totiž vypadá kód pro manipulaci se složitějšími datovými typy nebo pro volání funkcí (s parametry) v assembleru (a potom ve strojovém jazyku). Tam se adresace položky XY z tabulky UV provádí tak, že se sečte adresa začátku UV a offset (posunutí) XY od první položky v bajtech (v některých případech i bitech). Pokud přidáme nebo ubereme položku tabulky, která leží mezi začátkem UV a umístěním XY, offset nebude ukazovat na správná data. Konfliktních situací může vzniknout více, jako ukázka malé noční můry to však doufám stačí.

První řešení je nasnadě. Povolit vložení modulu pouze do jádra stejné verze, pro jakou byl modul zkompilován. To ovšem nemusí řešit všechny problémy (například vlastnoruční úpravy v jádře nebo cizí patche) a navíc je to značně omezující – pro každé nové jádro je třeba přeložit všechny moduly. Lepší bude řešit změny přímo na úrovni jednotlivých symbolů než pro celé jádro najednou. Pokud se tedy změní něco, co využívá daný symbol, bude nutné přeložit znovu příslušný modul. Jde tedy o způsob, jak jednoznačně reprezentovat použité rozhraní jednotlivých symbolů.

Konečným (v dnešní době) řešením je úprava jména symbolu přidáním digitálního podpisu rozhraní (v našem případě „prosté“ CRC) k názvu symbolu. Pokud se změní rozhraní, změní se i podpis a chyba se ohlásí jako nenalezený název symbolu. Zbývá zodpovědět otázku, jak se počítá CRC. Provádí to program /sbin/genksyms, který na vstup dostane zdrojový text (v jazyce C) zpracovaný preprocesorem (jazyka C) a pro každý datový typ provede jeho rozvinutí do základní podoby. To v podstatě znamená patřičná nahrazení pro všechny konstrukce jako typedef, struct, enum, union a další. Vznikne definice typu pouze na úrovni jednotek -datových typů – definovaných přímo jazykem. Takový popis se potom vezme jako řetězec a právě z něj se spočítá CRC. To se jako 32bitové číslo spolu se znaky _R nebo _Rsmp_ (smp je pro víceprocesorová jádra) přidá za název vlastního symbolu (což může být, jak jsem zmínil, název funkce nebo externí proměnné). Při překladu jádra je tento program volán v části, kde se tvoří závislosti spuštěním make dep. Podobné čachry provádí také překladač jazyka C++ z důvodů možnosti přetěžování funkcí.

V našem ukázkovém výpisu jsou vidět tři typy záznamů. První je pro ovladač ke grafické kartě. Modul se jmenuje NVdriver a při jeho překladu nebyly symboly explicitně exportovány pomocí příslušného makra EXPORT_SYMBOL(), nemají tudíž ochranu pomocí CRC. Proč tomu tak je, se zeptejte pánů od NVidia Corp. Pokud nejsou symboly exportovány, nevadí to samotnému modulu, ten funkce jádra používá ve správném tvaru podle definice (testy na MODVERSIONS na začátku souborů .c). Jde o to, že pokud bude někdo používat rozhraní definované tímto modulem, nemusí zaregistrovat změny v něm. Část tohoto modulu je ovšem distribuována v binární podobě (to by mohlo být hledané vysvětlení neEXPORTovaných symbolů). V této části modulu se rovněž vyskytují symboly se jménem _nv_rmsym_číslo, což by mohlo naznačovat další manipulaci ohledně exportu symbolů.

Klasickým příkladem modulu s exportovanými a „ocejchovanými“ symboly je ovladač souborového systému FAT – modul vfat. Dokonce i názvy funkcí jsou docela jasné. Jsou zde rutiny pro vytvoření a smazání souboru, adresáře. Posledním příkladem jsou symboly exportované samotným jádrem.

Ještě k první položce – adrese. Ta se dá využít třeba pro ladění, kdy získáme (z Oops) obsah registrů. Hodnota EIP (Instruction Pointer) označuje právě prováděné místo. Pokud tedy určíme z nabízených možností nejbližší nižší adresu, známe název funkce, kde došlo k chybě. Tuto práci za nás může také udělat utilitka /usr/bin/ksymo­ops. Adresa se vztahuje k adresovemu prostoru jadra a tudiz bude vetsi nebo rovna PAGE_OFFSET.

Výpis obsahu souboru ksyms zajišťuje funkce get_ksyms_list ze souboru kernel/module.c. Prochází všechny moduly a z každého vypíše jeho tabulku symbolů, což je seznam řetězců jmen v tabulce informací o modulu. Jako poslední modul je potom uvedeno samotné jádro (hlavní část jádra).

Byl pro vás článek přínosný?