osmk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
osmk/__init__.py ADDED
@@ -0,0 +1,619 @@
1
+ """
2
+ osmk — Write x86 operating systems in Python, one instruction at a time.
3
+
4
+ Usage:
5
+ import osmk
6
+ from osmk import ax, bx
7
+
8
+ osmk.start("hello.img")
9
+ osmk.ax(5)
10
+ osmk.bx(5)
11
+ osmk.add(ax, bx)
12
+ osmk.writehex()
13
+ osmk.build()
14
+
15
+ Every function call emits x86 assembly. build() assembles it into
16
+ a bootable disk image you can run in QEMU.
17
+ """
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ import os
22
+ import shutil
23
+ import subprocess
24
+ import tempfile
25
+ from pathlib import Path
26
+ from typing import Union
27
+
28
+ from osmk.registers import (
29
+ Register,
30
+ ax, bx, cx, dx,
31
+ si, di, sp, bp,
32
+ al, ah, bl, bh, cl, ch, dl, dh,
33
+ eax, ebx, ecx, edx, esi, edi, esp, ebp,
34
+ cs, ds, es, fs, gs, ss,
35
+ )
36
+ from osmk.engine import (
37
+ _Program, _require_started, _get_program, _set_program,
38
+ _operand, OSMKError, BuildError, StateError,
39
+ )
40
+
41
+ # Re-export registers at package level so users can do:
42
+ # from osmk import ax, bx
43
+ # osmk.add(ax, bx)
44
+
45
+ Operand = Union[Register, int, str]
46
+
47
+
48
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
49
+ # LIFECYCLE
50
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51
+
52
+ def start(output: str = "os.img", bits: int = 16, org: int = 0x7C00) -> None:
53
+ """
54
+ Begin a new OS program.
55
+
56
+ Args:
57
+ output: Output filename (.img or .iso).
58
+ bits: CPU mode — 16 (real mode) or 32 (protected mode).
59
+ org: Origin address (default 0x7C00 for MBR boot).
60
+
61
+ Example:
62
+ osmk.start("hello.img")
63
+ """
64
+ prog = _Program(output)
65
+ prog.bits = bits
66
+ prog.org = org
67
+ _set_program(prog)
68
+
69
+
70
+ def build() -> str:
71
+ """
72
+ Assemble everything and create the bootable image.
73
+
74
+ Returns the path to the generated image file.
75
+ Requires NASM to be installed on the system.
76
+
77
+ Example:
78
+ osmk.build() # creates the .img file
79
+ """
80
+ prog = _require_started()
81
+
82
+ if not shutil.which("nasm"):
83
+ raise BuildError(
84
+ "NASM not found. Install it:\n"
85
+ " Ubuntu/Debian: sudo apt install nasm\n"
86
+ " macOS: brew install nasm\n"
87
+ " Windows: https://nasm.us"
88
+ )
89
+
90
+ asm_source = prog.generate_asm()
91
+
92
+ with tempfile.TemporaryDirectory() as tmpdir:
93
+ asm_path = os.path.join(tmpdir, "program.asm")
94
+ bin_path = os.path.join(tmpdir, "program.bin")
95
+
96
+ with open(asm_path, "w") as f:
97
+ f.write(asm_source)
98
+
99
+ # Assemble with NASM
100
+ result = subprocess.run(
101
+ ["nasm", "-f", "bin", asm_path, "-o", bin_path],
102
+ capture_output=True, text=True,
103
+ )
104
+ if result.returncode != 0:
105
+ raise BuildError(f"NASM assembly failed:\n{result.stderr}")
106
+
107
+ # Copy to output path
108
+ output_path = os.path.abspath(prog.output)
109
+ shutil.copy2(bin_path, output_path)
110
+
111
+ size = os.path.getsize(output_path)
112
+ print(f"✓ Built {output_path} ({size} bytes)")
113
+ _set_program(None)
114
+ return output_path
115
+
116
+
117
+ def dump_asm() -> str:
118
+ """
119
+ Return the generated NASM source code (without building).
120
+ Useful for debugging or learning what your Python code produces.
121
+
122
+ Example:
123
+ print(osmk.dump_asm())
124
+ """
125
+ prog = _require_started()
126
+ return prog.generate_asm()
127
+
128
+
129
+ def run(memory: int = 128) -> None:
130
+ """
131
+ Build (if needed) and launch in QEMU.
132
+
133
+ Args:
134
+ memory: VM RAM in MB.
135
+ """
136
+ prog = _require_started()
137
+ output = prog.output
138
+
139
+ if not os.path.exists(output):
140
+ build()
141
+ else:
142
+ # Already built; prog may be cleared, just run
143
+ pass
144
+
145
+ qemu = "qemu-system-i386" if prog.bits <= 16 else "qemu-system-x86_64"
146
+ if not shutil.which(qemu):
147
+ qemu = "qemu-system-x86_64" if qemu == "qemu-system-i386" else "qemu-system-i386"
148
+ if not shutil.which(qemu):
149
+ raise BuildError("QEMU not found. Install: sudo apt install qemu-system-x86")
150
+
151
+ print(f"🚀 Launching {output} in QEMU...")
152
+ subprocess.run([
153
+ qemu, "-drive", f"format=raw,file={output}",
154
+ "-m", str(memory), "-no-reboot", "-no-shutdown",
155
+ ])
156
+
157
+
158
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
159
+ # REGISTER SETTERS (mov reg, value)
160
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
161
+
162
+ def _set_reg(reg: Register, val: Union[int, Register]) -> None:
163
+ """Emit: mov <reg>, <val>"""
164
+ prog = _require_started()
165
+ prog.emit(f"mov {reg.name}, {_operand(val)}")
166
+
167
+
168
+ # Wire every register's __call__ to _set_reg
169
+ # so that osmk.ax(5) calls ax.__call__(5) → _set_reg(ax, 5)
170
+ for _robj in [
171
+ ax, bx, cx, dx, si, di, sp, bp,
172
+ al, ah, bl, bh, cl, ch, dl, dh,
173
+ eax, ebx, ecx, edx, esi, edi, esp, ebp,
174
+ ]:
175
+ _robj._emit_fn = _set_reg
176
+
177
+
178
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
179
+ # MOV (explicit)
180
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
181
+
182
+ def mov(dest: Operand, src: Operand) -> None:
183
+ """
184
+ MOV instruction. → mov dest, src
185
+
186
+ Example:
187
+ osmk.mov(ax, 0x0E)
188
+ osmk.mov(bx, ax)
189
+ """
190
+ prog = _require_started()
191
+ prog.emit(f"mov {_operand(dest)}, {_operand(src)}")
192
+
193
+
194
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
195
+ # ARITHMETIC
196
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
197
+
198
+ def add(dest: Operand, src: Operand) -> None:
199
+ """ADD instruction. → add dest, src"""
200
+ prog = _require_started()
201
+ prog.emit(f"add {_operand(dest)}, {_operand(src)}")
202
+
203
+
204
+ def sub(dest: Operand, src: Operand) -> None:
205
+ """SUB instruction. → sub dest, src"""
206
+ prog = _require_started()
207
+ prog.emit(f"sub {_operand(dest)}, {_operand(src)}")
208
+
209
+
210
+ def mul(src: Operand) -> None:
211
+ """MUL instruction (unsigned AX = AL * src). → mul src"""
212
+ prog = _require_started()
213
+ prog.emit(f"mul {_operand(src)}")
214
+
215
+
216
+ def div(src: Operand) -> None:
217
+ """DIV instruction (unsigned AX / src → AL=quotient, AH=remainder). → div src"""
218
+ prog = _require_started()
219
+ prog.emit(f"div {_operand(src)}")
220
+
221
+
222
+ def inc(dest: Operand) -> None:
223
+ """INC instruction. → inc dest"""
224
+ prog = _require_started()
225
+ prog.emit(f"inc {_operand(dest)}")
226
+
227
+
228
+ def dec(dest: Operand) -> None:
229
+ """DEC instruction. → dec dest"""
230
+ prog = _require_started()
231
+ prog.emit(f"dec {_operand(dest)}")
232
+
233
+
234
+ def neg(dest: Operand) -> None:
235
+ """NEG instruction (two's complement). → neg dest"""
236
+ prog = _require_started()
237
+ prog.emit(f"neg {_operand(dest)}")
238
+
239
+
240
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
241
+ # BITWISE / LOGIC
242
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
243
+
244
+ def and_(dest: Operand, src: Operand) -> None:
245
+ """AND instruction. → and dest, src"""
246
+ prog = _require_started()
247
+ prog.emit(f"and {_operand(dest)}, {_operand(src)}")
248
+
249
+
250
+ def or_(dest: Operand, src: Operand) -> None:
251
+ """OR instruction. → or dest, src"""
252
+ prog = _require_started()
253
+ prog.emit(f"or {_operand(dest)}, {_operand(src)}")
254
+
255
+
256
+ def xor(dest: Operand, src: Operand) -> None:
257
+ """XOR instruction. → xor dest, src"""
258
+ prog = _require_started()
259
+ prog.emit(f"xor {_operand(dest)}, {_operand(src)}")
260
+
261
+
262
+ def not_(dest: Operand) -> None:
263
+ """NOT instruction. → not dest"""
264
+ prog = _require_started()
265
+ prog.emit(f"not {_operand(dest)}")
266
+
267
+
268
+ def shl(dest: Operand, count: Operand) -> None:
269
+ """Shift left. → shl dest, count"""
270
+ prog = _require_started()
271
+ prog.emit(f"shl {_operand(dest)}, {_operand(count)}")
272
+
273
+
274
+ def shr(dest: Operand, count: Operand) -> None:
275
+ """Shift right. → shr dest, count"""
276
+ prog = _require_started()
277
+ prog.emit(f"shr {_operand(dest)}, {_operand(count)}")
278
+
279
+
280
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
281
+ # COMPARE / JUMP / CONTROL FLOW
282
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
283
+
284
+ def cmp(a: Operand, b: Operand) -> None:
285
+ """CMP instruction. → cmp a, b"""
286
+ prog = _require_started()
287
+ prog.emit(f"cmp {_operand(a)}, {_operand(b)}")
288
+
289
+
290
+ def label(name: str) -> None:
291
+ """
292
+ Define a label at the current position.
293
+
294
+ Example:
295
+ osmk.label("loop")
296
+ osmk.dec(cx)
297
+ osmk.jnz("loop")
298
+ """
299
+ prog = _require_started()
300
+ # Labels go at column 0 (no indent) — we mark them specially
301
+ prog.instructions.append(f"\n{name}:")
302
+
303
+
304
+ def jmp(target: str) -> None:
305
+ """Unconditional jump. → jmp target"""
306
+ prog = _require_started()
307
+ prog.emit(f"jmp {target}")
308
+
309
+
310
+ def je(target: str) -> None:
311
+ """Jump if equal (ZF=1). → je target"""
312
+ prog = _require_started()
313
+ prog.emit(f"je {target}")
314
+
315
+
316
+ def jne(target: str) -> None:
317
+ """Jump if not equal. → jne target"""
318
+ prog = _require_started()
319
+ prog.emit(f"jne {target}")
320
+
321
+
322
+ def jl(target: str) -> None:
323
+ """Jump if less (signed). → jl target"""
324
+ prog = _require_started()
325
+ prog.emit(f"jl {target}")
326
+
327
+
328
+ def jg(target: str) -> None:
329
+ """Jump if greater (signed). → jg target"""
330
+ prog = _require_started()
331
+ prog.emit(f"jg {target}")
332
+
333
+
334
+ def jle(target: str) -> None:
335
+ """Jump if less or equal. → jle target"""
336
+ prog = _require_started()
337
+ prog.emit(f"jle {target}")
338
+
339
+
340
+ def jge(target: str) -> None:
341
+ """Jump if greater or equal. → jge target"""
342
+ prog = _require_started()
343
+ prog.emit(f"jge {target}")
344
+
345
+
346
+ def jz(target: str) -> None:
347
+ """Jump if zero. → jz target"""
348
+ prog = _require_started()
349
+ prog.emit(f"jz {target}")
350
+
351
+
352
+ def jnz(target: str) -> None:
353
+ """Jump if not zero. → jnz target"""
354
+ prog = _require_started()
355
+ prog.emit(f"jnz {target}")
356
+
357
+
358
+ def jc(target: str) -> None:
359
+ """Jump if carry. → jc target"""
360
+ prog = _require_started()
361
+ prog.emit(f"jc {target}")
362
+
363
+
364
+ def call(target: str) -> None:
365
+ """CALL a subroutine. → call target"""
366
+ prog = _require_started()
367
+ prog.emit(f"call {target}")
368
+
369
+
370
+ def ret() -> None:
371
+ """Return from subroutine. → ret"""
372
+ prog = _require_started()
373
+ prog.emit("ret")
374
+
375
+
376
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
377
+ # STACK
378
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
379
+
380
+ def push(src: Operand) -> None:
381
+ """PUSH to stack. → push src"""
382
+ prog = _require_started()
383
+ prog.emit(f"push {_operand(src)}")
384
+
385
+
386
+ def pop(dest: Operand) -> None:
387
+ """POP from stack. → pop dest"""
388
+ prog = _require_started()
389
+ prog.emit(f"pop {_operand(dest)}")
390
+
391
+
392
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
393
+ # INTERRUPTS
394
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
395
+
396
+ def interrupt(num: int) -> None:
397
+ """
398
+ Software interrupt. → int <num>
399
+
400
+ Common BIOS interrupts:
401
+ 0x10 — Video services
402
+ 0x13 — Disk services
403
+ 0x16 — Keyboard services
404
+ """
405
+ prog = _require_started()
406
+ prog.emit(f"int 0x{num:02X}")
407
+
408
+
409
+ def cli() -> None:
410
+ """Clear interrupt flag (disable interrupts)."""
411
+ prog = _require_started()
412
+ prog.emit("cli")
413
+
414
+
415
+ def sti() -> None:
416
+ """Set interrupt flag (enable interrupts)."""
417
+ prog = _require_started()
418
+ prog.emit("sti")
419
+
420
+
421
+ def hlt() -> None:
422
+ """Halt the CPU until next interrupt."""
423
+ prog = _require_started()
424
+ prog.emit("hlt")
425
+
426
+
427
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
428
+ # OUTPUT — HIGH-LEVEL HELPERS
429
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
430
+
431
+ def writehex() -> None:
432
+ """
433
+ Print AX as a 4-digit hex string (e.g. "000A").
434
+ Uses BIOS int 0x10 teletype output.
435
+
436
+ Example:
437
+ osmk.ax(255)
438
+ osmk.writehex() # prints "00FF"
439
+ """
440
+ prog = _require_started()
441
+ prog._has_writehex = True
442
+ prog.emit("call __writehex__")
443
+
444
+
445
+ def writechar(char: Union[str, int, None] = None) -> None:
446
+ """
447
+ Print a single character.
448
+
449
+ - No arg: prints whatever is currently in AL.
450
+ - str: prints that character.
451
+ - int: prints the ASCII character for that code.
452
+
453
+ Example:
454
+ osmk.writechar("A")
455
+ osmk.writechar(0x41) # also prints 'A'
456
+ """
457
+ prog = _require_started()
458
+ prog._has_writechar = True
459
+ if char is not None:
460
+ if isinstance(char, str):
461
+ prog.emit(f"mov al, '{char[0]}'")
462
+ else:
463
+ prog.emit(f"mov al, {char}")
464
+ prog.emit("call __writechar__")
465
+
466
+
467
+ def prints(text: str) -> None:
468
+ """
469
+ Print a string to the screen.
470
+
471
+ Defines the string in the data section and calls
472
+ the built-in print subroutine.
473
+
474
+ Example:
475
+ osmk.prints("Hello, World!")
476
+ """
477
+ prog = _require_started()
478
+ prog._has_print = True
479
+ lbl = prog.unique_label("str")
480
+ # Escape for NASM
481
+ safe = text.replace("'", "',0x27,'")
482
+ prog.emit_data(f"{lbl}: db '{safe}', 0")
483
+ prog.emit(f"mov si, {lbl}")
484
+ prog.emit("call __print__")
485
+
486
+
487
+ def newline() -> None:
488
+ """Print a CR+LF newline."""
489
+ prog = _require_started()
490
+ prog._has_newline = True
491
+ prog.emit("call __newline__")
492
+
493
+
494
+ def readchar() -> None:
495
+ """
496
+ Wait for a keypress. The ASCII code goes into AL.
497
+
498
+ Example:
499
+ osmk.readchar() # AL now has the key
500
+ osmk.writechar() # echo it back
501
+ """
502
+ prog = _require_started()
503
+ prog._has_readchar = True
504
+ prog.emit("call __readchar__")
505
+
506
+
507
+ def halt() -> None:
508
+ """
509
+ Halt the CPU in an infinite loop (cli + hlt).
510
+ If you don't call this, osmk adds one automatically at the end.
511
+ """
512
+ prog = _require_started()
513
+ prog._has_halt = True
514
+ prog.emit("cli")
515
+ prog.emit(".__halt_user:")
516
+ prog.emit("hlt")
517
+ prog.emit("jmp .__halt_user")
518
+
519
+
520
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
521
+ # CUSTOM / ESCAPE HATCH
522
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
523
+
524
+ def asm(code: str) -> None:
525
+ """
526
+ Inject raw NASM assembly. One or more lines.
527
+
528
+ Example:
529
+ osmk.asm("mov ah, 0x0E")
530
+ osmk.asm("int 0x10")
531
+
532
+ # Multi-line:
533
+ osmk.asm(\"\"\"
534
+ mov ah, 0x0E
535
+ mov al, 'X'
536
+ int 0x10
537
+ \"\"\")
538
+ """
539
+ prog = _require_started()
540
+ for line in code.strip().splitlines():
541
+ stripped = line.strip()
542
+ if stripped:
543
+ # If it's a label (ends with ':'), don't indent
544
+ if stripped.endswith(":"):
545
+ prog.instructions.append(f"\n{stripped}")
546
+ else:
547
+ prog.emit(stripped)
548
+
549
+
550
+ def raw(data: bytes) -> None:
551
+ """
552
+ Inject raw bytes into the binary.
553
+
554
+ Example:
555
+ osmk.raw(b"\\xEB\\xFE") # infinite loop: jmp $
556
+ """
557
+ prog = _require_started()
558
+ hex_str = ", ".join(f"0x{b:02X}" for b in data)
559
+ prog.emit(f"db {hex_str}")
560
+
561
+
562
+ def data(name: str, value: str) -> None:
563
+ """
564
+ Define a named data variable.
565
+
566
+ Example:
567
+ osmk.data("greeting", "db 'Hello!', 0")
568
+ osmk.asm("mov si, greeting")
569
+ """
570
+ prog = _require_started()
571
+ prog.emit_data(f"{name}: {value}")
572
+
573
+
574
+ def function(name: str) -> "_FunctionBuilder":
575
+ """
576
+ Define a callable subroutine.
577
+
578
+ Example:
579
+ with osmk.function("beep") as fn:
580
+ fn.asm("mov ah, 0x0E")
581
+ fn.asm("mov al, 0x07")
582
+ fn.asm("int 0x10")
583
+
584
+ osmk.call("beep")
585
+ """
586
+ prog = _require_started()
587
+ return _FunctionBuilder(prog, name)
588
+
589
+
590
+ class _FunctionBuilder:
591
+ """Context manager for building subroutines."""
592
+
593
+ def __init__(self, prog: _Program, name: str):
594
+ self.prog = prog
595
+ self.name = name
596
+ self.body: list[str] = []
597
+
598
+ def __enter__(self) -> "_FunctionBuilder":
599
+ return self
600
+
601
+ def __exit__(self, *_) -> None:
602
+ self.prog.subroutines[self.name] = self.body
603
+
604
+ def asm(self, code: str) -> None:
605
+ """Add raw assembly to this function."""
606
+ for line in code.strip().splitlines():
607
+ stripped = line.strip()
608
+ if stripped:
609
+ self.body.append(stripped)
610
+
611
+ def emit(self, instruction: str) -> None:
612
+ """Add a single instruction."""
613
+ self.body.append(instruction)
614
+
615
+
616
+ def comment(text: str) -> None:
617
+ """Add a comment to the assembly output."""
618
+ prog = _require_started()
619
+ prog.emit(f"; {text}")
osmk/engine.py ADDED
@@ -0,0 +1,261 @@
1
+ """
2
+ Core assembly engine — collects instructions and builds bootable images.
3
+
4
+ This is the heart of osmk. It accumulates x86 assembly instructions
5
+ from Python calls and compiles them into a bootable disk image or ISO.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ import tempfile
14
+ from pathlib import Path
15
+ from typing import Optional, Union
16
+
17
+ from osmk.registers import Register
18
+
19
+
20
+ class OSMKError(Exception):
21
+ """Base error for osmk."""
22
+ pass
23
+
24
+
25
+ class BuildError(OSMKError):
26
+ """Raised when NASM assembly or image creation fails."""
27
+ pass
28
+
29
+
30
+ class StateError(OSMKError):
31
+ """Raised when calling instructions before start()."""
32
+ pass
33
+
34
+
35
+ def _operand(val: Union[Register, int, str]) -> str:
36
+ """Convert a Python value to a NASM operand string."""
37
+ if isinstance(val, Register):
38
+ return val.name
39
+ if isinstance(val, int):
40
+ if val < 0:
41
+ return str(val)
42
+ return f"0x{val:X}" if val > 9 else str(val)
43
+ if isinstance(val, str):
44
+ return val # label name or raw operand
45
+ raise TypeError(f"Invalid operand: {val!r}")
46
+
47
+
48
+ class _Program:
49
+ """
50
+ Internal program state.
51
+
52
+ Tracks all emitted assembly lines, data, labels,
53
+ and the output filename.
54
+ """
55
+
56
+ def __init__(self, output: str):
57
+ self.output = output
58
+ self.bits = 16 # default: 16-bit real mode
59
+ self.org = 0x7C00 # default: MBR boot origin
60
+ self.instructions: list[str] = []
61
+ self.data_section: list[str] = []
62
+ self.label_counter = 0
63
+ self.subroutines: dict[str, list[str]] = {}
64
+ self._has_writehex = False
65
+ self._has_writechar = False
66
+ self._has_print = False
67
+ self._has_newline = False
68
+ self._has_readchar = False
69
+ self._has_halt = False
70
+
71
+ def emit(self, line: str) -> None:
72
+ """Add an assembly instruction."""
73
+ self.instructions.append(line)
74
+
75
+ def emit_data(self, line: str) -> None:
76
+ """Add a data definition."""
77
+ self.data_section.append(line)
78
+
79
+ def unique_label(self, prefix: str = "L") -> str:
80
+ """Generate a unique label name."""
81
+ self.label_counter += 1
82
+ return f"__{prefix}_{self.label_counter}__"
83
+
84
+ def generate_asm(self) -> str:
85
+ """Produce the full NASM source file."""
86
+ lines: list[str] = []
87
+ lines.append(f"; Generated by osmk")
88
+ lines.append(f"[BITS {self.bits}]")
89
+ lines.append(f"[ORG 0x{self.org:X}]")
90
+ lines.append("")
91
+
92
+ # ── Setup ──
93
+ lines.append("_start:")
94
+ lines.append(" xor ax, ax")
95
+ lines.append(" mov ds, ax")
96
+ lines.append(" mov es, ax")
97
+ lines.append("")
98
+
99
+ # ── User instructions ──
100
+ for inst in self.instructions:
101
+ lines.append(f" {inst}")
102
+ lines.append("")
103
+
104
+ # If user never explicitly halted, add a halt
105
+ if not self._has_halt:
106
+ lines.append(" ; --- end of program ---")
107
+ lines.append(" cli")
108
+ lines.append(".__halt:")
109
+ lines.append(" hlt")
110
+ lines.append(" jmp .__halt")
111
+ lines.append("")
112
+
113
+ # ── Built-in subroutines (only if used) ──
114
+ if self._has_writehex:
115
+ lines.extend(self._sub_writehex())
116
+ if self._has_writechar:
117
+ lines.extend(self._sub_writechar())
118
+ if self._has_print:
119
+ lines.extend(self._sub_print())
120
+ if self._has_newline:
121
+ lines.extend(self._sub_newline())
122
+ if self._has_readchar:
123
+ lines.extend(self._sub_readchar())
124
+
125
+ # ── User subroutines ──
126
+ for name, body in self.subroutines.items():
127
+ lines.append(f"{name}:")
128
+ for inst in body:
129
+ lines.append(f" {inst}")
130
+ lines.append(" ret")
131
+ lines.append("")
132
+
133
+ # ── Data section ──
134
+ if self.data_section:
135
+ lines.append("; --- data ---")
136
+ for d in self.data_section:
137
+ lines.append(d)
138
+ lines.append("")
139
+
140
+ # ── Boot signature ──
141
+ lines.append("; --- boot signature ---")
142
+ lines.append("times 510 - ($ - $$) db 0")
143
+ lines.append("dw 0xAA55")
144
+ lines.append("")
145
+
146
+ return "\n".join(lines)
147
+
148
+ # ── Built-in subroutines ──
149
+
150
+ def _sub_writehex(self) -> list[str]:
151
+ """Print AX as a 4-digit hex number using BIOS int 0x10."""
152
+ return [
153
+ "",
154
+ "; --- writehex: print AX as hex ---",
155
+ "__writehex__:",
156
+ " pusha",
157
+ " mov cx, 4 ; 4 hex digits",
158
+ ".__wh_loop:",
159
+ " rol ax, 4 ; rotate left to get next nibble",
160
+ " mov bx, ax",
161
+ " and bx, 0x0F ; mask nibble",
162
+ " cmp bl, 10",
163
+ " jl .__wh_digit",
164
+ " add bl, 'A' - 10 ; A-F",
165
+ " jmp .__wh_print",
166
+ ".__wh_digit:",
167
+ " add bl, '0' ; 0-9",
168
+ ".__wh_print:",
169
+ " mov ah, 0x0E",
170
+ " mov al, bl",
171
+ " int 0x10",
172
+ " dec cx",
173
+ " jnz .__wh_loop",
174
+ " popa",
175
+ " ret",
176
+ "",
177
+ ]
178
+
179
+ def _sub_writechar(self) -> list[str]:
180
+ """Print character in AL using BIOS int 0x10."""
181
+ return [
182
+ "",
183
+ "; --- writechar: print AL as char ---",
184
+ "__writechar__:",
185
+ " pusha",
186
+ " mov ah, 0x0E",
187
+ " int 0x10",
188
+ " popa",
189
+ " ret",
190
+ "",
191
+ ]
192
+
193
+ def _sub_print(self) -> list[str]:
194
+ """Print null-terminated string pointed to by SI."""
195
+ return [
196
+ "",
197
+ "; --- print: print string at SI ---",
198
+ "__print__:",
199
+ " pusha",
200
+ ".__pr_loop:",
201
+ " lodsb",
202
+ " test al, al",
203
+ " jz .__pr_done",
204
+ " mov ah, 0x0E",
205
+ " int 0x10",
206
+ " jmp .__pr_loop",
207
+ ".__pr_done:",
208
+ " popa",
209
+ " ret",
210
+ "",
211
+ ]
212
+
213
+ def _sub_newline(self) -> list[str]:
214
+ """Print CR+LF."""
215
+ return [
216
+ "",
217
+ "; --- newline ---",
218
+ "__newline__:",
219
+ " pusha",
220
+ " mov ah, 0x0E",
221
+ " mov al, 13",
222
+ " int 0x10",
223
+ " mov al, 10",
224
+ " int 0x10",
225
+ " popa",
226
+ " ret",
227
+ "",
228
+ ]
229
+
230
+ def _sub_readchar(self) -> list[str]:
231
+ """Read a key into AL using BIOS int 0x16."""
232
+ return [
233
+ "",
234
+ "; --- readchar: wait for key, result in AL ---",
235
+ "__readchar__:",
236
+ " mov ah, 0x00",
237
+ " int 0x16",
238
+ " ret",
239
+ "",
240
+ ]
241
+
242
+
243
+ # ── Module-level singleton ──
244
+
245
+ _current: Optional[_Program] = None
246
+
247
+
248
+ def _require_started() -> _Program:
249
+ """Ensure start() has been called."""
250
+ if _current is None:
251
+ raise StateError("Call osmk.start('output.img') before using instructions.")
252
+ return _current
253
+
254
+
255
+ def _get_program() -> Optional[_Program]:
256
+ return _current
257
+
258
+
259
+ def _set_program(prog: Optional[_Program]) -> None:
260
+ global _current
261
+ _current = prog
osmk/registers.py ADDED
@@ -0,0 +1,85 @@
1
+ """x86 register representations for the osmk DSL."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class Register:
7
+ """
8
+ Represents an x86 CPU register.
9
+
10
+ Registers are both objects and callable:
11
+ osmk.add(ax, bx) # use ax as operand
12
+ osmk.ax(5) # call ax to set it: mov ax, 5
13
+ """
14
+
15
+ def __init__(self, name: str, size: int = 16):
16
+ self.name = name
17
+ self.size = size # 8, 16, or 32 bits
18
+ self._emit_fn = None # set by engine after init
19
+
20
+ def __call__(self, val):
21
+ """Set this register: ax(5) → mov ax, 5"""
22
+ if self._emit_fn is None:
23
+ raise RuntimeError(
24
+ "Register setter not wired. Use osmk.ax(5) instead of bare ax(5), "
25
+ "or make sure osmk is imported."
26
+ )
27
+ self._emit_fn(self, val)
28
+
29
+ def __repr__(self) -> str:
30
+ return self.name
31
+
32
+ def __str__(self) -> str:
33
+ return self.name
34
+
35
+
36
+ # ── 16-bit General Purpose Registers ──
37
+ ax = Register("ax", 16)
38
+ bx = Register("bx", 16)
39
+ cx = Register("cx", 16)
40
+ dx = Register("dx", 16)
41
+
42
+ # ── 16-bit Index / Pointer Registers ──
43
+ si = Register("si", 16)
44
+ di = Register("di", 16)
45
+ sp = Register("sp", 16)
46
+ bp = Register("bp", 16)
47
+
48
+ # ── 8-bit Registers ──
49
+ al = Register("al", 8)
50
+ ah = Register("ah", 8)
51
+ bl = Register("bl", 8)
52
+ bh = Register("bh", 8)
53
+ cl = Register("cl", 8)
54
+ ch = Register("ch", 8)
55
+ dl = Register("dl", 8)
56
+ dh = Register("dh", 8)
57
+
58
+ # ── 32-bit Extended Registers ──
59
+ eax = Register("eax", 32)
60
+ ebx = Register("ebx", 32)
61
+ ecx = Register("ecx", 32)
62
+ edx = Register("edx", 32)
63
+ esi = Register("esi", 32)
64
+ edi = Register("edi", 32)
65
+ esp = Register("esp", 32)
66
+ ebp = Register("ebp", 32)
67
+
68
+ # ── Segment Registers ──
69
+ cs = Register("cs", 16)
70
+ ds = Register("ds", 16)
71
+ es = Register("es", 16)
72
+ fs = Register("fs", 16)
73
+ gs = Register("gs", 16)
74
+ ss = Register("ss", 16)
75
+
76
+ # All registers for easy import
77
+ ALL_REGISTERS = {
78
+ "ax": ax, "bx": bx, "cx": cx, "dx": dx,
79
+ "si": si, "di": di, "sp": sp, "bp": bp,
80
+ "al": al, "ah": ah, "bl": bl, "bh": bh,
81
+ "cl": cl, "ch": ch, "dl": dl, "dh": dh,
82
+ "eax": eax, "ebx": ebx, "ecx": ecx, "edx": edx,
83
+ "esi": esi, "edi": edi, "esp": esp, "ebp": ebp,
84
+ "cs": cs, "ds": ds, "es": es, "fs": fs, "gs": gs, "ss": ss,
85
+ }
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.4
2
+ Name: osmk
3
+ Version: 0.1.0
4
+ Summary: Write x86 operating systems in Python — one instruction at a time
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Keywords: os,assembly,x86,bootloader,osdev,low-level
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Education
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: System :: Operating System Kernels
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=7.0; extra == "dev"
17
+
18
+ # osmk
19
+
20
+ **Write x86 operating systems in Python — one instruction at a time.**
21
+
22
+ Every function call maps to an x86 assembly instruction. `build()` assembles it into a bootable 512-byte disk image.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install osmk
28
+ ```
29
+
30
+ You also need **NASM** (the assembler) and optionally **QEMU** (to run your OS):
31
+
32
+ ```bash
33
+ # Ubuntu/Debian
34
+ sudo apt install nasm qemu-system-x86
35
+
36
+ # macOS
37
+ brew install nasm qemu
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ import osmk
44
+ from osmk import ax, bx
45
+
46
+ osmk.start("hello.img") # begin a new OS
47
+
48
+ osmk.ax(5) # mov ax, 5
49
+ osmk.bx(5) # mov bx, 5
50
+ osmk.add(ax, bx) # add ax, bx → ax = 0x000A
51
+ osmk.writehex() # print AX as hex → "000A"
52
+
53
+ osmk.build() # assemble → hello.img (512 bytes)
54
+ ```
55
+
56
+ Run it:
57
+ ```bash
58
+ qemu-system-i386 -drive format=raw,file=hello.img
59
+ ```
60
+
61
+ ## API Reference
62
+
63
+ ### Lifecycle
64
+
65
+ | Function | Description |
66
+ |----------|-------------|
67
+ | `start(filename, bits=16, org=0x7C00)` | Begin a new OS program |
68
+ | `build()` | Assemble and create the bootable image |
69
+ | `dump_asm()` | Return the generated NASM source (for debugging) |
70
+ | `run(memory=128)` | Build and launch in QEMU |
71
+
72
+ ### Register Setters
73
+
74
+ Every register is callable. `osmk.ax(5)` → `mov ax, 5`.
75
+
76
+ ```python
77
+ osmk.ax(5) # mov ax, 5
78
+ osmk.bx(ax) # mov bx, ax
79
+ osmk.al(0x41) # mov al, 0x41
80
+ osmk.eax(0x1000) # mov eax, 0x1000
81
+ ```
82
+
83
+ 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`
84
+
85
+ ### Instructions
86
+
87
+ ```python
88
+ # Arithmetic
89
+ osmk.add(ax, bx) osmk.sub(ax, 1)
90
+ osmk.mul(bx) osmk.div(cx)
91
+ osmk.inc(ax) osmk.dec(cx)
92
+ osmk.neg(ax)
93
+
94
+ # Bitwise
95
+ osmk.and_(ax, 0xFF) osmk.or_(ax, bx)
96
+ osmk.xor(ax, ax) osmk.not_(ax)
97
+ osmk.shl(ax, 4) osmk.shr(ax, 1)
98
+
99
+ # Compare & Jump
100
+ osmk.cmp(ax, 0)
101
+ osmk.je("done") osmk.jne("loop")
102
+ osmk.jl("less") osmk.jg("greater")
103
+ osmk.jz("zero") osmk.jnz("nonzero")
104
+ osmk.jmp("start") osmk.jc("carry")
105
+
106
+ # Stack
107
+ osmk.push(ax) osmk.pop(bx)
108
+
109
+ # Subroutines
110
+ osmk.call("myfunc") osmk.ret()
111
+
112
+ # Interrupts
113
+ osmk.interrupt(0x10) # int 0x10
114
+ osmk.cli() osmk.sti()
115
+ osmk.hlt()
116
+
117
+ # Labels
118
+ osmk.label("loop")
119
+ ```
120
+
121
+ ### Output Helpers
122
+
123
+ ```python
124
+ osmk.writehex() # print AX as 4-digit hex (e.g. "000A")
125
+ osmk.writechar("A") # print character 'A'
126
+ osmk.writechar() # print whatever's in AL
127
+ osmk.prints("Hello!") # print a string
128
+ osmk.newline() # print CR+LF
129
+ osmk.readchar() # wait for keypress → AL
130
+ osmk.halt() # halt CPU (cli + hlt loop)
131
+ ```
132
+
133
+ ### Custom / Escape Hatch
134
+
135
+ When osmk doesn't cover what you need:
136
+
137
+ ```python
138
+ # Inject raw NASM assembly
139
+ osmk.asm("mov ah, 0x0E")
140
+ osmk.asm("""
141
+ mov ah, 0x00
142
+ mov al, 0x03
143
+ int 0x10
144
+ """)
145
+
146
+ # Inject raw bytes
147
+ osmk.raw(b"\x90\x90\x90") # 3x NOP
148
+
149
+ # Define data
150
+ osmk.data("my_var", "dw 0x1234")
151
+
152
+ # Define a subroutine
153
+ with osmk.function("beep") as fn:
154
+ fn.asm("mov ah, 0x0E")
155
+ fn.asm("mov al, 0x07") # BEL character
156
+ fn.asm("int 0x10")
157
+
158
+ osmk.call("beep")
159
+
160
+ # Add comments
161
+ osmk.comment("this sets up video mode")
162
+ ```
163
+
164
+ ### Explicit MOV
165
+
166
+ ```python
167
+ osmk.mov(ax, 0x0E) # same as osmk.ax(0x0E)
168
+ osmk.mov(bx, ax) # same as osmk.bx(ax)
169
+ ```
170
+
171
+ ## Examples
172
+
173
+ ### Hello World
174
+ ```python
175
+ import osmk
176
+
177
+ osmk.start("hello.img")
178
+ osmk.prints("Hello, World!")
179
+ osmk.newline()
180
+ osmk.prints("My first OS!")
181
+ osmk.halt()
182
+ osmk.build()
183
+ ```
184
+
185
+ ### Keyboard Echo
186
+ ```python
187
+ import osmk
188
+ from osmk import cx
189
+
190
+ osmk.start("echo.img")
191
+ osmk.prints("Type anything:")
192
+ osmk.newline()
193
+
194
+ osmk.label("loop")
195
+ osmk.readchar() # key → AL
196
+ osmk.writechar() # echo it
197
+ osmk.jmp("loop")
198
+
199
+ osmk.build()
200
+ ```
201
+
202
+ ### Counter
203
+ ```python
204
+ import osmk
205
+ from osmk import ax, cx
206
+
207
+ osmk.start("counter.img")
208
+
209
+ osmk.ax(0)
210
+ osmk.cx(16)
211
+
212
+ osmk.label("count")
213
+ osmk.writehex()
214
+ osmk.prints(" ")
215
+ osmk.inc(ax)
216
+ osmk.dec(cx)
217
+ osmk.jnz("count")
218
+
219
+ osmk.halt()
220
+ osmk.build()
221
+ ```
222
+
223
+ ## How It Works
224
+
225
+ 1. Each `osmk.*` call appends an x86 assembly instruction to an internal list
226
+ 2. `build()` wraps them in a bootloader template (16-bit real mode, MBR)
227
+ 3. NASM assembles it into a flat binary
228
+ 4. The binary is exactly 512 bytes with the `0xAA55` boot signature
229
+
230
+ 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.
231
+
232
+ ## Resources
233
+
234
+ - [OSDev Wiki](https://wiki.osdev.org/) — OS development reference
235
+ - [NASM Documentation](https://nasm.us/doc/) — assembler docs
236
+ - [x86 Instruction Reference](https://www.felixcloutier.com/x86/) — complete instruction set
237
+ - [Writing a Simple OS](https://www.cs.bham.ac.uk/~exr/lectures/opsys/10_11/lectures/os-dev.pdf) — Nick Blundell's guide
238
+ - [BIOS Interrupt List](https://en.wikipedia.org/wiki/BIOS_interrupt_call) — int 0x10, 0x13, 0x16, etc.
239
+
240
+ ## License
241
+
242
+ MIT
@@ -0,0 +1,7 @@
1
+ osmk/__init__.py,sha256=mPM3acC0Gwm-ivuWMIFOpwc-gevH38EvrKKhzJTt_gc,17319
2
+ osmk/engine.py,sha256=Nqn7CKRHOahhAqsTzuXsgAyoqQZeON2PmL899VP77Fc,7462
3
+ osmk/registers.py,sha256=jbDosSWkUjxOZOMMFH6_COQaAEFZlaCLQwPhanW8obw,2273
4
+ osmk-0.1.0.dist-info/METADATA,sha256=JvkmTIlki-W6Z36LWuV20srddjik7fG4gVqf0QA8drA,5709
5
+ osmk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ osmk-0.1.0.dist-info/top_level.txt,sha256=80_Iz5bMuRgmgHIm6jLSM_R8CXhwhpnkfo97QI-oyHQ,5
7
+ osmk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ osmk