用户程序 - 理论讲解

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条目:

索引选择子描述
00x00Null描述符
10x08内核代码段 (Ring 0)
20x10内核数据段 (Ring 0)
30x18用户代码段 (Ring 3, DPL=3)
40x20用户数据段 (Ring 3, DPL=3)
50x28TSS段

用户态段选择子需要设置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语言内核
  • 屏幕输出 → 中断处理 → 键盘输入
  • 物理内存管理 → 虚拟内存(分页)
  • 进程管理 → 多任务调度
  • 特权级分离 → 系统调用 → 用户程序

这是一个真正的操作系统内核的基础!