速通汇编(六)认识栈,SS、SP寄存器,push和pop指令的作用
一,栈
(一)栈的特点
栈是一种具有特殊访问方式的存储空间,特殊在于,进出这块存储空间的数据,“先进后出,后进先出”
由于栈的这个“先进后出”的特点,我们可以利用其来很好的操作内存
在《王爽汇编》中,作者用三本书十分形象地来描述了入栈和出栈的操作,对栈比较陌生的同学可以仔细理解,很理解的同学则可跳过
入栈:将数据放在栈的顶部
出栈:将栈顶部的数据取出
也就是说,无论是入栈和出栈,当前操作的数据一定是当前栈中最顶部的数据,我们假想有一个指针,它会一直指着栈顶数据,帮助我们定位
(二)CPU中的栈机制
栈先进后出的这个特点对于我们操作数据是十分方便的一个机制,因此CPU中设计好了提供给我们相关的指令用这种机制来访问内存
简而言之,我们可以主观地将某块内存视作栈(实际上它在内存中是没变的,只是我们主观去刻意认为),然后用相关指令来操作它
最基本的两个入栈和出栈命令是:push(入栈),pop(出栈)
注意,入栈和出栈的操作都是以字(一个字等于两个字节,1 word == 2 byte)为单位的,这意味着任何一次出入栈操作的数据一定是十六位的
例:
push ax (√)
push bh (×)bh是bx的高八位寄存器
pop cx (√)
pop dl (×)dl是dx的低八位寄存器
①初始空栈
规定为,如果将一块内存视作栈,那么这块内存最小的地址处(低地址)为栈顶,反之(高地址)为栈底,例如下图,[10000]为栈顶,[1000F]为栈底,你可以认为只不过是把从左到右(由低地址到高地址)的一块内存顺时针旋转了90度立起来方便我们看而已
此时栈指针指向[10010]处,即栈最底部[1000F]的再下一处,因为是空栈
下面将10000~1000F这段内存视作栈,并用相关指令来操作它
②【mov ax,0123】【push ax】
将0123赋值给ax,然后把ax的值压入栈中
这里再次涉及到小端存储数据的方式(现有的机器基本都是小端存储),ax分为ah高位的01和al低位的23
因此高位的01应该放在[1000F]处,低位的23放在[1000E]处,因此执行完这两条汇编之后,栈中的数据应如下图所示
此时栈指针指向[1000E]处,栈顶数据是23
③【mov bx,2266】【push bx】
这一步和②同理,高位的22被压入相对高位的[1000D]处,低位的66被压入相对低位的[1000C]处
此时栈指针指向[1000C]处,栈顶数据是66
④【mov cx,1122】【push cx】
这一步和②③同理,高位的11被压入相对高位的[1000B]处,低位的22被压入相对低位的[1000A]处
此时栈指针指向[1000C]处,栈顶数据是22
(接下来3步我们整合在一起)
形如【pop 寄存器】指令的作用:将当前栈指针指向的数据放入寄存器中
⑤【pop ax】,栈指针指向[1000A]处,因此将66放入al,22放入ah(别忘了push和pop一定是以字为单位操作数据的,而不是字节),因此执行完后,ax==1122
⑥【pop bx】,同理,栈指针指向的是[1000C],将66放入bl,22放入bh,bx==2266
⑦【pop cx】,同理,栈指针指向的是[1000E],将23放入cl,01放入ch,cx==0123
最后,栈指针再次归位到[10010]处,因为栈空
总结:
①内存中地址从左到右由低到高,栈中地址从上到下由低到高,可以认为栈不过是一块内存顺时针旋转了90度立起来方便我们看而已
②假想一个栈指针,它会时刻指向栈中目前最顶部的数据
③push、pop指令操作的是至少是一个字(不能单独操作一个字节),一个字(word)==两个字节(byte)
④每使用push指令压入一个字,栈指针会向栈顶(低地址)处移动一个字的大小(相当于-2);每使用pop指令弹出一个字,栈指针会向栈底(高地址)处移动一个字的大小(相当于+2)
二,SS、SP寄存器和push、pop的作用
(一)SS、SP寄存器
在前文中,我们反复提到以下两点:
①栈是我们主观假想出来的具有特殊存储方式的内存,与普通内存别无二致
②假想一个栈指针,它会时刻指向栈中目前最顶部的数据
怎么让cpu知道我们把哪块内存视作栈了呢?这个假想出来的栈指针要如何表现呢?cpu提供给我们两个寄存器解决了这两个问题
回想一下,其实就像学习CS、IP寄存器时一样,我们当时解决的问题是“汇编指令和普通数据在内存中存储起来后没有差别(都是二进制数),但是cpu会将[CS:IP]指向的地址视作汇编来执行”;
所以现在举一反三,又需要两个这样的寄存器,一个用来标识段地址,一个用来标识偏移地址,它们共同指向我们主观视作栈的一块内存的栈顶数据,它的地址则是[SS:SP]
因此,[SS:SP]就是实际cpu中的栈指针
(二)push指令
push指令主要有两种用法:
①push 寄存器,例如:【push ax】
此处必须是十六位寄存器(前文已经阐述过)
当本条指令被执行之后,cpu会将指定寄存器ax的数据赋值给[SS:SP]处(即栈顶)
执行完后,SP寄存器的值会-2,因为新压入了两个字节的数据成为了新的栈顶数据,栈指针会向低地址偏移两个字节
②push [偏移地址],例如:【push [0011]】
当本条指令被执行之后,cpu会将指定物理地址[DS:0011]处(不明白为什么是DS的,请仔细复习上一篇<速通汇编(五)>)的数据(同样是十六位数据,如FFFF)赋值给[SS:SP]处(即栈顶)
执行完后,SP寄存器的值会-2,因为新压入了两个字节的数据成为了新的栈顶数据,栈指针会向低地址偏移两个字节
实例:
①先用r命令查看寄存器,观察到SS和SP的值,然后用d命令查询[SS:SP]处内存的值
②在[0000:0000]处提前写入数据12ab34cd,修改DS的值为0000(这样待会push [0]时,[0]就代表[0000:0000])
④a命令在[CS:IP]即[073f:0100]处写入汇编
【mov ax,1234】【push ax】
【push [0]】
⑤执行前两条汇编【mov ax,1234】【push ax】,执行完后查看栈的变化
观察到,ax中的1234被压入栈中,ah的12在高地址,al的34在低地址,且SP==00FD-2==00FB,符合预期
(注:观察到此处栈顶数据左边的内存数据发生变化,这是正常现象,无需关心)
⑥执行第3条汇编【push [0]】,执行完后查看栈的变化
观察到,[0000:0000]处的两个字节12和AB,分别被压入栈的低和高地址处,且SP==00FB-2==00F9,符合预期
(三)pop指令
(pop指令用来弹出栈顶数据,弹出后不是说消失了,你要找个其它位置存放的)
pop指令主要有两种用法:
①pop 寄存器,例如:【pop ax】
此处必须是十六位寄存器(前文已经阐述过)
当本条指令被执行之后,cpu会将[SS:SP]处(即栈顶)的数据弹出,赋值给指定寄存器ax
执行完后,SP寄存器的值会+2,因为弹出了栈顶数据,栈指针会向高地址处偏移2个字节
②pop [偏移地址],例如:【pop [0011]】
当本条指令被执行之后,cpu会将[SS:SP]处(即栈顶)的数据弹出,赋值给指定物理地址[DS:0011]处
执行完后,SP寄存器的值会+2,因为弹出了栈顶数据,栈指针会向高地址处偏移2个字节
实例:
①a命令在在[CS:IP]处写入汇编
【pop ax】【pop [0011]】
②执行第一条汇编【pop ax】,执行完后查看栈与ax的变化
观察到,栈顶数据12AB被弹出,低位的12放入al中,高位的AB放入ah中,ax==AB12
且SP==00F9+2==00FB,符合预期
③执行第二条汇编【pop [0011]】,执行完后查看栈与[0000:0011]处的变化
观察到,栈顶数据3412被弹出,低位的34放入低位的[0000:0011]处,高位的12放入高位的[0000:0012]处
且SP==00FB+2==00FD,符合预期
三,栈越界相关问题
(以下是《王爽汇编》原文节选,提到的CPU通指8086CPU)
上文已经完全说明,CPU由SS和SP来指向栈顶地址,并提供push和pop指令来出入栈
有个问题是,CPU不会保证我们对栈的操作不超界
也就是说,CPU只知道栈顶在何处(由[SS:SP]指示),而不知道我们安排的栈空间有多大。这点就好像 CPU 只知道当前要执行的指令在何处(由CS:IP指示),而不知道要执行的指令有多少从这两点上我们可以看出CPU的工作机理,它只考虑当前的情况:当前的顶在何处、当前要执行的指令是哪一条
我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界