跳转至

完善系统调用 — 排掉 IST 的雷,让应用能打印、能退出、能开窗

这一章讲什么: 上一章的 syscall 能跑,但埋着一颗「运气不好才炸」的雷: 系统调用执行到一半来定时器中断,中断处理跑在应用栈上; 上下文切换一换 CR3,应用页被摘除,iret 取栈立刻页面故障。 本章先用 IST(中断堆栈表) 排雷——给定时器中断钉死一张内核栈; 然后连开三扇门:PutString(应用往终端印字,fd 设计的雏形)、 write()(让 Newlib 的 printf 活起来)、exit(应用第一次能「死得体面」)。

1. 先看现象:一颗延迟引爆的雷,和它的拆法

主线例子:

  雷:syscall 不改 RSP → LogString 跑在应用栈上(CPL=0、应用栈)
      → 定时器中断 → 上下文切换 → RestoreContext 换 CR3 → 应用页 present=0
      → 中断收尾 iret 要取栈 → 取到未映射页 → 页面故障 → OS 崩
      触发条件苛刻(中断要恰好落在系统调用窗口里),所以「潜伏」到第 30 天才发作
  拆法:IST —— TSS 里的 IST1~IST7,给指定中断钉一张专用栈;
      IDT 条目里填 IST 编号,定时器中断永远在内核栈上跑,与应用栈无关
  之后的门:
      rpn 2 5 + → 终端里打印栈变化并算出 7(printf 活了)
      → 程序结束,终端显示 app exited. ret = 7(退出码回来了)
      → winhello 应用开出一扇自己的窗口,里面写着字

2. 顶层全景

  21.1 IST:雷的机理(CR3 切换使应用栈页失效);TSS.IST1 + IDT 的 IST 字段
  21.2 分工:Linux 式「系统调用只管简单输出,格式化归库」;newlib_support 为应用而建
  21.3 PutString:任务 ID → 终端对应表(terminals);fd=1 = 终端;Result{value, error}
  21.4 write():SyscallPutString 的浅封装,errno 与 -1 的惯例
  21.5–21.6 exit:永不返回的系统调用;CallApp 保存 OS 栈指针;
       SyscallEntry.exit:mov rsp, rax 换回 OS 栈,pop 还原,ret 回 ExecuteFile
  21.7 整理:syscall.asm/syscall.h 提到应用公共层
  21.8–21.9 OpenWindow + WriteString:应用的第一扇窗

  图说:先排雷,再开门;每扇门都小,但它们是「应用生态」的全部地基。

3. 核心原理

3.1 IST:给中断钉一张专属栈

syscall 指令只换 CS/SS 不换 RSP——系统调用处理跑在应用栈上。 这里来定时器中断,CPU 看 CPL=0 就不用 TSS.RSP0,继续用应用栈; 可上下文切换会 RestoreContext 换 CR3,应用区域的页映射随之消失, 中断收尾 iret 取栈 = 访问已摘除的页 = 页面故障1。 修法两条:进 SyscallEntry 就换栈(窗口极短但仍存在); 或 IST——TSS 里 IST1–IST7 字段各存一张栈,IDT 条目声明本中断用哪个 IST, 硬件保证处理程序一进来就在那张栈上2。作者选了 IST:「简单可靠」。 雷太难复现,「无法验证是否真的修复,视为修复完成」——诚实的边界3。

3.2 库与调用的分工,以及 fd 的雏形

系统调用 vs 库函数:两者都链进应用;printf 的格式化可以放在系统调用里 (系统调用做重活),也可以放在库里(系统调用只吐字符串)。Linux 和 Newlib 选后者, MikanOS 跟随——要做的是「印字符串」的系统调用和桥接的 write()4。 PutString(fd, s, len) 的关键设计:fd(文件描述符)= 输出的目的地编号,1 = 终端—— 将来 fd 可以指向文件,同一接口两面用5。 怎么知道字符串该进哪个终端?应用跑在终端任务里,当前任务 ID 就是终端的钥匙: terminals 映射表(任务 ID → Terminal*,跨任务改要 cli/sti)6。 返回值改成 Result{value, error} 结构体(128 位,按 ABI 落在 RAX:RDX), 错误号用 POSIX 的 E2BIG 这类标准值——「类 POSIX」的自觉从这开始7。 显示后立刻重绘(折中方案:全宽 × 光标行差的矩形,不追求最小)8。

3.3 write():printf 的桥

应用侧 newlib_support.c 里实现 write():调 SyscallPutString,失败置 errno 返回 -1—— errno/-1 是 POSIX 惯例,Newlib 的 printf 内部就靠它9。 rpn 链上 newlib_support.o 后,printf 直接可用:数字、格式串全活14。

3.4 exit:永不返回的系统调用

