x86 架构设计了 4 个特权级(Ring 0-3),但几乎所有操作系统只用了 Ring 0 和 Ring 3 两个。为什么不用中间的 Ring 1 和 Ring 2?因为两级隔离已经足够:内核跑在 Ring 0,拥有全部权限;用户程序跑在 Ring 3,权限受限。所有代码都跑在 Ring 0 的危险在于——一个用户程序的 bug 就能摧毁整个系统。
特权级的动机
在操作系统中,区分用户态和内核态有以下几个重要原因:
- 安全性:防止用户程序直接访问硬件设备和关键内核数据结构
- 稳定性:用户程序错误不会导致整个系统崩溃
- 资源管理:内核可以统一管理系统资源,实现公平调度
- 权限控制:不同级别的用户程序拥有不同的访问权限
理解了特权级隔离的必要性,接下来看 x86 硬件是如何实现这种隔离的。
x86 特权级机制
x86 处理器提供了 4 个特权级(Ring 0-3),形成了分层的安全架构:
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)└─────────────────┘从用户态到内核态的切换
用户态通过以下方式进入内核态:
- 系统调用:
int 0x80或sysenter指令 - 硬件中断:定时器、键盘等外部设备触发
- 异常:缺页、除零、通用保护异常
当这些事件发生时,CPU 会:
- 检查特权级变化
- 自动从 TSS.esp0 加载内核栈
- 将用户态 SS、ESP、EFLAGS、CS、EIP 压入新栈
- 跳转到中断处理程序
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=3gdt_set_gate(4, 0, 0xFFFFF, GDT_CODE_USER, GDT_FLAGS_USER);
// 用户数据段描述符 (DPL=3)// Base=0, Limit=0xFFFFF, Type=Data/ReadWrite, DPL=3gdt_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 运行,但有时需要请求内核服务。直接调用内核函数是不允许的——必须通过系统调用这一受控通道。
系统调用框架
系统调用是用户程序请求内核服务的唯一合法途径:
系统调用表实现
/* 系统调用类型定义 */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 # 主程序和用户态测试└── MakefileRing 3 切换完整流程
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 的 esp0 和 ss0 字段定义了切换到 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 会自动:
- 从栈中弹出 EIP 到指令指针
- 弹出 CS 并验证特权级变化
- 弹出 EFLAGS
- 弹出 ESP
- 弹出 SS
- 将 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-userspacemake cleanmake allmake 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-bitGDT[1]: Base=0x0, Limit=0xFFFFF, Access=0x9A, Gran=0xC -> Type: Code, Execute-Only, DPL=0, Granularity=4K, Size=32-bitGDT[2]: Base=0x0, Limit=0xFFFFF, Access=0x92, Gran=0xC -> Type: Data, Read/Write, DPL=0, Granularity=4K, Size=32-bitGDT[4]: Base=0x0, Limit=0xFFFFF, Access=0xFA, Gran=0xC -> Type: Code, Readable, DPL=3, Granularity=4K, Size=32-bitGDT[5]: Base=0x0, Limit=0xFFFFF, Access=0xF2, Gran=0xC -> Type: Data, Read/Write, DPL=3, Granularity=4K, Size=32-bitGDT[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)验证要点
- GDT 配置正确:用户代码段和数据段的 DPL=3
- TSS 初始化成功:esp0 指向有效的内核栈
- 特权级切换成功:用户程序在 Ring 3 运行
- 系统调用正常:int 0x80 能正确触发并返回
踩坑记录
-
General Protection Fault (#GP)
- 原因:特权级切换时段选择子错误或访问权限不足
- 检查:GDT 配置、段选择子的 RPL、CPL 与 DPL 的关系
-
Stack Fault (#SS)
- 原因:内核栈未正确设置或 TSS.esp0 无效
- 检查:TSS.esp0 和 TSS.ss0 是否指向有效内存
-
Page Fault in User Mode (#PF)
- 原因:用户空间地址未映射
- 检查:页目录和页表设置,确保用户栈和代码已映射
-
系统调用无响应
- 原因: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,为用户态程序提供硬件访问接口。
参考
- x86 TSS and Privilege Levels - OSDev Wiki 上的 TSS 说明
- System Calls - 系统调用实现指南
- Segmentation - x86 段机制详解
- IRET Instruction - Intel 手册中的 iret 指令说明
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






