引言
前面,我们用[0]、[bx]的方法,在访问内存的指令中,定位内存单元的地址。这一章,主要学习一些更灵活的ding定位内存地址的方法和相关的编程方法。
(1)and指令:逻辑与指令,按位进行与运算。
如mov al,01100011B
and al,00100011B
执行后:al=00100011B
and指令的一点功能
通过该指令可将操作对象的xian相应位设为0,其他位不变。
例如:
将al的第6位设为0:and al,10111111B
将al的第7位设为0:and al,01111111B
将al的第0位设为0:and al,11111110B
(2)or指令:逻辑或指令,按位进行或运算。
如 mov al,01100011B
or al,00111011B
执行后:al=01111011B
or指令的一点功能
通过该指令可将操作对象的相应位设为1,其他位不变。
例如:
将al的第6位设为1:or al,01000000B
将al的第7位设为1:or al,10000000B
将al的第0位设为1:or al,00000001B
世界上有很多编码方案,有种方案叫ASCII编码,是在计算机系统中通常被采用的。
简单的说,所谓编码方案,就是一套规则,它约定了用什么样的信息来表示现实对象。
比如说,在ASCII编码方案中,用61H表示“a”,62H表示“b”。
一种规则需要人们遵守才有意义。
一个文本编辑过程中,就包含着按照ASCII编码规则进行的编码和解码。
在文本编辑过程中,我们按一下键盘的a键,就会在屏幕上kan'看到“a”。
过程:
键盘有一个芯片,将a键转化为ASCII码对应的数字61H,将数字送到内存空间,然后文本编辑软件从内存空间中指定读出数据,将它送入显存中。
我们可以在汇编程序中用‘……’的方式指明数据是以字符的形式给出的,编译器将把它们转化为转化为相对应的ASCII码。
例如:
assume ds:data
data segment
db 'unIX'
db 'foRK'
data ends
code segment
start:mov al,'a'
mov bl,'b'
mov ax,4c00h
int 21h
code ends
end start
上面的源程序中:
db 'unIX'相当于“db 75H,6EH,49H,58H”,“u”、“n”、“I”、“X”的ASCII码分别为75H、6EH、49H、58H;
db 'foRK'相当于“db 66H,6FH,52H,4BH”,“f”,“o”,“R”,“K”的ASCII码分别为66H、6FH、52H、4BH;
(ASCII码用1个字节就可以表示不需要定义字型数据)
mov al,'a'相当于mov al,61H a的ASCII码为61H;
mov al,'b'相当于mov al 62H b的ASCII码为62H。
首先分析一下,我们知道同一个字母的大写字符和小写字符对应的ASCII码是不同的,比如“A”的ASCII码是41H,“a”的ASCII码是61H。
要改变一个字母的大小写,实际上就是要改变它所对应的ASCII码。
我们可以将所有的字母的大写字符和小写字符所对应的ASCII码列出来,进行比对,从中找到规律。
大写 二进制 小写 二进制
A 01000001 a 01100001
B 01000010 b 01100010
C 01000011 c 01100011
D 01000100 d 01100100
通过对比可以发现,小写字母的ASCII码值比大写字母的ASCII码值大20H(十进制为32)
这样,我们可以想到,如果将"a"的ASCII码值减去20H,就可以得到“A”;如果将“A”的ASCII码值加上20H就可以得到“a”。
按照这样的方法,我们可以将段中大写字母变成小写,小写字母变成大写。
如果我们只要将大写变为小写,还要考虑让程序能够判断一个字母是大写还是小写
判断将用到一些我们目前还没有学习到的指令。现在面临的问题是,用已学的指令来解决将大写字母改为小写字母或是将小写字母改为大写字母这个问题,则我们不能对字母的大小写进行任何判断。
其实我们可以看出,就ASCII码的二进制形式来看,除了第5位(位数从0开始计算)外,大写字母和小写字母的其他各位都一样。
大写字母ASCII码的第5位(位数从0开始计算)为0,小写字母的第5位为1。
所以,一个字母我们不管它原来是大写还是小写:
我们将它的第5位置0,它就必将变为大写字母;
将它的第5位置1,它就必将变为小写字母。
可以用之前的or和and
问题:在codesg中填写代码,将datasg中的第一个字符串转化为大写,第二个字符串转化为小写。
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'iNfOrMaTiOn'
datasg ends
codesg segment
start:
codesg ends
end start
答案代码:
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'iNfOrMaTiOn'
datasg ends
codesg segment
start :mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s:mov al,[bx]
and al,11011111B
mov [bx],al
inc bx
loop s
mov bx,5
mov cx,11
t:mov al,[bx]
or al,00100000B
mov [bx],al
inc bx
loop t
mov ax,4c00h
int 21h
codesg ends
end start
在前面,我们用[bx]的方式lai'来指明一个内存单元,还可以用一种更为灵活的方式来指明内存单元:[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata(bx中的数值加上idata)。
[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata(bx中的数值加上idata)
我们看一下指令mov ax,[bx+200]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上200,段地址在ds中。
数学化的描述为:(ax)=((ds)*16+(bx)+200)
指令mov ax,[bx+200]也可以写成如下格式(常用):
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200
问题7.1
用Debug查看内存,结果如下:
2000:1000 BE 00 06 00 00 00……
写出下面的程序执行后,ax、bx、cx中的内容。
mov ax,2000H
mov ds,ax
mov bx,1000H
mov ax,[bx]
mov cx,[bx+1]
add cx,[bx+2]
问题7.1分析
mov ax,[bx]
是访问的字单元的段地址在ds,即(ds)=2000H;
偏移地址在bx中,(bx)=1000H
指令执行后(ax)=00BEH
mov cx,[bx+1]
访问的字单元的段地址在ds中,
(ds)=2000H
偏移地址=(bx)+1=1001H;
指令执行后(cx)=0600H。
add cx,[bx+2]
访问的字单元的段地址在ds中,
(ds)=2000H;
偏移地址=(bx)+2=1002H;
指令执行后(cx)=0606H。
有了[bx+idata]这种表示内存单元的方式,我们就可以用更高级的结构来看待所要处理的数据。
问题:
在codesg中填写代码,将datasg中定义的第一个字符串转化为大写,第二个字符串转化为小写。
按照我们原来的方法,用[bx]的方式定位字符串中的字符。
原来代码段的中的程序代码:
现在,我们有了[bx+idata]的方式,就可以用更简化的方法来完成上面的程序。
我们观察datasg段中的两个字符串,一个的起始地址为0,另一个的起始地址为5。
我们可以将这两个字符串看作两个数组,一个从0地址开始存放,另一个从5开始存放。
那么我们可以用[0+bx]和[5+bx]的方式在同一个循环中定位这两个字符串中的字符。
在这里,0和5给定了两个字符串的起始偏移地址,bx中给出了从起始偏移地址开始的相对偏移地址。
这两个字符串在内存中的起始地址是不同的,但是,它们中的每一个字符,从起始地址开始的相对地址的变化是相同的
改进后的程序如下:
程序还可以写成这样:
如果我们用高级语言,比如C语言来描述上面的程序,大概就是这样:
C语言定位字符串中字符的方式:a[i],b[i]
汇编语言定位字符串中字符的方式:0[bx],5[bx]
通过比较,我们可以发现:
[bx+idata]的方式为高级语言实现数组提供了便利机制。
SI和DI是8086CPU中和bx功能相近的寄存器,SI和DI不能够分成两个8为寄存器来使用。
下面的三组指令实现了相同的功能:
(1)mov bx,0
mov ax,[bx]
(2)mov si,0
mov ax,[si]
(3)mov di,0
mov ax,[di]
下面的三组指令也实现了相同的功能:
(1)mov bx,0
mov ax,[bx+123]
(2)mov si,0
mov ax,[si+123]
(3)mov di,0
mov ax,[di+123]
问题7.2
用寄存器SI和DI实现将字符串'welcome to masm!'复制到它后面的数据区中。
assume cs:codesg,ds:datasg
datasg segment
db 'welcome to masm!'
db'………………’
datasg ends
分析:
我们编写程序大都是进行数据的处理而数据在内存中存放,所以我们在处理数据之前首先要搞清楚数据存储在什么地方,也就是说数据的内存地址。
现在我们要对datasg段中的数据进行复制,我们先来看一下要复制的数据在什么地方,datasg:0,这是要进行复制的数据的地址。
应该要复制到它后面的数据区
因为“welcome to masm!"从偏移地址0开始存放,长度为16个字节,所以它后面的数据区的偏移地址为16,就是字符串所要存放的空间。
我们用ds:si指向要复制的源始字符串,用ds:si指向复制的目的空间,然后用一个循环来完成复制。
(SI:Source Index DI:Destination Index)
代码段:
注意:在程序中,我们用16位寄存器进行内存单元之间的数据传送,一次复制2个字节,一共循环8次。
问题7.3
用更少的代码,实现问题7.2中的程序
我们可以利用[bx(si或di)+idata]的方式,来使程序变得简洁。
程序如下:
在前面,我们用[bx(si或di)]和[bx(si或di)+idata]的方式来指明一个内存单元,我们还可以用更灵活的方式:
[bx+si]
[bx+di]
[bx+si]和[bx+di的含义相似,我们以[bx+si]为例
[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si中的数值)。
我们看下指令mov ax,[bx+si]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值,段地址在ds中。
指令mov ax,[bx+si]的数学化的描述为:
(ax)=((ds)*16+(bx)+(si))
该指令也可以写成如下格式(常用):
mov ax,[bx][si]
问题7.4
用Debug查看内存就,结果如下:
2000:1000 BE 00 06 00 00 00…………
写出下面的程序执行后,ax、bx、cx中的内容
分析:
mov ax,[bx+si]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+(si)=1000H;
指令执行后(ax)=00BEH。
mov cx,[bx+si]
访问的字单元的段地址在ds中,(ds)=2000H;
偏移地址=(bx)+(si)=1001H;
指令执行后(cx)=0600H。
经过两次inc si和mov di,si之后,si中的数据为2;
所以add ax,[bx+di]之后
(cx)原来为0600,add cx,[bx+di]之后,(cx)=0606H
[bx+si+idata]和[bx+di+idata]的含义相似,我们以[bx+si+idata]为例进行讲解。
[bx+si+idata]表示一个内存单元。
它的偏移地址为(bx)+(si)+idata。(即bx中的数值加上si中的数值再加上idata)
指令mov ax,[bx+si+idata]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值再加上idata,段地址在ds中。
数学化的描述为:
(ax)=((ds)*16+(bx)+(si)+idata)
指令mov ax,[bx+si+idata]:
该指令也可以写成如下格式:
mov ax,[bx+200+si]
mov ax,[200+bx+si]
mov ax,200[bx][si]
mov ax,[bx].200[si]
mov ax,[bx][si].200
规律:常数在后面的都要加点
问题7.5
用Debug查看内存,结果如下:
2000:1000 BE 00 06 00 6A 22……
写出下面程序执行后,ax、bx、cx中的内容。
分析:
mov ax,[bx+2+si]
访问的字单元的段地址在ds中,(ds)=2000H;偏移地址=(bx)+(si)+2=1002H;指令执行后(ax)=0006H。
mov cx,[bx+2+si]
访问的字单元的段地址在ds中,(ds)=2000H;偏移地址=(bx)+(si)+2=1003H;指令执行后(cx)=6A00H。
mov bx,[bx+2+di]
访问的字单元的段地址在ds中,(ds)=2000H;偏移地址=(bx)+(di)+2=1004H;指令执行后(bx)=226AH。
如果我们比较一下前面用到的几种定位内存地址的方法(可称为寻址方式),就可以发现有yi'x以下几种方式:
(1)[iata]用一个常量来表示地址,可用于直接定位一个内存单元;
(2)[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;
(3)[bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变脸简介定位一个内存单元
(4)[bx+si]用两个变量表示地址;
(5)[bx+si+idata]用两个变量和一个常量表示地址。
可以看到从[idata]一直到[bx+si+idata],我们可以用更加灵活的方式来定位一个内存单元的地址。
这使我们可以从更加结构化的角度来看待所要处理的数据。
问题7.6
编程,将datasg段中每个单词的头一个字母改为大写字母。
个人代码:
assume cs:codesg,ds:datasg
datasg segment
db '1.file '
db '2.edit '
db '3.search '
db '4.view '
db '5.options '
db '6.help '
datasg ends
codesg segment
start :mov ax,datasg
mov ds,ax
mov bx,2
mov cx,6
s:mov al,[bx]
and al,11011111B
mov [bx],al
add bx,16
loop s
mov ax,4c00h
int 21h
codesg ends
end start
课本分析:datasg中的数据的存储结构,如图
我们可以看到:
在datasg中定义了6个字符串,每个长度为16字节。
(注意,为了直观,每个字符串的后面都加上了空格符,以使它们的长度刚好为16字节)
我们需要进行6次循环,用一个变量R定位行,用常量3定位列,处理的过程如下:
BX先存放第一行的地址
mov cx,6;因为总共有六行
s:改变第BX行,第三列的字母为大写改变BX的值是它指向下一行的地址loop
我们用bx作变量,定位每行的起始地址,用3定位要修改的列,用[bx+idata]的方式来对目标单元进行寻址。
问题7.7
编程:将datasg段中的每个单词改为大写字母。
分析:
datasg中数据的存储结构如图:
在datasg中定义了4个字符串,每个长度为16字节。
(注意,为了使我们在Debug中可以直观地查看,每个字符串的后面都加上了空格符,以使它们的长度刚好为16byte)
因为它们是连续存放的,我们可以将这4个字符串看成一个4行16列的二维数组。
按照要求,我们需要修改每一个单词,即二维数组的每一行的前3列。
我们需要进行4x3次的二重循环(循环嵌套),用变量R定位行,变量C定位列。
外层循环按行来进行;
内层内层按列来进行
我们首先用R定位第一行,然后循环修改R行的前3列;
然后再用R定位到下一行,再次循环修改R行的前3列……,
如此重复直到所有的数据修改完毕。
处理的过程大致如下:
R=第一行的地址;
mov cx,4
s0:C=第一列的地址
mov cx,3
s:改变R行,C列的字母为大写
C=下一列的地址;
loop s
R=下一行的地址
loop s0
我们用bx来作变量,定位每行的起始地址,用si定位要修改的列,用[bx+si]的方式来对目标单元进行寻址。
代码如下:
问题7.8:
仔细阅读上面的代码,是否有问题?
分析:
问题在于cx的使用,我们进行了二重循环,却只用了一个循环计数器,造成再进行内层循环的时候覆盖了外层循环的循环计数值。
我们应该在每次开始内层循环的时候,将外层循环的cx中的数值保存起来,在执行外层循环的loop指令前,再恢复外层循环的cx数值。
我们可以用寄存器dx来临时保存cx中的数值。
改进后的程序:
上面的程序用dx来暂时存放cx中的值;
如果在内存循环中,dx寄存器也被使用,该怎么解决?
我们似乎可以使用别的寄存器,但是,CPU中的寄存器数量毕竟是有限的,如8086CPU只有14个寄存器。
我们要讨论的问题是,程序中经常需要进行数据的暂存,我们怎样做将更为合理。
这些数据可能是寄存器中的,也可能是内存中的。
我们可以用寄存器暂存它们,但是这不是一个一般化的解决方案,因为寄存器的数量有限,每个程序中可使用的寄存器都不一样。
我们希望寻找一个通用的方案,来解决这种在编程中经常会出现的问题。
显然我们不能选择寄存器,那么可以使用的就是内存了。
我们可以考虑将需要咱村的数据放到内存单元中,需要使用的时候,在从内存单元中恢复。这样我们就要开辟一段内存空间。
改进程序如下:
改造后的程序中,用内存单元来保存数据;
可是上面的做法有些麻烦,因为若需要保存多个数据时,我们必须要记住数据放到了哪个单元中,这样程序容易混乱。
我们一定要用内存来暂存数据,但用怎样的结构来保存这些数据,使得我们的程序更加清晰值得思考。
一般来说,在需要暂存数据的时候,我们都应该使用栈。
改进代码如下: