首先定义一下符号:
描述性符号: “()”
为了描述上的简洁,我们使用一个描述性的符号"()"来表示一个寄存器或一个内存单元中的内容,比如:
- (ax)表示ax中的内容、(al)表示al中的内容。
- (20000H)表示内存20000H单元的内容。
- ((ds)*16+(bx))表示ds中的内容为ADR1,bx中的内容为ADR2,内存ADR1*16+ADR2单元的内容。这同样可以理解为,ds中的ADR1作为段地址,bx中的ADR2作为偏移地址,内存ADR1:ADR2单元的内容。
注意"()"中的元素可以有三种类型:寄存器名、段寄存器名、内存单元的物理地址(一个20位数据)。比如:
(ax)、(ds)、(al)、(cx)、(20000H)、((ds)*16+(bx))等是正确的用法。
我们来看下(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)得到的数据为字节型;(ax)=(20000H)得到的数据为字型
符号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等
一、[BX]
[bx]和[0]有些类似,[0]表示段地址在ds中,偏移量为0的内存单元,[bx]同样也表示一个内存单元,它的偏移地址在bx中,比如下面的指令:
mov ax,[bx]
将一个内存单元中的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在ds中。即:(ax)=((ds)*16=(bx))
mov al,[bx]
将一个内存单元的内容送入al,这个内存单元的长度为1字节(字节单元),存放一个字节,偏移地址在bx中,段地址在ds中
二、Loop指令
loop指令的格式是:loop 标号,CPU执行loop指令的时候,要进行两步操作:
- (cx)=(cx)-1
- 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
从上面的描述中,可以看到,cx中的值影响着loop指令的执行结果。通常我们用lool指令来实现循环功能,cx中存放循环次数。
接下来通过一个程序来看下loop指令的具体应用:
任务1:编程计算 2 2 2^2 22,结果存在ax中。
设 ( a x ) = 2 (ax)=2 (ax)=2,可计算 ( a x ) = ( a x ) × 2 (ax)=(ax)\times2 (ax)=(ax)×2,最后 ( a x ) (ax) (ax)中为 2 2 2^2 22的值。 N × 2 N\times2 N×2可用 N + N N+N N+N实现,程序如下:
assume cs:code
code segment
mov ax,2
add ax,ax
mov ax,4c00h
int 21h
code ends
end
但是按照上面的算法,计算 2 12 2^{12} 212需要11条重复的指令 add ax,ax。这种情况下,我们就可以用loop来简化我们的程序。
程序5.1
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
接下来分析下程序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)以下三条指令
mov cx,11
s: add ax,ax
loop s
执行loop s时,首先要将(cx)减一,然后若(cx)不为0,则向前转至s处执行add ax,ax。所以,可以利用cx来控制add ax,ax的执行次数。
用cs和loop指令相配合实现循环功能的3个要点:
- 在cx中存放循环次数
- loop指令中的标号所标识地址要在前面
- 要循环执行的程序段,要写在标号和loop指令的中间。
用cx和loop指令相配合实现循环功能的程序框架如下:
mov cx,循环次数
s:
循环执行的程序段
loop s
五、loop和[bx]的联合应用
考虑这样一个问题,计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
分析下可能出现的问题:
- 运算后的结果是否会超出dx所能存储的范围?
ffff:0~ffff:b内存单元中的数据是字节型数据,范围在0~255之间,12个这样的数据相加,结果不会大于65535,可以在dx中存放下 - 能否将ffff:0~fff:b中的数据直接累加到dx中?
不可以,因为ffff:0~ffff:b中的数据都是8位的,不能直接加到16位寄存器dx中 - 能否将ffff:0~ffff:b中的数据累加到dl中,并设置(dh)=0,从而实现累加到dx中?
这也不行,因为dl是8位寄存器,能容纳的数据的范围在0~255之间,ffff:0~ffff:b中的数据也都是8位,如果仅向dl中累加12个8位数据,有可能造成进位丢失
从上面的分析中,可以看出两个主要问题:类型的匹配和结果的不越界。解决的方法就是用一个16位寄存器来做中介,将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使得两个运算对象的类型匹配并且结果不会越界。
程序如下:
程序5.5
assume cs:code
code segment
mov ax,ffffh
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:[0bh]
mov ah,0 //(ax)=((ds)*16+0bh)=(ffff0bh)
add dx,ax //向dx中加上ffff:0bh单元的数值
mov ax,4c00h //程序返回
int 21h
code ends
end
上面的程序很简单,但是存在大量重复。所以可以使用loop来改进上面的程序:
程序5.6
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
add dx,ax //间接向dx中加上((ds)*16+(bx))单元的数值
inc bx //ds:bx指向下一个单元
loop s
mov ax,4c00h
int 21h
code ends
end
六、段前缀
我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。比如:
- mov ax,ds:[bx]
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在ds中。 - mov ax,cs:[bx]
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在cs中 - mov ax,ss:[bx]
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在ss中 - mov ax,es:[bx]
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在es中
这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的"ds:", “cs:”, “ss:”, “es:”,在汇编语言中称为段前缀。
七、段前缀的使用
考虑一个问题,将内存ffff:0~ffff:b单元中的数据复制到0:200~0:20b单元中。
程序如下:
程序5.8
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段里,程序5.8中,每次循环要设置两次ds。这样做是正确的,但是效率不高。我们可以使用两个段寄存器分别存放源始单元ffff:X和目标单元0020:X的段地址,这样就可以省略循环中需要重复做12次的设置ds的程序段。
改进的程序如下:
程序5.9:
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
程序5.9中,使用es存放目标空间0020:0~0020:b的段地址,用ds存放源始空间ffff:0~ffff:b的段地址。在访问内存单元的指令"mov es:[bx],al"中,显式地用段前缀"es:"给出单元的段地址,这样就不必在循环中重复设置ds。