mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2082 字
6 分钟
用户空间:TSS 与 Ring 3 切换
2021-08-20

x86 架构设计了 4 个特权级(Ring 0-3),但几乎所有操作系统只用了 Ring 0 和 Ring 3 两个。为什么不用中间的 Ring 1 和 Ring 2?因为两级隔离已经足够:内核跑在 Ring 0,拥有全部权限;用户程序跑在 Ring 3,权限受限。所有代码都跑在 Ring 0 的危险在于——一个用户程序的 bug 就能摧毁整个系统。

特权级的动机#

在操作系统中,区分用户态和内核态有以下几个重要原因:

  1. 安全性:防止用户程序直接访问硬件设备和关键内核数据结构
  2. 稳定性:用户程序错误不会导致整个系统崩溃
  3. 资源管理:内核可以统一管理系统资源,实现公平调度
  4. 权限控制:不同级别的用户程序拥有不同的访问权限

理解了特权级隔离的必要性,接下来看 x86 硬件是如何实现这种隔离的。

x86 特权级机制#

x86 处理器提供了 4 个特权级(Ring 0-3),形成了分层的安全架构:

graph TD A[Ring 0 内核态<br/>最高权限] --> B[Ring 1] B --> C[Ring 2] C --> D[Ring 3 用户态<br/>最低权限] style A fill:#f66,stroke:#333,stroke-width:2px style B fill:#f96,stroke:#333,stroke-width:1px style C fill:#fc9,stroke:#333,stroke-width:1px style D fill:#9f9,stroke:#333,stroke-width:1px

x86 定义了三个与特权级相关的概念:

  • CPL(Current Privilege Level):当前代码段的特权级,存储在 CS 段寄存器的低 2 位
  • DPL(Descriptor Privilege Level):段描述符或门描述符的特权级,规定了访问该段所需的最低权限
  • RPL(Requested Privilege Level):请求者特权级,存储在段选择子的低 2 位

特权级检查规则max(CPL, RPL) <= DPL

这意味着访问权限由当前特权级和请求特权级中的较高者决定,只有当这个级别小于等于目标的 DPL 时,访问才被允许。

特权级切换涉及栈的切换——从 Ring 0 栈切换到 Ring 3 栈。CPU 需要知道 Ring 3 栈在哪里,这个信息存储在 TSS 中。

TSS(任务状态段)#

TSS 是 x86 处理器用于存储任务状态的特殊数据结构。在特权级切换时,CPU 会自动从 TSS 中读取新的栈指针,这是实现用户态/内核态切换的关键。

typedef struct tss {
uint16_t link; // 上一任务的链接
uint16_t link_high;
uint32_t esp0; // Ring 0 栈指针
uint16_t ss0; // Ring 0 栈段选择子
uint16_t ss0_high;
uint32_t esp1; // Ring 1 栈指针
uint16_t ss1; // Ring 1 栈段选择子
uint16_t ss1_high;
uint32_t esp2; // Ring 2 栈指针
uint16_t ss2; // Ring 2 栈段选择子
uint16_t ss2_high;
uint32_t cr3; // 页目录基址
uint32_t eip; // 指令指针
uint32_t eflags; // 标志寄存器
uint32_t eax; // 通用寄存器
uint32_t ecx;
uint32_t edx;
uint32_t ebx;
uint32_t esp; // 栈指针
uint32_t ebp;
uint32_t esi;
uint32_t edi;
uint16_t es; // 段选择子
uint16_t es_high;
uint16_t cs;
uint16_t cs_high;
uint16_t ss;
uint16_t ss_high;
uint16_t ds;
uint16_t ds_high;
uint16_t fs;
uint16_t fs_high;
uint16_t gs;
uint16_t gs_high;
uint16_t ldtr;
uint16_t ldtr_high;
uint16_t trap;
uint16_t trap_high;
uint32_t iomap; // I/O 位图基址
} __attribute__((packed)) tss_t;

ESP0 和 SS0 是实现用户态切换的核心:

  • 当 CPU 从 Ring 3 切换到 Ring 0 时(如通过中断或系统调用)
  • CPU 自动从 TSS.esp0 加载新的栈指针
  • CPU 自动从 TSS.ss0 加载新的栈段选择子
  • 这确保了每个用户进程有独立的内核栈

