Skip to main content

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

  1. Each osmk.* call appends an x86 assembly instruction to an internal list
  2. build() wraps them in a bootloader template (16-bit real mode, MBR)
  3. NASM assembles it into a flat binary
  4. The binary is exactly 512 bytes with the 0xAA55 boot 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

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

osmk-0.1.3.tar.gz (15.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

osmk-0.1.3-py3-none-any.whl (12.2 kB view details)

Uploaded Python 3

File details

Details for the file osmk-0.1.3.tar.gz.

File metadata

  • Download URL: osmk-0.1.3.tar.gz
  • Upload date:
  • Size: 15.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.6

File hashes

Hashes for osmk-0.1.3.tar.gz
Algorithm Hash digest
SHA256 a2053dd0237b93a6e07eaf0c0e9cea5f06a4e80d9cae89e837a2174f4588da1e
MD5 73765b41ffb3c330037b2aba8b3e6fc6
BLAKE2b-256 ec7f2d98058430a05d74fc0457f514c06e4079d4d3cc96f8db1ca68d0ea77246

See more details on using hashes here.

File details

Details for the file osmk-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: osmk-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 12.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.6

File hashes

Hashes for osmk-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 36141aa0fbbe03d527539b7898409886e45b76ecf419e8c363c72faa1628d645
MD5 95de5802dff9eae2f647dc5d6b298ecb
BLAKE2b-256 62438d6f6744677762c5ec82cf347c430ab56aa11958c9d3ef27515f886f5efb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page