Si të zhvilloni një sistem operativ për një kompjuter. Os - Çfarë nevojitet për të shkruar një sistem operativ? Si të krijoni sistemin tuaj operativ


Nëse arrijmë te pika...

Një OS është një gjë që zbaton multitasking (zakonisht) dhe menaxhon shpërndarjen e burimeve midis këtyre detyrave dhe në përgjithësi. Ju duhet të siguroheni që detyrat nuk mund të dëmtojnë njëra-tjetrën dhe të funksionojnë në fusha të ndryshme të kujtesës dhe të punoni me pajisje nga ana tjetër, të paktën kjo është. Ju gjithashtu duhet të siguroni aftësinë për të transmetuar mesazhe nga një detyrë në tjetrën.

Gjithashtu, OS, nëse ka memorie afatgjatë, duhet të sigurojë akses në të: domethënë të sigurojë të gjitha funksionet për të punuar me sistemin e skedarëve. Është minimale.

Pothuajse kudo, kodi i parë i nisjes duhet të shkruhet në gjuhën e asamblesë - ka një sërë rregullash se ku duhet të jetë, si duhet të duket, çfarë duhet të bëjë dhe çfarë madhësie nuk duhet të kalojë.

Për një PC, duhet të shkruani një ngarkues në ASMA, i cili do të thirret nga BIOS dhe i cili duhet, pa i kaluar katër e disa qindra bajt, të bëjë diçka dhe të nisë OS kryesor - të transferojë kontrollin në kodin kryesor, i cili në e ardhmja e afërt mund të shkruhet në C.

Për ARM, duhet të krijoni një tabelë ndërprerjesh në ACMA (rivendosje, gabime të ndryshme, ndërprerje IRQ, FIQ, etj.) dhe të transferoni kontrollin në kodin kryesor. Megjithëse, në shumë mjedise zhvillimi, një kod i tillë është i disponueshëm për pothuajse çdo kontrollues.

Kjo do të thotë, për këtë ju duhet:

  1. Njihni gjuhën e asemblerit të platformës së synuar.
  2. Njihni arkitekturën e procesorit dhe të gjitha llojet e komandave dhe regjistrave të shërbimit në mënyrë që ta konfiguroni që të funksionojë në modalitetin e dëshiruar. Në një PC, ky është një kalim në modalitetin e mbrojtur, për shembull, ose në modalitetin 64-bit... Në ARM, kjo është vendosja e klockimit të bërthamës dhe pajisjeve periferike.
  3. Dijeni saktësisht se si do të fillojë OS, ku dhe si duhet të shtyni kodin tuaj.
  4. Njihni gjuhën C - është e vështirë të shkruani kod të madh në Asma pa përvojë, mirëmbajtja e tij do të jetë edhe më e vështirë. Prandaj, duhet të shkruani kernelin në C.
  5. Njihni parimet e funksionimit të OS. Epo, ka shumë libra në rusisht për këtë temë, megjithëse nuk e di nëse janë të gjithë të mirë.
  6. Kini shumë e shumë durim dhe këmbëngulje. Do të ketë gabime dhe ato do të duhet të gjenden dhe korrigjohen. Ju gjithashtu do të duhet të lexoni shumë.
  7. Keni shumë e shumë kohë.

Me tutje. Le të themi se keni shkruar diçka. Duhet ta testojmë këtë gjë. Ose ju duhet një pajisje fizike në të cilën do të zhvillohen eksperimentet (bordi i korrigjimit, kompjuteri i dytë), ose një emulator për të. E dyta është zakonisht më e lehtë dhe më e shpejtë për t'u përdorur. Për PC, për shembull, VMWare.

Ka edhe mjaft artikuj mbi këtë temë në internet, nëse kërkoni mirë. Ka edhe shumë shembuj të sistemeve operative të gatshme me kode burimore.

Nëse vërtet dëshironi, mund të shikoni edhe kodin burimor të kernelit të vjetër të sistemeve NT (Windows), si veçmas (i cili është postuar nga Microsoft, me komente dhe lloje të ndryshme materialesh referimi), ashtu edhe në lidhje me të vjetrën OS (rrjedhur).

Zhvillimi i një kerneli me të drejtë konsiderohet jo një detyrë e lehtë, por çdokush mund të shkruajë një kernel të thjeshtë. Për të përjetuar magjinë e hakimit të kernelit, ju vetëm duhet të ndiqni disa konventa dhe të zotëroni gjuhën e asemblerit. Në këtë artikull do t'ju tregojmë se si ta bëni këtë.


Përshendetje Botë!

Le të shkruajmë një kernel që do të niset nëpërmjet GRUB në sistemet e përputhshme me x86. Kerneli ynë i parë do të shfaqë një mesazh në ekran dhe do të ndalet atje.

Si nisin makinat x86

Para se të mendojmë se si të shkruajmë një kernel, le të shohim se si një kompjuter niset dhe transferon kontrollin në kernel. Shumica e regjistrave të procesorit x86 kanë vlera specifike pas nisjes. Regjistri i treguesit të instruksionit (EIP) përmban adresën e instruksionit që do të ekzekutohet nga procesori. Vlera e tij e koduar është 0xFFFFFFFF0. Kjo do të thotë, procesori x86 do të fillojë gjithmonë ekzekutimin nga adresa fizike 0xFFFFFFF0. Këto janë 16 bajtët e fundit të hapësirës së adresave 32-bit. Kjo adresë quhet vektori i rivendosjes.

Karta e kujtesës që gjendet në chipset thotë se adresa 0xFFFFFFF0 i referohet një pjese specifike të BIOS-it dhe jo RAM-it. Sidoqoftë, BIOS kopjon veten në RAM për qasje më të shpejtë - ky proces quhet "hije", duke krijuar një kopje hije. Pra, adresa 0xFFFFFFF0 do të përmbajë vetëm një udhëzim për të kaluar në vendndodhjen në memorie ku BIOS ka kopjuar veten.

Pra, BIOS fillon të ekzekutohet. Së pari, ai kërkon pajisje nga të cilat mund të niset në rendin e specifikuar në cilësimet. Ai kontrollon median për një "numër magjik" që dallon disqet e bootable nga ato të rregullta: nëse bajtet 511 dhe 512 në sektorin e parë janë 0xAA55, atëherë disku është i bootable.

Pasi BIOS të gjejë pajisjen e nisjes, do të kopjojë përmbajtjen e sektorit të parë në RAM, duke filluar në adresën 0x7C00, dhe më pas do të zhvendosë ekzekutimin në atë adresë dhe do të fillojë të ekzekutojë kodin që sapo ngarkoi. Ky kod quhet bootloader.

Bootloader ngarkon kernelin në adresën fizike 0x100000. Kjo është ajo që përdorin kernelet më të njohura x86.

Të gjithë procesorët e pajtueshëm me x86 fillojnë në një modalitet primitiv 16-bit të quajtur "real mode". Ngarkuesi GRUB e kalon procesorin në modalitetin e mbrojtur 32-bit duke vendosur bitin e poshtëm të regjistrit CR0 në një. Prandaj, kerneli fillon të ngarkohet në modalitetin e mbrojtur 32-bit.

Vini re se GRUB, në rastin e kernelit Linux, zgjedh protokollin e duhur të nisjes dhe nis kernelin në modalitetin real. Kernelet Linux kalojnë automatikisht në modalitetin e mbrojtur.

Çfarë na duhet

  • Kompjuter i pajtueshëm x86 (natyrisht)
  • Linux
  • Montues NASM,
  • ld (Lidhës GNU),
  • GRUB.

Pika e hyrjes në gjuhën e asamblesë

Natyrisht, do të dëshironim të shkruanim gjithçka në C, por nuk do të jemi në gjendje të shmangim plotësisht përdorimin e asemblerit. Ne do të shkruajmë një skedar të vogël në asembler x86 që do të bëhet pika fillestare për kernelin tonë. Gjithçka që do të bëjë kodi i montimit është të thërrasë një funksion të jashtëm që do ta shkruajmë në C dhe më pas të ndalojmë ekzekutimin e programit.

Si mund ta bëjmë kodin e montimit pikënisje për kernelin tonë? Ne përdorim një skript lidhës që lidh skedarët e objekteve dhe krijon skedarin përfundimtar të ekzekutueshëm të kernelit (do të shpjegoj më shumë më poshtë). Në këtë skript, ne do të tregojmë drejtpërdrejt se duam që binari ynë të shkarkohet në adresën 0x100000. Kjo është adresa, siç kam shkruar tashmë, në të cilën ngarkuesi pret të shohë pikën e hyrjes në kernel.