CR3 字段:

  • 存储页目录基址
  • 在进程切换时需要更新
  • 实现地址空间隔离

TSS 提供了 Ring 3 的栈信息,但实际的切换操作由 iret 指令完成。

iret 与特权级切换#

iret(Interrupt Return)指令用于从中断处理程序返回。当返回的目标特权级低于当前特权级时,iret 会执行特权级切换。

从内核态到用户态的切换#

使用 iret 指令从 Ring 0 切换到 Ring 3 需要构建特殊的栈帧:

; void switch_to_usermode(uint32_t entry, uint32_t user_esp)
switch_to_usermode:
mov eax, [esp + 4] ; entry - 用户态入口地址
mov ebx, [esp + 8] ; user_esp - 用户态栈顶
cli
; 设置用户态数据段寄存器
mov cx, USER_DATA_SEL
mov ds, cx
mov es, cx
mov fs, cx
mov gs, cx
; 构建 iret 帧(从低地址到高地址)
push dword USER_DATA_SEL ; SS3 (用户数据段选择子)
push ebx ; ESP3 (用户栈顶)
pushfd ; EFLAGS
pop edx
or edx, 0x200 ; 设置 IF 标志,启用中断
push edx ; EFLAGS (with IF=1)
push dword USER_CODE_SEL ; CS3 (用户代码段选择子)
push eax ; EIP (用户入口地址)
iret ; 执行特权级切换

iret 帧结构说明

栈(从高地址到低地址):
┌─────────────────┐
│ 用户代码入口 │ ← EIP
│ 用户代码段选择子 │ ← CS (USER_CODE_SEL)
│ EFLAGS 寄存器 │
│ 用户栈顶 │ ← ESP
│ 用户数据段选择子 │ ← SS (USER_DATA_SEL)
└─────────────────┘

从用户态到内核态的切换#

用户态通过以下方式进入内核态:

  1. 系统调用int 0x80sysenter 指令
  2. 硬件中断:定时器、键盘等外部设备触发
  3. 异常:缺页、除零、通用保护异常

当这些事件发生时,CPU 会:

  1. 检查特权级变化
  2. 自动从 TSS.esp0 加载内核栈
  3. 将用户态 SS、ESP、EFLAGS、CS、EIP 压入新栈
  4. 跳转到中断处理程序

iret 指令需要知道 Ring 3 的代码段和数据段选择子——这些需要在 GDT 中预先配置。

用户代码/数据段#

为了让用户程序在 Ring 3 运行,需要在 GDT 中创建 DPL=3 的段描述符:

// 段选择子定义
#define KERNEL_CODE_SEL 0x08 // index=1, RPL=0
#define KERNEL_DATA_SEL 0x10 // index=2, RPL=0
#define USER_CODE_SEL 0x23 // index=4, RPL=3 = (4<<3)|3
#define USER_DATA_SEL 0x2B // index=5, RPL=3 = (5<<3)|3
#define TSS_SEL 0x28 // index=5, RPL=0
// 用户代码段描述符 (DPL=3)
// Base=0, Limit=0xFFFFF, Type=Code/Execute/Read, DPL=3
gdt_set_gate(4, 0, 0xFFFFF, GDT_CODE_USER, GDT_FLAGS_USER);
// 用户数据段描述符 (DPL=3)
// Base=0, Limit=0xFFFFF, Type=Data/ReadWrite, DPL=3
gdt_set_gate(5, 0, 0xFFFFF, GDT_DATA_USER, GDT_FLAGS_USER);

段描述符访问字节

  • GDT_CODE_USER = 0xFA

    • P=1(存在)
    • DPL=3(Ring 3)
    • S=1(代码/数据段)
    • Type=1010(代码段,可读,可执行)
  • GDT_DATA_USER = 0xF2

    • P=1(存在)
    • DPL=3(Ring 3)
    • S=1(代码/数据段)
    • Type=0010(数据段,可读写)

用户程序在 Ring 3 运行,但有时需要请求内核服务。直接调用内核函数是不允许的——必须通过系统调用这一受控通道。

系统调用框架#

系统调用是用户程序请求内核服务的唯一合法途径:

