Bootstrapping Deel 2: Het echte(re) werk
----------------------------------------
Moeilijkheidsgraad: matig
Voorkennis: i386 assembly, FAT12
Referenties: Ralfs Brown Interrupt List
Tools: nasm
Zoals eerder gezegd gaan weg een MS-DOS compatible floppy maken met een eigen bootsector. Hierdoor is het mogelijk om onze schijfjes met eigen OS te voorzien onder windows, linux, DOS of praktisch elk ander OS. We gebruiken het FAT12 filesysteem, mede omdat het erg simpel is, en erg effectief (voor the time being in ieder geval). Praktisch bezwaar aan de FAT12 is natuurlijk de 8.3 bestandsnamen. Maar ik denk dat we daar in ieder geval ons niet druk over moeten maken. Zodra je je eigen OS gaat ontwerpen, ga je je daar natuurlijk wel druk over maken..
Ok, hoe ziet een FAT12-schijfje eruit?
Formateer een normale floppy onder linux, windows of DOS. Maar zorg er wel voor dat hij FAT12 formateerd (mkfs.msdos -F 12 /dev/fd0). Na het formateren hebben we een FAT12-schijfje, compleet met DOS-bootsector en FAT-blokken. Voor de complete layout van FAT12 moet je zelf eventjes de google gebruiken. Ik kan en ga zeker niet alles uitleggen (geld terug Bert!)
Let er op dat een FAT12-bootsector extra informatie heeft over het schijfje. Deze info wordt de BPB (BIOS Parameter Block) genoemd en ziet er zo uit:
code:
1
2
3
4
5
6
7
8
9
10
11
12
| BytesPerSector dword
SectorsPerCluster dbyte
ReservedSectors dwword
NumberOfFATs dbyte
RootEntries dword
TotalSectors dword
Media dbyte
SectorsPerFAT dword
SectorsPerTrack dword
HeadsPerCylinder dword
HiddenSectors ddouble
TotalSectorsBig ddouble |
Deze BPB begint op offset 11 in de bootsector. De rest van de bootsector ziet er alsvolgt uit:
code:
1
2
3
4
5
6
7
8
9
10
11
| 3-bytes jumpinstructie naar eigenlijke bootsector-code
8-bytes OEM Name
BPB structure
1-byte drive letter
1-byte ongebruikt
1-byte bootsignature
4-bytes serial number
11-bytes volume naam
8-bytes filesystem naam ("FAT12 ", of "MYOWNOS " ofzow)
448 bytes aan bootcode
2 bytes bootsector terminator (0xAA55) |
Zoals je ziet, heb je in een FAT12-schijfje al heel wat minder ruimte voor je bootsector code. Je snapt natuurlijk wel dat deze dus uitermate geoptimaliseerd moet zijn wil je alles voor elkaar krijgen wat je wilt doen in je bootsector. Een veelgebruikte truuk is om in de bootsector een soort van tweede (of zelfs derde) bootsector te laden, waardoor je ineens 1024 bytes (of zelfs nog meer) tot je beschikking krijgt. Veel OS'en waarin vaak veel moet gebeuren voordat de echte kernel geladen kan worden (zoals linux of windows) gebruiken deze truukjes volop.
Een bootsectortje die de naam van de schijf laat zien tijdens het opstarten. Het blijft simpele code:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
| [bits 16] ; 16 bits code
org 0 ; We staan op offset 0
jmp End_Of_BootData
OEMName db "MyOwnOS "
BytesPerSector dw 0x0200
SectorsPerCluster db 0x01
ReservedSectors dw 0x01
NumberOfFATs db 0x02
RootEntries dw 224
TotalSectors dw 2880
Media db 0xF0
SectorsPerFAT dw 0x09
SectorsPerTrack dw 0x0012
HeadsPerCylinder dw 0x0002
HiddenSectors dd 0x00000000
TotalSectorsBig dd 0x00000000
DriveNumber db 0x00
db 0x00
ExtBootSignature db 0x00
SerialNumber dd 0x12345678
VolumeLabel db "BLAATAAP "
FileSystem db "FAT12 "
End_Of_BootData:
; We weten niet of we op 0000:7C00 of 07C0:0000 staan, daarom kiezen
; we er zelf een.
mov ax, 0x7C0 ; Segment 07C0
mov ds, ax ; Data staat op 07C0:0000 ipv 0000:7C00
xor ax, ax ; Stack Segment Goedzetten
mov ss, ax
mov sp, 0xFFFF
mov [bootdrive],dl ; Onthoud de bootdrive
; De volgende code maakt van het duffe zwarte achtergrond een
; mooi blauw kleurtje.
mov dx,0x3c8 ; VGA Palette register (werkt dus alleen
xor al,al ; op een VGA)
out dx,al ; We willen kleur 0 (background) instellen
inc dx ; VGA Data register (0x3c9)
mov al,8
out dx,al ; Output rood component
xor al,al
out dx,al ; Output groen component
mov al,32
out dx,al ; Output blauw component
dec dx ; En doe hetzelfde voor kleur 1
mov al,1
out dx,al
inc dx
mov al,48
out dx,al
out dx,al
mov al,48
out dx,al
; Nu hebben we een mooi kleurtje, print de string op
; het beeld.
mov si, msg ; Print bericht
mov cx, 0 ; 'oneindig' (max 65535) karakters
; maximaal
call PrintMsg
mov si, VolumeLabel ; Print string op het beeld
mov cx, 11 ; Maximaal 11 karakters
call PrintMsg
xor ah,ah ; Wacht op een toets
int 0x16
int 0x19 ; En reboot, de rest komt later...
; ------------------------------------------------------------------------
; Routine die een string in SI print die eindigt op een 0.
; In: CX = maximale lengte om af te drukken
PrintMsg:
cld
lodsb ; Laad uit DS:SI naar AL
cmp al,0
jz End_Of_String ; Is het een 0, dan einde
dec cx ; Maximaal aantal karakters geprint
jz End_Of_String
call PrintChar ; Print character
jmp PrintMsg ; En volgende character
End_Of_String:
ret
; -------------------------------------------------------------------------
; Routine die een karakter in AL op het scherm print. Deze routine maakt
; gebruik van BIOS-interrupt 0x10 om een karakter te plotten.
PrintChar:
mov ah,0x0E ; BIOS functie 15
mov bx,0x0001 ; Print op page 0 in kleur 1
int 0x10
ret
; -------------------------------------------------------------------------
; Data die we gebruiken in de bootsector
msg db 'hello bootsector. The volume name is: ',0
bootdrive db 0
times 510-($-$$) db 0x90 ; Opvullen met NOP (doet niets)
dw 0xAA55 ; En de boot terminator |
Veel is er ook weer niet veranderd. Eerst word over de data gesprongen, en vandaar uit is het allemaal eigenlijk hetzelfde. Het enige verschil is dat de PrintMsg routine nu in het CX-register een maximaal aantal tekens bevat die geprint wordt. Dit omdat de volumenaam niet op een 0 eindigd. Maar aangezien we weten dat de volnaam maximaal 11 characters groot kan zijn, printen we er ook maar maximaal 11 af.
Lezen van sectoren van schijf naar geheugen:
--------------------------------------------
Een stukje uit de Ralf Brown's Interrupt Lists:
Interrupt 13, functie 02
Read disk sectors into memory
AH = 02
AL = number of sectors to read (non-zero)
CH = low eight bits of cylinder
CL = sector number 1-63 (bits 0-5)
high two bits of cylinder (bits 6-7, hard disk only)
DH = head number
DL = drive number (bit 7 set for hard disk)
ES:BX -> data buffer
Return: CF set on error
if AH=11h (corrected ECC error), AL = burst length
CF clear if successfull
AH = status
AL = number of sectors transferred
Deze functie gebruiken we om sectoren te lezen vanaf de schijf.
Uitlezen van de FAT12 root-directory.
-------------------------------------
Om een root-directory uit te lezen, moeten we eerst weten hoe deze precies is opgebouwd.
Hierna gaan we bekijken op welke sector op de schijf de root-directory begint. Dit is een beetje rekenwerk met de BPB informatie.
RootDirectoryStart = (TotalNrOfFATs * SectorsPerFAT) + ReservedSectors
Elke root-entry is 32 bytes groot. Een snelle rekensom leert ons dat we maximaal BPB.RootEntries * 32 / BPB.BytesPerSector moeten inlezen voordat de gehele root-directory in het geheugen staat. Een andere manier van oplossen, is om sector voor sector in te laden. Dit scheelt geheugen (aangezien we maar maximaal 1 sector tegelijkertijd in het geheugen hebben), en als we een file moeten laden en deze staat in de eerste rootentry-sector, dan hoeven we alle andere sectoren niet in te laden (wat weer een snelheidswinst is).
Zodra de rootentries zijn ingelezen gaan we ze allemaal af en drukken we ze af in een soort van directory-listen a la MS-DOS. Daarna wachten we weer netjes op een toets om vervolgens te rebooten.
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
| [bits 16] ; 16 bits code
org 0 ; We staan op offset 0
jmp End_Of_BootData
OEMName db "MyOwnOS "
BytesPerSector dw 0x0200
SectorsPerCluster db 0x01
ReservedSectors dw 0x01
NumberOfFATs db 0x02
RootEntries dw 224
TotalSectors dw 2880
Media db 0xF0
SectorsPerFAT dw 0x09
SectorsPerTrack dw 0x0012
HeadsPerCylinder dw 0x0002
HiddenSectors dd 0x00000000
TotalSectorsBig dd 0x00000000
DriveNumber db 0x00
db 0x00
ExtBootSignature db 0x00
erialNumber dd 0x12345678
VolumeLabel db "BLAATAAP "
FileSystem db "FAT12 "
End_Of_BootData:
; We weten niet of we op 0000:7C00 of 07C0:0000 staan, daarom
; kiezen we er zelf een.
mov ax, 0x07C0 ; Segment 07C0
mov ds, ax ; Data staat op 07C0:0000
jmp 0x07C0:Relocation
Relocation: ; En de code nu ook
; Setup Stack
cli ; Geen interrupts
mov ax, 0x9000
mov ss,ax
mov sp, 0xFFFF
sti ; Nu mag het weer
; Onthoud de bootdrive
mov [bootdrive],dl
; Print a bootmessage
mov si, BootMsg
call PrintMsg
; Reset disk system
xor ah,ah
mov dl, [bootdrive]
int 0x13
; -------------------------------------------------------
; Lees all FAT sectoren in op 1000:0000
xor ax, ax
mov al, [NumberOfFATs]
mov cx, [SectorsPerFAT]
mul cx
mov cx, ax
mov bp, [ReservedSectors]
call ReadSectors
; -------------------------------------------------------
; Lees de root-entry in op 2000:0000
mov ax, 0x0000
mov [Cur_Off], ax
mov ax, 0x2000
mov [Cur_Seg], ax
xor dx, dx
mov ax, [RootEntries]
shl ax, 5 ; Maal 32
div word [BytesPerSector] ; ax = ax:dx / bytespersector
mov cx, ax ; Aantal sectoren
call ReadSectors ; BP already set correctly
mov [DataStartSector], bp
; -------------------------------------------------------
; Zoek naar een kernel.sys
mov bp, [RootEntries]
mov ax, 0x2000
mov es, ax
push ds
mov ds, ax
xor si, si
ShowNextEntry:
push si
push bp
xor al,al ; Niks laten zien als ie leeg is
cmp [es:si], al
je SkipEmptyEntry
cmp [es:si+0x1A], al ; Long File Name Entry
je SkipEmptyEntry
mov cx, 12 ; maximaal 11 karakters printen
call PrintMsg
mov al, 10 ; Linefeed
call PrintChar
mov al, 13 ; en carriage return
call PrintChar
SkipEmptyEntry:
pop bp
pop si
add si, 32
dec bp ; Alle entries al gehad?
jnz ShowNextEntry
pop ds
DeadLock:
jmp DeadLock
; ------------------------------------------------------------------------
; Routine die een string in DS:SI print die eindigt op een 0.
; In: CX = maximale lengte om af te drukken
PrintMsg:
cld
lodsb ; Laad uit DS:SI naar AL
cmp al,0
jz End_Of_String ; Is het een 0, dan einde
dec cx ; Maximaal aantal karakters geprint?
jz End_Of_String
call PrintChar ; Print character
jmp PrintMsg ; En volgende character
End_Of_String:
ret
; -------------------------------------------------------------------------
; Routine die een karakter in AL op het scherm print. Deze routine maakt
; gebruik van BIOS-interrupt 0x10 om een karakter te plotten.
PrintChar:
mov ah,0x0E ; BIOS functie 15
mov bx,0x000 ; Print op page 0 in kleur 7
int 0x10
ret
; -------------------------------------------------------------------------
; Routine die een of meerdere sectoren inleest
; CX = aantal sectoren
; BP = startsector (LBA)
ReadSectors:
push cx
mov di, 5
RS_Retry:
call ConvertLBA2CHS
mov dl, [bootdrive]
mov bx, [Cur_Seg] ; Data wordt in CurSeg:CurOff
mov es, bx ; geladen.
mov bx, [Cur_Off]
mov ax, 0x0201
int 0x13
jnc RS_GoodLoad
mov al, 'X' ; Niet goed? Print een X
call PrintChar
dec di ; Al 5 keer geprobeerd? Dan
jnz RS_Retry ; kappen we ermee...
mov al, '!'
call PrintChar
jmp $ ; Deadlock
RS_GoodLoad:
mov bx, [Cur_Off] ; Increase memory offset
add bx, 512
mov [Cur_Off], bx
inc bp ; Next LBA sector
pop cx
loop ReadSectors
ret
; -------------------------------------------------------------------------
; Convert LBA to CHS
; in : BP = LBA sector
; out: ch = cylinder
; dh = head
; cl = sector (high cylinder not used)
ConvertLBA2CHS:
mov ax, bp
xor dx, dx
div word [SectorsPerTrack]
inc dl ; Sectors starts at 1, not 0
mov bl, dl
xor dx, dx
div word [HeadsPerCylinder]
mov ch, al ; Cylinder
mov cl, bl ; Sectors
mov dh, dl ; Head
ret
; -------------------------------------------------------------------------
; Data die we gebruiken in de bootsector
BootMsg db 'Booting...', 0
ErrorMsg db 'Cannot find kernel.sys', 13, 10, 0
DoneMsg db 'done!',0
kernel db 'KERNEL SYS'
DataStartSector dw 0
bootdrive db 0
Cur_Seg dw 0x1000
Cur_Off dw 0x0000
times 510-($-$$) db 0x90 ; Opvullen met NOP (doet niets)
dw 0xAA55 ; En de boot terminator |
Schrijf de binary weg op een geformateerde floppy, zet er een aantal test-bestandjes en directories (bestanden IN die directories worden dus niet getoond, aangezien deze niet in de root-directory staan) en kijk hoe goed het (wel|niet) werkt..
Een leuke opdracht zou zijn om ervoor te zorgen dat ook lange bestandsnamen kunnen worden afgedrukt. Op
http://hjem.get2net.dk/rune_moeller_barnkob/filesystems/vfat.html vind je een hoop info om zelf hiermee aan de slag te gaan.
Lezen van een file vanaf schijf naar het geheugen
-------------------------------------------------
Dit is niet zo heel erg veel meer dan het lezen van de directory. Als eerste moeten we natuurlijk de rootdirectory lezen om te kijken of de file uberhaupt bestaat. Hebben we iets gevonden dat voldoet aan onze bestandsnaam (in ons geval: KERNEL.SYS), dan laden we deze file in door alle clusters van de file in te lezen. Voor info over het inlezen van clusters kun je het beste eventjes wat extra documentatie raadplegen (het stelt niet zo veel voor, het is eigenlijk een soort van linklist).
Onze "hello world" kernel
-------------------------
Als eerste gaan we even een simpele kernel bouwen. Schrik niet, want wat deze doet is niets meer dan wat text op het beeldscherm zetten op precies dezelfde manier als in de bootsector (met interrupt 10 dus). Ook zal deze kernel in assembler geschreven worden, aangezien we nog heeeeel veel moeten doen voordat we ook maar een C-kernel kunnen inladen.
Waarom kunnen we nog geen C-kernel inladen?
Dat heeft een paar redenen. De belangrijkste reden is wel dat onze compiler (gcc/djgpp) alle code in 32bit assembleerd, en wij in ons OS nog steeds in 16 bit werken. Verder heeft C de eigenschap dat veel functies (strcpy, printf, memcpy etc) specifieke functies zijn per OS, en dat deze worden opgeslagen in de C-lib. We moeten dus voordat we ook maar 1 printf'je kunnen gebruiken een compleet eigen C-library moeten schrijven. Gelukkig is dit niet zo heel moeilijk, maar het is wel een langdradig karwei (het leven van een OS-coder is niet altijd feest). Zodra we deze 2 "problemen" hebben opgelost, kunnen we beginnen met het schrijven van de eerste echte C-kernels.
Goed, de assembler-kernel dan maar voorlopig. Wat komt erin te staan?
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
| [bits 16] ; 16 bits code
org 0 ; We staan op offset 0
mov ax, 0x0050
mov ds, ax
mov es, ax
mov si, msg
call PrintMsg
xor bx,bx ; BX houd de huidige kleur vast
NextLoop:
; Wacht op de monitor totdat deze klaar is met het maken
; van horizontale lijn. Dit is een soort van vertragings-lus
; die we inbouwen en hierdoor lijkt het alsof er allemaal kleine
; balkjes in de achtergrond liggen.
mov dx,0x3da
NHR:
in al,dx
test al,1
jne NHR
HR:
in al,dx
test al,1
je HR
mov dx,0x3c8 ; Achtergrond kleur veranderen
xor al,al
out dx,al
inc dx
mov al,[r]
out dx,al
mov al,[g]
out dx,al
mov al,[b]
out dx,al
add byte [r], 3
jmp NextLoop ; Dit gaan oneindig door... :)
; ------------------------------------------------------------------------
; Routine die een string in SI print die eindigt op een 0.
PrintMsg:
cld
lodsb ; Laad uit DS:SI naar AL
cmp al,0
jz End_Of_String ; Is het een 0, dan einde
call PrintChar ; Print character
jmp PrintMsg ; En volgende character
End_Of_String:
ret
; -------------------------------------------------------------------------
; Routine die een karakter in AL op het scherm print. Deze routine maakt
; gebruik van BIOS-interrupt 0x10 om een karakter te plotten.
PrintChar:
mov ah,0x0E ; BIOS functie 15
mov bx,0x0001 ; Print op page 0 in kleur 1
int 0x10
ret
; -------------------------------------------------------------------------
; Data die we gebruiken in de bootsector
msg db 13,10,13,10,"hallo! Dit is onze eerste kernel!",13,10
db 13,10
db "We doen hierin nog niet zo heel erg veel, behalve leuke",13,10
db "kleurtjes en deze text laten zien op het scherm.",13,10
db 13,10
db "Mocht je zelf nog leuke ideetjes hebben, dan mag je dat ",13,10
db "dat altijd zelf uitproberen...",13,10
db 13,10
db 0
; Rood, Groen en Blauw component
r db 0
g db 0
b db 0
; De kernel is niet gebonden aan een grootte. Zorg er alleen voor dat
; deze kernel niet boven de 64KB uitkomt ivm offset-wrapping tijdens
; het laden.
times 2345 db 0x90
db "blaat" |
Niet veel dus. Een simpel bericht op het beeldscherm en wat gepiel met kleurtjes. Als je echt teveel vrije tijd hebt, dan kan je hier vandaan een soort van bootloader in elkaar zetten: zorg dat de bootsector alle files in de root laat zien, en laat de gebruiker hieruit 1 kiezen. Mocht je dit niet halen qua code, dan kun je altijd nog een extra bootloader laden. Zorg dat je bootsector STAGE2.SYS laad, en je hebt in stage2 alle ruimte voor je loader (wat dacht je van een leuk plaatje op de achtergrond, scrollertje onderdoor etc).
ok, onze bootsector om KERNEL.SYS in te laden en uit te voeren:<blockquote><font size="1" face="verdana, arial, helvetica">code:</font><hr><font face="courier, fixedsys, lucida console"><nobr>[bits 16] ; 16 bits code
org 0 ; We staan op offset 0
jmp End_Of_BootData
OEMName db "MyOwnOS "
BytesPerSector dw 0x0200
SectorsPerCluster db 0x01
ReservedSectors dw 0x01
NumberOfFATs db 0x02
RootEntries dw 224
TotalSectors dw 2880
Media db 0xF0
SectorsPerFAT dw 0x09
SectorsPerTrack dw 0x0012
HeadsPerCylinder dw 0x0002
HiddenSectors dd 0x00000000
TotalSectorsBig dd 0x00000000
DriveNumber db 0x00
db 0x00
ExtBootSignature db 0x00
erialNumber dd 0x12345678
VolumeLabel db "BLAATAAP "
FileSystem db "FAT12 "
End_Of_BootData:
; We weten niet of we op 0000:7C00 of 07C0:0000 staan, daarom
; kiezen we er zelf een.
mov ax, 0x07C0 ; Segment 07C0
mov ds, ax ; Data staat op 07C0:0000
jmp 0x07C0:Relocation
Relocation: ; En de code nu ook
; Setup Stack
cli ; Geen interrupts
mov ax, 0x9000
mov ss,ax
mov sp, 0xFFFF
sti ; Nu mag het weer
; Onthoud de bootdrive
mov [bootdrive],dl
; Print a bootmessage
mov si, BootMsg
call PrintMsg
; Reset disk system
xor ah,ah
mov dl, [bootdrive]
int 0x13
; -------------------------------------------------------
; Lees all FAT sectoren in op 1000:0000
xor ax, ax
mov al, [NumberOfFATs]
mov cx, [SectorsPerFAT]
mul cx
mov cx, ax
mov bp, [ReservedSectors]
call ReadSectors
; -------------------------------------------------------
; Lees de root-entry in op 2000:0000
mov ax, 0x0000
mov [Cur_Off], ax
mov ax, 0x2000
mov [Cur_Seg], ax
xor dx, dx
mov ax, [RootEntries]
shl ax, 5 ; Maal 32
div word [BytesPerSector] ; ax = ax:dx / bytespersector
mov cx, ax ; Aantal sectoren
call ReadSectors ; BP already set correctly
mov [DataStartSector], bp
; -------------------------------------------------------
; Zoek naar een kernel.sys
mov bp, [RootEntries]
mov ax, 0x2000
mov es, ax
xor di, di
CheckNextEntry:
pusha
mov si, kernel ; check naar KERNEL SYS
&nb
Yo dawg, I heard you like posts so I posted below your post so you can post again.