Këtu është kodi i asemblerit.

bërthama.asm
bit 32 seksioni .tekst starti global starti i jashtëm i kmain: cli mov esp, stack_space thirr kmain seksion hlt .bss resb 8192 stack_space:

Instruksioni i parë i biteve 32 nuk është assembler x86, por një direktivë NASM që i thotë të gjenerojë kod që procesori të funksionojë në modalitetin 32-bit. Kjo nuk është e nevojshme për shembullin tonë, por është praktikë e mirë ta tregojmë në mënyrë eksplicite.

Rreshti i dytë fillon seksionin e tekstit, i njohur gjithashtu si seksioni i kodit. I gjithë kodi ynë do të shkojë këtu.

globale është një tjetër direktivë NASM, ajo deklaron se simbolet në kodin tonë janë globale. Kjo do të lejojë që lidhësi të gjejë simbolin e fillimit, i cili shërben si pika jonë e hyrjes.

kmain është një funksion që do të përcaktohet në skedarin tonë kernel.c. extern deklaron se funksioni është deklaruar diku tjetër.

Më pas vjen funksioni start, i cili thërret kmain dhe ndalon procesorin me instruksionin hlt. Ndërprerjet mund ta zgjojnë procesorin pas hlt, kështu që ne fillimisht çaktivizojmë ndërprerjet me instruksionin cli (pastroni ndërprerjet).

Idealisht, ne duhet të ndajmë një sasi të memories për stivën dhe të drejtojmë treguesin e stivës (esp) në të. GRUB duket se e bën këtë për ne gjithsesi, dhe në këtë pikë treguesi i stivës është vendosur tashmë. Sidoqoftë, për çdo rast, le të ndajmë pak memorie në seksionin BSS dhe të drejtojmë treguesin e stivës në fillimin e tij. Ne përdorim instruksionin resb - ai rezervon memorien e specifikuar në byte. Më pas lihet një shenjë që tregon skajin e pjesës së rezervuar të kujtesës. Pak përpara se të thirret kmain, treguesi i stivës (esp) drejtohet në këtë zonë me anë të instruksionit mov.

Kernel në C

Në skedarin kernel.asm kemi thirrur funksionin kmain(). Pra, në kodin C, ekzekutimi do të fillojë nga atje.

bërthama.c
void kmain(void) ( const char *str = "kerneli im i parë"; char *vidptr = (char*)0xb8000; i panënshkruar int i = 0; i panënshkruar int j = 0; ndërsa (j< 80 * 25 * 2) { vidptr[j] = " "; vidptr = 0x07; j = j + 2; } j = 0; while(str[j] != "\0") { vidptr[i] = str[j]; vidptr = 0x07; ++j; i = i + 2; } return; }

Gjithçka që kerneli ynë do të bëjë është të pastrojë ekranin dhe të printojë rreshtin kernelin tim të parë.

Së pari, ne krijojmë një tregues vidptr që tregon adresën 0xb8000. Në modalitetin e mbrojtur, ky është fillimi i kujtesës video. Kujtesa e ekranit të tekstit është thjesht pjesë e hapësirës së adresave. Një pjesë e memories ndahet për hyrjen/daljen e ekranit, e cila fillon në adresën 0xb8000; në të vendosen 25 rreshta me 80 karaktere ASCII.

Çdo karakter në kujtesën e tekstit përfaqësohet nga 16 bit (2 bajt), në vend të 8 bit (1 bajt) me të cilët jemi mësuar. Bajt i parë është kodi ASCII i karakterit, dhe bajt i dytë është atribut-byte. Ky është një përkufizim i formatit të karakterit, duke përfshirë ngjyrën e tij.

Për të nxjerrë karakterin e gjelbër në të zezë, duhet të vendosim s në bajtin e parë të kujtesës video dhe vlerën 0x02 në bajtin e dytë. 0 këtu do të thotë sfond i zi dhe 2 do të thotë ngjyrë jeshile. Ne do të përdorim një ngjyrë gri të lehtë, kodi i saj është 0x07.

Në ciklin e parë while, programi plotëson të 25 rreshtat me 80 karaktere me karaktere boshe me atributin 0x07. Kjo do të pastrojë ekranin.

Në ciklin e dytë while, vargu i përfunduar me null kerneli im i parë shkruhet në memorien e videos dhe çdo karakter merr një atribut-byte prej 0x07. Kjo duhet të nxjerrë një varg.

Paraqitja

Tani duhet të përpilojmë kernel.asm në një skedar objekti duke përdorur NASM, dhe më pas të përdorim GCC për të përpiluar kernel.c në një skedar tjetër objekti. Detyra jonë është t'i lidhim këto objekte në një kernel të ekzekutueshëm të përshtatshëm për ngarkim. Për ta bërë këtë, do të na duhet të shkruajmë një skript për lidhësin (ld), të cilin do ta kalojmë si argument.

lidhje.ld
OUTPUT_FORMAT(elf32-i386) HYRJA (fillimi) SECTIONS ( . = 0x100000; .tekst: ( *(.text) ) .data: ( *(.data) ) .bss: ( *(.bss) ) )

Këtu fillimisht vendosëm formatin (OUTPUT_FORMAT) të skedarit tonë të ekzekutueshëm në ELF 32-bit (Format i ekzekutueshëm dhe i lidhur), një format standard binar për sistemet e bazuara në Unix për arkitekturën x86.

HYRJA merr një argument. Ai specifikon emrin e simbolit që do të shërbejë si pikë hyrëse e skedarit të ekzekutueshëm.

SECTIONS është pjesa më e rëndësishme për ne. Këtu ne përcaktojmë paraqitjen e skedarit tonë të ekzekutueshëm. Ne mund të përcaktojmë se si do të kombinohen seksionet e ndryshme dhe ku do të vendoset secili seksion.

Në kllapat kaçurrelë që ndjekin shprehjen SECTIONS, pika tregon numëruesin e vendndodhjes. Inicializohet automatikisht në 0x0 në fillim të bllokut SECTIONS, por mund të ndryshohet duke caktuar një vlerë të re.

Kam shkruar më herët se kodi i kernelit duhet të fillojë në adresën 0x100000. Kjo është arsyeja pse numëruesit të pozicionit i caktojmë vlerën 0x100000.

Hidhini një sy rreshtit.tekst: ( *(.tekst) ) . Ylli këtu specifikon një maskë që mund të përputhet me çdo emër skedari. Prandaj, shprehja *(.text) nënkupton të gjitha seksionet .tekst hyrëse në të gjithë skedarët hyrës.

Si rezultat, lidhësi do të bashkojë të gjitha seksionet e tekstit të të gjithë skedarëve të objektit në seksionin e tekstit të skedarit të ekzekutueshëm dhe do ta vendosë atë në adresën e specifikuar në numëruesin e pozicionit. Seksioni i kodit të ekzekutuesit tonë do të fillojë në adresën 0x100000.

Pasi lidhësi të prodhojë një seksion teksti, vlera e numëruesit të pozicionit do të jetë 0x100000 plus madhësinë e seksionit të tekstit. Në mënyrë të ngjashme, seksionet e të dhënave dhe bss do të bashkohen dhe do të vendosen në adresën e dhënë nga numëruesi i pozicionit.

GRUB dhe multiboot

Tani të gjithë skedarët tanë janë gati për të ndërtuar kernelin. Por meqenëse do të nisim kernelin duke përdorur GRUB, ka mbetur edhe një hap.

Ekziston një standard për ngarkimin e kerneleve të ndryshme x86 duke përdorur një ngarkues. Ky quhet "specifikim multiboot". GRUB do të ngarkojë vetëm kernelet që përputhen me të.

Sipas këtij specifikimi, kerneli mund të përmbajë një kokë (header Multiboot) në 8 kilobajt e parë. Ky titull duhet të përmbajë tre fusha:

  • magjike- përmban numrin “magjik” 0x1BADB002, me të cilin identifikohet titulli;
  • flamuj- kjo fushë nuk është e rëndësishme për ne, mund ta lini zero;
  • shuma e kontrollit- checksum, duhet të japë zero nëse shtohet në fushat magjike dhe flamuj.

Skedari ynë kernel.asm tani do të duket kështu.

