bootcom.s 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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 input descriptor at
  39. # 2000h sits past the end of any fixture and is simply part of the disk
  40. # image the harness writes, so the addresses here are assembly constants.
  41. #
  42. # The one piece of scratch the handler keeps INSIDE the loaded region is
  43. # 0702h, the output string cursor. Since 0100h is image offset 0, that is
  44. # image offset 602h -- which is inside every fixture's image, because a
  45. # .COM is padded out to the data base at rtSz+1000h = 11B4h. It is in the
  46. # ZERO GAP between the end of the code and that data base, so it is
  47. # harmless today, and nothing reads it before writing it. But it is a
  48. # real limit and belongs in writing rather than in a discovery later: an
  49. # AH=09h against a program whose code reaches 602h scribbles over that
  50. # program's own code. The code starts at 1C7h and the longest fixture's
  51. # ends at 468h, so the margin is 19Ah = 410 bytes of code -- and it is the
  52. # same unenforced 4 KiB code window the linker documents, seen from the
  53. # other end. Moving the cursor above everything the read covers would
  54. # remove the limit; it stays at 0702h because nothing needs it yet, and
  55. # because every address in this file is restated in run_com_exec.py, which
  56. # is the thing that would have to change with it. See the layout block at
  57. # the end of this file.
  58. #
  59. # Built and driven by tests/run_com_exec.py. tests/exec/rtdrv.s assembles the
  60. # other half of this machinery -- the driver that calls runtime entries directly
  61. # -- and reuses this file rather than growing a second boot path.
  62. .code16
  63. .text
  64. .globl _start
  65. _start:
  66. cli
  67. xorw %ax, %ax
  68. movw %ax, %ds
  69. movw %ax, %es
  70. movw %ax, %ss
  71. movw $0xfffe, %sp
  72. sti
  73. # IVT entry 21h lives at 0000:0084 (21h * 4): offset word then segment.
  74. # `handler' is a section-relative offset because the section starts at 0;
  75. # the segment this sector was loaded at is 7C00h.
  76. movw $handler, %ax
  77. addw $0x7c00, %ax
  78. movw %ax, 0x84
  79. movw $0, %ax
  80. movw %ax, 0x86
  81. # INT 13h AH=00h: reset the drive controller. A floppy that has just
  82. # been attached needs this before a read will succeed.
  83. movb $0x00, %ah
  84. int $0x13
  85. # Retry the read: qemu's floppy is a file image and a read can fail while
  86. # it settles. Three attempts, then give up loudly.
  87. #
  88. # The read lands at SCRATCH, not at 0100h, and is copied down afterwards.
  89. # That is not ceremony. 0400h-04FFh is the BIOS data area, and SeaBIOS
  90. # keeps live state in it; a transfer whose destination covers that window
  91. # destroys it, and SeaBIOS writes part of it back *after* the DMA, so ten
  92. # bytes of BIOS data end up on top of the image once the transfer is over.
  93. #
  94. # The damage is the worst kind. It is the right length, in the right
  95. # place, and nothing reports an error -- the read sets CF=0, exactly as
  96. # the jnc below expects. It was found by a 19-byte probe that read 0440h
  97. # as its first instruction after the jump and got the BIOS's own bytes
  98. # back, and then confirmed by dumping the whole loaded image: one 10-byte
  99. # run wrong inside an otherwise byte-perfect 512-byte sector, which no
  100. # partial-read or sector-count bug can produce.
  101. #
  102. # 8000h is clear of the IVT (0-3FFh), the BDA (400-4FFh), SeaBIOS's stack
  103. # at 700h and the option-ROM window at C000h. REP MOVSW is executed code,
  104. # so the copy is the LAST thing that touches 0100h-20FFh and no BIOS call
  105. # follows it. The copy is what the program then runs out of, unchanged.
  106. movw $3, %cx
  107. .Lretry:
  108. movw $SCRATCH, %bx # ES:BX = 0000:8000
  109. movb $0x02, %ah # read sectors
  110. movb $16, %al # 16 * 512 = 8192 bytes, far more than any fixture
  111. movb $0x00, %ch # cylinder 0
  112. movb $0x02, %cl # sector 2 -- the .COM, 1-based
  113. movb $0x00, %dh # head 0
  114. movb $0x00, %dl # drive A
  115. int $0x13
  116. jnc .Lcopy
  117. decw %cx
  118. jnz .Lretry
  119. .Lbootfail:
  120. movb $0x7f, %al
  121. movw $0x0501, %dx # isa-debug-exit
  122. outb %al, %dx
  123. cli
  124. hlt
  125. .Lcopy:
  126. movw $0x0100, %di
  127. movw $SCRATCH, %si
  128. movw $4096, %cx # 8192 bytes = 16 sectors
  129. cld
  130. rep movsw
  131. .byte 0xEA, 0x00, 0x01, 0x00, 0x00 # jmp 0000:0100
  132. # ---------------------------------------------------------------- INT 21h
  133. # Called with the program's registers. DS is the caller's data segment
  134. # (0000h here) and is preserved, so AH=09h can reach DS:DX the way DOS does.
  135. # Every path ends at .Ldone, which restores everything and IRETs -- except
  136. # AH=4Ch, which does not return at all.
  137. handler:
  138. pushw %ax
  139. pushw %bx
  140. pushw %cx
  141. pushw %dx
  142. pushw %si
  143. pushw %di
  144. pushw %ds
  145. pushw %es
  146. # Direction flag, before anything reads it. Both lodsb's below walk
  147. # FORWARD, and DF belongs to the interrupted program, not to the
  148. # handler: a handler that assumes the caller's DF is clear is correct
  149. # only for as long as no caller ever sets it. It has never fired -
  150. # nothing in Runtime.mod uses a string instruction, so DF is whatever
  151. # the BIOS left, and that is 0 - which is exactly why it is worth
  152. # writing down. DF is not restored, because nothing downstream reads
  153. # it and restoring it would mean saving EFLAGS around the whole handler.
  154. cld
  155. cmpb $0x02, %ah
  156. je .Lh02
  157. cmpb $0x09, %ah
  158. je .Lh09
  159. cmpb $0x08, %ah
  160. je .Lh08
  161. cmpb $0x4c, %ah
  162. je .Lh4c
  163. stc # unknown function
  164. jmp .Ldone
  165. .Lh02: # display character in AL
  166. call ser_put
  167. clc
  168. jmp .Ldone
  169. .Lh09: # display the $-terminated string at DS:DX
  170. movw %dx, 0x0702 # ser_put clobbers DX, so keep the pointer
  171. .Lh09next:
  172. movw 0x0702, %si # lodsb: AL = [DS:SI]. Register-indirect
  173. lodsb # addressing with no displacement is not
  174. cmpb $0x24, %al # expressible in gas .code16 syntax, and
  175. je .Lh09done # keeping the cursor in memory means the
  176. call ser_put # pointer survives ser_put's use of DX.
  177. incw 0x0702
  178. jmp .Lh09next
  179. .Lh09done:
  180. clc
  181. jmp .Ldone
  182. .Lh08: # read a character, no echo, 1Ah at EOF
  183. # NOTE: this is the one function whose RESULT is in AL, so it must return
  184. # through .Ldone8 and not .Ldone - see there.
  185. movw INLEN, %ax # inlen
  186. movw INCUR, %bx # input cursor
  187. # EOF is when the cursor has REACHED the length, i.e. INCUR >= INLEN.
  188. # `cmpw %bx, %ax / jbe' tests AX <= BX, that is INLEN <= INCUR, which is
  189. # true on the FIRST character: it returned 1Ah immediately, so readln saw
  190. # an empty input and then looped for the line terminator that never came.
  191. # t29_readln hung with no output at all. (This was a bug in the harness,
  192. # not in the compiler - but a harness that feeds the program nothing can
  193. # never tell you whether the program handles input, so it is a bug that
  194. # hides bugs.)
  195. cmpw %ax, %bx # INCUR vs INLEN
  196. jae .Lh08eof
  197. # INCUR is an index INTO the buffer, not an address: the bytes live at
  198. # INBUF, and SI = INCUR alone reads the interrupt vector table at 0000:0000
  199. # - so AH=08h returned IVT[0] (a low timer vector byte) as the first
  200. # character of input. INCUR is 0 on the first call, which is the one
  201. # address in segment 0 that is guaranteed to be wrong.
  202. movw %bx, %si
  203. addw $INBUF, %si
  204. lodsb
  205. incw INCUR
  206. clc
  207. jmp .Ldone8 # NOT .Ldone: AL is the result here
  208. .Lh08eof:
  209. movb $0x1a, %al
  210. clc
  211. jmp .Ldone8
  212. .Lh4c: # terminate: exit with AL as the code
  213. movw $0x0501, %dx # isa-debug-exit
  214. outb %al, %dx
  215. cli
  216. hlt
  217. jmp .Lh4c
  218. .Ldone:
  219. popw %es
  220. popw %ds
  221. popw %di
  222. popw %si
  223. popw %dx
  224. popw %cx
  225. popw %bx
  226. popw %ax
  227. iret
  228. # The same, but for the one function that RETURNS something in AL. DOS
  229. # AH=08h hands the character back in AL, so restoring AX on the way out
  230. # throws the answer away and the caller reads whatever AX held on entry.
  231. #
  232. # This was silent in a way that is worth recording: the shim's read path was
  233. # structurally correct - it found the buffer, advanced the cursor, cleared
  234. # the carry - and the character was plainly in AL, one instruction before the
  235. # return. The discard happened in the epilogue, which every OTHER function
  236. # needs. So t29_readln did not see a wrong byte; it saw the *uninitialised*
  237. # AX the runtime had at the call, which is the first byte of a pointer it was
  238. # about to overwrite with the parsed value. readln compared that against 0Dh,
  239. # 0Ah and 1Ah, rejected it, and looped forever - a hang with no output, from
  240. # a read that demonstrably worked.
  241. #
  242. # AX is therefore popped only on the paths where it is not a result, and
  243. # AH=02h/09h (which also leave AL alone in DOS) keep using .Ldone.
  244. .Ldone8:
  245. popw %es
  246. popw %ds
  247. popw %di
  248. popw %si
  249. popw %dx
  250. popw %cx
  251. popw %bx
  252. addw $2, %sp # drop the saved AX, keep AL
  253. iret
  254. # ser_put: send AL to the serial port, leaving AL and DX alone.
  255. # No line-status polling: qemu's 16550 always accepts a byte, and a poll
  256. # that never goes ready would hang the harness rather than fail it.
  257. ser_put:
  258. pushw %ax
  259. movw $0x03f8, %dx
  260. outb %al, %dx
  261. popw %ax
  262. ret
  263. # --------------------------------------------------------------- layout
  264. # The input descriptor lives at 2000h, which the read + copy above covers
  265. # (0100h + 2000h = 2100h) and which is well past the end of any fixture (the
  266. # largest is 4493 bytes, so the image stops at 1285h). So the descriptor is
  267. # simply part of the disk image the harness writes, not something it has to
  268. # patch into this boot sector afterwards -- which means the addresses here are
  269. # assembly constants and the harness only has to know where they land in the
  270. # file. See run_com_exec.py, FLAT_OFF.
  271. #
  272. # SCRATCH is where the sector read parks the bytes before the copy moves them
  273. # to 0100h. Nothing may ever be placed there: the region is not reserved by
  274. # this file, it is merely unused, and a future change that put a table at
  275. # 8000h would be overwritten by the read and would not notice.
  276. .set SCRATCH, 0x8000 # see the note above: not 0100h
  277. .set INLEN, 0x2000 # word: number of input bytes
  278. .set INCUR, 0x2002 # word: cursor, starts at INBUF
  279. .set INBUF, 0x2004 # the bytes themselves
  280. .set INMAX, 0x00fc # 2100h - 2004h, the most that fits
  281. .org 510
  282. .byte 0x55, 0xAA