3.6 栈
- 栈是一种具有特殊的访问方式的存储空间,他的特殊性就在于 最后一个进入这个空间的数据,是最先出去的
-
栈有两种基本的操作:入栈和出栈
-
- 入栈:将一个新的元素放到栈顶
- 出栈:从栈顶取出一个元素
- 栈顶元素总是最后一个入栈的,需要出的时候,又会是第一个被取出的
-
操作规则:LIFO
- (Last in first out) 后进先出
-
3.7 CPU提供的栈机制
- 现今的CPU都有栈的设计, CPU提供相关的指令来以栈的方式访问内存空间
-
这就意味着,我们再CPU编程的时候,可将一段内存当做栈来使用
push(入栈):
- push ax: 将寄存器ax中的数据送入栈中
pop(出栈):
- pop ax:从栈顶取出数据送入ax中
入栈和出栈的操作都是以 字 为单位进行的
- 字型数据用两个单元存放,高地址单元放高8位,低地址单元房地8位
1. CPU如何知道一段内存空间被当做栈使用?
-
有两个寄存器:
- 段寄存器:SS 存放栈顶的段地址
- 寄存器:SP 存放栈顶的偏移地址
- 任意时刻 SS:SP指向栈顶元素
2. 执行push和pop的时候,如何知道那个单元是栈顶单元的?
- push ax
-
- 先进行:SP = SP - 2
- 然后将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新的栈顶
- pop ax
-
- 先将栈顶(SS:SP)数据拿出来给ax
- 然后:SP = SP + 2
知识点:
- 当pop之后其实栈中的数据还是存在的但是并不属于栈中内内容了、不能操作、当下一次push的时候回将其进行覆盖,并没有一个真正的删除,和硬盘格式化之后还可以恢复是一个道理,只是改变的指针的指向
-
如果将10000~1000F作为栈的空间:初始转态的栈是空的,此时:
- ss=1000
- sp=0010(F的高一位地址 )
- 栈空间大小为16个字节,栈最底部的字节单元地址为 1000:000E
-
任意时刻 SS:SP指向栈顶,当栈中只有一个元素的时候:
**SS=1000,SP=000E**
- 栈为空,就相当于栈中唯一的元素被pop,出栈后,SP=SP+2,SP原来为000E,加2后SP=10
- 所以当栈为空的时候,SS=1000,SP=10
-
换个角度说:
- 任意时刻,SS:SP指向栈顶元素,当栈为空时候,栈中没有元素,也就不存在栈顶元素
- 所以SS:SP只能指向栈的最底部单元下面的单元,该单元中的偏移地址为栈最底部的字节单元的偏移地址+2
- 栈最底部字单元的地址为1000:000E,所以栈为空时 SP=000E-2=0010
只有栈内有元素的时候才有栈顶,否则不存在栈顶
3. CPU如何知道当前要执行的指令所在的位置?
- 寄存器cs和ip中存放着当前执行的段地址和偏移地址
3.8 栈顶超界的问题
- SS和SP只记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈是找到栈顶
- 当栈满的时候再使用push指令入栈,栈空的时候在pop指令出栈都会发证栈顶超界的问题。 栈顶越界是危险的,因为会覆盖掉其他数据
- 因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了一些具有其他用途的数据、代码等、这些数据、代码可能使我们自己程序中的、也有可能是别的程序中的、如果是系统关键内容就危险了
- 但是由于我们在入栈时的不小心而将这些数据、代码意外的改写、都会引发一连串的错误。我么当然希望CPU可以帮我们解决这个问题
-
可是,如何保证在入栈,出栈时,栈顶不会超出栈空间?
-
其实CPU在执行的时候只考虑两种情况
- 当前栈顶在何处
- 当前执行的指令是那一条
-
结论:
- 我们再编程的时候要看自己操心栈的超界问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界了;
- 执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致超界
-
3.9 push、pop指令
-
push指令和pop指令是可以在寄存器和内存之间传送数据的
栈与内存:
- 栈空间当然也是内存空间的一部分,它是一段可以以一种特殊方式进行访问的内存空间
- 栈只是一种数据结构的存放格式
-
push和pop的指令格式(1)
-
- push 寄存器:将一个寄存器的数据入栈
- pop 寄存器: 出栈,用一个寄存器接受出栈的数据
-
如:
- push ax
- pop bx
-
-
push和pop的指令格式(2)
-
- push 段寄存器:将一个段寄存器的数据入栈
- pop 段寄存器:出栈, 用一个段寄存器接受出栈的数据
-
如:
- push ds
- pop es
-
-
push和pop的指令格式(3)
-
- push 内存单元:将一对内存单元的数据入栈(栈操作都是以字为单位的)
- pop 内存单元:出栈, 用一对内存单元接受出栈的字单元数据
-
如:
- push [0]
- pop [2]
-
-
执行指令时,CPU要知道内存单元的地址,可以在push,pop指令中给出内存单元的偏移地址,段地址是在指令执行时,CPU从ds中自动获取的
段寄存器都是以S结尾的,通用寄存器都是以X结尾的
- 段地址在执行时可以从DS获得
- 数据的段地址永远是从DS获得
- 代码的段地址永远是从CS中获得
- 栈的段地址永远是从SS中获得
# 设置栈的段地址,ss=1000,不能直接向段寄存器送入数据,要通过ax通用寄存器传递 mov ax, 1000 mov ss, ax # 设置栈顶的偏移地址,因为栈为空,所以sp=0010 mov sp, 0010 # 压入数据 push ax push bx push ds
mov ax, 1000 mov ss, ax mov sp, 0100 mov ax=001a mov bx=001b push ax push bx sub ax, ax sub bx, bx pop bx pop ax
- 从上面的程序可以看到,用栈来暂存以后需要恢复的寄存器中的内存时,出栈的顺序要和入栈的顺序相反, 因为最后入栈的寄存器的内容在栈顶,所以在恢复时,是最先出栈的
mov bx, 1000 mov ss, bx mov ap, 10 mov ax, 002a mov bx, 002b push ax push bx pop ax pop bx
mov ax, 1000 mov ss, ax mov sp, 2 mov ax, 226 push, ax
- Pop、和push实际上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是 push和pop指令访问的内存单元的地址不是在指令中给出,而是ss:p指出的
- 我们需要是十分清楚的是,push和pop指令同mov指令不同, CPU执行mov指令只需要一步, 而执行push、pop指令却需要两步操作
-
执行push时:
- 先改变sp,后向ss:sp压入
-
执行pop时:
- 先读取ss:sp处的数据, 后改变sp
- push、pop指令修改的只是sp,也就是说栈顶的变化范围最大为:0~FFFF(SP寄存器的大小)
- 在ss:sp中存放栈顶的段地址和偏移地址, 提供入栈和出栈指令, 他们根据ss:sp指示的地址, 按照栈的方式访问内存单元
- 任意时刻。ss:sp执行栈顶元素
- 8086CPU只记录栈顶, 栈空间的大小我们要自己管理
-
用栈来暂存以后需要恢复的寄存器的内容, 寄存器出栈的顺序要和 入栈相反
- 栈是一种非常重要的机制, 一定要深入理解, 灵活掌握