应用没法「结束」:main 一 return,执行就跳进虚空,整个 OS 死给你看。 换个角度:应用退出 = 调用一个永不返回的系统调用——进 ring 0 后不再 sysret, 把执行还给调用 CallApp() 的地方(ExecuteFile)10。 实现是一段栈指针的接力:

  CallApp(入口前):push 保存寄存器;mov [r9], rsp  ← 把 OS 栈指针存进 os_stack_ptr_(R9 = 第6参)
  应用跑……调 SyscallExit(code):
      syscall::Exit() 返回 {OSStackPointer(), code}  ← RAX = OS 栈指针,RDX = 退出码
  SyscallEntry 发现号是 0x80000002 → 跳 .exit:
      mov rsp, rax   ← 栈瞬间换回 OS 的
      mov eax, edx   ← 退出码给 RAX
      pop×6(逆序还原 CallApp 保存的寄存器)
      ret            ← 直接回到 ExecuteFile 里 CallApp 的下一行

CallApp 的开头和 .exit 的收尾是镜像——push 与 pop 顺序相反, 栈一换,pop 读到的全是 OS 的值11。 对比 Linux:main 能 return,因为 crt0 启动例程包了一层「return 就调 exit」; MikanOS 没有 crt0,所以 main 必须 void、必须显式 SyscallExit,否则死机12。 (「应用出异常只杀应用不杀系统」作者留作习题——第 24 章会收这个账。)

3.5 开窗:OpenWindow 与 WriteString

OpenWindow(w, h, x, y, title) 把「建窗口 + 建图层 + 置顶层 + 注册 layer_task_map」 包成一个调用;应用拿到 layer_id,再用 WriteString(layer_id, x, y, 颜色, 串) 往自己的窗口写字——winhello 出世13。

4. 作者的判断与证据

判断:简单可靠优先于完美覆盖。 IST 一次性覆盖所有中断,进系统调用换栈还有个 「极短的剩余窗口」;选 IST 的理由就是这句「简单可靠」。

判断:向 POSIX 看齐,但不背 POSIX 的债。 fd/errno/write 的命名全按业界惯例 (出门撞得见),Result 结构、E2BIG 错误号照抄;但多终端、文件 fd、 异常杀应用的完整语义,该欠就欠,写明是习题。

证据: printf 输出整数(图 21.5)、不同退出码的反复执行(图 21.7)、winhello 窗口; IST 修复「无法验证,视为完成」——潜伏 bug 的诚实处理。

5. 边界与局限

  • fd 只有 1 有意义(终端),文件 fd 要等第 25、26 章;
  • PutString 重绘范围是折中矩形,没做逐行最小化;
  • exit 依赖 CallApp 保存的 OS 栈指针:多应用并发时 os_stack_ptr_ 的归属 要随任务走(第 24 章修);
  • 异常杀应用未实现(习题);「无法验证的修复」留下了技术债的注脚。

6. 可带走的

本章主线例子,一行写完: syscall 中定时器中断踩应用栈 + CR3 摘除 → IST1 钉死内核栈 → PutString(fd=1, 任务 ID 找终端)→ write() 接通 printf → exit = 永不返回的调用(CallApp 存 OS 栈指针,.exit 换回并 ret)。

  1. 中断处理程序跑在哪张栈上可以是硬件钉死的(IST):IDT 条目声明,TSS 供栈;
  2. 系统调用做薄、库做厚(Linux/Newlib 式),printf 只欠一个 write();
  3. fd = 输出去向的编号,1 = 终端;同一接口将来通文件;
  4. 任务 ID 是「应用 ↔ 终端」的天然钥匙(一终端一应用在跑);
  5. 系统调用返回值 = {value, error}(128 位,走 RAX:RDX),错误号用 POSIX 标准;
  6. 退出 = 永不返回的系统调用;OS 栈指针在 CallApp 入口存、exit 时换栈 pop 还原;
  7. 没有 crt0,main 必须显式 exit——启动例程这层壳的存在与否,决定了 main 能不能 return;
  8. 潜伏 bug 的修复允许「无法复现、视为完成」,但要把不确定性写下来。

7. 原文地图

