王爽《汇编语言》第四版 超级笔记
第3章 寄存器(内存访问)
3.1 内存中的存储:字(word)
CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。
在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
在图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+1号单元,可以将它们看成两个内存单元,也可看成一个地址为N的字单元中的高位字节单元和低位字节单元。
3.2 DS 和 [address]
CPU要读写一个内存单元的时候,必须先给岀这个内存单元的地址,在8086PC中,内存地址由段地址和偏移地址组成。
8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。比如我们要读取10000H单元的内容,可以用如下的程序段进行。
mov bx,1000H
mov ds,bx
mov al,[0]
上面的3条指令将10000H(1000:0)中的数据读到al中。
下面详细说明指令的含义:
mov al,[0]
前面我们使用mov指令,可完成两种传送:
①将数据直接送入寄存器;
②将一个寄存器中的内容送入另一个寄存器。
也可以使用mov指令将一个内存单元中的内容送入一个寄存器中。
从哪一个内存单元送到哪一个寄存器中呢?
在指令中必须指明。寄存器用寄存器名来指明,内存单元则需用内存单元的地址来指明。
显然,此时mov指令的格式应该是:mov 寄存器名,内存单元地址。
“[•••]”表示一个内存单元,“[•••]”中的0表示内存单元的偏移地址。
我们知道, 只有偏移地址是不能定位一个内存单元的,那么内存单元的段地址是多少呢?
指令执行时,8086CPU自动取ds中的数据为内存单元的段地址。
如何把一个数据送入寄存器呢?
我们以前用类似“mov ax,1”这样的指令来完成,从理论上讲,我们可以用相似的方式:mov ds,1000H,来将1000H送入ds。
可是,现实并非如此,8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器,所以 mov ds,1000H 这条指令是非法的。
那么如何将1000H送入ds呢?
只好用一个寄存器来进行中转,即先将1000H送入一个一般的寄存器如bx,再将bx中的内容送入ds。
为什么8086CPU不支持将数据直接送入段寄存器的操作?
这属于8086CPU硬件设计的问题,我们只要知道这一点就行了。
怎样将数据从寄存器送入内存单元?
从内存单元到寄存器的格式是:“mov 寄存器名, 内存单元地址”,从寄存器到内存单元则是:“mov 内存单元地址,寄存器名”。
10000H 可表示为1000:0,用ds存放段地址1000H,偏移地址是0,则mov [0],al 可完成从al到10000H的数据传送。
完整的几条指令是:
mov bx,1000H
mov ds,bx
mov [0],al
3.3 字的传送
8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是说可以一次性传送一个字。
只要在mov指令中给出16位的寄存器就可以进行16位数据的传送了。 比如:
mov bx,1000H
mov ds,bx
mov ax,[0] ;1000:0处的字型数据送入ax
mov [0],ex ;cx中的16位数据送到1000:0处
内存中的情况如图3.3所示,写出下面的指令执行后内存中的值,思考后看分析。
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命令在一个预设的地址0B39: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指向0B39:0105的时候,Debug显示当前的指令mov [0000],cs,因为这是一条访问内存的指令,Debug还显示出指令要访问的内存单元中的内容。
由于指令中的CS是一个16位寄存器,所以耍访问(写入)的内存单元是一个字单元,它的偏移地址为0,段地址在ds中,Debug在屏幕右边显示岀“DS:0000=0000”,我们可以知道这个字单元中的内容为0。
mov [0000],cs执行后,CS中的数据(0B39H)被写入1000:0处,1000:1单元存放0BH,1000:0单元存放39H。
最后,用D命令从1000:0开始查看指令执行后内存中的情况,注意1000:0、1000:1两个单元的内容。
(3)“mov 段寄存器,内存单元”也应该可行,比如我们可以用10000H处存放的字型数据设置ds(即将10000H处存放的字型数据送入ds),指令如下。
mov ax,1000H
mov ds,ax
mov ds,[0]
可以自行在Debug中进行试验。
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”,请自行在Debug中试验。
3.5 数据段
对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。
我们可以将一组长度为N(N<=64KB)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
比如用123B0H~123B9H这段内存空间来存放数据,我们就可以认为,123B0H~123B9H这段内存是一个数据段,它的段地址为123BH,长度为10个字节。
如何访问数据段中的数据呢?
将一段内存当作数据段,是我们在编程时的一种安排,可以在具体操作的时候,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
比如,将123E0H~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中
3.1~3.5 小 结
(1)字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
(2)用mov指令访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。
(3)[address]表示一个偏移地址为addiess的内存单元。
(4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
(5)mov、addx sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
(6)可以根据自己的推测,在Debug中实验指令的新格式。
3.6 栈、CPU提供的栈机制
在这里,我们对栈的研究仅限于这个角度:栈是一种具有特殊的访问方式的存储空间。
它的特殊性就在于,最后进入这个空间的数据,最先出去。
可以用一个盒子和3本书来描述栈的这种操作方式。
一个开口的盒子就可以看成一个栈空间,现在有3本书,《高等数学》、《C语言》、《软件工程》,把它们放到盒子中,操作的过程如图3.7所示。
现在的问题是,一次只允许取一本,我们如何将3本书从盒子中取岀来?
显然,必须从盒子的最上边取。
这样取出的顺序就是:《软件工程》、《C语言》、《高等数学》,和放入的顺序相反,如图3.8所示。
从程序化的角度来讲,应该有一个标记,这个标记一直指示着盒子最上边的书。
如果说,上例中的盒子就是一个栈,我们可以看出,栈有两个基本的操作:入栈和出栈。
入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。
栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为:LIFO(Last In First Out,后进先出)。
现今的CPU中都有栈的设计,8086CPU也不例外。
8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和岀栈指令,最基本的两个是PUSH(入栈)和 POP(岀栈)。
比如,push ax 表示将寄存器ax中的数据送入栈中,pop ax 表示从栈顶取出数据送入ax。
8086CPU的入栈和出栈操作都是以字为单位进行的。
下面举例说明,我们可以将10000H〜1000FH这段内存当作栈来使用。
mov ax,0123H
push ax
mov bx,2266H
push bx
mov ex,1122H
push ex
pop ax
pop bx
pop ex
注意,字型数据用两个单元存放,高地址单元存放高8位,低地址单元存放低8位。
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指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
图3.10描述了8086CPU对push指令的执行过程。
从图中我们可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
3.7 栈顶超界的问题
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.8 push、pop 指令
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中取得。
push ax是入栈指令,它将在栈顶之上压入新的数据。
一定要注意:它的执行过程是,先将记录栈顶偏移地址的SP寄存器中的内容减2,使得SS:SP指向新的栈顶单元,然后再将寄存器中的数据送入SS:SP指向的新的栈顶单元。
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.9 栈 段
对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。
我们可以将长度为N(N<=64KB)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。
比如,我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一个栈段,段地址为1001H,大小为16字节。
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行push、pop等栈操作指令时自动地将我们定义的栈段当作栈空间来访问。
如何使得如push、pop等栈操作指令访问我们定义的栈段呢?
前面我们己经讨论过,就是要将SS:SP指向我们定义的栈段。
任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的地址为栈最底部的字单元的地址+2。栈最底部字单元的地址为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中寄存器的设置,即CS、IP,SS、SP,DS的指向。
要非常清楚CPU的工作机理,才能在控制CPU按照我们的安排运行的时候做到游刃有余。