flowchart TD A[用户程序] -->|int 0x80| B[系统调用入口<br/>syscall_handler] B --> C[检查系统调用号<br/>frame->eax] C --> D[查找系统调用表<br/>syscall_table[]] D --> E[调用系统调用函数<br/>sys_write, sys_read...] E --> F[返回结果到 frame->eax] F --> G[iret 返回用户态] style A fill:#9f9,stroke:#333 style B fill:#ff9,stroke:#333 style E fill:#f66,stroke:#333

系统调用表实现#

/* 系统调用类型定义 */
typedef int (*syscall_handler_t)(uint32_t arg1, uint32_t arg2, uint32_t arg3);
/* 系统调用处理函数表 */
static syscall_handler_t syscall_table[SYSCALL_COUNT];
/* 系统调用中断处理函数 */
void syscall_handler(interrupt_frame_t *frame)
{
uint32_t syscall_num = frame->eax; // 系统调用号
uint32_t arg1 = frame->ebx; // 参数1
uint32_t arg2 = frame->ecx; // 参数2
uint32_t arg3 = frame->edx; // 参数3
int ret = -1;
if (syscall_num < SYSCALL_COUNT) {
syscall_handler_t handler = syscall_table[syscall_num];
if (handler != NULL) {
ret = handler(arg1, arg2, arg3);
}
}
/* 返回值存入 EAX */
frame->eax = (uint32_t)ret;
}

系统调用注册#

void syscall_init(void)
{
/* 清空系统调用表 */
for (int i = 0; i < SYSCALL_COUNT; i++) {
syscall_table[i] = NULL;
}
/* 注册系统调用 */
syscall_table[SYS_EXIT] = (syscall_handler_t)sys_exit;
syscall_table[SYS_READ] = (syscall_handler_t)sys_read;
syscall_table[SYS_WRITE] = (syscall_handler_t)sys_write;
syscall_table[SYS_YIELD] = (syscall_handler_t)sys_yield;
syscall_table[SYS_GETPID] = (syscall_handler_t)sys_getpid;
syscall_table[SYS_SLEEP] = (syscall_handler_t)sys_sleep;
syscall_table[SYS_PUTS] = (syscall_handler_t)sys_puts;
syscall_table[SYS_GETTID] = (syscall_handler_t)sys_gettid;
/* 注册 0x80 中断处理函数 */
register_interrupt_handler(0x80, syscall_handler);
vga_printf("[Syscall] Initialized with %d system calls\n", SYSCALL_COUNT);
}

用户态调用示例#

/* 用户程序:系统调用示例 */
static void user_program(void)
{
/* SYS_PUTS (syscall #6): 打印字符串 */
__asm__ volatile (
"mov $6, %%eax\n" ; 系统调用号
"mov %0, %%ebx\n" ; 参数1:字符串地址
"int $0x80\n" ; 触发系统调用
:
: "r"("[User] Hello from Ring 3!\n")
: "eax", "ebx"
);
/* SYS_GETTID (syscall #7): 获取线程 ID */
int tid;
__asm__ volatile (
"mov $7, %%eax\n"
"int $0x80\n"
: "=a"(tid) ; 返回值在 EAX 中
);
/* SYS_EXIT (syscall #0): 退出进程 */
__asm__ volatile (
"mov $0, %%eax\n"
"mov $0, %%ebx\n" ; 退出码
"int $0x80\n"
::: "eax", "ebx"
);
}

代码实现#

文件结构#

12.kernel-userspace/
├── boot/
│ ├── mbr.S # MBR 引导程序
│ └── loader.S # 加载器
├── kernel/
│ ├── include/
│ │ ├── process.h # 进程结构定义
│ │ ├── syscall.h # 系统调用定义
│ │ ├── tss.h # TSS 接口
│ │ └── gdt.h # GDT 接口
│ ├── task/
│ │ ├── process.c # 进程管理
│ │ └── tss.c # TSS 封装
│ ├── interrupt/
│ │ ├── syscall.c # 系统调用实现
│ │ └── interrupt.S# 中断处理汇编
│ ├── arch/x86/
│ │ ├── usermode.S # 特权级切换汇编
│ │ ├── gdt.S # GDT 加载汇编
│ │ └── context_switch.S# 上下文切换
│ ├── mem/
│ │ └── gdt.c # GDT 和 TSS 初始化
│ └── kernel.c # 主程序和用户态测试
└── Makefile

