Write x86 operating systems in Python — one instruction at a time
Project description
osmk
Write x86 operating systems in Python — one instruction at a time.
Every function call maps to an x86 assembly instruction. build() assembles it into a bootable 512-byte disk image.
Install
pip install osmk
You also need NASM (the assembler) and optionally QEMU (to run your OS):
# Ubuntu/Debian
sudo apt install nasm qemu-system-x86
# macOS
brew install nasm qemu
# Windows
# NASM: https://nasm.us → download & add to PATH
# QEMU: https://qemu.org/download/#windows
Or test without installing QEMU at copy.sh/v86 — upload your .img as a floppy disk image.
Quick Start
import osmk
from osmk import ax, bx
osmk.start("hello.img") # begin a new OS
osmk.ax(5) # mov ax, 5
osmk.bx(5) # mov bx, 5
osmk.add(ax, bx) # add ax, bx → ax = 0x000A
osmk.writehex() # print AX as hex → "000A"
osmk.build() # assemble → hello.img (512 bytes)
Run it:
qemu-system-i386 -drive format=raw,file=hello.img
API Reference
Lifecycle
| Function | Description |
|---|---|
start(filename, bits=16, org=0x7C00) |
Begin a new OS program |
build() |
Assemble and create the bootable image |
dump_asm() |
Return the generated NASM source (for debugging) |
run(memory=128) |
Build and launch in QEMU |
Register Setters
Every register is callable. osmk.ax(5) → mov ax, 5.
osmk.ax(5) # mov ax, 5
osmk.bx(ax) # mov bx, ax
osmk.al(0x41) # mov al, 0x41
osmk.eax(0x1000) # mov eax, 0x1000
Available: ax bx cx dx si di sp bp · al ah bl bh cl ch dl dh · eax ebx ecx edx esi edi esp ebp
Instructions
# Arithmetic
osmk.add(ax, bx) osmk.sub(ax, 1)
osmk.mul(bx) osmk.div(cx)
osmk.inc(ax) osmk.dec(cx)
osmk.neg(ax)
# Bitwise
osmk.and_(ax, 0xFF) osmk.or_(ax, bx)
osmk.xor(ax, ax) osmk.not_(ax)
osmk.shl(ax, 4) osmk.shr(ax, 1)
# Compare & Jump
osmk.cmp(ax, 0)
osmk.je("done") osmk.jne("loop")
osmk.jl("less") osmk.jg("greater")
osmk.jz("zero") osmk.jnz("nonzero")
osmk.jmp("start") osmk.jc("carry")
# Stack
osmk.push(ax) osmk.pop(bx)
# Subroutines
osmk.call("myfunc") osmk.ret()
# Interrupts
osmk.interrupt(0x10) # int 0x10
osmk.cli() osmk.sti()
osmk.hlt()
# Labels
osmk.label("loop")
Labels & Scoping
# Global labels — must be unique
osmk.label("main")
# Local labels — prefix with '.', scoped to nearest global label above
osmk.label("main")
osmk.label(".loop") # belongs to 'main'
osmk.jnz(".loop")
osmk.label("other")
osmk.label(".loop") # different .loop, belongs to 'other' — no collision
osmk.jnz(".loop")
Output Helpers
osmk.writehex() # print AX as 4-digit hex (e.g. "000A")
osmk.writechar("A") # print character 'A'
osmk.writechar() # print whatever's in AL
osmk.prints("Hello!") # print a string
osmk.newline() # print CR+LF
osmk.readchar() # wait for keypress → AL
osmk.halt() # halt CPU (cli + hlt loop)
Functions
Define reusable subroutines with the function() context manager. ret is appended automatically. Labels inside are auto-scoped as local labels, so multiple functions can use the same label names without collision.
with osmk.function("print_stars") as fn:
fn.mov(cx, 5)
fn.label("loop") # becomes .loop (local to print_stars)
fn.asm("mov ah, 0x0E")
fn.asm("mov al, '*'")
fn.interrupt(0x10)
fn.dec(cx)
fn.jnz(".loop")
# ret is auto-appended here
with osmk.function("print_dashes") as fn:
fn.mov(cx, 5)
fn.label("loop") # different .loop — no collision
fn.asm("mov ah, 0x0E")
fn.asm("mov al, '-'")
fn.interrupt(0x10)
fn.dec(cx)
fn.jnz(".loop")
osmk.call("print_stars")
osmk.call("print_dashes")
The function builder supports the full DSL: fn.mov(), fn.add(), fn.sub(), fn.inc(), fn.dec(), fn.cmp(), fn.push(), fn.pop(), fn.call(), fn.interrupt(), fn.label(), fn.jmp(), fn.je(), fn.jne(), fn.jz(), fn.jnz(), fn.jl(), fn.jg(), fn.asm(), fn.comment().
Custom / Escape Hatch
When osmk doesn't cover what you need:
# Inject raw NASM assembly
osmk.asm("mov ah, 0x0E")
osmk.asm("""
mov ah, 0x00
mov al, 0x03
int 0x10
""")
# Inject raw bytes
osmk.raw(b"\x90\x90\x90") # 3x NOP
# Define data
osmk.data("my_var", "dw 0x1234")
# Add comments
osmk.comment("this sets up video mode")
Explicit MOV
osmk.mov(ax, 0x0E) # same as osmk.ax(0x0E)
osmk.mov(bx, ax) # same as osmk.bx(ax)
Examples
Hello World
import osmk
osmk.start("hello.img")
osmk.prints("Hello, World!")
osmk.newline()
osmk.prints("My first OS!")
osmk.halt()
osmk.build()
Keyboard Echo
import osmk
from osmk import al
osmk.start("echo.img")
osmk.prints("Type anything:")
osmk.newline()
osmk.label("loop")
osmk.readchar() # key → AL
osmk.writechar() # echo it
osmk.cmp(al, 13) # Enter key?
osmk.jne("loop") # no → keep looping
osmk.writechar(10) # yes → also print line feed
osmk.jmp("loop")
osmk.build()
Counter
import osmk
from osmk import ax, cx
osmk.start("counter.img")
osmk.ax(0)
osmk.cx(16)
osmk.label("count")
osmk.writehex()
osmk.prints(" ")
osmk.inc(ax)
osmk.dec(cx)
osmk.jnz("count")
osmk.halt()
osmk.build()
How It Works
- Each
osmk.*call appends an x86 assembly instruction to an internal list build()wraps them in a bootloader template (16-bit real mode, MBR)- NASM assembles it into a flat binary
- The binary is exactly 512 bytes with the
0xAA55boot signature
The generated code runs in 16-bit real mode using BIOS interrupts for I/O. Use dump_asm() to see exactly what assembly your Python generates.
Resources
- OSDev Wiki — OS development reference
- NASM Documentation — assembler docs
- x86 Instruction Reference — complete instruction set
- Writing a Simple OS — Nick Blundell's guide
- BIOS Interrupt List — int 0x10, 0x13, 0x16, etc.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file osmk-0.1.4.tar.gz.
File metadata
- Download URL: osmk-0.1.4.tar.gz
- Upload date:
- Size: 16.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e4fe86f4eb7f4a7b593547524e6d55801ffa14917a5306b9fc4d409928d884e
|
|
| MD5 |
b4365c1833de050b87570fe74f9a784c
|
|
| BLAKE2b-256 |
14bdd00daa32460ac0e65e5071425bbbc68a55c3a9e4be4c834f29b74bf9a8de
|
File details
Details for the file osmk-0.1.4-py3-none-any.whl.
File metadata
- Download URL: osmk-0.1.4-py3-none-any.whl
- Upload date:
- Size: 12.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
880097923a5ebed3fee94ebdc1a52d0cd63b448e3971d782df956d96e810af2e
|
|
| MD5 |
7b0de17a1466f823b1224ebc8db7a9a0
|
|
| BLAKE2b-256 |
443506286daf3490b5f93fb799216241f57212de9ed05f3d5c3fa2c47012792d
|