《汇编语言》第3章 寄存器(内存访问)

第2章中,我们主要从CPU如何执行指令的角度讲解了8086CPU的逻辑结构、形成物理地址的方法、相关的寄存器以及一些指令。这一章中,我们从访问内存的角度继续学习几个寄存器。

3.1 内存中字的存储

        CPU中,用16位寄存器来存储一个字,高8位存放高位字节,低8位存放低字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。比如我们从0地址开始存放20000,这种情况如图3.1所示。

        在图3.1中,我们用0、1两个内存单元存放数据20000(4E20H)。0、1两个内存单元用来存储一个字,这两个单元可以看作一个起始地址为0的字单元(存放一个字的内存单元,由0、1两个字节单元组成)。对于这个字单元来说,0号单元是低地址单元,1号单元是高地址单元,则字型数据4E20H的低位字节存放在0号单元中,高位字节存放在1号单元中。同理,将2、3号单元看作一个字单元,它的起始地址为2.在这个字单元中存放数据18(0012H),则在2号单元中存放低位字节12H,在3号单元中存放高位字节00H。

        我们提出字单元的概念:字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。

        在以后的课程中,我们将起始地址为N的单元简称为N地址字单元。比如一个字单元由2、3两个内存单元组成,则这个单元的起始地址为2,我们可以说这是2地址字单元。

问题3.1
对于图3.1:
(1)0地址单元中存放的字节型数据是多少? 20H
(2)0地址字单元中存放的字型数据是多少? 4E20H
(3)2地址单元中存放的字节型数据是多少? 12H
(4)2地址字单元中存放的字型数据是多少? 0012H
(5)1地址字单元中存放的字型数据是多少? 124EH
思考后看分析 :
分析:
(1)0地址单元中存放的字节型数据: 20H
(2)0地址字单元中存放的字型数据: 4E20H
(3)2地址单元中存放的字节型数据: 12H
(4)2地址字单元中存放的字型数据: 0012H
(5)1地址字单元,即起始地十为1的字单元,它由1号单元和2号单元组成,用这两个单元存储一个字型数据,高位放在2号单元中,即:12H,低位放在1号单元中,即4EH,它们组成字型数据是124EH,大小为:4686。

3.2 DS和[address]
CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086PC中,内存地址由段和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。比如我们要读取10000H单元的内容,可以用如下的程序段进行。
mov bx, 1000H
mov ds,bx
mov al,[0]
上面的3条指令将10000H(1000:0 = 1000X16+0)中的数据读到al中。
下面详细说明指令的含义。
mov al,[0]
前面我们使用mov指令,可完成两种传送:1、将数据直接送入寄存器;2、将一个寄存器中的内容送入另一个寄存器。
也可以使用mov指令将一个内存单元中的内容送入一个寄存器。从哪一个内存单元送到哪一个寄存器中呢?在指令中必须指明。寄存器用寄存器名来指明,内存单元则需用内存单元的地址来指明。显然,此时mov指令的格式应该是:mov 寄存器名,内存单元地址。
“[...]”表示一个内存单元,“[...]”中的0表示内存单元的偏移地址。我们知道,只有偏移地址是不能定位一个内存单元的,那么内存单元的段地址是多少呢?指令执行时,8086CPU自动取ds中的数据为内存单元的段时址。

再来看一下,如何用mov指令从10000H中读取数据。10000H用段地址和偏移地址表示为1000:0,我们先将段地址10000H放入ds,然后用mov al,[0]完成传送。mov指令中的[]说明操作对象是一个内存单元,[]中的0说明这个内存单元的偏移地址是0,它的段地址默认放在ds中,指令执行时,8086CPU会自动从ds中取出。
mov bx,1000H  
mov ds,bx
如何把一个数据送入寄存器呢?我们以前用类似“mov ax,1”这样的指令来完成,从理论上讲,我们可以用相似的方式:mov ds,1000H,来将1000H送入ds。可是,现实并非如此,8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器,所以mov ds,1000H这条指令是非法的。那么如何将1000H送入ds呢?只好用一个寄存器来进行中转,即先将1000H送入一个一般的寄存器,如bx,再将bx中的内容送入ds。
为什么8086CPU不支持将数据直接送入段寄存器的操作?这属于8086CPU硬件设计的问题,我们只要知道这一点就行了。

