Linux - sistemski pozivi. Sistemski pozivi u Linuxu. Man syscalls (2): Linux sistemski pozivi Naredbe sistemskog poziva u Linuxu

Najčešće, pozivni kod sustava s brojem __NR_xxx, definiran u /usr/include/asm/unistd.h, može se pronaći u izvornom kodu Linux kernela u funkciji sys_xxx(). (Tablica poziva za i386 može se pronaći u /usr/src/linux/arch/i386/kernel/entry.S.) Postoje mnoge iznimke od ovog pravila, uglavnom zbog činjenice da se većina starih sistemskih poziva zamjenjuje novima, i to bez ikakvog sustava. Na vlasničkim platformama za emulaciju OS-a kao što su parisc, sparc, sparc64 i alpha postoji mnogo dodatnih sistemskih poziva; mips64 također ima kompletan skup 32-bitnih sistemskih poziva.

S vremenom su se dogodile promjene u sučelju nekih sistemskih poziva, ako je bilo potrebno. Jedan od razloga za ove promjene bila je potreba za povećanjem veličine struktura ili skalarnih vrijednosti proslijeđenih pozivu sustava. Zbog ovih promjena, na nekim arhitekturama (odnosno starijem 32-bitnom i386) pojavile su se različite grupe sličnih sistemskih poziva (npr. skratiti(2) i skratiti64(2)), koji obavljaju iste zadatke, ali se razlikuju po veličini svojih argumenata. (Kao što je navedeno, ovo ne utječe na aplikacije: glibc wrapper funkcije rade nešto kako bi pokrenule ispravan sistemski poziv, a to osigurava ABI kompatibilnost za starije binarne datoteke.) Primjeri sistemskih poziva koji imaju više verzija:

*Trenutno postoje tri različite verzije stat(2): sys_stat() (mjesto __NR_oldstat), sys_newstat() (mjesto __NR_stat) I sys_stat64() (mjesto __NR_stat64), potonji je trenutno u upotrebi. Slična situacija sa lstat(2) i fstat(2). * Definirano na sličan način, __NR_staro ime __NR_staro ime I __NR_uime za pozive(), sys_staroime sys_uname () I sys_novoime ().* Linux 2.0 ima novu verziju vm86(2) nazivaju se nova i stara verzija nuklearnih postupaka sys_vm86stari() I sys_vm86(). * Linux 2.4 ima novu verziju() (mjesto getrlimit(2) nazivaju se nova i stara verzija nuklearnih postupaka sys_old_getrlimit() (mjesto __NR_getrlimit) I sys_getrlimit(2), __NR_ugetrlimit(2), ).(2), * U Linuxu 2.4, veličina ID polja korisnika i grupe povećana je sa 16 na 32 bita. Dodano je nekoliko sistemskih poziva koji podržavaju ovu promjenu (npr.(2)), eliminirajući ranije pozive s istim imenima, ali bez sufiksa "32". * Linux 2.4 je dodao podršku za pristup velikim datotekama (čije veličine i pomaci ne stanu u 32 bita) u aplikacijama na 32-bitnoj arhitekturi. To je zahtijevalo promjene u pozivima sustava koji rade s veličinama datoteka i pomacima. Dodani su sljedeći sistemski pozivi:(2), fcntl64(2), getdents64(2), stat64(2), skratiti64 statfs64

(2) i njihovi analozi, koji rukuju deskriptorima datoteka ili simboličkim vezama. Ovi sistemski pozivi uklanjaju stare sistemske pozive, koji su, s iznimkom "stat" poziva, također imenovani, ali nemaju sufiks "64".

Na novijim platformama koje imaju samo 64-bitni pristup datoteci i 32-bitni UID/GID (npr. alpha, ia64, s390x, x86-64), postoji samo jedna verzija poziva sustava za UID/GID i pristup datoteci. Na platformama (obično 32-bitne platforme) gdje su dostupni *64 i *32 pozivi, ostale verzije su zastarjele. * Izazovi rt_sig* dodan u kernel 2.2 za podršku dodatnim signalima u stvarnom vremenu (pogledajte kernel 2.2). signal (7)). Ovi sistemski pozivi nadjačavaju stare sistemske pozive s istim imenima, ali bez prefiksa "rt_".(2) i * U sistemskim pozivima odabrati mmap(2) nazivaju se nova i stara verzija nuklearnih postupaka (2) koristi se pet ili više argumenata, što uzrokuje probleme u određivanju načina prosljeđivanja argumenata na i386. Kao posljedica toga, dok na drugim arhitekturama poziva sys_select sys_mmap() utakmica __NR_odaberi I __NR_mmap(2) nazivaju se nova i stara verzija nuklearnih postupaka , na i386 odgovaraju stari_odaberi stara_mmap() (procedure koje koriste pokazivač na blok argumenata). Trenutno više nema problema s prosljeđivanjem više od pet argumenata i postoji mmap __NR__novi odabir , što točno odgovara.

(), a ista situacija sa

__NR_mmap2

Ovaj materijal je modifikacija istoimenog članka Vladimira Meshkova, objavljenog u časopisu "System Administrator" Ovaj materijal je kopija članaka Vladimira Meshkova iz časopisa "System Administrator". Ovi se članci mogu pronaći putem poveznica u nastavku. Neki primjeri izvornih kodova programa također su promijenjeni - poboljšani, finalizirani. (Primjer 4.2 je uvelike modificiran, jer smo morali presresti malo drugačiji sistemski poziv) URL-ovi: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/uploaded /a3.pdf

  • Ima li pitanja? Onda izvolite:
  • [e-mail zaštićen]
    • 2. Modul jezgre koji se može učitavati

4. Primjeri presretanja sistemskih poziva temeljenih na LKM

Najopćenitiji pogled omogućuje nam da vidimo dvorazinski model sustava.<=>kernel

progs U sredini (lijevo) je jezgra sustava. Kernel izravno komunicira s računalnim hardverom, izolirajući aplikacijske programe od arhitektonskih značajki. Kernel ima skup usluga koje se pružaju aplikacijskim programima. Servisi kernela uključuju I/O operacije (otvaranje, čitanje, pisanje i upravljanje datotekama), stvaranje i upravljanje procesima, njihovu sinkronizaciju i međuprocesnu komunikaciju. Sve aplikacije zahtijevaju usluge jezgre putem sistemskih poziva.

Drugu razinu čine aplikacije ili zadaci, kako sistemski, koji određuju funkcionalnost sustava, tako i aplikativni, koji osiguravaju korisničko sučelje Linuxa. Međutim, unatoč vanjskoj heterogenosti aplikacija, sheme interakcije s jezgrom su iste.

Interakcija s kernelom odvija se kroz standardno sučelje za pozive sustava. Sučelje sistemskog poziva predstavlja skup kernel usluga i definira format servisnih zahtjeva. Proces zahtijeva uslugu putem sistemskog poziva određenoj proceduri jezgre, sličnog izgleda običnom pozivu funkcije knjižnice. Kernel, u ime procesa, izvršava zahtjev i procesu vraća potrebne podatke.