bërthama.asm
bit 32 seksion .tekst ;përafrimi i specifikave të shumëboot 4 dd 0x1BADB002 ;dd magjike 0x00 ;flamujt dd - (0x1BADB002 + 0x00) ;shumës kontrolluese globale startuese starti i jashtëm kmain start: cli mov esp esp. :

Instruksioni dd specifikon një fjalë të dyfishtë 4 bajt.

Montimi i kernelit

Pra, gjithçka është gati për të krijuar një skedar objekti nga kernel.asm dhe kernel.c dhe për t'i lidhur ato duke përdorur skriptin tonë. Ne shkruajmë në tastierë:

$ nasm -f elf32 kernel.asm -o kasm.o

Duke përdorur këtë komandë, asembleri do të krijojë një skedar kasm.o në formatin ELF-32 bit. Tani është radha e GCC:

$ gcc -m32 -c kernel.c -o kc.o

Parametri -c tregon se skedari nuk ka nevojë të lidhet pas kompilimit. Ne do ta bëjmë vetë:

$ ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o

Kjo komandë do të ekzekutojë lidhësin me skriptin tonë dhe do të gjenerojë një ekzekutues të quajtur kernel.

PARALAJMËRIM

Hakerimi i kernelit bëhet më së miri në një mjedis virtual. Për të ekzekutuar kernelin në QEMU në vend të GRUB, përdorni komandën qemu-system-i386 -kernel kernel .

Vendosja e GRUB dhe fillimi i kernelit

GRUB kërkon që emri i skedarit të kernelit të ndjekë kernelin-<версия>. Pra, le të riemërtojmë skedarin - unë do ta quaj kernel-701.

Tani e vendosim kernelin në drejtorinë /boot. Kjo do të kërkojë privilegje të superpërdoruesit.

Do t'ju duhet të shtoni diçka të tillë në skedarin e konfigurimit GRUB grub.cfg:

Titulli myKernel root (hd0,0) kernel /boot/kernel-701 ro

Mos harroni të hiqni direktivën e menusë së fshehur nëse është e përfshirë.

GRUB 2

Për të ekzekutuar kernelin që krijuam në GRUB 2, i cili ofrohet si parazgjedhje në shpërndarjet e reja, konfigurimi juaj duhet të duket si ky:

Menuentry "kernel 701" ( set root = "hd0,msdos1" multiboot /boot/kernel-701 ro )

Faleminderit Ruben Laguana për këtë shtesë.

Rinisni kompjuterin tuaj dhe duhet të shihni kernelin tuaj në listë! Dhe kur ta zgjidhni, do të shihni të njëjtën linjë.



Kjo është thelbi juaj!

Shkrimi i një kerneli me mbështetjen e tastierës dhe ekranit

Ne kemi përfunduar punën në një kernel minimal që niset përmes GRUB, funksionon në modalitetin e mbrojtur dhe printon një rresht të vetëm në ekran. Është koha për ta zgjeruar atë dhe për të shtuar një drejtues tastiere që do të lexojë karakteret nga tastiera dhe do t'i shfaqë ato në ekran.

Ne do të komunikojmë me pajisjet I/O përmes porteve I/O. Në thelb, ato janë vetëm adresa në autobusin I/O. Ekzistojnë udhëzime të veçanta të procesorit për operacionet e leximit dhe shkrimit.

Puna me portet: lexim dhe dalje

read_port: mov edx, në al, dx ret write_port: mov edx, mov al, out dx, al ret

Portat I/O aksesohen duke përdorur udhëzimet hyrëse dhe dalëse të përfshira në grupin x86.

Në read_port, numri i portit kalohet si argument. Kur përpiluesi thërret një funksion, ai i shtyn të gjitha argumentet në pirg. Argumenti kopjohet në regjistrin edx duke përdorur një tregues stack. Regjistri dx është 16 bitët e poshtëm të regjistrit edx. Udhëzimi këtu lexon numrin e portit të dhënë në dx dhe e vendos rezultatin në al. Regjistri al është 8 bitët e poshtëm të regjistrit eax. Ju mund të mbani mend nga kolegji se vlerat e kthyera nga funksionet kalohen përmes regjistrit eax. Pra read_port na lejon të lexojmë nga portat I/O.

Funksioni write_port funksionon në mënyrë të ngjashme. Marrim dy argumente: numrin e portit dhe të dhënat që do të shkruhen. Udhëzimi jashtë shkruan të dhënat në një port.

Ndërpret

Tani, përpara se të kthehemi te shkrimi i drejtuesit, duhet të kuptojmë se si e di procesori që një nga pajisjet ka kryer një operacion.

Zgjidhja më e thjeshtë është të anketoni pajisjet - kontrolloni vazhdimisht statusin e tyre në një rreth. Kjo është, për arsye të dukshme, joefektive dhe jopraktike. Pra, këtu hyjnë në lojë ndërprerjet. Një ndërprerje është një sinjal i dërguar procesorit nga një pajisje ose program që tregon se një ngjarje ka ndodhur. Duke përdorur ndërprerje, ne mund të shmangim nevojën për të anketuar pajisjet dhe do t'u përgjigjemi vetëm ngjarjeve që na interesojnë.

Një çip i quajtur Kontrolluesi i Ndërprerjes i Programueshëm (PIC) është përgjegjës për ndërprerjet në arkitekturën x86. Ai trajton ndërprerjet dhe rrugëtimin e harduerit dhe i kthen ato në ndërprerje të përshtatshme të sistemit.

Kur përdoruesi bën diçka me pajisjen, një puls i quajtur Kërkesë për Ndërprerje (IRQ) dërgohet në çipin PIC. PIC e përkthen ndërprerjen e marrë në një ndërprerje të sistemit dhe i dërgon një mesazh procesorit se është koha për të ndaluar atë që po bën. Trajtimi i mëtejshëm i ndërprerjeve është detyrë e kernelit.

Pa PIC, do të na duhej të anketonim të gjitha pajisjet e pranishme në sistem për të parë nëse ka ndodhur një ngjarje që përfshin ndonjë prej tyre.

Le të shohim se si funksionon kjo me një tastierë. Tastiera varet në portat 0x60 dhe 0x64. Porta 0x60 dërgon të dhëna (kur shtypet një buton) dhe porta 0x64 dërgon statusin. Megjithatë, ne duhet të dimë se kur saktësisht t'i lexojmë këto porte.

Ndërprerjet vijnë në ndihmë këtu. Kur shtypet butoni, tastiera dërgon një sinjal PIC përmes linjës së ndërprerjes IRQ1. PIC ruan vlerën e kompensimit të ruajtur gjatë inicializimit të saj. Ai shton numrin e linjës hyrëse në këtë mbushje për të formuar një vektor të ndërprerjes. Më pas procesori kërkon një strukturë të dhënash të quajtur Tabela e Përshkruesit të Ndërprerjeve (IDT) për t'i dhënë mbajtësit të ndërprerjes adresën që korrespondon me numrin e tij.

Kodi në atë adresë më pas ekzekutohet dhe trajton ndërprerjen.

Cakto IDT

struct IDT_entry( int offset_lowerbits të shkurtra të panënshkruara; përzgjedhës int i shkurtër i panënshkruar; zero karakter i panënshkruar; tip_attr char i panënshkruar; offset_higherbits të shkurtër int të panënshkruar; ); struct IDT_hyrja IDT; void idt_init(void) (adresa_tastierë_gjatë e panënshkruar; idt_adresa e gjatë e panënshkruar; idt_ptr e gjatë e panënshkruar; adresa_tastie = (e gjatë e panënshkruar)keyboard_handler; IDT.offset_lowerbits = adresa_tastierë & 0xffff; IDT. IDT.zero = 0 ; IDT.type_attr = 0x8e; /* INTERRUPT_GATE */ IDT.offset_higherbits = (adresa_tastierë & 0xffff0000) >> 16; shkruani_port (0x20 , 0x11); shkruani_port (0xA0 , 0x10,port, 0x10x2, shkruani_); 0x28);write_port(0x21, 0x00);write_port(0xA1, 0x00);write_port(0x21,0x01);write_port(0xA1, 0x01);write_port(0x21, 0xff);write_port(0xA1, 0x00);write_port(0xA1, 0x01); )IDT ; idt_ptr = (madhësia e (struktura IDT_hyrja) * IDT_SIZE) + ((adresa_idt & 0xffff)<< 16); idt_ptr = idt_address >> 16; load_idt (idt_ptr); )

