| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- # bootcom.s -- load a DOS .COM from the boot floppy and run it, with INT 21h
- # wired to the serial port instead of a screen.
- #
- # This is the piece that lets a .COM produced by this compiler actually run.
- # It is deliberately not DOS: it is a 512-byte boot sector that does the four
- # things a DOS .COM loader does, and sends everything the program prints out
- # the serial port so a test harness can read it.
- #
- # 1. install an INT 21h vector pointing at a handler below
- # 2. INT 13h AH=02h: read the .COM off drive A: to 0000:0100
- # 3. SS:SP = 0000:FFFE, the segment top -- where DOS puts a .COM's stack
- # 4. JMP 0000:0100
- #
- # The INT 21h handler implements the four functions this runtime actually
- # calls, and nothing else:
- #
- # AH=02h display character in AL
- # AH=09h display the $-terminated string at DS:DX
- # AH=08h read a character without echo; 1Ah at end of input
- # AH=4Ch terminate
- #
- # Those four are the complete set. See Runtime.mod: every Int21 in it is one
- # of them (the "MovAh (0)" in EmitRdInt is not an INT 21h call, it is the
- # MOV AH,0 that loads a digit into AL before an ADD -- an easy thing to
- # misread as a fourth function).
- #
- # Two properties are load-bearing:
- #
- # * The handler NEVER writes to the serial port as a marker. The program's
- # output is arbitrary bytes, so any sentinel could collide with real
- # output. Termination travels out of band instead, through the
- # isa-debug-exit device at port 0501h: the program exits with code 0 and
- # qemu exits (value<<1)|1 = 1. A boot failure exits with value 7Fh, i.e.
- # qemu exit code 255, which is unambiguous. Every byte on the serial port
- # is therefore program output, with nothing to strip.
- #
- # * The program is loaded at 0000:0100, so segment 0 holds the IVT at
- # 0000:0000-00FF and the program from 0100 up. The two scratch words the
- # INT 21h handler keeps (0700h input cursor, 0702h output string cursor)
- # sit inside the region the read covers but are written before they are
- # read, and the input descriptor at 2000h sits past the end of any
- # fixture. See the layout block at the end of this file.
- #
- # Built and driven by tests/run_com_exec.py; see tests/exec/README.md.
- .code16
- .text
- .globl _start
- _start:
- cli
- xorw %ax, %ax
- movw %ax, %ds
- movw %ax, %es
- movw %ax, %ss
- movw $0xfffe, %sp
- sti
- # IVT entry 21h lives at 0000:0084 (21h * 4): offset word then segment.
- # `handler' is a section-relative offset because the section starts at 0;
- # the segment this sector was loaded at is 7C00h.
- movw $handler, %ax
- addw $0x7c00, %ax
- movw %ax, 0x84
- movw $0, %ax
- movw %ax, 0x86
- # INT 13h AH=00h: reset the drive controller. A floppy that has just
- # been attached needs this before a read will succeed.
- movb $0x00, %ah
- int $0x13
- # Retry the read: qemu's floppy is a file image and a read can fail while
- # it settles. Three attempts, then give up loudly.
- movw $3, %cx
- .Lretry:
- movw $0x0100, %bx # ES:BX = 0000:0100
- movb $0x02, %ah # read sectors
- movb $16, %al # 16 * 512 = 8192 bytes, far more than any fixture
- movb $0x00, %ch # cylinder 0
- movb $0x02, %cl # sector 2 -- the .COM, 1-based
- movb $0x00, %dh # head 0
- movb $0x00, %dl # drive A
- int $0x13
- jnc .Lok
- decw %cx
- jnz .Lretry
- .Lbootfail:
- movb $0x7f, %al
- movw $0x0501, %dx # isa-debug-exit
- outb %al, %dx
- cli
- hlt
- .Lok:
- .byte 0xEA, 0x00, 0x01, 0x00, 0x00 # jmp 0000:0100
- # ---------------------------------------------------------------- INT 21h
- # Called with the program's registers. DS is the caller's data segment
- # (0000h here) and is preserved, so AH=09h can reach DS:DX the way DOS does.
- # Every path ends at .Ldone, which restores everything and IRETs -- except
- # AH=4Ch, which does not return at all.
- handler:
- pushw %ax
- pushw %bx
- pushw %cx
- pushw %dx
- pushw %si
- pushw %di
- pushw %ds
- pushw %es
- cmpb $0x02, %ah
- je .Lh02
- cmpb $0x09, %ah
- je .Lh09
- cmpb $0x08, %ah
- je .Lh08
- cmpb $0x4c, %ah
- je .Lh4c
- stc # unknown function
- jmp .Ldone
- .Lh02: # display character in AL
- call ser_put
- clc
- jmp .Ldone
- .Lh09: # display the $-terminated string at DS:DX
- movw %dx, 0x0702 # ser_put clobbers DX, so keep the pointer
- .Lh09next:
- movw 0x0702, %si # lodsb: AL = [DS:SI]. Register-indirect
- lodsb # addressing with no displacement is not
- cmpb $0x24, %al # expressible in gas .code16 syntax, and
- je .Lh09done # keeping the cursor in memory means the
- call ser_put # pointer survives ser_put's use of DX.
- incw 0x0702
- jmp .Lh09next
- .Lh09done:
- clc
- jmp .Ldone
- .Lh08: # read a character, no echo, 1Ah at EOF
- # NOTE: this is the one function whose RESULT is in AL, so it must return
- # through .Ldone8 and not .Ldone - see there.
- movw INLEN, %ax # inlen
- movw INCUR, %bx # input cursor
- # EOF is when the cursor has REACHED the length, i.e. INCUR >= INLEN.
- # `cmpw %bx, %ax / jbe' tests AX <= BX, that is INLEN <= INCUR, which is
- # true on the FIRST character: it returned 1Ah immediately, so readln saw
- # an empty input and then looped for the line terminator that never came.
- # t29_readln hung with no output at all. (This was a bug in the harness,
- # not in the compiler - but a harness that feeds the program nothing can
- # never tell you whether the program handles input, so it is a bug that
- # hides bugs.)
- cmpw %ax, %bx # INCUR vs INLEN
- jae .Lh08eof
- # INCUR is an index INTO the buffer, not an address: the bytes live at
- # INBUF, and SI = INCUR alone reads the interrupt vector table at 0000:0000
- # - so AH=08h returned IVT[0] (a low timer vector byte) as the first
- # character of input. INCUR is 0 on the first call, which is the one
- # address in segment 0 that is guaranteed to be wrong.
- movw %bx, %si
- addw $INBUF, %si
- lodsb
- incw INCUR
- clc
- jmp .Ldone8 # NOT .Ldone: AL is the result here
- .Lh08eof:
- movb $0x1a, %al
- clc
- jmp .Ldone8
- .Lh4c: # terminate: exit with AL as the code
- movw $0x0501, %dx # isa-debug-exit
- outb %al, %dx
- cli
- hlt
- jmp .Lh4c
- .Ldone:
- popw %es
- popw %ds
- popw %di
- popw %si
- popw %dx
- popw %cx
- popw %bx
- popw %ax
- iret
- # The same, but for the one function that RETURNS something in AL. DOS
- # AH=08h hands the character back in AL, so restoring AX on the way out
- # throws the answer away and the caller reads whatever AX held on entry.
- #
- # This was silent in a way that is worth recording: the shim's read path was
- # structurally correct - it found the buffer, advanced the cursor, cleared
- # the carry - and the character was plainly in AL, one instruction before the
- # return. The discard happened in the epilogue, which every OTHER function
- # needs. So t29_readln did not see a wrong byte; it saw the *uninitialised*
- # AX the runtime had at the call, which is the first byte of a pointer it was
- # about to overwrite with the parsed value. readln compared that against 0Dh,
- # 0Ah and 1Ah, rejected it, and looped forever - a hang with no output, from
- # a read that demonstrably worked.
- #
- # AX is therefore popped only on the paths where it is not a result, and
- # AH=02h/09h (which also leave AL alone in DOS) keep using .Ldone.
- .Ldone8:
- popw %es
- popw %ds
- popw %di
- popw %si
- popw %dx
- popw %cx
- popw %bx
- addw $2, %sp # drop the saved AX, keep AL
- iret
- # ser_put: send AL to the serial port, leaving AL and DX alone.
- # No line-status polling: qemu's 16550 always accepts a byte, and a poll
- # that never goes ready would hang the harness rather than fail it.
- ser_put:
- pushw %ax
- movw $0x03f8, %dx
- outb %al, %dx
- popw %ax
- ret
- # --------------------------------------------------------------- layout
- # The input descriptor lives at 2000h, which the 16-sector read above covers
- # (0100h + 2000h = 2100h) and which is well past the end of any fixture (the
- # largest is 4493 bytes, so the image stops at 1285h). So the descriptor is
- # simply part of the disk image the harness writes, not something it has to
- # patch into this boot sector afterwards -- which means the addresses here are
- # assembly constants and the harness only has to know where they land in the
- # file. See run_com_exec.py, FLAT_OFF.
- .set INLEN, 0x2000 # word: number of input bytes
- .set INCUR, 0x2002 # word: cursor, starts at INBUF
- .set INBUF, 0x2004 # the bytes themselves
- .set INMAX, 0x00fc # 2100h - 2004h, the most that fits
- .org 510
- .byte 0x55, 0xAA
|