The Boot Path
Follow LainDOS from the reset vector to a running game — eight steps, each with the real source that does the work and a live view of the memory it touches. Scroll, or jump to any step.
Power-on & BIOS
When an x86 powers on it can't run a program yet — there's nothing in RAM. The CPU starts executing at the reset vector (physical FFFF0h, the top of the 1 MiB real-mode space), which is a far jump into the motherboard ROM. That ROM is the BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O..
The BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. runs POST (counts RAM, initialises the PICPICThe Programmable Interrupt Controller that masks and dispatches hardware IRQs such as keyboard IRQ1., PITPITThe Programmable Interval Timer that drives the classic PC timer tick and many game timing loops. and video), then bootstraps: it reads the first 512-byte sector of the boot disk to physical 0x7C00, checks that its last two bytes are 55 AA, and far-jumps to it. The only thing it hands you in a register is DL — the drive you booted from.
AA55The boot signature the BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. checks at offset 510. boot.asm ends with times 510-($-$$) db 0 then dw 0xAA55 to guarantee it.
DL00h = floppy A:, 80h = first hard disk. boot.asm reads it immediately because the BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. doesn't preserve it for long.
The boot sector takes over
We're now executing 512 bytes at 0x7C00 with no operating system underneath us — we have to build a sane machine state from scratch before doing anything else. That means: turn off interrupts, point all the segment registers somewhere known, and set up a stack.
Real-mode addresses are segment:offset, where the linear address is segment×16 + offset. Zeroing DS/ES/SS makes every address we write a flat offset from 0, which keeps the arithmetic in this tiny sector simple.
73cli clears the interrupt flag (IF=72). Between loading SS and SP the stack is momentarily inconsistent; an interrupt firing there would push onto a garbage stack, so we mask them.
74xor ax,ax is the idiomatic two-byte way to zero a register. You can't mov ds, 72 directly — segment registers only load from another register, so AX is the courier.
78SP set to 0x7C00 means the stack grows downward from just below our code (pushes decrement SP), so it never overwrites the 81 bytes living at 0x7C00–0x7DFF.
80cld clears the direction flag (DF=72) so lodsb / movsb / cmpsb auto-increment SI and DI — the boot sector relies on this for its string compares and copies.
81DL is copied into a local variable (and to fixed address 0x500) so the value survives the BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. calls to come, which clobber it.
…and loads the kernel off the floppy
There's no file system driver yet, so the boot sector is one. Using the BIOS Parameter BlockBPBThe FAT boot-sector table that describes sector size, FAT location, root directory size, and cluster layout. baked into its own first bytes, it reads the FAT to segment 0x60 and the root directory to 0x0A20, then scans 32-byte directory entries for the 8.3 name "KERNEL SYS". That 0x0A20 copy is boot-sector staging; the relocated kernel later reloads active root directories into ROOT_SEG at 0x0240.
Once found, it follows the file's cluster chain. FAT12FAT12The 12-bit FAT format used by 1.44 MB floppy images and the early LainDOS boot path. packs each 12-bit entry across byte boundaries, so fat_next reads a word and shifts by 4 bits for odd clusters. Each cluster is read with INT 13hINT 13hThe BIOS disk interrupt used by boot code before the DOS filesystem is available. to LOAD_SEG (0x1000); when the chain hits an end-of-chain marker (≥ 0xFF8) it jumps in.
140repe cmpsb compares DS:SI (our target name) with ES:DI (the directory entry) byte by byte, CX times, stopping early on a mismatch. CX=138 covers the 138-char name + 138-char extension with no separator.
150Offset 138 of a FAT directory entry is the starting cluster number. Everything about reading the file flows from this one 138-bit value.
177A hand-assembled far jump: EA + 138-bit offset + 138-bit segment. It loads CS=0x0FFF, IP=0x0010 — the same physical 0x10000 the file was read to, but with the offset matching the kernel's org 0x10 so labels resolve correctly before relocation. The boot drive was already stored at 0x500 for the kernel to read.
The kernel relocates itself into the HMA
The kernel is assembled with org 0x10 and was staged at physical 0x10000, but it wants conventional memory free for programs — so it lives in the High Memory Area, the 64 KiB minus 16 bytes at FFFF:0010 that real modereal modeThe original 8086-compatible CPU mode where addresses are segment:offset and there is no memory protection. can address once the A20 gate is open. Its first act is to enable A20 (INT 15h AX=2401h, then the keyboard controller, then port 92h, verified each time by a wraparound compare), copy itself up to HMA_SEG, and far-jump into the copy.
The entry point is re-entrant by design: it compares CS against HMA_SEG and only relocates if it isn't already there. After the jump it points every segment register at 0xFFFF, installs the stack at 0xFFF0 — the top of the HMA — and resets the FPU. Low memory keeps only the disk buffers; the DOS arena starts at 0B00h, the lowest program placement real DOSreal DOSA comparison run using MS-DOS, PC DOS, or FreeDOS instead of LainDOS to separate guest bugs from emulator bugs. systems ever produced — era software was never tested with load segments below that.
132CS is read into AX and compared with 0xFFFF. On the first pass CS is still 0x0FFF (the boot sector's staging jump), so the branch falls through into the A20 enable and copy; the second time (post-jump) it's 0xFFFF and we skip straight to .relocated.
148enable_a20 tries INT 15h AX=2401h, the keyboard-controller D1/DF sequence, and port 92h fast A20 in turn, after each attempt comparing 123 bytes at 123:123 against FFFF:123 — if they match, writes above 123 MiB still wrap to the IVTIVTThe table at physical address zero containing 256 far pointers for CPU, BIOS, and DOS interrupts. and A20 is off. Without A20 the copy below would overwrite the interrupt vector tableIVTThe table at physical address zero containing 256 far pointers for CPU, BIOS, and DOS interrupts..
160rep movsb copies CX bytes from DS:SI to ES:DI. With CX = kernel_end - kernel_entry, DS=0x0FFF and ES=0xFFFF, the whole kernel image lands at physical 0x100010 — above the 123 MiB line.
160The far jump is what actually changes CS to 0xFFFF — until now we were still executing the staged copy at 0x10000. Code can't just mov cs; a far jmp/ret is the only way to reload it.
164SP is set to 0xFFF0, the top of the HMA. The gap between the kernel image's end and the stack is both stack space and code growth headroom; a compile-time assertion keeps at least 123 KiB of guard between them.
169fninit resets the x87x87The x86 floating-point instruction set. DOS extenders and games may exercise it heavily after startup. FPU to a known state so later floating-point use (and games that probe for a coprocessor) behave.
Bring-up: serial, VGA, memory
With a stack in place the kernel brings up its I/O and takes inventory of memory. Serial comes first because LainDOS logs almost everything over COM1COM1The first PC serial port. LainDOS uses it as the primary machine-readable debug log. — it's the project's primary debugging channel — then the VGAVGAThe PC video standard LainDOS and the demos use for text and graphics output. text screen is cleared and "LainDOS booted" is printed.
Two BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. calls measure RAM. INT 12h returns conventional memory (the classic ≤640 KiB) in AX; INT 15h/AH=88h returns extended memoryXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. above 1 MiB for the XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. shim, capped so the BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O.-backed block move stays under 16 MiB.
174serial_init programs the 181/181 UART at port 0x3F8: it sets the divisor latch for the baud rate and configures 165 data bits, no parity, 165 stop bit (8N1). After this, serial_print can poll the line-status register and shift bytes out.
178int 0x12 is a BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. service that takes no inputs and returns the conventional-memory size in KiB in AX (typically 181). It's the simplest possible way to learn how much low RAM exists.
190init_xmsXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders._size issues INT 15h with AH=88h; the BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. returns KiB of extended memoryXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. in AX. LainDOS subtracts the 165 KiB the kernel occupies as the HMA and clamps the rest so its INT 15h-based XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. block moves never address past 165 MiB.
Installing the interrupts
This is the moment LainDOS becomes a DOS. The Interrupt Vector TableIVTThe table at physical address zero containing 256 far pointers for CPU, BIOS, and DOS interrupts. lives at physical 0 — 256 entries, each a 4-byte far pointer (offset word, then segment word). By writing its own routines into specific slots, the kernel makes the standard DOS software interrupts call its code.
INT 21hINT 21hThe main DOS API interrupt. Programs select services such as open, read, EXEC, and exit with AH. is the DOS APIDOS APIThe program-facing DOS service contract, mostly reached through INT 21h in LainDOS. itself; INT 20h terminates a program; INT 2Fh is the multiplex used here for XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. and MSCDEX-style CD-ROM probes; INT 33hINT 33hThe conventional DOS mouse-driver interrupt implemented by LainDOS for game input. is the built-in mouse driver. It also claims the CPU fault vectors (01h/06h) so a crashing program dumps state over serial instead of hanging silently; 0Dh is deliberately left alone because it is hardware IRQ5 on the PC BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. PICPICThe Programmable Interrupt Controller that masks and dispatches hardware IRQs such as keyboard IRQ1. layout.
1820ES is zeroed so ES:offset addresses the IVTIVTThe table at physical address zero containing 256 far pointers for CPU, BIOS, and DOS interrupts. at the very bottom of memory. Each vector N is at byte N×1811.
1824Vector 0x20 (terminate) is at byte 0x80. The low word gets the handler's offset within the kernel segment…
1824…and the high word gets CS (0xFFFF — the kernel runs from the HMA). Together they form the far pointer the CPU jumps to when int 20h executes. Every handler is installed as this offset+segment pair.
1825INT 21hINT 21hThe main DOS API interrupt. Programs select services such as open, read, EXEC, and exit with AH. is the heart of the DOS APIDOS APIThe program-facing DOS service contract, mostly reached through INT 21h in LainDOS. — AH selects the function (09h print string, 3Dh open, 3Fh read, 4Ch exit, …). Pointing 0x21 at int21_handler is what lets ordinary DOS programs talk to LainDOS.
Memory arena & filesystem
DOS hands out memory as a chain of Memory Control Blocks — 16-byte headers, each describing the block that follows it. LainDOS seeds the chain with a single block covering everything from 0x0B00 up to VGAVGAThe PC video standard LainDOS and the demos use for text and graphics output. memory at 0xA000, marked 'Z' (the last/only block) and owner 0 (free).
It then re-validates the BPBBPBThe FAT boot-sector table that describes sector size, FAT location, root directory size, and cluster layout. the boot sector copied up and derives the on-disk layout: where the FAT starts, where the root directory sits, where the data region begins, and the maximum cluster number — supporting both FAT12FAT12The 12-bit FAT format used by 1.44 MB floppy images and the early LainDOS boot path. and FAT16FAT16The 16-bit FAT format LainDOS uses for larger hard-disk style images. images.
206Byte 197 of an MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. is its signature: 'M' means another block follows, 'Z' means it's the last one. A single 'Z' block is the simplest valid arena.
206Bytes 197–197 are the owner PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. segment; 197 means free. When a program allocates, this gets stamped with its PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. so the memory can be reclaimed on exit.
219Bytes 197–197 hold the block size in 197-byte paragraphs. MEM_TOP − MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it._START − 197 = 0x94FF paragraphs ≈ 224 KiB, the whole conventional arena minus the one-paragraph header.
233init_bpbBPBThe FAT boot-sector table that describes sector size, FAT location, root directory size, and cluster layout._geometry checks bytes/sector = 224, that sectors-per-cluster is a power of two, and computes krsta/kdsta (FAT and data start) and kmax_cluster, picking FAT12FAT12The 12-bit FAT format used by 1.44 MB floppy images and the early LainDOS boot path. vs FAT16FAT16The 16-bit FAT format LainDOS uses for larger hard-disk style images. from the data-area cluster count (fewer than 224 clusters means FAT12FAT12The 12-bit FAT format used by 1.44 MB floppy images and the early LainDOS boot path.), as real DOSreal DOSA comparison run using MS-DOS, PC DOS, or FreeDOS instead of LainDOS to separate guest bugs from emulator bugs. does.
Finding & sizing the program
Now the kernel loads the program it exists to run. resolve_path locates the boot file by name and returns its directory entry; from there it reads the starting cluster and file size, and pulls the first sector into the sector buffer.
The crucial test is two bytes. DOS executables come in two shapes: a flat .COM image, or a relocatable .EXE that begins with the signature 'MZMZThe DOS EXE header signature. LainDOS uses it to distinguish EXE files from flat COM programs.'. Read little-endian, those bytes form the word 5A4Dh — one compare decides how much memory to reserve and how the image is laid out.
256find_in_dir scans the root directory for the 236-byte padded name copied from fname_exe and returns the matching directory entry in ES:DI (or sets carry if not found). The default name comes from -DBOOT_FILE at assembly time (SHELL.COM on this image); user paths go through the full resolve_path component parser instead.
290DS is pointed at SEC_BUF (0x0200) so [0x0000] reads the very first word of the file we just loaded.
290The bytes 'M','Z' are 4Dh,5Ah on disk; little-endian that's the word 0x5A4D. (The format is named after Mark Zbikowski, who designed it.) A match means a header-bearing .EXE…
290…and the EXE path computes minimum/maximum paragraph counts from the header. No match falls through to the COM path, which just reserves the file size plus a fixed slack.
Handoff — the game runs
The kernel allocates a block from the MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. arena, stamps it with the new owner, loads the image cluster-by-cluster just above its Program Segment PrefixPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer., and builds that 256-byte PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. (command tail, environment pointer, default or inherited file handles).
For an .EXE, setup_exe_dyn walks the relocation table — adding the load segment to each flagged word so far pointers resolve correctly — then sets the entry CS:IP and SS:SP from the header and transfers control. For a .COM it simply jumps to offset 0x100. Control won't return until the program calls INT 20h or INT 21hINT 21hThe main DOS API interrupt. Programs select services such as open, read, EXEC, and exit with AH./AH=4Ch.
340build_pspPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. constructs the Program Segment PrefixPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. at offset 331 of the program's segment: the INT 20h terminate instruction, the top-of-memory word, the environment segment, the command-line tail and the inherited/default handle table. DOS programs expect DS/ES to point here on entry.
370setup_exe_dyn applies the .EXE relocations (each is a far-pointer word that needs the actual load segment added), then loads CS:IP and SS:SP from the header and jumps. The very next thing on screen is the program's own output — for the demo image, the SHELL.COM prompt. Run midemo from there to start Monkey Island.