天天看点

[汇编]《汇编语言》第5章[BX]和 loop 指令

目录

王爽《汇编语言》第四版 超级笔记

第5章[BX]和 loop 指令

5.1 [BX]

5.2 loop 指令

5.3 Debug和汇编编译器masm对指令的不同处理

5.4 loop和[bx]的联合应用

5.5 段前缀及使用

5.6 一段安全的空间

1、[bx]和 内存单元的描述

[bx]是什么呢?和[0]有些类似,[0]表示内存单元,它的偏移地址是0。

比如在下面的指令中(在Debug中使用):

mov ax,[0]

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为0,段地址在ds中。

mov al,[0]

将一个内存单元的内容送入al,这个内存单元的长度为1字节(字节单元),存放一个字节,偏移地址为0,段地址在ds中。

要完整地描述一个内存单元,需要两种信息:

①内存单元的地址;

②内存单元的长度(类型)。

用[0]表示一个内存单元时,0表示单元的偏移地址,段地址默认在ds中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出。

[bx]同样也表示一个内存单元,它的偏移地址在bx中,比如下面的指令:

mov ax,[bx]

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在ds中。

mov al,[bx]

2、loop

英文单词“loop”有循环的含义,显然这个指令和循环有关。

3、我们定义的描述性的符号:"()”

我们将使用一个描述性的符号“()”来表示一个寄存器或一个内存单元中的内容。比如:

(ax)表示ax中的内容、(al)表示al中的内容;

(20000H)表示内存20000H单元的内容(()中的内存单元的地址为物理地址);

注意,“()”中的元素可以有3种类型:

①寄存器名;

②段寄存器名;

③内存单元的物理地址(一个20位数据)。

我们看一下(X)的应用,比如:

(1) ax中的内容为0010H,可以这样来描述:(ax)=0010H; (2) 2000:1000处的内容为0010H,可以这样来描述:(21000H)=0010H; (3) 对于mov ax,[2]的功能,可以这样来描述:(ax)=((ds)x16+2); (4) 对于mov [2],ax的功能,可以这样来描述:((ds)x16+2)=(ax); (5) 对于add ax,2的功能,可以这样来描述:(ax)=(ax)+2; (6) 对于add ax,bx的功能,可以这样来描述:(ax)=(ax)+(bx); (7) 对于push ax的功能,可以这样来描述: (sp)=(sp)-2 ((ss)x16+(sp))=(ax) (8) 对于pop ax的功能,可以这样来描述: (ax)=((ss)x16+(sp)) (sp)=(sp)+2

“(X)”所表示的数据有两种类型:①字节;②字。

是哪种类型由寄存器名或具体的运算决定。

4、约定符号idata表示常量

我们在Debug中写过类似的指令:mov ax,[0],表示将ds:0处的数据送入ax中。

指令中,在里用一个常量0表示内存单元的偏移地址。

以后,我们用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 等,它们都是非法指令。

功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中。即:(ax)=((ds)x16+(bx))。

mov [bx],ax

功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将ax中的数据送入内存

SA:EA处。即:((ds)x16+(bx))=(ax)。

问题5.1

程序和内存中的情况如图5.1所示,写出程序执行后,21000H-21007H单元中的内容。

[汇编]《汇编语言》第5章[BX]和 loop 指令

思考后看分析。

注意,inc bx 的含义是bx中的内容加1,比如下面两条指令:

mov bx,1

inc bx

执行后,bx=2。

分析:

(1)先看一下程序的前3条指令:

mov ax,2000H

mov ds,ax

mov bx,1000H

这3条指令执行后,ds=2000H,bx=1000H。

(2)接下来,第4条指令:

指令执行前:ds=2000H,bx=1000H,则mov ax,[bx] 将把内存2000:1000处的字型数据送入ax中。该指令执行后,ax=00beH。

(3)接下来,第5、6条指令:

这两条指令执行前bx=1000H,执行后bx=1002H。

(4)接下来,第7条指令:

指令执行前:ds=2000H,bx=1002H,则mov [bx],ax 将把ax中的数据送入内存2000:1002处。

指令执行后,2000:1002单元的内容为BE,2000:1003单元的内容为00。

(5)接下来,第8、9条指令:

这两条指令执行前bx=1002H,执行后bx=1004H。

(6)接下来,第10条指令:

指令执行前:ds=2000H,bx=1004H,则mov [bx],ax 将把ax中的数据送入内存2000:1004处。

指令执行后,2000:1004单元的内容为BE,2000:1005单元的内容为00。

(7)接下来,第11条指令:

这条指令执行前bx=1004H,执行后bx=1005H。

(8)接下来,第12条指令:

mov [bx],al

指令执行前:ds=2000H,bx=1005H,则mov [bx],al 将把al中的数据送入内存2000:1005处。

