SmallOS is a BIOS-booted 32-bit x86 hobby operating system. It builds a raw hard-disk image, boots through a two-stage loader, enters protected mode, enables paging, mounts an ext2 filesystem, and runs a small shell plus ring-3 user programs.
The project is intentionally small enough to understand end to end, but it now has real subsystems: process scheduling, user/kernel syscalls, persistent disk state, framebuffer graphics, TCP services, and a hosted TinyCC build inside the guest.
- BIOS boot from a raw disk image: stage 1 loads stage 2 with CHS, stage 2 uses LBA reads for the kernel.
- 32-bit protected-mode C kernel with its own GDT, IDT, TSS, paging setup, and page-fault handling.
- E820-aware physical memory manager plus a simple kernel heap for permanent kernel allocations.
- Preemptive round-robin scheduler with kernel tasks, ring-3 ELF processes,
per-process address spaces, per-process kernel stacks,
fork,execve, legacy spawn-styleexec,waitpid,yield, zombie reaping, and user-fault isolation. int 0x80syscall ABI for console I/O, files, directories, cwd, process control, pipes, descriptor duplication, heap growth, time, framebuffer display, input, sockets, polling, and timer/signalfd-style shims.- ATA and USB mass-storage block devices with an ext2-backed VFS. ATA is
writable; USB BOT/SCSI storage is mounted read-only today. The generated
filesystem includes
/bin,/usr/bin,/usr/sbin,/usr/libexec/tests,/etc,/boot,/var, and/tmp, with boot diagnostics persisted at/var/log/boot.txtwhen the mounted filesystem is writable. - Framebuffer terminal with VGA text fallback, boot timing prefixes captured in
/var/log/boot.txt, graphical boot splash that covers final startup work, PS/2 keyboard, retrying OHCI USB boot keyboard/mouse probing, PS/2 plus VMware mouse input, and several graphics demos. - PCI networking with e1000 and RTL8139 NIC support, DHCP, ARP, IPv4, UDP/NTP clock sync,
runtime
ip/ipconfiginspection and configuration, a compact TCP service task, passive sockets,poll/epollreadiness, FTP, echo, and HTTP server smoke paths. - Guest userland includes familiar commands such as
ls,tree,cat,more,man,pwd,touch,mkdir,rm,cp,mv,edit,date,ip,ipconfig,uptime,halt, andreboot, plus diagnostics such ascpuz,usbinfo,usbports,usbpower,mousetest, and demos/ports such asmandel,plasma,fractint, andwolf3d. - TinyCC is built as
usr/bin/tccand can compile sample C programs inside SmallOS.
Build tools:
nasm
i686-elf-gcc
i686-elf-ld
i686-elf-objcopy
gcc
gcc-multilib
python3
qemu-system-i386
svn
curl
unzip
Most third-party package sources are git submodules, including Wolfenstein 3-D;
Fractint is exported by make deps from the official SVN tag:
git clone --recurse-submodules <repo-url>
cd SmallOSIf the repository was cloned without submodules, run:
make depsThat initializes git submodules and exports Fractint from
https://svn.fractint.net/tags/fractint-20-04p17.
Build the canonical artifacts:
make clean && makeThis writes build/img/smallos.img and build/img/smallos.vmdk. QEMU boots
the raw image directly. For hardware USB testing, make usb-image refreshes
the stable burn target at build/img/smallos-wyse-s10-direct-usb.img.
Build-profile directories include the display backend, serial mode, and NIC
selection, for example build/bin/auto-serial-e1000/. The seeded ext2 image is
built under build/bin/<profile>/ext2.seed.img, then
copied to the mutable runtime partition at .state/ext2.img. Guest-created
files survive normal rebuilds, while the .state/ext2.img.stamp dependency
lets Make refresh the runtime partition when userland binaries or seeded manual
pages change. Reset it from the current seed image with:
make reset-diskWrite the USB image to a whole device, not a partition:
sudo dd if=build/img/smallos-wyse-s10-direct-usb.img of=/dev/sdX bs=4M conv=fsync status=progressTo rebuild only the raw image:
make imageTo rebuild only the VMware/ESXi wrapper from the same raw image:
make vmdkInteractive runs:
make run # QEMU curses display, default
make run-gtk # graphical GTK display
make run-sdl # graphical SDL displaymake run uses QEMU user-network NAT with an e1000 NIC by default, and the guest acquires
its IPv4 configuration with DHCP. make run-usb-storage boots the same raw
image through QEMU OHCI USB mass storage, which exercises the protected-mode
USB storage path instead of the IDE disk path. The USB image/run targets keep
the loader2 RAM fallback enabled for hardware safety while the kernel still
prefers the live usb0 mount when it validates. If terminal input feels
sluggish through curses, use make run-gtk or make run QEMU_DISPLAY=gtk.
Mouse-driven graphics demos and ports need a graphical QEMU backend and a
grabbed QEMU window. With GTK, click the guest and press Ctrl+Alt+G to toggle
mouse/keyboard grab; this is the most reliable Windows QEMU path for Wolf3D.
For Wolf3D sound, expose QEMU's AC97 device for digitized PCM and AdLib for
FM SFX/music, for example
-audiodev sdl,id=audio0,in.voices=0,out.frequency=48000,out.buffer-length=50000 plus
-device AC97,audiodev=audio0 and -device adlib,audiodev=audio0. SB16 is
still supported as a fallback, but QEMU's GTK frontend can freeze display
updates while SB16 ISA DMA playback is active.
Headless run with serial logging:
make run-headless
tail -f /tmp/smallos-serial.logHeadless QEMU writes its PID and monitor socket to:
/tmp/smallos.pid
/tmp/smallos-monitor.sock
Use QEMU_MEMORY_MB=128 to exercise more of the PMM-managed memory range.
The default run and test paths use QEMU user-network NAT. Host forwarding can
be passed through QEMU_NET_HOSTFWD; for example, this forwards host port
2323 to the guest echo service port:
make run-headless \
QEMU_NET_HOSTFWD=',hostfwd=tcp::2323-:2323'Inside the guest, the FTP and web services start by default:
usr/sbin/ftpd --quiet
usr/sbin/cserve --port 8080 --root /var/www --max-conn 28 --log off
Additional or replacement services can still be launched from the shell:
bg usr/sbin/tcpecho
bg usr/sbin/ftpd --log-file /var/log/ftpd.log
bg usr/sbin/cserve --config /etc/cserve.ini
Shell job control supports jobs, fg <jobid>, Ctrl+Z, and kill <jobid>.
Manual ftpd launches write service output to /var/log/ftpd.log; manual
cserve launches use the log path from /etc/cserve.ini.
For TAP networking, create and configure the TAP interface on the host first, then run:
make run-tap QEMU_NET_IFACE=tap0
make run-headless-tap QEMU_NET_IFACE=tap0One simple Linux TAP setup is:
sudo ip tuntap add dev tap0 mode tap user "$USER"
sudo ip link set tap0 up
sudo ip addr add 192.168.100.1/24 dev tap0Bridge or route that interface if the guest should reach beyond the host.
Inside SmallOS, ip and ipconfig show and update the runtime IPv4
configuration:
ip
ip addr add 192.168.100.2/24 gateway 192.168.100.1 dns 1.1.1.1
ip route add default via 192.168.100.1
ip dns set 1.1.1.1
ip dhcp
ipconfig /all
VMware ESXi deploys use the same VMDK and the same DHCP/NIC path:
make esxi-smoke ESXI_SMOKE_FLAGS="--host 10.10.0.13"That target builds, uploads, replaces the VM disk, reboots, waits for
SmallOS ready, and checks the VMware boot markers. See docs/build.md for
the baseline VM shape and lower-level deploy/log helpers.
Fast regression path:
make testmake test boots headlessly, checks the boot diagnostics, runs the shell
selftest, drives the interactive readline prompt, and verifies shipped
programs against expectations under tests/shell/ and tests/elfs/.
Useful verification targets:
make verify # layout checks, guest regression suite, reboot/halt smoke
make verify-display # framebuffer/VGA screenshots plus GUI launch smoke
make verify-network # socket EOF/parallel, FTP, FTP loop, cserve
make verify-full # all verification targetsFocused smoke targets are also available:
make smoke
make smoke-reboot
make smoke-halt
make display-smoke
make gui-smoke
make usb-storage-smoke
make socket-eof-smoke
make socket-parallel-smoke
make ftp-smoke
make ftp-loop-smoke
make cserve-smoke.
├── assets/ boot splash source/rendered assets
├── docs/ subsystem notes and deeper design docs
├── patches/ third-party patches applied in build-local copies
├── samples/ files seeded into the guest filesystem
├── src/
│ ├── boot/ BIOS stage 1, stage 2, kernel entry, ELF embedding helper
│ ├── drivers/ display, input, block, ATA/USB storage, ext2, PCI, net
│ ├── exec/ ELF loader
│ ├── kernel/ memory, paging, process, scheduler, syscall, VFS, time
│ ├── shell/ shell, parser, line editor, built-in commands
│ └── user/ user commands, demos, tests, runtime headers and libc-ish code
├── tests/ guest shell and ELF expectation files
├── third_party/ TinyCC, FTP packages, cserver, Wolf3D, and Fractint source
├── tools/ image builders, layout checks, QEMU test harnesses
├── Makefile
└── linker.ld
Generated artifacts live under build/. Persistent guest disk state lives
under .state/.
The README is meant to be the front door. For a practical, non-technical walkthrough, start with the SmallOS User Guide.
The detailed subsystem notes live in docs/:
- Build system
- Boot process
- Architecture
- Execution and scheduling
- Memory
- Filesystem
- Syscalls
- User runtime
- Socket subsystem
- Interrupts
- Development notes
The final image is assembled as:
LBA 0 boot sector / MBR
LBA 1-16 stage-2 loader
LBA 17+ sector-padded kernel
after that mutable ext2 partition
The boot sector stores MBR-style entries for the kernel region and the ext2
partition. Stage 2 reads the kernel location from the image metadata; the
kernel mounts ext2 through the first storage path that validates: writable ATA,
read-only USB mass storage, then the loader2-published RAM fallback. The default
BOOT_RAMDISK_FALLBACK=never policy skips the fallback preload for normal
VM/IDE boots. BOOT_RAMDISK_FALLBACK=auto preloads only when EDD does not
identify the boot drive as USB or ATA, and the explicit USB image/run targets
force it on so hardware boots remain recoverable when protected-mode USB
storage is not happy yet. USB EDD boots probe and byte-check direct high-memory
reads before using them; otherwise the loader falls back to its low-memory
bounce buffer. Boot diagnostics are captured with [ms=... tick=... cyc=...]
prefixes in /var/log/boot.txt; display output is muted once the protected-mode
kernel owns the terminal, then the bitmap splash is shown as soon as the shell
has been preloaded and remains visible until the welcome block and shell prompt
replace it. DHCP, NTP, and default services continue asynchronously during that
covered window, and their quiet-path messages are still appended to the boot
log. make boot-layout-check, make image-layout-check, and
make usb-storage-smoke keep those contracts honest before hardware runs.
