Spaces:
Running on Zero
Running on Zero
| """ | |
| 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") | |