Skip to content

系统调用:把一次函数请求变成受控的内核操作 ​

C 函数调用为什么不够 ​

普通函数调用只改变程序计数器并遵循调用约定,不会自动获得权限。系统调用需要一套两边约定的协议:操作编号、参数位置、结果位置和错误约定。RISC-V xv6 用 a7 保存系统调用编号,参数主要放在 a0 到 a5,通过 ecall 进入内核,返回值放回 a0。

以 getpid() 为例,用户头文件只负责让编译器知道接口;user/usys.pl 生成汇编桩;kernel/syscall.h 分配编号;kernel/syscall.c 查找实现;sys_getpid() 返回当前进程 PID。头文件声明、机器级入口、编号、分派表、实现五处中少任何一处,都可能在不同阶段失败。

flowchart LR
  A[user/user.h 声明] --> B[usys.pl 生成桩]
  B --> C[a7 编号与 ecall]
  C --> D[trampoline 保存寄存器]
  D --> E[usertrap]
  E --> F[syscall 分派]
  F --> G[sys_xxx 实现]
  G --> H[trapframe.a0 返回值]
查看流程图文本
flowchart LR
  A[user/user.h 声明] --> B[usys.pl 生成桩]
  B --> C[a7 编号与 ecall]
  C --> D[trampoline 保存寄存器]
  D --> E[usertrap]
  E --> F[syscall 分派]
  F --> G[sys_xxx 实现]
  G --> H[trapframe.a0 返回值]

调试时沿着这条链寻找最早不符合预期的状态。用户程序链接失败,优先看桩;打印 unknown sys call,优先看编号和分派;实现读到奇怪参数,优先看参数类型及用户地址复制。不要在 sys_xxx 中反复猜测一个实际上没有经过正确分派的请求。

参数读取不是指针可信化 ​

argint 读取整数参数,argaddr 取得用户地址的数值,argstr 将用户字符串复制到内核缓冲区。argaddr 成功不表示整个缓冲区存在,也不表示可写。假设 read(fd, buf, 100) 的 buf 位于一页最后 20 字节:访问会横跨第二页;第一页有效不能证明后 80 字节安全。

copyin/copyout/copyinstr 逐页处理用户内存。字符串除了地址合法,还要在上限内遇到终止符;数量参数还要考虑负数、整数溢出、资源耗尽。这些检查共同定义接口边界,不能只检查 pointer != 0。xv6 用户地址 0 也可能映射文本,是否允许访问要看语义和 PTE。

一个稳定设计是把用户输入先转成内核拥有的值,再执行业务逻辑。比如 sandbox 允许路径应复制进 struct proc 的固定数组;若只保存用户指针,用户之后能修改路径内容,exec 还可能替换整个地址空间。

2025 实验:拦截权限必须放在分派之前 ​

本年 syscall 实验新增 interpose(mask, path)。mask 中第 n 位对应第 n 个系统调用;被设置的调用默认拒绝,其中 open/exec 可按允许路径获得例外。进程的限制必须在 kfork() 中继承。官方 syscall 手册

设计时把问题拆开:先判断编号有效,再判断策略允许,最后执行实现。被拒绝后才调用 sys_open 是错的,因为打开文件的副作用已经发生。读取路径失败也不能理解成“没有匹配限制,所以放行”,应作为无效输入处理。

以下是教学伪代码,只说明检查层次:

text
读取系统调用编号
若编号不在分派表中:返回错误
若当前进程策略拒绝该调用:
    若不是受支持的路径例外:返回错误
    安全复制本次请求路径
    若复制失败或路径不匹配:返回错误
执行已允许的系统调用
写回返回值

位操作也有边界:在 C 中按大于等于类型宽度的位数左移可能产生未定义行为。应先验证编号范围,再选宽度足够的无符号类型。只用一个示例 mask 测试,很容易遗漏这一点。

快速调用与安全调用的共同本质 ​

pgtbl 实验将 PID 放进用户只读共享页,使 ugetpid() 可以普通内存读取完成。它快在避免了陷入和状态切换;没有取消隔离,因为用户映射没有写权限。适合这种路径的信息必须在读时有明确语义,不能随意把可变的多字段内核结构暴露出去,否则会有一致性和泄漏问题。

因此 API 设计要同时问两个问题:何时必须进入内核完成权限检查与副作用;何时可以把可信快照安全暴露给用户。把二者混成“所有系统调用都只是慢函数”,会错过系统边界的真正作用。

自测:为什么拦截策略放在用户库中不能真正限制进程?

进程可以绕过库,自行设置寄存器并执行 ecall。只有在进程无法绕过的内核入口检查,限制才有效。用户库可改善易用性,不能独自建立权限边界。

自测:允许路径是 README,传入 ./README 是否自动相同?

字符串匹配与路径解析后的对象身份不是一回事。实验按 handout 定义实现;生产安全设计则必须明确规范化、符号链接、目录变化等语义。不能把实验级字符串策略直接称为完整安全沙箱。

继续:syscall 实验、异常路径。源码以 syscall 分支 0e53502c9edd 为准。