U gornjem primjeru, program otvara datoteku, čita podatke iz nje i zatvara datoteku. U ovom slučaju operaciju otvaranja (open), čitanja (read) i zatvaranja (close) datoteke izvodi kernel na zahtjev zadatka, a open(2), read(2) i close(2 ) funkcije su sistemski pozivi. /* Izvor 1.0 */ #include

  • main () ( int fd; char buf; /* Otvori datoteku - dobij vezu (deskriptor datoteke) fd */ fd = open("file1",O_RDONLY); /* Učitaj 80 znakova u međuspremnik buf */ read( fd, buf, sizeof(buf)); /* Zatvori datoteku */ close(fd); Pogledajmo sada mehanizam za izvršavanje sistemskih poziva koristeći ovaj primjer. Kompajler, nakon što je naišao na funkciju open() za otvaranje datoteke, pretvara je u asemblerski kod, učitavajući broj sistemskog poziva koji odgovara ovoj funkciji i njenim parametrima u registre procesora i potom poziva prekid 0x80. U registre procesora učitavaju se sljedeće vrijednosti:
  • u EAX registru - broj sistemskog poziva. Dakle, za naš slučaj, broj poziva sustava je 5 (pogledajte __NR_open).
  • u ECX registar - drugi parametar (prava pristupa datoteci)
Treći parametar se učitava u EDX registar; u ovom slučaju ga nemamo. Za izvođenje sistemskog poziva u OS Linux koristite funkciju system_call koja je definirana (ovisno o arhitekturi, u ovom slučaju i386) u datoteci /usr/src/linux/arch/i386/kernel/entry.S. Ova funkcija je ulazna točka za sve sistemske pozive. Kernel odgovara na prekid 0x80 pozivanjem funkcije system_call, koja je u biti rukovatelj prekidom 0x80.

Kako bismo bili sigurni da smo na pravom putu, pogledajmo kod za funkciju open() u knjižnici sustava libc:

# gdb -q /lib/libc.so.6 (gdb) disas open Dump asemblerskog koda za otvorenu funkciju: 0x000c8080 : poziv 0x1082be< __i686.get_pc_thunk.cx >0x000c8085 : dodajte $0x6423b,%ecx 0x000c808b : cmpl $0x0.0x1a84(%ecx) 0x000c8092 :jne 0xc80b1 0x000c8094 : pritisnite %ebx 0x000c8095 : mov 0x10(%esp,1),%edx 0x000c8099 : mov 0xc(%esp,1),%ecx 0x000c809d : mov 0x8(%esp,1),%ebx 0x000c80a1 : mov $0x5,%eax 0x000c80a6 : int $0x80 ... Kao što nije teško primijetiti u zadnjim redovima, parametri se prenose u EDX, ECX, EBX registre, a sistemski pozivni broj se stavlja u zadnji EAX registar, jednak, kao što već znamo , 5.

Sada se vratimo na mehanizam pozivanja sustava. Dakle, kernel poziva rukovatelja prekidima 0x80 - funkciju system_call. System_call smješta kopije registara koji sadrže parametre poziva na stog pomoću makronaredbe SAVE_ALL i poziva željenu funkciju sustava s naredbom poziva. Tablica pokazivača na kernel funkcije koje implementiraju sistemske pozive nalazi se u polju sys_call_table (pogledajte datoteku arch/i386/kernel/entry.S). Sistemski pozivni broj, koji se nalazi u EAX registru, je indeks u ovom polju. Dakle, ako EAX sadrži vrijednost 5, bit će pozvana kernel funkcija sys_open(). Zašto je potrebna makronaredba SAVE_ALL? Objašnjenje je ovdje vrlo jednostavno. Budući da su gotovo sve funkcije sustava jezgre napisane u C-u, one svoje parametre traže na stogu. I parametri se guraju na stog koristeći SAVE_ALL! Povratna vrijednost sistemskog poziva pohranjuje se u EAX registar.

Sada shvatimo kako presresti sistemski poziv. U tome će nam pomoći mehanizam modula kernela koji se mogu učitavati.

Ima li pitanja? Onda izvolite:

Modul jezgre koji se može učitavati (uobičajena kratica LKM - Loadable Kernel Module) je programski kod koji se izvršava u prostoru jezgre. Glavna značajka LKM-a je mogućnost dinamičkog učitavanja i pražnjenja bez potrebe ponovnog pokretanja cijelog sustava ili ponovnog kompajliranja kernela.

Svaki LKM sastoji se od dvije glavne funkcije (minimalno):

  • funkcija inicijalizacije modula. Poziva se kada se LKM učita u memoriju: int init_module(void) ( ... )
  • funkcija uklanjanja modula: void cleanup_module(void) ( ... )
Evo primjera jednostavnog modula: /* Izvor 2.0 */ #include int init_module(void) ( printk("Hello World\n"); return 0; ) void cleanup_module(void) ( printk("Bye\n"); ) /* EOF */ Prevedi i učitaj modul. Učitavanje modula u memoriju vrši se naredbom insmod, a pregled učitanih modula naredbom lsmod: # gcc -c -DMODULE -I/usr/src/linux/include/ src-2.0.c # insmod src-2.0.o Upozorenje: učitavanje src-2.0 .o oštetit će kernel: nema licence Modul src-2.0 je učitan, s upozorenjima # dmesg | tail -n 1 Pozdrav svijete # lsmod | grep src src-2.0 336 0 (nekorišteno) # rmmod src-2.0 # dmesg | rep -n 1 Zbogom

3. Algoritam za presretanje poziva sustava temeljen na LKM

Za implementaciju modula koji presreće pozive sustava potrebno je definirati algoritam presretanja. Algoritam je sljedeći:
  • spremiti pokazivač na izvorni (izvorni) poziv tako da se može vratiti
  • stvoriti funkciju koja implementira novi sistemski poziv
  • u tablici sistemskih poziva sys_call_table zamijeniti pozive, tj. postaviti odgovarajući pokazivač na novi sistemski poziv
  • po završetku rada (prilikom pražnjenja modula), vratite izvorni sistemski poziv pomoću prethodno spremljenog pokazivača
Praćenje vam omogućuje da saznate koji se sistemski pozivi koriste tijekom rada korisničke aplikacije. Praćenjem možete odrediti koji sistemski poziv treba presresti da bi se preuzela kontrola nad aplikacijom. # ltrace -S ./src-1.0 ... otvori("datoteka1", 0, 01<... open resumed>SYS_open("datoteka1", 0, 01) = 3 ) = 3 čitanje(3,<... read resumed>SYS_read(3, "123\n", 80) = 4 "123\n", 80) = 4 zatvori(3<... close resumed>SYS_close(3) = 0

) = 0 ... Sada imamo dovoljno informacija da počnemo proučavati primjere implementacija modula koji presreću sistemske pozive.

2. Modul jezgre koji se može učitavati

4. Primjeri presretanja sistemskih poziva temeljenih na LKM Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. /* Eksport tablice sistemskih poziva */ extern void *sys_call_table;

/* Definirajte pokazivač za spremanje originalnog poziva */ int (*orig_mkdir)(const char *path);