指令执行后,2000:1005单元的内容为BE。

(9)接下来,第13条指令:

这条指令执行前bx=1005H,执行后bx=1006H。

(10)接下来,第14条指令:

指令执行前:ds=2000H,bx=1006H,则mov [bx],al 将把al中的数据送入内存2000:1006处。

指令执行后,2000:1006单元的内容为BE。

程序执行后,内存中的情况如图5.2所示。

[汇编]《汇编语言》第5章[BX]和 loop 指令

loop指令的格式是:loop标号,CPU执行loop指令的时候,要进行两步操作:

①(cx)=(cx)-l;

②判断CX中的值,不为零则转至标号处执行程序,如果为零则向下执行。

从上面的描述中,可以看到,CX中的值影响着loop指令的执行结果。

通常(注意,我们说的是通常)我们用loop指令来实现循环功能,CX中存放循环次数。

下面我们通过一个程序来看一下loop指令的具体应用。

编程计算2^12。

分析:212=2x2x2x2x2x2x2x2x2x2x2x2,若设(ax)=2,可计算(ax)=(ax)x2x2x2x2x2x2x2x2x2x2x2,最后(ax)中为212的值。Nx2可用N+N实现,程序如下。

可见,按照我们的算法,计算2^12需要11条重复的指令add ax,ax,我们显然不希望这样来写程序,这里,可用loop来简化我们的程序。

程序5.1

下面分析一下程序5.1。

(1)标号

在汇编语言中,标号代表一个地址,程序5.1中有一个标号s。它实际上标识了一个地址,这个地址处有一条指令:add ax,ax。

(2)loop s

CPU执行loop s的时候,要进行两步操作:

①(cx)=(cx)-l ;

②判断ex中的值,不为0则转至标号s所标识的地址处执行(这里的指令是add ax,ax),如果为0则执行下一条指令(下一条指令是mov ax,4c00h)。

(3)以下3条指令

执行loop s时,首先要将(cx)减1,然后若(cx)不为0,则向前转至s处执行add ax,ax。

所以,可以利用cx来控制add ax,ax的执行次数。

下面我们详细分析一下这段程序的执行过程,从中体会如何用cx和loop s相配合实现循环功能。

[汇编]《汇编语言》第5章[BX]和 loop 指令
[汇编]《汇编语言》第5章[BX]和 loop 指令

从上面的过程中,我们可以总结出用cx和loop指令相配合实现循环功能的3个要点:

(1) 在cx中存放循环次数;

(2) loop指令中的标号所标识地址要在前面;

(3) 要循环执行的程序段,要写在标号和loop指令的中间。

用cx和loop指令相配合实现循环功能的程序框架如下。

编程,用加法计算123x236,结果存在ax中。思考后看分析。

可用循环完成,将123加236次。可先设(ax)=0,然后循环做236次(ax)=(ax)+123。

程序如下

程序5.2

我们在Debug中写过类似的指令:

表示将ds:0处的数据送入ax中。

但是在汇编源程序中,指令“mov ax,[0]”被编译器当作指令“mov ax,0”处理。

下面通过具体的例子来看一下Debug和汇编编译器masm对形如“mov ax,[0]”这类指令的不同处理。

任务:将内存2000:0、2000:1、2000:2、2000:3单元中的数据送入al、bl、cl、dl中。

(1)在Debug中编程实现:

(2)汇编源程序实现:

我们看一下两种实现的实际实施情况:

(1)Debug中的情况如图5.16所示。

[汇编]《汇编语言》第5章[BX]和 loop 指令

(2)将汇编源程序存储为compare.asm,用masm、link生成compare.exe,用Debug加载compare.exe,如图5.17所示。

[汇编]《汇编语言》第5章[BX]和 loop 指令

从图5.16、图5.17中我们可以明显地看出,Debug和编译器masm对形如“mov ax,[0]”这类指令在解释上的不同。

我们在Debug中和源程序中写入同样形式的指令:"mov al,[0]"、"mov bl,[1]"、"mov cl,[2]"、"mov dl,[3]”,但Debug和编译器对这些指令中的“[idata]”却有不同的解释。

Debug将它解释为“[idata]”是一个内存单元,“idata”是内存单元的偏移地址;而编译器将“[idata]”解释为“idata”。

那么我们如何在源程序中实现将内存2000:0、2000:1、2000:2、2000:3单元中的数据送入al、bl、cl、dl中呢?

目前的方法是,可将偏移地址送入bx寄存器中,用[bx]的方式来访问内存单元。比如我们可以这样访问2000:0单元:

这样做是可以,可是比较麻烦,我们要用bx来间接地给出内存单元的偏移地址。

