Maelezo ya Linux ya simu za mfumo wa kernel. Mizani ya mtu (2): Simu za mfumo wa Linux. Nadharia. Simu za mfumo ni nini


Simu za mfumo

Kufikia sasa, programu zote ambazo tumetengeneza zimelazimika kutumia njia zilizoainishwa vizuri za kusajili faili za /proc na viendesha kifaa. Hii ni nzuri ikiwa unataka kufanya kitu ambacho tayari kimetolewa na watengenezaji wa programu za kernel, kama kuandika kiendeshi cha kifaa. Lakini vipi ikiwa unataka kufanya kitu cha kupendeza, ubadilishe tabia ya mfumo kwa njia fulani?

Hapa ndipo programu ya kernel inakuwa hatari. Wakati wa kuandika mfano hapa chini, nimeharibu simu ya mfumo wazi. Hii ilimaanisha kuwa siwezi kufungua faili zozote, siwezi kuendesha programu zozote, na siwezi kuzima mfumo na amri ya kuzima. Lazima nizime nguvu ya kuizuia. Kwa bahati nzuri, hakuna faili zilizoharibiwa. Ili kuhakikisha kuwa haupotezi faili zozote, tafadhali fanya usawazishaji kabla ya kutoa amri za insmod na rmmod.

Sahau kuhusu/proc faili na faili za kifaa. Ni maelezo madogo tu. Mchakato halisi wa mawasiliano wa kernel unaotumiwa na michakato yote ni simu za mfumo. Wakati mchakato unaomba huduma kutoka kwa kernel (kama vile kufungua faili, kuanzisha mchakato mpya, au kuomba kumbukumbu zaidi), utaratibu huu hutumiwa. Ikiwa unataka kubadilisha tabia ya kernel kwa njia za kuvutia, hii ndiyo mahali pazuri. Kwa njia, ikiwa unataka kuona ni mfumo gani unaita programu imetumia, endesha: strace .

