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

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]
将一个内存单元的内容送入al,这个内存单元的长度为1字节(字节单元),存放一个字节,偏移地址在bx中,段地址在ds中。
2.loop
英文单词“loop”有循环的含义,显然这个指令和循环有关。
我们在这一章,讲解[bx]和loop指令的应该、意义和相关的内容。

3.我们定义的描述性的符号:“()”
为了描述上的简洁,在以后的课程中,我们将使用一个描述性的符号“()”来表示一个寄存器或一个内存单元中的内容。比如:
(ax)表示ax中的内容、(al)表示al中的内容;
(20000H)表示内存20000H单元的内容(()中的内存单元的地址为物理地址);
((ds)*16+(bx))表示:
ds中的内容为ADR1,bx中的内容为ADR2,内存ADR1×16+ADR2单元的内容。
也可以理解为:ds中的ADR1作为段地址,bx中的ADR2作为 偏移地址,内存ADR1:ADR2单元的内容。
注意,“()”中的元素可以有3种类型:⑴寄存器名; ⑵ 段寄存器名;⑶ 内存单元的物理地址(一个20位数据)。比如:
(ax)、(ds)、(al)、(cx)、(20000H)、((ds)*16+(bx))等是正确的用法;
(2000:0)、((ds):1000H)等是不正确的用法。
我们看一下(X)的应该,比如,
⑴ ax中的内容为0010H,可以这样来描述:(ax)=0010H;
⑵ 2000:1000处的内容为0010H,可以这样来描述:(21000H)=0010H;
⑶ 对于mov ax,[2]的功能,可以这样来描述:(ax)=((ds)*16+2);
⑷ 对于mov [2],ax的功能,可以这样来描述:((ds)*16+2)=(ax);
⑸ 对于add ax,2的功能,可以这样来描述:(ax)=(ax)+2;
⑹ 对于add ax,bx的功能,可以这样来描述:(ax)=(ax)+(bx);
⑺ 对于push ax的功能,可以这样来描述:
    (sp)=(sp)-2
    ((ss)*16+(sp))=(ax)
⑻ 对于pop ax的功能,可以这样来描述:
    (ax)=((ss)*16+(sp))
    (sp)=(sp)+2
“(X)”所表示的数据有两种类型:⑴字节;⑵ 字。是哪种类型由寄存器名或具体的运算决定,比如:(al)、(bl)、(cl)等得到的数据为字节型:(ds)、(ax)、(bx)等得到的数据为字型。
(al)=(20000H),则(20000H)得到的数据为字节型:(ax)=(20000H),则(20000H)得到的数据为字型。
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等,它们都是非法指令。

5.1 [BX]
看一看下面指令的功能
mov ax,[bx]
功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中。即:(ax)=((ds)*16+(bx))。
mov [bx],ax
功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将as中的数据送入内存SA:EA处。即:((ds)*16+(bx))=(ax)。
问题5.1
程序和内存中的情况如图5.1所示,写出程序执行后,21000H~21007H单元中的内容。

《汇编语言》第5章 [BX]和loop指令_第1张图片

 解析:
mov ax,2000H  ;ax=2000H
mov ds,ax         ;ds=2000H
mov bx,1000H  ;bx=1000H
mov ax,[bx]       ;把ds:[bx]的内容传给ax, 即2000:1000(21000)=>ax=00BE,21000:BE,21001:00
inc bx                ;bx=1001
inc bx                ;bx=1002
mov [bx],ax       ;把ax的内容传给ds:[bx]地址上即2000:1002(21002) => 00 BE,21002:BE,21003:00
inc bx                ;bx=1003
inc bx                ;bx=1004
mov [bx],ax      ;把ax的内容传给ds:[bx]地址上即2000:1004(21004) => 00 BE,21004:BE,21005:00
inc bx               ;bx=1005
mov [bx],al       ;把al(ax的低8位:BE)的内容传给ds:[bx]地址上即2000:1005(21005) => BE
inc bx               ;bx=1006
mov [bx],al       ;把al(ax的低8位:BE)的内容传给ds:[bx]地址上即2000:1006(21006) => BE

