栈
- 栈:是一种具有特殊的访问方式的存储空间(先进后出 First In Last Out,FILO)
- 8086CPU 会将 CS 作为代码段的段地址,将 CS:IP 指向的指令作为下一条需要取出执行的指令
- 8086CPU 会将 DS 作为数据段的段地址,mov ax, [address] 就是取出 DS:[address] 的内存数据存放到ax寄存器中
- 8086CPU 会将 SS 作为栈段的段地址,任意时刻,SS:SP 指向栈顶元素。8086CPU 提供了PUSH(入栈)和POP(出栈)指令来操作栈段的数据。比如,push ax 是将 ax寄存器 中的数据入栈,pop ax 是将栈顶的数据送入 ax寄存器 中
Push操作
- push ax 的执行由以下两个步骤完成
1.SP = SP - 2,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
2.将 ax 中的内容送入 SS:SP 指向的内存单元处,SS:SP 此时指向新栈顶
Pop操作
- pop ax 的执行过程和 push ax刚好相反,由以下两个步骤完成
1.将 SS:SP 指向的内存单元处的数据送入 ax 中
2.SP = SP + 2,SS:SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶
空栈
思考:如果将 10000H 到 1000FH 这段空间当做栈,初始状态栈是空的,此时,SS=1000H,那么SP=? 思考后看分析
- 空栈,SS:SP 指向栈空间最高地址单元的下一个单元
栈顶超界
- Push操作导致的栈顶超界(栈顶超出栈空间的最小地址)
- Pop操作导致的栈顶超界(栈顶超出栈空间的最大地址)
- 上面描述了执行 push、pop 指令时,发生的栈顶超界问题。可以看到,当栈满的时候再使用 push 指令入栈,或空栈的时候再使用 pop 指令出栈,都将发生栈顶超界问题。
- 栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里面很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竟一个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发一连串的错误。
- 我们当然希望 CPU 可以帮我们解决这个问题,比如说,在 CUP 中有记录栈顶上限和栈底下限的寄存器,我们可以通过填写这些寄存器来指定栈空间的范围,然后, CUP 在执行 push 指令的时候靠检测栈顶上限寄存器、在执行 pop 指令的时候靠检测栈底下限寄存器,来保证不会超界。不过,对于 8086CPU 这只是我们的一个假想。实际情况是,8086CPU 中并没有这样的寄存器。(假设一个CPU设计成有提供栈顶上限寄存器和栈底下限寄存器用来约束栈段的地址范围,在同一个程序的实际开发中,很有可能不止定义一个栈空间,那么此时,这个CPU要提供多少个栈顶上限寄存器和栈底下限寄存器用来约束各个栈的地址范围呢?因此,通过CPU提供所谓的栈顶上限寄存器和栈底下限寄存器用来约束各个栈的地址范围,并不是一个合理的设计方式。要防止栈顶超界问题,还是需要程序员自己编写代码来进行约束。)
- 8086CPU 不保证程序员对栈的操作不会超界。也就是说,8086CPU 只知道栈顶当前在何处(由 SS:SP 指示),而不知道我们安排的栈空间有多大。这点就好像 CPU 只知道当前要执行的指令在何处(由 CS:IP 指示),而不知道要执行的指令有多少条。从这两点上我们可以看出 8086CPU 的工作机理,它只考虑当前的情况:当前的栈顶在何处、当前要执行的指令是哪一条。
- 我们在编程的时候要自己操心栈顶超界问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的栈顶超界;执行出栈的时候也要注意,以防止栈空的时候继续出栈而导致的栈顶超界。
push 和 pop 汇编指令的格式
push 寄存器 ; 入栈,将一个寄存器中的数据压入栈
pop 寄存器 ; 出栈,用一个寄存器接受出栈的数据
push 段寄存器 ; 入栈,将一个段寄存器中的数据压入栈
pop 段寄存器 ; 出栈,用一个段寄存器接收出栈的数据
push 内存单元 ; 入栈,将一个内存字单元处的字数据压入栈
pop 内存单元 ; 出栈,用一个内存字单元接受出栈的字数据
mov ax, 1000H
mov ds, ax ; 数据段内存单元的段地址要放在 ds 中
push [0] ; 将 1000:0000 处的字数据压入栈中
pop [2] ; 出栈的字数据送入 1000:0002 处
1.push、pop 指令可以直接操作段寄存器
2.在8086中 push、pop 操作的对象都是字型数据,即数据长度都是2个字节
3.push操作为放数据,会导致栈顶的地址减小;pop操作为取数据,会导致栈顶的地址增大。
栈段
- 对于 8086CPU 来说,在编程时,可以根据需要,将一组内存单元定义为一个段
- 我们可以将一组长度为N(N<=64K)、地址连续、起始地址为16的倍数的内存单元,当做栈空间来使用,称为栈段。比如将 10010H ~ 1001FH 这段内存空间当做栈来使用,我们就可以认为 10010H ~ 10001FH 是一个栈段,它的段地址为 1001H,长度为16字节
- Question:如何使用 push、pop 等栈操作指令访问我们定义的栈段?
Answer:用 SS 存放栈的段地址,用 SP 存放栈顶的偏移地址
段总结(代码段,数据段,栈段)
- 我们可以用一个段存放代码,将它定义为"代码段";
- 我们可以用一个段存放数据,将它定义为"数据段";
- 我们可以用一个段当做栈,将它定义为"栈段";
- 对于代码段,将它的段地址放在 CS寄存器 中,将段中的第一条指令的偏移地址放在 IP寄存器 中,这样 CPU 就会执行我们定义在代码段中的指令;
- 对于数据段,将它的段地址放在 DS寄存器 中,用 mov、add、sub 等汇编指令访问内存单元时,CPU 就将我们定义的数据段中的内容当做数据来访问;
- 对于栈段,将它的段地址放在 SS寄存器 中,将栈顶单元的偏移地址放在 SP寄存器 中,这样 CPU 在需要进行栈操作的时候,比如执行 push、pop 指令等,就将我们定义的栈段当做栈空间来使用。
练习
题目一:
编程:
(1)将 10000H 到 1000FH 这段空间当做栈,初始状态栈是空的;
(2)设置AX=001AH,BX=001BH;
(3)利用栈,交换AX和BX中的数据.;
mov ax,1000H
mov ss,ax;
mov sp,0010H
mov ax,001AH
mov bx,001BH
push ax
push bx
pop ax
pop bx
题目二:
题目三:
注意
- 8086CPU 在内存中分配栈空间的时候,总是以内存高物理地址作为栈底,随着数据的入栈,逐渐向内存低物理地址扩展。因为内存高物理地址处,存放着各类硬件和系统的BIOS,这样做可以有效防止因为数据入栈导致的栈顶超界时,改写到硬件和系统的BIOS。这里有一点需要格外注意:push 操作会改写 SP指针指向的内存空间,pop 操作不会改写 SP 指针指向的内存空间(pop 操作仅仅执行读取)
- 空栈时,SP指向栈段地址空间最高地址的下一个内存单元;栈满时,SP指向栈段地址空间最低地址(而不是栈段地址空间最低地址的上一个内存单元)
- 在实际汇编中,没有栈顶和栈底的概念,只有 SS:SP 指向的当前元素。栈顶和栈底是我们为了方便理解,加以定义和描述的
- 在 8086CPU 中,由于没有对栈顶越界进行限制,可以对栈无限地进行 push 和 pop 操作,从直观上,栈的大小好像等于整块物理内存的大小。但是,在进行 push 和 pop 操作的时候,是通过 SS:SP 实现的,在保持 SS 不变,通过更改 SP 进行物理内存寻址的情况下,一个栈的大小,不可能超过 64K(因为 SP 是16位的寄存器)。即 8086CPU 的栈大小,最大为 64K