1.写在前面
我在前面的博客的介绍了汇编中的循环机制,同时也介绍了怎么在一个汇编的程序中定义多个段,循环的机制的就是读取cx中的值,cx寄存器中的值决定循环的次数,今天的博客主要介绍更加高级的寻址的方式。
2.本篇博客的概述
3.更灵活的定位内存地址的方法
3.1and和or指令
- and指令:逻辑与指令,按位进行与运算。通过该指令可将操作的相应的位设为0,其他的位不变。想变0的位置,与上0就行了,其他的与上1就可以不变。
- or指令:逻辑或指令,按位进行或运算。通过该指令可将操作的相应的位置设为1,其他的位不变。想要变1的位置,或上1就行了,其他的或上0就可以不变。
3.2关于ASCII码
我们将现实中的一些信息存储到计算机中,就要对其进行编码,将器转化为二进制信息进行存储。而计算机要将这些存储的信息再现实给我们看,就要再对其进行解码。只要编码和解码采用同样的规则,我们就可以将人们理解的信息存入到计算机,再从计算机中取出。而ASCII码就是其中的一种的编码解码的方式。
3.3以字符形式给出的数据
我们先看如下的汇编程序,具体的如下:
assume cs:code,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
然后我们编译连接程序,最后通过debug的命令,查看的结果如下:
可以看到我们的值已经存进去了,这儿Debug也帮我们转换对应的ASCII码值,方便我们查看了。
3.4大小写转换的问题。
先看下面一个问题,在codesg中填写代码,将datasg中的第一个字符串转换为大写,第二个字符串转换成小写。
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'iNfOrMaTiOn'
datasg ends
codesg segment
start:
codesg ends
end start
首先我们先分析一下,同一个字母的大写字符和小写字符对应的ASCII码不同,它们之间相差20H,所以大写转小写就是减去20H,如果是小写转大写就是加上20H。有了上面的分析,于是我们写出如下的代码
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]
如果(al) > 61H,则为小写字母的ASCII码,则:sub al,20H
mov [bx],al
inc bx
loop s
.
.
.
codesg ends
end start
但是当我们写到一半的时候,发现我们到现在没有学比较指令,那么怎么判断啊是大写的还是小写,于是我们陷入了一个怪圈,就是一定要比较,但是当我们再分析一下,如果是相差20H,20H就等于100000
,于是就有了一种答案就是如果加上20H 只需要将从右往左数第5位变成1即可,如果减去20H只需要将从右往左数第5位变成0即可。于是我们利用And和Or指令即可指定的位置变成1或变成0。有了这些知识即可书写我们的代码了,具体的如下:
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'iNfOrMaTiOn'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax ;设置ds指向 datasg段
mov bx,0 ;设置(bx)=0,ds:bx指向'BaSiC'的第一个字母
mov cx,5 ;设置循环次数5,因为'BaSiC'有5个字母
s: mov al,[bx] ;将ASCII码从ds:bx所指向的单元中取出
and al,11011111B ;将al中的ASCII码的第5位置为0,变成大写字母
mov [bx],al
inc bx
loop s
mov bx,5 ;设置(bx)=5,ds:bx指向'iNfOrMaTiOn'的第一个字母
mov cx,11 ;设置循环次数11,因为'iNfOrMaTiOn'有11个字母
s0: mov al,[bx]
or al,00100000B ;将al中的ASCII码的第5位置为1,变成小写字母
mov [bx],al
inc bx
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
于是我们写出了上面代码,我们来编译连接一下,然后运行一下,查看我们的结果如下:
可以发现我们的大小的转换已经成功了。
3.5[bx+idata]
在前面,我们用[bx]的方式来指明一个内存单元,还可以用一种更为灵活的方式来指明内存单元:[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata(bx中的数值加上idata)。
我们来看下指令 mov ax,[bx+200]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2个字节(字单元),存放一个字,偏移地址为bx中的数值加上200,段地址在ds中。
数学化的描述为:(ax)=((ds)*16+(bx)+200)
该指令还有如下的格式:
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200
3.6用[bx+idata]的方式进行数组的处理
有了[bx+idata]的方式表示内存单元的方式,我们就可以用更高级的结构来看待所要处理的数据。我们通过下面的问题来解答这个问题。
在codesg中填写代码,将datasg中定义的第一个字符串转换为大写,第二个字符串转化为小写
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends
codesg segment
start:
codesg ends
end start
按照原来的方法,用[bx]的方式定位字符串的字符,代码段中的程序如下:
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,5
s0:mov al,[bx]
or al,00100000b
mov [bx],al
inc bx
loop s0
现在,我们有了[bx+idata]的方式,就可以简化上面的代码。这个程序有两个字符串,一个其实地址为0,另外一个起始地址为5。我们可以将这两个字符串看成两个数组,一个从0地址开始存放,另外一个从5开始存放。于是我们就可以用[0+bx]和[5+bx]的方式在同一个循环中定位这两个字符串中的字符。于是写出下面代码:
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s: mov al,[bx]
and al,11011111b
mov [bx],al
mov al,[bx+5]
or al,00100000b
mov [bx+5],al
inc bx
loop s
这个时候如果是高级语言,C语言描述上面的程序,大致是这样的:
char a[5]="BaSiC";
char b[5]="MinIX";
main(){
int i;
i = 0;
do{
a[i] = a[i]&0xDF;
b[i] = b[i]|0x20;
i++;
}while(i<5);
}
我们来对比一下,两种语言
C语言:a[i],b[i]
汇编语言:0[bx],5[bx]
通过比较,我们可以发现,[bx+idata]的方式为高级语言实现数组提供了便利机制。
3.7SI和DI
si和di是8086CPU中和bx功能相近的寄存器,si和di不能够分成两个8位寄存器来使用。下面的3组指令实现了相同的功能。
mov bx, 0
mov ax,[bx]
mov si,0
mov ax,[si]
mov di,0
mov ax,[di]
下面的三组指令也是
mov bx,0
mov ax,[bx+123]
mov si,0
mov ax,[si+123]
mov di,0
mov ax,[di+123]
问题:用si和di实现字符串’welcome to masm’复制到它后面的数据区中。具体的代码如下:
assume cs:codesg,ds:datasg
datasg segment
db 'welcome to masm!'
db '................'
datasg ends
分析:数据在内存中存放,我们在处理数据之前首先要搞清楚数据存储在什么地方,也就是说数据的内存地址。对数据进行复制,先看要复制的数据在什么地方,datasg:0,这是要进行复制的数据的地址。复制到什么地方去?它后面的数据区的偏移地址为16,就是要复制到的地方,清楚了地址之后,我们就可以进行处理了,具体的代码如下:
codesg segment
start: mov ax,datasg
mov ds,ax
mov si,0
mov di,16
mov cx,8
s: mov ax,[si]
mov [di],ax
add si,2
add di,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start
我们可以利用[bx(si 或 di)+idata]的方式,来使程序变得简洁,程序如下:
codesg segment
start: mov ax,datasg
mov ds,ax
mov si,0
mov cx,8
s: mov ax,0[si]
mov 16[si],ax
add si,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start
3.8[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中。
数学化的描述为:(ax)=((ds)*16+(bx)+(si))
3.9[bx+si+idata]和[bx+di+idata]
[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+200+si]
mov ax,[200+bx+si]
mov ax,200[bx][si]
mov ax,[bx].200[si]
mov ax,[bx][si].200
3.10不同的寻址方式的灵活应用
通过前面几种定位内存地址的方法,就可以发现:
- [idata]用一个常量来表示地址,可用于直接定位一个内存单元。
- [bx]用一个变量来表示内存地址,可用于间接定位一个内存单元。
- [bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元。
- [bx+si]用两个变量表示地址。
- [bx+si+idata]用两个变量和一个常量表示地址。
例子:编程:将datasg段中每个单词改为大写字母
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
codesg segment
start:
codesg ends
end start
分析:
datasg中的数据的存储结构如下:
在datasg中定义了4个字符串,每个长度为16个字节,我们可以将这4个字符串看成一个4行16列的二维数组。按照要求,我们需要修改每一个单词,即二维数组的每一行的前3列。
我们需要进行4*3次的二重循环,用变量R定位行,变量C定位列。外层循环按行来进行,内层按列来进行。首先用R定位第1行,然后循环修改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]的方式来对目标单元进行寻址,程序如下:
mov ax,datatsg
mov ds,ax
mov bx,0
mov cx,4
s0: mov si,0
mov cx,3
s:mov al,[bx+si]
and al,110111111b
mov [bx+si],al
inc si
loop s
add bx,16
loop s0
上面的代码问题在于cx的使用,我们进行二重循环,却只用了一个循环计数器,造成在进行内存循环的时候,覆盖了外层循环的循环计数值。多用了一个计数器又不可能,因为loop指令默认cx为循环计数器。这个时候需要我们在每次开始内存循环的时候,将外层循环的cx中的数值保存起来,在执行外层循环loop指令前,再恢复外层循环的cx的数值。可以用寄存器dx来临时保存cx中的数值,改进的程序如下:
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0:mov dx,cx ;将外层的循环的cx值保存在dx中
mov si,0
mov cx,3 ;cx设置为内层循环的次数
s:mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s
add bx,16
mov cx,dx ;用dx中存放的外层循环的计数值恢复cx
loop s0 ;外层循环的loop指令将cx中的计数值减1
上面的程序用dx来暂时存放cx中的值,如果在内存循环中,dx寄存器也被使用,这个时候怎么办?我么似乎没有可以使用的寄存器了,CPU中寄存器的数量毕竟是有限的。而且每个程序中可使用的寄存器都不一样。我们希望寻找一个通用的方案,那么可以使用的就是内存了。可以考虑将需要暂存的数据放到内存单元中,需要使用的时候,再从内存单元中恢复。于是写出如下的代码:
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
codesg segment
start:mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0:mov ds:[40H],cx ;将外层的循环的cx值保存在datasg:40H中
mov si,0
mov cx,3 ;cx设置为内层循环的次数
s:mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s
add bx,16
mov cx,ds:[40H] ;用datasg:40H中存放的外层循环的计数值恢复cx
loop s0 ;外层循环的loop指令将cx中的计数值减1
mov ax,4c00h
int 21h
codesg ends
end start
上面的程序中,用内存单元来保存数据,可是上面的作法还是比较麻烦,如果我们需要保存多个数据的时候,你必须要记住数据存放在哪个单元中。这样程序就很容易混乱。
我们用内存来保存数据,这点是知道了,但是我们用怎样的结构保存这些数据,使得我们的程序更加清晰。一般来说,在需要暂存数据的时候,我们都应该使用栈。于是我们再次改进我们的程序,如下:
assume cs:codesg,ds:datasg,ss:stacksg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
stacksg segment ;定义一个段,用来做栈段,容量为16个字节
dw 0,0,0,0,0,0,0,0
stacksg ends
codesg segment
start:mov ax,stacksg
mov ss,ax
mov sp,16
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0:push cx ;将外层的循环的cx值压栈
mov si,0
mov cx,3 ;cx设置为内层循环的次数
s:mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s
add bx,16
pop cx ;从栈顶弹出原cx的值,恢复cx
loop s0 ;外层循环的loop指令将cx中的计数值减1
mov ax,4c00h
int 21h
codesg ends
end start
4.数据处理的两个基本的问题
第一个问题:处理的数据在什么地方?
第二个问题:要处理的数据有多长?
为了描述上的简洁,我们将使用两个描述性的符合reg来表示一个寄存器,用sreg表示一个段寄存器。
reg的集合包括:ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di;
sreg的集合包括:ds、ss、cs、es
4.1bx、si、di和bp
-
在8086CPU中,只有这4个寄存器可以在[…]中进行内存单元的寻址。以下的指令是正确的:
mov ax,[bx] mov ax,[bx+si] mov ax,[bx+di] mov ax,[bp] mov ax,[bp+si] mov ax,[bp+di]
下面的指令是错误的:
mov ax,[cx] mov ax,[ax] mov ax,[dx] mov ax,[ds]
-
在[…]中,这4个寄存器可以单个出现,或只能以4中组合出现:bx和si、bx和di、bp和si、bp和di。下面的指令是正确的:
mov ax,[bx] mov ax,[si] mov ax,[di] mov ax,[bp] mov ax,[bx+si] mov ax,[bx+di] mov ax,[bp+si] mov ax,[bp+di] mov ax,[bx+si+idata] mov ax,[bx+di+idata] mov ax,[bp+si+idata] mov ax,[bp+di+idata]
下面的指令是错误的:
mov ax,[bx+bp] mov ax,[si+di]
-
只要在[…]中使用寄存器bp,而指令中没有显性的地给出段地址,段地址就默认在ss中。比如下面的指令
mov ax,[bp] mov ax,[bp+idata] mov ax,[bp+si] mov ax,[bp+si+idata]
4.2机器指令处理的数据在什么地方
机器指令都是进行数据处理的指令,处理大致可分为3类:读取、写入、运算。机器指令这一层来说,并不关心数据的值是多少,而关心指令执行的前一刻,它将要处理的数据所在的位置。指令执行前,所有的数据可以在3个地方:CPU内部、内存、端口
机器码 | 汇编指令 | 指令执行前数据的位置 |
---|---|---|
8E1E0000 | Mov bx,[0] | 内存,ds:0单元 |
89C3 | Mov bx,ax | Cpu内部,ax寄存器 |
BB0100 | Mov bx,1 | Cpu内部,指令缓冲区 |
4.3汇编语言中的数据位置的表达
-
立即数(idata)
对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中),在汇编语言中称为立即数,在汇编指令中直接给出。
-
寄存器
指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名
-
段地址(SA)和偏移地址(EA)
指令要处理的数据在内存中,在汇编指令中可用[x]的格式给出EA,SA在某个段寄存器中。
4.4寻址方式
8.5指令要处理的数据有多长
8086CPU的指令,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。
- 通过寄存器名指明要处理的数据的尺寸。
- 在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为word或byte
- 其他方法,例如:push 和 pop指令只能进行字操作
4.6寻址方式的综合应用
下面我们通过一个问题来进一步讨论一下各种寻址方式的作用。
关于DEC公司的一条记录如下:
公司名称:DEC
总裁姓名:Ken Olsen
排名:137
收入:40(40亿美元)
著名产品:PDP(小型机)
具体的数据按照下面的方式来存放的:
可以看到,这些数据存放在seg段中从偏移地址60H起始的位置,从seg:60起始以ASCII字符的形式存储了3个字节的公司名称;从seg:60+3起始以ASCII字符的形式存储了9个字节的总裁的姓名;从seg:60+0c起始存放了一个字型数据,总裁在富豪榜上的排名;从seg:60+0E起始存放了一个字型数据,公司的收入;从seg:60+10起始以ASCII字符的形式存储了3个字节的产品名称。
但是到了1988年DEC的公司的信息有了如下的变化
Ken Olsen在富豪榜上的排名已经上升到38位
DEC的收入增加了70亿美元。
该公司的著名的产品已经变成了VAX系列的计算机。
于是我们写出下面的简单的代码:
mov ax,seg
mov ds,ax
mov bx,60h ;确定记录地址,ds:bx
mov word ptr [bx+0ch],38 ;排名字段改为38
add word ptr [bx+0eh],70 ;收入字段增加70
mov si, 0 ;用si来定位产品字符串中的字符
mov byte ptr [bx+10h+si],'V'
inc si
mov byte ptr [bx+10h+si],'A'
inc si
mov byte ptr [bx+10h+si],'X'
如果你属性C语言的话,我们可以用C语言来描述这个程序,大致应该是这样的:
struct company{ //定义一个公司记录的结构体
char cn[3]; //公司名称
char hn[9]; //总裁姓名
int pm; //排名
int sr; //收入
char cp[3]; //著名产品
};
main(){
int i;
dec.pm = 38;
dec.sr = dec.sr+70;
i = 0;
dec.cp[i] = 'V';
i = 1;
dec.cp[i] = 'A';
i = 2;
dec.cp[i] = 'X';
return 0;
}
我们再按照C语言的风格,用汇编语言写一下这个程序,注意和C语言相关语句的比对:
mov ax,seg
mov ds,ax
mov bx,60h ;记录首址送BX
mov word ptr [bx].0ch,38 ;排名字段改为38
;C:dec.pm=38;
add word ptr [bx].0ech,70 ;收入字段增加70
;C: dec.sr = dec.sr +70;
;产品字段改为字符串 'VAX'
mov si,0 ;C: i=0;
mov byte ptr [bx].10h[si],'V' ;dec.cp[i]='V';
inc si ;i++;
mov byte ptr [bx].10h[si],'A' ;dec.cp[i]='A';
inc si ;i++;
mov byte ptr [bx].10h[si],'X' ;dec.cp[i]='X';
inc si ;i++;
4.7 div指令
div是除法指令,需要注意的如下:
- 除数:有8位和16位两种,在一个reg或内存单元中。
- 被除数:默认放在Ax或DX和Ax中,如果除数为8位,被除数则为16位,默认在AX中存放;如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。
- 结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。
可以看一个例子:利用除法指令计算1001/100
首先分析一下,被除数1001可用ax寄存器存放,除数100可用8位寄存器存放,也就是说,要进行8位的除法。程序如下:
mov ax,1001
mov bl,100
div bl
程序执行后,(al)=0AH(即10),(ah)=1(余数为1)。
4.8伪指令dd
前面我们用db和dw定义字节型数据和字型数据。dd是用来定义dword(double world 双字)型数据的。比如:
data segment
db 1
dw 1
dd 1
data ends
在data 段中定义了3个数据:
第一个数据为01H,在data:0处,占一个字节;
第二个数据为0001H,在data:1处,占一个字;
第三个数据为00000001H,在data:3处,占两个字;
4.9dup
dup是一个操作符,在汇编语言中同db、dw、dd等一样,也是由编译器识别处理符合。它是和db、dw、dd等数据定义伪指令配合使用的,用来进行数据的重复。比如:
db 3 dup(0)
定义了3个字节,它们的值都是0,相当于db 0,0,0
db 3 dup(0,1,2)
定义了9个字节,它们是0、1、2、0、1、2、0、1、2相当于db 0,1,2,0,1,2,0,1,2
可见dup的使用格式如下:
db 重复的次数dup(重复的字节型数据)
dw 重复的次数dup(重复的字型数据)
dd 重复的次数dup(重复的双字型数据)
5.写在最后
本篇博客主要其他的寻址方式,以及一些新的指令。