Tvoříme OS [2.díl]

Zdroj: SOOM.cz [ISSN 1804-7270]
Autor: DjH
Datum: 5.8.2008
Hodnocení/Hlasovalo: 2.12/67

Nyní již nabootujeme, a nahrajeme do paměti náš první jednoduchý kernel

Tvoříme OS

Díl 2

Nyní již pokročíme a věřte nebo ne, něco si na monitor vypíšeme. Jestli jste čekali, že budeme dělat bootloader, musím vás zklamat, je to nudné, obzvlášť když na internetu jich pár je. Doufám, že vás potěší alespoň to, že si ho upravíme.

Na něco jsme nemysleli...
Tento odstavec je krátký jak sami vidíte, jde jen o to, co hlavního nám vypadlo... Je to jméno OS. Já tomu dávám název AltairOS, podle prvního PC. Vy si tomu říkejte třeba MadonnaOS, ale já to budu přizpůsobovat na jméno AltairOS. Tento název pro OS vymysleli kamarádi independent a babča ještě v dobách, kdy moje myšlenky na stvoření vlastního OS byly někde na kyberhoubách.

Bootloader – co, kde, jak a proč, ve stručnosti
Disketový bootloader je 512 bajtů dlouhý kód v čisté binárce, zakončený wordem 0xAA55. BIOS po startu a pár kontrolách zjistí, jestli má bootovat i z diskety (pokud dosavad z ničeho jiného nenabootoval). Pokud zjistí, že nenabootoval a na disketě se zmiňovaný sektor nachází, BIOS ho nahraje do paměti 7C00h, (7c00h:0000). Odteď mu předává BIOS řízení. 512 bajtů? Není to málo? Ano je, ale Bůh není tak krutý a stačí to na nahrání dalšího programu/kernelu/ovladače.

Našel jsem vynikající bootsektor psaný v NASM, který na FAT12 formátované disketě najde určitý program, nahraje ho, a spustí. Dokonce umí nahrát i EXE, ale k čemu nám to probůh je, když nemáme ani int 21h, že? Tuto část nahrávající EXE můžeme vymazat, a nahradit vlastním kódem.

Vyhledávám ‚#‘, nahrávám ‚>‘
Upravíme to takto:
Nejprve vypíšeme hlášku „Zavadim jadro“, poté se bude nahrávat jádro do paměti na adresu 1000h:0000 (fyzická adresa: 10000h).
Při vyhledávání našeho požadovaného jádra na disketě, budeme vypisovat znak „#“, a při jeho nahrávání do paměti, budeme vypisovat „>“ na spodní pozici monitoru (řádek = 24, sloupec = 1). Po nahrání do paměti skočíme na obrazovce zpět na konec řetězce „Zavadim jadro“. To proto, abychom za to elegantně zapsali [  OK  ]
Jádro musí ovšem začínat segmentem 0, a ne 100h jako obyčejné DOS *.com aplikace. Kód bootloaderu tu rozebírat nebudu, je přiložen v archívu a hezky okomentovaný.

Hello world? Kdepak, trapné
Už se dostáváme do části, kde budeme psát v C, a to si nemůžeme nechat líbit, zahanbit se Hello worldem! Napíšeme si pár funkcí:
putchar()
getchar()
puts()
gets()
strcpy()
strcat()
a textcolor(), aby jsme si obarvili svět.

Vytvoříme si 6 souborů: asm.h, global.h, string.h, video.h, init.h a kernel.c, a budou přibývat. Do těchto souborů postupně budeme zapisovat funkce. Mám tu před vámi ten kód do všech souborů napsat? No dobře, ukecali jste mě.
Začneme global.h:
#ifndef _GLOBAL_INCLUDED_
#define _GLOBAL_INCLUDED_
/*
 *  global.h
 *  Zde jsou ulozene globalni promenne, 
 *  ktere se muzou pouzivat v celem tele kernelu.
 */


/* TEXT_COL, globalni promenna urcujici barvu textu */
char TEXT_COL = 0x07;

#endif
Jak jste si všimli, ve hlavičkovém souboru se nachází #ifndef ... podmínka. To znamená, pokud byl již soubor inkludován, neměl by se inkludovat znovu. Toto tomu zabrání. Dále následuje krátký popis o souboru, může tam být i výčet a krátký popis funkcí a typů, které se v hlavičkovém souboru nacházejí. Zde budu psát jen důležité funkce, či jejich hlavní části. Nejaktuálnější zdrojové kódy jsou vždy v archivu.
asm.h:
Zde se musí aktivovat makro pro Turbo C, které nám umožní jednoduché operace s registry dělat bez vkládaného ASM, a psát interrupt jako funkci __int__(char interrupt); Budou se zde nacházet funkce na té úplně nejnižší úrovni.
#define _Cdecl
void          _Cdecl	__int__        (int interruptnum);
...
To nám pomůže lépe psát funkce:
/*
 *  getch()
 *  pocka na stisk klavesy, vrati jeji ASCII hodnotu 
 */ 
char getch() {
  _AX = 0x00;
  __int__(0x16);
  return _AL;
}

/*
 *  putch()
 *  vytiskne ASCII znak
 */ 