IDT është një grup strukturash IDT_entry. Ne do të diskutojmë lidhjen e një ndërprerjeje të tastierës me një mbajtës më vonë, por tani le të shohim se si funksionon PIC.

Sistemet moderne x86 kanë dy çipa PIC, secili me tetë linja hyrëse. Ne do t'i quajmë ato PIC1 dhe PIC2. PIC1 merr IRQ0 në IRQ7, dhe PIC2 merr IRQ8 në IRQ15. PIC1 përdor portën 0x20 për komandat dhe 0x21 për të dhënat, dhe PIC2 përdor portën 0xA0 për komandat dhe 0xA1 për të dhënat.

Të dy PIC-të inicializohen me fjalë tetë-bit të quajtura fjalë të komandave të inicimit (ICW).

Në modalitetin e mbrojtur, të dy PIC-të duhet së pari të lëshojnë komandën e inicializimit ICW1 (0x11). Ai i thotë PIC-it të presë që tre fjalë të tjera të inicializimit të arrijnë në portën e të dhënave.

Këto komanda do të kalojnë PIC:

  • vektori i indentit (ICW2),
  • cilat janë marrëdhëniet master/skllav midis PIC-ve (ICW3),
  • informacion shtesë rreth mjedisit (ICW4).

Komanda e dytë e inicializimit (ICW2) dërgohet gjithashtu në hyrjen e çdo PIC. Ai cakton offset, e cila është vlera në të cilën shtojmë numrin e linjës për të marrë numrin e ndërprerjes.

PIC-të lejojnë që kunjat e tyre të kaskadohen në hyrjet e njëri-tjetrit. Kjo bëhet duke përdorur ICW3 dhe çdo bit përfaqëson statusin e kaskadës për IRQ-në përkatëse. Tani ne nuk do të përdorim ridrejtimin kaskadë dhe do ta vendosim atë në zero.

ICW4 specifikon parametra shtesë mjedisorë. Na duhet vetëm të përcaktojmë bitin e ulët në mënyrë që PIC-të të dinë se po ekzekutojmë në modalitetin 80x86.

Ta-dam! PIC-të tani janë inicializuar.

Çdo PIC ka një regjistër të brendshëm tetë-bitësh të quajtur Regjistri i Maskës së Ndërprerjes (IMR). Ai ruan një bitmap të linjave IRQ që shkojnë në PIC. Nëse biti është vendosur, PIC e injoron kërkesën. Kjo do të thotë që ne mund të aktivizojmë ose çaktivizojmë një linjë specifike IRQ duke vendosur vlerën përkatëse në 0 ose 1.

Leximi nga porta e të dhënave kthen vlerën në regjistrin IMR, ndërsa shkrimi ndryshon regjistrin. Në kodin tonë, pas inicializimit të PIC, ne vendosim të gjithë bitet në një, i cili çaktivizon të gjitha linjat IRQ. Më vonë do të aktivizojmë linjat që korrespondojnë me ndërprerjet e tastierës. Por së pari, le ta fikim!

Nëse linjat IRQ janë duke punuar, PIC-të tona mund të marrin sinjale në IRQ dhe t'i konvertojnë ato në një numër ndërprerjeje, duke shtuar kompensimin. Duhet të plotësojmë IDT-në në atë mënyrë që numri i ndërprerjes që vjen nga tastiera të përputhet me adresën e funksionit të mbajtësit që do të shkruajmë.

Me cilin numër ndërprerjeje na nevojitet për të lidhur mbajtësin e tastierës në IDT?

Tastiera përdor IRQ1. Kjo është linja hyrëse 1 dhe përpunohet nga PIC1. Ne inicializuam PIC1 me kompensim 0x20 (shih ICW2). Për të marrë numrin e ndërprerjes, duhet të shtoni 1 dhe 0x20, merrni 0x21. Kjo do të thotë që adresa e mbajtësit të tastierës do të lidhet në IDT për të ndërprerë 0x21.

Detyra zbret në plotësimin e IDT-së për ndërprerjen 0x21. Ne do ta vendosim këtë ndërprerje në funksionin keyboard_handler, të cilin do ta shkruajmë në skedarin e montimit.

Çdo hyrje në IDT përbëhet nga 64 bit. Në hyrjen që korrespondon me ndërprerjen, ne nuk e ruajmë të gjithë adresën e funksionit të mbajtësit. Në vend të kësaj, ne e ndajmë atë në dy pjesë 16-bitësh. Bitët e rendit të ulët ruhen në 16 bitët e parë të hyrjes IDT, dhe 16 bitët e rendit të lartë ruhen në 16 bitet e fundit të hyrjes. E gjithë kjo është bërë për pajtueshmërinë me 286 procesorë. Siç mund ta shihni, Intel prodhon numra të tillë rregullisht dhe në shumë e shumë vende!

Në hyrjen IDT, ne vetëm duhet të regjistrojmë llojin, duke treguar kështu se e gjithë kjo po bëhet për të kapur ndërprerjen. Ne gjithashtu duhet të vendosim kompensimin e segmentit të kodit të kernelit. GRUB përcakton GDT-në për ne. Çdo hyrje GDT është 8 bajt, ku përshkruesi i kodit të kernelit është segmenti i dytë, kështu që kompensimi i tij do të jetë 0x08 (detajet janë përtej qëllimit të këtij artikulli). Porta e ndërprerjes paraqitet si 0x8e. 8 bitet e mbetura ne mes jane te mbushura me zero. Në këtë mënyrë do të plotësojmë hyrjen IDT që korrespondon me ndërprerjen e tastierës.

Pasi të kemi mbaruar me hartën IDT, duhet t'i tregojmë procesorit se ku është IDT. Ekziston një udhëzim montimi i quajtur lidt për këtë; kërkon një operand. Ky është një tregues për një përshkrues të strukturës që përshkruan IDT.

Nuk ka vështirësi me përshkruesin. Ai përmban madhësinë e IDT-së në bajt dhe adresën e tij. Kam përdorur një grup për ta bërë atë më kompakt. Në të njëjtën mënyrë, ju mund të plotësoni një përshkrues duke përdorur një strukturë.

Në variablin idr_ptr kemi një pointer që ia kalojmë instruksionit lidt në funksionin load_idt().

Load_idt: mov edx, lidt sti ret

Për më tepër, funksioni load_idt() kthen një ndërprerje kur përdor instruksionin sti.

Me IDT të mbushur dhe të ngarkuar, ne mund të hyjmë në tastierën IRQ duke përdorur maskën e ndërprerjes për të cilën folëm më parë.

Void kb_init(void) (write_port(0x21, 0xFD);)

0xFD është 11111101 - aktivizoni vetëm IRQ1 (tastierë).

Funksioni - mbajtësi i ndërprerjeve të tastierës

Pra, ne kemi lidhur me sukses ndërprerjet e tastierës me funksionin keyboard_handler duke krijuar një hyrje IDT për ndërprerjen 0x21. Ky funksion do të thirret sa herë që shtypni një buton.

Keyboard_handler: telefononi keyboard_handler_main iretd

Ky funksion thërret një funksion tjetër të shkruar në C dhe kthen kontrollin duke përdorur udhëzimet e klasës iret. Mund të shkruajmë të gjithë mbajtësin tonë këtu, por është shumë më e lehtë të kodosh në C, kështu që le të kalojmë atje. Instruksionet iret/iretd duhet të përdoren në vend të ret kur kontrolli kthehet nga funksioni i trajtimit të ndërprerjeve në programin e ndërprerë. Kjo klasë udhëzimesh ngre një regjistër flamur, i cili shtyhet në pirg kur thirret një ndërprerje.

Void keyboard_handler_main(void) (status i panënshkruar i karakterit; kodi i tastierës; /* Shkruani EOI */write_port(0x20, 0x20); status = read_port(KEYBOARD_STATUS_PORT); /* Biti i statusit më të ulët do të vendoset nëse buferi nuk është bosh */ nëse (statusi & 0x01) ( kodi i çelësit = porta e leximit (KEYBOARD_DATA_PORT); nëse (kodi i çelësit< 0) return; vidptr = keyboard_map; vidptr = 0x07; } }

Këtu ne fillimisht japim një sinjal EOI (End Of Interrupt) duke e shkruar atë në portën e komandës PIC. Vetëm atëherë PIC do të lejojë kërkesa të mëtejshme për ndërprerje. Duhet të lexojmë dy porte: porta e të dhënave 0x60 dhe porta e komandës (aka porta e statusit) 0x64.

