定位“栈”的位置 —— %esp 和 %ss
概要:介绍定位“栈”的一对寄存器 %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 语言或汇编时,很多指令都在隐式地使用这两个寄存器:
- 函数调用(
call和ret):call指令会自动把下一条指令的地址(%eip)压入SS:ESP指向的栈中;ret指令会自动从栈中弹出一个地址写回%eip。 - 局部变量分配:在 C 语言中写一句
int a = 5;,编译器在底层通常会翻译成sub $4, %esp(将栈顶指针向下移动 4 个字节腾出空间),再把 5 存入其中。 - 中断上下文切换:即前文介绍的场景。
结合中断切换再看 %ss 和 %esp
在用户态运行时,%ss 和 %esp 指向的是用户栈。
一旦发生系统调用或中断,硬件查询 TSS 发现需要进入 Ring 0,便会执行以下操作:
- 保存旧栈:将用户态的
%ss和%esp暂时保存起来。 - 启用新栈:从 TSS 中读出内核栈的基址和栈顶地址,直接覆盖当前的
%ss和%esp。
- 这一步极其关键,相当于 CPU 把”写字板”从用户的桌子上,瞬间移到了内核保险柜的桌子上。
- 保存现场:在新启用的内核栈上,把刚才保存的用户态
%ss和%esp压入栈中,确保处理完毕后还能找回用户原来的栈。
总结来说,%ss 和 %esp 是 CPU 硬件自带的”栈管家”。无需手动计算内存地址,只需放心地 push 和 pop,这两个寄存器就会自动将数据整齐地压入和弹出栈。
留下评论