实验验证:

初始化21000~21007的内容,把2000:1000这个地址设置为BE,其它为00

《汇编语言》第5章 [BX]和loop指令_第2张图片

 查看汇编代码

《汇编语言》第5章 [BX]和loop指令_第3张图片

 执行指令

《汇编语言》第5章 [BX]和loop指令_第4张图片

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

 《汇编语言》第5章 [BX]和loop指令_第6张图片

 《汇编语言》第5章 [BX]和loop指令_第7张图片

 《汇编语言》第5章 [BX]和loop指令_第8张图片

 从以上实验结果验证解析正确

5.2 Loop指令
loop指令的格式是:loop标号,CPU执行loop指令的时候,要进行两步操作,⑴ (cx)=(cx)-1; ⑵判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
从上面的描述中,可以看到,cx中的值影响着loop指令的执行结果。通常(注意,我们说的是通常)我们用loop指令来实现循环功能,cx中存放循环次数。
这里讲解loop指令的功能,关于loop指令如何实现转至标号处的细节,将在后面的课程中讲解。下面我们通过一个程序来看一下loop指令的具体应用。
任务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:编程计算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
      
任务3:编程计算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 cs: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来简化我们的程序。
程序 5.1

assume cs:codesg
codesg segment
    mov ax,2
	mov cx,11
s:	add ax,ax
	loop s
	
	mov ax,4c00H
	int 21H
codesg ends
end

