一个程序在运行过程中有两种特权状态:在用户态(Ring 3)执行用户编写的代码,在内核态(Ring 0)代表当前线程执行系统调用或处理中断。为了保证特权隔离与系统安全,操作系统为每个线程分别配置了用户栈内核栈

维度用户栈(User Stack)内核栈(Kernel Stack)
所在空间进程用户虚拟地址空间(低地址区)内核虚拟地址空间(高地址区)
访问权限Ring 3(用户态、内核态均可访问)Ring 0(仅内核态具有读写权限)
栈空间大小较大且可按需动态扩展(通常默认 8MB)固定且非常紧凑(x86_64 下通常仅为 8KB 或 16KB)
存放内容用户层函数的局部变量、调用参数、函数返回地址中断/系统调用陷入时保存的硬件现场(pt_regs)、内核函数的局部变量与调用帧
指针绑定用户态下 CPU 的栈顶指针寄存器(RSP)指向此处进入内核态后,CPU 的 RSP 被重设为指向该栈

用户栈与内核栈


为什么必须将两个栈严格分开?

  • 防止特权越权与数据泄露:内核栈中包含内核函数的调用链、内部数据结构指针以及系统调用参数。如果内核代码复用用户栈,用户程序可以通过指针越界任意读取内核数据;多线程环境下,并发线程甚至能在系统调用执行过程中修改栈上的内核局部变量或返回地址,从而劫持内核控制流。
  • 保障内核执行的可靠性:用户栈由应用程序掌控,极易因死递归或大数组分配触发栈溢出(Stack Overflow)。如果用户栈耗尽并触发缺页异常(Page Fault),CPU 必须在内核中运行异常处理程序;若此时内核仍使用已损毁的用户栈,压栈操作会立即导致二次故障(Double Fault)乃至系统崩溃。
  • 硬件级特权边界强制要求:x86 等 CPU 架构在硬件层面规定,当处理器跨越特权级(如 Ring 3 到 Ring 0)时,必须切换到对应特权级的受信任栈,无法沿用低特权级的栈指针。

系统调用时的栈流转机制

每个线程在内核中都绑定了一个固定的内核栈。当应用程序执行 syscall 或发生硬件中断时,栈的切换遵循严格的现场保全流程:

  1. 获取内核栈顶:CPU 捕获到特权级提升信号,从预先由内核配置好的硬件寄存器(如 x86 的 TSS 任务状态段或 MSR 寄存器)中直接读取当前线程的内核栈基地址。
  2. 保存用户态现场:CPU 硬件与内核入口汇编(如 SAVE_ALL)将当前用户态的执行状态——包括用户栈指针 RSP、指令指针 RIP、标志寄存器 RFLAGS 等打包成 pt_regs 结构,压入内核栈底保存。
  3. 执行内核逻辑:CPU 将 RSP 正式切换到内核栈,后续内核所有的 C 语言函数调用与局部变量分配全部在该内核栈上展开。
  4. 恢复并返回:内核处理完毕后执行 sysretiret 指令,逆序弹出 pt_regs 中暂存的用户态寄存器,将 RSP 重新指回原来的用户栈,特权级降回 Ring 3,用户代码继续向下执行。

文章引用图片来源:用户栈和内核栈 - 华为云
图片归原作者所有,如有侵权,请联系删除!

封面图来源于网络,如有侵权,请联系删除!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