Ring 3 切换完整流程#

flowchart TD A[内核线程 usermode_launcher] --> B[分配用户栈页面<br/>USER_STACK_BASE] B --> C[映射用户栈到页表<br/>vmm_map_page_default] C --> D[获取当前任务<br/>current_task] D --> E[更新 TSS.esp0<br/>tss_set_kernel_stack] E --> F[调用 switch_to_usermode] F --> G[构建 iret 帧<br/>SS3/ESP3/EFLAGS/CS3/EIP] G --> H[执行 iret 指令] H --> I[CPU 切换到 Ring 3] I --> J[用户程序 user_program 运行] J --> K[执行系统调用 int 0x80] K --> L[CPU 自动切换到 TSS.esp0] L --> M[系统调用处理程序<br/>syscall_handler] M --> N[iret 返回用户态] style A fill:#ff9,stroke:#333 style I fill:#9f9,stroke:#333,stroke-width:3px style J fill:#9f9,stroke:#333 style M fill:#f66,stroke:#333

TSS 和用户态段配置#

文件kernel/mem/gdt.c

void gdt_init()
{
memset(&gdt, 0, sizeof(gdt));
memset(&tss, 0, sizeof(tss));
gdt_ptr.limit = sizeof(gdt_entry_t) * GDT_ENTRY_COUNT - 1;
gdt_ptr.base = (uint32_t)&gdt;
// Null Descriptor
gdt_set_gate(0, 0, 0, 0, 0);
// 内核代码段 (Ring 0)
gdt_set_gate(1, 0, 0xFFFFF, GDT_CODE_KERNEL, GDT_FLAGS_KERNEL);
// 内核数据段 (Ring 0)
gdt_set_gate(2, 0, 0xFFFFF, GDT_DATA_KERNEL, GDT_FLAGS_KERNEL);
// 视频段 (Ring 0)
gdt_set_gate(3, 0x000B8000, 0x07FFF, GDT_DATA_KERNEL, GDT_FLAGS_VEDIO);
// 用户代码段 (Ring 3)
gdt_set_gate(4, 0, 0xFFFFF, GDT_CODE_USER, GDT_FLAGS_USER);
// 用户数据段 (Ring 3)
gdt_set_gate(5, 0, 0xFFFFF, GDT_DATA_USER, GDT_FLAGS_USER);
// 设置 TSS
uint32_t base = (uint32_t)&tss;
uint32_t limit = sizeof(tss) - 1;
gdt_set_gate(6, base, limit, GDT_TSS_AVAIL, GDT_FLAGS_TSS);
// 初始化 TSS
tss.cs = GDT_KERNEL_CODE_SEL;
tss.ss = tss.ds = tss.es = tss.fs = tss.gs = GDT_KERNEL_DATA_SEL;
tss.ss0 = GDT_KERNEL_DATA_SEL;
tss.esp0 = GDT_TSS_ESP0; // 稍后更新
tss.iomap_base = sizeof(tss);
gdt_flush((uint32_t)&gdt_ptr);
tss_flush();
}

这段代码完成了 GDT 的初始化,重点创建了 Ring 3 的代码段和数据段,以及 TSS 段。用户段的 DPL=3 允许 Ring 3 访问,而 TSS 的 esp0ss0 字段定义了切换到 Ring 0 时使用的内核栈。

用户态切换汇编#

文件kernel/arch/x86/usermode.S

; void switch_to_usermode(uint32_t entry, uint32_t user_esp)
switch_to_usermode:
mov eax, [esp + 4] ; entry
mov ebx, [esp + 8] ; user_esp
cli
; 设置用户态数据段寄存器
mov cx, USER_DATA_SEL
mov ds, cx
mov es, cx
mov fs, cx
mov gs, cx
; 构建 iret 帧
push dword USER_DATA_SEL ; SS
push ebx ; ESP
pushfd
pop edx
or edx, 0x200 ; IF=1
push edx ; EFLAGS
push dword USER_CODE_SEL ; CS
push eax ; EIP
iret