Para së gjithash, ne lexojmë portin 0x64 për të marrë statusin. Nëse pjesa e poshtme e statusit është zero, atëherë buferi është bosh dhe nuk ka të dhëna për të lexuar. Në raste të tjera mund të lexojmë portën e të dhënave 0x60. Do të na japë kodin e tastit të shtypur. Çdo kod korrespondon me një buton. Ne përdorim një grup të thjeshtë karakteresh të përcaktuar në keyboard_map.h për të hartuar kodet me karakteret përkatëse. Simboli shfaqet më pas në ekran duke përdorur të njëjtën teknikë që kemi përdorur në versionin e parë të kernelit.

Për ta mbajtur kodin të thjeshtë, unë përpunoj vetëm shkronjat e vogla nga a në z dhe numrat nga 0 në 9. Mund të shtoni lehtësisht karaktere speciale, Alt, Shift dhe Caps Lock. Mund të zbuloni se një çelës është shtypur ose lëshuar nga dalja e portës së komandës dhe të kryeni veprimin e duhur. Në të njëjtën mënyrë, ju mund të lidhni çdo shkurtore të tastierës me funksione të veçanta si mbyllja.

Tani mund ta ndërtoni kernelin dhe ta ekzekutoni në një makinë reale ose në një emulator (QEMU) në të njëjtën mënyrë si në pjesën e parë.

Duke lexuar Habr gjatë dy viteve të fundit, kam parë vetëm disa përpjekje për të zhvilluar një OS (konkretisht: nga përdoruesit dhe (shtyrë për një kohë të pacaktuar) dhe (jo të braktisur, por tani për tani duket më shumë si një përshkrim i funksionimit të mënyrës së mbrojtur të procesorëve të përputhshëm me x86, që padyshim duhet të dini gjithashtu për të shkruar një OS për x86); dhe një përshkrim të sistemit të përfunduar nga (megjithëse jo nga e para, megjithëse nuk ka asgjë të keqe me këtë, ndoshta edhe anasjelltas )). Për disa arsye, unë mendoj se pothuajse të gjithë programuesit e sistemit (dhe disa aplikacioneve) kanë menduar të paktën një herë të shkruajnë sistemin e tyre operativ. Në lidhje me këtë, 3 OS nga komuniteti i madh i këtij burimi duket një numër qesharak. Me sa duket, shumica e atyre që mendojnë për OS-në e tyre nuk shkojnë askund përtej idesë, një pjesë e vogël ndalon pasi shkruan bootloader, disa shkruajnë pjesë të kernelit dhe vetëm kokëfortët e pashpresë krijojnë diçka që të kujton në mënyrë të paqartë një OS. (nëse krahasohet me diçka si Windows/Linux) . Ka shumë arsye për këtë, por kryesore për mendimin tim është se njerëzit heqin dorë nga zhvillimi (disa edhe para fillimit) për shkak të numrit të vogël të përshkrimeve të procesit të shkrimit dhe korrigjimit të një sistemi operativ, i cili është krejt i ndryshëm nga ajo që ndodh. gjatë zhvillimit të softuerit aplikativ.

Me këtë shënim të shkurtër do të doja të tregoja se nëse filloni siç duhet, atëherë nuk ka asgjë veçanërisht të vështirë në zhvillimin e OS tuaj. Më poshtë prerjes është një udhëzues i shkurtër dhe mjaft i përgjithshëm për të shkruar një OS nga e para.

Si Nuk ka nevojë Fillo
Ju lutemi, mos e merrni tekstin e mëposhtëm si kritikë të qartë ndaj artikujve ose udhëzuesve të dikujt për të shkruar një OS. Vetëm se shumë shpesh në artikuj të tillë nën tituj me zë të lartë theksi vihet në zbatimin e disa pjesëve minimale të punës dhe ai paraqitet si një prototip i kernelit. Në fakt, duhet të mendoni për strukturën e kernelit dhe ndërveprimin e pjesëve të sistemit operativ në tërësi dhe ta konsideroni atë prototip si një aplikacion standard "Hello, World!" në botën e softuerit aplikativ. Si një justifikim i lehtë për këto komente, duhet thënë se më poshtë është një nënseksion “Përshëndetje, Botë!”, të cilit në këtë rast i kushtohet pikërisht aq vëmendje sa duhet dhe jo më shumë.

Nuk ka nevojë të shkruani një bootloader. Njerëzit e zgjuar dolën me Specifikimin Multiboot, e zbatuan atë dhe përshkruan në detaje se çfarë është dhe si ta përdorin atë. Nuk dua të përsëris veten, thjesht do të them se funksionon, e bën jetën më të lehtë dhe duhet përdorur. Nga rruga, është më mirë të lexoni specifikimin plotësisht, është i vogël dhe madje përmban shembuj.

Nuk ka nevojë të shkruani OS tërësisht në asembler. Kjo nuk është aq e keqe, përkundrazi e kundërta - programet e shpejta dhe të vogla do të mbahen gjithmonë me nderim të lartë. Është thjesht se meqenëse kjo gjuhë kërkon shumë më shumë përpjekje për t'u zhvilluar, përdorimi i asemblerit do të çojë vetëm në një ulje të entuziazmit dhe, si rezultat, në hedhjen e burimeve të OS në një kuti të gjatë.

Nuk ka nevojë të ngarkoni një font të personalizuar në memorien video dhe të shfaqni ndonjë gjë në Rusisht. Kjo nuk ka kuptim. Është shumë më e lehtë dhe më e gjithanshme të përdoret anglishtja dhe të lihet ndryshimi i fontit për më vonë, duke e ngarkuar atë nga hard disku përmes drejtuesit të sistemit të skedarëve (në të njëjtën kohë do të ketë një nxitje shtesë për të bërë më shumë sesa thjesht të filloni).

Përgatitja
Për të filluar, si gjithmonë, duhet të njiheni me teorinë e përgjithshme në mënyrë që të keni një ide rreth fushës së ardhshme të punës. Burime të mira për çështjen në shqyrtim janë librat e E. Tanenbaum, të cilët tashmë janë përmendur në artikuj të tjerë rreth shkrimit të një OS në Habré. Ka gjithashtu artikuj që përshkruajnë sistemet ekzistuese dhe ka udhëzues/poste/artikuj/shembuj/faqe të ndryshme me theks në zhvillimin e OS, lidhjet me disa prej të cilave janë dhënë në fund të artikullit.

Pas programit arsimor fillestar, duhet të vendosni për pyetjet kryesore:

  • Arkitektura e synuar - x86 (modaliteti i vërtetë / i mbrojtur / i gjatë), PowerPC, ARM, ...
  • Arkitektura kernel/OS - monolit, monolit modular, mikrokernel, ekzokernel, hibride të ndryshme
  • gjuha dhe përpiluesi i saj - C, C++, ...
  • Formati i skedarit kernel - elf, a.out, coff, binary, ...
  • mjedisi i zhvillimit (po, kjo gjithashtu luan një rol të rëndësishëm) - IDE, vim, emacs, ...
Më pas, duhet të thelloni njohuritë tuaja sipas asaj të zgjedhurit dhe në fushat e mëposhtme:
  • memoria video dhe puna me të - prodhimi si provë funksionimi është i nevojshëm që në fillim
  • HAL (Hardware Abstraction layer) - edhe nëse ka mbështetje për disa arkitektura harduerike dhe nuk ka plane për të ndarë siç duhet pjesët e nivelit më të ulët të kernelit nga zbatimi i gjërave të tilla abstrakte si proceset, semaforët, etj. të mos jetë e tepërt
  • menaxhimi i kujtesës - fizik dhe virtual
  • menaxhimi i ekzekutimit - proceset dhe fijet, planifikimi i tyre
  • menaxhimi i pajisjes - drejtuesit
  • sistemet e skedarëve virtualë - për të siguruar një ndërfaqe të vetme për përmbajtjen e sistemeve të ndryshme të skedarëve
  • API (Application Programming Interface) - se si saktësisht aplikacionet do të hyjnë në kernel
  • IPC (Interprocess Communication) - herët a vonë proceset do të duhet të komunikojnë
