about summary refs log tree commit diff stats
path: root/baremetal
diff options
context:
space:
mode:
Diffstat (limited to 'baremetal')
-rw-r--r--baremetal/README.md2
-rw-r--r--baremetal/boot.bochsrc6
-rw-r--r--baremetal/boot0.hex346
-rw-r--r--baremetal/bos/3-1.hex51
-rw-r--r--baremetal/bos/3-2.hex65
-rw-r--r--baremetal/bos/32bit.hex132
-rw-r--r--baremetal/bos/4.asm143
-rw-r--r--baremetal/bos/4.xxd87
-rw-r--r--baremetal/bos/README.md7
-rw-r--r--baremetal/bos/bochsrc3
-rw-r--r--baremetal/bos/print-mem-real-mode.hex67
11 files changed, 909 insertions, 0 deletions
diff --git a/baremetal/README.md b/baremetal/README.md
new file mode 100644
index 00000000..af45760c
--- /dev/null
+++ b/baremetal/README.md
@@ -0,0 +1,2 @@
+Some apps written in SubX and Mu. Where the rest of this repo relies on a few
+Linux syscalls, the apps in this subdirectory interface directly with hardware.
diff --git a/baremetal/boot.bochsrc b/baremetal/boot.bochsrc
new file mode 100644
index 00000000..521e22d9
--- /dev/null
+++ b/baremetal/boot.bochsrc
@@ -0,0 +1,6 @@
+# Configuration for the Bochs x86 CPU emulator to run the output of apps/boot.hex
+# See apps/boot.hex for more details.
+
+ata0-master: type=disk, path="disk.img", mode=flat, cylinders=20, heads=16, spt=63  # 10MB, 512 bytes per sector
+boot: disk
+log: -
diff --git a/baremetal/boot0.hex b/baremetal/boot0.hex
new file mode 100644
index 00000000..793b2b9f
--- /dev/null
+++ b/baremetal/boot0.hex
@@ -0,0 +1,346 @@
+# A minimal bootable image that:
+#   - loads more sectors past the first boot sector (using BIOS primitives)
+#   - switches to 32-bit mode (giving up access to BIOS primitives)
+#   - sets up a handler for keyboard events
+#   - as an example program, prints '1' to the top-left position on screen (by writing to memory-mapped VGA memory) when the '1' key is typed
+#
+# When it's ready to accept keys, it prints 'H' to the top-left of the screen.
+#
+# If the initial load fails, it prints 'D' to the top-left of the screen and
+# halts.
+#
+# To convert to a disk image, first prepare a realistically sized disk image:
+#   dd if=/dev/zero of=disk.img count=20160  # 512-byte sectors, so 10MB
+# Now fill in sectors:
+#   ./bootstrap run apps/hex < baremetal/boot0.hex > boot.bin
+#   dd if=boot.bin of=disk.img conv=notrunc
+# To run:
+#   qemu-system-i386 disk.img
+# Or:
+#   bochs -f baremetal/boot.bochsrc  # boot.bochsrc loads disk.img
+#
+# Since we start out in 16-bit mode, we need instructions SubX doesn't
+# support.
+# This file contains just lowercase hex bytes and comments. Zero
+# error-checking. Make liberal use of:
+#   - comments documenting expected offsets
+#   - size checks on the emitted file (currently: 512 bytes)
+#   - xxd to eyeball that offsets contain expected bytes
+
+## 16-bit entry point
+
+# Upon reset, the IBM PC
+#   loads the first sector (512 bytes)
+#   from some bootable image (see the boot sector marker at the end of this file)
+#   to the address range [0x7c00, 0x7e00)
+
+# offset 00 (address 0x7c00):
+  # disable interrupts for this initialization
+  fa  # cli
+
+  # initialize segment registers
+  # this isn't always needed, but the recommendation is to not make assumptions
+  b8 00 00  # ax <- 0
+  8e d8  # ds <- ax
+  8e d0  # ss <- ax
+  8e c0  # es <- ax
+  8e e0  # fs <- ax
+  8e e8  # gs <- ax
+
+  # We don't read or write the stack before we get to 32-bit mode. No function
+  # calls, so we don't need to initialize the stack.
+
+# 0e:
+  # load more sectors from disk
+  b4 02  # ah <- 2  # read sectors from disk
+  # dl comes conveniently initialized at boot time with the index of the device being booted
+  b5 00  # ch <- 0  # cylinder 0
+  b6 00  # dh <- 0  # track 0
+  b1 02  # cl <- 2  # second sector, 1-based
+  b0 01  # al <- 1  # number of sectors to read
+  # address to write sectors to = es:bx = 0x7e00, contiguous with boot segment
+  bb 00 00  # bx <- 0
+  8e c3  # es <- bx
+  bb 00 7e  # bx <- 0x7e00
+  cd 13  # int 13h, BIOS disk service
+  0f 82 76 00  # jump-if-carry disk-error
+
+# 26:
+  # undo the A20 hack: https://en.wikipedia.org/wiki/A20_line
+  # this is from https://github.com/mit-pdos/xv6-public/blob/master/bootasm.S
+  # seta20.1:
+  e4 64  # al <- port 0x64
+  a8 02  # set zf if bit 1 (second-least significant) is not set
+  75 fa  # if zf not set, goto seta20.1 (-6)
+
+  b0 d1  # al <- 0xd1
+  e6 64  # port 0x64 <- al
+
+# 30:
+  # seta20.2:
+  e4 64  # al <- port 0x64
+  a8 02  # set zf if bit 1 (second-least significant) is not set
+  75 fa  # if zf not set, goto seta20.2 (-6)
+
+  b0 df  # al <- 0xdf
+  e6 64  # port 0x64 <- al
+
+# 3a:
+  # switch to 32-bit mode
+  0f 01 16  # lgdt 00/mod/indirect 010/subop 110/rm/use-disp16
+    80 7c  # *gdt_descriptor
+# 3f:
+  0f 20 c0  # eax <- cr0
+  66 83 c8 01  # eax <- or 0x1
+  0f 22 c0  # cr0 <- eax
+  ea c0 7c 08 00  # far jump to initialize_32bit_mode after setting cs to the record at offset 8 in the gdt (gdt_code)
+
+# padding
+# 4e:
+                                          00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+## GDT: 3 records of 8 bytes each
+
+# 60:
+# gdt_start:
+# gdt_null:  mandatory null descriptor
+  00 00 00 00 00 00 00 00
+# gdt_code:  (offset 8 from gdt_start)
+  ff ff  # limit[0:16]
+  00 00 00  # base[0:24]
+  9a  # 1/present 00/privilege 1/descriptor type = 1001b
+      # 1/code 0/conforming 1/readable 0/accessed = 1010b
+  cf  # 1/granularity 1/32-bit 0/64-bit-segment 0/AVL = 1100b
+      # limit[16:20] = 1111b
+  00  # base[24:32]
+# gdt_data:  (offset 16 from gdt_start)
+  ff ff  # limit[0:16]
+  00 00 00  # base[0:24]
+  92  # 1/present 00/privilege 1/descriptor type = 1001b
+      # 0/data 0/conforming 1/readable 0/accessed = 0010b
+  cf  # same as gdt_code
+  00  # base[24:32]
+# gdt_end:
+
+# padding
+# 78:
+                        00 00 00 00 00 00 00 00
+
+# 80:
+# gdt_descriptor:
+  17 00  # final index of gdt = gdt_end - gdt_start - 1
+  60 7c 00 00  # start = gdt_start
+
+# padding
+# 85:
+                  00 00 00 00 00 00 00 00 00 00
+
+# 90:
+# disk_error:
+  # print 'D' to top-left of screen to indicate disk error
+  # *0xb8000 <- 0x0f44
+  # bx <- 0xb800
+  bb 00 b8
+  # ds <- bx
+  8e db  # 11b/mod 011b/reg/ds 011b/rm/bx
+  # al <- 'D'
+  b0 44
+  # ah <- 0x0f  # white on black
+  b4 0f
+  # bx <- 0
+  bb 00 00
+  # *ds:bx <- ax
+  89 07  # 00b/mod/indirect 000b/reg/ax 111b/rm/bx
+
+e9 fb ff  # loop forever
+
+# padding
+# a1:
+   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+## 32-bit code from this point (still some instructions not in SubX)
+
+# c0:
+# initialize_32bit_mode:
+  66 b8 10 00  # ax <- offset 16 from gdt_start
+  8e d8  # ds <- ax
+  8e d0  # ss <- ax
+  8e c0  # es <- ax
+  8e e0  # fs <- ax
+  8e e8  # gs <- ax
+
+  # load interrupt handlers
+  0f 01 1d  # lidt 00/mod/indirect 011/subop 101/rm32/use-disp32
+    00 7f 00 00  # *idt_descriptor
+
+  # enable keyboard IRQ
+  b0 fd  # al <- 0xfd  # enable just IRQ1
+  e6 21  # port 0x21 <- al
+
+  # initialization is done; enable interrupts
+  fb
+  e9 21 00 00 00  # jump to 0x7d00
+
+# padding
+# df:
+                                             00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+## 'application' SubX code: print one character to top-left of screen
+
+# offset 100 (address 0x7d00):
+# Entry:
+  # eax <- *0x7ff4  # random address in second segment containing 'H'
+  8b  # copy rm32 to r32
+    05  # 00/mod/indirect 000/r32/eax 101/rm32/use-disp32
+    # disp32
+    f4 7f 00 00
+  # *0xb8000 <- eax
+  89  # copy r32 to rm32
+    05  # 00/mod/indirect 000/r32/eax 101/rm32/use-disp32
+    # disp32
+    00 80 0b 00
+
+e9 fb ff ff ff  # loop forever
+
+# padding
+# 111:
+   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+# 120:
+# null interrupt handler:
+  cf  # iret
+
+# padding
+# 121:
+   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+# 130:
+# keyboard interrupt handler:
+  # prologue
+  fa  # disable interrupts
+  60  # push all registers to stack
+  # acknowledge interrupt
+  b0 20  # al <- 0x20
+  e6 20  # port 0x20 <- al
+  # read keyboard status (TODO: why bit 0? Doesn't line up with https://web.archive.org/web/20040604041507/http://panda.cs.ndsu.nodak.edu/~achapwes/PICmicro/keyboard/atkeyboard.html)
+#?   e4 64  # al <- port 0x64
+#?   a8 01  # set zf if bit 0 (least significant) is not set
+#?   74 11  # if bit 0 is not set, skip to epilogue
+  # read keycode into eax
+  31 c0  # eax <- xor eax;  11/direct 000/r32/eax 000/rm32/eax
+  e4 60  # al <- port 0x60
+  # map key '1' to ascii; if eax == 2, eax = 0x31
+  3d 02 00 00 00  # compare eax with 0x02
+  75 0b  # if not equal, goto epilogue
+  b8 31 0f 00 00  # eax <- 0x0f31
+  # print eax to top-left of screen (*0xb8000)
+  89  # copy r32 to rm32
+    05  # 00/mod/indirect 000/r32/eax 101/rm32/use-disp32
+    # disp32
+    00 80 0b 00
+  # epilogue
+  61  # pop all registers
+  fb  # enable interrupts
+  cf  # iret
+
+# padding
+# 14f
+                                             00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+# final 2 bytes of boot sector
+55 aa
+
+## sector 2
+# loaded by load_disk, not automatically on boot
+
+# offset 200 (address 0x7e00): interrupt descriptor table
+# 32 entries * 8 bytes each = 256 bytes (0x100)
+# idt_start:
+
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+
+# entry 8: clock
+  20 7d  # target[0:16] = null interrupt handler
+  08 00  # segment selector (gdt_code)
+  00  # unused
+  8e  # 1/p 00/dpl 0 1110/type/32-bit-interrupt-gate
+  00 00  # target[16:32]
+
+# entry 9: keyboard
+  30 7d  # target[0:16] = keyboard interrupt handler
+  08 00  # segment selector (gdt_code)
+  00  # unused
+  8e  # 1/p 00/dpl 0 1110/type/32-bit-interrupt-gate
+  00 00  # target[16:32]
+
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+# idt_end:
+
+# offset 300 (address 0x7f00):
+# idt_descriptor:
+  ff 00  # idt_end - idt_start - 1
+  00 7e 00 00  # start = idt_start
+
+# padding
+                  00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 48 0f 00 00 00 00 00 00 00 00 00 00  # spot the 'H' with attributes
+# offset 400 (address 0x8000)
+
+# vim:ft=conf
diff --git a/baremetal/bos/3-1.hex b/baremetal/bos/3-1.hex
new file mode 100644
index 00000000..bf4ef208
--- /dev/null
+++ b/baremetal/bos/3-1.hex
@@ -0,0 +1,51 @@
+# Bootable image.
+# Boot sector must have exactly 512 bytes.
+# 16-bit real mode.
+#
+# To convert to a disk image:
+#   ./bootstrap run apps/hex < baremetal/bos/3-1.hex > boot.bin
+# To run:
+#   qemu-system-i386 boot.bin
+# Or:
+#   bochs -f baremetal/bos/bochsrc  # bochsrc loads boot.bin
+#
+# Expected output inside emulator:
+#   Booting from floppy...
+
+e9 fd ff
+
+# padding to 512 bytes
+         00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa
+
+# vim:ft=subx
diff --git a/baremetal/bos/3-2.hex b/baremetal/bos/3-2.hex
new file mode 100644
index 00000000..85792e26
--- /dev/null
+++ b/baremetal/bos/3-2.hex
@@ -0,0 +1,65 @@
+# Bootable image.
+# Boot sector must have exactly 512 bytes.
+# 16-bit real mode.
+#
+# To convert to a disk image:
+#   ./bootstrap run apps/hex < baremetal/bos/3-2.hex > boot.bin
+# To run:
+#   qemu-system-i386 boot.bin
+# Or:
+#   bochs -f baremetal/bos/bochsrc  # bochsrc loads boot.bin
+#
+# Expected output inside emulator:
+#   Hello
+
+b4 0e  # ah <- 0e (teletype output)
+
+b0 48  # al <- 'H'
+cd 10  # int 10h
+b0 65  # al <- 'e'
+cd 10  # int 10h
+b0 6c  # al <- 'l'
+cd 10  # int 10h
+b0 6c  # al <- 'l'
+cd 10  # int 10h
+b0 6f  # al <- 'o'
+cd 10  # int 10h
+
+e9 fd ff  # loop forever
+
+# padding to 512 bytes
+                           00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+55 aa  # final 2 bytes: boot sector marker
+
+# vim:ft=subx
diff --git a/baremetal/bos/32bit.hex b/baremetal/bos/32bit.hex
new file mode 100644
index 00000000..5a85aa36
--- /dev/null
+++ b/baremetal/bos/32bit.hex
@@ -0,0 +1,132 @@
+# Bootable image demonstrating printing to screen in 32-bit mode.
+# Must have exactly 512 bytes.
+#
+# To convert to a disk image:
+#   ./bootstrap run apps/hex < baremetal/bos/32bit.hex > boot.bin
+# To run:
+#   qemu-system-i386 boot.bin
+# Or:
+#   bochs -f baremetal/bos/bochsrc  # bochsrc loads boot.bin
+#
+# Expected output inside emulator:
+#   H
+
+## 16-bit entry point
+
+# Boot image starts executing at address 0x7c00,
+# and so occupies [0x7c00, 0x7e00).
+# We don't read or write the stack before we get to 32-bit mode.
+
+# 00:
+  fa  # cli  # TODO: don't forget to reenable interrupts at some point
+  0f 01 16  # lgdt 00/mod/indirect 010/subop 110/rm32/TODO
+    80 7c  # *gdt_descriptor
+  0f 20 c0  # eax <- cr0
+  66 83 c8 01  # eax <- or 0x1
+  0f 22 c0  # cr0 <- eax
+  ea c0 7c 08 00  # far jump to initialize_32bit_mode after setting cs to the record at offset 8 in the gdt (gdt_code)
+
+# padding
+# 15:
+               00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+## GDT: 3 records of 8 bytes each
+
+# 60:
+# gdt_start:
+# gdt_null:  mandatory null descriptor
+  00 00 00 00 00 00 00 00
+# gdt_code:  (offset 8 from gdt_start)
+  ff ff  # limit[0:16]
+  00 00 00  # base[0:24]
+  9a  # 1/present 00/privilege 1/descriptor type = 1001b
+      # 1/code 0/conforming 1/readable 0/accessed = 1010b
+  cf  # 1/granularity 1/32-bit 0/64-bit-segment 0/AVL = 1100b
+      # limit[16:20] = 1111b
+  00  # base[24:32]
+# gdt_data:  (offset 16 from gdt_start)
+  ff ff  # limit[0:16]
+  00 00 00  # base[0:24]
+  92  # 1/present 00/privilege 1/descriptor type = 1001b
+      # 0/data 0/conforming 1/readable 0/accessed = 0010b
+  cf  # same as gdt_code
+  00  # base[24:32]
+# gdt_end:
+
+# padding
+# 78:
+                        00 00 00 00 00 00 00 00
+
+# 80:
+# gdt_descriptor:
+  17 00  # final index of gdt = gdt_end - gdt_start - 1
+  60 7c 00 00  # start = gdt_start
+
+# padding
+# 85:
+                  00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+## 32-bit code from this point (still some instructions not in SubX)
+
+# c0:
+# initialize_32bit_mode:
+  66 b8 10 00  # ax <- offset 16 from gdt_start
+  8e d8  # ds <- ax
+  8e d0  # ss <- ax
+  8e c0  # es <- ax
+  8e e0  # fs <- ax
+  8e e8  # gs <- ax
+  e9 1d 00 00 00  # jump to 0x7cf0
+
+# padding
+# d3:
+         00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+## 'application' SubX code: print one character to top-left of screen
+
+# f0:
+# Entry:
+  # *0xb8000 <- 0x0f48
+  c7  # opcode
+    # modrm
+    05  # 00/mod/indirect 000/subop/copy 101/rm32/use-disp32
+    # disp32
+    00 80 0b 00
+    # imm32
+    48  # 'H'
+    0f  # white on black
+    00 00
+
+e9 fb ff ff ff  # loop forever
+
+# padding to 512 bytes
+                                             00
+# 100:
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+55 aa  # final 2 bytes: boot sector marker
+
+# vim:ft=subx
diff --git a/baremetal/bos/4.asm b/baremetal/bos/4.asm
new file mode 100644
index 00000000..71c51d97
--- /dev/null
+++ b/baremetal/bos/4.asm
@@ -0,0 +1,143 @@
+; A boot sector that enters 32-bit protected mode.
+;
+; To convert to a disk image:
+;   cd apps/bos
+;   nasm 4.asm -f bin -o boot.bin
+; To run:
+;   qemu-system-i386 boot.bin
+; Or:
+;   bochs  # bochsrc loads boot.bin
+
+[org 0x7c00]
+
+  mov bp, 0x9000
+  mov sp, bp
+
+  mov bx, MSG_REAL_MODE
+  call print_string
+
+  call switch_to_pm
+  ; never gets here
+
+  jmp $
+
+switch_to_pm:
+  cli
+  lgdt [gdt_descriptor]
+  ; set LSB of CR0
+  mov eax, cr0
+  or eax, 0x1
+  mov cr0, eax
+  jmp CODE_SEG:init_pm  ; Far jump to a new segment containing 32-bit code.
+                        ; This also forces the CPU to flush its cache of
+                        ; prefetched and real-mode decoded instructions.
+  ; never gets here
+
+;; === GDT stuff
+gdt_start:
+
+gdt_null:  ; the mandatory null descriptor
+  dd 0x0  ; ’dd’ means define double word (i.e. 4 bytes)
+  dd 0x0
+
+gdt_code: ; the code segment descriptor
+  ; base=0x0, limit=0xfffff,
+  ; 1st flags: (present)1 (privilege)00 (descriptor type)1 -> 1001b
+  ; type flags: (code)1 (conforming)0 (readable)1 (accessed )0 -> 1010b
+  ; 2nd flags: (granularity)1 (32-bit default)1 (64-bit seg)0 (AVL)0 -> 1100b
+  dw 0xffff     ; Limit (bits 0-15)
+  dw 0x0        ; Base (bits 0-15)
+  db 0x0        ; Base (bits 16 -23)
+  db 10011010b  ; 1st flags, type flags
+  db 11001111b  ; 2nd flags, Limit (bits 16-19)
+  db 0x0        ; Base (bits 24 -31)
+
+gdt_data: ; the  data segment descriptor
+  ; Same as code segment except for the type flags:
+  ; type flags: (code)0 (expand down)0 (writable)1 (accessed)0 -> 0010b
+  dw 0xffff    ; Limit (bits 0-15)
+  dw 0x0       ; Base (bits 0-15)
+  db 0x0       ; Base (bits 16 -23)
+  db 10010010b ; 1st flags, type flags
+  db 11001111b ; 2nd flags, Limit (bits 16-19)
+  db 0x0       ; Base (bits 24 -31)
+
+gdt_end:
+
+gdt_descriptor:
+  dw gdt_end - gdt_start - 1  ; last valid byte
+  dd gdt_start
+
+; some handy constants
+CODE_SEG equ gdt_code - gdt_start
+DATA_SEG equ gdt_data - gdt_start
+;; === end GDT stuff
+
+;; some 16-bit helpers
+
+; print string whose address is in bx
+print_string:
+  pusha
+  mov ah, 0x0e
+loop:
+  mov al, [bx]
+  int 0x10
+  add bx, 1
+  cmp al, 0
+  jne loop
+  popa
+  ret
+
+;; 32-bit code starts here
+[bits  32]
+
+; Initialise registers and the stack once in PM.
+init_pm:
+
+  mov ax, DATA_SEG        ; Now in PM, our old segments are meaningless,
+  mov ds, ax              ; so we point our segment registers to the
+  mov ss, ax              ; data selector we defined in our GDT
+  mov es, ax
+  mov fs, ax
+  mov gs, ax
+
+  mov ebp, 0x90000        ; Update our stack position so it is right
+  mov esp, ebp            ; at the top of the free space.
+
+  ;; Protected Mode is now initialized
+
+  mov ebx, MSG_PROT_MODE
+  call print_string_pm
+
+  ; all done; hang
+  jmp $
+
+; Define  some  constants
+VIDEO_MEMORY  equ 0xb8000
+WHITE_ON_BLACK  equ 0x0f
+
+; prints a null -terminated  string  pointed  to by EDX
+print_string_pm:
+  pusha
+  mov edx, VIDEO_MEMORY  ; Set  edx to the  start  of vid  mem.
+print_string_pm_loop:
+  mov al, [ebx]          ; Store  the  char at EBX in AL
+  mov ah, WHITE_ON_BLACK ; Store  the  attributes  in AH
+  cmp al, 0         ; if (al == 0), at end of string , so
+  je print_string_pm_done
+  mov [edx], ax     ; Store  char  and  attributes  at  current
+                    ; character  cell.
+  add ebx , 1       ; Increment  EBX to the  next  char in  string.
+  add edx , 2       ; Move to next  character  cell in vid  mem.
+  jmp  print_string_pm_loop   ; loop  around  to  print  the  next  char.
+print_string_pm_done:
+  popa
+  ret               ; Return  from  the  function
+
+; Global variables
+MSG_REAL_MODE   db "Started in 16-bit Real Mode", 0
+MSG_PROT_MODE   db "Successfully landed in 32-bit Protected Mode", 0
+
+; Bootsector padding
+times 510-($-$$) db 0
+dw 0xaa55
diff --git a/baremetal/bos/4.xxd b/baremetal/bos/4.xxd
new file mode 100644
index 00000000..be677134
--- /dev/null
+++ b/baremetal/bos/4.xxd
@@ -0,0 +1,87 @@
+## 16-bit code
+
+# Entry:
+00:
+  bd 00 90  # bp <- 0x9000
+  89 ec  # sp <- bp
+
+  bb 8f 7c  # bx <- MSG_REAL_MODE
+  e8 38 00  # call print_string
+
+  e8 02 00  # call switch_to_pm
+  eb fe  # jump $  (should never get here)
+
+# switch_to_pm:
+10:
+  fa  # cli
+  0f 01 16 3d 7c  # lgdt [gdt_descriptor]
+  0f 20 c0  # eax <- cr0
+  66 83 c8 01  # eax <- or 0x1
+  0f 22 c0  # cr0 <- eax
+20:
+  ea 53 7c 08 00  # jmp CODE_SEG:init_pm
+
+# gdt_start:
+# gdt_null:  mandatory null descriptor
+  00 00 00 00 00 00 00 00
+# gdt_code:
+  ff ff 00
+30:
+  00 00 9a cf 00
+# gdt_data:
+  ff ff 00 00 00 92 cf 00
+# gdt_end:
+
+# gdt_descriptor:
+3d:
+  17 00  # limit
+  25 7c 00 00  # start
+
+# print_string:
+43:
+  60  # pusha
+  b4 0e  # ah <- 0x0e
+# loop:
+  8a 07  # al <- *bx
+  cd 10  # int 10h
+  83 c3 01  # add bx, 1
+  3c 00  # cmp al, 0
+  75 f5  # jne loop
+  61  # popa
+  c3  # ret
+
+## 32-bit code
+
+# init_pm:
+53:
+  66 b8 10 00  # ax <- DATA_SEG
+  8e d8  # ds <- ax
+  8e d0  # ss <- ax
+  8e c0  # es <- ax
+  8e e0  # fs <- ax
+  8e e8  # gs <- ax
+61:
+  bd 00 00 09 00  # ebp <- 0x90000
+  89 ec  # esp <- ebp
+  bb ab 7c 00 00  # ebx <- MSG_PROT_MODE
+  e8 02 00 00 00  # call print_string_pm
+  eb fe  # hang
+
+# print_string_pm:
+74:
+  60  # pusha
+  ba 00 80 0b 00 8a 03 b4 0f 3c 00
+80:
+  74 0b 66 89 02 83 c3 01 83 c2 02 eb ed 61 c3 53
+90:
+  74 61 72 74 65 64 20 69 6e 20 31 36 2d 62 69 74
+a0:
+  20 52 65 61 6c 20 4d 6f 64 65 00 53 75 63 63 65
+b0:
+  73 73 66 75 6c 6c 79 20 6c 61 6e 64 65 64 20 69
+c0:
+  6e 20 33 32 2d 62 69 74 20 50 72 6f 74 65 63 74
+d0:
+  65 64 20 4d 6f 64 65
+
+# vim:ft=conf
diff --git a/baremetal/bos/README.md b/baremetal/bos/README.md
new file mode 100644
index 00000000..989ebf44
--- /dev/null
+++ b/baremetal/bos/README.md
@@ -0,0 +1,7 @@
+Exercises while reading the incomplete "Writing a simple operating system from
+scratch" by Nick Blundell.
+  https://www.cs.bham.ac.uk/~exr/lectures/opsys/10\_11/lectures/os-dev.pdf
+
+The name of this directory is a reference to "Bootstrapping a simple compiler
+from nothing" by Edmund Grimley-Evans.
+  https://github.com/certik/bcompiler
diff --git a/baremetal/bos/bochsrc b/baremetal/bos/bochsrc
new file mode 100644
index 00000000..de225b4c
--- /dev/null
+++ b/baremetal/bos/bochsrc
@@ -0,0 +1,3 @@
+ata0-master: type=disk, path="boot.bin", mode=flat, cylinders=1, heads=1, spt=1
+boot: disk
+log: -
diff --git a/baremetal/bos/print-mem-real-mode.hex b/baremetal/bos/print-mem-real-mode.hex
new file mode 100644
index 00000000..3610308c
--- /dev/null
+++ b/baremetal/bos/print-mem-real-mode.hex
@@ -0,0 +1,67 @@
+# Experiment: write to video RAM from 16-bit real mode. And it works.
+#
+# To convert to a disk image:
+#   ./bootstrap run apps/hex < baremetal/bos/print-mem-real-mode.hex > boot.bin
+# To run:
+#   qemu-system-i386 boot.bin
+# Or:
+#   bochs  # reads bochsrc, which loads boot.bin
+
+# - figure 4.1 of https://www.cs.bham.ac.uk/~exr/lectures/opsys/10_11/lectures/os-dev.pdf in protected mode
+# edx <- 0xb8000
+# al <- 'H'
+# ah <- 0x0f  # white on black
+# *edx <- ax
+
+# - translating to real mode
+# bx <- 0xb800
+bb 00 b8
+# ds <- bx
+8e db  # 11b/mod 011b/reg/ds 011b/rm/bx
+# al <- 'H'
+b0 48
+# ah <- 0x0f  # white on black
+b4 0f
+# bx <- 0
+bb 00 00
+# *ds:bx <- ax
+89 07  # 00b/mod/indirect 000b/reg/ax 111b/rm/bx
+
+e9 fd ff  # loop forever
+
+# padding to 512 bytes
+   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00 00 00 00 00 00 00
+
+55 aa  # final 2 bytes: boot sector marker
+
+# vim:ft=subx