18 KiB
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 号段已覆盖:
文件 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、Linuxclone/fork、TLS、时间、管道、信号投递、reboot(169)
1.4 维护风险
两份 enum Syscall 靠人工同步。P0 起改为生成:
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 方式布置用户栈:
[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 契约与基础设施(本次完成,待编译验证)
abi/linux.vp:errno、O_*、AT_*、DT_*、PROT_*、MAP_*、ARCH_*、结构体- 统一错误模型:Linux 号段调用返回
-errno;未知调用-ENOSYS(私有 0x1000+ 段在 P6 删除前保留旧返回值) open采用 LinuxO_*,内核翻译到 FAT32FA_*lseek返回新偏移(而不是状态码),支持 64 位 offset(FAT32 上限内)- 新调用:
unamegetuid/geteuid/getgid/getegidgetppidgettidgetcwdchdiraccessfaccessatreadlinkreadlinkatset_tid_addressset_robust_listrseqprlimit64getrandom Process.cwd(默认/)- syscall 表由
Scripts/syscall_table.py生成,build.py自动同步 vpsdk增加对应包装与常量
验收:现有应用行为不回退;所有失败路径可观察到负 errno;Scripts/syscall_table.py --check 通过。
P1 — 文件系统 ABI(本次完成,待编译验证)
openat(257)(AT_FDCWD+ 绝对路径 + cwd 相对路径)fstat(5)、newfstatat(262)、statx(332)(返回-ENOSYS,glibc 回退)getdents64(217)+ 目录 fd(O_DIRECTORY)dup(32)/dup2(33)/dup3(292)、fcntl(72)(F_GETFL/F_SETFL/F_DUPFD)pread64(17)/pwrite64(18)rename(82)、rmdir(84)、unlinkat(263)- 删除私有
OPENDIR/READDIR/CLOSEDIR - 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 回退到newfstatatrename只支持同一 FAT32 卷内;dup共享文件偏移,最后一次close才释放对象
P2 — 内存管理(本次完成,待编译验证)
brk(12):独立 brk 区(0x40000000起),按页增长/收缩mmap真语义:MAP_FIXED、PROT_*、失败返回MAP_FAILED(-1)- FAT32 文件映射:把文件内容复制进新页(
MAP_PRIVATE语义) munmap(11)真正解映射并把物理页归还 buddymprotect(10)修改 PTE 权限(支持 VMA 拆分)mremap(25)(MREMAP_MAYMOVE/MREMAP_FIXED)、madvise(28)- 每进程 VMA 表(64 项),记录 base/size/prot/kind/flags
- 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 — 进程、启动栈、解释器(本次完成)
- 用户栈按 Linux 布局写入
argc/argv/envp/auxv(AT_PHDR/PHENT/PHNUM/PAGESZ/ENTRY/BASE/RANDOM/EXECFN/UID/EUID/GID/EGID 等) execve(59)原型(path, argv, envp);SDK 保留spawn走私有SPAWN_PROCexit_group(231)终止进程内所有线程;exit(60)只退当前线程wait4(61)(WNOHANG、状态字、子进程回收)、getppidclone(56):(flags, stack, ptid, ctid, tls);CLONE_VM 建线程, 不带 CLONE_VM 时复制地址空间fork(57):复制用户页 + 启动在父进程 syscall 返回点(RAX=0)PT_INTERP+ ET_DYN 基址加载;AT_BASE指向解释器- 旧的 ViperOS
thread_create(func,arg)移到私有THREAD_CREATE_OLD
验收:静态非 PIE、动态 PIE(安装 ld.so 与 libc)都可从正确的启动栈进入;
fork 子进程在父进程同一返回点继续执行,父进程得到子 pid。
P4 — TLS 与线程(本次完成)
arch_prctl(158):SET_FS/GET_FS(SET_GS/GET_GS返回-ENOSYS)- 线程切换保存/恢复
IA32_FS_BASE(独立表,不改Thread的 600B 布局) rseq(334)返回-ENOSYS;set_robust_list记录futex(202):WAIT/WAKE采用“检查值 + sched_yield”轮询实现clone(CLONE_VM|CLONE_SETTLS):子线程从 post-syscall RIP 开始,FS=tlsCLONE_CHILD_CLEARTID:线程退出时写 0,配合 futex 唤醒 join
验收:pthread_create/join 走 Linux clone 路径;TLS 随线程切换。
P5 — 时间、信号、IPC(本次完成)
clock_gettime(228)/clock_getres(229)(RTC 秒 + 1ms tick)gettimeofday(96)、nanosleep(35)readv(19)/writev(20)pipe(22)/pipe2(293)(8 个管道 × 4KB,轮询读写)poll(7)(当前固定返回 0 = 超时)rt_sigaction(13)/rt_sigprocmask(14)/kill(62):完整登记- 信号投递:syscall 返回前重写用户上下文,进入 handler;
rt_sigreturn(15)恢复原上下文与 syscall 返回值 ppoll/select/epoll
验收:signal()/sigaction() 注册的 handler 能在下一次 syscall 返回时执行;
raise()/kill(getpid(), sig) 可触发。
P6 — 清理私有段(部分完成)
reboot(169):校验 Linux magic,支持 RESTART / POWER_OFF- 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. 测试与验收
- ABI 单元测试(用户态小程序)
- 每个调用至少覆盖成功 + 一个错误路径,断言返回值为
-errno - 结构体偏移/大小与 Linux 对照(编译期
__sizeof__)
- 每个调用至少覆盖成功 + 一个错误路径,断言返回值为
Scripts/syscall_table.py --check- CI/本地构建前检查两份表是否与权威表一致
- glibc 里程碑
- M1:静态
hello(write/exit/brk/mmap/arch_prctl) - M2:动态
hello(ld.so、openat、mmap、mprotect) - M3:
pthread+malloc压力测试 - M4:
ls/cat/sh等 coreutils
- M1:静态
- 回归
- 现有 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 单独提交,先静态后动态,避免与内存阶段交叉 |