---------------------------------------------------------------------------------------------------
WLASNY BOOTLOADER W NASM by t0m_k
---------------------------------------------------------------------------------------------------
0x00 - zaopatrzenie
0x01 - troche teorii
0x02 - pamiec bios
0x03 - hello world
0x04 - tryb graficzny
0x05 - stos
0x06 - ladowanie kernela
0x07 - A20
0x08 - tryb chroniony
0x09 - koniec, czyli gotowiec plus kernel ;)
---------------------------------------------------------------------------------------------------
0x00 ZAOPATRZENIE
---------------------------------------------------------------------------------------------------
Na poczatku trzeba sie zaopatrzyc w jakis system Linux, Unix, badz Windows, w kilka narzedzi oraz
wypadaloby znac podstawy asemblera i orientowac sie w architekturze x86.
Programy, z ktorych bede korzystal:
- NASM - http://sourceforge.net/projects/nasm/files/
- DD - standardowo w Uniksach powinien byc, dla Windowsa: http://www.chrysocome.net/download
- QEMU - http://www.qemu.org/download.html
- KQEMU - j.w.
- DJGPP - dla Windowsa, z MinGW moga wystepowac problemy przy linkowaniu calego kodu
- merge - maly program, tworzacy z naszych kodow obraz dyskietki, przydatny na Linuksie, kod
programu znajduje sie w paczce na koncu tutorialu
---------------------------------------------------------------------------------------------------
0x01 TROCHE TEORII
---------------------------------------------------------------------------------------------------
Komputer po uruchomieniu pracuje w trybie rzeczywistym procesora, w ktorym mamy dostep do okolo
1MB pamieci, pamiec jest podzielona na segmenty po 64kB oraz mamy dostep do przerwan, dzieki
systemowi bios, ktory tworzy tablice wektorow przerwan na najnizszych adresach pamieci.
Przerwan jest tylko 256. Bios oprocz tej tablicy umieszcza w pamieci kilka potrzenych dla siebie
danych. Nastepnie wykonywana jest sekwencja bootujaca.
Jesli ustawimy w biosie, ze od razu chcemy korzystac z dysku twardego, to bios sprawdzi, czy jest
taka mozliwosc. Tutaj wlasnie mozemy zaobserwowac jak wazna jest rola bootloadera do uruchomienia
systemu, poniewaz bez bootloader'a nie ma mowy o korzystaniu z systemu.
Bios nie uruchamia systemu ale ma za zadanie znalezc program bootujacy, czyli wlasnie bootloader,
ktory to uczyni.
Bootloader, aby byc rozpoznanym przez bios, musi :
- znajdowac sie w pierwszym sektorze dysku, plyty, dyskietki, czy tez innego nosnika danych
- byc zaladowany pod adres 0x7C00
- zajmowac dokladnie jeden sektor dysku
- konczyc sie dwoma bajtami o wartosciach 0x55 oraz 0xAA
---------------------------------------------------------------------------------------------------
0x02 PAMIEC BIOS
---------------------------------------------------------------------------------------------------
0x00000000 - 0x000003FF - tablica wektorow przerwan
0x00000400 - 0x000004FF - BDA przestrzen danych biosu
0x00000500 - 0x00007BFF - wolna, dostepna pamiec
0x00007C00 - 0x00007DFF - miejsce na bootloader
0x00007E00 - 0x0007FFFF - wolna, dostepna pamiec
0x00080000 - 0x0009FBFF - wolna, dostepna pamiec, jesli istnieje
0x0009FC00 - 0x0009FFFF - EBDA rozszerzona przestzen danych biosu
----------------------- -------------------------------------------
0x0000A000 : 0x00000000 - pamiec video karty VGA
0x0000B000 : 0x00000000 - pamiec video karty Hercules Monochrome
0x0000B800 : 0x00000000 - pamiec trybu tekstowego karty VGA
---------------------------------------------------------------------------------------------------
0x03 HELLO WORLD
---------------------------------------------------------------------------------------------------
Na poczatek stworzymy obraz dyskietki, poniewaz watpie zeby ktokolwiek mial jeszcze stacje
dyskietek ale zanim to zrobimy na Linuksach, musimy zaladowac modul kqemu:
modprobe kqemu
Obraz tworzymy poleceniem:
qemu-img create test.img 2880 ; lub 1440
Mozemy rownie dobrze podac inny rozmiar ale inaczej bedzie wygladalo bootowanie go przez emulator,
a nie jest to miejsce na opisywanie jak korzystac z qemu.
Wystarczajace minimum zostalo omowione, wiec mozemy sie zabrac za pisanie, oto przykladowy kod:
///////////////////////////////////////////////////////////////////////////////////////////////////
[ORG 0x7C00] ; ustawiamy CS na 0x7C00
xor ax, ax ; zerujemy AX
mov ds, ax ; do DS dajemy zero
mov si, hello ; kopiujemy hello do SI
pisz:
lodsb ; pobieramy jeden znak z SI do AL
or al, al ; sprawdzamy czy AL == 0
jz dalej ; jesli tak to przestajemy pisac
mov ah, 0x0E ; do ah kopiujemy numer funkcji
int 0x10 ; wywolujemy przerwanie
jmp pisz ; wracamy do dalszego pisania
dalej:
jmp dalej
;------------------------------------------------ ponizej dane
hello db "Hello World!", 13, 10, 0 ; nasz napis
times 510-($-$$) db 0 ; wypelniamy pozostale bajty zerami
db 0x55 ; nie chyba
db 0xAA ; trzeba tlumaczyc
///////////////////////////////////////////////////////////////////////////////////////////////////
Kompilujemy kod poleceniem:
-LINUX, WINDOWS:
nasm -f bin boot.asm -o boot.bin
Zapisujemy w naszym obrazie:
-LINUX, WINDOWS:
dd if=boot.bin of=test.img count=1
Wreszcie testujemy:
-LINUX, WINDOWS:
qemu -fda test.img -boot a
---------------------------------------------------------------------------------------------------
0x04 TRYB GRAFICZNY
---------------------------------------------------------------------------------------------------
Standardowo nasze kody pracuja w trybie tekstowym karty VGA, dopoki nie zmienimy trybu.
Ekran w tym trybie ma rozmiar 80x25, co oznacza 2000 znakow, czyli 4000 bajtow, poniewaz kazdy
znak sklada sie z dwoch bajtow, odpowiednio samego znaku oraz jego atrybutu.
Znak 1, czyli znak w lewym gornym rogu znajduje sie pod adresem 0xB800:0x0000, a jego atrybut
pod adresem 0xB800:0x0001. Oczywiscie przy bezposrednim odwolywaniu sie do pamieci ekranu
sami musimy zadbac o przejscie do nowej linii.
Przykladowy kod wypisujacy Hello World na kolorowo:
///////////////////////////////////////////////////////////////////////////////////////////////////
[ORG 0x7C00]
xor ax, ax
mov ds, ax
mov ax, 0xB800
mov es, ax ; ustawienie pamieci ekranu w segmencie ES
mov di, 0x0100 ; skopiowanie do DI pozycji poczatkowej tekstu
mov si, msg ; do SI tekst
mov cx, dlg ; do CX dlugosc tekstu, czyli ilosc powtorzen rep
rep movsb ; skopiuj bajt z DS:SI do ES:DI
jmp $ ; nieskonczona petla
;------------------------------------------------ ponizej dane
msg db "H",0x01,"e",0x02,"l",0x23,"l",0x32,"o",0x5b," ",
0x96,"W",0x62,"o",0x7d,"r",0x3e,"l",0x23,"d",0x67
dlg equ $-msg
times 510-($-$$) db 0
db 0x55
db 0xAA
///////////////////////////////////////////////////////////////////////////////////////////////////
Kolory znakow sa okreslone liczbami od 0x00 do 0x0F, a kolory tla od 0x00 do 0x70.
Mozemy oczywiscie skorzystac z trybu 13h, badz innego:
///////////////////////////////////////////////////////////////////////////////////////////////////
; inicjalizacja trybu graficznego
xor ah, ah
mov al, 0x13
int 0x10
; powrot do trybu tekstowego
xor ah, ah
mov al, 0x3
int 0x10
///////////////////////////////////////////////////////////////////////////////////////////////////
---------------------------------------------------------------------------------------------------
0x05 STOS
---------------------------------------------------------------------------------------------------
Jakby nie bylo przydal by nam sie stos oraz mozna by do wypisywania tekstow napisac funkcje:
///////////////////////////////////////////////////////////////////////////////////////////////////
[ORG 0x7C00]
jmp short start ; skaczemy na poczatek, pomijajac funkcje
print: ; funkcja wypisujaca tekst
pisz:
lodsb ; pobieramy jeden znak z SI do AL
or al, al
jz dalej
mov ah, 0x0E
int 0x10
jmp pisz
dalej:
ret ; powrot do miejsca wywolania funkcji
start:
xor ax, ax
mov ds, ax
mov ss, ax ; ustawiamy segment stosu
mov sp, 0x9C00 ; ustawiamy wskaznik stosu
push hello ; odkladamy na stos napis
pop si ; zdejmujemy ze stosu napis i zapisujemy w SI
call print ; wywolanie naszej funkcji
mov si, copy
call print ; znowu wywolanie naszej funkcji
jmp $ ; petla nieskonczona
;------------------------------------------------ ponizej dane
hello db "Bootloader", 13, 10, 0
copy db "(c) 2010 by t0m_k", 13, 10, 0
times 510-($-$$) db 0
db 0x55
db 0xAA
///////////////////////////////////////////////////////////////////////////////////////////////////
W powyzszym przykladzie przydzielilismy na stos 0x9C00 bajtow oraz moglismy skorzystac
z instrukcji push, pop, jak rowniez wykonywac wszystkie operacje na stosie.
Nie jest nigdzie napisane, ani nie jest powiedziane ze wskaznik oraz wierzcholek stosu musza byc
wlasnie pod tymi adresami. Wazne, aby byly w dostepnej dla nas pamieci oraz wszystko zeby
bylo rozmieszczone ze zdrowym rozsadkiem.
---------------------------------------------------------------------------------------------------
0x06 LADOWANIE KERNELA
---------------------------------------------------------------------------------------------------
Do zaladowania jadra systemu mozemy wykorzystac funkcje numer 0x02, przerwania 0x13 w nastepujacy
sposob:
///////////////////////////////////////////////////////////////////////////////////////////////////
[ORG 0x7C00]
xor ax, ax
mov ds, ax
mov ah, 0x02 ; czytaj sektory dyskietki
mov al, 0x01 ; ilosc sektorow(kazdy 512 bajtow) do zaladowania
xor ch, ch ; czytaj z cylindra 0
mov cl, 0x02 ; sektora 2 - w 1 jest bootloader
xor dx, dx ; glowicy 0, dysku 0
mov bx, 0x0800 ; zaladuj kernel pod adres ES:BX
mov es, bx ; czyli
xor bx, bx ; 0x0800:0x0000
int 0x13
xor ah, ah ; pobierz znak z klawiatury do al
int 0x16
cmp al, 27 ; jesli znak == ESC
je reset ; restartuj komputer
jmp 0x0800:0x0000 ; skaczemy do kodu kernela
reset:
mov bx, 0x40
mov ds, bx
mov word [ds:72h], 0x1234 ; wybieramy typ restartu
jmp 0xFFFF:0x0000 ; restartujemy
;------------------------------------------------ ponizej dane
times 510-($-$$) db 0
db 0x55
db 0xAA
///////////////////////////////////////////////////////////////////////////////////////////////////
Artykul dotyczy tylko programowania bootloaderow, wiec kernel musicie napisac sobie sami, ale
mysle ze nikt nie bedzie mial z tym problemow. Na tym etapie wystarczy jakis kod w asemblerze, aby
sprawdzic czy wszystko dziala jak nalezy.
Nalezy jedynie pamietac, aby kernel znalazl sie na dyskietce, badz jej obrazie w sektorze ktory
odczytalismy oraz jesli odczytujemy dokladnie tak jak pokazalem to wypadaloby go zaczac [ORG 0x0800]
ORG nie mozna stosowac przy przejsciu na kod jezyka C, badz tez innego poniewaz kompilacja sie nie
powiedzie.
Po skompilowaniu kodu bootloadera i bootsectora laczymy je w calosc:
-LINUX:
cat boot.bin kernel.bin > os.bin
-WINDOWS:
copy /b boot.bin+kernel.bin os.bin
Nastepnie nagrywamy i testujemy.
-LINUX:
merge os.bin obraz.img ; moj program utworzy obraz dyskietki, wiec
; nie trzeba bedzie go nagrywac
; zalecam stosowanie programiku, ktory wrzucam, poniewaz dd, nie wiedziec czemu przy
; kopiowaniu na utworzony wczesniej obraz zmiejsza go do rozmiaru pliku os.bin i uniemozliwia
; bootloaderowi odpalenie kernela
-WINDOWS:
dd if=os.bin of=obraz.img bs=512 ; lepiej dd wedlug mnie w tej kwesti sprawuje
; sie wlasnie pod windowsem :D nie ucina zer !
Dodam jeszcze, ze aby nie martwic sie, w ktorym sektorze co sie znajduje nalezy zaimplementowac
w kodzie bootloadera obsluge systemu plikow. Na poczatek polecam FAT12, poniewaz jest to chyba
najprostszy system plikow, dzieki ktoremu bedziecie mogli wczytywac pliki za pomoca ich nazw, a nie
polozenia na dyskietce. Warto takze zapoznac sie z pojeciami CHS i LBA, napewno sie przydadza ;)
---------------------------------------------------------------------------------------------------
0x07 A20
---------------------------------------------------------------------------------------------------
A20 jest to bramka logiczna AND podlaczona do kontrolera klawiatury oraz polaczona z 20 linia
adresowa. Dlatego tez czesto sie mowi linia A20. W dawnych komputerach bylo tylko 20 linii, czyli
mozna bylo zaadresowac tylko 2^20 bitow pamieci. Z czasem szyna adresowa sie rozrosla o kolejne
linie, ale aby zachowac wsteczna kompatybilnosc wymyslono wlasnie bramke A20, ktora po wlaczeniu
komputera jest wylaczona, wiec aby skorzystac z pelnej mocy adresowej i dostepnej pamieci
musi zostac odblokowana. Tym zadaniem zajmuje sie bootloader albo system operacyjny.
Znane mi sa trzy metody odblokowania tej linii i wszystkie z nich przedstawie, lecz to Waszym
zadaniem bedzie wybor tej najbardziej Wam odpowiadajacej.
Pierwsza z nich polega na wykorzystaniu 0x0F przerwania biosu.
Niestety niewiem jak wyglada procent biosow, ktore zaimplementowaly ta mozliwosc. Oto i kod:
///////////////////////////////////////////////////////////////////////////////////////////////////
[ORG 0x7C00]
jmp start
print: ; funkcja znana z poprzednich przykladow
pisz:
lodsb
or al, al
jz dalej
mov ah, 0x0E
int 0x10
jmp pisz
dalej:
ret
check_a20: ; funkcja sprawdzajaca, czy bramka jest odblokowana
mov ax, 0x962 ; zwraca wynik do AL,jesli jest odblokowana
int 0xF ; AL = 1, a jesli zablokowana AL = 0
ret
enable_a20: ; funkcja odblokowujaca bramke A20
mov ax, 0x961 ; jesli sie uda zeruje flage CF oraz AH = 0
int 0xF ; jesli nie CF = 1, a w AH status bledu
ret
disable_a20: ; funkcja blokujaca bramke A20
mov ax, 0x960 ; ustawia rejestry i flagi tak samo jak funkcja 0x961
int 0xF
ret
start:
xor ax, ax
mov ds, ax
mov ss, ax
mov sp, 0x9C00
sprawdz_a20:
call check_a20 ; wywolanie funkcji sprawdzajacej stan A20
or al, al ; czy AL = 0 ?
jnz koniec ; jesli tak to odblokuj
; jesli nie skacz na koniec
wlacz_a20:
call enable_a20 ; wywolanie funkcji odblokowujacej
jc blad_a20 ; jesli wystapil blad poinformuj o nim
mov si, odblok ; jesli sie udalo wypisz komunikat
call print
jmp koniec
blad_a20:
mov si, blad ; wypisz komunikat o bledzie
call print
koniec:
mov si, end
call print
jmp $
;------------------------------------------------ ponizej dane
odblok db "Pomyslnie odblokowano bramke A20.", 13, 10, 0
blad db "Nie mozna odblokowac bramki A20!", 13, 10, 0
end db "To juz koniec :)", 13, 10, 0
times 510-($-$$) db 0
dw 0xAA55 ; patrz co to LITTLE ENDIAN
///////////////////////////////////////////////////////////////////////////////////////////////////
Jest jeszcze funkcja 0x963 tego przerwania sprawdzajaca, z ktorego portu nalezy skorzystac, aby
odblokowac A20, jesli z portow 0x64 i 0x60 to BX = 0, a jesli z 0x92 to BX = 1.
Druga metoda polega na zapisywaniu oraz odczytywaniu odpowiednich wartosci z portow kontrolera
klawiatury. Ta metoda bedzie dzialac na 99,99% komputerow, wiec napewno jest warta uwagi.
Przedstawiam ponizej kod funkcji, ktorej jedynym zadaniem jest odblokowanie linii A20:
///////////////////////////////////////////////////////////////////////////////////////////////////
enable_a20:
cli
call a20wait
mov al, 0xAD ; wysylamy sygnal do portu kotrolera klawiatury
out 0x64, al ; ktory wylaczy klawiature
call a20wait
mov al, 0xD0 ; ustawiamy czytanie z portu
out 0x64, al ; 0x60
call a20wait2
in al, 0x60 ; pobieramy wartosc z portu 0x60
push eax ; oraz umieszczamy na stosie
call a20wait
mov al, 0xD1 ; ustawiamy zapisywanie do portu
out 0x64, al ; 0x60
call a20wait
pop eax ; zdejmujemy ze stosu pobrana wczesniej wartosc
or al, 2 ; do eax oraz wykonujemy roznice logiczna na
out 0x60, al ; najmlodszym slowie eax i zapisyjemy do portu 0x60
call a20wait
mov al, 0xAE ; wysylamy sygnal
out 0x64, al ; wlaczajacy klawiature
call a20wait
ret
a20wait: ; w tej funkcji szukamy utawionego
.zero: ; bitu blokady bramki A20
mov ecx, 65536
.jeden:
in al, 0x64
test al, 2
jz .dwa
loop .jeden
jmp .zero
.dwa:
ret
a20wait2: ; w tej funkcji robimy to samo co w poprzedniej
.zero: ; zaleznie od naszego sprzetu bedzie
mov ecx, 65536 ; ustawiony na 1 lub na 2
.jeden:
in al, 0x64
test al, 1
jnz .dwa
loop .jeden
jmp .zero
.dwa:
ret
///////////////////////////////////////////////////////////////////////////////////////////////////
Zostala trzecia metoda, mysle ze tez dostepna na wiekszosci komputerow oraz napewno szybsza od
swoich poprzedniczek, poniewaz zostala wymyslona po to aby przyspieszyc wlaczenie tej bramki oraz,
aby ominac kontroler klawiatury. Polega na wykorzystaniu Systemowego Kontrolera Portu A.
///////////////////////////////////////////////////////////////////////////////////////////////////
in al, 0x92 ; odczytujemy wartosc z portu 0x92 do al
or al, 0x02 ; zerujemy bit blokady A20
out 0x92, al ; zapisujemy zmiany
///////////////////////////////////////////////////////////////////////////////////////////////////
Mam nadzieje, ze wszystko do tej pory, bylo zrozumiale, poniewaz przejdziemy teraz do troszke
trudniejszych rzeczy, ktorymi nie koniecznie zajmuja sie bootloadery, ale nic nie stoi na
przeszkodzie zeby sie nimi zajmowaly.
---------------------------------------------------------------------------------------------------
0x08 Tryb chroniony
---------------------------------------------------------------------------------------------------
W trybie chronionym jak zapewne wiecie rejestry segmentowe nie zawieraja adresow bezposrednich
segmentu, tylko zawieraja indeks deskryptora segmentu w tablicy GDT (globalna tablica deskryptorow)
lub LDT (lokalna tablica deskryptorow), czyli rejestry segmentowe sa selektorami segmentow.
Selektory wskazuja bezposrednio na deskryptor segmentu, a posrednio na segment.
Nasuwa sie oczywisty wniosek, ze aby przejsc do trybu chronionego procesora nalezy najpierw
stworzyc tablice deskryptorow. Stworzymy tylko globalna tablice, poniewaz lokalna jest glownie
wykorzystywana w metodach ochrony pamieci, a narazie nie ma co utrudniac.
Deskryptor segmentu sklada sie z 8 bajtow, to jest 32 bitow, a wyglada tak:
///////////////////////////////////////////////////////////////////////////////////////////////////
--------------------------------------------------------------------------------------
| BASE 32:24 | G |D/B| L |AVL|SEG-LIMIT 16:19| P |DPL| S | TYPE | BASE 23:16 |
--------------------------------------------------------------------------------------
31 16:15 0
--------------------------------------------------------------------------------------
| BASE ADDRESS 15:00 | SEGMENT LIMIT 15:00 |
--------------------------------------------------------------------------------------
G (granularity) - umozliwia opis segmentu do 4GB
- G = 0 - max 1MB
- G = 1 - max 4GB
D (default) - bit dlugosci slowa, wartosci dla segmentu kodu
- D = 0 - 16 bitowe przemieszczenia
- D = 1 - 32 bitowe przemieszczenia
L (large) - dla trybu 64 bitowego, wiec dla nas bez znaczenia narazie
AVL (available to software) - nie wykorzystywane przez procesor, bez znaczenia dla nas
P (present) - obecnosc segmentu
- P = 0 - nie ma go
- P = 1 - jest :D
DPL (descriptor privilage level) - poziom ochrony, tzw ring
S - rodzaj segmentu
- S = 1 - segment pamieci, kodu lub danych
- S = 0 - segmenty specjalne, furtki, LDT, itp
TYPE (11:8) - prawa do segmentu
- dla danych (zwykly/rozszerzalny w dol):
0000/0100 - odczyt
0001/0101 - odczyt, dostep
0010/0110 - odczyt, zapis
0011/0111 - odczyt, zapis, dostep
- dla kodu (zwykly/zgodny):
1000/1100 - wykonanie
1001/1101 - wykonanie i dostep
1010/1110 - wykonanie i odczyt
1011/1111 - wykonanie, odczyt, dostep
///////////////////////////////////////////////////////////////////////////////////////////////////
W asemblerze tablica GDT, bedzie wiec wygladac nastepujaco:
///////////////////////////////////////////////////////////////////////////////////////////////////
gdtr: ; opis tablicy potrzebny do jej zaladowania
dw gdt - gdtend - 1 ; rozmiar
dd gdt ; adres
gdt:
dd 0x00000000, 0x00000000 ; zerowy deskryptor
dd 0x0000FFFF, 0x00CF9A00 ; deskryptor kodu, DPL - 0, limit - 4GB, baza - 0
dd 0x0000FFFF, 0x00CF9200 ; deskryptor danych, DPL - 0, limit - 4GB, baza - 0
dd 0x0000FFFF, 0x00CFFA00 ; deskryptor kodu, DPL - 3, limit - 4GB, baza - 0
dd 0x0000FFFF, 0x00CFF200 ; deskryptor danych, DPL - 3, limit - 4GB, baza - 0
gdtend:
///////////////////////////////////////////////////////////////////////////////////////////////////
Teraz juz mozemy jej uzyc w naszym kodzie i przejsc do trybu chronionego, wiec do dziela:
///////////////////////////////////////////////////////////////////////////////////////////////////
cli ; obowiazkowo wylaczamy przerwania
lgdt [gdtr] ; ladujemy wczesniej przygotowana tablice do GDT
mov eax, cr0 ; kopiujemy rejestr CR0 do EAX
or eax, 1 ; zapalamy bit trybu chronionego
mov cr0, eax ; zapisujemy zmiany w CR0
; po tych czynnosciach musimy od razu wykonac
jmp 0x08:pmode ; daleki skok, aby wyczyscic chache procesora
[BITS 32] ; sygnalizujemy, ze bedziemy korzystac z 32bit mode
pmode:
mov ax, 0x10 ; ustawiamy rejestry na segment danych
mov ds, ax
mov es, ax
mov ss, ax
xor ax, ax
mov fs, ax ; FS i GS najbezpieczniej ustawic na 0
mov gs, ax
mov esp, 0x200000
jmp $ ; to by bylo na tyle, mozemy korzystac z PMODE :)
///////////////////////////////////////////////////////////////////////////////////////////////////
---------------------------------------------------------------------------------------------------
0x09 KONIEC
---------------------------------------------------------------------------------------------------
Dziekuje za uwage i mam nadzieje, ze przyda sie komus ten tutorial ;)
Na koniec jeszcze paczka z programem merge kodem bootloadera i malutkim kernelem.
Przed kompilacja nalezy przeczytac plik README.
materiały do artykułu:
klik