-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
boot.s
418 lines (365 loc) · 8.59 KB
/
boot.s
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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
; Register usage:
; SP = parameter stack pointer (grows downwards from 0x7c00 - just before the entrypoint)
; DI = return stack pointer (grows upwards from 0xc00)
; SI = execution pointer
; BX = top of stack
;
; Dictionary structure:
; link: dw
; name: counted string (with flags)
;
; The Forth is DTC, as this saves 2 bytes for each defcode, while costing 3 bytes
; for each defword (of which there are none in the bootsector)
;
; Memory layout:
; 0000 - 03ff IVT
; 0400 - 04ff Reserved by BIOS
; 0500 - 05ff Keyboard input buffer
; 0600 - 09ff Disk block buffer (for LOAD)
; 0a00 - 0aff Assorted variables (only 3 bytes are actually used at the moment)
; 0b00 - ... Return stack (grows upwards)
; ... - ... Space for manual allocation by user
; ... -~7c10 Parameter stack
; 7c00 - 7dff MBR (code loaded by BIOS)
; 7e00 - ... Decompressed code and dictionary space (HERE / ALLOT)
F_IMMEDIATE equ 0x80
F_HIDDEN equ 0x40
F_LENMASK equ 0x1f
InputBuf equ 0x500
BlockBuf equ 0x600
BlockBuf.end equ 0xa00
; we put it at 7d00, since that's in the middle of CompressedData,
; which we effectively discard before the first access to InputPtr
InputPtr equ 0x7d00 ; dw
RS0 equ 0xb00
SPECIAL_BYTE equ 0xff
%assign savings 0
%macro compression_sentinel 0
%assign savings savings+4
db SPECIAL_BYTE
dd 0xdeadbeef
%endmacro
; defcode PLUS, "+"
; defcode SEMI, ";", F_IMMEDIATE
%macro defcode 2-3 0
compression_sentinel
%strlen namelength %2
db %3 | namelength, %2
%1:
%endmacro
org 0x7c00
jmp 0:start
stack:
dw HERE
dw STATE
dw LATEST
start:
push cs
push cs
push cs
pop ds
pop es
; Little known fact: writing to SS disables interrupts for the next instruction,
; so this is safe without an explicit cli/sti.
pop ss
mov sp, stack
mov bp, BP_POS
cld
mov si, bp ; BP_POS = CompressedData
mov di, CompressedBegin
mov cx, COMPRESSED_SIZE
.decompress:
lodsb
stosb
cmp al, SPECIAL_BYTE
jnz short .not_special
mov byte [di-1], 0xad ; lodsw
; since SPECIAL_BYTE, we only need to load half of FF E0 jmp ax
mov ah, 0xe0
stosw
mov ax, di
xchg [byte bp-BP_POS+LATEST], ax ; AX now points at the old entry, while
; LATEST and DI point at the new one.
stosw
.not_special:
loop .decompress
; di = CompressedEnd here
mov [di + DRIVE_NUMBER - CompressedEnd], dl
push dx ; for FORTH code
%ifdef AUTOLOAD
push 1
mov ax, LOAD
jmp InterpreterLoop.execute
%endif
ReadLine:
mov di, InputBuf
mov [byte bp-BP_POS+InputPtr], di
.loop:
mov ah, 0
int 0x16
cmp al, 0x08
jne short .write
; backspace
cmp di, InputBuf ; underflow check
je short .loop
dec di
db 0x3c ; mask next instruction
.write:
stosb
xor bx, bx
mov ah, 0x0e
int 0x10
cmp al, 0x0d
jne short .loop
.enter:
mov al, 0x0a
int 0x10
mov [di-1], bl ; BX = 0 at this point
pop bx
InterpreterLoopSaveBX:
push bx
InterpreterLoop:
call ParseWord
jcxz short ReadLine
; Try to find the word in the dictionary.
; SI = dictionary pointer
; DX = string pointer
; CX = string length
; Take care to preserve BX, which holds the numeric value.
LATEST equ $+1
mov si, 0
.find:
lodsw
push ax ; save pointer to next entry
lodsb
xor al, cl ; if the length matches, then AL contains only the flags
test al, F_HIDDEN | F_LENMASK
jnz short .next
mov di, dx
push cx
repe cmpsb
pop cx
je short .found
.next:
pop si
or si, si
jnz short .find
; It's a number. Push its value - we'll pop it later if it turns out we need to compile
; it instead.
push bx
; At this point, AH is zero, since it contains the higher half of the pointer
; to the next word, which we know is NULL.
cmp byte[byte bp-BP_POS+STATE], ah
jnz short InterpreterLoop
; Otherwise, compile the literal.
mov ax, LIT
call COMMA
pop ax
.compile:
call COMMA
jmp short InterpreterLoop
.found:
pop bx ; discard pointer to next entry
; When we get here, SI points to the code of the word, and AL contains
; the F_IMMEDIATE flag
STATE equ $+1
or al, 1
xchg ax, si ; both codepaths need the pointer to be in AX
jz short .compile
; Execute the word
.execute:
mov di, RS0
pop bx
mov si, .return
jmp ax
.return:
dw InterpreterLoopSaveBX
COMMA:
HERE equ $+1
mov [CompressedEnd], ax
add word[byte bp-BP_POS+HERE], 2
Return:
ret
; returns
; DX = pointer to string
; CX = string length
; BX = numeric value
; clobbers SI
ParseWord:
mov si, [byte bp-BP_POS+InputPtr]
; repe scasb would probably save some bytes here if the registers worked out - scasb
; uses DI instead of SI :(
.skiploop:
mov dx, si ; if we exit the loop in this iteration, dx will point to the first letter
; of the word
lodsb
cmp al, " "
je short .skiploop
xor cx, cx
xor bx, bx
.takeloop:
; AL is already loaded by the end of the previous iteration, or the previous loop
and al, ~0x20 ; to uppercase, but also integrate null check and space check
jz short Return
inc cx
sub al, "0" &~0x20
cmp al, 9
jbe .digit_ok
sub al, "A" - ("0" &~0x20) - 10
.digit_ok
cbw
shl bx, 4
add bx, ax
mov [byte bp-BP_POS+InputPtr], si
lodsb
jmp short .takeloop
DiskPacket:
db 0x10, 0
.count:
dw 2
.buffer:
; rest is filled out at runtime, overwriting the compressed data,
; which isn't necessary anymore
BP_POS equ $ - $$ + 0x7c00
CompressedData:
times COMPRESSED_SIZE db 0xcc
; Invariant: due to the use of compression_sentinel without a dictionary header following it,
; the first byte of LIT and EXIT must have the 0x40 (F_HIDDEN) bit set.
CompressedBegin:
DOCOL:
xchg ax, si
stosw
pop si ; grab the pointer pushed by `call`
compression_sentinel
LIT:
push bx
lodsw
xchg bx, ax
compression_sentinel
EXIT:
dec di
dec di
mov si, [di]
defcode MINUS, "-"
pop ax
sub ax, bx
xchg bx, ax
defcode PEEK, "@" ; ( addr -- val )
mov bx, [bx]
defcode POKE, "!" ; ( val addr -- )
pop word [bx]
pop bx
defcode CPEEK, "c@" ; ( addr -- ch )
movzx bx, byte[bx]
defcode CPOKE, "c!" ; ( ch addr -- )
pop ax
mov [bx], al
pop bx
defcode DUP, "dup" ; ( a -- a a )
push bx
defcode SWAP, "swap" ; ( a b -- b a )
pop ax
push bx
xchg ax, bx
defcode TO_R, ">r"
xchg ax, bx
stosw
pop bx
defcode FROM_R, "r>"
dec di
dec di
push bx
mov bx, [di]
defcode UDOT, "u."
; the hexdigit conversion algo below turns 0x89 into a space.
; 0x89 itself doesn't fit in a signed 8-bit immediate that
; a two-byte push instruction uses, but we don't care about the
; high 8 bits of the value
push byte 0x89 - 0x100
.split:
mov al, bl
and al, 0x0f
push ax
shr bx, 4
jnz .split
.print:
pop ax
add al, 0x90
daa
adc al, 0x40
daa
xor bx, bx
mov ah, 0x0e
int 0x10
cmp al, " "
jne short .print
pop bx
defcode LOAD, "load"
pusha
mov di, DiskPacket.buffer
mov ax, BlockBuf
mov [InputPtr], ax
stosw
xor ax, ax
stosw
shl bx, 1
xchg ax, bx
stosw
xchg ax, bx
stosw
stosw
stosw
mov [BlockBuf.end], al
DRIVE_NUMBER equ $+1
mov dl, 0
mov ah, 0x42
mov si, DiskPacket
int 0x13
jc short $
popa
pop bx
;; Copies the rest of the line to buf.
defcode LINE, "s:" ; ( buf -- buf+len )
xchg si, [byte bp-BP_POS+InputPtr]
.copy:
lodsb
mov [bx], al
inc bx
or al, al
jnz short .copy
.done:
dec bx
dec si
xchg si, [byte bp-BP_POS+InputPtr]
defcode SWITCH, "|", F_IMMEDIATE
xor byte[byte bp-BP_POS+STATE], 1
defcode COLON, ":"
pusha
mov di, [byte bp-BP_POS+HERE]
mov ax, di
xchg [byte bp-BP_POS+LATEST], ax ; AX now points at the old entry, while
; LATEST and DI point at the new one.
stosw
call ParseWord
mov ax, cx
stosb
mov si, dx
rep movsb
mov al, 0xe8 ; call
stosb
; The offset is defined as (call target) - (ip after the call instruction)
; That works out to DOCOL - (di + 2) = DOCOL - 2 - di
mov ax, DOCOL - 2
sub ax, di
stosw
mov [byte bp-BP_POS+HERE], di
popa
jmp short SWITCH
defcode SEMI, ";", F_IMMEDIATE
mov ax, EXIT
call COMMA
jmp short SWITCH
; INVARIANT: last word in compressed block does not rely on having NEXT appended by
; decompressor
CompressedEnd:
COMPRESSED_SIZE equ CompressedEnd - CompressedBegin - savings