https://github.com/akkartik/mu/blob/main/baremetal/boot.hex
   1 # Code for the first few disk sectors that all programs in this directory need:
   2 #   - load sectors past the first (using BIOS primitives) since only the first is available by default
   3 #     - if this fails, print 'D' at top-left of screen and halt
   4 #   - initialize a minimal graphics mode
   5 #   - switch to 32-bit mode (giving up access to BIOS primitives)
   6 #   - set up a handler for keyboard events
   7 #   - jump to start of program
   8 #
   9 # To convert to a disk image, first prepare a realistically sized disk image:
  10 #   dd if=/dev/zero of=disk.img count=20160  # 512-byte sectors, so 10MB
  11 # Create initial sectors from this file:
  12 #   ./bootstrap run apps/hex < baremetal/boot.hex > boot.bin
  13 # Translate other sectors into a file called a.img
  14 # Load all sectors into the disk image:
  15 #   cat boot.bin a.img > disk.bin
  16 #   dd if=disk.bin of=disk.img conv=notrunc
  17 # To run:
  18 #   qemu-system-i386 disk.img
  19 # Or:
  20 #   bochs -f baremetal/boot.bochsrc  # boot.bochsrc loads disk.img
  21 #
  22 # Since we start out in 16-bit mode, we need instructions SubX doesn't
  23 # support.
  24 # This file contains just lowercase hex bytes and comments. Programming it
  25 # requires liberal use of:
  26 #   - comments documenting expected offsets
  27 #   - size checks on the emitted file (currently: 6144 bytes)
  28 #   - xxd to spot-check contents of specific offsets in the generated output
  29 #
  30 # Programs using this initialization:
  31 #   - can't use any syscalls
  32 #   - can't print text to video memory (past these boot sectors)
  33 #   - must only print raw pixels (256 colors) to video memory (resolution 1024x768)
  34 #   - must start executing immediately after this file (see outline below)
  35 #
  36 # Don't panic! This file doesn't contain any loops or function calls. 80% of
  37 # it is data. One pass through less than 1KB of code (there's lots of
  38 # padding), and then we jump into a better notation. The rest of the stack
  39 # (really only in a couple of slightly higher-level places) needs to know just
  40 # a few magic constants:
  41 #   Video memory: start is stored at 0x8128
  42 #   Keyboard buffer: starts at 0x8028
  43 #
  44 # No mouse support. _That_ would require panicking.
  45 
  46 # Outline of this file with offsets and the addresses they map to at run-time:
  47 # -- 16-bit mode code
  48 #   offset    0 (address 7c00): boot code
  49 # -- 16-bit mode data
  50 #            e0 (address 7c80) global descriptor table
  51 #            f8 (address 7ca0) <== gdt_descriptor
  52 # -- 32-bit mode code
  53 #   offset  100 (address 7d00): boot code
  54 #           1fe (address 7dfe) boot sector marker (2 bytes)
  55 #   offset  200 (address 7e00): interrupt handler code
  56 # -- 32-bit mode data
  57 #   offset  400 (address 8000): handler data
  58 #           410 (address 8010): keyboard handler data
  59 #           428 (address 8028) <== keyboard buffer
  60 #   offset  500 (address 8100): video mode data (256 bytes)
  61 #           528 (address 8128) <== start of video RAM stored here
  62 #   offset  600 (address 8200): interrupt descriptor table (1KB)
  63 #   offset  a00 (address 8600): keyboard mappings (1.5KB)
  64 #   offset 1000 (address 8c00): bitmap font (2KB)
  65 #   offset 1800 (address 9400): entrypoint for applications (don't forget to adjust survey_baremetal if this changes)
  66 
  67 # Other details of the current memory map:
  68 #   code: first two default-sized disk tracks get loaded to [0x00007c00, 0x00017800)
  69 #   stack grows down from 0x00070000
  70 #     see below
  71 #   heap: [0x01000000, 0x02000000)
  72 #     see baremetal/120allocate.subx
  73 # Consult https://wiki.osdev.org/Memory_Map_(x86) before modifying any of this.
  74 
  75 ## 16-bit entry point
  76 
  77 # Upon reset, the IBM PC:
  78 #   - loads the first sector (512 bytes)
  79 #     from some bootable image (see the boot sector marker at the end of this file)
  80 #     to the address range [0x7c00, 0x7e00)
  81 #   - starts executing code at address 0x7c00
  82 
  83 # offset 00 (address 0x7c00):
  84   # disable interrupts for this initialization
  85   fa  # cli
  86 
  87   # initialize segment registers
  88   # this isn't always needed, but the recommendation is to not make assumptions
  89   b8 00 00  # ax <- 0
  90   8e d8  # ds <- ax
  91   8e c0  # es <- ax
  92   8e e0  # fs <- ax
  93   8e e8  # gs <- ax
  94 
  95   # initialize stack to 0x00070000
  96   # We don't read or write the stack before we get to 32-bit mode, but BIOS
  97   # calls do. We need to move the stack in case BIOS initializes it to some
  98   # low address that we want to write code into.
  99   b8 00 70  # ax <- 0x7000
 100   8e d0  # ss <- ax
 101   bc 00 00  # sp <- 0x0000
 102 
 103 # 14:
 104   # load remaining sectors from first two tracks of disk into addresses [0x7e00, 0x17800)
 105   b4 02  # ah <- 2  # read sectors from disk
 106   # dl comes conveniently initialized at boot time with the index of the device being booted
 107   b5 00  # ch <- 0  # cylinder 0
 108   b6 00  # dh <- 0  # track 0
 109   b1 02  # cl <- 2  # second sector, 1-based
 110   b0 7d  # al <- 125  # number of sectors to read
 111   # address to write sectors to = es:bx = 0x7e00, contiguous with boot segment
 112   bb 00 00  # bx <- 0
 113   8e c3  # es <- bx
 114   bb 00 7e  # bx <- 0x7e00 [label]
 115   cd 13  # int 13h, BIOS disk service
 116   0f 82 a3 00  # jump-if-carry disk_error [label]
 117 
 118 # 2c:
 119   # undo the A20 hack: https://en.wikipedia.org/wiki/A20_line
 120   # this is from https://github.com/mit-pdos/xv6-public/blob/master/bootasm.S
 121   # seta20.1:
 122   e4 64  # al <- port 0x64
 123   a8 02  # set zf if bit 1 (second-least significant) is not set
 124   75 fa  # if zf not set, goto seta20.1 (-6)
 125 
 126   b0 d1  # al <- 0xd1
 127   e6 64  # port 0x64 <- al
 128 
 129 # 36:
 130   # seta20.2:
 131   e4 64  # al <- port 0x64
 132   a8 02  # set zf if bit 1 (second-least significant) is not set
 133   75 fa  # if zf not set, goto seta20.2 (-6)
 134 
 135   b0 df  # al <- 0xdf
 136   e6 64  # port 0x64 <- al
 137 
 138 # 40:
 139   # adjust video mode
 140   b4 4f  # ah <- 4f (VBE)
 141   b0 02  # al <- 02 (set video mode)
 142   bb 05 41  # bx <- 0x0105 (graphics 1024x768x256
 143             #               0x4000 bit = configure linear frame buffer in Bochs emulator; hopefully this doesn't hurt anything when running natively)
 144             # fallback mode: 0x0101 (640x480x256)
 145   cd 10  # int 10h, Vesa BIOS extensions
 146 
 147 # 49:
 148   # load information for the (hopefully) current video mode
 149   # mostly just for the address to the linear frame buffer
 150   b4 4f  # ah <- 4f (VBE)
 151   b0 01  # al <- 01 (get video mode)
 152   b9 07 01  # cx <- 0x0107 (mode we requested)
 153   bf 00 81  # di <- 0x7f00 (video mode info) [label]
 154   cd 10
 155 
 156 # 55:
 157   # switch to 32-bit mode
 158   0f 01 16  # lgdt 00/mod/indirect 010/subop 110/rm/use-disp16
 159     f8 7c  # *gdt_descriptor [label]
 160   0f 20 c0  # eax <- cr0
 161   66 83 c8 01  # eax <- or 0x1
 162   0f 22 c0  # cr0 <- eax
 163   ea 00 7d 08 00  # far jump to initialize_32bit_mode after setting cs to the record at offset 8 in the gdt (gdt_code) [label]
 164 
 165 # padding
 166 # 69:
 167                            00 00 00 00 00 00 00
 168 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 169 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 171 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 172 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 173 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 174 
 175 # cf:
 176 # disk_error:
 177   # print 'D' to top-left of screen to indicate disk error
 178   # *0xb8000 <- 0x0f44
 179   # bx <- 0xb800
 180   bb 00 b8
 181   # ds <- bx
 182   8e db  # 11b/mod 011b/reg/ds 011b/rm/bx
 183   # al <- 'D'
 184   b0 44
 185   # ah <- 0x0f  # white on black
 186   b4 0f
 187   # bx <- 0
 188   bb 00 00
 189   # *ds:bx <- ax
 190   89 07  # 00b/mod/indirect 000b/reg/ax 111b/rm/bx
 191 
 192 e9 fd ff  # loop forever
 193 
 194 ## GDT: 3 records of 8 bytes each
 195 
 196 # e0:
 197 # gdt_start:
 198 # gdt_null:  mandatory null descriptor
 199   00 00 00 00 00 00 00 00
 200 # gdt_code:  (offset 8 from gdt_start)
 201   ff ff  # limit[0:16]
 202   00 00 00  # base[0:24]
 203   9a  # 1/present 00/privilege 1/descriptor type = 1001b
 204       # 1/code 0/conforming 1/readable 0/accessed = 1010b
 205   cf  # 1/granularity 1/32-bit 0/64-bit-segment 0/AVL = 1100b
 206       # limit[16:20] = 1111b
 207   00  # base[24:32]
 208 # gdt_data:  (offset 16 from gdt_start)
 209   ff ff  # limit[0:16]
 210   00 00 00  # base[0:24]
 211   92  # 1/present 00/privilege 1/descriptor type = 1001b
 212       # 0/data 0/conforming 1/readable 0/accessed = 0010b
 213   cf  # same as gdt_code
 214   00  # base[24:32]
 215 # gdt_end:
 216 
 217 # f8:
 218 # gdt_descriptor:
 219   17 00  # final index of gdt = gdt_end - gdt_start - 1
 220   e0 7c 00 00  # start = gdt_start [label]
 221 
 222 # padding
 223 # fe:
 224                                           00 00
 225 
 226 ## 32-bit code from this point (still some instructions not in SubX)
 227 
 228 # offset 100 (address 0x7d00):
 229 # initialize_32bit_mode:
 230   66 b8 10 00  # ax <- offset 16 from gdt_start
 231   8e d8  # ds <- ax
 232   8e d0  # ss <- ax
 233   8e c0  # es <- ax
 234   8e e0  # fs <- ax
 235   8e e8  # gs <- ax
 236 
 237 # 10e:
 238   bc 00 00 07 00  # esp <- 0x00070000
 239 
 240 # 113:
 241   # load interrupt handlers
 242   0f 01 1d  # lidt 00/mod/indirect 011/subop 101/rm32/use-disp32
 243     00 80 00 00  # *idt_descriptor [label]
 244 
 245   # For now, not bothering reprogramming the IRQ to not conflict with software
 246   # exceptions.
 247   #   https://wiki.osdev.org/index.php?title=8259_PIC&oldid=24650#Protected_Mode
 248   #
 249   # Interrupt 1 (keyboard) conflicts with debugger faults. We don't use a
 250   # debugger.
 251   # Reference:
 252   #   https://wiki.osdev.org/Exceptions
 253 
 254 # 11a:
 255   # enable keyboard IRQ (1)
 256   b0 fd  # al <- 0xfd  # disable mask for IRQ1
 257   e6 21  # port 0x21 <- al
 258 
 259 # 11e:
 260   # initialization is done; enable interrupts
 261   fb
 262   e9 dc 16 00 00  # jump to 0x9400 [label]
 263 
 264 # padding
 265 # 124:
 266             00 00 00 00 00 00 00 00 00 00 00 00
 267 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 268 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 269 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 270 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 271 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 272 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 273 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 274 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 275 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 276 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 277 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 278 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 279 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 280 
 281 # 1fe:
 282 # final 2 bytes of boot sector
 283 55 aa
 284 
 285 ## sector 2 onwards loaded by load_disk, not automatically on boot
 286 
 287 # offset 200 (address 0x7e00):
 288 # null interrupt handler:
 289   cf  # iret
 290 
 291 # padding
 292 # 201:
 293    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 294 
 295 # 210:
 296 # keyboard interrupt handler:
 297   # prologue
 298   fa  # disable interrupts
 299   60  # push all registers to stack
 300   # acknowledge interrupt
 301   b0 20  # al <- 0x20
 302   e6 20  # port 0x20 <- al
 303   31 c0  # eax <- xor eax;  11/direct 000/r32/eax 000/rm32/eax
 304   # check output buffer of 8042 keyboard controller (https://web.archive.org/web/20040604041507/http://panda.cs.ndsu.nodak.edu/~achapwes/PICmicro/keyboard/atkeyboard.html)
 305   e4 64  # al <- port 0x64
 306   a8 01  # set zf if bit 0 (least significant) is not set
 307   74 89  # jump to epilogue if 0 bit is not set [label]
 308 # 21e:
 309   # - if keyboard buffer is full, return
 310   31 c9  # ecx <- xor ecx;  11/direct 001/r32/ecx 001/rm32/ecx
 311   # var index/ecx: byte
 312   8a  # copy m8 at r32 to r8
 313     0d  # 00/mod/indirect 001/r8/cl 101/rm32/use-disp32
 314     28 80 00 00  # disp32 [label]
 315   # al = *(keyboard buffer + index)
 316   8a  # copy m8 at r32 to r8
 317     81  # 10/mod/*+disp32 000/r8/al 001/rm32/ecx
 318     30 80 00 00  # disp32 [label]
 319   # if (al != 0) return
 320   3c 00  # compare al, 0
 321   75 77  # jump to epilogue if != [label]
 322 # 230:
 323   # - read keycode
 324   e4 60  # al <- port 0x60
 325   # - key released
 326   # if (al == 0xaa) shift = false  # left shift is being lifted
 327   3c aa  # compare al, 0xaa
 328   75 0a  # jump to $1 if != [label]
 329   # *shift = 0
 330   c7  # copy imm32 to rm32
 331     05  # 00/mod/indirect 000/subop/copy 101/rm32/use-disp32
 332     10 80 00 00  # disp32 [label]
 333     00 00 00 00  # imm32
 334 # 240:
 335 # $1:
 336   # if (al == 0xb6) shift = false  # right shift is being lifted
 337   3c b6  # compare al, 0xb6
 338   75 0a  # jump to $1 if != [label]
 339   # *shift = 0
 340   c7  # copy imm32 to rm32
 341     05  # 00/mod/indirect 000/subop/copy 101/rm32/use-disp32
 342     10 80 00 00  # disp32 [label]
 343     00 00 00 00  # imm32
 344 # $2:
 345   # if (al & 0x80) a key is being lifted; return
 346   50  # push eax
 347   24 80  # al <- and 0x80
 348   3c 00  # compare al, 0
 349   58  # pop to eax (without touching flags)
 350   75 51  # jump to epilogue if != [label]
 351 # 256:
 352   # - key pressed
 353   # if (al == 0x2a) shift = true, return  # left shift pressed
 354   3c 2a  # compare al, 0x2a
 355   75 0c  # jump to $3 if != [label]
 356   # *shift = 1
 357   c7  # copy imm32 to rm32
 358     05  # 00/mod/indirect 000/subop/copy 101/rm32/use-disp32
 359     10 80 00 00  # disp32 [label]
 360     01 00 00 00  # imm32
 361   eb 41 # jump to epilogue [label]
 362 # 266:
 363 # $3:
 364   # if (al == 0x36) shift = true, return  # right shift pressed
 365   3c 36  # compare al, 0x36
 366   75 0c  # jump to $4 if != [label]
 367   # *shift = 1
 368   c7  # copy imm32 to rm32
 369     05  # 00/mod/indirect 000/subop/copy 101/rm32/use-disp32
 370     10 80 00 00  # disp32 [label]
 371     01 00 00 00  # imm32
 372   eb 31 # jump to epilogue [label]
 373 # 276:
 374 # $4:
 375   # - convert key to character
 376   # if (shift) use keyboard normal map
 377   81  # operate on rm32 and imm32
 378     3d  # 00/mod/indirect 111/subop/compare 101/rm32/use-disp32
 379     10 80 00 00  # disp32 = shift [label]
 380     00 00 00 00  # imm32
 381   74 08  # jump to $5 if = [label]
 382   # otherwise use keyboard shift map
 383   # al <- *(keyboard shift map + eax)
 384   8a  # copy m8 at rm32 to r8
 385     80  # 10/mod/*+disp32 000/r8/al 000/rm32/eax
 386     00 87 00 00  # disp32 [label]
 387   eb 06  # jump to $6 [label]
 388 # 28a:
 389 # $5:
 390   # al <- *(keyboard normal map + eax)
 391   8a  # copy m8 at rm32 to r8
 392     80  # 10/mod/*+disp32 000/r8/al 000/rm32/eax
 393     00 86 00 00  # disp32 [label]
 394 # $6:
 395   # - if there's no character mapping, return
 396   3c 00  # compare al, 0
 397   74 13  # jump to epilogue if = [label]
 398 # 294:
 399   # - store al in keyboard buffer
 400   88  # copy r8 to m8 at r32
 401     81  # 10/mod/*+disp32 000/r8/al 001/rm32/ecx
 402     30 80 00 00  # disp32 [label]
 403   # increment index
 404   fe  # increment byte
 405     05  # 00/mod/indirect 000/subop/increment 101/rm32/use-disp32
 406     28 80 00 00  # disp32 [label]
 407   # clear top nibble of index (keyboard buffer is circular)
 408   80  # and byte
 409     25  # 00/mod/indirect 100/subop/and 101/rm32/use-disp32
 410     28 80 00 00  # disp32 [label]
 411     0f  # imm8
 412 # 2a9:
 413   # epilogue
 414   61  # pop all registers
 415   fb  # enable interrupts
 416   cf  # iret
 417 
 418 # padding
 419 # 2aa:
 420                               00 00 00 00 00 00
 421 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 422 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 423 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 424 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 425 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 426 # 300:
 427 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 428 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 429 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 430 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 431 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 432 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 433 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 434 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 435 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 436 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 437 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 438 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 439 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 440 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 441 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 442 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 443 
 444 # offset 400 (address 0x8000): interrupt handler data
 445 # idt_descriptor:
 446   ff 03  # idt_end - idt_start - 1
 447   00 82 00 00  # start = idt_start [label]
 448 
 449 # padding
 450 # 406:
 451                   00 00 00 00 00 00 00 00 00 00
 452 
 453 # 410:
 454 # var shift: boolean
 455   00 00 00 00
 456 
 457 # padding
 458 # 414:
 459             00 00 00 00 00 00 00 00 00 00 00 00
 460 00 00 00 00 00 00 00 00
 461 
 462 # 428:
 463 # var keyboard circular buffer
 464 # write index: nibble
 465 # still take up 4 bytes so SubX can handle it
 466   00 00 00 00
 467 # 42c:
 468 # read index: nibble
 469 # still take up 4 bytes so SubX can handle it
 470   00 00 00 00
 471 # 430:
 472 # circular buffer: byte[16]
 473   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 474 
 475 # padding
 476 # 440:
 477 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 478 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 479 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 481 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 482 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 483 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 484 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 485 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 486 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 487 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 488 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 489 
 490 # offset 500 (address 0x8100):
 491 +-- 53 lines: # video mode info ---------------------------------------------------------------------------------------------------------------------------------------
 544 
 545 # offset 600 (address 0x8200):
 546 +--161 lines: # interrupt descriptor table ----------------------------------------------------------------------------------------------------------------------------
 707 
 708 ## the rest of this file has data
 709 
 710 # offset a00 (address 0x8600):
 711 +--152 lines: # translating keys to ASCII -----------------------------------------------------------------------------------------------------------------------------
 863 
 864 # offset 1000 (address 0x8c00)
 865 +--236 lines: # Bitmaps for some ASCII characters (soon Unicode) ------------------------------------------------------------------------------------------------------
1101 
1102 # offset 1800 (address 0x9400)
1103 
1104 # vim:ft=subx