Mälumudelid ja protsessori ehitus

Operatsioonisüsteem DOS loodi algselt Intel 8086 tüüpi protsessoritega arvutitel (IBM XT ja temaga ühilduvad). Sellest on tingitud hulgaliselt Intel-protsessoritega arvutitel kasutatava C - keele eripärasid, mida nüüd seletame.

Protsessor i8086 (ja kõik temaga ühilduvad) omasid 14 registrit, 16-bitist andme- ja 20-bitist aadressisiini. Kõik registrid on 16-bitised. Need on nagu omamoodi protsessorisisesed mälupesad, kuhu saab salvestada väärtusi ja nendega opereerida. Nimetatud registrid jaotatakse omakorda üldisteks-, segmendi-, viida- ja tähiseregistriteks. Üldiseid registreid kasutatakse suuremas osas operatsioonides. Nende sisuga saab sooritada matemaatilisi-, loogilisi- ja muid tehteid. Segmendiregistrites salvestatakse segmentide algusaadresse. Viidaregistrid on vajalikud kahe segmendi vahel andmete vahetamisel ja paljudel muudel juhtudel. Tähisteregister jagatakse bittide kaupa eraldi tähisteks. Igaüks neist omab mingit omaette tähendust. Tähised näitavad matemaatiliste operatsioonide puhul tehtud vigadele, märgivad tingimuste tulemusi jne.


Joonis 7: Protsessori i8086 ehitus

Joonis 7 ei ole muidugi täiuslik. Tegelikult ei ole andmesiin ja aadressisiin ühendatud mälu ja väljundportidega otse, vaid üle siinikontrolleri. Peale seda omavad suurt tähtsust ka katkestustekontroller, kell ja muud arvuti osad.

Selleks, et mingi mälupesa sisu lugeda või muuta, tuleb aadressisiinile väljastada tema aadress ja kas lugeda andmesiinilt tema väärtus või väljastada tema uus väärtus. Kuna kõik registrid on 16-bitised, siis saab ühe registri sisuga adresseerida vaid 216 = 65.535 = 64 KB mälu. Protsessor i8086 suudab aga kasutada kuni 1 MB mälu. Selleks jaotab ta mälu eraldi osadesse - segmentidesse. Ühte segmendiregistrisse salvestatakse nüüd soovitava segmendi algusaadress ja seejärel piisab ühestainsast registrist, määramaks aadressi segmendi piires (tema algusest lugedes). Lõplik aadress luuakse nii, et segmendi aadress (samuti 16-bitine arv) nihutatakse 4 biti võrra vasakule (sama, mis 16-ga korrutamine) ja talle liidetakse aadress segmendi piires (offset). Tulemuseks on 20-bitine arv, mis ongi mälupesa aadress. Kuna 220 = 1 MB, siis suudab protsessor i8086 adresseerida kuni 1 MB mälu. Olgu näiteks soovitud mälupesa segmendi aadress A000 hex ja aadress segmendi piires 12D0 hex. Nihutades segmendi aadressi 4 biti võrra vasakule, saame A0000 hex ja liites sellele aadressi 12D0, saame lõpliku aadressina A12DO hex. Kuna segmendiregistrite sisu korrutatakse alati 16-ga enne liitmist segmendi siseseaadressiga, siis tuleneb sellest, et minimaalne segmendi suurus on 16 baiti. Tõsiasjast, et segmendisisene aadress peab mahtuma 16-bitisesse registrisse tuleneb aga, et segmendi maksimaalne suurus saab olla vaid 64 KB. Üks kilobait 1 KB = 1024 (ehk 210) baiti, mitte 1000 baiti.

Uuemad arvutid kasutavad protsessoreid i80286, i80386 ja i80486. Protsessor i80286 omab samuti 16-bitist andmesiini, kuid 24-bitist aadressisiini ja suudab seega adresseerida kuni 16 MB mälu. Protsessor i803886 ja i80486 omavad nii 32-bitist andme- kui ka aadressisiini ja suudavad seega adresseerida otseselt (ilma segmenteerimiseta) kuni 4 GB (gigabaiti) ja segmentide kasutamisel kuni 64 TB (terabaiti) mälu. Peale selle on nende protsessorite registrid kõik 32 biti laiused. Operatsioonisüsteemis DOS töötavad aga ka need protsessorid reaalrezhiimis, mis teeb nad täpselt sarnaseks oma eelkäija i8086-ga. Selles tööreshiimis ei suuda nad kasutada oma suuri andme- ja aadressisiine. Kuna selles raamatus käsitletakse vaid operatsioonisüsteemis DOS kasutatavaid C - keele variante, siis piirdume siin 8086 poolt loodud adresseerimisega. Protsessorite 80386 ja 80486 32 biti laiuseid registreid saab samuti kasutada vaid erirezhiimis. Nende nimedele lisatakse siis täht 'E' (extended), näiteks EAX, EBX. Reaalrezhiimis kasutatakse vanade tuntud nimede AX, BX all vaid nende registrite alumisi osasid (esimest 16 bitti) .

