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. 语法示例
三、 POP 指令:出栈
POP 的作用是将栈顶的数据取出,并存入目标位置。
1. 执行 POP 时,CPU 做了什么?
当你执行 pop rax 时,发生了两件事:
1. 读取数据:将当前 RSP 指向的内存内容读入到 rax 中。
2. 修改指针:RSP = RSP + 8(回收这 8 个字节的空间,栈顶指针向上移动)。
* 等价于:mov rax, [rsp] + add rsp, 8
2. 语法示例
四、 为什么要用 PUSH 和 POP?
在实际编程中,它们主要有三大用途:
1. 临时保护寄存器(最常用)
假设你正在写一个函数,你需要使用 rax 做计算,但 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)。
错误示例:
六、 总结与图解
假设 RSP 初始值为 0x2000:
push rax(值为 0xA):RSP变为0x1FF8- 内存
[0x1FF8]存入0xA
push rbx(值为 0xB):RSP变为0x1FF0- 内存
[0x1FF0]存入0xB
pop rbx:- 从
[0x1FF0]读取0xB给rbx RSP变回0x1FF8
- 从
思考题:
如果你连续执行了两次 push:
pop 顺序应该是怎样的?(提示:记住羽毛球筒的逻辑)。