TSS(Task-State Segment)任务状态段详解

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 的作用

  1. 任务状态保存:当任务被切换出去时,CPU 自动将当前任务的状态保存到其 TSS 中
  2. 任务状态恢复:当任务被切换回来时,CPU 从 TSS 中恢复任务的所有状态
  3. 特权级切换:提供不同特权级别的栈指针,支持特权级切换
  4. 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位模式下的变化

  1. 栈指针扩展为 64 位rsp0, rsp1, rsp2 都是 64 位
  2. 中断栈表(IST)ist[7] 提供 7 个中断栈指针,用于处理特定类型的中断
  3. 简化结构:移除了许多在 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    ; 通过任务门调用任务

任务门的作用:

  1. 间接任务切换:不直接指定 TSS,而是通过任务门间接访问
  2. 特权级控制:可以控制访问任务所需的特权级
  3. 任务隔离:提供额外的任务访问控制层

4.4 任务门 vs 直接 TSS 访问

特性 直接 TSS 访问 任务门访问
访问方式 直接指定 TSS 选择符 通过任务门间接访问
特权级检查 TSS 描述符中的特权级 任务门中的特权级
灵活性 较低 较高
使用场景 简单任务切换 需要额外控制的场景

5. IA-32e 模式(64位模式)下的 TSS

5.1 硬件任务切换的取消

在 IA-32e 模式(64位模式)下,硬件任务切换不再被支持。这意味着:

  • 不能使用 CALL 或 JMP 指令直接切换到 TSS
  • 不能使用任务门进行任务切换
  • 任务切换必须通过软件实现

5.2 TSS 的继续存在

尽管硬件任务切换被取消,TSS 仍然存在,但用途发生了变化:

  1. 栈指针存储:存储不同特权级的栈指针(主要是 rsp0)
  2. 中断栈表(IST):提供 7 个中断栈指针
  3. 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 的方式:

  1. 每 CPU 一个 TSS:每个 CPU 核心有自己的 TSS
  2. 主要用途
    • 存储内核栈指针(rsp0)
    • 存储 I/O 权限位图
    • 提供中断栈表(IST)
  3. 任务切换:完全通过软件实现,不依赖硬件任务切换

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 时才能执行:

  1. IN: 从 I/O 端口读取数据
  2. INS: 从 I/O 端口读取字符串
  3. OUT: 向 I/O 端口写入数据
  4. OUTS: 向 I/O 端口写入字符串
  5. CLI: 清除中断使能标志(Clear Interrupt-enable Flag)
  6. 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: 中断返回

重要限制

  1. 特权要求 :只有运行在特权级 0 的过程才能修改 IOPL
  2. 静默失败:如果较低特权级的过程尝试修改 IOPL,不会触发异常,IOPL 值保持不变
  3. 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:所有位都必须设置为 1(禁止访问)
  2. 必须在段限制内:该字节必须在 TSS 段限制范围内
  3. 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 地址

规则

  1. 位图覆盖的端口:位图中明确表示的端口按位图规则检查
  2. 位图未覆盖的端口:被视为位图中设置了对应位(禁止访问)

示例

复制代码
假设 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

工作流程

  1. sys_ioperm 修改线程的 I/O 位图
  2. 设置 io_bitmap_base = 0x9000(延迟加载标志)
  3. CPU 在下一次 I/O 操作时检测到特殊值,触发异常
  4. 异常处理程序将线程的位图复制到 TSS
  5. 更新 io_bitmap_base 为实际偏移
  6. 重试 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);
}
  1. 作用set_tss_desc 函数用于在指定 CPU 的 GDT(全局描述符表)中设置 TSS 描述符,使 CPU 能够通过段选择符访问该 TSS。
  2. 参数cpu 指定目标 CPU 编号,addr 是 TSS 结构体的内存地址,函数会将该地址写入 GDT 中索引为 GDT_ENTRY_TSS(16)的描述符。
  3. 段限制offsetof(struct tss_struct, __cacheline_filler) - 1 计算 TSS 的有效长度(到缓存行填充之前),作为描述符的段限制字段。
  4. 描述符类型0x89 是 32 位 TSS 描述符的类型标识,表示这是一个存在的、可用的 32 位 TSS 描述符(P=1, DPL=0, Type=1001)。
  5. 底层实现 :函数内部调用 _set_tssldt_desc 宏,使用内联汇编直接操作 GDT 表项,设置描述符的基地址、段限制和类型字段。
相关推荐
木里先森2 小时前
解决报错:/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32‘ not found
linux·python
shizhan_cloud2 小时前
IF 条件语句的知识与实践
linux·运维
郝学胜-神的一滴2 小时前
Linux信号四要素详解:从理论到实践
linux·服务器·开发语言·网络·c++·程序人生
赖small强2 小时前
【Linux驱动开发】DDR 内存架构与 Linux 平台工作机制深度解析
linux·驱动开发·ddr·sdram·ddr controller
阿干tkl2 小时前
CentOS Stream 8 网络绑定(Bonding)配置方案
linux·网络·centos
Leon-Ning Liu2 小时前
【系列实验二】RAC 19C集群:CentOS 7.9 原地升级至 Oracle Linux 8.10 实战笔记
linux·数据库·oracle·centos
大聪明-PLUS2 小时前
C++编程中存在的问题
linux·嵌入式·arm·smarc
pingzhuyan2 小时前
linux运维异常(总) - 排查与修复(系统yum,docker,网络dns解析等)
linux·运维·docker·centos·shell
问道飞鱼2 小时前
【Linux知识】Shell 脚本参数详解:从基础到高级应用
linux·运维·服务器·shell