/* Kreirajmo vlastiti sistemski poziv. Naš poziv ne radi ništa, samo vraća null vrijednost */ int own_mkdir(const char *path) ( return 0; ) /* Tijekom inicijalizacije modula spremamo pokazivač na izvorni poziv i zamjenjujemo sistemski poziv */ int init_module(void ) ( orig_mkdir =sys_call_table; sys_call_table=own_mkdir; printk("sys_mkdir replaced\n"); return(0); "); ) /* EOF */ Da biste dobili objektni modul, pokrenite sljedeću naredbu i provedite niz eksperimenata na sustavu: # gcc -c -DMODULE -I/usr/src/linux/include/ src-3.1.c # dmesg | tail -n 1 sys_mkdir zamijenjen # mkdir test # ls -ald test ls: test: Nema takve datoteke ili direktorija # rmmod src-3.1 # dmesg | tail -n 1 sys_mkdir pomaknut natrag # mkdir test # ls -ald test drwxr-xr-x 2 root root 4096 2003-12-23 03:46 test Kao što vidite, naredba "mkdir" ne radi, odnosno, ništa se ne događa. Za vraćanje funkcionalnosti sustava, jednostavno izvadite modul. Što je gore učinjeno. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. 4.2 Skrivanje unosa datoteke u direktoriju Odredimo koji je sistemski poziv odgovoran za čitanje sadržaja imenika. Da bismo to učinili, napišimo još jedan testni fragment koji čita trenutni direktorij: /* Izvor 4.2.1 */ #include<... opendir resumed>int main() ( DIR *d; struct dirent *dp; d = opendir("."); dp = readdir(d); return 0; ) /* EOF */ Dobijte izvršnu datoteku i pratite je: # gcc -o src-3.2.1 src-3.2.1.c # ltrace -S ./src-3.2.1 ... opendir("." SYS_open(".", 100352, 010005141300) = 3 SYS_fstat64(3, 0xbffff79c, 0x4014c2c0, 3, 0xbffff874) = 0 SYS_fcntl64(3, 2, 1, 1, 0x4014c2c0) = 0 SYS _ brk(NULL) = 0x080495f4 SYS_brk(0x0806a5f4 ) = 0x0806a5f4 SYS_brk(NULL) = 0x0806a5f4 SYS_brk(0x0806b000) = 0x0806b000<... readdir resumed>) = 0x08049648 readdir(0x08049648
  • d_reclen - rekordna veličina
  • d_name - naziv datoteke
Da biste sakrili zapis o datoteci (drugim riječima, učinili ga nevidljivim), morate presresti sistemski poziv sys_getdents64, pronaći odgovarajući zapis u listi primljenih struktura i izbrisati ga. Pogledajmo kod koji izvodi ovu operaciju (autor originalnog koda je Michal Zalewski): /* Izvor 4.2.2 */ #include Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. extern void *sys_call_table;

int (*orig_getdents)(u_int fd, struct dirent *dirp, u_int count);

Razmotrimo najprije teoretski kako se presretanje provodi izravnim pristupom adresnom prostoru jezgre, a zatim ćemo prijeći na praktičnu implementaciju.

Izravan pristup adresnom prostoru kernela omogućuje datoteka uređaja /dev/kmem. Ova datoteka prikazuje sav dostupni virtualni adresni prostor, uključujući swap particiju. Za rad s kmem datotekom koriste se standardne funkcije sustava - open(), read(), write(). Otvaranjem /dev/kmem na standardni način možemo pristupiti bilo kojoj adresi u sustavu, postavljajući je kao offset u ovoj datoteci. Ovu metodu razvio je Silvio Cesare.

Funkcijama sustava se pristupa učitavanjem parametara funkcije u registre procesora i zatim pozivanjem softverskog prekida 0x80. Rukovatelj za ovaj prekid, funkcija system_call, gura parametre poziva na stog, dohvaća adresu pozvane sistemske funkcije iz tablice sys_call_table i prenosi kontrolu na ovu adresu.

Imajući puni pristup adresnom prostoru jezgre, možemo dobiti cijeli sadržaj tablice sistemskih poziva, tj. adrese svih funkcija sustava. Promjenom adrese bilo kojeg poziva sustava, mi ćemo ga time presresti. Ali za ovo morate znati adresu tablice, ili, drugim riječima, pomak u /dev/kmem datoteci na kojoj se ova tablica nalazi.

Da biste odredili adresu tablice sys_call_table, prvo morate izračunati adresu funkcije system_call. Budući da je ova funkcija rukovatelj prekidima, pogledajmo kako se rukuje prekidima u zaštićenom načinu rada.

U stvarnom načinu rada, prilikom registracije prekida, procesor pristupa tablici vektora prekida koja se uvijek nalazi na samom početku memorije i sadrži adrese od dvije riječi programa za obradu prekida. U zaštićenom načinu rada, analog tablice vektora prekida je tablica deskriptora prekida (IDT, Interrupt Descriptor Table), koja se nalazi u operativnom sustavu zaštićenog načina rada. Da bi procesor mogao pristupiti ovoj tablici, njegova adresa mora biti učitana u registar IDTR (Interrupt Descriptor Table Register). IDT tablica sadrži deskriptore rukovatelja prekidima, koji posebno uključuju njihove adrese. Ti se deskriptori nazivaju vratima. Procesor, nakon što je registrirao prekid, dohvaća pristupnik iz IDT-a koristeći njegov broj, određuje adresu rukovatelja i prenosi kontrolu na njega.

Za izračunavanje adrese system_call funkcije iz IDT tablice potrebno je ekstrahirati prekidni gate int $0x80, a iz njega adresu odgovarajućeg rukovatelja, tj. adresa funkcije system_call. U funkciji system_call tablici system_call_table pristupa se pomoću naredbe call<адрес_таблицы>(,%eax,4). Pronašavši opcode (potpis) ove naredbe u datoteci /dev/kmem, pronaći ćemo i adresu tablice sistemskih poziva.

Za određivanje operativnog koda upotrijebit ćemo program za ispravljanje pogrešaka i rastaviti funkciju system_call:

# gdb -q /usr/src/linux/vmlinux (gdb) disas system_call Dump asemblerskog koda za funkciju system_call: 0xc0194cbc : pritisnite %eax 0xc0194cbd : cld 0xc0194cbe : pritisnite %es 0xc0194cbf : pritisnite %ds 0xc0194cc0 : pritisnite %eax 0xc0194cc1 : pritisnite %ebp 0xc0194cc2 : pritisnite %edi 0xc0194cc3 : pritisnite %esi 0xc0194cc4 : pritisnite %edx 0xc0194cc5 : pritisnite %ecx 0xc0194cc6 : pritisnite %ebx 0xc0194cc7 : mov $0x18,%edx 0xc0194ccc : mov %edx,%ds 0xc0194cce : mov %edx,%es 0xc0194cd0 : mov $0xffffe000,%ebx 0xc0194cd5 : i %esp,%ebx 0xc0194cd7 : testb $0x2.0x18(%ebx) 0xc0194cdb :jne 0xc0194d3c 0xc0194cdd : cmp $0x10e,%eax 0xc0194ce2 :jae0xc0194d69 0xc0194ce8 : poziv *0xc02cbb0c(,%eax,4) 0xc0194cef : mov %eax,0x18(%esp,1) 0xc0194cf3 : nop Kraj ispisa asemblera. Redak "call *0xc02cbb0c(,%eax,4)" je poziv tablici sys_call_table. Vrijednost 0xc02cbb0c je adresa tablice (najvjerojatnije će vaši brojevi biti drugačiji). Uzmimo operativni kod ove naredbe: (gdb) x/xw system_call+44 0xc0194ce8 : 0x0c8514ff Pronašli smo operativni kod naredbe za pristup tablici sys_call_table. Jednako je \xff\x14\x85. Sljedeća 4 bajta su adresa tablice. To možete provjeriti unosom naredbe: (gdb) x/xw system_call+44+3 0xc0194ceb : 0xc02cbb0c Dakle, pronalaženjem niza \xff\x14\x85 u datoteci /dev/kmem i čitanjem sljedeća 4 bajta, dobivamo adresu tablice sistemskih poziva sys_call_table. Znajući njegovu adresu, možemo dobiti sadržaj ove tablice (adrese svih funkcija sustava) i promijeniti adresu bilo kojeg poziva sustava njegovim presretanjem.

Pogledajmo pseudokod koji izvodi operaciju presretanja:

Readaddr(stari_syscall, scr + SYS_CALL*4, 4);

writeaddr(novi_syscall, scr + SYS_CALL*4, 4); Funkcija readaddr čita adresu sistemskog poziva iz tablice sistemskih poziva i pohranjuje je u varijablu old_syscall. Svaki unos u tablici sys_call_table zauzima 4 bajta. Potrebna adresa nalazi se na offsetu sct + SYS_CALL*4 u datoteci /dev/kmem (ovdje je sct adresa tablice sys_call_table, SYS_CALL je serijski broj sistemskog poziva). Funkcija writeaddr prepisuje adresu sistemskog poziva SYS_CALL adresom funkcije new_syscall, a svi pozivi na sistemski poziv SYS_CALL bit će servisirani ovom funkcijom.

Čini se da je sve jednostavno i cilj je postignut. Međutim, upamtimo da radimo u adresnom prostoru korisnika. Ako postavimo novu sistemsku funkciju u ovaj adresni prostor, kada pozovemo tu funkciju, dobit ćemo lijepu poruku o pogrešci. Stoga zaključak - novi sistemski poziv mora biti smješten u adresni prostor jezgre. Da biste to učinili, trebate: nabaviti memorijski blok u prostoru jezgre, postaviti novi sistemski poziv u ovaj blok.

  • Možete dodijeliti memoriju u prostoru jezgre pomoću funkcije kmalloc. Ali nemoguće je pozvati kernel funkciju izravno iz korisničkog adresnog prostora, pa ćemo koristiti sljedeći algoritam:
  • znajući adresu tablice sys_call_table, dobivamo adresu nekog sistemskog poziva (na primjer, sys_mkdir)
  • Definiramo funkciju koja poziva kmalloc funkciju. Ova funkcija vraća pokazivač na blok memorije u adresnom prostoru jezgre.
  • Nazovimo ovu funkciju get_kmalloc
  • spremite prvih N bajtova sistemskog poziva sys_mkdir, gdje je N veličina funkcije get_kmalloc
  • prepisati prvih N bajtova poziva sys_mkdir funkcijom get_kmalloc
pozivamo sistemski poziv sys_mkdir, čime pokrećemo funkciju get_kmalloc

vratiti prvih N bajtova sistemskog poziva sys_mkdir

Funkcija kmalloc uzima dva parametra: veličinu tražene memorije i GFP specifikator. Za traženje operacijskog koda upotrijebit ćemo program za ispravljanje pogrešaka i rastaviti svaku funkciju jezgre koja sadrži poziv funkcije kmalloc.

# gdb -q /usr/src/linux/vmlinux (gdb) disas inter_module_register Dump asemblerskog koda za funkciju inter_module_register: 0xc01a57b4 : pritisnite %ebp 0xc01a57b5 : pritisnite %edi 0xc01a57b6 : pritisnite %esi 0xc01a57b7 : pritisnite %ebx 0xc01a57b8 : sub $0x10,%esp 0xc01a57bb : mov 0x24(%esp,1),%ebx 0xc01a57bf : mov 0x28(%esp,1),%esi 0xc01a57c3 : mov 0x2c(%esp,1),%ebp 0xc01a57c7 : movl $0x1f0,0x4(%esp,1) 0xc01a57cf : movl $0x14,(%esp,1) 0xc01a57d6 : nazovite 0xc01bea2a ... Nije važno što funkcija radi, glavna stvar u njoj je ono što nam treba - poziv funkciji kmalloc. Obratite pozornost na posljednje retke. Prvo se parametri učitavaju na stog (esp registar pokazuje na vrh stoga), nakon čega slijedi poziv funkcije. GFP specifikator se prvi učitava na stog ($0x1f0,0x4(%esp,1). Za verzije kernela 2.4.9 i novije, ova vrijednost je 0x1f0. Pronađimo operativni kod ove naredbe: (gdb) x/xw inter_module_register +19 0xc01a57c7 : 0x042444c7 Ako pronađemo ovaj opcode, možemo izračunati adresu kmalloc funkcije. Na prvi pogled, adresa ove funkcije je argument instrukcije poziva, ali to nije sasvim točno. Za razliku od funkcije system_call, ovdje instrukcija ne sadrži kmalloc adresu, već njen pomak u odnosu na trenutnu adresu. Provjerimo ovo definiranjem operacijskog koda poziva naredbe 0xc01bea2a: (gdb) x/xw inter_module_register+34 0xc01a57d6 : 0x01924fe8 Prvi bajt je e8 - ovo je operativni kod instrukcije poziva. Pronađimo vrijednost argumenta ove naredbe: (gdb) x/xw inter_module_register+35 0xc01a57d7 : 0x0001924f Sada ako dodamo trenutnu adresu 0xc01a57d6, offset 0x0001924f i 5 bajtova naredbe, dobit ćemo željenu adresu funkcije kmalloc - 0xc01bea2a.

Ovime završavamo teoretske izračune i, koristeći gornju metodu, presret ćemo sistemski poziv sys_mkdir.

6. Primjer presretanja pomoću /dev/kmem

/* izvor 6.0 */ #include Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. Kada se stvori direktorij, poziva se kernel funkcija sys_mkdir. Parametar je niz koji sadrži naziv direktorija koji se kreira. Pogledajmo kod koji presreće odgovarajući sistemski poziv. /* Broj poziva sustava za presretanje */ #define _SYS_MKDIR_ 39 #define KMEM_FILE "/dev/kmem" #define MAX_SYMS 4096 /* Opis formata registra IDTR */ struct ( unsigned short limit; unsigned int base; ) __attribute__ (( upakirano) ) idtr;< 0) return 0; get_kernel_syms(tab); for (i = 0; i < numsyms; i++) { if (!strncmp(n, tab[i].name, strlen(n))) return tab[i].value; } return 0; } /* Наша новая системная функция, ничего не делает;) */ int new_mkdir(const char *path) { return 0; } /* Читает из /dev/kmem с offset size данных в buf */ static inline int rkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset){ printf("lseek err\n"); return 0; } if (read(fd, buf, size) != size) return 0; return size; } /* Аналогично, но только пишет в /dev/kmem */ static inline int wkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset) return 0; if (write(fd, buf, size) != size) return 0; return size; } /* Читает из /dev/kmem данные размером 4 байта */ static inline int rkml(int fd, uint offset, ulong *buf) { return rkm(fd, offset, buf, sizeof(ulong)); } /* Аналогично, но только пишет */ static inline int wkml(int fd, uint offset, ulong buf) { return wkm(fd, offset, &buf, sizeof(ulong)); } /* Функция для получения адреса sys_call_table */ ulong get_sct(int kmem) { ulong sys_call_off; // - адрес обработчика // прерывания int $0x80 (функция system_call) char *p; char sc_asm; asm("sidt %0" : "=m" (idtr)); if (!rkm(kmem, idtr.base+(8*0x80), &idt, sizeof(idt))) return 0; sys_call_off = (idt.off2 << 16) | idt.off1; if (!rkm(kmem, sys_call_off, &sc_asm, 128)) return 0; p = (char *)memmem(sc_asm, 128, "\xff\x14\x85", 3) + 3; printf("call for sys_call_table at %08x\n",p); if (p) return *(ulong *)p; return 0; } /* Функция для определения адреса функции kmalloc */ ulong get_kma(ulong pgoff) { uint i; unsigned char buf, *p, *p1; int kmemz; ulong ret; ret = get_sym("kmalloc"); if (ret) { printf("\nZer gut!\n"); return ret; } kmemz = open("/dev/kmem", O_RDONLY); if (kmemz < 0) return 0; for (i = pgoff+0x100000; i < (pgoff + 0x1000000); i += 0x10000){ if (!rkm(kmemz, i, buf, sizeof(buf))) return 0; p1=(char *)memmem(buf,sizeof(buf),"\x68\xf0\x01\x00",4); if(p1) { p=(char *)memmem(p1+4,sizeof(buf),"\xe8",1)+1; if (p) { close(kmemz); return *(unsigned long *)p+i+(p-buf)+4; } } } close(kmemz); return 0; } int main() { int kmem; // !! - пустые, нужно подставить ulong get_kmalloc_size; // - размер функции get_kmalloc !! ulong get_kmalloc_addr; // - адрес функции get_kmalloc !! ulong new_mkdir_size; // - размер функции-перехватчика!! ulong new_mkdir_addr; // - адрес функции-перехватчика!! ulong sys_mkdir_addr; // - адрес системного вызова sys_mkdir ulong page_offset; // - нижняя граница адресного // пространства ядра ulong sct; // - адрес таблицы sys_call_table ulong kma; // - адрес функции kmalloc unsigned char tmp; kmem = open(KMEM_FILE, O_RDWR, 0); if (kmem < 0) return 0; sct = get_sct(kmem); page_offset = sct & 0xF0000000; kma = get_kma(page_offset); printf("OK\n" "page_offset\t\t:\t0x%08x\n" "sys_call_table\t:\t0x%08x\n" "kmalloc()\t\t:\t0x%08x\n", page_offset,sct,kma); /* Найдем адрес sys_mkdir */ if (!rkml(kmem, sct+(_SYS_MKDIR_*4), &sys_mkdir_addr)) { printf("Cannot get addr of %d syscall\n", _SYS_MKDIR_); perror("er: "); return 1; } /* Сохраним первые N байт вызова sys_mkdir */ if (!rkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Cannot save old %d syscall!\n", _SYS_MKDIR_); return 1; } /* Перепишем первые N байт, функцией get_kmalloc */ if (!wkm(kmem, sys_mkdir_addr,(void *)get_kmalloc_addr, get_kmalloc_size)) { printf("Can"t overwrite our syscall %d!\n",_SYS_MKDIR_); return 1; } kmalloc.kmalloc = (void *) kma; //- адрес функции kmalloc kmalloc.size = new_mkdir_size; //- размер запращевоемой // памяти (размер функции-перехватчика new_mkdir) kmalloc.flags = 0x1f0; //- спецификатор GFP /* Выполним сис. вызов sys_mkdir, тем самым выполним нашу функцию get_kmalloc */ mkdir((char *)&kmalloc,0); /* Востановим оригинальный вызов sys_mkdir */ if (!wkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Can"t restore syscall %d !\n",_SYS_MKDIR_); return 1; } if (kmalloc.mem < page_offset) { printf("Allocated memory is too low (%08x < %08x)\n", kmalloc.mem, page_offset); return 1; } /* Оторбразим результаты */ printf("sys_mkdir_addr\t\t:\t0x%08x\n" "get_kmalloc_size\t:\t0x%08x (%d bytes)\n\n" "our kmem region\t\t:\t0x%08x\n" "size of our kmem\t:\t0x%08x (%d bytes)\n\n", sys_mkdir_addr, get_kmalloc_size, get_kmalloc_size, kmalloc.mem, kmalloc.size, kmalloc.size); /* Разместим в пространстве ядра наш новый сис. вызво */ if(!wkm(kmem, kmalloc.mem, (void *)new_mkdir_addr, new_mkdir_size)) { printf("Unable to locate new system call !\n"); return 1; } /* Перепишем таблицу sys_call_table на наш новый вызов */ if(!wkml(kmem, sct+(_SYS_MKDIR_*4), kmalloc.mem)) { printf("Eh ..."); return 1; } return 1; } /* EOF */ Скомпилируем полученый код и определим адреса и размеры функций get_kmalloc и new_mkdir. Запускать полученое творение рано! Для вычисления адресов и размеров воспользуемся утилитой objdump: # gcc -o src-6.0 src-6.0.c # objdump -x ./src-6.0 >/* Opis IDT tablice interrupt gate format */ struct ( unsigned short off1; unsigned short sel; unsigned char none, flags; unsigned short off2; ) __attribute__ ((packed)) idt;

/* Opis strukture za funkciju get_kmalloc */ struct kma_struc ( ulong (*kmalloc) (uint, int); // - adresa funkcije kmalloc int veličina; // - veličina memorije za dodjelu int zastavica; // - zastavica, za kernele > 2.4.9 = 0x1f0 (GFP) ulong mem ) __attribute__ ((packed)) kmalloc;

/* Funkcija koja samo dodjeljuje blok memorije u adresnom prostoru jezgre */ int get_kmalloc(struct kma_struc *k) ( k->mem = k->kmalloc(k->size, k->flags); return 0 ; ) /* Funkcija koja vraća adresu funkcije (potrebna za kmalloc pretragu) */ ulong get_sym(char *n) ( struct kernel_sym tab; int numsyms; int i; numsyms = get_kernel_syms(NULL); if (numsyms > MAX_SYMS ||
dump Otvorimo datoteku dumpa i pronađimo podatke koji nas zanimaju: 080485a4 g F .text 00000032 get_kmalloc 080486b1 g F .text 0000000a new_mkdir Sada unesimo ove vrijednosti u naš program: ulong get_kmalloc_size=0x32;

ulong get_kmalloc_addr=0x080485a4 ;

Ovdje programiranje kernela postaje opasno. Dok sam pisao primjer u nastavku, uništio sam poziv otvorenog sustava. To je značilo da nisam mogao otvoriti nijednu datoteku, nisam mogao pokrenuti nikakve programe i nisam mogao ugasiti sustav naredbom za isključivanje. Moram isključiti struju da to zaustavim. Srećom, nijedan spis nije uništen. Kako biste bili sigurni da nećete izgubiti datoteke, izvršite sinkronizaciju prije izdavanja naredbi insmod i rmmod.

Zaboravite na /proc datoteke i datoteke uređaja. Oni su samo mali detalji. Pravi proces komunikacije s jezgrom, koji koriste svi procesi, su sistemski pozivi. Kada proces zatraži uslugu od kernela (kao što je otvaranje datoteke, pokretanje novog procesa ili zahtjev za više memorije), koristi se ovaj mehanizam. Ako želite promijeniti ponašanje kernela na zanimljive načine, ovo je pravo mjesto. Usput, ako želite vidjeti koje sistemske pozive koristi program, pokrenite: strace .

Općenito, proces ne može pristupiti kernelu. Ne može pristupiti memoriji jezgre i ne može pozvati funkcije jezgre. Hardver CPU-a diktira ovo stanje (postoji razlog zašto se zove "zaštićeni način rada"). Sistemski pozivi su iznimka od ovog općeg pravila. Proces ispunjava registre odgovarajućim vrijednostima i zatim poziva posebnu instrukciju unaprijed definirana lokacija u jezgri (naravno, čitaju je korisnički procesi, ali ne i prepisuju je preko Intelovih procesora, to se postiže prekidom 0x80) kada dođete do ove lokacije, više se ne pokrećete Umjesto toga, radite kao kernel operativnog sustava i stoga vam je dopušteno raditi što god želite.

Lokacija u kernelu koju proces može pozvati zove se system_call. Procedura koja se nalazi tamo provjerava broj poziva sustava, koji govori kernelu što točno proces želi. Zatim traži tablicu sistemskih poziva (sys_call_table) kako bi pronašao adresu funkcije jezgre koju treba pozvati. Zatim se poziva željena funkcija, a nakon što vrati vrijednost, vrši se nekoliko provjera sustava. Rezultat se zatim vraća procesu (ili drugom procesu ako je proces prekinut). Ako želite vidjeti kod koji sve ovo radi, nalazi se u datoteci arch/source< architecture >/kernel/entry.S , iza retka ENTRY(system_call) .

Dakle, ako želimo promijeniti način na koji funkcionira neki sistemski poziv, prva stvar koju moramo učiniti je napisati vlastitu funkciju koja će učiniti odgovarajuću stvar (obično dodavanjem malo vlastitog koda, a zatim pozivanjem izvorne funkcije), zatim promijenite pokazivač na sys_call_table da pokazuje na našu funkciju. Budući da kasnije možemo biti izbrisani i ne želimo ostaviti sustav u nekonzistentnom stanju, važno je da cleanup_module vrati tablicu u izvorno stanje.

Ovdje naveden izvorni kod je primjer takvog modula. Želimo "špijunirati" određenog korisnika i poslati poruku putem printk-a kad god taj korisnik otvori datoteku. Sistemski poziv za otvaranje datoteke zamjenjujemo vlastitom funkcijom koja se zove our_sys_open. Ova funkcija provjerava uid (korisnički ID) trenutnog procesa, i ako je jednak uid-u koji špijuniramo, poziva printk da prikaže naziv datoteke koja će se otvoriti. Zatim poziva izvornu funkciju otvaranja s istim parametrima, zapravo otvara datoteku.

Funkcija init_module mijenja odgovarajuću lokaciju u sys_call_table i pohranjuje originalni pokazivač u varijablu. Funkcija cleanup_module koristi ovu varijablu za vraćanje svega u normalu. Ovaj pristup je opasan zbog mogućnosti da dva modula modificiraju isti poziv sustava. Zamislimo da imamo dva modula, A i B. Nazovimo poziv otvorenog sustava modula A A_open i nazovimo isti poziv modulu B B_open. Sada kada je syscall umetnut u kernel zamijenjen je s A_open, koji će pozvati izvorni sys_open kada učini ono što treba. Zatim će B biti umetnut u jezgru i zamijenit će sistemski poziv s B_open, koji će pozvati ono što misli da je izvorni sistemski poziv, ali je zapravo A_open.

Ako se prvo ukloni B, sve će biti u redu: jednostavno će se vratiti sistemski poziv na A_open koji poziva izvorni. Međutim, ako se A ukloni, a zatim B ukloni, sustav će se urušiti. Uklanjanje A će vratiti sistemski poziv na izvorni, sys_open, izrezujući B iz petlje. Zatim, kada se B ukloni, vratit će poziv sustava na ono za što misli da je izvorni, zapravo će biti usmjeren na A_open, koji više nije u memoriji. Na prvi pogled se čini da bismo mogli riješiti ovaj određeni problem provjerom je li sistemski poziv jednak našoj open funkciji i ako je tako, ne mijenjajući vrijednost tog poziva (tako da B ne promijeni sistemski poziv kada se izbriše ), ali to bi uzrokovalo još jedan najveći problem. Kada se A ukloni, vidi da je sistemski poziv promijenjen u B_open tako da više ne pokazuje na A_open, tako da neće vratiti pokazivač na sys_open prije nego što se ukloni iz memorije. Nažalost, B_open će i dalje pokušavati pozvati A_open, koji više nije u memoriji, pa čak i bez uklanjanja B, sustav će se i dalje srušiti.

Vidim dva načina za sprječavanje ovog problema. Prvo: vratite pristup na izvornu vrijednost sys_open. Nažalost, sys_open nije dio kernel tablice u /proc/ksyms, pa mu ne možemo pristupiti. Drugo rješenje je korištenje referentnog brojača za sprječavanje pražnjenja modula. Ovo je dobro za obične module, ali loše za "obrazovne" module.

/* syscall.c * * Uzorak "krađe" sistemskog poziva */ /* Autorsko pravo (C) 1998-99 Ori Pomerantz */ /* Potrebne datoteke zaglavlja */ /* Standardno u modulima kernela */ #include /* Radimo na kernelu */ #include /* Konkretno, modul */ /* Bavi se CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif #include /* Popis sistemskih poziva */ /* Za trenutnu (procesnu) strukturu, potrebno nam je * ovo da znamo tko je trenutni korisnik. */ #uključi /* U 2.2.3 /usr/include/linux/version.h uključuje * makronaredbu za ovo, ali 2.0.35 ne uključuje - pa je dodajem * ovdje ako je potrebno. */ #ifndef KERNEL_VERSION (a ,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include #endif /* Tablica poziva sustava (tablica funkcija). Mi * samo definiramo ovo kao eksterno, a kernel će * to popuniti za nas kada smo insmod"ed */ extern void *sys_call_table; /* UID koji želimo špijunirati - popunit će se iz * naredbenog retka */ int uid; #if LINUX_VERSION_CODE (2,2,0) MODULE_PARM(uid, "i"); #endif /* Pokazivač na izvorni sistemski poziv. * (sys_open), možda je netko drugi zamijenio sistemski poziv prije nas Imajte na umu da ovo * nije 100% sigurno, jer ako je neki drugi modul * zamijenio sys_open prije nas, tada ćemo kada umetnemo * pozvati funkciju u tom modulu - i ona * može biti uklonjena prije mi jesmo. * * Drugi razlog za to je taj što ne možemo dobiti sys_open. .. */ printk("Opasan sam. Nadam se da si napravio "); printk("sinkroniziraj prije nego si me insmodirao.\n");

Morž je o mnogim stvarima rekao "došlo je vrijeme za razgovor".
L. Carroll (Citat iz knjige B. Stroustrapa)

Umjesto uvoda.

Puno je napisano i prepisano o temi unutarnje strukture Linux kernela općenito, njegovih različitih podsustava i posebno sistemskih poziva. Vjerojatno bi svaki autor koji drži do sebe trebao barem jednom pisati o ovome, kao što bi svaki programer koji drži do sebe svakako trebao napisati svoj vlastiti upravitelj datoteka :) Iako nisam profesionalni IT pisac, i općenito, svoje bilješke pravim isključivo za prvo svega, kako ne biste prebrzo zaboravili ono što ste naučili. Ali, ako moje bilješke s putovanja budu nekome koristile, naravno, bit ću samo sretan. Pa općenito, uljem se kaša ne može pokvariti, pa možda i uspijem napisati ili opisati nešto što se nitko nije udostojio spomenuti.

Teorija. Što su sistemski pozivi?

Kada neupućenima objašnjavaju što je softver (ili OS), obično kažu sljedeće: računalo je samo po sebi dio hardvera, ali softver je ono što vam omogućuje da izvučete neku korist od tog dijela hardvera. Grubo, naravno, ali općenito, donekle istinito. Vjerojatno bih rekao isto za OS i sistemske pozive. Zapravo, u različitim OS-ima, sistemski pozivi mogu se implementirati drugačije, broj tih poziva može varirati, ali na ovaj ili onaj način, u ovom ili onom obliku, mehanizam sistemskog poziva prisutan je u bilo kojem OS-u. Svaki dan korisnik eksplicitno ili implicitno radi s datotekama. Naravno, on može jasno otvoriti datoteku za uređivanje u svom omiljenom MS Wordu ili Notepadu, ili može jednostavno pokrenuti igračku, čija je izvršna slika, usput rečeno, također pohranjena u datoteci, koja zauzvrat mora otvaraju i čitaju izvršne datoteke bootloadera. Zauzvrat, igračka također može otvarati i čitati desetke datoteka tijekom svog rada. Naravno, datoteke se ne mogu samo čitati, već i pisati (ne uvijek, ali ovdje ne govorimo o odvajanju prava i diskretnom pristupu :)). Svim ovim upravlja kernel (u mikrokernel operativnim sustavima situacija može biti drugačija, ali sada ćemo nenametljivo prijeći na predmet naše rasprave - Linux, pa ćemo ovu točku zanemariti). Sama izrada novog procesa također je usluga koju pruža jezgra OS-a. Sve je to sjajno, kao i činjenica da moderni procesori rade na frekvencijama u rasponu gigaherca i sastoje se od mnogo milijuna tranzistora, ali što dalje? Da, što ako ne postoji mehanizam pomoću kojeg korisničke aplikacije mogu obavljati neke prilično svakodnevne, a u isto vrijeme potrebne stvari ( zapravo, ove trivijalne radnje u svakom slučaju ne izvodi korisnička aplikacija, već OS kernel - autor.), onda je OS jednostavno bio stvar za sebe - apsolutno beskoristan, ili bi, naprotiv, svaka korisnička aplikacija morala sama postati operativni sustav kako bi samostalno služila svim svojim potrebama. Slatko, zar ne?

Tako smo došli do definicije sistemskog poziva u prvoj aproksimaciji: sistemski poziv je određena usluga koju jezgra OS-a pruža korisničkoj aplikaciji na zahtjev potonje. Takva usluga može biti već spomenuto otvaranje datoteke, njezino kreiranje, čitanje, pisanje, kreiranje novog procesa, dobivanje identifikatora procesa (pid), montiranje datotečnog sustava, zaustavljanje sustava, na kraju. U stvarnom životu postoji mnogo više sistemskih poziva nego što je ovdje navedeno.

Kako izgleda i što je sistemski poziv? Pa, iz onoga što je gore rečeno, postaje jasno da je sistemski poziv potprogram kernela koji ima odgovarajući oblik. Oni koji su imali iskustva s programiranjem pod Win9x/DOS-om vjerojatno se sjećaju int 0x21 prekida sa svim (ili barem nekim) od njegovih brojnih funkcija. Međutim, postoji jedna mala mana koja se odnosi na sve pozive Unix sustava. Prema konvenciji, funkcija koja implementira sistemski poziv može uzeti N argumenata ili niti jedan, ali na ovaj ili onaj način, funkcija mora vratiti int vrijednost. Svaka nenegativna vrijednost tumači se kao uspješno izvršenje funkcije sistemskog poziva, a time i samog sistemskog poziva. Vrijednost manja od nule je znak greške i istovremeno sadrži kod greške (kodovi grešaka definirani su u zaglavljima include/asm-generic/errno-base.h i include/asm-generic/errno.h) . U Linuxu je pristupnik za sistemske pozive donedavno bio prekid int 0x80, dok je u Windowsima (do verzije XP Service Pack 2, ako se ne varam) pristupnik prekid 0x2e. Opet, u jezgri Linuxa, donedavno je svim sistemskim pozivima upravljala funkcija system_call(). Međutim, kako se kasnije pokazalo, klasični mehanizam za obradu poziva sustava preko gatewaya 0x80 dovodi do značajnog pada performansi na procesorima Intel Pentium 4. Stoga je klasični mehanizam zamijenjen metodom virtualnih dinamičkih zajedničkih objekata (DSO - dynamic datoteka zajedničkog objekta Ne mogu jamčiti za točan prijevod, ali DSO je ono što korisnici Windowsa znaju kao DLL - dinamički učitana i povezana biblioteka) - VDSO. Koja je razlika između nove metode i klasične? Prvo, pogledajmo klasičnu metodu, koja radi kroz vrata 0x80.

Klasični mehanizam za servisiranje sistemskih poziva u Linuxu.

Prekidi u x86 arhitekturi.

Kao što je gore spomenuto, prethodno se pristupnik 0x80 (int 0x80) koristio za servisiranje zahtjeva korisničkih aplikacija. Rad sustava temeljenog na IA-32 arhitekturi kontroliran je prekidima (strogo govoreći, to se općenito odnosi na sve sustave temeljene na x86). Kada se dogodi neki događaj (novi otkucaj timera, neka aktivnost na nekom uređaju, greške - dijeljenje s nulom itd.), generira se prekid. Prekid je tako nazvan jer obično prekida normalan tok koda. Prekidi se obično dijele na hardverske i softverske prekide. Hardverski prekidi su prekidi koje generiraju sustav i periferni uređaji. Kada uređaj treba privući pozornost jezgre OS-a, on (uređaj) generira signal na svojoj liniji zahtjeva za prekid (IRQ - Interrupt ReQuest line). To dovodi do toga da se na određenim ulazima procesora generira odgovarajući signal na temelju kojeg procesor odlučuje prekinuti izvođenje toka instrukcija i prenijeti kontrolu na rukovatelja prekida koji već saznaje što se dogodilo i što treba biti učinjeno. Hardverski prekidi su po prirodi asinkroni. To znači da do prekida može doći bilo kada. Osim perifernih uređaja, i sam procesor može generirati prekide (ili, točnije, hardverske iznimke – Hardware Exceptions – npr. već spomenuto dijeljenje s nulom). Ovo se radi kako bi se OS obavijestio da je došlo do nenormalne situacije kako bi OS mogao poduzeti neke radnje kao odgovor na pojavu takve situacije. Nakon obrade prekida, procesor se vraća na izvršavanje prekinutog programa. Prekid može pokrenuti korisnička aplikacija. Ova vrsta prekida naziva se softverski prekid. Programski su prekidi, za razliku od hardverskih, sinkroni. To jest, kada se pozove prekid, kod koji ga je pozvao pauzira dok se prekid ne servisira. Prilikom izlaska iz rukovatelja prekidom, vraća se na najdalju adresu pohranjenu ranije (kada je prekid pozvan) na stogu, na sljedeću instrukciju nakon instrukcije koja poziva prekid (int). Rukovatelj prekidom je rezidentni (trajno smješten u memoriji) dio koda. U pravilu, ovo je mali program. Iako, ako govorimo o Linux kernelu, tada rukovatelj prekidima nije uvijek tako mali. Rukovatelj prekidom definiran je vektorom. Vektor nije ništa drugo nego adresa (segment i pomak) početka koda koji bi trebao obraditi prekide na danom indeksu. Rad s prekidima bitno se razlikuje u stvarnom (Real Mode) i zaštićenom (Protected Mode) načinu rada procesora (podsjećam da se u nastavku misli na Intelove procesore i njima kompatibilne). U stvarnom (nezaštićenom) načinu rada procesora rukovatelji prekidima određeni su svojim vektorima koji su uvijek pohranjeni na početku memorije; tražena adresa se bira iz vektorske tablice pomoću indeksa koji je ujedno i broj prekida. Prepisivanjem vektora s određenim indeksom, možete dodijeliti vlastiti rukovatelj prekidu.

U zaštićenom načinu rada rukovatelji prekidima (vrata, vrata ili vrata) više se ne definiraju korištenjem vektorske tablice. Umjesto ove tablice koristi se gate table ili točnije tablica prekida - IDT (Interrupt Descriptors Table). Ovu tablicu generira kernel, a njena adresa je pohranjena u procesorskom idtr registru. Ovaj registar nije izravno dostupan. Rad s njim moguć je samo korištenjem lidt/sidt uputa. Prvi od njih (lidt) učitava idtr registar s vrijednošću navedenom u operandu i osnovna je adresa tablice deskriptora prekida, drugi (sidt) pohranjuje adresu tablice koja se nalazi u idtr u navedeni operand. Na isti način na koji se informacije o segmentu dohvaćaju iz tablice deskriptora pomoću selektora, dohvaća se i deskriptor segmenta koji služi prekidu u zaštićenom načinu rada. Zaštitu memorije podržavaju Intelovi procesori počevši od CPU-a i80286 (ne baš u obliku u kojem je sada predstavljen, barem zato što je 286 bio 16-bitni procesor - tako da Linux ne može raditi na tim procesorima) i i80386, i stoga procesor sam vrši sve potrebne odabire i, stoga, nećemo ulaziti duboko u sve zamršenosti zaštićenog načina rada (naime, Linux radi u zaštićenom načinu rada). Nažalost, ni vrijeme ni mogućnosti ne dopuštaju nam da se dugo zadržavamo na mehanizmu za rukovanje prekidima u zaštićenom načinu rada. Da, to nije bio cilj pisanja ovog članka. Sve ovdje navedene informacije o radu procesora obitelji x86 prilično su površne i dane su samo da bi se malo bolje razumio mehanizam funkcioniranja sistemskih poziva jezgre. Neke se stvari mogu naučiti izravno iz koda kernela, iako je za potpuno razumijevanje što se događa ipak preporučljivo upoznati se s načelima zaštićenog načina rada. Dio koda koji inicijalizira (ali ne postavlja!) IDT nalazi se u arch/i386/kernel/head.S: /* * setup_idt * * postavlja idt s 256 unosa koji pokazuju na * ignore_int, vrata prekida. Zapravo ne učitava * idt - to se može učiniti samo nakon što je straničenje omogućeno * i kernel premješten na PAGE_OFFSET. Prekidi * su omogućeni negdje drugdje, kada možemo biti relativno * sigurni da je sve u redu. * * Upozorenje: %esi aktivan je preko ove funkcije */ 1.setup_idt: 2. lea ignore_int,%edx 3. movl $(__KERNEL_CS.<< 16),%eax 4. movw %dx,%ax /* selector = 0x0010 = cs */ 5. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 6. lea idt_table,%edi 7. mov $256,%ecx 8.rp_sidt: 9. movl %eax,(%edi) 10. movl %edx,4(%edi) 11. addl $8,%edi 12. dec %ecx 13. jne rp_sidt 14..macro set_early_handler handler,trapno 15. lea \handler,%edx 16. movl $(__KERNEL_CS << 16),%eax 17. movw %dx,%ax 18. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 19. lea idt_table,%edi 20. movl %eax,8*\trapno(%edi) 21. movl %edx,8*\trapno+4(%edi) 22..endm 23. set_early_handler handler=early_divide_err,trapno=0 24. set_early_handler handler=early_illegal_opcode,trapno=6 25. set_early_handler handler=early_protection_fault,trapno=13 26. set_early_handler handler=early_page_fault,trapno=14 28. ret Nekoliko napomena o kodu: gornji kod je napisan u verziji AT&T asemblera, tako da vaše poznavanje asemblera u njegovoj uobičajenoj Intel notaciji može biti samo zbunjujuće. Najvažnija razlika je redoslijed operanda. Ako je redoslijed definiran za Intelov zapis - “akumulator”< "источник", то для ассемблера AT&T порядок прямой. Регистры процессора, как правило, должны иметь префикс "%", непосредственные значения (константы) префиксируются символом доллара "$". Синтаксис AT&T традиционно используется в Un*x-системах.

U gornjem primjeru, linije 2-4 postavljaju zadanu adresu svih rukovatelja prekidima. Zadani rukovatelj je funkcija ignore_int, koja ne radi ništa. Prisutnost takvog stuba je neophodna za ispravnu obradu svih prekida u ovoj fazi, budući da jednostavno još nema drugih (međutim, zamke su instalirane malo niže u kodu - za zamke pogledajte Intel Architecture Manual Reference ili nešto slično slične, nećemo o njima raspravljati ovdje dodirujte zamke). Linija 5 postavlja tip vrata. U retku 6 učitavamo registar indeksa s adresom naše IDT tablice. Tablica treba sadržavati 255 zapisa, svaki od 8 bajtova. U redovima 8-13 popunjavamo cijelu tablicu s istim vrijednostima koje su ranije postavljene u eax i edx registrima - tj., ovo su vrata prekida koja referenciraju rukovatelj ignore_int. Odmah ispod definiramo makronaredbu za postavljanje zamki - linije 14-22. U redovima 23-26, koristeći gornju makronaredbu, postavljamo zamke za sljedeće iznimke: early_divide_err - dijeljenje s nulom (0), early_illegal_opcode - nepoznata instrukcija procesora (6), early_protection_fault - greška zaštite memorije (13), early_page_fault - prijevod stranice kvar (14) . Brojevi "prekida" generiranih kada se dogodi odgovarajuća abnormalna situacija navedeni su u zagradama. Prije provjere tipa procesora u arch/i386/kernel/head.S, IDT tablica se postavlja pozivom setup_idt: /* * pokreni 32-bitno postavljanje sustava. Moramo ponoviti neke stvari učinjene * u 16-bitnom načinu za "prave" operacije.*/ 1. poziv setup_idt ... 2. poziv check_x87 3. lgdt early_gdt_descr 4. lidt idt_descr

Nakon što saznamo tip (ko)procesora i izvršimo sve pripremne korake u redovima 3 i 4, učitavamo GDT i IDT tablice koje će se koristiti tijekom prvih faza kernela.

Od prekida, vratimo se na sistemske pozive. Dakle, što je potrebno za posluživanje procesa koji zahtijeva neku uslugu? Za početak, trebate prijeći iz prstena 3 (razina privilegije CPL=3) na najpovlašteniju razinu 0 (Prsten 0, CPL=0), jer Kod kernela nalazi se u segmentu s najvišim privilegijama. Osim toga, potreban je kod rukovatelja koji će opsluživati ​​proces. To je upravo ono za što se koristi 0x80 gateway. Iako postoji dosta sistemskih poziva, svi koriste jednu ulaznu točku - int 0x80. Sam rukovatelj se instalira prilikom poziva funkcije arch/i386/kernel/traps.c::trap_init(): void __init trap_init(void) ( ... set_system_gate(SYSCALL_VECTOR,&system_call); ... ) Najviše nas zanima ovaj redak u trap_init(). U istoj gornjoj datoteci možete pogledati kod za funkciju set_system_gate(): static void __init set_system_gate(unsigned int n, void *addr) ( _set_gate(n, DESCTYPE_TRAP | DESCTYPE_DPL3, addr, __KERNEL_CS); ) Ovdje možete vidjeti da je gate za prekid 0x80 (naime, ova vrijednost je definirana makroom SYSCALL_VECTOR - možete mi vjerovati na riječ :)) instaliran kao trap s razinom privilegije DPL=3 (Ring 3), tj. ovaj prekid će biti uhvaćen kada se pozove iz korisničkog prostora. Problem s prijelazom iz prstena 3 u prsten 0, tj. riješena. Funkcija _set_gate() definirana je u datoteci zaglavlja include/asm-i386/desc.h. Za one koji su posebno znatiželjni, kod je dat ispod, ali bez dugih objašnjenja: static inline void _set_gate(int gate, unsigned int type, void *addr, unsigned short seg) ( __u32 a, b; pack_gate(&a, &b, (unsigned long)addr, seg, type, 0); write_idt_entry(idt_table, gate) , a, b) Vratimo se na funkciju trap_init(). Poziva se iz funkcije start_kernel() u init/main.c. Ako pogledate kod trap_init(), možete vidjeti da ova funkcija iznova prepisuje neke vrijednosti IDT tablice - rukovatelji koji su korišteni u ranim fazama inicijalizacije kernela (early_page_fault, early_divide_err, early_illegal_opcode, early_protection_fault) su zamijenjeni s onima koji će se koristiti već tijekom rada jezgre procesa. Dakle, skoro smo došli do točke i već znamo da se svi sistemski pozivi obrađuju uniformno - kroz int 0x80 gateway. Funkcija system_call() instalirana je kao rukovatelj za int 0x80, kao što se opet može vidjeti iz gornjeg dijela koda arch/i386/kernel/traps.c::trap_init().

sistemski_poziv().

Kod za funkciju system_call() nalazi se u datoteci arch/i386/kernel/entry.S i izgleda ovako: # stub rukovatelja sistemskim pozivom ENTRY(system_call) RING0_INT_FRAME # ionako se ne može odmotati u korisnički prostor pushl %eax # spremi orig_eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO(%ebp) # praćenje sistemskog poziva u radu / emulacija /* Napomena, _TIF_SECCOMP je bit broj 8 , pa mu je potreban testw, a ne testb */ testw $(_TIF_SYSCALL_EMU|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_syscalls), %eax jae syscall_badsys syscall_call: poziv *sys _call_ tablica(,%eax , 4) movl %eax,PT_EAX(%esp) # pohranjivanje povratne vrijednosti ... Kod nije prikazan u cijelosti. Kao što možete vidjeti, prvo system_call() konfigurira stog za rad u prstenu 0, sprema vrijednost koja mu je proslijeđena preko eax-a na stog, sprema sve registre na stog, prima podatke o pozivnoj niti i provjerava je li proslijeđena vrijednost, broj sistemskog poziva, nije izvan granica tablice sistemskih poziva i onda konačno, korištenjem vrijednosti proslijeđene eax-u kao argumenta, system_call() skače na stvarni sistemski rukovatelj izlazom na temelju elementa tablice na koji se referira indeks u eax. Sjetite se sada dobre stare tablice vektora prekida iz stvarnog načina rada. Ne podsjeća te ni na što? U stvarnosti je, naravno, sve nešto kompliciranije. Konkretno, sistemski poziv mora kopirati rezultate s kernel stoga na korisnički stog, proslijediti povratni kod i neke druge stvari. U slučaju kada se argument naveden u eax ne odnosi na postojeći sistemski poziv (vrijednost je izvan raspona), dolazi do prijelaza na oznaku syscall_badsys. Ovdje se vrijednost -ENOSYS gura na stog na pomaku na kojem bi se trebala nalaziti eax vrijednost - sistemski poziv nije implementiran. Time je izvršenje system_call().

Tablica sistemskih poziva nalazi se u datoteci arch/i386/kernel/syscall_table.S i ima prilično jednostavan oblik: ENTRY(sys_call_table) .long sys_restart_syscall /* 0 - stari "setup()" sistemski poziv, koji se koristi za ponovno pokretanje */ .long sys_exit .long sys_fork .long sys_read .long sys_write .long sys_open /* 5 */ .long sys_close .long sys_waitpid .long sys_creat ... Drugim riječima, cijela tablica nije ništa više od niza adresa funkcija raspoređenih prema redoslijedu brojeva poziva sustava koje te funkcije služe. Tablica je običan niz dvostrukih strojnih riječi (ili 32-bitnih riječi - kako želite). Kod za neke od funkcija koje opslužuju sistemske pozive nalazi se u dijelu koji ovisi o platformi - arch/i386/kernel/sys_i386.c, i dijelu koji ne ovisi o platformi - u kernel/sys.c.

Ovo je slučaj sa sistemskim pozivima i vratima 0x80.

Novi mehanizam za rukovanje sistemskim pozivima u Linuxu. sysenter/sysexit.

Kao što je spomenuto, brzo je postalo jasno da korištenje tradicionalne metode obrade sistemskih poziva temeljene na vratima 0x80 dovodi do gubitka performansi na procesorima Intel Pentium 4. Stoga je Linus Torvalds implementirao novi mehanizam u kernelu, temeljen na sysenter/sysexit. upute i dizajniran za povećanje performansi jezgre na strojevima, opremljenim Pentium II procesorom i novijim (Intel procesori podržavaju spomenute sysenter/sysexit instrukcije s Pentium II+). Što je bit novog mehanizma? Čudno, ali suština ostaje ista. Izvršenje je promijenjeno. Prema Intelovoj dokumentaciji, sysenter instrukcija je dio mehanizma "brzog sistemskog poziva". Konkretno, ova je uputa optimizirana za brzo premještanje s jedne razine privilegija na drugu. Točnije, ubrzava prijelaz na prsten 0 (Ring 0, CPL=0). U tom slučaju operativni sustav mora pripremiti procesor za korištenje instrukcije sysenter. Ova se postavka provodi jednom prilikom učitavanja i pokretanja jezgre OS-a. Kada se pozove, sysenter postavlja registre procesora prema registrima specifičnim za stroj koje je prethodno postavio OS. Konkretno, instalirani su segmentni registar i registar pokazivača instrukcija - cs:eip, te segment steka i pokazivač na vrh steka - ss, esp. Prijelaz na novi segment koda i pomak se provodi od prstena 3 do 0.

Uputa sysexit radi suprotno. Brzi je prijelaz s razine povlastice 0 na razinu povlastice 3 (CPL=3). U ovom slučaju, registar segmenta koda postavljen je na 16 + vrijednost segmenta cs pohranjena u registru ovisnom o stroju procesora. Eip registar sadrži sadržaj edx registra. Zbroj 24 i cs vrijednosti unesenih u ss OS je prethodno unio u strojno ovisan registar procesora prilikom pripreme konteksta za rad sysenter instrukcije. Sadržaj ecx registra unosi se u esp. Vrijednosti potrebne za rad sysenter/sysexit instrukcija pohranjene su na sljedećim adresama:

  1. SYSENTER_CS_MSR 0x174 - segment koda u koji se upisuje vrijednost segmenta u kojem se nalazi kod rukovatelja sistemskim pozivom.
  2. SYSENTER_ESP_MSR 0x175 - pokazivač na vrh stoga za rukovatelja sistemskim pozivima.
  3. SYSENTER_EIP_MSR 0x176 - pokazivač na pomak unutar segmenta koda. Pokazuje na početak koda rukovatelja pozivima sustava.
Ove adrese se odnose na registre ovisne o modelu koji nemaju imena. Vrijednosti se zapisuju u registre ovisne o modelu korištenjem instrukcije wrmsr, dok edx:eax mora sadržavati vodeći i niži dio 64-bitne strojne riječi, respektivno, a ecx mora sadržavati adresu registra u koji će unos biti napravljen. U Linuxu, adrese registara ovisnih o modelu definirane su u datoteci zaglavlja include/asm-i368/msr-index.h na sljedeći način (prije verzije 2.6.22, barem su bile definirane u datoteci zaglavlja include/asm-i386 /msr.h, dopustite mi da vas podsjetim da mehanizam poziva sustava razmatramo na primjeru Linux kernela 2.6.22): #define MSR_IA32_SYSENTER_CS 0x00000174 #define MSR_IA32_SYSENTER_ESP 0x00000175 #define MSR_IA32_SYSENTER_EIP 0x00000176 Kod kernela odgovoran za postavljanje registara ovisnih o modelu nalazi se u datoteci arch/i386/sysenter.c i izgleda ovako: 1. void enable_sep_cpu(void) ( 2. int cpu = get_cpu(); 3. struct tss_struct *tss = &per_cpu(init_tss, cpu); 4. if (!boot_cpu_has(X86_FEATURE_SEP)) ( 5. put_cpu(); 6. return; ) tss->x86_tss.ss1 = __KERNEL_CS; 8. tss->x86_tss.esp1 = sizeof(struct tss_struct) tss; MSR_IA32_SYSENTER_ESP, tss->x86_tss.esp1, 0); (MSR_IA32_SYSENTER_EIP, (unsigned long) sysenter_entry, 0); Ovdje u varijabli tss dobivamo adresu strukture koja opisuje segment stanja zadatka. TSS (Task State Segment) koristi se za opisivanje konteksta zadatka i dio je hardverskog mehanizma multitaskinga za x86 arhitekturu. Međutim, Linux praktički ne koristi prebacivanje konteksta hardverskih zadataka. Prema Intelovoj dokumentaciji, prebacivanje na drugi zadatak postiže se ili izvršavanjem instrukcije za međusegmentni skok (jmp ili poziv) koja se odnosi na TSS segment ili izvršavanjem deskriptora vrata zadatka u GDT (LDT). Poseban procesorski registar, programeru nevidljiv - TR (Task Register) sadrži selektor deskriptora zadatka. Učitavanje ovog registra također učitava softverski nevidljive osnovne i granične registre povezane s TR.

Iako Linux ne koristi hardversko prebacivanje konteksta, kernel je prisiljen dodijeliti TSS unos za svaki procesor instaliran na sustavu. To je zato što kada se procesor prebaci iz korisničkog načina rada u način rada jezgre, on dohvaća adresu steka jezgre iz TSS-a. Osim toga, TSS je potreban za kontrolu pristupa I/O portovima. TSS sadrži mapu prava pristupa portu. Na temelju ove karte, postaje moguće kontrolirati pristup priključcima za svaki proces koristeći in/out instrukcije. Ovdje tss->x86_tss.esp1 pokazuje na stog kernela. __KERNEL_CS prirodno ukazuje na segment koda jezgre. Offset-eip je adresa funkcije sysenter_entry().

Funkcija sysenter_entry() definirana je u datoteci arch/i386/kernel/entry.S i izgleda ovako: /* SYSENTER_RETURN pokazuje nakon instrukcije "sysenter" na stranici vsyscall. Pogledajte vsyscall-sysentry.S, koji definira simbol. */ # SYSENTER ULAZNIKA PUTNICA (SYSENTER_ENTRY) CFI_STARTPROC Jednostavno CFI_SIGNAL_FRAME CFI_DEF_CFA ESP, 0 CFI_REGISTER ESP, EBP MOVL TSSSSENTER_SSCALL (%ESPQUSENS,/%OFFQNEST_PAST_PAST_PAST_PAST_PAST_PAST_PAST_PAST_PAST_PAST_PASTA onemogućeni irqs i ovdje ga omogućujemo odmah nakon unosa: */ ENABLE_INTERRUPTS(CLBR_NONE) pushl $(__USER_DS) CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET ss, 0*/ pushl %ebp CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET esp, 0 pushfl CFI_ADJUST_CFA_OFFSET 4 pushl $(__USER_CS) CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET cs, 0*/ /* * Gurni current_thread_info()->sysenter_return na stog. Kao i kod system_call(), većina posla se obavlja u pozivu linije *sys_call_table(,%eax,4). Ovdje se poziva određeni sistemski rukovatelj pozivom. Dakle, jasno je da se malo toga bitno promijenilo. Činjenica da je vektor prekida sada ugrađen u hardver i procesor pomaže nam da se brže krećemo s jedne razine privilegija na drugu mijenja samo neke detalje izvršenja s istim sadržajem. Istina, promjenama tu nije kraj. Prisjetite se kako je priča počela. Već sam na samom početku spomenuo virtualne zajedničke objekte. Dakle, ako je prije implementacija sistemskog poziva, recimo, iz knjižnice sustava libc izgledala kao prekidni poziv (unatoč činjenici da je biblioteka preuzela neke funkcije kako bi smanjila broj preklopa konteksta), sada zahvaljujući VDSO poziv sustava može se napraviti gotovo izravno, bez sudjelovanja libc-a. Mogao se provesti izravno prije, opet, kao prekid. Ali sada se poziv može zatražiti kao redovita funkcija izvezena iz dinamički povezane biblioteke (DSO). Pri dizanju, kernel određuje koji se mehanizam treba i može koristiti za određenu platformu. Ovisno o okolnostima, kernel postavlja ulaznu točku na funkciju koja izvršava sistemski poziv. Zatim se funkcija izvozi u korisnički prostor kao biblioteka linux-gate.so.1. Knjižnica linux-gate.so.1 fizički ne postoji na disku. Njega, da tako kažemo, emulira kernel i postoji točno onoliko dugo koliko je sustav pokrenut. Ako zaustavite sustav i montirate korijenski datotečni sustav s drugog sustava, nećete pronaći ovu datoteku u korijenskom datotečnom sustavu zaustavljenog sustava. Zapravo, nećete ga moći pronaći čak ni na pokrenutom sustavu. Fizički jednostavno ne postoji. Zbog toga je linux-gate.so.1 nešto drugo od VDSO - tj. Virtualni dinamički dijeljeni objekt. Kernel preslikava ovako emuliranu dinamičku biblioteku u adresni prostor svakog procesa. To možete lako provjeriti pokretanjem sljedeće naredbe: f0x@devel0:~$ cat /proc/self/maps 08048000-0804c000 r-xp 00000000 08:01 46 /bin/cat 0804c000-0804d000 rw-p 00003000 08:01 46 /bin/cat 0804d000-080 6e0 00 rw-p 0804d000 00:00 0 ... b7fdf000-b7fe1000 rw-p 00019000 08:01 2066 /lib/ld-2.5.so bffd2000-bffe8000 rw-p bffd2000 00:00 0 ffffe000-ffffff000 r-xp 000 00000 00 :00 0 Ovdje je posljednji redak objekt koji nas zanima: ffffe000-fffff000 r-xp 00000000 00:00 0 Iz gornjeg primjera jasno je da objekt zauzima točno jednu stranicu u memoriji - 4096 bajtova, gotovo na začelju adresnog prostora. Napravimo još jedan eksperiment: f0x@devel0:~$ ldd `koja mačka` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e87000) /lib/ ld-linux.so.2 (0xb7fdf000) f0x@devel0:~$ ldd `koji gcc` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc .so.6 (0xb7e3c000) /lib/ld-linux.so.2 (0xb7f94000) f0x@devel0:~$ Ovdje smo upravo uzeli dvije prijave. Može se vidjeti da je biblioteka preslikana u adresni prostor procesa na istoj stalnoj adresi - 0xffffe000. Pokušajmo sada vidjeti što je zapravo pohranjeno na ovoj memorijskoj stranici...

Možete ispisati memorijsku stranicu na kojoj je pohranjen zajednički VDSO kod pomoću sljedećeg programa: #include #include #include int main () ( char* vdso = 0xffffe000; char* buffer; FILE* f; buffer = malloc (4096); if (!buffer) exit (1); memcpy (buffer, vdso, 4096) ; if (!(f = fopen", "w+b") ( free (buffer); exit (1); ) fwrite (buffer, 1, f); ; free (buffer ); povratak 0; Strogo govoreći, ranije se to moglo učiniti lakše, korištenjem naredbe dd if=/proc/self/mem of=test.dump bs=4096 skip=1048574 count=1, ali kerneli od verzije 2.6.22, ili možda čak i ranije, više ne mapiraju memoriju procesa u /proc/`pid`/mem. Ova je datoteka očito spremljena radi kompatibilnosti, ali ne sadrži više informacija.

Prevedimo i pokrenimo zadani program. Pokušajmo rastaviti rezultirajući kod: f0x@devel0:~/tmp$ objdump --disassemble ./test.dump ./test.dump: format datoteke elf32-i386 Rastavljanje odjeljka .text: ffffe400<__kernel_vsyscall>: ffffe400: 51 push %ecx ffffe401: 52 push %edx ffffe402: 55 push %ebp ffffe403: 89 e5 mov %esp,%ebp ffffe405: 0f 34 sysenter ... ffffe40e: eb f3 jmp ffffe403<__kernel_vsyscall+0x3>ffffe410: 5d pop %ebp ffffe411: 5a pop %edx ffffe412: 59 pop %ecx ffffe413: c3 ret ... f0x@devel0:~/tmp$ Ovdje je naš pristupnik za sistemske pozive, sve u punom prikazu. Proces (ili biblioteka sustava libc) koji poziva funkciju __kernel_vsyscall završava na adresi 0xffffe400 (u našem slučaju). Zatim, __kernel_vsyscall sprema sadržaj registara ecx, edx, ebp na stog korisničkih procesa Već smo govorili o svrsi registara ecx i edx ranije; u ebp-u se kasnije koristi za vraćanje korisničkog stoga. Izvršava se instrukcija sysenter, "presretanje prekida" i, kao rezultat, sljedeći prijelaz na sysenter_entry (vidi gore). Instrukcija jmp na 0xffffe40e umetnuta je za ponovno pokretanje sistemskog poziva sa 6 argumenata (pogledajte http://lkml.org/lkml/2002/12/18/). Kod postavljen na stranicu nalazi se u datoteci arch/i386/kernel/vsyscall-enter.S (ili arch/i386/kernel/vsyscall-int80.S za kuku 0x80). Iako sam otkrio da je adresa funkcije __kernel_vsyscall konstantna, postoji mišljenje da to nije slučaj. Obično se položaj ulazne točke u __kernel_vsyscall() može pronaći iz ELF-auxv vektora pomoću parametra AT_SYSINFO. ELF-auxv vektor sadrži informacije proslijeđene procesu preko stoga pri pokretanju i sadrži razne informacije potrebne dok program radi. Ovaj vektor posebno sadrži varijable okruženja procesa, argumente itd.

Evo malog primjera u C-u kako možete izravno pozvati funkciju __kernel_vsyscall: #uključi int pid; int main () ( __asm ​​​​("movl $20, %eax \n" "poziv *%gs:0x10 \n" "movl %eax, pid \n"); printf ("pid: %d\n", pid) ; povratak 0; Ovaj primjer je preuzet sa stranice Manu Garg, http://www.manugarg.com. Dakle, u gornjem primjeru, upućujemo sistemski poziv getpid() (broj 20 ili inače __NR_getpid). Kako se ne bismo penjali po procesnom stogu u potrazi za varijablom AT_SYSINFO, iskoristit ćemo činjenicu da knjižnica sustava libc.so kopira vrijednost varijable AT_SYSINFO u blok kontrole niti (TCB) kada se učita. Na ovaj blok informacija obično upućuje selektor u gs. Pretpostavljamo da se željeni parametar nalazi na pomaku 0x10 i upućujemo poziv na adresu pohranjenu u %gs:$0x10.

Rezultati.

Zapravo, u praksi nije uvijek moguće postići značajno povećanje performansi čak ni uz podršku za FSCF (Fast System Call Facility) na ovoj platformi. Problem je u tome što, na ovaj ili onaj način, proces rijetko pristupa kernelu izravno. I za to postoje dobri razlozi. Korištenje knjižnice libc omogućuje vam jamčenje prenosivosti programa bez obzira na verziju kernela. Većina sistemskih poziva prolazi kroz standardnu ​​sistemsku biblioteku. Čak i ako kompajlirate i instalirate najnoviji kernel kompiliran za platformu koja podržava FSCF, to nije jamstvo poboljšanja performansi. Činjenica je da će vaša sistemska biblioteka libc.so nastaviti koristiti int 0x80 i s tim se možete nositi samo ponovnom izgradnjom glibc-a. Da li su VDSO sučelje i __kernel_vsyscall općenito podržani u glibc-u, trenutno mi je iskreno teško odgovoriti.

Linkovi.

Manu Gargova stranica, http://www.manugarg.com
Scatter/Gather thoughts Johan Petersson, http://www.trilithium.com/johan/2005/08/linux-gate/
Dobri stari Razumijevanje Linux kernela Gdje bismo bili bez njega :)
I naravno, Linux izvorni kodovi (2.6.22)