Mjetet
Duke marrë parasysh gjuhën e zgjedhur dhe mjetet e zhvillimit, duhet të zgjidhni një grup shërbimesh dhe cilësimet e tyre që në të ardhmen, duke shkruar skripta, do ta bëjnë sa më të lehtë dhe të shpejtë ndërtimin, përgatitjen e një imazhi dhe lëshimin e një makine virtuale me nje projekt. Le të hedhim një vështrim më të afërt në secilën nga këto pika:
  • çdo vegël standarde është e përshtatshme për montim, si p.sh. make, cmake,... Këtu mund të përdoren skriptet për lidhësin dhe shërbimet (të shkruara posaçërisht) për shtimin e një titulli Multiboot, shumat e kontrollit ose për çdo qëllim tjetër.
  • Me përgatitjen e një imazhi nënkuptojmë montimin e tij dhe kopjimin e skedarëve. Prandaj, formati i skedarit të imazhit duhet të zgjidhet në mënyrë që të mbështetet si nga programi i montimit/kopjimit ashtu edhe nga makina virtuale. Natyrisht, askush nuk e ndalon kryerjen e veprimeve nga kjo pikë, as si pjesa përfundimtare e asamblesë, as si përgatitje për nisjen e emulatorit. E gjitha varet nga mjetet specifike dhe opsionet e zgjedhura për përdorimin e tyre.
  • nisja e një makine virtuale nuk është e vështirë, por duhet të mbani mend që fillimisht të çmontoni imazhin (çmontimi në këtë pikë, pasi nuk ka asnjë pikë reale në këtë operacion përpara se të filloni makinën virtuale). Do të ishte gjithashtu e dobishme të kishim një skript për të nisur emulatorin në modalitetin e korrigjimit (nëse disponohet).
Nëse të gjithë hapat e mëparshëm janë përfunduar, duhet të shkruani një program minimal që do të niset si kernel dhe do të printojë diçka në ekran. Nëse zbulohen shqetësime ose mangësi të mjeteve të zgjedhura, është e nevojshme që ato të eliminohen (të metat), ose, në rastin më të keq, të merren si të mirëqena.

Në këtë hap, duhet të kontrolloni sa më shumë veçori të mjeteve të zhvillimit që planifikoni të përdorni në të ardhmen. Për shembull, ngarkimi i moduleve në GRUB ose përdorimi i një disk fizik/particioni/flash drive në një makinë virtuale në vend të një imazhi.

Pasi kjo fazë të jetë e suksesshme, fillon zhvillimi i vërtetë.

Ofrimi i mbështetjes në kohën e ekzekutimit
Meqenëse propozohet të shkruhet në gjuhë të nivelit të lartë, duhet pasur kujdes për të ofruar mbështetje për disa nga veçoritë gjuhësore që zakonisht zbatohen nga autorët e paketës së përpiluesit. Për shembull, për C++, këto përfshijnë:
  • funksion për shpërndarjen dinamike të një blloku të dhënash në pirg
  • duke punuar me grumbull
  • funksioni i kopjimit të bllokut të të dhënave (memcpy)
  • funksioni është pika hyrëse në program
  • thirrje për konstruktorët dhe destruktorët e objekteve globale
  • një numër funksionesh për të punuar me përjashtime
  • cung për funksione të parealizuara virtuale
Kur shkruani "Përshëndetje, Botë!" Mungesa e këtyre funksioneve mund të mos ndihet, por me shtimin e kodit, lidhësi do të fillojë të ankohet për varësi të paplotësuara.

Natyrisht, duhet të përmendim edhe bibliotekën standarde. Një zbatim i plotë nuk është i nevojshëm, por një nëngrup thelbësor i funksionalitetit ia vlen të zbatohet. Atëherë shkrimi i kodit do të jetë shumë më i njohur dhe më i shpejtë.

Korrigjimi
Mos shikoni se çfarë thuhet për korrigjimin në fund të artikullit. Në fakt, kjo është një çështje shumë serioze dhe e vështirë në zhvillimin e OS, pasi mjetet konvencionale nuk janë të zbatueshme këtu (me disa përjashtime).

Ne mund të rekomandojmë sa vijon:

  • vetëkuptohet, korrigjimi i daljes
  • pohoni me dalje të menjëhershme në "debugger" (shih paragrafin tjetër)
  • disa pamje të një korrigjuesi të konsolës
  • kontrolloni nëse emulatori ju lejon të lidhni një korrigjues, tabela simbolesh ose diçka tjetër
Pa një korrigjues të integruar në kernel, kërkimi i gabimeve ka një shans shumë real për t'u kthyer në një makth. Pra, thjesht nuk ka shpëtim nga shkrimi i tij në një fazë të zhvillimit. Dhe meqenëse kjo është e pashmangshme, është më mirë të filloni ta shkruani paraprakisht dhe kështu të lehtësoni shumë zhvillimin dhe të kurseni mjaft kohë. Është e rëndësishme të jeni në gjendje të implementoni një korrigjues në një mënyrë të pavarur nga kerneli, në mënyrë që korrigjimi të ketë ndikim minimal në funksionimin normal të sistemit. Këtu janë disa lloje komandash që mund të jenë të dobishme:
  • pjesë e operacioneve standarde të korrigjimit: pikat e ndërprerjes, grupi i thirrjeve, vlerat e printimit, deponimi i printimit, ...
  • komandat për të shfaqur informacione të ndryshme të dobishme, të tilla si radha e ekzekutimit të planifikuesit ose statistika të ndryshme (nuk është aq e padobishme sa mund të duket në fillim)
  • komandat për kontrollimin e konsistencës së gjendjes së strukturave të ndryshme: listat e memories së lirë/të përdorur, grumbulli ose radha e mesazheve
Zhvillimi
Tjetra, është e nevojshme të shkruani dhe korrigjoni elementët kryesorë të OS, të cilat për momentin duhet të sigurojnë funksionimin e tij të qëndrueshëm, dhe në të ardhmen - zgjerim dhe fleksibilitet të lehtë. Përveç menaxherëve të kujtesës/procesit/(çfarëdo qoftë), ndërfaqja e drejtuesve dhe sistemeve të skedarëve është shumë e rëndësishme. Dizajni i tyre duhet trajtuar me kujdes të veçantë, duke marrë parasysh shumëllojshmërinë e llojeve të pajisjeve/FS. Me kalimin e kohës, sigurisht, ato mund të ndryshohen, por ky është një proces shumë i dhimbshëm dhe i prirur ndaj gabimeve (dhe korrigjimi i kernelit nuk është një detyrë e lehtë), kështu që vetëm mbani mend - mendoni për këto ndërfaqe të paktën dhjetë herë përpara se të filloni zbatimin ato.
SDK e ngjashme
Ndërsa projekti zhvillohet, atij duhet t'i shtohen drejtues dhe programe të reja. Me shumë mundësi, tashmë në drejtuesin e dytë (ndoshta të një lloji të caktuar) / program do të vërehen disa veçori të përbashkëta (struktura e drejtorisë, skedarët e kontrollit të montimit, specifikimi i varësive midis moduleve, kodi i përsëritur në mbajtësit e kërkesave kryesore ose të sistemit (për shembull, nëse vetë drejtuesit kontrollojnë përputhshmërinë e tyre me pajisjen )). Nëse është kështu, atëherë kjo është një shenjë e nevojës për të zhvilluar shabllone për lloje të ndryshme programesh për sistemin tuaj operativ.

Nuk ka nevojë për dokumentacion që përshkruan procesin e shkrimit të këtij ose atij lloj programi. Por ia vlen të bëni një bosh nga elementët standardë. Kjo jo vetëm që do ta bëjë më të lehtë shtimin e programeve (që mund të bëhet duke kopjuar programet ekzistuese dhe më pas duke i ndryshuar ato, por kjo do të marrë më shumë kohë), por gjithashtu do ta bëjë më të lehtë përditësimin e tyre kur ndërfaqet, formatet ose çdo gjë tjetër ndryshojnë. Është e qartë se ndryshime të tilla në mënyrë ideale nuk duhet të ndodhin, por meqenëse zhvillimi i OS është një gjë atipike, ka mjaft vende për vendime potencialisht të gabuara. Por kuptimi i gabimit të vendimeve të marra, si gjithmonë, do të vijë pak kohë pas zbatimit të tyre.

