任务段(Task Segmentt)和内核栈
概要:介绍任务段(TSS)的作用:当发生特权级切换时,CPU 如何通过 TSS 找到内核栈,以及 xv6 用一个 TSS 配合动态修改 ESP0 实现多进程内核栈切换的巧妙设计,并梳理中断发生时硬件自动完成的完整流程。
1. 为什么内核绝不能使用用户的栈?
核心原因可以归结为两点:防黑客、防手残。
如果运行在 Ring 3 的用户程序是一个恶意病毒,它在触发系统调用之前,故意把自己的 %esp 寄存器指向操作系统的核心数据区(例如覆盖掉 IDT 表所在的位置)。此时若内核直接沿用这个 %esp 来压栈保存数据,就会写穿自己的核心防线,导致系统崩溃。因此,一切进入 Ring 0 的操作,第一件事必须是切换到绝对安全的内核栈。
2. 硬件如何得知内核栈的位置?(TSS 的作用)
当硬件中断或者系统调用发生时,CPU 需要瞬间切换到 Ring 0,并且立刻需要一个安全的栈来保存现场。此时操作系统来不及插手,整个过程必须由 CPU 硬件全自动完成。
那么 CPU 去哪里查找内核栈的地址呢?答案就是 TSS(Task State Segment,任务状态段)。
TSS 是内存中一块特殊的数据结构,内部预留了很多字段,其中最核心的两个字段是 SS0 和 ESP0:
SS0:Ring 0 的栈段选择子。ESP0:Ring 0 的栈顶指针(也就是当前进程的内核栈地址)。
3. xv6(和 Linux)的巧妙做法
Intel 在设计 TSS 时,原本的构想是为每个进程都分配一个独立的 TSS,由 CPU 通过硬件切换 TSS 来实现进程切换。但后来的操作系统开发者(包括 xv6 和现代 Linux)认为这套硬件切换机制过于笨重,于是采用了一种更简洁的做法:
整个系统里,每个 CPU 核心只分配唯一的一个 TSS!
那么每个进程都有自己的内核栈,一个 TSS 如何满足需求?
方法很简单:每次操作系统准备把 CPU 交给一个新的进程运行时,就会修改这个唯一 TSS 中的 ESP0 字段,将其更新为即将运行的进程的内核栈地址。
这样一来,硬件行为不变,仍然只读取 TSS 中的信息,但每次读到的内核栈地址都已经被操作系统精准替换为当前进程的内核栈了。
中断发生那一瞬间的完整流程
为了清晰地展示这个过程,下面把中断发生时硬件在极短时间内完成的操作拆解如下(注意,这几步全部由 CPU 硬件自动完成,顺序不可改变):
-
查 TSS 找内核栈 CPU 发现特权级要从 Ring 3 切换到 Ring 0,立即查询当前 CPU 绑定的那个唯一 TSS,从中读出
ESP0(内核栈顶地址)和SS0。 -
切换栈 CPU 将内部的栈指针寄存器(
%esp和%ss)强制替换为刚读出的内核栈地址。从这一刻起,CPU 压栈的位置已经安全转移到了内核地盘。 -
压栈保护现场(构建 trapframe 的前奏) CPU 开始在新的内核栈上依次压入刚才的 Ring 3 现场:依次压入用户态的
%ss、%esp、EFLAGS标志位、%cs,最后是%eip。 -
查 IDT 跳转 最后,CPU 根据中断号查询 IDT 表,将对应的处理函数地址写入
%cs和%eip,正式开始执行内核 C 代码。
可以看到,TSS 就像是用户态与内核态之间的一个”交接箱”。两者完全不共享栈内存,唯一的联系纽带,就是硬件在切换权限的那一瞬间,从这个交接箱中取出的内核栈地址(ESP0)。
留下评论