少于 1 分钟阅读 次阅读

概要:介绍定位“栈”的一对寄存器 %ss(堆栈段寄存器)与 %esp(栈指针寄存器):各自的字长与作用、x86 栈向下生长的反直觉设计,以及 SS:ESP 组合在函数调用、局部变量分配和中断切换中的隐式配合。

前面介绍过 %cs%eip 是一对用来定位代码的寄存器,而在 x86 架构中,%ss%esp 则是专门用来定位”栈”(Stack)的一对寄存器。

如果没有它们,C 语言里的局部变量、函数传参,甚至前面介绍的中断”保存现场”,统统都无法实现。

下面分别介绍这两个寄存器:

1. %ss (Stack Segment Register,堆栈段寄存器)

  • 字长:16 位。
  • 作用:存放的是当前栈段的选择子,用于告诉 CPU 当前使用的栈内存在哪里、权限是什么。
  • 在保护模式下%ss 指向全局描述符表(GDT)中的一个表项,该表项规定了这块栈内存的基地址、最大长度,以及关键的读写权限和 DPL(特权级)
  • 在用户态(Ring 3)时,%ss 指向一个 DPL 为 3 的普通数据段。
  • 进入内核(Ring 0)后,%ss 会被硬件强制切换为一个 DPL 为 0 的高权限数据段。

2. %esp (Extended Stack Pointer,扩展栈指针寄存器)

  • 字长:32 位(在 16 位实模式下叫 %sp,在 64 位下叫 %rsp)。
  • 作用:始终指向当前栈的”栈顶”,也就是栈中最新压入数据的内存偏移地址。
  • 一个反直觉的设计(重点):在 x86 架构中,栈是”向下生长”的
  • 具体来说,栈底位于内存的高地址,栈顶位于低地址。
  • 执行 push %eax(把数据压入栈)时,硬件会自动把 %esp 的值减 4,然后把 %eax 的数据存到新的 %esp 指向的地址。
  • 执行 pop %ebx(把数据弹出栈)时,硬件会把当前 %esp 指向的数据取出放入 %ebx,然后自动把 %esp 的值加 4

它们的配合方式(SS:ESP 机制)

与代码寻址依赖 CS:EIP 类似,所有的栈操作寻址,底层都依赖 SS:ESP

物理/线性地址 = %ss 指定的段基址 + %esp 的偏移量。

在编写 C 语言或汇编时,很多指令都在隐式地使用这两个寄存器:

  1. 函数调用(callretcall 指令会自动把下一条指令的地址(%eip)压入 SS:ESP 指向的栈中;ret 指令会自动从栈中弹出一个地址写回 %eip
  2. 局部变量分配:在 C 语言中写一句 int a = 5;,编译器在底层通常会翻译成 sub $4, %esp(将栈顶指针向下移动 4 个字节腾出空间),再把 5 存入其中。
  3. 中断上下文切换:即前文介绍的场景。

结合中断切换再看 %ss%esp

在用户态运行时,%ss%esp 指向的是用户栈

一旦发生系统调用或中断,硬件查询 TSS 发现需要进入 Ring 0,便会执行以下操作:

  1. 保存旧栈:将用户态的 %ss%esp 暂时保存起来。
  2. 启用新栈:从 TSS 中读出内核栈的基址和栈顶地址,直接覆盖当前的 %ss%esp
  • 这一步极其关键,相当于 CPU 把”写字板”从用户的桌子上,瞬间移到了内核保险柜的桌子上。
  1. 保存现场:在新启用的内核栈上,把刚才保存的用户态 %ss%esp 压入栈中,确保处理完毕后还能找回用户原来的栈。

总结来说,%ss%esp 是 CPU 硬件自带的”栈管家”。无需手动计算内存地址,只需放心地 pushpop,这两个寄存器就会自动将数据整齐地压入和弹出栈。

留下评论