我们还是希望能够像在Debug中那样,在“[]”中直接给出内存单元的偏移地址。这样做,在汇编源程序中也是可以的,只不过,要在“[]”的前面显式地给出段地址所在的段寄存器。比如我们可以这样访问2000:0单元:

mov ax,2000h mov al,ds:[0]

比较一下汇编源程序中以下指令的含义。

“mov al,[0]”,含义:(al)=0,将常量0送入al中(与mov al,0含义相同);

“mov al,ds:[0]”,含义:(al)=((ds)x16+0),将内存单元中的数据送入al中;

“mov al,[bx]”,含义:(al)=((ds)x16+(bx)),将内存单元中的数据送入al中;

“mov al,ds:[bx]”,含义:与“mov al,[bx]”相同。

从上面的比较中可以看出:

(1)在汇编源程序中,如果用指令访问一个内存单元,则在指令中必须用“[…]”来表示内存单元,如果在“[]”里用一个常量idata直接给出内存单元的偏移地址,就要在“[]”的前面显式地给出段地址所在的段寄存器。比如

如果没有在“[]”的前面显式地给出段地址所在的段寄存器,比如

那么,编译器masm将把指令中的“[idata]”解释为“idata”。

(2)如果在“[]”里用寄存器,比如bx,间接给出内存单元的偏移地址,则段地址默认在ds中。当然,也可以显式地给出段地址所在的段寄存器。

考虑这样一个问题,计算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中,并设置(dh)=0,从而实现累加到dx中?

这也不行,因为dl是8位寄存器,能容纳的数据的范围在0~255之间,ffff:0~ffff:b中的数据也都是8位,如果仅向dl中累加12个8位数据,很有可能造成进位丢失。

(4)我们到底怎样将ffff:0~ffff:b中的8位数据,累加到16位寄存器dx中?

从上面的分析中,可以看到,这里面有两个问题:类型的匹配和结果的不超界。

具体地说,就是在做加法的时候,我们有两种方法:

1、(dx)=(dx)+内存中的8位数据; 2、(dl)=(dl)+内存中的8位数据。

第一种方法中的问题是两个运算对象的类型不匹配,第二种方法中的问题是结果有可能超界。

怎样解决这两个看似矛盾的问题?

目前的方法(在后面的课程中我们还有别的方法)就是得用一个16位寄存器来做中介。

将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界。

程序5.5

上面的程序很简单,不用解释,你一看就懂。不过,在看懂了之后,你是否觉得这个程序编得有些问题?

它似乎没有必要写这么长。这是累加ffff:0~ffff:b中的12个数据,如果要累加0000:0~0000:7fff中的32K个数据,按照这个程序的思路,将要写将近10万行程序(写一个简单的操作系统也就这个长度了)。

问题5.4

应用loop指令,改进程序5.5,使它的指令行数让人能够接受。

可以看出,在程序中,有12个相似的程序段,我们将它们一般化地描述为:

mov al,ds:[X] ;ds:X指向ffff:X单元 mov ah,0 ;(ax)=((ds)x16+(X))=(ffffXh) add dx,ax ;向dx中加上ffff:X单元的数值

从程序实现上,我们将循环做。

(al)=((ds)x16+X) (ah)=0 (dx)=(dx)+(ax)

一共循环12次,在循环开始前(ds)=ffffh,X=0,ds:X指向第一个内存单元。每次循环后,X递增,ds:X指向下一个内存单元。

完整的算法描述如下。

初始化:

(ds)=ffffh X=0 (dx)=0

循环12次:

X=X+1

可见,表示内存单元偏移地址的X应该是一个变量,因为在循环的过程中,偏移地址必须能够递增。

这样,在指令中,我们就不能用常量来表示偏移地址。我们可以将偏移地址放到bx中,用[bx]的方式访问内存单元。在循环开始前设(bx)=0,每次循环,将bx中的内容加1即可。

最后一个问题是,如何实现循环12次?

我们的loop指令该发挥作用了。更详细的算法描述如下。

(bx)=0 (cx)=12

最后,我们写出程序。

程序5.6

在实际编程中,经常会遇到,用同一种方法处理地址连续的内存单元中的数据的问题。

我们需要用循环来解决这类问题,同时我们必须能够在每次循环的时候按照同一种方法来改变要访问的内存单元的地址。

这时,就不能用常量来给出内存单元的地址(比如,[0]、[1]、[2]中,0、1、2是常量),而应用变量。“mov al,[bx]”中的bx就可以看作一个代表内存单元地址的变量,我们可以不写新的指令,仅通过改变bx中的数值,改变指令访问的内存单元。

指令“mov ax,[bx]”中,内存单元的偏移地址由bx给出,而段地址默认在ds中。

我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。比如:

(1) mov ax,ds:[bx]

