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 +619 -0
- osmk/engine.py +261 -0
- osmk/registers.py +85 -0
- osmk-0.1.0.dist-info/METADATA +242 -0
- osmk-0.1.0.dist-info/RECORD +7 -0
- osmk-0.1.0.dist-info/WHEEL +5 -0
- osmk-0.1.0.dist-info/top_level.txt +1 -0
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 @@
|
|
|
1
|
+
osmk
|