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

Zdroj: SOOM.cz [ISSN 1804-7270]
Autor: DjH
Datum: 19.8.2008
Hodnocení/Hlasovalo: 2.28/46

V tomto díle provedeme mnoho změn. Přidáme si třeba paměťový manažer, přidáme si funkci printf(), ale jako hlavní, vypíšeme si náš první obsah složky

Tvoříme OS

Díl 3

Jako hlavní cíl tohoto dílu si dáme vypsání obsahu složky root. Ale jako hlavní změnu celého OS si přidáme jednoduchý paměťový manažer a funkci printf(). Napsal jsem „přidáme“, to znamená, že si je nebudeme programovat, poněvadž jsme líní a neschopní a chceme si okamžitě vypsat slíbený obsah složky. Práci jsem udělal za vás, funkce malloc(), free() a printf() jsem našel na internetu. Omlouvám se těm, kterým jsem zkazil radost tím, že jsem je připravil o programování správce paměti...

ještě než začneme...
...tak vám tu vypíšu funkce, které si ještě přidáme (a kam)
memcpy(), memcmp(), memset()      > do souboru mem.h
již zmíněný printf() a sprintf()  > do printf.h
malloc(), realloc(), free()       > do malloc.h

vytvoříme si soubor _null.h, a tam si definujeme pouze NULL: #define NULL 0
vytvoříme si soubor types.h, a tam budeme definovat své datové typy:
typedef unsigned char byte;
typedef unsigned int word;
typedef unsigned long dword;
typedef unsigned size_t;

Hezky rychle jsme přešli práci s pamětí, někteří to možná budou mít za zlé, ale proč se namáhat s paměťovým manažerem? Vždyť to je nuda...

Hurá na disk!
Disk? To je aspoň zábava! Budeme číst z disku přes BIOSový interrupt int 13h, ale nejdříve si povíme něco o struktuře diskety. Disketa má hlavy, disketa má stopy, disketa má sektory. Klasická 3,5“ 1.44 MB disketa má 2 hlavy, každá hlava má 80 stop, každá stopa má 18 sektorů a každý sektor má 512 bajtů. Lze tak potom vypočítat velikost disku: 2*80*18*512 = 1'474'560 bajtů. Bootloader se nachází na prvních 512ti bajtech v disketě, tedy hlava 0, stopa 0, sektor 1. BIOSový int 13h však nerozumí tomu, že chceme přečíst data od 27‘136-tého bajtu po 31‘232-hý bajt. S funkcí AH 2 (read), interruptu 13h se zachází takto:
DL = číslo mechaniky
DH = číslo hlavy
CH = číslo stopy
CL = číslo sektoru
AL = kolik sektorů číst (do jedné stopy)
ES:BX = adresa bufferu


Před samotným čtením z disku je nejlepší mechaniku vyresetovat funkcí AH 0, intu 13h:
/*
 *  resetdisk()
 *  restartuje floppy A: 
 */
void resetdisk() {
resetd:
  asm mov ax, 0  ;
  asm mov dl, 0  ;
  asm int 13h    ;
  asm jc resetd  ;
}

Našimi aktuálními znalostmi snadno vytvoříme funkci bios_read_disk():
/*
 *  bios_read_disk()
 *  nahraje do pameti <offset> <sectors> sektoru, na pozici CHS 
 */
void bios_read_disk(byte cylinder, byte head, byte sector, byte sectors, void *offset) {
  resetdisk();
  asm cli;
  _ES = _DS;            /* BUFF 			*/ 
  _BX = (int)offset;    /* BUFF 			*/  
  
  _AH = 2;              /* READ 			*/
  _AL = sectors;        /* pocet sektoru  */
  
  _CH = cylinder;       /* cislo stopy    */ 
  _DH = head;           /* cislo hlavy    */
  _CL = sector;         /* cislo sektoru  */ 
  
  _DL = 0;              /* cislo drive    */
  asm sti;
  __int__(0x13);

  return;
}
Nic zajímavého na kódu není, ale přece jenom vysvětlím řádky
  _ES = _DS;            /* BUFF 			*/ 
  _BX = (int)offset;    /* BUFF 			*/  
