""" 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("= 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")