bootcom.s 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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 a scratch segment
  11. # 3. copy it to LOAD_SEG:0100 (default 1000h, above the boot/BIOS areas)
  12. # 4. SS:SP = LOAD_SEG:FFFE and JMP LOAD_SEG: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=0Bh input status: AL=FFh if a character waits, 0 if not
  21. # AH=4Ch terminate
  22. #
  23. # Those five are the complete set. See Runtime.mod: every Int21 in it is one
  24. # of them (the "MovAh (0)" in EmitRdInt is not an INT 21h call, it is the
  25. # MOV AH,0 that loads a digit into AL before an ADD -- an easy thing to
  26. # misread as a fourth function).
  27. #
  28. # Two properties are load-bearing:
  29. #
  30. # * The handler NEVER writes to the serial port as a marker. The program's
  31. # output is arbitrary bytes, so any sentinel could collide with real
  32. # output. Termination travels out of band instead, through the
  33. # isa-debug-exit device at port 0501h: the program exits with code 0 and
  34. # qemu exits (value<<1)|1 = 1. A boot failure exits with value 7Fh, i.e.
  35. # qemu exit code 255, which is unambiguous. Every byte on the serial port
  36. # is therefore program output, with nothing to strip.
  37. #
  38. # * The default program segment is 1000h, so the image starts at 1000:0100
  39. # and the boot sector/IVT stay untouched. The input descriptor is at
  40. # 1000:9000, above the largest current image (CALC's code ends below 8A00h)
  41. # and below the data area at 1000:A100. It is simply part of the disk image
  42. # the harness writes. rt_exec.py overrides LOAD_SEG/INLEN back to 0/7000
  43. # for its own driver image.
  44. #
  45. # The handler owns no scratch inside the loaded region. It used to keep
  46. # the AH=09h cursor in memory at 0702h -- image offset 602h, inside every
  47. # fixture's image, because a .COM is padded out to the data base at
  48. # rtSz+1000h. That was a real limit, and it was hit: string concatenation
  49. # emits enough code that a program's code can reach 602h, and the AH=09h
  50. # that prints a writeln's CRLF then overwrote that program's own `call
  51. # progend'. The cursor lives in SI now, not in memory at all -- lodsb
  52. # advances SI and ser_put leaves SI alone -- so the handler cannot scribble
  53. # on the program, and the code window is the linker's 4 KiB again rather
  54. # than this handler's 602h. See the layout block at the end of this file.
  55. #
  56. # Built and driven by tests/run_com_exec.py. tests/exec/rtdrv.s assembles the
  57. # other half of this machinery -- the driver that calls runtime entries directly
  58. # -- and reuses this file rather than growing a second boot path.
  59. .code16
  60. .text
  61. .globl _start
  62. _start:
  63. cli
  64. xorw %ax, %ax
  65. movw %ax, %ds
  66. movw %ax, %es
  67. movw %ax, %ss
  68. movw $0xfffe, %sp
  69. sti
  70. # IVT entry 21h lives at 0000:0084 (21h * 4): offset word then segment.
  71. # `handler' is a section-relative offset because the section starts at 0;
  72. # the segment this sector was loaded at is 7C00h.
  73. movw $handler, %ax
  74. addw $0x7c00, %ax
  75. movw %ax, 0x84
  76. movw $0, %ax
  77. movw %ax, 0x86
  78. # INT 13h AH=00h: reset the drive controller. A floppy that has just
  79. # been attached needs this before a read will succeed.
  80. movb $0x00, %ah
  81. int $0x13
  82. # Retry the read: qemu's floppy is a file image and a read can fail while
  83. # it settles. Three attempts, then give up loudly.
  84. #
  85. # The read lands at SCRATCH, not at 0100h, and is copied down afterwards.
  86. # That is not ceremony. 0400h-04FFh is the BIOS data area, and SeaBIOS
  87. # keeps live state in it; a transfer whose destination covers that window
  88. # destroys it, and SeaBIOS writes part of it back *after* the DMA, so ten
  89. # bytes of BIOS data end up on top of the image once the transfer is over.
  90. #
  91. # The damage is the worst kind. It is the right length, in the right
  92. # place, and nothing reports an error -- the read sets CF=0, exactly as
  93. # the jnc below expects. It was found by a 19-byte probe that read 0440h
  94. # as its first instruction after the jump and got the BIOS's own bytes
  95. # back, and then confirmed by dumping the whole loaded image: one 10-byte
  96. # run wrong inside an otherwise byte-perfect 512-byte sector, which no
  97. # partial-read or sector-count bug can produce.
  98. #
  99. # 1000:0000 (linear 10000h) is clear of the IVT (0-3FFh), the BDA
  100. # (400-4FFh), SeaBIOS's stack at 700h and the option-ROM window at C000h.
  101. # REP MOVSW is executed code, so the copy is the LAST thing that touches
  102. # 0100h-7100h, and it stops below the boot sector at 7C00h. The copy is
  103. # what the program then runs out of, unchanged.
  104. # The image is read one track at a time. A single INT 13h call cannot
  105. # cross a track reliably, and a per-sector loop has to keep CHS and the
  106. # destination across a BIOS call that may clobber any register. Five
  107. # fixed calls are simpler: track 0 starts at sector 2 (sector 1 is this
  108. # boot sector), every later track starts at sector 1, and the last read
  109. # takes the one sector that is left of the 72.
  110. .set READS, 5
  111. # Read the image one sector at a time. The loop state lives in registers,
  112. # and every register is pushed around INT 13h: the BIOS is allowed to
  113. # clobber all of them, and POP does not touch the carry, so the error flag
  114. # survives the restore. One sector per call also cannot cross a track.
  115. movw $SECTORS, %si # sectors still to read
  116. movw $SCRATCH_SEG, %ax
  117. movw %ax, %es
  118. xorw %bx, %bx # destination offset in ES
  119. xorw %cx, %cx # CH=cylinder 0, CL=sector 0
  120. xorw %dx, %dx # DH=head 0, DL=drive A
  121. movb $2, %cl # sector 2 -- the .COM, 1-based
  122. .Lread:
  123. movw $3, %bp
  124. .Lretry:
  125. movb $0x02, %ah
  126. movb $1, %al
  127. pushw %es
  128. pushw %bp
  129. pushw %si
  130. pushw %bx
  131. pushw %cx
  132. pushw %dx
  133. int $0x13
  134. popw %dx
  135. popw %cx
  136. popw %bx
  137. popw %si
  138. popw %bp
  139. popw %es
  140. jnc .Lreadok
  141. decw %bp
  142. jnz .Lretry
  143. jmp .Lbootfail
  144. .Lreadok:
  145. addw $512, %bx
  146. incb %cl
  147. cmpb $19, %cl
  148. jb .Lreadnext
  149. movb $1, %cl
  150. xorb $1, %dh
  151. jnz .Lreadnext
  152. incb %ch
  153. .Lreadnext:
  154. decw %si
  155. jnz .Lread
  156. jmp .Lcopy
  157. .Lbootfail:
  158. movb $0x7f, %al
  159. movw $0x0501, %dx # isa-debug-exit
  160. outb %al, %dx
  161. cli
  162. hlt
  163. .Lcopy:
  164. movw $SCRATCH_SEG, %ax
  165. movw %ax, %ds
  166. xorw %si, %si # source 1000:0000
  167. movw $LOAD_SEG, %ax
  168. movw %ax, %es
  169. movw $0x0100, %di # destination LOAD_SEG:0100
  170. movw $(COPY_BYTES / 2), %cx # bytes / 2
  171. cld
  172. rep movsw
  173. movw $LOAD_SEG, %ax
  174. movw %ax, %ds # the .COM expects DS=CS
  175. movw %ax, %es
  176. movw %ax, %ss
  177. movw $0xfffe, %sp
  178. .byte 0xEA, 0x00, 0x01
  179. .word LOAD_SEG # jmp LOAD_SEG:0100
  180. # ---------------------------------------------------------------- INT 21h
  181. # Called with the program's registers. DS is the caller's data segment
  182. # (0000h here) and is preserved, so AH=09h can reach DS:DX the way DOS does.
  183. # Every path ends at .Ldone, which restores everything and IRETs -- except
  184. # AH=4Ch, which does not return at all.
  185. handler:
  186. pushw %ax
  187. pushw %bx
  188. pushw %cx
  189. pushw %dx
  190. pushw %si
  191. pushw %di
  192. pushw %ds
  193. pushw %es
  194. # Direction flag, before anything reads it. Both lodsb's below walk
  195. # FORWARD, and DF belongs to the interrupted program, not to the
  196. # handler: a handler that assumes the caller's DF is clear is correct
  197. # only for as long as no caller ever sets it. It has never fired -
  198. # nothing in Runtime.mod uses a string instruction, so DF is whatever
  199. # the BIOS left, and that is 0 - which is exactly why it is worth
  200. # writing down. DF is not restored, because nothing downstream reads
  201. # it and restoring it would mean saving EFLAGS around the whole handler.
  202. cld
  203. cmpb $0x02, %ah
  204. je .Lh02
  205. cmpb $0x09, %ah
  206. je .Lh09
  207. cmpb $0x08, %ah
  208. je .Lh08
  209. cmpb $0x0b, %ah
  210. je .Lh0b
  211. cmpb $0x3d, %ah
  212. je .Lh3d
  213. cmpb $0x4c, %ah
  214. je .Lh4c
  215. stc # unknown function
  216. jmp .Ldone
  217. .Lh02: # display character in AL
  218. call ser_put
  219. clc
  220. jmp .Ldone
  221. .Lh09: # display the $-terminated string at DS:DX
  222. movw %dx, %si # SI is the cursor. lodsb walks it forward,
  223. .Lh09next: # and ser_put leaves SI alone -- it clobbers
  224. lodsb # only DX, which is why the pointer cannot
  225. cmpb $0x24, %al # simply stay in DX across the call.
  226. je .Lh09done
  227. call ser_put
  228. jmp .Lh09next
  229. .Lh09done:
  230. clc
  231. jmp .Ldone
  232. .Lh08: # read a character, no echo, 1Ah at EOF
  233. # NOTE: this is the one function whose RESULT is in AL, so it must return
  234. # through .Ldone8 and not .Ldone - see there.
  235. movw INLEN, %ax # inlen
  236. movw INCUR, %bx # input cursor
  237. # EOF is when the cursor has REACHED the length, i.e. INCUR >= INLEN.
  238. # `cmpw %bx, %ax / jbe' tests AX <= BX, that is INLEN <= INCUR, which is
  239. # true on the FIRST character: it returned 1Ah immediately, so readln saw
  240. # an empty input and then looped for the line terminator that never came.
  241. # t29_readln hung with no output at all. (This was a bug in the harness,
  242. # not in the compiler - but a harness that feeds the program nothing can
  243. # never tell you whether the program handles input, so it is a bug that
  244. # hides bugs.)
  245. cmpw %ax, %bx # INCUR vs INLEN
  246. jae .Lh08eof
  247. # INCUR is an index INTO the buffer, not an address: the bytes live at
  248. # INBUF, and SI = INCUR alone reads the interrupt vector table at 0000:0000
  249. # - so AH=08h returned IVT[0] (a low timer vector byte) as the first
  250. # character of input. INCUR is 0 on the first call, which is the one
  251. # address in segment 0 that is guaranteed to be wrong.
  252. movw %bx, %si
  253. addw $INBUF, %si
  254. lodsb
  255. incw INCUR
  256. clc
  257. jmp .Ldone8 # NOT .Ldone: AL is the result here
  258. .Lh08eof:
  259. movb $0x1a, %al
  260. clc
  261. jmp .Ldone8
  262. .Lh0b: # input status: AL=FFh if a character waits
  263. movw INLEN, %ax
  264. movw INCUR, %bx
  265. cmpw %ax, %bx # INCUR vs INLEN: >= is end of input
  266. jae .Lh0b_none
  267. movb $0xff, %al
  268. clc
  269. jmp .Ldone8 # AL is the result, as with AH=08h
  270. .Lh0b_none:
  271. xorw %ax, %ax # AL = 0: nothing waiting
  272. clc
  273. jmp .Ldone8
  274. .Lh3d: # open file: no filesystem in this harness
  275. # The runtime uses real DOS AH=3Dh. A boot floppy has no DOS and no
  276. # filesystem driver, so answer the one question the file-operation
  277. # fixture asks - "the file is absent" - the way DOS does: CF=1,
  278. # AX=2 (File not found). The result must survive the epilogue, and
  279. # CF must be visible to the guest, so this uses .LdoneFile.
  280. movw $2, %ax
  281. stc
  282. jmp .LdoneFile
  283. .Lh4c: # terminate: exit with AL as the code
  284. movw $0x0501, %dx # isa-debug-exit
  285. outb %al, %dx
  286. cli
  287. hlt
  288. jmp .Lh4c
  289. .Ldone:
  290. popw %es
  291. popw %ds
  292. popw %di
  293. popw %si
  294. popw %dx
  295. popw %cx
  296. popw %bx
  297. popw %ax
  298. iret
  299. # The same, but for the one function that RETURNS something in AL. DOS
  300. # AH=08h hands the character back in AL, so restoring AX on the way out
  301. # throws the answer away and the caller reads whatever AX held on entry.
  302. #
  303. # This was silent in a way that is worth recording: the shim's read path was
  304. # structurally correct - it found the buffer, advanced the cursor, cleared
  305. # the carry - and the character was plainly in AL, one instruction before the
  306. # return. The discard happened in the epilogue, which every OTHER function
  307. # needs. So t29_readln did not see a wrong byte; it saw the *uninitialised*
  308. # AX the runtime had at the call, which is the first byte of a pointer it was
  309. # about to overwrite with the parsed value. readln compared that against 0Dh,
  310. # 0Ah and 1Ah, rejected it, and looped forever - a hang with no output, from
  311. # a read that demonstrably worked.
  312. #
  313. # AX is therefore popped only on the paths where it is not a result, and
  314. # AH=02h/09h (which also leave AL alone in DOS) keep using .Ldone.
  315. .LdoneFile:
  316. # AX is the result and CF is the DOS error flag. POP does not change
  317. # CF, so restore the saved registers first, then copy CF into the saved
  318. # FLAGS word before dropping the saved AX and IRETing. The 16-bit ADD
  319. # used to discard AX would clobber CF, so set FLAGS before it.
  320. popw %es
  321. popw %ds
  322. popw %di
  323. popw %si
  324. popw %dx
  325. popw %cx
  326. popw %bx
  327. # 16-bit addressing has no [SP] form. Borrow BX as the frame pointer
  328. # for two instructions; POP does not affect CF, so the branch still sees
  329. # the handler's result. After the push, SP points at the saved BX, so
  330. # saved AX is +2 and saved FLAGS are +8.
  331. pushw %bx
  332. movw %sp, %bx
  333. jnc .Ldf_ok
  334. orw $1, 8(%bx) # saved FLAGS: set CF
  335. jmp .Ldf_drop
  336. .Ldf_ok:
  337. andw $0xfffe, 8(%bx) # saved FLAGS: clear CF
  338. .Ldf_drop:
  339. popw %bx
  340. addw $2, %sp # drop the saved AX, keep the result
  341. iret
  342. .Ldone8:
  343. popw %es
  344. popw %ds
  345. popw %di
  346. popw %si
  347. popw %dx
  348. popw %cx
  349. popw %bx
  350. addw $2, %sp # drop the saved AX, keep AL
  351. iret
  352. # ser_put: send AL to the serial port, leaving AL and DX alone.
  353. # No line-status polling: qemu's 16550 always accepts a byte, and a poll
  354. # that never goes ready would hang the harness rather than fail it.
  355. ser_put:
  356. pushw %ax
  357. movw $0x03f8, %dx
  358. outb %al, %dx
  359. popw %ax
  360. ret
  361. # --------------------------------------------------------------- layout
  362. # The defaults are LOAD_SEG=1000h and INLEN=9000h: the image is copied to
  363. # 1000:0100, the input descriptor is at 1000:9000, and the data area is at
  364. # 1000:A100. The copy and the boot sector are in different segments, so the
  365. # boot sector at 0000:7C00h is never touched. The constants are assembly
  366. # constants and the harness only has to know where they land in the disk image.
  367. # See run_com_exec.py, FLAT_OFF.
  368. #
  369. # SCRATCH is where the sector read parks the bytes before the copy moves them
  370. # to LOAD_SEG:0100. Nothing may ever be placed there: the region is not reserved by
  371. # this file, it is merely unused, and a future change that put a table at
  372. # 10000h would be overwritten by the read and would not notice.
  373. .ifndef LOAD_SEG
  374. .set LOAD_SEG, 0x1000 # load above the boot sector/BIOS areas
  375. .endif
  376. .ifndef INLEN
  377. .set INLEN, 0x9000 # word: number of input bytes
  378. .endif
  379. .ifndef SCRATCH_SEG
  380. .set SCRATCH_SEG, LOAD_SEG + 0x1000
  381. .endif
  382. .ifndef COPY_BYTES
  383. .set COPY_BYTES, INLEN
  384. .endif
  385. .set SECTORS, 72 # 36,864 bytes read; input is inside this
  386. .set INCUR, INLEN + 2 # word: cursor, starts at INBUF
  387. .set INBUF, INLEN + 4 # the bytes themselves
  388. .set INMAX, 0x00fc # INLEN+100h - INBUF, the most that fits
  389. .org 510
  390. .byte 0x55, 0xAA