Adresseerimisel on vaja niisiis kahe registri koostööd. Ühes asub segmendi- ja teises segmendisisene aadress. Segmendiaadresside jaoks kasutatakse segmendiregistreid. Register CS (code segment) sisaldab hetkelise koodisegmendi aadressi ja register IP (instruction pointer) näitab selle segmendi sees järgmisele täidetavale masinkoodis käsule. Register DS (data segment) näitab programmi andmesegmendile. Andmesegmendist andmeid lugedes või sinna kirjutades võib kasutada segmendisisese aadressi jaoks suvalist üldist registrit või ka otsest aadressi masinkoodi osana. Register SS (stack segment) näitab programmi pinu sisaldavale segmendile. Tavaliselt on see programmi andmesegment. Pinusegmenti täidetakse ülevalt allapoole. Register SP (stack pointer) näitab pinusegmendi viimasele sissekandele (lõpust lugedes). Kui pinusse midagi sisestatakse, siis vähendatakse registri SP väärtust. Programmid kasutavad veel registrit BP (base pointer), osutamaks funktsiooni kohalike muutujate algusesse pinul. Selle registri abil loetakse funktsioonide kohalikke muutujaid. Registrit ES (extra segment) kasutatakse tavaliselt andmete transporteerimiseks mingist muust segmendist programmi andmesegmenti. Seejuures osutab register SI (source index) kopeeritavate andmete algusele ja register DI (destination index) andmete jaoks ettenähtud puhvrile. Protsessor 8086 tunneb paari operatsiooni, mis transporteerivad määratud arvu baite nõutud kohast teise ja seejuures ka iseseisvalt muudavad registreid DI ja SI. Transporteeritavate baitide arv salvestatakse seejuures registrisse CX (count).

Üldised registrid AX, BX, CX ja DX lubavad kasutada oma ülemisi ja alumisi 8-bitiseid osasid ka eraldi. Nende nimed on AH (high) ja AL (low) ning vastavalt BH, BL jne. Need 8-bitised registrid on pärit veel protsessori 8086 eelkäijalt, mis oli 8-bitine protsessor. Selleks, et lihtsustada tolle protsessori jaoks loodud tarkvara ülekandmist protsessorile 8086, oli vaja võimaldada nonde 8-bitiste registrite kasutamist.

Programmeerimiskeel C tunneb kolme tüüpi viitasid: near, far ja huge. Lühikesed viidad (near) on 16-bitised väärtused ja sisaldavad vaid segmendisisese aadressi. Neid on lihtne kasutada ja nende kasutamine on ka kiirem. Kui near viit näitab mingile andmestruktuurile, siis oletatakse, et see andmestruktuur asub programmi oma andmesegmendis ja segmendiregistrina kasutatakse registrit DS. Nii on see alati programmi staatiliste (globaalsete) muutujatega ja programmi andmesegmendist reserveeritud mälublokkidega. Kui near viit näitab mingile funktsioonile, siis kasutatakse registrit CS.

Pikad (far) viidad on 32-bitised muutujad. Nad sisaldavad nii segmendi-, kui ka segmendisisese aadressi. Nende kasutamine on natuke aeglasem, kuna see nõuab segmendiregistrite CS või DS sisu muutmist. Mälu reserveerimisel üldisest mälust (FAR HEAP) on nad aga ilmtingimata vajalikud.

Viimane tüüp (huge) märgib viitasid, mis samuti nagu far viidad sisaldavad nii segmendi-, kui ka segmendisisese aadressi. Nende viitade nihutamine kontrollib aga programmi segmendipiire. Tavaline pikk viit võib küll näidata teise segmenti, kuid niipea kui teda nihutatakse üle tolle segmendi piiri, muutub vaid viida segmendisisene osa ja seega näitab viit uuesti tolle segmendi algusesse. Kui te soovite kasutada suuremaid mälublokke kui 64 KB, siis peate kasutama huge viitasid. Nende viitade nihutamisel üle 64 KB piiri suurendadatse ka viida segmendiaadressi määravat osa. Teine probleem pikkade viitadega (far) on see, et kaks far viita võivad näidata samasse mälupunkti, kuid omada erinevaid segmendi- ja segmendisiseseid aadresse. Seega ei saa neid osuteid võrrelda loogiliste tehetega. Nimetatud huge tüüpi viitade sisu on aga alati normaliseeritud, s.o. see on viidud teatud standardsele kujule. Seepärast omavad kaks samasse mälupunkti osutavat huge viita alati sama väärtust.

Viidad tüübist huge nõuavad aga erilist programmiloogikat, mis nende sisu pidevalt normaliseerib ja segmendipiiride ületamisel õige aadressi arvutab. Seda kõike teeb translaator, kuid need "üleliigsed" operatsioonid teevad huge viitade kasutamise natuke aeglasemaks.

Milliseid viitasid te kasutate, sõltub valitud mälumudelist. Iga mälumudel määrab teatud kindlad viidapikkused andmete ja koodi jaoks. Te võite aga alati ise mingi viida tüübi määrata, kasutades võtmesõnu: near, far ja huge. Näiteks:

char	near    *pTmp1;
char	far	*pTmp2;
char	huge	*pTmp3;