about summary refs log tree commit diff stats
path: root/baremetal
diff options
context:
space:
mode:
authorKartik Agaram <vc@akkartik.com>2021-01-16 16:24:38 -0800
committerKartik Agaram <vc@akkartik.com>2021-01-16 16:27:58 -0800
commit117a710a9d04c7176ce192cdb26134d2828478f9 (patch)
tree27049218b292f865ad55c907bacc1824e5022cfb /baremetal
parent6efc1ebed7131a8cd88aafdce8eaa8ee0260d692 (diff)
downloadmu-117a710a9d04c7176ce192cdb26134d2828478f9.tar.gz
7530 - baremetal: print ints to screen
Diffstat (limited to 'baremetal')
-rw-r--r--baremetal/117write-int-hex.subx202
-rw-r--r--baremetal/126write-int-decimal.subx428
-rw-r--r--baremetal/313index-bounds-check.subx13
-rw-r--r--baremetal/400.mu14
-rw-r--r--baremetal/501draw-text.mu150
-rw-r--r--baremetal/mu-init.subx2
6 files changed, 807 insertions, 2 deletions
diff --git a/baremetal/117write-int-hex.subx b/baremetal/117write-int-hex.subx
new file mode 100644
index 00000000..091bd874
--- /dev/null
+++ b/baremetal/117write-int-hex.subx
@@ -0,0 +1,202 @@
+# Write out the (hex) textual representation of numbers.
+
+== code
+#   instruction                     effective address                                                   register    displacement    immediate
+# . op          subop               mod             rm32          base        index         scale       r32
+# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
+
+# convert the lowest nibble of eax to ascii and return it in the lowest byte of eax
+to-hex-char:  # in/eax: int -> out/eax: int
+    # no error checking; accepts argument in eax
+    # if (eax <= 9) return eax + '0'
+    3d/compare-eax-with  0x9/imm32/9
+    7f/jump-if->  $to-hex-char:else/disp8
+    05/add-to-eax  0x30/imm32/0
+    c3/return
+$to-hex-char:else:
+    # otherwise return eax + 'a' - 10
+    05/add-to-eax  0x57/imm32/a-10
+    c3/return
+
+append-byte-hex:  # f: (addr stream byte), n: int
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # . save registers
+    50/push-eax
+    # AL = convert upper nibble to hex
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0xc/disp8       .                 # copy *(ebp+12) to eax
+    c1/shift    5/subop/logic-right 3/mod/direct    0/rm32/eax    .           .             .           .           .               4/imm8            # shift eax right by 4 bits, while padding zeroes
+    25/and-eax  0xf/imm32
+    # . AL = to-hex-char(AL)
+    e8/call  to-hex-char/disp32
+    # append-byte(f, AL)
+    # . . push args
+    50/push-eax
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
+    # . . call
+    e8/call  append-byte/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+    # AL = convert lower nibble to hex
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0xc/disp8       .                 # copy *(ebp+12) to eax
+    25/and-eax  0xf/imm32
+    # . AL = to-hex-char(AL)
+    e8/call  to-hex-char/disp32
+    # append-byte(f, AL)
+    # . . push args
+    50/push-eax
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
+    # . . call
+    e8/call  append-byte/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+$append-byte-hex:end:
+    # . restore registers
+    58/pop-to-eax
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-append-byte-hex:
+    # - check that append-byte-hex adds the hex textual representation
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # append-byte-hex(_test-stream, 0xa)  # exercises digit, non-digit as well as leading zero
+    # . . push args
+    68/push  0xa/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  append-byte-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+    # check-stream-equal(_test-stream, "0a", msg)
+    # . . push args
+    68/push  "F - test-append-byte-hex"/imm32
+    68/push  "0a"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . end
+    c3/return
+
+write-int32-hex:  # f: (addr stream byte), n: int
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+$write-int32-hex:hex-prefix:
+    # write(f, "0x")
+    # . . push args
+    68/push  "0x"/imm32
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
+    # . . call
+    e8/call  write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+$write-int32-hex:rest:
+    # write-int32-hex-bits(f, n, 32)
+    # . . push args
+    68/push  0x20/imm32
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
+    # . . call
+    e8/call  write-int32-hex-bits/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+$write-int32-hex:end:
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+# print rightmost 'bits' of 'n'
+# bits must be multiple of 4
+write-int32-hex-bits:  # f: (addr stream byte), n: int, bits: int
+    # pseudocode:
+    #  bits -= 4
+    #  while true
+    #    if (bits < 0) break
+    #    eax = n >> bits
+    #    eax = eax & 0xf
+    #    append-byte(f, AL)
+    #    bits -= 4
+    #
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # . save registers
+    50/push-eax
+    51/push-ecx
+    # ecx = bits-4
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # copy *(ebp+16) to ecx
+    81          5/subop/subtract    3/mod/direct    1/rm32/ecx    .           .             .           .           .               4/imm32           # subtract from ecx
+$write-int32-hex-bits:loop:
+    # if (bits < 0) break
+    81          7/subop/compare     3/mod/direct    1/rm32/ecx    .           .             .           .           .               0/imm32           # compare ecx
+    7c/jump-if-<  $write-int32-hex-bits:end/disp8
+    # eax = n >> bits
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0xc/disp8       .                 # copy *(ebp+12) to eax
+    d3/>>ecx    5/subop/pad-zeroes  3/mod/direct    0/rm32/eax    .           .             .           .           .               .                 # shift eax right by ecx bits, padding zeroes
+    # eax = to-hex-char(AL)
+    25/and-eax  0xf/imm32
+    e8/call  to-hex-char/disp32
+    # append-byte(f, AL)
+    # . . push args
+    50/push-eax
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
+    # . . call
+    e8/call  append-byte/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+    # bits -= 4
+    81          5/subop/subtract    3/mod/direct    1/rm32/ecx    .           .             .           .           .               4/imm32           # subtract from ecx
+    eb/jump  $write-int32-hex-bits:loop/disp8
+$write-int32-hex-bits:end:
+    # . restore registers
+    59/pop-to-ecx
+    58/pop-to-eax
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-write-int32-hex:
+    # - check that write-int32-hex prints the hex textual representation
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # write-int32-hex(_test-stream, 0x8899aa)
+    # . . push args
+    68/push  0x8899aa/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  write-int32-hex/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+    # check-stream-equal(_test-stream, "0x008899aa", msg)
+    # . . push args
+    68/push  "F - test-write-int32-hex"/imm32
+    68/push  "0x008899aa"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . end
+    c3/return
+
+# . . vim:nowrap:textwidth=0
diff --git a/baremetal/126write-int-decimal.subx b/baremetal/126write-int-decimal.subx
new file mode 100644
index 00000000..eae331a1
--- /dev/null
+++ b/baremetal/126write-int-decimal.subx
@@ -0,0 +1,428 @@
+# Helper to print an int32 in decimal.
+
+== code
+#   instruction                     effective address                                                   register    displacement    immediate
+# . op          subop               mod             rm32          base        index         scale       r32
+# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
+
+write-int32-decimal:  # out: (addr stream byte), n: int
+    # works by generating characters from lowest to highest and pushing them
+    # to the stack, before popping them one by one into the stream
+    #
+    # pseudocode:
+    #   push sentinel
+    #   eax = abs(n)
+    #   while true
+    #     sign-extend eax into edx
+    #     eax, edx = eax/10, eax%10
+    #     edx += '0'
+    #     push edx
+    #     if (eax == 0) break
+    #   if n < 0
+    #     push '-'
+    #   w = out->write
+    #   curr = &out->data[out->write]
+    #   max = &out->data[out->size]
+    #   while true
+    #     pop into eax
+    #     if (eax == sentinel) break
+    #     if (curr >= max) abort
+    #     *curr = AL
+    #     ++curr
+    #     ++w
+    #   out->write = w
+    # (based on K&R itoa: https://en.wikibooks.org/wiki/C_Programming/stdlib.h/itoa)
+    # (this pseudocode contains registers because operations like division
+    # require specific registers in x86)
+    #
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # . save registers
+    50/push-eax
+    51/push-ecx
+    52/push-edx
+    53/push-ebx
+    57/push-edi
+    # const ten/ecx = 10
+    b9/copy-to-ecx  0xa/imm32
+    # push sentinel
+    68/push  0/imm32/sentinel
+    # var eax: int = abs(n)
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0xc/disp8       .                 # copy *(ebp+12) to eax
+    3d/compare-eax-with  0/imm32
+    7d/jump-if->=  $write-int32-decimal:read-loop/disp8
+$write-int32-decimal:negative:
+    f7          3/subop/negate      3/mod/direct    0/rm32/eax    .           .             .           .           .               .                 # negate eax
+$write-int32-decimal:read-loop:
+    # eax, edx = eax / 10, eax % 10
+    99/sign-extend-eax-into-edx
+    f7          7/subop/idiv        3/mod/direct    1/rm32/ecx    .           .             .           .           .               .                 # divide edx:eax by ecx, storing quotient in eax and remainder in edx
+    # edx += '0'
+    81          0/subop/add         3/mod/direct    2/rm32/edx    .           .             .           .           .               0x30/imm32        # add to edx
+    # push edx
+    52/push-edx
+    # if (eax == 0) break
+    3d/compare-eax-and  0/imm32
+    7f/jump-if->  $write-int32-decimal:read-loop/disp8
+$write-int32-decimal:read-break:
+    # if (n < 0) push('-')
+    81          7/subop/compare     1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       0/imm32           # compare *(ebp+12)
+    7d/jump-if->=  $write-int32-decimal:write/disp8
+$write-int32-decimal:push-negative:
+    68/push  0x2d/imm32/-
+$write-int32-decimal:write:
+    # edi = out
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           7/r32/edi   8/disp8         .                 # copy *(ebp+8) to edi
+    # var w/edx: int = out->write
+    8b/copy                         0/mod/indirect  7/rm32/edi    .           .             .           2/r32/edx   .               .                 # copy *edi to edx
+    # var curr/ecx: (addr byte) = &out->data[out->write]
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    7/base/edi  2/index/edx   .           1/r32/ecx   0xc/disp8       .                 # copy ebx+edx+12 to ecx
+    # var max/ebx: (addr byte) = &out->data[out->size]
+    8b/copy                         1/mod/*+disp8   7/rm32/edi    .           .             .           3/r32/ebx   8/disp8         .                 # copy *(edi+8) to ebx
+    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    7/base/edi  3/index/ebx   .           3/r32/ebx   0xc/disp8       .                 # copy edi+ebx+12 to ebx
+$write-int32-decimal:write-loop:
+    # pop into eax
+    58/pop-to-eax
+    # if (eax == sentinel) break
+    3d/compare-eax-and  0/imm32/sentinel
+    74/jump-if-=  $write-int32-decimal:write-break/disp8
+    # if (curr >= max) abort
+    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           3/r32/ebx   .               .                 # compare ecx with ebx
+    73/jump-if-addr>=  $write-int32-decimal:abort/disp8
+$write-int32-decimal:write-char:
+    # *curr = AL
+    88/copy-byte                    0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/AL    .               .                 # copy AL to byte at *ecx
+    # ++curr
+    41/increment-ecx
+    # ++w
+    42/increment-edx
+    eb/jump  $write-int32-decimal:write-loop/disp8
+$write-int32-decimal:write-break:
+    # out->write = w
+    89/copy                         0/mod/indirect  7/rm32/edi    .           .             .           2/r32/edx   .               .                 # copy edx to *edi
+$write-int32-decimal:end:
+    # . restore registers
+    5f/pop-to-edi
+    5b/pop-to-ebx
+    5a/pop-to-edx
+    59/pop-to-ecx
+    58/pop-to-eax
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+$write-int32-decimal:abort:
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "write-int32-decimal: stream out of space" 3)  # 3=cyan
+    {
+      eb/jump loop/disp8
+    }
+    # never gets here
+
+test-write-int32-decimal:
+    # - check that a single-digit number converts correctly
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # write-int32-decimal(_test-stream, 9)
+    # . . push args
+    68/push  9/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  write-int32-decimal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+    # check-stream-equal(_test-stream, "9", msg)
+    # . . push args
+    68/push  "F - test-write-int32-decimal"/imm32
+    68/push  "9"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . end
+    c3/return
+
+test-write-int32-decimal-zero:
+    # - check that 0 converts correctly
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # write-int32-decimal(_test-stream, 0)
+    # . . push args
+    68/push  0/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  write-int32-decimal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+    # check-stream-equal(_test-stream, "0", msg)
+    # . . push args
+    68/push  "F - test-write-int32-decimal-zero"/imm32
+    68/push  "0"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . end
+    c3/return
+
+test-write-int32-decimal-multiple-digits:
+    # - check that a multi-digit number converts correctly
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # write-int32-decimal(_test-stream, 10)
+    # . . push args
+    68/push  0xa/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  write-int32-decimal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+    # check-stream-equal(_test-stream, "10", msg)
+    # . . push args
+    68/push  "F - test-write-int32-decimal-multiple-digits"/imm32
+    68/push  "10"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . end
+    c3/return
+
+test-write-int32-decimal-negative:
+    # - check that a negative single-digit number converts correctly
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # write-int32-decimal(_test-stream, -9)
+    # . . push args
+    68/push  -9/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  write-int32-decimal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+#?     # dump _test-stream {{{
+#?     # . write(2/stderr, "^")
+#?     # . . push args
+#?     68/push  "^"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+#?     # . write-stream(2/stderr, _test-stream)
+#?     # . . push args
+#?     68/push  _test-stream/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+#?     # }}}
+    # check-stream-equal(_test-stream, "-9", msg)
+    # . . push args
+    68/push  "F - test-write-int32-decimal-negative"/imm32
+    68/push  "-9"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . end
+    c3/return
+
+test-write-int32-decimal-negative-multiple-digits:
+    # - check that a multi-digit number converts correctly
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # write-int32-decimal(_test-stream, -10)
+    # . . push args
+    68/push  -0xa/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  write-int32-decimal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
+    # check-stream-equal(_test-stream, "-10", msg)
+    # . . push args
+    68/push  "F - test-write-int32-decimal-negative-multiple-digits"/imm32
+    68/push  "-10"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # . end
+    c3/return
+
+is-decimal-digit?:  # c: grapheme -> result/eax: boolean
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # . save registers
+    51/push-ecx
+    # ecx = c
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   8/disp8         .                 # copy *(ebp+8) to ecx
+    # result = false
+    b8/copy-to-eax  0/imm32/false
+    # return false if c < '0'
+    81          7/subop/compare     3/mod/direct    1/rm32/ecx    .           .             .           .           .               0x30/imm32        # compare ecx
+    7c/jump-if-<  $is-decimal-digit?:end/disp8
+    # return (c <= '9')
+    81          7/subop/compare     3/mod/direct    1/rm32/ecx    .           .             .           .           .               0x39/imm32        # compare ecx
+    7f/jump-if->  $is-decimal-digit?:end/disp8
+$is-decimal-digit?:true:
+    b8/copy-to-eax  1/imm32/true
+$is-decimal-digit?:end:
+    # . restore registers
+    59/pop-to-ecx
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+test-is-decimal-digit-below-0:
+    # eax = is-decimal-digit?(0x2f)
+    # . . push args
+    68/push  0x2f/imm32
+    # . . call
+    e8/call  is-decimal-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0, msg)
+    # . . push args
+    68/push  "F - test-is-decimal-digit-below-0"/imm32
+    68/push  0/imm32/false
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    c3/return
+
+test-is-decimal-digit-0-to-9:
+    # eax = is-decimal-digit?(0x30)
+    # . . push args
+    68/push  0x30/imm32
+    # . . call
+    e8/call  is-decimal-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-is-decimal-digit-at-0"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    # eax = is-decimal-digit?(0x39)
+    # . . push args
+    68/push  0x39/imm32
+    # . . call
+    e8/call  is-decimal-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 1, msg)
+    # . . push args
+    68/push  "F - test-is-decimal-digit-at-9"/imm32
+    68/push  1/imm32/true
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    c3/return
+
+test-is-decimal-digit-above-9:
+    # eax = is-decimal-digit?(0x3a)
+    # . . push args
+    68/push  0x3a/imm32
+    # . . call
+    e8/call  is-decimal-digit?/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
+    # check-ints-equal(eax, 0, msg)
+    # . . push args
+    68/push  "F - test-is-decimal-digit-above-9"/imm32
+    68/push  0/imm32/false
+    50/push-eax
+    # . . call
+    e8/call  check-ints-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
+    c3/return
+
+to-decimal-digit:  # in: grapheme -> out/eax: int
+    # . prologue
+    55/push-ebp
+    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
+    # eax = in
+    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   8/disp8         .                 # copy *(ebp+8) to eax
+$to-decimal-digit:check0:
+    # if (eax < '0') goto abort
+    3d/compare-eax-with  0x30/imm32/0
+    7c/jump-if-<  $to-decimal-digit:abort/disp8
+$to-decimal-digit:check1:
+    # if (eax > '9') goto abort
+    3d/compare-eax-with  0x39/imm32/f
+    7f/jump-if->  $to-decimal-digit:abort/disp8
+$to-decimal-digit:digit:
+    # return eax - '0'
+    2d/subtract-from-eax  0x30/imm32/0
+$to-decimal-digit:end:
+    # . epilogue
+    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
+    5d/pop-to-ebp
+    c3/return
+
+$to-decimal-digit:abort:
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "to-decimal-digit: not a digit character" 3)  # 3=cyan
+    {
+      eb/jump loop/disp8
+    }
+    # never gets here
+
+# . . vim:nowrap:textwidth=0
diff --git a/baremetal/313index-bounds-check.subx b/baremetal/313index-bounds-check.subx
index 8a7700c6..6970e23a 100644
--- a/baremetal/313index-bounds-check.subx
+++ b/baremetal/313index-bounds-check.subx
@@ -26,7 +26,13 @@ __check-mu-array-bounds:  # index: int, elem-size: int, arr-size: int, function-
     39/compare %eax 1/r32/ecx
     0f 82/jump-if-unsigned< $__check-mu-array-bounds:end/disp32  # negative index should always abort
     # abort if necessary
-    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "offset too large for array" 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "fn " 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 *(ebp+0x14) 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 ": offset " 3)  # 3=cyan
+    (draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0 %eax 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 " is too large for array '" 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 *(ebp+0x18) 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "'" 3)  # 3=cyan
     {
       eb/jump loop/disp8
     }
@@ -42,7 +48,12 @@ $__check-mu-array-bounds:end:
     c3/return
 
 __check-mu-array-bounds:overflow:
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "fn " 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 *(ebp+0x14) 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 ": offset to array '" 3)  # 3=cyan
     (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "offset to array overflowed 32 bits" 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 *(ebp+0x18) 3)  # 3=cyan
+    (draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0 "' overflowed 32 bits" 3)  # 3=cyan
     {
       eb/jump loop/disp8
     }
diff --git a/baremetal/400.mu b/baremetal/400.mu
index 94820e68..4dfb9951 100644
--- a/baremetal/400.mu
+++ b/baremetal/400.mu
@@ -15,8 +15,17 @@ sig num-test-failures -> _/eax: int
 sig clear-stream f: (addr stream _)
 sig rewind-stream f: (addr stream _)
 sig write f: (addr stream byte), s: (addr array byte)
-sig append-byte f: (addr stream byte), n: int
 sig read-byte s: (addr stream byte) -> _/eax: byte
+sig append-byte f: (addr stream byte), n: int
+#sig to-hex-char in/eax: int -> out/eax: int
+sig append-byte-hex f: (addr stream byte), n: int
+sig write-int32-hex f: (addr stream byte), n: int
+sig write-int32-hex-bits f: (addr stream byte), n: int, bits: int
+sig is-hex-int? in: (addr slice) -> _/eax: boolean
+sig parse-hex-int in: (addr array byte) -> _/eax: int
+sig parse-hex-int-from-slice in: (addr slice) -> _/eax: int
+#sig parse-hex-int-helper start: (addr byte), end: (addr byte) -> _/eax: int
+sig is-hex-digit? c: byte -> _/eax: boolean
 #sig allocate ad: (addr allocation-descriptor), n: int, out: (addr handle _)
 #sig allocate-raw ad: (addr allocation-descriptor), n: int, out: (addr handle _)
 sig lookup h: (handle _T) -> _/eax: (addr _T)
@@ -26,4 +35,7 @@ sig copy-handle src: (handle _T), dest: (addr handle _T)
 #sig allocate-array ad: (addr allocation-descriptor), n: int, out: (addr handle _)
 sig copy-array ad: (addr allocation-descriptor), src: (addr array _T), out: (addr handle array _T)
 #sig zero-out start: (addr byte), size: int
+sig write-int32-decimal out: (addr stream byte), n: int
+sig is-decimal-digit? c: grapheme -> _/eax: boolean
+sig to-decimal-digit in: grapheme -> _/eax: int
 sig stream-empty? s: (addr stream _) -> _/eax: boolean
diff --git a/baremetal/501draw-text.mu b/baremetal/501draw-text.mu
index be8a06c5..5ab280db 100644
--- a/baremetal/501draw-text.mu
+++ b/baremetal/501draw-text.mu
@@ -184,6 +184,156 @@ fn draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen: (addr
   draw-text-wrapping-right-then-down-from-cursor screen, text, 0, 0, 0x400, 0x300, color  # 1024, 768
 }
 
+fn draw-int32-hex-wrapping-right-then-down screen: (addr screen), n: int, xmin: int, ymin: int, xmax: int, ymax: int, x: int, y: int, color: int -> _/eax: int, _/ecx: int {
+  var stream-storage: (stream byte 0x100)
+  var stream/esi: (addr stream byte) <- address stream-storage
+  write-int32-hex stream, n
+  # check if we have enough space
+  var xcurr/edx: int <- copy x
+  var ycurr/ecx: int <- copy y
+  {
+    compare ycurr, ymax
+    break-if->=
+    var g/eax: grapheme <- read-grapheme stream
+    compare g, 0xffffffff  # end-of-file
+    break-if-=
+    xcurr <- add 8  # font-width
+    compare xcurr, xmax
+    {
+      break-if-<
+      xcurr <- copy xmin
+      ycurr <- add 0x10  # font-height
+    }
+    loop
+  }
+  compare ycurr, ymax
+  {
+    break-if-<
+    return 0, 0
+  }
+  # we do; actually draw
+  rewind-stream stream
+  xcurr <- copy x
+  ycurr <- copy y
+  {
+    var g/eax: grapheme <- read-grapheme stream
+    compare g, 0xffffffff  # end-of-file
+    break-if-=
+    draw-grapheme screen, g, xcurr, ycurr, color
+    xcurr <- add 8  # font-width
+    compare xcurr, xmax
+    {
+      break-if-<
+      xcurr <- copy xmin
+      ycurr <- add 0x10  # font-height
+    }
+    loop
+  }
+  set-cursor-position screen, xcurr, ycurr
+  return xcurr, ycurr
+}
+
+fn draw-int32-hex-wrapping-right-then-down-over-full-screen screen: (addr screen), n: int, x: int, y: int, color: int -> _/eax: int, _/ecx: int {
+  var cursor-x/eax: int <- copy 0
+  var cursor-y/ecx: int <- copy 0
+  cursor-x, cursor-y <- draw-int32-hex-wrapping-right-then-down screen, n, 0, 0, 0x400, 0x300, x, y, color  # 1024, 768
+  return cursor-x, cursor-y
+}
+
+fn draw-int32-hex-wrapping-right-then-down-from-cursor screen: (addr screen), n: int, xmin: int, ymin: int, xmax: int, ymax: int, color: int {
+  var cursor-x/eax: int <- copy 0
+  var cursor-y/ecx: int <- copy 0
+  cursor-x, cursor-y <- cursor-position screen
+  var end-x/edx: int <- copy cursor-x
+  end-x <- add 8  # font-width
+  compare end-x, xmax
+  {
+    break-if-<
+    cursor-x <- copy xmin
+    cursor-y <- add 0x10  # font-height
+  }
+  cursor-x, cursor-y <- draw-int32-hex-wrapping-right-then-down screen, n, xmin, ymin, xmax, ymax, cursor-x, cursor-y, color
+}
+
+fn draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen screen: (addr screen), n: int, color: int {
+  draw-int32-hex-wrapping-right-then-down-from-cursor screen, n, 0, 0, 0x400, 0x300, color  # 1024, 768
+}
+
+fn draw-int32-decimal-wrapping-right-then-down screen: (addr screen), n: int, xmin: int, ymin: int, xmax: int, ymax: int, x: int, y: int, color: int -> _/eax: int, _/ecx: int {
+  var stream-storage: (stream byte 0x100)
+  var stream/esi: (addr stream byte) <- address stream-storage
+  write-int32-decimal stream, n
+  # check if we have enough space
+  var xcurr/edx: int <- copy x
+  var ycurr/ecx: int <- copy y
+  {
+    compare ycurr, ymax
+    break-if->=
+    var g/eax: grapheme <- read-grapheme stream
+    compare g, 0xffffffff  # end-of-file
+    break-if-=
+    xcurr <- add 8  # font-width
+    compare xcurr, xmax
+    {
+      break-if-<
+      xcurr <- copy xmin
+      ycurr <- add 0x10  # font-height
+    }
+    loop
+  }
+  compare ycurr, ymax
+  {
+    break-if-<
+    return 0, 0
+  }
+  # we do; actually draw
+  rewind-stream stream
+  xcurr <- copy x
+  ycurr <- copy y
+  {
+    var g/eax: grapheme <- read-grapheme stream
+    compare g, 0xffffffff  # end-of-file
+    break-if-=
+    draw-grapheme screen, g, xcurr, ycurr, color
+    xcurr <- add 8  # font-width
+    compare xcurr, xmax
+    {
+      break-if-<
+      xcurr <- copy xmin
+      ycurr <- add 0x10  # font-height
+    }
+    loop
+  }
+  set-cursor-position screen, xcurr, ycurr
+  return xcurr, ycurr
+}
+
+fn draw-int32-decimal-wrapping-right-then-down-over-full-screen screen: (addr screen), n: int, x: int, y: int, color: int -> _/eax: int, _/ecx: int {
+  var cursor-x/eax: int <- copy 0
+  var cursor-y/ecx: int <- copy 0
+  cursor-x, cursor-y <- draw-int32-decimal-wrapping-right-then-down screen, n, 0, 0, 0x400, 0x300, x, y, color  # 1024, 768
+  return cursor-x, cursor-y
+}
+
+fn draw-int32-decimal-wrapping-right-then-down-from-cursor screen: (addr screen), n: int, xmin: int, ymin: int, xmax: int, ymax: int, color: int {
+  var cursor-x/eax: int <- copy 0
+  var cursor-y/ecx: int <- copy 0
+  cursor-x, cursor-y <- cursor-position screen
+  var end-x/edx: int <- copy cursor-x
+  end-x <- add 8  # font-width
+  compare end-x, xmax
+  {
+    break-if-<
+    cursor-x <- copy xmin
+    cursor-y <- add 0x10  # font-height
+  }
+  cursor-x, cursor-y <- draw-int32-decimal-wrapping-right-then-down screen, n, xmin, ymin, xmax, ymax, cursor-x, cursor-y, color
+}
+
+fn draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen: (addr screen), n: int, color: int {
+  draw-int32-decimal-wrapping-right-then-down-from-cursor screen, n, 0, 0, 0x400, 0x300, color  # 1024, 768
+}
+
 ## Text direction: down then right
 
 # draw a single line of text vertically from x, y to ymax
diff --git a/baremetal/mu-init.subx b/baremetal/mu-init.subx
index 5bf4a3bb..3c52a0b7 100644
--- a/baremetal/mu-init.subx
+++ b/baremetal/mu-init.subx
@@ -10,6 +10,8 @@
 bd/copy-to-ebp 0/imm32
 # always first run tests
 (run-tests)
+c7 0/subop/copy *Default-next-x 0/imm32
+c7 0/subop/copy *Default-next-y 0/imm32
 (num-test-failures)  # => eax
 # call main if tests all passed
 {