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 Shell
4 Skrypt linkera
5 Makefile
6 QEMU
7 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; * Kompilator Free Pascal (http://www.freepascal.org) * Kompilator NASM (http://sourceforge.net/projects/nasm/) * Program make (http://pl.wikipedia.org/wiki/Make) * Linker GNU LD (http://osdev.pl/wiki/index.php/GNU_LD) * Emulatory QEMU (http://www.qemu.org/) lub Bochs (http://bochs.sourceforge.net/) * i program który pozwoli nam stworzyć obraz dyskietki, ja będę posługiwał się linuksowym mount (w Windowsie można użyć VFD (http://chitchat.at.infoseek.co.jp/vmware/vfd.html#download))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:
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: ```asm [BITS 32] [SECTION .text] EXTERN code,bss,end mboot: ;informacje dla gruba dd 0x1BADB002 dd 0x10001 dd -(0x1BADB002+0x10001) dd bss dd end dd _startGLOBAL _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.
<h1>Kernel</h1>
A ten kod umieszczamy w pliku kernel.pas w katalogu 4pOS.
```delphi
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](//static.4programmers.net/uploads/attachment/4ccd36dcb26e7.png)Załączam też źródła:
OS_w_pascalu-src1.zip
W arcie wszystko jest napisane:
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ć ?