汇编语言个人学习笔记——第三章 寄存器(内存访问)第二部分
3.6栈
我们研究栈的角度:
栈是一种具有特殊的访问方式的的存储空间。它的特殊性就在于,最后进入这个空间的数据最先出去。
可以用一个盒子和三本书来描述。
一个开口的盒子就可以看作一个栈空间,现有三本书,《高等数学》,《C语言》,《软件工程》,把他们放到盒子中过程如图所示:
栈有两个基本操作:入栈和出栈。
入栈:将一个新的元素放到栈顶;
出栈:从栈顶取出一个元素。
栈顶元素总是最后入栈,需要出栈时,又最先被从栈中取出。
栈的操作原则:LIFO(Last In First Out,后进先出)
3.7 CPU提供的栈机制
现今的CPU都有栈的设计。
8086CPU提供相关的指令来以栈的方式访问内存空间。
这就意味着,我们在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086提供的入栈和出栈指令(最基本的):
push(入栈)
pop(出栈)
push ax:将寄存器ax中的数据送入栈中;
pop ax:从栈顶取出数据送入ax。
8086CPU的入栈和出栈操作都是以字为单位进行的。
下面举例说明,我们可以将10000H~1000FH这段内存当作栈来使用。
下面一段指令的执行过程:
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
注意:字型数据用两个单元存放,高地质单元放高8位,低地址单元放低8位
两个疑惑:
1、CPU如何知道一段内存空间被当作栈使用?
(个人猜想:CS和IP指向的是指令,段寄存器DS和偏移地址指向的是数据,应该有一个特殊的段寄存器,叫做栈的使用的寄存器。)
2、执行push和pop的时候,CPU如何知道哪个单元是栈顶单元?
分析:
(1)CPU如何指导当前要执行的指令所在的位置?
答:寄存器CS和IP中存放着当前指令的段地址和偏移地址。
8086CPU中,有两个寄存器:
段寄存器SS:存放栈顶的段地址
寄存器SP:存放栈顶的偏移地址
结论:任意时刻,SS:SP指向栈顶元素。
push指令的执行过程
push ax
(1)SP=SP-2;
(2)将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。
问题:如果我们将10000H~1000FH这段空间当作栈,初始状态栈是空的,此时SS=1000H,SP=?
分析:
SP=0010H
注:pop是相反的过程,先将数据复制出来,再将SP加2,数据复制之后原地址的数据还是存在的,但是下一轮push的时候会把数据覆盖掉,类似于硬盘格式化之后还可以恢复。(所以硬盘中的数据想要完全删除就要覆盖很多次,硬盘是不提供删除功能的,所以高手可以取出被删除的数据)
总结:
我们将10000H~1000FH这段空间当作栈段,SS=1000H,栈空间大小为16字节,栈最底部的字单元地址为1000:000E。任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候,SS=1000H,SP=000EH。
栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2,SP原来为000EH,加2后SP=10H,所以栈为空的时候,SS=1000H,SP=10H。
换个角度看:
任何时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素。
所以,SS:SP只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址加2.
栈最底部字单元的地址为1000:000E,所以,栈空时,SP=0010H。
pop指令的执行过程:
pop ax
1、将SS:SP指向的内存单元处的数据送入ax中;
2、SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
注意:
出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素1000CH处的2266H依然存在,但是它已不在栈中。
当再次执行push等入栈指令后,SS:SP移至1000CH,并在里面写入新的数据,它将被覆盖。
3.8栈顶超界的问题
SS和SP只记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。
可是,如何能够保证在入栈、出栈时,栈顶不会超出栈空间?
当栈满的时候再使用push指令入栈:
(类似溢出攻击)
栈空的时候再使用pop指令出栈:
这也是一种溢出
栈顶超界是危险的:
因为我们既然将一段空间安排为栈,那么,在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竟一个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发一连串的错误。
我们希望CPU提供有记录栈顶上限和下限的寄存器,我们可以通过填写这些寄存器来指定栈空间的范围,然后,CPU在执行push指令的时候靠检测栈顶上限寄存器、在执行pop指令的时候靠检测栈底寄存器保证不会超界。
实际情况,8086CPU并没有提供这样的寄存器。CPU特别小,里面没有那么大的空间来放这样的寄存器是一个重要原因。
8086CPU不保证我们对栈的操作不会超界,这也就是说,8086CPU只知道栈顶在何处(由SS:SP指示),而不知道要执行的指令有多少。
从这两点上可以看出,8086CPU的工作原理:
当前栈顶在何处;
当前要执行的指令是哪一条。
结论:
我们在编程时,要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;
执行出栈操作时也要注意,以防止栈空的时候继续出栈导致的超界问题。
(C语言、C++栈空间是编译器自动分配的,程序员分配的空间是堆)
3.9 pop、push指令
push和pop指令是可以在寄存器和内存之间传送数据的。
栈与内存:
栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式(先进后出)进行访问的内存空间。
push和pop指令的格式(1):
push 寄存器 :将一个寄存器中的数据入栈
pop 寄存器:出栈,用一个寄存器接收出栈的数据
例如:push ax
pop bx
push和pop指令的格式(2):
push 段寄存器:将一个段寄存器中的数据入栈
pop段寄存器:出栈,用一个段寄存器接收出栈的数据
例如:push ds
pop es
(段寄存器都是以s结尾的,通用寄存器都是以x结尾的)
push和pop指令的格式(3):
push 内存单元 :将一个内存单元处的字入栈(栈操作都是以字为单位)
pop 内存单元:出栈,用一个内存字单元接收出栈的数据
例如:push [0]
pop [2]
执行指令时,CPU要知道内存单元的地址可以在push、pop指令中给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。
问题:
编程:将10000H~1000FH这段空间当作栈,初始状态是空的,将AX,BX,DS中的数据入栈。
分析:
代码如下:
mov ax,1000H
mov ss,ax ;设置栈的段地址,SS=1000H,不能直接向段寄存器SS中送入数据,所以用ax中转
mov sp,0010H ;设置栈顶的偏移地址,因栈为空,所以sp=0010H。这三条指令设置栈顶地址,编程中要自己注意栈的大小。
push ax
push bx
push cx
问题:
编程:
1、将10000H~1000FH这段空间当作栈,初始状态栈是空的;
2、设置AX=001AH,BX=001BH;
3、将AX、BX中的数据入栈
4、然后将AX、BX清零
5、从栈中恢复AX、BX原来的内容
分析:
mov ax,1000H
mov ss,ax ;设置栈的段寄存器
mov sp,0010H ;设置栈顶的偏移地址
mov ax,001AH
push ax
mov bx,001BH
push bx
mov ax,0
mov bx,0
pop bx
pop ax
从上面的程序我们可以看到,用栈来暂存以后需要恢复的寄存器中的内容时,出栈的顺序要和入栈的顺序相反,因为最后入栈的寄存器的内容在栈顶,所以在恢复时,要最先出栈。
问题:
编程:
1、将10000H~1000FH这段空间当作栈,初始状态栈是空的;
2、设置AX=002AH,BX=002BH;
3、利用栈,交换AX和BX中的数据。
分析:
mov ax,1000H
mov ss,ax
mov sp,0010H
mov ax,002AH
mov bx,002BH
push ax
push bx
pop ax
pop bx
问题:
我们如果要在10000H处写入字型数据2266H,可以用以下代码完成:
mov ax,1000H
mov ds,ax
mov ax,2266
mov [0],ax
补全下面代码,使它能够完成同样的功能:在10000H处写入字型数据2266H。
要求:不能使用“mov 内存单元,寄存器”这类指令。
——————
——————
——————
mov ax,2266H
push ax
答案:
mov ax,1000H
mov ss,ax
mov sp,2
mov ax,2266H
push ax
分析:我们看出需补全代码的最后两条指令,将ax中的2266H压入栈中,也就是说,最终应由push ax将2266H写入10000H处。
问题的关键就在于:如何使push ax访问的内存单元是10000H。
push ax指令的执行过程要先让SP=SP-2,再将ax中的内容放入SS:SP字单元中。所以要让SS:SP=10002H。
结论:push、pop指令shi实质上就是一种内存访问传送指令,可以在寄存器和内存数据之间传送数据,与mov指令不同的是,push和pop指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP指出的。
同时,push和pop指令还要改变sp中的内容。
我们十分清楚的是,push和pop指令同mov指令不同,CPU执行mov指令只需一步操作,就是传送,而执行push、pop指令却需要两步操作。
执行push时,CPU的两步操作是:先改变SP,后向SS:SP处传送。
执行pop时,CPU的两步操作是:先读取SS:SP处的数据,后改变SP。
注意:
push,pop等栈操作指令,修改的只是SP。也就是说,栈顶的变化范围最大为:0~FFFFH。
提示:SS、SP指示栈顶;改变SP后写内存的入栈指令;读内存后改变SP的出栈指令。
这就是8086CPU提供的栈操作机制。
栈的综述:
1、8086CPU提供了栈操作机制,方案如下:
在SS:SP中存放栈顶的段地址和偏移地址
提供入栈和出栈指令,它们根据SS:SP指示的地址,按照栈的方式访问内存单元。
2、push指令的执行步骤:
(1)SP=SP-2
(2)向SS:SP指向的字单元中送入数据
3、pop指令的执行步骤:
(1)从SS:SP指向的字单元中读取数据
(2)SP=SP+2
4、任意时刻,SS:SP指向栈顶元素
5、8086CPU只记录栈顶,栈空间的大小我们要自己管理。
6、用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
7、push、pop实质上是一种内存传送指令,注意它们的灵活应用。
注:栈是一种非常重要的机制,一定要深入理解,灵活掌握。
3.10栈段
前面讲过,对于8086PC机,在编程时,我们可以根据需要,将一组内存单元定义为一个段。我们可以将长度为N(N不大于64KB)的一组地址连续、起始地址为16的倍数的内存单元,当作栈来使用,从而定义了一个栈段。
比如我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以成为栈段,段地址为1000H,大小为16字节。
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行push、pop等栈操作指令时就自动地将我们定义的栈段当作栈空间来访问。
如何使得如push、po'ppop等栈操作指令访问我们定义的栈段呢?
答:将SS:SP指向我们定义的栈段。
问题:如果将10000H~1FFFFH这段空间当作栈段,初始状态栈是空的,此时,SS=1000H,SP=?
我们将10000H~1FFFFH,这段空间当作栈段,SS=1000H,栈空间大小为64KB,栈最底部的字单元地址为:1000:FFFE。任意时刻,SS:SP指向栈顶单元,当栈中只有一个元素的时候,SS=1000H,SP=FFFEH。栈为空,就相当于栈中唯一元素出栈,出栈后,SP=SP+2.
SP原来为FFFEH,加2后SP=0,所以,当栈为空的时候,SS=1000H,SP=0。
换个角度看
任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单最底部单元的字单元的偏移地址+2,栈最底部字单元的地址为1000:FFFE,所以栈空时,SP=0000H。
问题:
一个栈段最大可以设为多少?为什么?
分析:
这个问题显而易见,提出来只是为了提示我们将相关的知识融会起来。
首先从栈操作指令所完成的功能的角度上来看,push、pop等指令在执行的时候只修改SP,所以栈顶的变化范围是:0~FFFFH,从栈空时候的SP=0,一直压栈,直到栈满时,SP=0;如果再次压栈,栈顶将环绕,覆盖了原来栈中的内容,所以一个栈段的rong容量最大为64KB。
段的综述:
我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全是我们自己的安排。
我们可以用一个段存放数据,将它定义为“数据段”;
我们可以用一个段存放代码,将它定义为“代码段”;
我们可以用一个段当作栈,将它定义为“栈段”。
我们可以这样安排,但若要让CPU按照我们的安排来访问这些段,就要:
对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问;
对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令。
对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当作栈空间来使用。
可见,不管我们如何安排,CPU将内存中的某段内容当作代码,是因为CS:IP指向了哪里;CPU将某段内存当作栈,是因为SS:IP指向指向了那里。
举例:
我们将10000H~1001FH安排为代码段,并在里面存储如下代码:
mov ax,1000H
mov ss,ax
mov sp,0020H
mov ax,cs
mov ds,ax
mov ax,[0]
add ax,[2]
mov bx,[4]
add bx,[6]
push ax
push bx
pop ax
pop bx
设置CS=1000H,IP=0,这段代码将得到执行,可以看到,在这段代码中,我们又将10000H~1001FH安排为栈段和数据段。10000H~1001FH这段内存,即是代码段,又是栈段和数据段。
一段内存,可以既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么都不是,关键在于CPU中寄存器的设置,即CS、IP、SS、SP、DS的指向。
先做完检测题再进行下面的学习。