Veprime të mëtejshme
Me pak fjalë, atëherë: lexoni për sistemet operative (dhe, para së gjithash, për dizajnin e tyre), zhvilloni sistemin tuaj (ritmi nuk është vërtet i rëndësishëm, gjëja kryesore është të mos ndaleni plotësisht dhe të ktheheni në projekt herë pas here me të reja forca dhe idetë) dhe është e natyrshme të korrigjoni gabimet në të (për të gjetur të cilat ndonjëherë ju duhet të filloni sistemin dhe të "luani" me të). Me kalimin e kohës, procesi i zhvillimit do të bëhet më i lehtë dhe më i lehtë, gabimet do të ndodhin më rrallë dhe ju do të përfshiheni në listën e "kokëfortëve të pashpresë", ata pak që, pavarësisht absurditetit të caktuar të idesë për të zhvilluar vet OS, ende e bëri atë.

Origjinali: "Rrotulloni lodrën tuaj UNIX-klone OS"
Autori: James Molloy
Data e publikimit: 2008
Përkthim: N. Romodanov
Data e përkthimit: Janar 2012

Ky grup mësimesh është krijuar për t'ju treguar në detaje se si të programoni një sistem operativ të thjeshtë si UNIX për arkitekturën x86. Në këto tutoriale, gjuha e programimit e zgjedhur është C, e plotësuar me gjuhën e asamblesë aty ku kërkohet. Qëllimi i mësimeve është t'ju tregojë për hartimin dhe zbatimin e zgjidhjeve të përdorura në krijimin e sistemit operativ OS që po krijojmë, i cili është monolit në strukturën e tij (drivers ngarkohen në modulin e kernelit, dhe jo në modalitetin e përdoruesit siç ndodh me programe), pasi një zgjidhje e tillë është më e thjeshtë.

Ky grup udhëzuesish është shumë praktik në natyrë. Çdo seksion ofron informacion teorik, por pjesa më e madhe e manualit ka të bëjë me zbatimin e ideve dhe mekanizmave abstrakte të diskutuara në praktikë. Është e rëndësishme të theksohet se bërthama zbatohet si një bërthamë trajnimi. E di që algoritmet e përdorura nuk janë as ato më efikaset në hapësirë ​​dhe as ato optimale. Ata u zgjodhën në përgjithësi për shkak të thjeshtësisë dhe lehtësisë së tyre të të kuptuarit. Qëllimi i kësaj është t'ju japë mendimin e duhur dhe një bazë nga e cila të punoni. Kjo bërthamë është e zgjerueshme dhe ju lehtë mund të lidhni algoritmet më të mira. Nëse keni ndonjë problem në lidhje me teorinë, ka shumë faqe që mund t'ju ndihmojnë ta kuptoni atë. Shumica e pyetjeve të diskutuara në forumin OSDev janë të lidhura me zbatimin ("Funksioni im merr nuk funksionon! ndihmon!") dhe për shumë, një pyetje rreth teorisë është si një frymë e freskët. Lidhjet mund të gjenden në fund të kësaj Prezantimi.

Përgatitja paraprake

Për të përpiluar dhe ekzekutuar kodin shembull, supozoj se ju nevojiten vetëm GCC, ld, NASM dhe GNU Make. NASM është një montues x86 me burim të hapur dhe është zgjedhja e shumë zhvilluesve të sistemit operativ x86.

Megjithatë, nuk ka kuptim thjesht të përpiloni dhe ekzekutoni shembuj nëse nuk i kuptoni. Duhet të kuptosh se çfarë është duke u koduar dhe për ta bërë këtë duhet të njohësh shumë mirë gjuhën C, veçanërisht kur bëhet fjalë për tregues. Ju gjithashtu duhet të kuptoni disa gjuhë të asamblesë (këto mësime përdorin sintaksën e Intel), duke përfshirë atë për çfarë përdoret regjistri EBP.

Burimet

Ka shumë burime nëse e dini si t'i kërkojmë ato. Në veçanti, do të gjeni të dobishme lidhjet e mëposhtme:

  • RTFM! Manualët e Intel janë një dhuratë nga perëndia.
  • Faqet Wiki dhe forumi i faqes osdev.org.
  • Ka shumë mësime dhe artikuj të mirë në Osdever.net dhe në veçanti mësimet e zhvillimit të kernelit të Bran, kodi i mëparshëm në të cilin bazohet ky tutorial. Unë i përdora vetë këto mësime për të filluar, dhe kodi në to ishte aq i mirë saqë e bëra mos e ndryshoni për disa vite.
  • Nëse nuk jeni fillestar, atëherë mund të merrni përgjigje për shumë pyetje në grup

Kjo seri artikujsh i kushtohet programimit të nivelit të ulët, domethënë arkitekturës kompjuterike, dizajnit të sistemeve operative, programimit të gjuhës së asamblesë dhe fushave të ngjashme. Deri më tani, dy habrausers janë duke bërë shkrimin - dhe . Për shumë gjimnazistë, studentë, madje edhe programues profesionistë, këto tema rezultojnë të jenë shumë të vështira për t'u mësuar. Ka shumë literaturë dhe kurse që i kushtohen programimit të nivelit të ulët, por është e vështirë për të marrë një pamje të plotë dhe gjithëpërfshirëse. Është e vështirë, pasi të keni lexuar një ose dy libra mbi gjuhën e asamblesë dhe sistemet operative, të paktën në terma të përgjithshëm të imagjinoni se si funksionon në të vërtetë ky sistem kompleks prej hekuri, silikoni dhe shumë programesh - një kompjuter.

Secili e zgjidh problemin e të mësuarit në mënyrën e vet. Disa njerëz lexojnë shumë literaturë, disa përpiqen të kalojnë shpejt në praktikë dhe ta kuptojnë atë ndërsa shkojnë, disa përpiqen t'u shpjegojnë miqve të tyre gjithçka që po studiojnë. Dhe ne vendosëm t'i kombinojmë këto qasje. Pra, në këtë kurs artikujsh do të demonstrojmë hap pas hapi se si të shkruajmë një sistem operativ të thjeshtë. Artikujt do të jenë të natyrës rishikuese, domethënë nuk do të përmbajnë informacione shteruese teorike, por ne do të përpiqemi gjithmonë të ofrojmë lidhje me materiale të mira teorike dhe t'u përgjigjemi të gjitha pyetjeve që lindin. Ne nuk kemi një plan të qartë, kështu që shumë vendime të rëndësishme do të merren gjatë rrugës, duke marrë parasysh komentet tuaja.

Ne mund ta frenojmë qëllimisht procesin e zhvillimit në mënyrë që t'ju lejojmë juve dhe vetes të kuptoni plotësisht pasojat e një vendimi të keq, si dhe të përmirësojmë disa aftësi teknike mbi të. Pra, nuk duhet t'i perceptoni vendimet tona si të vetmet të sakta dhe të na besoni verbërisht. Le të theksojmë edhe një herë se presim që lexuesit të jenë aktivë në diskutimin e artikujve, gjë që duhet të ndikojë shumë në procesin e përgjithshëm të zhvillimit dhe shkrimit të artikujve pasues. Idealisht, me kalimin e kohës, do të dëshiroja që disa nga lexuesit të bashkohen në zhvillimin e sistemit.

Do të supozojmë se lexuesi tashmë është i njohur me bazat e asamblesë dhe gjuhëve C, si dhe me konceptet elementare të arkitekturës kompjuterike. Kjo do të thotë, ne nuk do të shpjegojmë se çfarë është një regjistër ose, të themi, RAM. Nëse nuk keni njohuri të mjaftueshme, gjithmonë mund t'i drejtoheni literaturës shtesë. Një listë e shkurtër e referencave dhe lidhjeve për faqet me artikuj të mirë janë në fund të artikullit. Këshillohet gjithashtu të dini se si të përdorni Linux, pasi të gjitha udhëzimet e përpilimit do të jepen posaçërisht për këtë sistem.

Dhe tani - më afër pikës. Në pjesën tjetër të artikullit, ne do të shkruajmë programin klasik "Hello World". Helloworld ynë do të jetë pak specifik. Ai nuk do të lansohet nga ndonjë sistem operativ, por drejtpërdrejt, si të thuash, "në metal të zhveshur". Para se të fillojmë të shkruajmë kodin, le të kuptojmë saktësisht se si po përpiqemi ta bëjmë këtë. Dhe për këtë duhet të shqyrtojmë procesin e nisjes së kompjuterit.

Pra, merrni kompjuterin tuaj të preferuar dhe shtypni butonin më të madh në njësinë e sistemit. Ne shohim një mbrojtës të gëzuar të ekranit, njësia e sistemit bie me kënaqësi me altoparlantin e saj dhe pas një kohe sistemi operativ ngarkohet. Siç e kuptoni, sistemi operativ ruhet në një hard disk, dhe këtu lind pyetja: si u ngarkua sistemi operativ në mënyrë magjike në RAM dhe filloi të ekzekutohej?

