TSS(Task-State Segment)任务状态段详解
概述
TSS(Task-State Segment,任务状态段)是 x86 架构中用于定义任务执行环境状态的数据结构。它是保护模式下任务管理机制的核心组件,存储了任务的所有关键状态信息,包括寄存器状态、栈指针、段选择符等。
1. TSS 的定义和作用
1.1 基本定义
根据 Intel x86 架构手册,TSS 定义了任务的执行环境状态,包括:
- 通用寄存器状态:EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI
- 段寄存器状态:CS, DS, ES, FS, GS, SS
- 控制寄存器:EFLAGS, EIP
- 栈指针:三个特权级别的栈指针(Ring 0, Ring 1, Ring 2)
- 段选择符:LDT(Local Descriptor Table)选择符
- 页表基址:CR3 寄存器值(页表层次结构的基地址)
- I/O 权限位图:控制 I/O 端口访问权限
1.2 TSS 的作用
- 任务状态保存:当任务被切换出去时,CPU 自动将当前任务的状态保存到其 TSS 中
- 任务状态恢复:当任务被切换回来时,CPU 从 TSS 中恢复任务的所有状态
- 特权级切换:提供不同特权级别的栈指针,支持特权级切换
- I/O 权限控制:通过 I/O 权限位图控制任务的 I/O 端口访问权限
1.3 当前任务的概念
在保护模式下,所有程序执行都在某个任务的上下文中进行 ,这个任务被称为当前任务(Current Task)。
- 当前任务的 TSS 选择符存储在**任务寄存器(Task Register, TR)**中
- CPU 通过 TR 寄存器找到当前任务的 TSS
- 每个 CPU 核心都有自己的 TR 寄存器
2. TSS 的数据结构
2.1 Linux 内核中的 TSS 结构
在 Linux 内核中,TSS 结构定义如下(32位系统):
387:431:include/asm-i386/processor.h
struct tss_struct {
unsigned short back_link,__blh;
unsigned long esp0;
unsigned short ss0,__ss0h;
unsigned long esp1;
unsigned short ss1,__ss1h; /* ss1 is used to cache MSR_IA32_SYSENTER_CS */
unsigned long esp2;
unsigned short ss2,__ss2h;
unsigned long __cr3;
unsigned long eip;
unsigned long eflags;
unsigned long eax,ecx,edx,ebx;
unsigned long esp;
unsigned long ebp;
unsigned long esi;
unsigned long edi;
unsigned short es, __esh;
unsigned short cs, __csh;
unsigned short ss, __ssh;
unsigned short ds, __dsh;
unsigned short fs, __fsh;
unsigned short gs, __gsh;
unsigned short ldt, __ldth;
unsigned short trace, io_bitmap_base;
/*
* The extra 1 is there because the CPU will access an
* additional byte beyond the end of the IO permission
* bitmap. The extra byte must be all 1 bits, and must
* be within the limit.
*/
unsigned long io_bitmap[IO_BITMAP_LONGS + 1];
/*
* Cache the current maximum and the last task that used the bitmap:
*/
unsigned long io_bitmap_max;
struct thread_struct *io_bitmap_owner;
/*
* pads the TSS to be cacheline-aligned (size is 0x100)
*/
unsigned long __cacheline_filler[35];
/*
* .. and then another 0x100 bytes for emergency kernel stack
*/
unsigned long stack[64];
} __attribute__((packed));
2.2 TSS 字段详细说明
2.2.1 链接字段
back_link: 指向前一个任务的 TSS 选择符(用于嵌套任务切换)
2.2.2 特权级栈指针(Ring 0-2)
esp0,ss0: Ring 0(内核态)的栈指针和栈段选择符esp1,ss1: Ring 1 的栈指针和栈段选择符(Linux 不使用)esp2,ss2: Ring 2 的栈指针和栈段选择符(Linux 不使用)
重要 :esp0 是内核栈指针,当从用户态切换到内核态时,CPU 会自动使用 esp0 作为新的栈指针。
2.2.3 页表基址
__cr3: CR3 寄存器的值,存储页表层次结构的基地址- 每个任务可以有自己的页表,实现进程地址空间隔离
2.2.4 通用寄存器
eax,ecx,edx,ebx,esp,ebp,esi,edi: 通用寄存器状态
2.2.5 控制寄存器
eip: 指令指针寄存器eflags: 标志寄存器
2.2.6 段寄存器
es,cs,ss,ds,fs,gs: 段寄存器状态ldt: LDT(本地描述符表)选择符
2.2.7 I/O 权限位图
io_bitmap_base: I/O 权限位图在 TSS 中的偏移地址io_bitmap: I/O 权限位图数组(65536 位 = 8192 字节 + 额外保护字节)io_bitmap_max: 位图的有效最大字节数(优化用)io_bitmap_owner: 拥有此位图的线程指针
2.2.8 其他字段
trace: 调试跟踪位__cacheline_filler: 缓存行对齐填充stack[64]: 紧急内核栈(256 字节)
2.3 64位模式下的 TSS
在 IA-32e 模式(64位模式)下,TSS 结构有所不同:
201:222:include/asm-x86_64/processor.h
struct tss_struct {
u32 reserved1;
u64 rsp0;
u64 rsp1;
u64 rsp2;
u64 reserved2;
u64 ist[7];
u32 reserved3;
u32 reserved4;
u16 reserved5;
u16 io_bitmap_base;
/*
* The extra 1 is there because the CPU will access an
* additional byte beyond the end of the IO permission
* bitmap. The extra byte must be all 1 bits, and must
* be within the limit. Thus we have:
*
* 128 bytes, the bitmap itself, for ports 0..0x3ff
* 8 bytes, for an extra "long" of ~0UL
*/
unsigned long io_bitmap[IO_BITMAP_LONGS + 1];
} __attribute__((packed)) ____cacheline_aligned;
64位模式下的变化:
- 栈指针扩展为 64 位 :
rsp0,rsp1,rsp2都是 64 位 - 中断栈表(IST) :
ist[7]提供 7 个中断栈指针,用于处理特定类型的中断 - 简化结构:移除了许多在 64 位模式下不需要的字段(如通用寄存器、段寄存器等)
3. 任务切换过程
3.1 任务切换的方法
切换到新任务的最简单方法是使用 CALL 或 JMP 指令,新任务的 TSS 选择符作为操作数:
assembly
CALL TSS_SELECTOR ; 调用新任务
JMP TSS_SELECTOR ; 跳转到新任务
3.2 任务切换的详细步骤
当 CPU 执行任务切换时,会执行以下操作:
步骤 1:保存当前任务状态
CPU 将当前任务的状态保存到当前 TSS 中:
- 所有通用寄存器(EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI)
- 所有段寄存器(ES, CS, SS, DS, FS, GS)
- EFLAGS 寄存器
- EIP 寄存器(当前指令指针)
- 段选择符和栈指针
步骤 2:加载新任务的 TSS 选择符
将新任务的 TSS 选择符加载到任务寄存器(TR)中:
c
TR = new_task_tss_selector;
步骤 3:通过 GDT 访问新 TSS
CPU 通过全局描述符表(GDT)中的 TSS 描述符访问新任务的 TSS:
- 从 GDT 中读取 TSS 描述符
- 验证描述符的有效性
- 获取 TSS 的基地址和段限制
步骤 4:加载新任务状态
从新 TSS 中加载任务状态到 CPU 寄存器:
- 通用寄存器(EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI)
- 段寄存器(ES, CS, SS, DS, FS, GS)
- LDTR(本地描述符表寄存器)
- CR3(页表基址寄存器)
- EFLAGS 寄存器
- EIP 寄存器
步骤 5:开始执行新任务
CPU 从新任务的 EIP 寄存器指向的地址开始执行。
3.3 任务切换流程图
┌─────────────────────────────────────┐
│ 当前任务执行中 │
│ (Task A) │
└──────────────┬──────────────────────┘
│
│ CALL/JMP to Task B
▼
┌─────────────────────────────────────┐
│ 步骤1: 保存当前任务状态 │
│ - 寄存器 → TSS_A │
│ - EIP, EFLAGS → TSS_A │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 步骤2: 加载新任务 TSS 选择符 │
│ TR = TSS_B_SELECTOR │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 步骤3: 通过 GDT 访问新 TSS │
│ - 读取 TSS_B 描述符 │
│ - 验证有效性 │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 步骤4: 加载新任务状态 │
│ - TSS_B → 寄存器 │
│ - 加载 CR3(页表切换) │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 步骤5: 开始执行新任务 │
│ (Task B) │
└─────────────────────────────────────┘
3.4 任务切换的开销
硬件任务切换虽然功能强大,但开销较大:
- 性能开销:需要保存和恢复大量寄存器状态
- 内存开销:每个任务都需要一个 TSS
- 灵活性限制:硬件任务切换机制相对固定
Linux 内核的做法:
- Linux 内核不使用硬件任务切换
- 使用软件任务切换(上下文切换)
- TSS 主要用于存储内核栈指针(esp0)和 I/O 权限位图
- 任务切换通过软件实现,更加灵活高效
4. 任务门(Task Gate)
4.1 任务门的定义
任务门(Task Gate)类似于调用门(Call Gate),但它提供的是对 TSS 的访问(通过段选择符),而不是对代码段的访问。
4.2 任务门的结构
任务门描述符包含:
- TSS 选择符:指向目标任务的 TSS
- 特权级:访问任务门所需的特权级
- 类型字段:标识这是一个任务门
4.3 任务门的使用
通过任务门访问任务:
assembly
CALL TASK_GATE_SELECTOR ; 通过任务门调用任务
任务门的作用:
- 间接任务切换:不直接指定 TSS,而是通过任务门间接访问
- 特权级控制:可以控制访问任务所需的特权级
- 任务隔离:提供额外的任务访问控制层
4.4 任务门 vs 直接 TSS 访问
| 特性 | 直接 TSS 访问 | 任务门访问 |
|---|---|---|
| 访问方式 | 直接指定 TSS 选择符 | 通过任务门间接访问 |
| 特权级检查 | TSS 描述符中的特权级 | 任务门中的特权级 |
| 灵活性 | 较低 | 较高 |
| 使用场景 | 简单任务切换 | 需要额外控制的场景 |
5. IA-32e 模式(64位模式)下的 TSS
5.1 硬件任务切换的取消
在 IA-32e 模式(64位模式)下,硬件任务切换不再被支持。这意味着:
- 不能使用 CALL 或 JMP 指令直接切换到 TSS
- 不能使用任务门进行任务切换
- 任务切换必须通过软件实现
5.2 TSS 的继续存在
尽管硬件任务切换被取消,TSS 仍然存在,但用途发生了变化:
- 栈指针存储:存储不同特权级的栈指针(主要是 rsp0)
- 中断栈表(IST):提供 7 个中断栈指针
- I/O 权限位图:继续用于 I/O 端口访问控制
5.3 64位 TSS 的关键信息
根据 Intel 文档,64位 TSS 包含以下重要信息:
5.3.1 每个特权级的栈指针地址
- rsp0: Ring 0(内核态)栈指针
- rsp1: Ring 1 栈指针(通常不使用)
- rsp2: Ring 2 栈指针(通常不使用)
5.3.2 中断栈表(IST)指针地址
- ist[0] 到 ist[6]: 7 个中断栈指针
- 用于处理特定类型的中断(如 NMI、双重故障等)
- 提供独立的中断处理栈,避免栈溢出
5.3.3 I/O 权限位图偏移地址
- io_bitmap_base: I/O 权限位图在 TSS 中的偏移地址
- 从 TSS 基地址开始的字节偏移
- 用于定位 I/O 权限位图的位置
5.4 任务寄存器的扩展
在 IA-32e 模式下,任务寄存器(TR)被扩展为保存 64 位基地址:
- 32位模式下:TR 存储 16 位选择符,通过 GDT 间接访问 TSS
- 64位模式下:TR 直接存储 64 位 TSS 基地址,提高访问效率
5.5 Linux 内核在 64 位模式下的 TSS 使用
Linux 内核在 64 位模式下使用 TSS 的方式:
- 每 CPU 一个 TSS:每个 CPU 核心有自己的 TSS
- 主要用途 :
- 存储内核栈指针(rsp0)
- 存储 I/O 权限位图
- 提供中断栈表(IST)
- 任务切换:完全通过软件实现,不依赖硬件任务切换
6. I/O 权限控制机制
x86 架构提供了两种机制来控制 I/O 端口访问:I/O 特权级(IOPL)和I/O 权限位图(I/O Permission Bit Map)。这两种机制协同工作,提供了灵活而强大的 I/O 访问控制。
6.1 I/O 特权级(I/O Privilege Level, IOPL)
6.1.1 IOPL 的定义
IOPL 是存储在 EFLAGS 寄存器中的一个 2 位字段(位 12-13),用于控制系统对 I/O 地址空间的访问。它通过限制特定指令的使用来实现 I/O 保护。
6.1.2 IOPL 的作用
在典型的保护环模型中,I/O 地址空间的访问通常被限制在特权级 0 和 1:
- 特权级 0(Ring 0):内核和核心设备驱动程序可以执行 I/O
- 特权级 1(Ring 1):某些设备驱动程序可以执行 I/O
- 特权级 2-3(Ring 2-3):应用程序被拒绝直接访问 I/O 地址空间,必须通过操作系统调用
6.1.3 I/O 敏感指令
以下指令只有在当前特权级(CPL)≤ IOPL 时才能执行:
IN: 从 I/O 端口读取数据INS: 从 I/O 端口读取字符串OUT: 向 I/O 端口写入数据OUTS: 向 I/O 端口写入字符串CLI: 清除中断使能标志(Clear Interrupt-enable Flag)STI: 设置中断使能标志(Set Interrupt-enable Flag)
这些指令被称为I/O 敏感指令(I/O Sensitive Instructions),因为它们对 IOPL 字段敏感。
6.1.4 IOPL 检查机制
检查流程:
1. CPU 执行 I/O 敏感指令(如 OUT DX, AL)
2. 读取 EFLAGS 寄存器中的 IOPL 字段
3. 比较当前特权级(CPL)与 IOPL:
- 如果 CPL ≤ IOPL:允许执行指令
- 如果 CPL > IOPL:触发通用保护异常(#GP)
示例:
c
// 假设 IOPL = 0(只有内核可以执行 I/O)
// 用户程序(CPL = 3)尝试执行:
outb(0x60, 0x00); // 尝试访问键盘控制器端口
// CPU 检查:CPL (3) > IOPL (0)
// 结果:触发 #GP 异常,指令被拒绝
6.1.5 修改 IOPL
程序或任务只能通过以下指令修改 IOPL:
POPF: 从栈弹出标志寄存器IRET: 中断返回
重要限制:
- 特权要求 :只有运行在特权级 0 的过程才能修改 IOPL
- 静默失败:如果较低特权级的过程尝试修改 IOPL,不会触发异常,IOPL 值保持不变
- IF 标志的特殊处理 :
POPF指令也可以修改 IF 标志,但这也是 I/O 敏感的。只有 CPL ≤ IOPL 时才能修改 IF 标志
代码示例:
c
// 在内核中(CPL = 0)
void set_iopl(unsigned int level) {
unsigned long flags;
// 读取当前 EFLAGS
asm volatile("pushf; pop %0" : "=r" (flags));
// 修改 IOPL 字段(位 12-13)
flags = (flags & ~0x3000) | (level << 12);
// 写回 EFLAGS(需要特权级 0)
asm volatile("push %0; popf" : : "r" (flags));
}
6.1.6 每任务的 IOPL
因为每个任务都有自己的 EFLAGS 寄存器副本,所以每个任务可以有不同的 IOPL:
- 任务 A:IOPL = 0(完全禁止用户态 I/O)
- 任务 B:IOPL = 3(允许用户态 I/O,不推荐)
6.2 I/O 权限位图(I/O Permission Bit Map)
6.2.1 I/O 权限位图的定义
I/O 权限位图是一种机制,允许较低特权级的程序或任务以及虚拟-8086 模式下的任务进行有限的 I/O 端口访问。它位于当前运行任务或程序的 TSS 中。
6.2.2 I/O 权限位图的位置
I/O 权限位图在 TSS 中的位置通过 io_bitmap_base 字段指定:
c
struct tss_struct {
// ... 其他字段 ...
unsigned short io_bitmap_base; // I/O 位图在 TSS 中的偏移地址
unsigned long io_bitmap[IO_BITMAP_LONGS + 1]; // I/O 权限位图数组
// ...
};
关键点:
io_bitmap_base:给出 I/O 权限位图第一个字节的地址(从 TSS 基地址开始的偏移)- I/O 权限位图的大小和位置是可变的
- 每个任务都有自己的 TSS,因此每个任务都有自己的 I/O 权限位图
6.2.3 I/O 权限检查流程
CPU 执行 I/O 指令时的完整检查流程:
步骤 1:IOPL 检查
如果 CPL ≤ IOPL:
→ 允许所有 I/O 操作,跳过位图检查
如果 CPL > IOPL 或虚拟-8086 模式:
→ 继续步骤 2(位图检查)
步骤 2:I/O 权限位图检查
1. 读取 TSS 中的 io_bitmap_base
2. 计算位图地址:位图地址 = TSS基地址 + io_bitmap_base
3. 计算端口对应的位:
- 位位置 = 端口地址
- 字节位置 = 端口地址 / 8
- 位偏移 = 端口地址 % 8
4. 读取位图中的对应位
5. 检查权限:
- 如果位 = 1:禁止访问,触发 #GP 异常
- 如果位 = 0:允许访问,执行 I/O 指令
位图语义:
- 位 = 1:禁止访问该 I/O 端口
- 位 = 0:允许访问该 I/O 端口
示例:
假设访问端口 0x29(十进制 41):
- 字节位置 = 41 / 8 = 5
- 位偏移 = 41 % 8 = 1
- 检查位图第 5 字节的第 1 位(从 0 开始)
6.2.4 多字节 I/O 访问的处理
对于字(16位)或双字(32位)的 I/O 访问,CPU 会检查所有对应的位:
示例:双字访问(32位)
访问端口 0x100 的双字(4 字节):
- 需要检查端口 0x100, 0x101, 0x102, 0x103 的权限位
- 如果任何一个位被设置(= 1),触发 #GP 异常
- 只有当所有 4 个位都清除(= 0)时,才允许访问
原因:I/O 端口地址不一定对齐到字或双字边界,CPU 需要确保所有涉及的端口都允许访问。
6.2.5 额外保护字节的要求
关键要求 :CPU 在检查 I/O 权限时,会为每个 I/O 端口访问读取位图中的两个字节 。为了防止访问最高地址端口时产生异常,必须在 TSS 中位图之后立即包含一个额外字节。
要求:
- 额外字节必须全为 1:所有位都必须设置为 1(禁止访问)
- 必须在段限制内:该字节必须在 TSS 段限制范围内
- I/O 位图基址限制 :
io_bitmap_base不能超过 0xDFFF(这是 Intel x86 架构的硬件限制,超过此值 CPU 无法正确处理)
内存布局示例:
TSS 结构:
┌─────────────────────────────────────┐
│ TSS 标准字段 │
│ ... │
│ io_bitmap_base = 0x0068 │
├─────────────────────────────────────┤
│ I/O Permission Bit Map │
│ 字节 0: 端口 0-7 的权限 │
│ 字节 1: 端口 8-15 的权限 │
│ ... │
│ 字节 N: 端口 8N-8N+7 的权限 │
├─────────────────────────────────────┤
│ 额外保护字节: 0xFF (全 1) │ ← 必须存在
└─────────────────────────────────────┘
Linux 内核实现:
c
// 在 TSS 结构中
unsigned long io_bitmap[IO_BITMAP_LONGS + 1]; // +1 提供额外保护
// 初始化时
for (i = 0; i <= IO_BITMAP_LONGS; i++)
t->io_bitmap[i] = ~0UL; // 全部初始化为全 1(禁止访问)
6.2.6 位图大小和覆盖范围
重要特性 :I/O 权限位图不需要表示所有 I/O 地址。
规则:
- 位图覆盖的端口:位图中明确表示的端口按位图规则检查
- 位图未覆盖的端口:被视为位图中设置了对应位(禁止访问)
示例:
假设 TSS 段限制 = 位图基址 + 10 字节
- 位图有 11 字节(包括额外保护字节)
- 前 10 字节映射前 80 个 I/O 端口(0-79)
- 端口 80 及以上的地址被视为禁止访问(触发异常)
6.2.7 禁用 I/O 权限位图
如果 io_bitmap_base 大于或等于 TSS 段限制,则没有 I/O 权限位图:
- 当 CPL > IOPL 时,所有 I/O 指令都会触发异常
- 这是 Linux 内核的默认配置(
INVALID_IO_BITMAP_OFFSET = 0x8000)
0xDFFF 与 0x8000 的区别:
- 0xDFFF :Intel x86 架构规定的硬件上限,表示启用位图时
io_bitmap_base的最大有效值(必须 ≤ 0xDFFF) - 0x8000 :Linux 内核的软件约定值,表示禁用 I/O 权限位图(当
io_bitmap_base ≥ TSS段限制时,位图被禁用) - 关系:0x8000 < 0xDFFF,0x8000 是软件用来表示"禁用"的特殊值,而 0xDFFF 是硬件允许的最大值
- 使用场景:启用位图时使用 0x0068 等小值(≤ 0xDFFF),禁用位图时使用 0x8000
6.3 IOPL 与 I/O 权限位图的协同工作
6.3.1 检查优先级
┌─────────────────────────────────────┐
│ CPU 执行 I/O 指令 │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 检查 1: CPL ≤ IOPL? │
└──────────────┬──────────────────────┘
│
┌──────┴──────┐
│ │
是│ │否
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ 允许访问 │ │ 检查 2: 位图检查 │
│ 跳过位图 │ │ CPL > IOPL 或 │
│ │ │ 虚拟-8086 模式 │
└──────────────┘ └────────┬─────────┘
│
▼
┌──────────────────┐
│ 读取 io_bitmap │
│ 检查对应位 │
└────────┬─────────┘
│
┌────────┴────────┐
│ │
位=0 位=1
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ 允许访问 │ │ 触发 #GP │
└──────────┘ └──────────┘
6.3.2 使用场景
场景 1:完全禁止用户态 I/O
c
// 设置 IOPL = 0,禁用 I/O 权限位图
IOPL = 0;
io_bitmap_base = INVALID_IO_BITMAP_OFFSET; // 0x8000
// 结果:只有内核(CPL = 0)可以执行 I/O
场景 2:允许特定端口访问
c
// 设置 IOPL = 0,启用 I/O 权限位图
IOPL = 0;
io_bitmap_base = offsetof(struct tss_struct, io_bitmap);
// 在位图中允许端口 0x60-0x64(键盘控制器)
// 设置对应位为 0(允许访问)
// 结果:用户程序可以访问端口 0x60-0x64,其他端口被禁止
场景 3:虚拟-8086 模式
c
// 在虚拟-8086 模式下,CPL 被视为 3
// 必须使用 I/O 权限位图来控制访问
// 即使 IOPL = 3,也需要位图检查
6.4 Linux 内核中的 I/O 权限控制
6.4.1 默认配置
Linux 内核的默认 I/O 权限配置:
c
// 每个 CPU 的 TSS 初始化
void cpu_init(void) {
struct tss_struct *t = &per_cpu(init_tss, cpu);
// 禁用 I/O 权限位图(默认禁止所有用户态 I/O)
t->io_bitmap_base = INVALID_IO_BITMAP_OFFSET; // 0x8000
// 初始化位图(即使不使用,也初始化为全禁止)
for (i = 0; i <= IO_BITMAP_LONGS; i++)
t->io_bitmap[i] = ~0UL; // 全 1 = 禁止访问
}
6.4.2 sys_ioperm 系统调用
sys_ioperm 系统调用允许特权程序(需要 CAP_SYS_RAWIO)为当前任务启用特定的 I/O 端口访问:
c
// 用户程序调用
ioperm(0x60, 5, 1); // 允许访问端口 0x60-0x64
// 内核实现(简化)
long sys_ioperm(unsigned long from, unsigned long num, int turn_on) {
// 1. 权限检查:需要 CAP_SYS_RAWIO
// 2. 分配/初始化 I/O 位图
// 3. 设置位图中的对应位
// 4. 设置延迟加载标志
}
6.4.3 延迟加载机制
Linux 内核使用延迟加载优化 I/O 权限位图的更新:
c
// 在 sys_ioperm 中
tss->io_bitmap_base = INVALID_IO_BITMAP_OFFSET_LAZY; // 0x9000
工作流程:
sys_ioperm修改线程的 I/O 位图- 设置
io_bitmap_base = 0x9000(延迟加载标志) - CPU 在下一次 I/O 操作时检测到特殊值,触发异常
- 异常处理程序将线程的位图复制到 TSS
- 更新
io_bitmap_base为实际偏移 - 重试 I/O 操作
优势 :避免每次 ioperm() 调用都立即更新 TSS,提高性能。
6.5 每任务 I/O 权限
每个任务可以有自己的 I/O 权限设置:
- 任务 A:允许访问端口 0x60-0x64(键盘控制器)
- 任务 B:禁止所有 I/O 访问
- 任务 C:允许访问端口 0x3F8-0x3FF(串口)
这种设计提供了细粒度的 I/O 访问控制,允许不同任务有不同的 I/O 权限,提高了系统的安全性和灵活性。
7. TSS 在 Linux 内核中的实现
7.1 每 CPU TSS
Linux 内核为每个 CPU 核心维护一个 TSS:
c
DECLARE_PER_CPU(struct tss_struct, init_tss);
- 每个 CPU 有自己的 TSS 实例
- 避免多 CPU 之间的竞争条件
- 提高性能(避免锁竞争)
7.2 TSS 初始化
TSS 在 CPU 初始化时设置:
c
// 在 arch/i386/kernel/process.c 中
void __init cpu_init(void)
{
struct tss_struct *t = &per_cpu(init_tss, cpu);
// 设置 TSS 描述符
set_tss_desc(cpu, t);
// 初始化 I/O 权限位图
t->io_bitmap_base = offsetof(struct tss_struct, io_bitmap);
for (i = 0; i <= IO_BITMAP_LONGS; i++)
t->io_bitmap[i] = ~0UL; // 全部禁止
// 加载 TSS
load_TR_desc();
}
7.3 TSS 描述符设置
TSS 描述符在 GDT 中设置:
c
// 在 include/asm-i386/desc.h 中
static inline void set_tss_desc(unsigned int cpu, void *addr)
{
_set_tssldt_desc(&per_cpu(cpu_gdt_table, cpu)[GDT_ENTRY_TSS],
(int)addr,
offsetof(struct tss_struct, __cacheline_filler) - 1,
0x89);
}
- 作用 :
set_tss_desc函数用于在指定 CPU 的 GDT(全局描述符表)中设置 TSS 描述符,使 CPU 能够通过段选择符访问该 TSS。 - 参数 :
cpu指定目标 CPU 编号,addr是 TSS 结构体的内存地址,函数会将该地址写入 GDT 中索引为GDT_ENTRY_TSS(16)的描述符。 - 段限制 :
offsetof(struct tss_struct, __cacheline_filler) - 1计算 TSS 的有效长度(到缓存行填充之前),作为描述符的段限制字段。 - 描述符类型 :
0x89是 32 位 TSS 描述符的类型标识,表示这是一个存在的、可用的 32 位 TSS 描述符(P=1, DPL=0, Type=1001)。 - 底层实现 :函数内部调用
_set_tssldt_desc宏,使用内联汇编直接操作 GDT 表项,设置描述符的基地址、段限制和类型字段。