void putch(char s) {
  if((s!='\r') && (s!='\n') && (s!='\t') && (s!='\b')) { /* barevne
                                          NEtisknout tyto znaky */
    _AL = 0;
    _CX = 1;
    _BH = 0;
    _BL = TEXT_COL;
    _AH = 0x09;
    __int__(0x10);
  }
  
  _AL = s;
  _AH = 0x0e;
  _BL = 0x07;
  __int__(0x10);
}
video.h:
Zde budou funkce pro různé práci s textovým kurzorem, a všeho co se děje zvláštního na monitoru, jako základní kámen sem pokládám funkci
void textcolor(char col) {
  TEXT_COL = col;
}
string.h:
Tady nás čekají různé operace s textovými řetězci jako kopírování, spojování, zjištění délky...
/*
 *  putchar()
 *  vytiskne ASCII znak, pokud je vstup 0x08
 *  (backspace), umaze jeden znak
 */ 
void putchar(char s) {
  if(s == '\b') {     /* backspace */
    putch('\b');
    putch(' ');
    putch('\b');
  } else {
    putch(s);
  }  
}
   > Tisknutý znak (backspace) přes interrupt obrazovky posune kurzor o jedno místo doleva (o konec se zastaví), ale znak nevymaže, pomůžeme tomu tak, že posuneme kurzor doleva, přepíšeme znak mezerou a vrátíme se zpět.

/*
 *  puts()
 *  vstup: ukazatel na string, ktery se bude tisknout na vystup 
 *  vystup: void
 */
void puts(const char *str) {
  int i;
  for(i = 0; str[i] != '\0' ; i++) {
    putchar(str[i]);
  } 
}

...
char *strcpy(char * dest,const char *src) {
char *tmp = dest;
  while ((*dest++ = *src++) != '\0');
    return tmp;
}
...
char *strcat(char * pDst, const char *pSrc) {
    char *r = pDst;

    while (*pDst != '\0')
        pDst++;

    while ((*pDst++ = *pSrc++) != '\0')
        continue;

    return r;
}
   > funkci gets() jsem pořešil trochu jinak, a to tak, že jako parametr se nepřidá jen buffer, ale i maximální délka povoleného stringu k zadání. Tím zamezíme přetečení bufferu.

/*
 *  gets() 
 *  sejme string z klavesnice o maximalni delce <size>,
 *  kazdy znak vytiskne na monitor
 */ 
char *gets(char *cs, int size) {
  unsigned int counter = 0;
  unsigned char c = '\0';
  char *s;

  s = cs;
  while (1) {
  if(counter >= size) break;
  c = getch();
  if(c == '\r') break;
  if(c == '\n') break;
  if(c == '\0') break;
  if(c == '\t') c = ' ';

    if(!(counter == 0 && c == '\b')) {
      putchar(c);
    }
    if( c == '\b' ) {
      if (counter > 0)
      counter--;
    }
    else {
      *(s+(counter++)) = c;
    }
  }
  puts("\r\n");
  *(s+(counter++)) = '\0';
  return s;
}
init.h:
Zde se budou nacházet funkce, které se provedou bezprostředně po bootu. Nachází se zde funkce init(); která je volaná jako úplně první hned po bootu:
void init(void) {
  puts("............................ [  ");
  textcolor(LIGHTGREEN);
  puts("OK");
  textcolor(LIGHTGRAY);
  puts("  ]\r\n\n");
  puts("Startuji SHELL...\r\n\n");
  
  return;
}
kernel.c: Nyní půjdemé psát samotné jednoduché jádro:
/* 
 * KERNEL 
 */

#include <asm.h>
#include <global.h>
#include <init.h>
#include <string.h>
#include <video.h>

int main(void) {
char cmd[80];
  init();

  while(1) {
    puts("[altair@os]/$ ");
    gets(cmd, 79);
    puts("Zadali jste: '");
    puts(cmd);
    puts("'\r\n");
  }
  return 0;
}
Jak jste si jistě všimli, inkludujeme všechny hlavičkové soubory. Mělo by být jedno v jakém pořadí. Nyní po startu vám OS vypíše [altair@os]/$ a počká na váš string ukončený enterem, nebo 79 znakem. Po stisknutí enteru se váš zadaný řetězec vypíše na monitor.
   Nyní jdeme na kompilování aplikace. Ve složce sources/kernel máme soubor kernel.c. Přidáme do něj dva nové soubory: kernel_a.asm a kernel_j.conf. Jde o to, že náš kernel musí někde startovat. K tomu nám pomůže kernel_a.asm:
[BITS 16]       ; budeme pracovat v 16tibit. rezimu (RealMode)
[EXTERN _main]  ; fce main() z kernel.c, C pri kompilaci
                ; do OBJ pridava podtrzitko pred kazdy nazev fce
[GLOBAL start]  ; start bude mozno pouzit i jinde

start:
  call _main    ; zavolame main() z kernel.c
  jmp $         ; halt PC -> neco jako 'halt: jmp halt'
Myslím že není co dodat a že je to jasné. Nyní to jen musíme nějak slinkovat. Na to nám pomůže jednoduchý jloc skriptík:
ALL: 
	tmp\kernel_a.obj 
	tmp\kernel.obj 
	CBL: 0 0 0 
	*
Třetí řádku trochu vysvětím, CBL je jen název „odkazu“, první nula je jaké mají mít segmentové registry číslo, což je nula, druhá nula je číslo, na jakém segmentu začíná aplikace. Kdyby to byla výše zmiňovaná *.com, tak tam bude 100, a třetí nula je začátek kódu, kde aplikace začíná. Hvězdička na konci značí konec sekce a může se linkovat.
Na závěr přidávám obrázek:

a archív