Skip to content

PUSH & POP

在汇编语言中,PUSH 和 POP 是操作 栈(Stack) 最核心的指令。

要理解这两条指令,必须先理解两个前提概念:栈的 LIFO 结构 和 栈指针寄存器 RSP。


一、 核心概念:什么是“栈”?

栈是一块特殊的内存区域,遵循 “后进先出”(LIFO, Last-In First-Out) 的原则。 * 类比:就像一个窄长的羽毛球筒,你最后放进去的球,必须最先拿出来。 * 增长方向(重要!):在 x86 架构中,栈是向低地址增长的。 * 当你往栈里放东西时,栈顶的内存地址会变小。 * 当你从栈里取东西时,栈顶的内存地址会变大。


二、 PUSH 指令:压栈

PUSH 的作用是将一个操作数(寄存器、内存值或立即数)存入栈中。

1. 执行 PUSH 时,CPU 做了什么?(以 64 位为例)

当你执行 push rax 时,发生了两件事: 1. 修改指针:RSP = RSP - 8(因为 RAX 是 8 字节,为了腾出空间,栈顶指针向下移动 8 个字节)。 2. 写入数据:将 rax 的值写入到当前 RSP 指向的内存位置。 * 等价于:sub rsp, 8 + mov [rsp], rax

2. 语法示例

push rax      ; 将 64 位寄存器值压入栈
push 100      ; 将立即数压入栈
push qword [rbx] ; 将内存中的 8 字节值压入栈

三、 POP 指令:出栈

POP 的作用是将栈顶的数据取出,并存入目标位置。

1. 执行 POP 时,CPU 做了什么?

当你执行 pop rax 时,发生了两件事: 1. 读取数据:将当前 RSP 指向的内存内容读入到 rax 中。 2. 修改指针:RSP = RSP + 8(回收这 8 个字节的空间,栈顶指针向上移动)。 * 等价于:mov rax, [rsp] + add rsp, 8

2. 语法示例

pop rbx       ; 将栈顶内容弹出到 rbx
pop qword [rcx] ; 将栈顶内容弹出到内存地址 rcx 处
; 注意:不能 pop 到立即数(pop 100 是非法的)

四、 为什么要用 PUSH 和 POP?

在实际编程中,它们主要有三大用途:

1. 临时保护寄存器(最常用)

假设你正在写一个函数,你需要使用 rax 做计算,但 rax 里原本存着非常重要的数据。

push rax      ; 备份:把旧的 rax 存到栈里
; ... 这里你可以随意使用 rax 做各种计算 ...
pop rax       ; 还原:计算完了,把旧值从栈里变回来

2. 函数调用与参数传递

  • 在 32 位汇编中,参数通常全部通过 push 压入栈。
  • 在 64 位汇编中,虽然前 6 个参数用寄存器传,但如果参数超过 6 个,剩下的也要靠 push 压栈。
  • 保存返回地址:当你执行 CALL 指令时,CPU 会自动 PUSH RIP(把下一条指令地址存入栈),这样函数执行完后,RET 指令通过 POP RIP 就能找回来时的路。

3. 局部变量空间

虽然我们常用 sub rsp, 32 这种方式一次性开辟空间,但本质上栈就是存放局部变量的地方。


五、 关键警告:栈平衡 (Stack Balance)

这是初学者最容易导致程序崩溃的原因:PUSH 和 POP 必须成对出现。

  • 如果你 push 了一次,却忘记了 pop,那么当函数执行到 RET 指令时,CPU 会把刚才你 push 进去的数据误认为是“返回地址”,然后跳到一个莫名其妙的地方,程序直接崩溃(Segmentation Fault)。

错误示例:

_start:
    push rax
    ; 忘记 pop rax 了!
    ret        ; 这里的 ret 会把栈顶本该是地址的东西读成刚才 push 的 rax 值,Boom!


六、 总结与图解

假设 RSP 初始值为 0x2000:

  1. push rax (值为 0xA):
    • RSP 变为 0x1FF8
    • 内存 [0x1FF8] 存入 0xA
  2. push rbx (值为 0xB):
    • RSP 变为 0x1FF0
    • 内存 [0x1FF0] 存入 0xB
  3. pop rbx:
    • 从 [0x1FF0] 读取 0xB 给 rbx
    • RSP 变回 0x1FF8

思考题: 如果你连续执行了两次 push:

push rax
push rbx
为了让寄存器里的值原封不动地还原回来,你的 pop 顺序应该是怎样的?(提示:记住羽毛球筒的逻辑)。