söndag 20 augusti 2017

Att skriva en kärnmodul

När jag för cirka två år sedan började använda Linux dagligen var kärnan och kärnprogrammering något magiskt, främmande och läskigt. Två år senare är den fortfarande till stor del främmande, men inte läskig, och jag är numera nästan helt säker på att inget magiskt pågår under huven på min dator.

Min väg in i kärnan startade med Understanding Linux Network Internals av Christian Benvenuti, och det är också nätverksstacken jag jobbar med och har bäst koll på. Jag vet inte huruvida den är en bra introduktionsbok eller inte, men eftersom mitt arbete kretsar kring routrar och annat nätverksrelaterat så kändes den passande. Den har i skrivande stund 13 år på nacken, men den är fortfarande relevant som en introduktion till hur ett nätverkspacket färdas genom ett Linuxsystem.

Linux nätverksstack är ett fascinerande område som jag kommer att skriva mer om i senare inlägg, men just den här blogposten tänkte jag ägna åt att visa er hur enkelt det är att börja skriva kärnkod!

Vad är egentligen en kärna?

...och vad är skillnaden mellan en kärna och ett operativsystem? Om vi jämför med webbutveckling så är operativsystemet datorns 'front-end' och kärnan dess 'back-end'. Kärnan tillhandahåller de mest grundläggande tjänster som krävs för att kunna använda hårdvaran på ett effektivt sätt. Bland dessa tjänster kan man bland annat hitta processhantering, minneshantering, läsning och skrivning till disk och sändning och mottagning av nätverksdata. Ett operativsystem tillhandahåller istället tjänster riktade till användaren av systemet, till exempel grafiska gränssnitt, kommandotolk, ordbehandlingsprogram med mera.
Operativsystem v.s. Kärna

Typ av kärna

För att göra det enkelt för sig så kan man koka ner operativsystemskärnor till två olika varianter. Antingen har man en monolitisk kärna, eller en så kallad microkernel.

En monolitisk kärna är ett enda program där allt som behövs finns med från början. Där finns minneshantering, input/output-hantering, nätverkshantering och så vidare. En microkernel å andra sidan tillhandahåller bara en väldigt liten 'kärna' som i stort sätt endast innehåller funktionalitet för att synkronisera och skicka information mellan andra delar av kärnan som länkas in oberoende av varandra. För er som känner till fenomenet microservices fungerar en microkernel enligt samma princip.
Monolitisk vs Microkernel


Linux är en monolitisk kärna. Har man väl byggt den så finns där allt man behöver. Där finns hela nätverksstacken, processchemaläggning,  hantering av virtuellt minne, filsystem och en massa annat. Men utöver detta erbjuder Linux även så kallade LKMs, eller 'Loadable Kernel Modules'. Dessa är, något förenklat, kod som dynamiskt, alltså samtidigt som kärnar kör, kan laddas in och laddas ur. När den laddas in blir kärnmodulens kod en del av den aktiva kärnan. Från modulens synvinkel är det i stort sätt ingen skillnad mellan att laddas in som modul och att vara inbyggd i kärnan från början.
En modul laddas in i en aktiv kärna

Det där med kärnmodul låter roligt. Hur skriver man en?

Tack för frågan! Det ska bli mig ett nöje att besvara den. För att kunna ta till dig texten är det bra om du har koll på grundläggande C-programmering och någon form av vana vid kommandotolken i ett Linux-system. Jag använder personligen Ubuntu som operativsystem, men det borde inte vara några problem att hänga med så länge man kör valfri Linux-distribution.

För att kunna bygga kärnmoduler behöver du ha din kärnas header-filer, gcc och GNU make installerat. På debian-baserade system (Till exempel Ubuntu) görs detta enklast med kommandot
sudo apt-get update && sudo apt-get install build-essential linux-headers-`uname -r`
Vi börjar med att skapa en mapp för vårt projekt.
~$ mkdir module
~$ cd module
~/module$ 

Så långt allt väl. Nu är det väl dags att skriva lite kod? Jajemän! Öppna din favorit-editor så sätter vi igång.

Så här ser koden i mymodule.c ut:


