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:
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: ```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 _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.

<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

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ć ?