问题3.2
写几条指令,将al中的数据送入内存单元1000H中,思考后看分析。
分析:
怎样将数据从寄存器送入内存单元?从内存单元 到寄存器的格式是:“mov 寄存器,内存单元地址”,从寄存器到内存单元则是:“mov 内存单元地址,寄存器名”。10000H可表示为1000:0,用ds存放段地址1000H,偏移地址是0,则mov [0],al可完成从al到10000H的数据传送,完整的几要指令是:
mov bx,1000H
mov ds,ax
mov [0],al 

3.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处
问题3.3
内存中的情况如图3.2所示,写出下面的指令执行后寄存器ax,bx,cx中的值。


mov ax,1000H        ;ax=1000H
mov ds,ax              ;ds=1000H
mov ax,[0]              ;ax=1123H
mov bx,[2]              ;bx=6622H
mov cx,[1]              ;cx=2211H
add bx,[1]              ;bx=8833H  (6622H+2211H)
add cx,[2]              ;cx=8833H  (6622H+2211H)

 思考后看分析:
分析:
进行单步跟踪,看一下每条指令执行后相关寄存器中的值,见表3.1

问题3.4
内存中的情况如图3.3所示,写出下面的指令执行后内存中的值,思考后看分析。


mov ax,1000H        ;ax=1000H
mov ds,ax              ;ds=1000H
mov ax,11316        ;ax=2C34H
mov [0],ax              ;10000H=>34H,10001H=>2C
mov bx,[0]              ;bx=2C34H
sub bx,[2]               ;bx=1B11H====>2C34H-1122H
mov [2],bx              ;10002H=>12H,10003H=>1BH

 分析:
进行单步跟踪,看一下每条指令执行后相关寄存器或内存单元中的值,见表3.2

3.4 mov、add、sub指令
前面我们用到了mov、add、sub指令,它们都带有两个操作对象。
到现在,我们知道,mov指令可以有以下几种形式。
mov 寄存器,数据      比如:mov ax,8
mov 寄存器,寄存器    比如:mov ax,bx
mov 寄存器,内存单元  比如:mov ax,[0]
mov 内存单元,寄存器  比如:mov [0],ax
mov 段寄存器,寄存器  比如:mov ds,ax
我们可以根据这些已知指令进行下面的推测。
(1)即然有“mov 段寄存器,寄存器”,从寄存器向段寄存器传送数据,那么也应该有“mov 寄存器,段寄存器”,从段寄存器向寄存器传送数据。一个合理的设想是:8086CPU内部有寄存器到段寄存器的通路,那么也应该有相反的通路。
有了推测,我们还要验证一下。进入Debug,用A命令,如图3.4所示。

图3.4中,用A命令在一个预设的地址073F:0100处,用汇编的形式mov ax,ds写入指令,再用T命令执行,可以看到执行的结果,段寄存器ds中的值送到了寄存器ax中。通过验证我们知道,“mov 寄存器,段寄存器”是正确的指令。
(2)既然有“mov 内存单元,寄存器”,从寄存器向内存单元传送数据,那么也应该有“mov 内存单元,段寄存器”,从段寄存器向内存单元传送数据。比如我们可以将段寄存器cs中的内容送入内存10000H处,指令如下。
mov ax,1000H
mov ds,ax
mov [0],cs
在debug中进行试验,如图3.5所示

 
图3.5中,当CS:IP指向073F:0105的时候,debug显示当前的指令mov [0000],cs,因为这是一个访问内存的指令,debug还显示出指令要访问的内存单元中的内容。由于指令中的CS是一个16位寄存器,所以要访问(写入)的内存单元是一个单元,它的偏移地址为0,段地址在ds中,debug在屏幕右边显示出“DS:0000=0000”,我们可以知道这个字单元中的内容为0.
mov [0000],cs执行后,CS中的数据(073F)被写入1000:0处,1000:1单元存放07H,1000:0单元存放3FH。
最后,用d命令从1000:0开始查看指令执行后内存中的情况,注意1000:0,1000:1两个单元的内容。
(3)“mov 段寄存器,内存单元”也应该可行,比如我们可以用10000H处存放的字型数据设置ds(即将10000H处存放的字型数据送入ds),指令如下。
mov ax,1000H
mov ds,ax
mov ds,[0]

