这一章,从访问内存的角度继续学习几个寄存器。
1、内存中字的存储
CPU中,用16位寄存器中来存储一个字。高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节)则一个字要用两个地址连续的内存单元来存放,这个字的低位字节放在低地址单元中,高位字节存放在高地址单元中。比如我们从0地址开始存放2000。
在图中,我们用0、1两个内存单元存放数据20000(4E20H)。0、1两个内存单元用来存储一个字,这两个单元可以看作一个起始地址为О的字单元(存放一个字的内存单元,由0、1两个字节单元组成)。对于这个字单元来说,0号单元是低地址单元,1号单元是高地址单元,则字型数据4E20H 的低位字节存放在О号单元中,高位字节存放在1号单元中。同理,将2、3号单元看作一个字单元,它的起始地址为2。在这个字单元中存放数据18(0012H),则在2号单元中放低位字节12H,在3号单元中放高位字节00H。
我们提出字单元的概念:字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
2、DS和[address]
CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086中,内存地址由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。比如我们要读取1000H单元的内容,可以用如下的程序段进行:
mov bx,1000H
mov ds,bx
mov al,[0]
上面的三条指令将1000H(1000:0)中的数据读到al中。
3、字的传送
前面我们用mov指令在寄存器和内存之间进行字节型数据的传送。因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是说可以一次性传送一个字。我们只要在mov指令中给出16位的寄存器就可以进行16位数据的传送了。比如:
mov bx,1000H
mov ds,bx
mov ax,[0] ;1000:0处的字型数据送入ax
mov [0],cx ;cx中的16位数据送到1000:0处
4、mov、add、sub指令
这三个指令都有两个操作对象。
mov指令可以有以下几种形式:
mov 寄存器,数据 比如:mov ax,8
mov 寄存器,寄存器 比如:mov ax,bx
mov 寄存器,内存单元 比如:mov ax,[0]
mov 内存单元,寄存器 比如:mov [0],ax
mov 段寄存器,寄存器 比如:mov ds,ax
add和sub指令同mov一样,都有两个操作对象。有以下形式:
5、数据段
在编程时,可以根据需要,将一组内存单元定义为一个段。
我们可以将一组长度为N(N<=64K)、地址连续、起始地址为16的倍数的内存单元当做专门存储数据的内存空间,从而定义一个数据段。
比如我们用123B0H~ 123BAH这段内存空间来存放数据,我们就可以认为,123B0H~123BAH这段内存是一个数据段,他的段地址为123BH,长度是10字节。
如何访问数据段中的数据呢﹖将一段内存当作数据段,是我们在编程时的一种安排,我们可以在具体操作的时候,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
6、栈
在这里,我们对栈的研究仅限于这个角度:栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。
可以用一个盒子和3本书来描述栈的这种操作方式:
一个开口的盒子就可以看成-一个栈空间,现在有3本书,《高等数学》、《C语言》、《软件工程》,把它们放到盒子中,我们看一下操作的过程。如图3.7所示。
现在的问题是,一次只允许取一本,我们如何将3本书从盒子中取出来?
显然,必须从盒子的最上边取。这样取出的顺序就是:《软件工程》、《C语言》、《高等数学》,和放入的顺序相反。如图3.8所示。
从程序化的角度来讲,应该有一个标记,这个标记一直指示着盒子最上边的书。如果说,上例中的盒子就是一个栈,我们可以看出,栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为:LIFO(Last In FirstOut,后进先出)。
7、CPU提供的栈机制
现今的CPU中都有栈的设计,8086CPU也不例外。8086CPU 提供相关的指令来以栈的方式访问内存空间。这意味着,我们在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令,最基本的两个是 PUSH(入栈)和 POP(出栈)。比如:push ax表示将寄存器ax中的数据送入栈中, pop ax表示从栈顶取出数据送入ax.8086CPU的入栈和出栈操作都是以字为单位进行的。
下面举例说明,我们可以将10000H~1000FH这段内存当作栈来使用。图3.9描述了下面一段指令的执行过程:
注意,字型数据用两个单元存放,高地址单元放高8位,低地址单元放低8位。读者看到图3.9所描述的push和pop 指令的执行过程,是否有一些疑惑﹖总结一下,大概是这两个问题:
其一,我们将10000H~~1000FH这段内存当作栈来使用,CPU执行push和pop指令时,将对这段空间按照栈的后进先出的规则进行访问。但是,一个重要的问题是,CPU如何知道1000OH~1000FH这段空间被当作栈来使用?
其二,push ax 等入栈指令执行时,要将寄存器中的内容放入当前栈顶单元的上方,成为新的栈顶元素;pop ax等指令执行时,要从栈顶单元中取出数据,送入寄存器中。显然,push、pop在执行的时候,必须知道哪个单元是栈顶单元,可是,如何知道呢?
这不禁让我们想起另外一个讨论过的问题,就是,CPU如何知道当前要执行的指令所在的位置﹖我们现在知道答案,那就是CS、IP中存放这当前指令的段地址和偏移地址。现在的问题是:CPU如何知道栈顶的位置?显然,也应该有相应的寄存器来存放栈顶的地址,8086CPU中,有两个寄存器,段寄存器SS 和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。push 指令和pop指令执行时,CPU从SS和SP中得到栈顶的地址。
现在,我们可以完整地描述push和 pop指令的功能了,例如 push ax:push ax 的执行,由以下两步完成:
(1) SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;(2)将ax 中的内容送入ss:SP指向的内存单元处, SS:SP此时指向新栈顶。
图3.10描述了8086CPU对push 指令的执行过程。
从图中我们可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
8、栈顶超界的问题
我们现在知道,8086CPU用SS和SP指示栈顶的地址,并提供 push和 pop 指令实现入栈和出栈。
但是,还有一个问题需要讨论,就是SS和SP只是记录了栈顶的地址,依靠SS和 SP可以保证在入栈和出栈时找到栈顶。可是,如何能够保证在入栈、出栈时,栈顶不会超出栈空间?
图3.13中,将10010H~1001FH当作栈空间,该栈空间容量为16字节(8字),初始状态为空,SS=1000H、SP=0020H,SS:SP指向10020H;
在执行8次push ax后,向栈中压入8个字,栈满,SS:SP指向10010H:
再次执行push ax: sp=sp-2,SS:SP指向1000EH,栈顶超出了栈空间,ax 中的数据送入1000EH单元处,将栈空间外的数据覆盖。
图3.14描述了在执行pop指令后,栈顶超出栈空间的情况。
图3.14中,将10010H~1001FH当作栈空间,该栈空间容量为16字节(8字长),当前状态为满,SS=1000H、SP-0010H,SS:SP指向10010H;
在执行8次pop ax后,从栈中弹出8个字,栈空,SS:SP指向10020H:
再次执行 pop ax: sp=sp+2,SS:SP指向10022H,栈顶超出了栈空间。此后,如果再执行push指令,10020H、10021H 中的数据将被覆盖。
上面我们描述了执行push、pop 指令时,发生的栈顶超界问题。可以看到,当栈满的时候再使用push 指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界问题。
栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竟-个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发一连串的错误。
我们当然希望CPU可以帮我们解决这个问题,比如说在CPU中有记录栈顶上限和栈底的寄存器,我们可以通过填写这些寄存器来指定栈空间的范围,然后,CPU在执行push指令的时候靠检测栈顶上限寄存器、在执行 pop 指令的时候靠检测栈底寄存器保证不会超界。
不过,对于8086CPU,这只是我们的一个设想(我们当然可以这样设想,如果 CPU是我们设计的话,这也就不仅仅是-一-个设想)。实际的情况是,8086CPU中并没有这样的寄存器。
8086CPU不保证我们对栈的操作不会超界。这也就是说,8086CPU只知道栈顶在何处(由SS:SP指示),而不知道读者安排的栈空间有多大。这点就好像,CPU只知道当前要执行的指令在何处(由CS:IP指示),而不知道读者要执行的指令有多少。从这两点上我们可以看出8086CPU的工作机理,它只考虑当前的情况:当前的栈顶在何处、当前要执行的指令是哪一条。
我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
9、push、pop指令
前面我们一直在使用push ax和pop ax,显然push和pop指令是可以在寄存器和内存(栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间。)之间传送数据的。
push和 pop指令的格式可以是如下形式:
push_寄存器;将一个寄存器中的数据入栈
pop寄存器:出栈,
用一个寄存器接收出栈的数据当然也可以是如下形式:
push段寄存器;将一个段寄存器中的数据入栈
pop段寄存器 ;出栈,用一个段寄存器接收出栈的数据
push和pop也可以在内存单元和内存单元之间传送数据,
我们可以:
push内存单元;将-个内存字单元处的字入栈(注意:栈操作都是以字为单位。)
pop内存单元;出栈,用一个内存字单元接收出栈的数据
比如:
mov ax,1000H
mov ds, ax ;内存单元的段地址要放在ds中。
push [ 0];将1000:0处的字压入栈中。
pop [2];出栈,出栈的数据送入100o:2处。
指令执行时,CPU 要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。
10、栈段
对于8086PC机,在编程时,我们可以根据需要,将一组内存单元定义为一个段。我们可以将长度为N(N≤64 K)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。比如,我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一个栈段,段地址为1000H,大小为16字节。
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行 push、pop 等栈操作指令时就自动地将我们定义的栈段当作栈空间来访问。如何使得如 push、pop等栈操作指令访问我们定义的栈段呢?前面我们已经讨论过,就是要将ss:SP指向我们的定义的栈段。