bootcom.s 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. # bootcom.s -- load a DOS .COM from the boot floppy and run it, with INT 21h
  2. # wired to the serial port instead of a screen.
  3. #
  4. # This is the piece that lets a .COM produced by this compiler actually run.
  5. # It is deliberately not DOS: it is a 512-byte boot sector that does the four
  6. # things a DOS .COM loader does, and sends everything the program prints out
  7. # the serial port so a test harness can read it.
  8. #
  9. # 1. install an INT 21h vector pointing at a handler below
  10. # 2. INT 13h AH=02h: read the .COM off drive A: to 0000:0100
  11. # 3. SS:SP = 0000:FFFE, the segment top -- where DOS puts a .COM's stack
  12. # 4. JMP 0000:0100
  13. #
  14. # The INT 21h handler implements the four functions this runtime actually
  15. # calls, and nothing else:
  16. #
  17. # AH=02h display character in AL
  18. # AH=09h display the $-terminated string at DS:DX
  19. # AH=08h read a character without echo; 1Ah at end of input
  20. # AH=4Ch terminate
  21. #
  22. # Those four are the complete set. See Runtime.mod: every Int21 in it is one
  23. # of them (the "MovAh (0)" in EmitRdInt is not an INT 21h call, it is the
  24. # MOV AH,0 that loads a digit into AL before an ADD -- an easy thing to
  25. # misread as a fourth function).
  26. #
  27. # Two properties are load-bearing:
  28. #
  29. # * The handler NEVER writes to the serial port as a marker. The program's
  30. # output is arbitrary bytes, so any sentinel could collide with real
  31. # output. Termination travels out of band instead, through the
  32. # isa-debug-exit device at port 0501h: the program exits with code 0 and
  33. # qemu exits (value<<1)|1 = 1. A boot failure exits with value 7Fh, i.e.
  34. # qemu exit code 255, which is unambiguous. Every byte on the serial port
  35. # is therefore program output, with nothing to strip.
  36. #
  37. # * The program is loaded at 0000:0100, so segment 0 holds the IVT at
  38. # 0000:0000-00FF and the program from 0100 up. The two scratch words the
  39. # INT 21h handler keeps (0700h input cursor, 0702h output string cursor)
  40. # sit inside the region the read covers but are written before they are
  41. # read, and the input descriptor at 2000h sits past the end of any
  42. # fixture. See the layout block at the end of this file.
  43. #
  44. # Built and driven by tests/run_com_exec.py; see tests/exec/README.md.
  45. .code16
  46. .text
  47. .globl _start
  48. _start:
  49. cli
  50. xorw %ax, %ax
  51. movw %ax, %ds
  52. movw %ax, %es
  53. movw %ax, %ss
  54. movw $0xfffe, %sp
  55. sti
  56. # IVT entry 21h lives at 0000:0084 (21h * 4): offset word then segment.
  57. # `handler' is a section-relative offset because the section starts at 0;
  58. # the segment this sector was loaded at is 7C00h.
  59. movw $handler, %ax
  60. addw $0x7c00, %ax
  61. movw %ax, 0x84
  62. movw $0, %ax
  63. movw %ax, 0x86
  64. # INT 13h AH=00h: reset the drive controller. A floppy that has just
  65. # been attached needs this before a read will succeed.
  66. movb $0x00, %ah
  67. int $0x13
  68. # Retry the read: qemu's floppy is a file image and a read can fail while
  69. # it settles. Three attempts, then give up loudly.
  70. movw $3, %cx
  71. .Lretry:
  72. movw $0x0100, %bx # ES:BX = 0000:0100
  73. movb $0x02, %ah # read sectors
  74. movb $16, %al # 16 * 512 = 8192 bytes, far more than any fixture
  75. movb $0x00, %ch # cylinder 0
  76. movb $0x02, %cl # sector 2 -- the .COM, 1-based
  77. movb $0x00, %dh # head 0
  78. movb $0x00, %dl # drive A
  79. int $0x13
  80. jnc .Lok
  81. decw %cx
  82. jnz .Lretry
  83. .Lbootfail:
  84. movb $0x7f, %al
  85. movw $0x0501, %dx # isa-debug-exit
  86. outb %al, %dx
  87. cli
  88. hlt
  89. .Lok:
  90. .byte 0xEA, 0x00, 0x01, 0x00, 0x00 # jmp 0000:0100
  91. # ---------------------------------------------------------------- INT 21h
  92. # Called with the program's registers. DS is the caller's data segment
  93. # (0000h here) and is preserved, so AH=09h can reach DS:DX the way DOS does.
  94. # Every path ends at .Ldone, which restores everything and IRETs -- except
  95. # AH=4Ch, which does not return at all.
  96. handler:
  97. pushw %ax
  98. pushw %bx
  99. pushw %cx
  100. pushw %dx
  101. pushw %si
  102. pushw %di
  103. pushw %ds
  104. pushw %es
  105. cmpb $0x02, %ah
  106. je .Lh02
  107. cmpb $0x09, %ah
  108. je .Lh09
  109. cmpb $0x08, %ah
  110. je .Lh08
  111. cmpb $0x4c, %ah
  112. je .Lh4c
  113. stc # unknown function
  114. jmp .Ldone
  115. .Lh02: # display character in AL
  116. call ser_put
  117. clc
  118. jmp .Ldone
  119. .Lh09: # display the $-terminated string at DS:DX
  120. movw %dx, 0x0702 # ser_put clobbers DX, so keep the pointer
  121. .Lh09next:
  122. movw 0x0702, %si # lodsb: AL = [DS:SI]. Register-indirect
  123. lodsb # addressing with no displacement is not
  124. cmpb $0x24, %al # expressible in gas .code16 syntax, and
  125. je .Lh09done # keeping the cursor in memory means the
  126. call ser_put # pointer survives ser_put's use of DX.
  127. incw 0x0702
  128. jmp .Lh09next
  129. .Lh09done:
  130. clc
  131. jmp .Ldone
  132. .Lh08: # read a character, no echo, 1Ah at EOF
  133. # NOTE: this is the one function whose RESULT is in AL, so it must return
  134. # through .Ldone8 and not .Ldone - see there.
  135. movw INLEN, %ax # inlen
  136. movw INCUR, %bx # input cursor
  137. # EOF is when the cursor has REACHED the length, i.e. INCUR >= INLEN.
  138. # `cmpw %bx, %ax / jbe' tests AX <= BX, that is INLEN <= INCUR, which is
  139. # true on the FIRST character: it returned 1Ah immediately, so readln saw
  140. # an empty input and then looped for the line terminator that never came.
  141. # t29_readln hung with no output at all. (This was a bug in the harness,
  142. # not in the compiler - but a harness that feeds the program nothing can
  143. # never tell you whether the program handles input, so it is a bug that
  144. # hides bugs.)
  145. cmpw %ax, %bx # INCUR vs INLEN
  146. jae .Lh08eof
  147. # INCUR is an index INTO the buffer, not an address: the bytes live at
  148. # INBUF, and SI = INCUR alone reads the interrupt vector table at 0000:0000
  149. # - so AH=08h returned IVT[0] (a low timer vector byte) as the first
  150. # character of input. INCUR is 0 on the first call, which is the one
  151. # address in segment 0 that is guaranteed to be wrong.
  152. movw %bx, %si
  153. addw $INBUF, %si
  154. lodsb
  155. incw INCUR
  156. clc
  157. jmp .Ldone8 # NOT .Ldone: AL is the result here
  158. .Lh08eof:
  159. movb $0x1a, %al
  160. clc
  161. jmp .Ldone8
  162. .Lh4c: # terminate: exit with AL as the code
  163. movw $0x0501, %dx # isa-debug-exit
  164. outb %al, %dx
  165. cli
  166. hlt
  167. jmp .Lh4c
  168. .Ldone:
  169. popw %es
  170. popw %ds
  171. popw %di
  172. popw %si
  173. popw %dx
  174. popw %cx
  175. popw %bx
  176. popw %ax
  177. iret
  178. # The same, but for the one function that RETURNS something in AL. DOS
  179. # AH=08h hands the character back in AL, so restoring AX on the way out
  180. # throws the answer away and the caller reads whatever AX held on entry.
  181. #
  182. # This was silent in a way that is worth recording: the shim's read path was
  183. # structurally correct - it found the buffer, advanced the cursor, cleared
  184. # the carry - and the character was plainly in AL, one instruction before the
  185. # return. The discard happened in the epilogue, which every OTHER function
  186. # needs. So t29_readln did not see a wrong byte; it saw the *uninitialised*
  187. # AX the runtime had at the call, which is the first byte of a pointer it was
  188. # about to overwrite with the parsed value. readln compared that against 0Dh,
  189. # 0Ah and 1Ah, rejected it, and looped forever - a hang with no output, from
  190. # a read that demonstrably worked.
  191. #
  192. # AX is therefore popped only on the paths where it is not a result, and
  193. # AH=02h/09h (which also leave AL alone in DOS) keep using .Ldone.
  194. .Ldone8:
  195. popw %es
  196. popw %ds
  197. popw %di
  198. popw %si
  199. popw %dx
  200. popw %cx
  201. popw %bx
  202. addw $2, %sp # drop the saved AX, keep AL
  203. iret
  204. # ser_put: send AL to the serial port, leaving AL and DX alone.
  205. # No line-status polling: qemu's 16550 always accepts a byte, and a poll
  206. # that never goes ready would hang the harness rather than fail it.
  207. ser_put:
  208. pushw %ax
  209. movw $0x03f8, %dx
  210. outb %al, %dx
  211. popw %ax
  212. ret
  213. # --------------------------------------------------------------- layout
  214. # The input descriptor lives at 2000h, which the 16-sector read above covers
  215. # (0100h + 2000h = 2100h) and which is well past the end of any fixture (the
  216. # largest is 4493 bytes, so the image stops at 1285h). So the descriptor is
  217. # simply part of the disk image the harness writes, not something it has to
  218. # patch into this boot sector afterwards -- which means the addresses here are
  219. # assembly constants and the harness only has to know where they land in the
  220. # file. See run_com_exec.py, FLAT_OFF.
  221. .set INLEN, 0x2000 # word: number of input bytes
  222. .set INCUR, 0x2002 # word: cursor, starts at INBUF
  223. .set INBUF, 0x2004 # the bytes themselves
  224. .set INMAX, 0x00fc # 2100h - 2004h, the most that fits
  225. .org 510
  226. .byte 0x55, 0xAA