试验如下:

 add和sub指令同mov一样,都有两个操作对象。它们也可以有以下几种形式。
add 寄存器,数据                 比如:add ax,8
add 寄存器,寄存器             比如:add ax,bx
add 寄存器,内存单元         比如:add ax,[0]
add 内存单元,寄存器         比如:add [0],ax
sub 寄存器,数据                比如:sub ax,9
sub 寄存器,寄存器            比如:sub ax,bx
sub 寄存器,内存单元        比如:sub ax,[0]
sub 内存单元,寄存器        比如:sub [0],ax
它们可以对段寄存器进行操作吗,比如 “add ds,ax”,试验如下

 从试验可以看出,不能对段寄存器进行add操作。

3.5 数据段
        前面讲过(参见2.8节),对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将一组长度为N(N≤64KB)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。比如用123B0H~123B9H这段内存空间来存放数据,我们就可以认为,123B0H~123B9H这段内存是一个数据段,它的段地址为123BH,长度为10个字节。

        如何访问数据段中的数据呢?将一段内存当作数据段,是我们在编程时的一种安排,可以在具体操作的时候,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
比如,将123B0H~123B9H的内存单元定义为数据段,现在要累加这个数据段中的前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中   

试验过程,先把123B0开始的三个单元分别设置1,2,3的值

 输入指令

 执行指令

 由实验结果得出我们的结论,1+2+3=6,由于相加的是al,低8位,因此高8位不会还是12H

总的结果为1206H

问题3.5
写几条指令,累加数据段中的前3个字型数据,思考后看分析。
mov ax,123BH
mov ds,ax;将123BH送入ds中,作为数据段的段地址
mov ax,0;用al存放累加结果
add ax,[0];将数据段第一个字(偏移地址为0)中的数值加到ax中
add ax,[2];将数据段第二个字(偏移地址为2)中的数值加到ax中
add ax,[4];将数据段第三个字(偏移地址为4)中的数值加到ax中
注意,一个字型数据占两个单元,所以偏移地址是0、2、4。
这里我们把1111、2222、3333分别作这三个字的值,那边最终结果应该是6666H,下面我们用实验来验证一下

 实验结果和我们的推论一致

                                                     3.1~3.5 小结