Program v Turbo C ukládá poitery v ds:[offset]. Pokud tedy chceme načíst data tam, kam ukazuje ukazatel na offset, musíme ES (Extra Segment) nastavit na DS (Data Segment). BX už jen nastavíme na ukazatel „offset“, protože v podstatě „void *“, nebo „char *“ je „int“, tedy číslo - ukazatel, kde v paměti v daném segmentu začínají data.
My ale budeme chtít s diskem pracovat jako s „rovnou plochou“. Musíme si tedy udělat funkci pro převod z fyzické adresy na CHS (Cylindr (stopa), Hlava, Sektor):

/*
 *  readdisk()
 *  nacte do <buffer> cast disku od <start> s poctem <sectors> sektoru
 *  - start je fyzicka adresa disku, prevadi se na CHS  
 */ 
void readdisk(int start, int sectors, void *buffer) {
  unsigned int c=0,
               h=0,
               s=start /*((addr/512)+1)*/ ;
  
  if(s > 36) {
    c = (s/36);
    s = (s%36);
  }
  
  if(s > 18) {
    h = (s/18);
    s = (s%18);
  }
      
  bios_read_disk((byte)c,(byte)h,(byte)s, sectors, buffer);
  return;
}

Myslím, že není co dodat – zadáme počáteční sektor (což je (fyzicka_adresa/512)+1), buffer a počet sektorů k načtení, funkce zjistí CHS:
Pokud je sektor vyšší než 36 -> stopu (c) zjistí vydělením sektorů 36ti (18 sektorů * 2 hlavy), zbytek sektorů je ponecháno.
Pokud je (stále) sektorů více než 18, zjistíme hlavu vydělením 18ti (vyjde nám buď 1 nebo 0, protože sektor nemůže být vyšší jak 35) a zbytek sektorů se opět ponechává. Tyto hodnoty se proženou funkcí bios_read_disk(), která načte do bufferu data z požadované pozice.
Abychom mohli číst z disku s FAT, musíme si vytvořit pár struktur – třeba bootloaderu, protože tam jsou důležité ůdaje o FAT12 - a pár globálních proměnných – například kde na disketě začínají data -.

/* struktura MasterBootRecordu */
typedef struct {
	word	jump;
	byte	nop;
	byte	oem[8];
	word	sector_size;
	byte	cluster_size;
	word	sector_reserved;
	byte	fat_count;
	word	root_size;
	word	total_sectors16;
	byte	media;
	word	fat_size16;
	word	sectors_per_track;
	word	head_count;
	dword	sectors_hidden;
	dword	total_sectors32;
	byte	phys_drv;
	byte	reserved;
	byte	ext_sign;
	dword	serial;
	byte	label[11];
	byte	fs_id[8];
	byte	data[512-62-2];
	word	boot_sign;
	} BootRecord;

My si vytvoříme pár globálních proměnných. Dále do init.h inkludujeme fat.h, kde bude výše uvedená struktura, inkludujeme malloc.h a global.h, a dodáme tuto funkci:
int iBootLoader(void) {
  mbr = malloc(sizeof(BootRecord));
  readdisk(1, 1, mbr);
  
  RootDirSect = (((mbr->root_size*32)+(mbr->sector_size-1))/mbr->sector_size); /* vypocitej kolik sektoru ma root*/
  FATSectors	 = mbr->fat_count*mbr->fat_size16;	                               /* kolik ma fat sektoru   */
  DataStart	 = mbr->sector_reserved+FATSectors+RootDirSect;          /* zacatek dat    */
  StartSector    = (DataStart+1);
  return 0;
}

Tím si zinicalizujeme celý bootsektor do ukazatele na pointer proměnné mbr. Nyní si vytvoříme strukturu takového souboru ve FAT:

/* struktura "casu" u souboru */
typedef struct {
	word
		sec          :5,
		min          :6,
		hour         :5;
	} FileTime;

/* struktura "datumu" u souboru */
typedef struct {
  word
  	day          :5,    /* 5 je pocet bitu kolikami se vyjadruje pocet dni v mesici 2^5 = 32-1 = 31dni */
  	month        :4,    /* 2^4-1=15 max 15 mesicu, nam staci jen 12 :) */
  	year         :7;
  } FileDate;

