完善系统调用 — 排掉 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)。
- 中断处理程序跑在哪张栈上可以是硬件钉死的(IST):IDT 条目声明,TSS 供栈;
- 系统调用做薄、库做厚(Linux/Newlib 式),printf 只欠一个 write();
- fd = 输出去向的编号,1 = 终端;同一接口将来通文件;
- 任务 ID 是「应用 ↔ 终端」的天然钥匙(一终端一应用在跑);
- 系统调用返回值 = {value, error}(128 位,走 RAX:RDX),错误号用 POSIX 标准;
- 退出 = 永不返回的系统调用;OS 栈指针在 CallApp 入口存、exit 时换栈 pop 还原;
- 没有 crt0,main 必须显式 exit——启动例程这层壳的存在与否,决定了 main 能不能 return;
- 潜伏 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」——文件以日文残页为主,中文在同节) |
-
出处:「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(搜「此时页面上的」))。 ↩ -
出处:「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中」))。 ↩ -
出处:「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」))。 ↩ -
出处:「21.3 创建系统调用」(
text/135-ch21-03-21-3.txt:54(搜「value代表」);text/135-ch21-03-21-3.txt:47(搜「E2BIG」))。 ↩ -
出处:「21.4 创建write()」(
text/136-ch21-04-21-4-write.txt:13(搜「errno」))。 ↩ -
出处:「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指令不会被执行」))。 ↩ -
出处:「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中」))。 ↩ -
出处:「21.6 返回栈指针」(
text/138-ch21-06-21-6.txt:49(搜「启动例程」);text/138-ch21-06-21-6.txt:51(搜「返回类型设为int」))。 ↩ -
出处:「21.8 打开一个窗口(osbook_day21e)」(
text/140-ch21-08-21-8-osbook-day21e.txt:5(搜「打开窗口的系统调用」))。 ↩ -
出处:「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中」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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指令不会被执行」))。 ↩ -
出处:「21.7 代码整理」(
text/139-ch21-07-21-7-osbook-day21d.txt:24,搜「apps/syscall.asm」;:43,搜「syscall.h」)。 ↩