汇编语言
[BX]和loop指令
-
[bx]和内存单元的概述
- [bx]是什么?
- 和[0]有些类似,[0]表示内存单元,它的偏移地址是0。
- 我们要完整地描述一个内存单元,需要两种信息:
- 内存单元的地址;
- 内存单元的长度(类型)
- 我们用[0]表示一个内存单元时,0表示单元的偏移地址,段地址默认在ds中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出,如前边的AX,AL。
- [bx]同样也表示一个内存单元,它的偏移地址在bx中,比如下面的指令:
- mov ax,[bx]
- mov al,[bx]
- [bx]是什么?
-
loop
- 英文单词loop有循环的含义,显然这个指令和循环有关。
- 描述性符号“()”
- 为了描述上的简洁,在以后的课程中,我们将使用一个描述性的符号“()”来表示一个寄存器或一个内存单元中的内容
- 我们看一下(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
- 约定符号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]指令的功能
-
mov ax,[bx]
-
功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中。即:
(ax)=((ds)*16+(bx));
-
-
loop指令
-
指令的格式是:loop标号,CPU执行loop指令的时候,要进行两步操作:
- (1) (cx)=(cx)-1;
- (2) 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
- 从上面的描述中,我们可以看到,cx中的值影响着loop指令的执行结果。
- 通常(注意,我们说的是通常)我们用loop指令来实现循环功能,cx中存放循环次数。
-
举例:编程计算2^12
-
程序分析:
-
标号
在汇编语言中,标号代表一个地址,此程序中有一个标号s。它实际上标识了一个地址,这个地址处有一条指令:add ax,ax
-
loop s
CPU执行 loop s的时候,要进行两步操作:
- (1) (cx)=(cx)-1;
- (2) 判断cx中的值,不为零则转至标号s所表示的地址处执行程序(这里的指令是add ax,ax),如果为零则执行下一条指令(下一条指令是mov ax,4c00H)。
-
以下三条指令
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
-
注意:
- 我们说的是”赋值“,就是说,让ax中的数据的值(数据的大小)和ffff:0006单元中的数据的值(数据的大小)相等。
- 8位数据01H和16位数据0001H的数据长度不一样,但他们的值是相等的。
-
示例任务:将内存2000:0、2000:1、2000:2、2000:3单元中的数据送入al,bl,cl,dl中
- 在Debug中编程实现
- 汇编程序实现
在MASM中mov ax,[2]是解释为mov ax,2的。一般我们是通过BX来代替,像这道题我们先mov bx,2再通过mov ax,[bx]来实现
我们要像DEBUG一样直接用[2]也可以,不过要加上段地址。像下面这样:
-
-
loop和[bx]的联合应用
-
计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中
-
ffff:0ffff:b内存单元中的数据是字节型数据,范围在0255之间,12个这样的数据相加,结果不会大于65535,可以在dx中存放下
-
我们是否将ffff:0~ffff:b中的数据直接累加到dx中?
当然不行,因为ffff:0~ffff:b中的数据是8位的,不能直接加到16位寄存器dx中
-
我们能否将ffff:0~ffff:b中的数据累加到dl中,并设置dh=0,从而实现累加到dx中的目标?
这也不行,因为dl是8位寄存器,能容纳的数据的范围在0255之间,ffff:0ffff:b中的数据也都是8位,能容纳的数据的范围也是在0~255之间。如果仅向dl中累加12个8位数据,很有可能造成进位丢失。
-
从上面的分析我们可以看到,这里有两个问题:类型的匹配和结果的不超界
-
具体的说、就是在做加法的时候,我们有两种方法:
- (dx)=(dx)+内存中的8位数据;
- (dl)=(dl)+内存中的8位数据;
- 第一种方法中的问题是两个运算对象的类型不匹配,第二种方法中的问题是结果有可能超界。
-
怎样解决这两个看似矛盾的问题?
-
目前的方法(在后面的课程我们还有别的方法)就是我们得用一个16位寄存器来做中介。
-
我们将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx中,从而使两个运算对象的类型匹配并且结果不会超界。
-
我们这样做太麻烦了,我们就可以应用loop指令,改进这个程序,使它的指令行数可以让人接受
我们可以看出,在程序中,有12个相似的程序段,我们将它们一般化地描述为:
mov al,ds:[x]
mov ah,0
add dx,ax
-
我们可以看到12个相似的程序段中,只有mov al,ds:[x]指令中的内存单元的偏移地址是不同的,其他都一样。
而这些不同的偏移地址是可在0≤X≤0bH的范围内递增变化的
-
从程序实现上,我们将循环做:
- (al)=((ds)*16+X)
- (ah)=0
- (dx)=(dx)+(ax)
- 一共循环12次,在循环开始前(dx)=0FFFFH,X=0,ds:X指向第一个内存单元。每次循环后,X递增,ds:X指向下一个内存单元。
-
完整的算法描述
-
初始化:
(ds)=0ffffh
X=0
(dx)=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
-
在实际的编程中,我们经常会遇到,用同一种方法处理地址连续的内存单元中的数据的问题。
我们需要用循环来解决这类问题,,同时我们必须能够在每次循环的时候按照同一种方法来改变要访问的内存单元的地址。
这时,我们不能用常量来给出内存单元的地址(比如[0],[1],[2]中,0,1,2是常量),而应用变量
“mov al,[bx]”中的bx就可以看作一个代表内存单元地址的变量,我们可以不写新的指令,仅通过改变bx中的数值,改变指令访问的内存单元。
-
-
-
-
段前缀
- 指令“mov ax,[bx]”中,内存单元的偏移地址由bx给出,而段地址默认在ds中
- 我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。
- 这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:” ,“cs:”,“ss:”或“es:”,在汇编语言中称为段前缀
-
一段安全的空间
-
在8086模式中,随意向一段内存空间写入内容是很危险的,因为这段空间中可能存放着很重要的系统数据或代码。
-
比如下面的指令:
mov ax,1000h
mov ds,ax
mov al,0
mov ds:[0],al
-
我们以前的Debug中,为了讲解上的方便,写过类似的指令。但这种做法是不合理的,因为之前我们并没有论证过1000:0中是否存放着重要的系统数据或代码。如果1000:0中存放着重要的系统数据或代码,“mov ds:[0],al”将其改写,将引发错误。
-
我们在不能确定一段内存空间中是否存放着重要的数据或代码的时候,不能随意向其中写入内容。
-
不要忘记,我们是在操作系统的环境中工作,操作系统管理的所有资源,也包括内存。
-
同样不能忘记,我们正在学习的是汇编语言,要通过它来获得底层的编程体验,理解计算机底层的基本工作机理。
所以我们尽量直接对硬件编程,而不去理会操作系统。
-
我们似乎面临一种选择,是在操作系统中安全,规矩地编程,还是自由,直接地用汇编语言去操作真实的硬件,了解那些早已被层层系统软件掩盖的真相?
在大部分情况下,我们选择后者,除非我们是在学习操作系统本身的内容。
-
注意:
我们在纯DOS方式(实模式)下,可以不理会DOS,直接用回避那语言去操作真实的硬件,因为运行在CPU实模式下的DOS,没有能力对硬件系统进行全面,严格地管理。
但在Windows XP\2000、UNIX这些运行于CPU保护模式下的操作系统中,不理会操作系统,用汇编语言去操作真实的硬件,是根本不可能的。
硬件已被这些操作系统利用CPU保护模式所提供的功能全面而严格地管理了。
-
所以我们要找到一段安全的空间供我们使用。
在一般的PC机中,DOS方式下,DOS和其他合法的程序一般都不会使用**0:2000:2FF(0:200h0:2FFh)**的256个字节的空间。所以,我们使用这段空间是安全的。
-
不过为了谨慎起见,在进入DOS后,我们可以先用Debug查看一下,如果0:200~0:2FF单元的内容都是0的话,则证明DOS和其他合法的程序没有使用这里。
-
-
段前缀的使用
-
我们考虑一个问题:将内存ffff:0ffff:b段元中的数据拷贝到0:2000:20b单元中
-
拷贝的过程应用循环实现,简要描述如下:
初始化:X=0
循环12次
将ffff:X单元中的数据送入0020:X(需要用一个寄存器中转)
X=X+1
-
在循环中源单元fff:X和目标单元0020:X的偏移地址X是变量。我们用bx来存放。
-
我们用将0:2000:20b用0020:00020:b描述,就是为了使目标单元的偏移地址和源单元的偏移地址从同一数值0开始。
-
因源单元ffff:X和目标单元0020:X相距大于64KB,在不同的64KB段里,程序中,每次循环要设置两次ds。
这样做是正确的,但是效率不高。
-
我们可以使用两个段寄存器分别存放源单元和目标单元的段地址,这样就可以省略循环中需要重复做12次的设置ds的程序段。
-
-