知识点:内存中字的存储、DS和[address]、字的传送、mov,add,sub指令、数据段、栈、CPU提供的栈机制、栈顶超界的问题、push,pop指令、栈段。
内存中字的存储
CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。
在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连接的内存单元来存放,这个字的低位字节放在低地址单元中,高位字节存放在高地址单元中。
字单元:即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
任何两个地址连续的内存单元,N号单元和N+1号单元,可以将它们看成两个内存单元,也可看成一个地址为N的字单元中的高位字节单元和低位字节单元。
DS和[address]
CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086CPU中,内存地址由段地址和偏移地址组成。
8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。
mov指令,可完成三种传送:
(1)将数据直接送入寄存器;
mov 寄存器名,数据
(2)将一个寄存器中的内容送入另一个寄存器;
mov 寄存器名,寄存器名
(3)将一个内存单元中的内容送入一个寄存器中。
mov 寄存器,[内存单元的偏移地址]
“[...]”表示一个内存单元的偏移地址,我们知道,只有偏移地址是不能定位一个内存单元的,那么内存单元的段地址是多少呢?
指令执行时,8086CPU自动取ds中的数据为内存单元的段地址。
所以,我们需要根据情况,改变ds中的数据。
比如 mov ds, 1000H
但是,8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器,所以mov ds, 1000H这条指令是非法的。
那么如何将1000H送入ds呢?只好用一个寄存器来进行中转,即先将1000H送入一个一般的寄存器,如bx,再将bx中的内容送入ds。
怎样将数据从寄存器送入内存单元?
从内存单元到寄存器的格式是:mov 寄存器名,内存单元地址
从寄存器到内存单元则是:mov 内存单元地址,寄存器名。
将al中的数据送入内存单元10000H。
10000H可表示为1000:0,用ds存放段地址1000H,偏移地址是0,则:mov [0], al可完成从al到10000H的数据传送。
mov bx, 1000H
mov ds, bx
mov [0], al
字的传送
mov指令在寄存器和内存之间进行字节型数据的传送。
因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是说可以一次性传送一个字。
我们只要在mov指令中给出16位的寄存器就可以进行16位数据的传送了。
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一样,都有两个操作对象,它们也可以有上面的几种形式。
这些形式中有些,两个操作数可以相互交换操作,这些需要用debug中的a命令和t命令多实践实践。
数据段
前面讲过,对于8086CPU,在编程时,我们可以根据需要,将一组内在单元定义为一个段。
我们可以将一组长度为N(N≤64K)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
比如我们用:123B0H~123BAH这段内存空间来存放数据,我们就可以认为,123B0H~123BAH这段内存是一个数据段,它的段地址为123B,长度为10字节。
如何访问数据段中的数据呢?
将一段内存当作数据段,是我们在编程时的一种安排,我们可以在具体操作的时候,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
比如,我们将123B0H~123BAH的内存单元定义为数据段。
我们现在要累加这个数据段中的前3个单元中的数据。
mov ax, 123BH
mov ds, ax ;将123BH送入ds中,作为数据段的段地址。
mov al, 0 ;用al存放累加结果,先把它清零。
add al, [0] ;将数据段第一个单元(偏移地址为0)中的数值加到al中。
add al, [1] ;将数据段第一个单元(偏移地址为1)中的数值加到al中。
add al, [2] ;将数据段第一个单元(偏移地址为2)中的数值加到al中。
累加数据段中的前3个字型数据。
mov ax, 123BH
mov ds, ax ;将123BH送入ds中,作为数据段的段地址。
mov ax, 0 ;用ax存放累加结果,先把它清零。
add al, [0] ;将数据段第一个字(偏移地址为0)加到ax中。
add al, [2] ;将数据段第一个字(偏移地址为2)加到ax中。
add al, [4] ;将数据段第一个字(偏移地址为4)加到ax中。
小结
(1)字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
(2)用mov指令要访问内存单元,可以在mov指令中只给出单元的偏移地址。此时,段地址默认在DS寄存器中。
(3)[address]表示一个偏移地址为address的内存单元。
(4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
(5)mov, add, sub是具有两个操作对象的指令,jmp是具有一个操作对象的指令。
(6)可以根据自己的推测,在Debug中实验指令的新格式。
栈
在这里,我们对栈的研究仅限于这个角度:栈是一种具有特殊的访问方式的存储空间。
它的特殊性就在于,最后进入这个空间的数据,最先出去。
栈有两个基本的操作:入栈和出栈。
入栈就是将一个新的元素到栈顶,出栈就是从栈顶取出一个元素。
栈顶的元素总是最后入栈,需要出栈和时,又最先被从栈中取出。
栈的这种操作规则被称为:LIFO(Last In First Out, 后进先出)。
CPU提供的栈机制
现今的CPU中都有栈的设计,8086CPU也不例外。
8086CPU提供相关的指令来以栈的方式访问内存空间。
这意味着,我们在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。
比如:push ax 表示将寄存器ax中的数据据送入栈中,pop ax表示从栈顶取出数据送入ax。
8086CPU的入栈和出栈操作都是以字为单位进行的。
注意,字型数据用两个内存存储单元存放,高地址单元放高8位,低地址单元放低8位。
8086CPU中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。
任意时刻,SS:SP指向栈顶元素。push指令和pop指令执行时,CPU从SS和SP中得到栈顶的地址。
push ax的执行:
1)SP=SP-2, SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
2)将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。
pop ax的执行过程和push ax刚好相反:
1)将SS:SP指向的内存单元处的数据送入ax中;
2)SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
栈顶超界的问题
8086CPU用SS和SP指示栈顶的地址,并提供push和pop指令实现入栈和出栈。
但是,SS和SP只是记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶,可是,如何能够保证在入栈、出栈时,栈顶不会超出栈空间?
当栈满的时候再使用push指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界问题。
栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序的,也可能是别的程序中的(毕竟一个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发一连串的错误。
8086CPU不保证我们对栈的操作不会超界。
也就是说,8086CPU只知道栈顶在何处(由SS:SP指示),而不知道读者安排的栈空间有多大。
我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
push、pop指令
push和pop指令是可以在寄存器和内存(栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间。)之间传送数据的。
push和pop指令的格式可以是如下形式:
push 寄存器 ;将一个寄存器中的数据入栈
pop 寄存器 ;出栈,用一个寄存器接收出栈的数据
push 段寄存器 ;将一个段寄存器中的数据入栈
pop 段寄存器 ;出栈,用一个段寄存器接收出栈的数据
push和pop也可以在内存单元和内存单元之间传送数据
push 内存单元 ;将一个内存单元处的字入栈(注意,栈操作都是以字为单位)
pop 内存单元 ;出栈,用一个内存单元接收出栈的数据
指令执行时,CPU要知道内存单元的地址,可以在push、pop指令只给出内存单元的偏移地址,段地址在执行指令时,CPU从ds中取得。
栈的综述
1)8086CPU提供了栈操作机制,方案如下:
在SS、SP中存放栈顶的段地址和偏移地址;
提供入栈和出栈指令,它们根据SS:SP指示的地址,按照栈的方式访问内存单元。
2)push指令的执行步骤:a、SP=SP-2;b、向SS:SP指向的字单元中送入数据。
3)pop指令的执行步骤:a、从SS:SP指向的字单元中读取数据;b、SP=SP+2。
4)任意时刻,SS:SP指向栈顶元素。
5)8086CPU只记录栈顶,栈空间的大小我们要自己管理。
6)用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序和入栈的顺序相反。
7)push、pop实质上是一种内存传送指令,注意它们的灵活应用。
栈是一种非常重要的机制,一定要深入理解,灵活掌握。
栈段
前面讲过,对于8086CPU,在编程时,我们可以根据需要,将一组内存单元定义为一个段。我们可以将长度为N(N<=64K)的一组地址连接、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。
比如,我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一个栈段,段地址为1000H,大小为16字节。
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行push、pop等栈操作指令时就自动地将我们定义的栈段当作栈空间来访问。
如何使得如push、pop等栈操作指令访问我们定义的栈段呢?就是要将SS:SP指向我们定义的栈段。
任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的地址为栈最底部的字单元的地址+2。
如果将10000H~1FFFFH这段空间当作栈段,栈最底部字单元的地址为1000:FFFE,所以栈空时,SP=0000H。
一个栈段最大可以设为多少?
从栈操作指令所完成的功能的角度上来看,push、pop等指令在执行的时候只修改SP,所以栈顶的变化范围是0~FFFFH,人栈空时候的SP=0,一直压栈,直到栈满时SP=0;如果再次压栈,栈顶将循环,覆盖了原来栈中的内容。所以一个栈段的容量最大为64KB。
段的综述
我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全是我们自己的安排。
我们可以用一个段丰放数据,将它定义为“数据段”;
我们可以用一个段存放代码,将它定义为“代码段”;
我们可以用一个段当作栈,将它定义为“栈段”;
我们可以这样安排,但若要让CPU按照我们的安排来访问这些段,就要:
对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问;
对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令;
对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当作栈空间来用。
可见,不管我们如何安排,CPU将的某段内容当作代码,是为因为CS:IP指向了那里;CPU将某段内存当作栈,是为因SS:SP指向了那里。
我们一定要清楚,什么是我们的安排,以及如何让CPU按我们的安排行事。要非常地清楚CPU的工作机理,才能在控制CPU来按照我们的安排运行的时候做到游刃有余。
比如我们将10000H~1001FH安排为代码段,并在这里存储如下代码:
mov ax,1000H
mov ss,ax
mov sp,0020H ;初始化栈顶
mov ax,cs
mov ds,ax ;设置数据段段地址
mov ax,[0]
mov ax,[2]
mov bx,[4]
mov bx,[6]
push ax
push bx
pop bx
pop ax
设置CS=1000H,IP=0,这段代码将得到执行,可以看到,在这段代码中,我们双将10000H~1001FH安排为栈段和数据段,10000H~1001FH这段内存,即是代码段,又是栈段和数据段。
一段内存,可骒既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是。
关键在于CPU中寄存器的设置,即:CS、IP、SS、SP、DS的指向。