主题 原书章 原文位置
IST 雷的机理 21.1 设置IST(osbook_day21a) text/133-ch21-01-21-1-ist-osbook-day21a.txt:8(搜「定时器中断和上下文切换」) · text/133-ch21-01-21-1-ist-osbook-day21a.txt:19(搜「此时页面上的」)
IST 方案与「视为修复」 21.1 设置IST(osbook_day21a) text/133-ch21-01-21-1-ist-osbook-day21a.txt:33(搜「中断堆栈表」) · text/134-ch21-02-21-2-osbook-day21b.txt:14(搜「无法验证」)
库与调用的分工 21.2 字符串显示系统调用(osbook_day21b) text/134-ch21-02-21-2-osbook-day21b.txt:23(搜「printf()和系统调用的关系」) · text/134-ch21-02-21-2-osbook-day21b.txt:38(搜「Linux中」)
fd 设计与任务→终端表 21.3 创建系统调用 text/135-ch21-03-21-3.txt:43(搜「fd是一个数字」) · text/135-ch21-03-21-3.txt:16(搜「terminals」) · text/135-ch21-03-21-3.txt:65(搜「当前运行任务的ID」)
Result 返回类型 21.3 创建系统调用 text/135-ch21-03-21-3.txt:54(搜「value代表」) · text/135-ch21-03-21-3.txt:47(搜「E2BIG」)
write() 与 errno 21.4 创建write() text/136-ch21-04-21-4-write.txt:13(搜「errno」)
exit = 永不返回的调用 21.5 退出系统调用(osbook_day21c) text/137-ch21-05-21-5-osbook-day21c.txt:22(搜「永不结束」) · text/137-ch21-05-21-5-osbook-day21c.txt:42(搜「sysret指令不会被执行」)
CallApp 存 OS 栈指针 21.5 退出系统调用(osbook_day21c) text/137-ch21-05-21-5-osbook-day21c.txt:146(搜「mov [r9], rsp」) · text/138-ch21-06-21-6.txt:18(搜「为什么操作系统的栈指针会在RAX中」)
与 Linux crt0 的对比 21.6 返回栈指针 text/138-ch21-06-21-6.txt:49(搜「启动例程」) · text/138-ch21-06-21-6.txt:51(搜「返回类型设为int」)
公共 syscall 层 21.7 代码整理(osbook_day21d) text/139-ch21-07-21-7-osbook-day21d.txt:24(搜「apps/syscall.asm」) · text/139-ch21-07-21-7-osbook-day21d.txt:43(搜「syscall.h」)
OpenWindow 与 WriteString 21.8 打开一个窗口(osbook_day21e) text/140-ch21-08-21-8-osbook-day21e.txt:5(搜「打开窗口的系统调用」) · text/141-ch21-09-21-9-osbook-day21f.txt(搜「WriteString」——文件以日文残页为主,中文在同节)

  1. 出处:「21.1 设置IST(osbook_day21a)」(text/133-ch21-01-21-1-ist-osbook-day21a.txt:8(搜「定时器中断和上下文切换」); text/133-ch21-01-21-1-ist-osbook-day21a.txt:19(搜「此时页面上的」))。 ↩

  2. 出处:「21.1 设置IST(osbook_day21a)」(text/133-ch21-01-21-1-ist-osbook-day21a.txt:33(搜「中断堆栈表」); text/134-ch21-02-21-2-osbook-day21b.txt:14(搜「无法验证」))。 ↩

  3. 出处:「21.2 字符串显示系统调用(osbook_day21b)」(text/134-ch21-02-21-2-osbook-day21b.txt:23(搜「printf()和系统调用的关系」); text/134-ch21-02-21-2-osbook-day21b.txt:38(搜「Linux中」))。 ↩

  4. 出处:「21.3 创建系统调用」(text/135-ch21-03-21-3.txt:43(搜「fd是一个数字」); text/135-ch21-03-21-3.txt:16(搜「terminals」); text/135-ch21-03-21-3.txt:65(搜「当前运行任务的ID」))。 ↩

  5. 出处:「21.3 创建系统调用」(text/135-ch21-03-21-3.txt:54(搜「value代表」); text/135-ch21-03-21-3.txt:47(搜「E2BIG」))。 ↩

  6. 出处:「21.4 创建write()」(text/136-ch21-04-21-4-write.txt:13(搜「errno」))。 ↩

  7. 出处:「21.5 退出系统调用(osbook_day21c)」(text/137-ch21-05-21-5-osbook-day21c.txt:22(搜「永不结束」); text/137-ch21-05-21-5-osbook-day21c.txt:42(搜「sysret指令不会被执行」))。 ↩

  8. 出处:「21.5 退出系统调用(osbook_day21c)」(text/137-ch21-05-21-5-osbook-day21c.txt:146(搜「mov [r9], rsp」); text/138-ch21-06-21-6.txt:18(搜「为什么操作系统的栈指针会在RAX中」))。 ↩

  9. 出处:「21.6 返回栈指针」(text/138-ch21-06-21-6.txt:49(搜「启动例程」); text/138-ch21-06-21-6.txt:51(搜「返回类型设为int」))。 ↩

  10. 出处:「21.8 打开一个窗口(osbook_day21e)」(text/140-ch21-08-21-8-osbook-day21e.txt:5(搜「打开窗口的系统调用」))。 ↩

  11. 出处:「21.2 字符串显示系统调用(osbook_day21b)」(text/134-ch21-02-21-2-osbook-day21b.txt:23(搜「printf()和系统调用的关系」); text/134-ch21-02-21-2-osbook-day21b.txt:38(搜「Linux中」))。 ↩

  12. 出处:「21.3 创建系统调用」(text/135-ch21-03-21-3.txt:43(搜「fd是一个数字」); text/135-ch21-03-21-3.txt:16(搜「terminals」); text/135-ch21-03-21-3.txt:65(搜「当前运行任务的ID」))。 ↩

  13. 出处:「21.5 退出系统调用(osbook_day21c)」(text/137-ch21-05-21-5-osbook-day21c.txt:22(搜「永不结束」); text/137-ch21-05-21-5-osbook-day21c.txt:42(搜「sysret指令不会被执行」))。 ↩

  14. 出处:「21.7 代码整理」(text/139-ch21-07-21-7-osbook-day21d.txt:24,搜「apps/syscall.asm」;:43,搜「syscall.h」)。 ↩