Dije këtë: sistemi që është në çdo kompjuter është përgjegjës për këtë, dhe emri i tij - jo, jo Windows, majë gjuhën - quhet BIOS. Emri i tij qëndron për Sistemin Bazë Input-Output, domethënë një sistem bazë hyrje-dalje. BIOS ndodhet në një çip të vogël në motherboard dhe fillon menjëherë pasi të keni shtypur butonin e madh ON. BIOS ka tre detyra kryesore:

  1. Zbuloni të gjitha pajisjet e lidhura (procesor, tastierë, monitor, RAM, kartë video, kokë, krahë, krahë, këmbë dhe bisht...) dhe kontrolloni funksionalitetin e tyre. Programi POST (Power On Self Test) është përgjegjës për këtë. Nëse hardueri jetik nuk zbulohet, atëherë asnjë softuer nuk do të jetë në gjendje të ndihmojë dhe në këtë pikë altoparlanti i sistemit do të kërcëjë diçka ogurzezë dhe OS nuk do të arrijë fare në pikën. Le të mos flasim për gjëra të trishtueshme, le të supozojmë se kemi një kompjuter plotësisht që funksionon, gëzohemi dhe kalojmë në shqyrtimin e funksionit të dytë BIOS:
  2. Sigurimi i sistemit operativ me një grup funksionesh bazë për të punuar me harduer. Për shembull, përmes funksioneve të BIOS-it mund të shfaqni tekst në ekran ose të lexoni të dhëna nga tastiera. Kjo është arsyeja pse ai quhet sistemi bazë hyrës/dalës. Në mënyrë tipike, sistemi operativ i qaset këtyre funksioneve përmes ndërprerjeve.
  3. Nisja e ngarkuesit të sistemit operativ. Në këtë rast, si rregull, lexohet sektori i nisjes - sektori i parë i mediumit të ruajtjes (floppy disk, hard disk, CD, flash drive). Rendi i votimit në media mund të caktohet në BIOS SETUP. Sektori i nisjes përmban një program që nganjëherë quhet ngarkuesi kryesor i nisjes. Përafërsisht, detyra e bootloader-it është të nisë sistemin operativ. Procesi i ngarkimit të një sistemi operativ mund të jetë shumë specifik dhe shumë i varur nga veçoritë e tij. Prandaj, ngarkuesi primar shkruhet drejtpërdrejt nga zhvilluesit e OS dhe shkruhet në sektorin e nisjes gjatë instalimit. Kur fillon ngarkuesi, procesori është në modalitetin real.
Lajmi i trishtuar është se bootloader duhet të jetë vetëm 512 byte në madhësi. Pse kaq pak? Për ta bërë këtë, ne duhet të njihemi me strukturën e disketës. Këtu është një foto informative:

Fotografia tregon sipërfaqen e një disku. Një disketë ka 2 sipërfaqe. Çdo sipërfaqe ka shtigje (gjurmë) në formë unaze. Çdo pistë është e ndarë në pjesë të vogla në formë harku të quajtura sektorë. Pra, historikisht, një sektor i disketës ka një madhësi prej 512 bajt. Sektori i parë në disk, sektori i nisjes, lexohet nga BIOS në segmentin zero të memories në offset 0x7C00, dhe më pas kontrolli transferohet në këtë adresë. Ngarkuesi zakonisht ngarkon në memorje jo vetë OS, por një program tjetër bootloader të ruajtura në disk, por për ndonjë arsye (ka shumë të ngjarë, kjo arsye është madhësia) nuk përshtatet në një sektor. Dhe duke qenë se tani për tani roli i sistemit operativ tonë luhet nga një Helloworld banale, qëllimi ynë kryesor është ta bëjmë kompjuterin të besojë në ekzistencën e sistemit tonë operativ, qoftë edhe në një sektor, dhe ta lëshojmë atë.

Si është i strukturuar sektori i nisjes? Në një PC, kërkesa e vetme për sektorin e nisjes është që dy bajtët e tij të fundit të përmbajnë vlerat 0x55 dhe 0xAA - nënshkrimi i sektorit të nisjes. Pra, tashmë është pak a shumë e qartë se çfarë duhet të bëjmë. Le të shkruajmë kodin! Kodi i dhënë është shkruar për montuesin yasm.

Seksioni .përdorimi i tekstit16 org 0x7C00 ; programi ynë është i ngarkuar në adresën 0x7C00 start: mov ax, cs mov ds, ax ; zgjidhni segmentin e të dhënave mov si, mesazhi cld ; drejtimi për komandat e vargut mov ah, 0x0E ; Numri i funksionit BIOS mov bh, 0x00 ; faqe memorie video puts_loop: lodsb ; ngarkoni simbolin tjetër në al test al, al ; karakteri null nënkupton fundin e rreshtit jz puts_loop_exit int 0x10 ; thirrni funksionin BIOS jmp puts_loop puts_loop_exit: jmp $ ; mesazhi i lakut të përjetshëm: db "Përshëndetje Botë!", 0 mbarimi: herë 0x1FE-mbarimi+fillimi db 0 db 0x55, 0xAA ; nënshkrimi i sektorit të nisjes

Ky program i shkurtër kërkon disa shpjegime të rëndësishme. Linja org 0x7C00 nevojitet në mënyrë që asembleri (që do të thotë programi, jo gjuha) të llogarisë saktë adresat për etiketat dhe variablat (puts_loop, puts_loop_exit, message). Kështu e informojmë se programi do të ngarkohet në memorie në adresën 0x7C00.
Në rreshta
mov ax, cs mov ds, sëpatë
segmenti i të dhënave (ds) vendoset i barabartë me segmentin e kodit (cs), pasi në programin tonë të dhënat dhe kodi ruhen në të njëjtin segment.

Më pas në ciklin, mesazhi "Hello World!" shfaqet karakter për karakter. Për këtë qëllim përdoret funksioni 0x0E i ndërprerjes 0x10. Ai ka parametrat e mëposhtëm:
AH = 0x0E (numri i funksionit)
BH = numri i faqes së videos (mos u shqetësoni akoma, tregoni 0)
AL = kodi i karakterit ASCII

Në rreshtin "jmp$" programi ngrin. Dhe me të drejtë, nuk ka nevojë që ai të ekzekutojë kod shtesë. Sidoqoftë, në mënyrë që kompjuteri të funksionojë përsëri, do t'ju duhet të rindizni.

Në rreshtin "herë 0x1FE-finish+start db 0" pjesa tjetër e kodit të programit (përveç dy bajtit të fundit) është e mbushur me zero. Kjo bëhet në mënyrë që pas kompilimit, dy bajtët e fundit të programit të përmbajnë nënshkrimin e sektorit të nisjes.

Duket se kemi rregulluar kodin e programit, le të përpiqemi tani ta përpilojmë këtë lumturi. Për përpilim, do të na duhet, në mënyrë rigoroze, një montues - jazmi i lartpërmendur. Është i disponueshëm në shumicën e depove Linux. Programi mund të kompilohet si më poshtë:

$ yasm -f bin -o përshëndetje.bin përshëndetje.asm

Skedari hello.bin që rezulton duhet të shkruhet në sektorin e nisjes së disketës. Kjo është bërë diçka e tillë (natyrisht, në vend të fd ju duhet të zëvendësoni emrin e diskut tuaj).

$ dd if=hello.bin of=/dev/fd

Meqenëse jo të gjithë kanë ende disqe dhe disqe, mund të përdorni një makinë virtuale, për shembull, qemu ose VirtualBox. Për ta bërë këtë, do t'ju duhet të krijoni një imazh të diskut të diskut me ngarkuesin tonë dhe ta futni atë në "disket virtuale".
Krijoni një imazh të diskut dhe mbusheni me zero:

$ dd nëse=/dev/zero of=disk.img bs=1024 count=1440

Ne shkruajmë programin tonë në fillim të figurës:
$ dd if=hello.bin of=disk.img conv=notrunc

Ne lëshojmë imazhin që rezulton në qemu:
$ qemu -fda disk.img -boot a

Pas nisjes, duhet të shihni dritaren qemu me linjën e lumtur "Hello World!" Këtu përfundon artikulli i parë. Do të jemi të lumtur të shohim komentet dhe dëshirat tuaja.