Kapu leírók

Ahhoz, hogy a különböző privilégium szinteken elhelyezkedő kódszegmenseket irányított, felügyelt módon érhessük el, a processzor speciális leírókat, úgynevezett kapu leírókat nyújt. Négy féle kapu leírót különböztetünk meg:

  • Call-kapu (Call gate)
  • Csapda kapu (Trap gate)
  • Megszakítás kapu (Interrupt gate)
  • Taszk kapu (Task gate)

A taszk kapukat a taszk váltás során használjuk. A csapda és megszakítás kapuk egy speciális fajtája a call-kapunak, melyeket a kivétel és megszakítás kezelő rutinok használnak. Ezeket itt részletesebben nem tárgyaljuk. Ez a fejezet a call-kapukat írja le.

Call-kapuk

A call-kapuk megkönnyítik a programoknak az irányított vezérlésátadást különböző privilégium szintek között. A call-kapukat jellegzetesen csak az operációs rendszerekben használják, melyek a privilégium szint védelmet valósítják meg segítségükkel. A call-kapuk továbbá használatosak a 16 bites és 32 bites kódszegmensek közötti irányított vezérlés átadásra.

Az alábbi ábra bemutatja a call-kapu leíró felépítését. A call-kapu leíró elhelyezhető a GDT vagy az LDT-ben, de nem helyezhető el az IDT-ben, a megszakítás leíró táblában. A call-kapu hat különféle szerepet lát el:

  • Meghatározza a kódszegmenst, melyet elkívánunk érni
  • Definiál egy belépési pontot (entry point) a kódszegmensben az elérni kívánt rutinra
  • Meghatározza a privilégium szintet a hívó számára ahhoz, hogy elérhesse a rutint
  • Ha verem váltás történik, meghatározza a vermek között átmásolandó lehetséges paraméterek számát
  • Definiálja a cél veremre lementendő értékek méretét: a 16 bites kapuk 16 bitet, míg a 32 bites kapuk 32 bitet mentenek le
  • Meghatározza, hogy a call-kapu érvényes -e

Call-kapu leíró

A szegmens kiválasztó mező a call-kapu leíróban meghatározza az elérni kívánt kódszegmenst. Az ofszet pedig belépési pontot szolgáltat a kódszegmensbe. Ez a belépési pont pedig általában a meghívott rutin az első végrehajtandó utasítását (annak címét) adja meg. A DPL mező megadja a call-kapu privilégium szintjét, azt a privilégium szintet, mellyel a hívónak rendelkeznie kell, hogy elérhesse a kívánt rutint a call-kapun keresztül. A P flag jelzi, hogy a call-kapu érvényes -e. (A call-kapu által mutatott kódszegmenst jelenlététét, annak szegmens leírójának P flagjét jelenti.) A paraméterek száma mező megadja verem váltás esetén az átmásolandó paraméterek számát a hívó verméből a meghívott, új kódszegmenshez tartozó verembe (lásd a 4.8.5-ös, a "Verem váltás" részt). A paraméterek száma megadja 16 bites call-kapu esetén az átmásolandó word-ök (szavak), illetve 32 bites call-kapu esetén az átmásolandó doubleword-ök (dupa szavak) számát.