Kwa ujumla, mchakato hauwezi kupata kernel. Haiwezi kufikia kumbukumbu ya kernel na haiwezi kuita vitendaji vya kernel. Vifaa vya CPU huamuru hali hii ya mambo (inaitwa `hali iliyolindwa' kwa sababu fulani). Simu za mfumo ni ubaguzi kwa sheria hii ya jumla. Mchakato huo unajaza rejista na maadili yanayofaa na kisha kuita maagizo maalum ambayo yanaruka kwa a. Mahali palipofafanuliwa kwenye kernel (bila shaka , inasomwa na michakato ya mtumiaji, lakini haijaandikwa tena nao.) Chini ya Intel CPUs, hii inafanywa kupitia kukatizwa kwa 0x80. Vifaa vinajua kwamba mara tu unaporuka kwenye eneo hili, hauendeshi tena. katika hali ya mtumiaji iliyowekewa vikwazo.Badala yake, unaendesha kama kernel ya mfumo wa uendeshaji, na kwa hivyo unaruhusiwa kufanya chochote unachotaka kufanya.

Mahali kwenye kernel ambayo mchakato unaweza kuruka inaitwa system_call . Utaratibu unaokaa hapo huangalia nambari ya simu ya mfumo, ambayo huambia kernel kile mchakato unataka. Halafu, hutafuta jedwali la simu la mfumo (sys_call_table) kupata anwani ya kitendakazi cha kernel kupiga simu. Kisha kazi inayotakiwa inaitwa, na baada ya kurudisha thamani, hundi kadhaa hufanywa kwenye mfumo. Matokeo yake hurejeshwa kwenye mchakato (au kwa mchakato mwingine ikiwa mchakato umekwisha). Ikiwa unataka kuona nambari inayofanya haya yote, iko kwenye faili ya chanzo arch/< architecture >/kernel/entry.S , baada ya mstari wa ENTRY(system_call).

Kwa hivyo, ikiwa tunataka kubadilisha jinsi simu fulani ya mfumo inavyofanya kazi, jambo la kwanza tunalopaswa kufanya ni kuandika kazi yetu wenyewe ili kufanya jambo linalofaa (kawaida kuongeza baadhi ya msimbo wetu na kisha kupiga kazi ya awali), kisha kubadilisha pointer. kwa sys_call_table kuelekeza kazi yetu. Kwa kuwa tunaweza kufutwa baadaye na hatutaki kuacha mfumo katika hali tete, ni muhimu kwa cleanup_module ili kurejesha jedwali katika hali yake ya asili.

Nambari ya chanzo iliyotolewa hapa ni mfano wa moduli kama hiyo. Tunataka "kupeleleza" kwa baadhi ya mtumiaji, na kutuma ujumbe kupitia printk kila mtumiaji huyo anapofungua faili. Tunabadilisha simu ya mfumo wa kufungua faili na chaguo zetu wenyewe zinazoitwa our_sys_open . Chaguo hili la kukokotoa hukagua uid (kitambulisho cha mtumiaji) cha mchakato wa sasa, na ikiwa ni sawa na uid tunayopeleleza, huita printk ili kuonyesha jina la faili itakayofunguliwa. Kisha huita kazi ya awali ya wazi na vigezo sawa, kwa kweli kufungua faili.

Kitendaji cha init_module kinabadilisha eneo linalofaa katika sys_call_table na kuhifadhi kiashirio asili katika kigezo. Kitendaji cha cleanup_module kinatumia kigezo hiki kurejesha kila kitu kuwa kawaida. Njia hii ni hatari, kutokana na uwezekano wa modules mbili kubadilisha simu ya mfumo sawa. Fikiria kuwa tuna moduli mbili, A na B. Simu ya mfumo wazi ya Moduli A itaitwa A_open, na simu sawa ya moduli B itaitwa B_open. Sasa kwa kuwa syscall ya kernel iliyodungwa imebadilishwa na A_open, ambayo itaita sys_open ya asili itakapokamilika kile inachohitaji kufanya. Kisha, B itaingiza kwenye kernel, na kuchukua nafasi ya simu ya mfumo na B_open, ambayo itaomba kile inachofikiria kuwa simu asili ya mfumo, lakini kwa kweli ni A_wazi.

Sasa, ikiwa B itaondolewa kwanza, kila kitu kitakuwa sawa: hii itarejesha tu simu ya mfumo kwenye A_open inayoita asili. Walakini, ikiwa A itaondolewa na kisha B kuondolewa, mfumo utaanguka. Kuondoa A kutarejesha simu ya mfumo kwa ya awali, sys_open, kukata B nje ya kitanzi. Kisha, B inapoondolewa, itarejesha simu ya mfumo kwa kile inachofikiri ni ya awali.Kwa kweli, simu itaelekezwa kwa A_open, ambayo haipo tena kwenye kumbukumbu. Kwa mtazamo wa kwanza, inaonekana kama tunaweza kutatua shida hii kwa kuangalia ikiwa syscall ni sawa na kazi yetu wazi na, ikiwa ni hivyo, bila kubadilisha thamani ya simu hiyo (ili B isibadilishe syscall inapoondolewa), lakini hiyo bado ingeita shida mbaya zaidi. Wakati A inapoondolewa, inaona kuwa simu ya mfumo imebadilishwa kuwa B_open ili isielekeze tena kwa A_kufungua, kwa hivyo haitarejesha pointer sys_open kabla ya kuondolewa kwenye kumbukumbu. Kwa bahati mbaya, B_open bado itajaribu kupiga simu A_open, ambayo haipo tena kwenye kumbukumbu, kwa hivyo hata bila kuondoa B, mfumo bado utaanguka.

Ninaona njia mbili za kuzuia shida hii. Kwanza: kurejesha simu kwa thamani ya asili ya sys_open. Kwa bahati mbaya, sys_open sio sehemu ya jedwali la kernel ya mfumo ndani /proc/ksyms , kwa hivyo hatuwezi kuipata. Suluhisho lingine ni kutumia kaunta ya kiunga ili kuzuia moduli isipakuliwe. Hii ni nzuri kwa moduli za kawaida, lakini mbaya kwa moduli za "elimu".

/* syscall.c * * Simu ya mfumo "kuiba" sampuli */ /* Hakimiliki (C) 1998-99 na Ori Pomerantz */ /* Faili za kichwa zinazohitajika */ /* Kawaida katika moduli za kernel */ #jumuisha /* Tunafanya kazi ya kernel */ #include /* Hasa, sehemu */ /* Shughulikia CONFIG_MODVERSIONS */ #kama CONFIG_MODVERSIONS==1 #fafanua MODVERSIONS #pamoja na #endif #pamoja na /* Orodha ya simu za mfumo */ /* Kwa muundo wa sasa (mchakato), tunahitaji * hii ili kujua mtumiaji wa sasa ni nani. */ #pamoja na /* Katika 2.2.3 /usr/include/linux/version.h inajumuisha * macro kwa hili, lakini 2.0.35 haifanyi hivyo - kwa hivyo ninaiongeza * hapa ikiwa ni lazima. */ #ifndef KERNEL_VERSION #fafanua KERNEL_VERSION(a ,b,c) ((a)*65536+(b)*256+(c)) #endif #kama LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #jumuisha #endif /* Jedwali la simu la mfumo (jedwali la vitendaji). Sisi * tunafafanua hii kama ya nje, na kernel * itajaza kwa ajili yetu tunapokuwa insmod"ed */ extern void *sys_call_table; /* UID tunataka kupeleleza - itajazwa kutoka kwa * mstari wa amri */ int uid; #kama LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) MODULE_PARM(uid, "i"); #endif /* Kielekezi cha simu asilia ya mfumo. Sababu * tunaweka hii, badala ya kuita chaguo za kukokotoa asilia. * (sys_open), ni kwa sababu mtu mwingine anaweza kuwa * amebadilisha simu ya mfumo iliyo mbele yetu. chaguo la kukokotoa katika sehemu hiyo - na * linaweza kuondolewa kabla sisi hatujaondolewa. * Ni tofauti tuli, kwa hivyo haisafirishwi nje. */ asmlinkage int (*original_call)(const char *, int, int); /* Kwa sababu fulani, katika 2.2.3 current->uid ilinipa * sifuri, sio kitambulisho halisi cha mtumiaji. Nilijaribu kutafuta * makosa, lakini sikuweza kuifanya kwa muda mfupi, na * mimi ni mvivu - kwa hivyo nitatumia tu simu ya mfumo kupata * uid, the njia ya mchakato. * * Kwa sababu fulani, baada ya kuunda tena kernel shida hii * ilienda. */ asmlinkage int (*getuid_call)(); /* Kitendakazi "tutabadilisha sys_open (kitendakazi * kinachoitwa unapopiga simu ya mfumo wazi) nacho. Ili * kupata mfano halisi, pamoja na nambari na aina * ya hoja, tunapata chaguo la kukokotoa la awali * (ni" s kwa fs/open.c). * * Kinadharia, hii ina maana kwamba tumefungamanishwa na * toleo la sasa la kernel. Kiutendaji, * mfumo huita karibu kamwe usibadilike (ungeweza kuharibu uharibifu * na kuhitaji programu kukusanywa tena, kwa kuwa mfumo * simu hupigwa. kiolesura kati ya kernel na * michakato).*/ asmlinkage int our_sys_open(const char *jina la faili, bendera za int, int mode) ( int i = 0; char ch; /* Angalia kama huyu ndiye mtumiaji tunayepeleleza */ if (uid == getuid_call()) ( /* getuid_call ni simu ya mfumo wa getuid, * ambayo inatoa uid ya mtumiaji ambaye * aliendesha mchakato ulioita mfumo * simu tuliyopata */ /* Ripoti faili, ikiwa inafaa */ printk("Imefunguliwa faili na %d: ", uid); fanya ( #kama LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(ch, filename+i); #else ch = get_user(jina la faili+ i); ); #endif i++; printk("%c", ch); ) wakati (ch != 0); printk("\n"); ) /* Piga simu sys_open asili - vinginevyo, tunapoteza * uwezo wa kufungua faili */ return original_call(jina la faili, bendera, modi); ) /* Anzisha moduli - badilisha simu ya mfumo */ int init_module() ( /* Onyo - imechelewa sana kwa sasa, lakini labda kwa * wakati ujao. ... printk("Mwenzangu, cleanup_module(), ni sawa"); printk("hatari zaidi. Ikiwa\n"); printk("unathamini mfumo wako wa faili, itakuwa"); printk("kuwa \"sync; rmmod\" \n"); printk("unapoondoa moduli hii.\n"); /* Weka kiashirio kwa chaguo za kukokotoa asili katika * original_call, kisha ubadilishe simu ya mfumo * katika jedwali la simu la mfumo na our_sys_open */ original_call = sys_call_table[__NR_open]; sys_call_table[__NR_open] = our_sys_open; /* Ili kupata anwani ya chaguo za kukokotoa za mfumo * piga foo, nenda kwenye sys_call_table[__NR_foo]. */ printk("Kupeleleza kwenye UID:%d\n", uid); /* Pata simu ya mfumo kwa getuid */ getuid_call = sys_call_table[__NR_getuid]; kurudi 0; ) /* Safisha - futa usajili wa faili inayofaa kutoka /proc */ void cleanup_module() ( /* Rejesha simu ya mfumo kuwa ya kawaida */ ikiwa (sys_call_table[__NR_open] != our_sys_open) ( printk("Mtu mwingine pia alicheza na "); printk("fungua simu ya mfumo\n"); printk("Mfumo unaweza kuachwa "); printk("hali isiyo thabiti.\n"); ) sys_call_table[__NR_open] = original_call; )

Mara nyingi, msimbo wa simu ya mfumo wenye nambari __NR_xxx, iliyofafanuliwa /usr/include/asm/unistd.h, inaweza kupatikana katika msimbo wa chanzo wa kernel ya Linux kwenye chaguo la kukokotoa sys_xxx(). (Jedwali la simu la i386 linaweza kupatikana ndani /usr/src/linux/arch/i386/kernel/entry.S.) Kuna tofauti nyingi kwa sheria hii, hasa kutokana na ukweli kwamba simu nyingi za mfumo wa zamani hubadilishwa na mpya, na bila mfumo wowote. Kwenye majukwaa yenye uigaji wa wamiliki wa OS, kama vile parisc, sparc, sparc64, na alpha, kuna simu nyingi za ziada za mfumo; mips64 pia ina seti kamili ya simu za mfumo wa 32-bit.

Baada ya muda, kumekuwa na mabadiliko kwenye kiolesura cha baadhi ya simu za mfumo inapohitajika. Moja ya sababu za mabadiliko haya ilikuwa hitaji la kuongeza saizi ya miundo au maadili ya scalar yaliyopitishwa kwa simu ya mfumo. Kwa sababu ya mabadiliko haya, kwenye usanifu fulani (yaani, kwenye 32-bit i386 ya zamani), vikundi anuwai vya simu za mfumo sawa zilionekana (kwa mfano, punguza(2) na punguza64(2)), ambao hufanya kazi zinazofanana lakini hutofautiana katika saizi ya hoja zao. (Kama ilivyobainishwa, programu hazijaathiriwa: vifungashio vya glibc hufanya kazi fulani kuanzisha simu sahihi ya mfumo, na hii inahakikisha uoanifu wa ABI kwa jozi za zamani.) Mifano ya simu za mfumo ambazo zina matoleo mengi:

*Kwa sasa kuna matoleo matatu tofauti takwimu(2): sys_stat() (mahali __NR_oldstat), sys_newstat() (mahali __NR_takwimu) Na sys_stat64() (mahali __NR_stat64), ya mwisho inatumika kwa sasa. Hali sawa na lstat(2) na fstat(2). * Vile vile hufafanuliwa __NR_jina la zamani, __NR_jina_la_zamani Na __NR_ jina kwa simu sys_oldname(), sys_uname() Na sys_jina jipya(). * Linux 2.0 ina toleo jipya vm86(2), matoleo mapya na ya zamani ya taratibu za nyuklia yanaitwa sys_vm86old() Na sys_vm86(). * Linux 2.4 ina toleo jipya getrlimit(2) matoleo mapya na ya zamani ya taratibu za nyuklia yanaitwa sys_old_getrlimit() (mahali __NR_getrlimit) Na sys_getrlimit() (mahali __NR_ugetrlimit) * Katika Linux 2.4, ukubwa wa sehemu ya kitambulisho cha mtumiaji na kikundi umeongezwa kutoka biti 16 hadi 32. Simu kadhaa za mfumo zimeongezwa ili kusaidia mabadiliko haya (kwa mfano, chown32(2), getuid32(2), getgroups32(2), setresuid32(2)), ikighairi simu za awali zilizo na majina yale yale lakini bila kiambishi tamati "32". * Linux 2.4 iliongeza usaidizi wa kufikia faili kubwa (ambazo saizi na urekebishaji wake hautoshi katika biti 32) katika programu kwenye usanifu wa 32-bit. Hii ilihitaji mabadiliko kwenye simu za mfumo zinazofanya kazi na saizi za faili na urekebishaji. Simu zifuatazo za mfumo zimeongezwa: fcntl64(2), wapataji64(2), takwimu 64(2), takwimu 64(2), punguza64(2) na wenzao wanaoshughulikia maelezo ya faili au viungo vya ishara. Simu hizi za mfumo huondoa simu za mfumo wa zamani, ambazo, isipokuwa simu za "stat", zimepewa jina sawa lakini hazina kiambishi cha "64".

Kwenye majukwaa mapya ambayo yana ufikiaji wa faili wa 64-bit na 32-bit UID/GID (km alpha, ia64, s390x, x86-64), kuna toleo moja tu la mfumo linaloita UID/GID na ufikiaji wa faili. Kwenye majukwaa (kawaida majukwaa ya 32-bit) ambayo yana simu za *64 na *32, matoleo mengine hayatumiki.

*Changamoto rt_sig* imeongezwa kwa 2.2 kernel ili kusaidia ishara za ziada za wakati halisi (ona ishara(7)). Simu hizi za mfumo huondoa wito wa mfumo wa zamani wenye jina sawa lakini bila kiambishi awali cha "rt_". * Katika simu za mfumo chagua(2) na ramani(2) hoja tano au zaidi zinatumiwa, jambo ambalo lilisababisha matatizo katika kuamua jinsi hoja zilipitishwa kwenye i386. Matokeo yake, wakati kwenye usanifu mwingine wito sys_chagua() Na sys_map() mechi __NR_chagua Na __ramani_ya_NR, kwenye i386 zinalingana na zamani_chagua() Na ramani_ya_zamani() (taratibu kwa kutumia pointer kwa block ya hoja). Hivi sasa, hakuna tena suala la kupitisha hoja zaidi ya tano na ipo __NR__chagua habari, ambayo inalingana kabisa sys_chagua(), na hali sawa na __ramani_ya_NR2.

Kuhusu mambo mengi - alisema Walrus, - ni wakati wa kuzungumza.
L. Carroll (Nukuu kutoka kwa kitabu cha B. Straustrap)

badala ya utangulizi.

Juu ya mada ya muundo wa ndani wa kernel ya Linux kwa ujumla, mifumo yake ndogo na simu za mfumo haswa, imeandikwa na kuandikwa tena kwa mpangilio. Pengine, kila mwandishi anayejiheshimu anapaswa kuandika kuhusu hili angalau mara moja, kama vile kila mpangaji programu anayejiheshimu lazima aandike meneja wake wa faili manufaa katika nafasi ya kwanza, ili usisahau kile kilichojifunza haraka sana. Lakini, ikiwa maelezo yangu ya kusafiri ni muhimu kwa mtu, bila shaka, nitafurahi tu. Kweli, kwa ujumla, huwezi kuharibu uji na siagi, kwa hivyo, labda hata nitaweza kuandika au kuelezea kitu ambacho hakuna mtu aliyejisumbua kutaja.

Nadharia. Simu za mfumo ni nini?

Wanapoelezea kwa wasiojua programu (au OS) ni nini, kwa kawaida husema yafuatayo: kompyuta yenyewe ni kipande cha chuma, lakini programu ndiyo inafanya kipande hiki cha chuma kuwa muhimu. Mbaya, kwa kweli, lakini kwa ujumla, ni kweli. Labda ningesema sawa juu ya OS na simu za mfumo. Kwa kweli, katika mifumo tofauti ya uendeshaji, simu za mfumo zinaweza kutekelezwa kwa njia tofauti, idadi ya simu hizi zinaweza kutofautiana, lakini kwa njia moja au nyingine, kwa namna moja au nyingine, OS yoyote ina utaratibu wa kupiga simu. Kila siku, mtumiaji hufanya kazi kwa uwazi au bila ukamilifu na faili. Kwa kweli, anaweza kufungua faili kwa uhariri katika Neno lake la kupenda la MS au Notepad, au anaweza tu kuzindua toy, picha inayoweza kutekelezwa ambayo, kwa njia, pia imehifadhiwa kwenye faili, ambayo, kwa upande wake, lazima. kufunguliwa na kusomwa na faili zinazoweza kutekelezwa za kipakiaji. Kwa upande wake, toy pia inaweza kufungua na kusoma kadhaa ya faili wakati wa kazi yake. Kwa kawaida, faili haziwezi kusomwa tu, bali pia zimeandikwa (sio kila wakati, hata hivyo, lakini hapa hatuzungumzii juu ya mgawanyo wa haki na upatikanaji tofauti :)). Kernel inasimamia haya yote (katika mifumo ya uendeshaji ya microkernel, hali inaweza kutofautiana, lakini sasa tutaelekea kwa unobtrusively kitu cha majadiliano yetu - Linux, kwa hiyo tutapuuza hatua hii). Katika yenyewe, kuzalisha mchakato mpya pia ni huduma inayotolewa na OS kernel. Haya yote ni ya ajabu, kama vile ukweli kwamba wasindikaji wa kisasa hufanya kazi kwa masafa katika safu ya gigahertz na inajumuisha mamilioni mengi ya transistors, lakini ni nini kinachofuata? Ndio, vipi ikiwa hakukuwa na utaratibu ambao maombi ya mtumiaji yanaweza kufanya mambo ya kawaida na, wakati huo huo, mambo muhimu ( kwa kweli, vitendo hivi vidogo kwa hali yoyote havifanyiki na matumizi ya mtumiaji, lakini na OS kernel - mwandishi.), basi OS ilikuwa kitu yenyewe - haina maana kabisa, au, kinyume chake, kila programu ya mtumiaji yenyewe ingelazimika kuwa mfumo wa kufanya kazi ili kuhudumia mahitaji yake yote kwa uhuru. Nzuri, sivyo?

Kwa hivyo, tumekuja kwa ufafanuzi wa simu ya mfumo katika makadirio ya kwanza: simu ya mfumo ni aina ya huduma ambayo kernel ya OS hutoa kwa maombi ya mtumiaji kwa ombi la mwisho. Huduma kama hiyo inaweza kuwa ufunguzi uliotajwa tayari wa faili, uundaji wake, kusoma, kuandika, kuunda mchakato mpya, kupata kitambulisho cha mchakato (pid), kuweka mfumo wa faili, kusimamisha mfumo, mwishowe. Katika maisha halisi, kuna simu nyingi zaidi za mfumo kuliko zilizoorodheshwa hapa.

Je, simu ya mfumo inaonekanaje na ni nini? Naam, kutokana na kile kilichosemwa hapo juu, inakuwa wazi kwamba simu ya mfumo ni subroutine ya kernel ambayo ina fomu inayofaa. Wale ambao wamekuwa na uzoefu wa programu ya Win9x/DOS labda watakumbuka usumbufu wa int 0x21 na yote (au angalau baadhi) ya kazi zake nyingi. Walakini, kuna quirk moja ndogo ambayo inatumika kwa simu zote za mfumo wa Unix. Kwa kawaida, chaguo la kukokotoa linalotekeleza simu ya mfumo linaweza kuchukua hoja za N au kutozipokea kabisa, lakini kwa njia moja au nyingine, chaguo la kukokotoa lazima lirudishe thamani ya aina int. Thamani yoyote isiyo hasi inachukuliwa kama utekelezaji mzuri wa kitendakazi cha simu ya mfumo, na kwa hivyo mfumo unajiita. Thamani iliyo chini ya sifuri inaonyesha hitilafu na pia ina msimbo wa hitilafu (misimbo ya hitilafu imefafanuliwa katika include/asm-generic/errno-base.h na inajumuisha/asm-generic/errno.h vichwa). Katika Linux, hadi hivi majuzi, usumbufu wa int 0x80 ulikuwa lango la simu za mfumo, wakati kwenye Windows (hadi XP Service Pack 2, ikiwa sijakosea), usumbufu wa 0x2e ulikuwa lango kama hilo. Tena, kwenye kinu cha Linux, hadi hivi majuzi simu zote za mfumo zilishughulikiwa na kazi ya system_call(). Walakini, kama ilivyotokea baadaye, utaratibu wa classical wa simu za mfumo wa usindikaji kupitia lango la 0x80 husababisha kushuka kwa utendaji kwa wasindikaji wa Intel Pentium 4. Kwa hivyo, utaratibu wa classical ulibadilishwa na njia ya vitu vya pamoja vya nguvu (DSO - dynamic). faili ya kitu kilichoshirikiwa. Siwezi kuthibitisha tafsiri sahihi, lakini DSO ndiyo watumiaji wa Windows wanajua kama DLL (Maktaba ya Kiungo cha Dynamic) - VDSO. Kuna tofauti gani kati ya njia mpya na ile ya zamani? Kwanza, hebu tushughulike na njia ya classic ambayo inafanya kazi kupitia lango la 0x80.

Utaratibu wa kawaida wa kushughulikia simu katika Linux.

Inakatiza katika usanifu wa x86.

Kama ilivyoelezwa hapo juu, lango 0x80 (int 0x80) lilitumika hapo awali kuhudumia maombi ya mtumiaji. Uendeshaji wa mfumo kulingana na usanifu wa IA-32 unadhibitiwa na kukatizwa (kuzungumza madhubuti, hii inatumika kwa mifumo yote ya msingi wa x86 kwa ujumla). Wakati tukio linatokea (tiki mpya ya kipima muda, shughuli fulani kwenye kifaa fulani, hitilafu - mgawanyiko kwa sifuri, n.k.), ukatizaji hutolewa. Kukatiza kunaitwa hivyo kwa sababu kwa kawaida hukatiza mtiririko wa kawaida wa msimbo. Vikwazo kwa kawaida hugawanywa katika maunzi na programu (vifaa na programu hukatizwa). Vikwazo vya maunzi ni vikatizo vinavyotokana na mfumo na vifaa vya pembeni. Wakati kifaa kinahitaji kuvutia usikivu wa kernel ya OS, (kifaa) hutoa ishara kwenye laini yake ya ombi la kukatiza (IRQ - Mstari wa Ombi la Kukatiza). Hii inasababisha ukweli kwamba ishara inayolingana inatolewa kwa pembejeo fulani za processor, kwa msingi ambao processor huamua kukatiza utekelezaji wa mtiririko wa maagizo na udhibiti wa uhamishaji kwa kidhibiti cha usumbufu, ambacho tayari hugundua kilichotokea na kile kinachohitajika kufanywa. kufanyika. Vikwazo vya maunzi ni asilia isiyolingana. Hii inamaanisha kuwa usumbufu unaweza kutokea wakati wowote. Mbali na vifaa vya pembeni, processor yenyewe inaweza kuzalisha usumbufu (au, kwa usahihi, isipokuwa vifaa - Vighairi vya Vifaa - kwa mfano, mgawanyiko uliotajwa tayari na sifuri). Hii imefanywa ili kujulisha OS kuhusu tukio la hali isiyo ya kawaida, ili OS inaweza kuchukua hatua fulani kwa kukabiliana na tukio la hali hiyo. Baada ya kusindika usumbufu, processor inarudi kwenye utekelezaji wa programu iliyoingiliwa. Kukatiza kunaweza kuanzishwa na programu ya mtumiaji. Ukatizaji kama huo unaitwa usumbufu wa programu. Kukatiza kwa programu, tofauti na kukatizwa kwa maunzi, ni sawa. Hiyo ni, wakati usumbufu unapoitwa, msimbo uliouita unasimamishwa hadi usumbufu utumiwe. Wakati kidhibiti cha kukatiza kinatoka, kinarudi kwa anwani ya mbali iliyohifadhiwa mapema (ukatizaji ulipoitwa) kwenye rafu, kwa maagizo yanayofuata baada ya maagizo ya kukatiza kwa simu (int). Kidhibiti cha kukatiza ni mkaazi (katika kumbukumbu kabisa) kipande cha msimbo. Kama sheria, hii ni programu ndogo. Ingawa, ikiwa tunazungumza juu ya kernel ya Linux, basi kidhibiti cha usumbufu sio kidogo kila wakati. Kidhibiti cha kukatiza kinafafanuliwa na vekta. Vekta sio chochote zaidi ya anwani (sehemu na kukabiliana) ya mwanzo wa nambari ambayo inapaswa kushughulikia kukatizwa kwa faharisi uliyopewa. Kufanya kazi na vikatizo hutofautiana kwa kiasi kikubwa katika hali halisi (Njia Halisi) na hali ya uendeshaji ya kichakataji (Njia Iliyolindwa) iliyolindwa (Nakukumbusha kwamba hapa tunamaanisha vichakataji vya Intel na zile zinazotangamana). Katika hali halisi (isiyolindwa) ya processor, washughulikiaji wa kuingilia hufafanuliwa na vectors zao, ambazo huhifadhiwa daima mwanzoni mwa kumbukumbu.Uteuzi wa anwani inayotakiwa kutoka kwa meza ya vector hutokea kwa index, ambayo pia ni nambari ya kupinga. Kwa kubatilisha vekta kwa faharasa maalum, unaweza kukabidhi kidhibiti chako cha kukatiza.

Katika hali iliyolindwa, vidhibiti vya kukatiza (lango, milango, au milango) hazifafanuliwa tena kwa kutumia jedwali la vekta. Badala ya meza hii, meza ya lango au, kwa usahihi zaidi, meza ya kupinga - IDT (Jedwali la Wafafanuzi wa Kuingilia) hutumiwa. Jedwali hili linatolewa na kernel na anwani yake imehifadhiwa kwenye rejista ya processor ya idtr. Rejesta hii haipatikani moja kwa moja. Unaweza tu kufanya kazi nayo kwa kutumia maagizo ya lidt/sidt. Ya kwanza (lidt) hupakia kwenye rejista ya idtr thamani iliyotajwa kwenye operesheni, ambayo ni anwani ya msingi ya jedwali la maelezo ya kukatiza, ya pili (sidt) huhifadhi anwani ya jedwali katika idtr kwenye operesheni maalum. Kama vile maelezo ya sehemu yanavyoletwa kutoka kwa jedwali la kifafanuzi na kiteua, kifafanuzi cha sehemu kinachohudumia ukatizaji katika hali ya ulinzi pia huletwa. Ulinzi wa kumbukumbu unasaidiwa na wasindikaji wa Intel kuanzia na CPU i80286 (sio kabisa katika fomu ambayo imewasilishwa sasa, ikiwa tu kwa sababu 286 ilikuwa processor ya 16-bit - kwa hiyo Linux haiwezi kukimbia kwenye wasindikaji hawa) na i80386, na kwa hiyo processor yenyewe hufanya chaguzi zote muhimu na, kwa hivyo, hatutaingia kwa undani katika hila zote za hali iliyolindwa (yaani, Linux inafanya kazi katika hali iliyolindwa). Kwa bahati mbaya, hakuna wakati au fursa zinazoturuhusu kukaa juu ya utaratibu wa kushughulikia usumbufu katika hali iliyolindwa kwa muda mrefu. Ndio, hii haikuwa lengo wakati wa kuandika nakala hii. Taarifa zote zilizotolewa hapa kuhusu uendeshaji wa familia ya x86 ya wasindikaji ni ya juu juu na hutolewa tu ili kusaidia kuelewa vizuri utaratibu wa simu za mfumo wa kernel. Mambo mengine yanaweza kujifunza moja kwa moja kutoka kwa msimbo wa kernel, ingawa, ili kuelewa kikamilifu kile kinachotokea, bado ni kuhitajika kujitambulisha na kanuni za hali ya ulinzi. Sehemu ya msimbo inayoanzisha (lakini haijawekwa!) IDT iko katika arch/i386/kernel/head.S: /* * setup_idt * * husanidi kitambulisho chenye maingizo 256 yanayoelekeza kwa * ignore_int, kukatiza milango. Kwa hakika haipakii * idt - hiyo inaweza kufanywa tu baada ya paging kuwashwa * na kernel kuhamishwa hadi PAGE_OFFSET. Vikwazo * vinawashwa mahali pengine, wakati tunaweza kuwa na hakika * kiasi kwamba kila kitu kiko sawa. * * Onyo: %esi inapatikana katika kipengele hiki cha kukokotoa.*/ 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 Vidokezo vichache kwenye msimbo: Nambari iliyo hapo juu imeandikwa kwa tofauti ya kiunganishi cha AT&T, kwa hivyo maarifa yako ya mkusanyaji katika nukuu yake ya kawaida ya Intel yanaweza kutatanisha. Tofauti kuu zaidi iko katika mpangilio wa operesheni. Ikiwa agizo limefafanuliwa kwa nukuu ya Intel - "kikusanyaji"< "источник", то для ассемблера AT&T порядок прямой. Регистры процессора, как правило, должны иметь префикс "%", непосредственные значения (константы) префиксируются символом доллара "$". Синтаксис AT&T традиционно используется в Un*x-системах.

Katika mfano hapo juu, mistari ya 2-4 huweka anwani ya kidhibiti chaguo-msingi cha usumbufu kwa kila usumbufu. Kidhibiti chaguo-msingi ni kitendakazi cha kupuuza_int, ambacho hakifanyi chochote. Uwepo wa mbegu kama hiyo ni muhimu kwa usindikaji sahihi wa usumbufu wote katika hatua hii, kwani hakuna zingine bado (ingawa mitego imewekwa chini kidogo kwenye nambari - kwa mitego, angalia Rejeleo la Mwongozo wa Usanifu wa Intel au kitu kama hicho, hatutakuwa hapa mitego ya kugusa). Mstari wa 5 huweka aina ya valve. Kwenye mstari wa 6, tunapakia anwani ya jedwali letu la IDT kwenye rejista ya faharasa. Jedwali linapaswa kuwa na maingizo 255, baiti 8 kila moja. Katika mistari 8-13 tunajaza jedwali zima na maadili sawa yaliyowekwa hapo awali kwenye rejista za eax na edx - i.e., hili ni lango la usumbufu linalorejelea kidhibiti_int. Chini kidogo tunafafanua macro kwa kuweka mitego - mistari 14-22. Katika mstari wa 23-26, kwa kutumia jumla iliyo hapo juu, tunaweka mitego kwa tofauti zifuatazo: mapema_gawanya_err - mgawanyiko kwa sifuri (0), early_illegal_opcode - maagizo yasiyojulikana ya kichakataji (6), mapema_protection_fault - kushindwa kwa ulinzi wa kumbukumbu (13), early_page_fault - tafsiri ya ukurasa. kushindwa (14). Katika mabano kuna nambari za "kukatizwa" zinazozalishwa wakati hali inayolingana isiyo ya kawaida inatokea. Kabla ya kuangalia aina ya kichakataji katika arch/i386/kernel/head.S, jedwali la IDT huwekwa kwa kupiga simu setup_idt: /* * anza usanidi wa mfumo wa 32-bit. Tunahitaji kufanya upya baadhi ya mambo yaliyofanywa * katika hali ya 16-bit kwa shughuli "halisi". */ 1. piga simu setup_idt ... 2. call check_x87 3. lgdt early_gdt_descr 4. lidt idt_descr Baada ya kujua aina ya (co) processor na kufanya kazi zote za maandalizi katika mstari wa 3 na 4, tunapakia meza za GDT na IDT, ambazo zitatumika katika hatua za kwanza za punje.

Simu za mfumo na int 0x80.

Kutokana na kukatizwa, hebu turudi kwenye simu za mfumo. Kwa hivyo, ni nini kinachohitajika kutumikia mchakato ambao unaomba aina fulani ya huduma? Kwanza, unahitaji kuhama kutoka pete 3 (kiwango cha mapendeleo CPL=3) hadi kiwango cha upendeleo zaidi 0 (Pete 0, CPL=0). msimbo wa kernel iko katika sehemu yenye mapendeleo ya juu zaidi. Kwa kuongeza, unahitaji msimbo wa kushughulikia ambao utatumikia mchakato. Hivi ndivyo lango la 0x80 linatumika. Ingawa kuna simu nyingi za mfumo, zote hutumia sehemu moja ya kuingilia - int 0x80. Kidhibiti chenyewe kimewekwa wakati wa kupiga kazi ya arch/i386/kernel/traps.c::trap_init(): batili __init trap_init(batili) ( ... set_system_gate(SYSCALL_VECTOR,&system_call); ... ) Tunavutiwa zaidi na laini hii katika trap_init(). Katika faili ile ile hapo juu, unaweza kuangalia set_system_gate() msimbo wa kazi: utupu tuli __init set_system_gate(int n haijasainiwa, batili *addr) ( _set_gate(n, DESCTYPE_TRAP | DESCTYPE_DPL3, addr, __KERNEL_CS); ) Hapa unaweza kuona kwamba lango la kukatiza 0x80 (yaani, thamani hii inafafanuliwa na SYSCALL_VECTOR macro - unaweza kuchukua neno kwa hilo :)) imewekwa kama mtego na kiwango cha upendeleo DPL=3 (Pete 3), i.e. usumbufu huu utanaswa ukipigiwa simu kutoka kwa nafasi ya mtumiaji. Shida na mabadiliko kutoka kwa Gonga 3 hadi Gonga 0 hivi. kutatuliwa. Chaguo la kukokotoa la _set_gate() limefafanuliwa katika faili ya kijajuu/asm-i386/desc.h. Kwa wale wanaotamani sana, hapa chini kuna nambari, bila maelezo marefu, hata hivyo: utupu tuli wa ndani _set_gate(lango la ndani, aina ya int ambayo haijatiwa saini, batili *addr, sehemu fupi isiyotiwa saini) ( __u32 a, b; pack_gate(&a, &b, (muda ambao haujasainiwa) addr, seg, aina, 0); write_idt_entry(idt_table, gate) , a, b);) Wacha turudi kwenye kazi ya trap_init(). Inaitwa kutoka kwa kazi ya start_kernel() init/main.c . Ukiangalia trap_init() msimbo, unaweza kuona kwamba chaguo hili la kukokotoa huandika tena thamani za jedwali la IDT - vidhibiti vilivyotumika katika hatua za awali za uanzishaji wa kernel (early_page_fault, early_divide_err, early_illegal_opcode, early_protection_fault) hubadilishwa na hizo. ambayo itatumika tayari katika kazi ya kernel ya mchakato. Kwa hivyo, karibu tulifikia hatua na tayari tunajua kuwa simu zote za mfumo zinachakatwa kwa njia ile ile - kupitia lango la int 0x80. Kama kidhibiti cha int 0x80, kama inavyoonekana tena kutoka kwa kipande cha msimbo hapo juu arch/i386/kernel/traps.c::trap_init(), mfumo_call() utendakazi umewekwa.

system_call().

Msimbo wa kazi wa system_call() unapatikana katika faili ya arch/i386/kernel/entry.S na inaonekana kama hii: # mfumo wa kidhibiti simu KUINGIA(system_call) RING0_INT_FRAME # haiwezi kujifungulia kwenye nafasi ya mtumiaji pushl %eax # anyway save orig_eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO(%ebp) # ufuatiliaji wa simu kwenye mfumo unafanya kazi / uigaji_wa_nambari ya 8F Kumbuka , na kwa hivyo inahitaji testw na sio testb */ testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_callsycalls_calls_calls)%(nr_callsycalls_calls_sycalls)%(nr_callsycalls_calls) movl %eax,PT_EAX(%esp) # hifadhi thamani ya kurejesha ... Msimbo haujakamilika. Kama unavyoona, system_call() kwanza husanifu stack ili kufanya kazi katika Gonga 0, huhifadhi thamani iliyopitishwa kwake kupitia eax kwenye rafu, huhifadhi rejista zote kwenye fungu pia, hupata data kuhusu uzi wa kupiga simu, na huangalia ikiwa thamani iliyopitishwa kwake, nambari ya simu ya mfumo, iko nje ya anuwai. nje ya jedwali la simu la mfumo, na hatimaye kutumia thamani iliyopitishwa kwa eax kama hoja, system_call() inaruka hadi kwa kidhibiti halisi cha pato la mfumo kulingana na ni jedwali gani la kuingiza faharisi katika eax inarejelea. Sasa kumbuka jedwali nzuri la zamani la kukatiza vekta kutoka kwa hali halisi. Je, haikukumbushi chochote? Kwa kweli, kila kitu ni ngumu zaidi. Hasa, simu ya mfumo lazima inakili matokeo kutoka kwa rafu ya kernel hadi rafu ya mtumiaji, kupitisha msimbo wa kurejesha, na vitu vingine vichache. Katika tukio ambalo hoja iliyobainishwa katika eax hairejelei simu iliyopo ya mfumo (thamani iko nje ya anuwai), kuruka kwa lebo ya syscall_badsys hutokea. Hapa, thamani -ENOSYS inasukumwa kwenye stack kwa kukabiliana na ambayo thamani ya eax inapaswa kupatikana - simu ya mfumo haijatekelezwa. Hii inakamilisha utekelezaji wa system_call().

Jedwali la simu la mfumo liko katika faili arch/i386/kernel/syscall_table.S na ina fomu rahisi kabisa: INGIA(sys_call_table) .long sys_restart_syscall /* 0 - simu ya mfumo ya "setup()" ya zamani, inayotumika kuanzisha upya */ .long sys_exit .long sys_fork .long sys_read .long sys_write .long sys_fungua /* long sys_exit. sys_waitpid .long sys_creat ... Kwa maneno mengine, jedwali zima si chochote zaidi ya safu ya anwani za kazi, zilizopangwa kwa utaratibu wa nambari za simu za mfumo ambazo kazi hizi hutumikia. Jedwali ni safu ya kawaida ya maneno ya mashine mbili (au maneno 32-bit - chochote unachopenda). Msimbo wa sehemu ya simu za mfumo wa huduma za utendakazi uko katika sehemu mahususi ya jukwaa - arch/i386/kernel/sys_i386.c, na sehemu inayojitegemea ya jukwaa - in kernel/sys.c .

Hivi ndivyo ilivyo kwa simu za mfumo na lango la 0x80.

Utaratibu mpya wa kushughulikia simu za mfumo katika Linux. sysenter/sysexit.

Kama ilivyoelezwa, haraka ikawa wazi kuwa kutumia njia ya jadi ya usindikaji wa simu kulingana na lango la 0x80 husababisha hasara ya utendaji kwa wasindikaji wa Intel Pentium 4. Kwa hiyo, Linus Torvalds alitekeleza utaratibu mpya kwenye kernel kulingana na maagizo ya sysenter / sysexit. na iliyoundwa ili kuongeza utendakazi wa kernel kwenye mashine , iliyo na kichakataji cha Pentium II na cha juu zaidi (ni kwa Pentium II + ambapo wasindikaji wa Intel wanaunga mkono maagizo yaliyotajwa ya sysenter / sysexit). Nini kiini cha utaratibu mpya? Oddly kutosha, lakini kiini kilibakia sawa. Utekelezaji umebadilika. Kulingana na nyaraka za Intel, maagizo ya sysenter ni sehemu ya utaratibu wa "simu za mfumo wa haraka". Hasa, maagizo haya yameboreshwa kwa mabadiliko ya haraka kutoka kiwango cha upendeleo hadi kingine. Ili kuwa sahihi zaidi, inaharakisha mpito hadi 0 (Pete 0, CPL=0). Katika kesi hii, mfumo wa uendeshaji lazima uandae processor kutumia maagizo ya sysenter. Mpangilio huu unafanywa mara moja wakati wa kupakia na kuanzisha kernel ya OS. Inapoitwa, sysenter huweka rejista za processor kulingana na rejista zinazotegemea mashine zilizowekwa hapo awali na OS. Hasa, rejista ya sehemu na rejista ya pointer ya maagizo - cs:eip imewekwa, pamoja na sehemu ya stack na pointer ya juu ya stack - ss, esp. Mpito kwa sehemu mpya ya msimbo na mabadiliko hufanywa kutoka kwa pete 3 hadi 0.

Taarifa ya sysexit hufanya kinyume. Hufanya mpito wa haraka kutoka kiwango cha upendeleo 0 hadi kiwango cha upendeleo 3 (CPL=3). Katika kesi hii, rejista ya sehemu ya msimbo imewekwa kwa 16 + thamani ya sehemu ya cs iliyohifadhiwa kwenye rejista ya processor inayotegemea mashine. Rejesta ya eip imejazwa na yaliyomo kwenye rejista ya edx. Jumla ya 24 na thamani ya cs, iliyoingia na OS mapema katika rejista ya tegemezi ya mashine ya processor, imeingia kwenye ss wakati wa kuandaa muktadha wa uendeshaji wa maagizo ya sysenter. esp imejazwa na yaliyomo kwenye rejista ya ecx. Thamani zinazohitajika kwa maagizo ya sysenter/sysexit kufanya kazi huhifadhiwa katika maeneo yafuatayo:

  1. SYSENTER_CS_MSR 0x174 - sehemu ya msimbo ambapo thamani ya sehemu iliyo na msimbo wa kidhibiti simu imeingizwa.
  2. SYSENTER_ESP_MSR 0x175 - kielekezi hadi juu ya rafu kwa kidhibiti simu cha mfumo.
  3. SYSENTER_EIP_MSR 0x176 - pointer ya kurekebisha ndani ya sehemu ya msimbo. Inaashiria mwanzo wa msimbo wa kidhibiti simu.
Anwani hizi zinarejelea rejista zinazotegemea modeli ambazo hazina majina. Maadili yameandikwa kwa rejista zinazotegemea mfano kwa kutumia maagizo ya wrmsr, wakati edx:eax lazima iwe na sehemu kuu na ndogo za neno la mashine ya 64-bit, mtawaliwa, na anwani ya rejista itakayoandikwa lazima iingizwe. nk. Katika Linux, anwani za rejista tegemezi za kielelezo zimefafanuliwa katika faili ya kichwa ni pamoja na/asm-i368/msr-index.h kama ifuatavyo (kabla ya toleo la 2.6.22, angalau zilifafanuliwa katika pamoja/asm-i386/msr. .h faili ya kichwa, wacha nikukumbushe kwamba tunazingatia utaratibu wa simu za mfumo kwa kutumia Linux kernel 2.6.22 kama mfano): #fafanua MSR_IA32_SYSENTER_CS 0x00000174 #fafanua MSR_IA32_SYSENTER_ESP 0x00000175 #fafanua MSR_IA32_SYSENTER_EIP 0x00000176 Nambari ya kernel inayohusika na kuweka rejista zinazotegemea mfano iko kwenye faili arch/i386/sysenter.c na inaonekana kama hii: 1. batili wezesha_sep_cpu(batili) ( 2. int cpu = get_cpu(); 3. struct tss_struct *tss = &per_cpu(init_tss, cpu); 4. ikiwa (!boot_cpu_has(X86_FEATURE_SEP)) ( 5. put_cpu( 5. put_cpu) rudisha;) 7. tss->x86_tss.ss1 = __KERNEL_CS; 8. tss->x86_tss.esp1 = sizeof(muundo tss_struct) + (urefu ambao haujasainiwa) tss; 9. wrmsr(MSR_IA32_SYSENTER_CS, _0.0rms MSR_IA32_SYSENTER_ESP, tss->x86_tss.esp1, 0); 11. wrmsr(MSR_IA32_SYSENTER_EIP, (muda mrefu ambao haujasainiwa) sysenter_entry, 0); 12. put_cpu(); ) Hapa, katika kutofautiana kwa tss, tunapata anwani ya muundo unaoelezea sehemu ya hali ya kazi. TSS (Sehemu ya Hali ya Kazi) hutumiwa kuelezea muktadha wa kazi na ni sehemu ya utaratibu wa kufanya kazi nyingi wa maunzi ya x86. Walakini, Linux haitumii sana ubadilishaji wa muktadha wa kazi ya vifaa. Kulingana na hati za Intel, kubadili kazi nyingine hufanywa ama kwa kutekeleza maagizo ya kuruka kwa sehemu (jmp au simu) ambayo inarejelea sehemu ya TSS, au kushughulikia lango la kazi katika GDT (LDT). Rejista maalum ya processor, isiyoonekana kwa programu - TR (Daftari la Kazi - rejista ya kazi) ina kichaguzi cha maelezo ya kazi. Kupakia rejista hii pia hupakia msingi usioonekana wa programu na rejista za kikomo zinazohusiana na TR.

Ingawa Linux haitumii ubadilishaji wa muktadha wa kazi ya maunzi, kernel inalazimika kutenga kiingilio cha TSS kwa kila kichakataji kilichosanikishwa kwenye mfumo. Hii ni kwa sababu wakati kichakataji kinapobadilika kutoka modi ya mtumiaji hadi modi ya kernel, huchota anwani ya mrundikano wa kernel kutoka TSS. Kwa kuongeza, TSS inahitajika kudhibiti ufikiaji wa bandari za I/O. TSS ina ramani ya ruhusa za bandari. Kulingana na ramani hii, inawezekana kudhibiti ufikiaji wa bandari kwa kila mchakato kwa kutumia maagizo ya ndani / nje. Hapa tss->x86_tss.esp1 inaelekeza kwenye mkusanyiko wa kernel. __KERNEL_CS inaelekeza kwa kawaida sehemu ya msimbo wa kernel. Kukabiliana-eip ni anwani ya kitendakazi cha sysenter_entry().

Kazi ya sysenter_entry() imefafanuliwa katika faili arch/i386/kernel/entry.S na inaonekana kama hii: /* SYSENTER_RETURN inaelekeza baada ya maagizo ya "sysenter" katika ukurasa wa vsyscall. Tazama vsyscall-sysentry.S, ambayo inafafanua ishara. */ # sysenter call handler stub ENTRY(sysenter_entry) CFI_STARTPROC simple CFI_SIGNAL_FRAME CFI_DEF_CFA esp, 0 CFI_REGISTER esp, ebp movl TSS_sysenter_esp0(%esp),%esp sysenter_call_past_offs hitaji la *sehemu hii *Nos fuata: irq zilizozimwa na hapa tunaiwasha moja kwa moja baada ya kuingia: */ ENABLE_INTERRUPTS(CLBR_NONE) pushl $(__USER_DS) CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET ss, 0*/ pushl %ebp CFI_ADJUST_CFA_OFF_OFFSET_OFFSET_OFFSET_OFFSET_OFFSET_OFFSET_OFFSET_OFFSET_OFFSET_OFFSET, , 0*/ /* * Push current_thread_info()->sysenter_return kwenye stack. * Kidogo kidogo cha kurekebisha kukabiliana ni muhimu - 4*4 ina maana maneno 4 * kusukuma juu; +8 inalingana na mpangilio wa esp0 wa copy_thread. */ pushl (TI_sysenter_return-THREAD_SIZE+8+4*4)(%esp) CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET eip, 0 /* * Pakia hoja ya sita inayowezekana kutoka kwa bunda la watumiaji. * Makini kuhusu usalama. .*/ cmpl $__PAGE_OFFSET-3,%ebp jae syscall_fault 1: movl (%ebp),%ebp .sehemu ya __ex_table,"a" .align 4 .long 1b,syscall_fault .pushl iliyotangulia %eax CFI_ADJUST_GET_OFF_DEADJUST_GET_CFA_GET_ADJUST_GET_ADJUST_GET_DEA ) /* Kumbuka, _TIF_SECCOMP ni nambari 8 kidogo, na kwa hivyo inahitaji testw na si testb */ testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz_callssylls_callssyllsyplsscallsscallsplcm piga simu *sys_call_table(,%eax,4) movl %eax,PT_EAX(%esp) DISABLE_INTERRUPTS(CLBR_ANY) TRACE_IRQS_OFF movl TI_flags(%ebp), %ecx testw $_TIF_ALLWORK_MASK, %syworkdiess rejista pia* ikiwa lazima isajili kitu* zima sysexit */ movl PT_EIP(%esp), %edx movl PT_OLDESP(%esp), %ecx xorl % ebp,%ebp TRACE_IRQS_ON 1: mov PT_FS(%esp), %fs ENABLE_INTERRUPTS_SYSEXIT CFI_ENDPROC .pushsection .fixup,"ax" 2: movl $0,PT_FS(%esp) jmp 1b _sehemu ya_refu . 1b,2b .popsection ENDPROC(sysenter_entry) Kama ilivyo kwa system_call() , kazi kuu inafanywa kwa simu *sys_call_table(,%eax,4) line. Hapa ndipo kidhibiti simu mahususi kinapoalikwa. Kwa hivyo, ni wazi kuwa kidogo imebadilika kimsingi. Ukweli kwamba vekta ya kukatiza sasa imejaa kwenye maunzi na kichakataji hutusaidia kuhama haraka kutoka kiwango kimoja cha upendeleo hadi kingine hubadilisha tu baadhi ya maelezo ya utekelezaji yenye maudhui sawa. Kweli, mabadiliko hayaishii hapo. Kumbuka jinsi hadithi ilianza. Hapo mwanzoni, tayari nilitaja vitu vilivyoshirikiwa. Kwa hivyo, ikiwa mapema utekelezaji wa simu ya mfumo, sema, kutoka kwa maktaba ya mfumo wa libc ilionekana kama simu ya kukatiza (licha ya ukweli kwamba maktaba ilichukua majukumu kadhaa ili kupunguza idadi ya swichi za muktadha), sasa shukrani kwa VDSO simu ya mfumo. inaweza kufanywa karibu moja kwa moja , bila kuhusika kwa libc. Inaweza kuwa imefanywa moja kwa moja hapo awali, tena, kama usumbufu. Lakini sasa simu inaweza kuombwa kama kazi ya kawaida inayosafirishwa kutoka kwa maktaba iliyounganishwa kwa nguvu (DSO). Wakati wa kuwasha, kernel huamua ni utaratibu gani unapaswa na unaweza kutumika kwa jukwaa fulani. Kulingana na hali, kernel huweka mahali pa kuingilia kwa kazi ambayo hutekeleza simu ya mfumo. Kisha, chaguo hili la kukokotoa litatumwa kwa nafasi ya mtumiaji kama maktaba ya linux-gate.so.1. Maktaba ya linux-gate.so.1 haipo kwenye diski. Ni, kwa kusema, kuigwa na kernel na inapatikana tu mradi mfumo unafanya kazi. Ikiwa utafanya kuzima kwa mfumo, weka FS ya mizizi kutoka kwa mfumo mwingine, basi huwezi kupata faili hii kwenye mizizi ya FS ya mfumo uliosimamishwa. Kwa kweli, hautaweza kuipata hata kwenye mfumo unaoendesha. Kimwili, haipo. Ndio maana linux-gate.so.1 ni kitu kingine isipokuwa VDSO - i.e. Kipengee Kinachoshirikiwa Kiukweli. Kernel huweka ramani ya maktaba yenye nguvu iliyoigwa kwa njia hii katika nafasi ya anwani ya kila mchakato. Unaweza kuthibitisha hili kwa urahisi kwa kutekeleza amri ifuatayo: [barua pepe imelindwa]... bffd2000 00:00 0 fffe000-ffffff000 r-xp 00000000 00:00 0 Hapa mstari wa mwisho ndio kitu cha kupendeza kwetu: fffe000-ffff000 r-xp 00000000 00:00 0 Kutoka kwa mfano hapo juu, inaweza kuonekana kuwa kitu kinachukua ukurasa mmoja katika kumbukumbu - 4096 byte, kivitendo nje ya nafasi ya anwani. Wacha tufanye jaribio lingine: [barua pepe imelindwa]:~$ ldd `paka yupi` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e87000) /lib/ld-linux .so.2 (0xb7fdf000) [barua pepe imelindwa]:~$ ldd `ambayo gcc` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e3c000) /lib/ld-linux .so.2 (0xb7f94000) [barua pepe imelindwa]:~$ Hapa tumetoka tu kuchukua maombi mawili. Inaweza kuonekana kuwa maktaba imechorwa kwa nafasi ya anwani ya mchakato katika anwani ile ile ya kudumu - 0xffffe000. Sasa hebu tujaribu kuona ni nini kimehifadhiwa kwenye ukurasa huu wa kumbukumbu...

Unaweza kutupa ukurasa wa kumbukumbu ambapo nambari ya pamoja ya VDSO imehifadhiwa kwa kutumia programu ifuatayo: #jumuisha #jumuisha #jumuisha int main () ( char* vdso = 0xffffe000; char* buffer; FILE* f; bafa = malloc(4096); ikiwa (!bafa) toka(1); memcpy(bafa, vdso, 4096) ; ikiwa (!(f = fopen ("test.dump", "w+b"))) ( bila malipo (bafa); toka (1); ) fwrite (bafa, 4096, 1, f); funga (f) ; bure (bafa); rudisha 0;) Kwa kusema kweli, mapema hii inaweza kufanywa kwa urahisi zaidi na amri dd if=/proc/self/mem of=test.dump bs=4096 skip=1048574 count=1, lakini kokwa tangu toleo la 2.6.22 au labda hata mapema hazichakata kumbukumbu tena kwa /proc/`pid`/mem. Faili hii, ambayo inaonekana kuhifadhiwa kwa uoanifu, haina taarifa zaidi.

Kukusanya na kuendesha programu hapo juu. Wacha tujaribu kutenganisha nambari inayosababisha: [barua pepe imelindwa]:~/tmp$ objdump --disassemble ./test.dump ./test.dump: umbizo la faili elf32-i386 Disassembly ya sehemu .text: ffffe400<__kernel_vsyscall>...<__kernel_vsyscall+0x3>ffffe410: 5d pop %ebp ffffe411: 5a pop %edx ffffe412: 59 pop %ecx ffffe413: c3 ret ... [barua pepe imelindwa]:~/tmp$ Hapa ni lango letu la simu za mfumo, zote zikiwa katika mwonekano kamili. Mchakato (au, maktaba ya mfumo wa libc), ikiita __kernel_vsyscall kazi, inafika kwa anwani 0хffffe400 (kwa upande wetu). Zaidi ya hayo, __kernel_vsyscall huhifadhi maudhui ya rejista za ecx, edx, ebp kwenye mrundikano wa mchakato wa mtumiaji.Tayari tulizungumza kuhusu madhumuni ya rejista za ecx na edx hapo awali, katika ebp inatumiwa baadaye kurejesha rafu ya mtumiaji. Maagizo ya sysenter yanatekelezwa, "kuzuia usumbufu" na, kwa sababu hiyo, mpito unaofuata kwa sysenter_entry (tazama hapo juu). Maagizo ya jmp katika 0xffffe40e yameingizwa ili kuanzisha upya simu ya mfumo kwa hoja 6 (ona http://lkml.org/lkml/2002/12/18/). Nambari iliyowekwa kwenye ukurasa iko kwenye faili arch/i386/kernel/vsyscall-enter.S (au arch/i386/kernel/vsyscall-int80.S kwa ndoano ya 0x80). Ingawa niligundua kuwa anwani ya __kernel_vsyscall kazi ni ya mara kwa mara, lakini kuna maoni kwamba hii sivyo. Kawaida, nafasi ya mahali pa kuingilia katika __kernel_vsyscall() inaweza kupatikana kutoka kwa vekta ya ELF-auxv kwa kutumia parameta ya AT_SYSINFO. Vekta ya ELF-auxv ina taarifa iliyopitishwa kwa mchakato kupitia mkusanyiko wakati wa kuanza na ina taarifa mbalimbali zinazohitajika wakati wa utekelezaji wa programu. Vekta hii ina anuwai ya mazingira ya mchakato, hoja, na kadhalika.

Hapa kuna mfano mdogo wa C wa jinsi ya kupiga __kernel_vsyscall kazi moja kwa moja: #pamoja na int pid; int main () ( __asm ​​​​("movl $20, %eax \n" "call *%gs:0x10 \n" "movl %eax, pid \n"); printf ("pid: %d\n", pid); kurudi 0;) Mfano huu umechukuliwa kutoka kwa ukurasa wa Manu Garg, http://www.manugarg.com. Kwa hivyo, katika mfano ulio hapo juu, tunapiga simu ya mfumo wa getpid() (nambari 20 au vinginevyo __NR_getpid). Ili tusipande mrundikano wa mchakato katika kutafuta utofauti wa AT_SYSINFO, tutatumia ukweli kwamba maktaba ya mfumo wa libc.so inakili thamani ya lahaja ya AT_SYSINFO kwenye Kizuizi cha Kudhibiti Minyororo (TCB) inapopakiwa. Sehemu hii ya habari kwa kawaida inarejelewa na kiteuzi katika gs. Tunadhania kuwa kigezo tunachotaka kiko katika kukabiliana na 0x10 na kupiga simu kwa anwani iliyohifadhiwa katika %gs:$0x10.

Matokeo.

Kwa kweli, katika mazoezi, si mara zote inawezekana kufikia ongezeko maalum la utendaji hata kwa usaidizi wa FSCF (Fast System Call Facility) kwenye jukwaa hili. Shida ni kwamba kwa njia moja au nyingine, mchakato mara chache hupata kernel moja kwa moja. Na kuna sababu nzuri za hii. Kutumia maktaba ya libc hukuruhusu kuhakikisha kubebeka kwa programu, bila kujali toleo la kernel. Na ni kupitia maktaba ya kawaida ya mfumo ambapo simu nyingi za mfumo huenda. Hata kama utaunda na kusakinisha kernel ya hivi punde iliyokusanywa kwa ajili ya jukwaa linaloauni FSCF, hii sio hakikisho la faida za utendakazi. Jambo ni kwamba maktaba ya mfumo wako libc.so bado itatumia int 0x80 na inaweza tu kushughulikiwa kwa kujenga upya glibc. Iwapo kiolesura cha VDSO na __kernel_vsyscall kwa ujumla vinatumika katika glibc, mimi, kusema ukweli, ninapata ugumu kujibu kwa sasa.

Viungo.

Ukurasa wa Manu Garg, http://www.manugarg.com
Tawanya/Kusanya mawazo na Johan Petersson, http://www.trilithium.com/johan/2005/08/linux-gate/
Mzee mzuri Kuelewa kernel ya Linux Ambapo bila hiyo :)
Na bila shaka, vyanzo vya Linux (2.6.22)