这是从内核态切换到用户态的关键代码。它首先设置所有数据段寄存器为用户数据段选择子,然后构建 iret 帧,最后执行 iret 指令。iret 会自动:

  1. 从栈中弹出 EIP 到指令指针
  2. 弹出 CS 并验证特权级变化
  3. 弹出 EFLAGS
  4. 弹出 ESP
  5. 弹出 SS
  6. 将 CPL 设置为 CS 的低 2 位(Ring 3)

系统调用处理#

文件kernel/interrupt/syscall.c

void syscall_handler(interrupt_frame_t *frame)
{
uint32_t syscall_num = frame->eax;
uint32_t arg1 = frame->ebx;
uint32_t arg2 = frame->ecx;
uint32_t arg3 = frame->edx;
int ret = -1;
if (syscall_num < SYSCALL_COUNT) {
syscall_handler_t handler = syscall_table[syscall_num];
if (handler != NULL) {
ret = handler(arg1, arg2, arg3);
}
}
/* 返回值存入 EAX */
frame->eax = (uint32_t)ret;
}
/* 系统调用示例:写入 */
int sys_write(int fd, const void *buf, uint32_t count)
{
if (fd != 1) {
return -1;
}
const char *str = (const char *)buf;
for (uint32_t i = 0; i < count; i++) {
vga_putc(str[i]);
}
return (int)count;
}

系统调用处理函数从中断帧中提取系统调用号和参数,通过系统调用表找到对应的处理函数并执行,最后将返回值写入 frame->eax。当 iret 返回用户态时,EAX 寄存器会被恢复,用户程序可以从中获取返回值。

用户态启动流程#

文件kernel/kernel.c

/* 用户态入口函数 */
static void user_program(void)
{
/* SYS_PUTS: 打印字符串 */
__asm__ volatile (
"mov $6, %%eax\n"
"mov %0, %%ebx\n"
"int $0x80\n"
:
: "r"("[User] Hello from Ring 3!\n")
: "eax", "ebx"
);
/* SYS_GETTID: 获取线程 ID */
int tid;
__asm__ volatile (
"mov $7, %%eax\n"
"int $0x80\n"
: "=a"(tid)
);
/* SYS_YIELD: 让出 CPU */
for (int i = 0; i < 5; i++) {
__asm__ volatile (
"mov $3, %%eax\n"
"int $0x80\n"
::: "eax"
);
}
/* SYS_EXIT: 退出 */
__asm__ volatile (
"mov $0, %%eax\n"
"mov $0, %%ebx\n"
"int $0x80\n"
::: "eax", "ebx"
);
while (1);
}
/* 内核线程:负责设置用户栈并切换到 Ring 3 */
static void usermode_launcher(void *arg)
{
(void)arg;
vga_printf("[Kernel] Preparing user-mode transition...\n");
/* 分配并映射用户栈页面 */
uint32_t stack_bottom = USER_STACK_BASE;
uint32_t stack_top = stack_bottom + USER_STACK_SIZE;
for (uint32_t addr = stack_bottom; addr < stack_top; addr += 4096) {
vmm_map_page_default(addr);
}
/* 更新 TSS 的 esp0 */
task_t *self = current_task();
tss_set_kernel_stack((uint32_t)self->kstack_top);
vga_printf("[Kernel] Jumping to user mode (entry=0x%x, stack=0x%x)...\n",
(uint32_t)user_program, stack_top);
/* iret 到 Ring 3 */
switch_to_usermode((uint32_t)user_program, stack_top);
vga_printf("[Kernel] ERROR: returned from user mode?!\n");
task_exit(-1);
}

这个示例展示了完整的用户态启动流程。usermode_launcher 是一个内核线程,负责准备用户栈、更新 TSS、然后切换到用户态。user_program 是真正的用户态函数,通过 int 0x80 系统调用与内核交互。

运行与验证#

编译运行#

cd 12.kernel-userspace
make clean
make all
make run

预期输出#