(1)字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节放在低地址单元中,高位字节存放在高地址单元中。
(2)用mov指令访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。
(3)[address]表示一个偏移地址为address的内存单元。
(4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
(5)mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
(6)可以根据自已的推测,在Debug中实验指令的新格式。

检测点3.1
(1)在Debug中,用“d 0:0 1f”查看内存,结果如下。
0000:0000 70 80 F0 30 EF 60 30 E2-00 80 80 12 66 20 22 60
0000:0010 62 26 E6 D6 CC 2E 3C 3B-AB BA 00 00 26 06 66 88
下面的程序执行前,AX=0,BX=0,写出每条汇编指令执行完后相关寄存器中的值。
mov ax,1                           ; AX=1
mov ds,ax                         ; DS=1
mov ax,[0000]                   ;AX=2662=>ds:[0] 0010地址1个字单元 (16位)
mov bx,[0001]                   ;BX=E626=>ds:[1] 0011地址1个字单元 (16位)
mov ax,bx                         ;AX=E626
mov ax,[0000]                   ;AX=2662
mov bx,[0002]                   ;BX=D6E6
add ax,bx                          ;AX=FD48
add ax,[0004]                    ;AX=2C14
mov ax,0                           ;AX=0
mov al,[0002]                    ;AX=E6   0012地址1个字节单元  (8位)
mov bx,0                           ;BX=0
mov bl,[000C]                   ;BX=26   001C地址1个字节单元 (8位)
add al,bl                           ;AX=000C   低8位相加,进位丢失了

实验验证

 执行命令

 

 

 

 实验结果与上面的计算结果一致

(2)内存中的情况如图3.6所示。
各寄存器的初始值:CS=2000H, IP=0,DS=1000H, AX=0,BX=0;
⑴ 写出CPU执行的指令序列(用汇编指令写出)
⑵ 写出CPU执行每条指令后,CS、IP和相关寄存器中的数值。
⑶ 再次体会:数据和程序有区别吗?如何确定内存中的信息有哪些是数据,哪些是程序?

解答:

1,指令序列

mov ax,6622
jmp 0ff0:0100
mov ax,2000
mov ds,ax
mov ax,[0008]
mov ax,[0002]

2、执行指令CS和IP寄存器的值

mov ax,6622  ; ====>CS = 2000, IP = 3
jmp 0ff0:0100 ;====>CS = 0ff0, IP = 0100
mov ax,2000   ;====>CS = 0ff0, IP = 103
mov ds,ax       ;====>CS = 0ff0, IP = 105
mov ax,[0008] ;====>CS = 0ff0, IP = 108
mov ax,[0002] ;====>CS = 0ff0, IP = 10B

实验验证如下:

 3.数据和程序在内存中是没有区别的,只是根据寄存器的访问来区分是数据还是程序,如果通用CS:IP的形式访问表示是程序,通过DS:[address]的形式访问表示数据。

3.6 栈
在这里,我们对栈的研究仅限于这个角度:栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。
可以用一个盒子和3本书来描述栈的这种操作方式
一个开口的盒子就可以看成一个栈空间,现在有3本书,《高等数学》、《C语言》、《软件工程》,把它们放到盒子中,操作的过程如图3.7所示。

 现在的问题是,一次只允许取一本,我们如何将3本书从盒子中取出来?
显然,必须从盒子的最上边取。这样取出的顺序就是:《软件工程》、《C语言》、《高等数学》,和放入的顺序相反,如图3.8所示。

从程序化的角度来讲,应该有一个标记,这个标记一直指示着盒子最上边的书。
如果说,上例中的盒子就是一个栈,我们可以看出,栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为:LIFO(Last In First Out,后进先出)。    

3.7 CPU提供的栈机制
现今的CPU中都有栈的设计,8086CPU也不例外。8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。比如,push ax 表示将寄存器ax中的数据送入栈中,pop ax表示从栈顶取出数据送入ax。8086CPU的入栈和出栈操作都是以字为单位进行的。
下面举例说明,我们可以将10000H~1000FH这段内存当作栈来使用。
图3.9描述了下面一段指令的执行过程。

 mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx

注意,字型数据用两个单元存放,高地址单元存放高8位,低地址单元存放低8位。
读者看到图3.9所描述的push和pop指令的执行过程,是否有一些疑惑?总结一下,大概是这两个问题。
其一,我们将10000H~1000FH这段内存当人生栈来使用,CPU执行push和pop指令时,将对这段空间按照栈的后进先出的规则进行访问。但是,一个重要的问题是,CPU如何知道10000H ~ 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中,入栈时,栈顶从高地址向低地址方向增长。

                       关于栈是向下增长,堆向上增长,我们可以用一个C++程序来验证

#include <iostream>
using namespace std;

int main(int argc, char *argv[])
{

    int s1 = 11;
    int s2 = 12;
    int s3 = 13;
    int s4 = 14;
    cout << "s1=" << hex << &s1 << endl;
    cout << "s2=" << hex << &s2 << endl;
    cout << "s3=" << hex << &s3 << endl;
    cout << "s4=" << hex << &s4 << endl;

    cout << endl;
    int *p1 = new int(11);
    int *p2 = new int(12);
    int *p3 = new int(13);
    int *p4 = new int(14);

    cout << "p1=" << hex << p1 << endl;
    cout << "p2=" << hex << p2 << endl;
    cout << "p3=" << hex << p3 << endl;
    cout << "p4=" << hex << p4 << endl;

    delete p1;
    p1 = nullptr;
    delete p2;
    p2 = nullptr;
    delete p3;
    p3 = nullptr;
    delete p4;
    p4 = nullptr;
    return 0;

}

运行结果:

问题3.6
如果将10000H~1000FH这段空间当作栈,初始状态是空的,此时,SS=1000H,SP=?思考后看分析
解答:由于栈是向下增长的,当压入数据时,栈指针向下(低地址)偏移,因此初始化时SP是指向1000FH的下一个位置,指向100010,即SP=010H,当有数据进栈时,SP的指针会偏移到0~F之内。

分析
SP=0010h,如图3.11所示。

将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.
pop ax的指执行过程和push ax则后相反,由以下两步完成。
(1)将SS:SP指向的内存单元处理的数据送入ax中
(2)SP=SP+2,SS:SP指各当前 栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
图3.12描述了8086CPU对pop指令的执行过程。

 注意,图3.12中,出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素,1000CH处的2266H依然存在,但是,它已不在栈中。当再次执行push等入栈指令后,SS:SP移至1000CH,并在里面写入新的数据,它将被覆盖。

 3.8 栈顶超界的问题
我们现在知道,8086CPU用SS和SP指示栈顶的地址,并提供push和pop指令实现入栈和出栈。
但是,还有一个问题需要讨论,就是SS和SP只是记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶 。可是,如何能够保证在入栈、出栈时,栈顶不会超出栈空间呢?
图3.13描述了在执行push指令后,栈顶超出栈空间的情况。
图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的工作机理,它只考虑当前的情况:当前的栈顶在何处,当前要执行的指令是哪一条。
我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。

3.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];  出栈,出栈的数据送入1000:2处
指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得