下面分析一下程序5.1
(1)标号
在汇编语言中,标号代表一个地址,程序5.1中有一个标号s。它实际上标识了一个地址,这个地址处有一条指令:add ax,ax。
(2)loop s
CPU执行loop s的时候,要进行两步操作:
⑴ (cx)=(cx)-1;
⑵ 判断cx中的值,不为0则转至标号s所标识的地址处执行(这里的指令是add ax,ax),如果为零则执行下一条指令(下一条指令是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的执行次数。

用cx和loop指令相配合实现循环功能的3个要点:
(1)在cx中存放循环次数;
(2)loop指令中的标号所标识地址要在前面;
(3)要循环执行的程序段,要写在标号和loop指令的中间。
用cx和loop指令相配合实现循环功能的程序框架如下。
    mov cx,循环次数
s:
    循环执行的程序段
    loop s

问题5.2
编程,用加法计算123*236,结果存ax中。思考后看分析。
分析:
可用循环完成,将123加236次,可先设(ax)=0,然后循环做236次(ax)=(ax)+123。
程序如下

assume cs:codesg
codesg segment
    mov ax,0
	mov cx,236
s:	add ax,123
	loop s
	
	mov ax,4c00H
	int 21H
codesg ends
end

问题5.3
改进程序5.2,提高123*236的计算速度。思考后看分析。
解析:
程序5.2做了236次加法,我们可以将236加123次。可先设(ax)=0,然后循环做123次(ax)=(ax)+236,这样可以用123次加法实现相同的功能。

5.3 在debug中跟踪用loop指令实现的循环程序
考虑这样一个问题,计算ffff:0006单元中的数乘以3,结果丰储在dx中。
我们分析一下。
(1)运行后的结果是否会超出dx所能存储的范围?
ffff:00006单元中的数是一个字切型的数据,范围在0~255之间,则用它和3相乘结果不会大于65535,可以在dx中存放下。
(2)用循环累加来实现乘法,用哪个寄存器进行累加?
将ffff:0006单元中的数赋值给ax,用dx进行累加,先设(dx)=0,然后做3次(dx)=(dx)+(ax)。
(3)ffff:6单元是一个字节单元,ax是一个16位寄存器,数据的长度不一样,如何赋值?
注意,我们说的是“赋值”,就是说,让ax中的数据的值(数据的大小)和ffff:0006单元中的数据的值(数据的大小)相等。8位数据01H和16位数据0001H的数据工度不一样,但它们的值是相等的。
那么我们如何赋值?设ffff:0006单元中的数据是XXH,若要ax中的值和ffff:0006单元向ax赋值,应该令(ah)=0,(al)=(ffff6H)。
想清楚以上的3个问题之后,编写编程如下。
程序5.3
assume cs:codesg
codesg segment
    mov ax,0ffffh
    mov ds,ax
    mov bx,6        ;以上,设置ds:bx指向ffff:6
    mov al,[bx]
    mov ah,0        ;以上,设置(al)=((ds*16)+(bx)), (ah)=0
    mov dx,0        ;累加寄存器清0
    mov cx,3        ;循环3次
s:    add dx,ax
    loop s            ;以上累加计算(ax)*3
    
    mov ax,4c00h
    int 21h            ;程序返回
codesg ends
end

注意程序中的第一条指令mov ax,0ffffh。我们知道,大于9FFFh的十六进制数据A000H、A001H...C000H、C001H...FFFEH、FFFFH等,在书写的时候都是以母开头的。而在汇编源程序中,数据不能以字母开头,所以要在前面加0。比如,9138H在汇编源程序中可以直接写为“9138H”,而A000H在汇编源程序中要写为“0A000H”。
下面我们对程序的执行过程进行跟踪。
用debug加载t5_3.exe后,用r命令查看寄存器中的内容,如下图所示。

 表示这个程序机器码长度为27个字节。

上图中(ds)=075AH,所以,程序在076A:0(程序段前缀点256个字节即100H)。我们看一下,(cs)=076AH,(IP)=0,CS:IP正指向程序的第一条指令。再用u命令看一下被debug加载入内存的程序。如下图所示。

《汇编语言》第5章 [BX]和loop指令_第9张图片

 可以看到,从076A:0000~076A:001A是我们的程序,076A:0014处是源程序中的指令loop s,只是此处loop s中的示号s已经变为一个地址0012H。如果在执行“loop 0012”时,cx减1后不为0,“loop 0012”就把IP设置为0012H,从而使用CS:IP指向076A:0012处的add dx,ax,实现跳转。我拉开始跟踪。

《汇编语言》第5章 [BX]和loop指令_第10张图片

 上图中,前3条指令执行后,(ds)=ffffh,(bx)=6,ds:bx指向ffff:6单元。debug显示出当前要执行的指令"mov al,[bx]",因为是读取内存的指令,所以debug将要访问的内存单元中的内容也显示出来,可以看到屏幕最右边显示的“ds:0006=31”,由此,我们可以方便地知道目标单元(ffff6)中的内容为31H。
继续执行:

《汇编语言》第5章 [BX]和loop指令_第11张图片

上图这两条指令执行后,(ax)=0031h,完成了从ffff:6单元向ax的赋值。继续:

《汇编语言》第5章 [BX]和loop指令_第12张图片

上图这两指令执行后,(dx)=0,完成对累加寄存器的初始化:(cx)=3,完成对循环计数寄存器的初始化,下面,将开始循环程序段的执行。我们继续:

《汇编语言》第5章 [BX]和loop指令_第13张图片

 上图中,CPU执行076A:0012处的指令“add dx,ax”后,(IP)=0014H,CS:IP指向076A:0014处的指令“loop 0012”。CPU执行“loop 0012”,第一步先将(cx)减1,(cs)=2;第二步因(cx)不等于0.将IP设为0012H,指令“loop 0012”执行后,(IP)=0012H,CS:IP再次指向076A:0012处的指令“add dx,ax”,这条指令将再次得到执行。注意,“loop 0012”执行后(cx)=2,也就是说,“loop 0012”还可以进行两次循环。
接着,将重复执行"add dx,ax"和"loop 0012",直到(cx)=0为止,如下图所示。

《汇编语言》第5章 [BX]和loop指令_第14张图片

 上图中,最后一次执行“loop 0012”的结果。执行前(cx)=1,CPU执行“loop 0012”,第一步,(cx)=(cx)-1,(cx)=0; 第二步,因为(cx)=0,所以loop指令不跳转,(IP)=0016H,CPU向下执行076A:0016处的指令“mov ax,4c00”。
在完成最后一次“add dx,ax”后,(dx)=93H,此时dx中为累加计算(ax)*3的最后结果。
我们继续,将程序执行完。如下图所示。

《汇编语言》第5章 [BX]和loop指令_第15张图片

上图中,执行完最后两条指令后,程序返回到debug中,注意"int 21"要用p命令执行。

上面,我们通过对一个循环程序的跟踪,更深入一步地计解了loop指令实现循环的原理。下面,我们将程序5.3改一下,计算ffff:0006单元中的数乘以123,结果存储在dx中。只需将循环的次数改为123就可以了,程序如下:

assume cs:codesg
codesg segment
    mov ax,0ffffh
	mov ds,ax
	mov bx,6
	mov al,[bx]
	mov ah,0
	mov dx,0
	mov cx,123
s:	add dx,ax
	loop s
	
	mov ax,4c00H
	int 21H
codesg ends
end

我们用debug对这个程序的循环程序段进行跟踪,标号s前的7要指令,我们已经调试过逻辑上不有问题,这里只跟踪循环过程,所以希望一次执行完标号s前的指倒序。可以用一个新的debug命令g来达到目的。
下面来看实际的操作,生成的最终可执行文件为T5_4.exe。用debug加载,看内存情况。

《汇编语言》第5章 [BX]和loop指令_第16张图片

 循环程序段从CS:0012开始,CS:0012前面的指令,一次执行完,然后从CS:0012处开始跟踪,可以使用g命令,“g 0012”,它表示执行程序到当前代码段(段地址在CS中)的0012H处。也就是说“g 0012”将使debug从当前的CS:IP指向的指令执行,一下到(IO)=0012H为止。具体情况如下:

 debug执行“g 0012”后,CS:0012前的程序段被执行,从各个相关的寄存器中的值,我们可以看出执行的结果。下面我们对循环的地程进行跟踪。如下图所示。

《汇编语言》第5章 [BX]和loop指令_第17张图片

 上图中,我们跟踪了两次循环的过程。其实,通过这两次循环过程,已经可以确定循环程序段的逻辑上是正确的。如果再这种使用t命令执行,那得进行121((cx)=79H)次循环,执行121*2=242次t命令才能从循环中出来。在这里,我们希望将循环一次执行完,可以使用p命令来达到目的。再次遇到loop指令时,使用p命令来执行,debug就会自动重复执行循环中的指令,直到(cx)=0为止,具体情况如下图所示。

《汇编语言》第5章 [BX]和loop指令_第18张图片

 上图中,在遇到“loop 0012”时,用p命令执行,debug自动重复执行“loop 0012”和“add dx,ax”两条指令,直到(cx)=0。最后一次执行“loop 0012”后,(cx)=0,(IP)=0016H,当前指令为CS:0016处的“mov ax,4c00”。
当然,也可以用g命令来达到目的,可以用"g 0016"直接执行CS:0016处。具体情况如下所示。

《汇编语言》第5章 [BX]和loop指令_第19张图片

5.4 debug和汇编编译器masm对指令的不同处理
本节知道点为下面课程的顺利进行提供一点预备知识。
我们在debug中写过类似的指令:
mov ax,[0]
表示将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中编程实现:
mov ax,2000
mov ds,ax
mov al,[0]
mov bl,[1]
mov cl,[2]
mov dl,[3]
(2)汇编源程序实现:

assume cs:codesg
codesg segment
    mov ax,2000H
    mov ds,ax
    mov al,[0]
    mov bl,[1]
    mov cl,[2]
    mov dl,[3]
    mov ax,4c00H
    int 21H
codesg ends
end


我们看一下两种实现的实际实施情况:
(1)debug中的情况如下所示:

《汇编语言》第5章 [BX]和loop指令_第20张图片

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

《汇编语言》第5章 [BX]和loop指令_第21张图片

从上面的图中可以看出,debug和编译器masm对形如"mov ax,[0]"这类指令在解释上的不同。我们在debug中和源程序中写入同样形式的指令:“mov al,[0]”, “mov bl,[1]”, “mov cl,[2]”, "mov cl,[3]",但debug和编译器对之些指令中的“[idata]”却有不同的解释。debug将它解释为“[idata]”是一个内存单元,“idata”是内存单元的偏移地址:而编译器将“[idata]”解释为“idata”。
那么我们如何在源程序中实现将内存2000:0,2000:1,2000:2,2000:3单元中的数据送入al,bl,cl,dl中呢?
目前的方法是,可将偏移地址送入bx寄存器中,用[bx]的方式来访问内存单元。比如我们可以这样访问2000:0单元;
mov ax,2000H
mov ds,ax        ;段地址2000H送入ds        
mov bx,0        ;偏移地址0送入bx
mov al,[bx]        ;ds:bx单元中的数据送入al
这样做是可以,可是比较麻烦,我们要用bx来间接地给出内存单元的偏移地址。我们还是希望能够像在debug中那样,在“[]”中直接给出内存单元的偏移地址。这样做,在汇编源程序中也是可以的,只不过,要在"[]"前面显式地给出段地址所在的段寄存器。比如我们可以这样访问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中。
我们还是先分析一下。
(1)运算后的结果是否会超出dx所能存储的范围?
ffff:o~ffff:b内存单元中的数据是字节型数据,范围在0~225之间,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中?
从上面的分析中,可以看到,这里面有两个问题:类型的匹配和结果的不超界。具体来说,就是在做加固法的时候,我们有两种方法;
⑴ (dx)=(dx)+内存中的8位数据;
⑵ (dl)=(dx)+内存中的8位数据。
第一种方法中的问题是两个运行对象的类型不匹配,第二种方法中的问题是结果有可能超界。
怎样解决这两个看似矛盾的问题?目前的方法(在后面的课程中我们还有别的方法)就是得用一个16位寄存器来做中介,将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界。
想清楚以上的问题之后,编写程序如下。

assume cs:codesg
codesg segment
	mov ax,0ffffh
	mov ds,ax		;设置(ds)=ffffh
	
	mov dx,0		;初始化累加寄存器,(dx)=0
	
	mov al,ds:[0]
	mov ah,0		;(ax)=((ds)*16+0)=(ffff0h)
	add dx,ax		;向dx中加上ffff:0单元的数值
	
	mov al,ds:[1]
	mov ah,0		;(ax)=((ds)*16+1)=(ffff1h)
	add dx,ax		;向dx中加上ffff:1单元的数值
	
	mov al,ds:[2]
	mov ah,0		;(ax)=((ds)*16+2)=(ffff2h)
	add dx,ax		;向dx中加上ffff:2单元的数值

	mov al,ds:[3]
	mov ah,0		;(ax)=((ds)*16+3)=(ffff3h)
	add dx,ax		;向dx中加上ffff:3单元的数值

	mov al,ds:[4]
	mov ah,0		;(ax)=((ds)*16+4)=(ffff4h)
	add dx,ax		;向dx中加上ffff:4单元的数值

	mov al,ds:[5]
	mov ah,0		;(ax)=((ds)*16+5)=(ffff5h)
	add dx,ax		;向dx中加上ffff:5单元的数值

	mov al,ds:[6]
	mov ah,0		;(ax)=((ds)*16+6)=(ffff6h)
	add dx,ax		;向dx中加上ffff:6单元的数值

	mov al,ds:[7]
	mov ah,0		;(ax)=((ds)*16+7)=(ffff7h)
	add dx,ax		;向dx中加上ffff:7单元的数值

	mov al,ds:[8]
	mov ah,0		;(ax)=((ds)*16+8)=(ffff8h)
	add dx,ax		;向dx中加上ffff:8单元的数值

	mov al,ds:[9]
	mov ah,0		;(ax)=((ds)*16+9)=(ffff9h)
	add dx,ax		;向dx中加上ffff:9单元的数值

	mov al,ds:[a]
	mov ah,0		;(ax)=((ds)*16+a)=(ffffah)
	add dx,ax		;向dx中加上ffff:a单元的数值

	mov al,ds:[b]
	mov ah,0		;(ax)=((ds)*16+b)=(ffffbh)
	add dx,ax		;向dx中加上ffff:b单元的数值

	mov ax,4c00H	;程序返回
	int 21h
codesg ends
end

上面的程序很简单,不用解释,你一看就性。不过,在看懂了之后,你是否觉得这个程序编得有些问题?它似乎没有必要写这么长。就是累加ffff:0~ffff:b中的12个数据,如果要累加0000:0~0000:7fff中的32KB个数据,按照这个程序的思路,将要写将近10万行程序。

问题5.4
应用loop指令,改进程序5.5,使它的指令行数让人能够接受。
解析:
可以看出,在程序中,有12个相似的程序段,我们将它们一般化地描述为:
mov al,ds:[X]    ;ds:X指向ffff:X单元
mov ah,0        ;(ax)=((ds)*16+(X))=(ffffXh)
add dx,ax         ;向dx中加上ffff:X单元的数值
我们可以看到,12个相似的程序段中,只有mov al,ds:[X]指令中的内存单元的偏移地址是不同的,其他都一样。而这些不同的偏移地址是在0≤X≤bH的范围内递增变化的。
从程序实现上,我们将循环做。
(al)=((ds)*16+X)
(ah)=0
(dx)=(dx)+(ax)
一共循环12次,在循环开始前(ds)=ffffh,X=0,ds:X指向第一个内存单元。每次循环后,X递增,ds:X指向下一个内存单元。
完整的算法描述如下。
初始化:
(ds)=ffffh
X=0
(dx)=0
循环12次:
(al0=((ds)*16+X)
(ah)=0
(dx)=(dx)+(ax)
X=X+1

可见,表示内存单元偏移地址的X应该是一个变量,因为在循环的过程中,偏移地址必须能够递增。这样,在指令中,我们就不能用常量来表示偏移地址。我们可以将偏移地址放到bx中,用[bx]的方式访问内存单元。在循环开始前设(bx)=0,每次循环,将bx中的内容加1即可。
最后一个问题是,如何实现循环12次?我们的loop指令该发挥作用了。
更详细的算法描述如下。
初始化:
(ds)=ffffh
(bx)=0
(dx)=0
(cx)=12
循环12次:
s:(al)=((ds)*16+(bx))
    (ah)=0
    (dx)=(dx)+(ax)
    (bx)=(bx)+1
    loop s
最后,我们写出程序。
程序5.6

assume cs:codesg
codesg 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
	add dx,ax		;间接向dx中加上((ds)*16+(bx))单元的数值
	inc bx			;ds:bx指向下一个单元
	loop s
	
	mov ax,4c00H
	int 21H
codesg ends
end

在实际编程中,经常会遇到,用同一种方法处理地址连接的内存单元中的数据的问题。我们需要用循环来解决这类问题,同时我们必须能够在每次循环的时候按照同一种方法来改变访问的内存单元的地址。这时,就不能用常量来给出内存单元的地址(比如,[0],[1],[2]中,0,1,2是常量),而应用变量。"mov al,[bx]"中的bx就可以看作一个代表内存单元地址的变量,我们可以不写新的指令,仅通过改变bx中的数值,改变指令访问的内存单元。

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中。
(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:",在汇编语言中称为段前缀。

5.7 一段安全的空间
在8086模式中,随意向一段内存空间写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。比如下面的指令:
mov ax,1000h
mov ds,ax
mov al,0
mov ds:[0],al
我们以前在debug中,为了讲解上的方便,写过类似的指令。但这种做法是不合理的,因为之前我们并淌有论证过1000:0中是否存放着重要的系统数据或代码。如果1000:0中存放着重要的系统数据或代码,“mov ds:[0],al”将其改写,将引发错误。比如下面的程序。
程序5.7

assume cs:codesg
codesg segment

    mov ax,0
	mov ds,ax
	mov ds:[26h],ax
	
	mov ax,4c00H
	int 21H
codesg ends
end

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

《汇编语言》第5章 [BX]和loop指令_第22张图片

在上图中,我们可以看到,源程序中的“mov ds:[26h],ax”被masm翻译为机器码“a3 26 00”,而debug将这个机器码解释为"mov [0026],ax"。可见,汇编源程序中的汇编指令“mov ds:[26H],ax”和debug中的汇编指令“mov [0026],ax”同义。
我们看一下“mov [0026],ax”的执行结果,如下面所示。 

《汇编语言》第5章 [BX]和loop指令_第23张图片

 在DOSBox模拟8086环境程序卡死了。


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

不要忘记,我们是在操作系统的环境中工作,操作系统管理所有的资源,也包括内存。如果我们需要向内存空间写入数据的话,要使用操作系统给我们分配的空间,而不应直接用地址任意指定内存单元,向里面写入。下一章会对“使用操作系统给我们分配的空间”有所认识。
但是,同样不能忘记,我们正在学习的是汇编语言,要通过它来获得底层的编程体验,理解计算机底层的基本工作机理。所以我们尽量直接对硬件编程,而不去理会操作系统。
为什么DOS和其他合法的程序一般都不会使用0:200~0:2ff这段空间?我们将在以后的课程中讨论这个问题。我们总结一下:
(1)我们需要直接向一段内存中写入内容;
(2)这段内容闪空间不应存放系统或其程序的数据或代码,否则写入操作很可能引发错误;
(3)DOS方式下,一般情况,0:@00~0:2ff空间中没有系统或其他程序的数据或代码;
(4)以后,我们需要直接向一段内存中写入内容时,就使用0:200~0:2ff这段空间。

5.8 段前缀的使用
我们考虑一个问题,将内存ffff:0~ffff:b单元中的数据复制到0:200~0:20b单元中。分析一下。
(1)0:200~0:20b单元等同于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用0020:0~0020:B描述,就是为了使目标单元的偏移地址和源始单元的偏移地址从同一数值0开始。
程序如下:

assume cs:codesg
codesg 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
	
codesg ends
end

使用debug调试:

《汇编语言》第5章 [BX]和loop指令_第24张图片

 执行指令

《汇编语言》第5章 [BX]和loop指令_第25张图片

 《汇编语言》第5章 [BX]和loop指令_第26张图片

 《汇编语言》第5章 [BX]和loop指令_第27张图片

因源始单元ffff:X和目标单元0020:X相距大于64KB,在不同的64KB段里,程序5.8中,每次循环要设置两次ds.这样做是正确的,但是效率不高。我们可以使用两个段寄存器分别存放源始单元ffff:X和目标单元0020:X的段地址,这样就可以省略循环中需要重复做12次的设置ds的程序段。
改进的程序如下。
程序5.9

assume cs:codesg
codesg 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		;((es)*16+(bx))=(dl),将dl的数据送入0020:bx
	
	inc bx				;(bx)=(bx)+1
	loop s
	
	mov ax,4c00H
	int 21H
	
codesg ends
end

debug调试

《汇编语言》第5章 [BX]和loop指令_第28张图片

 《汇编语言》第5章 [BX]和loop指令_第29张图片

 《汇编语言》第5章 [BX]和loop指令_第30张图片

 《汇编语言》第5章 [BX]和loop指令_第31张图片

 程序5.9中,使用es存放目标空间0020:0~0020:b的段地址,用ds存放源始空间ffff:0~ffff:b的段地址。在访问内存单元的指令“mov es:[bx],al”中,显式地用段前缀“es:”给出单元的段地址,这样就不必在循环中重得设置ds.

你可能感兴趣的:(汇编语言,汇编语言)