/* struktura celeho souboru ve FAT */
typedef struct {
  byte name[8];
  byte ext[3];
  byte attr;            /* atribut */
  byte reserved[0x0A];
  FileTime time;
  FileDate date;
  word ClstrNo;
  dword filesize;
  byte realname[12];   /* skutecne jmeno + pripona */
  } DirectoryEntry;

Nyní se můžeme pustit do funkcí pro čtení z disku na úrovni souborů. Můžeme si tedy vytvořit funkci na vypsání složky ROOT. Vytvoříme si funkci, která nám převede 8.3 název na klasický název souboru:
/*
 *  getRealFileName()
 *  vytvori nazev souboru s teckou a bez mezer z <name> a <ext>
 *  a vrati ho v <realname> 
 */  
char *getRealFileName(char *name, char *ext, char *realname) {
int cykl = 0;
   while((*name != ' ') && (*name != '\0') && (cykl != 8)) {        
    *realname = *name;
    realname++;
    name++;
    cykl++;
  }
  if(!(*(ext)==' ' && *(ext+1)==' ' && *(ext+2)==' ')) {
    *realname++ = '.';

    cykl = 0;  
    while((*ext != ' ') && (*ext != '\0') && (cykl != 3)) {    
      *realname = *ext;
      realname++;
      ext++;
      cykl++;
    }
  }
  *realname = '\0';
  return realname;  
}

Vytvoříme si funkci, která nám asociuje blok paměti se souborem:
/*
 *  assocfile()
 *  asociuje hodnoty pro <file>
 */  
void assocfile(char *mem, DirectoryEntry *file) {
  memcpy(file->name,         (mem), 8);
  memcpy(file->ext,          (mem+8), 3);
  file->attr     = *(byte*)  (mem+11);
  file->ClstrNo  = *(word*)  (mem+0x1A);
  file->filesize = *(dword*) (mem+0x1C);
  getRealFileName(file->name, file->ext, file->realname);
  return;
}

a naší nejhlavnější funkci pro hledání souborů:
/*
 *  findfile()
 *  hleda soubor <filename>, vraci 0 pri nenalezenem,
 *  1 pri nalezenem souboru , a nastavi do <file> dane atributy
 */ 
int findfile(DirectoryEntry *file, int pos) {
signed int _ret   = pos;
int _stranka      = 0;
int _pos          = 0;
char *bkp_sdBuff;                           /* zaloha sdBuff (viz. nize) */
char *sdBuff      = (char*)malloc(0x200);   /* alokujeme 512 bajtu pro sektor */

bkp_sdBuff = sdBuff;

_stranka = (pos/16);          /* ((pos*32)/512) zjistime na kolikate 512ti bajtove strance je pointer na dalsi fajl */
_pos     = (pos%16);          /* (((pos*32)%512)*32) */

  readdisk((dir_ptr+_stranka), 1, sdBuff);
  sdBuff += (_pos*32);
  while(1) { 
  if (*sdBuff == '\0') { _ret = -1; break; }
  if ((*sdBuff == DELETED) || (*(sdBuff+0x0B) == 0x08)) { /* vymazany soubor? Label? Dekuji, nechci */
     sdBuff += 32;
     _ret++;
     _pos++;
     if(_pos >= 16) {
       _stranka = (_pos/16);
       _pos     = (_pos%16);
       readdisk((dir_ptr+_stranka), 1, sdBuff);
       sdBuff += (_pos*32);
     }
    } else {
      assocfile(sdBuff, file);
      sdBuff  += 32;
      _ret++;
      _pos++;
      break;
    }
  }
  free(bkp_sdBuff);                         /* zde dealokujeme puvodni sdBuff */
  return (_ret);
}