问题3.7
编程,将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 ds

问题3.8
编程:
(1)将10000H~1000FH这段空间当作栈,初始状态栈是空的;
(2)设置AX=001AH,BX=001BH;
(3)将AX、BX中的数据入栈;
(4)然后将AX、BX清零
(5)从栈中恢复AX、BX原来的内容。
mov ax, 1000
mov ss, ax
mov sp, 0010    ;初始化栈顶,栈的情况如图3.16(a)所示
mov ax, 001A
mov bx, 001B
push ax         ;ax入栈
push bx         ;bx入栈
sub ax, ax      ; 将ax清零,也可以用mov ax, 0,  
sub bx, bx      ;sub bx,bx的机器码为2个字节,mov bx, 0的机器码为3个字节。
pop bx          ;从栈中恢复ax、bx原来的数据,当前栈顶的内容是bx中原来的内容001A
pop ax          ;在栈顶的下面,所以要先pop bx,然后再pop ax。

从上面的程序我们看到,用栈来暂存以后需要恢复的寄存器中的内容时,出栈的顺序要和入栈的顺序相反,因为最后入栈的寄存器的内容在栈顶,所以在恢复时,要最先出栈。

问题3.9
编程:
(1)将10000H~1000FH这段空间当作栈,初始状态是空的;
(2)设置AX=001AH,BX=001BH;
(3)利用栈,交换AX和BX中的数据;
解答:
mov ax, 1000
mov ss, ax
mov sp, 0010    ;初始化栈顶,栈的情况如图3.16(a)所示;
mov ax, 001A
mov bx, 001B
push ax         ;ax入栈
push bx         ;bx入栈
pop ax          ;当前栈顶的数据是bx中原来的数据:001B所以先pop ax, ax=001B
pop bx          ;执行pop ax后,栈顶的数据为ax原来的数据,所以再pop bx,bx=001A

 问题3.10