Fontos, hogy a kapu leíróban a P flag alapállapotban mindig 1-re állított. Amennyiben 0, akkor egy nem jelenlévő, nem betöltött (#NP - not present) kivétel generálódik, amikor a program ezen nem jelenlévő szegmens leírójához kíván hozzáférni. Az operációs rendszer használhatja a P flaget speciális saját céljaira is. Például megfigyelhető segítségével, hogy a kaput hányszor használták. Ilyen esetben a P flag kezdőértéke legyen 0, és első használatakor meghívódik a nem jelenlévő kivételkezelő rutin. Ekkor a kivétel kezelő rutin növeli eggyel a hozzáférés számát megadó számlálót, majd a P flaget 1-re állítja. Így a kivétel kezelő rutinból való visszatérés után már érvényes lesz a kapu leíró.

Kódszegmens elérése call-kapun keresztül

Ahhoz, hogy a call-kaput elérhessük, egy a kapura mutató távoli mutatót (far pointer) szolgáltat a CALL, illetve JMP utasítás operandusa. Ebből a mutatóból a szegmens kiválasztó rész azonosítja a call-kaput (lásd az ábra); a mutatóban az ofszet szükséges, de a processzor által nem használt, s nem is vizsgált, így bármilyen értékre állítható.

Amikor a processzor elérte a call-kaput, akkor a call-kapuban található szegmens kiválasztót használja, hogy elérje a cél kódszegmens szegmens leíróját. (Ez a szegmens leíró lehet akár a GDT illetve akár az LDT-ben is.) Ezután kombinálja a kódszegmens leíróból nyert kezdőcímet a call-kapuban elhelyezett ofszettel, hogy lineáris címet képezzen az elérni kívánt kódszegmens rutinjának belépési pontjához.

Call-kapu működési mechanizmusa

Privilégium vizsgálat call-kapuval történő vezérlés átadás során

Ahogy a fenti ábra mutatja, négy különböző privilégium szintet vizsgál meg a processzor, hogy eldöntse érvényes -e a vezérlés átadás a call-kapun keresztül:

  • A CPL (aktuális privilégium szint).
  • Az RPL (a kérelmező privilégium szintje) a call-kapu kiválasztójában.
  • A DPL (leíró privilégium szint) a call-kapu leírójában.
  • És a cél kódszegmens leírójának DPL-je.

Továbbá a cél kódszegmens leírójában a C (illeszkedő) flag is vizsgált.

A privilégium vizsgálati szabályok különbözőek attól függően, hogy a vezérlés átirányítás CALL vagy JMP paranccsal történt -e. Az alábbi táblázat foglalja össze a privilégium vizsgálati szabályokat:

Utasítás

Privilégium vizsgálati szabályok

CALL

CPL £ call-kapu DPL; RPL £ call-kapu DPL

Az illeszkedő cél kódszegmens DPL £ CPL

A nem illeszkedő cél kódszegmens DPL £ DPL

JMP

CPL £ call-kapu DPL; RPL £ call-kapu DPL

Az illeszkedő cél kódszegmens DPL £ CPL

A nem illeszkedő cél kódszegmens DPL = DPL

Privilégium vizsgálati szabályok a call-kapuk esetén

A call-kapu leírójának DPL mezője meghatározza szám szerint azt a legmagasabb privilégium szintet ahonnan a hívó rutin még elérheti a call-kaput; ez a feltétele a call-kapu elérésének, hogy a hívó rutin CPL-jének kisebb vagy egyenlőnek kell lennie, mint a call-kapu DPL-jének. Például, az alábbi ábrában az A call-kapu DPL-je 3. Így bármely privilégium szintről (CPL lehet 0-tól 3-ig) elérhető a kapu, tehát tartalmazza mind az A, B, és C hívó rutinok kódszegmenseit. A B call-kapu DPL-je 2, így csak a 0, 1, 2 CPL-ü hívó rutinok érhetik el, tehát csak a B és C kódszegmensbeli rutinok számára hozzáférhető. A szaggatott vonal az ábrán azt jelenti, hogy az A kódszegmensbeli hívó rutin nem férhet hozzá a B call-kapuhoz.

A call-kapu szegmens kiválasztójában található RPL-nek a hívó CPL-jével megegyező követelményeket kell teljesítenie; tehát az RPL-nek kisebb vagy egyenlőnek kell lennie, mint a call-kapu DPL-jének. Például az ábrában, a C kódszegmensbeli hívó rutin hozzáférhet a B call-kapuhoz a B2 vagy B1 kapu kiválasztókat használva, de nem érheti el a B call-kaput a B3 kiválasztó használatával.

Ha a privilégium szint vizsgálat sikeres, a hívó rutin és a call-kapu között, akkor a processzor a továbbiakban összehasonlítja a kódszegmens leírójának DPL-jét a hívó rutin CPL-jével. Itt különböznek a privilégium szint vizsgálatok attól függően, hogy a CALL vagy JMP utasítás kezdeményezte a vezérlés átirányítást. Csak a CALL utasítás használhatja a call-kaput arra, hogy privilegizáltabb (szám szerint alacsonyabb privilégium szintű) nem illeszkedő kódszegmensbe irányítsa a vezérlést. Ez olyan esetben történik meg amikor a nem illeszkedő kódszegmens DPL-je kisebb, mint a CPL. A JMP utasítás nem illeszkedő kódszegmens esetén csak arra használhatja a call-kaput, hogy olyan (nem illeszkedő) kódszegmensbe adhatja át a vezérlést melynek DPL-je megegyezik a CPL-lel. A CALL és JMP utasítások egyaránt átirányíthatják a vezérlést egy privilegizáltabb illeszkedő kódszegmensbe; ez az amikor az illeszkedő kódszegmens DPL-je kisebb vagy egyenlő a CPL-lel.

Ha meghívott nem illeszkedő kódszegmens privilegizáltabb (szám szerint alacsonyabb privilégium szinttű), a CPL kisebb, mint a cél kódszegmens DPL-je, akkor verem váltás történik (lásd a "Verem váltás" c. részt). Ha a hívás vagy ugrás (CALL / JMP) egy privilegizáltabb, de illeszkedő cél kódszegmensbe történik, akkor a CPL nem változik meg és nem lép fel verem váltás.

Példa call-kapu elérésére különböző privilégium szintekről

A call-kapuk megengedik egy sima kódszegmensnek, hogy olyan rutinai legyenek melyeket különböző privilégium szintekről lehet elérni. Például ha az operációs rendszer egy kódszegmensben van, akkor lehet néhány olyan szolgáltatás, melyet mind az operációs rendszer és mind a felhasználói program használni kíván (pl. az olyan rutinok melyek a karakteres I/O-t látják el). Ezen rutinok számára a call-kaput beállíthatjuk úgy, hogy bármely privilégium szinről (0-tól 3-ig) elérhető legyen. A privilegizáltabb call-kapuk (0-s, vagy 1-es DPL-lel) beállíthatóak úgy, hogy csak az operációs rendszer szolgáltatásai (services) érhessék el, melyeket pedig az operációs rendszer fog használni (ilyenek pl az eszköz meghajtókat inicializáló rutinok).

Veremváltás

Valahányszor call-kaput használunk a program vezérlés átadására egy privilegizáltabb, nem illeszkedő kódszegmensbe (amikor a nem illeszkedő cél kódszegmens DPL-je kisebb, mint a CPL), akkor a processzor automatikusan a cél szegmens privilégium szintjének megfelelő veremre vált. Erre a verem váltásra azért van szükség, hogy elkerüljük a privilegizáltabb kódszegmensű programok összeomlását az elégtelen veremterület miatt (amit az alacsony privilégiumú programok futtatása okozhat). Továbbá még megakadályozza az alacsonyabb privilégiumú rutinokat abban, hogy érintkezhessen (véletlenül vagy szándékosan) a privilegizáltabb rutinokkal a vermen keresztül.

Minden taszknak 4 vermet kell definiálnia: egyet a felhasználó kód részére (a 3-as privilégium szintű kód) és egyet-egyet a további 2-es, 1-es és 0-s privilégium szintek számára. Csak a használt - a létező - privilégium szintek számára kell vermet definiálni, így ha csak kettő (pl. 3-ás és a 0-s) használt akkor elég csak nekik, kettőt definiálni. Mindegyik így definiált verem egymástól elkülönített szegmensben kell, hogy elhelyezkedjen és mindegyiket megfelelően szegmens kiválasztóval, illetve az ofszettel azonosítjuk (stack pointer - verem mutató).

A 3-as privilégium szint számára a verem kiválasztó és a verem mutató az SS illetve ESS regiszterben található, amikor 3-as privilégium szintű kódot kívánunk végre hajtatni. Ha verem váltás történik, akkor automatikusan elmentődik az új verembe a hívó (a kódszegmens melyről elváltottunk) verem kiválasztója és verem mutatója.

Mutatók a 0-s, 1-es és 2-es privilégium szintű vermekre az éppen futó taszk TSS-ében találhatóak (lásd az alábbi ábrát illetve a teljes ábrát: 6.2-es ábra).

6.2. ábra: 32 bites Taszk állapot szegmens (TSS). Részlet.

Minden verem mutató (amint az ábrán is megfigyelhető) tartalmaz egy (verem) szegmens kiválasztót és egy verem mutatót (az ESP regiszterbe kell tölteni). Ezen kezdeti mutatók szigorúan csak olvasható értékűek. A processzor nem változtatja meg őket míg a taszk fut. A (kezdeti) mutatókat csak arra használja a processzor, hogy új vermet hozzon létre, amikor privilegizáltabb (szám szerint alacsonyabb számú privilégium szint) kódszegmenst hívunk meg. Ezek a vermek felszabadulnak a hívott rutinból való visszatérés után. Amikor a rutint újra meghívjuk, akkor újra létrehoz a processzor egy új vermet a kezdeti mutatókkal. (A TSS, amint a fenti 6.2-es ábra részlet is mutatja, nem tartalmaz a 3-as privilégium szint számára verem kiválasztót vagy mutatót, ugyanis a processzor nem engedélyezi a program vezérlés átirányítást a 0, 1, 2-es CPL-ról egy 3-as CPL-ü rutinra kivéve, ha ilyen (0, 1, 2 CPL-ü) rutinból térünk vissza az őt meghívóra.)

Az operációs rendszer a felelős azért, hogy minden használt privilégium szinthez készítsen verem leírót és, hogy ezen leírókra mutató kiválasztókat és a verem mutatókat a TSS-be másolja. Minden veremnek írható/olvashatónak kell lennie (a szegmens leíró típus mezője adja meg) és megfelelő nagyságúnak kell lennie (a határ (limit) mező adja meg), hogy az alábbi elemeket eltárolhassa:

  • A hívó rutin SS, ESP, CS és EIP regisztereinek tartalmát el kell menteni.
  • A hívott rutinnak átadandó paramétereket, illetve az ideiglenes változókat is el kell tárolnia.
  • Az EFLAGS regisztert, illetve a hiba kódot is el kell tudnia tárolni, ha implicit hívás történik egy kivételt vagy megszakítást lekezelő rutinra.

A vermen továbbá még kellhet annyi hely, hogy tartalmazhassa a további meghívott rutinok hasonló eltárolandó elemeit. Ugyanis a rutinok maguk is meghívnak más rutinokat, és az operációs rendszer is használhatja az egymásba ágyazott, többszörös megszakításokat. Minden veremnek olyan nagynak kell lennie, hogy a legrosszabb esetre is felkészítsük, az-az nagyobb nagyságú paraméter listával és nagyon mély hívás egymásba ágyazással kell számolni az adott privilégium szinten, hogy elkerüljük az esetleges program hibát.

(Ha az operációs rendszer nem használja a processzor multitaszking mechanizmusát, akkor is létre kell hozni legalább egy TSS-t, részint a verem szintű követelmények teljesítése végett.)

Amikor egy rutin call-kapun keresztül hív egy másik rutint, akkor privilégium szint váltás történik, és a processzor az alábbi lépéseket hajtja végre a verem váltás megvalósításához, illetve hívott rutin új privilégium szinten történő futtatásához:

  1. Használja a cél kódszegmens DPL-jét (az új CPL-t) mutatót képezve a TSS-ben elhelyezett új veremre (szegmens kiválasztó és verem mutató).
  2. Kiolvassa az új verem (a verem amire váltunk) szegmens kiválasztóját és verem mutatóját az aktuális TSS-ből. Bárminemű határ sértés a verem szegmens kiválasztó vagy verem mutató vagy a verem szegmens leíró olvasása esetén, érvénytelen TSS (#TS) kivétel generálását vonja maga után.
  3. Megvizsgálja a verem szegmens leírót a megfelelő jogosultságért és típusért, bármilyen jog illetve típus sértés hasonlóan érvénytelen TSS (#TS) kivétel generálását vonja maga után.
  4. Átmenetileg elmenti az SS és ESP regiszterek aktuális értékét.
  5. Betölti az SS és ESP regiszterekbe a verem szegmens kiválasztót és verem mutatót.
  6. Lenyomja az ideiglenesen elmentett SS és ESP regisztereket (a hívó rutin verem kiválasztója és verem mutatója) az új verembe (lásd ábra).
  7. Átmásolja a call-kapuban (a call-kapu paraméterek száma mezőben) meghatározott számú paramétert, a hívó verméből az új verembe. Ha a paraméterek száma 0, akkor nem másolódik át paraméter.
  8. Lenyomja az új verembe a visszatérési utasítás mutatót (a CS és EIP regiszterek aktuális értékét).
  9. Betölti a CS regiszterbe az új kódszegmens kiválasztót, és az EIP-be az új utasítás mutatót (a belépési pont ofszetje). És végül megkezdődik az meghívott rutin futtatása.

Ezt a hívási mechanizmust bővebben a 2. felhasznált irodalom 3. fejezete az "Utasításkészlet referencia", a CALL utasítás leírásánál tárgyalja. Itt bővebben tárgyalt a processzor által call-kapun keresztüli hívás esetén végrehajtott a privilégium szint és egyéb vizsgálat.

Verem váltás privilégium szintek közötti hívás során

A call-kapuban a paraméterek száma mező meghatározza azon adat elemek számét (egészen 31-ig) melyeket a processzor átmásol a hívó rutin verméből a hívott rutin vermébe. Ha több mint 31 paramétert kell átadni a meghívott rutinnak, akkor az egyik paraméter lehet egy mutató, mely egy a maradék átadandó paraméterek által létrehozott adatstruktúrára mutat, vagy az elmentett régi (az új rutint meghívó régi rutin regiszterei) SS és ESP regiszterek is használhatóak, hogy elérhessük a régi verem területen a paramétereket. (Ez elméletileg sem sérti a biztonsági megfontolásokat, ugyanis csak olvasunk a régi veremből. Ugyanakkor multiuser-es környezetű operációs rendszer felépítésekor óvatosan bánjunk ezen lehetőséggel is.) A meghívott rutin számára átadandó adatelemek méretét a call-kapu mérete határozza meg, amint ezt a "Call-kapuk" rész leírja.

Visszatérés a meghívott rutinból

A RET utasítás használható egy közeli visszatérés (near return) végrehajtására, a távoli visszatérés (far return) egyaránt használható az azonos privilégium szinten belüli visszatérésre illetve egy másik privilégium szintre való visszatérésre. Ez az utasítás feltételezi, hogy a hívott rutinra CALL utasítással jutottunk el. Nem támogatja a visszatérést egy JMP-vel elért utasításról, mert a JMP utasítás nem ment el visszatérési utasítás mutatót a vermen.

A közeli visszatérés csak az aktuális kódszegmensen belül hajt végre vezérlés átirányítást; viszont a processzor határ vizsgálatot végez. Amikor a processzor kiemeli a veremből a visszatérési utasítás mutatót az EIP regiszterbe, akkor végez vizsgálatot, hogy a pointer nem haladja -e meg az aktuális kódszegmens méretét.

Azonos privilégium szinten a távoli visszatérés esetén, a processzor kiemeli a veremből mind a szegmens kiválasztót melyre majd a vezérlés visszatér, és mind az utasítás mutatót, mely helyen a végrehajtás folytatódik. Normál körülmények között ezek a mutatók valósak, hiszen a CALL utasítás által lettek a veremre lementve. Ennek ellenére, a processzor végrehajt privilégium vizsgálatot, hogy lekezelje azon eseteket mikor a rutin (véletlenül) módosítja a mutatót, vagy a verem integritása véletlenül megsérült egyéb hiba miatt.

Távoli visszatérésnél mikor privilégium szintet váltunk, csak kevésbé privilegizált szintre térhetünk vissza (amikor a kódszegmens DPL-je melyre visszatérünk szám szerint nagyobb, mint a CPL). A processzor használja a hívó rutin elmentett a CS regiszter RPL mezőjét (lásd ábra), hogy meghatározza, ha magasabb privilégium szint szükséges. Ha az RPL szám szerint nagyobb (kevésbé privilegizált), mint a CPL, akkor privilégium szinteken keresztüli visszatérés történik.

A processzor az alábbi lépéseket hajtja végre távoli rutinból való visszatérés esetén:

A verem a közeli és a távoli hívások esetén

Verem váltás más a privilégium szintre történő hívás esetén

  1. A processzor megvizsgálja az elmentett a CS regiszter RPL mezőjét, hogy szükség van -e privilégium szint váltásra a visszatérés során.
  2. Betölti a CS és EIP regiszterekbe a hívott rutin vermébe elmentett megfelelő értékeket. (Típus és privilégium szint vizsgálatot végez a processzor a kódszegmens leírón és a kódszegmens kiválasztó RPL-jén.)
  3. (Ha a RET utasítás tartalmaz egy paraméterek száma operandust és a visszatérés privilégium szint váltást igényel.) Hozzáadódik a paraméterek száma (bájtban mérve, a RET utasítás operandusából véve) az aktuális ESP regiszter értékéhez (miután ki lett véve a régi - hívó CS és EIP regiszter értéke), hogy átugorjuk a régi, most már szükségtelen paramétereket. Ezen művelet elvégzése után az ESP regiszter által mutatott elem a veremben az elmentett SS és ESP, a hívó rutin vermének mutatói (lásd a fenti két ábrát). (Figyelem, a RET utasításnál megadható paraméterek számának mérete bájtban meg kell, hogy egyezzen a call-kapuban megadott paraméterek számának szorozva egy paraméter méretével kialakított bájtok számával. Ennek hibás megadása általános védelmi hibát (#GP) vonhat maga után.)
  4. (Ha a visszatérés privilégium szint váltást igényel.) Betöltődik az SS és ESP regiszterekbe a hívó vermén elmentett SS és ESP értékek (amikhez az előző pontban jutottunk hozzá a paraméterek felszabadításával), majd megtörténik a hívó vermére történő verem visszaváltás. A meghívott rutin SS és ESP regisztereinek értéke (melyek immáron felülíródtak) megsemmisülnek, ugyanis nincs rájuk szükség a továbbiakban. Bármilyen határ sértés a veremszegmens kiválasztó illetve a verem mutató regiszterbe töltése során általános védelmi hibát (#GP) generál. Az új veremszegmens leírót is megvizsgálja a processzor, a típusát és privilégiumait tekintve.
  5. (Ha a RET utasítás tartalmaz egy paraméterek száma operandust.) Hozzáadja a processzor a paraméterek számát (bájtban mért, a RET utasítás operandusából véve) az aktuális ESP regiszterhez, hogy visszafelé átlépjünk a hívó rutin vermében a paramétereket. Ezután a lépés után az ESP már nem vizsgált határ szempontjából. Ha az ESP értéke a szegmens határon túl van, akkor azt csak a következő verem művelet során észleljük hibaként.
  6. (Ha a visszatérés privilégium szint váltást igényel.) Megvizsgálja a processzor a DS, ES, FS, és GS szegmens regisztereket. Ha bármelyik regiszternek ezek közül a DPL-je kisebb, mint az új CPL (kivéve az illeszkedő kódszegmenseket), akkor azon szegmens regiszterek null szegmens kiválasztóval lesznek feltöltve.

A RET utasítás bővebb leírását lásd a 2. felhasznált irodalom 3. fejezete az "Utasításkészlet referencia", RET utasítás helyen. Itt bővebben tárgyalt a privilégium szint és egyéb védelmi vizsgálat, amikor a processzor távoli visszatérést hajt végre.