[bx]和loop指令
1.[bx]和内存单元的描述
1.1 [bx]是什么呢?
和[0]有些类似,[0]表示内存单元,它的偏移地址是0。
1.2 我们要完整地描述一个内存单元,需要两种信息:
(1)内存单元的地址;
(2)内存单元的长度(类型)。
我们用[0]表示一个内存单元时,0 表示单元的偏移地址,
段地址默认在ds中,单元的长度(类型)可以由具体指令中的
其他操作对象(比如说寄存器)指出。
1.3 [bx]同样也表示一个内存单元,它的偏移地址在bx中,
比如下面的指令:
mov ax,[bx]
mov al,[bx]
2.loop指令(循环指令)
2.1 指令的格式是:loop 标号,CPU 执行loop指令的时候,要进行两步操作:
① (cx)=(cx)-1;
② 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
2.2 从上面的描述中,我们可以看到,cx中的值影响着loop指令的执行结果。
通常(注意,我们说的是通常)我们用loop指令来实现循环功能,
cx 中存放循环次数。
2.3 从上面的过程中,我们可以总结出用cx和loop 指令相配合实现循环
功能的三个要点:
(1)在cx中存放循环次数;
(2)loop 指令中的标号所标识地址要在前面;
(3)要循环执行的程序段,要写在标号和loop 指令的中间。
2.4 用cx和loop指令相配合实现循环功能的程序框架如下:
mov cx,循环次数
s:循环执行的程序段
loop s
问题分析:5.2
//循环了236次
assume cs:code
code segment
mov ax,0
mov cx,236
s:add ax,123
loop s
mov ax,4c00h
int 21h
code ends
end
//循环了123次
assume cs:code
code segment
mov ax,0
mov cx,123
s:add ax,236
loop s
mov ax,4c00h
int 21h
code ends
end
3.loop和[bx]的联合应用
在实际编程中,经常会遇到,用同一种方法处理地址连续的
内存单元中的数据的问题。
我们需要用循环来解决这类问题,同时我们必须能够在每次循环
的时候按照同一种方法来改变要访问的内存单元的地址。
这时,我们就不能用常量来给出内存单元的地址
(比如[0]、[1]、[2]中,0、1、2是常量),而应用变量。
“mov al,[bx]”中的 bx就可以看作一个代表内存单元地址的变量,
我们可以不写新的指令,仅通过改变bx中的数值,改变指令访问的内存单元。
4.段前缀
指令“mov ax,[bx]”中,内存单元的偏移地址由bx给出,
而段地址默认在ds中。
我们可以在访问内存单元的指令中显式地给出内存单元
的段地址所在的段寄存器。
这些出现在访问内存单元的指令中,用于显式地指明内存单元的段
地址的“ds:”、“cs:”、“ss:”或“es:”,在汇编语言中称为段前缀。
六、更灵活的定位内存地址的方法
1 and和or指令
(1)and 指令:逻辑与指令,按位进行与运算。
如 mov al, 01100011B
and al, 00111011B
执行后 al = 00100011B
通过该指令可将操作对象的相应位设为0,其他位不变。
例如: al = 1111 1111B
将al的第6位设为0: and al,1011 1111B
将al的第7位设为0: and al,0111 1111B
将al的第0位设为0: and al,1111 1110B
同C语言里面的"&"
(2)or 指令:逻辑或指令,按位进行或运算
如 mov al, 01100011B
or al, 00111011B
执行后 al = 01111011B
通过该指令可将操作对象的相应位设为1,其他位不变。
例如:al = 0000 0000B
将al的第6位设为1: and al,0100 0000B
将al的第7位设为1: and al,1000 0000B
将al的第0位设为1: and al,0000 0001B
同C语言里面的"|"
2.[bx+idata]
在前面,我们可以用[bx]的方式来指明一个内存单元,
我们还可以用一种更为灵活的方式来指明内存单元:
[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata
(bx中的数值加上idata)。
我们看一下指令mov ax,[bx+200]的含义:
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),
存放一个字,偏移地址为bx中的数值加上200,段地址在ds中。
数学化的描述为: (ax)=((ds)*16+(bx)+200)
3.SI和DI
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]
(4) mov bx,0
mov ax,[bx+123]
(5) mov si,0
mov ax,[si+123]
(6) mov di,0
mov ax,[di+123]
4. [bx+si]和[bx+di]
在前面,我们用[bx(si或di)]和[bx(si或di)+idata] 的方式来指明
一个内存单元,我们还可以用更灵活的方式:
[bx+si]
[bx+di]
[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si中的数值)。
5. [bx+si+idata]和[bx+di+idata]
[bx+si+idata]表示一个内存单元
它的偏移地址为(bx)+(si)+idata。
(即bx中的数值加上si中的数值再加上idata)
6.如果我们比较一下前而用到的几种定位内存地址的方法(可称为寻址方式),
就可以发现有以下几种方式:
(1)[iata] 用一个常量来表示地址,可用于直接定位一个内存单元;
(2)[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;
(3)[bx+idata] 用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;
(4)[bx+si]用两个变量表示地址;
(5)[bx+si+idata] 用两个变量和一个常量表示地址。
直接寻址
寄存器间接寻址
寄存器相对寻址
基址变址寻址
相对基址变址寻址
七、转移指令的原理
8086CPU的转移指令分为以下几类:
无条件转移指令 (如:jmp)
条件转移指令
循环指令(如:loop)
过程 (函数调用)
中断
1.jmp 指令
jmp为无条件转移,可以只修改IP,也可以同时修改CS和IP;
jmp指令要给出两种信息:
转移的目的地址
转移的距离(段间转移、段内短转移,段内近转移)
(1)jmp short 标号(转到标号处执行指令)
这种格式的 jmp 指令实现的是段内短转移,它对IP的修改范围为
-128~127,也就是说,它向前转移时可以最多越过128个字节,
向后转移可以最多越过127个字节。
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s
add ax,1
s:inc ax
codesg ends
end start
左面的程序执行后, ax中的值为 1 ,因为执行 jmp short s 后 ,
越过了add ax,1 ,IP 指向了标号 s处的 inc ax。也就是说,
程序只进行了一次ax加1操作。
(2)依据位移进行转移的jmp指令
jmp short s指令的读取和执行过程:
(1)(CS)=0BBDH,(IP)=0006,CS:IP指向EB 03(jmp short s的机器码);
(2)读取指令码EB 03进入指令缓冲器;
(3)(IP)=(IP)+所读取指令的长度=(IP)+2=0008,CS:IP指向add ax,1;
(4)CPU指行指令缓冲器中的指令EB 03;
(5)指令EB 03执行后,(IP)=000BH,CS:IP指向inc ax。
注意: 要转移的目的地址是CS:000B,而CPU 执行 EB 03时,当前的(IP)=0008,
如果将当前的IP值加3,使(IP)=000BH,CS:IP就可以指向目标指令。
在转移指令EB 03中并没有告诉CPU要转移的目的地址,却告诉了
CPU 要转移的位移,即将当前的IP向后移动3个字节。
补码:
-9的补码怎么算出来的
+9 原码: 0000 1001 =>09H
反码: 1111 0110
+ 1
——————————————
-9补码: 1111 0111 =>F7H
结论:
CPU执行 jmp short 标号 指令时并不需要转移的目的地址,
只需要知道转移的位移就行了。
(3) 实际上,指令“jmp short 标号”的功能为(IP)=(IP)+8位位移。
(1)8位位移=“标号”处的地址-jmp指令后的第一个字节的地址;
(2)short指明此处的位移为8位位移;
(3)8位位移的范围为-128~127,用补码表示
(4)8位位移由编译程序在编译时算出。
(4) 指令“jmp near ptr 标号”的说明:
(1)16位位移=“标号”处的地址-jmp指令后的第一个字节的地址;
(2)near ptr指明此处的位移为16位位移,进行的是段内近转移;
(3)16位位移的范围为
-32769~32767,用补码表示;
(4)16位位移由编译程序在编译时算出。
(5)jmp near ptr 标号
它实现的时段内近转移。
指令“jmp near ptr 标号”的功能为:
(IP)=(IP)+16位位移。
指令“jmp near ptr 标号”的说明:
(1)16位位移=“标号”处的地址-jmp指令后的第一个字节的地址;
(2)near ptr指明此处的位移为16位位移,进行的是段内近转移;
(3)16位位移的范围为
-32769~32767,用补码表示;
(4)16位位移由编译程序在编译时算出。
(6)指令 “jmp far ptr 标号”
实现的是段间转移,又称为远转移。
指令 “jmp far ptr 标号” 功能如下:
(CS)=标号所在段的段地址;
(IP)=标号所在段中的偏移地址。
far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。
(7) 指令格式:jmp 16位寄存器
功能:IP =(16位寄存器)
这种指令我们在前面的课程(参见2.11节)中已经讲过,这里就不再详述。
(8)转移地址在内存中的jmp指令有两种格式:
(1) jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。
内存单元地址可用寻址方式的任一格式给出。
mov ax,0123H
mov ds:[0],ax
jmp word ptr ds:[0]
执行后,(IP)=0123H
mov ax,0123H
mov [bx],ax
jmp word ptr [bx]
执行后,(IP)=0123H
2) jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,
低地址处是转移的目的偏移地址。
(CS)=(内存单元地址+2)
(IP)=(内存单元地址)
内存单元地址可用寻址方式的任一格式给出。
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]
执行后,
(CS)=0
(IP)=0123H
CS:IP指向0000:0123。
mov ax,0123H
mov [bx],ax
mov word ptr [bx+2],0
jmp dword ptr [bx]
执行后,
(CS)=0
(IP)=0123H
CS:IP指向0000:0123。
2. jcxz指令
(1)jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中
包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127。
指令格式:jcxz 标号
(如果(cx)=0,则转移到标号处执行。)
(2)jcxz 标号 指令操作:
- 当(cx)=0时,(IP)=(IP)+8位位移)
8位位移=“标号”处的地址-jcxz指令后的第一个字节的地址;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。
当(cx)=0时,什么也不做(程序向下执行)。
- jcxz 标号 指令操作:
当(cx)=0时,(IP)=(IP)+8位位移)
8位位移=“标号”处的地址-jcxz指令后的第一个字节的地址;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。
当(cx)!=0时,不进行跳转(程序向下执行)。
我们从 jcxz的功能中可以看出,指令“jcxz 标号”的功能相当于:
if((cx)==0)
jmp short 标号;
(这种用C语言和汇编语言进行的综合描述,或许能使你对有条
件指令理解得更加清楚。)
检测点9.2
s:mov ch,0
mov cl,[bx]
jcxz ok ;当cx=0时,CS:IP指向ok
inc bx
3.loop指令
(1) loop指令为循环指令,所有的循环指令都是短转移,在对应的机
器码中包含转移的位移,而不是目的地址。对IP的修改范围
都为-128~127。
指令格式:loop 标号
((cx))=(cx)-1,如果(cx)≠0,转移到标号处执行。
(2)loop 标号 指令操作:
(1)(cx)=(cx)-1;
(2)如果(cx)≠0,(IP)=(IP)+8位位移。
8位位移 = “标号”处的地址 - loop指令后的第一个字节的地址;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。
当(cx)=0,什么也不做(程序向下执行)
我们从loop的功能中可以看出,指令“loop 标号”的功能相当于:
(cx)--;
if((cx)≠0)
jmp short 标号
检点的9.3
inc cx
4. 根据位移进行转移的意义
前面我们讲到:
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号
等几种汇编指令,它们对 IP的修改是根据转移目的
地址和转移起始地址之间的位移来进行的。在它们
对应的机器码中不包含转移的目的地址,而包含的起
始地址到目的地址的位移。
这样设计,方便了程序段在内存中的浮动装配。
5.编译器对转移位移超界的检测
注意,根据位移进行转移的指令,它们的转移范围受到转移位移的限制,
如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将
报错。
八、 call 和 ret 指令
1.ret 和 retf
(1)ret指令用栈中的数据,修改IP的内容,从而实现近转移;
CPU执行ret指令时,进行下面两步操作:
(1)(IP)=((ss)*16+(sp))
(2)(sp)=(sp)+2
(2)retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移;
CPU执行retf指令时,进行下面四步操作:
(1)(IP)=((ss)*16+(sp))
(2)(sp)=(sp)+2
(3)(CS)=((ss)*16+(sp))
(4)(sp)=(sp)+2
可以看出,如果我们用汇编语法来解释ret和retf指令,则:
CPU执行ret指令时,相当于进行:
pop IP
CPU执行retf指令时,相当于进行:
pop IP
pop CS
检测点10.1
分析:retf执行后 CS = 1000H IP:0000H
mov ax,1000H ;段地址cs入栈
mov ax,0000H ;偏移地址ip入栈
执行retf指令时,相当于进行:
pop IP
pop CS
根据栈先进后出原则,应先将段地址CS入栈,
再将偏移地址ip入栈
2. call 指令
(1) CPU执行call指令,进行两步操作:
(1)将当前的 IP 或 CS和IP 压入栈中;
(2)转移。
call 指令不能实现短转移,除此之外,call指令实现转移的方法
和 jmp 指令的原理相同,下面的几个小节中 ,我们以给出转移目
的地址的不同方法为主线,讲解call指令的主要应用格式。
(2)call 标号(将当前的 IP 压栈后,转到标号处执行指令)
(3)CPU执行此种格式的call指令时,进行如下的操作:
(1) (sp) = (sp) – 2
((ss)*16+(sp)) = (IP)
(2) (IP) = (IP) + 16位位移
3. 依据位移进行转移的call指令
(1)call 标号
16位位移 = “标号”处的地址 - call指令后的第一个字节的地址;
16位位移的范围为 -32768~32767,用补码表示;
16位位移由编译程序在编译时算出。
(2)如果我们用汇编语法来解释此种格式的 call指令,则:
CPU 执行指令“call 标号”时,相当于进行:
push IP
jmp near ptr 标号
检测点10.2
mov ax,0 ;ax=0,ip指向1000:3
call s ;ax=0,
读取call指令后ip=6, push ip
执行后ip指向1000:7
inc ax ;被跳过的代码
s:pop ax ;ax = 6
答案:ax = 6
4. 转移的目的地址在指令中的call指令
前面讲解的call指令,其对应的机器指令中并没有转移的目的地址 ,
而是相对于当前IP的转移位移。
指令“call far ptr 标号”实现的是段间转移。
CPU执行“call far ptr 标号”这种格式的call指令时的操作:
(1) (sp) = (sp) – 2
((ss) ×16+(sp)) = (CS)
(sp) = (sp) – 2
((ss) ×16+(sp)) = (IP)
(2) (CS) = 标号所在的段地址
(IP) = 标号所在的偏移地址
从上面的描述中可以看出,如果我们用汇编语法来解释此种格式的 call 指令,则:
CPU 执行指令 “call far ptr 标号” 时,相当于进行:
push CS
push IP
jmp far ptr 标号
检测点10.3
mov ax,0 ;ax=0, ip指向1000:3
call far ptr s ;读取call之后,ip=8, push cs push ip,ip指向1000:8
;执行call后, ip=9 cs=1000H
inc ax ;被跳过的指令
s:pop ax ;ax=8H
add ax,ax ;ax=10H
pop bx ;bx = 1000H
add ax,bx ;ax = 1010H
答案:ax = 1010H
出栈时,根据先进后出的原则,先出的为ip=8H,后出的为
cs=1000H
5. 转移地址在寄存器中的call指令
(1)指令格式:call 16位寄存器
功能:
(sp) = (sp) – 2
((ss)*16+(sp)) = (IP)
(IP) = (16位寄存器)
(2)指令格式:call 16位寄存器
汇编语法解释此种格式的 call 指令,CPU执行call 16位reg时,相当于进行:
push IP
jmp 16位寄存器
检测点10.4
mov ax,6 ;ax=6,ip指向1000:3
call ax ;读取call指令后,ip=5,push ip
;执行call指令后,ip=6
inc ax ;被跳过的代码
mov bp,sp ;
add ax,[bp] ;ax = 6 + 5 = 0bH
"call ax" 是先将该指令后的第一个字节偏移地址ip入栈,
再转到偏移地址为ax出执行指令
自己在debug进行跟踪确认
6. 转移地址在内存中的call指令
转移地址在内存中的call指令有两种格式:
(1) call word ptr 内存单元地址
(2) call dword ptr 内存单元地址
(1) call word ptr 内存单元地址
汇编语法解释:
push IP
jmp word ptr 内存单元地址
call word ptr 内存单元地址(示例)
比如下面的指令:
mov sp,10h
mov ax,0123h
mov ds:[0],ax
call word ptr ds:[0]
执行后,(IP)=0123H,(sp)=0EH
(2) call dword ptr 内存单元地址
汇编语法解释:
push CS
push IP
jmp dword ptr 内存单元地址
call dword ptr 内存单元地址(示例)
比如,下面的指令:
mov sp,10h
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
call dword ptr ds:[0]
执行后,(CS)=0,(IP)=0123H,(sp)=0CH
7.call 和 ret 的配合使用
(1)前面,我们已经分别学习了 ret 和call指令的原理。现在我们看一下,
如何将它们配合使用来实现子程序的机制。
(2)可是执行完子程序后,如何让CPU接着call指令向下执行?
call指令转去执行子程序之前,call指令后面的指令的地址将存储在栈中,
所以可以在子程序的后面使用 ret 指令,用栈中的数据设置IP的值,
从而转到 call 指令后面的代码处继续执行
这样,我们可以利用call和ret来实现子程序的机制。
(3)子程序的框架:
标号:
指令
ret
(4)mul 指令
mul是乘法指令,使用 mul 做乘法的时候:
(1)相乘的两个数:要么都是8位,要么都是16位。
8 位: AL中和 8位寄存器或内存字节单元中;
16 位: AX中和 16 位寄存器或内存字单元中。
(2)使用mul座乘法的时候:
结果
8位乘法:AX中;
16位乘法:DX(高位)和AX(低位)中。
(4) 格式如下:
mul reg
mul 内存单元
(5)内存单元可以用不同的寻址方式给出,比如:
mul byte ptr ds:[0]
含义为: (ax)=(al)*((ds)*16+0);
mul word ptr [bx+si+8]
含义为:
(ax)=(ax)*((ds)*16+(bx)+(si)+8)结果的低16位;
(dx)=(ax)*((ds)*16+(bx)+(si)+8)结果的高16位;
8.模块化程序设计
(1) call 与 ret 指令共同支持了汇编语言编程中的模块化设计。
在实际编程中,程序的模块化是必不可少的。
(2) 因为现实的问题比较复杂,对现实问题进行分析时,
把它转化成为相互联系、不同层次的子问题,是必须的解决方法。
(3) 而call和ret 指令对这种分析方法提供了程序实现上的支持。利用 call和ret指令,
我们可以用简洁的方法,实现多个互相联系、功能独立的子程序来解决一个复杂的问题。
9.参数和结果传递的问题
(1)子程序一般都要根据提供的参数处理一定的事务,
处理后,将结果(返回值)提供给调用者。
(2)其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,
应该如何存储子程序需要的参数和产生的返回值。
(3)我们设计一个子程序,可以根据提供的N,来计算N的3次方。
这里有两个问题:
(1)我们将参数N存储在什么地方?
(2)计算得到的数值,我们存储在什么地方?
子程序:
说明:计算N的3次方
参数: (bx)=N
结果: (dx:ax)=N∧3
cube:mov ax,bx
mul bx
mul bx
ret
注意,我们在编程的时候要注意良好的风格,对于程序应有详细的注释。
子程序的注释信息应该包含对子程序的功能、参数和结果的说明。
因为今天写的子程序,以后可能还会用到;自己写的子程序,也很可能要给别人使用,
所以一定要有全面的说明。
(4) 用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结
果的寄存器,调用者和子程序的读写操作恰恰相反:
- 调用者将参数送入参数寄存器,从结果寄存器中取到返回值;
- 子程序从参数寄存器中取到参数,将返回值送入结果寄存器。
dw data word 2byte
dd data dword 4byte
10.批量数据的传递
(1)前面的例程中,子程序 cube 只有一个参数,放在bx中。
如果有两个参数,那么可以用两个寄存器来放,可是如果
需要传递的数据有3个、4个或更多直至 N个,我们怎样存放呢?
(2)寄存器的数量终究有限,我们不可能简单地用寄存器来存放多
个需要传递的数据。对于返回值,也有同样的问题。
(3)在这种时候,我们将批量数据放到内存中,然后将它们所在内存空间的
首地址放在寄存器中,传递给需要的子程序。
对于具有批量数据的返回结果,也可用同样的方法。
(4)将一个全是字母的字符串转化为大写
A: 65 1000001B
a: 97 1100001B
B: 66 1000010B
b: 98 1100010B
11. 寄存器冲突的问题