用户程序 - 理论讲解
1. x86 特权级机制
Ring 0-3
x86处理器有4个特权级(Ring 0-3),但大多数OS只使用两个:
| Ring | 用途 | 能力 |
|---|---|---|
| Ring 0 | 内核 | 所有指令、所有端口、所有内存 |
| Ring 3 | 用户 | 受限指令、无直接端口访问、受页表保护 |
特权级由什么决定?
CS寄存器的低2位(CPL, Current Privilege Level):
CS = 0x08 → 0000 1000 → CPL = 0 (内核)
CS = 0x1B → 0001 1011 → CPL = 3 (用户)
2. GDT更新
需要为用户态添加GDT条目:
| 索引 | 选择子 | 描述 |
|---|---|---|
| 0 | 0x00 | Null描述符 |
| 1 | 0x08 | 内核代码段 (Ring 0) |
| 2 | 0x10 | 内核数据段 (Ring 0) |
| 3 | 0x18 | 用户代码段 (Ring 3, DPL=3) |
| 4 | 0x20 | 用户数据段 (Ring 3, DPL=3) |
| 5 | 0x28 | TSS段 |
用户态段选择子需要设置RPL=3:
用户代码段选择子 = 0x18 | 3 = 0x1B
用户数据段选择子 = 0x20 | 3 = 0x23
3. TSS(任务状态段)
为什么需要TSS?
当从Ring 3切换到Ring 0(如中断发生时),CPU需要知道内核栈在哪里。这个信息存储在TSS中。
TSS结构(简化)
typedef struct {
uint32_t prev_tss; // 上一个TSS(硬件任务切换用,我们不用)
uint32_t esp0; // Ring 0栈指针 ← 关键!
uint32_t ss0; // Ring 0栈段选择子 ← 关键!
// ... 其他字段(大部分不用)
uint32_t esp1, ss1; // Ring 1(不用)
uint32_t esp2, ss2; // Ring 2(不用)
// ...
} __attribute__((packed)) tss_entry_t;进程切换时更新TSS
每次切换到新进程时,必须更新TSS中的esp0为该进程的内核栈顶:
void tss_set_kernel_stack(uint32_t stack) {
tss.esp0 = stack;
}4. 系统调用实现
INT 0x80
Linux传统上使用INT 0x80作为系统调用入口。我们也采用这个方案:
// 用户态调用约定:
// eax = 系统调用号
// ebx = 参数1
// ecx = 参数2
// edx = 参数3
// 返回值在eax中系统调用表
typedef int (*syscall_fn)(uint32_t, uint32_t, uint32_t);
static syscall_fn syscall_table[] = {
[SYS_EXIT] = sys_exit,
[SYS_WRITE] = sys_write,
[SYS_READ] = sys_read,
[SYS_YIELD] = sys_yield,
};
void syscall_handler(registers_t *regs) {
if (regs->eax >= NUM_SYSCALLS) return;
syscall_fn fn = syscall_table[regs->eax];
if (fn) {
regs->eax = fn(regs->ebx, regs->ecx, regs->edx);
}
}IDT中的系统调用门
系统调用中断的IDT条目需要特殊标志——DPL=3,允许用户态触发:
// 标志 0xEE = Present(1) + DPL=3(11) + 中断门(01110)
idt_set_gate(0x80, (uint32_t)isr128, 0x08, 0xEE);5. 进入用户态
使用IRET进入Ring 3
IRET不仅能从中断返回,还能用来切换到低特权级:
void enter_usermode(uint32_t entry, uint32_t user_stack) {
__asm__ volatile(
"cli\n"
"mov $0x23, %%ax\n" // 用户数据段选择子
"mov %%ax, %%ds\n"
"mov %%ax, %%es\n"
"mov %%ax, %%fs\n"
"mov %%ax, %%gs\n"
"push $0x23\n" // SS (用户数据段)
"push %1\n" // ESP (用户栈)
"pushf\n" // EFLAGS
"pop %%eax\n"
"or $0x200, %%eax\n" // 确保IF=1
"push %%eax\n"
"push $0x1B\n" // CS (用户代码段)
"push %0\n" // EIP (入口地址)
"iret\n" // 切换到Ring 3!
:
: "r"(entry), "r"(user_stack)
: "eax"
);
}IRET栈帧(跨特权级)
高地址
┌──────────┐
│ SS │ ← Ring 3的栈段
├──────────┤
│ ESP │ ← Ring 3的栈指针
├──────────┤
│ EFLAGS │
├──────────┤
│ CS │ ← Ring 3的代码段 (0x1B)
├──────────┤
│ EIP │ ← 用户程序入口
└──────────┘ ← ESP
6. 完整流程
GDT
1. 启动 → 加载GDT(含用户段) → [null|kcode|kdata|ucode|udata|tss]
↓
2. 加载TSS → ltr指令 TSS.esp0 = 内核栈
↓
3. 注册INT 0x80 → IDT[0x80] DPL=3, 允许用户态调用
↓
4. IRET到Ring 3 → 用户程序开始执行
↓
5. 用户调用int 0x80 → CPU自动:
- 切换到TSS.esp0/ss0 (内核栈)
- 压入Ring3的SS/ESP/EFLAGS/CS/EIP
- 跳转到IDT[0x80]的处理程序
↓
6. 内核处理系统调用 → IRET返回Ring 3
恭喜!
至此,你已经构建了一个具备以下功能的操作系统内核:
- 启动引导 → 保护模式 → C语言内核
- 屏幕输出 → 中断处理 → 键盘输入
- 物理内存管理 → 虚拟内存(分页)
- 进程管理 → 多任务调度
- 特权级分离 → 系统调用 → 用户程序
这是一个真正的操作系统内核的基础!