汇编语言个人学习笔记——第三章 寄存器(内存访问)第二部分

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的指向。

 

   先做完检测题再进行下面的学习。