# ViperOS syscall 与 Linux x86-64 ABI 对齐计划 状态:**P0–P6 完成**(含明确受限项;内核/应用构建通过,`fork`、`wait4`、信号投递已在 QEMU 冒烟验证) 最后更新:2026-10-28 目标:让 ViperOS 的 syscall 面逐步收敛到 Linux x86-64 ABI,最终能够**直接运行静态/动态链接的 glibc 程序**。 非目标(至少本计划内不做): - 网络协议栈相关 syscall(socket/connect/…) - ptrace / seccomp / cgroup / namespace - 完整 VFS(符号链接、硬链接、inode 语义) - 与 Linux 内核模块、io_uring、epoll 的兼容 --- ## 1. 现状 ### 1.1 入口与调用约定 | 项目 | 现状 | |---|---| | 指令 | `syscall`(MSR `STAR`/`LSTAR`/`FMASK`),另注册了 DPL=3 的 `int 0x80` 遗留门 | | 调用号 | `rax` | | 参数 | `rdi`, `rsi`, `rdx`, `r10`, `r8`, `r9`(与 Linux 一致) | | 返回 | `rax`;目前正负混用 | | 错误 | 无 `errno`、无 TLS;部分返回 `-1`,部分返回 FAT32 `FRESULT` **正数**,未知调用号返回 `0` | | 表 | `VKernel/Kernel/intr/syscall.vp` 与 `vpsdk/syscall.vp` 两份手工镜像 | ### 1.2 已实现的调用 权威列表见 `Scripts/syscall_table.py`(`build.py` 构建前自动同步内核与 SDK)。 截至 P6,Linux 号段已覆盖: ```text 文件 open/openat/close/read/write/pread/pwrite/lseek/stat/fstat/newfstatat/ getdents64/dup/dup2/dup3/fcntl/mkdir/unlink/rename/rmdir/getcwd/chdir/ access/faccessat/readlink/readlinkat 内存 brk/mmap/munmap/mprotect/mremap/madvise 进程 getpid/getppid/gettid/exit/exit_group/wait4/clone/fork/execve 线程 arch_prctl(FS)/set_tid_address/set_robust_list/futex/sched_yield 时间 clock_gettime/clock_getres/gettimeofday/nanosleep 信号 rt_sigaction/rt_sigprocmask/rt_sigreturn/kill IPC pipe/pipe2/readv/writev/poll(超时桩) 系统 uname/getuid/geteuid/getgid/getegid/prlimit64/getrandom/reboot ``` 私有段仅剩兼容别名(`LOAD_SO`/`SO_CALL1`/`SPAWN_PROC`/`THREAD_CREATE_OLD`/ `REBOOT`/`SHUTDOWN`)。 ### 1.3 已知偏差 - `execve(59)` 通过“加载新进程 + 退出调用者”模拟,PID 会改变 - `fork(57)` / `clone(56)` 会复制/共享地址空间;`CLONE_VM` 无 `CLONE_THREAD` 的 vfork 场景按同进程线程近似 - 信号只在 **syscall 返回** 时投递(没有中断返回钩子);`ucontext_t` 为自定义 紧凑布局,`siglongjmp` 不可用 - `futex` 为“查值 + yield”轮询;`poll` 恒返回 0(仅超时语义) - `exit_group(231)` 终止进程内所有线程;进程退出不回收管道/文件引用计数 - fork/exec 继承的非管道 fd 标记为两侧共享:close 只清槽位不释放对象(避免 交叉释放;代价是泄漏) - 资源上限:8 进程 / 每进程 8 线程 / 16 fd / 8 目录句柄 / 64 VMA / 8 管道 - P1 已补:`openat` / `fstat` / `newfstatat` / `getdents64` / `dup` / `fcntl`, 私有目录调用已删除 - P2 已补:`brk` / `mprotect` / `munmap` / `mremap` / `madvise`,文件映射 - P3–P6 已补:启动栈/auxv、`PT_INTERP`/ET_DYN、`execve`、`wait4`、Linux `clone`/`fork`、TLS、时间、管道、信号投递、`reboot(169)` ### 1.4 维护风险 两份 `enum Syscall` 靠人工同步。P0 起改为**生成**: ```text Scripts/syscall_table.py ← 唯一权威表 │ ├─ VKernel/Kernel/intr/syscall.vp [BEGIN/END GENERATED] └─ vpsdk/syscall.vp [BEGIN/END GENERATED] ``` `build.py` 在编译前运行生成器;两边不一致会直接覆盖,不再依赖记忆。 --- ## 2. 目标 ABI 契约 ### 2.1 调用与返回 - `rax` = 调用号,参数 `rdi/rsi/rdx/r10/r8/r9` - 成功:返回非负值(字节数、fd、地址、0…) - 失败:返回 `-errno`(`-ENOENT`、`-EINVAL`…),**不再返回 `FRESULT` 正数** - 未实现:`-ENOSYS` - 指针参数越界/为空:`-EFAULT` - 用户态 `errno` 由 glibc 从返回值设置,内核不维护 `errno` ### 2.2 数据结构 内核新增 `abi/linux.vp`(内核工程 include 增加仓库根目录),字段布局按 x86-64 Linux; SDK 侧需要结构体时可直接 `import abi.linux`。两者共用同一份定义,避免布局漂移。 | 类型 | 大小 | 用途 | |---|---:|---| | `abi.linux_stat` | 144 | `stat/fstat/newfstatat` | | `abi.linux_dirent64` | 19+name | `getdents64` | | `abi.linux_timespec` | 16 | 时间 | | `abi.linux_utsname` | 390 | `uname` | | `abi.linux_rlimit` | 16 | `prlimit64` | | `abi.iovec` | 16 | `readv/writev` | ### 2.3 启动栈(P3) glibc 不读 `_start(argc, argv)`,它要求内核按 Linux 方式布置用户栈: ```text [argc][argv0..argvN][NULL][envp...][NULL][auxv pairs][AT_NULL] ``` 必须提供的 auxv:`AT_PHDR`、`AT_PHENT`、`AT_PHNUM`、`AT_ENTRY`、`AT_BASE`、 `AT_PAGESZ`、`AT_RANDOM`、`AT_UID/EUID/GID/EGID`、`AT_SECURE=0`、 `AT_EXECFN`、`AT_SYSINFO_EHDR`(可选,无 vDSO 时省略)。 动态链接:`PT_INTERP` 指向的解释器按 ET_DYN 基址(`0x70000000`)加载, 主程序 PIE 基址 `0x60000000`,`AT_BASE` 指向解释器,`AT_PHDR/AT_ENTRY` 仍指向主程序。系统镜像中需要实际存在该解释器与 libc。 ### 2.4 TLS 与线程(P4) - `arch_prctl(ARCH_SET_FS)`:设置每线程 `IA32_FS_BASE` - 线程切换时保存/恢复 `FS_BASE` - `set_tid_address`、`set_robust_list`、`rseq`(可为 `-ENOSYS`) - futex 至少覆盖 `FUTEX_WAIT/FUTEX_WAKE`(glibc pthread 必需) ### 2.5 兼容口径 - `uname().sysname` 报 `"Linux"`,`release` 报一个 Linux 风格版本号;真实产品名放在 `version` 字段。这样大量脚本/构建系统不会因 `sysname != Linux` 走特殊分支。 - 文件权限位使用固定值:普通文件 `0644`、目录 `0755`;FAT32 没有 uid/gid。 --- ## 3. 阶段计划 ### P0 — ABI 契约与基础设施(本次完成,待编译验证) - [x] `abi/linux.vp`:errno、`O_*`、`AT_*`、`DT_*`、`PROT_*`、`MAP_*`、`ARCH_*`、结构体 - [x] 统一错误模型:Linux 号段调用返回 `-errno`;未知调用 `-ENOSYS` (私有 0x1000+ 段在 P6 删除前保留旧返回值) - [x] `open` 采用 Linux `O_*`,内核翻译到 FAT32 `FA_*` - [x] `lseek` 返回新偏移(而不是状态码),支持 64 位 offset(FAT32 上限内) - [x] 新调用:`uname` `getuid/geteuid/getgid/getegid` `getppid` `gettid` `getcwd` `chdir` `access` `faccessat` `readlink` `readlinkat` `set_tid_address` `set_robust_list` `rseq` `prlimit64` `getrandom` - [x] `Process.cwd`(默认 `/`) - [x] syscall 表由 `Scripts/syscall_table.py` 生成,`build.py` 自动同步 - [x] `vpsdk` 增加对应包装与常量 验收:现有应用行为不回退;所有失败路径可观察到负 errno;`Scripts/syscall_table.py --check` 通过。 ### P1 — 文件系统 ABI(本次完成,待编译验证) - [x] `openat(257)`(`AT_FDCWD` + 绝对路径 + cwd 相对路径) - [x] `fstat(5)`、`newfstatat(262)`、`statx(332)`(返回 `-ENOSYS`,glibc 回退) - [x] `getdents64(217)` + 目录 fd(`O_DIRECTORY`) - [x] `dup(32)` / `dup2(33)` / `dup3(292)`、`fcntl(72)`(`F_GETFL/F_SETFL/F_DUPFD`) - [x] `pread64(17)` / `pwrite64(18)` - [x] `rename(82)`、`rmdir(84)`、`unlinkat(263)` - [x] 删除私有 `OPENDIR/READDIR/CLOSEDIR` - [x] SDK 的 `opendir/readdir/closedir` 改由 `openat + getdents64 + close` 实现, Terminal 等调用方无需改动 验收:`ls` 走 `openat + getdents64`;SDK 的 `open` 也统一经 `openat(AT_FDCWD)`; `ld.so` 的 `openat/read/fstat/getdents64` 路径可用(文件映射属于 P2)。 P1 已知限制: - `getdents64` 每次只返回一条记录(glibc 合法,只是多一次 syscall) - `d_ino` 固定 1、`d_off` 固定 0;`telldir/seekdir/rewinddir` 暂不保证 - `dirfd != AT_FDCWD` 的相对路径查找返回 `-ENOTSUP`(P3/P6 再补目录 fd 反查) - `statx` 返回 `-ENOSYS`,glibc 回退到 `newfstatat` - `rename` 只支持同一 FAT32 卷内;`dup` 共享文件偏移,最后一次 `close` 才释放对象 ### P2 — 内存管理(本次完成,待编译验证) - [x] `brk(12)`:独立 brk 区(`0x40000000` 起),按页增长/收缩 - [x] `mmap` 真语义:`MAP_FIXED`、`PROT_*`、失败返回 `MAP_FAILED`(-1) - [x] FAT32 文件映射:把文件内容复制进新页(`MAP_PRIVATE` 语义) - [x] `munmap(11)` 真正解映射并把物理页归还 buddy - [x] `mprotect(10)` 修改 PTE 权限(支持 VMA 拆分) - [x] `mremap(25)`(`MREMAP_MAYMOVE` / `MREMAP_FIXED`)、`madvise(28)` - [x] 每进程 VMA 表(64 项),记录 base/size/prot/kind/flags - [x] SDK 的 `mmap` 把 `MAP_FAILED` 归一化为 0,既有 Explorer/devfs 调用方无需修改 验收:glibc 的 `malloc` 能使用 brk 主 arena;共享库文件可 mmap 后按段 mprotect; Explorer 的 framebuffer 映射不回退。 P2 已知限制: - 文件映射是“读入页”,不支持 `MAP_SHARED` 回写/`msync`(ld.so 用 `MAP_PRIVATE`) - `PROT_NONE` 仍映射为 present(可读),没有 NX;`PROT_EXEC` 仅记录 - `munmap` 只归还数据页,页表页仍留在 paging 池 - `brk` 区不在 VMA 表里(glibc 不会对它 munmap) - `mremap` 增长必须带 `MREMAP_MAYMOVE`,通过“新建+拷贝+解映射”实现 ### P3 — 进程、启动栈、解释器(本次完成) - [x] 用户栈按 Linux 布局写入 `argc/argv/envp/auxv` (AT_PHDR/PHENT/PHNUM/PAGESZ/ENTRY/BASE/RANDOM/EXECFN/UID/EUID/GID/EGID 等) - [x] `execve(59)` 原型 `(path, argv, envp)`;SDK 保留 `spawn` 走私有 `SPAWN_PROC` - [x] `exit_group(231)` 终止进程内所有线程;`exit(60)` 只退当前线程 - [x] `wait4(61)`(`WNOHANG`、状态字、子进程回收)、`getppid` - [x] `clone(56)`:`(flags, stack, ptid, ctid, tls)`;CLONE_VM 建线程, 不带 CLONE_VM 时复制地址空间 - [x] `fork(57)`:复制用户页 + 启动在父进程 syscall 返回点(RAX=0) - [x] `PT_INTERP` + ET_DYN 基址加载;`AT_BASE` 指向解释器 - [x] 旧的 ViperOS `thread_create(func,arg)` 移到私有 `THREAD_CREATE_OLD` 验收:静态非 PIE、动态 PIE(安装 `ld.so` 与 libc)都可从正确的启动栈进入; fork 子进程在父进程同一返回点继续执行,父进程得到子 pid。 ### P4 — TLS 与线程(本次完成) - [x] `arch_prctl(158)`:`SET_FS/GET_FS`(`SET_GS/GET_GS` 返回 `-ENOSYS`) - [x] 线程切换保存/恢复 `IA32_FS_BASE`(独立表,不改 `Thread` 的 600B 布局) - [x] `rseq(334)` 返回 `-ENOSYS`;`set_robust_list` 记录 - [x] `futex(202)`:`WAIT/WAKE` 采用“检查值 + sched_yield”轮询实现 - [x] `clone(CLONE_VM|CLONE_SETTLS)`:子线程从 post-syscall RIP 开始,FS=tls - [x] `CLONE_CHILD_CLEARTID`:线程退出时写 0,配合 futex 唤醒 join 验收:`pthread_create/join` 走 Linux clone 路径;TLS 随线程切换。 ### P5 — 时间、信号、IPC(本次完成) - [x] `clock_gettime(228)` / `clock_getres(229)`(RTC 秒 + 1ms tick) - [x] `gettimeofday(96)`、`nanosleep(35)` - [x] `readv(19)` / `writev(20)` - [x] `pipe(22)` / `pipe2(293)`(8 个管道 × 4KB,轮询读写) - [x] `poll(7)`(当前固定返回 0 = 超时) - [x] `rt_sigaction(13)` / `rt_sigprocmask(14)` / `kill(62)`:完整登记 - [x] 信号投递:syscall 返回前重写用户上下文,进入 handler; `rt_sigreturn(15)` 恢复原上下文与 syscall 返回值 - [ ] `ppoll` / `select` / `epoll` 验收:`signal()`/`sigaction()` 注册的 handler 能在下一次 syscall 返回时执行; `raise()`/`kill(getpid(), sig)` 可触发。 ### P6 — 清理私有段(部分完成) - [x] `reboot(169)`:校验 Linux magic,支持 RESTART / POWER_OFF - [x] SDK 的 `process.reboot/shutdown` 改用 `reboot(169)` - [~] 私有 `LOAD_SO/SO_CALL1` 保留(内核启动仍加载 `slog.so`,用户态链接器未实现) - [~] 私有 `SPAWN_PROC` / `THREAD_CREATE_OLD` 保留(Terminal 与旧 SDK) - [~] 私有 `REBOOT/SHUTDOWN` 保留作为兼容别名 - [ ] 用户态动态链接器 + 0x1000+ 段清空 验收:Linux `reboot` 可用;私有段尚未清零(受用户态加载器缺失限制)。 --- ## 4. 调用迁移表 `P0` = 本次;`—` 表示尚未实现(内核统一返回 `-ENOSYS`)。 | 号 | Linux | ViperOS | 状态 | 备注 | |---:|---|---|---|---| | 0 | read | READ | P0 | 字节数 / `-errno` | | 1 | write | WRITE | P0 | 同上 | | 2 | open | OPEN | P0 | Linux `O_*`,内核翻译 `FA_*` | | 3 | close | CLOSE | P0 | 0 / `-EBADF` | | 4 | stat | STAT | P0 | 保留私有 `fileinfo`(兼容旧 SDK) | | 5 | fstat | FSTAT | P1 | `struct stat` | | 7 | poll | POLL | P5 | 固定返回 0(超时) | | 8 | lseek | SEEK | P0 | 返回新偏移 | | 9 | mmap | MMAP | P2 | 匿名/文件/设备三种;失败 `MAP_FAILED` | | 10 | mprotect | MPROTECT | P2 | | | 11 | munmap | MUNMAP | P2 | | | 12 | brk | BRK | P2 | | | 13 | rt_sigaction | RT_SIGACTION | P5 | 登记 + 投递 | | 14 | rt_sigprocmask | RT_SIGPROCMASK | P5 | 支持 block/unblock/setmask | | 15 | rt_sigreturn | RT_SIGRETURN | P5 | 恢复原上下文 | | 16 | ioctl | IOCTL | P0 | devfs 子集 | | 17 | pread64 | PREAD64 | P1 | | | 18 | pwrite64 | PWRITE64 | P1 | | | 19 | readv | READV | P5 | | | 20 | writev | WRITEV | P5 | | | 21 | access | ACCESS | P0 | | | 22 | pipe | PIPE | P5 | 8 × 4KB,轮询 | | 24 | sched_yield | YIELD | P0 | | | 25 | mremap | MREMAP | P2 | 增长需 `MREMAP_MAYMOVE` | | 28 | madvise | MADVISE | P2 | 空操作 | | 32 | dup | DUP | P1 | 末次 close 才释放对象 | | 33 | dup2 | DUP2 | P1 | | | 35 | nanosleep | NANOSLEEP | P5 | 1ms 精度 | | 39 | getpid | GETPID | P0 | | | 56 | clone | CLONE | P3 | Linux clone;CLONE_VM 建线程 | | 57 | fork | FORK | P3 | 复制用户地址空间 | | 59 | execve | SPAWN | P3 | 新进程 + 退出调用者 | | 60 | exit | THREAD_EXIT | P0 | | | 61 | wait4 | WAIT4 | P3 | 支持 WNOHANG | | 62 | kill | KILL | P5 | 待下次 syscall 投递 | | 63 | uname | UNAME | P0 | | | 72 | fcntl | FCNTL | P1 | | | 79 | getcwd | GETCWD | P0 | | | 80 | chdir | CHDIR | P0 | | | 82 | rename | RENAME | P1 | | | 83 | mkdir | MKDIR | P0 | | | 84 | rmdir | RMDIR | P1 | | | 87 | unlink | REMOVE | P0 | | | 89 | readlink | READLINK | P0 | 当前恒 `-EINVAL` | | 96 | gettimeofday | GETTIMEOFDAY | P5 | RTC 秒 | | 102 | getuid | GETUID | P0 | 恒 0 | | 104 | getgid | GETGID | P0 | 恒 0 | | 107 | geteuid | GETEUID | P0 | 恒 0 | | 108 | getegid | GETEGID | P0 | 恒 0 | | 110 | getppid | GETPPID | P0 | 恒 0 | | 158 | arch_prctl | ARCH_PRCTL | P4 | SET_FS/GET_FS | | 169 | reboot | REBOOT_LINUX | P6 | magic 校验;RESTART/POWER_OFF | | 186 | gettid | GETTID | P0 | 返回线程 tid | | 202 | futex | FUTEX | P4 | 轮询 WAIT/WAKE | | 217 | getdents64 | GETDENTS64 | P1 | 每次返回一条记录 | | 218 | set_tid_address | SET_TID_ADDRESS | P0 | | | 228 | clock_gettime | CLOCK_GETTIME | P5 | RTC/1ms tick | | 229 | clock_getres | CLOCK_GETRES | P5 | 1ms | | 231 | exit_group | EXIT | P3 | 终止进程内所有线程 | | 257 | openat | OPENAT | P1 | | | 262 | newfstatat | NEWFSTATAT | P1 | | | 263 | unlinkat | UNLINKAT | P1 | | | 267 | readlinkat | READLINKAT | P0 | 当前恒 `-EINVAL` | | 269 | faccessat | FACCESSAT | P0 | | | 273 | set_robust_list | SET_ROBUST_LIST | P0 | 仅记录 | | 292 | dup3 | DUP3 | P1 | flags 忽略 | | 293 | pipe2 | PIPE2 | P5 | flags 仅记录 | | 302 | prlimit64 | PRLIMIT64 | P0 | 只读限制 | | 318 | getrandom | GETRANDOM | P0 | 软件 DRNG | | 332 | statx | — | P1 | `-ENOSYS`,glibc 回退 | | 334 | rseq | RSEQ | P0 | `-ENOSYS` | | 0x1006 | — | LOAD_SO | P6 删除 | | | 0x1007 | — | SO_CALL1 | P6 删除 | | | 0x1008 | — | REBOOT | P6 保留别名 | | | 0x1009 | — | SHUTDOWN | P6 保留别名 | | | 0x100A | — | SPAWN_PROC | P6 保留 | Terminal `run` 用 | | 0x100B | — | THREAD_CREATE_OLD | P6 保留 | 旧 `thread_create(func,arg)` | --- ## 5. 测试与验收 1. **ABI 单元测试**(用户态小程序) - 每个调用至少覆盖成功 + 一个错误路径,断言返回值为 `-errno` - 结构体偏移/大小与 Linux 对照(编译期 `__sizeof__`) 2. **`Scripts/syscall_table.py --check`** - CI/本地构建前检查两份表是否与权威表一致 3. **glibc 里程碑** - M1:静态 `hello`(write/exit/brk/mmap/arch_prctl) - M2:动态 `hello`(`ld.so`、`openat`、`mmap`、`mprotect`) - M3:`pthread` + `malloc` 压力测试 - M4:`ls`/`cat`/`sh` 等 coreutils 4. **回归** - 现有 7 个应用(Terminal / Explorer / Scene3D / Gargantua / FontDemo / HelloWorld / ImageSee)必须继续运行 ### 5.1 已完成的冒烟验证(2026-10-28,QEMU) - 7 个内置应用全部启动,进入主循环,无异常 - ImageSee 在同一启动中解码 PNG/JPEG 并在窗口显示(见 `docs/imagesee.md`) - 临时测试程序调用 `fork()`:父进程得到子 pid,子进程在同一返回点继续运行 (`getpid/getppid` 正确),`wait4` 返回子 pid 且状态为 `0x700`(exit code 7) - 信号:`rt_sigaction(SIGUSR1)` + `kill(self)` → handler 在 `kill` 返回时执行, `kill` 仍返回 0;`SIGUSR2` 被 `rt_sigprocmask` 阻塞时不投递,解除阻塞后的下一次 syscall 返回时投递 - 无人值守运行:`python build.py --headless --runsecs=45`(`-display none`, 45 秒后自动结束 QEMU,串口仍写入 `serial.log`) --- ## 6. 风险与已定决策 | 风险 | 处理 | |---|---| | 错误码改造影响旧应用 | SDK 已按 `<0` 判断;`FRESULT` 正数路径一并改掉,旧应用需回归测试 | | 两份表再次漂移 | 生成器 + `build.py` 前缀同步;禁止手改生成段 | | FAT32 无 inode/权限/符号链接 | `struct stat` 用固定值;`readlink` 一律 `-EINVAL`;`chmod` 后置 | | `mprotect` 未实现时 RELRO 失效 | P2 前在文档标注为“安全降级”,不假装成功 | | `getrandom` 无硬件熵 | 软件 xorshift + 启动期混合,仅用于 canary/非密码学场景 | | `uname` 报 Linux | 提升兼容性;`version` 字段保留 ViperOS 标识 | | 用户态栈布局改动大 | P3 单独提交,先静态后动态,避免与内存阶段交叉 |