Z kódu je myslím zkušenému vše jasné, jen podotknu, že proměnná dir_ptr je (int), a to číslo ukazuje na sektor, kde začíná aktuální složka. V 1,44MB disketě začíná root složka na 20 sektoru. Tato proměnná nám později pomůže s jednodušším zacházením s chdir() (tedy se změnou aktuální složky). Možnost vypsání složky zatím implementujeme pouze do shellu, tedy do kernel.c přidáme:
int main(void) {
char            cmd[80];
int             i       = 0;
DirectoryEntry *file    = malloc(sizeof(DirectoryEntry));

  init();
  while(1) {
    if(strlen(cmd)!=0) {
      if(!strcmp(cmd,"dir")) {
        printf("Nazev souboru | Velikost\r\n");
        i = 0;
        while((i = findfile(file, i)) != -1)       /* najdeme soubor */
          printf(" %12s > %u bajtu\r\n", file->realname, file->filesize);
      } else {
        printf("Zadali jste retezec '");
          textcolor(YELLOW);
        printf("%s", cmd);
          textcolor(LIGHTGRAY);
        printf("'\r\n");
      }
    }
    printf("[%s@%s]%s$ ", user_name, pc_name, path);
    gets(cmd, 79);
  }
  return 0;
}

Po startu se provede standardní inicializace a nastartuje se shell. Pokud zadáte „dir“, vypíše se vám obsah složky root, pokud zadáte něco jiného, žlutě vám vypíše daný řetězec. Vím, není to nic extra, ale jako dnešní ukázka to stačí. Ještě vám přiložím seznam vylepšení, které jsem provedl a nenapsal tady v postupu: přidal jsem pár funkcí do string.h, tento soubor tedy obsahuje funkce:
/*
 *  string.h
 *  ruzne prace pro praci se stringy
 *  kopirovani, porovnavani, presouvani...  
 *  funkce:
 *    putchar()     - vytiskne znak
 *    strcat()      - spoji dva stringy
 *    strcmp()      - porovna dva stringy 
 *    strcpy()      - natavi string
 *    strlen()      - vrati delku stringu
 *    toupper()     - zvetsi pismeno (a-z) z maleho na velke
 *    tolower()     - zmensi pismeno (A-Z) z velkeho na male  
 *    upStr()       - zvetsi pismena ve stringu z malych na velke
 *    downStr()     - zmensi pismena ve stringu z velkych na male
 *    puts()        - vytiskne string (neformatovane)
 *    pos()         - urci pozici prvniho znaku ve string
 *    getchar()     - sejme znak z klavesnice, vrati jako [int]
 *    gets()        - sejme string z klavesnice
 *    itoa()        - prevede integer na string
 */

...
Přidal jsem pár funkcí do video.h:
/*
 *  video.h
 *  ruzne fce pro praci s monitorem a textovym kurzorem
 *    
 *  funkce:
 *    gotoxy()      - posune textovy kurzor na radek x, sloupec y
 *    getxy()       - zjisti v jakem radku a sloupci se nachazi textovy kurzor
 *    vidmode()     - nastavi danne video mode
 *    textcolor()   - "alokuje" znaky v konzoli barvou 
 *    prowdown()    - prida x radku ze spoda, se zadanou barvou
 *    prowup()      - prida x radku ze zhora, se zadanou barvou
 *    rowdown()     - prida x radku ze spoda, s implictni barvou
 *    rowup()       - prida x radku ze zhora, s implictni barvou
 *    clrscr()      - vymaze obrazovku, presune kurzor na 0, 0 
 *    scrchr()      - vrati znak, ktery je na aktualni pozici kurzoru
 *    scrchrxy()    - vrati znak, ktery je na pozici x,y
 *    charto()      - napise znak <ch> na pozici <x, y> 
 */

...a dalších pár funkcí pro vaši zábavu a pohodlnost, třeba v init.h je funkce iRslt(int), která podle vstupního čísla vypíše stav akce (
0 – [  OK  ]
1 – [ERROR!]
2 – [ WARN ] atp)
Dále po startu vykreslí krásným ASCII Artem na vrch obrazovky AltairOS (viz screenshot níže). Více se taky dozvíte po shlédnutí dosud kompletního kódu. Ještě vám dám inspiraci, co se týče výpisu složky > do image našeho floppy dejte (v UltraISO) obsah (ne složku, jen obsah) složky include, aby nám „dir“ měl co vypsat. Ještě dodám, že příště bych se chtěl podívat na výpis souboru (pod linuxem známo jako „cat“, na Windows jako „type“). No a na úplný závěr přikládám 2 screenshoty:



archív zde