diff --git a/docs/syscall.md b/docs/syscall.md index c87c33f..32a1a85 100644 --- a/docs/syscall.md +++ b/docs/syscall.md @@ -697,8 +697,16 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2); ## 7. Wine 兼容说明 -- `wine/cleonos_wine_lib/runner.py` 当前已覆盖到 `0..80`(含 `stats/fd/exec_pathv_io`)。 -- `DL_*`(`77..79`)在 Wine 中当前为占位实现:`DL_OPEN=-1`、`DL_CLOSE=0`、`DL_SYM=0`。 -- framebuffer 相关 syscall(`81..83`)尚未在 Wine 中实现(会返回未支持路径)。 +- `wine/cleonos_wine_lib/runner.py` 当前已覆盖到 `0..83`(含 `DL_*`、`FB_*`)。 +- `DL_*`(`77..79`)在 Wine 中为“可运行兼容”实现: +- `DL_OPEN`:加载 guest ELF 到当前 Unicorn 地址空间,返回稳定 `handle`,并做引用计数。 +- `DL_SYM`:解析 ELF `SYMTAB/DYNSYM` 并返回 guest 可调用地址。 +- `DL_CLOSE`:引用计数归零后释放句柄。 +- `DL_*` 兼容限制:未实现完整动态链接器语义(例如完整重定位/依赖库链),但对 CLeonOS 现有用户态库调用场景可工作。 +- framebuffer syscall(`81..83`)在 Wine 中已实现兼容: +- `FB_INFO` 返回 framebuffer 参数(默认 `1280x800x32`,可用环境变量 `CLEONOS_WINE_FB_WIDTH/HEIGHT` 调整)。 +- `FB_BLIT` 实现内核同类参数校验并支持 `scale>=1` 绘制。 +- 配合 Wine 参数 `--fb-window` 可将 framebuffer 实时显示到主机窗口(pygame 后端);未启用时保持内存缓冲模式。 +- `FB_CLEAR` 支持清屏颜色写入。 - Wine 在运行时崩溃场景下会生成与内核一致格式的“信号编码退出状态”,可通过 `WAITPID` 读取。 - Wine 当前音频 syscall 为占位实现:`AUDIO_AVAILABLE=0`,`AUDIO_PLAY_TONE=0`,`AUDIO_STOP=1`。 diff --git a/wine/README.md b/wine/README.md index 0aaaff7..4f51dc8 100644 --- a/wine/README.md +++ b/wine/README.md @@ -13,7 +13,7 @@ CLeonOS-Wine 现在改为自研运行器:基于 Python + Unicorn,直接运 - `wine/cleonos_wine_lib/input_pump.py`:主机键盘输入线程 - `wine/cleonos_wine_lib/constants.py`:常量与 syscall ID - `wine/cleonos_wine_lib/platform.py`:Unicorn 导入与平台适配 -- `wine/requirements.txt`:Python 依赖(Unicorn) +- `wine/requirements.txt`:Python 依赖(Unicorn + pygame) ## 安装 @@ -26,6 +26,7 @@ pip install -r wine/requirements.txt ```bash python wine/cleonos_wine.py /hello.elf --rootfs build/x86_64/ramdisk_root python wine/cleonos_wine.py /shell/shell.elf --rootfs build/x86_64/ramdisk_root +python wine/cleonos_wine.py /shell/qrcode.elf --rootfs build/x86_64/ramdisk_root --fb-window -- --ecc H "hello wine" ``` 也支持直接传宿主路径: @@ -37,7 +38,7 @@ python wine/cleonos_wine.py build/x86_64/ramdisk_root/shell/shell.elf --rootfs b ## 支持 - ELF64 (x86_64) PT_LOAD 段装载 -- CLeonOS `int 0x80` syscall 0..80(含 `FD_*`、`PROC_*`、`STATS_*`、`EXEC_PATHV_IO`) +- CLeonOS `int 0x80` syscall 0..83(含 `FD_*`、`DL_*`、`FB_*`、`PROC_*`、`STATS_*`、`EXEC_PATHV_IO`) - TTY 输出与键盘输入队列 - rootfs 文件/目录访问(`FS_*`) - `/temp` 写入限制(`FS_MKDIR/WRITE/APPEND/REMOVE`) @@ -48,11 +49,20 @@ python wine/cleonos_wine.py build/x86_64/ramdisk_root/shell/shell.elf --rootfs b - 进程枚举与快照(`PROC_COUNT/PROC_PID_AT/PROC_SNAPSHOT/PROC_KILL`) - syscall 统计(`STATS_TOTAL/STATS_ID_COUNT/STATS_RECENT_*`) - 文件描述符(`FD_OPEN/FD_READ/FD_WRITE/FD_CLOSE/FD_DUP`) +- 动态库兼容加载(`DL_OPEN/DL_CLOSE/DL_SYM`,基于 ELF 符号解析) +- framebuffer 兼容(`FB_INFO/FB_BLIT/FB_CLEAR`,支持内存缓冲与窗口显示) - 异常退出状态编码与故障元信息(`PROC_LAST_SIGNAL/PROC_FAULT_*`) ## 参数 - `--no-kbd`:关闭输入线程 +- `--fb-window`:启用 framebuffer 窗口显示(pygame) +- `--fb-scale N`:窗口缩放倍数(默认 `2`) +- `--fb-max-fps N`:窗口刷新上限(默认 `60`) +- `--fb-hold-ms N`:程序退出后窗口保留毫秒数(默认 `2500`,静态图更容易看清) +- `--argv-line "..."`:直接指定 guest 参数行(等价于 shell 参数字符串) +- `--cwd PATH`:写入命令上下文中的工作目录(默认 `/`) +- `--` 之后内容:作为 guest argv 透传(推荐) - `--max-exec-depth N`:设置 exec 嵌套深度上限 - `--verbose`:打印更多日志 diff --git a/wine/cleonos_wine_lib/__pycache__/cli.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/cli.cpython-313.pyc index f83dbe1..5769293 100644 Binary files a/wine/cleonos_wine_lib/__pycache__/cli.cpython-313.pyc and b/wine/cleonos_wine_lib/__pycache__/cli.cpython-313.pyc differ diff --git a/wine/cleonos_wine_lib/__pycache__/constants.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/constants.cpython-313.pyc index ede9991..350f806 100644 Binary files a/wine/cleonos_wine_lib/__pycache__/constants.cpython-313.pyc and b/wine/cleonos_wine_lib/__pycache__/constants.cpython-313.pyc differ diff --git a/wine/cleonos_wine_lib/__pycache__/fb_window.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/fb_window.cpython-313.pyc new file mode 100644 index 0000000..95fa9d0 Binary files /dev/null and b/wine/cleonos_wine_lib/__pycache__/fb_window.cpython-313.pyc differ diff --git a/wine/cleonos_wine_lib/__pycache__/runner.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/runner.cpython-313.pyc index dea5b2a..8ef94a4 100644 Binary files a/wine/cleonos_wine_lib/__pycache__/runner.cpython-313.pyc and b/wine/cleonos_wine_lib/__pycache__/runner.cpython-313.pyc differ diff --git a/wine/cleonos_wine_lib/cli.py b/wine/cleonos_wine_lib/cli.py index 4f50527..e04a4ed 100644 --- a/wine/cleonos_wine_lib/cli.py +++ b/wine/cleonos_wine_lib/cli.py @@ -1,18 +1,63 @@ from __future__ import annotations import argparse +import shlex import sys +from pathlib import Path +from typing import List from .constants import DEFAULT_MAX_EXEC_DEPTH from .runner import CLeonOSWineNative, resolve_elf_target, resolve_rootfs from .state import SharedKernelState +def _encode_cstr(text: str, size: int) -> bytes: + if size <= 0: + return b"" + + data = text.encode("utf-8", errors="replace") + if len(data) >= size: + data = data[: size - 1] + return data + (b"\x00" * (size - len(data))) + + +def _normalize_guest_cwd(cwd: str) -> str: + value = (cwd or "").strip().replace("\\", "/") + if not value: + return "/" + if not value.startswith("/"): + value = "/" + value + return value + + +def _write_command_context(rootfs: Path, guest_path: str, guest_args: List[str], cwd: str) -> None: + name = Path(guest_path).name + cmd = name[:-4] if name.lower().endswith(".elf") else name + arg = " ".join(guest_args) + ctx_payload = b"".join( + ( + _encode_cstr(cmd, 32), + _encode_cstr(arg, 160), + _encode_cstr(_normalize_guest_cwd(cwd), 192), + ) + ) + ctx_path = rootfs / "temp" / ".ush_cmd_ctx.bin" + ctx_path.parent.mkdir(parents=True, exist_ok=True) + ctx_path.write_bytes(ctx_payload) + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="CLeonOS-Wine: run CLeonOS ELF with Unicorn.") parser.add_argument("elf", help="Target ELF path. Supports /guest/path or host file path.") parser.add_argument("--rootfs", help="Rootfs directory (default: build/x86_64/ramdisk_root).") parser.add_argument("--no-kbd", action="store_true", help="Disable host keyboard input pump.") + parser.add_argument("--fb-window", action="store_true", help="Enable host framebuffer window (pygame backend).") + parser.add_argument("--fb-scale", type=int, default=2, help="Framebuffer window scale factor (default: 2).") + parser.add_argument("--fb-max-fps", type=int, default=60, help="Framebuffer present FPS limit (default: 60).") + parser.add_argument("--fb-hold-ms", type=int, default=2500, help="Keep fb window visible after app exits (ms).") + parser.add_argument("--argv-line", default="", help="Guest argv as one line (whitespace-separated).") + parser.add_argument("--cwd", default="/", help="Guest cwd for command-context apps.") + parser.add_argument("guest_args", nargs="*", help="Guest args (for dash-prefixed args use '--').") parser.add_argument("--max-exec-depth", type=int, default=DEFAULT_MAX_EXEC_DEPTH, help="Nested exec depth guard.") parser.add_argument("--verbose", action="store_true", help="Enable verbose runner output.") return parser.parse_args() @@ -20,6 +65,8 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() + guest_args = list(args.guest_args or []) + argv_items: List[str] try: rootfs = resolve_rootfs(args.rootfs) @@ -28,6 +75,24 @@ def main() -> int: print(f"[WINE][ERROR] {exc}", file=sys.stderr) return 2 + if len(guest_args) > 0 and guest_args[0] == "--": + guest_args = guest_args[1:] + + if (args.argv_line or "").strip(): + try: + guest_args = shlex.split(args.argv_line) + except Exception: + guest_args = (args.argv_line or "").split() + + argv_items = [guest_path] + argv_items.extend(guest_args) + + if guest_args: + try: + _write_command_context(rootfs, guest_path, guest_args, args.cwd) + except Exception as exc: + print(f"[WINE][WARN] failed to write command context: {exc}", file=sys.stderr) + if args.verbose: print("[WINE] backend=unicorn", file=sys.stderr) print(f"[WINE] rootfs={rootfs}", file=sys.stderr) @@ -44,6 +109,11 @@ def main() -> int: no_kbd=args.no_kbd, verbose=args.verbose, top_level=True, + fb_window=args.fb_window, + fb_scale=max(1, args.fb_scale), + fb_max_fps=max(1, args.fb_max_fps), + fb_hold_ms=max(0, args.fb_hold_ms), + argv_items=argv_items, ) ret = runner.run() if ret is None: @@ -51,4 +121,4 @@ def main() -> int: if args.verbose: print(f"\n[WINE] exit=0x{ret:016X}", file=sys.stderr) - return int(ret & 0xFF) \ No newline at end of file + return int(ret & 0xFF) diff --git a/wine/cleonos_wine_lib/constants.py b/wine/cleonos_wine_lib/constants.py index 310aa0f..5c5be38 100644 --- a/wine/cleonos_wine_lib/constants.py +++ b/wine/cleonos_wine_lib/constants.py @@ -89,6 +89,9 @@ SYS_DL_OPEN = 77 SYS_DL_CLOSE = 78 SYS_DL_SYM = 79 SYS_EXEC_PATHV_IO = 80 +SYS_FB_INFO = 81 +SYS_FB_BLIT = 82 +SYS_FB_CLEAR = 83 # proc states (from cleonos/c/include/cleonos_syscall.h) PROC_STATE_UNUSED = 0 diff --git a/wine/cleonos_wine_lib/fb_window.py b/wine/cleonos_wine_lib/fb_window.py new file mode 100644 index 0000000..57c9865 --- /dev/null +++ b/wine/cleonos_wine_lib/fb_window.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import sys +import time +from typing import Optional + +from .state import SharedKernelState + + +CLEONOS_KEY_LEFT = 0x01 +CLEONOS_KEY_RIGHT = 0x02 +CLEONOS_KEY_UP = 0x03 +CLEONOS_KEY_DOWN = 0x04 + + +class FBWindow: + def __init__(self, pygame_mod, width: int, height: int, scale: int, max_fps: int, *, verbose: bool = False) -> None: + self._pygame = pygame_mod + self.width = int(width) + self.height = int(height) + self.scale = max(1, int(scale)) + self._verbose = bool(verbose) + self._closed = False + self._last_present_ns = 0 + self._frame_interval_ns = int(1_000_000_000 / max_fps) if int(max_fps) > 0 else 0 + + window_w = self.width * self.scale + window_h = self.height * self.scale + + self._pygame.display.set_caption("CLeonOS Wine Framebuffer") + self._screen = self._pygame.display.set_mode((window_w, window_h)) + + @classmethod + def create( + cls, + width: int, + height: int, + scale: int, + max_fps: int, + *, + verbose: bool = False, + ) -> Optional["FBWindow"]: + try: + import pygame # type: ignore + except Exception as exc: + print(f"[WINE][WARN] framebuffer window disabled: pygame import failed ({exc})", file=sys.stderr) + return None + + try: + pygame.init() + pygame.display.init() + return cls(pygame, width, height, scale, max_fps, verbose=verbose) + except Exception as exc: + print(f"[WINE][WARN] framebuffer window disabled: unable to init display ({exc})", file=sys.stderr) + try: + pygame.quit() + except Exception: + pass + return None + + def close(self) -> None: + if self._closed: + return + + self._closed = True + try: + self._pygame.display.quit() + except Exception: + pass + try: + self._pygame.quit() + except Exception: + pass + + def is_closed(self) -> bool: + return self._closed + + def pump_input(self, state: SharedKernelState) -> None: + if self._closed: + return + + try: + events = self._pygame.event.get() + except Exception: + self.close() + return + + for event in events: + if event.type == self._pygame.QUIT: + self.close() + continue + + if event.type != self._pygame.KEYDOWN: + continue + + key = event.key + ch = 0 + + if key == self._pygame.K_LEFT: + ch = CLEONOS_KEY_LEFT + elif key == self._pygame.K_RIGHT: + ch = CLEONOS_KEY_RIGHT + elif key == self._pygame.K_UP: + ch = CLEONOS_KEY_UP + elif key == self._pygame.K_DOWN: + ch = CLEONOS_KEY_DOWN + elif key in (self._pygame.K_RETURN, self._pygame.K_KP_ENTER): + ch = ord("\n") + elif key == self._pygame.K_BACKSPACE: + ch = 8 + elif key == self._pygame.K_ESCAPE: + ch = 27 + elif key == self._pygame.K_TAB: + ch = ord("\t") + else: + text = getattr(event, "unicode", "") + if isinstance(text, str) and len(text) == 1: + code = ord(text) + if 32 <= code <= 126: + ch = code + + if ch != 0: + state.push_key(ch) + + def present(self, pixels_bgra: bytearray, *, force: bool = False) -> bool: + if self._closed: + return False + + now_ns = time.monotonic_ns() + if not force and self._frame_interval_ns > 0 and (now_ns - self._last_present_ns) < self._frame_interval_ns: + return False + + try: + src = self._pygame.image.frombuffer(pixels_bgra, (self.width, self.height), "BGRA") + if self.scale != 1: + src = self._pygame.transform.scale(src, (self.width * self.scale, self.height * self.scale)) + self._screen.blit(src, (0, 0)) + self._pygame.display.flip() + self._last_present_ns = now_ns + return True + except Exception: + self.close() + return False diff --git a/wine/cleonos_wine_lib/runner.py b/wine/cleonos_wine_lib/runner.py index 57b1384..cffb1de 100644 --- a/wine/cleonos_wine_lib/runner.py +++ b/wine/cleonos_wine_lib/runner.py @@ -4,7 +4,7 @@ import os import struct import sys import time -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -48,6 +48,9 @@ from .constants import ( SYS_FD_OPEN, SYS_FD_READ, SYS_FD_WRITE, + SYS_FB_BLIT, + SYS_FB_CLEAR, + SYS_FB_INFO, SYS_FS_APPEND, SYS_FS_CHILD_COUNT, SYS_FS_GET_CHILD_NAME, @@ -112,6 +115,7 @@ from .constants import ( u64, u64_neg1, ) +from .fb_window import FBWindow from .input_pump import InputPump from .platform import ( Uc, @@ -165,6 +169,19 @@ class FDEntry: tty_index: int = 0 +@dataclass +class DLImage: + handle: int + guest_path: str + host_path: str + owner_pid: int + ref_count: int + map_start: int + map_end: int + load_bias: int + symbols: Dict[str, int] = field(default_factory=dict) + + EXEC_PATH_MAX = 192 EXEC_ARG_LINE_MAX = 256 EXEC_ENV_LINE_MAX = 512 @@ -174,6 +191,14 @@ EXEC_ITEM_MAX = 128 EXEC_STATUS_SIGNAL_FLAG = 1 << 63 PROC_PATH_MAX = 192 FD_MAX = 64 +DL_MAX_NAME = 192 +DL_MAX_SYMBOL = 128 +DL_BASE_START = 0x0000000100000000 +DL_BASE_GAP = 0x0000000000100000 +FB_DEFAULT_WIDTH = 1280 +FB_DEFAULT_HEIGHT = 800 +FB_MAX_DIM = 4096 +FB_MAX_UPLOAD_BYTES = 64 * 1024 * 1024 class CLeonOSWineNative: @@ -194,6 +219,10 @@ class CLeonOSWineNative: argv_items: Optional[List[str]] = None, env_items: Optional[List[str]] = None, inherited_fds: Optional[Dict[int, FDEntry]] = None, + fb_window: bool = False, + fb_scale: int = 2, + fb_max_fps: int = 60, + fb_hold_ms: int = 2500, ) -> None: self.elf_path = elf_path self.rootfs = rootfs @@ -206,6 +235,10 @@ class CLeonOSWineNative: self.top_level = top_level self.pid = int(pid) self.ppid = int(ppid) + self.fb_window = bool(fb_window) + self.fb_scale = max(1, int(fb_scale)) + self.fb_max_fps = max(1, int(fb_max_fps)) + self.fb_hold_ms = max(0, int(fb_hold_ms)) self.argv_items = list(argv_items) if argv_items is not None else [] self.env_items = list(env_items) if env_items is not None else [] self._exit_requested = False @@ -222,6 +255,20 @@ class CLeonOSWineNative: self._tty_index = int(self.state.tty_active) self._fds: Dict[int, FDEntry] = {} self._fd_inherited = inherited_fds if inherited_fds is not None else {} + self._dl_images: Dict[int, DLImage] = {} + self._dl_path_to_handle: Dict[str, int] = {} + self._dl_next_handle = 1 + self._dl_next_base = DL_BASE_START + + self._fb_width = self._bounded_env_int("CLEONOS_WINE_FB_WIDTH", FB_DEFAULT_WIDTH, 64, FB_MAX_DIM) + self._fb_height = self._bounded_env_int("CLEONOS_WINE_FB_HEIGHT", FB_DEFAULT_HEIGHT, 64, FB_MAX_DIM) + self._fb_bpp = 32 + self._fb_pitch = self._fb_width * 4 + self._fb_pixels = bytearray(self._fb_pitch * self._fb_height) + self._fb_window: Optional[FBWindow] = None + self._fb_window_failed = False + self._fb_dirty = False + self._fb_presented_once = False default_path = self._normalize_guest_path(self.guest_path_hint or f"/{self.elf_path.name}") self.argv_items, self.env_items = self._prepare_exec_items(default_path, self.argv_items, self.env_items) @@ -250,6 +297,89 @@ class CLeonOSWineNative: mode = cls._fd_access_mode(flags) return mode in (O_WRONLY, O_RDWR) + @staticmethod + def _bounded_env_int(name: str, default: int, min_value: int, max_value: int) -> int: + raw = os.environ.get(name) + value = default + + if raw is not None: + try: + value = int(raw.strip(), 10) + except Exception: + value = default + + if value < min_value: + value = min_value + if value > max_value: + value = max_value + return value + + def _ensure_fb_window(self) -> None: + if not self.fb_window: + return + + if self._fb_window is not None or self._fb_window_failed: + return + + self._fb_window = FBWindow.create( + self._fb_width, + self._fb_height, + self.fb_scale, + self.fb_max_fps, + verbose=self.verbose, + ) + + if self._fb_window is None: + self._fb_window_failed = True + + def _fb_poll_window(self) -> None: + self._ensure_fb_window() + if self._fb_window is not None: + self._fb_window.pump_input(self.state) + if self._fb_window.is_closed(): + self._fb_window = None + + def _fb_present(self, *, force: bool = False) -> None: + self._ensure_fb_window() + if self._fb_window is None: + return + + self._fb_window.pump_input(self.state) + did_present = self._fb_window.present(self._fb_pixels, force=force) + if did_present: + self._fb_dirty = False + self._fb_presented_once = True + + if self._fb_window.is_closed(): + self._fb_window = None + + def _fb_mark_dirty(self) -> None: + self._fb_dirty = True + self._fb_present(force=False) + + def _fb_hold_after_exit(self) -> None: + end_ns: int + + if self._fb_window is None: + return + + if self.fb_hold_ms <= 0 or self._fb_presented_once is False: + return + + end_ns = time.monotonic_ns() + (self.fb_hold_ms * 1_000_000) + + while time.monotonic_ns() < end_ns: + if self._fb_window is None: + return + + self._fb_window.pump_input(self.state) + if self._fb_window.is_closed(): + self._fb_window = None + return + + self._fb_window.present(self._fb_pixels, force=True) + time.sleep(0.016) + def _init_default_fds(self) -> None: self._fds = { 0: FDEntry(kind="tty", flags=O_RDONLY, offset=0, tty_index=self._tty_index), @@ -325,6 +455,8 @@ class CLeonOSWineNative: self._install_hooks(uc) self._load_segments(uc) self._prepare_stack_and_return(uc) + self._ensure_fb_window() + self._fb_present(force=True) if self.top_level and not self.no_kbd: self._input_pump = InputPump(self.state) @@ -346,6 +478,11 @@ class CLeonOSWineNative: finally: if self.top_level and self._input_pump is not None: self._input_pump.stop() + if self._fb_window is not None: + self._fb_hold_after_exit() + if self._fb_window is not None: + self._fb_window.close() + self._fb_window = None if interrupted: self.state.mark_exited(self.pid, u64_neg1()) @@ -391,6 +528,8 @@ class CLeonOSWineNative: self._reg_write(uc, UC_X86_REG_RAX, u64(ret)) def _dispatch_syscall(self, uc: Uc, sid: int, arg0: int, arg1: int, arg2: int) -> int: + self._fb_poll_window() + if sid == SYS_LOG_WRITE: data = self._read_guest_bytes(uc, arg0, arg1) text = data.decode("utf-8", errors="replace") @@ -563,11 +702,17 @@ class CLeonOSWineNative: if sid == SYS_FD_DUP: return self._fd_dup(arg0) if sid == SYS_DL_OPEN: - return u64_neg1() + return self._dl_open(uc, arg0) if sid == SYS_DL_CLOSE: - return 0 + return self._dl_close(arg0) if sid == SYS_DL_SYM: - return 0 + return self._dl_sym(uc, arg0, arg1) + if sid == SYS_FB_INFO: + return self._fb_info(uc, arg0) + if sid == SYS_FB_BLIT: + return self._fb_blit(uc, arg0) + if sid == SYS_FB_CLEAR: + return self._fb_clear(arg0) return u64_neg1() @@ -643,6 +788,12 @@ class CLeonOSWineNative: return True return False + def _range_overlaps_mapped(self, start: int, end: int) -> bool: + for ms, me in self._mapped_ranges: + if start < me and end > ms: + return True + return False + @staticmethod def _reg_read(uc: Uc, reg: int) -> int: return int(uc.reg_read(reg)) @@ -715,24 +866,23 @@ class CLeonOSWineNative: return False @staticmethod - def _parse_elf(path: Path) -> ELFImage: - data = path.read_bytes() + def _parse_elf_image_from_blob(data: bytes, *, require_entry: bool) -> ELFImage: if len(data) < 64: - raise RuntimeError(f"ELF too small: {path}") + raise RuntimeError("ELF too small") if data[0:4] != b"\x7fELF": - raise RuntimeError(f"invalid ELF magic: {path}") + raise RuntimeError("invalid ELF magic") if data[4] != 2 or data[5] != 1: - raise RuntimeError(f"unsupported ELF class/endianness: {path}") + raise RuntimeError("unsupported ELF class/endianness") entry = struct.unpack_from(" ELFImage: + data = path.read_bytes() + try: + return CLeonOSWineNative._parse_elf_image_from_blob(data, require_entry=True) + except RuntimeError as exc: + raise RuntimeError(f"{exc}: {path}") from exc + def _fs_node_count(self) -> int: count = 1 for _root, dirs, files in os.walk(self.rootfs): @@ -1087,6 +1245,10 @@ class CLeonOSWineNative: argv_items=argv_items, env_items=env_items, inherited_fds=child_stdio, + fb_window=self.fb_window, + fb_scale=self.fb_scale, + fb_max_fps=self.fb_max_fps, + fb_hold_ms=self.fb_hold_ms, ) child_ret = child.run() @@ -1445,6 +1607,403 @@ class CLeonOSWineNative: self._fds[slot] = self._clone_fd_entry(src) return slot + def _dl_alloc_handle(self) -> int: + handle = int(u64(self._dl_next_handle)) + if handle == 0: + handle = 1 + + while handle in self._dl_images or handle == 0: + handle = int(u64(handle + 1)) + if handle == 0: + handle = 1 + + self._dl_next_handle = int(u64(handle + 1)) + if self._dl_next_handle == 0: + self._dl_next_handle = 1 + + return handle + + def _dl_pick_base(self, min_vaddr: int, max_vaddr: int) -> Tuple[int, int, int]: + old_base = page_floor(min_vaddr) + span = page_ceil(max_vaddr - old_base) + candidate = page_ceil(self._dl_next_base) + retries = 0 + + if span <= 0: + return 0, 0, 0 + + while retries < 1024: + start = candidate + end = start + span + + if not self._range_overlaps_mapped(start, end): + self._dl_next_base = end + DL_BASE_GAP + return start, end, start - old_base + + candidate = end + DL_BASE_GAP + retries += 1 + + return 0, 0, 0 + + @staticmethod + def _dl_rebase_non_exec_segment(data: bytes, old_base: int, old_end: int, delta: int) -> bytes: + if not data or delta == 0: + return data + + patched = bytearray(data) + limit = len(patched) - (len(patched) % 8) + + for off in range(0, limit, 8): + value = struct.unpack_from(" str: + if start < 0 or end <= start or start >= len(blob): + return "" + + limit = min(end, len(blob)) + cur = start + while cur < limit and blob[cur] != 0: + cur += 1 + + if cur <= start: + return "" + + return blob[start:cur].decode("utf-8", errors="ignore") + + @classmethod + def _dl_extract_symbols(cls, blob: bytes, load_bias: int) -> Dict[str, int]: + symbols: Dict[str, int] = {} + sections: List[Tuple[int, int, int, int, int, int, int, int, int, int]] = [] + + if len(blob) < 0x40: + return symbols + + try: + shoff = struct.unpack_from(" len(blob): + return symbols + + for idx in range(shnum): + off = shoff + (idx * shentsize) + try: + sh = struct.unpack_from("= len(sections): + continue + + if sh_entsize < 24: + sh_entsize = 24 + + if sh_offset < 0 or sh_size <= 0 or sh_offset >= len(blob): + continue + + sym_end = min(len(blob), sh_offset + sh_size) + if sym_end <= sh_offset: + continue + + strtab = sections[sh_link] + str_off = int(strtab[4]) + str_size = int(strtab[5]) + + if str_size <= 0 or str_off < 0 or str_off >= len(blob): + continue + + str_end = min(len(blob), str_off + str_size) + count = (sym_end - sh_offset) // sh_entsize + + for i in range(count): + ent_off = sh_offset + (i * sh_entsize) + if ent_off + 24 > len(blob): + break + + try: + st_name, _st_info, _st_other, st_shndx, st_value, _st_size = struct.unpack_from( + " int: + guest_path = self._normalize_guest_path(self._read_guest_cstring(uc, path_ptr, DL_MAX_NAME)) + cached_handle = self._dl_path_to_handle.get(guest_path) + host_path: Optional[Path] + file_blob: bytes + image: ELFImage + e_type: int + min_vaddr: int + max_vaddr: int + map_start: int + map_end: int + load_bias: int + handle: int + owner_pid: int + + if not guest_path.startswith("/") or guest_path == "/": + return u64_neg1() + if len(guest_path.encode("utf-8", errors="replace")) >= DL_MAX_NAME: + return u64_neg1() + + if cached_handle is not None: + cached = self._dl_images.get(cached_handle) + if cached is not None: + cached.ref_count += 1 + return cached.handle + + host_path = self._guest_to_host(guest_path, must_exist=True) + if host_path is None or not host_path.is_file(): + return u64_neg1() + + try: + file_blob = host_path.read_bytes() + image = self._parse_elf_image_from_blob(file_blob, require_entry=False) + e_type = struct.unpack_from(" int: + key = int(handle) + image = self._dl_images.get(key) + + if key == 0 or image is None: + return u64_neg1() + + if image.ref_count > 1: + image.ref_count -= 1 + return 0 + + del self._dl_images[key] + if self._dl_path_to_handle.get(image.guest_path) == key: + del self._dl_path_to_handle[image.guest_path] + return 0 + + def _dl_sym(self, uc: Uc, handle: int, symbol_ptr: int) -> int: + symbol = self._read_guest_cstring(uc, symbol_ptr, DL_MAX_SYMBOL) + image = self._dl_images.get(int(handle)) + addr: Optional[int] + + if image is None or not symbol: + return 0 + + addr = image.symbols.get(symbol) + if addr is None and not symbol.startswith("_"): + addr = image.symbols.get(f"_{symbol}") + + return int(u64(addr)) if addr is not None else 0 + + def _fb_info(self, uc: Uc, out_ptr: int) -> int: + if out_ptr == 0: + return 0 + + self._ensure_fb_window() + + payload = struct.pack( + " int: + if len(self._fb_pixels) == 0: + return 0 + + pixel = struct.pack(" int: + req_blob = self._read_guest_bytes_exact(uc, int(req_ptr), 56) + pixels_ptr: int + src_width: int + src_height: int + src_pitch_bytes: int + dst_x: int + dst_y: int + scale: int + total_src_bytes: int + src_blob: Optional[bytes] + max_src_w: int + max_src_h: int + copy_w: int + copy_h: int + + if req_ptr == 0 or req_blob is None or len(req_blob) != 56: + return 0 + + pixels_ptr, src_width, src_height, src_pitch_bytes, dst_x, dst_y, scale = struct.unpack(" 4096 or src_height > 4096 or scale > 8: + return 0 + + if src_pitch_bytes == 0: + src_pitch_bytes = src_width * 4 + + if src_pitch_bytes < (src_width * 4): + return 0 + + if src_height > (u64_neg1() // src_pitch_bytes): + return 0 + + if dst_x >= self._fb_width or dst_y >= self._fb_height: + return 0 + + total_src_bytes = src_pitch_bytes * src_height + if total_src_bytes == 0 or total_src_bytes > FB_MAX_UPLOAD_BYTES: + return 0 + + src_blob = self._read_guest_bytes_exact(uc, pixels_ptr, total_src_bytes) + if src_blob is None: + return 0 + + max_src_w = (self._fb_width - dst_x + scale - 1) // scale + max_src_h = (self._fb_height - dst_y + scale - 1) // scale + copy_w = min(src_width, max_src_w) + copy_h = min(src_height, max_src_h) + + if copy_w <= 0 or copy_h <= 0: + return 0 + + if scale == 1: + row_bytes = copy_w * 4 + for y in range(copy_h): + src_off = y * src_pitch_bytes + dst_off = ((dst_y + y) * self._fb_width + dst_x) * 4 + self._fb_pixels[dst_off : dst_off + row_bytes] = src_blob[src_off : src_off + row_bytes] + else: + draw_row_pixels = min(self._fb_width - dst_x, copy_w * scale) + draw_row_bytes = draw_row_pixels * 4 + + for y in range(copy_h): + src_row_off = y * src_pitch_bytes + src_row = memoryview(src_blob)[src_row_off : src_row_off + (copy_w * 4)] + expanded = bytearray(copy_w * scale * 4) + write_off = 0 + + for x in range(copy_w): + pixel = bytes(src_row[x * 4 : (x + 1) * 4]) + expanded[write_off : write_off + (scale * 4)] = pixel * scale + write_off += scale * 4 + + draw_y = dst_y + (y * scale) + repeat_h = min(scale, self._fb_height - draw_y) + row_data = expanded[:draw_row_bytes] + + for sy in range(repeat_h): + dst_off = ((draw_y + sy) * self._fb_width + dst_x) * 4 + self._fb_pixels[dst_off : dst_off + draw_row_bytes] = row_data + + self._fb_mark_dirty() + + return 1 + @staticmethod def _truncate_item_text(text: str, max_bytes: int = EXEC_ITEM_MAX) -> str: if max_bytes <= 1: diff --git a/wine/requirements.txt b/wine/requirements.txt index 3873be6..c501798 100644 --- a/wine/requirements.txt +++ b/wine/requirements.txt @@ -1 +1,2 @@ unicorn>=2.0.1 +pygame>=2.5.2