如果要在10000H处写入字型数据2266H,可以用以下的代码完成:
mov ax,1000H
mov ds,ax
mov ax,2266H
mov [0],ax
补全下面的代码,使它能够完成同样的功能:在10000H处写入字型数据2266H。
要求,不能使用“mov 内存单元,寄存器”,这类指令。

mov ax, 1000
mov ss, ax
mov sp, 2

mov ax, 2266
push ax

解析:

我们来看需补全代码的最后两条指令,将ax中的2266H压入栈中,也就是说,最终应由push ax将2266H写入10000H处。问题的关键就在于:如何使push ax访问的内存单元是10000H。
push ax是入栈指令,它将在栈顶之上压入新的数据。一定要注意:它的执行过程是,先将记录栈顶偏移地址的SP寄存器中的内容减2,使得SS:SP指向新的栈顶单元,然后再将寄存器中的数据送入SS:SP指向的新的栈顶单元。
所以,要在执行push ax之前,将SS:SP指向10002H(可以设SS=1000H,SP=0002H),这样,在执行push ax的时候,CPU先将SP=SP-2,使得SS:SP指向10000H,再将ax中的数据送入SS:SP指向的内存单元处,即10000H处。

mov ax, 1000
mov ss, ax
mov sp, 2
mov ax, 2266
push ax

从问题3.10的分析中可以看出,push、pop实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与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指令的执行步骤:⑴ SP=SP-2;⑵向SS:SP指向的字单元中送入数据。
(3)pop指令的执行步骤:⑴ 从SS:SP指向的字单元中读取数据;⑵SP=SP+2.
(4)任意时刻,SS:SP指向栈顶无素。
(5)8086CPU只记录栈栈顶,栈空间的大小我们要自己管理。
(6)用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
(7)push、pop实质上是一种内存传送指令,注意它们的灵活应用。
栈是一种非常重要的机制,一定要深入理解,灵活掌握

3.10 栈段
前面讲过(参见2.8节),对于8086PC机,在编码时,可以根据需要,将一组内存单元定义为一个段。我们可以将长度为N(N≤64KB)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。比如,我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一个栈段,段地址为1001H,大小为16字节。
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行push,pop等栈操作指令时自动地将我们定义的栈段当作 栈空间来访问。如何使得如push,pop等栈操作指令访问我们定义的栈段呢?前面我们已经讨论过,就是要将SS:SP指向我们定义的栈段。
问题3.11
如果将10000H~1FFFFH这段空间当作栈段,初始状态栈是空的,此时,SS=1000H,SP=?
SP=0
解析:
如果将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。

问题3.12
一个栈段最大可以设为多少?为什么?
解析:
栈的操作在指令执行的时候只修改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,0020  ;初始化栈顶
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,DS的指向。

检测点3.2
(1)补全下面的程序,使其可以将10000H~1000FH中的8个字,逆序复制到20000H~2000FH中。逆序复制的含义如图3.17所示(图中内存里的数据均为假设)。

代码实现

mov ax, 1000
mov ds, ax
mov ax, 2000
mov ss, ax     ;把数据压入到ss=2000段中
mov sp, 10     ;初始化栈偏移地址,push一次sp-2
push [0]
push [2]
push [4]
push [6]
push [8]
push [a]
push [c]
push [e]

实验验证:

 

 

 

(2)补全下面的程序,使其可以将10000H~1000FH中的8个字,逆序复制到20000H~2000FH中。

mov ax, 2000
mov ds, ax
mov ax, 1000
mov ss, ax   ;从ss=1000段中处取数据
mov sp, 0   ;设置栈顶地址,pop一次sp+2
pop [e]
pop [c]
pop [a]
pop [8]
pop [6]
pop [4]
pop [2]
pop [0]

实验验证:

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值