neural-doom / x86_linux.py
Quazim0t0's picture
Neural DOOM gradio app (chromatic dark)
e72b994 verified
Raw
History Blame Contribute Delete
11.2 kB
"""
x86_linux.py -- minimal Linux userland around the x86 core: static ELF loader,
stack/auxv setup, and the i386 int-0x80 syscall shim. This is the orchestrator
shell (all wiring) -- the analogue of the GB console around the SM83.
Just enough POSIX for a static musl binary: write/writev for output, read/open/
close/lseek over an in-memory FS, brk + anonymous mmap2 for malloc, the i386
TLS dance (set_thread_area + %gs), clock_gettime, and exit_group.
Special fds let a host front-end talk to the program without a kernel:
fd 100: DG_DrawFrame writes the framebuffer here (host captures frames)
fd 101: key events are read from here (host-provided queue)
"""
import struct, time
from x86_core import X86, CPUError, EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, M32
MEM_SIZE = 0x08000000 # 128 MB flat
STACK_TOP = 0x07FF0000
MMAP_BASE = 0x04000000
ENOSYS, EBADF, ENOENT, ENOTTY, EINVAL = 38, 9, 2, 25, 22
class Linux386:
def __init__(self, elf_bytes, argv=("prog",), fs=None, trace_sys=False):
self.mem = bytearray(MEM_SIZE)
self.cpu = X86(self.mem)
self.cpu.syscall = self.do_syscall
self.fs = dict(fs or {}) # path -> bytes
self.fds = {0: None, 1: None, 2: None}
self.next_fd = 3
self.stdout = bytearray()
self.frames = [] # fd 100 writes land here
self.keys = [] # fd 101 reads consume this
self.brk = 0
self.mmap_ptr = MMAP_BASE
self.trace = trace_sys
self.tls_base = 0
self.clock_ns = 0
self.load_elf(elf_bytes, argv)
# ---------------- ELF ----------------
def load_elf(self, b, argv):
assert b[:4] == b"\x7fELF" and b[4] == 1, "need ELF32"
e_entry, e_phoff = struct.unpack_from("<II", b, 24)
e_phentsize, e_phnum = struct.unpack_from("<HH", b, 42)
phdr_vaddr = 0
top = 0
for i in range(e_phnum):
off = e_phoff + i * e_phentsize
p_type, p_offset, p_vaddr, _, p_filesz, p_memsz, p_flags, _ = \
struct.unpack_from("<8I", b, off)
if p_type == 1: # PT_LOAD
self.mem[p_vaddr:p_vaddr + p_filesz] = b[p_offset:p_offset + p_filesz]
top = max(top, p_vaddr + p_memsz)
if p_offset <= e_phoff < p_offset + p_filesz:
phdr_vaddr = p_vaddr + (e_phoff - p_offset)
elif p_type == 6: # PT_PHDR
phdr_vaddr = p_vaddr
self.brk = (top + 0xFFF) & ~0xFFF
# ---- stack: argc argv envp auxv ----
sp = STACK_TOP
def push_bytes(data):
nonlocal sp
sp -= len(data); self.mem[sp:sp + len(data)] = data
return sp
arg_ptrs = [push_bytes(a.encode() + b"\0") for a in argv]
rnd = push_bytes(bytes(range(16)))
sp &= ~0xF
aux = [(3, phdr_vaddr), (4, e_phentsize), (5, e_phnum), (6, 4096),
(9, e_entry), (11, 1000), (12, 1000), (13, 1000), (14, 1000),
(16, 0), (17, 100), (23, 0), (25, rnd), (0, 0)]
blob = b""
for k, v in aux:
blob += struct.pack("<II", k, v)
blob = struct.pack("<I", len(argv)) \
+ b"".join(struct.pack("<I", p) for p in arg_ptrs) + b"\0\0\0\0" \
+ b"\0\0\0\0" + blob # empty envp
sp -= len(blob); sp &= ~0xF
self.mem[sp:sp + len(blob)] = blob
self.cpu.r[ESP] = sp
self.cpu.eip = e_entry
# ---------------- syscalls ----------------
def cstr(self, a):
e = self.mem.index(b"\0", a)
return self.mem[a:e].decode("latin1")
def do_syscall(self, cpu):
n = cpu.r[EAX]
a1, a2, a3 = cpu.r[EBX], cpu.r[ECX], cpu.r[EDX]
r = self.sys(n, a1, a2, a3, cpu)
if self.trace:
print(f" sys{n}({a1:#x},{a2:#x},{a3:#x}) = {r:#x}" if r >= 0
else f" sys{n} = -{-r}")
cpu.r[EAX] = r & M32
def sys(self, n, a1, a2, a3, cpu):
if n in (1, 252): # exit / exit_group
cpu.exited = a1
return 0
if n == 3 or n == 145: # read / readv
return self.do_read(n, a1, a2, a3)
if n == 4 or n == 146: # write / writev
return self.do_write(n, a1, a2, a3)
if n == 5: # open
path = self.cstr(a1)
if path not in self.fs:
if a2 & 0x40: # O_CREAT
self.fs[path] = b""
else:
return -ENOENT
if a2 & 0x200: # O_TRUNC
self.fs[path] = b""
self.fds[self.next_fd] = [path, 0]
self.next_fd += 1
return self.next_fd - 1
if n == 6: self.fds.pop(a1, None); return 0 # close
if n == 19 or n == 140: # lseek / _llseek
f = self.fds.get(a1)
if not f: return -EBADF
if n == 19:
off, whence = (a2 if a2 < 0x80000000 else a2 - (1 << 32)), a3
else:
off = (a2 << 32) | a3
whence = self.cpu.r[EDI]
size = len(self.fs[f[0]])
f[1] = off if whence == 0 else f[1] + off if whence == 1 else size + off
if n == 140:
self.cpu.wr(self.cpu.r[ESI], f[1], 8)
return 0
return f[1]
if n == 45: # brk
if a1: self.brk = a1
return self.brk
if n == 54: return -ENOTTY # ioctl
if n == 90 or n == 192: # mmap / mmap2
length = a2
ptr = self.mmap_ptr
self.mmap_ptr = (self.mmap_ptr + length + 0xFFF) & ~0xFFF
return ptr
if n == 91: return 0 # munmap
if n == 125: return 0 # mprotect
if n in (174, 175, 126): return 0 # signals: ignore
if n == 197 or n == 195: # fstat64/stat64 -> zeros
self.mem[a2:a2 + 96] = bytes(96)
return 0
if n == 243: # set_thread_area
entry = self.cpu.rd(a1, 4)
base = self.cpu.rd(a1 + 4, 4)
self.cpu.gs_base = base
if entry == M32:
self.cpu.wr(a1, 6, 4) # assign entry 6
return 0
if n == 258: return 1 # set_tid_address -> tid
if n == 224: return 1 # gettid
if n == 20: return 1 # getpid
if n in (199, 200, 201, 202): return 1000 # get*id32
if n == 78: # gettimeofday (deterministic)
self.clock_ns += 1_000_000
self.cpu.wr(a1, self.clock_ns // 10**9, 4)
self.cpu.wr(a1 + 4, self.clock_ns % 10**9 // 1000, 4)
return 0
if n == 265 or n == 407: # clock_gettime(64), deterministic
self.clock_ns += 1_000_000
s, ns = self.clock_ns // 10**9, self.clock_ns % 10**9
if n == 265:
self.cpu.wr(a2, s, 4); self.cpu.wr(a2 + 4, ns, 4)
else:
self.cpu.wr(a2, s, 8); self.cpu.wr(a2 + 8, ns, 4)
return 0
if n == 162 or n == 158: # nanosleep / yield
return 0
if n == 122: # uname
self.mem[a1:a1 + 65 * 6] = bytes(65 * 6)
for i, s in enumerate([b"Linux", b"neural", b"5.0.0", b"#1", b"i686", b""]):
self.mem[a1 + 65 * i:a1 + 65 * i + len(s)] = s
return 0
if n == 33: return -ENOENT # access
if n == 221: return 0 # fcntl64
if n == 240: return 0 # futex
if n == 270: return 0 # tgkill
if n == 39: return 0 # mkdir
if n in (10, 38, 12): return 0 # unlink/rename/chdir
if n == 13: # time (deterministic)
self.clock_ns += 10**9
t = self.clock_ns // 10**9
if a1: self.cpu.wr(a1, t, 4)
return t
if n == 183: # getcwd
self.mem[a1:a1+2] = b"/" + bytes(1)
return 2
if n == 85: return -EINVAL # readlink
raise CPUError(f"unimplemented syscall {n}")
def do_read(self, n, fd, buf, cnt):
if fd == 101: # key queue
if not self.keys: return 0
data = bytes(self.keys[:cnt]); del self.keys[:cnt]
self.mem[buf:buf + len(data)] = data
return len(data)
f = self.fds.get(fd)
if not f: return -EBADF
if n == 145: # readv
total = 0
for i in range(cnt):
p = self.cpu.rd(buf + 8 * i, 4); l = self.cpu.rd(buf + 8 * i + 4, 4)
total += self.do_read(3, fd, p, l)
return total
data = self.fs[f[0]][f[1]:f[1] + cnt]
self.mem[buf:buf + len(data)] = data
f[1] += len(data)
return len(data)
def do_write(self, n, fd, buf, cnt):
if n == 146: # writev
total = 0
for i in range(cnt):
p = self.cpu.rd(buf + 8 * i, 4); l = self.cpu.rd(buf + 8 * i + 4, 4)
total += self.do_write(4, fd, p, l)
return total
data = bytes(self.mem[buf:buf + cnt])
if fd in (1, 2): self.stdout += data
elif fd == 100: self.frames.append(data)
elif fd in self.fds and self.fds[fd]:
path, off = self.fds[fd]
cur = self.fs[path]
if off > len(cur): cur = cur + bytes(off - len(cur))
self.fs[path] = cur[:off] + data + cur[off + len(data):]
self.fds[fd][1] = off + len(data)
return cnt
# ---------------- run ----------------
def run(self, max_instr=200_000_000):
cpu = self.cpu
for _ in range(max_instr):
cpu.step()
if cpu.exited is not None:
return cpu.exited
raise RuntimeError("instruction budget exhausted")