(2)mov ax,cs:[bx]

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在cs中。

(3)mov ax,ss:[bx]

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在ss中。

(4)mov ax,es:[bx]

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在es中。

(5)mov ax,ss:[0]

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为0,段地址在ss中。

(6)mov ax,cs:[0]

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为0,段地址在cs中。

这些岀现在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:”“cs:”“ss:”“es:”,在汇编语言中称为段前缀。

我们考虑一个问题,将内存ffff:0~ffff:b单元中的数据复制到0:200~0:20b单元中。

分析一下。

(1)0:200~0:20b单元等同于0020:0~0020:b单元,它们描述的是同一段内存空间。

(2)复制的过程应用循环实现,简要描述如下。

将ffff:X单元中的数据送入0020:X(需要用一个寄存器中转)

(3)在循环中,源始单元ffff:X和目标单元0020:X的偏移地址X是变量。我们用bx来存放。

(4)将0:200~0:20b用0020:0~0020:b描述,就是为了使目标单元的偏移地址和源始单元的偏移地址从同一数值0开始。

程序5.8

因源始单元ffff:X和目标单元0020:X相距大于64KB,在不同的64KB段里,程序5.8中,每次循环要设置两次ds。

这样做是正确的,但是效率不高。我们可以使用两个段寄存器分别存放源始单元ffff:X和目标单元0020:X的段地址,这样就可以省略循环中需要重复做12次的设置ds的程序段。

改进的程序如下。

程序5.9

程序5.9中,使用es存放目标空间0020:0~0020:b的段地址,用ds存放源始空间ffff:0~ffff:b的段地址。

在访问内存单元的指令“mov es:[bx],al”中,显式地用段前缀“es:”给出单元的段地址,这样就不必在循环中重复设置ds。

在8086模式中,随意向一段内存空间写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。比如下面的指令:

mov ax,1000h mov al,0 mov ds:[0],al

我们以前在Debug中,为了讲解上的方便,写过类似的指令。但这种做法是不合理的,因为之前我们并没有论证过1000:0中是否存放着重要的系统数据或代码。

如果1000:0中存放着重要的系统数据或代码,“mov ds:[0],al”将其改写,将引发错误。

比如下面的程序。

程序5.7

将源程序编辑为p7.asm,编译、连接后生成p7.exe,用Debug加载,跟踪它的运行,如图5.18所示。

[汇编]《汇编语言》第5章[BX]和 loop 指令

图5.18中,我们可以看到,源程序中的“mov ds:[26h],ax”被masm翻译为机器码"a3 26 00",而Debug将这个机器码解释为"mov [0026],ax"。

可见,汇编源程序中的汇编指令“mov ds:[26h],ax”和Debug中的汇编指令"mov [0026],ax"同义。

我们看一下“mov [0026],ax”的执行结果,如图5.19所示。

[汇编]《汇编语言》第5章[BX]和 loop 指令

图5.19中,是在windows2000的DOS方式中,在Debug里执行“mov [0026],ax”的结果。

如果在实模式(即纯DOS方式)下执行程序p7.exe,将会引起死机。产生这种结果的原因是0:0026处存放着重要的系统数据,而“mov [0026],ax”将其改写。

可见,在不能确定一段内存空间中是否存放着重要的数据或代码的时候,不能随意向其中写入内容。

同样不能忘记,我们正在学习的是汇编语言,要通过它来获得底层的编程体验,理解计算机底层的基本工作机理。所以我们尽量直接对硬件编程,而不去理会操作系统。

注意,我们在纯DOS方式(实模式)下,可以不理会DOS,直接用汇编语言去操作真实的硬件,因为运行在CPU实模式下的DOS,没有能力对硬件系统进行全面、严格的管理。

但在Windows2000、Unix这些运行于CPU保护模式下的操作系统中,不理会操作系统,用汇编语言去操作真实的硬件,是根本不可能的。硬件已被这些操作系统利用CPU保护模式所提供的功能全面而严格地管理了。

在一般的PC机中,DOS方式下,DOS和其他合法的程序一般都不会使用0:200~0:2ff(00200h~002ffh)的256个字节的空间。

所以,我们使用这段空间是安全的。不过为了谨慎起见,在进入DOS后,我们可以先用Debug查看一下,如果0:200~0:2ff单元的内容都是0的话,则证明DOS和其他合法的程序没有使用这里。

好了,我们总结一下:

(1)我们需要直接向一段内存中写入内容;

(2)这段内存空间不应存放系统或其他程序的数据或代码,否则写入操作很可能引发错误;

(3)DOS方式下,一般情况,0:200~0:2ff空间中没有系统或其他程序的数据或代码;

(4)以后,我们需要直接向一段内存中写入内容时,就使用0:200~0:2ff这段空间。

继续阅读