OS w pascalu cz. 1 - Prosty shell (TMP)

lukasz1235
Jest to stara wersja tej części serii. Jak widać usunięcie jej to dla moderatorów wielki problem.

1 Wstęp
2 Na początek assembler
3 Kernel
4 Shell
5 Skrypt linkera
6 Makefile
7 QEMU
8 Zakończenie

Wstęp

W tym kursię pokażę jak napisać prosty system we Free Pascalu. Nasz OS (skrót od Operating System) będzie posiadał prostego shella, obsługę przerwań, wyjątków i systemu plików FAT. Na razie może nic ci to nie mówić, ale myślę, że po przeczytaniu tego kursu wszystko powoli się wyjaśni.
Do napisania systemu będziemy potrzebowali kilku narzędzi. Oto one;

Na początku stwórz gdzieś na dysku folder o nazwie 4pOS w którym będą przechowywane źródła systemu. Tam stwórz katalogi img i output. Całość powinna wyglądać tak:
OS_w_pascalu-img1.png

Na początek assembler

Jest to trochę dziwne, że w kursie o systemie w pascalu zacznę od assemblera, ale obiecuję, że później będziemy pisać głównie w pascalu.
Oto kod:

[BITS 32]
[SECTION .text]
EXTERN code,bss,end
mboot: ;informacje dla gruba
dd 0x1BADB002
dd 0x10001
dd -(0x1BADB002+0x10001)
dd bss
dd end
dd _start 
 
GLOBAL _start
 
EXTERN StartKernel
 
L6:
 jmp L6
 
_start:
call StartKernel ;uruchamiamy kernela w pascalu
 
[SECTION .bss]
kstack: resd 1024
 
[SECTION .data]

Pierwsza linijka informuje kompilator, że piszemy kod 32bitowy. Dalej mamy informacje dla GRUBa. A na końcu przechodzimy do kodu w pascalu.

Kernel

A ten kod umieszczamy w pliku kernel.pas w katalogu 4pOS.

unit kernel;
 
interface
 
uses shell;
 
procedure StartKernel;
function inportb(port:word):byte;
procedure outportb(port, zn:word);
procedure reset;
 
implementation
 
procedure StartKernel;[public,alias:'StartKernel'];
var
    i:integer;
