栈(Stack)

栈是一种**后进先出(LIFO,Last In First Out)**的数据结构,在汇编中由 CPU 直接支持,通过 SS:SP 两个寄存器操作。

前置知识

需要了解 01-寄存器 中的 SS 和 SP 寄存器,以及 05-多段程序设计 中如何设置栈段。

栈的基本概念

8086 的栈有两个关键特性:

  1. 栈从高地址向低地址增长(和代码、数据的增长方向相反)
  2. 栈操作以字(2字节)为单位,push 和 pop 都操作 16 位数据

两个寄存器控制栈:

  • SS(Stack Segment):栈段寄存器,存放栈所在段的段地址
  • SP(Stack Pointer):栈顶指针,存放栈顶的偏移地址

SS:SP 永远指向栈顶元素(即最后压入栈的数据)。

push 指令(入栈)

格式:push 寄存器/内存单元/段寄存器

执行 push ax 时,CPU 做两件事:

SP = SP - 2                    ; 栈顶向上(低地址方向)移动2字节
将 AX 的值送入 SS:SP 指向的字单元  ; 数据写入新的栈顶位置
mov ax, 0123H
push ax            ; SP 减2,将 0123H 压入栈
push ds:[0]        ; 将内存中的字压入栈
push cs            ; 段寄存器也可以压栈

pop 指令(出栈)

格式:pop 寄存器/内存单元/段寄存器(注意:不能 pop CS!)

执行 pop ax 时,CPU 做两件事:

将 SS:SP 指向的字单元数据送入 AX  ; 从栈顶读取数据
SP = SP + 2                     ; 栈顶向下(高地址方向)回缩2字节
pop ax             ; 栈顶数据弹出到 AX
pop ds:[0]         ; 栈顶数据弹出到内存
pop ds             ; 弹出到段寄存器(CS 除外!)

pop CS 为什么不行?

和不能 mov cs, ax 一样,pop CS 会直接改变 CS 的值,导致 CPU 跳到错误位置执行。修改 CS 只能通过转移指令(jmp/call/ret/int)。

栈的可视化

假设 SS=1000H,SP=0010H,依次执行 push ax、push bx、pop cx:

初始状态(SP=0010H,栈空):
地址      内容
1000:00   ..
1000:02   ..
1000:04   ..
1000:06   ..
1000:08   ..
1000:0A   ..
1000:0C   ..
1000:0E   ..
1000:10   ..  ← SP 指向这里(栈底/空栈位置)

push ax(假设 AX=1122H,SP=000EH):
1000:0E   22H ← SP
1000:0F   11H

push bx(假设 BX=3344H,SP=000CH):
1000:0C   44H ← SP
1000:0D   33H
1000:0E   22H
1000:0F   11H

pop cx(CX=3344H,SP=000EH):
1000:0E   22H ← SP
1000:0F   11H
(1000:0C 和 1000:0D 的数据并没有被"删除",只是 SP 不再指向它们了,下次 push 会覆盖)

栈顶超界问题

8086 不会自动检查栈顶是否越界!如果 push 太多次(SP 减到0以下)或 pop 太多次(SP 超过栈底),会覆盖栈外的数据或代码,导致不可预测的错误。程序员必须自己保证栈的大小足够。

栈的用途

1. 临时保存寄存器的值

当你需要使用一个寄存器,但不想破坏它原来的值时:

push ax            ; 保存 AX
push bx            ; 保存 BX

mov ax, 1000H      ; 使用 AX 和 BX 做事情
mov bx, 2000H
; ... 一些操作

pop bx             ; 恢复 BX(注意顺序:先进后出!)
pop ax             ; 恢复 AX

push 和 pop 的顺序

先 push 的要后 pop,顺序必须相反。就像往箱子里放衣服,先放的在最下面,最后才能拿出来。

2. 数据传递(内存到内存的间接方式)

x86 不支持内存到内存的直接 mov,但可以用栈中转:

push [0]           ; 把 ds:0 的字压栈
pop [2]            ; 弹出到 ds:2,相当于把 [0] 的数据复制到 [2]

3. 函数调用(核心用途)

call 和 ret 指令就是用栈来保存返回地址的,这是栈最重要的用途。

4. 子程序间传递参数、保存局部变量

高级语言(C语言等)的函数调用栈帧就是用这个机制实现的。

在程序中设置栈

完整的栈设置方法(详见 05-多段程序设计):

stack segment
    dw 16 dup(0)    ; 分配 16 个字 = 32 字节的栈空间
stack ends

code segment
start:
    mov ax, stack
    mov ss, ax
    mov sp, 32      ; SP = 栈空间大小(字节数)。初始时栈空,SP 指向栈空间最顶端+1的位置
    ; ...
code ends

为什么 SP 初始值设为栈空间大小?因为栈从高地址往低地址增长,空栈时 SP 指向栈空间末尾之后的位置,第一次 push 时 SP 减 2 恰好指向栈空间最高的字单元。

push/pop 支持的操作数

; 寄存器
push ax
pop bx

; 段寄存器
push ds
pop es

; 内存单元
push word ptr [bx]
pop word ptr [si]

; ❌ 不支持的操作
push 1234H         ; 8086 不支持 push 立即数(80186以后才支持)
pop cs             ; 不能 pop CS
push al            ; ❌ push/pop 只操作字(16位),不能操作字节

相关