汇编语言个人学习笔记——第五章 [BX]和loop指令
引言:
[bx]和内存单元的描述
[bx]是什么?
和[0]有些类似,[0]表示内存单元,它的偏移地址是0。
如:
mov ax,[0]
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为0,段地址在ds中。
mov al,[0]
将一个内存单元的内容送入al,这个内存单元的长度为1字节(字节单元),存放一个字节,偏移地址为0,段地址在ds中。
要完整地描述一个内存单元,需要两种信息:
(1)内存单元的地址
(2)内存单元的长度(类型)
我们用[0]表示一个内存单元时,0表示单元的偏移地址,段地址默认在ds中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出,如前面的ax,al。
[bx]同样也表示一个内存单元,它的偏移地址在bx中,比如下面的指令:
mov ax,[bx]
mov al,[bx]
在写程序时,不能直接mov ax,[0](在debug中可以,但是在写程序时,经过编译器的编译,会让此命令变为mov ax,0),所以要用mov ax,[bx]的形式。
loop
这个指令和循环有关
我们定义描述性符号“()”
为了描述上的简洁,在以后的学习中,我们将使用一个描述性的符号“()”来表示一个寄存器或一个内存单元中的内容。比如:
(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
约定符号idata表示常量
我们在Debug中写过类似的指令:mov ax,[0],表示将ds:0处的数据送入ax中。指令中,在“[……]”里用一个常量()表示内存单元的偏移地址。以后,我们用idata表示常量。
例如:
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、mov ds,2等,它们都是非法指令非法指令。
5.1 [bx]
mov ax,[bx]
功能:bx中存放数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中。即(ax)=((ds)*16+(bx));
问题5.1:程序和内存中的情况如下图所示,写出程序执行后,21000H~21007H单元中的内容。
注:inc指令为自动加1,如mov ax,1 inc ax 则(ax)=2
问题5.1分析:
(1)先看一下程序的前三条指令
mov ax,2000H
mov ds,ax
mov bx,1000H
执行之后,ds=2000H,bx=1000H
(2)再看第四条指令:
mov ax,[bx]
执行之前,ds=2000H,bx=1000H,则mov ax,[bx]将内存2000:1000处的字型数据送入ax中。
执行后,ax=00beH。
(3)再看第5、6条指令:
inc bx
inc bx
指令执行前:bx=1000H,执行后:bx=0002H。
(4)再看第7条指令:
mov [bx],ax
指令执行前:ds=2000H,bx=1002H,则mov [bx],ax将把ax中的数据送入内存2000:1002处。
指令执行后:2000:1002单元的内容为BE,2000:1003单元的内容为00。
5.2 Loop指令
指令的格式是:loop标号,CPU执行loop指令的时候,要进行两步操作:
(1)(cx)=(cx)-1;
(2)判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
我们可以看出,cx中的值影响着loop指令的执行结果。
通常我们用loop指令来实现循环功能,cx中存放着循环次数。
例如:
任务1:编程计算2^2,结果存放结果存放在ax中。
分析:
设(ax)=2,可计算:(ax)=(ax)*2,最后(ax)就是2^2的值。N*2可用N+N实现。
代码:
assume cs:code
code segment
mov ax,2
add ax,ax
mov ax,4c00h
int 21h
code ends
end
任务二:编程计算2^3。
分析:
2^3=2*2*2,若设(ax)=2,可计算(ax)=(ax)*2*2,最后(ax)为2^3的值。N*2可用N+N实现。
代码:
assume cs:code
code segment
mov ax,2
add ax,ax
add ax,ax
mov ax,4c00h
int 21h
code ends
end
任务三:编程计算2^12。
分析:
2^12=2*2*2*2*2*2*2*2*2*2*2*2,若设(ax)=2,可计算:
(ax)=(ax)*2*2*2*2*2*2*2*2*2*2*2,最后(ax)中为2^12的值。N*2可用N+N实现。
代码:
assume cd:code
code segment
mov ax,2
;做11次add ax,ax
mov ax,4c00h
int 21h
code ends
end
按照我们的算法,计算2^12需要11条重复的指令add ax,ax。我们显然不希望这样来写程序,这里,可用loop来简化我们的程序。
代码:
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
其中,s就是标号。
程序分析:
(1)标号:
在汇编语言中,标号代表了一个地址,此程序中有一个标号s。它实际上标识了一个地址,这个地址处有一条指令:add ax,ax。
(2)loop s
CPU执行loop s的时候,要进行两步操作:
①(cx)=(cx)-1;
②判断cx中的值,不为0则转至标号s所标识的地址处执行(这里的指令是add ax,ax),如果为零则执行下一条指令(下一条指令是mov ax,4c00h)。
(3)以下三条指令
mov cx,11
s: add ax,ax
loop s
执行loop s时,首先要将(cx)减1,然后若(cx)不为0,则向前转至s处执行add ax,ax。所以,我们可以利用cx来控制add ax,ax的执行执行次数。
cx和loop指令相配合实现循环功能的三个要点:
(1)在cx中存放循环次数
(2)loop指令中的标号所标识地址要在前面
(3)要执行的程序段,要写在标号和loop指令的中间
用cx和loop指令相配合实现循环功能的程序框架如下:
mov cx,循环次数
s:
循环执行的程序段
loop s
问题5.2
用加法计算123x236,结果存在ax中。
分析:
可用循环完成,将123加236次,可先设(ax)=0,然后循环做236次(ax)=(ax)+123。
程序代码:
assume cs:code
code segmentg
mov ax,0
mov cx,236
s:add ax,123
loop s
mov ax,4c00h
int 21h
code ends
end
问题5.3:
改进问题5.2的程序,提高123x236的计算速度。
分析:我们可以将236加123次。可先设(ax)=0,然后循环做123次(ax)=(ax)+236,这样可以用123次加法实现相同的功能。
assume cs:code
code segment
mov ax,0
mov cx,123
s:add ax,236
loop s
mov ax,4c00h
code ends
end
5.3在Debug中跟踪用loop指令实现的循环程序
考虑这样一个问题,计算ffff:0006单元中的数乘以3,结果存储在dx中。我们分析一下:
(1)运算后的结果是否会超出dx所能存储的范围?
ffff:0006单元中的数是一个字节型的数据,范围在0~255之间,则用它和3相乘结果不会大于65535,可以在dx中存放下。
(2)我们用循环累加来实现乘法,用哪个寄存器进行累加?
我们将ffff:0006单元中的数赋值给ax,用dx进行累加。先设(dx)=0,然后做3次(dx)=(dx)+(ax)。
(3)ffff:0006单元是一个字节单元,ax是一个16位寄存器,数据长度不一样,如何赋值?
注意,我们说的是“赋值”,就是说让ax中的数据的值(数据的大小)和ffff:0006单元中的数据的值(数据的大小)相等。
如:8位数据01H和16位数据0001H的数据长度不一样,但它们的值是相等的。
如何赋值:
ffff:0006单元中的数据是XXH,若要ax中的值和ffff:0006da单元中的相等,ax中的数据应为00XXH。
所以若实现ffff:0006单元向ax赋值,我们应该令(ah)=0,(al)=(ffff6H)。
实现计算ffff:0006单元中的数乘以3,结果存储在dx中的程序代码。
assume cs:code
code segment
mov ax,0fffh
mov ds,ax
mov bx,6
mov al,[bx]
mov ah,0
mov dx,0
mov cx,3
s:add dx,ax
loop s
mov ax,4c00h
int 21h
code ends
end
(mov ah,0 mov al,[bx]是为了防止出错,因为ds:bx指向的内存单元是8位,而ax是16位寄存器,如果直接mov ax,[bx],ds:bx会将ds:bx+1作为它的高位一起移动到ax中)
注意程序中的第一条指令mov ax,0ffffh
我们知道大于9fffh的十六进制数据a000h、a001h、……、c000h、c001h、……、fffeh、ffffh等,在书写的时候都是以字母开头的。
而在汇编源程序中,数据不能以字母开头,所以要在前面加0。
下面我们对一个程序进行一次跟踪,再深入地了解以下loop指令。
assume cs:u
u segment
start:mov ax,0
mov bx,2
mov cx,3
s:add ax,bx
loop s
mov ax,4c00h
int 21h
u ends
end start
可以看到,如果执行到loop,cx中的值会先减1,然后再判断cx是否为0,如果不为0,则将ip改为标号处的ip值,如果为0,会执行下一条指令。
下面我们编程计算下ffff:0006单元中的数乘以123,结果存储在dx中。
我们可以用debug中的g 偏移地址执行到某个地方执行完循环,也可以直接p执行完当前的循环。
5.4Debug和汇编编译器Masm对指令的不同处理
我们在Debug中写过类似的指令:
mov ax,[0]
表示将ds:0处的数据送入al中。
但是在汇编源程序中,指令mov ax,[0]被编译器当作指令mov ax,0处理。
示例任务:将内存2000:0、2000:1、2000:2、2000:3单元中的数据送入al,bl,cl,dl中。
(1)在Debug中编程实现
(2)汇编程序实现
两种实现的实际情况:
(1)在Debug中编程实现:
mov ax,2000h
mov ds,ax
mov al,[0]
mov bl,[1]
mov cl,[2]
mov dl,[3]
对比汇编程序实现:
assume cs:code
code segment
start: mov ax,2000h
mov ds,ax
mov bx,0
mov al,[bx]
mov bx,2
mov cl,[bx]
mov bx,3
mov dl,[bx]
mov bx,1
mov bl,[bx]
mov ax,4c00h
int 21h
code ends
end start
在Masm中mov ax,[2]是解释为mov ax,2的。一般我们是通过bx来代替,像这道题我们先mov bx,2再通过mov ax,[bx]来实现。
但我们要像Debug一样直接用[2],就要加上段地址。
代码:
assume cs:code
code segment
start :mov ax,2000h
mov ds,ax
mov al,ds:[0]
mov bl,ds:[1]
mov cl,ds:[2]
mov dl,ds:[3]
mov ax,4c00h
int 21h
code ends
end start
总结:
在编译器masm中
mov al,[0] 等同于(al)=0
mov al,ds:[0] 等同于(al)=(ds:0)
mov al,[bx] 等同于(al)=(ds:(bx))
mov al,ds:[bx] 等同于(al)=(ds:(bx))
5.5 loop和[bx]的联合应用
考虑这样一个问题:
计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
分析:
(1)运算后的结果是否会超出dx所能存储的范围?
ffff:0~ffff:b内存单元中的数据是字节型数据,范围在0~255之间,12个这样的数据相加,结果不会大于65535,可以在dx中存放下。
(2)我们是否将ffff:0~ffff:b中的数据直接累加到dx中?
ffff:0~ffff:b中的数据是8位的,不能直接加到16位寄存器dx中。
(3)我们能否将ffff:0~ffff:b中的数据累加到dl中,并设置dx=0,从而实现累加到dx中的目标?
dl是8位寄存器,能容纳的数据的范围在0~255之间,ffff:0~ffff:b中的数据也都是8位,如果仅向dl中累加12个8位数据很有可能造成进位丢失。
(4)如何将ffff:0~ffff:b中的8位数据累加到16位寄存器中?
这里有两个问题:类型的匹配和结果的不超界。
如何解决这两个看似矛盾的问题?
目前的方法(在后面的学习中还有别的方法)就是我们得用一个16位寄存器来做中介。
我们将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界。
代码如下:
assume cs:code
code segment
start :mov ax,0ffffh
mov ds,ax
mov dx,0
mov al,ds:[0]
mov ah,0
add dx,ax
mov al,ds:[1]
mov ah,0
add dx,ax
mov al,ds:[2]
mov ah,0
add dx,ax
mov al,ds:[3]
mov ah,0
add dx,ax
mov al,ds:[4]
mov ah,0
add dx,ax
mov al,ds:[5]
mov ah,0
add dx,ax
mov al,ds:[6]
mov ah,0
add dx,ax
mov al,ds:[7]
mov ah,0
add dx,ax
mov al,ds:[8]
mov ah,0
add dx,ax
mov al,ds:[9]
mov ah,0
add dx,ax
mov al,ds:[a]
mov ah,0
add dx,ax
mov al,ds:[b]
mov ah,0
add dx,ax
mov al,ds:[c]
mov ah,0
add dx,ax
mov al,ds:[d]
mov ah,0
add dx,ax
mov al,ds:[e]
mov ah,0
add dx,ax
mov al,ds:[f]
mov ah,0
add dx,ax
mov ax,4c00h
int 21h
code ends
end start
问题5.4
应用loop指令,改进程序5.5,使它简化。
分析:
我们可以看出,在程序中,有12个相似的程序段,我们将它一般化地描述为:
mov al,ds:[x]
mov ah,0
add dx,ax
我们可以看到,12个相似的程序段中,只有mov al,ds:[x]指令中的内存单元的偏移地址是不同的,其他都一样。
而这些不同的偏移地址是可在0<=X<=0bH的范围内递增变化的。
我们可以用数学语言来描述这个累加的运算:
cong从程序实现上,我们将循环做:
(al)=((ds)*16+X)
(ah)=0
(dx)=(dx)+(ax)
一共循环12次,在循环开始前(dx)=0fffh,X=0,ds:X指向第一个内存单元。每次循环后,X递增,ds:X指向下一个内存单元。
完整的算法描述:
初始化:
(ds)=0ffffh
X=0
(ds)=0
循环12次:
(al)=((ds)*16+X)
(ah)=0
(dx)=(dx)+(ax)
X=X+1
可见,表示内存单元偏移地址的X应该是一个变量,因为在循环的过程中,偏移地址必须能够递增。
在指令中,我们就不能用常量来表示偏移地址。我们可以将偏移地址放到bx中,用[bx]的方式访问内存单元。
在循环开始前设(bx)=0,每次循环,将bx中的内容加1即可。
最后一个问题:如何实现循环12次?
我们要用loop指令。
初始化
(ds)=0ffffh
(bx)=0
(dx)=0
(cx)=12
循环12次:
s:(al)=((ds)*16+(bx))
(ah)=0
(dx)=(dx)+(ax)
(bx)=(bx)+1
loop s
完整代码:
assume cs:code
code segment
start :mov ax,0ffffh
mov ds,ax
mov bx,0
mov cx,12
mov dx,0
s:mov al,[bx]
mov ah,0
add dx,ax
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end start
在实际编程中,经常会遇到,用同一种方法处理地址连续的内存单元中的数据的问题。
我们需要用循环来解决这类问题,同时我们必须能够在每次循环的时候按照同一种方法来改变要访问的内存单元的地址。
这时,我们就不能用常量来给出内存单元的地址(比如[0]、[1]、[2]中,0、1、2是常量),而应用变量。
“mov al,[bx]”中的bx就可以看作一个代表内存单元地址的变量,我们可以不写新的指令,仅通过改变bx中的数值,改变指令访问的内存单元。
5.6段前缀
指令”mov ax,[bx]”中,内存单元的偏移地址由bx给出,而段地址默认在ds中。
我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。
这些出现在访问内存单元的指令中,用于显示地指明内存单元的段地址的“ds:”、“cs:”、“ss:”或“es:”,在汇编语言中称为段前缀段前缀
如:
mov ax,cs:[0]
mov bx,es:[0]
5.7一段安全的空间
在8086模式中,随意向一段内存空间写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。
比如下面的指令:
mov ax,1000h
mov ds,ax
mov al,0
mov ds:[0],al
在以前的Debug中,为了方便,写过类似的指令。
但这种做法是不合理的,因为之前我们并没有论证过1000:0中是否cun'存放着重要的系统数据或代码。
如果1000:0中存放着重要的的系统数据或代码,“mov ds:[0],al”将其改写,将引发错误。
比如程序:
assume cs:code
code segment
mov ax,0
mov ds,ax
mov ds:[26h],ax
mov ax,4c00h
int 21h
code ends
end
调试下:
发现,当执行到 mov ds:[26h],ax时,出现错误。
可见,我们在不能确定一段内存空间中是否存放着重要的数据或代码的时候,不能随意向其中写入内容。
不要忘记,我们是在操作系统的环境中工作,操作系统管理所有的资源,也包括内存。我们要通过汇编来获得底层的编程体验,理解计算机底层的基本工作机理。
注意:我们在纯dos方式(实模式)下,可以不理会dos,直接用汇编语言去操作真实的硬件,因为运行在cpu实模式xia'下的dos,没有能力对硬件系统进行全面严格的管理。
但在Windows XP\2000、UNIX这些运行于CPU保护模式下的操作系统中,不理会操作系统,用汇编语言去操作真实的硬件,是根本不可能的。
硬件已被这些操作系统利用CPU保护模式所提供的功能全面而严格地管理了。
在后面的课程中,我们需要直接向内存中写入内容,可我们又不希望出现错误,所以要找到一段安全的空间供我们使用。
在一般的PC机中,,DOS方式下,DOS和其他合法的程序一般都不会使用0:200~0:2FF(0:200h~0:2FFh)的256个字节的空间。所以,我们使用这段空间是安全的。
谨慎起见,进入dos后,我们先用debug查看一下,如果0:200~0:2FF单元的内容都是0的话,则证明dos和其他合法的程序没有使用这里。
为什么dos和其他合法程序一般都不会使用0:200~0:2FF这段空间?(以后再说)
总结:
(1)我们需要直接向一段内存中写入内容;
(2)这段内存空间不应存放系统或其他程序的数据或代码,否则写入操作系统很可能引发错误。
(3)dos方式下,一般情况,0:200~0:2FF空间中没有系统或其他程序的数据或代码;
(4)以后,我们需要直接向一段内存中写入内容时,就使用0:200~0:2FF这段空间。
5.8段前缀的使用
我们考虑一个问题:
将内存ffff:0~ffff:b单元中的数据拷贝到0:200~0:20b单元中。
(1)0:200~020b单元等同于0020:0~0020:b单元,它们描述的是同一段内存空间:
(2)拷贝的过程应用循环实现,简要描述如下:
初始化:X=0
循环12次:
将ffff:X单元中的数据送入0020:X(需要用一个寄存器中转)
X=X+1
(3)在循环中,源单元ffff:X单元和目标单元的0020:X的偏移地址X是变量。我们用bx来存放。
(4)我们将用0:200~0:20b描述,就是为了使目标单元的偏移地址和原始单元的偏移地址从同一数值0开始。
代码:
assume cs:code
code segment
mov bx,0
mov cx,12
s:mov ax,0ffffh
mov ds,ax
mov dl,[bx]
mov ax,0020h
mov ds,ax
mov [bx],dl
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end
(书中所给代码,略繁琐)
程序代码分析:
因源单元ffff:X和目标单元0020:X相距大于64KB,在不同的64KB段里,程序中,每次xu循环要设置两次ds。
这样做sh是正确的,但是效率不高。
我们可以使用两个段寄存器分别存放源单元ffff:X和目标单元0020:X的段地址,这样就可以省略循环中需要重复做24次的设置ds的程序段。
改进:
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov ax,0020h
mov es,ax
mov bx,0
mov cx,12
s:mov dl,[bx]
mov es:[bx],dl
inc bx
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。
注:在使用段前缀时一定要用段寄存器+偏移地址