=== Chapter 12: User Space Support ===
[GDT] Initialized.
[TSS] Ready (managed by GDT)
[Syscall] Initialized with 8 system calls
--- GDT Segments ---
GDT Base: 0x..., Limit: 0x...
GDT[0]: Base=0x0, Limit=0x0, Access=0x0, Gran=0x0
-> Type: Not Present, DPL=0, Granularity=1B, Size=32-bit
GDT[1]: Base=0x0, Limit=0xFFFFF, Access=0x9A, Gran=0xC
-> Type: Code, Execute-Only, DPL=0, Granularity=4K, Size=32-bit
GDT[2]: Base=0x0, Limit=0xFFFFF, Access=0x92, Gran=0xC
-> Type: Data, Read/Write, DPL=0, Granularity=4K, Size=32-bit
GDT[4]: Base=0x0, Limit=0xFFFFF, Access=0xFA, Gran=0xC
-> Type: Code, Readable, DPL=3, Granularity=4K, Size=32-bit
GDT[5]: Base=0x0, Limit=0xFFFFF, Access=0xF2, Gran=0xC
-> Type: Data, Read/Write, DPL=3, Granularity=4K, Size=32-bit
GDT[6]: Base=0x..., Limit=0x..., Access=0x89, Gran=0x0
-> Type: TSS (Available 32-bit), DPL=0, Granularity=1B, Size=32-bit
--- Creating tasks ---
[Kernel Thread 1] iteration 0
[Kernel Thread 2] iteration 0
[Kernel Thread 1] iteration 1
[Kernel Thread 2] iteration 1
[Kernel] Preparing user-mode transition...
[Kernel] TSS esp0 = 0x...
[Kernel] Jumping to user mode (entry=0x..., stack=0x00B10000)...
[User] Hello from Ring 3!
[User] System call works!
[Kernel Thread 1] iteration 2
[Kernel Thread 2] iteration 2
[Kernel Thread 1] done
[Kernel Thread 2] done
[Syscall] sys_exit(0)

验证要点#

  1. GDT 配置正确:用户代码段和数据段的 DPL=3
  2. TSS 初始化成功:esp0 指向有效的内核栈
  3. 特权级切换成功:用户程序在 Ring 3 运行
  4. 系统调用正常:int 0x80 能正确触发并返回

踩坑记录#

  1. General Protection Fault (#GP)

    • 原因:特权级切换时段选择子错误或访问权限不足
    • 检查:GDT 配置、段选择子的 RPL、CPL 与 DPL 的关系
  2. Stack Fault (#SS)

    • 原因:内核栈未正确设置或 TSS.esp0 无效
    • 检查:TSS.esp0 和 TSS.ss0 是否指向有效内存
  3. Page Fault in User Mode (#PF)

    • 原因:用户空间地址未映射
    • 检查:页目录和页表设置,确保用户栈和代码已映射
  4. 系统调用无响应

    • 原因:IDT 中 0x80 中断未正确注册
    • 检查register_interrupt_handler(0x80, syscall_handler) 是否调用

调试技巧#

打印当前特权级#

void print_cpl(void)
{
uint16_t cs;
__asm__ volatile ("mov %%cs, %0" : "=r"(cs));
int cpl = cs & 0x3;
vga_printf("Current CPL: %d (Ring %d)\n", cpl, cpl);
}

打印 TSS 信息#

void print_tss_info(void)
{
extern struct tss_struct tss;
vga_printf("TSS: esp0=0x%x, ss0=0x%x\n", tss.esp0, tss.ss0);
vga_printf("TSS: cs=0x%x, ss=0x%x, ds=0x%x\n",
tss.cs, tss.ss, tss.ds);
}

GDT 调试输出#

本章已提供 gdt_dump() 函数,可以打印所有 GDT 表项和当前段寄存器的值,用于验证配置是否正确。

小结#

TSS 的 ESP0/SS0 字段为 Ring 3 到 Ring 0 的栈切换提供了硬件支持,iret 指令通过构造特定的栈帧完成从 Ring 0 到 Ring 3 的跳转,GDT 中 DPL=3 的段描述符让用户代码获得了合法的运行身份,而 int 0x80 系统调用框架则为 Ring 3 程序提供了安全请求内核服务的受控通道。这四部分共同构成了特权级隔离的完整机制。下一章将进入设备驱动开发,包括键盘、定时器和硬盘驱动,这些驱动运行在 Ring 0,为用户态程序提供硬件访问接口。

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

用户空间:TSS 与 Ring 3 切换
https://blog.souloss.com/posts/os/userspace-tss-ring3/
作者
Souloss
发布于
2021-08-20
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时