begin
        for i:=0 to 127 do //dzieki temu nie beda wyswietlaly sie niepozadane znaki na starcie
                keyp[i]:=True;
 
        for i:=0 to 100 do //czyszczenie zmiennej "Command"
                Command[i]:=#0;
 
    ClearScreen;
    SetColor(White, Black);
    PrintStr('Witaj w systemie ');
    SetColor(Green, Black);
    PrintStr('4pOS!');
 
    PrintStr(#13);
    SetColor(LightGray,Black);
    PrintStr('>');
    SetColor(White,Black);
        while true do
                keys; //procedura odczytujaca nacisniete klawisze
end;
 
function inportb(port:word):byte;[public, alias: 'inportb'];
var
    temp : byte ;
begin
    asm
        mov dx,port
        in al,dx
        mov temp , al
    end;
end;
 
procedure outportb(port, zn:word);[public, alias: 'outportb'];
var
    zz:char;
begin
    zz:=char(zn);
    asm
        mov dx, port
        mov al, zz
        out dx, al
    end;
end;
 
procedure reset;[public, alias: 'reset'];
begin
    asm
        mov al, 0feh
        out 64h, al
    end;
end;
 
end.

Mamy tu procedurę startową, i kilka innych procedur, które przydadzą się póżniej.

Shell

Ponieważ shell pisze się najłatwiej i najprzyjemniej zaczniemy od niego.
Ale najpierw wyjaśnię co to jest shell.
Shell to inaczej powłoka. Jest pośrednikiem pomiędzy użytkownikiem a systemem operacyjnym. Od użytkownika przyjmuje polecenia, a system dzięki niemu wyświetla informacje dla użytkownika.

Plik shell.pas zaczniemy od ustalenia kilku stałych

const
    Black = 0;
    Blue = 1;
    Green = 2;
    Cyan = 3;
    Red = 4;
    Magenta = 5;
    Brown = 6;
    LightGray = 7;
    DarkGray = 8;
    LightBlue = 9;
    LightGreen = 10;
    LightCyan = 11;
    LightRed = 12;
    LightMagenta = 13;
    Yellow = 14;
    White = 15;
 
    F1=59;
    F2=60;
    F3=61;
    F4=62;
    F5=63;
    F6=64;
    F7=65;
    F8=66;
    F9=67;
    F10=68;
    F11=87;
    F12=88;
    Enter=28;
    Crtl=29;
    Alt=56;
    BkSpc=14;
    Space=57;
    Shift=42;
    CapsLock=58;
    up=72 ;
    left=75;
    right=77;
    down=80 ;
 
    tblchar : array [1..57] of char=
       ('0','1','2','3','4','5','6','7','8','9','0','?','=','0',' ',
       'Q','W','E','R','T','Y','U','I','O','P','[',']','0','0',
       'A','S','D','F','G','H','J','K','L',' ','{','}','0','0',
       'Z','X','C','V','B','N','M',',','.','/','0','*','0',' ');

... i zmiennych

var
    Screen: pchar =  PChar ($B8000);
    CursorX, CursorY: integer;
    Color: char;
    Background: integer = 0;
    backspace: array [0..4000] of integer;
    Command: PChar;
    e: integer = 0;
    keyp: array[0..127] of Boolean;
    address: Word;
    cmd: integer =0;

Odczytywanie naciśniętego klawisza to tylko odczytanie bajtu z portu 0x60, a potem zamienienie go na znak. Na koniec wysłanie bajtu 0x20 na port 0x20. W zależności od naciśniętego klawisza procedura wykonuje różne czynności. Dla klawisza backspace będzie to skasowanie ostatniego znaku, dla klawisza enter wywołania polecenia, a dla reszty wyświetlanie znaku na ekranie.

procedure keys;
var
    b: Byte;
    key: array[0..127] of Boolean; 
    z: integer=2;
begin
    b:= inportb($60); //czytamy z portu
    if b>$7F then
        key[b xor $80]:= False 
    else
        key[b]:= True;  
//BACKSPACE
    if key[BkSpc] then //jezeli nacisniety backspace
    begin 
        if keyp[BkSpc]<>True then
        begin
            address:=CursorX*2+CursorY*160;
            if backspace[address-2] = 1 then //jezeli zostalo wpisane z klawiatury
            begin
                CursorX := CursorX - 1; //cofamy kursor
                Screen[CursorX*2+CursorY*160]:=#0; //usuwamy znak
                e:=e-1; //cofamy pozycje komendy
                Command[e]:=#0; //usuwamy znak z komendy
                SetXY(CursorX,CursorY); //ustawiamy kursor
            end;
            keyp[BkSpc]:=True;
        end;
    end 
    else
    begin
        keyp[BkSpc]:=False;
    end;
//ENTER
    if key[enter] then //jezeli enter
    begin 
        if keyp[enter]<>True then
        begin
            Command2; //wykonujemy polecenie
            keyp[enter]:=True;
        end;
    end 
    else
    begin
        keyp[enter]:=False;
    end;
 
    repeat
        if key[z] then //jezeli cos innego
        begin
            if keyp[z]<>True then
            begin 
                PrintKey(tblchar[z]); //piszemy na ekranie
                Command[e]:=tblchar[z]; //dodajemy do komendy
                e+=1;
                keyp[z]:=True; 
            end;
        end
        else
            keyp[z]:=False;
        z+=1;
    until z=127;
    outportb($20, $20);
end;

Chwilę zatrzymajmy się na backspace. Wiadomo, że backspace kasuje tylko te znaki które uzytkownik wpisał z klawiatury, a nie ruszał tych które napisał program. Informacje o tym przechowuje zmienna backspace. Kiedy ma wartość 1 to znak możemy spokojnie skasować.

Mówiłem o pisaniu na ekran. Jest to nic innego niż kopiowanie znaku do pamięci ekranu. W nieparzystych komórkach tej tablicy przechowywany jest kolor znaku, a sam znak jest przechowywany w komórkach parzystych.
Procedury, które wypiszą na ekran znak (PrintKey) i łacuch (PrintStr):

procedure PrintKey(ch: Char);
var
    address: Word;
begin
    if (CursorX > 79) or (ch = #13) then
        SetXY(0, CursorY+1)
    else
    if ch = #10 then
        SetXY(CursorX, CursorY+1)
    else
    if CursorY > 24 then
    begin
        SetXY(CursorX, 24);
        Scroll;
    end;
 
    address:= CursorX*2 + CursorY * 160;
    Screen[address]:= ch;
    Screen[address+1]:= color;  
    backspace[address]:=1;
    backspace[address+1]:=1;
    SetXY(CursorX+1, CursorY);
end;
 
procedure PrintStr(text: PChar);
var
    address: Word;
    i: integer;
begin
    i:=0;
    if (CursorX > 79) or (text[i] = #13) then 
    begin
        SetXY(0, CursorY+1);
    end 
    else
    begin
        if (text[i] = #10) then
        begin
            SetXY(CursorX, CursorY+1);
        end
    else
    begin
        if (CursorY > 24) then
            begin
                SetXY(CursorX, 24);
                Scroll;
            end;
 
    repeat
        address:= CursorX*2 + CursorY * 160;
        Screen[address]:= text[i];
        Screen[address+1]:= color;  
        backspace[address]:=0;
        backspace[address+1]:=0;
        SetXY(CursorX+1, CursorY);
        i:=i + 1;
    until text[i] = #0
 
        end;
    end;
end;

Procedura do czyszczenia ekranu jest bardzo prosta

procedure ClearScreen;
var
    i: integer;
begin
    for i:=0 to 2000 do
    begin
        Screen[i*2]:=#0;
        Screen[i*2-1]:=char(white);
    end;
end;

A procedurą SetColor ustawimy kolor aktualnego znaku

procedure SetColor(txt,back: integer);
begin
    color:=char(txt+back*16);
end;

Trzeba jeszcze ustawić pozycję kursora

procedure SetXY(x,y: integer);
var
    temp: integer;
begin
    CursorX := x;
    CursorY := y;
    temp:=y*80+x;
    outportb($3D4, 14);
    outportb($3D5, temp>>8);
    outportb($3D4, 15);
    outportb($3D5, temp);
end;

I w przypadku gdy znaków będzie za dużo pomoże nam procedura Scroll

procedure Scroll;
var
    address : word;
begin
    for address:=0 to 1920 do
    begin
        Screen[address*2] := Screen[address*2+160];
        Screen[address*2+1] := Screen[address*2+1+160];
    end;
 
    for address:=1921 to 2000 do
    begin
        Screen[address*2]:=#0;
        Screen[address*2+1]:=char(15); 
    end;        
end;

Teraz pora na interpreter poleceń

procedure Command2;
var 
    pol : integer= 0;
    a:integer;
    i : integer = 0;
    q : pchar;
    b:integer=0;
    wynik: pchar;
    ii:integer =0;
begin
    cmd := 0;
    if (Command[0]='E') and (Command[1]='X') and (Command[2]='I') and (Command[3]='T') and (Command[4]=#0) then
    begin // exit
        reset;
    end
    else
    if (Command[0]='H') and (Command[1]='E') and (Command[2]='L') and (Command[3]='P') and (Command[4]=#0) then
    begin // help
        PrintStr(#13);
        PrintStr('EXIT - resetuje komputer');
        PrintStr(#13);
        PrintStr('HELP - wyswietla pomoc');
            end
    else
    begin
        PrintStr(#13);
        PrintStr('Polecenie "');
        PrintStr(Command);
        PrintStr('" nie zostalo odnalezione.');
    end;
 
    e:=0;
    PrintStr(#13);
    SetColor(LightGray,Black);
    PrintStr('>');
    SetColor(White,Black);
    repeat //czyszczenie zmiennej "Command"
        Command[pol]:=#0;
        pol+=1;
    until pol=100;
end;

I shell gotowy :)

Skrypt linkera

OUTPUT_FORMAT("elf32-i386")
ENTRY(_start)
SECTIONS
{
  .text  0x100000 :
  {
    text = .; _text = .; __text = .;
    *(.text)
    . = ALIGN(4096);
  }
  .data  :
  {
    data = .; _data = .; __data = .;
    *(.data)
    kimage_text = .;
    LONG(text);
    kimage_data = .;
    LONG(data);
    kimage_bss = .;
    LONG(bss);
    kimage_end = .;
    LONG(end);
    . = ALIGN(4096);
  }
  .bss  :
  {
    bss = .; _bss = .; __bss = .;
    *(.bss)
    . = ALIGN(4096);
  }
  end = .; _end = .; __end = .;
}

Nię będę go opisywał, bo szczerze mówiąc sam dokładnie nie wiem o co w nim chodzi ;)

Makefile

Makefile wygeneruje nam ładny obraz dyskietki

4pOS:
    nasm -f elf start.asm -o output/start.o
    fpc -a -Anasmelf kernel.pas -FE"output" -Fu"shell.pas" -RIntel
    ld --emit-relocs output/start.o output/kernel.o output/shell.o -T"kernel.ld" -o "4pOS.bin"
    sudo mount 4pOS.ima img -o loop
    sudo cp 4pOS.bin img
    sudo umount img

QEMU

Obraz wczytujemy do emulatora QEMU poleceniem
qemu -fda 4pOS.ima

Zakończenie

A oto efekt naszej pracy:
OS_w_pascalu-img2.png

Załączam też źródła:
OS_w_pascalu-src1.zip

3 komentarzy

W arcie wszystko jest napisane:

Kompilator NASM (http://sourceforge.net/projects/nasm/)

do kompilatora NASM bądź TASM (w tym przypadku chyba TASM, patrząc na składnię)

"Na początek assembler"
Do jakiego kompilatora to wpisać ?