| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- # rtdrv.s -- call runtime entries, with known arguments, and report what
- # happened. Assembled by tests/rt_exec.py into a complete .COM image.
- #
- # This replaces the Unicorn harness. The expectations did not change; only the
- # machine did. Everything here exists because qemu, unlike an emulator with a
- # Python API, gives you one thing: a serial port. So a check has to be
- # expressed as a program that reports on itself.
- #
- # The report is a framed record on the serial port:
- #
- # 01 <case index, 2 hex digits> 02 <whatever the entries printed> 03
- # <dumplen bytes at dumpaddr, 2 hex digits each> 04
- #
- # The four control bytes occur in no expected output, and rt_exec.py REFUSES to
- # run a case whose expectation contains one -- so a marker can never be mistaken
- # for output, and a driver that emitted nothing is a parse failure rather than
- # a silent pass.
- #
- # Why one qemu boot per check instead of one boot for all of them: the runtime
- # keeps state that outlives a call -- the one-character pushback slot, and the
- # input cursor the boot sector's INT 21h shim owns. One boot for everything
- # would make each check depend on the ones before it, so a check could fail
- # because of its neighbour and a green run would prove less than it appears to.
- # A boot costs about a tenth of a second and 35 of them cost under four, which
- # buys every check a machine that has provably never executed anything. (The
- # input descriptor is still re-armed on every boot, because it lives in the
- # image and the image is the same bytes each time.)
- #
- # Two things about the 8086 shape the code below, and both were found by
- # running it rather than by reading it:
- #
- # * Every entry preserves BP and SP and NO other register. So the cursor
- # into the case record is BP: a loop counter in CX or DI would be destroyed
- # by the first call. The entry's own BP frame is pushed and restored, so
- # our BP comes back.
- #
- # * `movb n(%bp), %cl' leaves the HIGH byte of CX exactly as it was. Reading
- # the input length that way and then writing CX to INLEN handed the boot
- # sector's INT 21h shim a 16-bit length with an undefined top half. The
- # 8086 has no memory-to-memory move and no zero-extending byte load, so the
- # high half has to be cleared by hand, every time.
- #
- # * `dec' sets the sign flag from the RESULT, so `decw cnt / js done' runs the
- # body for cnt = 1 (1 -> 0, SF clear) and stops on the wrap (0 -> FFFF).
- # It reads like an off-by-one and is not; the comment at the loop says so
- # again because the next person will wonder.
- #
- # The image is assembled in ONE piece, here, so every address is a label or an
- # .org and nothing has to be agreed with Python in two places. What Python does
- # have to agree about -- the runtime's base, and the input descriptor -- is
- # checked rather than trusted: it assembles this file and then asserts that the
- # blob it got from RtProbe really is at RT_BASE in the bytes that came out.
- .set LOAD_BIAS, 0x0100 # a .COM lives at CS:0100 (that is the PSP)
- .set SOH, 0x01
- .set STX, 0x02
- .set ETX, 0x03
- .set EOT, 0x04
- # Restated from tests/exec/bootcom.s's layout block ON PURPOSE. A harness that
- # asks the boot sector where its input buffer is proves only that the two agree
- # with each other; two independent statements of the same layout have to be
- # written down, and a disagreement is a loud failure rather than a program that
- # is fed nothing. (Same reasoning, and the same lesson, as the hard-coded
- # RT_SZ that run_com_tests.sh used to carry: the descriptor has to be where the
- # shim looks, and the only way to be sure is to have written both down and
- # checked.)
- .set INLEN, 0x2000 # word: how many input bytes follow
- .set INCUR, 0x2002 # word: index into INBUF, NOT an address
- .set INBUF, 0x2004 # the bytes themselves
- # The case record, as rt_exec.py writes it. These offsets are the contract; the
- # two places that read them (this file, and case_bytes() over there) are
- # separate statements of it on purpose, and the cases only pass if both are
- # right. SLOT0 is the one that was wrong the first time round.
- .set OFF_DUMPLEN, 0 # word how many bytes to report
- .set OFF_DUMPADR, 2 # word image offset to report them from
- .set OFF_INLEN, 4 # byte bytes of input to hand the shim
- .set OFF_INBUF, 5 # byte inbuf[8]
- .set OFF_CASE, 13 # byte the index printed in the header
- .set OFF_NCALLS, 14 # word how many call slots follow
- .set SLOT0, 16 # call slots, 6 bytes each:
- # +0 word entry, +2 word arg,
- # +4 byte flags, +5 pad
- .set SLOT_SZ, 6
- .set CASE_SZ, 40 # 16 + 4 slots
- # The image layout. RT_BASE is where the runtime blob goes; everything else is
- # above 0x0800 so that a runtime which grew by a few hundred bytes would not
- # silently run into the test scaffolding. rt_exec.py asserts
- # RT_BASE + blob size <= HDR, so the bound is a checked one.
- .set RT_BASE, 0x0200
- .set HDR, 0x0800
- .set DATA, 0x0810
- .set DLEN, 32 # bytes of data area for initmem to clear
- .set STORE, 0x0840 # where a read entry's result is stored
- .set CASE, 0x0860 # the case record
- .set D_DUMPLEN, 0x0890 # the driver's own three words
- .set D_DUMPADR, 0x0892
- .set D_CNT, 0x0894
- .code16
- .text
- .globl _start
- # The entry JMP -- the same three bytes every image this compiler writes opens
- # with. bootcom.s jumps to 0000:0100, which is this instruction. It is
- # written as raw bytes rather than `jmp drive' because gas would shorten that to
- # a two-byte EB 00 short jump, and the byte it dropped would then be executed as
- # the first instruction of the driver.
- _start:
- .byte 0xE9 # JMP rel16
- .word drive - _start - 3
- # --------------------------------------------------------------- the driver
- drive:
- cld
- # 1. Poison the two places a check is allowed to look. STORE gets EEEH so
- # a read entry that stores nothing is visible as such, and the data
- # area gets AAH so initmem has something to clear. The storage below
- # is zeroed; the poison is what turns a zero into a RESULT.
- movw $STORE + LOAD_BIAS, %si
- movw $0xEEEE, %ax
- movw %ax, (%si)
- movw $DATA + LOAD_BIAS, %si
- movw $DLEN, %cx
- movb $0xAA, %al
- .Lpoison:
- movb %al, (%si)
- incw %si
- loop .Lpoison
- # BP is the case-record cursor, and it is the only register that survives a
- # call into the runtime, so everything the loop needs is read through it
- # or parked in the driver's own words first.
- movw $CASE + LOAD_BIAS, %bp
- # 2. Open the record: SOH, the case index as two hex digits, STX.
- movb $SOH, %al
- call putc
- movb OFF_CASE(%bp), %al
- call hexb
- movb $STX, %al
- call putc
- # 3. Arm the input. INCUR is an INDEX into INBUF, and zeroing it is what
- # makes this boot's input start at its first byte. CX is cleared
- # first: see the note at the top about the high byte of CX.
- xorw %cx, %cx
- movb OFF_INLEN(%bp), %cl
- movw %cx, INLEN
- movw $0, INCUR
- movw $CASE + OFF_INBUF + LOAD_BIAS, %si
- movw $INBUF, %di
- xorw %ax, %ax
- repe cmpsb
- # 4. The calls. dumplen and the report address are needed after the loop,
- # by which time BP has moved, so they are parked now; so is the count,
- # because CX is gone by the second iteration.
- movw OFF_DUMPLEN(%bp), %ax
- movw %ax, D_DUMPLEN + LOAD_BIAS
- movw OFF_DUMPADR(%bp), %ax
- movw %ax, D_DUMPADR + LOAD_BIAS
- movw OFF_NCALLS(%bp), %ax
- movw %ax, D_CNT + LOAD_BIAS
- addw $SLOT0, %bp # BP now points AT the first call slot
- .Lcall:
- decw D_CNT + LOAD_BIAS # 1 -> 0 has SF clear, so the body runs once;
- jns .Lgo # 0 -> FFFF is what ends the loop
- jmp .Lcalldone
- .Lgo:
- call doslot
- addw $SLOT_SZ, %bp
- jmp .Lcall
- .Lcalldone:
- # 5. Close the output half and report the memory. A store that never
- # happened is the EEEH left in step 1, which is why it is there.
- movb $ETX, %al
- call putc
- movw D_DUMPLEN + LOAD_BIAS, %cx
- jcxz .Lnodump
- movw D_DUMPADR + LOAD_BIAS, %si
- addw $LOAD_BIAS, %si
- .Ldump:
- lodsb
- call hexb
- loop .Ldump
- .Lnodump:
- movb $EOT, %al
- call putc
- # 6. Exit. Reaching here IS the assertion that every entry returned: an
- # entry that hung produces no EOT, and the harness says so.
- movw $0x4C00, %ax # INT 21h AH=4Ch, AL=0
- int $0x21
- # doslot: call the entry described by the 6 bytes at BP, with the same
- # conventions Compiler.IoCall uses -- one 16-bit argument on the stack, popped
- # by the caller -- except for initmem, which takes the header in AX.
- #
- # The flags are re-read after the call because the entry has just used AX.
- doslot:
- movb 4(%bp), %al
- testb $1, %al
- jz .Lnostack
- pushw 2(%bp)
- .Lnostack:
- testb $2, %al
- jz .Lnoax
- movw 2(%bp), %ax
- .Lnoax:
- movw (%bp), %si # image-absolute, as RT_Entry reports it
- addw $LOAD_BIAS, %si # so the load bias is added here, not baked in
- testb $4, %al
- jnz .Linl
- call *%si
- movb 4(%bp), %al
- testb $1, %al
- jz .Lnoclean
- addw $2, %sp # caller-cleaned
- .Lnoclean:
- ret
- # wrtinl's argument is NOT a stack word: the entry reads a length byte and that
- # many characters from its own return address. That is the caller's half of
- # the contract, so it is written literally here -- the length byte and the
- # characters must be the bytes immediately after the call, which is why this is
- # a second copy of the instruction rather than a jump. The compiler's own
- # placement of the same three things is what the string-literal fixtures in
- # run_com_exec.py check; duplicating a variable one here would be a second
- # thing to keep right, and a check that cannot fail is not a check.
- .Linl:
- call *%si
- .byte 5
- .byte 'h', 'e', 'l', 'l', 'o'
- ret
- # putc: AL to the serial port. No status polling -- qemu's 16550 always accepts,
- # and a poll that never went ready would hang the harness instead of failing it.
- putc:
- pushw %ax
- movw $0x03f8, %dx
- outb %al, %dx
- popw %ax
- ret
- # hexb: AL as two uppercase hex digits, high nibble first.
- #
- # The 8086 has no `SHR r/m16, imm8' and no shift-by-four, so the high nibble
- # comes from four one-bit shifts through CL. Both facts have to be written
- # down: gas assembles `shr $4, %ah' without complaint and rejects `shrw $1, %ah'
- # outright, so the only thing standing between this file and an image that
- # assembles cleanly and dies on a real 8086 is refusing the modern encodings.
- hexb:
- pushw %ax
- pushw %cx
- movb %al, %ah
- movb $1, %cl
- shrw %cl, %ax
- shrw %cl, %ax
- shrw %cl, %ax
- shrw %cl, %ax
- popw %cx
- call putdig
- popw %ax
- andb $0x0f, %al
- movb %al, %ah
- call putdig
- ret
- # putdig: the nibble in AH as one character, moved into AL so that setting AH for
- # INT 21h AH=02h cannot overwrite the digit it is about to print.
- putdig:
- movb %ah, %al
- cmpb $10, %al
- jb .Ldigit
- addb $0x37, %al # 0Ah + 37h = 'A'
- jmp .Lemit
- .Ldigit:
- addb $0x30, %al # '0'
- .Lemit:
- movb $2, %ah
- int $0x21
- ret
- # --------------------------------------------------------------- the image
- # The runtime blob, from tests/RtProbe.mod built at RT_BASE. The gap is the
- # checked bound: rt_exec.py asserts that 0x0180..RT_BASE is still all zeros, so
- # a driver that outgrew its space fails the suite instead of overwriting the
- # library it is testing.
- .org RT_BASE
- .incbin "rtblob.bin"
- # The program header initmem is handed: +0 flag, +2 code, +4 data base, +6 data
- # end. Built HERE, from the same DATA and DLEN the poison loop uses, because
- # the compiler's own header layout is a separate claim with its own check and
- # this harness is not that check. The two fields initmem READS are memory
- # addresses, so they carry the bias; the caller passes AX the header's memory
- # address too, which is the one thing rt_exec.py gets right only by saying so
- # here and in case_bytes().
- .org HDR
- HDR:
- .word 1 # hdrFlag
- .word 0 # hdrCS, unused by initmem
- .word DATA + LOAD_BIAS # hdrDS = data base
- .word DATA + DLEN + LOAD_BIAS # hdrHeap = data end
- .org DATA
- .zero DLEN # poisoned to AAH by the driver
- .org STORE
- .zero 2 # poisoned to EEEH by the driver
- .org CASE
- .incbin "rtcase.bin"
- .org D_DUMPLEN
- .zero 6 # D_DUMPLEN, D_DUMPADR, D_CNT
|