Inte så farligt va? Man behöver bara inkludera filen <linux/module.h> och definiera två funktioner, init_module och cleanup_module. Den första funktionen körs när modulen laddas in, den andra när modulen tas bort. Vi returnerar 0 i vår init-funktion för att signalera att allt gått bra.

Nu vill vi bygga modulen. Linux använder sig av GNU make, så det gör vi med. Oroa dig inte om du saknar erfarenhet av GNU make, Linux-kärnan har färdiga make-filer för oss att använda. För att bygga en modul som ska kunna laddas in i kärnan din dator använder sig av just nu, använder vi oss av make-filen som ligger i mappen /lib/modules/`uname -r`/build/. På något sätt behöver vi få denna make-fil att hitta mymodule.c och bygga en färdig modul åt oss. För att lyckas med detta behöver den lite hjälp. För att ge kbuild, som Linuxkärnans byggsystem heter, den information den behöver skapar vi en egen enkel make-fil, Makefile, i vår projektkatalog. Just nu behöver den bara innehålla följande:


Nu vet kärnans make-fil om att den ska bygga en modul som heter 'mymodule'. 'm'-et säger att modulen ska byggas separat och inte byggas in i en komplett kärna.

Nu bygger vi! Kör följande kommando i projektkatalogen.
~/module$ make -C /lib/modules/`uname -r`/build M=`pwd` modules
 -C säger åt make att använda sig av make-filen i den efterföljande katalogen. modules innebär att make-filen ska bygga alla moduler den hittar i katalogen M som vi sätter till katalogen vi står i just nu.

Om inget gått allvarligt fel så ska vi nu ha en färdig modul, mymodule.ko i vår projektmapp. Vi laddar in den i vår kärna!
~/module$ sudo insmod mymodule.ko
Grattis, du är nu kärnprogrammerare! Du har skrivit kod som i detta nu existerar i ditt operativsystems kärna. Den kanske inte gör så spännande saker, men det kommer vi ändra på i kommande inlägg i denna bloggserie.

Vi kan på flera sätt verifiera att vår kärna verkligen laddats in och nu anses vara en legitim del av den aktiva kärnan. För det första kan vi se att vår inladdnings-funktion kördes genom att leta efter texten "I'm alive!" i syslogen och/eller kärnans egen log dmesg. Kommandot lsmod listar alla moduler som just nu är inladdade i den aktiva kärnan. mymodule borde finnas med även där. Om du är väldigt skeptiskt lagd och av någon anledning fortfarande inte är helt övertygad så kan du som ett sista bevis se att vi har fått en helt egen mapp i /sys/module/mymodule.

Nu när jag visat hur vi laddar in vår kärnmodul ska jag också visa hur man tar bort den.
~/module$ sudo rmmod mymodule
Lägg märke till att vi när vi laddade in modulen var tvungna att skriva hela filnamnet, men när vi laddar ur modulen så räcker det med mymodule. Du kan verifiera att modulen tagits bort ur kärnan på samma sätt som vi verifierade att den hade laddats in.

Nu kanske du oroar dig över meddelanden om att du numera kör en "Tainted kernel". Detta beror på att vi inte specificerat vilken licens vår modul kör under. Detta ska vi rätta till nu! Vi ska samtidigt ändra så att våra två funktioner får lite roligare och bättre namn. Här är den slutgiltiga koden för den här blogposten:



Som du ser om du följt alla steg och byggt en modul så har vi fått en massa skräp i vår projektmapp. Vi kan köra följande kommando för att rensa bort alla automatiskt genererade filer:
~/module$ make -C /lib/modules/`uname -r`/build M=`pwd` clean
Sådär! Nu är det rent och snyggt i vår projektmapp igen. För att slippa komma ihåg alla långa kommandon för att bygga och städa uppdaterar vi vår make-fil lite grann:



Nu är byggning och städning av vårt projekt lika enkelt som att skriva make respektive make clean.

I nästa post kommer jag visa hur man 'hookar in' i linux nätverksstack och manipulerar paket som färdas igenom den.

Inga kommentarer:

Skicka en kommentar