[BX] 和 loop 指令
1、[bx] 是什么呢?和 [0] 有些类似,[0] 表示内存单元,它的偏移地址是 0。比如下面的指令中(在Debug中使用):
mov ax,[0]
将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址为 0,段地址在 ds 中。
mov al,[0]
讲一个内存单元的内容送入 ax,这个内存单元的长度为 1 字节(字节单元),存放一个字节,偏移地址为 0,段地址在 ds 中。
要完整地描述一个内存单元,需要两种信息:①内存单元的地址;②内存单元的长度(类型)。
用 [0] 表示一个内存单元时,0 表示单元的偏移地址,段地址默认在 ds 中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出。
[bx] 同样也表示一个内存单元,它的偏移地址在 bx 中,比如下面的指令:
mov ax,[bx]
将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址在 bx 中,段地址在 ds 中。
mov al,[bx]
将一个内存单元的内容送入 al,这个内存单元的长度为 1 字节(字节单元),存放一个字节,偏移地址在 bx 中,段地址在 ds 中。
2、我们定义的描述性符号:"()"
为了描述上的简洁,我们将使用一个描述性符号“()”来表示一个寄存器或一个内存单元中的内容。比如:
(ax) 表示寄存器 ax 中的内容,(al) 表示 al 中的内容;
(20000H) 表示内存 20000H 单元的内容(()中的内存单元的地址为物理地址);
((ds) * 16 + (bx))表示:
ds 中的内容为 ADR1,bx 中的内容为 ADR2,内存 ADR1x16+ADR2 单元的内容。
也可以理解为:ds 中的 ADR1 作为段地址, bx 中的 ADR2 作为偏移地址,内存 ADR1:ADR2 单元的内容。
“()”中的元素可以有 3 种类型:①寄存器名;②段寄存器名;③内存单元的一个物理地址(一个 20 位数据)。比如:
(ax)、(ds)、(al)、(cx)、(20000H)、((ds)x16+(bx))等是正确的用法。
我们看下 (X) 的应用,比如:
(1)ax 中的内容为 0010H,可以这样来描述:(ax)=0010H;
(2)2000:1000 处的内容为 0010H,可以这样描述:(21000H)=0010H;
(3)对于 mov ax,[2] 的功能,可以这样来描述:(ax)=((ds)*16+2);
(4)对于 mov [2],ax 的功能,可以这样描述:((ds)*16+2)=(ax);
(5)对于 add ax,2 的功能,可以这样描述:(ax)=(ax)+2;
(6)对于 add ax,bx 的功能,可以这样描述:(ax)=(ax)+(bx);
(7)对于 push ax 的功能,可以这样描述:
(sp)=(sp)-2
((ss)*16+(sp))=(ax)
(8)对于 pop ax 的功能,可以这样描述:
(ax)=((ss)*16+(sp))
(sp)=(sp)+2
“(X)”所表示的数据有两种类型:①字节;②字。是那种类型由寄存器名或具体的运算决定。
(al)、(bl)、(cl) 等得到的数据为字节型;(ds)、(ax)、(bx) 等得到的数据为字型。
(al)=(20000H),则 (20000H) 得到的数据为字节型;(ax)=(20000H),则 (20000H) 得到的数据为字型。
3、约定符号 idata 表示常量
我们在 debug 中写过类似的指令:mov ax,[0],表示将 ds:0 处的数据送入 ax 中。指令中,在“[···]”里用一个常量 0 表示内存单元的偏移地址。
mov ax,[idata] 代表 mov ax,[1]、mov ax,[2]、mov ax,[3] 等。
mov bx,idata 代表 mov bx,1、mov bx,2、mov bx,3 等。
mov ds,idata 代表 mov ds,1 等。
5.1 [bx]
看一看下面的指令的功能。
mov ax,[bx]
功能:bx 中存放的数据作为一个偏移地址 EA,段地址 SA 默认在 ds 中,将 SA:EA 处的数据送入 ax 中。即:(ax)=((ds)*16+(bx))。
mov [bx],ax
功能:bx 中存放的数据作为一个偏移地址 EA,段地址 SA 默认在 ds 中,将 ax 中的数据送入内存 SA:EA 处。即((ds)*16+(bx))=(ax)。
5.2 Loop 指令
loop 指令的格式是:loop 标号,CPU 执行 loop 指令的时候,要进行两步操作,①(cx)=(cx)-1;②判断 cx 中的值,不为零则转至标号处执行程序,如果为零则向下执行。
从上面的描述中,可以看到,cx 中的值影响着 loop 指令的执行结果。通常(注意,我们说的是通常)我们用 loop 指令来实现循环,cx 中存放循环次数。
例子:计算2的12次方
assume cs:code code segment mov ax,2 mov cx,11 s: add ax,ax loop s mov ax,4c00h int 21h code ends end
分析:
(1)标号:在汇编语言中,标号代表一个地址,上面的程序中有一个标号 s。它实际上标识了一个地址,这个地址处有一个指令:add ax,ax。
(2)loop s:CPU 执行 loop s 的时候,要进行两步操作:
① (cx)=(cx)-1;
② 判断 cx 中的值,不为 0 则转至 标号 s 所标识的地址处执行(这里的指令是 add ax,ax),如果为 0 则执行下一条指令(这里的下一条指令是 mov ax,4c00h)
(3)以下 3 条指令:
mov cx,11 s: add ax,ax loop s
执行 loop s 的时候,首先要将 (cx) 减 1,然后若 (cx) 不为 0,则向前转至 s 处执行 add ax,ax。所以,可以利用 cx 来控制 add ax,ax 的执行次数。
5.3 在 Debug 中跟踪 loop 指令实现的循环程序
① 在汇编源程序中,数据不能以字母开头,如:ffffh 在汇编源程序中要写为 “0ffffh”。
② g xxx,t 是单步执行,g 命令直接执行前面的指令,在 ip=xxx 处停下。
③ 把内存中的单个字节单元送入寄存器:
mov bx,6 mov al,[bx] mov ah,0
我们从上图可以看出,在 loop 指令执行的时候(cx 大于 0 的时候),IP 又指向了 s 标识指向的地方(0010),loop 指令执行之后,cx 的值减少了 1。
5.4 Debug 和 汇编编译器 masm 对指令的不同处理
我们在 Debug 中写过类似的指令:
mov ax,[0]
表示将 ds:0 处的数据送入 ax 中。
但是在汇编源程序中,指令“mov ax,[0]”被编译器当作指令“mov ax,0”处理。
如:将内存 2000:0、2000:1、2000:2、2000:3 单元中的数据送入 al、bl、cl、dl 中。
(1)在 Debug 中编程实现:
mov ax,2000 mov ds,ax mov al,[0] mov bl,[1] mov cl,[2] mov dl,[3]
(2)汇编源程序实现:
assume cs:code code segment mov ax,2000h mov ds,ax mov al,[0] mov bl,[1] mov cl,[2] mov dl,[3] mov ax,4c00h int 21h code ends end
我们看一下两种实现的实际实施情况:
(1)Debug 中的情况如下:
(2)masm 的情况如下:
从以上两图我们可以看出,Debug 和编译器 masm 对形如 “mov ax,[0]”这类指令在解释上不同。Debug 将它解释为一个内存单元,"idata" 是内存单元的偏移地址;而编译器将“[idata]”解释为“idata”。
那么我们如何在汇编源程序中实现将内存 2000:0、2000:1、2000:2、2000:3 单元中的数据送入 al、bl、cl、dl 中呢?目前的方法是,可将偏移地址送入寄存器 bx 中,用 [bx] 的方式来访问内存单元。比如:
mov ax,2000h mov ds,ax ;段地址 2000h 送入 ds mov bx,0 ;偏移地址 0 送入 bx mov al,[bx] ;ds:bx 单元中的数据送入 al
这样做可以,可是比较麻烦,我们可以在“[]”前面显式地给出段地址所在的段寄存器,比如我们可以这样访问 2000:0 单元:
mov ax,2000h mov ds,ax mov al,ds:[0]
比较一下汇编源程序中以下指令的含义。
“mov al,[0]”,含义:(al)=0,将常量 0 送入 al 中(与 mov al,0 含义相同)
“mov al,ds:[0]”,含义:(al)=((ds)*16+0),将内存单元中的数据送入 al 中
“mov al,[bx]”,含义:(al)=((ds)*16+(bx)),将内存单元中的数据送入 al 中
“mov al,ds:[bx]”,含义:与“mov al,[bx]”相同
从上面的比较可以看出:
(1)在汇编源程序中,如果用指令访问一个内存单元,则在指令中必须用“[···]”来表示内存单元,如果在“[]”里用一个常量 idata 直接给出内存单元的偏移地址,就要在“[]”前面显式地给出段地址所在的寄存器。比如:
mov al,ds:[0]
如果没有在“[]”前面给出段寄存器,比如:
mov al,[0]
那么,编译器 masm 将把指令中的“[idata]”解释为“idata”。
(2)如果在“[]”里用寄存器,比如bx,间接给出内存单元的偏移地址,则段地址默认在 ds 中。当然,也可以显式给出段地址所在的段寄存器。
5.5 loop 和 [bx] 的联合应用
例子:计算 ffff:0~ffff:b 单元中的数据的和,结果存储在 dx 中。
assume cs:code code segment mov ax,0ffffh mov ds,ax mov bx,0 ;初始化 ds:bx 指向 ffff:0 mov dx,0 ;初始化累加寄存器dx,(dx)=0 mov cx,12 ;初始化循环计数寄存器cx,(cx)=12 s: mov al,[bx] mov ah,0 ;将bx指向的字节单元送入ax add dx,ax ;将ax中的字节单元累加到dx中 inc bx ;ds:bx 指向下一个单元 loop s mov ax,4c00h int 21h code ends end
5.6 段前缀
指令“mov ax,[bx]”中,内存单元的偏移地址由 bx 给出,而段地址默认在 ds 中,我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。比如:
(1)mov ax,ds:[bx]
将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址存放在 bx 中,段地址在 ds 中。
(2)mov ax,cs:[bx]
将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址在bx 中,段地址在 cs 中。
这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址“ds:”、“cs:”,在汇编语言中称为 段前缀。
5.7 一段安全的空间
在 8086 模式中,随意向一段内存空间写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。比如下面的指令:
mov ax,1000h mov ds,ax mov al,0 mov ds:[0],al
这种做法是很不合理的,如果 1000:0 中存放着重要的系统数据或代码,“mov ds:[0],al”将其改写,将引发错误。
在不确定一段内存空间中是否存放着重要的数据或代码的时候,不能随意向其中写入内容。如果我们需要向内存空间写入内容的话,要使用操作系统给我们分配的空间,而不应直接用地址任意指定内存单元,向里面写入。
在一般 PC 机,DOS 方式下,DOS 和 其他合法的程序一般都不会使用 0:200~0:2ff(00200h~002ffh) 的 256 个字节的空间。所以,我们使用这段空间是安全的。不过为了谨慎起见,在进入 DOS 后,我们可以先用 Debug 查看一下,如果 0:200~0:2ff 单元的内容都是 0 的话,则证明 DOS 和 其他合法的程序没有使用这里。
5.8 段前缀的使用
问题:将内存 ffff:0~ffff:b 单元中的数据复制到 0:200~0:20b 单元中。
(1)不使用段前缀:
assume cs:code code segment mov bx,0 ;(bx)=0,循环地址从 0 开始 mov cx,12 ;(cx)=12,循环 12 次 s: mov ax,0ffffh mov ds,ax ;(ds)=0ffffh mov dl,[bx] ;(dl)=((ds)*16+(bx)),将 ffff:bx 中的数据送入dl mov ax,0020h mov ds,ax ;(ds)=0020h mov [bx],dl ;((ds)*16+(bx))=(dl),将 dl 中的数据送入0020:bx inc bx ;(bx)=(bx)+1 loop s mov ax,4c00h int 21h code ends end
因原始单元 ffff:X 和目标单元 0020:X 相距大于 64KB,在不同的 64KB 段里,上面的程序每次循环都要设置两次 ds。这样做是正确的,但是效率不高。我们可以使用两个寄存器分别存放原始单元 ffff:X 和目标单元 0020:X 的段地址,这样就可以省略循环中需要重复做 12 次的设置 ds 的程序段。
(2)使用段前缀:
assume cs:code code segment mov ax,0ffffh mov ds,ax ;(ds)=0ffffh mov ax,0020h mov es,ax ;(es)=0020h mov bx,0 ;(bx)=0,此时 ds:bx 指向 ffff:0,es:bx 指向0020:0 mov cx,12 ;(cx)=12,循环 12 次 s: mov dl,[bx] ;(dl)=((ds)*16+(bx)),将 ffff:bx 中的数据送入 dl mov es:[bx],dl ;((ds)*16+(bx))=(dl),将 dl 中的数据送入 0020:bx inc bx ;(bx)=(bx)+1 loop s mov ax,4c00h int 21h code ends end
上面的代码中,使用 es 存放 0020:0~0020:b 的段地址,用 ds 存放源始空间 ffff:0~ffff:b 的段地址。在访问内存指令“mov es:[bx],al”中,显式地用段前缀“es:”给出单元的段地址,这样就不必在循环中循环设置 ds。
注意:dx 不能用类似 mov dl,[dx] 的操作,不能使用 dx 间接寻址,如果需要,可以使用 si、di。