必读
[bx]和内存单元的描述
[bx]是什么?和[0]类似,[0]表示偏移地址为0的内存单元,就比如下面的指令
mov ax,[0]
将一个内存单元的内容送入ax
,内存单元的长度为2字节(1个字单元),该内存单元的段地址为ds
,偏移地址为0.
将一个内存单元的内容送入al
,这个内存单元的长度为1字节,存放一个字节,偏移地址为0,段地址在ds中
要完整描述一个内存单元,需要两种信息:
- 内存单元的地址
- 内存单元的长度类型
用[0]
表示一个内存单元时,0
表示单元的偏移地址,段地址默认在ds中,单元的长度可以由具体指令中的其他操作对象(比如寄存器)指出来。
[bx]
同样表示一个内存单元,它的偏移地址在bx
中,比如下面的指令
; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移
; 地址在bx中,段地址在ds中
mov al,[bx]
; 将一个内存单元的内容送入al,这个内存单元的长度为1字节,存放一个字节,偏移地址在bx中
; 段地址在ds中
mov al,[bx]
loop
loop
指令和循环有关
定义的描述性的符号:**()**
为了描述上的简洁,使用一个描述性的符号()
来表示一个寄存器或一个内存单元中的内容,比如
(ax)
:表示ax
中的内容(al)
:表示al
中的内容(20000H)
:表示内存20000H单元的内容((ds)*16+(bx))
:表示ds中的内容为ADR1,bx中的内容为ADR2,内存ADR1x16+ADR2
单元的内容
约定符号**idata**
表示常量
之前写过类似的指令:mov ax,[0]
,表示将ds:0处的数据送入ax中,指令中[ ]
中用一个常量0表示内存单元的偏移地址,以后就使用idata
来表示这个偏移地址
mov ax,[idata] ; 代表mov ax,[1] mov ax,[2]等
mov bx, idata ; 代表mov bx,1 mov bx,2等
一、[BX]
下面指令的功能
mov ax,[bx]
功能:bx中存放的的数据作为一个偏移地址EA,段地址SA默认在DS中,将SA:EA处的数据送入ax中。即(ax)=((ds)*16+(bx))
mov [bx],ax
功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在DS中,将ax中的数据送入内存SA:EA处,即((ds)*16+(bx))=(ax)
建议:书上的题5.1非常适合巩固
二、loop指令
loop
指令的格式是:loop 标号
,CPU执行到loop
指令的时候,要进行两步操作
(cx)=(cx)-1
- 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行
2.1 任务1
编程计算,结果存在ax中。
分析:设(ax)=2
,可计算(ax)=(ax)*2
,最后(ax)
中为的值,可用实现,程序如下。
assume cs:code
code segment
mov ax,2
add ax,ax
mov ax,4c00h
int 21h
code ends
end
2.2 任务2
编程计算
分析:,若设(ax)=2
,可计算(ax)=(ax)*2*2
,最后(ax)
中的值为的值,可用实现,程序如下
assume cs:code
code segment
mov ax,2
add ax,ax
add ax,ax
mov ax,4c00h
int 21h
code ends
end
2.3 任务3
编程计算
assume cs:code
code segment
mov ax,2
; 需要11个 add ax,ax
mov ax,4c00h
int 21h
code ends
end
写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
,它实际上标识了一个地址,这个地址处有一条指令:add ax,ax
loop s
CPU在执行loop s
的时候,要进行两步操作(cx)=(cx)-1
- 判断
cx
中的值,不为0则转至标号s
所标识的地址处执行(这里的指令是add ax,ax
),如果为0则执行下一条指令(下一条指令是mov ax,4c00h
)
从上面的过程中,我们可以总结出cx
和loop
指令相配合实现循环功能的3个要点:
- 在
cx
中存放循环次数 loop
指令中的标号所标识地址要在前面- 要循环执行的程序段,写在标号和
loop
指令的中间
实现循环功能的框架
mov cx,循环次数
s:
; 循环执行的程序段
loop s
2.4 问题一
用加法计算,结果存放在ax中
assume cs:code
code segment
mov ax,7BH ; 当然这里也可以用123表示
mov cx,0EBH ; 同理,这里也可用236十进制表示
s:add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
注意在写16进制的数据的时候,如果第一个字符是a~f或A~F,应该在前面加个0。
例如,要把eb07h
改成0eb07h
。
三、Debug中跟踪用loop指令实现的循环程序
考虑这样一个问题,计算ffff:0006单元中的数乘以3,结果存储在dx中。
这里有3个问题需要拿出来分析一下
问题1:运算后的结果是否会超过dx所能存储的范围?
ffff:0006单元中存放的是字节型的数据,而一个字节型的数据是8位,数据大小为#card=math&code=0%5Ctext%7B~%7D8%5C%3B%282%5E8%3D256%29&id=QYkRI),而一个dx是16位的字单元,其大小为#card=math&code=0%5Ctext%7B~%7D65535%5C%3B%20%282%5E%7B16%7D%3D65536%29&id=yGIpR),所以完全没问题。
问题2:用循环累加来实现乘法,用哪个寄存器进行累加?
将ffff:0006单元中的数赋值给ax,用dx进行累加。先设(dx)=0
,然后再做3次(dx)=(dx)+(ax)
。
问题3:ffff:6单元是一个字节单元,ax是一个16位寄存器,数据长度不一样,如何赋值?
一个是8位的,一个是16位的,如果想实现ffff:0006
处的单元向ax
赋值,应该令(ah)=0, (al)=(ffff6H)
。
接下来,就开始编写程序
assume cs:code
code 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 ; 程序返回
code ends
end
四、汇编编译器masm对指令的不同处理
assume cs:code
code segment
mov ax,2000H
mov ds,ax
mov ax,2 ; 关键点1
mov ax,[2] ; 关键点2
mov ax,ds:[2] ; 关键点3
mov ax,4c00h
int 21h ; 程序返回
code ends
end
编译、连接后,使用Debug查看编译器masm生成的机器码如下:
这里有一个关键的地方:mov ax,[2]
本应该表示的是ds:2中的数据,但是现在它和mov ax,2
的作用却是一样的。正确的实现方式是3这种写法,mov ax,ds:[2]
,所以不能只用[idata]
来表示一个偏移地址idata
,需要用ds:[idata]
来表示。
五、loop和[bx]的联合应用
考虑这样一个问题,计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中,在写程序之前,需要进行一下分析
问题1:运算后的结果是否会超出dx所能存储的范围?
ffff:0006单元中存放的是字节型的数据,而一个字节型的数据是8位,数据大小为#card=math&code=0%5Ctext%7B~%7D8%5C%3B%282%5E8%3D256%29&id=dcvkd),而一个dx是16位的字单元,其大小为#card=math&code=0%5Ctext%7B~%7D65535%5C%3B%20%282%5E%7B16%7D%3D65536%29&id=SjPJW),所以完全没问题。
问题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位数据,很有可能造成进位丢失。
上面的问题总结下就是在做加法的时候,会有两种方法
(dx)=(dx)+内存中的8位数据
(dl)=(dl)+内存中的8位数据
第一种方法的问题是运算对象的类型不匹配,第二种方法的问题是结果有可能越界。
如何解决上述的看似矛盾的问题?
目前的方法就是用一个16位寄存器来做中介,将内存单元中的8位数据赋值到一个16位寄存器ax
中,再将ax
中的数据加到dx
上,从而使得两个运算对象的类型匹配且不超界。
上述问题想清楚后,就开始编写程序了。
assume cs:codesg
codesg segment
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:[0a]
mov ah,0
add dx,ax
mov al,ds:[0b]
mov ah,0
add dx,ax
mov ax,4c00h
int 21h
codesg ends
end
上述程序太长了,所以需要使用loop
指令帮助它减减肥
减肥思路:
上面程序中,有12个相似的程序段,我们将它们一般化地描述为
mov al,ds:[X]
mov ah,0 ; 将ffff:X单元处的值赋给ax
add dx,ax ; 向dx中加上ffff:X单元的数值
上面的代码段就是循环体中需要执行的操作,另外还有一句话就是inc bx
来对bx寄存器中的数进行自增运算
assume cs:codesg
codesg segment
mov ax,0ffffh
mov ds,ax
mov bx,0
mov dx,0
mov cx,12
s: mov al,[bx]
mov ah,0
add dx,ax
inc bx ; 在这里给bx增加数值
loop s
mov ax,4c00h
int 21h
codesg ends
end
六、段前缀
指令mov ax,[bx]
中,内存单元的偏移地址由bx给出,而段地址默认在ds中,我们可以在访问内存单元的指令中显示地给出内存单元的段地址所在的段寄存器。
这些出现在访问内存单元的指令中,用于显示地指明内存单元的段地址的ds
, cs
, ss
, es
等的就称为段前缀。
七、一段安全的空间
在8086模式中,随意向一段内存空间中写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。比如下面的指令:
mov ax,1000h
mov ds,ax
mov al,0
mov ds:[0],al
这样做的结果就是向1000:0的内存单元中写入数据,但是如果1000:0中存放着重要的系统数据或代码,这句话就会引发错误。
可见,在不确定一段内存空间是否存放着重要的数据或代码的时候,不能随意向其中写入内容。
但是不要忘记,我们是在操作系统的环境中工作,操作系统管理所有的资源,也包括内存。如果我们需要向内存空间中写入数据的话,要使用操作系统分配给我们的空间,而不应该直接使用地址任意指定内存单元。
我们学习的汇编语言,要通过它来获得底层的编程体验,理解计算机底层的基本工作原理。我们要尽量对硬件编程,而不去理会操作系统,但是我们是在Win10的操作系统学习的,这个时候也要遵守操作系统给我们的规矩。
在一般的PC机中,DOS方式下,DOS和其他合法的程序一般都不会使用 0:200~0:2ff (00200h~002ffh)的256自己的空间,所以我们使用这段空间是安全的。不过为了谨慎起见,在进入DOS后,我们可以用Debug查看一下,如果0:200~0:2ff单元的内容都是0的话,则证明DOS和其他合法的程序没有使用这里。
八、段前缀的使用
现在考虑一个问题,将内存ffff:0~ffff:b单元中的数据复制到0:200~0:20b单元中
注意:0:200~0:20b和0020:0~0020:b描述的是同一段内存中的空间
实现上述过程的程序如下
assume cs:code
code segment
mov bx,0 ; 要使用寄存器实现增加的效果
mov cx,12 ; 要循环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段里,程序里要设置两次ds,这样做虽然正确但是效率却不高。我们可以使用两个寄存器分别存放源始单元ffff:X和目标单元0020:X的段地址,这样就省略了要做的12次设置ds的操作了。
assume cs:code
code segment
mov bx,0 ; 要使用寄存器实现增加的效果
mov cx,12 ; 要循环12次
; 源始内存
mov ax,0ffffh
mov ds,ax
; 目标内容
mov ax,0020h
mov es,ax
s:
mov dl,[bx]
mov es:[bx],dl ; 这里指定段寄存器为es而不是默认的ds
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end
实验
实验1
编程,向内存0:200~0:23F依次传送数据0~63(3FH)
; 这里不使用段地址:偏移地址的方式来表示内存单元
; 而是直接使用物理地址来表示
assume cs:code
code segments
mov bx,200h
mov cx,3FH
mov ax,0
s: mov [bx],ax
inc bx
inc ax
loop s
mov ax,4c00h
int 21h
code ends
end
实验2
编程,向内存0:200~0:23F依次传送数据0~63(3FH),程序中只能使用9条指令,9条指令中包括mov ax,4c00h
和int 21h
上面实验1的指令刚好是9条,所以实验1的解答正好符合实验2的要求
实验3
下面的程序的功能是将mov ax,4c00h
之前的指令复制到内存0:200处,补全程序。上机调试,跟踪运行结果。
assume cs:code
code segment
mov ax,cs ; 2个字节
mov ds,ax ; 2个字节
mov ax,0020h ; 3个字节
mov es,ax ; 2个字节
mov bx,0 ; 3个字节
mov cx,17h ; 3个字节
; 有一个隐藏的指令ES,这个指令的大小为1个字节
s: mov al,[bx] ; 2个字节
mov es:[bx],al ; 2个字节
inc bx ; 1个字节
loop s ; 2个字节
mov ax,4c00h
int 21h
code ends
end
- 复制的是什么?从哪里到哪里?
- 复制的是什么?有多少个字节?你如何知道要复制的字节的数量
注意:一定要做完这个实验才能进行下面的课程
实验步骤如下:
- 这道题参考的地址:《汇编语言(王爽)第三版》实验【未完待续】 - !ao!ao - 博客园 (cnblogs.com)
- 寄存器和数据之间传递是占3个字节,寄存器之间传递是两个字节,更多关于字节大小计算,点击这里
首先,查看内存0:200处是否有程序或数据
其次,查看这段程序的大小,下图中的17H
表示这句话之前的指令所占据的大小为17个字节