当然也可写为0[bx]和5[bx],注意这种写法和C语言中数组的相似之处:C语言中数组表示为a[i],汇编语言中表示为5[bx]。
SI和DI寄存器
SI和DI功能和BX相似,但不可以拆分为两个8位寄存器。也就是说下面代码等价:
mov bx|si|di,0 mov ax,[bx|si|di] mov ax,[bx|si|di+123]
所以在这里可以使用更方便的方式:[bx+si]和[bx+di],这两个式子表示偏移地址为(bx)+(si)的内存单元,使用方法如:mov ax,[bx+si]等价于mov ax,[bx][si]。
当然,有了这些表示方法,自然就有[bx+si+idata]和[bx+di+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
那我们总结一下这些内存寻址方法:
[idata]用一个常量表示偏移地址,直接定位一个内存单元
[bx]用一个变量表示偏移地址,定位一个内存单元
[bx+idata]用一个常量和一个变量表示偏移地址,可在一个起始地址的基础上间接定位一个内存单元
[bx+si]用两个变量表示偏移地址
[bx+si+idata]用两个变量和一个常量表示偏移地址
使用双循环,使用一个寄存器暂存cs的值,如:
··· mov cx,4 s0:mov dx,cx mov si,0 mov cx,3 s:mov al,[bx+si] and al,11011111b mov [bx+si],al inc si loop s add bx,16 mov cx,dx loop s0 ···
假如循环比较复杂,没有多余的寄存器可用,我们可以使用内存暂存cx或其他数据:
··· dw 0 ··· mov cx,4 s0:mov ds:[40H],cx mov si,0 mov cx,3 s:mov al,[bx+si] and al,11011111b mov [bx+si],al inc si loop s add bx,16 mov cx,ds:[40H] loop s0 ···
这么使用的话注意需要在数据段声明用来暂存的内存,好在程序加载时分配出来。当然,在需要暂存的地方,还是建议使用栈:
··· dw 0,0,0,0,0,0,0,0 ··· mov ax,stacksg mov ss,ax mov sp,16 ··· mov cx,4 s0:push cx mov si,0 mov cx,3 s:mov al,[bx+si] and al,11011111b mov [bx+si],al inc si loop s add bx,16 pop cx loop s0 ···
数据处理的两个基本问题
两个基本问题
1.处理的数据在什么地方
2.要处理的数据有多长
接下来的讨论中,使用reg来表示一个寄存器,使用sreg来表示一个段寄存器。所以:
reg:ax,bx,cx,dx,ah,al,bh,bl,ch,cl,dh,dl,sp,bp,si,di sreg:ds,ss,cs,es
bx,si,di和bp
在8086CPU中,只有这四个寄存器可以使用[···]来进行内存寻址,可以单个出现,或以下面组合出现(常数可以随意出现在这些表示方法中):
bx+si/di
bp+si/di
注:如果使用了bp来寻址,而没有显式的表明段地址,默认使用ss段寄存器,如:
mov ax,[bp] ;(ax)=((ss)*16+(bp)) mov ax,[bp+idata] ;(ax)=((ss)*16+(bp)+idata) mov ax,[bp+si] ;(ax)=((ss)*16+(bp)+(si)+idata)
数据的位置
绝大部分机器指令都是用来处理数据的,基本可分为读取,写入,运算。在机器指令这个层面上,并不关心数据是什么,而关心指令执行前数据的位置。一般数据会在三个地方,CPU内部,内存,端口。
汇编语言中使用三个概念来表示数据的位置:
立即数(idata)
o对于直接包含在机器指令中的数据,在汇编语言中称为立即数
o例:mov ax,1 add bx,2000h
寄存器
o指令要处理的数据在寄存器中,在汇编指令中给出相应寄存器名
o例:mov ax,bx mov ds,ax
段地址(SA)和偏移地址(EA)
o指令要处理的数据在内存中,在指令中使用[X]方式给出,SA在某个段寄存器中
o例:mov ax,[0] mov ax,[di]
总结一下寻址方式:
寻址方式 |
含义 |
名称 |
[idata] |
EA=idata;SA=(DS) |
直接寻址 |
[bx|si|di|bp] |
EA=(bx|si|di|bp);SA=(DS) |
寄存器间接寻址 |
[bx|si|di|bp+idata] |
EA=(bx|si|di|bp+idata);SA=(DS) |
寄存器相对寻址 |
[bx|bp+si|di] |
EA=(bx|bp+si|di);SA=(DS|SS) |
基址变址寻址 |
[bx|bp+si|di+idata] |
EA=(bx|bp+si|di+idata);SA=(DS|SS) |
相对基址变址寻址 |
数据的长度
8086CPU中可以指定两种尺寸的数据,byte和word,所以在使用数据的时候要指明数据尺寸。
在有寄存器参与的时候使用寄存器的种类区分
o字:mov ax,1
o字节:mov al,1
在没有寄存器参与的时候,使用X ptr指明内存单元长度,X是word或byte
o字:mov word ptr ds:[0],1 add word ptr [bx],2
o字节:mov byte ptr ds:[0],1 add byte ptr [bx],2
其他默认指明处理类型的指令
opush [1000H],push默认只进行字操作
灵活使用寻址方式的例子,修改下面内存空间中的数据:
段seg:60
起始地址 |
内容 |
00 |
‘DEC’ |
03 |
‘Ken Oslen’ |
0C |
137 |
0E |
40 |
10 |
‘PDP’ |
··· mov ax,seg mov ds,ax mov bx,60h mov word ptr [bx].0ch,38 ;第三字段改为38 add word ptr [bx].0eh,70 ;第四字段改为70 mov si,0 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++中结构体的使用。[bx].idata.[si],就类似与c++中的dec.cp[i]。dec是结构体,cp是结构体中的字符串成员,[i]表示第几个字符。
div指令
div是除法指令,需要注意以下三点:
除数:8位或16位,在一个reg或内存单元中
被除数:默认在AX或DX中,如果除数8位,被除数则为16位,放在AX中;如果除数16位,则被除数32位,在DX和AX中,DX存放高16位,AX放低16位。
结果,除数8位,结果(商)存放在AL中,AH存放余数;如果除数16位,则AX存放商,DX存放余数
格式:div reg或div 内存单元,所以div byte ptr ds:[0]表示:
(al)=(ax)/((ds)*16+0)的商; (ah)=(ax)/((ds)*16+0)的余数; div word ptr es:[0]表示: (al)=[(dx)*10000H+(ax)]/((es)*16+0)的商 (ah)=[(dx)*10000H+(ax)]/((es)*16+0)的余数
例:计算100001/100,因为100001(186A1H)大于65535,则需要存放在ax和dx两个寄存器,那么除数100只能存放在一个16位的寄存器中,实现代码:
mov dx,1 mov ax,86A1H mov bx,100 div bx 执行之后(ax)=03E8H(1000),(dx)=1。
伪指令dd
dd是一个伪指令,类似dw,但dd是用来定义dword(double word,双字),如:
dd 1 ;2字,4字节
dw 1 ;1字,2字节
db 1 ;1字节
将data段中第一个数据除以第二个数据,商存入第三个数据:
··· data segment dd 100001 dw 100 dw 0 data ends ··· mov ax,data mov ds,ax mov ax,ds:[0] mov dx,ds:[2] div word ptr ds:[4] mov ds:[6],ax ···
总结一下div相关:
div后面跟的是除数
被除数位数是除数两倍
被除数存在ax中或ax+dx(ax低,dx高)
商在ax或al中,余数在ah或dx中(高余数,低商)
dup
dup是一个操作符,由编译器识别,和db,dw,dd配合使用,如:
db 3 dup (0)表示定义了三个值是0的字节,等价于db 0,0,0
db 3 dup (1,2,3)等价于db 1,2,3,1,2,3,1,2,3 共九个字节
db 3 dup (‘abc’,’ABC’)等价于db ‘abcABCabcABCabcABC’
综上,db|dw|dd 重复次数 dup (重复内容)
转移指令原理
转移指令
可以修改IP或同时修改CS,IP的系统指令称为转移指令,可分为以下几类:
转移行为:
o只修改IP,称为段内转移,如jmp ax
o同时修改CS和IP,称为段间转移,如jmp 1000:0
修改范围(段内转移):
o短转移:修改IP范围-128~127
o近转移:修改IP范围-32768~32767
转移指令分类:
o无条件转移:jmp
o条件转移
o循环指令
o过程
o中断
offset操作符
offset是由编译器处理的符号,它能去的标号的偏移地址,如:
start:mov ax,offset start
s:mov ax,offset s
这里就是将start和s的偏移地址分别送给ax,也就是0和3
jmp指令
jmp是无条件转移指令,可以只修改IP也可以同时修改CS和IP,只要给出两种信息,要转移的目的地址和专一的距离。
依据位移的jmp指令:jmp short 标号(转到标号处执行指令)。这个指令实现的是段内短转移,对IP修改范围是-128~127,指令结束后CS:IP指向标号的地址,如:
0BBD:0000 start:mov ax,0 (B80000) 0BBD:0003 jmp short s (EB03) 0BBD:0005 add ax,1 (050100) 0BBD:0008 s:inc ax (40)
执行之后ax值为1,因为跳过了add指令。
还应注意的是,jmp short短转移指令并不会在机器码中直接写明需要转移的地址(0BBD:0008),jmp的机器码是EB03并没有包含转移的地址,这里的转移距离是相对计算而出的地址,来看下面的执行过程:
1.(CS)=0BBDH,(IP)=0006H,CS:IP指向EB03(jmp short s)
2.读取指令EB03进入指令缓冲器
3.(IP)=(IP)+指令长度,即(IP)=(IP)+2=0008H,之后CS:IP指向add ax,1
4.CPU指向指令缓冲器中的指令EB03
5.执行之后(IP)=000BH,指向inc ax
在jmp short s的机器码中,包含的并不是转移的地址,而是转移的位移,这里的位移是相对计算出来的,用8位一字节来表示,所以表示范围是-128~127,用补码表示。计算方法如是,8位位移=标号处地址-jmp下一条指令的地址。当然还有一种类似的指令是jmp near ptr 标号,是近转移,原理一样,只是表示位移的是字类型16位,表示范围-32768~32767。
jmp+地址远转移
jmp far ptr 标号实现的是段间转移,也就是远转移,它的机器码中指明了转移的目的地址的CS和IP的值,如下面例子:
0BBD:0000 start:mov ax,0 (B80000) 0BBD:0003 mov bx,0 (BB0000) 0BBD:0006 jmp far ptr s (EA0B01BD0B) 0BBD:000B db 256 dup (0) 0BBD:010B s:add ax,1 0BBD:010X inc ax
可以看出,jmp的机器码中明确指明了跳转位置s的地址0BBD:010B,在低位的是IP的值,高位的是CS的值。
jmp+寄存器|内存转移
jmp+寄存器:jmp 16位reg,实现的是(IP)=(16位reg),之前讨论过,直接修改IP的值为寄存器中的值。
jmp+内存:jmp加内存使用的时候有两种用法:
jmp word ptr 内存单元地址(段内转移)
o从内存单元地址处开始存放一个座位转移目的的偏移地址的字
o内存单元支持任何寻址方式
o如jmp word ptr ds:[0],执行后(IP)=0123H(ds:[0]中的值是123H)
jmp dword ptr 内存单元地址(段间转移)
o从内存单元地址处开始存放两个字,高位存放段地址,低位偏移地址作为转移的目的地址
o(CS)=(内存单元地址+2),(IP)=(内存单元地址),支持任一种寻址方式
o如jmp dword ptr [bx]跳转到0:123H
jcxz指令
jcxz指令为条件转移指令,所有的条件转移指令都是短转移,转移范围是-128~127。使用格式是jcxz 标号,功能是如果(cx)=0则跳转到标号处执行;如果(cx)!=0,那么什么也不做继续执行代码。
loop指令
loop为循环指令,所有的循环指令都是短转移,转移范围是-128~127。使用格式是loop 标号,功能是如果(cx)!=0那么跳转到标号处执行;如果(cx)=0那么什么也不做继续执行程序。
根据位移进行转移的指令总结
下面几条指令是根据位移进行转移(相对计算转移位置,而不是直接提供转移目的的IP和CS的值)
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号
这些指令之所以是间接计算标号的位置,是为了方便在代码中浮动装配,使得循环体或这些指令的代码段在任何位置都可以执行(不要超跳转范围)。而编译器会对跳转的范围进行检测,如果跳转超过了范围,编译器会报错。
注:jmp 2100:0是debug使用的汇编指令,编译器并不认识。
call和ret指令
ret和retf
ret和call都是转移指令,都是修改IP的值,或同时修改CS和IP。
ret指令用栈中的数据修改IP,实现的是近转移;retf指令用栈中的数据修改CS和IP的值,实现远转移。格式:直接用 ret。
ret执行步骤:
1.(IP)=((SS)*16+(SP)) 2.(SP)=(SP)+2
retf执行步骤:
1.(IP)=((SS)*16+(SP)) 2.(SP)=(SP)+2 3.(CS)=((SS)*16+(SP)) 4.(SP)=(SP)+2
所以ret指令相当于 pop ip,执行retf指令相当于执行pop ip,pop cs。
call指令
call指令也是一个转移指令,执行格式:call 目标(具体使用接下来说明),call的执行步骤:
1.将当前的IP或CS和IP入栈
2.转移
call不能实现短转移,但它实现转移的原理和jmp相同。
根据位移转移:call 标号,近转移,16位转移范围,也是使用相对的转移地址。
执行步骤:
1.(SP)=(SP)-2 2.((SS)*16+(SP))=(IP) 3.(IP)=(IP)+16
所以执行这条命令相当于执行push ip,jmp near ptr 标号。
直接使用地址进行(远)转移:call far ptr 标号,执行步骤:
1.(SP)=(SP)-2 2.((SS)*16+(SP))=(CS) 3.(SP)=(SP)-2 4.((SS)*16+(SP))=(IP) 5.(CS)=标号所在的段的段地址 6.(IP)=标号的偏移地址
所以执行call far ptr 标号相当于执行push cs,push ip,jmp far ptr 标号
使用寄存器的值作为call的跳转地址:call 16位reg
1.(SP)=(SP)-2 2.((SS)*16+(SP))=(IP) 3.(IP)=(16为reg) 相当于执行push ip,jmp 16位reg
使用内存中的值作为call的跳转地址:call word ptr 内存单元地址,当然还有call dword ptr 内存单元地址,这样进行的就是远转移。
联合使用ret和call
联合使用ret和call实现子程序的框架:
assume cs:code code segment main: ··· call sub1 ··· mov ax,4c00h int 21h sub1: ··· call sub2 ··· ret sub2: ··· ret code ends end main
mul指令
mul是乘法指令,使用时应注意,两个相乘的数,要么都是8位,要么都是16位,如果是8位,那么其中一个默认放在al中,另一个在一个8位reg或字节内存单元中;若是16位,则一个默认在ax中,另一个在16位reg或字内存单元中。如果是8位乘法, 则结果放在ax中,结果是16位;若是16位乘法,结果默认在ax和dx中,dx高位,ax低位,共32位。
格式:mul reg 或 mul 内存单元,支持内存单元的各种寻址方式。
如mul word ptr [bx+si+8]代表:
(ax)=(ax)*((ds)*16+(bx)+(si)+8)低16位 (dx)=(ax)*((ds)*16+(bx)+(si)+8)高16位 例:计算100*10 mov al,100 mov bl,10 mul bl